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 socket |
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. |
25 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 28 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
26 | 29 |
27 # Root of infra gclient solution. | 30 # Root of infra gclient solution. |
28 GCLIENT_ROOT = os.path.dirname(ROOT) | 31 GCLIENT_ROOT = os.path.dirname(ROOT) |
29 | 32 |
30 # Where to upload packages to by default. | 33 # Where to upload packages to by default. |
31 PACKAGE_REPO_SERVICE = 'https://chrome-infra-packages.appspot.com' | 34 PACKAGE_REPO_SERVICE = 'https://chrome-infra-packages.appspot.com' |
32 | 35 |
33 # .exe on Windows. | 36 # .exe on Windows. |
34 EXE_SUFFIX = '.exe' if sys.platform == 'win32' else '' | 37 EXE_SUFFIX = '.exe' if sys.platform == 'win32' else '' |
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 bool(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 the 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: |
| 80 return False |
| 81 |
| 82 # If cross-compiling, pick only packages that support cross-compilation. |
| 83 if is_cross_compiling(): |
| 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 += '\n' + '-' * 80 |
| 158 title += '\n GOOS=%s' % os.environ['GOOS'] |
| 159 title += '\n GOARCH=%s' % os.environ['GOARCH'] |
| 160 if 'GOARM' in os.environ: |
| 161 title += '\n GOARM=%s' % os.environ['GOARM'] |
| 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): |
| 167 """Symlinks Go workspace into new root, modifies os.environ. |
| 168 |
| 169 Go toolset 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) | 193 |
132 shutil.rmtree(os.path.join(new_workspace, 'pkg'), ignore_errors=True) | 194 if goos is not None: |
133 | 195 if goos == '': |
134 # Recompile ('-a'). | 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 |
| 205 # Make sure we build ARMv6 code even if the host is ARMv7. See the comment in |
| 206 # get_host_package_vars for reasons why. Also explicitly set GOARM to 6 when |
| 207 # cross-compiling (it should be '6' in this case by default anyway). |
| 208 plat = platform.machine().lower() |
| 209 if plat.startswith('arm') or os.environ.get('GOARCH') == 'arm': |
| 210 os.environ['GOARM'] = '6' |
| 211 else: |
| 212 os.environ.pop('GOARM', None) |
| 213 |
135 try: | 214 try: |
| 215 yield new_workspace |
| 216 finally: |
| 217 # Apparently 'os.environ = orig_environ' doesn't actually modify process |
| 218 # environment, only modifications of os.environ object itself do. |
| 219 for k, v in orig_environ.iteritems(): |
| 220 os.environ[k] = v |
| 221 for k in os.environ.keys(): |
| 222 if k not in orig_environ: |
| 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 # See https://github.com/golang/go/blob/master/src/cmd/go/env.go for format |
| 244 # of the output. |
| 245 print 'Go environ:' |
| 246 print output.strip() |
| 247 env = {} |
| 248 for line in output.splitlines(): |
| 249 k, _, v = line.lstrip('set ').partition('=') |
| 250 if v.startswith('"') and v.endswith('"'): |
| 251 v = v.strip('"') |
| 252 env[k] = v |
| 253 return env |
| 254 |
| 255 |
| 256 def run_go_clean(go_workspace, packages, goos=None, goarch=None): |
| 257 """Removes object files and executables left from building given packages. |
| 258 |
| 259 Transitively cleans all dependencies (including stdlib!) and removes |
| 260 executables from GOBIN. |
| 261 |
| 262 Args: |
| 263 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 264 packages: list of go packages to clean (can include '...' patterns). |
| 265 goos: if set, overrides GOOS environment variable (removes it if ''). |
| 266 goarch: if set, overrides GOARCH environment variable (removes it if ''). |
| 267 """ |
| 268 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: |
| 269 print_go_step_title('Cleaning:\n %s' % '\n '.join(packages)) |
136 subprocess.check_call( | 270 subprocess.check_call( |
137 args=[ | 271 args=[ |
138 'python', '-u', os.path.join(new_workspace, 'env.py'), | 272 'python', '-u', os.path.join(new_workspace, 'env.py'), |
139 'go', 'install', '-a', '-v', | 273 'go', 'clean', '-i', '-r', |
140 ] + list(packages), | 274 ] + list(packages), |
141 executable=sys.executable, | 275 executable=sys.executable, |
142 stderr=subprocess.STDOUT) | 276 stderr=subprocess.STDOUT) |
143 finally: | 277 # Above command is either silent (without '-x') or too verbose (with '-x'). |
144 if new_root: | 278 # Prefer silent version, but add a note that it's alright. |
145 os.remove(new_root) | 279 print 'Done.' |
146 | 280 |
147 | 281 |
148 def enumerate_packages_to_build(package_def_dir, package_def_files=None): | 282 def run_go_install( |
149 """Returns a list of absolute paths to files in build/packages/*.yaml. | 283 go_workspace, packages, rebuild=False, goos=None, goarch=None): |
150 | 284 """Builds (and installs) Go packages into GOBIN via 'go install ...'. |
151 Args: | 285 |
| 286 Compiles and installs packages into default GOBIN, which is <go_workspace>/bin |
| 287 (it is setup by go/env.py). |
| 288 |
| 289 Args: |
| 290 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 291 packages: list of go packages to build (can include '...' patterns). |
| 292 rebuild: if True, will forcefully rebuild all dependences. |
| 293 goos: if set, overrides GOOS environment variable (removes it if ''). |
| 294 goarch: if set, overrides GOARCH environment variable (removes it if ''). |
| 295 """ |
| 296 rebuild_opt = ['-a'] if rebuild else [] |
| 297 title = 'Rebuilding' if rebuild else 'Building' |
| 298 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: |
| 299 print_go_step_title('%s:\n %s' % (title, '\n '.join(packages))) |
| 300 subprocess.check_call( |
| 301 args=[ |
| 302 'python', '-u', os.path.join(new_workspace, 'env.py'), |
| 303 'go', 'install', '-v', |
| 304 ] + rebuild_opt + list(packages), |
| 305 executable=sys.executable, |
| 306 stderr=subprocess.STDOUT) |
| 307 |
| 308 |
| 309 def run_go_build( |
| 310 go_workspace, package, output, rebuild=False, goos=None, goarch=None): |
| 311 """Builds single Go package. |
| 312 |
| 313 Args: |
| 314 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 315 package: go package to build. |
| 316 output: where to put the resulting binary. |
| 317 rebuild: if True, will forcefully rebuild all dependences. |
| 318 goos: if set, overrides GOOS environment variable (removes it if ''). |
| 319 goarch: if set, overrides GOARCH environment variable (removes it if ''). |
| 320 """ |
| 321 rebuild_opt = ['-a'] if rebuild else [] |
| 322 title = 'Rebuilding' if rebuild else 'Building' |
| 323 with hacked_workspace(go_workspace, goos, goarch) as new_workspace: |
| 324 print_go_step_title('%s %s' % (title, package)) |
| 325 subprocess.check_call( |
| 326 args=[ |
| 327 'python', '-u', os.path.join(new_workspace, 'env.py'), |
| 328 'go', 'build', |
| 329 ] + rebuild_opt + ['-v', '-o', output, package], |
| 330 executable=sys.executable, |
| 331 stderr=subprocess.STDOUT) |
| 332 |
| 333 |
| 334 def build_go_code(go_workspace, pkg_defs): |
| 335 """Builds and installs all Go packages used by the given PackageDefs. |
| 336 |
| 337 Understands GOOS and GOARCH and uses slightly different build strategy when |
| 338 cross-compiling. In the end <go_workspace>/bin will have all built binaries, |
| 339 and only them (regardless of whether we are cross-compiling or not). |
| 340 |
| 341 Args: |
| 342 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
| 343 pkg_defs: list of PackageDef objects that define what to build. |
| 344 """ |
| 345 # Grab a set of all go packages we need to build and install into GOBIN. |
| 346 to_install = [] |
| 347 for p in pkg_defs: |
| 348 to_install.extend(p.go_packages) |
| 349 to_install = sorted(set(to_install)) |
| 350 if not to_install: |
| 351 return |
| 352 |
| 353 # Make sure there are no stale files in the workspace. |
| 354 run_go_clean(go_workspace, to_install) |
| 355 |
| 356 if not is_cross_compiling(): |
| 357 # If not cross-compiling, build all Go code in a single "go install" step, |
| 358 # it's faster that way. We can't do that when cross-compiling, since |
| 359 # 'go install' isn't supposed to be used for cross-compilation and the |
| 360 # toolset actively complains with "go install: cannot install cross-compiled |
| 361 # binaries when GOBIN is set". |
| 362 run_go_install(go_workspace, to_install) |
| 363 else: |
| 364 # Prebuild stdlib once. 'go build' calls below are discarding build results, |
| 365 # so it's better to install as much shared stuff as possible beforehand. |
| 366 run_go_install(go_workspace, ['std']) |
| 367 |
| 368 # Build packages one by one and put the resulting binaries into GOBIN, as if |
| 369 # they were installed there. It's where the rest of the build.py code |
| 370 # expects them to be (see also 'root' property in package definition YAMLs). |
| 371 go_bin = os.path.join(go_workspace, 'bin') |
| 372 exe_suffix = get_target_package_vars()['exe_suffix'] |
| 373 for pkg in to_install: |
| 374 name = pkg[pkg.rfind('/')+1:] |
| 375 run_go_build(go_workspace, pkg, os.path.join(go_bin, name + exe_suffix)) |
| 376 |
| 377 |
| 378 def enumerate_packages(py_venv, package_def_dir, package_def_files): |
| 379 """Returns a list PackageDef instances for files in build/packages/*.yaml. |
| 380 |
| 381 Args: |
| 382 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. | 383 package_def_dir: path to build/packages dir to search for *.yaml. |
153 package_def_files: optional list of filenames to limit results to. | 384 package_def_files: optional list of filenames to limit results to. |
154 | 385 |
155 Returns: | 386 Returns: |
156 List of absolute paths to *.yaml files under packages_dir. | 387 List of PackageDef instances parsed from *.yaml files under packages_dir. |
157 """ | 388 """ |
158 # All existing package by default. | 389 paths = [] |
159 if not package_def_files: | 390 if not package_def_files: |
160 return sorted(glob.glob(os.path.join(package_def_dir, '*.yaml'))) | 391 # All existing package by default. |
161 paths = [] | 392 paths = glob.glob(os.path.join(package_def_dir, '*.yaml')) |
162 for name in package_def_files: | 393 else: |
163 abs_path = os.path.join(package_def_dir, name) | 394 # Otherwise pick only the ones in 'package_def_files' list. |
164 if not os.path.isfile(abs_path): | 395 for name in package_def_files: |
165 raise BuildException('No such package definition file: %s' % name) | 396 abs_path = os.path.abspath(os.path.join(package_def_dir, name)) |
166 paths.append(abs_path) | 397 if not os.path.isfile(abs_path): |
167 return sorted(paths) | 398 raise BuildException('No such package definition file: %s' % name) |
| 399 paths.append(abs_path) |
| 400 return [PackageDef(p, read_yaml(py_venv, p)) for p in sorted(paths)] |
168 | 401 |
169 | 402 |
170 def read_yaml(py_venv, path): | 403 def read_yaml(py_venv, path): |
171 """Returns content of YAML file as python dict.""" | 404 """Returns content of YAML file as python dict.""" |
172 # YAML lib is in venv, not activated here. Go through hoops. | 405 # YAML lib is in venv, not activated here. Go through hoops. |
| 406 # TODO(vadimsh): Doesn't work on ARM, since we have no working infra_python |
| 407 # venv there. Replace this hack with vendored pure-python PyYAML. |
173 oneliner = ( | 408 oneliner = ( |
174 'import json, sys, yaml; ' | 409 'import json, sys, yaml; ' |
175 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') | 410 'json.dump(yaml.safe_load(sys.stdin), sys.stdout)') |
176 if sys.platform == 'win32': | 411 if sys.platform == 'win32': |
177 python_venv_path = ('Scripts', 'python.exe') | 412 python_venv_path = ('Scripts', 'python.exe') |
178 else: | 413 else: |
179 python_venv_path = ('bin', 'python') | 414 python_venv_path = ('bin', 'python') |
180 executable = os.path.join(py_venv, *python_venv_path) | 415 executable = os.path.join(py_venv, *python_venv_path) |
181 env = os.environ.copy() | 416 env = os.environ.copy() |
182 env.pop('PYTHONPATH', None) | 417 env.pop('PYTHONPATH', None) |
183 proc = subprocess.Popen( | 418 proc = subprocess.Popen( |
184 [executable, '-c', oneliner], | 419 [executable, '-c', oneliner], |
185 executable=executable, | 420 executable=executable, |
186 stdin=subprocess.PIPE, | 421 stdin=subprocess.PIPE, |
187 stdout=subprocess.PIPE, | 422 stdout=subprocess.PIPE, |
188 env=env) | 423 env=env) |
189 with open(path, 'r') as f: | 424 with open(path, 'r') as f: |
190 out, _ = proc.communicate(f.read()) | 425 out, _ = proc.communicate(f.read()) |
191 if proc.returncode: | 426 if proc.returncode: |
192 raise BuildException('Failed to parse YAML at %s' % path) | 427 raise BuildException('Failed to parse YAML at %s' % path) |
193 return json.loads(out) | 428 return json.loads(out) |
194 | 429 |
195 | 430 |
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(): | 431 def get_package_vars(): |
205 """Returns a dict with variables that describe the current environment. | 432 """Returns a dict with variables that describe the package target environment. |
206 | 433 |
207 Variables can be referenced in the package definition YAML as | 434 Variables can be referenced in the package definition YAML as |
208 ${variable_name}. It allows to reuse exact same definition file for similar | 435 ${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 | 436 packages (e.g. packages with same cross platform binary, but for different |
210 platforms). | 437 platforms). |
| 438 |
| 439 If running in cross-compilation mode, uses GOOS and GOARCH to figure out the |
| 440 target platform instead of examining the host environment. |
| 441 """ |
| 442 if is_cross_compiling(): |
| 443 return get_target_package_vars() |
| 444 return get_host_package_vars() |
| 445 |
| 446 |
| 447 def get_target_package_vars(): |
| 448 """Returns a dict with variables that describe cross-compilation target env. |
| 449 |
| 450 Examines os.environ for GOOS, GOARCH and GOARM. |
| 451 |
| 452 The returned dict contains only 'platform' and 'exe_suffix' entries. |
| 453 """ |
| 454 assert is_cross_compiling() |
| 455 goos = os.environ['GOOS'] |
| 456 goarch = os.environ['GOARCH'] |
| 457 |
| 458 if goarch not in ('386', 'amd64', 'arm'): |
| 459 raise BuildException('Unsupported GOARCH %s' % goarch) |
| 460 |
| 461 # There are many ARMs, pick the concrete instruction set. 'v6' is the default, |
| 462 # don't try to support other variants for now. |
| 463 # |
| 464 # See https://golang.org/doc/install/source#environment. |
| 465 if goarch == 'arm': |
| 466 goarm = os.environ.get('GOARM', '6') |
| 467 if goarm != '6': |
| 468 raise BuildException('Unsupported GOARM value %s' % goarm) |
| 469 arch = 'armv6l' |
| 470 else: |
| 471 arch = goarch |
| 472 |
| 473 # We use 'mac' instead of 'darwin'. |
| 474 if goos == 'darwin': |
| 475 goos = 'mac' |
| 476 |
| 477 return { |
| 478 'exe_suffix': '.exe' if goos == 'windows' else '', |
| 479 'platform': '%s-%s' % (goos, arch), |
| 480 } |
| 481 |
| 482 |
| 483 def get_host_package_vars(): |
| 484 """Returns a dict with variables that describe the current host environment. |
| 485 |
| 486 The returned platform may not match the machine environment exactly, but it is |
| 487 compatible with it. |
| 488 |
| 489 For example, on ARMv7 machines we claim that we are in fact running ARMv6 |
| 490 (which is subset of ARMv7), since we don't really care about v7 over v6 |
| 491 difference and want to reduce the variability in supported architectures |
| 492 instead. |
| 493 |
| 494 Similarly, if running on 64-bit Linux with 32-bit user space (based on python |
| 495 interpreter bitness), we claim that machine is 32-bit, since most 32-bit Linux |
| 496 Chrome Infra bots are in fact running 64-bit kernels with 32-bit userlands. |
211 """ | 497 """ |
212 # linux, mac or windows. | 498 # linux, mac or windows. |
213 platform_variant = { | 499 platform_variant = { |
214 'darwin': 'mac', | 500 'darwin': 'mac', |
215 'linux2': 'linux', | 501 'linux2': 'linux', |
216 'win32': 'windows', | 502 'win32': 'windows', |
217 }.get(sys.platform) | 503 }.get(sys.platform) |
218 if not platform_variant: | 504 if not platform_variant: |
219 raise ValueError('Unknown OS: %s' % sys.platform) | 505 raise ValueError('Unknown OS: %s' % sys.platform) |
220 | 506 |
(...skipping 12 matching lines...) Expand all Loading... |
233 else: | 519 else: |
234 raise ValueError('Unknown OS: %s' % sys.platform) | 520 raise ValueError('Unknown OS: %s' % sys.platform) |
235 | 521 |
236 # amd64, 386, etc. | 522 # amd64, 386, etc. |
237 platform_arch = { | 523 platform_arch = { |
238 'amd64': 'amd64', | 524 'amd64': 'amd64', |
239 'i386': '386', | 525 'i386': '386', |
240 'i686': '386', | 526 'i686': '386', |
241 'x86': '386', | 527 'x86': '386', |
242 'x86_64': 'amd64', | 528 'x86_64': 'amd64', |
| 529 'armv6l': 'armv6l', |
| 530 'armv7l': 'armv6l', # we prefer to use older instruction set for builds |
243 }.get(platform.machine().lower()) | 531 }.get(platform.machine().lower()) |
244 if not platform_arch: | 532 if not platform_arch: |
245 raise ValueError('Unknown machine arch: %s' % platform.machine()) | 533 raise ValueError('Unknown machine arch: %s' % platform.machine()) |
246 | 534 |
| 535 # Most 32-bit Linux Chrome Infra bots are in fact running 64-bit kernel with |
| 536 # 32-bit userland. Detect this case (based on bitness of the python |
| 537 # interpreter) and report the bot as '386'. |
| 538 if (platform_variant == 'linux' and |
| 539 platform_arch == 'amd64' and |
| 540 sys.maxsize == (2 ** 31) - 1): |
| 541 platform_arch = '386' |
| 542 |
247 return { | 543 return { |
248 # e.g. '.exe' or ''. | 544 # e.g. '.exe' or ''. |
249 'exe_suffix': EXE_SUFFIX, | 545 'exe_suffix': EXE_SUFFIX, |
250 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. | 546 # e.g. 'ubuntu14_04' or 'mac10_9' or 'win6_1'. |
251 'os_ver': os_ver, | 547 'os_ver': os_ver, |
252 # e.g. 'linux-amd64' | 548 # e.g. 'linux-amd64' |
253 'platform': '%s-%s' % (platform_variant, platform_arch), | 549 'platform': '%s-%s' % (platform_variant, platform_arch), |
254 # e.g. '27' (dots are not allowed in package names). | 550 # e.g. '27' (dots are not allowed in package names). |
255 'python_version': '%s%s' % sys.version_info[:2], | 551 'python_version': '%s%s' % sys.version_info[:2], |
256 } | 552 } |
257 | 553 |
258 | 554 |
259 def build_pkg(go_workspace, pkg_def_file, out_file, package_vars): | 555 def build_pkg(cipd_exe, pkg_def, out_file, package_vars): |
260 """Invokes CIPD client to build a package. | 556 """Invokes CIPD client to build a package. |
261 | 557 |
262 Args: | 558 Args: |
263 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 559 cipd_exe: path to cipd client binary to use. |
264 pkg_def_file: path to *.yaml file with package definition. | 560 pkg_def: instance of PackageDef representing this package. |
265 out_file: where to store the built package. | 561 out_file: where to store the built package. |
266 package_vars: dict with variables to pass as -pkg-var to cipd. | 562 package_vars: dict with variables to pass as -pkg-var to cipd. |
267 | 563 |
268 Returns: | 564 Returns: |
269 {'package': <name>, 'instance_id': <sha1>} | 565 {'package': <name>, 'instance_id': <sha1>} |
270 | 566 |
271 Raises: | 567 Raises: |
272 BuildException on error. | 568 BuildException on error. |
273 """ | 569 """ |
274 print_title('Building: %s' % os.path.basename(pkg_def_file)) | 570 print_title('Building: %s' % os.path.basename(out_file)) |
275 | 571 |
276 # Make sure not stale output remains. | 572 # Make sure not stale output remains. |
277 if os.path.isfile(out_file): | 573 if os.path.isfile(out_file): |
278 os.remove(out_file) | 574 os.remove(out_file) |
279 | 575 |
280 # Build the package. | 576 # Build the package. |
281 args = ['-pkg-def', pkg_def_file] | 577 args = ['-pkg-def', pkg_def.path] |
282 for k, v in sorted(package_vars.items()): | 578 for k, v in sorted(package_vars.items()): |
283 args.extend(['-pkg-var', '%s:%s' % (k, v)]) | 579 args.extend(['-pkg-var', '%s:%s' % (k, v)]) |
284 args.extend(['-out', out_file]) | 580 args.extend(['-out', out_file]) |
285 exit_code, json_output = run_cipd(go_workspace, 'pkg-build', args) | 581 exit_code, json_output = run_cipd(cipd_exe, 'pkg-build', args) |
286 if exit_code: | 582 if exit_code: |
287 print | 583 print |
288 print >> sys.stderr, 'FAILED! ' * 10 | 584 print >> sys.stderr, 'FAILED! ' * 10 |
289 raise BuildException('Failed to build the CIPD package, see logs') | 585 raise BuildException('Failed to build the CIPD package, see logs') |
290 | 586 |
291 # Expected result is {'package': 'name', 'instance_id': 'sha1'} | 587 # Expected result is {'package': 'name', 'instance_id': 'sha1'} |
292 info = json_output['result'] | 588 info = json_output['result'] |
293 print '%s %s' % (info['package'], info['instance_id']) | 589 print '%s %s' % (info['package'], info['instance_id']) |
294 return info | 590 return info |
295 | 591 |
296 | 592 |
297 def upload_pkg(go_workspace, pkg_file, service_url, tags, service_account): | 593 def upload_pkg(cipd_exe, pkg_file, service_url, tags, service_account): |
298 """Uploads existing *.cipd file to the storage and tags it. | 594 """Uploads existing *.cipd file to the storage and tags it. |
299 | 595 |
300 Args: | 596 Args: |
301 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 597 cipd_exe: path to cipd client binary to use. |
302 pkg_file: path to *.cipd file to upload. | 598 pkg_file: path to *.cipd file to upload. |
303 service_url: URL of a package repository service. | 599 service_url: URL of a package repository service. |
304 tags: a list of tags to attach to uploaded package instance. | 600 tags: a list of tags to attach to uploaded package instance. |
305 service_account: path to *.json file with service account to use. | 601 service_account: path to *.json file with service account to use. |
306 | 602 |
307 Returns: | 603 Returns: |
308 {'package': <name>, 'instance_id': <sha1>} | 604 {'package': <name>, 'instance_id': <sha1>} |
309 | 605 |
310 Raises: | 606 Raises: |
311 UploadException on error. | 607 UploadException on error. |
312 """ | 608 """ |
313 print_title('Uploading: %s' % os.path.basename(pkg_file)) | 609 print_title('Uploading: %s' % os.path.basename(pkg_file)) |
314 | 610 |
315 args = ['-service-url', service_url] | 611 args = ['-service-url', service_url] |
316 for tag in sorted(tags): | 612 for tag in sorted(tags): |
317 args.extend(['-tag', tag]) | 613 args.extend(['-tag', tag]) |
318 args.extend(['-ref', 'latest']) | 614 args.extend(['-ref', 'latest']) |
319 if service_account: | 615 if service_account: |
320 args.extend(['-service-account-json', service_account]) | 616 args.extend(['-service-account-json', service_account]) |
321 args.append(pkg_file) | 617 args.append(pkg_file) |
322 exit_code, json_output = run_cipd(go_workspace, 'pkg-register', args) | 618 exit_code, json_output = run_cipd(cipd_exe, 'pkg-register', args) |
323 if exit_code: | 619 if exit_code: |
324 print | 620 print |
325 print >> sys.stderr, 'FAILED! ' * 10 | 621 print >> sys.stderr, 'FAILED! ' * 10 |
326 raise UploadException('Failed to upload the CIPD package, see logs') | 622 raise UploadException('Failed to upload the CIPD package, see logs') |
327 info = json_output['result'] | 623 info = json_output['result'] |
328 print '%s %s' % (info['package'], info['instance_id']) | 624 print '%s %s' % (info['package'], info['instance_id']) |
329 return info | 625 return info |
330 | 626 |
331 | 627 |
| 628 def build_cipd_client(go_workspace, out_dir): |
| 629 """Builds cipd client binary for the host platform. |
| 630 |
| 631 Ignores GOOS and GOARCH env vars. Puts the client binary into |
| 632 '<out_dir>/.cipd_client/cipd_<digest>'. |
| 633 |
| 634 This binary is used by build.py itself and later by test_packages.py. |
| 635 |
| 636 Args: |
| 637 go_workspace: path to Go workspace root (contains 'env.py', 'src', etc). |
| 638 out_dir: build output directory, will be used to store the binary. |
| 639 |
| 640 Returns: |
| 641 Path to the built binary. |
| 642 """ |
| 643 # To avoid rebuilding cipd client all the time, we cache it in out/*, using |
| 644 # a combination of DEPS+deps.lock+bootstrap.py as a cache key (they define |
| 645 # exact set of sources used to build the cipd binary). |
| 646 # |
| 647 # We can't just use the client in infra.git/cipd/* because it is built by this |
| 648 # script itself: it introduced bootstrap dependency cycle in case we need to |
| 649 # add a new platform or if we wipe cipd backend storage. |
| 650 seed_paths = [ |
| 651 os.path.join(ROOT, 'DEPS'), |
| 652 os.path.join(ROOT, 'go', 'deps.lock'), |
| 653 os.path.join(ROOT, 'go', 'bootstrap.py'), |
| 654 ] |
| 655 digest = hashlib.sha1() |
| 656 for p in seed_paths: |
| 657 with open(p, 'rb') as f: |
| 658 digest.update(f.read()) |
| 659 cache_key = digest.hexdigest()[:20] |
| 660 |
| 661 # Already have it? |
| 662 cipd_out_dir = os.path.join(out_dir, '.cipd_client') |
| 663 cipd_exe = os.path.join(cipd_out_dir, 'cipd_%s%s' % (cache_key, EXE_SUFFIX)) |
| 664 if os.path.exists(cipd_exe): |
| 665 return cipd_exe |
| 666 |
| 667 # Nuke all previous copies, make sure out_dir exists. |
| 668 if os.path.exists(cipd_out_dir): |
| 669 for p in glob.glob(os.path.join(cipd_out_dir, 'cipd_*')): |
| 670 os.remove(p) |
| 671 else: |
| 672 os.makedirs(cipd_out_dir) |
| 673 |
| 674 # Build cipd client binary for the host platform. |
| 675 run_go_build( |
| 676 go_workspace, |
| 677 package='github.com/luci/luci-go/client/cmd/cipd', |
| 678 output=cipd_exe, |
| 679 rebuild=True, |
| 680 goos='', |
| 681 goarch='') |
| 682 |
| 683 return cipd_exe |
| 684 |
| 685 |
| 686 def get_build_out_file(package_out_dir, pkg_def): |
| 687 """Returns a path where to put built *.cipd package file. |
| 688 |
| 689 Args: |
| 690 package_out_dir: root directory where to put *.cipd files. |
| 691 pkg_def: instance of PackageDef being built. |
| 692 """ |
| 693 # When cross-compiling, append a suffix to package file name to indicate that |
| 694 # it's for foreign platform. |
| 695 sfx = '' |
| 696 if is_cross_compiling(): |
| 697 sfx = '+' + get_target_package_vars()['platform'] |
| 698 return os.path.join(package_out_dir, pkg_def.name + sfx + '.cipd') |
| 699 |
| 700 |
332 def run( | 701 def run( |
333 py_venv, | 702 py_venv, |
334 go_workspace, | 703 go_workspace, |
335 build_callback, | 704 build_callback, |
336 builder, | 705 builder, |
337 package_def_dir, | 706 package_def_dir, |
338 package_out_dir, | 707 package_out_dir, |
339 package_def_files, | 708 package_def_files, |
340 build, | 709 build, |
341 upload, | 710 upload, |
342 service_url, | 711 service_url, |
343 tags, | 712 tags, |
344 service_account_json, | 713 service_account_json, |
345 json_output): | 714 json_output): |
346 """Rebuild python and Go universes and CIPD packages. | 715 """Rebuilds python and Go universes and CIPD packages. |
347 | 716 |
348 Args: | 717 Args: |
349 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. | 718 py_venv: path to 'infra/ENV' or 'infra_internal/ENV'. |
350 go_workspace: path to 'infra/go' or 'infra_internal/go'. | 719 go_workspace: path to 'infra/go' or 'infra_internal/go'. |
351 build_callback: called to build binaries, virtual environment, etc. | 720 build_callback: called to build binaries, virtual environment, etc. |
352 builder: name of CI buildbot builder that invoked the script. | 721 builder: name of CI buildbot builder that invoked the script. |
353 package_def_dir: path to build/packages dir to search for *.yaml. | 722 package_def_dir: path to build/packages dir to search for *.yaml. |
354 package_out_dir: where to put built packages. | 723 package_out_dir: where to put built packages. |
355 package_def_files: names of *.yaml files in package_def_dir or [] for all. | 724 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). | 725 build: False to skip building packages (valid only when upload==True). |
357 upload: True to also upload built packages, False just to build them. | 726 upload: True to also upload built packages, False just to build them. |
358 service_url: URL of a package repository service. | 727 service_url: URL of a package repository service. |
359 tags: a list of tags to attach to uploaded package instances. | 728 tags: a list of tags to attach to uploaded package instances. |
360 service_account_json: path to *.json service account credential. | 729 service_account_json: path to *.json service account credential. |
361 json_output: path to *.json file to write info about built packages to. | 730 json_output: path to *.json file to write info about built packages to. |
362 | 731 |
363 Returns: | 732 Returns: |
364 0 on success, 1 or error. | 733 0 on success, 1 or error. |
365 """ | 734 """ |
366 assert build or upload, 'Both build and upload are False, nothing to do' | 735 assert build or upload, 'Both build and upload are False, nothing to do' |
367 | 736 |
368 # Remove stale output so that test_packages.py do not test old files when | 737 # We need both GOOS and GOARCH or none. |
369 # invoked without arguments. | 738 if is_cross_compiling(): |
370 if build: | 739 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')): | 740 print >> sys.stderr, ( |
372 os.remove(path) | 741 'When cross-compiling both GOOS and GOARCH environment variables ' |
| 742 'must be set.') |
| 743 return 1 |
| 744 if os.environ.get('GOARM', '6') != '6': |
| 745 print >> sys.stderr, 'Only GOARM=6 is supported for now.' |
| 746 return 1 |
373 | 747 |
374 packages_to_build = [ | 748 # 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) | 749 # cross-compiling: cross-compiled packages can be identified by comparing the |
376 if should_process_on_builder(p, py_venv, builder) | 750 # platform in the package name with value of 'build_host_platform' tag. |
377 ] | 751 tags = list(tags) |
| 752 host_vars = get_host_package_vars() |
| 753 tags.append('build_host_hostname:' + socket.gethostname().split('.')[0]) |
| 754 tags.append('build_host_platform:' + host_vars['platform']) |
| 755 tags.append('build_host_os_ver:' + host_vars['os_ver']) |
| 756 |
| 757 all_packages = enumerate_packages(py_venv, package_def_dir, package_def_files) |
| 758 packages_to_build = [p for p in all_packages if p.should_build(builder)] |
378 | 759 |
379 print_title('Overview') | 760 print_title('Overview') |
380 print 'Service URL: %s' % service_url | 761 if upload: |
381 print | 762 print 'Service URL: %s' % service_url |
| 763 print |
382 if builder: | 764 if builder: |
383 print 'Package definition files to process on %s:' % builder | 765 print 'Package definition files to process on %s:' % builder |
384 else: | 766 else: |
385 print 'Package definition files to process:' | 767 print 'Package definition files to process:' |
386 for pkg_def_file in packages_to_build: | 768 for pkg_def in packages_to_build: |
387 print ' %s' % os.path.basename(pkg_def_file) | 769 print ' %s' % pkg_def.name |
388 if not packages_to_build: | 770 if not packages_to_build: |
389 print ' <none>' | 771 print ' <none>' |
390 print | 772 print |
391 print 'Variables to pass to CIPD:' | 773 print 'Variables to pass to CIPD:' |
392 package_vars = get_package_vars() | 774 package_vars = get_package_vars() |
393 for k, v in sorted(package_vars.items()): | 775 for k, v in sorted(package_vars.items()): |
394 print ' %s = %s' % (k, v) | 776 print ' %s = %s' % (k, v) |
395 if tags: | 777 if upload and tags: |
396 print | 778 print |
397 print 'Tags to attach to uploaded packages:' | 779 print 'Tags to attach to uploaded packages:' |
398 for tag in sorted(tags): | 780 for tag in sorted(tags): |
399 print ' %s' % tag | 781 print ' %s' % tag |
400 if not packages_to_build: | 782 if not packages_to_build: |
401 print | 783 print |
402 print 'Nothing to do.' | 784 print 'Nothing to do.' |
403 return 0 | 785 return 0 |
404 | 786 |
| 787 # Remove old build artifacts to avoid stale files in case the script crashes |
| 788 # for some reason. |
| 789 if build: |
| 790 print_title('Cleaning %s' % package_out_dir) |
| 791 if not os.path.exists(package_out_dir): |
| 792 os.makedirs(package_out_dir) |
| 793 cleaned = False |
| 794 for pkg_def in packages_to_build: |
| 795 out_file = get_build_out_file(package_out_dir, pkg_def) |
| 796 if os.path.exists(out_file): |
| 797 print 'Removing stale %s' % os.path.basename(out_file) |
| 798 os.remove(out_file) |
| 799 cleaned = True |
| 800 if not cleaned: |
| 801 print 'Nothing to clean' |
| 802 |
| 803 # Make sure we have a Go toolset and it matches the host platform we detected |
| 804 # in get_host_package_vars(). Otherwise we may end up uploading wrong binaries |
| 805 # under host platform CIPD package suffix. It's important on Linux with 64-bit |
| 806 # kernel and 32-bit userland (we must use 32-bit Go in that case, even if |
| 807 # 64-bit Go works too). |
| 808 go_env = bootstrap_go_toolset(go_workspace) |
| 809 expected_arch = host_vars['platform'].split('-')[1] |
| 810 if go_env['GOHOSTARCH'] != expected_arch: |
| 811 print >> sys.stderr, ( |
| 812 'Go toolset GOHOSTARCH (%s) doesn\'t match expected architecture (%s)' % |
| 813 (go_env['GOHOSTARCH'], expected_arch)) |
| 814 return 1 |
| 815 |
| 816 # Build the cipd client needed later to build or upload packages. |
| 817 cipd_exe = build_cipd_client(go_workspace, package_out_dir) |
| 818 |
405 # Build the world. | 819 # Build the world. |
406 if build: | 820 if build: |
407 build_callback() | 821 build_callback(packages_to_build) |
408 | 822 |
409 # Package it. | 823 # Package it. |
410 failed = [] | 824 failed = [] |
411 succeeded = [] | 825 succeeded = [] |
412 for pkg_def_file in packages_to_build: | 826 for pkg_def in packages_to_build: |
413 # path/name.yaml -> out/name.cipd. | 827 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: | 828 try: |
417 info = None | 829 info = None |
418 if build: | 830 if build: |
419 info = build_pkg(go_workspace, pkg_def_file, out_file, package_vars) | 831 info = build_pkg(cipd_exe, pkg_def, out_file, package_vars) |
420 if upload: | 832 if upload: |
421 info = upload_pkg( | 833 info = upload_pkg( |
422 go_workspace, | 834 cipd_exe, |
423 out_file, | 835 out_file, |
424 service_url, | 836 service_url, |
425 tags, | 837 tags, |
426 service_account_json) | 838 service_account_json) |
427 assert info is not None | 839 assert info is not None |
428 succeeded.append({'pkg_def_name': name, 'info': info}) | 840 succeeded.append({'pkg_def_name': pkg_def.name, 'info': info}) |
429 except (BuildException, UploadException) as e: | 841 except (BuildException, UploadException) as e: |
430 failed.append({'pkg_def_name': name, 'error': str(e)}) | 842 failed.append({'pkg_def_name': pkg_def.name, 'error': str(e)}) |
431 | 843 |
432 print_title('Summary') | 844 print_title('Summary') |
433 for d in failed: | 845 for d in failed: |
434 print 'FAILED %s, see log above' % d['pkg_def_name'] | 846 print 'FAILED %s, see log above' % d['pkg_def_name'] |
435 for d in succeeded: | 847 for d in succeeded: |
436 print '%s %s' % (d['info']['package'], d['info']['instance_id']) | 848 print '%s %s' % (d['info']['package'], d['info']['instance_id']) |
437 | 849 |
438 if json_output: | 850 if json_output: |
439 with open(json_output, 'w') as f: | 851 with open(json_output, 'w') as f: |
440 summary = { | 852 summary = { |
441 'failed': failed, | 853 'failed': failed, |
442 'succeeded': succeeded, | 854 'succeeded': succeeded, |
443 'tags': sorted(tags), | 855 'tags': sorted(tags), |
444 'vars': package_vars, | 856 'vars': package_vars, |
445 } | 857 } |
446 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) | 858 json.dump(summary, f, sort_keys=True, indent=2, separators=(',', ': ')) |
447 | 859 |
448 return 1 if failed else 0 | 860 return 1 if failed else 0 |
449 | 861 |
450 | 862 |
451 def build_infra(): | 863 def build_infra(pkg_defs): |
452 """Builds infra.git multiverse.""" | 864 """Builds infra.git multiverse. |
453 # Python side. | 865 |
454 print_title('Making sure python virtual environment is fresh') | 866 Args: |
455 run_python( | 867 pkg_defs: list of PackageDef instances for packages being built. |
456 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), | 868 """ |
457 args=[ | 869 # Skip building python if not used or if cross-compiling. |
458 '--deps_file', | 870 if any(p.uses_python_env for p in pkg_defs) and not is_cross_compiling(): |
459 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), | 871 print_title('Making sure python virtual environment is fresh') |
460 os.path.join(ROOT, 'ENV'), | 872 run_python( |
461 ]) | 873 script=os.path.join(ROOT, 'bootstrap', 'bootstrap.py'), |
462 # Go side. | 874 args=[ |
463 build_go(os.path.join(ROOT, 'go'), [ | 875 '--deps_file', |
464 'infra/...', | 876 os.path.join(ROOT, 'bootstrap', 'deps.pyl'), |
465 'github.com/luci/luci-go/client/...', | 877 os.path.join(ROOT, 'ENV'), |
466 'github.com/luci/luci-go/tools/...', | 878 ]) |
467 ]) | 879 # Build all necessary go binaries. |
| 880 build_go_code(os.path.join(ROOT, 'go'), pkg_defs) |
468 | 881 |
469 | 882 |
470 def main( | 883 def main( |
471 args, | 884 args, |
472 build_callback=build_infra, | 885 build_callback=build_infra, |
473 py_venv=os.path.join(ROOT, 'ENV'), | 886 py_venv=os.path.join(ROOT, 'ENV'), |
474 go_workspace=os.path.join(ROOT, 'go'), | 887 go_workspace=os.path.join(ROOT, 'go'), |
475 package_def_dir=os.path.join(ROOT, 'build', 'packages'), | 888 package_def_dir=os.path.join(ROOT, 'build', 'packages'), |
476 package_out_dir=os.path.join(ROOT, 'build', 'out')): | 889 package_out_dir=os.path.join(ROOT, 'build', 'out')): |
477 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') | 890 parser = argparse.ArgumentParser(description='Builds infra CIPD packages') |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
514 args.build, | 927 args.build, |
515 args.upload, | 928 args.upload, |
516 args.service_url, | 929 args.service_url, |
517 args.tags or [], | 930 args.tags or [], |
518 args.service_account_json, | 931 args.service_account_json, |
519 args.json_output) | 932 args.json_output) |
520 | 933 |
521 | 934 |
522 if __name__ == '__main__': | 935 if __name__ == '__main__': |
523 sys.exit(main(sys.argv[1:])) | 936 sys.exit(main(sys.argv[1:])) |
OLD | NEW |