 Chromium Code Reviews
 Chromium Code Reviews Issue 2095173002:
  Teach build.py to cross-compile go-based packages.  (Closed) 
  Base URL: https://chromium.googlesource.com/infra/infra.git@master
    
  
    Issue 2095173002:
  Teach build.py to cross-compile go-based packages.  (Closed) 
  Base URL: https://chromium.googlesource.com/infra/infra.git@master| 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 | 
| 22 import socket | |
| 19 import subprocess | 23 import subprocess | 
| 20 import sys | 24 import sys | 
| 21 import tempfile | 25 import tempfile | 
| 22 | 26 | 
| 23 | 27 | 
| 24 # Root of infra.git repository. | 28 # Root of infra.git repository. | 
| 25 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 29 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 
| 26 | 30 | 
| 27 # Root of infra gclient solution. | 31 # Root of infra gclient solution. | 
| 28 GCLIENT_ROOT = os.path.dirname(ROOT) | 32 GCLIENT_ROOT = os.path.dirname(ROOT) | 
| 29 | 33 | 
| 30 # Where to upload packages to by default. | 34 # Where to upload packages to by default. | 
| 31 PACKAGE_REPO_SERVICE = 'https://chrome-infra-packages.appspot.com' | 35 PACKAGE_REPO_SERVICE = 'https://chrome-infra-packages.appspot.com' | 
| 32 | 36 | 
| 33 # .exe on Windows. | 37 # .exe on Windows. | 
| 34 EXE_SUFFIX = '.exe' if sys.platform == 'win32' else '' | 38 EXE_SUFFIX = '.exe' if sys.platform == 'win32' else '' | 
| 35 | 39 | 
| 36 | 40 | 
| 37 class BuildException(Exception): | 41 class BuildException(Exception): | 
| 38 """Raised on errors during package build step.""" | 42 """Raised on errors during package build step.""" | 
| 39 | 43 | 
| 40 | 44 | 
| 41 class UploadException(Exception): | 45 class UploadException(Exception): | 
| 42 """Raised on errors during package upload step.""" | 46 """Raised on errors during package upload step.""" | 
| 43 | 47 | 
| 44 | 48 | 
| 49 class PackageDef(object): | |
| 50 """Represents parsed package *.yaml file.""" | |
| 51 | |
| 52 def __init__(self, path, pkg_def): | |
| 53 self.path = path | |
| 54 self.pkg_def = pkg_def | |
| 55 | |
| 56 @property | |
| 57 def name(self): | |
| 58 """Returns name of YAML file (without the directory path and extension).""" | |
| 59 return os.path.splitext(os.path.basename(self.path))[0] | |
| 60 | |
| 61 @property | |
| 62 def uses_python_env(self): | |
| 63 """Returns True if 'uses_python_env' in the YAML file is set.""" | |
| 64 return self.pkg_def.get('uses_python_env') | |
| 
nodir
2016/06/26 05:00:47
consider `return bool(...)` here like in other met
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 65 | |
| 66 @property | |
| 67 def go_packages(self): | |
| 68 """Returns a list of Go packages that must be installed for this package.""" | |
| 69 return self.pkg_def.get('go_packages') or [] | |
| 70 | |
| 71 def should_build(self, builder): | |
| 72 """Returns True if package should be built in the current environment. | |
| 73 | |
| 74 Takes into account 'builders' and 'supports_cross_compilation' properties of | |
| 75 package definition file. | |
| 
nodir
2016/06/26 05:00:47
the
 
Vadim Sh.
2016/06/27 21:37:29
Done.
 | |
| 76 """ | |
| 77 # If '--builder' is not specified, ignore 'builders' property. Otherwise, if | |
| 78 # 'builders' YAML attribute it not empty, verify --builder is listed there. | |
| 79 builders = self.pkg_def.get('builders') | |
| 80 if builder and builders and builder not in builders: | |
| 81 return False | |
| 82 | |
| 83 # If cross compiling, pick only packages that supports cross compilation. | |
| 
nodir
2016/06/26 05:00:47
support
 
Vadim Sh.
2016/06/27 21:37:29
Done.
 | |
| 84 if is_cross_compiling(): | |
| 85 return bool(self.pkg_def.get('supports_cross_compilation')) | |
| 86 | |
| 87 return True | |
| 88 | |
| 89 | |
| 90 def is_cross_compiling(): | |
| 91 """Returns true if using GOOS or GOARCH env vars. | |
| 
nodir
2016/06/26 05:00:47
True
 
Vadim Sh.
2016/06/27 21:37:29
Done.
 | |
| 92 | |
| 93 We also check at the start of the script that if one of them is used, then | |
| 94 the other is specified as well. | |
| 95 """ | |
| 96 return bool(os.environ.get('GOOS')) or bool(os.environ.get('GOARCH')) | |
| 97 | |
| 98 | |
| 45 def run_python(script, args): | 99 def run_python(script, args): | 
| 46 """Invokes a python script. | 100 """Invokes a python script. | 
| 47 | 101 | 
| 48 Raises: | 102 Raises: | 
| 49 subprocess.CalledProcessError on non zero exit code. | 103 subprocess.CalledProcessError on non zero exit code. | 
| 50 """ | 104 """ | 
| 51 print 'Running %s %s' % (script, ' '.join(args)) | 105 print 'Running %s %s' % (script, ' '.join(args)) | 
| 52 subprocess.check_call( | 106 subprocess.check_call( | 
| 53 args=['python', '-u', script] + list(args), executable=sys.executable) | 107 args=['python', '-u', script] + list(args), executable=sys.executable) | 
| 54 | 108 | 
| 55 | 109 | 
| 56 def run_cipd(go_workspace, cmd, args): | 110 def run_cipd(cipd_exe, cmd, args): | 
| 57 """Invokes CIPD, parsing -json-output result. | 111 """Invokes CIPD, parsing -json-output result. | 
| 58 | 112 | 
| 59 Args: | 113 Args: | 
| 60 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 114 cipd_exe: path to cipd client binary to run. | 
| 61 cmd: cipd subcommand to run. | 115 cmd: cipd subcommand to run. | 
| 62 args: list of command line arguments to pass to the subcommand. | 116 args: list of command line arguments to pass to the subcommand. | 
| 63 | 117 | 
| 64 Returns: | 118 Returns: | 
| 65 (Process exit code, parsed JSON output or None). | 119 (Process exit code, parsed JSON output or None). | 
| 66 """ | 120 """ | 
| 67 temp_file = None | 121 temp_file = None | 
| 68 try: | 122 try: | 
| 69 fd, temp_file = tempfile.mkstemp(suffix='.json', prefix='cipd_%s' % cmd) | 123 fd, temp_file = tempfile.mkstemp(suffix='.json', prefix='cipd_%s' % cmd) | 
| 70 os.close(fd) | 124 os.close(fd) | 
| 71 | 125 | 
| 72 cmd_line = [ | 126 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 | 127 | 
| 77 print 'Running %s' % ' '.join(cmd_line) | 128 print 'Running %s' % ' '.join(cmd_line) | 
| 78 exit_code = subprocess.call(args=cmd_line, executable=cmd_line[0]) | 129 exit_code = subprocess.call(args=cmd_line, executable=cmd_line[0]) | 
| 79 try: | 130 try: | 
| 80 with open(temp_file, 'r') as f: | 131 with open(temp_file, 'r') as f: | 
| 81 json_output = json.load(f) | 132 json_output = json.load(f) | 
| 82 except (IOError, ValueError): | 133 except (IOError, ValueError): | 
| 83 json_output = None | 134 json_output = None | 
| 84 | 135 | 
| 85 return exit_code, json_output | 136 return exit_code, json_output | 
| 86 finally: | 137 finally: | 
| 87 try: | 138 try: | 
| 88 if temp_file: | 139 if temp_file: | 
| 89 os.remove(temp_file) | 140 os.remove(temp_file) | 
| 90 except OSError: | 141 except OSError: | 
| 91 pass | 142 pass | 
| 92 | 143 | 
| 93 | 144 | 
| 94 def print_title(title): | 145 def print_title(title): | 
| 95 """Pretty prints a banner to stdout.""" | 146 """Pretty prints a banner to stdout.""" | 
| 96 sys.stdout.flush() | 147 sys.stdout.flush() | 
| 97 sys.stderr.flush() | 148 sys.stderr.flush() | 
| 98 print | 149 print | 
| 99 print '-' * 80 | 150 print '-' * 80 | 
| 100 print title | 151 print title | 
| 101 print '-' * 80 | 152 print '-' * 80 | 
| 102 | 153 | 
| 103 | 154 | 
| 104 def build_go(go_workspace, packages): | 155 def print_go_step_title(title): | 
| 105 """Bootstraps go environment and rebuilds (and installs) Go packages. | 156 """Same as 'print_title', but also appends values of GOOS and GOARCH.""" | 
| 157 if is_cross_compiling(): | |
| 158 title += '\n' + '-' * 80 | |
| 159 title += '\n GOOS=%s' % os.environ['GOOS'] | |
| 160 title += '\n GOARCH=%s' % os.environ['GOARCH'] | |
| 161 if 'GOARM' in os.environ: | |
| 162 title += '\n GOARM=%s' % os.environ['GOARM'] | |
| 163 print_title(title) | |
| 106 | 164 | 
| 107 Compiles and installs packages into default GOBIN, which is <path>/go/bin/ | 165 | 
| 108 (it is setup by go/env.py and depends on what workspace is used). | 166 @contextlib.contextmanager | 
| 167 def hacked_workspace(go_workspace, goos=None, goarch=None): | |
| 168 """Symlinks Go workspace into new root, modifies os.environ. | |
| 169 | |
| 170 Go toolset embeds absolute paths to *.go files into the executable. Use | |
| 171 symlink with stable path to make executables independent of checkout path. | |
| 109 | 172 | 
| 110 Args: | 173 Args: | 
| 111 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 174 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| 112 packages: list of packages to build (can include '...' patterns). | 175 goos: if set, overrides GOOS environment variable (removes it if ''). | 
| 176 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 177 | |
| 178 Yields: | |
| 179 Path where go_workspace is symlinked to. | |
| 113 """ | 180 """ | 
| 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 | 181 new_root = None | 
| 119 new_workspace = go_workspace | 182 new_workspace = go_workspace | 
| 120 if sys.platform != 'win32': | 183 if sys.platform != 'win32': | 
| 121 new_root = '/tmp/_chrome_infra_build' | 184 new_root = '/tmp/_chrome_infra_build' | 
| 122 if os.path.exists(new_root): | 185 if os.path.exists(new_root): | 
| 123 assert os.path.islink(new_root) | 186 assert os.path.islink(new_root) | 
| 124 os.remove(new_root) | 187 os.remove(new_root) | 
| 125 os.symlink(GCLIENT_ROOT, new_root) | 188 os.symlink(GCLIENT_ROOT, new_root) | 
| 126 rel = os.path.relpath(go_workspace, GCLIENT_ROOT) | 189 rel = os.path.relpath(go_workspace, GCLIENT_ROOT) | 
| 127 assert not rel.startswith('..'), rel | 190 assert not rel.startswith('..'), rel | 
| 128 new_workspace = os.path.join(new_root, rel) | 191 new_workspace = os.path.join(new_root, rel) | 
| 129 | 192 | 
| 130 # Remove any stale binaries and libraries. | 193 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 | 194 | 
| 134 # Recompile ('-a'). | 195 if goos is not None: | 
| 196 if goos == '': | |
| 197 os.environ.pop('GOOS', None) | |
| 198 else: | |
| 199 os.environ['GOOS'] = goos | |
| 200 if goarch is not None: | |
| 201 if goarch == '': | |
| 202 os.environ.pop('GOARCH', None) | |
| 203 else: | |
| 204 os.environ['GOARCH'] = goarch | |
| 205 | |
| 206 # Make sure we build ARMv6 code even if the host is ARMv7. See the comment in | |
| 207 # get_host_package_vars for reasons why. Also explicitly set GOARM to 6 when | |
| 208 # cross-compiling (it should be '6' in this case by default anyway). | |
| 209 plat = platform.machine().lower() | |
| 210 if plat.startswith('arm') or os.environ.get('GOARCH') == 'arm': | |
| 211 os.environ['GOARM'] = '6' | |
| 212 else: | |
| 213 os.environ.pop('GOARM', None) | |
| 214 | |
| 135 try: | 215 try: | 
| 216 yield new_workspace | |
| 217 finally: | |
| 218 # Apparently 'os.environ = orig_environ' doesn't actually modify process | |
| 219 # environment, only modifications of os.environ object itself do. | |
| 220 for k, v in orig_environ.iteritems(): | |
| 221 os.environ[k] = v | |
| 222 for k in [key for key in os.environ if key not in orig_environ]: | |
| 
nodir
2016/06/26 05:00:47
fwiu this is complicated because you don't want to
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 223 os.environ.pop(k) | |
| 224 if new_root: | |
| 225 os.remove(new_root) | |
| 226 | |
| 227 | |
| 228 def bootstrap_go_toolset(go_workspace): | |
| 229 """Makes sure go toolset is installed and returns its 'go env' environment. | |
| 230 | |
| 231 Used to verify that our platform detection in get_host_package_vars() matches | |
| 232 the Go toolset being used. | |
| 233 """ | |
| 234 with hacked_workspace(go_workspace) as new_workspace: | |
| 235 print_go_step_title('Making sure Go toolset is installed') | |
| 236 # env.py does the actual job of bootstrapping if the toolset is missing. | |
| 237 output = subprocess.check_output( | |
| 238 args=[ | |
| 239 'python', '-u', os.path.join(new_workspace, 'env.py'), | |
| 240 'go', 'env', | |
| 241 ], | |
| 242 executable=sys.executable) | |
| 243 print 'Go environ:' | |
| 244 print output.strip() | |
| 245 env = {} | |
| 246 for line in output.splitlines(): | |
| 247 k, _, v = line.partition('=') | |
| 248 env[k] = v.strip('"') | |
| 
nodir
2016/06/26 05:00:47
strictly speaking v may end with \"
Maybe do that
 
Vadim Sh.
2016/06/27 21:37:28
Done. It can also be different on Windows (and pla
 | |
| 249 return env | |
| 250 | |
| 251 | |
| 252 def run_go_install(go_workspace, packages, goos=None, goarch=None): | |
| 253 """Rebuilds (and installs) Go packages into GOBIN via 'go install ...'. | |
| 254 | |
| 255 Compiles and installs packages into default GOBIN, which is <go_workspace>/bin | |
| 256 (it is setup by go/env.py). | |
| 257 | |
| 258 Args: | |
| 259 go_workspace: path to 'infra/go' or 'infra_internal/go'. | |
| 260 packages: list of packages to build (can include '...' patterns). | |
| 261 goos: if set, overrides GOOS environment variable (removes it if ''). | |
| 262 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 263 """ | |
| 264 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | |
| 265 print_go_step_title('Building:\n %s' % '\n '.join(packages)) | |
| 136 subprocess.check_call( | 266 subprocess.check_call( | 
| 137 args=[ | 267 args=[ | 
| 138 'python', '-u', os.path.join(new_workspace, 'env.py'), | 268 'python', '-u', os.path.join(new_workspace, 'env.py'), | 
| 139 'go', 'install', '-a', '-v', | 269 'go', 'install', '-a', '-v', | 
| 140 ] + list(packages), | 270 ] + list(packages), | 
| 
nodir
2016/06/26 05:00:47
this may hit cmd length limit on win
 
Vadim Sh.
2016/06/27 21:37:28
Acknowledged. Not a problem in the near future tho
 | |
| 141 executable=sys.executable, | 271 executable=sys.executable, | 
| 142 stderr=subprocess.STDOUT) | 272 stderr=subprocess.STDOUT) | 
| 143 finally: | |
| 144 if new_root: | |
| 145 os.remove(new_root) | |
| 146 | 273 | 
| 147 | 274 | 
| 148 def enumerate_packages_to_build(package_def_dir, package_def_files=None): | 275 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. | 276 """Builds single Go package (rebuilding all its dependencies). | 
| 150 | 277 | 
| 151 Args: | 278 Args: | 
| 279 package: package to build. | |
| 
nodir
2016/06/26 05:00:48
document go_workspace
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 280 output: where to put the resulting binary. | |
| 281 goos: if set, overrides GOOS environment variable (removes it if ''). | |
| 282 goarch: if set, overrides GOARCH environment variable (removes it if ''). | |
| 283 """ | |
| 284 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: | |
| 285 print_go_step_title('Building %s' % package) | |
| 286 subprocess.check_call( | |
| 287 args=[ | |
| 288 'python', '-u', os.path.join(new_workspace, 'env.py'), | |
| 289 'go', 'build', '-a', '-v', '-o', output, package, | |
| 290 ], | |
| 291 executable=sys.executable, | |
| 292 stderr=subprocess.STDOUT) | |
| 293 | |
| 294 | |
| 295 def build_go_code(go_workspace, pkg_defs): | |
| 296 """Builds and installs all Go packages used by the given PackageDefs. | |
| 297 | |
| 298 Understands GOOS and GOARCH and uses slightly different build strategy when | |
| 299 cross-compiling. In the end <go_workspace>/bin will have all built binaries, | |
| 300 and only them (regardless of whether we are cross compiling or not). | |
| 301 | |
| 302 Args: | |
| 303 go_workspace: path to 'infra/go' or 'infra_internal/go'. | |
| 304 pkg_defs: list of PackageDef objects that define what to build. | |
| 305 """ | |
| 306 # Make sure there are no stale files in the workspace. | |
| 307 shutil.rmtree(os.path.join(go_workspace, 'bin'), ignore_errors=True) | |
| 
nodir
2016/06/26 05:00:47
is this necessary? fwiu if I want infra/go/bin/foo
 
Vadim Sh.
2016/06/27 21:37:29
Using "go clean" now and not passing '-a' ("rebuil
 | |
| 308 os.mkdir(os.path.join(go_workspace, 'bin')) | |
| 309 shutil.rmtree(os.path.join(go_workspace, 'pkg'), ignore_errors=True) | |
| 310 os.mkdir(os.path.join(go_workspace, 'pkg')) | |
| 311 | |
| 312 # Grab a set of all go packages we need to build and install into GOBIN. | |
| 313 to_install = [] | |
| 314 for p in pkg_defs: | |
| 315 to_install.extend(p.go_packages) | |
| 316 to_install = sorted(set(to_install)) | |
| 317 if not to_install: | |
| 318 return | |
| 319 | |
| 320 if not is_cross_compiling(): | |
| 321 # If not cross compiling, build all Go code in a single "go install" step, | |
| 322 # it's faster that way. We can't do that when cross-compiling, since | |
| 323 # 'go install' isn't supposed to be used for cross-compilation and the | |
| 324 # toolset actively complains with "go install: cannot install cross-compiled | |
| 325 # binaries when GOBIN is set". | |
| 326 run_go_install(go_workspace, to_install) | |
| 327 else: | |
| 328 # Build packages one by one and put the resulting binaries into GOBIN, as if | |
| 329 # they were installed there. It's where the rest of the build.py code | |
| 330 # expects them to be (see also 'root' property in package definition YAMLs). | |
| 331 # It's much slower compared to a single 'go install' since will rebuild all | |
| 332 # dependent libraries at each 'go build' invocation. | |
| 333 # | |
| 334 # TODO(vadimsh): Use 'go install std' to prebuild std lib for the target | |
| 335 # platform. Unfortunately '-a' flag we pass in run_go_build causes the | |
| 336 # compiler to rebuild stdlib anyway, even if it is already installed. We use | |
| 337 # '-a' as a precaution against stale *.a in .vendor/ and other GOPATHs, | |
| 338 # since Go for some reason fails to rebuild them sometimes. | |
| 339 go_bin = os.path.join(go_workspace, 'bin') | |
| 340 exe_suffix = get_target_package_vars()['exe_suffix'] | |
| 341 for pkg in to_install: | |
| 342 name = pkg[pkg.rfind('/')+1:] | |
| 343 run_go_build(go_workspace, pkg, os.path.join(go_bin, name + exe_suffix)) | |
| 344 | |
| 345 | |
| 346 def enumerate_packages(py_venv, package_def_dir, package_def_files): | |
| 347 """Returns a list PackageDef instances for files in build/packages/*.yaml. | |
| 348 | |
| 349 Args: | |
| 350 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. | 351 package_def_dir: path to build/packages dir to search for *.yaml. | 
| 153 package_def_files: optional list of filenames to limit results to. | 352 package_def_files: optional list of filenames to limit results to. | 
| 154 | 353 | 
| 155 Returns: | 354 Returns: | 
| 156 List of absolute paths to *.yaml files under packages_dir. | 355 List of PackageDef instances parsed from *.yaml files under packages_dir. | 
| 157 """ | 356 """ | 
| 158 # All existing package by default. | 357 paths = [] | 
| 159 if not package_def_files: | 358 if not package_def_files: | 
| 160 return sorted(glob.glob(os.path.join(package_def_dir, '*.yaml'))) | 359 # All existing package by default. | 
| 161 paths = [] | 360 paths = glob.glob(os.path.join(package_def_dir, '*.yaml')) | 
| 162 for name in package_def_files: | 361 else: | 
| 163 abs_path = os.path.join(package_def_dir, name) | 362 # Otherwise pick only the ones in 'package_def_files' list. | 
| 164 if not os.path.isfile(abs_path): | 363 for name in package_def_files: | 
| 165 raise BuildException('No such package definition file: %s' % name) | 364 abs_path = os.path.join(package_def_dir, name) | 
| 
nodir
2016/06/26 05:00:47
not necessarily absolute though
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 166 paths.append(abs_path) | 365 if not os.path.isfile(abs_path): | 
| 167 return sorted(paths) | 366 raise BuildException('No such package definition file: %s' % name) | 
| 367 paths.append(abs_path) | |
| 368 return [PackageDef(p, read_yaml(py_venv, p)) for p in sorted(paths)] | |
| 168 | 369 | 
| 169 | 370 | 
| 170 def read_yaml(py_venv, path): | 371 def read_yaml(py_venv, path): | 
| 171 """Returns content of YAML file as python dict.""" | 372 """Returns content of YAML file as python dict.""" | 
| 172 # YAML lib is in venv, not activated here. Go through hoops. | 373 # YAML lib is in venv, not activated here. Go through hoops. | 
| 374 # TODO(vadimsh): Doesn't work on ARM, since we have no working infra_python | |
| 375 # venv there. Replace this hack with vendored pure-python PyYAML. | |
| 173 oneliner = ( | 376 oneliner = ( | 
| 174 'import json, sys, yaml; ' | 377 'import json, sys, yaml; ' | 
| 175 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') | 378 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') | 
| 176 if sys.platform == 'win32': | 379 if sys.platform == 'win32': | 
| 177 python_venv_path = ('Scripts', 'python.exe') | 380 python_venv_path = ('Scripts', 'python.exe') | 
| 178 else: | 381 else: | 
| 179 python_venv_path = ('bin', 'python') | 382 python_venv_path = ('bin', 'python') | 
| 180 executable = os.path.join(py_venv, *python_venv_path) | 383 executable = os.path.join(py_venv, *python_venv_path) | 
| 181 env = os.environ.copy() | 384 env = os.environ.copy() | 
| 182 env.pop('PYTHONPATH', None) | 385 env.pop('PYTHONPATH', None) | 
| 183 proc = subprocess.Popen( | 386 proc = subprocess.Popen( | 
| 184 [executable, '-c', oneliner], | 387 [executable, '-c', oneliner], | 
| 185 executable=executable, | 388 executable=executable, | 
| 186 stdin=subprocess.PIPE, | 389 stdin=subprocess.PIPE, | 
| 187 stdout=subprocess.PIPE, | 390 stdout=subprocess.PIPE, | 
| 188 env=env) | 391 env=env) | 
| 189 with open(path, 'r') as f: | 392 with open(path, 'r') as f: | 
| 190 out, _ = proc.communicate(f.read()) | 393 out, _ = proc.communicate(f.read()) | 
| 191 if proc.returncode: | 394 if proc.returncode: | 
| 192 raise BuildException('Failed to parse YAML at %s' % path) | 395 raise BuildException('Failed to parse YAML at %s' % path) | 
| 193 return json.loads(out) | 396 return json.loads(out) | 
| 194 | 397 | 
| 195 | 398 | 
| 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(): | 399 def get_package_vars(): | 
| 205 """Returns a dict with variables that describe the current environment. | 400 """Returns a dict with variables that describe the package target environment. | 
| 206 | 401 | 
| 207 Variables can be referenced in the package definition YAML as | 402 Variables can be referenced in the package definition YAML as | 
| 208 ${variable_name}. It allows to reuse exact same definition file for similar | 403 ${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 | 404 packages (e.g. packages with same cross platform binary, but for different | 
| 210 platforms). | 405 platforms). | 
| 406 | |
| 407 If running in cross-compilation mode, uses GOOS and GOARCH to figure out the | |
| 408 target platform instead of examining the host environment. | |
| 409 """ | |
| 410 if is_cross_compiling(): | |
| 411 return get_target_package_vars() | |
| 412 return get_host_package_vars() | |
| 413 | |
| 414 | |
| 415 def get_target_package_vars(): | |
| 416 """Returns a dict with variables that describe cross compilation target env. | |
| 
nodir
2016/06/26 05:00:48
cross-compilation
here and other places
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 417 | |
| 418 Examines os.environ for GOOS, GOARCH and GOARM. | |
| 419 | |
| 420 It contains only 'platform' and 'exe_suffix'. | |
| 
nodir
2016/06/26 05:00:47
s/It/Returned dict/
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 421 """ | |
| 422 assert is_cross_compiling() | |
| 423 goos = os.environ['GOOS'] | |
| 424 goarch = os.environ['GOARCH'] | |
| 425 | |
| 426 if goarch not in ('386', 'amd64', 'arm'): | |
| 427 raise BuildException('Unsupported GOARCH %s' % goarch) | |
| 428 | |
| 429 # There are many ARMs, pick the concrete instruction set. 'v6' is the default, | |
| 430 # don't try to support other variants for now. | |
| 431 # | |
| 432 # See https://golang.org/doc/install/source#environment. | |
| 433 if goarch == 'arm': | |
| 434 goarm = os.environ.get('GOARM', '6') | |
| 435 if goarm == '6': | |
| 
nodir
2016/06/26 05:00:47
invert and remove else
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 436 arch = 'armv6l' | |
| 437 else: | |
| 438 raise BuildException('Unsupported GOARM value %s' % goarm) | |
| 439 else: | |
| 440 arch = goarch | |
| 441 | |
| 442 # We use 'mac' instead of 'darwin'. | |
| 443 if goos == 'darwin': | |
| 444 goos = 'mac' | |
| 445 | |
| 446 return { | |
| 447 'exe_suffix': '.exe' if goos == 'windows' else '', | |
| 448 'platform': '%s-%s' % (goos, arch), | |
| 449 } | |
| 450 | |
| 451 | |
| 452 def get_host_package_vars(): | |
| 453 """Returns a dict with variables that describe the current host environment. | |
| 454 | |
| 455 The returned platform may not match the machine environment exactly, but it is | |
| 456 compatible with it. | |
| 457 | |
| 458 For example, on ARMv7 machines we claim that we are in fact running ARMv6 | |
| 459 (which is subset of ARMv7), since we don't really care about v7 over v6 | |
| 460 difference and want to reduce the variability in supported architectures | |
| 461 instead. | |
| 462 | |
| 463 Similarly, if running on 64-bit Linux with 32-bit user space (based on python | |
| 464 interpreter bitness), we claim that machine is 32-bit, since most 32-bit Linux | |
| 465 Chrome Infra bots are in fact running 64-bit kernels with 32-bit userlands. | |
| 211 """ | 466 """ | 
| 212 # linux, mac or windows. | 467 # linux, mac or windows. | 
| 213 platform_variant = { | 468 platform_variant = { | 
| 214 'darwin': 'mac', | 469 'darwin': 'mac', | 
| 215 'linux2': 'linux', | 470 'linux2': 'linux', | 
| 216 'win32': 'windows', | 471 'win32': 'windows', | 
| 217 }.get(sys.platform) | 472 }.get(sys.platform) | 
| 218 if not platform_variant: | 473 if not platform_variant: | 
| 219 raise ValueError('Unknown OS: %s' % sys.platform) | 474 raise ValueError('Unknown OS: %s' % sys.platform) | 
| 220 | 475 | 
| (...skipping 12 matching lines...) Expand all Loading... | |
| 233 else: | 488 else: | 
| 234 raise ValueError('Unknown OS: %s' % sys.platform) | 489 raise ValueError('Unknown OS: %s' % sys.platform) | 
| 235 | 490 | 
| 236 # amd64, 386, etc. | 491 # amd64, 386, etc. | 
| 237 platform_arch = { | 492 platform_arch = { | 
| 238 'amd64': 'amd64', | 493 'amd64': 'amd64', | 
| 239 'i386': '386', | 494 'i386': '386', | 
| 240 'i686': '386', | 495 'i686': '386', | 
| 241 'x86': '386', | 496 'x86': '386', | 
| 242 'x86_64': 'amd64', | 497 'x86_64': 'amd64', | 
| 498 'armv6l': 'armv6l', | |
| 499 'armv7l': 'armv6l', # we prefer to use older instruction set for builds | |
| 243 }.get(platform.machine().lower()) | 500 }.get(platform.machine().lower()) | 
| 244 if not platform_arch: | 501 if not platform_arch: | 
| 245 raise ValueError('Unknown machine arch: %s' % platform.machine()) | 502 raise ValueError('Unknown machine arch: %s' % platform.machine()) | 
| 246 | 503 | 
| 504 # Most 32-bit Linux Chrome Infra bots are in fact running 64-bit kernel with | |
| 505 # 32-bit userland. Detect this case (based on bitness of the python | |
| 506 # interpreter) and report the bot as '386'. | |
| 507 if (platform_variant == 'linux' and | |
| 508 platform_arch == 'amd64' and | |
| 509 sys.maxsize == (2 ** 31) - 1): | |
| 510 platform_arch = '386' | |
| 511 | |
| 247 return { | 512 return { | 
| 248 # e.g. '.exe' or ''. | 513 # e.g. '.exe' or ''. | 
| 249 'exe_suffix': EXE_SUFFIX, | 514 'exe_suffix': EXE_SUFFIX, | 
| 250 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. | 515 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. | 
| 251 'os_ver': os_ver, | 516 'os_ver': os_ver, | 
| 252 # e.g. 'linux-amd64' | 517 # e.g. 'linux-amd64' | 
| 253 'platform': '%s-%s' % (platform_variant, platform_arch), | 518 'platform': '%s-%s' % (platform_variant, platform_arch), | 
| 254 # e.g. '27' (dots are not allowed in package names). | 519 # e.g. '27' (dots are not allowed in package names). | 
| 255 'python_version': '%s%s' % sys.version_info[:2], | 520 'python_version': '%s%s' % sys.version_info[:2], | 
| 256 } | 521 } | 
| 257 | 522 | 
| 258 | 523 | 
| 259 def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 524 def build_pkg(cipd_exe, pkg_def, out_file, package_vars): | 
| 260 """Invokes CIPD client to build a package. | 525 """Invokes CIPD client to build a package. | 
| 261 | 526 | 
| 262 Args: | 527 Args: | 
| 263 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 528 cipd_exe: path to cipd client binary to use. | 
| 264 pkg_def_file: path to *.yaml file with package definition. | 529 pkg_def: instance of PackageDef representing this package. | 
| 265 out_file: where to store the built package. | 530 out_file: where to store the built package. | 
| 266 package_vars: dict with variables to pass as -pkg-var to cipd. | 531 package_vars: dict with variables to pass as -pkg-var to cipd. | 
| 267 | 532 | 
| 268 Returns: | 533 Returns: | 
| 269 {'package': <name>, 'instance_id': <sha1>} | 534 {'package': <name>, 'instance_id': <sha1>} | 
| 270 | 535 | 
| 271 Raises: | 536 Raises: | 
| 272 BuildException on error. | 537 BuildException on error. | 
| 273 """ | 538 """ | 
| 274 print_title('Building: %s' % os.path.basename(pkg_def_file)) | 539 print_title('Building: %s' % os.path.basename(out_file)) | 
| 275 | 540 | 
| 276 # Make sure not stale output remains. | 541 # Make sure not stale output remains. | 
| 277 if os.path.isfile(out_file): | 542 if os.path.isfile(out_file): | 
| 278 os.remove(out_file) | 543 os.remove(out_file) | 
| 279 | 544 | 
| 280 # Build the package. | 545 # Build the package. | 
| 281 args = ['-pkg-def', pkg_def_file] | 546 args = ['-pkg-def', pkg_def.path] | 
| 282 for k, v in sorted(package_vars.items()): | 547 for k, v in sorted(package_vars.items()): | 
| 283 args.extend(['-pkg-var', '%s:%s' % (k, v)]) | 548 args.extend(['-pkg-var', '%s:%s' % (k, v)]) | 
| 284 args.extend(['-out', out_file]) | 549 args.extend(['-out', out_file]) | 
| 285 exit_code, json_output = run_cipd(go_workspace, 'pkg-build', args) | 550 exit_code, json_output = run_cipd(cipd_exe, 'pkg-build', args) | 
| 286 if exit_code: | 551 if exit_code: | 
| 287 print | 552 print | 
| 288 print >> sys.stderr, 'FAILED! ' * 10 | 553 print >> sys.stderr, 'FAILED! ' * 10 | 
| 289 raise BuildException('Failed to build the CIPD package, see logs') | 554 raise BuildException('Failed to build the CIPD package, see logs') | 
| 290 | 555 | 
| 291 # Expected result is {'package': 'name', 'instance_id': 'sha1'} | 556 # Expected result is {'package': 'name', 'instance_id': 'sha1'} | 
| 292 info = json_output['result'] | 557 info = json_output['result'] | 
| 293 print '%s %s' % (info['package'], info['instance_id']) | 558 print '%s %s' % (info['package'], info['instance_id']) | 
| 294 return info | 559 return info | 
| 295 | 560 | 
| 296 | 561 | 
| 297 def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 562 def upload_pkg(cipd_exe, pkg_file, service_url, tags, service_account): | 
| 298 """Uploads existing *.cipd file to the storage and tags it. | 563 """Uploads existing *.cipd file to the storage and tags it. | 
| 299 | 564 | 
| 300 Args: | 565 Args: | 
| 301 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 566 cipd_exe: path to cipd client binary to use. | 
| 302 pkg_file: path to *.cipd file to upload. | 567 pkg_file: path to *.cipd file to upload. | 
| 303 service_url: URL of a package repository service. | 568 service_url: URL of a package repository service. | 
| 304 tags: a list of tags to attach to uploaded package instance. | 569 tags: a list of tags to attach to uploaded package instance. | 
| 305 service_account: path to *.json file with service account to use. | 570 service_account: path to *.json file with service account to use. | 
| 306 | 571 | 
| 307 Returns: | 572 Returns: | 
| 308 {'package': <name>, 'instance_id': <sha1>} | 573 {'package': <name>, 'instance_id': <sha1>} | 
| 309 | 574 | 
| 310 Raises: | 575 Raises: | 
| 311 UploadException on error. | 576 UploadException on error. | 
| 312 """ | 577 """ | 
| 313 print_title('Uploading: %s' % os.path.basename(pkg_file)) | 578 print_title('Uploading: %s' % os.path.basename(pkg_file)) | 
| 314 | 579 | 
| 315 args = ['-service-url', service_url] | 580 args = ['-service-url', service_url] | 
| 316 for tag in sorted(tags): | 581 for tag in sorted(tags): | 
| 317 args.extend(['-tag', tag]) | 582 args.extend(['-tag', tag]) | 
| 318 args.extend(['-ref', 'latest']) | 583 args.extend(['-ref', 'latest']) | 
| 319 if service_account: | 584 if service_account: | 
| 320 args.extend(['-service-account-json', service_account]) | 585 args.extend(['-service-account-json', service_account]) | 
| 321 args.append(pkg_file) | 586 args.append(pkg_file) | 
| 322 exit_code, json_output = run_cipd(go_workspace, 'pkg-register', args) | 587 exit_code, json_output = run_cipd(cipd_exe, 'pkg-register', args) | 
| 323 if exit_code: | 588 if exit_code: | 
| 324 print | 589 print | 
| 325 print >> sys.stderr, 'FAILED! ' * 10 | 590 print >> sys.stderr, 'FAILED! ' * 10 | 
| 326 raise UploadException('Failed to upload the CIPD package, see logs') | 591 raise UploadException('Failed to upload the CIPD package, see logs') | 
| 327 info = json_output['result'] | 592 info = json_output['result'] | 
| 328 print '%s %s' % (info['package'], info['instance_id']) | 593 print '%s %s' % (info['package'], info['instance_id']) | 
| 329 return info | 594 return info | 
| 330 | 595 | 
| 331 | 596 | 
| 597 def build_cipd_client(go_workspace, out_dir): | |
| 598 """Builds cipd client binary for the host platform. | |
| 599 | |
| 600 Ignores GOOS and GOARCH env vars. Puts the client binary into | |
| 601 '<out_dir>/.cipd_client/cipd_<digest>'. | |
| 602 | |
| 603 This binary is used by build.py itself and later by test_packages.py. | |
| 604 | |
| 605 Args: | |
| 606 go_workspace: path to Go workspace root (contains 'env.py', 'src', etc). | |
| 607 out_dir: build output directory, will be used to store the binary. | |
| 608 | |
| 609 Returns: | |
| 610 Path to the built binary. | |
| 611 """ | |
| 612 # To avoid rebuilding cipd client all the time, we cache it in out/*, using | |
| 613 # a combination of DEPS+deps.lock+bootstrap.py as a cache key (they define | |
| 614 # exact set of sources used to build the cipd binary). | |
| 615 # | |
| 616 # We can't just use the client in infra.git/cipd/* because it is built by this | |
| 617 # script itself: it introduced bootstrap dependency cycle in case we need to | |
| 618 # add a new platform or if we wipe cipd backend storage. | |
| 619 seed_paths = [ | |
| 620 os.path.join(ROOT, 'DEPS'), | |
| 621 os.path.join(ROOT, 'go', 'deps.lock'), | |
| 622 os.path.join(ROOT, 'go', 'bootstrap.py'), | |
| 623 ] | |
| 624 digest = hashlib.sha1() | |
| 625 for p in seed_paths: | |
| 626 with open(p, 'rb') as f: | |
| 627 digest.update(f.read()) | |
| 628 cache_key = digest.hexdigest()[:20] | |
| 629 | |
| 630 # Already have it? | |
| 631 cipd_out_dir = os.path.join(out_dir, '.cipd_client') | |
| 632 cipd_exe = os.path.join(cipd_out_dir, 'cipd_%s%s' % (cache_key, EXE_SUFFIX)) | |
| 633 if os.path.exists(cipd_exe): | |
| 634 return cipd_exe | |
| 635 | |
| 636 # Nuke all previous copies, make sure out_dir exists. | |
| 637 if os.path.exists(cipd_out_dir): | |
| 638 for p in glob.glob(os.path.join(cipd_out_dir, 'cipd_*')): | |
| 639 os.remove(p) | |
| 640 else: | |
| 641 os.makedirs(cipd_out_dir) | |
| 642 | |
| 643 # Build cipd client binary for the host platform. | |
| 644 run_go_build( | |
| 645 go_workspace, | |
| 646 package='github.com/luci/luci-go/client/cmd/cipd', | |
| 647 output=cipd_exe, | |
| 648 goos='', | |
| 649 goarch='') | |
| 650 | |
| 651 return cipd_exe | |
| 652 | |
| 653 | |
| 654 def get_build_out_file(package_out_dir, pkg_def): | |
| 655 """Returns a path where to put built *.cipd package file. | |
| 656 | |
| 657 Args: | |
| 658 package_out_dir: root directory where to put *.cipd files. | |
| 659 pkg_def: instance of PackageDef being built. | |
| 660 """ | |
| 661 # When cross-compiling, append a suffix to package file name to indicate that | |
| 662 # it's for foreign platform. | |
| 663 sfx = '' | |
| 664 if is_cross_compiling(): | |
| 665 sfx = '+' + get_target_package_vars()['platform'] | |
| 666 return os.path.join(package_out_dir, pkg_def.name + sfx + '.cipd') | |
| 667 | |
| 668 | |
| 332 def run( | 669 def run( | 
| 333 py_venv, | 670 py_venv, | 
| 334 go_workspace, | 671 go_workspace, | 
| 335 build_callback, | 672 build_callback, | 
| 336 builder, | 673 builder, | 
| 337 package_def_dir, | 674 package_def_dir, | 
| 338 package_out_dir, | 675 package_out_dir, | 
| 339 package_def_files, | 676 package_def_files, | 
| 340 build, | 677 build, | 
| 341 upload, | 678 upload, | 
| 342 service_url, | 679 service_url, | 
| 343 tags, | 680 tags, | 
| 344 service_account_json, | 681 service_account_json, | 
| 345 json_output): | 682 json_output): | 
| 346 """Rebuild python and Go universes and CIPD packages. | 683 """Rebuilds python and Go universes and CIPD packages. | 
| 347 | 684 | 
| 348 Args: | 685 Args: | 
| 349 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. | 686 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. | 
| 350 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 687 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 
| 351 build_callback: called to build binaries, virtual environment, etc. | 688 build_callback: called to build binaries, virtual environment, etc. | 
| 352 builder: name of CI buildbot builder that invoked the script. | 689 builder: name of CI buildbot builder that invoked the script. | 
| 353 package_def_dir: path to build/packages dir to search for *.yaml. | 690 package_def_dir: path to build/packages dir to search for *.yaml. | 
| 354 package_out_dir: where to put built packages. | 691 package_out_dir: where to put built packages. | 
| 355 package_def_files: names of *.yaml files in package_def_dir or [] for all. | 692 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). | 693 build: False to skip building packages (valid only when upload==True). | 
| 357 upload: True to also upload built packages, False just to build them. | 694 upload: True to also upload built packages, False just to build them. | 
| 358 service_url: URL of a package repository service. | 695 service_url: URL of a package repository service. | 
| 359 tags: a list of tags to attach to uploaded package instances. | 696 tags: a list of tags to attach to uploaded package instances. | 
| 360 service_account_json: path to *.json service account credential. | 697 service_account_json: path to *.json service account credential. | 
| 361 json_output: path to *.json file to write info about built packages to. | 698 json_output: path to *.json file to write info about built packages to. | 
| 362 | 699 | 
| 363 Returns: | 700 Returns: | 
| 364 0 on success, 1 or error. | 701 0 on success, 1 or error. | 
| 365 """ | 702 """ | 
| 366 assert build or upload, 'Both build and upload are False, nothing to do' | 703 assert build or upload, 'Both build and upload are False, nothing to do' | 
| 367 | 704 | 
| 368 # Remove stale output so that test_packages.py do not test old files when | 705 # We need both GOOS and GOARCH or none. | 
| 369 # invoked without arguments. | 706 if is_cross_compiling(): | 
| 370 if build: | 707 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')): | 708 print >> sys.stderr, ( | 
| 372 os.remove(path) | 709 'When cross-compiling both GOOS and GOARCH environment variables ' | 
| 710 'must be set.') | |
| 711 return 1 | |
| 373 | 712 | 
| 
nodir
2016/06/26 05:00:47
check that GOARM env var is '6' or None
 
Vadim Sh.
2016/06/27 21:37:28
Done.
 | |
| 374 packages_to_build = [ | 713 # Append tags related to the build host. They are especially important when | 
| 375 p for p in enumerate_packages_to_build(package_def_dir, package_def_files) | 714 # cross-compiling: cross-compiled packages can be identified by comparing the | 
| 376 if should_process_on_builder(p, py_venv, builder) | 715 # platform in the package name with value of 'build_host_platform' tag. | 
| 377 ] | 716 tags = list(tags) | 
| 717 host_vars = get_host_package_vars() | |
| 718 tags.append('build_host_fqdn:' + socket.getfqdn()) | |
| 
nodir
2016/06/26 05:00:47
maybe only first component? full name may contain
 
Vadim Sh.
2016/06/27 21:37:29
Done.
 | |
| 719 tags.append('build_host_platform:' + host_vars['platform']) | |
| 720 tags.append('build_host_os_ver:' + host_vars['os_ver']) | |
| 721 | |
| 722 all_packages = enumerate_packages(py_venv, package_def_dir, package_def_files) | |
| 723 packages_to_build = [p for p in all_packages if p.should_build(builder)] | |
| 378 | 724 | 
| 379 print_title('Overview') | 725 print_title('Overview') | 
| 380 print 'Service URL: %s' % service_url | 726 if upload: | 
| 381 print | 727 print 'Service URL: %s' % service_url | 
| 728 print | |
| 382 if builder: | 729 if builder: | 
| 383 print 'Package definition files to process on %s:' % builder | 730 print 'Package definition files to process on %s:' % builder | 
| 384 else: | 731 else: | 
| 385 print 'Package definition files to process:' | 732 print 'Package definition files to process:' | 
| 386 for pkg_def_file in packages_to_build: | 733 for pkg_def in packages_to_build: | 
| 387 print ' %s' % os.path.basename(pkg_def_file) | 734 print ' %s' % pkg_def.name | 
| 388 if not packages_to_build: | 735 if not packages_to_build: | 
| 389 print ' <none>' | 736 print ' <none>' | 
| 390 print | 737 print | 
| 391 print 'Variables to pass to CIPD:' | 738 print 'Variables to pass to CIPD:' | 
| 392 package_vars = get_package_vars() | 739 package_vars = get_package_vars() | 
| 393 for k, v in sorted(package_vars.items()): | 740 for k, v in sorted(package_vars.items()): | 
| 394 print ' %s = %s' % (k, v) | 741 print ' %s = %s' % (k, v) | 
| 395 if tags: | 742 if upload and tags: | 
| 396 print | 743 print | 
| 397 print 'Tags to attach to uploaded packages:' | 744 print 'Tags to attach to uploaded packages:' | 
| 398 for tag in sorted(tags): | 745 for tag in sorted(tags): | 
| 399 print ' %s' % tag | 746 print ' %s' % tag | 
| 400 if not packages_to_build: | 747 if not packages_to_build: | 
| 401 print | 748 print | 
| 402 print 'Nothing to do.' | 749 print 'Nothing to do.' | 
| 403 return 0 | 750 return 0 | 
| 404 | 751 | 
| 752 # Remove old build artifacts to avoid stale files in case the script crashes | |
| 753 # for some reason. | |
| 754 if build: | |
| 755 print_title('Cleaning %s' % package_out_dir) | |
| 756 if not os.path.exists(package_out_dir): | |
| 757 os.makedirs(package_out_dir) | |
| 758 cleaned = False | |
| 759 for pkg_def in packages_to_build: | |
| 760 out_file = get_build_out_file(package_out_dir, pkg_def) | |
| 761 if os.path.exists(out_file): | |
| 762 print 'Removing stale %s' % os.path.basename(out_file) | |
| 763 os.remove(out_file) | |
| 764 cleaned = True | |
| 765 if not cleaned: | |
| 766 print 'Nothing to clean' | |
| 767 | |
| 768 # Make sure we have a Go toolset and it matches the host platform we detected | |
| 769 # in get_host_package_vars(). Otherwise we may end up uploading wrong binaries | |
| 770 # under host platform CIPD package suffix. It's important on Linux with 64-bit | |
| 771 # kernel and 32-bit userland (we must use 32-bit Go in that case, even if | |
| 772 # 64-bit Go works too). | |
| 773 go_env = bootstrap_go_toolset(go_workspace) | |
| 774 expected_arch = host_vars['platform'].split('-')[1] | |
| 775 if go_env['GOHOSTARCH'] != expected_arch: | |
| 
nodir
2016/06/26 05:00:47
GOHOST* vars are hardly documented. Can minimally
 
Vadim Sh.
2016/06/27 21:37:29
IMHO, it's pretty obvious what they mean.
 | |
| 776 print >> sys.stderr, ( | |
| 777 'Go toolset GOHOSTARCH (%s) doesn\'t match expected architecture (%s)' % | |
| 778 (go_env['GOHOSTARCH'], expected_arch)) | |
| 779 return 1 | |
| 780 | |
| 781 # Build the cipd client needed later to build or upload packages. | |
| 782 cipd_exe = build_cipd_client(go_workspace, package_out_dir) | |
| 783 | |
| 405 # Build the world. | 784 # Build the world. | 
| 406 if build: | 785 if build: | 
| 407 build_callback() | 786 build_callback(packages_to_build) | 
| 408 | 787 | 
| 409 # Package it. | 788 # Package it. | 
| 410 failed = [] | 789 failed = [] | 
| 411 succeeded = [] | 790 succeeded = [] | 
| 412 for pkg_def_file in packages_to_build: | 791 for pkg_def in packages_to_build: | 
| 413 # path/name.yaml -> out/name.cipd. | 792 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: | 793 try: | 
| 417 info = None | 794 info = None | 
| 418 if build: | 795 if build: | 
| 419 info = build_pkg(go_workspace, pkg_def_file, out_file, package_vars) | 796 info = build_pkg(cipd_exe, pkg_def, out_file, package_vars) | 
| 420 if upload: | 797 if upload: | 
| 421 info = upload_pkg( | 798 info = upload_pkg( | 
| 422 go_workspace, | 799 cipd_exe, | 
| 423 out_file, | 800 out_file, | 
| 424 service_url, | 801 service_url, | 
| 425 tags, | 802 tags, | 
| 426 service_account_json) | 803 service_account_json) | 
| 427 assert info is not None | 804 assert info is not None | 
| 428 succeeded.append({'pkg_def_name': name, 'info': info}) | 805 succeeded.append({'pkg_def_name': pkg_def.name, 'info': info}) | 
| 429 except (BuildException, UploadException) as e: | 806 except (BuildException, UploadException) as e: | 
| 430 failed.append({'pkg_def_name': name, 'error': str(e)}) | 807 failed.append({'pkg_def_name': pkg_def.name, 'error': str(e)}) | 
| 431 | 808 | 
| 432 print_title('Summary') | 809 print_title('Summary') | 
| 433 for d in failed: | 810 for d in failed: | 
| 434 print 'FAILED %s, see log above' % d['pkg_def_name'] | 811 print 'FAILED %s, see log above' % d['pkg_def_name'] | 
| 435 for d in succeeded: | 812 for d in succeeded: | 
| 436 print '%s %s' % (d['info']['package'], d['info']['instance_id']) | 813 print '%s %s' % (d['info']['package'], d['info']['instance_id']) | 
| 437 | 814 | 
| 438 if json_output: | 815 if json_output: | 
| 439 with open(json_output, 'w') as f: | 816 with open(json_output, 'w') as f: | 
| 440 summary = { | 817 summary = { | 
| 441 'failed': failed, | 818 'failed': failed, | 
| 442 'succeeded': succeeded, | 819 'succeeded': succeeded, | 
| 443 'tags': sorted(tags), | 820 'tags': sorted(tags), | 
| 444 'vars': package_vars, | 821 'vars': package_vars, | 
| 445 } | 822 } | 
| 446 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) | 823 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) | 
| 447 | 824 | 
| 448 return 1 if failed else 0 | 825 return 1 if failed else 0 | 
| 449 | 826 | 
| 450 | 827 | 
| 451 def build_infra(): | 828 def build_infra(pkg_defs): | 
| 452 """Builds infra.git multiverse.""" | 829 """Builds infra.git multiverse. | 
| 453 # Python side. | 830 | 
| 454 print_title('Making sure python virtual environment is fresh') | 831 Args: | 
| 455 run_python( | 832 pkg_defs: list of PackageDef instances for packages being built. | 
| 456 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 833 """ | 
| 457 args=[ | 834 # Skip building python if not used or if cross-compiling. | 
| 458 '--deps_file', | 835 if any(p.uses_python_env for p in pkg_defs) and not is_cross_compiling(): | 
| 459 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 836 print_title('Making sure python virtual environment is fresh') | 
| 460 os.path.join(ROOT, 'ENV'), | 837 run_python( | 
| 461 ]) | 838 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 
| 462 # Go side. | 839 args=[ | 
| 463 build_go(os.path.join(ROOT, 'go'), [ | 840 '--deps_file', | 
| 464 'infra/...', | 841 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 
| 465 'github.com/luci/luci-go/client/...', | 842 os.path.join(ROOT, 'ENV'), | 
| 466 'github.com/luci/luci-go/tools/...', | 843 ]) | 
| 467 ]) | 844 # Build all necessary go binaries. | 
| 845 build_go_code(os.path.join(ROOT, 'go'), pkg_defs) | |
| 468 | 846 | 
| 469 | 847 | 
| 470 def main( | 848 def main( | 
| 471 args, | 849 args, | 
| 472 build_callback=build_infra, | 850 build_callback=build_infra, | 
| 473 py_venv=os.path.join(ROOT, 'ENV'), | 851 py_venv=os.path.join(ROOT, 'ENV'), | 
| 474 go_workspace=os.path.join(ROOT, 'go'), | 852 go_workspace=os.path.join(ROOT, 'go'), | 
| 475 package_def_dir=os.path.join(ROOT, 'build', 'packages'), | 853 package_def_dir=os.path.join(ROOT, 'build', 'packages'), | 
| 476 package_out_dir=os.path.join(ROOT, 'build', 'out')): | 854 package_out_dir=os.path.join(ROOT, 'build', 'out')): | 
| 477 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') | 855 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') | 
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 514 args.build, | 892 args.build, | 
| 515 args.upload, | 893 args.upload, | 
| 516 args.service_url, | 894 args.service_url, | 
| 517 args.tags or [], | 895 args.tags or [], | 
| 518 args.service_account_json, | 896 args.service_account_json, | 
| 519 args.json_output) | 897 args.json_output) | 
| 520 | 898 | 
| 521 | 899 | 
| 522 if __name__ == '__main__': | 900 if __name__ == '__main__': | 
| 523 sys.exit(main(sys.argv[1:])) | 901 sys.exit(main(sys.argv[1:])) | 
| OLD | NEW |