blob: b77ac66ebbd5f63fde3b8ca5a6739ce940e5b531 [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 Wue4bae0b2018-07-19 12:10:14 +080024from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080025from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080026from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080027from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080028from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080029from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080030from bisect_kit import util
31
32logger = logging.getLogger(__name__)
33
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080034re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080035re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
36re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080037
38gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
39gs_release_path = (
40 'gs://chromeos-releases/{channel}-channel/{board}/{short_version}')
41
42# Assume gsutil is in PATH.
43gsutil_bin = 'gsutil'
44
Kuang-che Wub9705bd2018-06-28 17:59:18 +080045chromeos_root_inside_chroot = '/mnt/host/source'
46# relative to chromeos_root
47prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
48
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080049VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
50VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
51VERSION_KEY_MILESTONE = 'milestone'
52VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080053VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080054VERSION_KEY_ANDROID_BRANCH = 'android_branch'
55
56
Kuang-che Wu9890ce82018-07-07 15:14:10 +080057class NeedRecreateChrootException(Exception):
58 """Failed to build ChromeOS because of chroot mismatch or corruption"""
59
60
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080061def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080062 """Determines if `s` is chromeos short version.
63
64 This function doesn't accept version number of local build.
65 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080066 return bool(re.match(re_chromeos_short_version, s))
67
68
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080069def is_cros_localbuild_version(s):
70 """Determines if `s` is chromeos local build version."""
71 return bool(re.match(re_chromeos_localbuild_version, s))
72
73
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080074def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080075 """Determines if `s` is chromeos full version.
76
77 This function doesn't accept version number of local build.
78 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080079 return bool(re.match(re_chromeos_full_version, s))
80
81
82def is_cros_version(s):
83 """Determines if `s` is chromeos version (either short or full)"""
84 return is_cros_short_version(s) or is_cros_full_version(s)
85
86
87def make_cros_full_version(milestone, short_version):
88 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080089 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080090 return 'R%s-%s' % (milestone, short_version)
91
92
93def version_split(full_version):
94 """Splits full_version into milestone and short_version"""
95 assert is_cros_full_version(full_version)
96 milestone, short_version = full_version.split('-')
97 return milestone[1:], short_version
98
99
100def argtype_cros_version(s):
101 if not is_cros_version(s):
102 msg = 'invalid cros version'
103 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
104 return s
105
106
107def query_dut_lsb_release(host):
108 """Query /etc/lsb-release of given DUT
109
110 Args:
111 host: the DUT address
112
113 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800114 dict for keys and values of /etc/lsb-release.
115
116 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800117 errors.SshConnectionError: cannot connect to host
118 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800119 """
120 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800121 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800122 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800123 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800124 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
125
126
127def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800128 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800129
130 Args:
131 host: the DUT address
132
133 Returns:
134 True if the host is a chromeos device.
135 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800136 try:
137 return query_dut_lsb_release(host).get('DEVICETYPE') in [
138 'CHROMEBASE',
139 'CHROMEBIT',
140 'CHROMEBOOK',
141 'CHROMEBOX',
142 'REFERENCE',
143 ]
144 except (errors.ExternalError, errors.SshConnectionError):
145 return False
146
147
148def is_good_dut(host):
149 if not is_dut(host):
150 return False
151
152 # Sometimes python is broken after 'cros flash'.
153 try:
154 util.ssh_cmd(host, 'python', '-c', '1')
155 return True
156 except (subprocess.CalledProcessError, errors.SshConnectionError):
157 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800158
159
160def query_dut_board(host):
161 """Query board name of a given DUT"""
162 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
163
164
165def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800166 """Query short version of a given DUT.
167
168 This function may return version of local build, which
169 is_cros_short_version() is false.
170 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800171 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
172
173
174def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800175 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800176
177 Args:
178 host: DUT address
179 connect_timeout: connection timeout
180
181 Returns:
182 boot uuid
183 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800184 return util.ssh_cmd(
185 host,
186 'cat',
187 '/proc/sys/kernel/random/boot_id',
188 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800189
190
191def reboot(host):
192 """Reboot a DUT and verify"""
193 logger.debug('reboot %s', host)
194 boot_id = query_dut_boot_id(host)
195
Kuang-che Wu44278142019-03-04 11:33:57 +0800196 try:
197 util.ssh_cmd(host, 'reboot')
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800198 except errors.SshConnectionError:
199 # Depends on timing, ssh may return failure due to broken pipe, which is
200 # working as intended. Ignore such kind of errors.
Kuang-che Wu44278142019-03-04 11:33:57 +0800201 pass
Kuang-che Wu708310b2018-03-28 17:24:34 +0800202 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800203
Kuang-che Wu708310b2018-03-28 17:24:34 +0800204
205def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800206 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800207 # (dev screen short delay) or more (long delay).
208 time.sleep(15)
209 for _ in range(100):
210 try:
211 # During boot, DUT does not response and thus ssh may hang a while. So
212 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
213 # set tight limit because it's inside retry loop.
214 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
215 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800216 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800217 logger.debug('reboot not ready? sleep wait 1 sec')
218 time.sleep(1)
219
Kuang-che Wue121fae2018-11-09 16:18:39 +0800220 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800221
222
223def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800224 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800225
226 Args:
227 args: command line arguments passed to gsutil
228 kwargs:
229 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
230 but the path not found.
231
232 Returns:
233 stdout of gsutil
234
235 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800236 errors.InternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800237 subprocess.CalledProcessError: command failed
238 """
239 stderr_lines = []
240 try:
241 return util.check_output(
242 gsutil_bin, *args, stderr_callback=stderr_lines.append)
243 except subprocess.CalledProcessError as e:
244 stderr = ''.join(stderr_lines)
245 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800246 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800247 'gsutil failed due to permission. ' +
248 'Run "%s config" and follow its instruction. ' % gsutil_bin +
249 'Fill any string if it asks for project-id')
250 if kwargs.get('ignore_errors'):
251 return ''
252 raise
253 except OSError as e:
254 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800255 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800256 'Unable to run %s. gsutil is not installed or not in PATH?' %
257 gsutil_bin)
258 raise
259
260
261def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800262 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800263
264 Args:
265 args: arguments passed to 'gsutil ls'
266 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800267 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800268 exception, ex. path not found.
269
270 Returns:
271 list of 'gsutil ls' result. One element for one line of gsutil output.
272
273 Raises:
274 subprocess.CalledProcessError: gsutil failed, usually means path not found
275 """
276 return gsutil('ls', *args, **kwargs).splitlines()
277
278
279def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800280 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800281
282 Args:
283 board: ChromeOS board name
284 short_version: ChromeOS version number in short format, ex. 9300.0.0
285
286 Returns:
287 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
288 None if failed.
289 """
290 path = gs_archive_path.format(board=board) + '/R*-' + short_version
291 for line in gsutil_ls('-d', path, ignore_errors=True):
292 m = re.search(r'/R(\d+)-', line)
293 if not m:
294 continue
295 return m.group(1)
296
297 for channel in ['canary', 'dev', 'beta', 'stable']:
298 path = gs_release_path.format(
299 channel=channel, board=board, short_version=short_version)
300 for line in gsutil_ls(path, ignore_errors=True):
301 m = re.search(r'\bR(\d+)-' + short_version, line)
302 if not m:
303 continue
304 return m.group(1)
305
306 logger.error('unable to query milestone of %s for %s', short_version, board)
307 return None
308
309
310def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800311 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800312
313 Args:
314 board: ChromeOS board name
315 version: ChromeOS version number in short or full format
316
317 Returns:
318 (milestone, version in short format)
319 """
320 if is_cros_short_version(version):
321 milestone = query_milestone_by_version(board, version)
322 short_version = version
323 else:
324 milestone, short_version = version_split(version)
325 return milestone, short_version
326
327
328def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800329 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800330
331 Args:
332 version: ChromeOS version number in short or full format
333
334 Returns:
335 version number in short format
336 """
337 if is_cros_short_version(version):
338 return version
339 _, short_version = version_split(version)
340 return short_version
341
342
343def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800344 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800345
346 Args:
347 board: ChromeOS board name
348 version: ChromeOS version number in short or full format
349
350 Returns:
351 version number in full format
352 """
353 if is_cros_full_version(version):
354 return version
355 milestone = query_milestone_by_version(board, version)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800356 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800357 return make_cros_full_version(milestone, version)
358
359
Kuang-che Wu575dc442019-03-05 10:30:55 +0800360def list_prebuilt_from_image_archive(board):
361 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
362
363 gs://chromeos-image-archive contains only recent builds (in two years).
364 We prefer this function to list_prebuilt_from_chromeos_releases() because
365 - this is what "cros flash" supports directly.
366 - the paths have milestone information, so we don't need to do slow query
367 by ourselves.
368
369 Args:
370 board: ChromeOS board name
371
372 Returns:
373 list of (version, gs_path):
374 version: Chrome OS version in full format
375 gs_path: gs path of test image
376 """
377 result = []
378 for line in gsutil_ls(gs_archive_path.format(board=board)):
379 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
380 if m:
381 full_version = m.group(1)
382 test_image = 'chromiumos_test_image.tar.xz'
383 assert line.endswith('/')
384 gs_path = line + test_image
385 result.append((full_version, gs_path))
386 return result
387
388
389def list_prebuilt_from_chromeos_releases(board):
390 """Lists ChromeOS versions available from gs://chromeos-releases.
391
392 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
393 support it.
394
395 Args:
396 board: ChromeOS board name
397
398 Returns:
399 list of (version, gs_path):
400 version: Chrome OS version in short format
401 gs_path: gs path of test image (with wildcard)
402 """
403 result = []
404 for line in gsutil_ls(
405 gs_release_path.format(channel='*', board=board, short_version=''),
406 ignore_errors=True):
407 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
408 if m:
409 short_version = m.group(1)
410 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
411 short_version=short_version, board=board)
412 gs_path = line + test_image
413 result.append((short_version, gs_path))
414 return result
415
416
417def list_chromeos_prebuilt_versions(board,
418 old,
419 new,
420 only_good_build=True,
421 include_older_build=True):
422 """Lists ChromeOS version numbers with prebuilt between given range
423
424 Args:
425 board: ChromeOS board name
426 old: start version (inclusive)
427 new: end version (inclusive)
428 only_good_build: only if test image is available
429 include_older_build: include prebuilt in gs://chromeos-releases
430
431 Returns:
432 list of sorted version numbers (in full format) between [old, new] range
433 (inclusive).
434 """
435 old = version_to_short(old)
436 new = version_to_short(new)
437
438 rev_map = {} # dict: short version -> (short or full version, gs line)
439 for full_version, gs_path in list_prebuilt_from_image_archive(board):
440 short_version = version_to_short(full_version)
441 rev_map[short_version] = full_version, gs_path
442
443 if include_older_build and old not in rev_map:
444 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
445 if short_version not in rev_map:
446 rev_map[short_version] = short_version, gs_path
447
448 result = []
449 for rev in sorted(rev_map, key=util.version_key_func):
450 if not util.is_direct_relative_version(new, rev):
451 continue
452 if not util.is_version_lesseq(old, rev):
453 continue
454 if not util.is_version_lesseq(rev, new):
455 continue
456
457 version, gs_path = rev_map[rev]
458
459 # version_to_full() and gsutil_ls() may take long time if versions are a
460 # lot. This is acceptable because we usually bisect only short range.
461
462 if only_good_build:
463 gs_result = gsutil_ls(gs_path, ignore_errors=True)
464 if not gs_result:
465 logger.warning('%s is not a good build, ignore', version)
466 continue
467 assert len(gs_result) == 1
468 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
469 if not m:
470 logger.warning('format of image path is unexpected: %s', gs_result[0])
471 continue
472 version = m.group(1)
473 elif is_cros_short_version(version):
474 version = version_to_full(board, version)
475
476 result.append(version)
477
478 return result
479
480
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800481def prepare_prebuilt_image(board, version):
482 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800483
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800484 It searches for xbuddy image which "cros flash" can use, or fetch image to
485 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800486
487 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800488 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800489 version: ChromeOS version number in short or full format
490
491 Returns:
492 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800493 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800494 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800495 full_version = version_to_full(board, version)
496 short_version = version_to_short(full_version)
497
498 image_path = None
499 gs_path = gs_archive_path.format(board=board) + '/' + full_version
500 if gsutil_ls('-d', gs_path, ignore_errors=True):
501 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
502 board=board, full_version=full_version)
503 else:
504 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
505 if not os.path.exists(tmp_dir):
506 os.makedirs(tmp_dir)
507 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800508 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800509 # to fetch the image by ourselves
510 for channel in ['canary', 'dev', 'beta', 'stable']:
511 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
512 full_version=full_version, board=board)
513 gs_path = gs_release_path.format(
514 channel=channel, board=board, short_version=short_version)
515 gs_path += '/' + fn
516 if gsutil_ls(gs_path, ignore_errors=True):
517 # TODO(kcwu): delete tmp
518 gsutil('cp', gs_path, tmp_dir)
519 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
520 image_path = os.path.abspath(
521 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
522 break
523
524 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800525 return image_path
526
527
528def cros_flash(chromeos_root,
529 host,
530 board,
531 image_path,
532 version=None,
533 clobber_stateful=False,
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800534 disable_rootfs_verification=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800535 """Flash a DUT with given ChromeOS image.
536
537 This is implemented by 'cros flash' command line.
538
539 Args:
540 chromeos_root: use 'cros flash' of which chromeos tree
541 host: DUT address
542 board: ChromeOS board name
543 image_path: chromeos image xbuddy path or file path. If
544 run_inside_chroot is True, the file path is relative to src/scrips.
545 Otherwise, the file path is relative to chromeos_root.
546 version: ChromeOS version in short or full format
547 clobber_stateful: Clobber stateful partition when performing update
548 disable_rootfs_verification: Disable rootfs verification after update
549 is completed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800550 """
551 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
552
553 # Reboot is necessary because sometimes previous 'cros flash' failed and
554 # entered a bad state.
555 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800556
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800557 args = [
558 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
559 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800560 if clobber_stateful:
561 args.append('--clobber-stateful')
562 if disable_rootfs_verification:
563 args.append('--disable-rootfs-verification')
564
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800565 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800566
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800567 if version:
568 # In the past, cros flash may fail with returncode=0
569 # So let's have an extra check.
570 short_version = version_to_short(version)
571 dut_version = query_dut_short_version(host)
572 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800573
574
575def version_info(board, version):
576 """Query subcomponents version info of given version of ChromeOS
577
578 Args:
579 board: ChromeOS board name
580 version: ChromeOS version number in short or full format
581
582 Returns:
583 dict of component and version info, including (if available):
584 cros_short_version: ChromeOS version
585 cros_full_version: ChromeOS version
586 milestone: milestone of ChromeOS
587 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800588 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800589 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
590 """
591 info = {}
592 full_version = version_to_full(board, version)
593
594 # Some boards may have only partial-metadata.json but no metadata.json.
595 # e.g. caroline R60-9462.0.0
596 # Let's try both.
597 metadata = None
598 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
599 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
600 metadata_filename)
601 metadata = gsutil('cat', path, ignore_errors=True)
602 if metadata:
603 o = json.loads(metadata)
604 v = o['version']
605 board_metadata = o['board-metadata'][board]
606 info.update({
607 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
608 VERSION_KEY_CROS_FULL_VERSION: v['full'],
609 VERSION_KEY_MILESTONE: v['milestone'],
610 VERSION_KEY_CR_VERSION: v['chrome'],
611 })
612
613 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800614 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800615 if 'android-branch' in v: # this appears since R58-9317.0.0
616 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
617 elif 'android-container-branch' in board_metadata:
618 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
619 break
620 else:
621 logger.error('Failed to read metadata from gs://chromeos-image-archive')
622 logger.error(
623 'Note, so far no quick way to look up version info for too old builds')
624
625 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800626
627
628def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800629 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800630
631 Args:
632 board: ChromeOS board name
633 version: ChromeOS version number in short or full format
634
635 Returns:
636 Chrome version number
637 """
638 info = version_info(board, version)
639 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800640
641
642def query_android_build_id(board, rev):
643 info = version_info(board, rev)
644 rev = info['android_build_id']
645 return rev
646
647
648def query_android_branch(board, rev):
649 info = version_info(board, rev)
650 rev = info['android_branch']
651 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800652
653
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800654def guess_chrome_version(board, rev):
655 """Guess chrome version number.
656
657 Args:
658 board: chromeos board name
659 rev: chrome or chromeos version
660
661 Returns:
662 chrome version number
663 """
664 if is_cros_version(rev):
665 assert board, 'need to specify BOARD for cros version'
666 rev = query_chrome_version(board, rev)
667 assert cr_util.is_chrome_version(rev)
668
669 return rev
670
671
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800672def is_inside_chroot():
673 """Returns True if we are inside chroot."""
674 return os.path.exists('/etc/cros_chroot_version')
675
676
677def cros_sdk(chromeos_root, *args, **kwargs):
678 """Run commands inside chromeos chroot.
679
680 Args:
681 chromeos_root: chromeos tree root
682 *args: command to run
683 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +0800684 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800685 env: (dict) environment variables for the command
686 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800687 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800688 """
689 envs = []
690 for k, v in kwargs.get('env', {}).items():
691 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
692 envs.append('%s=%s' % (k, v))
693
694 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
695 # commands would be considered as background process.
Kuang-che Wud4603d72018-11-29 17:51:21 +0800696 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid']
697
698 if kwargs.get('chrome_root'):
699 cmd += ['--chrome_root', kwargs['chrome_root']]
700
701 cmd += envs + ['--'] + list(args)
702
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800703 return util.check_output(
704 *cmd,
705 cwd=chromeos_root,
706 stdin=kwargs.get('stdin'),
707 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800708
709
710def copy_into_chroot(chromeos_root, src, dst):
711 """Copies file into chromeos chroot.
712
713 Args:
714 chromeos_root: chromeos tree root
715 src: path outside chroot
716 dst: path inside chroot
717 """
718 # chroot may be an image, so we cannot copy to corresponding path
719 # directly.
720 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
721
722
723def exists_in_chroot(chromeos_root, path):
724 """Determine whether a path exists in the chroot.
725
726 Args:
727 chromeos_root: chromeos tree root
728 path: path inside chroot, relative to src/scripts
729
730 Returns:
731 True if a path exists
732 """
733 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800734 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800735 except subprocess.CalledProcessError:
736 return False
737 return True
738
739
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800740def check_if_need_recreate_chroot(stdout, stderr):
741 """Analyze build log and determine if chroot should be recreated.
742
743 Args:
744 stdout: stdout output of build
745 stderr: stderr output of build
746
747 Returns:
748 the reason if chroot needs recreated; None otherwise
749 """
Kuang-che Wu74768d32018-09-07 12:03:24 +0800750 if re.search(
751 r"The current version of portage supports EAPI '\d+'. "
752 "You must upgrade", stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800753 return 'EAPI version mismatch'
754
Kuang-che Wu5ac81322018-11-26 14:04:06 +0800755 if 'Chroot is too new. Consider running:' in stderr:
756 return 'chroot version is too new'
757
758 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800759 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
760 return 'chroot version is too new'
761
Kuang-che Wu6fe987f2018-08-28 15:24:20 +0800762 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
763 if "undefined reference to 'std::__1::basic_string" in stdout:
764 return 'might be due to compiler change'
765
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800766 return None
767
768
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800769def build_image(chromeos_root, board, rev):
770 """Build ChromeOS image.
771
772 Args:
773 chromeos_root: chromeos tree root
774 board: ChromeOS board name
775 rev: the version name to build
776
777 Returns:
778 Image path
779 """
780
781 # If the given version is already built, reuse it.
Kuang-che Wuf41599c2018-08-03 16:11:11 +0800782 image_name = 'bisect-%s' % rev.replace('/', '_')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800783 image_path = os.path.join('../build/images', board, image_name,
784 'chromiumos_test_image.bin')
785 if exists_in_chroot(chromeos_root, image_path):
786 logger.info('"%s" already exists, skip build step', image_path)
787 return image_path
788
789 dirname = os.path.dirname(os.path.abspath(__file__))
790 script_name = 'build_cros_helper.sh'
791 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
792 script_name)
793 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800794
795 stderr_lines = []
796 try:
Kuang-che Wufb553102018-10-02 18:14:29 +0800797 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
798 cros_sdk(
799 chromeos_root,
800 './%s' % script_name,
801 board,
802 image_name,
803 stderr_callback=stderr_lines.append)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800804 except subprocess.CalledProcessError as e:
805 # Detect failures due to incompatibility between chroot and source tree. If
806 # so, notify the caller to recreate chroot and retry.
807 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
808 if reason:
809 raise NeedRecreateChrootException(reason)
810
811 # For other failures, don't know how to handle. Just bail out.
812 raise
813
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800814 return image_path
815
816
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800817class AutotestControlInfo(object):
818 """Parsed content of autotest control file.
819
820 Attributes:
821 name: test name
822 path: control file path
823 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
824 DOC, ATTRIBUTES, DEPENDENCIES, etc.
825 """
826
827 def __init__(self, path, variables):
828 self.name = variables['NAME']
829 self.path = path
830 self.variables = variables
831
832
833def parse_autotest_control_file(path):
834 """Parses autotest control file.
835
836 This only parses simple top-level string assignments.
837
838 Returns:
839 AutotestControlInfo object
840 """
841 variables = {}
842 code = ast.parse(open(path).read())
843 for stmt in code.body:
844 # Skip if not simple "NAME = *" assignment.
845 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
846 isinstance(stmt.targets[0], ast.Name)):
847 continue
848
849 # Only support string value.
850 if isinstance(stmt.value, ast.Str):
851 variables[stmt.targets[0].id] = stmt.value.s
852
853 return AutotestControlInfo(path, variables)
854
855
856def enumerate_autotest_control_files(autotest_dir):
857 """Enumerate autotest control files.
858
859 Args:
860 autotest_dir: autotest folder
861
862 Returns:
863 list of paths to control files
864 """
865 # Where to find control files. Relative to autotest_dir.
866 subpaths = [
867 'server/site_tests',
868 'client/site_tests',
869 'server/tests',
870 'client/tests',
871 ]
872
873 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
874 result = []
875 for subpath in subpaths:
876 path = os.path.join(autotest_dir, subpath)
877 for root, dirs, files in os.walk(path):
878
879 for black in blacklist:
880 if black in dirs:
881 dirs.remove(black)
882
883 for filename in files:
884 if filename == 'control' or filename.startswith('control.'):
885 result.append(os.path.join(root, filename))
886
887 return result
888
889
890def get_autotest_test_info(autotest_dir, test_name):
891 """Get metadata of given test.
892
893 Args:
894 autotest_dir: autotest folder
895 test_name: test name
896
897 Returns:
898 AutotestControlInfo object. None if test not found.
899 """
900 for control_file in enumerate_autotest_control_files(autotest_dir):
901 info = parse_autotest_control_file(control_file)
902 if info.name == test_name:
903 return info
904 return None
905
906
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800907class ChromeOSSpecManager(codechange.SpecManager):
908 """Repo manifest related operations.
909
910 This class enumerates chromeos manifest files, parses them,
911 and sync to disk state according to them.
912 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800913
914 def __init__(self, config):
915 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800916 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
917 'manifests')
918 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800919 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800920 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800921 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
922 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800923
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800924 def lookup_build_timestamp(self, rev):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800925 assert is_cros_full_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800926
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800927 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800928 path = os.path.join('buildspecs', milestone, short_version + '.xml')
929 try:
930 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
931 'refs/heads/master', path)
932 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800933 raise errors.InternalError(
Kuang-che Wu74768d32018-09-07 12:03:24 +0800934 '%s does not have %s' % (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800935 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800936
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800937 def collect_float_spec(self, old, new):
938 old_timestamp = self.lookup_build_timestamp(old)
939 new_timestamp = self.lookup_build_timestamp(new)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800940
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800941 path = os.path.join(self.manifest_dir, 'default.xml')
942 if not os.path.islink(path) or os.readlink(path) != 'full.xml':
Kuang-che Wue121fae2018-11-09 16:18:39 +0800943 raise errors.InternalError(
944 'default.xml not symlink to full.xml is not supported')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800945
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800946 result = []
947 path = 'full.xml'
948 parser = repo_util.ManifestParser(self.manifest_dir)
949 for timestamp, git_rev in parser.enumerate_manifest_commits(
950 old_timestamp, new_timestamp, path):
951 result.append(
952 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
953 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800954
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800955 def collect_fixed_spec(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800956 assert is_cros_full_version(old)
957 assert is_cros_full_version(new)
958 old_milestone, old_short_version = version_split(old)
959 new_milestone, new_short_version = version_split(new)
960
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800961 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800962 for milestone in git_util.list_dir_from_revision(
963 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
964 if not milestone.isdigit():
965 continue
966 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
967 continue
968
Kuang-che Wu74768d32018-09-07 12:03:24 +0800969 files = git_util.list_dir_from_revision(
970 self.historical_manifest_git_dir, 'refs/heads/master',
971 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800972
973 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800974 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800975 short_version, ext = os.path.splitext(fn)
976 if ext != '.xml':
977 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800978 if (util.is_version_lesseq(old_short_version, short_version) and
979 util.is_version_lesseq(short_version, new_short_version) and
980 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800981 rev = make_cros_full_version(milestone, short_version)
982 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
983 'refs/heads/master', path)
984 result.append(
985 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800986
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800987 def version_key_func(spec):
988 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800989 return util.version_key_func(short_version)
990
991 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800992 assert result[0].name == old
993 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800994 return result
995
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800996 def get_manifest(self, rev):
997 assert is_cros_full_version(rev)
998 milestone, short_version = version_split(rev)
999 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
1000 manifest = git_util.get_file_from_revision(self.historical_manifest_git_dir,
1001 'refs/heads/master', path)
1002
1003 manifest_name = 'manifest_%s.xml' % rev
1004 manifest_path = os.path.join(self.manifest_dir, manifest_name)
1005 with open(manifest_path, 'w') as f:
1006 f.write(manifest)
1007
1008 return manifest_name
1009
1010 def parse_spec(self, spec):
1011 parser = repo_util.ManifestParser(self.manifest_dir)
1012 if spec.spec_type == codechange.SPEC_FIXED:
1013 manifest_name = self.get_manifest(spec.name)
1014 manifest_path = os.path.join(self.manifest_dir, manifest_name)
1015 content = open(manifest_path).read()
1016 root = parser.parse_single_xml(content, allow_include=False)
1017 else:
1018 root = parser.parse_xml_recursive(spec.name, spec.path)
1019
1020 spec.entries = parser.process_parsed_result(root)
1021 if spec.spec_type == codechange.SPEC_FIXED:
1022 assert spec.is_static()
1023
1024 def sync_disk_state(self, rev):
1025 manifest_name = self.get_manifest(rev)
1026
1027 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
1028 # manifest. 'repo sync -m' is not enough
1029 repo_util.init(
1030 self.config['chromeos_root'],
1031 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
1032 manifest_name=manifest_name,
1033 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001034 reference=self.config['chromeos_mirror'],
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001035 )
1036
1037 # Note, don't sync with current_branch=True for chromeos. One of its
1038 # build steps (inside mark_as_stable) executes "git describe" which
1039 # needs git tag information.
1040 repo_util.sync(self.config['chromeos_root'])