blob: 071f0d6e69d2d4ec088197c53a16d008f0061808 [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
55def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080056 """Determines if `s` is chromeos short version.
57
58 This function doesn't accept version number of local build.
59 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080060 return bool(re.match(re_chromeos_short_version, s))
61
62
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080063def is_cros_localbuild_version(s):
64 """Determines if `s` is chromeos local build version."""
65 return bool(re.match(re_chromeos_localbuild_version, s))
66
67
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080068def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080069 """Determines if `s` is chromeos full version.
70
71 This function doesn't accept version number of local build.
72 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080073 return bool(re.match(re_chromeos_full_version, s))
74
75
76def is_cros_version(s):
77 """Determines if `s` is chromeos version (either short or full)"""
78 return is_cros_short_version(s) or is_cros_full_version(s)
79
80
81def make_cros_full_version(milestone, short_version):
82 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080083 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080084 return 'R%s-%s' % (milestone, short_version)
85
86
87def version_split(full_version):
88 """Splits full_version into milestone and short_version"""
89 assert is_cros_full_version(full_version)
90 milestone, short_version = full_version.split('-')
91 return milestone[1:], short_version
92
93
94def argtype_cros_version(s):
95 if not is_cros_version(s):
96 msg = 'invalid cros version'
97 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
98 return s
99
100
101def query_dut_lsb_release(host):
102 """Query /etc/lsb-release of given DUT
103
104 Args:
105 host: the DUT address
106
107 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800108 dict for keys and values of /etc/lsb-release.
109
110 Raises:
111 core.ExecutionFatalError: cannot connect to host or lsb-release file
112 doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800113 """
114 try:
115 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
116 except subprocess.CalledProcessError:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800117 raise core.ExecutionFatalError('cannot connect to DUT or not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800118 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
119
120
121def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800122 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800123
124 Args:
125 host: the DUT address
126
127 Returns:
128 True if the host is a chromeos device.
129 """
130 return query_dut_lsb_release(host).get('DEVICETYPE') in [
131 'CHROMEBASE',
132 'CHROMEBIT',
133 'CHROMEBOOK',
134 'CHROMEBOX',
135 'REFERENCE',
136 ]
137
138
139def query_dut_board(host):
140 """Query board name of a given DUT"""
141 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
142
143
144def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800145 """Query short version of a given DUT.
146
147 This function may return version of local build, which
148 is_cros_short_version() is false.
149 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800150 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
151
152
153def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800154 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800155
156 Args:
157 host: DUT address
158 connect_timeout: connection timeout
159
160 Returns:
161 boot uuid
162 """
163 cmd = ['ssh']
164 if connect_timeout:
165 cmd += ['-oConnectTimeout=%d' % connect_timeout]
166 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
167 return util.check_output(*cmd).strip()
168
169
170def reboot(host):
171 """Reboot a DUT and verify"""
172 logger.debug('reboot %s', host)
173 boot_id = query_dut_boot_id(host)
174
175 # Depends on timing, ssh may return failure due to broken pipe,
176 # so don't check ssh return code.
177 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800178 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800179
Kuang-che Wu708310b2018-03-28 17:24:34 +0800180
181def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800182 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800183 # (dev screen short delay) or more (long delay).
184 time.sleep(15)
185 for _ in range(100):
186 try:
187 # During boot, DUT does not response and thus ssh may hang a while. So
188 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
189 # set tight limit because it's inside retry loop.
190 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
191 return
192 except subprocess.CalledProcessError:
193 logger.debug('reboot not ready? sleep wait 1 sec')
194 time.sleep(1)
195
196 raise core.ExecutionFatalError('reboot failed?')
197
198
199def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800200 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800201
202 Args:
203 args: command line arguments passed to gsutil
204 kwargs:
205 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
206 but the path not found.
207
208 Returns:
209 stdout of gsutil
210
211 Raises:
212 core.ExecutionFatalError: gsutil failed to run
213 subprocess.CalledProcessError: command failed
214 """
215 stderr_lines = []
216 try:
217 return util.check_output(
218 gsutil_bin, *args, stderr_callback=stderr_lines.append)
219 except subprocess.CalledProcessError as e:
220 stderr = ''.join(stderr_lines)
221 if re.search(r'ServiceException:.* does not have .*access', stderr):
222 raise core.ExecutionFatalError(
223 'gsutil failed due to permission. ' +
224 'Run "%s config" and follow its instruction. ' % gsutil_bin +
225 'Fill any string if it asks for project-id')
226 if kwargs.get('ignore_errors'):
227 return ''
228 raise
229 except OSError as e:
230 if e.errno == errno.ENOENT:
231 raise core.ExecutionFatalError(
232 'Unable to run %s. gsutil is not installed or not in PATH?' %
233 gsutil_bin)
234 raise
235
236
237def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800238 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800239
240 Args:
241 args: arguments passed to 'gsutil ls'
242 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800243 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800244 exception, ex. path not found.
245
246 Returns:
247 list of 'gsutil ls' result. One element for one line of gsutil output.
248
249 Raises:
250 subprocess.CalledProcessError: gsutil failed, usually means path not found
251 """
252 return gsutil('ls', *args, **kwargs).splitlines()
253
254
255def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800256 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800257
258 Args:
259 board: ChromeOS board name
260 short_version: ChromeOS version number in short format, ex. 9300.0.0
261
262 Returns:
263 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
264 None if failed.
265 """
266 path = gs_archive_path.format(board=board) + '/R*-' + short_version
267 for line in gsutil_ls('-d', path, ignore_errors=True):
268 m = re.search(r'/R(\d+)-', line)
269 if not m:
270 continue
271 return m.group(1)
272
273 for channel in ['canary', 'dev', 'beta', 'stable']:
274 path = gs_release_path.format(
275 channel=channel, board=board, short_version=short_version)
276 for line in gsutil_ls(path, ignore_errors=True):
277 m = re.search(r'\bR(\d+)-' + short_version, line)
278 if not m:
279 continue
280 return m.group(1)
281
282 logger.error('unable to query milestone of %s for %s', short_version, board)
283 return None
284
285
286def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800287 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800288
289 Args:
290 board: ChromeOS board name
291 version: ChromeOS version number in short or full format
292
293 Returns:
294 (milestone, version in short format)
295 """
296 if is_cros_short_version(version):
297 milestone = query_milestone_by_version(board, version)
298 short_version = version
299 else:
300 milestone, short_version = version_split(version)
301 return milestone, short_version
302
303
304def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800305 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800306
307 Args:
308 version: ChromeOS version number in short or full format
309
310 Returns:
311 version number in short format
312 """
313 if is_cros_short_version(version):
314 return version
315 _, short_version = version_split(version)
316 return short_version
317
318
319def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800320 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800321
322 Args:
323 board: ChromeOS board name
324 version: ChromeOS version number in short or full format
325
326 Returns:
327 version number in full format
328 """
329 if is_cros_full_version(version):
330 return version
331 milestone = query_milestone_by_version(board, version)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800332 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800333 return make_cros_full_version(milestone, version)
334
335
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800336def prepare_prebuilt_image(board, version):
337 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800338
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800339 It searches for xbuddy image which "cros flash" can use, or fetch image to
340 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800341
342 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800343 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800344 version: ChromeOS version number in short or full format
345
346 Returns:
347 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800348 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800349 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800350 full_version = version_to_full(board, version)
351 short_version = version_to_short(full_version)
352
353 image_path = None
354 gs_path = gs_archive_path.format(board=board) + '/' + full_version
355 if gsutil_ls('-d', gs_path, ignore_errors=True):
356 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
357 board=board, full_version=full_version)
358 else:
359 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
360 if not os.path.exists(tmp_dir):
361 os.makedirs(tmp_dir)
362 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800363 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800364 # to fetch the image by ourselves
365 for channel in ['canary', 'dev', 'beta', 'stable']:
366 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
367 full_version=full_version, board=board)
368 gs_path = gs_release_path.format(
369 channel=channel, board=board, short_version=short_version)
370 gs_path += '/' + fn
371 if gsutil_ls(gs_path, ignore_errors=True):
372 # TODO(kcwu): delete tmp
373 gsutil('cp', gs_path, tmp_dir)
374 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
375 image_path = os.path.abspath(
376 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
377 break
378
379 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800380 return image_path
381
382
383def cros_flash(chromeos_root,
384 host,
385 board,
386 image_path,
387 version=None,
388 clobber_stateful=False,
389 disable_rootfs_verification=True,
390 run_inside_chroot=False):
391 """Flash a DUT with given ChromeOS image.
392
393 This is implemented by 'cros flash' command line.
394
395 Args:
396 chromeos_root: use 'cros flash' of which chromeos tree
397 host: DUT address
398 board: ChromeOS board name
399 image_path: chromeos image xbuddy path or file path. If
400 run_inside_chroot is True, the file path is relative to src/scrips.
401 Otherwise, the file path is relative to chromeos_root.
402 version: ChromeOS version in short or full format
403 clobber_stateful: Clobber stateful partition when performing update
404 disable_rootfs_verification: Disable rootfs verification after update
405 is completed
406 run_inside_chroot: if True, run 'cros flash' command inside the chroot
407 """
408 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
409
410 # Reboot is necessary because sometimes previous 'cros flash' failed and
411 # entered a bad state.
412 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800413
414 args = ['--no-ping', host, image_path]
415 if clobber_stateful:
416 args.append('--clobber-stateful')
417 if disable_rootfs_verification:
418 args.append('--disable-rootfs-verification')
419
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800420 if run_inside_chroot:
421 cros_sdk(chromeos_root, 'cros', 'flash', *args)
422 else:
423 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800424
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425 if version:
426 # In the past, cros flash may fail with returncode=0
427 # So let's have an extra check.
428 short_version = version_to_short(version)
429 dut_version = query_dut_short_version(host)
430 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800431
432
433def version_info(board, version):
434 """Query subcomponents version info of given version of ChromeOS
435
436 Args:
437 board: ChromeOS board name
438 version: ChromeOS version number in short or full format
439
440 Returns:
441 dict of component and version info, including (if available):
442 cros_short_version: ChromeOS version
443 cros_full_version: ChromeOS version
444 milestone: milestone of ChromeOS
445 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800446 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800447 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
448 """
449 info = {}
450 full_version = version_to_full(board, version)
451
452 # Some boards may have only partial-metadata.json but no metadata.json.
453 # e.g. caroline R60-9462.0.0
454 # Let's try both.
455 metadata = None
456 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
457 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
458 metadata_filename)
459 metadata = gsutil('cat', path, ignore_errors=True)
460 if metadata:
461 o = json.loads(metadata)
462 v = o['version']
463 board_metadata = o['board-metadata'][board]
464 info.update({
465 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
466 VERSION_KEY_CROS_FULL_VERSION: v['full'],
467 VERSION_KEY_MILESTONE: v['milestone'],
468 VERSION_KEY_CR_VERSION: v['chrome'],
469 })
470
471 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800472 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800473 if 'android-branch' in v: # this appears since R58-9317.0.0
474 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
475 elif 'android-container-branch' in board_metadata:
476 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
477 break
478 else:
479 logger.error('Failed to read metadata from gs://chromeos-image-archive')
480 logger.error(
481 'Note, so far no quick way to look up version info for too old builds')
482
483 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800484
485
486def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800487 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800488
489 Args:
490 board: ChromeOS board name
491 version: ChromeOS version number in short or full format
492
493 Returns:
494 Chrome version number
495 """
496 info = version_info(board, version)
497 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800498
499
500def query_android_build_id(board, rev):
501 info = version_info(board, rev)
502 rev = info['android_build_id']
503 return rev
504
505
506def query_android_branch(board, rev):
507 info = version_info(board, rev)
508 rev = info['android_branch']
509 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800510
511
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800512def guess_chrome_version(board, rev):
513 """Guess chrome version number.
514
515 Args:
516 board: chromeos board name
517 rev: chrome or chromeos version
518
519 Returns:
520 chrome version number
521 """
522 if is_cros_version(rev):
523 assert board, 'need to specify BOARD for cros version'
524 rev = query_chrome_version(board, rev)
525 assert cr_util.is_chrome_version(rev)
526
527 return rev
528
529
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800530def is_inside_chroot():
531 """Returns True if we are inside chroot."""
532 return os.path.exists('/etc/cros_chroot_version')
533
534
535def cros_sdk(chromeos_root, *args, **kwargs):
536 """Run commands inside chromeos chroot.
537
538 Args:
539 chromeos_root: chromeos tree root
540 *args: command to run
541 **kwargs:
542 env: (dict) environment variables for the command
543 stdin: standard input file handle for the command
544 """
545 envs = []
546 for k, v in kwargs.get('env', {}).items():
547 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
548 envs.append('%s=%s' % (k, v))
549
550 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
551 # commands would be considered as background process.
552 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid'] + envs + ['--'] + list(args)
553 return util.check_output(*cmd, cwd=chromeos_root, stdin=kwargs.get('stdin'))
554
555
556def copy_into_chroot(chromeos_root, src, dst):
557 """Copies file into chromeos chroot.
558
559 Args:
560 chromeos_root: chromeos tree root
561 src: path outside chroot
562 dst: path inside chroot
563 """
564 # chroot may be an image, so we cannot copy to corresponding path
565 # directly.
566 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
567
568
569def exists_in_chroot(chromeos_root, path):
570 """Determine whether a path exists in the chroot.
571
572 Args:
573 chromeos_root: chromeos tree root
574 path: path inside chroot, relative to src/scripts
575
576 Returns:
577 True if a path exists
578 """
579 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800580 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800581 except subprocess.CalledProcessError:
582 return False
583 return True
584
585
586def build_image(chromeos_root, board, rev):
587 """Build ChromeOS image.
588
589 Args:
590 chromeos_root: chromeos tree root
591 board: ChromeOS board name
592 rev: the version name to build
593
594 Returns:
595 Image path
596 """
597
598 # If the given version is already built, reuse it.
599 image_name = 'bisect-%s' % rev
600 image_path = os.path.join('../build/images', board, image_name,
601 'chromiumos_test_image.bin')
602 if exists_in_chroot(chromeos_root, image_path):
603 logger.info('"%s" already exists, skip build step', image_path)
604 return image_path
605
606 dirname = os.path.dirname(os.path.abspath(__file__))
607 script_name = 'build_cros_helper.sh'
608 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
609 script_name)
610 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
611 cros_sdk(chromeos_root, './%s' % script_name, board, image_name)
612 return image_path
613
614
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800615class AutotestControlInfo(object):
616 """Parsed content of autotest control file.
617
618 Attributes:
619 name: test name
620 path: control file path
621 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
622 DOC, ATTRIBUTES, DEPENDENCIES, etc.
623 """
624
625 def __init__(self, path, variables):
626 self.name = variables['NAME']
627 self.path = path
628 self.variables = variables
629
630
631def parse_autotest_control_file(path):
632 """Parses autotest control file.
633
634 This only parses simple top-level string assignments.
635
636 Returns:
637 AutotestControlInfo object
638 """
639 variables = {}
640 code = ast.parse(open(path).read())
641 for stmt in code.body:
642 # Skip if not simple "NAME = *" assignment.
643 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
644 isinstance(stmt.targets[0], ast.Name)):
645 continue
646
647 # Only support string value.
648 if isinstance(stmt.value, ast.Str):
649 variables[stmt.targets[0].id] = stmt.value.s
650
651 return AutotestControlInfo(path, variables)
652
653
654def enumerate_autotest_control_files(autotest_dir):
655 """Enumerate autotest control files.
656
657 Args:
658 autotest_dir: autotest folder
659
660 Returns:
661 list of paths to control files
662 """
663 # Where to find control files. Relative to autotest_dir.
664 subpaths = [
665 'server/site_tests',
666 'client/site_tests',
667 'server/tests',
668 'client/tests',
669 ]
670
671 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
672 result = []
673 for subpath in subpaths:
674 path = os.path.join(autotest_dir, subpath)
675 for root, dirs, files in os.walk(path):
676
677 for black in blacklist:
678 if black in dirs:
679 dirs.remove(black)
680
681 for filename in files:
682 if filename == 'control' or filename.startswith('control.'):
683 result.append(os.path.join(root, filename))
684
685 return result
686
687
688def get_autotest_test_info(autotest_dir, test_name):
689 """Get metadata of given test.
690
691 Args:
692 autotest_dir: autotest folder
693 test_name: test name
694
695 Returns:
696 AutotestControlInfo object. None if test not found.
697 """
698 for control_file in enumerate_autotest_control_files(autotest_dir):
699 info = parse_autotest_control_file(control_file)
700 if info.name == test_name:
701 return info
702 return None
703
704
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800705class ChromeOSManifestManager(repo_util.ManifestManager):
706 """Manifest operations for chromeos repo"""
707
708 def __init__(self, config):
709 self.config = config
710
711 def _query_manifest_name(self, rev):
712 assert is_cros_full_version(rev)
713 milestone, short_version = version_split(rev)
714 manifest_name = os.path.join('buildspecs', milestone,
715 '%s.xml' % short_version)
716 return manifest_name
717
718 def sync_disk_state(self, rev):
719 manifest_name = self._query_manifest_name(rev)
720
721 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
722 # manifest. 'repo sync -m' is not enough
723 repo_util.init(
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800724 self.config['chromeos_root'],
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800725 'https://chrome-internal.googlesource.com/chromeos/manifest-versions/',
726 manifest_name=manifest_name,
727 repo_url='https://chromium.googlesource.com/external/repo.git')
728
729 # Note, don't sync with current_branch=True for chromeos. One of its
730 # build steps (inside mark_as_stable) executes "git describe" which
731 # needs git tag information.
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800732 repo_util.sync(self.config['chromeos_root'])
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800733
734 def fetch_git_repos(self, rev):
735 # Early return if necessary git history are already fetched.
736 mf = self.fetch_manifest(rev)
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800737 repo_set = repo_util.parse_repo_set(self.config['chromeos_root'], mf)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800738 all_have = True
739 for path, git_rev in repo_set.items():
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800740 git_root = os.path.join(self.config['chromeos_root'], path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800741 if not os.path.exists(git_root):
742 all_have = False
743 break
744 if not git_util.is_containing_commit(git_root, git_rev):
745 all_have = False
746 break
747 if all_have:
748 return
749
750 # TODO(kcwu): fetch git history but don't switch current disk state.
751 self.sync_disk_state(rev)
752
753 def enumerate_manifest(self, old, new):
754 assert is_cros_full_version(old)
755 assert is_cros_full_version(new)
756 old_milestone, old_short_version = version_split(old)
757 new_milestone, new_short_version = version_split(new)
758
759 # TODO(kcwu): fetch manifests but don't switch current disk state.
760 self.sync_disk_state(new)
761
Kuang-che Wu3d01f4d2018-07-05 08:49:07 +0800762 spec_dir = os.path.join(self.config['chromeos_root'], '.repo', 'manifests',
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800763 'buildspecs')
764 result = []
765 for root, dirs, files in os.walk(spec_dir):
766 dirs[:] = [
767 dn for dn in dirs if dn.isdigit() and
768 int(old_milestone) <= int(dn) <= int(new_milestone)
769 ]
770
771 for fn in files:
772 short_version, ext = os.path.splitext(fn)
773 if ext != '.xml':
774 continue
775 milestone = os.path.basename(root)
776 if (util.is_version_lesseq(old_short_version, short_version) and
777 util.is_version_lesseq(short_version, new_short_version) and
778 util.is_direct_relative_version(short_version, new_short_version)):
779 result.append(make_cros_full_version(milestone, short_version))
780
781 def version_key_func(full_version):
782 _milestone, short_version = version_split(full_version)
783 return util.version_key_func(short_version)
784
785 result.sort(key=version_key_func)
786 assert result[0] == old
787 assert result[-1] == new
788 return result
789
790 def fetch_manifest(self, rev):
791 # The manifest is already synced, no need to fetch.
792 return self._query_manifest_name(rev)