blob: b8ce7beaf9ddd9f3fcdf9c8c54cb6ff2d58961df [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_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
46re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080047re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080048
49gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
50gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080051 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080052
53# Assume gsutil is in PATH.
54gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080055
56# Since snapshots with version >= 12618.0.0 have android and chrome version
57# info.
58snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080059
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080060# current earliest buildbucket buildable versions
61# picked from https://crrev.com/c/2072618
62buildbucket_cutover_versions = [
63 '12931.0.0',
64 '12871.26.0', # R81
65 '12871.24.2', # stabilize-12871.24.B
66 '12812.10.0', # factory-excelsior-12812.B
67 '12768.14.0', # firmware-servo-12768.B
68 '12739.85.0', # R80
69 '12739.67.1', # stabilize-excelsior-12739.67.B
70 '12692.36.0', # factory-hatch-12692.B
71 '12672.104.0', # firmware-hatch-12672.B
72 '12607.110.0', # R79
73 '12607.83.2', # stabilize-quickfix-12607.83.B
74 '12587.59.0', # factory-kukui-12587.B
75 '12573.78.0', # firmware-kukui-12573.B
76 '12499.96.0', # R78
77 '12422.33.0', # firmware-mistral-12422.B
78 '12371.190.0', # R77
79 '12361.38.0', # factory-mistral-12361.B
80 '12200.65.0', # firmware-sarien-12200.B
81 '12105.128.0', # R75
82 '12033.82.0', # factory-sarien-12033.B
83]
84
Kuang-che Wub9705bd2018-06-28 17:59:18 +080085chromeos_root_inside_chroot = '/mnt/host/source'
86# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080087prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080088prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080089# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
90cached_images_dir = 'src/build/images'
91test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080092
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080093VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
94VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
95VERSION_KEY_MILESTONE = 'milestone'
96VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080097VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080098VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +080099CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800100
101
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800102class NeedRecreateChrootException(Exception):
103 """Failed to build ChromeOS because of chroot mismatch or corruption"""
104
105
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800106def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800107 """Determines if `s` is chromeos short version.
108
109 This function doesn't accept version number of local build.
110 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800111 return bool(re.match(re_chromeos_short_version, s))
112
113
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800114def is_cros_localbuild_version(s):
115 """Determines if `s` is chromeos local build version."""
116 return bool(re.match(re_chromeos_localbuild_version, s))
117
118
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800119def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800120 """Determines if `s` is chromeos full version.
121
122 This function doesn't accept version number of local build.
123 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800124 return bool(re.match(re_chromeos_full_version, s))
125
126
127def is_cros_version(s):
128 """Determines if `s` is chromeos version (either short or full)"""
129 return is_cros_short_version(s) or is_cros_full_version(s)
130
131
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800132def is_cros_snapshot_version(s):
133 """Determines if `s` is chromeos snapshot version"""
134 return bool(re.match(re_chromeos_snapshot_version, s))
135
136
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800137def is_cros_version_lesseq(ver1, ver2):
138 """Determines if ver1 is less or equal to ver2.
139
140 Args:
141 ver1: a Chrome OS version in short, full, or snapshot format.
142 ver2: a Chrome OS version in short, full, or snapshot format.
143
144 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800145 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800146 """
147 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
148 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
149
150 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
151 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
152 return ver1 <= ver2
153
154
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800155def is_buildbucket_buildable(version):
156 """Determines if a version is buildable on buildbucket."""
157 short_version = version_to_short(version)
158 # If given version is child of any cutover, then it's buildable
159 return any([
160 util.is_direct_relative_version(x, short_version) and
161 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
162 ])
163
164
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800165def make_cros_full_version(milestone, short_version):
166 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800167 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800168 return 'R%s-%s' % (milestone, short_version)
169
170
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800171def make_cros_snapshot_version(milestone, short_version, snapshot_id):
172 """Makes snapshot version from milestone, short_version and snapshot id"""
173 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
174
175
176def version_split(version):
177 """Splits full_version or snapshot_version into milestone and short_version"""
178 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
179 if is_cros_snapshot_version(version):
180 return snapshot_version_split(version)[0:2]
181 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800182 return milestone[1:], short_version
183
184
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800185def snapshot_version_split(snapshot_version):
186 """Splits snapshot_version into milestone, short_version and snapshot_id"""
187 assert is_cros_snapshot_version(snapshot_version)
188 milestone, shot_version, snapshot_id = snapshot_version.split('-')
189 return milestone[1:], shot_version, snapshot_id
190
191
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800192def query_snapshot_buildbucket_id(board, snapshot_version):
193 """Query buildbucket id of a snapshot"""
194 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800195 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800196 '/{snapshot_version}-*/image.zip')
197 output = gsutil_ls(
198 '-d',
199 path.format(board=board, snapshot_version=snapshot_version),
200 ignore_errors=True)
201 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800202 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800203 if m:
204 return m.group(1)
205 return None
206
207
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800208def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800209 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800210 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800211 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 +0800212 return s
213
214
215def query_dut_lsb_release(host):
216 """Query /etc/lsb-release of given DUT
217
218 Args:
219 host: the DUT address
220
221 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800222 dict for keys and values of /etc/lsb-release.
223
224 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800225 errors.SshConnectionError: cannot connect to host
226 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800227 """
228 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800229 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800230 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800231 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800232 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
233
234
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800235def query_dut_os_release(host):
236 """Query /etc/os-release of given DUT
237
238 Args:
239 host: the DUT address
240
241 Returns:
242 dict for keys and values of /etc/os-release.
243
244 Raises:
245 errors.SshConnectionError: cannot connect to host
246 errors.ExternalError: lsb-release file doesn't exist
247 """
248 try:
249 output = util.ssh_cmd(host, 'cat', '/etc/os-release')
250 except subprocess.CalledProcessError:
251 raise errors.ExternalError('unable to read /etc/os-release; not a DUT')
252 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
253
254
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800255def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800256 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800257
258 Args:
259 host: the DUT address
260
261 Returns:
262 True if the host is a chromeos device.
263 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800264 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800265 return query_dut_os_release(host).get('ID') in [
266 'chromiumos',
267 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800268 ]
269 except (errors.ExternalError, errors.SshConnectionError):
270 return False
271
272
273def is_good_dut(host):
274 if not is_dut(host):
275 return False
276
277 # Sometimes python is broken after 'cros flash'.
278 try:
279 util.ssh_cmd(host, 'python', '-c', '1')
280 return True
281 except (subprocess.CalledProcessError, errors.SshConnectionError):
282 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800283
284
285def query_dut_board(host):
286 """Query board name of a given DUT"""
287 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
288
289
290def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800291 """Query short version of a given DUT.
292
293 This function may return version of local build, which
294 is_cros_short_version() is false.
295 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800296 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
297
298
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800299def query_dut_prebuilt_version(host):
300 """Return a snapshot version or short version of a given DUT.
301
302 Args:
303 host: dut host
304
305 Returns:
306 Snapshot version or short version.
307 """
308 lsb_release = query_dut_lsb_release(host)
309 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
310 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH')
311 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
312 builder_path)
313 if match:
314 return match.group(1)
315 return release_version
316
317
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800318def query_dut_is_by_official_builder(host):
319 """Query if given DUT is build by buildbucket builder"""
320 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
321 '').startswith('Continuous Builder')
322
323
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800324def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800325 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800326
327 Args:
328 host: DUT address
329 connect_timeout: connection timeout
330
331 Returns:
332 boot uuid
333 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800334 return util.ssh_cmd(
335 host,
336 'cat',
337 '/proc/sys/kernel/random/boot_id',
338 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800339
340
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800341def reboot(host, force_reboot_callback=None):
342 """Reboot a DUT and verify.
343
344 Args:
345 host: DUT address
346 force_reboot_callback: powerful reboot hook (via servo). This will be
347 invoked if normal reboot failed.
348 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800349 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800350 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800351 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800352 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800353
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800354 try:
355 util.ssh_cmd(host, 'reboot')
356 except errors.SshConnectionError:
357 # Depends on timing, ssh may return failure due to broken pipe, which is
358 # working as intended. Ignore such kind of errors.
359 pass
360
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800361 wait_reboot_done(host, boot_id)
362 except (errors.SshConnectionError, errors.ExternalError):
363 if force_reboot_callback and force_reboot_callback(host):
364 wait_reboot_done(host, boot_id)
365 return
366 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800367
Kuang-che Wu708310b2018-03-28 17:24:34 +0800368
369def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800370 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800371 # (dev screen short delay) or more (long delay).
372 time.sleep(15)
373 for _ in range(100):
374 try:
375 # During boot, DUT does not response and thus ssh may hang a while. So
376 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
377 # set tight limit because it's inside retry loop.
378 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
379 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800380 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800381 logger.debug('reboot not ready? sleep wait 1 sec')
382 time.sleep(1)
383
Kuang-che Wue121fae2018-11-09 16:18:39 +0800384 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800385
386
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800387def gs_release_boardpath(board):
388 """Normalizes board name for gs://chromeos-releases/
389
390 This follows behavior of PushImage() in chromite/scripts/pushimage.py
391 Note, only gs://chromeos-releases/ needs normalization,
392 gs://chromeos-image-archive does not.
393
394 Args:
395 board: ChromeOS board name
396
397 Returns:
398 normalized board name
399 """
400 return board.replace('_', '-')
401
402
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800403def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800404 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800405
406 Args:
407 args: command line arguments passed to gsutil
408 kwargs:
409 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
410 but the path not found.
411
412 Returns:
413 stdout of gsutil
414
415 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800416 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800417 subprocess.CalledProcessError: command failed
418 """
419 stderr_lines = []
420 try:
421 return util.check_output(
422 gsutil_bin, *args, stderr_callback=stderr_lines.append)
423 except subprocess.CalledProcessError as e:
424 stderr = ''.join(stderr_lines)
425 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800426 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800427 'gsutil failed due to permission. ' +
428 'Run "%s config" and follow its instruction. ' % gsutil_bin +
429 'Fill any string if it asks for project-id')
430 if kwargs.get('ignore_errors'):
431 return ''
432 raise
433 except OSError as e:
434 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800435 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800436 'Unable to run %s. gsutil is not installed or not in PATH?' %
437 gsutil_bin)
438 raise
439
440
441def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800442 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800443
444 Args:
445 args: arguments passed to 'gsutil ls'
446 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800447 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800448 exception, ex. path not found.
449
450 Returns:
451 list of 'gsutil ls' result. One element for one line of gsutil output.
452
453 Raises:
454 subprocess.CalledProcessError: gsutil failed, usually means path not found
455 """
456 return gsutil('ls', *args, **kwargs).splitlines()
457
458
Kuang-che Wu876a5382020-10-27 14:24:58 +0800459def gsutil_stat_creation_time(*args, **kwargs):
460 """Returns the creation time of a file or multiple files.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800461
462 Args:
463 args: arguments passed to 'gsutil stat'.
464 kwargs: extra parameters for gsutil.
465
466 Returns:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800467 A integer indicates the creation timestamp.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800468
469 Raises:
470 subprocess.CalledProcessError: gsutil failed, usually means path not found
Kuang-che Wu876a5382020-10-27 14:24:58 +0800471 errors.ExternalError: creation time is not found
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800472 """
473 result = -1
474 # Currently we believe stat always returns a UTC time, and strptime also
475 # parses a UTC time by default.
476 time_format = '%a, %d %b %Y %H:%M:%S GMT'
477
478 for line in gsutil('stat', *args, **kwargs).splitlines():
479 if ':' not in line:
480 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800481 key, value = line.split(':', 1)
482 key, value = key.strip(), value.strip()
Kuang-che Wu876a5382020-10-27 14:24:58 +0800483 if key != 'Creation time':
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800484 continue
485 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800486 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800487 result = max(result, unixtime)
488
489 if result == -1:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800490 raise errors.ExternalError("didn't find creation time")
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800491 return result
492
493
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800494def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800495 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800496
497 Args:
498 board: ChromeOS board name
499 short_version: ChromeOS version number in short format, ex. 9300.0.0
500
501 Returns:
502 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
503 None if failed.
504 """
505 path = gs_archive_path.format(board=board) + '/R*-' + short_version
506 for line in gsutil_ls('-d', path, ignore_errors=True):
507 m = re.search(r'/R(\d+)-', line)
508 if not m:
509 continue
510 return m.group(1)
511
512 for channel in ['canary', 'dev', 'beta', 'stable']:
513 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800514 channel=channel,
515 boardpath=gs_release_boardpath(board),
516 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800517 for line in gsutil_ls(path, ignore_errors=True):
518 m = re.search(r'\bR(\d+)-' + short_version, line)
519 if not m:
520 continue
521 return m.group(1)
522
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800523 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800524 return None
525
526
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800527def list_board_names(chromeos_root):
528 """List board names.
529
530 Args:
531 chromeos_root: chromeos tree root
532
533 Returns:
534 list of board names
535 """
536 # Following logic is simplified from chromite/lib/portage_util.py
537 cros_list_overlays = os.path.join(chromeos_root,
538 'chromite/bin/cros_list_overlays')
539 overlays = util.check_output(cros_list_overlays).splitlines()
540 result = set()
541 for overlay in overlays:
542 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
543 name = None
544 if os.path.exists(conf_file):
545 for line in open(conf_file):
546 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
547 if m:
548 name = m.group(1)
549 break
550
551 if not name:
552 name_file = os.path.join(overlay, 'profiles', 'repo_name')
553 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800554 with open(name_file) as f:
555 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800556
557 if name:
558 name = re.sub(r'-private$', '', name)
559 result.add(name)
560
561 return list(result)
562
563
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800564def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800565 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800566
567 Args:
568 board: ChromeOS board name
569 version: ChromeOS version number in short or full format
570
571 Returns:
572 (milestone, version in short format)
573 """
574 if is_cros_short_version(version):
575 milestone = query_milestone_by_version(board, version)
576 short_version = version
577 else:
578 milestone, short_version = version_split(version)
579 return milestone, short_version
580
581
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800582def extract_major_version(version):
583 """Converts a version to its major version.
584
585 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800586 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800587
588 Returns:
589 major version number in string format
590 """
591 version = version_to_short(version)
592 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
593 return m.group(1)
594
595
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800596def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800597 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800598
599 Args:
600 version: ChromeOS version number in short or full format
601
602 Returns:
603 version number in short format
604 """
605 if is_cros_short_version(version):
606 return version
607 _, short_version = version_split(version)
608 return short_version
609
610
611def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800612 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800613
614 Args:
615 board: ChromeOS board name
616 version: ChromeOS version number in short or full format
617
618 Returns:
619 version number in full format
620 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800621 if is_cros_snapshot_version(version):
622 milestone, short_version, _ = snapshot_version_split(version)
623 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800624 if is_cros_full_version(version):
625 return version
626 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800627 if not milestone:
628 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
629 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800630 return make_cros_full_version(milestone, version)
631
632
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800633def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800634 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800635
636 Args:
637 board: ChromeOS board
638 major_version: ChromeOS major version
639
640 Returns:
641 list of (version, gs_path):
642 version: Chrome OS snapshot version
643 gs_path: gs path of test image
644 """
645
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800646 def extract_snapshot_id(result):
647 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
648 assert m
649 return int(m.group(1))
650
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800651 short_version = '%s.0.0' % major_version
652 milestone = query_milestone_by_version(board, short_version)
653 if not milestone:
654 milestone = '*'
655
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800656 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800657 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800658 result = []
659 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800660 path.format(
661 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800662 ignore_errors=True)
663
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800664 for gs_path in sorted(output):
665 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800666 if m:
667 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800668 # we should skip if there is duplicate snapshot
669 if result and result[-1][0] == snapshot_version:
670 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800671
672 # b/151054108: snapshot version in [29288, 29439] is broken
673 _, _, snapshot_id = snapshot_version_split(snapshot_version)
674 if 29288 <= int(snapshot_id) <= 29439:
675 continue
676
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800677 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800678
679 # sort by its snapshot_id
680 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800681 return result
682
683
Kuang-che Wu575dc442019-03-05 10:30:55 +0800684def list_prebuilt_from_image_archive(board):
685 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
686
687 gs://chromeos-image-archive contains only recent builds (in two years).
688 We prefer this function to list_prebuilt_from_chromeos_releases() because
689 - this is what "cros flash" supports directly.
690 - the paths have milestone information, so we don't need to do slow query
691 by ourselves.
692
693 Args:
694 board: ChromeOS board name
695
696 Returns:
697 list of (version, gs_path):
698 version: Chrome OS version in full format
699 gs_path: gs path of test image
700 """
701 result = []
702 for line in gsutil_ls(gs_archive_path.format(board=board)):
703 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
704 if m:
705 full_version = m.group(1)
706 test_image = 'chromiumos_test_image.tar.xz'
707 assert line.endswith('/')
708 gs_path = line + test_image
709 result.append((full_version, gs_path))
710 return result
711
712
713def list_prebuilt_from_chromeos_releases(board):
714 """Lists ChromeOS versions available from gs://chromeos-releases.
715
716 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
717 support it.
718
719 Args:
720 board: ChromeOS board name
721
722 Returns:
723 list of (version, gs_path):
724 version: Chrome OS version in short format
725 gs_path: gs path of test image (with wildcard)
726 """
727 result = []
728 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800729 gs_release_path.format(
730 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800731 ignore_errors=True):
732 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
733 if m:
734 short_version = m.group(1)
735 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
736 short_version=short_version, board=board)
737 gs_path = line + test_image
738 result.append((short_version, gs_path))
739 return result
740
741
Kuang-che Wue1808402020-01-06 20:27:45 +0800742def has_test_image(board, version):
743 if is_cros_snapshot_version(version):
744 return bool(query_snapshot_buildbucket_id(board, version))
745
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800746 try:
747 full_version = version_to_full(board, version)
748 except errors.ExternalError:
749 # version_to_full() is implemented by checking image, thus its failure
750 # means no image.
751 return False
Kuang-che Wue1808402020-01-06 20:27:45 +0800752 short_version = version_to_short(version)
753 paths = [
754 gs_archive_path.format(board=board) +
755 '/%s/chromiumos_test_image.tar.xz' % full_version,
756 gs_release_path.format(
757 channel='*',
758 boardpath=gs_release_boardpath(board),
759 short_version=short_version),
760 ]
761
762 for path in paths:
763 if gsutil_ls(path, ignore_errors=True):
764 return True
765 return False
766
767
Kuang-che Wu575dc442019-03-05 10:30:55 +0800768def list_chromeos_prebuilt_versions(board,
769 old,
770 new,
771 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800772 include_older_build=True,
773 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800774 """Lists ChromeOS version numbers with prebuilt between given range
775
776 Args:
777 board: ChromeOS board name
778 old: start version (inclusive)
779 new: end version (inclusive)
780 only_good_build: only if test image is available
781 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800782 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800783
784 Returns:
785 list of sorted version numbers (in full format) between [old, new] range
786 (inclusive).
787 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800788 old_short = version_to_short(old)
789 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800790
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800791 rev_map = {
792 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800793 for full_version, gs_path in list_prebuilt_from_image_archive(board):
794 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800795 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800796
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800797 if include_older_build and old_short not in rev_map:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800798 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
799 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800800 rev_map[short_version] = [(short_version, gs_path)]
801
802 if use_snapshot:
803 for major_version in range(
804 int(extract_major_version(old)),
805 int(extract_major_version(new)) + 1):
806 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800807 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800808 # If current version is smaller than cutover, ignore it as it might not
809 # contain enough information for continuing android and chrome bisection.
810 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
811 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800812
813 # Given the fact that snapshots are images between two release versions.
814 # Adding snapshots of 12345.0.0 should be treated as adding commits
815 # between [12345.0.0, 12346.0.0).
816 # So in the following lines we check two facts:
817 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800818 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800819 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800820 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800821 continue
822 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800823 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800824 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800825 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800826 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800827
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800828 snapshots = list_snapshots_from_image_archive(board, str(major_version))
829 if snapshots:
830 # if snapshots found, we can append them after the release version,
831 # so the prebuilt image list of this version will be
832 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800833 if short_version not in rev_map:
834 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800835 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800836
837 result = []
838 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800839 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800840 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800841 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800842 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800843 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800844 continue
845
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800846 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800847
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800848 # version_to_full() and gsutil_ls() may take long time if versions are a
849 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800850
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800851 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800852 gs_result = gsutil_ls(gs_path, ignore_errors=True)
853 if not gs_result:
854 logger.warning('%s is not a good build, ignore', version)
855 continue
856 assert len(gs_result) == 1
857 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
858 if not m:
859 logger.warning('format of image path is unexpected: %s', gs_result[0])
860 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800861 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800862 elif is_cros_short_version(version):
863 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800864
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800865 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
866 version, new):
867 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800868
869 return result
870
871
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800872def prepare_snapshot_image(chromeos_root, board, snapshot_version):
873 """Prepare chromeos snapshot image.
874
875 Args:
876 chromeos_root: chromeos tree root
877 board: ChromeOS board name
878 snapshot_version: ChromeOS snapshot version number
879
880 Returns:
881 local file path of test image relative to chromeos_root
882 """
883 assert is_cros_snapshot_version(snapshot_version)
884 milestone, short_version, snapshot_id = snapshot_version_split(
885 snapshot_version)
886 full_version = make_cros_full_version(milestone, short_version)
887 tmp_dir = os.path.join(
888 chromeos_root, 'tmp',
889 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
890 if not os.path.exists(tmp_dir):
891 os.makedirs(tmp_dir)
892
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800893 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/' +
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800894 '{snapshot_version}-*/image.zip')
895 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
896
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800897 full_path = os.path.join(tmp_dir, test_image_filename)
898 rel_path = os.path.relpath(full_path, chromeos_root)
899 if os.path.exists(full_path):
900 return rel_path
901
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800902 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800903 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800904 gs_path = files[0]
905 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800906 util.check_call(
907 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
908 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800909 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800910
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800911 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800912 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800913
914
Kuang-che Wu28980b22019-07-31 19:51:45 +0800915def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800916 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800917
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800918 It searches for xbuddy image which "cros flash" can use, or fetch image to
919 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800920
921 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800922 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800923 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800924 version: ChromeOS version number in short or full format
925
926 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800927 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800928 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800929 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800930 full_version = version_to_full(board, version)
931 short_version = version_to_short(full_version)
932
933 image_path = None
934 gs_path = gs_archive_path.format(board=board) + '/' + full_version
935 if gsutil_ls('-d', gs_path, ignore_errors=True):
936 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
937 board=board, full_version=full_version)
938 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800939 tmp_dir = os.path.join(chromeos_root, 'tmp',
940 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800941 full_path = os.path.join(tmp_dir, test_image_filename)
942 rel_path = os.path.relpath(full_path, chromeos_root)
943 if os.path.exists(full_path):
944 return rel_path
945
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800946 if not os.path.exists(tmp_dir):
947 os.makedirs(tmp_dir)
948 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800949 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800950 # to fetch the image by ourselves
951 for channel in ['canary', 'dev', 'beta', 'stable']:
952 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
953 full_version=full_version, board=board)
954 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800955 channel=channel,
956 boardpath=gs_release_boardpath(board),
957 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800958 gs_path += '/' + fn
959 if gsutil_ls(gs_path, ignore_errors=True):
960 # TODO(kcwu): delete tmp
961 gsutil('cp', gs_path, tmp_dir)
962 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800963 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800964 break
965
966 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800967 return image_path
968
969
970def cros_flash(chromeos_root,
971 host,
972 board,
973 image_path,
974 version=None,
975 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800976 disable_rootfs_verification=True,
977 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800978 """Flash a DUT with given ChromeOS image.
979
980 This is implemented by 'cros flash' command line.
981
982 Args:
983 chromeos_root: use 'cros flash' of which chromeos tree
984 host: DUT address
985 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800986 image_path: chromeos image xbuddy path or file path. For relative
987 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800988 version: ChromeOS version in short or full format
989 clobber_stateful: Clobber stateful partition when performing update
990 disable_rootfs_verification: Disable rootfs verification after update
991 is completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800992 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800993
994 Raises:
995 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800996 """
997 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
998
999 # Reboot is necessary because sometimes previous 'cros flash' failed and
1000 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001001 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001002
Kuang-che Wu020a1182020-09-08 17:17:22 +08001003 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001004 # The service is used in jetstream boards, but not other CrOS devices.
1005 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
1006 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001007 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
1008 # For simplicity, we wait 60 seconds here, which is the timeout value of
1009 # jetstream_host.
1010 # https://chromium.googlesource.com/chromiumos/third_party/autotest
1011 # /+/master/server/hosts/jetstream_host.py#27
1012 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001013 util.ssh_cmd(host, 'stop', 'ap-update-manager')
1014 except subprocess.CalledProcessError:
1015 pass # not started; do nothing
1016
Kuang-che Wu28980b22019-07-31 19:51:45 +08001017 # Handle relative path.
1018 if '://' not in image_path and not os.path.isabs(image_path):
1019 assert os.path.exists(os.path.join(chromeos_root, image_path))
1020 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
1021
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001022 args = [
1023 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
1024 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001025 if clobber_stateful:
1026 args.append('--clobber-stateful')
1027 if disable_rootfs_verification:
1028 args.append('--disable-rootfs-verification')
1029
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001030 try:
1031 cros_sdk(chromeos_root, 'cros', 'flash', *args)
1032 except subprocess.CalledProcessError:
1033 raise errors.ExternalError('cros flash failed')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001034
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001035 if version:
1036 # In the past, cros flash may fail with returncode=0
1037 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001038 if is_cros_snapshot_version(version):
1039 builder_path = query_dut_lsb_release(host).get(
1040 'CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001041 expect_prefix = '%s-snapshot/%s-' % (board, version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001042 if not builder_path.startswith(expect_prefix):
1043 raise errors.ExternalError(
1044 'although cros flash succeeded, the OS builder path is '
1045 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1046 else:
1047 expect_version = version_to_short(version)
1048 dut_version = query_dut_short_version(host)
1049 if dut_version != expect_version:
1050 raise errors.ExternalError(
1051 'although cros flash succeeded, the OS version is unexpected: '
1052 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001053
Kuang-che Wu4a81ea72019-10-05 15:35:17 +08001054 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001055 # (b/130786578), so it's necessary to do sanity check.
1056 if not is_good_dut(host):
1057 raise errors.ExternalError(
1058 'although cros flash succeeded, the DUT is in bad state')
1059
1060
1061def cros_flash_with_retry(chromeos_root,
1062 host,
1063 board,
1064 image_path,
1065 version=None,
1066 clobber_stateful=False,
1067 disable_rootfs_verification=True,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001068 repair_callback=None,
1069 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001070 # 'cros flash' is not 100% reliable, retry if necessary.
1071 for attempt in range(2):
1072 if attempt > 0:
1073 logger.info('will retry 60 seconds later')
1074 time.sleep(60)
1075
1076 try:
1077 cros_flash(
1078 chromeos_root,
1079 host,
1080 board,
1081 image_path,
1082 version=version,
1083 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001084 disable_rootfs_verification=disable_rootfs_verification,
1085 force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001086 return True
1087 except errors.ExternalError:
1088 logger.exception('cros flash failed')
1089 if repair_callback and not repair_callback(host):
1090 logger.warning('not repaired, assume it is harmless')
1091 continue
1092 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001093
1094
1095def version_info(board, version):
1096 """Query subcomponents version info of given version of ChromeOS
1097
1098 Args:
1099 board: ChromeOS board name
1100 version: ChromeOS version number in short or full format
1101
1102 Returns:
1103 dict of component and version info, including (if available):
1104 cros_short_version: ChromeOS version
1105 cros_full_version: ChromeOS version
1106 milestone: milestone of ChromeOS
1107 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001108 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001109 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1110 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001111 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001112 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001113 milestone, short_version, _ = snapshot_version_split(version)
1114 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001115 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001116 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001117 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001118 VERSION_KEY_MILESTONE: milestone,
1119 VERSION_KEY_CROS_FULL_VERSION: version,
1120 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001121 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1122 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1123 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001124 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001125 info = {}
1126 full_version = version_to_full(board, version)
1127
1128 # Some boards may have only partial-metadata.json but no metadata.json.
1129 # e.g. caroline R60-9462.0.0
1130 # Let's try both.
1131 metadata = None
1132 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001133 path = gs_archive_path.format(
1134 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001135 metadata = gsutil('cat', path, ignore_errors=True)
1136 if metadata:
1137 o = json.loads(metadata)
1138 v = o['version']
1139 board_metadata = o['board-metadata'][board]
1140 info.update({
1141 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1142 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1143 VERSION_KEY_MILESTONE: v['milestone'],
1144 VERSION_KEY_CR_VERSION: v['chrome'],
1145 })
1146
1147 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001148 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001149 if 'android-branch' in v: # this appears since R58-9317.0.0
1150 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1151 elif 'android-container-branch' in board_metadata:
1152 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1153 break
1154 else:
1155 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1156 logger.error(
1157 'Note, so far no quick way to look up version info for too old builds')
1158
1159 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001160
1161
1162def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001163 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001164
1165 Args:
1166 board: ChromeOS board name
1167 version: ChromeOS version number in short or full format
1168
1169 Returns:
1170 Chrome version number
1171 """
1172 info = version_info(board, version)
1173 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001174
1175
1176def query_android_build_id(board, rev):
1177 info = version_info(board, rev)
1178 rev = info['android_build_id']
1179 return rev
1180
1181
1182def query_android_branch(board, rev):
1183 info = version_info(board, rev)
1184 rev = info['android_branch']
1185 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001186
1187
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001188def guess_chrome_version(board, rev):
1189 """Guess chrome version number.
1190
1191 Args:
1192 board: chromeos board name
1193 rev: chrome or chromeos version
1194
1195 Returns:
1196 chrome version number
1197 """
1198 if is_cros_version(rev):
1199 assert board, 'need to specify BOARD for cros version'
1200 rev = query_chrome_version(board, rev)
1201 assert cr_util.is_chrome_version(rev)
1202
1203 return rev
1204
1205
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001206def is_inside_chroot():
1207 """Returns True if we are inside chroot."""
1208 return os.path.exists('/etc/cros_chroot_version')
1209
1210
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001211def convert_path_outside_chroot(chromeos_root, path):
1212 """Converts path in chroot to outside.
1213
1214 Args:
1215 chromeos_root: chromeos tree root
1216 path: path inside chroot; support starting with '~/'
1217
1218 Returns:
1219 The corresponding path outside chroot assuming the chroot is mounted
1220 """
1221 if path.startswith('~/'):
1222 path = path.replace('~', '/home/' + os.environ['USER'])
1223 assert '~' not in path, 'tilde (~) character is not fully supported'
1224
1225 assert os.path.isabs(path)
1226 assert path[0] == os.sep
1227 return os.path.join(chromeos_root, 'chroot', path[1:])
1228
1229
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001230def cros_sdk(chromeos_root, *args, **kwargs):
1231 """Run commands inside chromeos chroot.
1232
1233 Args:
1234 chromeos_root: chromeos tree root
1235 *args: command to run
1236 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001237 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001238 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001239 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001240 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001241 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001242 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001243 """
1244 envs = []
1245 for k, v in kwargs.get('env', {}).items():
1246 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1247 envs.append('%s=%s' % (k, v))
1248
1249 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1250 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001251 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001252
1253 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001254 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001255 if kwargs.get('goma_dir'):
1256 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001257
Kuang-che Wu399d4662019-06-06 15:23:37 +08001258 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001259
Kuang-che Wu399d4662019-06-06 15:23:37 +08001260 # In addition to the output of command we are interested, cros_sdk may
1261 # generate its own messages. For example, chroot creation messages if we run
1262 # cros_sdk the first time.
1263 # This is the hack to run dummy command once, so we can get clean output for
1264 # the command we are interested.
1265 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001266 try:
1267 util.check_call(*cmd, cwd=chromeos_root)
1268 except subprocess.CalledProcessError:
1269 logger.exception('cros_sdk init/update failed')
1270 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001271
1272 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001273 return util.check_output(
1274 *cmd,
1275 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001276 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001277 stdin=kwargs.get('stdin'),
1278 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001279
1280
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001281def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001282 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001283
1284 Args:
1285 chromeos_root: chromeos tree root
1286 """
1287 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1288 return
1289 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1290 return
1291
1292 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1293
1294
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001295def mount_chroot(chromeos_root):
1296 """Creates ChromeOS chroot if necessary.
1297
1298 Args:
1299 chromeos_root: chromeos tree root
1300 """
1301 # An arbitrary file must exist in chroot.
1302 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1303
1304 # Not created or mounted yet.
1305 if not os.path.exists(path):
1306 create_chroot(chromeos_root)
1307 # After this command, the chroot is mounted.
1308 cros_sdk(chromeos_root, 'true')
1309 assert os.path.exists(path)
1310
1311
1312def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001313 """Copies file into chromeos chroot.
1314
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001315 The side effect is chroot created and mounted.
1316
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001317 Args:
1318 chromeos_root: chromeos tree root
1319 src: path outside chroot
1320 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001321 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001322 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001323 mount_chroot(chromeos_root)
1324 src = os.path.expanduser(src)
1325 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1326 if not overwrite and os.path.exists(dst_outside):
1327 return
1328
1329 # Haven't support directory or special files yet.
1330 assert os.path.isfile(src)
1331 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1332
1333 dirname = os.path.dirname(dst_outside)
1334 if not os.path.exists(dirname):
1335 os.makedirs(dirname)
1336 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001337
1338
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001339def prepare_chroot(chromeos_root):
1340 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001341
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001342 # Work around b/149077936:
1343 # The creds file is copied into the chroot since 12866.0.0.
1344 # But earlier versions need this file as well because of cipd ACL change.
1345 creds_path = '~/.config/chrome_infra/auth/creds.json'
1346 if os.path.exists(os.path.expanduser(creds_path)):
1347 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001348
1349
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001350def check_if_need_recreate_chroot(stdout, stderr):
1351 """Analyze build log and determine if chroot should be recreated.
1352
1353 Args:
1354 stdout: stdout output of build
1355 stderr: stderr output of build
1356
1357 Returns:
1358 the reason if chroot needs recreated; None otherwise
1359 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001360 if re.search(
1361 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001362 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001363 return 'EAPI version mismatch'
1364
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001365 if 'Chroot is too new. Consider running:' in stderr:
1366 return 'chroot version is too new'
1367
1368 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001369 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1370 return 'chroot version is too new'
1371
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001372 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1373 if "undefined reference to 'std::__1::basic_string" in stdout:
1374 return 'might be due to compiler change'
1375
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001376 # Detect failures due to file collisions.
1377 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1378 # and conflict with each other. Other possible cases are package renaming or
1379 # refactoring. Let's recreate chroot to work around them.
1380 if 'Detected file collision' in stdout:
1381 # Using wildcard between words because the text wraps to the next line
1382 # depending on length of package name and each line is prefixed with
1383 # package name.
1384 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1385 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1386 # package name (65 now).
1387 m = re.search(
1388 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1389 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1390 if m:
1391 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001392
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001393 return None
1394
1395
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001396def build_packages(chromeos_root,
1397 board,
1398 chrome_root=None,
1399 goma_dir=None,
1400 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001401 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001402
1403 Args:
1404 chromeos_root: chromeos tree root
1405 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001406 chrome_root: Chrome tree root. If specified, build chrome using the
1407 provided tree
1408 goma_dir: Goma installed directory to mount into the chroot. If specified,
1409 build chrome with goma.
1410 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001411 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001412
1413 def has_build_package_argument(argument):
1414 stderr_lines = []
1415 try:
1416 util.check_call(
1417 'src/scripts/build_packages',
1418 '--help',
1419 cwd=chromeos_root,
1420 stderr_callback=stderr_lines.append)
1421 except subprocess.CalledProcessError:
1422 help_output = ''.join(stderr_lines)
1423 return '--[no]%s' % argument in help_output
1424
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001425 common_env = {
1426 'USE': '-cros-debug chrome_internal',
1427 'FEATURES': 'separatedebug',
1428 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001429 stderr_lines = []
1430 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001431 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001432 env = common_env.copy()
1433 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001434 cros_sdk(
1435 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001436 './update_chroot',
1437 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001438 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001439 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001440 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001441
1442 env = common_env.copy()
1443 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001444 './build_packages',
1445 '--board',
1446 board,
1447 '--withdev',
1448 '--noworkon',
1449 '--skip_chroot_upgrade',
1450 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001451 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001452
1453 # `use_any_chrome` flag is default on and will force to use a chrome
1454 # prebuilt even if the version doesn't match.
1455
1456 # As this argument is landed in 12681, we should check if the argument
1457 # exists before adding this.
1458 if has_build_package_argument('use_any_chrome'):
1459 cmd.append('--nouse_any_chrome')
1460
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001461 if goma_dir:
1462 # Tell build_packages to start and stop goma
1463 cmd.append('--run_goma')
1464 env['USE_GOMA'] = 'true'
1465 if afdo_use:
1466 env['USE'] += ' afdo_use'
1467 cros_sdk(
1468 chromeos_root,
1469 *cmd,
1470 env=env,
1471 chrome_root=chrome_root,
1472 stderr_callback=stderr_lines.append,
1473 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001474 except subprocess.CalledProcessError as e:
1475 # Detect failures due to incompatibility between chroot and source tree. If
1476 # so, notify the caller to recreate chroot and retry.
1477 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1478 if reason:
1479 raise NeedRecreateChrootException(reason)
1480
1481 # For other failures, don't know how to handle. Just bail out.
1482 raise
1483
Kuang-che Wu28980b22019-07-31 19:51:45 +08001484
1485def build_image(chromeos_root, board):
1486 """Build ChromeOS image.
1487
1488 Args:
1489 chromeos_root: chromeos tree root
1490 board: ChromeOS board name
1491
1492 Returns:
1493 image folder; relative to chromeos_root
1494 """
1495 stderr_lines = []
1496 try:
1497 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1498 cros_sdk(
1499 chromeos_root,
1500 './build_image',
1501 '--board',
1502 board,
1503 '--noenable_rootfs_verification',
1504 'test',
1505 env={
1506 'USE': '-cros-debug chrome_internal',
1507 'FEATURES': 'separatedebug',
1508 },
1509 stderr_callback=stderr_lines.append)
1510 except subprocess.CalledProcessError as e:
1511 # Detect failures due to incompatibility between chroot and source tree. If
1512 # so, notify the caller to recreate chroot and retry.
1513 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1514 if reason:
1515 raise NeedRecreateChrootException(reason)
1516
1517 # For other failures, don't know how to handle. Just bail out.
1518 raise
1519
1520 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1521 'latest')
1522 assert os.path.exists(image_symlink)
1523 image_name = os.readlink(image_symlink)
1524 image_folder = os.path.join(cached_images_dir, board, image_name)
1525 assert os.path.exists(
1526 os.path.join(chromeos_root, image_folder, test_image_filename))
1527 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001528
1529
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001530class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001531 """Parsed content of autotest control file.
1532
1533 Attributes:
1534 name: test name
1535 path: control file path
1536 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1537 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1538 """
1539
1540 def __init__(self, path, variables):
1541 self.name = variables['NAME']
1542 self.path = path
1543 self.variables = variables
1544
1545
1546def parse_autotest_control_file(path):
1547 """Parses autotest control file.
1548
1549 This only parses simple top-level string assignments.
1550
1551 Returns:
1552 AutotestControlInfo object
1553 """
1554 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001555 with open(path) as f:
1556 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001557 for stmt in code.body:
1558 # Skip if not simple "NAME = *" assignment.
1559 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1560 isinstance(stmt.targets[0], ast.Name)):
1561 continue
1562
1563 # Only support string value.
1564 if isinstance(stmt.value, ast.Str):
1565 variables[stmt.targets[0].id] = stmt.value.s
1566
1567 return AutotestControlInfo(path, variables)
1568
1569
1570def enumerate_autotest_control_files(autotest_dir):
1571 """Enumerate autotest control files.
1572
1573 Args:
1574 autotest_dir: autotest folder
1575
1576 Returns:
1577 list of paths to control files
1578 """
1579 # Where to find control files. Relative to autotest_dir.
1580 subpaths = [
1581 'server/site_tests',
1582 'client/site_tests',
1583 'server/tests',
1584 'client/tests',
1585 ]
1586
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001587 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001588 result = []
1589 for subpath in subpaths:
1590 path = os.path.join(autotest_dir, subpath)
1591 for root, dirs, files in os.walk(path):
1592
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001593 for deny in denylist:
1594 if deny in dirs:
1595 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001596
1597 for filename in files:
1598 if filename == 'control' or filename.startswith('control.'):
1599 result.append(os.path.join(root, filename))
1600
1601 return result
1602
1603
1604def get_autotest_test_info(autotest_dir, test_name):
1605 """Get metadata of given test.
1606
1607 Args:
1608 autotest_dir: autotest folder
1609 test_name: test name
1610
1611 Returns:
1612 AutotestControlInfo object. None if test not found.
1613 """
1614 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001615 try:
1616 info = parse_autotest_control_file(control_file)
1617 except SyntaxError:
1618 logger.warning('%s is not parsable, ignore', control_file)
1619 continue
1620
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001621 if info.name == test_name:
1622 return info
1623 return None
1624
1625
Kuang-che Wu9d14c162020-11-03 19:35:18 +08001626def _get_overlay_name(overlay):
1627 path = os.path.join(overlay, 'metadata', 'layout.conf')
1628 if os.path.exists(path):
1629 with open(path) as f:
1630 for line in f:
1631 m = re.search(r'repo-name\s*=\s*(\S+)', line)
1632 if m:
1633 return m.group(1)
1634
1635 path = os.path.join(overlay, 'profiles', 'repo_name')
1636 if os.path.exists(path):
1637 with open(path) as f:
1638 return f.readline().rstrip()
1639
1640 return None
1641
1642
1643def parse_chromeos_overlays(chromeos_root):
1644 # ref: chromite's lib/portage_util.py ListOverlays().
1645 overlays = {}
1646 paths = ['src/overlays', 'src/private-overlays']
1647
1648 for path in paths:
1649 path = os.path.join(chromeos_root, path, 'overlay-*')
1650 for overlay in sorted(glob.glob(path)):
1651 name = _get_overlay_name(overlay)
1652 if not name:
1653 continue
1654 # Special cases which have variant boards.
1655 if name in ['auron', 'guado', 'nyan', 'veyron']:
1656 continue
1657
1658 path = os.path.join(overlay, 'metadata', 'layout.conf')
1659 masters = []
1660 if os.path.exists(path):
1661 with open(path) as f:
1662 for line in f:
1663 m = re.search(r'masters\s*=(.*)', line)
1664 if m:
1665 masters = m.group(1).split()
1666 overlays[name] = masters
1667 return overlays
1668
1669
1670def resolve_basic_boards(overlays):
1671
1672 def normalize(name):
1673 return name.replace('-private', '')
1674
1675 def resolve(name):
1676 result = set()
1677 for parent in overlays[name]:
1678 assert parent != name, 'recursive overlays definition?'
1679 if parent not in overlays:
1680 continue
1681 for basic in resolve(parent):
1682 result.add(basic)
1683 if not result:
1684 result.add(name)
1685 return set(map(normalize, result))
1686
1687 result = {}
1688 for name in overlays:
1689 board = normalize(name)
1690 basic = resolve(name)
1691 assert len(basic) == 1
1692 basic_board = basic.pop()
1693 result[board] = basic_board
1694 return result
1695
1696
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001697def detect_branch_level(branch):
1698 """Given a branch name of manifest-internal, detect it's branch level.
1699
1700 level1: if ChromeOS version is x.0.0
1701 level2: if ChromeOS version is x.x.0
1702 level3: if ChromeOS version is x.x.x
1703 Where x is an non-zero integer.
1704
1705 Args:
1706 branch: branch name or ref name in manifest-internal
1707
1708 Returns:
1709 An integer indicates the branch level, or zero if not detectable.
1710 """
1711 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1712 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1713 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1714
1715 if re.match(level1, branch):
1716 return 1
1717 if re.match(level2, branch):
1718 return 2
1719 if re.match(level3, branch):
1720 return 3
1721 return 0
1722
1723
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001724def get_crosland_link(old, new):
1725 """Generates crosland link between two versions.
1726
1727 Args:
1728 old: ChromeOS version
1729 new: ChromeOS version
1730
1731 Returns:
1732 A crosland url.
1733 """
1734
1735 def version_to_url_parameter(ver):
1736 if is_cros_snapshot_version(ver):
1737 return snapshot_version_split(ver)[2]
1738 return version_to_short(ver)
1739
1740 old_parameter = version_to_url_parameter(old)
1741 new_parameter = version_to_url_parameter(new)
1742 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1743
1744
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001745class ChromeOSSpecManager(codechange.SpecManager):
1746 """Repo manifest related operations.
1747
1748 This class enumerates chromeos manifest files, parses them,
1749 and sync to disk state according to them.
1750 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001751
1752 def __init__(self, config):
1753 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001754 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1755 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001756 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1757 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001758 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001759 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001760 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001761 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1762 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001763
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001764 def lookup_snapshot_manifest_revisions(self, old, new):
1765 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001766
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001767 Returns:
1768 list of (timestamp, commit_id, snapshot_id):
1769 timestamp: integer unix timestamp
1770 commit_id: a string indicates commit hash
1771 snapshot_id: a string indicates snapshot id
1772 """
1773 assert is_cros_snapshot_version(old)
1774 assert is_cros_snapshot_version(new)
1775
1776 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001777 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001778 # Try to guess the commit time of a snapshot manifest, it is usually a few
1779 # minutes different between snapshot manifest commit and image.zip
1780 # generate.
1781 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001782 old_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001783 gs_path.format(board=self.config['board'], version=old)) - 86400
1784 except subprocess.CalledProcessError:
1785 old_timestamp = None
1786 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001787 new_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001788 gs_path.format(board=self.config['board'], version=new)) + 86400
1789 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1790 # we can find snapshot 5982
1791 # snapshot_id <= 5982 has different commit message format, so we need
1792 # to identify its id in different ways, see below comment for more info.
1793 new_timestamp = max(new_timestamp, 1558657989 + 1)
1794 except subprocess.CalledProcessError:
1795 new_timestamp = None
1796 result = []
1797 _, _, old_snapshot_id = snapshot_version_split(old)
1798 _, _, new_snapshot_id = snapshot_version_split(new)
1799 repo = self.manifest_internal_dir
1800 path = 'snapshot.xml'
1801 branch = 'snapshot'
1802 commits = git_util.get_history(
1803 repo,
1804 path,
1805 branch,
1806 after=old_timestamp,
1807 before=new_timestamp,
1808 with_subject=True)
1809
1810 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1811 # subject, as their subjects are all `Annealing manifest snapshot.`.
1812 # So instead we count the snapshot_id manually.
1813 count = 5982
1814 # There are two snapshot_id = 2633 in commit history, ignore the former
1815 # one.
1816 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1817 # We examine the commits in reverse order as there are some testing
1818 # commits before snapshot_id=2, this method works fine after
1819 # snapshot 2, except snapshot 2633
1820 for commit in reversed(commits):
1821 msg = commit[2]
1822 if commit[1] in ignore_list:
1823 continue
1824
1825 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1826 if match:
1827 snapshot_id = match.group(1)
1828 elif 'Annealing manifest snapshot' in msg:
1829 snapshot_id = str(count)
1830 count -= 1
1831 else:
1832 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08001833 # b/151054108: snapshot version in [29288, 29439] is broken
1834 if 29288 <= int(snapshot_id) <= 29439:
1835 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001836 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1837 result.append((commit[0], commit[1], snapshot_id))
1838 # We find commits in reversed order, now reverse it again to chronological
1839 # order.
1840 return list(reversed(result))
1841
1842 def lookup_build_timestamp(self, rev):
1843 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1844 if is_cros_full_version(rev):
1845 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001846 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001847
1848 def lookup_snapshot_build_timestamp(self, rev):
1849 assert is_cros_snapshot_version(rev)
1850 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1851
1852 def lookup_release_build_timestamp(self, rev):
1853 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001854 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001855 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1856 try:
1857 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1858 'refs/heads/master', path)
1859 except ValueError:
Kuang-che Wud1b74152020-05-20 08:46:46 +08001860 raise errors.InternalError('%s does not have %s' %
1861 (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001862 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001863
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001864 def detect_float_spec_branch_level(self, spec):
1865 results = [
1866 detect_branch_level(branch) for branch in git_util.get_branches(
1867 self.manifest_dir, commit=spec.name)
1868 ]
1869 results = [x for x in results if x > 0]
1870 return min(results) if results else 0
1871
1872 def branch_between_float_specs(self, old_spec, new_spec):
1873 if old_spec.spec_type != codechange.SPEC_FLOAT:
1874 return False
1875 if new_spec.spec_type != codechange.SPEC_FLOAT:
1876 return False
1877
1878 level_old = self.detect_float_spec_branch_level(old_spec)
1879 level_new = self.detect_float_spec_branch_level(new_spec)
1880
1881 if not level_old or not level_new:
1882 logger.warning('branch level detect failed, assume master')
1883 return False
1884 return level_old != level_new
1885
Kuang-che Wud558a042020-06-06 02:11:00 +08001886 def _determine_float_branch(self, old, new, fixed_specs):
1887 # There is no revision tag in snapshot's xml. We know snapshot
1888 # builds are on master branch.
1889 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001890 if fixed_specs[0].revision:
1891 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001892 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
1893 else:
1894 old_branches = [master_refname]
1895
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001896 if fixed_specs[-1].revision:
1897 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001898 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001899 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08001900 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001901
Kuang-che Wud558a042020-06-06 02:11:00 +08001902 common_branches = list(set(old_branches) & set(new_branches))
1903 assert common_branches, '%s and %s are not on common branches?' % (old, new)
1904
1905 if len(common_branches) == 1:
1906 return common_branches[0]
1907
1908 # There are more than one common branches, use heuristic to tie breaking.
1909 # The heuristic is simple: choice the branch with "smallest" number.
1910 # "Smaller" means the more major branch (not branched) or branched later.
1911 #
1912 # Following is the commit graph of manifest-internal repo. It shows many
1913 # interesting cases.
1914 #
1915 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
1916 # --A--+---X--------------X------B-------X-----------> master
1917 # \
1918 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
1919 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
1920 # \ \
1921 # \ E------------> stabilize-13020.67.B
1922 # \ 83/13020.55.1
1923 # F-----X--------------------> stabilize-13020.55.B
1924 #
1925 # How to read this graph:
1926 # - Time goes from left to right. Branch names are on the right side of
1927 # arrows.
1928 # - Letters A-F are manifest commits.
1929 # - Marker X means release image build at that time, the version numbers
1930 # are labeled above the X marker.
1931 # For example,
1932 # 1) 13021.0.0 release is based on manifest A, which is on all branches
1933 # shown on the graph.
1934 # We know 13021.0.0 is on master (and R84 branch later, not shown in
1935 # this graph), not on 13020* branches.
1936 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
1937 # (R83-13020.B, 13020.67.B, and 13020.55.B).
1938 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
1939 # 13020.55.B.
1940 #
1941 # There is an important property here. Every time a new branch is created,
1942 # there will always be a commit (like C, E, and F) to fix "revision" field
1943 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
1944 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
1945 # xxxxx.B branch.
1946 #
1947 # With such property, among the branches containing the given manifest
1948 # file, the branch with "smallest" number guarantees where the release is.
1949
1950 def branch_key(s):
1951 if s == master_refname:
1952 return 0, 0, 0
1953 m = re.search(r'-(\d+)\.B$', s)
1954 if m:
1955 return int(m.group(1)), 0, 0
1956 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
1957 if m:
1958 return int(m.group(1)), int(m.group(2)), 0
1959 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
1960 if m:
1961 return int(m.group(1)), int(m.group(2)), int(m.group(3))
1962
1963 logger.warning('unexpected branch name: %s', s)
1964 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
1965
1966 common_branches.sort(key=branch_key)
1967 return common_branches[0]
1968
1969 def collect_float_spec(self, old, new, fixed_specs=None):
1970 assert fixed_specs
1971 branch = self._determine_float_branch(old, new, fixed_specs)
1972 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001973
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001974 old_timestamp = self.lookup_build_timestamp(old)
1975 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001976 # snapshot time is different from commit time
1977 # usually it's a few minutes different
1978 # 30 minutes should be safe in most cases
1979 if is_cros_snapshot_version(old):
1980 old_timestamp = old_timestamp - 1800
1981 if is_cros_snapshot_version(new):
1982 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001983
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001984 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001985 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001986 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001987 parser = repo_util.ManifestParser(self.manifest_dir)
1988 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001989 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001990 result.append(
1991 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1992 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001993
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001994 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001995 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1996 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1997
1998 # case 1: if both are snapshot, return a list of snapshot
1999 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
2000 return self.collect_snapshot_specs(old, new)
2001
2002 # case 2: if both are release version
2003 # return a list of release version
2004 if is_cros_full_version(old) and is_cros_full_version(new):
2005 return self.collect_release_specs(old, new)
2006
2007 # case 3: return a list of release version and append a snapshot
2008 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002009 result = self.collect_release_specs(
2010 version_to_full(self.config['board'], old),
2011 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002012 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002013 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002014 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08002015 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002016 return result
2017
2018 def collect_snapshot_specs(self, old, new):
2019 assert is_cros_snapshot_version(old)
2020 assert is_cros_snapshot_version(new)
2021
2022 def guess_snapshot_version(board, snapshot_id, old, new):
2023 if old.endswith('-' + snapshot_id):
2024 return old
2025 if new.endswith('-' + snapshot_id):
2026 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08002027 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002028 'R*-{snapshot_id}-*'.format(
2029 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002030 for line in gsutil_ls(gs_path, ignore_errors=True):
2031 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
2032 if m:
2033 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002034 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002035
2036 result = []
2037 path = 'snapshot.xml'
2038 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002039 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002040 snapshot_version = guess_snapshot_version(self.config['board'],
2041 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002042 if snapshot_version:
2043 result.append(
2044 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
2045 path))
2046 else:
2047 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002048 return result
2049
2050 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002051 assert is_cros_full_version(old)
2052 assert is_cros_full_version(new)
2053 old_milestone, old_short_version = version_split(old)
2054 new_milestone, new_short_version = version_split(new)
2055
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002056 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002057 for milestone in git_util.list_dir_from_revision(
2058 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
2059 if not milestone.isdigit():
2060 continue
2061 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
2062 continue
2063
Kuang-che Wu74768d32018-09-07 12:03:24 +08002064 files = git_util.list_dir_from_revision(
2065 self.historical_manifest_git_dir, 'refs/heads/master',
2066 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002067
2068 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002069 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002070 short_version, ext = os.path.splitext(fn)
2071 if ext != '.xml':
2072 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002073 if (util.is_version_lesseq(old_short_version, short_version) and
2074 util.is_version_lesseq(short_version, new_short_version) and
2075 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002076 rev = make_cros_full_version(milestone, short_version)
2077 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
2078 'refs/heads/master', path)
2079 result.append(
2080 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002081
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002082 def version_key_func(spec):
2083 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002084 return util.version_key_func(short_version)
2085
2086 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002087 assert result[0].name == old
2088 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002089 return result
2090
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002091 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002092 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2093 if is_cros_full_version(rev):
2094 milestone, short_version = version_split(rev)
2095 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2096 manifest = git_util.get_file_from_revision(
2097 self.historical_manifest_git_dir, 'refs/heads/master', path)
2098 else:
2099 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2100 commit_id = revisions[0][1]
2101 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2102 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002103 return manifest
2104
2105 def get_manifest_file(self, rev):
2106 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002107 manifest_name = 'manifest_%s.xml' % rev
2108 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2109 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002110 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002111
2112 # workaround for b/150572399
2113 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2114 # folder .repo instead of.repo/manifests
2115 if is_cros_version_lesseq(rev, '12931.0.0'):
2116 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2117 manifest_patch_path = os.path.join(repo_path, manifest_name)
2118 with open(manifest_patch_path, 'w') as f:
2119 f.write(self.get_manifest(rev))
2120
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002121 return manifest_name
2122
2123 def parse_spec(self, spec):
2124 parser = repo_util.ManifestParser(self.manifest_dir)
2125 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002126 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002127 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002128 with open(manifest_path) as f:
2129 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002130 root = parser.parse_single_xml(content, allow_include=False)
2131 else:
2132 root = parser.parse_xml_recursive(spec.name, spec.path)
2133
2134 spec.entries = parser.process_parsed_result(root)
2135 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002136 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002137 raise ValueError('fixed spec %r has unexpected floating entries' %
2138 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002139 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002140
2141 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002142 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002143
2144 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2145 # manifest. 'repo sync -m' is not enough
2146 repo_util.init(
2147 self.config['chromeos_root'],
2148 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2149 manifest_name=manifest_name,
2150 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002151 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002152 # b/150753074: moblab is in non-default group and causes mark_as_stable
2153 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002154 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002155 )
2156
2157 # Note, don't sync with current_branch=True for chromeos. One of its
2158 # build steps (inside mark_as_stable) executes "git describe" which
2159 # needs git tag information.
2160 repo_util.sync(self.config['chromeos_root'])