Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The Chromium Authors. All rights reserved. | 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """This script rebuilds Python & Go universes of infra.git multiverse and | 6 """This script rebuilds Python & Go universes of infra.git multiverse and |
| 7 invokes CIPD client to package and upload chunks of it to the CIPD repository as | 7 invokes CIPD client to package and upload chunks of it to the CIPD repository as |
| 8 individual packages. | 8 individual packages. |
| 9 | 9 |
| 10 See build/packages/*.yaml for definition of packages. | 10 See build/packages/*.yaml for definition of packages and README.md for more |
| 11 details. | |
| 11 """ | 12 """ |
| 12 | 13 |
| 13 import argparse | 14 import argparse |
| 15 import contextlib | |
| 14 import glob | 16 import glob |
| 17 import hashlib | |
| 15 import json | 18 import json |
| 16 import os | 19 import os |
| 17 import platform | 20 import platform |
| 18 import shutil | 21 import shutil |
| 19 import subprocess | 22 import subprocess |
| 20 import sys | 23 import sys |
| 21 import tempfile | 24 import tempfile |
| 22 | 25 |
| 23 | 26 |
| 24 # Root of infra.git repository. | 27 # Root of infra.git repository. |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 35 | 38 |
| 36 | 39 |
| 37 class BuildException(Exception): | 40 class BuildException(Exception): |
| 38 """Raised on errors during package build step.""" | 41 """Raised on errors during package build step.""" |
| 39 | 42 |
| 40 | 43 |
| 41 class UploadException(Exception): | 44 class UploadException(Exception): |
| 42 """Raised on errors during package upload step.""" | 45 """Raised on errors during package upload step.""" |
| 43 | 46 |
| 44 | 47 |
| 48 class PackageDef(object): | |
| 49 """Represents parsed package *.yaml file.""" | |
| 50 | |
| 51 def __init__(self, path, pkg_def): | |
| 52 self.path = path | |
| 53 self.pkg_def = pkg_def | |
| 54 | |
| 55 @property | |
| 56 def name(self): | |
| 57 """Returns name of YAML file (without the directory path and extension).""" | |
| 58 return os.path.splitext(os.path.basename(self.path))[0] | |
| 59 | |
| 60 @property | |
| 61 def uses_python_env(self): | |
| 62 """Returns True if 'uses_python_env' in the YAML file is set.""" | |
| 63 return self.pkg_def.get('uses_python_env') | |
| 64 | |
| 65 @property | |
| 66 def go_packages(self): | |
| 67 """Returns a list of Go packages that must be installed for this package.""" | |
| 68 return self.pkg_def.get('go_packages') or [] | |
| 69 | |
| 70 def should_build(self, builder): | |
| 71 """Returns True if package should be built in the current environment. | |
| 72 | |
| 73 Takes into account 'builders' and 'supports_cross_compilation' properties of | |
| 74 package definition file. | |
| 75 """ | |
| 76 # If '--builder' is not specified, ignore 'builders' property. Otherwise, if | |
| 77 # 'builders' YAML attribute it not empty, verify --builder is listed there. | |
| 78 builders = self.pkg_def.get('builders') | |
| 79 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
| |
| 80 return False | |
| 81 | |
| 82 # If cross compiling, pick only packages that supports cross compilation. | |
| 83 if is_cross_compiling(): | |
|
Vadim Sh.
2016/06/25 03:51:04
this is new
| |
| 84 return bool(self.pkg_def.get('supports_cross_compilation')) | |
| 85 | |
| 86 return True | |
| 87 | |
| 88 | |
| 89 def is_cross_compiling(): | |
| 90 """Returns true if using GOOS or GOARCH env vars. | |
| 91 | |
| 92 We also check at the start of the script that if one of them is used, then | |
| 93 the other is specified as well. | |
| 94 """ | |
| 95 return bool(os.environ.get('GOOS')) or bool(os.environ.get('GOARCH')) | |
| 96 | |
| 97 | |
| 45 def run_python(script, args): | 98 def run_python(script, args): |
| 46 """Invokes a python script. | 99 """Invokes a python script. |
| 47 | 100 |
| 48 Raises: | 101 Raises: |
| 49 subprocess.CalledProcessError on non zero exit code. | 102 subprocess.CalledProcessError on non zero exit code. |
| 50 """ | 103 """ |
| 51 print 'Running %s %s' % (script, ' '.join(args)) | 104 print 'Running %s %s' % (script, ' '.join(args)) |
| 52 subprocess.check_call( | 105 subprocess.check_call( |
| 53 args=['python', '-u', script] + list(args), executable=sys.executable) | 106 args=['python', '-u', script] + list(args), executable=sys.executable) |
| 54 | 107 |
| 55 | 108 |
| 56 def run_cipd(go_workspace, cmd, args): | 109 def run_cipd(cipd_exe, cmd, args): |
| 57 """Invokes CIPD, parsing -json-output result. | 110 """Invokes CIPD, parsing -json-output result. |
| 58 | 111 |
| 59 Args: | 112 Args: |
| 60 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 113 cipd_exe: path to cipd client binary to run. |
| 61 cmd: cipd subcommand to run. | 114 cmd: cipd subcommand to run. |
| 62 args: list of command line arguments to pass to the subcommand. | 115 args: list of command line arguments to pass to the subcommand. |
| 63 | 116 |
| 64 Returns: | 117 Returns: |
| 65 (Process exit code, parsed JSON output or None). | 118 (Process exit code, parsed JSON output or None). |
| 66 """ | 119 """ |
| 67 temp_file = None | 120 temp_file = None |
| 68 try: | 121 try: |
| 69 fd, temp_file = tempfile.mkstemp(suffix='.json', prefix='cipd_%s' % cmd) | 122 fd, temp_file = tempfile.mkstemp(suffix='.json', prefix='cipd_%s' % cmd) |
| 70 os.close(fd) | 123 os.close(fd) |
| 71 | 124 |
| 72 cmd_line = [ | 125 cmd_line = [cipd_exe, cmd, '-json-output', temp_file] + list(args) |
| 73 os.path.join(go_workspace, 'bin', 'cipd' + EXE_SUFFIX), | |
| 74 cmd, '-json-output', temp_file, | |
| 75 ] + list(args) | |
| 76 | 126 |
| 77 print 'Running %s' % ' '.join(cmd_line) | 127 print 'Running %s' % ' '.join(cmd_line) |
| 78 exit_code = subprocess.call(args=cmd_line, executable=cmd_line[0]) | 128 exit_code = subprocess.call(args=cmd_line, executable=cmd_line[0]) |
| 79 try: | 129 try: |
| 80 with open(temp_file, 'r') as f: | 130 with open(temp_file, 'r') as f: |
| 81 json_output = json.load(f) | 131 json_output = json.load(f) |
| 82 except (IOError, ValueError): | 132 except (IOError, ValueError): |
| 83 json_output = None | 133 json_output = None |
| 84 | 134 |
| 85 return exit_code, json_output | 135 return exit_code, json_output |
| 86 finally: | 136 finally: |
| 87 try: | 137 try: |
| 88 if temp_file: | 138 if temp_file: |
| 89 os.remove(temp_file) | 139 os.remove(temp_file) |
| 90 except OSError: | 140 except OSError: |
| 91 pass | 141 pass |
| 92 | 142 |
| 93 | 143 |
| 94 def print_title(title): | 144 def print_title(title): |
| 95 """Pretty prints a banner to stdout.""" | 145 """Pretty prints a banner to stdout.""" |
| 96 sys.stdout.flush() | 146 sys.stdout.flush() |
| 97 sys.stderr.flush() | 147 sys.stderr.flush() |
| 98 print | 148 print |
| 99 print '-' * 80 | 149 print '-' * 80 |
| 100 print title | 150 print title |
| 101 print '-' * 80 | 151 print '-' * 80 |
| 102 | 152 |
| 103 | 153 |
| 104 def build_go(go_workspace, packages): | 154 def print_go_step_title(title): |
| 105 """Bootstraps go environment and rebuilds (and installs) Go packages. | 155 """Same as 'print_title', but also appends values of GOOS and GOARCH.""" |
| 156 if is_cross_compiling(): | |
| 157 title += '\nwith' | |
| 158 title += '\n GOOS=%s' % ( | |
| 159 os.environ['GOOS'] if 'GOOS' in os.environ else '<native>') | |
| 160 title += '\n GOARCH=%s' % ( | |
| 161 os.environ['GOARCH'] if 'GOARCH' in os.environ else '<native>') | |
| 162 print_title(title) | |
| 106 | 163 |
| 107 Compiles and installs packages into default GOBIN, which is <path>/go/bin/ | 164 |
| 108 (it is setup by go/env.py and depends on what workspace is used). | 165 @contextlib.contextmanager |
| 166 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
| |
| 167 """Symlinks Go workspace into new root, modifies os.environ. | |
| 168 | |
| 169 Go toolchain embeds absolute paths to *.go files into the executable. Use | |
| 170 symlink with stable path to make executables independent of checkout path. | |
| 109 | 171 |
| 110 Args: | 172 Args: |
| 111 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 173 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 112 packages: list of packages to build (can include '...' patterns). | 174 goos: if set, overrides GOOS environment variable (removes it if ''). |
| 175 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 176 | |
| 177 Yields: | |
| 178 Path where go_workspace is symlinked to. | |
| 113 """ | 179 """ |
| 114 print_title('Compiling Go code: %s' % ', '.join(packages)) | |
| 115 | |
| 116 # Go toolchain embeds absolute paths to *.go files into the executable. Use | |
| 117 # symlink with stable path to make executables independent of checkout path. | |
| 118 new_root = None | 180 new_root = None |
| 119 new_workspace = go_workspace | 181 new_workspace = go_workspace |
| 120 if sys.platform != 'win32': | 182 if sys.platform != 'win32': |
| 121 new_root = '/tmp/_chrome_infra_build' | 183 new_root = '/tmp/_chrome_infra_build' |
| 122 if os.path.exists(new_root): | 184 if os.path.exists(new_root): |
| 123 assert os.path.islink(new_root) | 185 assert os.path.islink(new_root) |
| 124 os.remove(new_root) | 186 os.remove(new_root) |
| 125 os.symlink(GCLIENT_ROOT, new_root) | 187 os.symlink(GCLIENT_ROOT, new_root) |
| 126 rel = os.path.relpath(go_workspace, GCLIENT_ROOT) | 188 rel = os.path.relpath(go_workspace, GCLIENT_ROOT) |
| 127 assert not rel.startswith('..'), rel | 189 assert not rel.startswith('..'), rel |
| 128 new_workspace = os.path.join(new_root, rel) | 190 new_workspace = os.path.join(new_root, rel) |
| 129 | 191 |
| 130 # Remove any stale binaries and libraries. | 192 orig_environ = os.environ.copy() |
| 131 shutil.rmtree(os.path.join(new_workspace, 'bin'), ignore_errors=True) | |
| 132 shutil.rmtree(os.path.join(new_workspace, 'pkg'), ignore_errors=True) | |
| 133 | 193 |
| 134 # Recompile ('-a'). | 194 if goos is not None: |
| 195 if goos == '': | |
| 196 os.environ.pop('GOOS', None) | |
| 197 else: | |
| 198 os.environ['GOOS'] = goos | |
| 199 if goarch is not None: | |
| 200 if goarch == '': | |
| 201 os.environ.pop('GOARCH', None) | |
| 202 else: | |
| 203 os.environ['GOARCH'] = goarch | |
| 204 | |
| 135 try: | 205 try: |
| 206 yield new_workspace | |
| 207 finally: | |
| 208 if new_root: | |
| 209 os.remove(new_root) | |
| 210 os.environ = orig_environ | |
| 211 | |
| 212 | |
| 213 def run_go_install(go_workspace, packages, goos=None, goarch=None): | |
| 214 """Rebuilds (and installs) Go packages into GOBIN via 'go install ...'. | |
| 215 | |
| 216 Compiles and installs packages into default GOBIN, which is <go_workspace>/bin | |
| 217 (it is setup by go/env.py). | |
| 218 | |
| 219 Args: | |
| 220 go_workspace: path to 'infra/go' or 'infra_internal/go'. | |
| 221 packages: list of packages to build (can include '...' patterns). | |
| 222 goos: if set, overrides GOOS environment variable (removes it if ''). | |
| 223 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 224 """ | |
| 225 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | |
| 226 print_go_step_title('Building:\n %s' % '\n '.join(packages)) | |
| 136 subprocess.check_call( | 227 subprocess.check_call( |
| 137 args=[ | 228 args=[ |
| 138 'python', '-u', os.path.join(new_workspace, 'env.py'), | 229 'python', '-u', os.path.join(new_workspace, 'env.py'), |
| 139 'go', 'install', '-a', '-v', | 230 'go', 'install', '-a', '-v', |
| 140 ] + list(packages), | 231 ] + list(packages), |
| 141 executable=sys.executable, | 232 executable=sys.executable, |
| 142 stderr=subprocess.STDOUT) | 233 stderr=subprocess.STDOUT) |
| 143 finally: | |
| 144 if new_root: | |
| 145 os.remove(new_root) | |
| 146 | 234 |
| 147 | 235 |
| 148 def enumerate_packages_to_build(package_def_dir, package_def_files=None): | 236 def run_go_build(go_workspace, package, output, goos=None, goarch=None): |
| 149 """Returns a list of absolute paths to files in build/packages/*.yaml. | 237 """Builds single Go package (rebuilding all its dependencies). |
| 150 | 238 |
| 151 Args: | 239 Args: |
| 240 package: package to build. | |
| 241 output: where to put the resulting binary. | |
| 242 goos: if set, overrides GOOS environment variable (removes it if ''). | |
| 243 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 244 """ | |
| 245 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | |
| 246 print_go_step_title('Building %s' % package) | |
| 247 subprocess.check_call( | |
| 248 args=[ | |
| 249 'python', '-u', os.path.join(new_workspace, 'env.py'), | |
| 250 'go', 'build', '-a', '-v', '-o', output, package, | |
| 251 ], | |
| 252 executable=sys.executable, | |
| 253 stderr=subprocess.STDOUT) | |
| 254 | |
| 255 | |
| 256 def build_go_code(go_workspace, pkg_defs): | |
| 257 """Builds and installs all Go packages used by the given PackageDefs. | |
| 258 | |
| 259 Understands GOOS and GOARCH and uses slightly different build strategy when | |
| 260 cross-compiling. In the end <go_workspace>/bin will have all built binaries, | |
| 261 and only them (regardless of whether we are cross compiling or not). | |
| 262 | |
| 263 Args: | |
| 264 go_workspace: path to 'infra/go' or 'infra_internal/go'. | |
| 265 pkg_defs: list of PackageDef objects that define what to build. | |
| 266 """ | |
| 267 # Make sure there are no stale files in the workspace. | |
| 268 shutil.rmtree(os.path.join(go_workspace, 'bin'), ignore_errors=True) | |
| 269 shutil.rmtree(os.path.join(go_workspace, 'pkg'), ignore_errors=True) | |
| 270 os.mkdir(os.path.join(go_workspace, 'bin')) | |
| 271 os.mkdir(os.path.join(go_workspace, 'pkg')) | |
| 272 | |
| 273 # Grab a set of all go packages we need to build and install into GOBIN. | |
| 274 to_install = [] | |
| 275 for p in pkg_defs: | |
| 276 to_install.extend(p.go_packages) | |
| 277 to_install = sorted(set(to_install)) | |
| 278 if not to_install: | |
| 279 return | |
| 280 | |
| 281 if not is_cross_compiling(): | |
| 282 # If not cross compiling, build all Go code in a single "go install" step, | |
| 283 # it's faster that way. We can't do that when cross-compiling, since | |
| 284 # 'go install' isn't supposed to be used for cross-compilation and the | |
| 285 # toolset actively complains with "go install: cannot install cross-compiled | |
| 286 # binaries when GOBIN is set". | |
| 287 run_go_install(go_workspace, to_install) | |
| 288 else: | |
| 289 # If cross compiling, build packages one by one and put the resulting | |
| 290 # binaries into GOBIN, as if they were installed there. It's where the rest | |
| 291 # of the build.py code expected them to be (see also 'root' property in | |
| 292 # package definition YAMLs). It's much slower than single 'go install' since | |
| 293 # we rebuild all dependent libraries (including std lib!) at each 'go build' | |
| 294 # invocation. | |
| 295 go_bin = os.path.join(go_workspace, 'bin') | |
| 296 exe_suffix = get_target_package_vars()['exe_suffix'] | |
| 297 for pkg in to_install: | |
| 298 name = pkg[pkg.rfind('/')+1:] | |
| 299 run_go_build(go_workspace, pkg, os.path.join(go_bin, name + exe_suffix)) | |
| 300 | |
| 301 | |
| 302 def enumerate_packages(py_venv, package_def_dir, package_def_files): | |
| 303 """Returns a list PackageDef instances for files in build/packages/*.yaml. | |
| 304 | |
| 305 Args: | |
| 306 py_env: path to python ENV where to look for YAML parser. | |
| 152 package_def_dir: path to build/packages dir to search for *.yaml. | 307 package_def_dir: path to build/packages dir to search for *.yaml. |
| 153 package_def_files: optional list of filenames to limit results to. | 308 package_def_files: optional list of filenames to limit results to. |
| 154 | 309 |
| 155 Returns: | 310 Returns: |
| 156 List of absolute paths to *.yaml files under packages_dir. | 311 List of PackageDef instances parsed from *.yaml files under packages_dir. |
| 157 """ | 312 """ |
| 158 # All existing package by default. | 313 paths = [] |
| 159 if not package_def_files: | 314 if not package_def_files: |
| 160 return sorted(glob.glob(os.path.join(package_def_dir, '*.yaml'))) | 315 # All existing package by default. |
| 161 paths = [] | 316 paths = glob.glob(os.path.join(package_def_dir, '*.yaml')) |
| 162 for name in package_def_files: | 317 else: |
| 163 abs_path = os.path.join(package_def_dir, name) | 318 # Otherwise pick only the ones in 'package_def_files' list. |
| 164 if not os.path.isfile(abs_path): | 319 for name in package_def_files: |
| 165 raise BuildException('No such package definition file: %s' % name) | 320 abs_path = os.path.join(package_def_dir, name) |
| 166 paths.append(abs_path) | 321 if not os.path.isfile(abs_path): |
| 167 return sorted(paths) | 322 raise BuildException('No such package definition file: %s' % name) |
| 323 paths.append(abs_path) | |
| 324 return [PackageDef(p, read_yaml(py_venv, p)) for p in sorted(paths)] | |
| 168 | 325 |
| 169 | 326 |
| 170 def read_yaml(py_venv, path): | 327 def read_yaml(py_venv, path): |
| 171 """Returns content of YAML file as python dict.""" | 328 """Returns content of YAML file as python dict.""" |
| 172 # YAML lib is in venv, not activated here. Go through hoops. | 329 # YAML lib is in venv, not activated here. Go through hoops. |
| 173 oneliner = ( | 330 oneliner = ( |
| 174 'import json, sys, yaml; ' | 331 'import json, sys, yaml; ' |
| 175 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') | 332 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') |
| 176 if sys.platform == 'win32': | 333 if sys.platform == 'win32': |
| 177 python_venv_path = ('Scripts', 'python.exe') | 334 python_venv_path = ('Scripts', 'python.exe') |
| 178 else: | 335 else: |
| 179 python_venv_path = ('bin', 'python') | 336 python_venv_path = ('bin', 'python') |
| 180 executable = os.path.join(py_venv, *python_venv_path) | 337 executable = os.path.join(py_venv, *python_venv_path) |
| 181 env = os.environ.copy() | 338 env = os.environ.copy() |
| 182 env.pop('PYTHONPATH', None) | 339 env.pop('PYTHONPATH', None) |
| 183 proc = subprocess.Popen( | 340 proc = subprocess.Popen( |
| 184 [executable, '-c', oneliner], | 341 [executable, '-c', oneliner], |
| 185 executable=executable, | 342 executable=executable, |
| 186 stdin=subprocess.PIPE, | 343 stdin=subprocess.PIPE, |
| 187 stdout=subprocess.PIPE, | 344 stdout=subprocess.PIPE, |
| 188 env=env) | 345 env=env) |
| 189 with open(path, 'r') as f: | 346 with open(path, 'r') as f: |
| 190 out, _ = proc.communicate(f.read()) | 347 out, _ = proc.communicate(f.read()) |
| 191 if proc.returncode: | 348 if proc.returncode: |
| 192 raise BuildException('Failed to parse YAML at %s' % path) | 349 raise BuildException('Failed to parse YAML at %s' % path) |
| 193 return json.loads(out) | 350 return json.loads(out) |
| 194 | 351 |
| 195 | 352 |
| 196 def should_process_on_builder(pkg_def_file, py_venv, builder): | |
| 197 """Returns True if package should be processed by current CI builder.""" | |
| 198 if not builder: | |
| 199 return True | |
| 200 builders = read_yaml(py_venv, pkg_def_file).get('builders') | |
| 201 return not builders or builder in builders | |
| 202 | |
| 203 | |
| 204 def get_package_vars(): | 353 def get_package_vars(): |
| 205 """Returns a dict with variables that describe the current environment. | 354 """Returns a dict with variables that describe the current environment. |
| 206 | 355 |
| 207 Variables can be referenced in the package definition YAML as | 356 Variables can be referenced in the package definition YAML as |
| 208 ${variable_name}. It allows to reuse exact same definition file for similar | 357 ${variable_name}. It allows to reuse exact same definition file for similar |
| 209 packages (e.g. packages with same cross platform binary, but for different | 358 packages (e.g. packages with same cross platform binary, but for different |
| 210 platforms). | 359 platforms). |
| 360 | |
| 361 If running in cross-compilation mode, uses GOOS and GOARCH to figure out the | |
| 362 target platform instead of examining the host environment. | |
| 363 """ | |
| 364 if is_cross_compiling(): | |
| 365 return get_target_package_vars() | |
| 366 return get_host_package_vars() | |
| 367 | |
| 368 | |
| 369 def get_target_package_vars(): | |
| 370 """Returns a dict with variables that describe cross compilation target env. | |
| 371 | |
| 372 It contains only 'platform' and 'exe_suffix'. See 'get_package_vars' for more | |
| 373 details. | |
| 374 """ | |
| 375 assert is_cross_compiling() | |
| 376 goos = os.environ['GOOS'] | |
| 377 goarch = os.environ['GOARCH'] | |
| 378 | |
| 379 if goarch not in ['386', 'amd64', 'arm']: | |
| 380 raise BuildException('Unsupported GOARCH %s' % goarch) | |
| 381 | |
| 382 # There are many ARMs, pick the concrete instruction set. 'v6' is the default. | |
| 383 if goarch == 'arm': | |
| 384 goarm = os.environ.get('GOARM', '6') | |
| 385 if goarm == '6': | |
| 386 arch = 'armv6l' | |
| 387 elif goarm == '7': | |
| 388 arch = 'armv7l' | |
| 389 else: | |
| 390 raise BuildException('Unsupported GOARM value %s' % goarm) | |
| 391 else: | |
| 392 arch = goarch | |
| 393 | |
| 394 # We use 'mac' instead of 'darwin'. | |
|
Vadim Sh.
2016/06/25 03:51:03
I can't remember why :(
| |
| 395 if goos == 'darwin': | |
| 396 goos = 'mac' | |
| 397 | |
| 398 return { | |
| 399 'exe_suffix': '.exe' if goos == 'windows' else '', | |
| 400 'platform': '%s-%s' % (goos, arch), | |
| 401 } | |
| 402 | |
| 403 | |
| 404 def get_host_package_vars(): | |
| 405 """Returns a dict with variables that describe the current host environment. | |
| 406 | |
| 407 See 'get_package_vars' for more details. | |
| 211 """ | 408 """ |
| 212 # linux, mac or windows. | 409 # linux, mac or windows. |
| 213 platform_variant = { | 410 platform_variant = { |
| 214 'darwin': 'mac', | 411 'darwin': 'mac', |
| 215 'linux2': 'linux', | 412 'linux2': 'linux', |
| 216 'win32': 'windows', | 413 'win32': 'windows', |
| 217 }.get(sys.platform) | 414 }.get(sys.platform) |
| 218 if not platform_variant: | 415 if not platform_variant: |
| 219 raise ValueError('Unknown OS: %s' % sys.platform) | 416 raise ValueError('Unknown OS: %s' % sys.platform) |
| 220 | 417 |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 249 'exe_suffix': EXE_SUFFIX, | 446 'exe_suffix': EXE_SUFFIX, |
| 250 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. | 447 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. |
| 251 'os_ver': os_ver, | 448 'os_ver': os_ver, |
| 252 # e.g. 'linux-amd64' | 449 # e.g. 'linux-amd64' |
| 253 'platform': '%s-%s' % (platform_variant, platform_arch), | 450 'platform': '%s-%s' % (platform_variant, platform_arch), |
| 254 # e.g. '27' (dots are not allowed in package names). | 451 # e.g. '27' (dots are not allowed in package names). |
| 255 'python_version': '%s%s' % sys.version_info[:2], | 452 'python_version': '%s%s' % sys.version_info[:2], |
| 256 } | 453 } |
| 257 | 454 |
| 258 | 455 |
| 259 def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 456 def build_pkg(cipd_exe, pkg_def, out_file, package_vars): |
| 260 """Invokes CIPD client to build a package. | 457 """Invokes CIPD client to build a package. |
| 261 | 458 |
| 262 Args: | 459 Args: |
| 263 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 460 cipd_exe: path to cipd client binary to use. |
| 264 pkg_def_file: path to *.yaml file with package definition. | 461 pkg_def: instance of PackageDef representing this package. |
| 265 out_file: where to store the built package. | 462 out_file: where to store the built package. |
| 266 package_vars: dict with variables to pass as -pkg-var to cipd. | 463 package_vars: dict with variables to pass as -pkg-var to cipd. |
| 267 | 464 |
| 268 Returns: | 465 Returns: |
| 269 {'package': <name>, 'instance_id': <sha1>} | 466 {'package': <name>, 'instance_id': <sha1>} |
| 270 | 467 |
| 271 Raises: | 468 Raises: |
| 272 BuildException on error. | 469 BuildException on error. |
| 273 """ | 470 """ |
| 274 print_title('Building: %s' % os.path.basename(pkg_def_file)) | 471 print_title('Building: %s' % os.path.basename(out_file)) |
| 275 | 472 |
| 276 # Make sure not stale output remains. | 473 # Make sure not stale output remains. |
| 277 if os.path.isfile(out_file): | 474 if os.path.isfile(out_file): |
| 278 os.remove(out_file) | 475 os.remove(out_file) |
| 279 | 476 |
| 280 # Build the package. | 477 # Build the package. |
| 281 args = ['-pkg-def', pkg_def_file] | 478 args = ['-pkg-def', pkg_def.path] |
| 282 for k, v in sorted(package_vars.items()): | 479 for k, v in sorted(package_vars.items()): |
| 283 args.extend(['-pkg-var', '%s:%s' % (k, v)]) | 480 args.extend(['-pkg-var', '%s:%s' % (k, v)]) |
| 284 args.extend(['-out', out_file]) | 481 args.extend(['-out', out_file]) |
| 285 exit_code, json_output = run_cipd(go_workspace, 'pkg-build', args) | 482 exit_code, json_output = run_cipd(cipd_exe, 'pkg-build', args) |
| 286 if exit_code: | 483 if exit_code: |
| 287 print | 484 print |
| 288 print >> sys.stderr, 'FAILED! ' * 10 | 485 print >> sys.stderr, 'FAILED! ' * 10 |
| 289 raise BuildException('Failed to build the CIPD package, see logs') | 486 raise BuildException('Failed to build the CIPD package, see logs') |
| 290 | 487 |
| 291 # Expected result is {'package': 'name', 'instance_id': 'sha1'} | 488 # Expected result is {'package': 'name', 'instance_id': 'sha1'} |
| 292 info = json_output['result'] | 489 info = json_output['result'] |
| 293 print '%s %s' % (info['package'], info['instance_id']) | 490 print '%s %s' % (info['package'], info['instance_id']) |
| 294 return info | 491 return info |
| 295 | 492 |
| 296 | 493 |
| 297 def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 494 def upload_pkg(cipd_exe, pkg_file, service_url, tags, service_account): |
| 298 """Uploads existing *.cipd file to the storage and tags it. | 495 """Uploads existing *.cipd file to the storage and tags it. |
| 299 | 496 |
| 300 Args: | 497 Args: |
| 301 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 498 cipd_exe: path to cipd client binary to use. |
| 302 pkg_file: path to *.cipd file to upload. | 499 pkg_file: path to *.cipd file to upload. |
| 303 service_url: URL of a package repository service. | 500 service_url: URL of a package repository service. |
| 304 tags: a list of tags to attach to uploaded package instance. | 501 tags: a list of tags to attach to uploaded package instance. |
| 305 service_account: path to *.json file with service account to use. | 502 service_account: path to *.json file with service account to use. |
| 306 | 503 |
| 307 Returns: | 504 Returns: |
| 308 {'package': <name>, 'instance_id': <sha1>} | 505 {'package': <name>, 'instance_id': <sha1>} |
| 309 | 506 |
| 310 Raises: | 507 Raises: |
| 311 UploadException on error. | 508 UploadException on error. |
| 312 """ | 509 """ |
| 313 print_title('Uploading: %s' % os.path.basename(pkg_file)) | 510 print_title('Uploading: %s' % os.path.basename(pkg_file)) |
| 314 | 511 |
| 315 args = ['-service-url', service_url] | 512 args = ['-service-url', service_url] |
| 316 for tag in sorted(tags): | 513 for tag in sorted(tags): |
| 317 args.extend(['-tag', tag]) | 514 args.extend(['-tag', tag]) |
| 318 args.extend(['-ref', 'latest']) | 515 args.extend(['-ref', 'latest']) |
| 319 if service_account: | 516 if service_account: |
| 320 args.extend(['-service-account-json', service_account]) | 517 args.extend(['-service-account-json', service_account]) |
| 321 args.append(pkg_file) | 518 args.append(pkg_file) |
| 322 exit_code, json_output = run_cipd(go_workspace, 'pkg-register', args) | 519 exit_code, json_output = run_cipd(cipd_exe, 'pkg-register', args) |
| 323 if exit_code: | 520 if exit_code: |
| 324 print | 521 print |
| 325 print >> sys.stderr, 'FAILED! ' * 10 | 522 print >> sys.stderr, 'FAILED! ' * 10 |
| 326 raise UploadException('Failed to upload the CIPD package, see logs') | 523 raise UploadException('Failed to upload the CIPD package, see logs') |
| 327 info = json_output['result'] | 524 info = json_output['result'] |
| 328 print '%s %s' % (info['package'], info['instance_id']) | 525 print '%s %s' % (info['package'], info['instance_id']) |
| 329 return info | 526 return info |
| 330 | 527 |
| 331 | 528 |
| 529 def build_cipd_client(go_workspace, out_dir): | |
| 530 """Builds cipd client binary for the host platform. | |
| 531 | |
| 532 Ignores GOOS and GOARCH env vars. Puts the client binary into | |
| 533 '<out_dir>/.cipd_client/cipd_<digest>'. | |
| 534 | |
| 535 This binary is used by build.py itself and later by test_packages.py. | |
| 536 | |
| 537 Args: | |
| 538 go_workspace: path to Go workspace root (contains 'env.py', 'src', etc). | |
| 539 out_dir: build output directory, will be used to store the binary. | |
| 540 | |
| 541 Returns: | |
| 542 Path to the built binary. | |
| 543 """ | |
| 544 # 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
| |
| 545 # a combination of DEPS+deps.lock+bootstrap.py as a cache key (they define | |
| 546 # exact set of sources used to build the cipd binary). | |
| 547 # | |
| 548 # We can't just use the client in infra.git/cipd/* because it is built by this | |
| 549 # script itself: it introduced bootstrap dependency cycle in case we need to | |
| 550 # add a new platform or if we wipe cipd backend storage. | |
| 551 seed_paths = [ | |
| 552 os.path.join(ROOT, 'DEPS'), | |
| 553 os.path.join(ROOT, 'go', 'deps.lock'), | |
| 554 os.path.join(ROOT, 'go', 'bootstrap.py'), | |
|
Vadim Sh.
2016/06/25 03:51:03
indirectly defines stuff like Go toolset version
| |
| 555 ] | |
| 556 digest = hashlib.sha1() | |
| 557 for p in seed_paths: | |
| 558 with open(p, 'rb') as f: | |
| 559 digest.update(f.read()) | |
| 560 cache_key = digest.hexdigest()[:20] | |
| 561 | |
| 562 # Already have it? | |
| 563 cipd_out_dir = os.path.join(out_dir, '.cipd_client') | |
| 564 cipd_exe = os.path.join(cipd_out_dir, 'cipd_%s%s' % (cache_key, EXE_SUFFIX)) | |
| 565 if os.path.exists(cipd_exe): | |
| 566 return cipd_exe | |
| 567 | |
| 568 # Nuke all previous copies, make sure out_dir exists. | |
| 569 if os.path.exists(cipd_out_dir): | |
| 570 for p in glob.glob(os.path.join(cipd_out_dir, 'cipd_*')): | |
| 571 os.remove(p) | |
| 572 else: | |
| 573 os.makedirs(cipd_out_dir) | |
| 574 | |
| 575 # Build cipd client binary for the host platform. | |
| 576 run_go_build( | |
| 577 go_workspace, | |
| 578 package='github.com/luci/luci-go/client/cmd/cipd', | |
| 579 output=cipd_exe, | |
| 580 goos='', | |
| 581 goarch='') | |
| 582 | |
| 583 return cipd_exe | |
| 584 | |
| 585 | |
| 586 def get_build_out_file(package_out_dir, pkg_def): | |
| 587 """Returns a path where to put built *.cipd package file. | |
| 588 | |
| 589 Args: | |
| 590 package_out_dir: root directory where to put *.cipd files. | |
| 591 pkg_def: instance of PackageDef being built. | |
| 592 """ | |
| 593 # When cross-compiling, append a suffix to package file name to indicate that | |
| 594 # it's for foreign platform. | |
| 595 sfx = '' | |
| 596 if is_cross_compiling(): | |
| 597 sfx = '+' + get_target_package_vars()['platform'] | |
| 598 return os.path.join(package_out_dir, pkg_def.name + sfx + '.cipd') | |
| 599 | |
| 600 | |
| 332 def run( | 601 def run( |
| 333 py_venv, | 602 py_venv, |
| 334 go_workspace, | 603 go_workspace, |
| 335 build_callback, | 604 build_callback, |
| 336 builder, | 605 builder, |
| 337 package_def_dir, | 606 package_def_dir, |
| 338 package_out_dir, | 607 package_out_dir, |
| 339 package_def_files, | 608 package_def_files, |
| 340 build, | 609 build, |
| 341 upload, | 610 upload, |
| 342 service_url, | 611 service_url, |
| 343 tags, | 612 tags, |
| 344 service_account_json, | 613 service_account_json, |
| 345 json_output): | 614 json_output): |
| 346 """Rebuild python and Go universes and CIPD packages. | 615 """Rebuilds python and Go universes and CIPD packages. |
| 347 | 616 |
| 348 Args: | 617 Args: |
| 349 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. | 618 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. |
| 350 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 619 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 351 build_callback: called to build binaries, virtual environment, etc. | 620 build_callback: called to build binaries, virtual environment, etc. |
| 352 builder: name of CI buildbot builder that invoked the script. | 621 builder: name of CI buildbot builder that invoked the script. |
| 353 package_def_dir: path to build/packages dir to search for *.yaml. | 622 package_def_dir: path to build/packages dir to search for *.yaml. |
| 354 package_out_dir: where to put built packages. | 623 package_out_dir: where to put built packages. |
| 355 package_def_files: names of *.yaml files in package_def_dir or [] for all. | 624 package_def_files: names of *.yaml files in package_def_dir or [] for all. |
| 356 build: False to skip building packages (valid only when upload==True). | 625 build: False to skip building packages (valid only when upload==True). |
| 357 upload: True to also upload built packages, False just to build them. | 626 upload: True to also upload built packages, False just to build them. |
| 358 service_url: URL of a package repository service. | 627 service_url: URL of a package repository service. |
| 359 tags: a list of tags to attach to uploaded package instances. | 628 tags: a list of tags to attach to uploaded package instances. |
| 360 service_account_json: path to *.json service account credential. | 629 service_account_json: path to *.json service account credential. |
| 361 json_output: path to *.json file to write info about built packages to. | 630 json_output: path to *.json file to write info about built packages to. |
| 362 | 631 |
| 363 Returns: | 632 Returns: |
| 364 0 on success, 1 or error. | 633 0 on success, 1 or error. |
| 365 """ | 634 """ |
| 366 assert build or upload, 'Both build and upload are False, nothing to do' | 635 assert build or upload, 'Both build and upload are False, nothing to do' |
| 367 | 636 |
| 368 # Remove stale output so that test_packages.py do not test old files when | 637 # We need both GOOS and GOARCH or none. |
| 369 # invoked without arguments. | 638 if is_cross_compiling(): |
| 370 if build: | 639 if not os.environ.get('GOOS') or not os.environ.get('GOARCH'): |
| 371 for path in glob.glob(os.path.join(package_out_dir, '*.cipd')): | 640 print >> sys.stderr, ( |
| 372 os.remove(path) | 641 'When cross-compiling both GOOS and GOARCH environment variables ' |
| 642 'must be set.') | |
| 643 return 1 | |
| 373 | 644 |
| 374 packages_to_build = [ | 645 all_packages = enumerate_packages(py_venv, package_def_dir, package_def_files) |
| 375 p for p in enumerate_packages_to_build(package_def_dir, package_def_files) | 646 packages_to_build = [p for p in all_packages if p.should_build(builder)] |
| 376 if should_process_on_builder(p, py_venv, builder) | |
| 377 ] | |
| 378 | 647 |
| 379 print_title('Overview') | 648 print_title('Overview') |
| 380 print 'Service URL: %s' % service_url | 649 print 'Service URL: %s' % service_url |
| 381 print | 650 print |
| 382 if builder: | 651 if builder: |
| 383 print 'Package definition files to process on %s:' % builder | 652 print 'Package definition files to process on %s:' % builder |
| 384 else: | 653 else: |
| 385 print 'Package definition files to process:' | 654 print 'Package definition files to process:' |
| 386 for pkg_def_file in packages_to_build: | 655 for pkg_def in packages_to_build: |
| 387 print ' %s' % os.path.basename(pkg_def_file) | 656 print ' %s' % pkg_def.name |
| 388 if not packages_to_build: | 657 if not packages_to_build: |
| 389 print ' <none>' | 658 print ' <none>' |
| 390 print | 659 print |
| 391 print 'Variables to pass to CIPD:' | 660 print 'Variables to pass to CIPD:' |
| 392 package_vars = get_package_vars() | 661 package_vars = get_package_vars() |
| 393 for k, v in sorted(package_vars.items()): | 662 for k, v in sorted(package_vars.items()): |
| 394 print ' %s = %s' % (k, v) | 663 print ' %s = %s' % (k, v) |
| 395 if tags: | 664 if tags: |
| 396 print | 665 print |
| 397 print 'Tags to attach to uploaded packages:' | 666 print 'Tags to attach to uploaded packages:' |
| 398 for tag in sorted(tags): | 667 for tag in sorted(tags): |
| 399 print ' %s' % tag | 668 print ' %s' % tag |
| 400 if not packages_to_build: | 669 if not packages_to_build: |
| 401 print | 670 print |
| 402 print 'Nothing to do.' | 671 print 'Nothing to do.' |
| 403 return 0 | 672 return 0 |
| 404 | 673 |
| 674 # 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
| |
| 675 # for some reason. | |
| 676 if build: | |
| 677 print_title('Cleaning %s' % package_out_dir) | |
| 678 if not os.path.exists(package_out_dir): | |
| 679 os.makedirs(package_out_dir) | |
| 680 cleaned = False | |
| 681 for pkg_def in packages_to_build: | |
| 682 out_file = get_build_out_file(package_out_dir, pkg_def) | |
| 683 if os.path.exists(out_file): | |
| 684 print 'Removing stale %s' % os.path.basename(out_file) | |
| 685 os.remove(out_file) | |
| 686 cleaned = True | |
| 687 if not cleaned: | |
| 688 print 'Nothing to clean' | |
| 689 | |
| 690 # Build the cipd client needed later to build or upload packages. | |
| 691 cipd_exe = build_cipd_client(go_workspace, package_out_dir) | |
| 692 | |
| 405 # Build the world. | 693 # Build the world. |
| 406 if build: | 694 if build: |
| 407 build_callback() | 695 build_callback(packages_to_build) |
| 408 | 696 |
| 409 # Package it. | 697 # Package it. |
| 410 failed = [] | 698 failed = [] |
| 411 succeeded = [] | 699 succeeded = [] |
| 412 for pkg_def_file in packages_to_build: | 700 for pkg_def in packages_to_build: |
| 413 # path/name.yaml -> out/name.cipd. | 701 out_file = get_build_out_file(package_out_dir, pkg_def) |
| 414 name = os.path.splitext(os.path.basename(pkg_def_file))[0] | |
| 415 out_file = os.path.join(package_out_dir, name + '.cipd') | |
| 416 try: | 702 try: |
| 417 info = None | 703 info = None |
| 418 if build: | 704 if build: |
| 419 info = build_pkg(go_workspace, pkg_def_file, out_file, package_vars) | 705 info = build_pkg(cipd_exe, pkg_def, out_file, package_vars) |
| 420 if upload: | 706 if upload: |
| 421 info = upload_pkg( | 707 info = upload_pkg( |
| 422 go_workspace, | 708 cipd_exe, |
| 423 out_file, | 709 out_file, |
| 424 service_url, | 710 service_url, |
| 425 tags, | 711 tags, |
| 426 service_account_json) | 712 service_account_json) |
| 427 assert info is not None | 713 assert info is not None |
| 428 succeeded.append({'pkg_def_name': name, 'info': info}) | 714 succeeded.append({'pkg_def_name': pkg_def.name, 'info': info}) |
| 429 except (BuildException, UploadException) as e: | 715 except (BuildException, UploadException) as e: |
| 430 failed.append({'pkg_def_name': name, 'error': str(e)}) | 716 failed.append({'pkg_def_name': pkg_def.name, 'error': str(e)}) |
| 431 | 717 |
| 432 print_title('Summary') | 718 print_title('Summary') |
| 433 for d in failed: | 719 for d in failed: |
| 434 print 'FAILED %s, see log above' % d['pkg_def_name'] | 720 print 'FAILED %s, see log above' % d['pkg_def_name'] |
| 435 for d in succeeded: | 721 for d in succeeded: |
| 436 print '%s %s' % (d['info']['package'], d['info']['instance_id']) | 722 print '%s %s' % (d['info']['package'], d['info']['instance_id']) |
| 437 | 723 |
| 438 if json_output: | 724 if json_output: |
| 439 with open(json_output, 'w') as f: | 725 with open(json_output, 'w') as f: |
| 440 summary = { | 726 summary = { |
| 441 'failed': failed, | 727 'failed': failed, |
| 442 'succeeded': succeeded, | 728 'succeeded': succeeded, |
| 443 'tags': sorted(tags), | 729 'tags': sorted(tags), |
| 444 'vars': package_vars, | 730 'vars': package_vars, |
| 445 } | 731 } |
| 446 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) | 732 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) |
| 447 | 733 |
| 448 return 1 if failed else 0 | 734 return 1 if failed else 0 |
| 449 | 735 |
| 450 | 736 |
| 451 def build_infra(): | 737 def build_infra(pkg_defs): |
| 452 """Builds infra.git multiverse.""" | 738 """Builds infra.git multiverse. |
| 453 # Python side. | 739 |
| 454 print_title('Making sure python virtual environment is fresh') | 740 Args: |
| 455 run_python( | 741 pkg_defs: list of PackageDef instances for packages being built. |
| 456 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 742 """ |
| 457 args=[ | 743 # Skip building python if not used or if cross-compiling. |
| 458 '--deps_file', | 744 if any(p.uses_python_env for p in pkg_defs) and not is_cross_compiling(): |
| 459 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 745 print_title('Making sure python virtual environment is fresh') |
| 460 os.path.join(ROOT, 'ENV'), | 746 run_python( |
| 461 ]) | 747 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), |
| 462 # Go side. | 748 args=[ |
| 463 build_go(os.path.join(ROOT, 'go'), [ | 749 '--deps_file', |
| 464 'infra/...', | 750 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), |
| 465 'github.com/luci/luci-go/client/...', | 751 os.path.join(ROOT, 'ENV'), |
| 466 'github.com/luci/luci-go/tools/...', | 752 ]) |
| 467 ]) | 753 # Build all necessary go binaries. |
| 754 build_go_code(os.path.join(ROOT, 'go'), pkg_defs) | |
| 468 | 755 |
| 469 | 756 |
| 470 def main( | 757 def main( |
| 471 args, | 758 args, |
| 472 build_callback=build_infra, | 759 build_callback=build_infra, |
| 473 py_venv=os.path.join(ROOT, 'ENV'), | 760 py_venv=os.path.join(ROOT, 'ENV'), |
| 474 go_workspace=os.path.join(ROOT, 'go'), | 761 go_workspace=os.path.join(ROOT, 'go'), |
| 475 package_def_dir=os.path.join(ROOT, 'build', 'packages'), | 762 package_def_dir=os.path.join(ROOT, 'build', 'packages'), |
| 476 package_out_dir=os.path.join(ROOT, 'build', 'out')): | 763 package_out_dir=os.path.join(ROOT, 'build', 'out')): |
| 477 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') | 764 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 514 args.build, | 801 args.build, |
| 515 args.upload, | 802 args.upload, |
| 516 args.service_url, | 803 args.service_url, |
| 517 args.tags or [], | 804 args.tags or [], |
| 518 args.service_account_json, | 805 args.service_account_json, |
| 519 args.json_output) | 806 args.json_output) |
| 520 | 807 |
| 521 | 808 |
| 522 if __name__ == '__main__': | 809 if __name__ == '__main__': |
| 523 sys.exit(main(sys.argv[1:])) | 810 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |