blob: db97686669365b6d001c1e23b7ed9467e45a0d8a [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 Wue121fae2018-11-09 16:18:39 +0800117 errors.ExecutionFatalError: cannot connect to host or lsb-release file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800118 doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800119 """
120 try:
121 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
122 except subprocess.CalledProcessError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800123 raise errors.ExternalError('cannot connect to DUT or 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 """
136 return query_dut_lsb_release(host).get('DEVICETYPE') in [
137 'CHROMEBASE',
138 'CHROMEBIT',
139 'CHROMEBOOK',
140 'CHROMEBOX',
141 'REFERENCE',
142 ]
143
144
145def query_dut_board(host):
146 """Query board name of a given DUT"""
147 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
148
149
150def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800151 """Query short version of a given DUT.
152
153 This function may return version of local build, which
154 is_cros_short_version() is false.
155 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800156 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
157
158
159def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800160 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800161
162 Args:
163 host: DUT address
164 connect_timeout: connection timeout
165
166 Returns:
167 boot uuid
168 """
169 cmd = ['ssh']
170 if connect_timeout:
171 cmd += ['-oConnectTimeout=%d' % connect_timeout]
172 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
173 return util.check_output(*cmd).strip()
174
175
176def reboot(host):
177 """Reboot a DUT and verify"""
178 logger.debug('reboot %s', host)
179 boot_id = query_dut_boot_id(host)
180
181 # Depends on timing, ssh may return failure due to broken pipe,
182 # so don't check ssh return code.
183 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800184 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800185
Kuang-che Wu708310b2018-03-28 17:24:34 +0800186
187def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800188 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800189 # (dev screen short delay) or more (long delay).
190 time.sleep(15)
191 for _ in range(100):
192 try:
193 # During boot, DUT does not response and thus ssh may hang a while. So
194 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
195 # set tight limit because it's inside retry loop.
196 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
197 return
198 except subprocess.CalledProcessError:
199 logger.debug('reboot not ready? sleep wait 1 sec')
200 time.sleep(1)
201
Kuang-che Wue121fae2018-11-09 16:18:39 +0800202 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800203
204
205def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800206 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800207
208 Args:
209 args: command line arguments passed to gsutil
210 kwargs:
211 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
212 but the path not found.
213
214 Returns:
215 stdout of gsutil
216
217 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800218 errors.InternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800219 subprocess.CalledProcessError: command failed
220 """
221 stderr_lines = []
222 try:
223 return util.check_output(
224 gsutil_bin, *args, stderr_callback=stderr_lines.append)
225 except subprocess.CalledProcessError as e:
226 stderr = ''.join(stderr_lines)
227 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800228 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800229 'gsutil failed due to permission. ' +
230 'Run "%s config" and follow its instruction. ' % gsutil_bin +
231 'Fill any string if it asks for project-id')
232 if kwargs.get('ignore_errors'):
233 return ''
234 raise
235 except OSError as e:
236 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800237 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800238 'Unable to run %s. gsutil is not installed or not in PATH?' %
239 gsutil_bin)
240 raise
241
242
243def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800244 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800245
246 Args:
247 args: arguments passed to 'gsutil ls'
248 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800249 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800250 exception, ex. path not found.
251
252 Returns:
253 list of 'gsutil ls' result. One element for one line of gsutil output.
254
255 Raises:
256 subprocess.CalledProcessError: gsutil failed, usually means path not found
257 """
258 return gsutil('ls', *args, **kwargs).splitlines()
259
260
261def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800262 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800263
264 Args:
265 board: ChromeOS board name
266 short_version: ChromeOS version number in short format, ex. 9300.0.0
267
268 Returns:
269 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
270 None if failed.
271 """
272 path = gs_archive_path.format(board=board) + '/R*-' + short_version
273 for line in gsutil_ls('-d', path, ignore_errors=True):
274 m = re.search(r'/R(\d+)-', line)
275 if not m:
276 continue
277 return m.group(1)
278
279 for channel in ['canary', 'dev', 'beta', 'stable']:
280 path = gs_release_path.format(
281 channel=channel, board=board, short_version=short_version)
282 for line in gsutil_ls(path, ignore_errors=True):
283 m = re.search(r'\bR(\d+)-' + short_version, line)
284 if not m:
285 continue
286 return m.group(1)
287
288 logger.error('unable to query milestone of %s for %s', short_version, board)
289 return None
290
291
292def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800293 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800294
295 Args:
296 board: ChromeOS board name
297 version: ChromeOS version number in short or full format
298
299 Returns:
300 (milestone, version in short format)
301 """
302 if is_cros_short_version(version):
303 milestone = query_milestone_by_version(board, version)
304 short_version = version
305 else:
306 milestone, short_version = version_split(version)
307 return milestone, short_version
308
309
310def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800311 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800312
313 Args:
314 version: ChromeOS version number in short or full format
315
316 Returns:
317 version number in short format
318 """
319 if is_cros_short_version(version):
320 return version
321 _, short_version = version_split(version)
322 return short_version
323
324
325def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800326 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800327
328 Args:
329 board: ChromeOS board name
330 version: ChromeOS version number in short or full format
331
332 Returns:
333 version number in full format
334 """
335 if is_cros_full_version(version):
336 return version
337 milestone = query_milestone_by_version(board, version)
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800338 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800339 return make_cros_full_version(milestone, version)
340
341
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800342def prepare_prebuilt_image(board, version):
343 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800344
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800345 It searches for xbuddy image which "cros flash" can use, or fetch image to
346 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800347
348 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800349 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800350 version: ChromeOS version number in short or full format
351
352 Returns:
353 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800354 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800355 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800356 full_version = version_to_full(board, version)
357 short_version = version_to_short(full_version)
358
359 image_path = None
360 gs_path = gs_archive_path.format(board=board) + '/' + full_version
361 if gsutil_ls('-d', gs_path, ignore_errors=True):
362 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
363 board=board, full_version=full_version)
364 else:
365 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
366 if not os.path.exists(tmp_dir):
367 os.makedirs(tmp_dir)
368 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800369 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800370 # to fetch the image by ourselves
371 for channel in ['canary', 'dev', 'beta', 'stable']:
372 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
373 full_version=full_version, board=board)
374 gs_path = gs_release_path.format(
375 channel=channel, board=board, short_version=short_version)
376 gs_path += '/' + fn
377 if gsutil_ls(gs_path, ignore_errors=True):
378 # TODO(kcwu): delete tmp
379 gsutil('cp', gs_path, tmp_dir)
380 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
381 image_path = os.path.abspath(
382 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
383 break
384
385 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800386 return image_path
387
388
389def cros_flash(chromeos_root,
390 host,
391 board,
392 image_path,
393 version=None,
394 clobber_stateful=False,
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800395 disable_rootfs_verification=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800396 """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
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800411 """
412 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
413
414 # Reboot is necessary because sometimes previous 'cros flash' failed and
415 # entered a bad state.
416 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800417
Kuang-che Wu73e60172018-09-06 14:35:38 +0800418 args = ['--no-ping', '--send-payload-in-parallel', host, image_path]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800419 if clobber_stateful:
420 args.append('--clobber-stateful')
421 if disable_rootfs_verification:
422 args.append('--disable-rootfs-verification')
423
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800424 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800425
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800426 if version:
427 # In the past, cros flash may fail with returncode=0
428 # So let's have an extra check.
429 short_version = version_to_short(version)
430 dut_version = query_dut_short_version(host)
431 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800432
433
434def version_info(board, version):
435 """Query subcomponents version info of given version of ChromeOS
436
437 Args:
438 board: ChromeOS board name
439 version: ChromeOS version number in short or full format
440
441 Returns:
442 dict of component and version info, including (if available):
443 cros_short_version: ChromeOS version
444 cros_full_version: ChromeOS version
445 milestone: milestone of ChromeOS
446 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800447 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800448 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
449 """
450 info = {}
451 full_version = version_to_full(board, version)
452
453 # Some boards may have only partial-metadata.json but no metadata.json.
454 # e.g. caroline R60-9462.0.0
455 # Let's try both.
456 metadata = None
457 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
458 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
459 metadata_filename)
460 metadata = gsutil('cat', path, ignore_errors=True)
461 if metadata:
462 o = json.loads(metadata)
463 v = o['version']
464 board_metadata = o['board-metadata'][board]
465 info.update({
466 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
467 VERSION_KEY_CROS_FULL_VERSION: v['full'],
468 VERSION_KEY_MILESTONE: v['milestone'],
469 VERSION_KEY_CR_VERSION: v['chrome'],
470 })
471
472 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800473 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800474 if 'android-branch' in v: # this appears since R58-9317.0.0
475 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
476 elif 'android-container-branch' in board_metadata:
477 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
478 break
479 else:
480 logger.error('Failed to read metadata from gs://chromeos-image-archive')
481 logger.error(
482 'Note, so far no quick way to look up version info for too old builds')
483
484 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800485
486
487def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800488 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800489
490 Args:
491 board: ChromeOS board name
492 version: ChromeOS version number in short or full format
493
494 Returns:
495 Chrome version number
496 """
497 info = version_info(board, version)
498 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800499
500
501def query_android_build_id(board, rev):
502 info = version_info(board, rev)
503 rev = info['android_build_id']
504 return rev
505
506
507def query_android_branch(board, rev):
508 info = version_info(board, rev)
509 rev = info['android_branch']
510 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800511
512
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800513def guess_chrome_version(board, rev):
514 """Guess chrome version number.
515
516 Args:
517 board: chromeos board name
518 rev: chrome or chromeos version
519
520 Returns:
521 chrome version number
522 """
523 if is_cros_version(rev):
524 assert board, 'need to specify BOARD for cros version'
525 rev = query_chrome_version(board, rev)
526 assert cr_util.is_chrome_version(rev)
527
528 return rev
529
530
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800531def is_inside_chroot():
532 """Returns True if we are inside chroot."""
533 return os.path.exists('/etc/cros_chroot_version')
534
535
536def cros_sdk(chromeos_root, *args, **kwargs):
537 """Run commands inside chromeos chroot.
538
539 Args:
540 chromeos_root: chromeos tree root
541 *args: command to run
542 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +0800543 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800544 env: (dict) environment variables for the command
545 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800546 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800547 """
548 envs = []
549 for k, v in kwargs.get('env', {}).items():
550 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
551 envs.append('%s=%s' % (k, v))
552
553 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
554 # commands would be considered as background process.
Kuang-che Wud4603d72018-11-29 17:51:21 +0800555 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid']
556
557 if kwargs.get('chrome_root'):
558 cmd += ['--chrome_root', kwargs['chrome_root']]
559
560 cmd += envs + ['--'] + list(args)
561
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800562 return util.check_output(
563 *cmd,
564 cwd=chromeos_root,
565 stdin=kwargs.get('stdin'),
566 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800567
568
569def copy_into_chroot(chromeos_root, src, dst):
570 """Copies file into chromeos chroot.
571
572 Args:
573 chromeos_root: chromeos tree root
574 src: path outside chroot
575 dst: path inside chroot
576 """
577 # chroot may be an image, so we cannot copy to corresponding path
578 # directly.
579 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
580
581
582def exists_in_chroot(chromeos_root, path):
583 """Determine whether a path exists in the chroot.
584
585 Args:
586 chromeos_root: chromeos tree root
587 path: path inside chroot, relative to src/scripts
588
589 Returns:
590 True if a path exists
591 """
592 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800593 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800594 except subprocess.CalledProcessError:
595 return False
596 return True
597
598
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800599def check_if_need_recreate_chroot(stdout, stderr):
600 """Analyze build log and determine if chroot should be recreated.
601
602 Args:
603 stdout: stdout output of build
604 stderr: stderr output of build
605
606 Returns:
607 the reason if chroot needs recreated; None otherwise
608 """
Kuang-che Wu74768d32018-09-07 12:03:24 +0800609 if re.search(
610 r"The current version of portage supports EAPI '\d+'. "
611 "You must upgrade", stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800612 return 'EAPI version mismatch'
613
Kuang-che Wu5ac81322018-11-26 14:04:06 +0800614 if 'Chroot is too new. Consider running:' in stderr:
615 return 'chroot version is too new'
616
617 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800618 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
619 return 'chroot version is too new'
620
Kuang-che Wu6fe987f2018-08-28 15:24:20 +0800621 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
622 if "undefined reference to 'std::__1::basic_string" in stdout:
623 return 'might be due to compiler change'
624
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800625 return None
626
627
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800628def build_image(chromeos_root, board, rev):
629 """Build ChromeOS image.
630
631 Args:
632 chromeos_root: chromeos tree root
633 board: ChromeOS board name
634 rev: the version name to build
635
636 Returns:
637 Image path
638 """
639
640 # If the given version is already built, reuse it.
Kuang-che Wuf41599c2018-08-03 16:11:11 +0800641 image_name = 'bisect-%s' % rev.replace('/', '_')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800642 image_path = os.path.join('../build/images', board, image_name,
643 'chromiumos_test_image.bin')
644 if exists_in_chroot(chromeos_root, image_path):
645 logger.info('"%s" already exists, skip build step', image_path)
646 return image_path
647
648 dirname = os.path.dirname(os.path.abspath(__file__))
649 script_name = 'build_cros_helper.sh'
650 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
651 script_name)
652 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800653
654 stderr_lines = []
655 try:
Kuang-che Wufb553102018-10-02 18:14:29 +0800656 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
657 cros_sdk(
658 chromeos_root,
659 './%s' % script_name,
660 board,
661 image_name,
662 stderr_callback=stderr_lines.append)
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800663 except subprocess.CalledProcessError as e:
664 # Detect failures due to incompatibility between chroot and source tree. If
665 # so, notify the caller to recreate chroot and retry.
666 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
667 if reason:
668 raise NeedRecreateChrootException(reason)
669
670 # For other failures, don't know how to handle. Just bail out.
671 raise
672
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800673 return image_path
674
675
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800676class AutotestControlInfo(object):
677 """Parsed content of autotest control file.
678
679 Attributes:
680 name: test name
681 path: control file path
682 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
683 DOC, ATTRIBUTES, DEPENDENCIES, etc.
684 """
685
686 def __init__(self, path, variables):
687 self.name = variables['NAME']
688 self.path = path
689 self.variables = variables
690
691
692def parse_autotest_control_file(path):
693 """Parses autotest control file.
694
695 This only parses simple top-level string assignments.
696
697 Returns:
698 AutotestControlInfo object
699 """
700 variables = {}
701 code = ast.parse(open(path).read())
702 for stmt in code.body:
703 # Skip if not simple "NAME = *" assignment.
704 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
705 isinstance(stmt.targets[0], ast.Name)):
706 continue
707
708 # Only support string value.
709 if isinstance(stmt.value, ast.Str):
710 variables[stmt.targets[0].id] = stmt.value.s
711
712 return AutotestControlInfo(path, variables)
713
714
715def enumerate_autotest_control_files(autotest_dir):
716 """Enumerate autotest control files.
717
718 Args:
719 autotest_dir: autotest folder
720
721 Returns:
722 list of paths to control files
723 """
724 # Where to find control files. Relative to autotest_dir.
725 subpaths = [
726 'server/site_tests',
727 'client/site_tests',
728 'server/tests',
729 'client/tests',
730 ]
731
732 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
733 result = []
734 for subpath in subpaths:
735 path = os.path.join(autotest_dir, subpath)
736 for root, dirs, files in os.walk(path):
737
738 for black in blacklist:
739 if black in dirs:
740 dirs.remove(black)
741
742 for filename in files:
743 if filename == 'control' or filename.startswith('control.'):
744 result.append(os.path.join(root, filename))
745
746 return result
747
748
749def get_autotest_test_info(autotest_dir, test_name):
750 """Get metadata of given test.
751
752 Args:
753 autotest_dir: autotest folder
754 test_name: test name
755
756 Returns:
757 AutotestControlInfo object. None if test not found.
758 """
759 for control_file in enumerate_autotest_control_files(autotest_dir):
760 info = parse_autotest_control_file(control_file)
761 if info.name == test_name:
762 return info
763 return None
764
765
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800766class ChromeOSSpecManager(codechange.SpecManager):
767 """Repo manifest related operations.
768
769 This class enumerates chromeos manifest files, parses them,
770 and sync to disk state according to them.
771 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800772
773 def __init__(self, config):
774 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800775 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
776 'manifests')
777 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800778 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800779 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800780 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
781 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800782
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800783 def lookup_build_timestamp(self, rev):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800784 assert is_cros_full_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800785
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800786 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800787 path = os.path.join('buildspecs', milestone, short_version + '.xml')
788 try:
789 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
790 'refs/heads/master', path)
791 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800792 raise errors.InternalError(
Kuang-che Wu74768d32018-09-07 12:03:24 +0800793 '%s does not have %s' % (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800794 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800795
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800796 def collect_float_spec(self, old, new):
797 old_timestamp = self.lookup_build_timestamp(old)
798 new_timestamp = self.lookup_build_timestamp(new)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800799
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800800 path = os.path.join(self.manifest_dir, 'default.xml')
801 if not os.path.islink(path) or os.readlink(path) != 'full.xml':
Kuang-che Wue121fae2018-11-09 16:18:39 +0800802 raise errors.InternalError(
803 'default.xml not symlink to full.xml is not supported')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800804
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800805 result = []
806 path = 'full.xml'
807 parser = repo_util.ManifestParser(self.manifest_dir)
808 for timestamp, git_rev in parser.enumerate_manifest_commits(
809 old_timestamp, new_timestamp, path):
810 result.append(
811 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
812 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800813
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800814 def collect_fixed_spec(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800815 assert is_cros_full_version(old)
816 assert is_cros_full_version(new)
817 old_milestone, old_short_version = version_split(old)
818 new_milestone, new_short_version = version_split(new)
819
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800820 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800821 for milestone in git_util.list_dir_from_revision(
822 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
823 if not milestone.isdigit():
824 continue
825 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
826 continue
827
Kuang-che Wu74768d32018-09-07 12:03:24 +0800828 files = git_util.list_dir_from_revision(
829 self.historical_manifest_git_dir, 'refs/heads/master',
830 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800831
832 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800833 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800834 short_version, ext = os.path.splitext(fn)
835 if ext != '.xml':
836 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800837 if (util.is_version_lesseq(old_short_version, short_version) and
838 util.is_version_lesseq(short_version, new_short_version) and
839 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800840 rev = make_cros_full_version(milestone, short_version)
841 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
842 'refs/heads/master', path)
843 result.append(
844 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800845
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800846 def version_key_func(spec):
847 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800848 return util.version_key_func(short_version)
849
850 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800851 assert result[0].name == old
852 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800853 return result
854
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800855 def get_manifest(self, rev):
856 assert is_cros_full_version(rev)
857 milestone, short_version = version_split(rev)
858 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
859 manifest = git_util.get_file_from_revision(self.historical_manifest_git_dir,
860 'refs/heads/master', path)
861
862 manifest_name = 'manifest_%s.xml' % rev
863 manifest_path = os.path.join(self.manifest_dir, manifest_name)
864 with open(manifest_path, 'w') as f:
865 f.write(manifest)
866
867 return manifest_name
868
869 def parse_spec(self, spec):
870 parser = repo_util.ManifestParser(self.manifest_dir)
871 if spec.spec_type == codechange.SPEC_FIXED:
872 manifest_name = self.get_manifest(spec.name)
873 manifest_path = os.path.join(self.manifest_dir, manifest_name)
874 content = open(manifest_path).read()
875 root = parser.parse_single_xml(content, allow_include=False)
876 else:
877 root = parser.parse_xml_recursive(spec.name, spec.path)
878
879 spec.entries = parser.process_parsed_result(root)
880 if spec.spec_type == codechange.SPEC_FIXED:
881 assert spec.is_static()
882
883 def sync_disk_state(self, rev):
884 manifest_name = self.get_manifest(rev)
885
886 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
887 # manifest. 'repo sync -m' is not enough
888 repo_util.init(
889 self.config['chromeos_root'],
890 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
891 manifest_name=manifest_name,
892 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800893 reference=self.config['chromeos_mirror'],
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800894 )
895
896 # Note, don't sync with current_branch=True for chromeos. One of its
897 # build steps (inside mark_as_stable) executes "git describe" which
898 # needs git tag information.
899 repo_util.sync(self.config['chromeos_root'])