blob: 3d87324b548639e97b75c4a66cab9af36ecef9c9 [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".
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080010 snapshot_version: ChromeOS version number with milestone and snapshot id,
11 like "R62-9876.0.0-12345".
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080012 version: if not specified, it could be in short or full format.
13"""
14
15from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080016import ast
Kuang-che Wu72b5a572019-10-29 20:37:57 +080017import calendar
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080018import datetime
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080019import errno
20import json
21import logging
22import os
23import re
24import subprocess
25import time
26
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080027from google.protobuf import json_format
28
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080029from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080030from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080031from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080032from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080033from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080034from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080035from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080036from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080037from bisect_kit import util
38
39logger = logging.getLogger(__name__)
40
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080041re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080042re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
43re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080044re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080045
46gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
47gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080048 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080049
50# Assume gsutil is in PATH.
51gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080052
53# Since snapshots with version >= 12618.0.0 have android and chrome version
54# info.
55snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080056
Kuang-che Wub9705bd2018-06-28 17:59:18 +080057chromeos_root_inside_chroot = '/mnt/host/source'
58# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080059prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080060# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
61cached_images_dir = 'src/build/images'
62test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080063
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080064VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
65VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
66VERSION_KEY_MILESTONE = 'milestone'
67VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080068VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080069VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +080070CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080071
72
Kuang-che Wu9890ce82018-07-07 15:14:10 +080073class NeedRecreateChrootException(Exception):
74 """Failed to build ChromeOS because of chroot mismatch or corruption"""
75
76
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080077def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080078 """Determines if `s` is chromeos short version.
79
80 This function doesn't accept version number of local build.
81 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080082 return bool(re.match(re_chromeos_short_version, s))
83
84
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080085def is_cros_localbuild_version(s):
86 """Determines if `s` is chromeos local build version."""
87 return bool(re.match(re_chromeos_localbuild_version, s))
88
89
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080090def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080091 """Determines if `s` is chromeos full version.
92
93 This function doesn't accept version number of local build.
94 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080095 return bool(re.match(re_chromeos_full_version, s))
96
97
98def is_cros_version(s):
99 """Determines if `s` is chromeos version (either short or full)"""
100 return is_cros_short_version(s) or is_cros_full_version(s)
101
102
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800103def is_cros_snapshot_version(s):
104 """Determines if `s` is chromeos snapshot version"""
105 return bool(re.match(re_chromeos_snapshot_version, s))
106
107
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800108def is_cros_version_lesseq(ver1, ver2):
109 """Determines if ver1 is less or equal to ver2.
110
111 Args:
112 ver1: a Chrome OS version in short, full, or snapshot format.
113 ver2: a Chrome OS version in short, full, or snapshot format.
114
115 Returns:
116 True if ver1 is smaller than ver2.
117 """
118 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
119 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
120
121 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
122 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
123 return ver1 <= ver2
124
125
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800126def make_cros_full_version(milestone, short_version):
127 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800128 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800129 return 'R%s-%s' % (milestone, short_version)
130
131
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800132def make_cros_snapshot_version(milestone, short_version, snapshot_id):
133 """Makes snapshot version from milestone, short_version and snapshot id"""
134 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
135
136
137def version_split(version):
138 """Splits full_version or snapshot_version into milestone and short_version"""
139 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
140 if is_cros_snapshot_version(version):
141 return snapshot_version_split(version)[0:2]
142 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800143 return milestone[1:], short_version
144
145
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800146def snapshot_version_split(snapshot_version):
147 """Splits snapshot_version into milestone, short_version and snapshot_id"""
148 assert is_cros_snapshot_version(snapshot_version)
149 milestone, shot_version, snapshot_id = snapshot_version.split('-')
150 return milestone[1:], shot_version, snapshot_id
151
152
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800153def query_snapshot_buildbucket_id(board, snapshot_version):
154 """Query buildbucket id of a snapshot"""
155 assert is_cros_snapshot_version(snapshot_version)
156 path = ('gs://chromeos-image-archive/{board}-postsubmit'
157 '/{snapshot_version}-*/image.zip')
158 output = gsutil_ls(
159 '-d',
160 path.format(board=board, snapshot_version=snapshot_version),
161 ignore_errors=True)
162 for line in output:
163 m = re.match(r'.*-postsubmit/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
164 if m:
165 return m.group(1)
166 return None
167
168
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800169def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800170 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800171 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800172 raise cli.ArgTypeError(msg, '9876.0.0, R62-9876.0.0 or R77-12369.0.0-11681')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800173 return s
174
175
176def query_dut_lsb_release(host):
177 """Query /etc/lsb-release of given DUT
178
179 Args:
180 host: the DUT address
181
182 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800183 dict for keys and values of /etc/lsb-release.
184
185 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800186 errors.SshConnectionError: cannot connect to host
187 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800188 """
189 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800190 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800191 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800192 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800193 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
194
195
196def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800197 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800198
199 Args:
200 host: the DUT address
201
202 Returns:
203 True if the host is a chromeos device.
204 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800205 try:
206 return query_dut_lsb_release(host).get('DEVICETYPE') in [
207 'CHROMEBASE',
208 'CHROMEBIT',
209 'CHROMEBOOK',
210 'CHROMEBOX',
211 'REFERENCE',
212 ]
213 except (errors.ExternalError, errors.SshConnectionError):
214 return False
215
216
217def is_good_dut(host):
218 if not is_dut(host):
219 return False
220
221 # Sometimes python is broken after 'cros flash'.
222 try:
223 util.ssh_cmd(host, 'python', '-c', '1')
224 return True
225 except (subprocess.CalledProcessError, errors.SshConnectionError):
226 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800227
228
229def query_dut_board(host):
230 """Query board name of a given DUT"""
231 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
232
233
234def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800235 """Query short version of a given DUT.
236
237 This function may return version of local build, which
238 is_cros_short_version() is false.
239 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800240 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
241
242
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800243def query_dut_is_snapshot(host):
244 """Query if given DUT is a snapshot version."""
245 path = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILDER_PATH', '')
246 return '-postsubmit' in path
247
248
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800249def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800250 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800251
252 Args:
253 host: DUT address
254 connect_timeout: connection timeout
255
256 Returns:
257 boot uuid
258 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800259 return util.ssh_cmd(
260 host,
261 'cat',
262 '/proc/sys/kernel/random/boot_id',
263 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800264
265
266def reboot(host):
267 """Reboot a DUT and verify"""
268 logger.debug('reboot %s', host)
269 boot_id = query_dut_boot_id(host)
270
Kuang-che Wu44278142019-03-04 11:33:57 +0800271 try:
272 util.ssh_cmd(host, 'reboot')
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800273 except errors.SshConnectionError:
274 # Depends on timing, ssh may return failure due to broken pipe, which is
275 # working as intended. Ignore such kind of errors.
Kuang-che Wu44278142019-03-04 11:33:57 +0800276 pass
Kuang-che Wu708310b2018-03-28 17:24:34 +0800277 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800278
Kuang-che Wu708310b2018-03-28 17:24:34 +0800279
280def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800281 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800282 # (dev screen short delay) or more (long delay).
283 time.sleep(15)
284 for _ in range(100):
285 try:
286 # During boot, DUT does not response and thus ssh may hang a while. So
287 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
288 # set tight limit because it's inside retry loop.
289 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
290 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800291 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800292 logger.debug('reboot not ready? sleep wait 1 sec')
293 time.sleep(1)
294
Kuang-che Wue121fae2018-11-09 16:18:39 +0800295 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800296
297
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800298def gs_release_boardpath(board):
299 """Normalizes board name for gs://chromeos-releases/
300
301 This follows behavior of PushImage() in chromite/scripts/pushimage.py
302 Note, only gs://chromeos-releases/ needs normalization,
303 gs://chromeos-image-archive does not.
304
305 Args:
306 board: ChromeOS board name
307
308 Returns:
309 normalized board name
310 """
311 return board.replace('_', '-')
312
313
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800314def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800315 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800316
317 Args:
318 args: command line arguments passed to gsutil
319 kwargs:
320 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
321 but the path not found.
322
323 Returns:
324 stdout of gsutil
325
326 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800327 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800328 subprocess.CalledProcessError: command failed
329 """
330 stderr_lines = []
331 try:
332 return util.check_output(
333 gsutil_bin, *args, stderr_callback=stderr_lines.append)
334 except subprocess.CalledProcessError as e:
335 stderr = ''.join(stderr_lines)
336 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800337 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800338 'gsutil failed due to permission. ' +
339 'Run "%s config" and follow its instruction. ' % gsutil_bin +
340 'Fill any string if it asks for project-id')
341 if kwargs.get('ignore_errors'):
342 return ''
343 raise
344 except OSError as e:
345 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800346 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800347 'Unable to run %s. gsutil is not installed or not in PATH?' %
348 gsutil_bin)
349 raise
350
351
352def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800353 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800354
355 Args:
356 args: arguments passed to 'gsutil ls'
357 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800358 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800359 exception, ex. path not found.
360
361 Returns:
362 list of 'gsutil ls' result. One element for one line of gsutil output.
363
364 Raises:
365 subprocess.CalledProcessError: gsutil failed, usually means path not found
366 """
367 return gsutil('ls', *args, **kwargs).splitlines()
368
369
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800370def gsutil_stat_update_time(*args, **kwargs):
371 """Returns the last modified time of a file or multiple files.
372
373 Args:
374 args: arguments passed to 'gsutil stat'.
375 kwargs: extra parameters for gsutil.
376
377 Returns:
378 A integer indicates the last modified timestamp.
379
380 Raises:
381 subprocess.CalledProcessError: gsutil failed, usually means path not found
382 errors.ExternalError: update time is not found
383 """
384 result = -1
385 # Currently we believe stat always returns a UTC time, and strptime also
386 # parses a UTC time by default.
387 time_format = '%a, %d %b %Y %H:%M:%S GMT'
388
389 for line in gsutil('stat', *args, **kwargs).splitlines():
390 if ':' not in line:
391 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800392 key, value = line.split(':', 1)
393 key, value = key.strip(), value.strip()
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800394 if key != 'Update time':
395 continue
396 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800397 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800398 result = max(result, unixtime)
399
400 if result == -1:
401 raise errors.ExternalError("didn't find update time")
402 return result
403
404
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800405def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800406 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800407
408 Args:
409 board: ChromeOS board name
410 short_version: ChromeOS version number in short format, ex. 9300.0.0
411
412 Returns:
413 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
414 None if failed.
415 """
416 path = gs_archive_path.format(board=board) + '/R*-' + short_version
417 for line in gsutil_ls('-d', path, ignore_errors=True):
418 m = re.search(r'/R(\d+)-', line)
419 if not m:
420 continue
421 return m.group(1)
422
423 for channel in ['canary', 'dev', 'beta', 'stable']:
424 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800425 channel=channel,
426 boardpath=gs_release_boardpath(board),
427 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800428 for line in gsutil_ls(path, ignore_errors=True):
429 m = re.search(r'\bR(\d+)-' + short_version, line)
430 if not m:
431 continue
432 return m.group(1)
433
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800434 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800435 return None
436
437
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800438def list_board_names(chromeos_root):
439 """List board names.
440
441 Args:
442 chromeos_root: chromeos tree root
443
444 Returns:
445 list of board names
446 """
447 # Following logic is simplified from chromite/lib/portage_util.py
448 cros_list_overlays = os.path.join(chromeos_root,
449 'chromite/bin/cros_list_overlays')
450 overlays = util.check_output(cros_list_overlays).splitlines()
451 result = set()
452 for overlay in overlays:
453 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
454 name = None
455 if os.path.exists(conf_file):
456 for line in open(conf_file):
457 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
458 if m:
459 name = m.group(1)
460 break
461
462 if not name:
463 name_file = os.path.join(overlay, 'profiles', 'repo_name')
464 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800465 with open(name_file) as f:
466 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800467
468 if name:
469 name = re.sub(r'-private$', '', name)
470 result.add(name)
471
472 return list(result)
473
474
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800475def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800476 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800477
478 Args:
479 board: ChromeOS board name
480 version: ChromeOS version number in short or full format
481
482 Returns:
483 (milestone, version in short format)
484 """
485 if is_cros_short_version(version):
486 milestone = query_milestone_by_version(board, version)
487 short_version = version
488 else:
489 milestone, short_version = version_split(version)
490 return milestone, short_version
491
492
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800493def extract_major_version(version):
494 """Converts a version to its major version.
495
496 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800497 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800498
499 Returns:
500 major version number in string format
501 """
502 version = version_to_short(version)
503 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
504 return m.group(1)
505
506
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800507def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800508 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800509
510 Args:
511 version: ChromeOS version number in short or full format
512
513 Returns:
514 version number in short format
515 """
516 if is_cros_short_version(version):
517 return version
518 _, short_version = version_split(version)
519 return short_version
520
521
522def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800523 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800524
525 Args:
526 board: ChromeOS board name
527 version: ChromeOS version number in short or full format
528
529 Returns:
530 version number in full format
531 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800532 if is_cros_snapshot_version(version):
533 milestone, short_version, _ = snapshot_version_split(version)
534 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800535 if is_cros_full_version(version):
536 return version
537 milestone = query_milestone_by_version(board, version)
Kuang-che Wu0205f052019-05-23 12:48:37 +0800538 assert milestone, 'incorrect board=%s or version=%s ?' % (board, version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800539 return make_cros_full_version(milestone, version)
540
541
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800542def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800543 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800544
545 Args:
546 board: ChromeOS board
547 major_version: ChromeOS major version
548
549 Returns:
550 list of (version, gs_path):
551 version: Chrome OS snapshot version
552 gs_path: gs path of test image
553 """
554
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800555 def extract_snapshot_id(result):
556 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
557 assert m
558 return int(m.group(1))
559
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800560 short_version = '%s.0.0' % major_version
561 milestone = query_milestone_by_version(board, short_version)
562 if not milestone:
563 milestone = '*'
564
565 path = ('gs://chromeos-image-archive/{board}-postsubmit/R{milestone}-'
566 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800567 result = []
568 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800569 path.format(
570 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800571 ignore_errors=True)
572
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800573 for gs_path in sorted(output):
574 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800575 if m:
576 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800577 # we should skip if there is duplicate snapshot
578 if result and result[-1][0] == snapshot_version:
579 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800580 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800581
582 # sort by its snapshot_id
583 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800584 return result
585
586
Kuang-che Wu575dc442019-03-05 10:30:55 +0800587def list_prebuilt_from_image_archive(board):
588 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
589
590 gs://chromeos-image-archive contains only recent builds (in two years).
591 We prefer this function to list_prebuilt_from_chromeos_releases() because
592 - this is what "cros flash" supports directly.
593 - the paths have milestone information, so we don't need to do slow query
594 by ourselves.
595
596 Args:
597 board: ChromeOS board name
598
599 Returns:
600 list of (version, gs_path):
601 version: Chrome OS version in full format
602 gs_path: gs path of test image
603 """
604 result = []
605 for line in gsutil_ls(gs_archive_path.format(board=board)):
606 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
607 if m:
608 full_version = m.group(1)
609 test_image = 'chromiumos_test_image.tar.xz'
610 assert line.endswith('/')
611 gs_path = line + test_image
612 result.append((full_version, gs_path))
613 return result
614
615
616def list_prebuilt_from_chromeos_releases(board):
617 """Lists ChromeOS versions available from gs://chromeos-releases.
618
619 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
620 support it.
621
622 Args:
623 board: ChromeOS board name
624
625 Returns:
626 list of (version, gs_path):
627 version: Chrome OS version in short format
628 gs_path: gs path of test image (with wildcard)
629 """
630 result = []
631 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800632 gs_release_path.format(
633 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800634 ignore_errors=True):
635 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
636 if m:
637 short_version = m.group(1)
638 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
639 short_version=short_version, board=board)
640 gs_path = line + test_image
641 result.append((short_version, gs_path))
642 return result
643
644
Kuang-che Wue1808402020-01-06 20:27:45 +0800645def has_test_image(board, version):
646 if is_cros_snapshot_version(version):
647 return bool(query_snapshot_buildbucket_id(board, version))
648
649 full_version = version_to_full(board, version)
650 short_version = version_to_short(version)
651 paths = [
652 gs_archive_path.format(board=board) +
653 '/%s/chromiumos_test_image.tar.xz' % full_version,
654 gs_release_path.format(
655 channel='*',
656 boardpath=gs_release_boardpath(board),
657 short_version=short_version),
658 ]
659
660 for path in paths:
661 if gsutil_ls(path, ignore_errors=True):
662 return True
663 return False
664
665
Kuang-che Wu575dc442019-03-05 10:30:55 +0800666def list_chromeos_prebuilt_versions(board,
667 old,
668 new,
669 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800670 include_older_build=True,
671 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800672 """Lists ChromeOS version numbers with prebuilt between given range
673
674 Args:
675 board: ChromeOS board name
676 old: start version (inclusive)
677 new: end version (inclusive)
678 only_good_build: only if test image is available
679 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800680 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800681
682 Returns:
683 list of sorted version numbers (in full format) between [old, new] range
684 (inclusive).
685 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800686 old_short = version_to_short(old)
687 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800688
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800689 rev_map = {
690 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800691 for full_version, gs_path in list_prebuilt_from_image_archive(board):
692 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800693 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800694
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800695 if include_older_build and old_short not in rev_map:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800696 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
697 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800698 rev_map[short_version] = [(short_version, gs_path)]
699
700 if use_snapshot:
701 for major_version in range(
702 int(extract_major_version(old)),
703 int(extract_major_version(new)) + 1):
704 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800705 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800706 # If current version is smaller than cutover, ignore it as it might not
707 # contain enough information for continuing android and chrome bisection.
708 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
709 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800710
711 # Given the fact that snapshots are images between two release versions.
712 # Adding snapshots of 12345.0.0 should be treated as adding commits
713 # between [12345.0.0, 12346.0.0).
714 # So in the following lines we check two facts:
715 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800716 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800717 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800718 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800719 continue
720 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800721 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800722 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800723 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800724 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800725
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800726 snapshots = list_snapshots_from_image_archive(board, str(major_version))
727 if snapshots:
728 # if snapshots found, we can append them after the release version,
729 # so the prebuilt image list of this version will be
730 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800731 if short_version not in rev_map:
732 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800733 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800734
735 result = []
736 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800737 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800738 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800739 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800740 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800741 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800742 continue
743
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800744 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800745
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800746 # version_to_full() and gsutil_ls() may take long time if versions are a
747 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800748
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800749 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800750 gs_result = gsutil_ls(gs_path, ignore_errors=True)
751 if not gs_result:
752 logger.warning('%s is not a good build, ignore', version)
753 continue
754 assert len(gs_result) == 1
755 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
756 if not m:
757 logger.warning('format of image path is unexpected: %s', gs_result[0])
758 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800759 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800760 elif is_cros_short_version(version):
761 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800762
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800763 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
764 version, new):
765 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800766
767 return result
768
769
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800770def prepare_snapshot_image(chromeos_root, board, snapshot_version):
771 """Prepare chromeos snapshot image.
772
773 Args:
774 chromeos_root: chromeos tree root
775 board: ChromeOS board name
776 snapshot_version: ChromeOS snapshot version number
777
778 Returns:
779 local file path of test image relative to chromeos_root
780 """
781 assert is_cros_snapshot_version(snapshot_version)
782 milestone, short_version, snapshot_id = snapshot_version_split(
783 snapshot_version)
784 full_version = make_cros_full_version(milestone, short_version)
785 tmp_dir = os.path.join(
786 chromeos_root, 'tmp',
787 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
788 if not os.path.exists(tmp_dir):
789 os.makedirs(tmp_dir)
790
791 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/' +
792 '{snapshot_version}-*/image.zip')
793 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
794
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800795 full_path = os.path.join(tmp_dir, test_image_filename)
796 rel_path = os.path.relpath(full_path, chromeos_root)
797 if os.path.exists(full_path):
798 return rel_path
799
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800800 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800801 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800802 gs_path = files[0]
803 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800804 util.check_call(
805 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
806 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800807 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800808
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800809 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800810 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800811
812
Kuang-che Wu28980b22019-07-31 19:51:45 +0800813def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800814 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800815
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800816 It searches for xbuddy image which "cros flash" can use, or fetch image to
817 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800818
819 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800820 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800821 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800822 version: ChromeOS version number in short or full format
823
824 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800825 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800826 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800827 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800828 full_version = version_to_full(board, version)
829 short_version = version_to_short(full_version)
830
831 image_path = None
832 gs_path = gs_archive_path.format(board=board) + '/' + full_version
833 if gsutil_ls('-d', gs_path, ignore_errors=True):
834 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
835 board=board, full_version=full_version)
836 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800837 tmp_dir = os.path.join(chromeos_root, 'tmp',
838 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800839 full_path = os.path.join(tmp_dir, test_image_filename)
840 rel_path = os.path.relpath(full_path, chromeos_root)
841 if os.path.exists(full_path):
842 return rel_path
843
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800844 if not os.path.exists(tmp_dir):
845 os.makedirs(tmp_dir)
846 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800847 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800848 # to fetch the image by ourselves
849 for channel in ['canary', 'dev', 'beta', 'stable']:
850 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
851 full_version=full_version, board=board)
852 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800853 channel=channel,
854 boardpath=gs_release_boardpath(board),
855 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800856 gs_path += '/' + fn
857 if gsutil_ls(gs_path, ignore_errors=True):
858 # TODO(kcwu): delete tmp
859 gsutil('cp', gs_path, tmp_dir)
860 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800861 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800862 break
863
864 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800865 return image_path
866
867
868def cros_flash(chromeos_root,
869 host,
870 board,
871 image_path,
872 version=None,
873 clobber_stateful=False,
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800874 disable_rootfs_verification=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800875 """Flash a DUT with given ChromeOS image.
876
877 This is implemented by 'cros flash' command line.
878
879 Args:
880 chromeos_root: use 'cros flash' of which chromeos tree
881 host: DUT address
882 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800883 image_path: chromeos image xbuddy path or file path. For relative
884 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800885 version: ChromeOS version in short or full format
886 clobber_stateful: Clobber stateful partition when performing update
887 disable_rootfs_verification: Disable rootfs verification after update
888 is completed
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800889
890 Raises:
891 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800892 """
893 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
894
895 # Reboot is necessary because sometimes previous 'cros flash' failed and
896 # entered a bad state.
897 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800898
Kuang-che Wu28980b22019-07-31 19:51:45 +0800899 # Handle relative path.
900 if '://' not in image_path and not os.path.isabs(image_path):
901 assert os.path.exists(os.path.join(chromeos_root, image_path))
902 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
903
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800904 args = [
905 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
906 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800907 if clobber_stateful:
908 args.append('--clobber-stateful')
909 if disable_rootfs_verification:
910 args.append('--disable-rootfs-verification')
911
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800912 try:
913 cros_sdk(chromeos_root, 'cros', 'flash', *args)
914 except subprocess.CalledProcessError:
915 raise errors.ExternalError('cros flash failed')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800916
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800917 if version:
918 # In the past, cros flash may fail with returncode=0
919 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800920 if is_cros_snapshot_version(version):
921 builder_path = query_dut_lsb_release(host).get(
922 'CHROMEOS_RELEASE_BUILDER_PATH', '')
923 expect_prefix = '%s-postsubmit/%s-' % (board, version)
924 if not builder_path.startswith(expect_prefix):
925 raise errors.ExternalError(
926 'although cros flash succeeded, the OS builder path is '
927 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
928 else:
929 expect_version = version_to_short(version)
930 dut_version = query_dut_short_version(host)
931 if dut_version != expect_version:
932 raise errors.ExternalError(
933 'although cros flash succeeded, the OS version is unexpected: '
934 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800935
Kuang-che Wu4a81ea72019-10-05 15:35:17 +0800936 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800937 # (b/130786578), so it's necessary to do sanity check.
938 if not is_good_dut(host):
939 raise errors.ExternalError(
940 'although cros flash succeeded, the DUT is in bad state')
941
942
943def cros_flash_with_retry(chromeos_root,
944 host,
945 board,
946 image_path,
947 version=None,
948 clobber_stateful=False,
949 disable_rootfs_verification=True,
950 repair_callback=None):
951 # 'cros flash' is not 100% reliable, retry if necessary.
952 for attempt in range(2):
953 if attempt > 0:
954 logger.info('will retry 60 seconds later')
955 time.sleep(60)
956
957 try:
958 cros_flash(
959 chromeos_root,
960 host,
961 board,
962 image_path,
963 version=version,
964 clobber_stateful=clobber_stateful,
965 disable_rootfs_verification=disable_rootfs_verification)
966 return True
967 except errors.ExternalError:
968 logger.exception('cros flash failed')
969 if repair_callback and not repair_callback(host):
970 logger.warning('not repaired, assume it is harmless')
971 continue
972 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800973
974
975def version_info(board, version):
976 """Query subcomponents version info of given version of ChromeOS
977
978 Args:
979 board: ChromeOS board name
980 version: ChromeOS version number in short or full format
981
982 Returns:
983 dict of component and version info, including (if available):
984 cros_short_version: ChromeOS version
985 cros_full_version: ChromeOS version
986 milestone: milestone of ChromeOS
987 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800988 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800989 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
990 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800991 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800992 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800993 milestone, short_version, _ = snapshot_version_split(version)
994 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800995 data = api.get(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +0800996 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800997 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800998 VERSION_KEY_MILESTONE: milestone,
999 VERSION_KEY_CROS_FULL_VERSION: version,
1000 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001001 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1002 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1003 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001004 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001005 info = {}
1006 full_version = version_to_full(board, version)
1007
1008 # Some boards may have only partial-metadata.json but no metadata.json.
1009 # e.g. caroline R60-9462.0.0
1010 # Let's try both.
1011 metadata = None
1012 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001013 path = gs_archive_path.format(
1014 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001015 metadata = gsutil('cat', path, ignore_errors=True)
1016 if metadata:
1017 o = json.loads(metadata)
1018 v = o['version']
1019 board_metadata = o['board-metadata'][board]
1020 info.update({
1021 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1022 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1023 VERSION_KEY_MILESTONE: v['milestone'],
1024 VERSION_KEY_CR_VERSION: v['chrome'],
1025 })
1026
1027 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001028 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001029 if 'android-branch' in v: # this appears since R58-9317.0.0
1030 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1031 elif 'android-container-branch' in board_metadata:
1032 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1033 break
1034 else:
1035 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1036 logger.error(
1037 'Note, so far no quick way to look up version info for too old builds')
1038
1039 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001040
1041
1042def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001043 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001044
1045 Args:
1046 board: ChromeOS board name
1047 version: ChromeOS version number in short or full format
1048
1049 Returns:
1050 Chrome version number
1051 """
1052 info = version_info(board, version)
1053 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001054
1055
1056def query_android_build_id(board, rev):
1057 info = version_info(board, rev)
1058 rev = info['android_build_id']
1059 return rev
1060
1061
1062def query_android_branch(board, rev):
1063 info = version_info(board, rev)
1064 rev = info['android_branch']
1065 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001066
1067
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001068def guess_chrome_version(board, rev):
1069 """Guess chrome version number.
1070
1071 Args:
1072 board: chromeos board name
1073 rev: chrome or chromeos version
1074
1075 Returns:
1076 chrome version number
1077 """
1078 if is_cros_version(rev):
1079 assert board, 'need to specify BOARD for cros version'
1080 rev = query_chrome_version(board, rev)
1081 assert cr_util.is_chrome_version(rev)
1082
1083 return rev
1084
1085
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001086def is_inside_chroot():
1087 """Returns True if we are inside chroot."""
1088 return os.path.exists('/etc/cros_chroot_version')
1089
1090
1091def cros_sdk(chromeos_root, *args, **kwargs):
1092 """Run commands inside chromeos chroot.
1093
1094 Args:
1095 chromeos_root: chromeos tree root
1096 *args: command to run
1097 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001098 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001099 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001100 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001101 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001102 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001103 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001104 """
1105 envs = []
1106 for k, v in kwargs.get('env', {}).items():
1107 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1108 envs.append('%s=%s' % (k, v))
1109
1110 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1111 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001112 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001113
1114 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001115 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001116 if kwargs.get('goma_dir'):
1117 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001118
Kuang-che Wu399d4662019-06-06 15:23:37 +08001119 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001120
Kuang-che Wu399d4662019-06-06 15:23:37 +08001121 # In addition to the output of command we are interested, cros_sdk may
1122 # generate its own messages. For example, chroot creation messages if we run
1123 # cros_sdk the first time.
1124 # This is the hack to run dummy command once, so we can get clean output for
1125 # the command we are interested.
1126 cmd = prefix + ['true']
1127 util.check_call(*cmd, cwd=chromeos_root)
1128
1129 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001130 return util.check_output(
1131 *cmd,
1132 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001133 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001134 stdin=kwargs.get('stdin'),
1135 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001136
1137
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001138def create_chroot(chromeos_root):
1139 """Creates ChromeOS chroot.
1140
1141 Args:
1142 chromeos_root: chromeos tree root
1143 """
1144 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1145 return
1146 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1147 return
1148
1149 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1150
1151
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001152def copy_into_chroot(chromeos_root, src, dst):
1153 """Copies file into chromeos chroot.
1154
1155 Args:
1156 chromeos_root: chromeos tree root
1157 src: path outside chroot
1158 dst: path inside chroot
1159 """
1160 # chroot may be an image, so we cannot copy to corresponding path
1161 # directly.
1162 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
1163
1164
1165def exists_in_chroot(chromeos_root, path):
1166 """Determine whether a path exists in the chroot.
1167
1168 Args:
1169 chromeos_root: chromeos tree root
1170 path: path inside chroot, relative to src/scripts
1171
1172 Returns:
1173 True if a path exists
1174 """
1175 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +08001176 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001177 except subprocess.CalledProcessError:
1178 return False
1179 return True
1180
1181
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001182def check_if_need_recreate_chroot(stdout, stderr):
1183 """Analyze build log and determine if chroot should be recreated.
1184
1185 Args:
1186 stdout: stdout output of build
1187 stderr: stderr output of build
1188
1189 Returns:
1190 the reason if chroot needs recreated; None otherwise
1191 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001192 if re.search(
1193 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001194 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001195 return 'EAPI version mismatch'
1196
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001197 if 'Chroot is too new. Consider running:' in stderr:
1198 return 'chroot version is too new'
1199
1200 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001201 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1202 return 'chroot version is too new'
1203
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001204 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1205 if "undefined reference to 'std::__1::basic_string" in stdout:
1206 return 'might be due to compiler change'
1207
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001208 # Detect failures due to file collisions.
1209 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1210 # and conflict with each other. Other possible cases are package renaming or
1211 # refactoring. Let's recreate chroot to work around them.
1212 if 'Detected file collision' in stdout:
1213 # Using wildcard between words because the text wraps to the next line
1214 # depending on length of package name and each line is prefixed with
1215 # package name.
1216 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1217 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1218 # package name (65 now).
1219 m = re.search(
1220 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1221 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1222 if m:
1223 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001224
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001225 return None
1226
1227
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001228def build_packages(chromeos_root,
1229 board,
1230 chrome_root=None,
1231 goma_dir=None,
1232 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001233 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001234
1235 Args:
1236 chromeos_root: chromeos tree root
1237 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001238 chrome_root: Chrome tree root. If specified, build chrome using the
1239 provided tree
1240 goma_dir: Goma installed directory to mount into the chroot. If specified,
1241 build chrome with goma.
1242 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001243 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001244
1245 def has_build_package_argument(argument):
1246 stderr_lines = []
1247 try:
1248 util.check_call(
1249 'src/scripts/build_packages',
1250 '--help',
1251 cwd=chromeos_root,
1252 stderr_callback=stderr_lines.append)
1253 except subprocess.CalledProcessError:
1254 help_output = ''.join(stderr_lines)
1255 return '--[no]%s' % argument in help_output
1256
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001257 common_env = {
1258 'USE': '-cros-debug chrome_internal',
1259 'FEATURES': 'separatedebug',
1260 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001261 stderr_lines = []
1262 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001263 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001264 env = common_env.copy()
1265 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001266 cros_sdk(
1267 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001268 './update_chroot',
1269 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001270 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001271 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001272 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001273
1274 env = common_env.copy()
1275 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001276 './build_packages',
1277 '--board',
1278 board,
1279 '--withdev',
1280 '--noworkon',
1281 '--skip_chroot_upgrade',
1282 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001283 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001284
1285 # `use_any_chrome` flag is default on and will force to use a chrome
1286 # prebuilt even if the version doesn't match.
1287
1288 # As this argument is landed in 12681, we should check if the argument
1289 # exists before adding this.
1290 if has_build_package_argument('use_any_chrome'):
1291 cmd.append('--nouse_any_chrome')
1292
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001293 if goma_dir:
1294 # Tell build_packages to start and stop goma
1295 cmd.append('--run_goma')
1296 env['USE_GOMA'] = 'true'
1297 if afdo_use:
1298 env['USE'] += ' afdo_use'
1299 cros_sdk(
1300 chromeos_root,
1301 *cmd,
1302 env=env,
1303 chrome_root=chrome_root,
1304 stderr_callback=stderr_lines.append,
1305 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001306 except subprocess.CalledProcessError as e:
1307 # Detect failures due to incompatibility between chroot and source tree. If
1308 # so, notify the caller to recreate chroot and retry.
1309 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1310 if reason:
1311 raise NeedRecreateChrootException(reason)
1312
1313 # For other failures, don't know how to handle. Just bail out.
1314 raise
1315
Kuang-che Wu28980b22019-07-31 19:51:45 +08001316
1317def build_image(chromeos_root, board):
1318 """Build ChromeOS image.
1319
1320 Args:
1321 chromeos_root: chromeos tree root
1322 board: ChromeOS board name
1323
1324 Returns:
1325 image folder; relative to chromeos_root
1326 """
1327 stderr_lines = []
1328 try:
1329 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1330 cros_sdk(
1331 chromeos_root,
1332 './build_image',
1333 '--board',
1334 board,
1335 '--noenable_rootfs_verification',
1336 'test',
1337 env={
1338 'USE': '-cros-debug chrome_internal',
1339 'FEATURES': 'separatedebug',
1340 },
1341 stderr_callback=stderr_lines.append)
1342 except subprocess.CalledProcessError as e:
1343 # Detect failures due to incompatibility between chroot and source tree. If
1344 # so, notify the caller to recreate chroot and retry.
1345 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1346 if reason:
1347 raise NeedRecreateChrootException(reason)
1348
1349 # For other failures, don't know how to handle. Just bail out.
1350 raise
1351
1352 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1353 'latest')
1354 assert os.path.exists(image_symlink)
1355 image_name = os.readlink(image_symlink)
1356 image_folder = os.path.join(cached_images_dir, board, image_name)
1357 assert os.path.exists(
1358 os.path.join(chromeos_root, image_folder, test_image_filename))
1359 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001360
1361
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001362class AutotestControlInfo(object):
1363 """Parsed content of autotest control file.
1364
1365 Attributes:
1366 name: test name
1367 path: control file path
1368 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1369 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1370 """
1371
1372 def __init__(self, path, variables):
1373 self.name = variables['NAME']
1374 self.path = path
1375 self.variables = variables
1376
1377
1378def parse_autotest_control_file(path):
1379 """Parses autotest control file.
1380
1381 This only parses simple top-level string assignments.
1382
1383 Returns:
1384 AutotestControlInfo object
1385 """
1386 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001387 with open(path) as f:
1388 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001389 for stmt in code.body:
1390 # Skip if not simple "NAME = *" assignment.
1391 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1392 isinstance(stmt.targets[0], ast.Name)):
1393 continue
1394
1395 # Only support string value.
1396 if isinstance(stmt.value, ast.Str):
1397 variables[stmt.targets[0].id] = stmt.value.s
1398
1399 return AutotestControlInfo(path, variables)
1400
1401
1402def enumerate_autotest_control_files(autotest_dir):
1403 """Enumerate autotest control files.
1404
1405 Args:
1406 autotest_dir: autotest folder
1407
1408 Returns:
1409 list of paths to control files
1410 """
1411 # Where to find control files. Relative to autotest_dir.
1412 subpaths = [
1413 'server/site_tests',
1414 'client/site_tests',
1415 'server/tests',
1416 'client/tests',
1417 ]
1418
1419 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
1420 result = []
1421 for subpath in subpaths:
1422 path = os.path.join(autotest_dir, subpath)
1423 for root, dirs, files in os.walk(path):
1424
1425 for black in blacklist:
1426 if black in dirs:
1427 dirs.remove(black)
1428
1429 for filename in files:
1430 if filename == 'control' or filename.startswith('control.'):
1431 result.append(os.path.join(root, filename))
1432
1433 return result
1434
1435
1436def get_autotest_test_info(autotest_dir, test_name):
1437 """Get metadata of given test.
1438
1439 Args:
1440 autotest_dir: autotest folder
1441 test_name: test name
1442
1443 Returns:
1444 AutotestControlInfo object. None if test not found.
1445 """
1446 for control_file in enumerate_autotest_control_files(autotest_dir):
1447 info = parse_autotest_control_file(control_file)
1448 if info.name == test_name:
1449 return info
1450 return None
1451
1452
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001453def detect_branch_level(branch):
1454 """Given a branch name of manifest-internal, detect it's branch level.
1455
1456 level1: if ChromeOS version is x.0.0
1457 level2: if ChromeOS version is x.x.0
1458 level3: if ChromeOS version is x.x.x
1459 Where x is an non-zero integer.
1460
1461 Args:
1462 branch: branch name or ref name in manifest-internal
1463
1464 Returns:
1465 An integer indicates the branch level, or zero if not detectable.
1466 """
1467 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1468 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1469 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1470
1471 if re.match(level1, branch):
1472 return 1
1473 if re.match(level2, branch):
1474 return 2
1475 if re.match(level3, branch):
1476 return 3
1477 return 0
1478
1479
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001480def get_crosland_link(old, new):
1481 """Generates crosland link between two versions.
1482
1483 Args:
1484 old: ChromeOS version
1485 new: ChromeOS version
1486
1487 Returns:
1488 A crosland url.
1489 """
1490
1491 def version_to_url_parameter(ver):
1492 if is_cros_snapshot_version(ver):
1493 return snapshot_version_split(ver)[2]
1494 return version_to_short(ver)
1495
1496 old_parameter = version_to_url_parameter(old)
1497 new_parameter = version_to_url_parameter(new)
1498 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1499
1500
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001501class ChromeOSSpecManager(codechange.SpecManager):
1502 """Repo manifest related operations.
1503
1504 This class enumerates chromeos manifest files, parses them,
1505 and sync to disk state according to them.
1506 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001507
1508 def __init__(self, config):
1509 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001510 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1511 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001512 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1513 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001514 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001515 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001516 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001517 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1518 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001519
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001520 def lookup_snapshot_manifest_revisions(self, old, new):
1521 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001522
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001523 Returns:
1524 list of (timestamp, commit_id, snapshot_id):
1525 timestamp: integer unix timestamp
1526 commit_id: a string indicates commit hash
1527 snapshot_id: a string indicates snapshot id
1528 """
1529 assert is_cros_snapshot_version(old)
1530 assert is_cros_snapshot_version(new)
1531
1532 gs_path = (
1533 'gs://chromeos-image-archive/{board}-postsubmit/{version}-*/image.zip')
1534 # Try to guess the commit time of a snapshot manifest, it is usually a few
1535 # minutes different between snapshot manifest commit and image.zip
1536 # generate.
1537 try:
1538 old_timestamp = gsutil_stat_update_time(
1539 gs_path.format(board=self.config['board'], version=old)) - 86400
1540 except subprocess.CalledProcessError:
1541 old_timestamp = None
1542 try:
1543 new_timestamp = gsutil_stat_update_time(
1544 gs_path.format(board=self.config['board'], version=new)) + 86400
1545 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1546 # we can find snapshot 5982
1547 # snapshot_id <= 5982 has different commit message format, so we need
1548 # to identify its id in different ways, see below comment for more info.
1549 new_timestamp = max(new_timestamp, 1558657989 + 1)
1550 except subprocess.CalledProcessError:
1551 new_timestamp = None
1552 result = []
1553 _, _, old_snapshot_id = snapshot_version_split(old)
1554 _, _, new_snapshot_id = snapshot_version_split(new)
1555 repo = self.manifest_internal_dir
1556 path = 'snapshot.xml'
1557 branch = 'snapshot'
1558 commits = git_util.get_history(
1559 repo,
1560 path,
1561 branch,
1562 after=old_timestamp,
1563 before=new_timestamp,
1564 with_subject=True)
1565
1566 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1567 # subject, as their subjects are all `Annealing manifest snapshot.`.
1568 # So instead we count the snapshot_id manually.
1569 count = 5982
1570 # There are two snapshot_id = 2633 in commit history, ignore the former
1571 # one.
1572 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1573 # We examine the commits in reverse order as there are some testing
1574 # commits before snapshot_id=2, this method works fine after
1575 # snapshot 2, except snapshot 2633
1576 for commit in reversed(commits):
1577 msg = commit[2]
1578 if commit[1] in ignore_list:
1579 continue
1580
1581 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1582 if match:
1583 snapshot_id = match.group(1)
1584 elif 'Annealing manifest snapshot' in msg:
1585 snapshot_id = str(count)
1586 count -= 1
1587 else:
1588 continue
1589 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1590 result.append((commit[0], commit[1], snapshot_id))
1591 # We find commits in reversed order, now reverse it again to chronological
1592 # order.
1593 return list(reversed(result))
1594
1595 def lookup_build_timestamp(self, rev):
1596 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1597 if is_cros_full_version(rev):
1598 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001599 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001600
1601 def lookup_snapshot_build_timestamp(self, rev):
1602 assert is_cros_snapshot_version(rev)
1603 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1604
1605 def lookup_release_build_timestamp(self, rev):
1606 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001607 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001608 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1609 try:
1610 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1611 'refs/heads/master', path)
1612 except ValueError:
Kuang-che Wuce2f3be2019-10-28 19:44:54 +08001613 raise errors.InternalError(
1614 '%s does not have %s' % (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001615 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001616
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001617 def detect_float_spec_branch_level(self, spec):
1618 results = [
1619 detect_branch_level(branch) for branch in git_util.get_branches(
1620 self.manifest_dir, commit=spec.name)
1621 ]
1622 results = [x for x in results if x > 0]
1623 return min(results) if results else 0
1624
1625 def branch_between_float_specs(self, old_spec, new_spec):
1626 if old_spec.spec_type != codechange.SPEC_FLOAT:
1627 return False
1628 if new_spec.spec_type != codechange.SPEC_FLOAT:
1629 return False
1630
1631 level_old = self.detect_float_spec_branch_level(old_spec)
1632 level_new = self.detect_float_spec_branch_level(new_spec)
1633
1634 if not level_old or not level_new:
1635 logger.warning('branch level detect failed, assume master')
1636 return False
1637 return level_old != level_new
1638
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001639 def collect_float_spec(self, old, new, fixed_specs=None):
1640 assert fixed_specs
1641 branch = None
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001642 old_branches = []
1643 new_branches = []
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001644
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001645 # There is no revision tag in snapshot's xml
1646 if fixed_specs[0].revision:
1647 old_branches = git_util.get_branches(
1648 self.manifest_dir, commit=fixed_specs[0].revision)
1649 if fixed_specs[-1].revision:
1650 new_branches = git_util.get_branches(
1651 self.manifest_dir, commit=fixed_specs[-1].revision)
1652
1653 # 1. if both are not snapshot, do AND operation
1654 # 2. if new is snapshot, branch = master
1655 # 3. if old is snapshot but new is not, respect new's branch
1656 if fixed_specs[0].revision and fixed_specs[-1].revision:
1657 branches = list(set(old_branches) & set(new_branches))
1658 elif not fixed_specs[-1].revision:
1659 branches = ['refs/remotes/origin/master']
1660 else:
1661 branches = new_branches
1662
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001663 if branches:
1664 branch = branches[0]
1665 else:
1666 logger.warning(
1667 'unable to determine float spec branch, '
1668 'old = %s, new = %s', old_branches, new_branches)
1669
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001670 old_timestamp = self.lookup_build_timestamp(old)
1671 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001672 # snapshot time is different from commit time
1673 # usually it's a few minutes different
1674 # 30 minutes should be safe in most cases
1675 if is_cros_snapshot_version(old):
1676 old_timestamp = old_timestamp - 1800
1677 if is_cros_snapshot_version(new):
1678 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001679
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001680 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001681 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001682 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001683 parser = repo_util.ManifestParser(self.manifest_dir)
1684 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001685 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001686 result.append(
1687 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1688 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001689
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001690 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001691 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1692 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1693
1694 # case 1: if both are snapshot, return a list of snapshot
1695 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
1696 return self.collect_snapshot_specs(old, new)
1697
1698 # case 2: if both are release version
1699 # return a list of release version
1700 if is_cros_full_version(old) and is_cros_full_version(new):
1701 return self.collect_release_specs(old, new)
1702
1703 # case 3: return a list of release version and append a snapshot
1704 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001705 result = self.collect_release_specs(
1706 version_to_full(self.config['board'], old),
1707 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001708 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001709 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001710 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08001711 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001712 return result
1713
1714 def collect_snapshot_specs(self, old, new):
1715 assert is_cros_snapshot_version(old)
1716 assert is_cros_snapshot_version(new)
1717
1718 def guess_snapshot_version(board, snapshot_id, old, new):
1719 if old.endswith('-' + snapshot_id):
1720 return old
1721 if new.endswith('-' + snapshot_id):
1722 return new
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001723 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/'
1724 'R*-{snapshot_id}-*'.format(
1725 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001726 for line in gsutil_ls(gs_path, ignore_errors=True):
1727 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
1728 if m:
1729 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001730 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001731
1732 result = []
1733 path = 'snapshot.xml'
1734 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001735 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001736 snapshot_version = guess_snapshot_version(self.config['board'],
1737 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001738 if snapshot_version:
1739 result.append(
1740 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
1741 path))
1742 else:
1743 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001744 return result
1745
1746 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001747 assert is_cros_full_version(old)
1748 assert is_cros_full_version(new)
1749 old_milestone, old_short_version = version_split(old)
1750 new_milestone, new_short_version = version_split(new)
1751
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001752 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001753 for milestone in git_util.list_dir_from_revision(
1754 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
1755 if not milestone.isdigit():
1756 continue
1757 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
1758 continue
1759
Kuang-che Wu74768d32018-09-07 12:03:24 +08001760 files = git_util.list_dir_from_revision(
1761 self.historical_manifest_git_dir, 'refs/heads/master',
1762 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001763
1764 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001765 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001766 short_version, ext = os.path.splitext(fn)
1767 if ext != '.xml':
1768 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001769 if (util.is_version_lesseq(old_short_version, short_version) and
1770 util.is_version_lesseq(short_version, new_short_version) and
1771 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001772 rev = make_cros_full_version(milestone, short_version)
1773 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1774 'refs/heads/master', path)
1775 result.append(
1776 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001777
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001778 def version_key_func(spec):
1779 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001780 return util.version_key_func(short_version)
1781
1782 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001783 assert result[0].name == old
1784 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001785 return result
1786
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001787 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001788 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1789 if is_cros_full_version(rev):
1790 milestone, short_version = version_split(rev)
1791 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
1792 manifest = git_util.get_file_from_revision(
1793 self.historical_manifest_git_dir, 'refs/heads/master', path)
1794 else:
1795 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
1796 commit_id = revisions[0][1]
1797 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
1798 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001799 return manifest
1800
1801 def get_manifest_file(self, rev):
1802 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001803 manifest_name = 'manifest_%s.xml' % rev
1804 manifest_path = os.path.join(self.manifest_dir, manifest_name)
1805 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001806 f.write(self.get_manifest(rev))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001807 return manifest_name
1808
1809 def parse_spec(self, spec):
1810 parser = repo_util.ManifestParser(self.manifest_dir)
1811 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001812 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001813 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08001814 with open(manifest_path) as f:
1815 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001816 root = parser.parse_single_xml(content, allow_include=False)
1817 else:
1818 root = parser.parse_xml_recursive(spec.name, spec.path)
1819
1820 spec.entries = parser.process_parsed_result(root)
1821 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08001822 if not spec.is_static():
1823 raise ValueError(
1824 'fixed spec %r has unexpected floating entries' % spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001825 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001826
1827 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001828 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001829
1830 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
1831 # manifest. 'repo sync -m' is not enough
1832 repo_util.init(
1833 self.config['chromeos_root'],
1834 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
1835 manifest_name=manifest_name,
1836 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001837 reference=self.config['chromeos_mirror'],
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001838 )
1839
1840 # Note, don't sync with current_branch=True for chromeos. One of its
1841 # build steps (inside mark_as_stable) executes "git describe" which
1842 # needs git tag information.
1843 repo_util.sync(self.config['chromeos_root'])