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

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