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

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: prebuild stdlib Created 4 years, 5 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
« no previous file with comments | « build/README.md ('k') | build/out/.gitignore » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 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
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
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:]))
OLDNEW
« no previous file with comments | « build/README.md ('k') | build/out/.gitignore » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698