blob: b3b50a650b3695b068d93751b1b78ce62816c613 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""ChromeOS utility.
6
7Terminology used in this module.
8 short_version: ChromeOS version number without milestone, like "9876.0.0".
9 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
10 version: if not specified, it could be in short or full format.
11"""
12
13from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080014import ast
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080015import errno
16import json
17import logging
18import os
19import re
20import subprocess
21import time
22
23from bisect_kit import cli
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080024from bisect_kit import core
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080025from bisect_kit import cr_util
Kuang-che Wubfc4a642018-04-19 11:54:08 +080026from bisect_kit import git_util
27from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080028from bisect_kit import util
29
30logger = logging.getLogger(__name__)
31
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080032re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080033re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
34re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080035
36gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
37gs_release_path = (
38 'gs://chromeos-releases/{channel}-channel/{board}/{short_version}')
39
40# Assume gsutil is in PATH.
41gsutil_bin = 'gsutil'
42
Kuang-che Wub9705bd2018-06-28 17:59:18 +080043chromeos_root_inside_chroot = '/mnt/host/source'
44# relative to chromeos_root
45prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
46
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080047VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
48VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
49VERSION_KEY_MILESTONE = 'milestone'
50VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080051VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080052VERSION_KEY_ANDROID_BRANCH = 'android_branch'
53
54
Kuang-che Wu9890ce82018-07-07 15:14:10 +080055class NeedRecreateChrootException(Exception):
56 """Failed to build ChromeOS because of chroot mismatch or corruption"""
57
58
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080059def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080060 """Determines if `s` is chromeos short version.
61
62 This function doesn't accept version number of local build.
63 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080064 return bool(re.match(re_chromeos_short_version, s))
65
66
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080067def is_cros_localbuild_version(s):
68 """Determines if `s` is chromeos local build version."""
69 return bool(re.match(re_chromeos_localbuild_version, s))
70
71
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080072def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080073 """Determines if `s` is chromeos full version.
74
75 This function doesn't accept version number of local build.
76 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080077 return bool(re.match(re_chromeos_full_version, s))
78
79
80def is_cros_version(s):
81 """Determines if `s` is chromeos version (either short or full)"""
82 return is_cros_short_version(s) or is_cros_full_version(s)
83
84
85def make_cros_full_version(milestone, short_version):
86 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080087 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080088 return 'R%s-%s' % (milestone, short_version)
89
90
91def version_split(full_version):
92 """Splits full_version into milestone and short_version"""
93 assert is_cros_full_version(full_version)
94 milestone, short_version = full_version.split('-')
95 return milestone[1:], short_version
96
97
98def argtype_cros_version(s):
99 if not is_cros_version(s):
100 msg = 'invalid cros version'
101 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
102 return s
103
104
105def query_dut_lsb_release(host):
106 """Query /etc/lsb-release of given DUT
107
108 Args:
109 host: the DUT address
110
111 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800112 dict for keys and values of /etc/lsb-release.
113
114 Raises:
115 core.ExecutionFatalError: cannot connect to host or lsb-release file
116 doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800117 """
118 try:
119 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
120 except subprocess.CalledProcessError:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800121 raise core.ExecutionFatalError('cannot connect to DUT or not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800122 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
123
124
125def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800126 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800127
128 Args:
129 host: the DUT address
130
131 Returns:
132 True if the host is a chromeos device.
133 """
134 return query_dut_lsb_release(host).get('DEVICETYPE') in [
135 'CHROMEBASE',
136 'CHROMEBIT',
137 'CHROMEBOOK',
138 'CHROMEBOX',
139 'REFERENCE',
140 ]
141
142
143def query_dut_board(host):
144 """Query board name of a given DUT"""
145 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
146
147
148def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800149 """Query short version of a given DUT.
150
151 This function may return version of local build, which
152 is_cros_short_version() is false.
153 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800154 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
155
156
157def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800158 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800159
160 Args:
161 host: DUT address
162 connect_timeout: connection timeout
163
164 Returns:
165 boot uuid
166 """
167 cmd = ['ssh']
168 if connect_timeout:
169 cmd += ['-oConnectTimeout=%d' % connect_timeout]
170 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
171 return util.check_output(*cmd).strip()
172
173
174def reboot(host):
175 """Reboot a DUT and verify"""
176 logger.debug('reboot %s', host)
177 boot_id = query_dut_boot_id(host)
178
179 # Depends on timing, ssh may return failure due to broken pipe,
180 # so don't check ssh return code.
181 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800182 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800183
Kuang-che Wu708310b2018-03-28 17:24:34 +0800184
185def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800186 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800187 # (dev screen short delay) or more (long delay).
188 time.sleep(15)
189 for _ in range(100):
190 try:
191 # During boot, DUT does not response and thus ssh may hang a while. So
192 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
193 # set tight limit because it's inside retry loop.
194 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
195 return
196 except subprocess.CalledProcessError:
197 logger.debug('reboot not ready? sleep wait 1 sec')
198 time.sleep(1)
199
200 raise core.ExecutionFatalError('reboot failed?')
201
202
203def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800204 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800205
206 Args:
207 args: command line arguments passed to gsutil
208 kwargs:
209 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
210 but the path not found.
211
212 Returns:
213 stdout of gsutil
214
215 Raises:
216 core.ExecutionFatalError: gsutil failed to run
217 subprocess.CalledProcessError: command failed
218 """
219 stderr_lines = []
220 try:
221 return util.check_output(
222 gsutil_bin, *args, stderr_callback=stderr_lines.append)
223 except subprocess.CalledProcessError as e:
224 stderr = ''.join(stderr_lines)
225 if re.search(r'ServiceException:.* does not have .*access', stderr):
226 raise core.ExecutionFatalError(
227 'gsutil failed due to permission. ' +
228 'Run "%s config" and follow its instruction. ' % gsutil_bin +
229 'Fill any string if it asks for project-id')
230 if kwargs.get('ignore_errors'):
231 return ''
232 raise
233 except OSError as e:
234 if e.errno == errno.ENOENT:
235 raise core.ExecutionFatalError(
236 'Unable to run %s. gsutil is not installed or not in PATH?' %
237 gsutil_bin)
238 raise
239
240
241def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800242 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800243
244 Args:
245 args: arguments passed to 'gsutil ls'
246 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800247 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800248 exception, ex. path not found.
249
250 Returns:
251 list of 'gsutil ls' result. One element for one line of gsutil output.
252
253 Raises:
254 subprocess.CalledProcessError: gsutil failed, usually means path not found
255 """
256 return gsutil('ls', *args, **kwargs).splitlines()
257
258
259def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800260 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800261
262 Args:
263 board: ChromeOS board name
264 short_version: ChromeOS version number in short format, ex. 9300.0.0
265
266 Returns:
267 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
268 None if failed.
269 """
270 path = gs_archive_path.format(board=board) + '/R*-' + short_version
271 for line in gsutil_ls('-d', path, ignore_errors=True):
272 m = re.search(r'/R(\d+)-', line)
273 if not m:
274 continue
275 return m.group(1)
276
277 for channel in ['canary', 'dev', 'beta', 'stable']:
278 path = gs_release_path.format(
279 channel=channel, board=board, short_version=short_version)
280 for line in gsutil_ls(path, ignore_errors=True):
281 m = re.search(r'\bR(\d+)-' + short_version, line)
282 if not m:
283 continue
284 return m.group(1)
285
286 logger.error('unable to query milestone of %s for %s', short_version, board)
287 return None
288
289
290def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800291 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800292
293 Args:
294 board: ChromeOS board name
295 version: ChromeOS version number in short or full format
296
297 Returns:
298 (milestone, version in short format)
299 """
300 if is_cros_short_version(version):
301 milestone = query_milestone_by_version(board, version)
302 short_version = version
303 else:
304 milestone, short_version = version_split(version)
305 return milestone, short_version
306
307
308def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800309 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800310
311 Args:
312 version: ChromeOS version number in short or full format
313
314 Returns:
315 version number in short format
316 """
317 if is_cros_short_version(version):
318 return version
319 _, short_version = version_split(version)
320 return short_version
321
322
323def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800324 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800325
326 Args:
327 board: ChromeOS board name
328 version: ChromeOS version number in short or full format
329
330 Returns:
331 version number in full format
332 """
333 if is_cros_full_version(version):
334 return version
335 milestone = query_milestone_by_version(board, version)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800336 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800337 return make_cros_full_version(milestone, version)
338
339
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800340def prepare_prebuilt_image(board, version):
341 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800342
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800343 It searches for xbuddy image which "cros flash" can use, or fetch image to
344 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800345
346 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800347 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800348 version: ChromeOS version number in short or full format
349
350 Returns:
351 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800352 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800353 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800354 full_version = version_to_full(board, version)
355 short_version = version_to_short(full_version)
356
357 image_path = None
358 gs_path = gs_archive_path.format(board=board) + '/' + full_version
359 if gsutil_ls('-d', gs_path, ignore_errors=True):
360 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
361 board=board, full_version=full_version)
362 else:
363 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
364 if not os.path.exists(tmp_dir):
365 os.makedirs(tmp_dir)
366 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800367 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800368 # to fetch the image by ourselves
369 for channel in ['canary', 'dev', 'beta', 'stable']:
370 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
371 full_version=full_version, board=board)
372 gs_path = gs_release_path.format(
373 channel=channel, board=board, short_version=short_version)
374 gs_path += '/' + fn
375 if gsutil_ls(gs_path, ignore_errors=True):
376 # TODO(kcwu): delete tmp
377 gsutil('cp', gs_path, tmp_dir)
378 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
379 image_path = os.path.abspath(
380 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
381 break
382
383 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800384 return image_path
385
386
387def cros_flash(chromeos_root,
388 host,
389 board,
390 image_path,
391 version=None,
392 clobber_stateful=False,
393 disable_rootfs_verification=True,
394 run_inside_chroot=False):
395 """Flash a DUT with given ChromeOS image.
396
397 This is implemented by 'cros flash' command line.
398
399 Args:
400 chromeos_root: use 'cros flash' of which chromeos tree
401 host: DUT address
402 board: ChromeOS board name
403 image_path: chromeos image xbuddy path or file path. If
404 run_inside_chroot is True, the file path is relative to src/scrips.
405 Otherwise, the file path is relative to chromeos_root.
406 version: ChromeOS version in short or full format
407 clobber_stateful: Clobber stateful partition when performing update
408 disable_rootfs_verification: Disable rootfs verification after update
409 is completed
410 run_inside_chroot: if True, run 'cros flash' command inside the chroot
411 """
412 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
413
414 # Reboot is necessary because sometimes previous 'cros flash' failed and
415 # entered a bad state.
416 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800417
418 args = ['--no-ping', host, image_path]
419 if clobber_stateful:
420 args.append('--clobber-stateful')
421 if disable_rootfs_verification:
422 args.append('--disable-rootfs-verification')
423
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800424 if run_inside_chroot:
425 cros_sdk(chromeos_root, 'cros', 'flash', *args)
426 else:
427 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800428
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800429 if version:
430 # In the past, cros flash may fail with returncode=0
431 # So let's have an extra check.
432 short_version = version_to_short(version)
433 dut_version = query_dut_short_version(host)
434 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800435
436
437def version_info(board, version):
438 """Query subcomponents version info of given version of ChromeOS
439
440 Args:
441 board: ChromeOS board name
442 version: ChromeOS version number in short or full format
443
444 Returns:
445 dict of component and version info, including (if available):
446 cros_short_version: ChromeOS version
447 cros_full_version: ChromeOS version
448 milestone: milestone of ChromeOS
449 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800450 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800451 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
452 """
453 info = {}
454 full_version = version_to_full(board, version)
455
456 # Some boards may have only partial-metadata.json but no metadata.json.
457 # e.g. caroline R60-9462.0.0
458 # Let's try both.
459 metadata = None
460 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
461 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
462 metadata_filename)
463 metadata = gsutil('cat', path, ignore_errors=True)
464 if metadata:
465 o = json.loads(metadata)
466 v = o['version']
467 board_metadata = o['board-metadata'][board]
468 info.update({
469 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
470 VERSION_KEY_CROS_FULL_VERSION: v['full'],
471 VERSION_KEY_MILESTONE: v['milestone'],
472 VERSION_KEY_CR_VERSION: v['chrome'],
473 })
474
475 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800476 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800477 if 'android-branch' in v: # this appears since R58-9317.0.0
478 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
479 elif 'android-container-branch' in board_metadata:
480 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
481 break
482 else:
483 logger.error('Failed to read metadata from gs://chromeos-image-archive')
484 logger.error(
485 'Note, so far no quick way to look up version info for too old builds')
486
487 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800488
489
490def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800491 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800492
493 Args:
494 board: ChromeOS board name
495 version: ChromeOS version number in short or full format
496
497 Returns:
498 Chrome version number
499 """
500 info = version_info(board, version)
501 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800502
503
504def query_android_build_id(board, rev):
505 info = version_info(board, rev)
506 rev = info['android_build_id']
507 return rev
508
509
510def query_android_branch(board, rev):
511 info = version_info(board, rev)
512 rev = info['android_branch']
513 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800514
515
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800516def guess_chrome_version(board, rev):
517 """Guess chrome version number.
518
519 Args:
520 board: chromeos board name
521 rev: chrome or chromeos version
522
523 Returns:
524 chrome version number
525 """
526 if is_cros_version(rev):
527 assert board, 'need to specify BOARD for cros version'
528 rev = query_chrome_version(board, rev)
529 assert cr_util.is_chrome_version(rev)
530
531 return rev
532
533
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800534def is_inside_chroot():
535 """Returns True if we are inside chroot."""
536 return os.path.exists('/etc/cros_chroot_version')
537
538
539def cros_sdk(chromeos_root, *args, **kwargs):
540 """Run commands inside chromeos chroot.
541
542 Args:
543 chromeos_root: chromeos tree root
544 *args: command to run
545 **kwargs:
546 env: (dict) environment variables for the command
547 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800548 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800549 """
550 envs = []
551 for k, v in kwargs.get('env', {}).items():
552 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
553 envs.append('%s=%s' % (k, v))
554
555 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
556 # commands would be considered as background process.
557 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid'] + envs + ['--'] + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800558 return util.check_output(
559 *cmd,
560 cwd=chromeos_root,
561 stdin=kwargs.get('stdin'),
562 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800563
564
565def copy_into_chroot(chromeos_root, src, dst):
566 """Copies file into chromeos chroot.
567
568 Args:
569 chromeos_root: chromeos tree root
570 src: path outside chroot
571 dst: path inside chroot
572 """
573 # chroot may be an image, so we cannot copy to corresponding path
574 # directly.
575 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
576
577
578def exists_in_chroot(chromeos_root, path):
579 """Determine whether a path exists in the chroot.
580
581 Args:
582 chromeos_root: chromeos tree root
583 path: path inside chroot, relative to src/scripts
584
585 Returns:
586 True if a path exists
587 """
588 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800589 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800590 except subprocess.CalledProcessError:
591 return False
592 return True
593
594
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800595def check_if_need_recreate_chroot(stdout, stderr):
596 """Analyze build log and determine if chroot should be recreated.
597
598 Args:
599 stdout: stdout output of build
600 stderr: stderr output of build
601
602 Returns:
603 the reason if chroot needs recreated; None otherwise
604 """
605 del stdout # unused
606
607 if re.search(r"The current version of portage supports EAPI '\d+'. "
608 "You must upgrade", stderr):
609 return 'EAPI version mismatch'
610
611 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
612 return 'chroot version is too new'
613
614 return None
615
616
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800617def build_image(chromeos_root, board, rev):
618 """Build ChromeOS image.
619
620 Args:
621 chromeos_root: chromeos tree root
622 board: ChromeOS board name
623 rev: the version name to build
624
625 Returns:
626 Image path
627 """
628
629 # If the given version is already built, reuse it.
630 image_name = 'bisect-%s' % rev
631 image_path = os.path.join('../build/images', board, image_name,
632 'chromiumos_test_image.bin')
633 if exists_in_chroot(chromeos_root, image_path):
634 logger.info('"%s" already exists, skip build step', image_path)
635 return image_path
636
637 dirname = os.path.dirname(os.path.abspath(__file__))
638 script_name = 'build_cros_helper.sh'
639 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
640 script_name)
641 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800642
643 stderr_lines = []
644 try:
645 cros_sdk(
646 chromeos_root,
647 './%s' % script_name,
648 board,
649 image_name,
650 stderr_callback=stderr_lines.append)
651 except subprocess.CalledProcessError as e:
652 # Detect failures due to incompatibility between chroot and source tree. If
653 # so, notify the caller to recreate chroot and retry.
654 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
655 if reason:
656 raise NeedRecreateChrootException(reason)
657
658 # For other failures, don't know how to handle. Just bail out.
659 raise
660
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800661 return image_path
662
663
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800664class AutotestControlInfo(object):
665 """Parsed content of autotest control file.
666
667 Attributes:
668 name: test name
669 path: control file path
670 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
671 DOC, ATTRIBUTES, DEPENDENCIES, etc.
672 """
673
674 def __init__(self, path, variables):
675 self.name = variables['NAME']
676 self.path = path
677 self.variables = variables
678
679
680def parse_autotest_control_file(path):
681 """Parses autotest control file.
682
683 This only parses simple top-level string assignments.
684
685 Returns:
686 AutotestControlInfo object
687 """
688 variables = {}
689 code = ast.parse(open(path).read())
690 for stmt in code.body:
691 # Skip if not simple "NAME = *" assignment.
692 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
693 isinstance(stmt.targets[0], ast.Name)):
694 continue
695
696 # Only support string value.
697 if isinstance(stmt.value, ast.Str):
698 variables[stmt.targets[0].id] = stmt.value.s
699
700 return AutotestControlInfo(path, variables)
701
702
703def enumerate_autotest_control_files(autotest_dir):
704 """Enumerate autotest control files.
705
706 Args:
707 autotest_dir: autotest folder
708
709 Returns:
710 list of paths to control files
711 """
712 # Where to find control files. Relative to autotest_dir.
713 subpaths = [
714 'server/site_tests',
715 'client/site_tests',
716 'server/tests',
717 'client/tests',
718 ]
719
720 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
721 result = []
722 for subpath in subpaths:
723 path = os.path.join(autotest_dir, subpath)
724 for root, dirs, files in os.walk(path):
725
726 for black in blacklist:
727 if black in dirs:
728 dirs.remove(black)
729
730 for filename in files:
731 if filename == 'control' or filename.startswith('control.'):
732 result.append(os.path.join(root, filename))
733
734 return result
735
736
737def get_autotest_test_info(autotest_dir, test_name):
738 """Get metadata of given test.
739
740 Args:
741 autotest_dir: autotest folder
742 test_name: test name
743
744 Returns:
745 AutotestControlInfo object. None if test not found.
746 """
747 for control_file in enumerate_autotest_control_files(autotest_dir):
748 info = parse_autotest_control_file(control_file)
749 if info.name == test_name:
750 return info
751 return None
752
753
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800754class ChromeOSManifestManager(repo_util.ManifestManager):
755 """Manifest operations for chromeos repo"""
756
757 def __init__(self, config):
758 self.config = config
759
760 def _query_manifest_name(self, rev):
761 assert is_cros_full_version(rev)
762 milestone, short_version = version_split(rev)
763 manifest_name = os.path.join('buildspecs', milestone,
764 '%s.xml' % short_version)
765 return manifest_name
766
767 def sync_disk_state(self, rev):
768 manifest_name = self._query_manifest_name(rev)
769
770 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
771 # manifest. 'repo sync -m' is not enough
772 repo_util.init(
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800773 self.config['chromeos_root'],
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800774 'https://chrome-internal.googlesource.com/chromeos/manifest-versions/',
775 manifest_name=manifest_name,
776 repo_url='https://chromium.googlesource.com/external/repo.git')
777
778 # Note, don't sync with current_branch=True for chromeos. One of its
779 # build steps (inside mark_as_stable) executes "git describe" which
780 # needs git tag information.
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800781 repo_util.sync(self.config['chromeos_root'])
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800782
783 def fetch_git_repos(self, rev):
784 # Early return if necessary git history are already fetched.
785 mf = self.fetch_manifest(rev)
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800786 repo_set = repo_util.parse_repo_set(self.config['chromeos_root'], mf)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800787 all_have = True
788 for path, git_rev in repo_set.items():
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800789 git_root = os.path.join(self.config['chromeos_root'], path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800790 if not os.path.exists(git_root):
791 all_have = False
792 break
793 if not git_util.is_containing_commit(git_root, git_rev):
794 all_have = False
795 break
796 if all_have:
797 return
798
799 # TODO(kcwu): fetch git history but don't switch current disk state.
800 self.sync_disk_state(rev)
801
802 def enumerate_manifest(self, old, new):
803 assert is_cros_full_version(old)
804 assert is_cros_full_version(new)
805 old_milestone, old_short_version = version_split(old)
806 new_milestone, new_short_version = version_split(new)
807
808 # TODO(kcwu): fetch manifests but don't switch current disk state.
809 self.sync_disk_state(new)
810
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800811 spec_dir = os.path.join(self.config['chromeos_root'], '.repo', 'manifests',
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800812 'buildspecs')
813 result = []
814 for root, dirs, files in os.walk(spec_dir):
815 dirs[:] = [
816 dn for dn in dirs if dn.isdigit() and
817 int(old_milestone) <= int(dn) <= int(new_milestone)
818 ]
819
820 for fn in files:
821 short_version, ext = os.path.splitext(fn)
822 if ext != '.xml':
823 continue
824 milestone = os.path.basename(root)
825 if (util.is_version_lesseq(old_short_version, short_version) and
826 util.is_version_lesseq(short_version, new_short_version) and
827 util.is_direct_relative_version(short_version, new_short_version)):
828 result.append(make_cros_full_version(milestone, short_version))
829
830 def version_key_func(full_version):
831 _milestone, short_version = version_split(full_version)
832 return util.version_key_func(short_version)
833
834 result.sort(key=version_key_func)
835 assert result[0] == old
836 assert result[-1] == new
837 return result
838
839 def fetch_manifest(self, rev):
840 # The manifest is already synced, no need to fetch.
841 return self._query_manifest_name(rev)