Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(97)

Side by Side Diff: build/build.py

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

Powered by Google App Engine
This is Rietveld 408576698