Chromium Code Reviews| Index: build/build.py | 
| diff --git a/build/build.py b/build/build.py | 
| index cb280ab64ad71267c0f068e8838b42c30cb61c93..f5b68312726c060fce097999ebd5009cafe77f81 100755 | 
| --- a/build/build.py | 
| +++ b/build/build.py | 
| @@ -7,11 +7,14 @@ | 
| invokes CIPD client to package and upload chunks of it to the CIPD repository as | 
| individual packages. | 
| -See build/packages/*.yaml for definition of packages. | 
| +See build/packages/*.yaml for definition of packages and README.md for more | 
| +details. | 
| """ | 
| import argparse | 
| +import contextlib | 
| import glob | 
| +import hashlib | 
| import json | 
| import os | 
| import platform | 
| @@ -42,6 +45,56 @@ class UploadException(Exception): | 
| """Raised on errors during package upload step.""" | 
| +class PackageDef(object): | 
| + """Represents parsed package *.yaml file.""" | 
| + | 
| + def __init__(self, path, pkg_def): | 
| + self.path = path | 
| + self.pkg_def = pkg_def | 
| + | 
| + @property | 
| + def name(self): | 
| + """Returns name of YAML file (without the directory path and extension).""" | 
| + return os.path.splitext(os.path.basename(self.path))[0] | 
| + | 
| + @property | 
| + def uses_python_env(self): | 
| + """Returns True if 'uses_python_env' in the YAML file is set.""" | 
| + return self.pkg_def.get('uses_python_env') | 
| + | 
| + @property | 
| + def go_packages(self): | 
| + """Returns a list of Go packages that must be installed for this package.""" | 
| + return self.pkg_def.get('go_packages') or [] | 
| + | 
| + def should_build(self, builder): | 
| + """Returns True if package should be built in the current environment. | 
| + | 
| + Takes into account 'builders' and 'supports_cross_compilation' properties of | 
| + package definition file. | 
| + """ | 
| + # If '--builder' is not specified, ignore 'builders' property. Otherwise, if | 
| + # 'builders' YAML attribute it not empty, verify --builder is listed there. | 
| + builders = self.pkg_def.get('builders') | 
| + if builder and builders and builder not in builders: | 
| 
 
Vadim Sh.
2016/06/25 03:51:04
this is existing logic (moved from 'should_process
 
 | 
| + return False | 
| + | 
| + # If cross compiling, pick only packages that supports cross compilation. | 
| + if is_cross_compiling(): | 
| 
 
Vadim Sh.
2016/06/25 03:51:04
this is new
 
 | 
| + return bool(self.pkg_def.get('supports_cross_compilation')) | 
| + | 
| + return True | 
| + | 
| + | 
| +def is_cross_compiling(): | 
| + """Returns true if using GOOS or GOARCH env vars. | 
| + | 
| + We also check at the start of the script that if one of them is used, then | 
| + the other is specified as well. | 
| + """ | 
| + return bool(os.environ.get('GOOS')) or bool(os.environ.get('GOARCH')) | 
| + | 
| + | 
| def run_python(script, args): | 
| """Invokes a python script. | 
| @@ -53,11 +106,11 @@ def run_python(script, args): | 
| args=['python', '-u', script] + list(args), executable=sys.executable) | 
| -def run_cipd(go_workspace, cmd, args): | 
| +def run_cipd(cipd_exe, cmd, args): | 
| """Invokes CIPD, parsing -json-output result. | 
| Args: | 
| - go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| + cipd_exe: path to cipd client binary to run. | 
| cmd: cipd subcommand to run. | 
| args: list of command line arguments to pass to the subcommand. | 
| @@ -69,10 +122,7 @@ def run_cipd(go_workspace, cmd, args): | 
| fd, temp_file = tempfile.mkstemp(suffix='.json', prefix='cipd_%s' % cmd) | 
| os.close(fd) | 
| - cmd_line = [ | 
| - os.path.join(go_workspace, 'bin', 'cipd' + EXE_SUFFIX), | 
| - cmd, '-json-output', temp_file, | 
| - ] + list(args) | 
| + cmd_line = [cipd_exe, cmd, '-json-output', temp_file] + list(args) | 
| print 'Running %s' % ' '.join(cmd_line) | 
| exit_code = subprocess.call(args=cmd_line, executable=cmd_line[0]) | 
| @@ -101,20 +151,32 @@ def print_title(title): | 
| print '-' * 80 | 
| -def build_go(go_workspace, packages): | 
| - """Bootstraps go environment and rebuilds (and installs) Go packages. | 
| +def print_go_step_title(title): | 
| + """Same as 'print_title', but also appends values of GOOS and GOARCH.""" | 
| + if is_cross_compiling(): | 
| + title += '\nwith' | 
| + title += '\n GOOS=%s' % ( | 
| + os.environ['GOOS'] if 'GOOS' in os.environ else '<native>') | 
| + title += '\n GOARCH=%s' % ( | 
| + os.environ['GOARCH'] if 'GOARCH' in os.environ else '<native>') | 
| + print_title(title) | 
| + | 
| - Compiles and installs packages into default GOBIN, which is <path>/go/bin/ | 
| - (it is setup by go/env.py and depends on what workspace is used). | 
| +@contextlib.contextmanager | 
| +def hacked_workspace(go_workspace, goos=None, goarch=None): | 
| 
 
Vadim Sh.
2016/06/25 03:51:04
this is mostly existing logic, moved into separate
 
 | 
| + """Symlinks Go workspace into new root, modifies os.environ. | 
| + | 
| + Go toolchain embeds absolute paths to *.go files into the executable. Use | 
| + symlink with stable path to make executables independent of checkout path. | 
| Args: | 
| go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| - packages: list of packages to build (can include '...' patterns). | 
| - """ | 
| - print_title('Compiling Go code: %s' % ', '.join(packages)) | 
| + goos: if set, overrides GOOS environment variable (removes it if ''). | 
| + goarch: if set, overrides GOARCH environment variable (removes it if ''). | 
| - # Go toolchain embeds absolute paths to *.go files into the executable. Use | 
| - # symlink with stable path to make executables independent of checkout path. | 
| + Yields: | 
| + Path where go_workspace is symlinked to. | 
| + """ | 
| new_root = None | 
| new_workspace = go_workspace | 
| if sys.platform != 'win32': | 
| @@ -127,12 +189,41 @@ def build_go(go_workspace, packages): | 
| assert not rel.startswith('..'), rel | 
| new_workspace = os.path.join(new_root, rel) | 
| - # Remove any stale binaries and libraries. | 
| - shutil.rmtree(os.path.join(new_workspace, 'bin'), ignore_errors=True) | 
| - shutil.rmtree(os.path.join(new_workspace, 'pkg'), ignore_errors=True) | 
| + orig_environ = os.environ.copy() | 
| + | 
| + if goos is not None: | 
| + if goos == '': | 
| + os.environ.pop('GOOS', None) | 
| + else: | 
| + os.environ['GOOS'] = goos | 
| + if goarch is not None: | 
| + if goarch == '': | 
| + os.environ.pop('GOARCH', None) | 
| + else: | 
| + os.environ['GOARCH'] = goarch | 
| - # Recompile ('-a'). | 
| try: | 
| + yield new_workspace | 
| + finally: | 
| + if new_root: | 
| + os.remove(new_root) | 
| + os.environ = orig_environ | 
| + | 
| + | 
| +def run_go_install(go_workspace, packages, goos=None, goarch=None): | 
| + """Rebuilds (and installs) Go packages into GOBIN via 'go install ...'. | 
| + | 
| + Compiles and installs packages into default GOBIN, which is <go_workspace>/bin | 
| + (it is setup by go/env.py). | 
| + | 
| + Args: | 
| + go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| + packages: list of packages to build (can include '...' patterns). | 
| + goos: if set, overrides GOOS environment variable (removes it if ''). | 
| + goarch: if set, overrides GOARCH environment variable (removes it if ''). | 
| + """ | 
| + with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | 
| + print_go_step_title('Building:\n %s' % '\n '.join(packages)) | 
| subprocess.check_call( | 
| args=[ | 
| 'python', '-u', os.path.join(new_workspace, 'env.py'), | 
| @@ -140,31 +231,97 @@ def build_go(go_workspace, packages): | 
| ] + list(packages), | 
| executable=sys.executable, | 
| stderr=subprocess.STDOUT) | 
| - finally: | 
| - if new_root: | 
| - os.remove(new_root) | 
| -def enumerate_packages_to_build(package_def_dir, package_def_files=None): | 
| - """Returns a list of absolute paths to files in build/packages/*.yaml. | 
| +def run_go_build(go_workspace, package, output, goos=None, goarch=None): | 
| + """Builds single Go package (rebuilding all its dependencies). | 
| Args: | 
| + package: package to build. | 
| + output: where to put the resulting binary. | 
| + goos: if set, overrides GOOS environment variable (removes it if ''). | 
| + goarch: if set, overrides GOARCH environment variable (removes it if ''). | 
| + """ | 
| + with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | 
| + print_go_step_title('Building %s' % package) | 
| + subprocess.check_call( | 
| + args=[ | 
| + 'python', '-u', os.path.join(new_workspace, 'env.py'), | 
| + 'go', 'build', '-a', '-v', '-o', output, package, | 
| + ], | 
| + executable=sys.executable, | 
| + stderr=subprocess.STDOUT) | 
| + | 
| + | 
| +def build_go_code(go_workspace, pkg_defs): | 
| + """Builds and installs all Go packages used by the given PackageDefs. | 
| + | 
| + Understands GOOS and GOARCH and uses slightly different build strategy when | 
| + cross-compiling. In the end <go_workspace>/bin will have all built binaries, | 
| + and only them (regardless of whether we are cross compiling or not). | 
| + | 
| + Args: | 
| + go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| + pkg_defs: list of PackageDef objects that define what to build. | 
| + """ | 
| + # Make sure there are no stale files in the workspace. | 
| + shutil.rmtree(os.path.join(go_workspace, 'bin'), ignore_errors=True) | 
| + shutil.rmtree(os.path.join(go_workspace, 'pkg'), ignore_errors=True) | 
| + os.mkdir(os.path.join(go_workspace, 'bin')) | 
| + os.mkdir(os.path.join(go_workspace, 'pkg')) | 
| + | 
| + # Grab a set of all go packages we need to build and install into GOBIN. | 
| + to_install = [] | 
| + for p in pkg_defs: | 
| + to_install.extend(p.go_packages) | 
| + to_install = sorted(set(to_install)) | 
| + if not to_install: | 
| + return | 
| + | 
| + if not is_cross_compiling(): | 
| + # If not cross compiling, build all Go code in a single "go install" step, | 
| + # it's faster that way. We can't do that when cross-compiling, since | 
| + # 'go install' isn't supposed to be used for cross-compilation and the | 
| + # toolset actively complains with "go install: cannot install cross-compiled | 
| + # binaries when GOBIN is set". | 
| + run_go_install(go_workspace, to_install) | 
| + else: | 
| + # If cross compiling, build packages one by one and put the resulting | 
| + # binaries into GOBIN, as if they were installed there. It's where the rest | 
| + # of the build.py code expected them to be (see also 'root' property in | 
| + # package definition YAMLs). It's much slower than single 'go install' since | 
| + # we rebuild all dependent libraries (including std lib!) at each 'go build' | 
| + # invocation. | 
| + go_bin = os.path.join(go_workspace, 'bin') | 
| + exe_suffix = get_target_package_vars()['exe_suffix'] | 
| + for pkg in to_install: | 
| + name = pkg[pkg.rfind('/')+1:] | 
| + run_go_build(go_workspace, pkg, os.path.join(go_bin, name + exe_suffix)) | 
| + | 
| + | 
| +def enumerate_packages(py_venv, package_def_dir, package_def_files): | 
| + """Returns a list PackageDef instances for files in build/packages/*.yaml. | 
| + | 
| + Args: | 
| + py_env: path to python ENV where to look for YAML parser. | 
| package_def_dir: path to build/packages dir to search for *.yaml. | 
| package_def_files: optional list of filenames to limit results to. | 
| Returns: | 
| - List of absolute paths to *.yaml files under packages_dir. | 
| + List of PackageDef instances parsed from *.yaml files under packages_dir. | 
| """ | 
| - # All existing package by default. | 
| - if not package_def_files: | 
| - return sorted(glob.glob(os.path.join(package_def_dir, '*.yaml'))) | 
| paths = [] | 
| - for name in package_def_files: | 
| - abs_path = os.path.join(package_def_dir, name) | 
| - if not os.path.isfile(abs_path): | 
| - raise BuildException('No such package definition file: %s' % name) | 
| - paths.append(abs_path) | 
| - return sorted(paths) | 
| + if not package_def_files: | 
| + # All existing package by default. | 
| + paths = glob.glob(os.path.join(package_def_dir, '*.yaml')) | 
| + else: | 
| + # Otherwise pick only the ones in 'package_def_files' list. | 
| + for name in package_def_files: | 
| + abs_path = os.path.join(package_def_dir, name) | 
| + if not os.path.isfile(abs_path): | 
| + raise BuildException('No such package definition file: %s' % name) | 
| + paths.append(abs_path) | 
| + return [PackageDef(p, read_yaml(py_venv, p)) for p in sorted(paths)] | 
| def read_yaml(py_venv, path): | 
| @@ -193,14 +350,6 @@ def read_yaml(py_venv, path): | 
| return json.loads(out) | 
| -def should_process_on_builder(pkg_def_file, py_venv, builder): | 
| - """Returns True if package should be processed by current CI builder.""" | 
| - if not builder: | 
| - return True | 
| - builders = read_yaml(py_venv, pkg_def_file).get('builders') | 
| - return not builders or builder in builders | 
| - | 
| - | 
| def get_package_vars(): | 
| """Returns a dict with variables that describe the current environment. | 
| @@ -208,6 +357,54 @@ def get_package_vars(): | 
| ${variable_name}. It allows to reuse exact same definition file for similar | 
| packages (e.g. packages with same cross platform binary, but for different | 
| platforms). | 
| + | 
| + If running in cross-compilation mode, uses GOOS and GOARCH to figure out the | 
| + target platform instead of examining the host environment. | 
| + """ | 
| + if is_cross_compiling(): | 
| + return get_target_package_vars() | 
| + return get_host_package_vars() | 
| + | 
| + | 
| +def get_target_package_vars(): | 
| + """Returns a dict with variables that describe cross compilation target env. | 
| + | 
| + It contains only 'platform' and 'exe_suffix'. See 'get_package_vars' for more | 
| + details. | 
| + """ | 
| + assert is_cross_compiling() | 
| + goos = os.environ['GOOS'] | 
| + goarch = os.environ['GOARCH'] | 
| + | 
| + if goarch not in ['386', 'amd64', 'arm']: | 
| + raise BuildException('Unsupported GOARCH %s' % goarch) | 
| + | 
| + # There are many ARMs, pick the concrete instruction set. 'v6' is the default. | 
| + if goarch == 'arm': | 
| + goarm = os.environ.get('GOARM', '6') | 
| + if goarm == '6': | 
| + arch = 'armv6l' | 
| + elif goarm == '7': | 
| + arch = 'armv7l' | 
| + else: | 
| + raise BuildException('Unsupported GOARM value %s' % goarm) | 
| + else: | 
| + arch = goarch | 
| + | 
| + # We use 'mac' instead of 'darwin'. | 
| 
 
Vadim Sh.
2016/06/25 03:51:03
I can't remember why :(
 
 | 
| + if goos == 'darwin': | 
| + goos = 'mac' | 
| + | 
| + return { | 
| + 'exe_suffix': '.exe' if goos == 'windows' else '', | 
| + 'platform': '%s-%s' % (goos, arch), | 
| + } | 
| + | 
| + | 
| +def get_host_package_vars(): | 
| + """Returns a dict with variables that describe the current host environment. | 
| + | 
| + See 'get_package_vars' for more details. | 
| """ | 
| # linux, mac or windows. | 
| platform_variant = { | 
| @@ -256,12 +453,12 @@ def get_package_vars(): | 
| } | 
| -def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 
| +def build_pkg(cipd_exe, pkg_def, out_file, package_vars): | 
| """Invokes CIPD client to build a package. | 
| Args: | 
| - go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| - pkg_def_file: path to *.yaml file with package definition. | 
| + cipd_exe: path to cipd client binary to use. | 
| + pkg_def: instance of PackageDef representing this package. | 
| out_file: where to store the built package. | 
| package_vars: dict with variables to pass as -pkg-var to cipd. | 
| @@ -271,18 +468,18 @@ def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 
| Raises: | 
| BuildException on error. | 
| """ | 
| - print_title('Building: %s' % os.path.basename(pkg_def_file)) | 
| + print_title('Building: %s' % os.path.basename(out_file)) | 
| # Make sure not stale output remains. | 
| if os.path.isfile(out_file): | 
| os.remove(out_file) | 
| # Build the package. | 
| - args = ['-pkg-def', pkg_def_file] | 
| + args = ['-pkg-def', pkg_def.path] | 
| for k, v in sorted(package_vars.items()): | 
| args.extend(['-pkg-var', '%s:%s' % (k, v)]) | 
| args.extend(['-out', out_file]) | 
| - exit_code, json_output = run_cipd(go_workspace, 'pkg-build', args) | 
| + exit_code, json_output = run_cipd(cipd_exe, 'pkg-build', args) | 
| if exit_code: | 
| print >> sys.stderr, 'FAILED! ' * 10 | 
| @@ -294,11 +491,11 @@ def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 
| return info | 
| -def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 
| +def upload_pkg(cipd_exe, pkg_file, service_url, tags, service_account): | 
| """Uploads existing *.cipd file to the storage and tags it. | 
| Args: | 
| - go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| + cipd_exe: path to cipd client binary to use. | 
| pkg_file: path to *.cipd file to upload. | 
| service_url: URL of a package repository service. | 
| tags: a list of tags to attach to uploaded package instance. | 
| @@ -319,7 +516,7 @@ def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 
| if service_account: | 
| args.extend(['-service-account-json', service_account]) | 
| args.append(pkg_file) | 
| - exit_code, json_output = run_cipd(go_workspace, 'pkg-register', args) | 
| + exit_code, json_output = run_cipd(cipd_exe, 'pkg-register', args) | 
| if exit_code: | 
| print >> sys.stderr, 'FAILED! ' * 10 | 
| @@ -329,6 +526,78 @@ def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 
| return info | 
| +def build_cipd_client(go_workspace, out_dir): | 
| + """Builds cipd client binary for the host platform. | 
| + | 
| + Ignores GOOS and GOARCH env vars. Puts the client binary into | 
| + '<out_dir>/.cipd_client/cipd_<digest>'. | 
| + | 
| + This binary is used by build.py itself and later by test_packages.py. | 
| + | 
| + Args: | 
| + go_workspace: path to Go workspace root (contains 'env.py', 'src', etc). | 
| + out_dir: build output directory, will be used to store the binary. | 
| + | 
| + Returns: | 
| + Path to the built binary. | 
| + """ | 
| + # To avoid rebuilding cipd client all the time, we cache it in out/*, using | 
| 
 
Vadim Sh.
2016/06/25 03:51:03
this is the most hacky part of this CL
we need CI
 
 | 
| + # a combination of DEPS+deps.lock+bootstrap.py as a cache key (they define | 
| + # exact set of sources used to build the cipd binary). | 
| + # | 
| + # We can't just use the client in infra.git/cipd/* because it is built by this | 
| + # script itself: it introduced bootstrap dependency cycle in case we need to | 
| + # add a new platform or if we wipe cipd backend storage. | 
| + seed_paths = [ | 
| + os.path.join(ROOT, 'DEPS'), | 
| + os.path.join(ROOT, 'go', 'deps.lock'), | 
| + os.path.join(ROOT, 'go', 'bootstrap.py'), | 
| 
 
Vadim Sh.
2016/06/25 03:51:03
indirectly defines stuff like Go toolset version
 
 | 
| + ] | 
| + digest = hashlib.sha1() | 
| + for p in seed_paths: | 
| + with open(p, 'rb') as f: | 
| + digest.update(f.read()) | 
| + cache_key = digest.hexdigest()[:20] | 
| + | 
| + # Already have it? | 
| + cipd_out_dir = os.path.join(out_dir, '.cipd_client') | 
| + cipd_exe = os.path.join(cipd_out_dir, 'cipd_%s%s' % (cache_key, EXE_SUFFIX)) | 
| + if os.path.exists(cipd_exe): | 
| + return cipd_exe | 
| + | 
| + # Nuke all previous copies, make sure out_dir exists. | 
| + if os.path.exists(cipd_out_dir): | 
| + for p in glob.glob(os.path.join(cipd_out_dir, 'cipd_*')): | 
| + os.remove(p) | 
| + else: | 
| + os.makedirs(cipd_out_dir) | 
| + | 
| + # Build cipd client binary for the host platform. | 
| + run_go_build( | 
| + go_workspace, | 
| + package='github.com/luci/luci-go/client/cmd/cipd', | 
| + output=cipd_exe, | 
| + goos='', | 
| + goarch='') | 
| + | 
| + return cipd_exe | 
| + | 
| + | 
| +def get_build_out_file(package_out_dir, pkg_def): | 
| + """Returns a path where to put built *.cipd package file. | 
| + | 
| + Args: | 
| + package_out_dir: root directory where to put *.cipd files. | 
| + pkg_def: instance of PackageDef being built. | 
| + """ | 
| + # When cross-compiling, append a suffix to package file name to indicate that | 
| + # it's for foreign platform. | 
| + sfx = '' | 
| + if is_cross_compiling(): | 
| + sfx = '+' + get_target_package_vars()['platform'] | 
| + return os.path.join(package_out_dir, pkg_def.name + sfx + '.cipd') | 
| + | 
| + | 
| def run( | 
| py_venv, | 
| go_workspace, | 
| @@ -343,7 +612,7 @@ def run( | 
| tags, | 
| service_account_json, | 
| json_output): | 
| - """Rebuild python and Go universes and CIPD packages. | 
| + """Rebuilds python and Go universes and CIPD packages. | 
| Args: | 
| py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. | 
| @@ -365,16 +634,16 @@ def run( | 
| """ | 
| assert build or upload, 'Both build and upload are False, nothing to do' | 
| - # Remove stale output so that test_packages.py do not test old files when | 
| - # invoked without arguments. | 
| - if build: | 
| - for path in glob.glob(os.path.join(package_out_dir, '*.cipd')): | 
| - os.remove(path) | 
| + # We need both GOOS and GOARCH or none. | 
| + if is_cross_compiling(): | 
| + if not os.environ.get('GOOS') or not os.environ.get('GOARCH'): | 
| + print >> sys.stderr, ( | 
| + 'When cross-compiling both GOOS and GOARCH environment variables ' | 
| + 'must be set.') | 
| + return 1 | 
| - packages_to_build = [ | 
| - p for p in enumerate_packages_to_build(package_def_dir, package_def_files) | 
| - if should_process_on_builder(p, py_venv, builder) | 
| - ] | 
| + all_packages = enumerate_packages(py_venv, package_def_dir, package_def_files) | 
| + packages_to_build = [p for p in all_packages if p.should_build(builder)] | 
| print_title('Overview') | 
| print 'Service URL: %s' % service_url | 
| @@ -383,8 +652,8 @@ def run( | 
| print 'Package definition files to process on %s:' % builder | 
| else: | 
| print 'Package definition files to process:' | 
| - for pkg_def_file in packages_to_build: | 
| - print ' %s' % os.path.basename(pkg_def_file) | 
| + for pkg_def in packages_to_build: | 
| + print ' %s' % pkg_def.name | 
| if not packages_to_build: | 
| print ' <none>' | 
| @@ -402,32 +671,49 @@ def run( | 
| print 'Nothing to do.' | 
| return 0 | 
| + # Remove old build artifacts to avoid stale files in case the script crashes | 
| 
 
Vadim Sh.
2016/06/25 03:51:03
it used to remove everything under out/*, now buil
 
 | 
| + # for some reason. | 
| + if build: | 
| + print_title('Cleaning %s' % package_out_dir) | 
| + if not os.path.exists(package_out_dir): | 
| + os.makedirs(package_out_dir) | 
| + cleaned = False | 
| + for pkg_def in packages_to_build: | 
| + out_file = get_build_out_file(package_out_dir, pkg_def) | 
| + if os.path.exists(out_file): | 
| + print 'Removing stale %s' % os.path.basename(out_file) | 
| + os.remove(out_file) | 
| + cleaned = True | 
| + if not cleaned: | 
| + print 'Nothing to clean' | 
| + | 
| + # Build the cipd client needed later to build or upload packages. | 
| + cipd_exe = build_cipd_client(go_workspace, package_out_dir) | 
| + | 
| # Build the world. | 
| if build: | 
| - build_callback() | 
| + build_callback(packages_to_build) | 
| # Package it. | 
| failed = [] | 
| succeeded = [] | 
| - for pkg_def_file in packages_to_build: | 
| - # path/name.yaml -> out/name.cipd. | 
| - name = os.path.splitext(os.path.basename(pkg_def_file))[0] | 
| - out_file = os.path.join(package_out_dir, name + '.cipd') | 
| + for pkg_def in packages_to_build: | 
| + out_file = get_build_out_file(package_out_dir, pkg_def) | 
| try: | 
| info = None | 
| if build: | 
| - info = build_pkg(go_workspace, pkg_def_file, out_file, package_vars) | 
| + info = build_pkg(cipd_exe, pkg_def, out_file, package_vars) | 
| if upload: | 
| info = upload_pkg( | 
| - go_workspace, | 
| + cipd_exe, | 
| out_file, | 
| service_url, | 
| tags, | 
| service_account_json) | 
| assert info is not None | 
| - succeeded.append({'pkg_def_name': name, 'info': info}) | 
| + succeeded.append({'pkg_def_name': pkg_def.name, 'info': info}) | 
| except (BuildException, UploadException) as e: | 
| - failed.append({'pkg_def_name': name, 'error': str(e)}) | 
| + failed.append({'pkg_def_name': pkg_def.name, 'error': str(e)}) | 
| print_title('Summary') | 
| for d in failed: | 
| @@ -448,23 +734,24 @@ def run( | 
| return 1 if failed else 0 | 
| -def build_infra(): | 
| - """Builds infra.git multiverse.""" | 
| - # Python side. | 
| - print_title('Making sure python virtual environment is fresh') | 
| - run_python( | 
| - script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 
| - args=[ | 
| - '--deps_file', | 
| - os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 
| - os.path.join(ROOT, 'ENV'), | 
| - ]) | 
| - # Go side. | 
| - build_go(os.path.join(ROOT, 'go'), [ | 
| - 'infra/...', | 
| - 'github.com/luci/luci-go/client/...', | 
| - 'github.com/luci/luci-go/tools/...', | 
| - ]) | 
| +def build_infra(pkg_defs): | 
| + """Builds infra.git multiverse. | 
| + | 
| + Args: | 
| + pkg_defs: list of PackageDef instances for packages being built. | 
| + """ | 
| + # Skip building python if not used or if cross-compiling. | 
| + if any(p.uses_python_env for p in pkg_defs) and not is_cross_compiling(): | 
| + print_title('Making sure python virtual environment is fresh') | 
| + run_python( | 
| + script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 
| + args=[ | 
| + '--deps_file', | 
| + os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 
| + os.path.join(ROOT, 'ENV'), | 
| + ]) | 
| + # Build all necessary go binaries. | 
| + build_go_code(os.path.join(ROOT, 'go'), pkg_defs) | 
| def main( |