blob: b37bf295285a748c5121225c95638aa0b5445adf [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
Kuang-che Wu9d14c162020-11-03 19:35:18 +080020import glob
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080021import json
22import logging
23import os
24import re
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +080025import shutil
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080026import subprocess
Kuang-che Wud558a042020-06-06 02:11:00 +080027import sys
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080028import time
29
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080030from google.protobuf import json_format
31
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080032from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080033from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080034from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080035from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080036from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080037from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080038from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080039from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080040from bisect_kit import util
41
42logger = logging.getLogger(__name__)
43
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080045re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080046re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080047
48gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
49gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080050 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080051
52# Assume gsutil is in PATH.
53gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080054
55# Since snapshots with version >= 12618.0.0 have android and chrome version
56# info.
57snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080058
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080059# current earliest buildbucket buildable versions
60# picked from https://crrev.com/c/2072618
61buildbucket_cutover_versions = [
62 '12931.0.0',
63 '12871.26.0', # R81
64 '12871.24.2', # stabilize-12871.24.B
65 '12812.10.0', # factory-excelsior-12812.B
66 '12768.14.0', # firmware-servo-12768.B
67 '12739.85.0', # R80
68 '12739.67.1', # stabilize-excelsior-12739.67.B
69 '12692.36.0', # factory-hatch-12692.B
70 '12672.104.0', # firmware-hatch-12672.B
71 '12607.110.0', # R79
72 '12607.83.2', # stabilize-quickfix-12607.83.B
73 '12587.59.0', # factory-kukui-12587.B
74 '12573.78.0', # firmware-kukui-12573.B
75 '12499.96.0', # R78
76 '12422.33.0', # firmware-mistral-12422.B
77 '12371.190.0', # R77
78 '12361.38.0', # factory-mistral-12361.B
79 '12200.65.0', # firmware-sarien-12200.B
80 '12105.128.0', # R75
81 '12033.82.0', # factory-sarien-12033.B
82]
83
Kuang-che Wub9705bd2018-06-28 17:59:18 +080084chromeos_root_inside_chroot = '/mnt/host/source'
85# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080086prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080087prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080088# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
89cached_images_dir = 'src/build/images'
90test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080091
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080092VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
93VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
94VERSION_KEY_MILESTONE = 'milestone'
95VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080096VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080097VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +080098CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080099
100
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800101class NeedRecreateChrootException(Exception):
102 """Failed to build ChromeOS because of chroot mismatch or corruption"""
103
104
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800105def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800106 """Determines if `s` is chromeos short version.
107
108 This function doesn't accept version number of local build.
109 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800110 return bool(re.match(re_chromeos_short_version, s))
111
112
113def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800114 """Determines if `s` is chromeos full version.
115
116 This function doesn't accept version number of local build.
117 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800118 return bool(re.match(re_chromeos_full_version, s))
119
120
121def is_cros_version(s):
122 """Determines if `s` is chromeos version (either short or full)"""
123 return is_cros_short_version(s) or is_cros_full_version(s)
124
125
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800126def is_cros_snapshot_version(s):
127 """Determines if `s` is chromeos snapshot version"""
128 return bool(re.match(re_chromeos_snapshot_version, s))
129
130
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800131def is_cros_version_lesseq(ver1, ver2):
132 """Determines if ver1 is less or equal to ver2.
133
134 Args:
135 ver1: a Chrome OS version in short, full, or snapshot format.
136 ver2: a Chrome OS version in short, full, or snapshot format.
137
138 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800139 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800140 """
141 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
142 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
143
144 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
145 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
146 return ver1 <= ver2
147
148
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800149def is_buildbucket_buildable(version):
150 """Determines if a version is buildable on buildbucket."""
151 short_version = version_to_short(version)
152 # If given version is child of any cutover, then it's buildable
153 return any([
154 util.is_direct_relative_version(x, short_version) and
155 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
156 ])
157
158
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800159def make_cros_full_version(milestone, short_version):
160 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800161 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800162 return 'R%s-%s' % (milestone, short_version)
163
164
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800165def make_cros_snapshot_version(milestone, short_version, snapshot_id):
166 """Makes snapshot version from milestone, short_version and snapshot id"""
167 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
168
169
170def version_split(version):
171 """Splits full_version or snapshot_version into milestone and short_version"""
172 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
173 if is_cros_snapshot_version(version):
174 return snapshot_version_split(version)[0:2]
175 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800176 return milestone[1:], short_version
177
178
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800179def snapshot_version_split(snapshot_version):
180 """Splits snapshot_version into milestone, short_version and snapshot_id"""
181 assert is_cros_snapshot_version(snapshot_version)
182 milestone, shot_version, snapshot_id = snapshot_version.split('-')
183 return milestone[1:], shot_version, snapshot_id
184
185
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800186def query_snapshot_buildbucket_id(board, snapshot_version):
187 """Query buildbucket id of a snapshot"""
188 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800189 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800190 '/{snapshot_version}-*/image.zip')
191 output = gsutil_ls(
192 '-d',
193 path.format(board=board, snapshot_version=snapshot_version),
194 ignore_errors=True)
195 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800196 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800197 if m:
198 return m.group(1)
199 return None
200
201
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800202def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800203 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800204 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800205 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 +0800206 return s
207
208
209def query_dut_lsb_release(host):
210 """Query /etc/lsb-release of given DUT
211
212 Args:
213 host: the DUT address
214
215 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800216 dict for keys and values of /etc/lsb-release.
217
218 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800219 errors.SshConnectionError: cannot connect to host
220 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800221 """
222 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800223 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800224 except subprocess.CalledProcessError as e:
225 raise errors.ExternalError(
226 'unable to read /etc/lsb-release; not a DUT') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800227 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
228
229
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800230def query_dut_os_release(host):
231 """Query /etc/os-release of given DUT
232
233 Args:
234 host: the DUT address
235
236 Returns:
237 dict for keys and values of /etc/os-release.
238
239 Raises:
240 errors.SshConnectionError: cannot connect to host
241 errors.ExternalError: lsb-release file doesn't exist
242 """
243 try:
244 output = util.ssh_cmd(host, 'cat', '/etc/os-release')
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800245 except subprocess.CalledProcessError as e:
246 raise errors.ExternalError(
247 'unable to read /etc/os-release; not a DUT') from e
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800248 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
249
250
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800251def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800252 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800253
254 Args:
255 host: the DUT address
256
257 Returns:
258 True if the host is a chromeos device.
259 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800260 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800261 return query_dut_os_release(host).get('ID') in [
262 'chromiumos',
263 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800264 ]
265 except (errors.ExternalError, errors.SshConnectionError):
266 return False
267
268
269def is_good_dut(host):
270 if not is_dut(host):
271 return False
272
273 # Sometimes python is broken after 'cros flash'.
274 try:
275 util.ssh_cmd(host, 'python', '-c', '1')
276 return True
277 except (subprocess.CalledProcessError, errors.SshConnectionError):
278 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800279
280
281def query_dut_board(host):
282 """Query board name of a given DUT"""
283 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
284
285
286def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800287 """Query short version of a given DUT.
288
289 This function may return version of local build, which
290 is_cros_short_version() is false.
291 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800292 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
293
294
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800295def query_dut_prebuilt_version(host):
296 """Return a snapshot version or short version of a given DUT.
297
298 Args:
299 host: dut host
300
301 Returns:
302 Snapshot version or short version.
303 """
304 lsb_release = query_dut_lsb_release(host)
305 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
306 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH')
307 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
308 builder_path)
309 if match:
310 return match.group(1)
311 return release_version
312
313
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800314def query_dut_is_by_official_builder(host):
Kuang-che Wuc092bd52021-01-05 14:25:56 +0800315 """Query if given DUT is build by official builder"""
316 build_type = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
317 '')
318 build_type = build_type.split(' - ')[0]
319 assert build_type in ('Official Build', 'Continuous Builder',
320 'Developer Build',
321 'Test Build'), 'unknown build type (%s)' % build_type
322 return build_type in ['Official Build', 'Continuous Builder']
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800323
324
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800325def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800326 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800327
328 Args:
329 host: DUT address
330 connect_timeout: connection timeout
331
332 Returns:
333 boot uuid
334 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800335 return util.ssh_cmd(
336 host,
337 'cat',
338 '/proc/sys/kernel/random/boot_id',
339 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800340
341
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800342def reboot(host, force_reboot_callback=None):
343 """Reboot a DUT and verify.
344
345 Args:
346 host: DUT address
347 force_reboot_callback: powerful reboot hook (via servo). This will be
348 invoked if normal reboot failed.
349 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800350 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800351 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800352 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800353 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800354
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800355 try:
356 util.ssh_cmd(host, 'reboot')
357 except errors.SshConnectionError:
358 # Depends on timing, ssh may return failure due to broken pipe, which is
359 # working as intended. Ignore such kind of errors.
360 pass
361
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800362 wait_reboot_done(host, boot_id)
363 except (errors.SshConnectionError, errors.ExternalError):
364 if force_reboot_callback and force_reboot_callback(host):
365 wait_reboot_done(host, boot_id)
366 return
367 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800368
Kuang-che Wu708310b2018-03-28 17:24:34 +0800369
370def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800371 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800372 # (dev screen short delay) or more (long delay).
373 time.sleep(15)
374 for _ in range(100):
375 try:
376 # During boot, DUT does not response and thus ssh may hang a while. So
377 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
378 # set tight limit because it's inside retry loop.
379 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
380 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800381 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800382 logger.debug('reboot not ready? sleep wait 1 sec')
383 time.sleep(1)
384
Kuang-che Wue121fae2018-11-09 16:18:39 +0800385 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800386
387
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800388def gs_release_boardpath(board):
389 """Normalizes board name for gs://chromeos-releases/
390
391 This follows behavior of PushImage() in chromite/scripts/pushimage.py
392 Note, only gs://chromeos-releases/ needs normalization,
393 gs://chromeos-image-archive does not.
394
395 Args:
396 board: ChromeOS board name
397
398 Returns:
399 normalized board name
400 """
401 return board.replace('_', '-')
402
403
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800404def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800405 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800406
407 Args:
408 args: command line arguments passed to gsutil
409 kwargs:
410 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
411 but the path not found.
412
413 Returns:
414 stdout of gsutil
415
416 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800417 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800418 subprocess.CalledProcessError: command failed
419 """
420 stderr_lines = []
421 try:
422 return util.check_output(
423 gsutil_bin, *args, stderr_callback=stderr_lines.append)
424 except subprocess.CalledProcessError as e:
425 stderr = ''.join(stderr_lines)
426 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800427 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800428 'gsutil failed due to permission. ' +
429 'Run "%s config" and follow its instruction. ' % gsutil_bin +
430 'Fill any string if it asks for project-id')
431 if kwargs.get('ignore_errors'):
432 return ''
433 raise
434 except OSError as e:
435 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800436 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800437 'Unable to run %s. gsutil is not installed or not in PATH?' %
438 gsutil_bin)
439 raise
440
441
442def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800443 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800444
445 Args:
446 args: arguments passed to 'gsutil ls'
447 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800448 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800449 exception, ex. path not found.
450
451 Returns:
452 list of 'gsutil ls' result. One element for one line of gsutil output.
453
454 Raises:
455 subprocess.CalledProcessError: gsutil failed, usually means path not found
456 """
457 return gsutil('ls', *args, **kwargs).splitlines()
458
459
Kuang-che Wu876a5382020-10-27 14:24:58 +0800460def gsutil_stat_creation_time(*args, **kwargs):
461 """Returns the creation time of a file or multiple files.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800462
463 Args:
464 args: arguments passed to 'gsutil stat'.
465 kwargs: extra parameters for gsutil.
466
467 Returns:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800468 A integer indicates the creation timestamp.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800469
470 Raises:
471 subprocess.CalledProcessError: gsutil failed, usually means path not found
Kuang-che Wu876a5382020-10-27 14:24:58 +0800472 errors.ExternalError: creation time is not found
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800473 """
474 result = -1
475 # Currently we believe stat always returns a UTC time, and strptime also
476 # parses a UTC time by default.
477 time_format = '%a, %d %b %Y %H:%M:%S GMT'
478
479 for line in gsutil('stat', *args, **kwargs).splitlines():
480 if ':' not in line:
481 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800482 key, value = line.split(':', 1)
483 key, value = key.strip(), value.strip()
Kuang-che Wu876a5382020-10-27 14:24:58 +0800484 if key != 'Creation time':
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800485 continue
486 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800487 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800488 result = max(result, unixtime)
489
490 if result == -1:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800491 raise errors.ExternalError("didn't find creation time")
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800492 return result
493
494
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800495def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800496 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800497
498 Args:
499 board: ChromeOS board name
500 short_version: ChromeOS version number in short format, ex. 9300.0.0
501
502 Returns:
503 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
504 None if failed.
505 """
506 path = gs_archive_path.format(board=board) + '/R*-' + short_version
507 for line in gsutil_ls('-d', path, ignore_errors=True):
508 m = re.search(r'/R(\d+)-', line)
509 if not m:
510 continue
511 return m.group(1)
512
513 for channel in ['canary', 'dev', 'beta', 'stable']:
514 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800515 channel=channel,
516 boardpath=gs_release_boardpath(board),
517 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800518 for line in gsutil_ls(path, ignore_errors=True):
519 m = re.search(r'\bR(\d+)-' + short_version, line)
520 if not m:
521 continue
522 return m.group(1)
523
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800524 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800525 return None
526
527
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800528def list_board_names(chromeos_root):
529 """List board names.
530
531 Args:
532 chromeos_root: chromeos tree root
533
534 Returns:
535 list of board names
536 """
537 # Following logic is simplified from chromite/lib/portage_util.py
538 cros_list_overlays = os.path.join(chromeos_root,
539 'chromite/bin/cros_list_overlays')
540 overlays = util.check_output(cros_list_overlays).splitlines()
541 result = set()
542 for overlay in overlays:
543 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
544 name = None
545 if os.path.exists(conf_file):
546 for line in open(conf_file):
547 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
548 if m:
549 name = m.group(1)
550 break
551
552 if not name:
553 name_file = os.path.join(overlay, 'profiles', 'repo_name')
554 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800555 with open(name_file) as f:
556 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800557
558 if name:
559 name = re.sub(r'-private$', '', name)
560 result.add(name)
561
562 return list(result)
563
564
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800565def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800566 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800567
568 Args:
569 board: ChromeOS board name
570 version: ChromeOS version number in short or full format
571
572 Returns:
573 (milestone, version in short format)
574 """
575 if is_cros_short_version(version):
576 milestone = query_milestone_by_version(board, version)
577 short_version = version
578 else:
579 milestone, short_version = version_split(version)
580 return milestone, short_version
581
582
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800583def extract_major_version(version):
584 """Converts a version to its major version.
585
586 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800587 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800588
589 Returns:
590 major version number in string format
591 """
592 version = version_to_short(version)
593 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
594 return m.group(1)
595
596
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800597def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800598 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800599
600 Args:
601 version: ChromeOS version number in short or full format
602
603 Returns:
604 version number in short format
605 """
606 if is_cros_short_version(version):
607 return version
608 _, short_version = version_split(version)
609 return short_version
610
611
612def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800613 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800614
615 Args:
616 board: ChromeOS board name
617 version: ChromeOS version number in short or full format
618
619 Returns:
620 version number in full format
621 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800622 if is_cros_snapshot_version(version):
623 milestone, short_version, _ = snapshot_version_split(version)
624 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800625 if is_cros_full_version(version):
626 return version
627 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800628 if not milestone:
629 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
630 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800631 return make_cros_full_version(milestone, version)
632
633
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800634def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800635 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800636
637 Args:
638 board: ChromeOS board
639 major_version: ChromeOS major version
640
641 Returns:
642 list of (version, gs_path):
643 version: Chrome OS snapshot version
644 gs_path: gs path of test image
645 """
646
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800647 def extract_snapshot_id(result):
648 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
649 assert m
650 return int(m.group(1))
651
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800652 short_version = '%s.0.0' % major_version
653 milestone = query_milestone_by_version(board, short_version)
654 if not milestone:
655 milestone = '*'
656
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800657 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800658 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800659 result = []
660 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800661 path.format(
662 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800663 ignore_errors=True)
664
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800665 for gs_path in sorted(output):
666 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800667 if m:
668 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800669 # we should skip if there is duplicate snapshot
670 if result and result[-1][0] == snapshot_version:
671 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800672
673 # b/151054108: snapshot version in [29288, 29439] is broken
674 _, _, snapshot_id = snapshot_version_split(snapshot_version)
675 if 29288 <= int(snapshot_id) <= 29439:
676 continue
677
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800678 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800679
680 # sort by its snapshot_id
681 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800682 return result
683
684
Kuang-che Wu575dc442019-03-05 10:30:55 +0800685def list_prebuilt_from_image_archive(board):
686 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
687
688 gs://chromeos-image-archive contains only recent builds (in two years).
689 We prefer this function to list_prebuilt_from_chromeos_releases() because
690 - this is what "cros flash" supports directly.
691 - the paths have milestone information, so we don't need to do slow query
692 by ourselves.
693
694 Args:
695 board: ChromeOS board name
696
697 Returns:
698 list of (version, gs_path):
699 version: Chrome OS version in full format
700 gs_path: gs path of test image
701 """
702 result = []
703 for line in gsutil_ls(gs_archive_path.format(board=board)):
704 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
705 if m:
706 full_version = m.group(1)
707 test_image = 'chromiumos_test_image.tar.xz'
708 assert line.endswith('/')
709 gs_path = line + test_image
710 result.append((full_version, gs_path))
711 return result
712
713
714def list_prebuilt_from_chromeos_releases(board):
715 """Lists ChromeOS versions available from gs://chromeos-releases.
716
717 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
718 support it.
719
720 Args:
721 board: ChromeOS board name
722
723 Returns:
724 list of (version, gs_path):
725 version: Chrome OS version in short format
726 gs_path: gs path of test image (with wildcard)
727 """
728 result = []
729 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800730 gs_release_path.format(
731 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800732 ignore_errors=True):
733 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
734 if m:
735 short_version = m.group(1)
736 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
737 short_version=short_version, board=board)
738 gs_path = line + test_image
739 result.append((short_version, gs_path))
740 return result
741
742
Kuang-che Wue1808402020-01-06 20:27:45 +0800743def has_test_image(board, version):
744 if is_cros_snapshot_version(version):
745 return bool(query_snapshot_buildbucket_id(board, version))
746
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800747 try:
748 full_version = version_to_full(board, version)
749 except errors.ExternalError:
750 # version_to_full() is implemented by checking image, thus its failure
751 # means no image.
752 return False
Kuang-che Wue1808402020-01-06 20:27:45 +0800753 short_version = version_to_short(version)
754 paths = [
755 gs_archive_path.format(board=board) +
756 '/%s/chromiumos_test_image.tar.xz' % full_version,
757 gs_release_path.format(
758 channel='*',
759 boardpath=gs_release_boardpath(board),
760 short_version=short_version),
761 ]
762
763 for path in paths:
764 if gsutil_ls(path, ignore_errors=True):
765 return True
766 return False
767
768
Kuang-che Wu575dc442019-03-05 10:30:55 +0800769def list_chromeos_prebuilt_versions(board,
770 old,
771 new,
772 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800773 include_older_build=True,
774 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800775 """Lists ChromeOS version numbers with prebuilt between given range
776
777 Args:
778 board: ChromeOS board name
779 old: start version (inclusive)
780 new: end version (inclusive)
781 only_good_build: only if test image is available
782 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800783 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800784
785 Returns:
786 list of sorted version numbers (in full format) between [old, new] range
787 (inclusive).
788 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800789 old_short = version_to_short(old)
790 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800791
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800792 rev_map = {
793 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800794 for full_version, gs_path in list_prebuilt_from_image_archive(board):
795 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800796 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800797
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800798 if include_older_build and old_short not in rev_map:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800799 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
800 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800801 rev_map[short_version] = [(short_version, gs_path)]
802
803 if use_snapshot:
804 for major_version in range(
805 int(extract_major_version(old)),
806 int(extract_major_version(new)) + 1):
807 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800808 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800809 # If current version is smaller than cutover, ignore it as it might not
810 # contain enough information for continuing android and chrome bisection.
811 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
812 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800813
814 # Given the fact that snapshots are images between two release versions.
815 # Adding snapshots of 12345.0.0 should be treated as adding commits
816 # between [12345.0.0, 12346.0.0).
817 # So in the following lines we check two facts:
818 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800819 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800820 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800821 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800822 continue
823 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800824 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800825 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800826 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800827 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800828
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800829 snapshots = list_snapshots_from_image_archive(board, str(major_version))
830 if snapshots:
831 # if snapshots found, we can append them after the release version,
832 # so the prebuilt image list of this version will be
833 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800834 if short_version not in rev_map:
835 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800836 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800837
838 result = []
839 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800840 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800841 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800842 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800843 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800844 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800845 continue
846
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800847 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800848
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800849 # version_to_full() and gsutil_ls() may take long time if versions are a
850 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800851
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800852 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800853 gs_result = gsutil_ls(gs_path, ignore_errors=True)
854 if not gs_result:
855 logger.warning('%s is not a good build, ignore', version)
856 continue
857 assert len(gs_result) == 1
858 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
859 if not m:
860 logger.warning('format of image path is unexpected: %s', gs_result[0])
861 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800862 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800863 elif is_cros_short_version(version):
864 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800865
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800866 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
867 version, new):
868 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800869
870 return result
871
872
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800873def prepare_snapshot_image(chromeos_root, board, snapshot_version):
874 """Prepare chromeos snapshot image.
875
876 Args:
877 chromeos_root: chromeos tree root
878 board: ChromeOS board name
879 snapshot_version: ChromeOS snapshot version number
880
881 Returns:
882 local file path of test image relative to chromeos_root
883 """
884 assert is_cros_snapshot_version(snapshot_version)
885 milestone, short_version, snapshot_id = snapshot_version_split(
886 snapshot_version)
887 full_version = make_cros_full_version(milestone, short_version)
888 tmp_dir = os.path.join(
889 chromeos_root, 'tmp',
890 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
891 if not os.path.exists(tmp_dir):
892 os.makedirs(tmp_dir)
893
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800894 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/' +
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800895 '{snapshot_version}-*/image.zip')
896 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
897
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800898 full_path = os.path.join(tmp_dir, test_image_filename)
899 rel_path = os.path.relpath(full_path, chromeos_root)
900 if os.path.exists(full_path):
901 return rel_path
902
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800903 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800904 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800905 gs_path = files[0]
906 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800907 util.check_call(
908 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
909 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800910 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800911
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800912 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800913 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800914
915
Kuang-che Wu28980b22019-07-31 19:51:45 +0800916def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800917 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800918
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800919 It searches for xbuddy image which "cros flash" can use, or fetch image to
920 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800921
922 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800923 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800924 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800925 version: ChromeOS version number in short or full format
926
927 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800928 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800929 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800930 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800931 full_version = version_to_full(board, version)
932 short_version = version_to_short(full_version)
933
934 image_path = None
935 gs_path = gs_archive_path.format(board=board) + '/' + full_version
936 if gsutil_ls('-d', gs_path, ignore_errors=True):
937 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
938 board=board, full_version=full_version)
939 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800940 tmp_dir = os.path.join(chromeos_root, 'tmp',
941 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800942 full_path = os.path.join(tmp_dir, test_image_filename)
943 rel_path = os.path.relpath(full_path, chromeos_root)
944 if os.path.exists(full_path):
945 return rel_path
946
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800947 if not os.path.exists(tmp_dir):
948 os.makedirs(tmp_dir)
949 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800950 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800951 # to fetch the image by ourselves
952 for channel in ['canary', 'dev', 'beta', 'stable']:
953 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
954 full_version=full_version, board=board)
955 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800956 channel=channel,
957 boardpath=gs_release_boardpath(board),
958 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800959 gs_path += '/' + fn
960 if gsutil_ls(gs_path, ignore_errors=True):
961 # TODO(kcwu): delete tmp
962 gsutil('cp', gs_path, tmp_dir)
963 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800964 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800965 break
966
967 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800968 return image_path
969
970
971def cros_flash(chromeos_root,
972 host,
973 board,
974 image_path,
975 version=None,
976 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800977 disable_rootfs_verification=True,
978 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800979 """Flash a DUT with given ChromeOS image.
980
981 This is implemented by 'cros flash' command line.
982
983 Args:
984 chromeos_root: use 'cros flash' of which chromeos tree
985 host: DUT address
986 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800987 image_path: chromeos image xbuddy path or file path. For relative
988 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800989 version: ChromeOS version in short or full format
990 clobber_stateful: Clobber stateful partition when performing update
991 disable_rootfs_verification: Disable rootfs verification after update
992 is completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800993 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800994
995 Raises:
996 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800997 """
998 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
999
1000 # Reboot is necessary because sometimes previous 'cros flash' failed and
1001 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001002 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001003
Kuang-che Wu020a1182020-09-08 17:17:22 +08001004 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001005 # The service is used in jetstream boards, but not other CrOS devices.
1006 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
1007 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001008 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
1009 # For simplicity, we wait 60 seconds here, which is the timeout value of
1010 # jetstream_host.
1011 # https://chromium.googlesource.com/chromiumos/third_party/autotest
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001012 # /+/HEAD/server/hosts/jetstream_host.py#27
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001013 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001014 util.ssh_cmd(host, 'stop', 'ap-update-manager')
1015 except subprocess.CalledProcessError:
1016 pass # not started; do nothing
1017
Kuang-che Wu28980b22019-07-31 19:51:45 +08001018 # Handle relative path.
1019 if '://' not in image_path and not os.path.isabs(image_path):
1020 assert os.path.exists(os.path.join(chromeos_root, image_path))
1021 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
1022
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001023 args = [
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001024 '--debug',
1025 '--no-ping',
1026 # Speed up for slow network connection.
1027 '--send-payload-in-parallel',
1028 host,
1029 image_path,
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001030 ]
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001031 # TODO(kcwu): remove this check if we don't need to support chromeos versions
1032 # earlier than Dec 2020.
1033 if git_util.is_ancestor_commit(
1034 os.path.join(chromeos_root, 'chromite'), '9ed30bc3ed292b', 'HEAD'):
1035 # To reduce disk usage on DUT.
1036 args.append('--no-copy-payloads-to-device')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001037 if clobber_stateful:
1038 args.append('--clobber-stateful')
1039 if disable_rootfs_verification:
1040 args.append('--disable-rootfs-verification')
1041
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001042 try:
1043 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001044 except subprocess.CalledProcessError as e:
1045 raise errors.ExternalError('cros flash failed') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001046
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001047 if version:
1048 # In the past, cros flash may fail with returncode=0
1049 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001050 if is_cros_snapshot_version(version):
1051 builder_path = query_dut_lsb_release(host).get(
1052 'CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001053 expect_prefix = '%s-snapshot/%s-' % (board, version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001054 if not builder_path.startswith(expect_prefix):
1055 raise errors.ExternalError(
1056 'although cros flash succeeded, the OS builder path is '
1057 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1058 else:
1059 expect_version = version_to_short(version)
1060 dut_version = query_dut_short_version(host)
1061 if dut_version != expect_version:
1062 raise errors.ExternalError(
1063 'although cros flash succeeded, the OS version is unexpected: '
1064 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001065
Kuang-che Wu4a81ea72019-10-05 15:35:17 +08001066 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001067 # (b/130786578), so it's necessary to do sanity check.
1068 if not is_good_dut(host):
1069 raise errors.ExternalError(
1070 'although cros flash succeeded, the DUT is in bad state')
1071
1072
1073def cros_flash_with_retry(chromeos_root,
1074 host,
1075 board,
1076 image_path,
1077 version=None,
1078 clobber_stateful=False,
1079 disable_rootfs_verification=True,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001080 repair_callback=None,
1081 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001082 # 'cros flash' is not 100% reliable, retry if necessary.
1083 for attempt in range(2):
1084 if attempt > 0:
1085 logger.info('will retry 60 seconds later')
1086 time.sleep(60)
1087
1088 try:
1089 cros_flash(
1090 chromeos_root,
1091 host,
1092 board,
1093 image_path,
1094 version=version,
1095 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001096 disable_rootfs_verification=disable_rootfs_verification,
1097 force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001098 return True
1099 except errors.ExternalError:
1100 logger.exception('cros flash failed')
1101 if repair_callback and not repair_callback(host):
1102 logger.warning('not repaired, assume it is harmless')
1103 continue
1104 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001105
1106
1107def version_info(board, version):
1108 """Query subcomponents version info of given version of ChromeOS
1109
1110 Args:
1111 board: ChromeOS board name
1112 version: ChromeOS version number in short or full format
1113
1114 Returns:
1115 dict of component and version info, including (if available):
1116 cros_short_version: ChromeOS version
1117 cros_full_version: ChromeOS version
1118 milestone: milestone of ChromeOS
1119 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001120 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001121 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1122 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001123 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001124 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001125 milestone, short_version, _ = snapshot_version_split(version)
1126 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001127 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001128 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001129 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001130 VERSION_KEY_MILESTONE: milestone,
1131 VERSION_KEY_CROS_FULL_VERSION: version,
1132 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001133 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1134 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1135 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001136 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001137 info = {}
1138 full_version = version_to_full(board, version)
1139
1140 # Some boards may have only partial-metadata.json but no metadata.json.
1141 # e.g. caroline R60-9462.0.0
1142 # Let's try both.
1143 metadata = None
1144 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001145 path = gs_archive_path.format(
1146 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001147 metadata = gsutil('cat', path, ignore_errors=True)
1148 if metadata:
1149 o = json.loads(metadata)
1150 v = o['version']
1151 board_metadata = o['board-metadata'][board]
1152 info.update({
1153 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1154 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1155 VERSION_KEY_MILESTONE: v['milestone'],
1156 VERSION_KEY_CR_VERSION: v['chrome'],
1157 })
1158
1159 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001160 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001161 if 'android-branch' in v: # this appears since R58-9317.0.0
1162 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1163 elif 'android-container-branch' in board_metadata:
1164 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1165 break
1166 else:
1167 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1168 logger.error(
1169 'Note, so far no quick way to look up version info for too old builds')
1170
1171 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001172
1173
1174def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001175 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001176
1177 Args:
1178 board: ChromeOS board name
1179 version: ChromeOS version number in short or full format
1180
1181 Returns:
1182 Chrome version number
1183 """
1184 info = version_info(board, version)
1185 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001186
1187
1188def query_android_build_id(board, rev):
1189 info = version_info(board, rev)
1190 rev = info['android_build_id']
1191 return rev
1192
1193
1194def query_android_branch(board, rev):
1195 info = version_info(board, rev)
1196 rev = info['android_branch']
1197 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001198
1199
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001200def guess_chrome_version(board, rev):
1201 """Guess chrome version number.
1202
1203 Args:
1204 board: chromeos board name
1205 rev: chrome or chromeos version
1206
1207 Returns:
1208 chrome version number
1209 """
1210 if is_cros_version(rev):
1211 assert board, 'need to specify BOARD for cros version'
1212 rev = query_chrome_version(board, rev)
1213 assert cr_util.is_chrome_version(rev)
1214
1215 return rev
1216
1217
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001218def is_inside_chroot():
1219 """Returns True if we are inside chroot."""
1220 return os.path.exists('/etc/cros_chroot_version')
1221
1222
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001223def convert_path_outside_chroot(chromeos_root, path):
1224 """Converts path in chroot to outside.
1225
1226 Args:
1227 chromeos_root: chromeos tree root
1228 path: path inside chroot; support starting with '~/'
1229
1230 Returns:
1231 The corresponding path outside chroot assuming the chroot is mounted
1232 """
1233 if path.startswith('~/'):
1234 path = path.replace('~', '/home/' + os.environ['USER'])
1235 assert '~' not in path, 'tilde (~) character is not fully supported'
1236
1237 assert os.path.isabs(path)
1238 assert path[0] == os.sep
1239 return os.path.join(chromeos_root, 'chroot', path[1:])
1240
1241
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001242def cros_sdk(chromeos_root, *args, **kwargs):
1243 """Run commands inside chromeos chroot.
1244
1245 Args:
1246 chromeos_root: chromeos tree root
1247 *args: command to run
1248 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001249 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001250 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001251 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001252 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001253 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001254 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001255 """
1256 envs = []
1257 for k, v in kwargs.get('env', {}).items():
1258 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1259 envs.append('%s=%s' % (k, v))
1260
1261 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1262 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001263 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001264
1265 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001266 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001267 if kwargs.get('goma_dir'):
1268 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001269
Kuang-che Wu399d4662019-06-06 15:23:37 +08001270 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001271
Kuang-che Wu399d4662019-06-06 15:23:37 +08001272 # In addition to the output of command we are interested, cros_sdk may
1273 # generate its own messages. For example, chroot creation messages if we run
1274 # cros_sdk the first time.
1275 # This is the hack to run dummy command once, so we can get clean output for
1276 # the command we are interested.
1277 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001278 try:
1279 util.check_call(*cmd, cwd=chromeos_root)
1280 except subprocess.CalledProcessError:
1281 logger.exception('cros_sdk init/update failed')
1282 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001283
1284 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001285 return util.check_output(
1286 *cmd,
1287 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001288 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001289 stdin=kwargs.get('stdin'),
1290 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001291
1292
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001293def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001294 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001295
1296 Args:
1297 chromeos_root: chromeos tree root
1298 """
1299 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1300 return
1301 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1302 return
1303
1304 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1305
1306
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001307def mount_chroot(chromeos_root):
1308 """Creates ChromeOS chroot if necessary.
1309
1310 Args:
1311 chromeos_root: chromeos tree root
1312 """
1313 # An arbitrary file must exist in chroot.
1314 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1315
1316 # Not created or mounted yet.
1317 if not os.path.exists(path):
1318 create_chroot(chromeos_root)
1319 # After this command, the chroot is mounted.
1320 cros_sdk(chromeos_root, 'true')
1321 assert os.path.exists(path)
1322
1323
1324def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001325 """Copies file into chromeos chroot.
1326
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001327 The side effect is chroot created and mounted.
1328
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001329 Args:
1330 chromeos_root: chromeos tree root
1331 src: path outside chroot
1332 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001333 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001334 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001335 mount_chroot(chromeos_root)
1336 src = os.path.expanduser(src)
1337 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1338 if not overwrite and os.path.exists(dst_outside):
1339 return
1340
1341 # Haven't support directory or special files yet.
1342 assert os.path.isfile(src)
1343 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1344
1345 dirname = os.path.dirname(dst_outside)
1346 if not os.path.exists(dirname):
1347 os.makedirs(dirname)
1348 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001349
1350
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001351def prepare_chroot(chromeos_root):
1352 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001353
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001354 # Work around b/149077936:
1355 # The creds file is copied into the chroot since 12866.0.0.
1356 # But earlier versions need this file as well because of cipd ACL change.
1357 creds_path = '~/.config/chrome_infra/auth/creds.json'
1358 if os.path.exists(os.path.expanduser(creds_path)):
1359 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001360
1361
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001362def check_if_need_recreate_chroot(stdout, stderr):
1363 """Analyze build log and determine if chroot should be recreated.
1364
1365 Args:
1366 stdout: stdout output of build
1367 stderr: stderr output of build
1368
1369 Returns:
1370 the reason if chroot needs recreated; None otherwise
1371 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001372 if re.search(
1373 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001374 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001375 return 'EAPI version mismatch'
1376
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001377 if 'Chroot is too new. Consider running:' in stderr:
1378 return 'chroot version is too new'
1379
1380 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001381 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1382 return 'chroot version is too new'
1383
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001384 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1385 if "undefined reference to 'std::__1::basic_string" in stdout:
1386 return 'might be due to compiler change'
1387
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001388 # Detect failures due to file collisions.
1389 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1390 # and conflict with each other. Other possible cases are package renaming or
1391 # refactoring. Let's recreate chroot to work around them.
1392 if 'Detected file collision' in stdout:
1393 # Using wildcard between words because the text wraps to the next line
1394 # depending on length of package name and each line is prefixed with
1395 # package name.
1396 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1397 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1398 # package name (65 now).
1399 m = re.search(
1400 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1401 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1402 if m:
1403 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001404
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001405 return None
1406
1407
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001408def build_packages(chromeos_root,
1409 board,
1410 chrome_root=None,
1411 goma_dir=None,
1412 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001413 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001414
1415 Args:
1416 chromeos_root: chromeos tree root
1417 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001418 chrome_root: Chrome tree root. If specified, build chrome using the
1419 provided tree
1420 goma_dir: Goma installed directory to mount into the chroot. If specified,
1421 build chrome with goma.
1422 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001423 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001424
1425 def has_build_package_argument(argument):
1426 stderr_lines = []
1427 try:
1428 util.check_call(
1429 'src/scripts/build_packages',
1430 '--help',
1431 cwd=chromeos_root,
1432 stderr_callback=stderr_lines.append)
1433 except subprocess.CalledProcessError:
1434 help_output = ''.join(stderr_lines)
1435 return '--[no]%s' % argument in help_output
1436
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001437 common_env = {
1438 'USE': '-cros-debug chrome_internal',
1439 'FEATURES': 'separatedebug',
1440 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001441 stderr_lines = []
1442 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001443 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001444 env = common_env.copy()
1445 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001446 cros_sdk(
1447 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001448 './update_chroot',
1449 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001450 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001451 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001452 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001453
1454 env = common_env.copy()
1455 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001456 './build_packages',
1457 '--board',
1458 board,
1459 '--withdev',
1460 '--noworkon',
1461 '--skip_chroot_upgrade',
1462 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001463 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001464
1465 # `use_any_chrome` flag is default on and will force to use a chrome
1466 # prebuilt even if the version doesn't match.
1467
1468 # As this argument is landed in 12681, we should check if the argument
1469 # exists before adding this.
1470 if has_build_package_argument('use_any_chrome'):
1471 cmd.append('--nouse_any_chrome')
1472
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001473 if goma_dir:
1474 # Tell build_packages to start and stop goma
1475 cmd.append('--run_goma')
1476 env['USE_GOMA'] = 'true'
1477 if afdo_use:
1478 env['USE'] += ' afdo_use'
1479 cros_sdk(
1480 chromeos_root,
1481 *cmd,
1482 env=env,
1483 chrome_root=chrome_root,
1484 stderr_callback=stderr_lines.append,
1485 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001486 except subprocess.CalledProcessError as e:
1487 # Detect failures due to incompatibility between chroot and source tree. If
1488 # so, notify the caller to recreate chroot and retry.
1489 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1490 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001491 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001492
1493 # For other failures, don't know how to handle. Just bail out.
1494 raise
1495
Kuang-che Wu28980b22019-07-31 19:51:45 +08001496
1497def build_image(chromeos_root, board):
1498 """Build ChromeOS image.
1499
1500 Args:
1501 chromeos_root: chromeos tree root
1502 board: ChromeOS board name
1503
1504 Returns:
1505 image folder; relative to chromeos_root
1506 """
1507 stderr_lines = []
1508 try:
1509 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1510 cros_sdk(
1511 chromeos_root,
1512 './build_image',
1513 '--board',
1514 board,
1515 '--noenable_rootfs_verification',
1516 'test',
1517 env={
1518 'USE': '-cros-debug chrome_internal',
1519 'FEATURES': 'separatedebug',
1520 },
1521 stderr_callback=stderr_lines.append)
1522 except subprocess.CalledProcessError as e:
1523 # Detect failures due to incompatibility between chroot and source tree. If
1524 # so, notify the caller to recreate chroot and retry.
1525 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1526 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001527 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu28980b22019-07-31 19:51:45 +08001528
1529 # For other failures, don't know how to handle. Just bail out.
1530 raise
1531
1532 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1533 'latest')
1534 assert os.path.exists(image_symlink)
1535 image_name = os.readlink(image_symlink)
1536 image_folder = os.path.join(cached_images_dir, board, image_name)
1537 assert os.path.exists(
1538 os.path.join(chromeos_root, image_folder, test_image_filename))
1539 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001540
1541
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001542class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001543 """Parsed content of autotest control file.
1544
1545 Attributes:
1546 name: test name
1547 path: control file path
1548 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1549 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1550 """
1551
1552 def __init__(self, path, variables):
1553 self.name = variables['NAME']
1554 self.path = path
1555 self.variables = variables
1556
1557
1558def parse_autotest_control_file(path):
1559 """Parses autotest control file.
1560
1561 This only parses simple top-level string assignments.
1562
1563 Returns:
1564 AutotestControlInfo object
1565 """
1566 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001567 with open(path) as f:
1568 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001569 for stmt in code.body:
1570 # Skip if not simple "NAME = *" assignment.
1571 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1572 isinstance(stmt.targets[0], ast.Name)):
1573 continue
1574
1575 # Only support string value.
1576 if isinstance(stmt.value, ast.Str):
1577 variables[stmt.targets[0].id] = stmt.value.s
1578
1579 return AutotestControlInfo(path, variables)
1580
1581
1582def enumerate_autotest_control_files(autotest_dir):
1583 """Enumerate autotest control files.
1584
1585 Args:
1586 autotest_dir: autotest folder
1587
1588 Returns:
1589 list of paths to control files
1590 """
1591 # Where to find control files. Relative to autotest_dir.
1592 subpaths = [
1593 'server/site_tests',
1594 'client/site_tests',
1595 'server/tests',
1596 'client/tests',
1597 ]
1598
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001599 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001600 result = []
1601 for subpath in subpaths:
1602 path = os.path.join(autotest_dir, subpath)
1603 for root, dirs, files in os.walk(path):
1604
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001605 for deny in denylist:
1606 if deny in dirs:
1607 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001608
1609 for filename in files:
1610 if filename == 'control' or filename.startswith('control.'):
1611 result.append(os.path.join(root, filename))
1612
1613 return result
1614
1615
1616def get_autotest_test_info(autotest_dir, test_name):
1617 """Get metadata of given test.
1618
1619 Args:
1620 autotest_dir: autotest folder
1621 test_name: test name
1622
1623 Returns:
1624 AutotestControlInfo object. None if test not found.
1625 """
1626 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001627 try:
1628 info = parse_autotest_control_file(control_file)
1629 except SyntaxError:
1630 logger.warning('%s is not parsable, ignore', control_file)
1631 continue
1632
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001633 if info.name == test_name:
1634 return info
1635 return None
1636
1637
Kuang-che Wu9d14c162020-11-03 19:35:18 +08001638def _get_overlay_name(overlay):
1639 path = os.path.join(overlay, 'metadata', 'layout.conf')
1640 if os.path.exists(path):
1641 with open(path) as f:
1642 for line in f:
1643 m = re.search(r'repo-name\s*=\s*(\S+)', line)
1644 if m:
1645 return m.group(1)
1646
1647 path = os.path.join(overlay, 'profiles', 'repo_name')
1648 if os.path.exists(path):
1649 with open(path) as f:
1650 return f.readline().rstrip()
1651
1652 return None
1653
1654
1655def parse_chromeos_overlays(chromeos_root):
1656 # ref: chromite's lib/portage_util.py ListOverlays().
1657 overlays = {}
1658 paths = ['src/overlays', 'src/private-overlays']
1659
1660 for path in paths:
1661 path = os.path.join(chromeos_root, path, 'overlay-*')
1662 for overlay in sorted(glob.glob(path)):
1663 name = _get_overlay_name(overlay)
1664 if not name:
1665 continue
1666 # Special cases which have variant boards.
1667 if name in ['auron', 'guado', 'nyan', 'veyron']:
1668 continue
1669
1670 path = os.path.join(overlay, 'metadata', 'layout.conf')
1671 masters = []
1672 if os.path.exists(path):
1673 with open(path) as f:
1674 for line in f:
1675 m = re.search(r'masters\s*=(.*)', line)
1676 if m:
1677 masters = m.group(1).split()
1678 overlays[name] = masters
1679 return overlays
1680
1681
1682def resolve_basic_boards(overlays):
1683
1684 def normalize(name):
1685 return name.replace('-private', '')
1686
1687 def resolve(name):
1688 result = set()
1689 for parent in overlays[name]:
1690 assert parent != name, 'recursive overlays definition?'
1691 if parent not in overlays:
1692 continue
1693 for basic in resolve(parent):
1694 result.add(basic)
1695 if not result:
1696 result.add(name)
1697 return set(map(normalize, result))
1698
1699 result = {}
1700 for name in overlays:
1701 board = normalize(name)
1702 basic = resolve(name)
1703 assert len(basic) == 1
1704 basic_board = basic.pop()
1705 result[board] = basic_board
1706 return result
1707
1708
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001709def detect_branch_level(branch):
1710 """Given a branch name of manifest-internal, detect it's branch level.
1711
1712 level1: if ChromeOS version is x.0.0
1713 level2: if ChromeOS version is x.x.0
1714 level3: if ChromeOS version is x.x.x
1715 Where x is an non-zero integer.
1716
1717 Args:
1718 branch: branch name or ref name in manifest-internal
1719
1720 Returns:
1721 An integer indicates the branch level, or zero if not detectable.
1722 """
1723 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1724 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1725 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1726
1727 if re.match(level1, branch):
1728 return 1
1729 if re.match(level2, branch):
1730 return 2
1731 if re.match(level3, branch):
1732 return 3
1733 return 0
1734
1735
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001736def get_crosland_link(old, new):
1737 """Generates crosland link between two versions.
1738
1739 Args:
1740 old: ChromeOS version
1741 new: ChromeOS version
1742
1743 Returns:
1744 A crosland url.
1745 """
1746
1747 def version_to_url_parameter(ver):
1748 if is_cros_snapshot_version(ver):
1749 return snapshot_version_split(ver)[2]
1750 return version_to_short(ver)
1751
1752 old_parameter = version_to_url_parameter(old)
1753 new_parameter = version_to_url_parameter(new)
1754 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1755
1756
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001757class ChromeOSSpecManager(codechange.SpecManager):
1758 """Repo manifest related operations.
1759
1760 This class enumerates chromeos manifest files, parses them,
1761 and sync to disk state according to them.
1762 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001763
1764 def __init__(self, config):
1765 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001766 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1767 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001768 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1769 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001770 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001771 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001772 self.historical_manifest_branch_name = 'refs/heads/master'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001773 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001774 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1775 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001776
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001777 def lookup_snapshot_manifest_revisions(self, old, new):
1778 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001779
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001780 Returns:
1781 list of (timestamp, commit_id, snapshot_id):
1782 timestamp: integer unix timestamp
1783 commit_id: a string indicates commit hash
1784 snapshot_id: a string indicates snapshot id
1785 """
1786 assert is_cros_snapshot_version(old)
1787 assert is_cros_snapshot_version(new)
1788
1789 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001790 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001791 # Try to guess the commit time of a snapshot manifest, it is usually a few
1792 # minutes different between snapshot manifest commit and image.zip
1793 # generate.
1794 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001795 old_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001796 gs_path.format(board=self.config['board'], version=old)) - 86400
1797 except subprocess.CalledProcessError:
1798 old_timestamp = None
1799 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001800 new_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001801 gs_path.format(board=self.config['board'], version=new)) + 86400
1802 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1803 # we can find snapshot 5982
1804 # snapshot_id <= 5982 has different commit message format, so we need
1805 # to identify its id in different ways, see below comment for more info.
1806 new_timestamp = max(new_timestamp, 1558657989 + 1)
1807 except subprocess.CalledProcessError:
1808 new_timestamp = None
1809 result = []
1810 _, _, old_snapshot_id = snapshot_version_split(old)
1811 _, _, new_snapshot_id = snapshot_version_split(new)
1812 repo = self.manifest_internal_dir
1813 path = 'snapshot.xml'
1814 branch = 'snapshot'
1815 commits = git_util.get_history(
1816 repo,
1817 path,
1818 branch,
1819 after=old_timestamp,
1820 before=new_timestamp,
1821 with_subject=True)
1822
1823 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1824 # subject, as their subjects are all `Annealing manifest snapshot.`.
1825 # So instead we count the snapshot_id manually.
1826 count = 5982
1827 # There are two snapshot_id = 2633 in commit history, ignore the former
1828 # one.
1829 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1830 # We examine the commits in reverse order as there are some testing
1831 # commits before snapshot_id=2, this method works fine after
1832 # snapshot 2, except snapshot 2633
1833 for commit in reversed(commits):
1834 msg = commit[2]
1835 if commit[1] in ignore_list:
1836 continue
1837
1838 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1839 if match:
1840 snapshot_id = match.group(1)
1841 elif 'Annealing manifest snapshot' in msg:
1842 snapshot_id = str(count)
1843 count -= 1
1844 else:
1845 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08001846 # b/151054108: snapshot version in [29288, 29439] is broken
1847 if 29288 <= int(snapshot_id) <= 29439:
1848 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001849 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1850 result.append((commit[0], commit[1], snapshot_id))
1851 # We find commits in reversed order, now reverse it again to chronological
1852 # order.
1853 return list(reversed(result))
1854
1855 def lookup_build_timestamp(self, rev):
1856 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1857 if is_cros_full_version(rev):
1858 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001859 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001860
1861 def lookup_snapshot_build_timestamp(self, rev):
1862 assert is_cros_snapshot_version(rev)
1863 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1864
1865 def lookup_release_build_timestamp(self, rev):
1866 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001867 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001868 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1869 try:
1870 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001871 self.historical_manifest_branch_name,
1872 path)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001873 except ValueError as e:
1874 raise errors.InternalError(
1875 '%s does not have %s' %
1876 (self.historical_manifest_git_dir, path)) from e
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001877 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001878
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001879 def detect_float_spec_branch_level(self, spec):
1880 results = [
1881 detect_branch_level(branch) for branch in git_util.get_branches(
1882 self.manifest_dir, commit=spec.name)
1883 ]
1884 results = [x for x in results if x > 0]
1885 return min(results) if results else 0
1886
1887 def branch_between_float_specs(self, old_spec, new_spec):
1888 if old_spec.spec_type != codechange.SPEC_FLOAT:
1889 return False
1890 if new_spec.spec_type != codechange.SPEC_FLOAT:
1891 return False
1892
1893 level_old = self.detect_float_spec_branch_level(old_spec)
1894 level_new = self.detect_float_spec_branch_level(new_spec)
1895
1896 if not level_old or not level_new:
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001897 logger.warning('branch level detect failed, assume not branched')
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001898 return False
1899 return level_old != level_new
1900
Kuang-che Wud558a042020-06-06 02:11:00 +08001901 def _determine_float_branch(self, old, new, fixed_specs):
1902 # There is no revision tag in snapshot's xml. We know snapshot
1903 # builds are on master branch.
1904 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001905 if fixed_specs[0].revision:
1906 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001907 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
1908 else:
1909 old_branches = [master_refname]
1910
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001911 if fixed_specs[-1].revision:
1912 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001913 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001914 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08001915 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001916
Kuang-che Wud558a042020-06-06 02:11:00 +08001917 common_branches = list(set(old_branches) & set(new_branches))
1918 assert common_branches, '%s and %s are not on common branches?' % (old, new)
1919
1920 if len(common_branches) == 1:
1921 return common_branches[0]
1922
1923 # There are more than one common branches, use heuristic to tie breaking.
1924 # The heuristic is simple: choice the branch with "smallest" number.
1925 # "Smaller" means the more major branch (not branched) or branched later.
1926 #
1927 # Following is the commit graph of manifest-internal repo. It shows many
1928 # interesting cases.
1929 #
1930 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
1931 # --A--+---X--------------X------B-------X-----------> master
1932 # \
1933 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
1934 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
1935 # \ \
1936 # \ E------------> stabilize-13020.67.B
1937 # \ 83/13020.55.1
1938 # F-----X--------------------> stabilize-13020.55.B
1939 #
1940 # How to read this graph:
1941 # - Time goes from left to right. Branch names are on the right side of
1942 # arrows.
1943 # - Letters A-F are manifest commits.
1944 # - Marker X means release image build at that time, the version numbers
1945 # are labeled above the X marker.
1946 # For example,
1947 # 1) 13021.0.0 release is based on manifest A, which is on all branches
1948 # shown on the graph.
1949 # We know 13021.0.0 is on master (and R84 branch later, not shown in
1950 # this graph), not on 13020* branches.
1951 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
1952 # (R83-13020.B, 13020.67.B, and 13020.55.B).
1953 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
1954 # 13020.55.B.
1955 #
1956 # There is an important property here. Every time a new branch is created,
1957 # there will always be a commit (like C, E, and F) to fix "revision" field
1958 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
1959 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
1960 # xxxxx.B branch.
1961 #
1962 # With such property, among the branches containing the given manifest
1963 # file, the branch with "smallest" number guarantees where the release is.
1964
1965 def branch_key(s):
1966 if s == master_refname:
1967 return 0, 0, 0
1968 m = re.search(r'-(\d+)\.B$', s)
1969 if m:
1970 return int(m.group(1)), 0, 0
1971 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
1972 if m:
1973 return int(m.group(1)), int(m.group(2)), 0
1974 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
1975 if m:
1976 return int(m.group(1)), int(m.group(2)), int(m.group(3))
1977
1978 logger.warning('unexpected branch name: %s', s)
1979 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
1980
1981 common_branches.sort(key=branch_key)
1982 return common_branches[0]
1983
1984 def collect_float_spec(self, old, new, fixed_specs=None):
1985 assert fixed_specs
1986 branch = self._determine_float_branch(old, new, fixed_specs)
1987 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001988
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001989 old_timestamp = self.lookup_build_timestamp(old)
1990 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001991 # snapshot time is different from commit time
1992 # usually it's a few minutes different
1993 # 30 minutes should be safe in most cases
1994 if is_cros_snapshot_version(old):
1995 old_timestamp = old_timestamp - 1800
1996 if is_cros_snapshot_version(new):
1997 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001998
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001999 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002000 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08002001 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002002 parser = repo_util.ManifestParser(self.manifest_dir)
2003 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002004 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002005 result.append(
2006 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
2007 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002008
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002009 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002010 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
2011 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
2012
2013 # case 1: if both are snapshot, return a list of snapshot
2014 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
2015 return self.collect_snapshot_specs(old, new)
2016
2017 # case 2: if both are release version
2018 # return a list of release version
2019 if is_cros_full_version(old) and is_cros_full_version(new):
2020 return self.collect_release_specs(old, new)
2021
2022 # case 3: return a list of release version and append a snapshot
2023 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002024 result = self.collect_release_specs(
2025 version_to_full(self.config['board'], old),
2026 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002027 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002028 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002029 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08002030 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002031 return result
2032
2033 def collect_snapshot_specs(self, old, new):
2034 assert is_cros_snapshot_version(old)
2035 assert is_cros_snapshot_version(new)
2036
2037 def guess_snapshot_version(board, snapshot_id, old, new):
2038 if old.endswith('-' + snapshot_id):
2039 return old
2040 if new.endswith('-' + snapshot_id):
2041 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08002042 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002043 'R*-{snapshot_id}-*'.format(
2044 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002045 for line in gsutil_ls(gs_path, ignore_errors=True):
2046 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
2047 if m:
2048 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002049 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002050
2051 result = []
2052 path = 'snapshot.xml'
2053 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002054 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002055 snapshot_version = guess_snapshot_version(self.config['board'],
2056 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002057 if snapshot_version:
2058 result.append(
2059 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
2060 path))
2061 else:
2062 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002063 return result
2064
2065 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002066 assert is_cros_full_version(old)
2067 assert is_cros_full_version(new)
2068 old_milestone, old_short_version = version_split(old)
2069 new_milestone, new_short_version = version_split(new)
2070
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002071 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002072 for milestone in git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002073 self.historical_manifest_git_dir, self.historical_manifest_branch_name,
2074 'buildspecs'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002075 if not milestone.isdigit():
2076 continue
2077 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
2078 continue
2079
Kuang-che Wu74768d32018-09-07 12:03:24 +08002080 files = git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002081 self.historical_manifest_git_dir,
2082 self.historical_manifest_branch_name,
Kuang-che Wu74768d32018-09-07 12:03:24 +08002083 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002084
2085 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002086 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002087 short_version, ext = os.path.splitext(fn)
2088 if ext != '.xml':
2089 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002090 if (util.is_version_lesseq(old_short_version, short_version) and
2091 util.is_version_lesseq(short_version, new_short_version) and
2092 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002093 rev = make_cros_full_version(milestone, short_version)
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002094 timestamp = git_util.get_commit_time(
2095 self.historical_manifest_git_dir,
2096 self.historical_manifest_branch_name, path)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002097 result.append(
2098 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002099
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002100 def version_key_func(spec):
2101 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002102 return util.version_key_func(short_version)
2103
2104 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002105 assert result[0].name == old
2106 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002107 return result
2108
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002109 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002110 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2111 if is_cros_full_version(rev):
2112 milestone, short_version = version_split(rev)
2113 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2114 manifest = git_util.get_file_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002115 self.historical_manifest_git_dir,
2116 self.historical_manifest_branch_name, path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002117 else:
2118 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2119 commit_id = revisions[0][1]
2120 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2121 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002122 return manifest
2123
2124 def get_manifest_file(self, rev):
2125 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002126 manifest_name = 'manifest_%s.xml' % rev
2127 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2128 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002129 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002130
2131 # workaround for b/150572399
2132 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2133 # folder .repo instead of.repo/manifests
2134 if is_cros_version_lesseq(rev, '12931.0.0'):
2135 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2136 manifest_patch_path = os.path.join(repo_path, manifest_name)
2137 with open(manifest_patch_path, 'w') as f:
2138 f.write(self.get_manifest(rev))
2139
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002140 return manifest_name
2141
2142 def parse_spec(self, spec):
2143 parser = repo_util.ManifestParser(self.manifest_dir)
2144 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002145 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002146 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002147 with open(manifest_path) as f:
2148 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002149 root = parser.parse_single_xml(content, allow_include=False)
2150 else:
2151 root = parser.parse_xml_recursive(spec.name, spec.path)
2152
2153 spec.entries = parser.process_parsed_result(root)
2154 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002155 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002156 raise ValueError('fixed spec %r has unexpected floating entries' %
2157 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002158 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002159
2160 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002161 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002162
2163 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2164 # manifest. 'repo sync -m' is not enough
2165 repo_util.init(
2166 self.config['chromeos_root'],
2167 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2168 manifest_name=manifest_name,
2169 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002170 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002171 # b/150753074: moblab is in non-default group and causes mark_as_stable
2172 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002173 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002174 )
2175
2176 # Note, don't sync with current_branch=True for chromeos. One of its
2177 # build steps (inside mark_as_stable) executes "git describe" which
2178 # needs git tag information.
2179 repo_util.sync(self.config['chromeos_root'])