blob: 80c55f7f4c3555ccfd5ce3086482ce661dc4a780 [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 Wu2ea804f2017-11-28 17:11:41 +080025from bisect_kit import core
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080026from bisect_kit import cr_util
Kuang-che Wubfc4a642018-04-19 11:54:08 +080027from bisect_kit import git_util
28from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080029from bisect_kit import util
30
31logger = logging.getLogger(__name__)
32
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080033re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080034re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
35re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080036
37gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
38gs_release_path = (
39 'gs://chromeos-releases/{channel}-channel/{board}/{short_version}')
40
41# Assume gsutil is in PATH.
42gsutil_bin = 'gsutil'
43
Kuang-che Wub9705bd2018-06-28 17:59:18 +080044chromeos_root_inside_chroot = '/mnt/host/source'
45# relative to chromeos_root
46prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
47
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080048VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
49VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
50VERSION_KEY_MILESTONE = 'milestone'
51VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080052VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080053VERSION_KEY_ANDROID_BRANCH = 'android_branch'
54
55
Kuang-che Wu9890ce82018-07-07 15:14:10 +080056class NeedRecreateChrootException(Exception):
57 """Failed to build ChromeOS because of chroot mismatch or corruption"""
58
59
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080060def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080061 """Determines if `s` is chromeos short version.
62
63 This function doesn't accept version number of local build.
64 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080065 return bool(re.match(re_chromeos_short_version, s))
66
67
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080068def is_cros_localbuild_version(s):
69 """Determines if `s` is chromeos local build version."""
70 return bool(re.match(re_chromeos_localbuild_version, s))
71
72
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080073def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080074 """Determines if `s` is chromeos full version.
75
76 This function doesn't accept version number of local build.
77 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080078 return bool(re.match(re_chromeos_full_version, s))
79
80
81def is_cros_version(s):
82 """Determines if `s` is chromeos version (either short or full)"""
83 return is_cros_short_version(s) or is_cros_full_version(s)
84
85
86def make_cros_full_version(milestone, short_version):
87 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080088 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080089 return 'R%s-%s' % (milestone, short_version)
90
91
92def version_split(full_version):
93 """Splits full_version into milestone and short_version"""
94 assert is_cros_full_version(full_version)
95 milestone, short_version = full_version.split('-')
96 return milestone[1:], short_version
97
98
99def argtype_cros_version(s):
100 if not is_cros_version(s):
101 msg = 'invalid cros version'
102 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
103 return s
104
105
106def query_dut_lsb_release(host):
107 """Query /etc/lsb-release of given DUT
108
109 Args:
110 host: the DUT address
111
112 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800113 dict for keys and values of /etc/lsb-release.
114
115 Raises:
116 core.ExecutionFatalError: cannot connect to host or lsb-release file
117 doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800118 """
119 try:
120 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
121 except subprocess.CalledProcessError:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800122 raise core.ExecutionFatalError('cannot connect to DUT or not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800123 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
124
125
126def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800127 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800128
129 Args:
130 host: the DUT address
131
132 Returns:
133 True if the host is a chromeos device.
134 """
135 return query_dut_lsb_release(host).get('DEVICETYPE') in [
136 'CHROMEBASE',
137 'CHROMEBIT',
138 'CHROMEBOOK',
139 'CHROMEBOX',
140 'REFERENCE',
141 ]
142
143
144def query_dut_board(host):
145 """Query board name of a given DUT"""
146 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
147
148
149def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800150 """Query short version of a given DUT.
151
152 This function may return version of local build, which
153 is_cros_short_version() is false.
154 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800155 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
156
157
158def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800159 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800160
161 Args:
162 host: DUT address
163 connect_timeout: connection timeout
164
165 Returns:
166 boot uuid
167 """
168 cmd = ['ssh']
169 if connect_timeout:
170 cmd += ['-oConnectTimeout=%d' % connect_timeout]
171 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
172 return util.check_output(*cmd).strip()
173
174
175def reboot(host):
176 """Reboot a DUT and verify"""
177 logger.debug('reboot %s', host)
178 boot_id = query_dut_boot_id(host)
179
180 # Depends on timing, ssh may return failure due to broken pipe,
181 # so don't check ssh return code.
182 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800183 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800184
Kuang-che Wu708310b2018-03-28 17:24:34 +0800185
186def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800187 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800188 # (dev screen short delay) or more (long delay).
189 time.sleep(15)
190 for _ in range(100):
191 try:
192 # During boot, DUT does not response and thus ssh may hang a while. So
193 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
194 # set tight limit because it's inside retry loop.
195 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
196 return
197 except subprocess.CalledProcessError:
198 logger.debug('reboot not ready? sleep wait 1 sec')
199 time.sleep(1)
200
201 raise core.ExecutionFatalError('reboot failed?')
202
203
204def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800205 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800206
207 Args:
208 args: command line arguments passed to gsutil
209 kwargs:
210 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
211 but the path not found.
212
213 Returns:
214 stdout of gsutil
215
216 Raises:
217 core.ExecutionFatalError: gsutil failed to run
218 subprocess.CalledProcessError: command failed
219 """
220 stderr_lines = []
221 try:
222 return util.check_output(
223 gsutil_bin, *args, stderr_callback=stderr_lines.append)
224 except subprocess.CalledProcessError as e:
225 stderr = ''.join(stderr_lines)
226 if re.search(r'ServiceException:.* does not have .*access', stderr):
227 raise core.ExecutionFatalError(
228 'gsutil failed due to permission. ' +
229 'Run "%s config" and follow its instruction. ' % gsutil_bin +
230 'Fill any string if it asks for project-id')
231 if kwargs.get('ignore_errors'):
232 return ''
233 raise
234 except OSError as e:
235 if e.errno == errno.ENOENT:
236 raise core.ExecutionFatalError(
237 'Unable to run %s. gsutil is not installed or not in PATH?' %
238 gsutil_bin)
239 raise
240
241
242def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800243 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800244
245 Args:
246 args: arguments passed to 'gsutil ls'
247 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800248 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800249 exception, ex. path not found.
250
251 Returns:
252 list of 'gsutil ls' result. One element for one line of gsutil output.
253
254 Raises:
255 subprocess.CalledProcessError: gsutil failed, usually means path not found
256 """
257 return gsutil('ls', *args, **kwargs).splitlines()
258
259
260def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800261 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800262
263 Args:
264 board: ChromeOS board name
265 short_version: ChromeOS version number in short format, ex. 9300.0.0
266
267 Returns:
268 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
269 None if failed.
270 """
271 path = gs_archive_path.format(board=board) + '/R*-' + short_version
272 for line in gsutil_ls('-d', path, ignore_errors=True):
273 m = re.search(r'/R(\d+)-', line)
274 if not m:
275 continue
276 return m.group(1)
277
278 for channel in ['canary', 'dev', 'beta', 'stable']:
279 path = gs_release_path.format(
280 channel=channel, board=board, short_version=short_version)
281 for line in gsutil_ls(path, ignore_errors=True):
282 m = re.search(r'\bR(\d+)-' + short_version, line)
283 if not m:
284 continue
285 return m.group(1)
286
287 logger.error('unable to query milestone of %s for %s', short_version, board)
288 return None
289
290
291def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800292 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800293
294 Args:
295 board: ChromeOS board name
296 version: ChromeOS version number in short or full format
297
298 Returns:
299 (milestone, version in short format)
300 """
301 if is_cros_short_version(version):
302 milestone = query_milestone_by_version(board, version)
303 short_version = version
304 else:
305 milestone, short_version = version_split(version)
306 return milestone, short_version
307
308
309def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800310 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800311
312 Args:
313 version: ChromeOS version number in short or full format
314
315 Returns:
316 version number in short format
317 """
318 if is_cros_short_version(version):
319 return version
320 _, short_version = version_split(version)
321 return short_version
322
323
324def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800325 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800326
327 Args:
328 board: ChromeOS board name
329 version: ChromeOS version number in short or full format
330
331 Returns:
332 version number in full format
333 """
334 if is_cros_full_version(version):
335 return version
336 milestone = query_milestone_by_version(board, version)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800337 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800338 return make_cros_full_version(milestone, version)
339
340
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800341def prepare_prebuilt_image(board, version):
342 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800343
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800344 It searches for xbuddy image which "cros flash" can use, or fetch image to
345 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800346
347 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800348 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800349 version: ChromeOS version number in short or full format
350
351 Returns:
352 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800353 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800354 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800355 full_version = version_to_full(board, version)
356 short_version = version_to_short(full_version)
357
358 image_path = None
359 gs_path = gs_archive_path.format(board=board) + '/' + full_version
360 if gsutil_ls('-d', gs_path, ignore_errors=True):
361 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
362 board=board, full_version=full_version)
363 else:
364 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
365 if not os.path.exists(tmp_dir):
366 os.makedirs(tmp_dir)
367 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800368 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800369 # to fetch the image by ourselves
370 for channel in ['canary', 'dev', 'beta', 'stable']:
371 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
372 full_version=full_version, board=board)
373 gs_path = gs_release_path.format(
374 channel=channel, board=board, short_version=short_version)
375 gs_path += '/' + fn
376 if gsutil_ls(gs_path, ignore_errors=True):
377 # TODO(kcwu): delete tmp
378 gsutil('cp', gs_path, tmp_dir)
379 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
380 image_path = os.path.abspath(
381 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
382 break
383
384 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800385 return image_path
386
387
388def cros_flash(chromeos_root,
389 host,
390 board,
391 image_path,
392 version=None,
393 clobber_stateful=False,
394 disable_rootfs_verification=True,
395 run_inside_chroot=False):
396 """Flash a DUT with given ChromeOS image.
397
398 This is implemented by 'cros flash' command line.
399
400 Args:
401 chromeos_root: use 'cros flash' of which chromeos tree
402 host: DUT address
403 board: ChromeOS board name
404 image_path: chromeos image xbuddy path or file path. If
405 run_inside_chroot is True, the file path is relative to src/scrips.
406 Otherwise, the file path is relative to chromeos_root.
407 version: ChromeOS version in short or full format
408 clobber_stateful: Clobber stateful partition when performing update
409 disable_rootfs_verification: Disable rootfs verification after update
410 is completed
411 run_inside_chroot: if True, run 'cros flash' command inside the chroot
412 """
413 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
414
415 # Reboot is necessary because sometimes previous 'cros flash' failed and
416 # entered a bad state.
417 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800418
419 args = ['--no-ping', host, image_path]
420 if clobber_stateful:
421 args.append('--clobber-stateful')
422 if disable_rootfs_verification:
423 args.append('--disable-rootfs-verification')
424
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425 if run_inside_chroot:
426 cros_sdk(chromeos_root, 'cros', 'flash', *args)
427 else:
428 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800429
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800430 if version:
431 # In the past, cros flash may fail with returncode=0
432 # So let's have an extra check.
433 short_version = version_to_short(version)
434 dut_version = query_dut_short_version(host)
435 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800436
437
438def version_info(board, version):
439 """Query subcomponents version info of given version of ChromeOS
440
441 Args:
442 board: ChromeOS board name
443 version: ChromeOS version number in short or full format
444
445 Returns:
446 dict of component and version info, including (if available):
447 cros_short_version: ChromeOS version
448 cros_full_version: ChromeOS version
449 milestone: milestone of ChromeOS
450 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800451 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800452 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
453 """
454 info = {}
455 full_version = version_to_full(board, version)
456
457 # Some boards may have only partial-metadata.json but no metadata.json.
458 # e.g. caroline R60-9462.0.0
459 # Let's try both.
460 metadata = None
461 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
462 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
463 metadata_filename)
464 metadata = gsutil('cat', path, ignore_errors=True)
465 if metadata:
466 o = json.loads(metadata)
467 v = o['version']
468 board_metadata = o['board-metadata'][board]
469 info.update({
470 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
471 VERSION_KEY_CROS_FULL_VERSION: v['full'],
472 VERSION_KEY_MILESTONE: v['milestone'],
473 VERSION_KEY_CR_VERSION: v['chrome'],
474 })
475
476 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800477 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800478 if 'android-branch' in v: # this appears since R58-9317.0.0
479 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
480 elif 'android-container-branch' in board_metadata:
481 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
482 break
483 else:
484 logger.error('Failed to read metadata from gs://chromeos-image-archive')
485 logger.error(
486 'Note, so far no quick way to look up version info for too old builds')
487
488 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800489
490
491def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800492 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800493
494 Args:
495 board: ChromeOS board name
496 version: ChromeOS version number in short or full format
497
498 Returns:
499 Chrome version number
500 """
501 info = version_info(board, version)
502 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800503
504
505def query_android_build_id(board, rev):
506 info = version_info(board, rev)
507 rev = info['android_build_id']
508 return rev
509
510
511def query_android_branch(board, rev):
512 info = version_info(board, rev)
513 rev = info['android_branch']
514 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800515
516
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800517def guess_chrome_version(board, rev):
518 """Guess chrome version number.
519
520 Args:
521 board: chromeos board name
522 rev: chrome or chromeos version
523
524 Returns:
525 chrome version number
526 """
527 if is_cros_version(rev):
528 assert board, 'need to specify BOARD for cros version'
529 rev = query_chrome_version(board, rev)
530 assert cr_util.is_chrome_version(rev)
531
532 return rev
533
534
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800535def is_inside_chroot():
536 """Returns True if we are inside chroot."""
537 return os.path.exists('/etc/cros_chroot_version')
538
539
540def cros_sdk(chromeos_root, *args, **kwargs):
541 """Run commands inside chromeos chroot.
542
543 Args:
544 chromeos_root: chromeos tree root
545 *args: command to run
546 **kwargs:
547 env: (dict) environment variables for the command
548 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800549 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800550 """
551 envs = []
552 for k, v in kwargs.get('env', {}).items():
553 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
554 envs.append('%s=%s' % (k, v))
555
556 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
557 # commands would be considered as background process.
558 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid'] + envs + ['--'] + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800559 return util.check_output(
560 *cmd,
561 cwd=chromeos_root,
562 stdin=kwargs.get('stdin'),
563 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800564
565
566def copy_into_chroot(chromeos_root, src, dst):
567 """Copies file into chromeos chroot.
568
569 Args:
570 chromeos_root: chromeos tree root
571 src: path outside chroot
572 dst: path inside chroot
573 """
574 # chroot may be an image, so we cannot copy to corresponding path
575 # directly.
576 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
577
578
579def exists_in_chroot(chromeos_root, path):
580 """Determine whether a path exists in the chroot.
581
582 Args:
583 chromeos_root: chromeos tree root
584 path: path inside chroot, relative to src/scripts
585
586 Returns:
587 True if a path exists
588 """
589 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800590 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800591 except subprocess.CalledProcessError:
592 return False
593 return True
594
595
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800596def check_if_need_recreate_chroot(stdout, stderr):
597 """Analyze build log and determine if chroot should be recreated.
598
599 Args:
600 stdout: stdout output of build
601 stderr: stderr output of build
602
603 Returns:
604 the reason if chroot needs recreated; None otherwise
605 """
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800606 if re.search(r"The current version of portage supports EAPI '\d+'. "
607 "You must upgrade", stderr):
608 return 'EAPI version mismatch'
609
610 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
611 return 'chroot version is too new'
612
Kuang-che Wu6fe987f2018-08-28 15:24:20 +0800613 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
614 if "undefined reference to 'std::__1::basic_string" in stdout:
615 return 'might be due to compiler change'
616
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800617 return None
618
619
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800620def build_image(chromeos_root, board, rev):
621 """Build ChromeOS image.
622
623 Args:
624 chromeos_root: chromeos tree root
625 board: ChromeOS board name
626 rev: the version name to build
627
628 Returns:
629 Image path
630 """
631
632 # If the given version is already built, reuse it.
Kuang-che Wuf41599c2018-08-03 16:11:11 +0800633 image_name = 'bisect-%s' % rev.replace('/', '_')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800634 image_path = os.path.join('../build/images', board, image_name,
635 'chromiumos_test_image.bin')
636 if exists_in_chroot(chromeos_root, image_path):
637 logger.info('"%s" already exists, skip build step', image_path)
638 return image_path
639
640 dirname = os.path.dirname(os.path.abspath(__file__))
641 script_name = 'build_cros_helper.sh'
642 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
643 script_name)
644 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800645
646 stderr_lines = []
647 try:
648 cros_sdk(
649 chromeos_root,
650 './%s' % script_name,
651 board,
652 image_name,
653 stderr_callback=stderr_lines.append)
654 except subprocess.CalledProcessError as e:
655 # Detect failures due to incompatibility between chroot and source tree. If
656 # so, notify the caller to recreate chroot and retry.
657 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
658 if reason:
659 raise NeedRecreateChrootException(reason)
660
661 # For other failures, don't know how to handle. Just bail out.
662 raise
663
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800664 return image_path
665
666
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800667class AutotestControlInfo(object):
668 """Parsed content of autotest control file.
669
670 Attributes:
671 name: test name
672 path: control file path
673 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
674 DOC, ATTRIBUTES, DEPENDENCIES, etc.
675 """
676
677 def __init__(self, path, variables):
678 self.name = variables['NAME']
679 self.path = path
680 self.variables = variables
681
682
683def parse_autotest_control_file(path):
684 """Parses autotest control file.
685
686 This only parses simple top-level string assignments.
687
688 Returns:
689 AutotestControlInfo object
690 """
691 variables = {}
692 code = ast.parse(open(path).read())
693 for stmt in code.body:
694 # Skip if not simple "NAME = *" assignment.
695 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
696 isinstance(stmt.targets[0], ast.Name)):
697 continue
698
699 # Only support string value.
700 if isinstance(stmt.value, ast.Str):
701 variables[stmt.targets[0].id] = stmt.value.s
702
703 return AutotestControlInfo(path, variables)
704
705
706def enumerate_autotest_control_files(autotest_dir):
707 """Enumerate autotest control files.
708
709 Args:
710 autotest_dir: autotest folder
711
712 Returns:
713 list of paths to control files
714 """
715 # Where to find control files. Relative to autotest_dir.
716 subpaths = [
717 'server/site_tests',
718 'client/site_tests',
719 'server/tests',
720 'client/tests',
721 ]
722
723 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
724 result = []
725 for subpath in subpaths:
726 path = os.path.join(autotest_dir, subpath)
727 for root, dirs, files in os.walk(path):
728
729 for black in blacklist:
730 if black in dirs:
731 dirs.remove(black)
732
733 for filename in files:
734 if filename == 'control' or filename.startswith('control.'):
735 result.append(os.path.join(root, filename))
736
737 return result
738
739
740def get_autotest_test_info(autotest_dir, test_name):
741 """Get metadata of given test.
742
743 Args:
744 autotest_dir: autotest folder
745 test_name: test name
746
747 Returns:
748 AutotestControlInfo object. None if test not found.
749 """
750 for control_file in enumerate_autotest_control_files(autotest_dir):
751 info = parse_autotest_control_file(control_file)
752 if info.name == test_name:
753 return info
754 return None
755
756
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800757class ChromeOSSpecManager(codechange.SpecManager):
758 """Repo manifest related operations.
759
760 This class enumerates chromeos manifest files, parses them,
761 and sync to disk state according to them.
762 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800763
764 def __init__(self, config):
765 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800766 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
767 'manifests')
768 self.historical_manifest_git_dir = os.path.join(
769 self.config['chromeos_repo_mirror_dir'],
770 'chromeos/manifest-versions.git')
771 if not os.path.exists(self.historical_manifest_git_dir):
772 raise core.ExecutionFatalError(
773 'Manifest snapshots should be cloned into %s' %
774 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800775
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800776 def lookup_build_timestamp(self, rev):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800777 assert is_cros_full_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800778
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800779 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800780 path = os.path.join('buildspecs', milestone, short_version + '.xml')
781 try:
782 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
783 'refs/heads/master', path)
784 except ValueError:
785 raise core.ExecutionFatalError('%s does not have %s' %
786 (self.historical_manifest_git_dir, path))
787 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800788
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800789 def collect_float_spec(self, old, new):
790 old_timestamp = self.lookup_build_timestamp(old)
791 new_timestamp = self.lookup_build_timestamp(new)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800792
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800793 path = os.path.join(self.manifest_dir, 'default.xml')
794 if not os.path.islink(path) or os.readlink(path) != 'full.xml':
795 raise Exception('default.xml not symlink to full.xml is not supported')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800796
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800797 result = []
798 path = 'full.xml'
799 parser = repo_util.ManifestParser(self.manifest_dir)
800 for timestamp, git_rev in parser.enumerate_manifest_commits(
801 old_timestamp, new_timestamp, path):
802 result.append(
803 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
804 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800805
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800806 def collect_fixed_spec(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800807 assert is_cros_full_version(old)
808 assert is_cros_full_version(new)
809 old_milestone, old_short_version = version_split(old)
810 new_milestone, new_short_version = version_split(new)
811
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800812 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800813 for milestone in git_util.list_dir_from_revision(
814 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
815 if not milestone.isdigit():
816 continue
817 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
818 continue
819
820 files = git_util.list_dir_from_revision(self.historical_manifest_git_dir,
821 'refs/heads/master',
822 os.path.join(
823 'buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800824
825 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800826 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800827 short_version, ext = os.path.splitext(fn)
828 if ext != '.xml':
829 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800830 if (util.is_version_lesseq(old_short_version, short_version) and
831 util.is_version_lesseq(short_version, new_short_version) and
832 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800833 rev = make_cros_full_version(milestone, short_version)
834 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
835 'refs/heads/master', path)
836 result.append(
837 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800838
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800839 def version_key_func(spec):
840 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800841 return util.version_key_func(short_version)
842
843 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800844 assert result[0].name == old
845 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800846 return result
847
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800848 def get_manifest(self, rev):
849 assert is_cros_full_version(rev)
850 milestone, short_version = version_split(rev)
851 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
852 manifest = git_util.get_file_from_revision(self.historical_manifest_git_dir,
853 'refs/heads/master', path)
854
855 manifest_name = 'manifest_%s.xml' % rev
856 manifest_path = os.path.join(self.manifest_dir, manifest_name)
857 with open(manifest_path, 'w') as f:
858 f.write(manifest)
859
860 return manifest_name
861
862 def parse_spec(self, spec):
863 parser = repo_util.ManifestParser(self.manifest_dir)
864 if spec.spec_type == codechange.SPEC_FIXED:
865 manifest_name = self.get_manifest(spec.name)
866 manifest_path = os.path.join(self.manifest_dir, manifest_name)
867 content = open(manifest_path).read()
868 root = parser.parse_single_xml(content, allow_include=False)
869 else:
870 root = parser.parse_xml_recursive(spec.name, spec.path)
871
872 spec.entries = parser.process_parsed_result(root)
873 if spec.spec_type == codechange.SPEC_FIXED:
874 assert spec.is_static()
875
876 def sync_disk_state(self, rev):
877 manifest_name = self.get_manifest(rev)
878
879 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
880 # manifest. 'repo sync -m' is not enough
881 repo_util.init(
882 self.config['chromeos_root'],
883 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
884 manifest_name=manifest_name,
885 repo_url='https://chromium.googlesource.com/external/repo.git',
886 reference=self.config['chromeos_repo_mirror_dir'],
887 )
888
889 # Note, don't sync with current_branch=True for chromeos. One of its
890 # build steps (inside mark_as_stable) executes "git describe" which
891 # needs git tag information.
892 repo_util.sync(self.config['chromeos_root'])