blob: 5b338b30971ba36d5abf8b9295633cdcd62f5fa5 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""ChromeOS utility.
6
7Terminology used in this module.
8 short_version: ChromeOS version number without milestone, like "9876.0.0".
9 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080010 snapshot_version: ChromeOS version number with milestone and snapshot id,
11 like "R62-9876.0.0-12345".
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080012 version: if not specified, it could be in short or full format.
13"""
14
15from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080016import ast
Kuang-che Wu72b5a572019-10-29 20:37:57 +080017import calendar
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080018import datetime
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080019import errno
Kuang-che Wu9d14c162020-11-03 19:35:18 +080020import glob
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080021import json
22import logging
23import os
24import re
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +080025import shutil
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080026import subprocess
Kuang-che Wud558a042020-06-06 02:11:00 +080027import sys
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080028import time
29
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080030from google.protobuf import json_format
31
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080032from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080033from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080034from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080035from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080036from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080037from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080038from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080039from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080040from bisect_kit import util
41
42logger = logging.getLogger(__name__)
43
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080045re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080046re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080047
48gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080049
50# Assume gsutil is in PATH.
51gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080052
53# Since snapshots with version >= 12618.0.0 have android and chrome version
54# info.
55snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080056
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +080057# http://crbug.com/1170601, small snapshot ids should be ignored
58# 21000 is R80-12617.0.0
59snapshot_cutover_id = 21000
60
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080061# current earliest buildbucket buildable versions
62# picked from https://crrev.com/c/2072618
63buildbucket_cutover_versions = [
64 '12931.0.0',
65 '12871.26.0', # R81
66 '12871.24.2', # stabilize-12871.24.B
67 '12812.10.0', # factory-excelsior-12812.B
68 '12768.14.0', # firmware-servo-12768.B
69 '12739.85.0', # R80
70 '12739.67.1', # stabilize-excelsior-12739.67.B
71 '12692.36.0', # factory-hatch-12692.B
72 '12672.104.0', # firmware-hatch-12672.B
73 '12607.110.0', # R79
74 '12607.83.2', # stabilize-quickfix-12607.83.B
75 '12587.59.0', # factory-kukui-12587.B
76 '12573.78.0', # firmware-kukui-12573.B
77 '12499.96.0', # R78
78 '12422.33.0', # firmware-mistral-12422.B
79 '12371.190.0', # R77
80 '12361.38.0', # factory-mistral-12361.B
81 '12200.65.0', # firmware-sarien-12200.B
82 '12105.128.0', # R75
83 '12033.82.0', # factory-sarien-12033.B
84]
85
Kuang-che Wub9705bd2018-06-28 17:59:18 +080086chromeos_root_inside_chroot = '/mnt/host/source'
87# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080088prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080089prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080090# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
91cached_images_dir = 'src/build/images'
92test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080093
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080094VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
95VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
96VERSION_KEY_MILESTONE = 'milestone'
97VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080098VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080099VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +0800100CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800101
102
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800103class NeedRecreateChrootException(Exception):
104 """Failed to build ChromeOS because of chroot mismatch or corruption"""
105
106
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800107def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800108 """Determines if `s` is chromeos short version.
109
110 This function doesn't accept version number of local build.
111 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800112 return bool(re.match(re_chromeos_short_version, s))
113
114
115def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800116 """Determines if `s` is chromeos full version.
117
118 This function doesn't accept version number of local build.
119 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800120 return bool(re.match(re_chromeos_full_version, s))
121
122
123def is_cros_version(s):
124 """Determines if `s` is chromeos version (either short or full)"""
125 return is_cros_short_version(s) or is_cros_full_version(s)
126
127
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800128def is_cros_snapshot_version(s):
129 """Determines if `s` is chromeos snapshot version"""
130 return bool(re.match(re_chromeos_snapshot_version, s))
131
132
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800133def is_cros_version_lesseq(ver1, ver2):
134 """Determines if ver1 is less or equal to ver2.
135
136 Args:
137 ver1: a Chrome OS version in short, full, or snapshot format.
138 ver2: a Chrome OS version in short, full, or snapshot format.
139
140 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800141 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800142 """
143 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
144 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
145
Kuang-che Wu430c5282021-01-27 21:10:25 +0800146 # Compare milestone if available.
147 m1 = re.match(r'R(\d+)', ver1)
148 m2 = re.match(r'R(\d+)', ver2)
149 if m1 and m2 and int(m1.group(1)) > int(m2.group(1)):
150 return False
151
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800152 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
153 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
154 return ver1 <= ver2
155
156
Kuang-che Wu430c5282021-01-27 21:10:25 +0800157def is_ancestor_version(ver1, ver2):
158 """Determines `ver1` version is ancestor of `ver2` version.
159
160 Returns:
161 True only if `ver1` is the ancestor of `ver2`. One version is not considered
162 as ancestor of itself.
163 """
164 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
165 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
166
167 if is_cros_version_lesseq(ver2, ver1): # pylint: disable=arguments-out-of-order
168 return False
169 if not is_cros_version_lesseq(ver1, ver2):
170 return False
171
172 if not util.is_direct_relative_version(
173 version_to_short(ver1), version_to_short(ver2)):
174 return False
175
176 # Compare snapshot id if available.
177 if is_cros_snapshot_version(ver1) and is_cros_snapshot_version(ver2):
178 _, short_1, snapshot_1 = snapshot_version_split(ver1)
179 _, short_2, snapshot_2 = snapshot_version_split(ver2)
180 if short_1 == short_2 and snapshot_1 >= snapshot_2:
181 return False
182
183 return True
184
185
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800186def is_buildbucket_buildable(version):
187 """Determines if a version is buildable on buildbucket."""
188 short_version = version_to_short(version)
189 # If given version is child of any cutover, then it's buildable
190 return any([
191 util.is_direct_relative_version(x, short_version) and
192 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
193 ])
194
195
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800196def make_cros_full_version(milestone, short_version):
197 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800198 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800199 return 'R%s-%s' % (milestone, short_version)
200
201
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800202def make_cros_snapshot_version(milestone, short_version, snapshot_id):
203 """Makes snapshot version from milestone, short_version and snapshot id"""
204 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
205
206
207def version_split(version):
208 """Splits full_version or snapshot_version into milestone and short_version"""
209 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
210 if is_cros_snapshot_version(version):
211 return snapshot_version_split(version)[0:2]
212 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800213 return milestone[1:], short_version
214
215
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800216def snapshot_version_split(snapshot_version):
217 """Splits snapshot_version into milestone, short_version and snapshot_id"""
218 assert is_cros_snapshot_version(snapshot_version)
Kuang-che Wu430c5282021-01-27 21:10:25 +0800219 milestone, short_version, snapshot_id = snapshot_version.split('-')
220 return milestone[1:], short_version, snapshot_id
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800221
222
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800223def query_snapshot_buildbucket_id(board, snapshot_version):
224 """Query buildbucket id of a snapshot"""
225 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800226 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800227 '/{snapshot_version}-*/image.zip')
228 output = gsutil_ls(
229 '-d',
230 path.format(board=board, snapshot_version=snapshot_version),
231 ignore_errors=True)
232 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800233 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800234 if m:
235 return m.group(1)
236 return None
237
238
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800239def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800240 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800241 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800242 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 +0800243 return s
244
245
246def query_dut_lsb_release(host):
247 """Query /etc/lsb-release of given DUT
248
249 Args:
250 host: the DUT address
251
252 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800253 dict for keys and values of /etc/lsb-release.
254
255 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800256 errors.SshConnectionError: cannot connect to host
257 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800258 """
259 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800260 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800261 except subprocess.CalledProcessError as e:
262 raise errors.ExternalError(
263 'unable to read /etc/lsb-release; not a DUT') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800264 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
265
266
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800267def query_dut_os_release(host):
268 """Query /etc/os-release of given DUT
269
270 Args:
271 host: the DUT address
272
273 Returns:
274 dict for keys and values of /etc/os-release.
275
276 Raises:
277 errors.SshConnectionError: cannot connect to host
278 errors.ExternalError: lsb-release file doesn't exist
279 """
280 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800281 output = util.ssh_cmd(host, 'cat', '/etc/os-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800282 except subprocess.CalledProcessError as e:
283 raise errors.ExternalError(
284 'unable to read /etc/os-release; not a DUT') from e
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800285 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
286
287
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800288def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800289 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800290
291 Args:
292 host: the DUT address
293
294 Returns:
295 True if the host is a chromeos device.
296 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800297 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800298 return query_dut_os_release(host).get('ID') in [
299 'chromiumos',
300 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800301 ]
302 except (errors.ExternalError, errors.SshConnectionError):
303 return False
304
305
306def is_good_dut(host):
307 if not is_dut(host):
308 return False
309
310 # Sometimes python is broken after 'cros flash'.
311 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800312 util.ssh_cmd(host, 'python', '-c', '1', allow_retry=True)
Kuang-che Wu44278142019-03-04 11:33:57 +0800313 return True
314 except (subprocess.CalledProcessError, errors.SshConnectionError):
315 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800316
317
318def query_dut_board(host):
319 """Query board name of a given DUT"""
320 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
321
322
323def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800324 """Query short version of a given DUT.
325
326 This function may return version of local build, which
327 is_cros_short_version() is false.
328 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800329 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
330
331
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800332def query_dut_prebuilt_version(host):
333 """Return a snapshot version or short version of a given DUT.
334
335 Args:
336 host: dut host
337
338 Returns:
339 Snapshot version or short version.
340 """
341 lsb_release = query_dut_lsb_release(host)
342 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
Kuang-che Wu25fec6f2021-01-28 12:40:43 +0800343 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800344 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
345 builder_path)
346 if match:
347 return match.group(1)
348 return release_version
349
350
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800351def query_dut_is_by_official_builder(host):
Kuang-che Wuc092bd52021-01-05 14:25:56 +0800352 """Query if given DUT is build by official builder"""
353 build_type = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
354 '')
355 build_type = build_type.split(' - ')[0]
356 assert build_type in ('Official Build', 'Continuous Builder',
357 'Developer Build',
358 'Test Build'), 'unknown build type (%s)' % build_type
359 return build_type in ['Official Build', 'Continuous Builder']
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800360
361
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800362def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800363 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800364
365 Args:
366 host: DUT address
367 connect_timeout: connection timeout
368
369 Returns:
370 boot uuid
371 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800372 return util.ssh_cmd(
373 host,
374 'cat',
375 '/proc/sys/kernel/random/boot_id',
376 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800377
378
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800379def reboot(host, force_reboot_callback=None):
380 """Reboot a DUT and verify.
381
382 Args:
383 host: DUT address
384 force_reboot_callback: powerful reboot hook (via servo). This will be
385 invoked if normal reboot failed.
386 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800387 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800388 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800389 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800390 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800391
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800392 try:
393 util.ssh_cmd(host, 'reboot')
394 except errors.SshConnectionError:
395 # Depends on timing, ssh may return failure due to broken pipe, which is
396 # working as intended. Ignore such kind of errors.
397 pass
398
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800399 wait_reboot_done(host, boot_id)
400 except (errors.SshConnectionError, errors.ExternalError):
401 if force_reboot_callback and force_reboot_callback(host):
402 wait_reboot_done(host, boot_id)
403 return
404 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800405
Kuang-che Wu708310b2018-03-28 17:24:34 +0800406
407def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800408 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800409 # (dev screen short delay) or more (long delay).
410 time.sleep(15)
411 for _ in range(100):
412 try:
413 # During boot, DUT does not response and thus ssh may hang a while. So
414 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
415 # set tight limit because it's inside retry loop.
416 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
417 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800418 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800419 logger.debug('reboot not ready? sleep wait 1 sec')
420 time.sleep(1)
421
Kuang-che Wue121fae2018-11-09 16:18:39 +0800422 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800423
424
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800425def gs_release_boardpath(board):
426 """Normalizes board name for gs://chromeos-releases/
427
428 This follows behavior of PushImage() in chromite/scripts/pushimage.py
429 Note, only gs://chromeos-releases/ needs normalization,
430 gs://chromeos-image-archive does not.
431
432 Args:
433 board: ChromeOS board name
434
435 Returns:
436 normalized board name
437 """
438 return board.replace('_', '-')
439
440
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800441def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800442 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800443
444 Args:
445 args: command line arguments passed to gsutil
446 kwargs:
447 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
448 but the path not found.
449
450 Returns:
451 stdout of gsutil
452
453 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800454 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800455 subprocess.CalledProcessError: command failed
456 """
457 stderr_lines = []
458 try:
459 return util.check_output(
460 gsutil_bin, *args, stderr_callback=stderr_lines.append)
461 except subprocess.CalledProcessError as e:
462 stderr = ''.join(stderr_lines)
463 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800464 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800465 'gsutil failed due to permission. ' +
466 'Run "%s config" and follow its instruction. ' % gsutil_bin +
467 'Fill any string if it asks for project-id')
468 if kwargs.get('ignore_errors'):
469 return ''
470 raise
471 except OSError as e:
472 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800473 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800474 'Unable to run %s. gsutil is not installed or not in PATH?' %
475 gsutil_bin)
476 raise
477
478
479def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800480 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800481
482 Args:
483 args: arguments passed to 'gsutil ls'
484 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800485 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800486 exception, ex. path not found.
487
488 Returns:
489 list of 'gsutil ls' result. One element for one line of gsutil output.
490
491 Raises:
492 subprocess.CalledProcessError: gsutil failed, usually means path not found
493 """
494 return gsutil('ls', *args, **kwargs).splitlines()
495
496
Kuang-che Wu876a5382020-10-27 14:24:58 +0800497def gsutil_stat_creation_time(*args, **kwargs):
498 """Returns the creation time of a file or multiple files.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800499
500 Args:
501 args: arguments passed to 'gsutil stat'.
502 kwargs: extra parameters for gsutil.
503
504 Returns:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800505 A integer indicates the creation timestamp.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800506
507 Raises:
508 subprocess.CalledProcessError: gsutil failed, usually means path not found
Kuang-che Wu876a5382020-10-27 14:24:58 +0800509 errors.ExternalError: creation time is not found
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800510 """
511 result = -1
512 # Currently we believe stat always returns a UTC time, and strptime also
513 # parses a UTC time by default.
514 time_format = '%a, %d %b %Y %H:%M:%S GMT'
515
516 for line in gsutil('stat', *args, **kwargs).splitlines():
517 if ':' not in line:
518 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800519 key, value = line.split(':', 1)
520 key, value = key.strip(), value.strip()
Kuang-che Wu876a5382020-10-27 14:24:58 +0800521 if key != 'Creation time':
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800522 continue
523 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800524 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800525 result = max(result, unixtime)
526
527 if result == -1:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800528 raise errors.ExternalError("didn't find creation time")
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800529 return result
530
531
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800532def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800533 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800534
535 Args:
536 board: ChromeOS board name
537 short_version: ChromeOS version number in short format, ex. 9300.0.0
538
539 Returns:
540 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
541 None if failed.
542 """
543 path = gs_archive_path.format(board=board) + '/R*-' + short_version
544 for line in gsutil_ls('-d', path, ignore_errors=True):
545 m = re.search(r'/R(\d+)-', line)
546 if not m:
547 continue
548 return m.group(1)
549
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800550 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800551 return None
552
553
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800554def list_board_names(chromeos_root):
555 """List board names.
556
557 Args:
558 chromeos_root: chromeos tree root
559
560 Returns:
561 list of board names
562 """
563 # Following logic is simplified from chromite/lib/portage_util.py
564 cros_list_overlays = os.path.join(chromeos_root,
565 'chromite/bin/cros_list_overlays')
566 overlays = util.check_output(cros_list_overlays).splitlines()
567 result = set()
568 for overlay in overlays:
569 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
570 name = None
571 if os.path.exists(conf_file):
572 for line in open(conf_file):
573 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
574 if m:
575 name = m.group(1)
576 break
577
578 if not name:
579 name_file = os.path.join(overlay, 'profiles', 'repo_name')
580 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800581 with open(name_file) as f:
582 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800583
584 if name:
585 name = re.sub(r'-private$', '', name)
586 result.add(name)
587
588 return list(result)
589
590
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800591def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800592 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800593
594 Args:
595 board: ChromeOS board name
596 version: ChromeOS version number in short or full format
597
598 Returns:
599 (milestone, version in short format)
600 """
601 if is_cros_short_version(version):
602 milestone = query_milestone_by_version(board, version)
603 short_version = version
604 else:
605 milestone, short_version = version_split(version)
606 return milestone, short_version
607
608
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800609def extract_major_version(version):
610 """Converts a version to its major version.
611
612 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800613 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800614
615 Returns:
616 major version number in string format
617 """
618 version = version_to_short(version)
619 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
620 return m.group(1)
621
622
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800623def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800624 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800625
626 Args:
627 version: ChromeOS version number in short or full format
628
629 Returns:
630 version number in short format
631 """
632 if is_cros_short_version(version):
633 return version
634 _, short_version = version_split(version)
635 return short_version
636
637
638def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800639 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800640
641 Args:
642 board: ChromeOS board name
643 version: ChromeOS version number in short or full format
644
645 Returns:
646 version number in full format
647 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800648 if is_cros_snapshot_version(version):
649 milestone, short_version, _ = snapshot_version_split(version)
650 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800651 if is_cros_full_version(version):
652 return version
653 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800654 if not milestone:
655 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
656 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800657 return make_cros_full_version(milestone, version)
658
659
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800660def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800661 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800662
663 Args:
664 board: ChromeOS board
665 major_version: ChromeOS major version
666
667 Returns:
668 list of (version, gs_path):
669 version: Chrome OS snapshot version
670 gs_path: gs path of test image
671 """
672
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800673 def extract_snapshot_id(result):
674 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
675 assert m
676 return int(m.group(1))
677
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800678 short_version = '%s.0.0' % major_version
679 milestone = query_milestone_by_version(board, short_version)
680 if not milestone:
681 milestone = '*'
682
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800683 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800684 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800685 result = []
686 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800687 path.format(
688 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800689 ignore_errors=True)
690
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800691 for gs_path in sorted(output):
692 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800693 if m:
694 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800695 # we should skip if there is duplicate snapshot
696 if result and result[-1][0] == snapshot_version:
697 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800698
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800699 _, _, snapshot_id = snapshot_version_split(snapshot_version)
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +0800700
701 # crbug/1170601: ignore small snapshot ids
702 if int(snapshot_id) <= snapshot_cutover_id:
703 continue
704
705 # b/151054108: snapshot version in [29288, 29439] is broken
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800706 if 29288 <= int(snapshot_id) <= 29439:
707 continue
708
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800709 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800710
711 # sort by its snapshot_id
712 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800713 return result
714
715
Kuang-che Wu575dc442019-03-05 10:30:55 +0800716def list_prebuilt_from_image_archive(board):
717 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
718
Kuang-che Wu575dc442019-03-05 10:30:55 +0800719 Args:
720 board: ChromeOS board name
721
722 Returns:
723 list of (version, gs_path):
724 version: Chrome OS version in full format
725 gs_path: gs path of test image
726 """
727 result = []
728 for line in gsutil_ls(gs_archive_path.format(board=board)):
729 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
730 if m:
731 full_version = m.group(1)
732 test_image = 'chromiumos_test_image.tar.xz'
733 assert line.endswith('/')
734 gs_path = line + test_image
735 result.append((full_version, gs_path))
736 return result
737
738
Kuang-che Wue1808402020-01-06 20:27:45 +0800739def has_test_image(board, version):
740 if is_cros_snapshot_version(version):
741 return bool(query_snapshot_buildbucket_id(board, version))
742
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800743 try:
744 full_version = version_to_full(board, version)
745 except errors.ExternalError:
746 # version_to_full() is implemented by checking image, thus its failure
747 # means no image.
748 return False
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800749 path = (
Kuang-che Wue1808402020-01-06 20:27:45 +0800750 gs_archive_path.format(board=board) +
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800751 '/%s/chromiumos_test_image.tar.xz' % full_version)
Kuang-che Wue1808402020-01-06 20:27:45 +0800752
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800753 if gsutil_ls(path, ignore_errors=True):
754 return True
Kuang-che Wue1808402020-01-06 20:27:45 +0800755 return False
756
757
Kuang-che Wu575dc442019-03-05 10:30:55 +0800758def list_chromeos_prebuilt_versions(board,
759 old,
760 new,
761 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800762 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800763 """Lists ChromeOS version numbers with prebuilt between given range
764
765 Args:
766 board: ChromeOS board name
767 old: start version (inclusive)
768 new: end version (inclusive)
769 only_good_build: only if test image is available
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800770 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800771
772 Returns:
773 list of sorted version numbers (in full format) between [old, new] range
774 (inclusive).
775 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800776 old_short = version_to_short(old)
777 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800778
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800779 rev_map = {
780 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800781 for full_version, gs_path in list_prebuilt_from_image_archive(board):
782 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800783 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800784
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800785 if use_snapshot:
786 for major_version in range(
787 int(extract_major_version(old)),
788 int(extract_major_version(new)) + 1):
789 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800790 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800791 # If current version is smaller than cutover, ignore it as it might not
792 # contain enough information for continuing android and chrome bisection.
793 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
794 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800795
796 # Given the fact that snapshots are images between two release versions.
797 # Adding snapshots of 12345.0.0 should be treated as adding commits
798 # between [12345.0.0, 12346.0.0).
799 # So in the following lines we check two facts:
800 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800801 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800802 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800803 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800804 continue
805 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800806 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800807 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800808 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800809 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800810
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800811 snapshots = list_snapshots_from_image_archive(board, str(major_version))
812 if snapshots:
813 # if snapshots found, we can append them after the release version,
814 # so the prebuilt image list of this version will be
815 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800816 if short_version not in rev_map:
817 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800818 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800819
820 result = []
821 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800822 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800823 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800824 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800825 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800826 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800827 continue
828
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800829 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800830
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800831 # version_to_full() and gsutil_ls() may take long time if versions are a
832 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800833
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800834 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800835 gs_result = gsutil_ls(gs_path, ignore_errors=True)
836 if not gs_result:
837 logger.warning('%s is not a good build, ignore', version)
838 continue
839 assert len(gs_result) == 1
840 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
841 if not m:
842 logger.warning('format of image path is unexpected: %s', gs_result[0])
843 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800844 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800845 elif is_cros_short_version(version):
846 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800847
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800848 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
849 version, new):
850 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800851
852 return result
853
854
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800855def prepare_snapshot_image(chromeos_root, board, snapshot_version):
856 """Prepare chromeos snapshot image.
857
858 Args:
859 chromeos_root: chromeos tree root
860 board: ChromeOS board name
861 snapshot_version: ChromeOS snapshot version number
862
863 Returns:
864 local file path of test image relative to chromeos_root
865 """
866 assert is_cros_snapshot_version(snapshot_version)
867 milestone, short_version, snapshot_id = snapshot_version_split(
868 snapshot_version)
869 full_version = make_cros_full_version(milestone, short_version)
870 tmp_dir = os.path.join(
871 chromeos_root, 'tmp',
872 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
873 if not os.path.exists(tmp_dir):
874 os.makedirs(tmp_dir)
875
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800876 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/' +
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800877 '{snapshot_version}-*/image.zip')
878 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
879
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800880 full_path = os.path.join(tmp_dir, test_image_filename)
881 rel_path = os.path.relpath(full_path, chromeos_root)
882 if os.path.exists(full_path):
883 return rel_path
884
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800885 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800886 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800887 gs_path = files[0]
888 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800889 util.check_call(
890 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
891 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800892 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800893
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800894 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800895 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800896
897
Kuang-che Wu28980b22019-07-31 19:51:45 +0800898def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800899 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800900
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800901 It searches for xbuddy image which "cros flash" can use, or fetch image to
902 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800903
904 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800905 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800906 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800907 version: ChromeOS version number in short or full format
908
909 Returns:
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800910 xbuddy path or None
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800911 """
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800912 del chromeos_root # unused
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800913 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800914 full_version = version_to_full(board, version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800915
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800916 gs_path = gs_archive_path.format(board=board) + '/' + full_version
917 if gsutil_ls('-d', gs_path, ignore_errors=True):
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800918 return 'xbuddy://remote/{board}/{full_version}/test'.format(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800919 board=board, full_version=full_version)
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800920 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800921
922
923def cros_flash(chromeos_root,
924 host,
925 board,
926 image_path,
927 version=None,
928 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800929 disable_rootfs_verification=True,
930 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800931 """Flash a DUT with given ChromeOS image.
932
933 This is implemented by 'cros flash' command line.
934
935 Args:
936 chromeos_root: use 'cros flash' of which chromeos tree
937 host: DUT address
938 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800939 image_path: chromeos image xbuddy path or file path. For relative
940 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800941 version: ChromeOS version in short or full format
942 clobber_stateful: Clobber stateful partition when performing update
943 disable_rootfs_verification: Disable rootfs verification after update
944 is completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800945 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800946
947 Raises:
948 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800949 """
950 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
951
952 # Reboot is necessary because sometimes previous 'cros flash' failed and
953 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800954 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800955
Kuang-che Wu020a1182020-09-08 17:17:22 +0800956 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +0800957 # The service is used in jetstream boards, but not other CrOS devices.
958 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
959 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +0800960 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
961 # For simplicity, we wait 60 seconds here, which is the timeout value of
962 # jetstream_host.
963 # https://chromium.googlesource.com/chromiumos/third_party/autotest
Kuang-che Wuebc2c362020-12-14 16:27:09 +0800964 # /+/HEAD/server/hosts/jetstream_host.py#27
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +0800965 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +0800966 util.ssh_cmd(host, 'stop', 'ap-update-manager')
967 except subprocess.CalledProcessError:
968 pass # not started; do nothing
969
Kuang-che Wu28980b22019-07-31 19:51:45 +0800970 # Handle relative path.
971 if '://' not in image_path and not os.path.isabs(image_path):
972 assert os.path.exists(os.path.join(chromeos_root, image_path))
973 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
974
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800975 args = [
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +0800976 '--debug',
977 '--no-ping',
978 # Speed up for slow network connection.
979 '--send-payload-in-parallel',
980 host,
981 image_path,
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800982 ]
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +0800983 # TODO(kcwu): remove this check if we don't need to support chromeos versions
984 # earlier than Dec 2020.
985 if git_util.is_ancestor_commit(
986 os.path.join(chromeos_root, 'chromite'), '9ed30bc3ed292b', 'HEAD'):
987 # To reduce disk usage on DUT.
988 args.append('--no-copy-payloads-to-device')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800989 if clobber_stateful:
990 args.append('--clobber-stateful')
991 if disable_rootfs_verification:
992 args.append('--disable-rootfs-verification')
993
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800994 try:
995 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800996 except subprocess.CalledProcessError as e:
997 raise errors.ExternalError('cros flash failed') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800998
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800999 if version:
1000 # In the past, cros flash may fail with returncode=0
1001 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001002 if is_cros_snapshot_version(version):
1003 builder_path = query_dut_lsb_release(host).get(
1004 'CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001005 expect_prefix = '%s-snapshot/%s-' % (board, version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001006 if not builder_path.startswith(expect_prefix):
1007 raise errors.ExternalError(
1008 'although cros flash succeeded, the OS builder path is '
1009 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1010 else:
1011 expect_version = version_to_short(version)
1012 dut_version = query_dut_short_version(host)
1013 if dut_version != expect_version:
1014 raise errors.ExternalError(
1015 'although cros flash succeeded, the OS version is unexpected: '
1016 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001017
Kuang-che Wu4a81ea72019-10-05 15:35:17 +08001018 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001019 # (b/130786578), so it's necessary to do sanity check.
1020 if not is_good_dut(host):
1021 raise errors.ExternalError(
1022 'although cros flash succeeded, the DUT is in bad state')
1023
1024
1025def cros_flash_with_retry(chromeos_root,
1026 host,
1027 board,
1028 image_path,
1029 version=None,
1030 clobber_stateful=False,
1031 disable_rootfs_verification=True,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001032 repair_callback=None,
1033 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001034 # 'cros flash' is not 100% reliable, retry if necessary.
1035 for attempt in range(2):
1036 if attempt > 0:
1037 logger.info('will retry 60 seconds later')
1038 time.sleep(60)
1039
1040 try:
1041 cros_flash(
1042 chromeos_root,
1043 host,
1044 board,
1045 image_path,
1046 version=version,
1047 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001048 disable_rootfs_verification=disable_rootfs_verification,
1049 force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001050 return True
1051 except errors.ExternalError:
1052 logger.exception('cros flash failed')
1053 if repair_callback and not repair_callback(host):
1054 logger.warning('not repaired, assume it is harmless')
1055 continue
1056 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001057
1058
1059def version_info(board, version):
1060 """Query subcomponents version info of given version of ChromeOS
1061
1062 Args:
1063 board: ChromeOS board name
1064 version: ChromeOS version number in short or full format
1065
1066 Returns:
1067 dict of component and version info, including (if available):
1068 cros_short_version: ChromeOS version
1069 cros_full_version: ChromeOS version
1070 milestone: milestone of ChromeOS
1071 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001072 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001073 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1074 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001075 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001076 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001077 milestone, short_version, _ = snapshot_version_split(version)
1078 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001079 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001080 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001081 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001082 VERSION_KEY_MILESTONE: milestone,
1083 VERSION_KEY_CROS_FULL_VERSION: version,
1084 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001085 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1086 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1087 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001088 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001089 info = {}
1090 full_version = version_to_full(board, version)
1091
1092 # Some boards may have only partial-metadata.json but no metadata.json.
1093 # e.g. caroline R60-9462.0.0
1094 # Let's try both.
1095 metadata = None
1096 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001097 path = gs_archive_path.format(
1098 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001099 metadata = gsutil('cat', path, ignore_errors=True)
1100 if metadata:
1101 o = json.loads(metadata)
1102 v = o['version']
1103 board_metadata = o['board-metadata'][board]
1104 info.update({
1105 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1106 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1107 VERSION_KEY_MILESTONE: v['milestone'],
1108 VERSION_KEY_CR_VERSION: v['chrome'],
1109 })
1110
1111 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001112 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001113 if 'android-branch' in v: # this appears since R58-9317.0.0
1114 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1115 elif 'android-container-branch' in board_metadata:
1116 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1117 break
1118 else:
1119 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1120 logger.error(
1121 'Note, so far no quick way to look up version info for too old builds')
1122
1123 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001124
1125
1126def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001127 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001128
1129 Args:
1130 board: ChromeOS board name
1131 version: ChromeOS version number in short or full format
1132
1133 Returns:
1134 Chrome version number
1135 """
1136 info = version_info(board, version)
1137 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001138
1139
1140def query_android_build_id(board, rev):
1141 info = version_info(board, rev)
1142 rev = info['android_build_id']
1143 return rev
1144
1145
1146def query_android_branch(board, rev):
1147 info = version_info(board, rev)
1148 rev = info['android_branch']
1149 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001150
1151
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001152def guess_chrome_version(board, rev):
1153 """Guess chrome version number.
1154
1155 Args:
1156 board: chromeos board name
1157 rev: chrome or chromeos version
1158
1159 Returns:
1160 chrome version number
1161 """
1162 if is_cros_version(rev):
1163 assert board, 'need to specify BOARD for cros version'
1164 rev = query_chrome_version(board, rev)
1165 assert cr_util.is_chrome_version(rev)
1166
1167 return rev
1168
1169
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001170def is_inside_chroot():
1171 """Returns True if we are inside chroot."""
1172 return os.path.exists('/etc/cros_chroot_version')
1173
1174
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001175def convert_path_outside_chroot(chromeos_root, path):
1176 """Converts path in chroot to outside.
1177
1178 Args:
1179 chromeos_root: chromeos tree root
1180 path: path inside chroot; support starting with '~/'
1181
1182 Returns:
1183 The corresponding path outside chroot assuming the chroot is mounted
1184 """
1185 if path.startswith('~/'):
1186 path = path.replace('~', '/home/' + os.environ['USER'])
1187 assert '~' not in path, 'tilde (~) character is not fully supported'
1188
1189 assert os.path.isabs(path)
1190 assert path[0] == os.sep
1191 return os.path.join(chromeos_root, 'chroot', path[1:])
1192
1193
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001194def cros_sdk(chromeos_root,
1195 *args,
1196 chrome_root=None,
1197 env=None,
1198 log_stdout=True,
1199 stdin=None,
1200 stderr_callback=None,
1201 goma_dir=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001202 """Run commands inside chromeos chroot.
1203
1204 Args:
1205 chromeos_root: chromeos tree root
1206 *args: command to run
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001207 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
1208 env: (dict) environment variables for the command
1209 log_stdout: Whether write the stdout output of the child process to log.
1210 stdin: standard input file handle for the command
1211 stderr_callback: Callback function for stderr. Called once per line.
1212 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001213 """
1214 envs = []
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001215 if env:
1216 for k, v in env.items():
1217 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1218 envs.append('%s=%s' % (k, v))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001219
1220 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1221 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001222 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001223
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001224 if chrome_root:
1225 prefix += ['--chrome_root', chrome_root]
1226 if goma_dir:
1227 prefix += ['--goma_dir', goma_dir]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001228
Kuang-che Wu399d4662019-06-06 15:23:37 +08001229 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001230
Kuang-che Wu399d4662019-06-06 15:23:37 +08001231 # In addition to the output of command we are interested, cros_sdk may
1232 # generate its own messages. For example, chroot creation messages if we run
1233 # cros_sdk the first time.
1234 # This is the hack to run dummy command once, so we can get clean output for
1235 # the command we are interested.
1236 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001237 try:
1238 util.check_call(*cmd, cwd=chromeos_root)
1239 except subprocess.CalledProcessError:
1240 logger.exception('cros_sdk init/update failed')
1241 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001242
1243 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001244 return util.check_output(
1245 *cmd,
1246 cwd=chromeos_root,
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001247 log_stdout=log_stdout,
1248 stdin=stdin,
1249 stderr_callback=stderr_callback)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001250
1251
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001252def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001253 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001254
1255 Args:
1256 chromeos_root: chromeos tree root
1257 """
1258 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1259 return
1260 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1261 return
1262
1263 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1264
1265
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001266def mount_chroot(chromeos_root):
1267 """Creates ChromeOS chroot if necessary.
1268
1269 Args:
1270 chromeos_root: chromeos tree root
1271 """
1272 # An arbitrary file must exist in chroot.
1273 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1274
1275 # Not created or mounted yet.
1276 if not os.path.exists(path):
1277 create_chroot(chromeos_root)
1278 # After this command, the chroot is mounted.
1279 cros_sdk(chromeos_root, 'true')
1280 assert os.path.exists(path)
1281
1282
1283def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001284 """Copies file into chromeos chroot.
1285
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001286 The side effect is chroot created and mounted.
1287
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001288 Args:
1289 chromeos_root: chromeos tree root
1290 src: path outside chroot
1291 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001292 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001293 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001294 mount_chroot(chromeos_root)
1295 src = os.path.expanduser(src)
1296 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1297 if not overwrite and os.path.exists(dst_outside):
1298 return
1299
1300 # Haven't support directory or special files yet.
1301 assert os.path.isfile(src)
1302 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1303
1304 dirname = os.path.dirname(dst_outside)
1305 if not os.path.exists(dirname):
1306 os.makedirs(dirname)
1307 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001308
1309
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001310def prepare_chroot(chromeos_root):
1311 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001312
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001313 # Work around b/149077936:
1314 # The creds file is copied into the chroot since 12866.0.0.
1315 # But earlier versions need this file as well because of cipd ACL change.
1316 creds_path = '~/.config/chrome_infra/auth/creds.json'
1317 if os.path.exists(os.path.expanduser(creds_path)):
1318 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001319
1320
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001321def check_if_need_recreate_chroot(stdout, stderr):
1322 """Analyze build log and determine if chroot should be recreated.
1323
1324 Args:
1325 stdout: stdout output of build
1326 stderr: stderr output of build
1327
1328 Returns:
1329 the reason if chroot needs recreated; None otherwise
1330 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001331 if re.search(
1332 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001333 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001334 return 'EAPI version mismatch'
1335
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001336 if 'Chroot is too new. Consider running:' in stderr:
1337 return 'chroot version is too new'
1338
1339 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001340 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1341 return 'chroot version is too new'
1342
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001343 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1344 if "undefined reference to 'std::__1::basic_string" in stdout:
1345 return 'might be due to compiler change'
1346
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001347 # Detect failures due to file collisions.
1348 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1349 # and conflict with each other. Other possible cases are package renaming or
1350 # refactoring. Let's recreate chroot to work around them.
1351 if 'Detected file collision' in stdout:
1352 # Using wildcard between words because the text wraps to the next line
1353 # depending on length of package name and each line is prefixed with
1354 # package name.
1355 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1356 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1357 # package name (65 now).
1358 m = re.search(
1359 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1360 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1361 if m:
1362 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001363
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001364 return None
1365
1366
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001367def build_packages(chromeos_root,
1368 board,
1369 chrome_root=None,
1370 goma_dir=None,
1371 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001372 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001373
1374 Args:
1375 chromeos_root: chromeos tree root
1376 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001377 chrome_root: Chrome tree root. If specified, build chrome using the
1378 provided tree
1379 goma_dir: Goma installed directory to mount into the chroot. If specified,
1380 build chrome with goma.
1381 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001382 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001383
1384 def has_build_package_argument(argument):
1385 stderr_lines = []
1386 try:
1387 util.check_call(
1388 'src/scripts/build_packages',
1389 '--help',
1390 cwd=chromeos_root,
1391 stderr_callback=stderr_lines.append)
1392 except subprocess.CalledProcessError:
1393 help_output = ''.join(stderr_lines)
1394 return '--[no]%s' % argument in help_output
1395
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001396 common_env = {
1397 'USE': '-cros-debug chrome_internal',
1398 'FEATURES': 'separatedebug',
1399 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001400 stderr_lines = []
1401 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001402 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001403 env = common_env.copy()
1404 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001405 cros_sdk(
1406 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001407 './update_chroot',
1408 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001409 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001410 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001411 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001412
1413 env = common_env.copy()
1414 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001415 './build_packages',
1416 '--board',
1417 board,
1418 '--withdev',
1419 '--noworkon',
1420 '--skip_chroot_upgrade',
1421 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001422 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001423
1424 # `use_any_chrome` flag is default on and will force to use a chrome
1425 # prebuilt even if the version doesn't match.
1426
1427 # As this argument is landed in 12681, we should check if the argument
1428 # exists before adding this.
1429 if has_build_package_argument('use_any_chrome'):
1430 cmd.append('--nouse_any_chrome')
1431
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001432 if goma_dir:
1433 # Tell build_packages to start and stop goma
1434 cmd.append('--run_goma')
1435 env['USE_GOMA'] = 'true'
1436 if afdo_use:
1437 env['USE'] += ' afdo_use'
1438 cros_sdk(
1439 chromeos_root,
1440 *cmd,
1441 env=env,
1442 chrome_root=chrome_root,
1443 stderr_callback=stderr_lines.append,
1444 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001445 except subprocess.CalledProcessError as e:
1446 # Detect failures due to incompatibility between chroot and source tree. If
1447 # so, notify the caller to recreate chroot and retry.
1448 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1449 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001450 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001451
1452 # For other failures, don't know how to handle. Just bail out.
1453 raise
1454
Kuang-che Wu28980b22019-07-31 19:51:45 +08001455
1456def build_image(chromeos_root, board):
1457 """Build ChromeOS image.
1458
1459 Args:
1460 chromeos_root: chromeos tree root
1461 board: ChromeOS board name
1462
1463 Returns:
1464 image folder; relative to chromeos_root
1465 """
1466 stderr_lines = []
1467 try:
1468 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1469 cros_sdk(
1470 chromeos_root,
1471 './build_image',
1472 '--board',
1473 board,
1474 '--noenable_rootfs_verification',
1475 'test',
1476 env={
1477 'USE': '-cros-debug chrome_internal',
1478 'FEATURES': 'separatedebug',
1479 },
1480 stderr_callback=stderr_lines.append)
1481 except subprocess.CalledProcessError as e:
1482 # Detect failures due to incompatibility between chroot and source tree. If
1483 # so, notify the caller to recreate chroot and retry.
1484 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1485 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001486 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu28980b22019-07-31 19:51:45 +08001487
1488 # For other failures, don't know how to handle. Just bail out.
1489 raise
1490
1491 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1492 'latest')
1493 assert os.path.exists(image_symlink)
1494 image_name = os.readlink(image_symlink)
1495 image_folder = os.path.join(cached_images_dir, board, image_name)
1496 assert os.path.exists(
1497 os.path.join(chromeos_root, image_folder, test_image_filename))
1498 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001499
1500
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001501class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001502 """Parsed content of autotest control file.
1503
1504 Attributes:
1505 name: test name
1506 path: control file path
1507 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1508 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1509 """
1510
1511 def __init__(self, path, variables):
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001512 assert 'NAME' in variables, 'invalid control file'
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001513 self.name = variables['NAME']
1514 self.path = path
1515 self.variables = variables
1516
1517
1518def parse_autotest_control_file(path):
1519 """Parses autotest control file.
1520
1521 This only parses simple top-level string assignments.
1522
1523 Returns:
1524 AutotestControlInfo object
1525 """
1526 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001527 with open(path) as f:
1528 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001529 for stmt in code.body:
1530 # Skip if not simple "NAME = *" assignment.
1531 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1532 isinstance(stmt.targets[0], ast.Name)):
1533 continue
1534
1535 # Only support string value.
1536 if isinstance(stmt.value, ast.Str):
1537 variables[stmt.targets[0].id] = stmt.value.s
1538
1539 return AutotestControlInfo(path, variables)
1540
1541
1542def enumerate_autotest_control_files(autotest_dir):
1543 """Enumerate autotest control files.
1544
1545 Args:
1546 autotest_dir: autotest folder
1547
1548 Returns:
1549 list of paths to control files
1550 """
1551 # Where to find control files. Relative to autotest_dir.
1552 subpaths = [
1553 'server/site_tests',
1554 'client/site_tests',
1555 'server/tests',
1556 'client/tests',
1557 ]
1558
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001559 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001560 result = []
1561 for subpath in subpaths:
1562 path = os.path.join(autotest_dir, subpath)
1563 for root, dirs, files in os.walk(path):
1564
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001565 for deny in denylist:
1566 if deny in dirs:
1567 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001568
1569 for filename in files:
1570 if filename == 'control' or filename.startswith('control.'):
1571 result.append(os.path.join(root, filename))
1572
1573 return result
1574
1575
1576def get_autotest_test_info(autotest_dir, test_name):
1577 """Get metadata of given test.
1578
1579 Args:
1580 autotest_dir: autotest folder
1581 test_name: test name
1582
1583 Returns:
1584 AutotestControlInfo object. None if test not found.
1585 """
1586 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001587 try:
1588 info = parse_autotest_control_file(control_file)
1589 except SyntaxError:
1590 logger.warning('%s is not parsable, ignore', control_file)
1591 continue
1592
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001593 if info.name == test_name:
1594 return info
1595 return None
1596
1597
Kuang-che Wu9d14c162020-11-03 19:35:18 +08001598def _get_overlay_name(overlay):
1599 path = os.path.join(overlay, 'metadata', 'layout.conf')
1600 if os.path.exists(path):
1601 with open(path) as f:
1602 for line in f:
1603 m = re.search(r'repo-name\s*=\s*(\S+)', line)
1604 if m:
1605 return m.group(1)
1606
1607 path = os.path.join(overlay, 'profiles', 'repo_name')
1608 if os.path.exists(path):
1609 with open(path) as f:
1610 return f.readline().rstrip()
1611
1612 return None
1613
1614
1615def parse_chromeos_overlays(chromeos_root):
1616 # ref: chromite's lib/portage_util.py ListOverlays().
1617 overlays = {}
1618 paths = ['src/overlays', 'src/private-overlays']
1619
1620 for path in paths:
1621 path = os.path.join(chromeos_root, path, 'overlay-*')
1622 for overlay in sorted(glob.glob(path)):
1623 name = _get_overlay_name(overlay)
1624 if not name:
1625 continue
1626 # Special cases which have variant boards.
1627 if name in ['auron', 'guado', 'nyan', 'veyron']:
1628 continue
1629
1630 path = os.path.join(overlay, 'metadata', 'layout.conf')
1631 masters = []
1632 if os.path.exists(path):
1633 with open(path) as f:
1634 for line in f:
1635 m = re.search(r'masters\s*=(.*)', line)
1636 if m:
1637 masters = m.group(1).split()
1638 overlays[name] = masters
1639 return overlays
1640
1641
1642def resolve_basic_boards(overlays):
1643
1644 def normalize(name):
1645 return name.replace('-private', '')
1646
1647 def resolve(name):
1648 result = set()
1649 for parent in overlays[name]:
1650 assert parent != name, 'recursive overlays definition?'
1651 if parent not in overlays:
1652 continue
1653 for basic in resolve(parent):
1654 result.add(basic)
1655 if not result:
1656 result.add(name)
1657 return set(map(normalize, result))
1658
1659 result = {}
1660 for name in overlays:
1661 board = normalize(name)
1662 basic = resolve(name)
1663 assert len(basic) == 1
1664 basic_board = basic.pop()
1665 result[board] = basic_board
1666 return result
1667
1668
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001669def detect_branch_level(branch):
1670 """Given a branch name of manifest-internal, detect it's branch level.
1671
1672 level1: if ChromeOS version is x.0.0
1673 level2: if ChromeOS version is x.x.0
1674 level3: if ChromeOS version is x.x.x
1675 Where x is an non-zero integer.
1676
1677 Args:
1678 branch: branch name or ref name in manifest-internal
1679
1680 Returns:
1681 An integer indicates the branch level, or zero if not detectable.
1682 """
1683 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1684 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1685 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1686
1687 if re.match(level1, branch):
1688 return 1
1689 if re.match(level2, branch):
1690 return 2
1691 if re.match(level3, branch):
1692 return 3
1693 return 0
1694
1695
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001696def get_crosland_link(old, new):
1697 """Generates crosland link between two versions.
1698
1699 Args:
1700 old: ChromeOS version
1701 new: ChromeOS version
1702
1703 Returns:
1704 A crosland url.
1705 """
1706
1707 def version_to_url_parameter(ver):
1708 if is_cros_snapshot_version(ver):
1709 return snapshot_version_split(ver)[2]
1710 return version_to_short(ver)
1711
1712 old_parameter = version_to_url_parameter(old)
1713 new_parameter = version_to_url_parameter(new)
1714 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1715
1716
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001717class ChromeOSSpecManager(codechange.SpecManager):
1718 """Repo manifest related operations.
1719
1720 This class enumerates chromeos manifest files, parses them,
1721 and sync to disk state according to them.
1722 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001723
1724 def __init__(self, config):
1725 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001726 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1727 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001728 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1729 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001730 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001731 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001732 self.historical_manifest_branch_name = 'refs/heads/master'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001733 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001734 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1735 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001736
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001737 def lookup_snapshot_manifest_revisions(self, old, new):
1738 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001739
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001740 Returns:
1741 list of (timestamp, commit_id, snapshot_id):
1742 timestamp: integer unix timestamp
1743 commit_id: a string indicates commit hash
1744 snapshot_id: a string indicates snapshot id
1745 """
1746 assert is_cros_snapshot_version(old)
1747 assert is_cros_snapshot_version(new)
1748
1749 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001750 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001751 # Try to guess the commit time of a snapshot manifest, it is usually a few
1752 # minutes different between snapshot manifest commit and image.zip
1753 # generate.
1754 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001755 old_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001756 gs_path.format(board=self.config['board'], version=old)) - 86400
1757 except subprocess.CalledProcessError:
1758 old_timestamp = None
1759 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001760 new_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001761 gs_path.format(board=self.config['board'], version=new)) + 86400
1762 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1763 # we can find snapshot 5982
1764 # snapshot_id <= 5982 has different commit message format, so we need
1765 # to identify its id in different ways, see below comment for more info.
1766 new_timestamp = max(new_timestamp, 1558657989 + 1)
1767 except subprocess.CalledProcessError:
1768 new_timestamp = None
1769 result = []
1770 _, _, old_snapshot_id = snapshot_version_split(old)
1771 _, _, new_snapshot_id = snapshot_version_split(new)
1772 repo = self.manifest_internal_dir
1773 path = 'snapshot.xml'
1774 branch = 'snapshot'
1775 commits = git_util.get_history(
1776 repo,
1777 path,
1778 branch,
1779 after=old_timestamp,
1780 before=new_timestamp,
1781 with_subject=True)
1782
1783 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1784 # subject, as their subjects are all `Annealing manifest snapshot.`.
1785 # So instead we count the snapshot_id manually.
1786 count = 5982
1787 # There are two snapshot_id = 2633 in commit history, ignore the former
1788 # one.
1789 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1790 # We examine the commits in reverse order as there are some testing
1791 # commits before snapshot_id=2, this method works fine after
1792 # snapshot 2, except snapshot 2633
1793 for commit in reversed(commits):
1794 msg = commit[2]
1795 if commit[1] in ignore_list:
1796 continue
1797
1798 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1799 if match:
1800 snapshot_id = match.group(1)
1801 elif 'Annealing manifest snapshot' in msg:
1802 snapshot_id = str(count)
1803 count -= 1
1804 else:
1805 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08001806 # b/151054108: snapshot version in [29288, 29439] is broken
1807 if 29288 <= int(snapshot_id) <= 29439:
1808 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001809 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1810 result.append((commit[0], commit[1], snapshot_id))
1811 # We find commits in reversed order, now reverse it again to chronological
1812 # order.
1813 return list(reversed(result))
1814
1815 def lookup_build_timestamp(self, rev):
1816 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1817 if is_cros_full_version(rev):
1818 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001819 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001820
1821 def lookup_snapshot_build_timestamp(self, rev):
1822 assert is_cros_snapshot_version(rev)
1823 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1824
1825 def lookup_release_build_timestamp(self, rev):
1826 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001827 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001828 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1829 try:
1830 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001831 self.historical_manifest_branch_name,
1832 path)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001833 except ValueError as e:
1834 raise errors.InternalError(
1835 '%s does not have %s' %
1836 (self.historical_manifest_git_dir, path)) from e
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001837 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001838
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001839 def detect_float_spec_branch_level(self, spec):
1840 results = [
1841 detect_branch_level(branch) for branch in git_util.get_branches(
1842 self.manifest_dir, commit=spec.name)
1843 ]
1844 results = [x for x in results if x > 0]
1845 return min(results) if results else 0
1846
1847 def branch_between_float_specs(self, old_spec, new_spec):
1848 if old_spec.spec_type != codechange.SPEC_FLOAT:
1849 return False
1850 if new_spec.spec_type != codechange.SPEC_FLOAT:
1851 return False
1852
1853 level_old = self.detect_float_spec_branch_level(old_spec)
1854 level_new = self.detect_float_spec_branch_level(new_spec)
1855
1856 if not level_old or not level_new:
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001857 logger.warning('branch level detect failed, assume not branched')
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001858 return False
1859 return level_old != level_new
1860
Kuang-che Wud558a042020-06-06 02:11:00 +08001861 def _determine_float_branch(self, old, new, fixed_specs):
1862 # There is no revision tag in snapshot's xml. We know snapshot
1863 # builds are on master branch.
1864 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001865 if fixed_specs[0].revision:
1866 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001867 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
1868 else:
1869 old_branches = [master_refname]
1870
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001871 if fixed_specs[-1].revision:
1872 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001873 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001874 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08001875 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001876
Kuang-che Wud558a042020-06-06 02:11:00 +08001877 common_branches = list(set(old_branches) & set(new_branches))
1878 assert common_branches, '%s and %s are not on common branches?' % (old, new)
1879
1880 if len(common_branches) == 1:
1881 return common_branches[0]
1882
1883 # There are more than one common branches, use heuristic to tie breaking.
1884 # The heuristic is simple: choice the branch with "smallest" number.
1885 # "Smaller" means the more major branch (not branched) or branched later.
1886 #
1887 # Following is the commit graph of manifest-internal repo. It shows many
1888 # interesting cases.
1889 #
1890 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
1891 # --A--+---X--------------X------B-------X-----------> master
1892 # \
1893 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
1894 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
1895 # \ \
1896 # \ E------------> stabilize-13020.67.B
1897 # \ 83/13020.55.1
1898 # F-----X--------------------> stabilize-13020.55.B
1899 #
1900 # How to read this graph:
1901 # - Time goes from left to right. Branch names are on the right side of
1902 # arrows.
1903 # - Letters A-F are manifest commits.
1904 # - Marker X means release image build at that time, the version numbers
1905 # are labeled above the X marker.
1906 # For example,
1907 # 1) 13021.0.0 release is based on manifest A, which is on all branches
1908 # shown on the graph.
1909 # We know 13021.0.0 is on master (and R84 branch later, not shown in
1910 # this graph), not on 13020* branches.
1911 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
1912 # (R83-13020.B, 13020.67.B, and 13020.55.B).
1913 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
1914 # 13020.55.B.
1915 #
1916 # There is an important property here. Every time a new branch is created,
1917 # there will always be a commit (like C, E, and F) to fix "revision" field
1918 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
1919 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
1920 # xxxxx.B branch.
1921 #
1922 # With such property, among the branches containing the given manifest
1923 # file, the branch with "smallest" number guarantees where the release is.
1924
1925 def branch_key(s):
1926 if s == master_refname:
1927 return 0, 0, 0
1928 m = re.search(r'-(\d+)\.B$', s)
1929 if m:
1930 return int(m.group(1)), 0, 0
1931 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
1932 if m:
1933 return int(m.group(1)), int(m.group(2)), 0
1934 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
1935 if m:
1936 return int(m.group(1)), int(m.group(2)), int(m.group(3))
1937
1938 logger.warning('unexpected branch name: %s', s)
1939 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
1940
1941 common_branches.sort(key=branch_key)
1942 return common_branches[0]
1943
1944 def collect_float_spec(self, old, new, fixed_specs=None):
1945 assert fixed_specs
1946 branch = self._determine_float_branch(old, new, fixed_specs)
1947 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001948
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001949 old_timestamp = self.lookup_build_timestamp(old)
1950 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001951 # snapshot time is different from commit time
1952 # usually it's a few minutes different
1953 # 30 minutes should be safe in most cases
1954 if is_cros_snapshot_version(old):
1955 old_timestamp = old_timestamp - 1800
1956 if is_cros_snapshot_version(new):
1957 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001958
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001959 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001960 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001961 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001962 parser = repo_util.ManifestParser(self.manifest_dir)
1963 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001964 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001965 result.append(
1966 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1967 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001968
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001969 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001970 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1971 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1972
1973 # case 1: if both are snapshot, return a list of snapshot
1974 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
1975 return self.collect_snapshot_specs(old, new)
1976
1977 # case 2: if both are release version
1978 # return a list of release version
1979 if is_cros_full_version(old) and is_cros_full_version(new):
1980 return self.collect_release_specs(old, new)
1981
1982 # case 3: return a list of release version and append a snapshot
1983 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001984 result = self.collect_release_specs(
1985 version_to_full(self.config['board'], old),
1986 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001987 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001988 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001989 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08001990 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001991 return result
1992
1993 def collect_snapshot_specs(self, old, new):
1994 assert is_cros_snapshot_version(old)
1995 assert is_cros_snapshot_version(new)
1996
1997 def guess_snapshot_version(board, snapshot_id, old, new):
1998 if old.endswith('-' + snapshot_id):
1999 return old
2000 if new.endswith('-' + snapshot_id):
2001 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08002002 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002003 'R*-{snapshot_id}-*'.format(
2004 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002005 for line in gsutil_ls(gs_path, ignore_errors=True):
2006 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
2007 if m:
2008 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002009 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002010
2011 result = []
2012 path = 'snapshot.xml'
2013 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002014 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002015 snapshot_version = guess_snapshot_version(self.config['board'],
2016 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002017 if snapshot_version:
2018 result.append(
2019 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
2020 path))
2021 else:
2022 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002023 return result
2024
2025 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002026 assert is_cros_full_version(old)
2027 assert is_cros_full_version(new)
2028 old_milestone, old_short_version = version_split(old)
2029 new_milestone, new_short_version = version_split(new)
2030
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002031 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002032 for milestone in git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002033 self.historical_manifest_git_dir, self.historical_manifest_branch_name,
2034 'buildspecs'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002035 if not milestone.isdigit():
2036 continue
2037 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
2038 continue
2039
Kuang-che Wu74768d32018-09-07 12:03:24 +08002040 files = git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002041 self.historical_manifest_git_dir,
2042 self.historical_manifest_branch_name,
Kuang-che Wu74768d32018-09-07 12:03:24 +08002043 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002044
2045 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002046 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002047 short_version, ext = os.path.splitext(fn)
2048 if ext != '.xml':
2049 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002050 if (util.is_version_lesseq(old_short_version, short_version) and
2051 util.is_version_lesseq(short_version, new_short_version) and
2052 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002053 rev = make_cros_full_version(milestone, short_version)
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002054 timestamp = git_util.get_commit_time(
2055 self.historical_manifest_git_dir,
2056 self.historical_manifest_branch_name, path)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002057 result.append(
2058 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002059
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002060 def version_key_func(spec):
2061 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002062 return util.version_key_func(short_version)
2063
2064 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002065 assert result[0].name == old
2066 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002067 return result
2068
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002069 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002070 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2071 if is_cros_full_version(rev):
2072 milestone, short_version = version_split(rev)
2073 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2074 manifest = git_util.get_file_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002075 self.historical_manifest_git_dir,
2076 self.historical_manifest_branch_name, path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002077 else:
2078 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2079 commit_id = revisions[0][1]
2080 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2081 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002082 return manifest
2083
2084 def get_manifest_file(self, rev):
2085 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002086 manifest_name = 'manifest_%s.xml' % rev
2087 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2088 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002089 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002090
2091 # workaround for b/150572399
2092 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2093 # folder .repo instead of.repo/manifests
2094 if is_cros_version_lesseq(rev, '12931.0.0'):
2095 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2096 manifest_patch_path = os.path.join(repo_path, manifest_name)
2097 with open(manifest_patch_path, 'w') as f:
2098 f.write(self.get_manifest(rev))
2099
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002100 return manifest_name
2101
2102 def parse_spec(self, spec):
2103 parser = repo_util.ManifestParser(self.manifest_dir)
2104 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002105 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002106 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002107 with open(manifest_path) as f:
2108 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002109 root = parser.parse_single_xml(content, allow_include=False)
2110 else:
2111 root = parser.parse_xml_recursive(spec.name, spec.path)
2112
2113 spec.entries = parser.process_parsed_result(root)
2114 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002115 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002116 raise ValueError('fixed spec %r has unexpected floating entries' %
2117 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002118 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002119
2120 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002121 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002122
2123 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2124 # manifest. 'repo sync -m' is not enough
2125 repo_util.init(
2126 self.config['chromeos_root'],
2127 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2128 manifest_name=manifest_name,
2129 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002130 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002131 # b/150753074: moblab is in non-default group and causes mark_as_stable
2132 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002133 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002134 )
2135
2136 # Note, don't sync with current_branch=True for chromeos. One of its
2137 # build steps (inside mark_as_stable) executes "git describe" which
2138 # needs git tag information.
2139 repo_util.sync(self.config['chromeos_root'])