blob: 4e6e87d9f60fa77a6abc70fdaa25cfca875d4004 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""ChromeOS utility.
6
7Terminology used in this module.
8 short_version: ChromeOS version number without milestone, like "9876.0.0".
9 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080010 snapshot_version: ChromeOS version number with milestone and snapshot id,
11 like "R62-9876.0.0-12345".
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080012 version: if not specified, it could be in short or full format.
13"""
14
15from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080016import ast
Kuang-che Wu72b5a572019-10-29 20:37:57 +080017import calendar
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080018import datetime
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080019import errno
Kuang-che Wu9d14c162020-11-03 19:35:18 +080020import glob
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080021import json
22import logging
23import os
24import re
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +080025import shutil
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080026import subprocess
Kuang-che Wud558a042020-06-06 02:11:00 +080027import sys
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080028import time
29
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080030from google.protobuf import json_format
31
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080032from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080033from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080034from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080035from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080036from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080037from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080038from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080039from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080040from bisect_kit import util
41
42logger = logging.getLogger(__name__)
43
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080045re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080046re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080047
48gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
49gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080050 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080051
52# Assume gsutil is in PATH.
53gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080054
55# Since snapshots with version >= 12618.0.0 have android and chrome version
56# info.
57snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080058
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +080059# http://crbug.com/1170601, small snapshot ids should be ignored
60# 21000 is R80-12617.0.0
61snapshot_cutover_id = 21000
62
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080063# current earliest buildbucket buildable versions
64# picked from https://crrev.com/c/2072618
65buildbucket_cutover_versions = [
66 '12931.0.0',
67 '12871.26.0', # R81
68 '12871.24.2', # stabilize-12871.24.B
69 '12812.10.0', # factory-excelsior-12812.B
70 '12768.14.0', # firmware-servo-12768.B
71 '12739.85.0', # R80
72 '12739.67.1', # stabilize-excelsior-12739.67.B
73 '12692.36.0', # factory-hatch-12692.B
74 '12672.104.0', # firmware-hatch-12672.B
75 '12607.110.0', # R79
76 '12607.83.2', # stabilize-quickfix-12607.83.B
77 '12587.59.0', # factory-kukui-12587.B
78 '12573.78.0', # firmware-kukui-12573.B
79 '12499.96.0', # R78
80 '12422.33.0', # firmware-mistral-12422.B
81 '12371.190.0', # R77
82 '12361.38.0', # factory-mistral-12361.B
83 '12200.65.0', # firmware-sarien-12200.B
84 '12105.128.0', # R75
85 '12033.82.0', # factory-sarien-12033.B
86]
87
Kuang-che Wub9705bd2018-06-28 17:59:18 +080088chromeos_root_inside_chroot = '/mnt/host/source'
89# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080090prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080091prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080092# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
93cached_images_dir = 'src/build/images'
94test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080095
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080096VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
97VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
98VERSION_KEY_MILESTONE = 'milestone'
99VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +0800100VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800101VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +0800102CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800103
104
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800105class NeedRecreateChrootException(Exception):
106 """Failed to build ChromeOS because of chroot mismatch or corruption"""
107
108
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800109def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800110 """Determines if `s` is chromeos short version.
111
112 This function doesn't accept version number of local build.
113 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800114 return bool(re.match(re_chromeos_short_version, s))
115
116
117def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800118 """Determines if `s` is chromeos full version.
119
120 This function doesn't accept version number of local build.
121 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800122 return bool(re.match(re_chromeos_full_version, s))
123
124
125def is_cros_version(s):
126 """Determines if `s` is chromeos version (either short or full)"""
127 return is_cros_short_version(s) or is_cros_full_version(s)
128
129
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800130def is_cros_snapshot_version(s):
131 """Determines if `s` is chromeos snapshot version"""
132 return bool(re.match(re_chromeos_snapshot_version, s))
133
134
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800135def is_cros_version_lesseq(ver1, ver2):
136 """Determines if ver1 is less or equal to ver2.
137
138 Args:
139 ver1: a Chrome OS version in short, full, or snapshot format.
140 ver2: a Chrome OS version in short, full, or snapshot format.
141
142 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800143 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800144 """
145 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
146 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
147
Kuang-che Wu430c5282021-01-27 21:10:25 +0800148 # Compare milestone if available.
149 m1 = re.match(r'R(\d+)', ver1)
150 m2 = re.match(r'R(\d+)', ver2)
151 if m1 and m2 and int(m1.group(1)) > int(m2.group(1)):
152 return False
153
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800154 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
155 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
156 return ver1 <= ver2
157
158
Kuang-che Wu430c5282021-01-27 21:10:25 +0800159def is_ancestor_version(ver1, ver2):
160 """Determines `ver1` version is ancestor of `ver2` version.
161
162 Returns:
163 True only if `ver1` is the ancestor of `ver2`. One version is not considered
164 as ancestor of itself.
165 """
166 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
167 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
168
169 if is_cros_version_lesseq(ver2, ver1): # pylint: disable=arguments-out-of-order
170 return False
171 if not is_cros_version_lesseq(ver1, ver2):
172 return False
173
174 if not util.is_direct_relative_version(
175 version_to_short(ver1), version_to_short(ver2)):
176 return False
177
178 # Compare snapshot id if available.
179 if is_cros_snapshot_version(ver1) and is_cros_snapshot_version(ver2):
180 _, short_1, snapshot_1 = snapshot_version_split(ver1)
181 _, short_2, snapshot_2 = snapshot_version_split(ver2)
182 if short_1 == short_2 and snapshot_1 >= snapshot_2:
183 return False
184
185 return True
186
187
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800188def is_buildbucket_buildable(version):
189 """Determines if a version is buildable on buildbucket."""
190 short_version = version_to_short(version)
191 # If given version is child of any cutover, then it's buildable
192 return any([
193 util.is_direct_relative_version(x, short_version) and
194 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
195 ])
196
197
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800198def make_cros_full_version(milestone, short_version):
199 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800200 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800201 return 'R%s-%s' % (milestone, short_version)
202
203
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800204def make_cros_snapshot_version(milestone, short_version, snapshot_id):
205 """Makes snapshot version from milestone, short_version and snapshot id"""
206 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
207
208
209def version_split(version):
210 """Splits full_version or snapshot_version into milestone and short_version"""
211 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
212 if is_cros_snapshot_version(version):
213 return snapshot_version_split(version)[0:2]
214 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800215 return milestone[1:], short_version
216
217
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800218def snapshot_version_split(snapshot_version):
219 """Splits snapshot_version into milestone, short_version and snapshot_id"""
220 assert is_cros_snapshot_version(snapshot_version)
Kuang-che Wu430c5282021-01-27 21:10:25 +0800221 milestone, short_version, snapshot_id = snapshot_version.split('-')
222 return milestone[1:], short_version, snapshot_id
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800223
224
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800225def query_snapshot_buildbucket_id(board, snapshot_version):
226 """Query buildbucket id of a snapshot"""
227 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800228 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800229 '/{snapshot_version}-*/image.zip')
230 output = gsutil_ls(
231 '-d',
232 path.format(board=board, snapshot_version=snapshot_version),
233 ignore_errors=True)
234 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800235 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800236 if m:
237 return m.group(1)
238 return None
239
240
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800241def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800242 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800243 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800244 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 +0800245 return s
246
247
248def query_dut_lsb_release(host):
249 """Query /etc/lsb-release of given DUT
250
251 Args:
252 host: the DUT address
253
254 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800255 dict for keys and values of /etc/lsb-release.
256
257 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800258 errors.SshConnectionError: cannot connect to host
259 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800260 """
261 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800262 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800263 except subprocess.CalledProcessError as e:
264 raise errors.ExternalError(
265 'unable to read /etc/lsb-release; not a DUT') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800266 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
267
268
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800269def query_dut_os_release(host):
270 """Query /etc/os-release of given DUT
271
272 Args:
273 host: the DUT address
274
275 Returns:
276 dict for keys and values of /etc/os-release.
277
278 Raises:
279 errors.SshConnectionError: cannot connect to host
280 errors.ExternalError: lsb-release file doesn't exist
281 """
282 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800283 output = util.ssh_cmd(host, 'cat', '/etc/os-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800284 except subprocess.CalledProcessError as e:
285 raise errors.ExternalError(
286 'unable to read /etc/os-release; not a DUT') from e
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800287 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
288
289
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800290def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800291 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800292
293 Args:
294 host: the DUT address
295
296 Returns:
297 True if the host is a chromeos device.
298 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800299 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800300 return query_dut_os_release(host).get('ID') in [
301 'chromiumos',
302 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800303 ]
304 except (errors.ExternalError, errors.SshConnectionError):
305 return False
306
307
308def is_good_dut(host):
309 if not is_dut(host):
310 return False
311
312 # Sometimes python is broken after 'cros flash'.
313 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800314 util.ssh_cmd(host, 'python', '-c', '1', allow_retry=True)
Kuang-che Wu44278142019-03-04 11:33:57 +0800315 return True
316 except (subprocess.CalledProcessError, errors.SshConnectionError):
317 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800318
319
320def query_dut_board(host):
321 """Query board name of a given DUT"""
322 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
323
324
325def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800326 """Query short version of a given DUT.
327
328 This function may return version of local build, which
329 is_cros_short_version() is false.
330 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800331 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
332
333
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800334def query_dut_prebuilt_version(host):
335 """Return a snapshot version or short version of a given DUT.
336
337 Args:
338 host: dut host
339
340 Returns:
341 Snapshot version or short version.
342 """
343 lsb_release = query_dut_lsb_release(host)
344 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
Kuang-che Wu25fec6f2021-01-28 12:40:43 +0800345 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800346 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
347 builder_path)
348 if match:
349 return match.group(1)
350 return release_version
351
352
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800353def query_dut_is_by_official_builder(host):
Kuang-che Wuc092bd52021-01-05 14:25:56 +0800354 """Query if given DUT is build by official builder"""
355 build_type = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
356 '')
357 build_type = build_type.split(' - ')[0]
358 assert build_type in ('Official Build', 'Continuous Builder',
359 'Developer Build',
360 'Test Build'), 'unknown build type (%s)' % build_type
361 return build_type in ['Official Build', 'Continuous Builder']
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800362
363
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800364def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800365 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800366
367 Args:
368 host: DUT address
369 connect_timeout: connection timeout
370
371 Returns:
372 boot uuid
373 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800374 return util.ssh_cmd(
375 host,
376 'cat',
377 '/proc/sys/kernel/random/boot_id',
378 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800379
380
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800381def reboot(host, force_reboot_callback=None):
382 """Reboot a DUT and verify.
383
384 Args:
385 host: DUT address
386 force_reboot_callback: powerful reboot hook (via servo). This will be
387 invoked if normal reboot failed.
388 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800389 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800390 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800391 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800392 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800393
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800394 try:
395 util.ssh_cmd(host, 'reboot')
396 except errors.SshConnectionError:
397 # Depends on timing, ssh may return failure due to broken pipe, which is
398 # working as intended. Ignore such kind of errors.
399 pass
400
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800401 wait_reboot_done(host, boot_id)
402 except (errors.SshConnectionError, errors.ExternalError):
403 if force_reboot_callback and force_reboot_callback(host):
404 wait_reboot_done(host, boot_id)
405 return
406 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800407
Kuang-che Wu708310b2018-03-28 17:24:34 +0800408
409def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800410 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800411 # (dev screen short delay) or more (long delay).
412 time.sleep(15)
413 for _ in range(100):
414 try:
415 # During boot, DUT does not response and thus ssh may hang a while. So
416 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
417 # set tight limit because it's inside retry loop.
418 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
419 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800420 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800421 logger.debug('reboot not ready? sleep wait 1 sec')
422 time.sleep(1)
423
Kuang-che Wue121fae2018-11-09 16:18:39 +0800424 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800425
426
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800427def gs_release_boardpath(board):
428 """Normalizes board name for gs://chromeos-releases/
429
430 This follows behavior of PushImage() in chromite/scripts/pushimage.py
431 Note, only gs://chromeos-releases/ needs normalization,
432 gs://chromeos-image-archive does not.
433
434 Args:
435 board: ChromeOS board name
436
437 Returns:
438 normalized board name
439 """
440 return board.replace('_', '-')
441
442
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800443def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800444 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800445
446 Args:
447 args: command line arguments passed to gsutil
448 kwargs:
449 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
450 but the path not found.
451
452 Returns:
453 stdout of gsutil
454
455 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800456 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800457 subprocess.CalledProcessError: command failed
458 """
459 stderr_lines = []
460 try:
461 return util.check_output(
462 gsutil_bin, *args, stderr_callback=stderr_lines.append)
463 except subprocess.CalledProcessError as e:
464 stderr = ''.join(stderr_lines)
465 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800466 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800467 'gsutil failed due to permission. ' +
468 'Run "%s config" and follow its instruction. ' % gsutil_bin +
469 'Fill any string if it asks for project-id')
470 if kwargs.get('ignore_errors'):
471 return ''
472 raise
473 except OSError as e:
474 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800475 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800476 'Unable to run %s. gsutil is not installed or not in PATH?' %
477 gsutil_bin)
478 raise
479
480
481def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800482 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800483
484 Args:
485 args: arguments passed to 'gsutil ls'
486 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800487 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800488 exception, ex. path not found.
489
490 Returns:
491 list of 'gsutil ls' result. One element for one line of gsutil output.
492
493 Raises:
494 subprocess.CalledProcessError: gsutil failed, usually means path not found
495 """
496 return gsutil('ls', *args, **kwargs).splitlines()
497
498
Kuang-che Wu876a5382020-10-27 14:24:58 +0800499def gsutil_stat_creation_time(*args, **kwargs):
500 """Returns the creation time of a file or multiple files.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800501
502 Args:
503 args: arguments passed to 'gsutil stat'.
504 kwargs: extra parameters for gsutil.
505
506 Returns:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800507 A integer indicates the creation timestamp.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800508
509 Raises:
510 subprocess.CalledProcessError: gsutil failed, usually means path not found
Kuang-che Wu876a5382020-10-27 14:24:58 +0800511 errors.ExternalError: creation time is not found
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800512 """
513 result = -1
514 # Currently we believe stat always returns a UTC time, and strptime also
515 # parses a UTC time by default.
516 time_format = '%a, %d %b %Y %H:%M:%S GMT'
517
518 for line in gsutil('stat', *args, **kwargs).splitlines():
519 if ':' not in line:
520 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800521 key, value = line.split(':', 1)
522 key, value = key.strip(), value.strip()
Kuang-che Wu876a5382020-10-27 14:24:58 +0800523 if key != 'Creation time':
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800524 continue
525 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800526 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800527 result = max(result, unixtime)
528
529 if result == -1:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800530 raise errors.ExternalError("didn't find creation time")
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800531 return result
532
533
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800534def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800535 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800536
537 Args:
538 board: ChromeOS board name
539 short_version: ChromeOS version number in short format, ex. 9300.0.0
540
541 Returns:
542 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
543 None if failed.
544 """
545 path = gs_archive_path.format(board=board) + '/R*-' + short_version
546 for line in gsutil_ls('-d', path, ignore_errors=True):
547 m = re.search(r'/R(\d+)-', line)
548 if not m:
549 continue
550 return m.group(1)
551
552 for channel in ['canary', 'dev', 'beta', 'stable']:
553 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800554 channel=channel,
555 boardpath=gs_release_boardpath(board),
556 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800557 for line in gsutil_ls(path, ignore_errors=True):
558 m = re.search(r'\bR(\d+)-' + short_version, line)
559 if not m:
560 continue
561 return m.group(1)
562
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800563 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800564 return None
565
566
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800567def list_board_names(chromeos_root):
568 """List board names.
569
570 Args:
571 chromeos_root: chromeos tree root
572
573 Returns:
574 list of board names
575 """
576 # Following logic is simplified from chromite/lib/portage_util.py
577 cros_list_overlays = os.path.join(chromeos_root,
578 'chromite/bin/cros_list_overlays')
579 overlays = util.check_output(cros_list_overlays).splitlines()
580 result = set()
581 for overlay in overlays:
582 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
583 name = None
584 if os.path.exists(conf_file):
585 for line in open(conf_file):
586 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
587 if m:
588 name = m.group(1)
589 break
590
591 if not name:
592 name_file = os.path.join(overlay, 'profiles', 'repo_name')
593 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800594 with open(name_file) as f:
595 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800596
597 if name:
598 name = re.sub(r'-private$', '', name)
599 result.add(name)
600
601 return list(result)
602
603
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800604def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800605 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800606
607 Args:
608 board: ChromeOS board name
609 version: ChromeOS version number in short or full format
610
611 Returns:
612 (milestone, version in short format)
613 """
614 if is_cros_short_version(version):
615 milestone = query_milestone_by_version(board, version)
616 short_version = version
617 else:
618 milestone, short_version = version_split(version)
619 return milestone, short_version
620
621
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800622def extract_major_version(version):
623 """Converts a version to its major version.
624
625 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800626 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800627
628 Returns:
629 major version number in string format
630 """
631 version = version_to_short(version)
632 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
633 return m.group(1)
634
635
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800636def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800637 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800638
639 Args:
640 version: ChromeOS version number in short or full format
641
642 Returns:
643 version number in short format
644 """
645 if is_cros_short_version(version):
646 return version
647 _, short_version = version_split(version)
648 return short_version
649
650
651def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800652 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800653
654 Args:
655 board: ChromeOS board name
656 version: ChromeOS version number in short or full format
657
658 Returns:
659 version number in full format
660 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800661 if is_cros_snapshot_version(version):
662 milestone, short_version, _ = snapshot_version_split(version)
663 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800664 if is_cros_full_version(version):
665 return version
666 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800667 if not milestone:
668 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
669 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800670 return make_cros_full_version(milestone, version)
671
672
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800673def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800674 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800675
676 Args:
677 board: ChromeOS board
678 major_version: ChromeOS major version
679
680 Returns:
681 list of (version, gs_path):
682 version: Chrome OS snapshot version
683 gs_path: gs path of test image
684 """
685
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800686 def extract_snapshot_id(result):
687 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
688 assert m
689 return int(m.group(1))
690
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800691 short_version = '%s.0.0' % major_version
692 milestone = query_milestone_by_version(board, short_version)
693 if not milestone:
694 milestone = '*'
695
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800696 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800697 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800698 result = []
699 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800700 path.format(
701 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800702 ignore_errors=True)
703
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800704 for gs_path in sorted(output):
705 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800706 if m:
707 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800708 # we should skip if there is duplicate snapshot
709 if result and result[-1][0] == snapshot_version:
710 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800711
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800712 _, _, snapshot_id = snapshot_version_split(snapshot_version)
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +0800713
714 # crbug/1170601: ignore small snapshot ids
715 if int(snapshot_id) <= snapshot_cutover_id:
716 continue
717
718 # b/151054108: snapshot version in [29288, 29439] is broken
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800719 if 29288 <= int(snapshot_id) <= 29439:
720 continue
721
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800722 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800723
724 # sort by its snapshot_id
725 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800726 return result
727
728
Kuang-che Wu575dc442019-03-05 10:30:55 +0800729def list_prebuilt_from_image_archive(board):
730 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
731
732 gs://chromeos-image-archive contains only recent builds (in two years).
733 We prefer this function to list_prebuilt_from_chromeos_releases() because
734 - this is what "cros flash" supports directly.
735 - the paths have milestone information, so we don't need to do slow query
736 by ourselves.
737
738 Args:
739 board: ChromeOS board name
740
741 Returns:
742 list of (version, gs_path):
743 version: Chrome OS version in full format
744 gs_path: gs path of test image
745 """
746 result = []
747 for line in gsutil_ls(gs_archive_path.format(board=board)):
748 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
749 if m:
750 full_version = m.group(1)
751 test_image = 'chromiumos_test_image.tar.xz'
752 assert line.endswith('/')
753 gs_path = line + test_image
754 result.append((full_version, gs_path))
755 return result
756
757
758def list_prebuilt_from_chromeos_releases(board):
759 """Lists ChromeOS versions available from gs://chromeos-releases.
760
761 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
762 support it.
763
764 Args:
765 board: ChromeOS board name
766
767 Returns:
768 list of (version, gs_path):
769 version: Chrome OS version in short format
770 gs_path: gs path of test image (with wildcard)
771 """
772 result = []
773 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800774 gs_release_path.format(
775 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800776 ignore_errors=True):
777 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
778 if m:
779 short_version = m.group(1)
780 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
781 short_version=short_version, board=board)
782 gs_path = line + test_image
783 result.append((short_version, gs_path))
784 return result
785
786
Kuang-che Wue1808402020-01-06 20:27:45 +0800787def has_test_image(board, version):
788 if is_cros_snapshot_version(version):
789 return bool(query_snapshot_buildbucket_id(board, version))
790
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800791 try:
792 full_version = version_to_full(board, version)
793 except errors.ExternalError:
794 # version_to_full() is implemented by checking image, thus its failure
795 # means no image.
796 return False
Kuang-che Wue1808402020-01-06 20:27:45 +0800797 short_version = version_to_short(version)
798 paths = [
799 gs_archive_path.format(board=board) +
800 '/%s/chromiumos_test_image.tar.xz' % full_version,
801 gs_release_path.format(
802 channel='*',
803 boardpath=gs_release_boardpath(board),
804 short_version=short_version),
805 ]
806
807 for path in paths:
808 if gsutil_ls(path, ignore_errors=True):
809 return True
810 return False
811
812
Kuang-che Wu575dc442019-03-05 10:30:55 +0800813def list_chromeos_prebuilt_versions(board,
814 old,
815 new,
816 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800817 include_older_build=True,
818 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800819 """Lists ChromeOS version numbers with prebuilt between given range
820
821 Args:
822 board: ChromeOS board name
823 old: start version (inclusive)
824 new: end version (inclusive)
825 only_good_build: only if test image is available
826 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800827 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800828
829 Returns:
830 list of sorted version numbers (in full format) between [old, new] range
831 (inclusive).
832 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800833 old_short = version_to_short(old)
834 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800835
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800836 rev_map = {
837 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800838 for full_version, gs_path in list_prebuilt_from_image_archive(board):
839 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800840 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800841
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800842 if include_older_build and old_short not in rev_map:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800843 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
844 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800845 rev_map[short_version] = [(short_version, gs_path)]
846
847 if use_snapshot:
848 for major_version in range(
849 int(extract_major_version(old)),
850 int(extract_major_version(new)) + 1):
851 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800852 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800853 # If current version is smaller than cutover, ignore it as it might not
854 # contain enough information for continuing android and chrome bisection.
855 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
856 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800857
858 # Given the fact that snapshots are images between two release versions.
859 # Adding snapshots of 12345.0.0 should be treated as adding commits
860 # between [12345.0.0, 12346.0.0).
861 # So in the following lines we check two facts:
862 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800863 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800864 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800865 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800866 continue
867 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800868 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800869 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800870 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800871 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800872
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800873 snapshots = list_snapshots_from_image_archive(board, str(major_version))
874 if snapshots:
875 # if snapshots found, we can append them after the release version,
876 # so the prebuilt image list of this version will be
877 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800878 if short_version not in rev_map:
879 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800880 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800881
882 result = []
883 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800884 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800885 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800886 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800887 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800888 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800889 continue
890
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800891 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800892
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800893 # version_to_full() and gsutil_ls() may take long time if versions are a
894 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800895
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800896 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800897 gs_result = gsutil_ls(gs_path, ignore_errors=True)
898 if not gs_result:
899 logger.warning('%s is not a good build, ignore', version)
900 continue
901 assert len(gs_result) == 1
902 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
903 if not m:
904 logger.warning('format of image path is unexpected: %s', gs_result[0])
905 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800906 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800907 elif is_cros_short_version(version):
908 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800909
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800910 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
911 version, new):
912 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800913
914 return result
915
916
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800917def prepare_snapshot_image(chromeos_root, board, snapshot_version):
918 """Prepare chromeos snapshot image.
919
920 Args:
921 chromeos_root: chromeos tree root
922 board: ChromeOS board name
923 snapshot_version: ChromeOS snapshot version number
924
925 Returns:
926 local file path of test image relative to chromeos_root
927 """
928 assert is_cros_snapshot_version(snapshot_version)
929 milestone, short_version, snapshot_id = snapshot_version_split(
930 snapshot_version)
931 full_version = make_cros_full_version(milestone, short_version)
932 tmp_dir = os.path.join(
933 chromeos_root, 'tmp',
934 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
935 if not os.path.exists(tmp_dir):
936 os.makedirs(tmp_dir)
937
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800938 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/' +
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800939 '{snapshot_version}-*/image.zip')
940 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
941
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800942 full_path = os.path.join(tmp_dir, test_image_filename)
943 rel_path = os.path.relpath(full_path, chromeos_root)
944 if os.path.exists(full_path):
945 return rel_path
946
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800947 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800948 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800949 gs_path = files[0]
950 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800951 util.check_call(
952 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
953 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800954 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800955
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800956 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800957 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800958
959
Kuang-che Wu28980b22019-07-31 19:51:45 +0800960def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800961 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800962
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800963 It searches for xbuddy image which "cros flash" can use, or fetch image to
964 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800965
966 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800967 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800968 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800969 version: ChromeOS version number in short or full format
970
971 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800972 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800973 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800974 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800975 full_version = version_to_full(board, version)
976 short_version = version_to_short(full_version)
977
978 image_path = None
979 gs_path = gs_archive_path.format(board=board) + '/' + full_version
980 if gsutil_ls('-d', gs_path, ignore_errors=True):
981 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
982 board=board, full_version=full_version)
983 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800984 tmp_dir = os.path.join(chromeos_root, 'tmp',
985 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800986 full_path = os.path.join(tmp_dir, test_image_filename)
987 rel_path = os.path.relpath(full_path, chromeos_root)
988 if os.path.exists(full_path):
989 return rel_path
990
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800991 if not os.path.exists(tmp_dir):
992 os.makedirs(tmp_dir)
993 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800994 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800995 # to fetch the image by ourselves
996 for channel in ['canary', 'dev', 'beta', 'stable']:
997 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
998 full_version=full_version, board=board)
999 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +08001000 channel=channel,
1001 boardpath=gs_release_boardpath(board),
1002 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001003 gs_path += '/' + fn
1004 if gsutil_ls(gs_path, ignore_errors=True):
1005 # TODO(kcwu): delete tmp
1006 gsutil('cp', gs_path, tmp_dir)
1007 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001008 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001009 break
1010
1011 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001012 return image_path
1013
1014
1015def cros_flash(chromeos_root,
1016 host,
1017 board,
1018 image_path,
1019 version=None,
1020 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001021 disable_rootfs_verification=True,
1022 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001023 """Flash a DUT with given ChromeOS image.
1024
1025 This is implemented by 'cros flash' command line.
1026
1027 Args:
1028 chromeos_root: use 'cros flash' of which chromeos tree
1029 host: DUT address
1030 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +08001031 image_path: chromeos image xbuddy path or file path. For relative
1032 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001033 version: ChromeOS version in short or full format
1034 clobber_stateful: Clobber stateful partition when performing update
1035 disable_rootfs_verification: Disable rootfs verification after update
1036 is completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001037 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001038
1039 Raises:
1040 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001041 """
1042 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
1043
1044 # Reboot is necessary because sometimes previous 'cros flash' failed and
1045 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001046 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001047
Kuang-che Wu020a1182020-09-08 17:17:22 +08001048 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001049 # The service is used in jetstream boards, but not other CrOS devices.
1050 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
1051 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001052 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
1053 # For simplicity, we wait 60 seconds here, which is the timeout value of
1054 # jetstream_host.
1055 # https://chromium.googlesource.com/chromiumos/third_party/autotest
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001056 # /+/HEAD/server/hosts/jetstream_host.py#27
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001057 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001058 util.ssh_cmd(host, 'stop', 'ap-update-manager')
1059 except subprocess.CalledProcessError:
1060 pass # not started; do nothing
1061
Kuang-che Wu28980b22019-07-31 19:51:45 +08001062 # Handle relative path.
1063 if '://' not in image_path and not os.path.isabs(image_path):
1064 assert os.path.exists(os.path.join(chromeos_root, image_path))
1065 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
1066
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001067 args = [
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001068 '--debug',
1069 '--no-ping',
1070 # Speed up for slow network connection.
1071 '--send-payload-in-parallel',
1072 host,
1073 image_path,
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001074 ]
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001075 # TODO(kcwu): remove this check if we don't need to support chromeos versions
1076 # earlier than Dec 2020.
1077 if git_util.is_ancestor_commit(
1078 os.path.join(chromeos_root, 'chromite'), '9ed30bc3ed292b', 'HEAD'):
1079 # To reduce disk usage on DUT.
1080 args.append('--no-copy-payloads-to-device')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001081 if clobber_stateful:
1082 args.append('--clobber-stateful')
1083 if disable_rootfs_verification:
1084 args.append('--disable-rootfs-verification')
1085
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001086 try:
1087 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001088 except subprocess.CalledProcessError as e:
1089 raise errors.ExternalError('cros flash failed') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001090
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001091 if version:
1092 # In the past, cros flash may fail with returncode=0
1093 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001094 if is_cros_snapshot_version(version):
1095 builder_path = query_dut_lsb_release(host).get(
1096 'CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001097 expect_prefix = '%s-snapshot/%s-' % (board, version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001098 if not builder_path.startswith(expect_prefix):
1099 raise errors.ExternalError(
1100 'although cros flash succeeded, the OS builder path is '
1101 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1102 else:
1103 expect_version = version_to_short(version)
1104 dut_version = query_dut_short_version(host)
1105 if dut_version != expect_version:
1106 raise errors.ExternalError(
1107 'although cros flash succeeded, the OS version is unexpected: '
1108 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001109
Kuang-che Wu4a81ea72019-10-05 15:35:17 +08001110 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001111 # (b/130786578), so it's necessary to do sanity check.
1112 if not is_good_dut(host):
1113 raise errors.ExternalError(
1114 'although cros flash succeeded, the DUT is in bad state')
1115
1116
1117def cros_flash_with_retry(chromeos_root,
1118 host,
1119 board,
1120 image_path,
1121 version=None,
1122 clobber_stateful=False,
1123 disable_rootfs_verification=True,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001124 repair_callback=None,
1125 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001126 # 'cros flash' is not 100% reliable, retry if necessary.
1127 for attempt in range(2):
1128 if attempt > 0:
1129 logger.info('will retry 60 seconds later')
1130 time.sleep(60)
1131
1132 try:
1133 cros_flash(
1134 chromeos_root,
1135 host,
1136 board,
1137 image_path,
1138 version=version,
1139 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001140 disable_rootfs_verification=disable_rootfs_verification,
1141 force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001142 return True
1143 except errors.ExternalError:
1144 logger.exception('cros flash failed')
1145 if repair_callback and not repair_callback(host):
1146 logger.warning('not repaired, assume it is harmless')
1147 continue
1148 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001149
1150
1151def version_info(board, version):
1152 """Query subcomponents version info of given version of ChromeOS
1153
1154 Args:
1155 board: ChromeOS board name
1156 version: ChromeOS version number in short or full format
1157
1158 Returns:
1159 dict of component and version info, including (if available):
1160 cros_short_version: ChromeOS version
1161 cros_full_version: ChromeOS version
1162 milestone: milestone of ChromeOS
1163 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001164 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001165 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1166 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001167 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001168 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001169 milestone, short_version, _ = snapshot_version_split(version)
1170 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001171 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001172 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001173 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001174 VERSION_KEY_MILESTONE: milestone,
1175 VERSION_KEY_CROS_FULL_VERSION: version,
1176 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001177 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1178 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1179 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001180 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001181 info = {}
1182 full_version = version_to_full(board, version)
1183
1184 # Some boards may have only partial-metadata.json but no metadata.json.
1185 # e.g. caroline R60-9462.0.0
1186 # Let's try both.
1187 metadata = None
1188 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001189 path = gs_archive_path.format(
1190 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001191 metadata = gsutil('cat', path, ignore_errors=True)
1192 if metadata:
1193 o = json.loads(metadata)
1194 v = o['version']
1195 board_metadata = o['board-metadata'][board]
1196 info.update({
1197 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1198 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1199 VERSION_KEY_MILESTONE: v['milestone'],
1200 VERSION_KEY_CR_VERSION: v['chrome'],
1201 })
1202
1203 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001204 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001205 if 'android-branch' in v: # this appears since R58-9317.0.0
1206 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1207 elif 'android-container-branch' in board_metadata:
1208 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1209 break
1210 else:
1211 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1212 logger.error(
1213 'Note, so far no quick way to look up version info for too old builds')
1214
1215 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001216
1217
1218def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001219 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001220
1221 Args:
1222 board: ChromeOS board name
1223 version: ChromeOS version number in short or full format
1224
1225 Returns:
1226 Chrome version number
1227 """
1228 info = version_info(board, version)
1229 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001230
1231
1232def query_android_build_id(board, rev):
1233 info = version_info(board, rev)
1234 rev = info['android_build_id']
1235 return rev
1236
1237
1238def query_android_branch(board, rev):
1239 info = version_info(board, rev)
1240 rev = info['android_branch']
1241 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001242
1243
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001244def guess_chrome_version(board, rev):
1245 """Guess chrome version number.
1246
1247 Args:
1248 board: chromeos board name
1249 rev: chrome or chromeos version
1250
1251 Returns:
1252 chrome version number
1253 """
1254 if is_cros_version(rev):
1255 assert board, 'need to specify BOARD for cros version'
1256 rev = query_chrome_version(board, rev)
1257 assert cr_util.is_chrome_version(rev)
1258
1259 return rev
1260
1261
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001262def is_inside_chroot():
1263 """Returns True if we are inside chroot."""
1264 return os.path.exists('/etc/cros_chroot_version')
1265
1266
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001267def convert_path_outside_chroot(chromeos_root, path):
1268 """Converts path in chroot to outside.
1269
1270 Args:
1271 chromeos_root: chromeos tree root
1272 path: path inside chroot; support starting with '~/'
1273
1274 Returns:
1275 The corresponding path outside chroot assuming the chroot is mounted
1276 """
1277 if path.startswith('~/'):
1278 path = path.replace('~', '/home/' + os.environ['USER'])
1279 assert '~' not in path, 'tilde (~) character is not fully supported'
1280
1281 assert os.path.isabs(path)
1282 assert path[0] == os.sep
1283 return os.path.join(chromeos_root, 'chroot', path[1:])
1284
1285
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001286def cros_sdk(chromeos_root,
1287 *args,
1288 chrome_root=None,
1289 env=None,
1290 log_stdout=True,
1291 stdin=None,
1292 stderr_callback=None,
1293 goma_dir=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001294 """Run commands inside chromeos chroot.
1295
1296 Args:
1297 chromeos_root: chromeos tree root
1298 *args: command to run
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001299 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
1300 env: (dict) environment variables for the command
1301 log_stdout: Whether write the stdout output of the child process to log.
1302 stdin: standard input file handle for the command
1303 stderr_callback: Callback function for stderr. Called once per line.
1304 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001305 """
1306 envs = []
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001307 if env:
1308 for k, v in env.items():
1309 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1310 envs.append('%s=%s' % (k, v))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001311
1312 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1313 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001314 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001315
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001316 if chrome_root:
1317 prefix += ['--chrome_root', chrome_root]
1318 if goma_dir:
1319 prefix += ['--goma_dir', goma_dir]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001320
Kuang-che Wu399d4662019-06-06 15:23:37 +08001321 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001322
Kuang-che Wu399d4662019-06-06 15:23:37 +08001323 # In addition to the output of command we are interested, cros_sdk may
1324 # generate its own messages. For example, chroot creation messages if we run
1325 # cros_sdk the first time.
1326 # This is the hack to run dummy command once, so we can get clean output for
1327 # the command we are interested.
1328 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001329 try:
1330 util.check_call(*cmd, cwd=chromeos_root)
1331 except subprocess.CalledProcessError:
1332 logger.exception('cros_sdk init/update failed')
1333 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001334
1335 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001336 return util.check_output(
1337 *cmd,
1338 cwd=chromeos_root,
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001339 log_stdout=log_stdout,
1340 stdin=stdin,
1341 stderr_callback=stderr_callback)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001342
1343
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001344def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001345 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001346
1347 Args:
1348 chromeos_root: chromeos tree root
1349 """
1350 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1351 return
1352 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1353 return
1354
1355 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1356
1357
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001358def mount_chroot(chromeos_root):
1359 """Creates ChromeOS chroot if necessary.
1360
1361 Args:
1362 chromeos_root: chromeos tree root
1363 """
1364 # An arbitrary file must exist in chroot.
1365 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1366
1367 # Not created or mounted yet.
1368 if not os.path.exists(path):
1369 create_chroot(chromeos_root)
1370 # After this command, the chroot is mounted.
1371 cros_sdk(chromeos_root, 'true')
1372 assert os.path.exists(path)
1373
1374
1375def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001376 """Copies file into chromeos chroot.
1377
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001378 The side effect is chroot created and mounted.
1379
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001380 Args:
1381 chromeos_root: chromeos tree root
1382 src: path outside chroot
1383 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001384 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001385 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001386 mount_chroot(chromeos_root)
1387 src = os.path.expanduser(src)
1388 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1389 if not overwrite and os.path.exists(dst_outside):
1390 return
1391
1392 # Haven't support directory or special files yet.
1393 assert os.path.isfile(src)
1394 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1395
1396 dirname = os.path.dirname(dst_outside)
1397 if not os.path.exists(dirname):
1398 os.makedirs(dirname)
1399 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001400
1401
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001402def prepare_chroot(chromeos_root):
1403 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001404
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001405 # Work around b/149077936:
1406 # The creds file is copied into the chroot since 12866.0.0.
1407 # But earlier versions need this file as well because of cipd ACL change.
1408 creds_path = '~/.config/chrome_infra/auth/creds.json'
1409 if os.path.exists(os.path.expanduser(creds_path)):
1410 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001411
1412
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001413def check_if_need_recreate_chroot(stdout, stderr):
1414 """Analyze build log and determine if chroot should be recreated.
1415
1416 Args:
1417 stdout: stdout output of build
1418 stderr: stderr output of build
1419
1420 Returns:
1421 the reason if chroot needs recreated; None otherwise
1422 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001423 if re.search(
1424 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001425 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001426 return 'EAPI version mismatch'
1427
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001428 if 'Chroot is too new. Consider running:' in stderr:
1429 return 'chroot version is too new'
1430
1431 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001432 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1433 return 'chroot version is too new'
1434
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001435 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1436 if "undefined reference to 'std::__1::basic_string" in stdout:
1437 return 'might be due to compiler change'
1438
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001439 # Detect failures due to file collisions.
1440 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1441 # and conflict with each other. Other possible cases are package renaming or
1442 # refactoring. Let's recreate chroot to work around them.
1443 if 'Detected file collision' in stdout:
1444 # Using wildcard between words because the text wraps to the next line
1445 # depending on length of package name and each line is prefixed with
1446 # package name.
1447 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1448 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1449 # package name (65 now).
1450 m = re.search(
1451 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1452 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1453 if m:
1454 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001455
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001456 return None
1457
1458
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001459def build_packages(chromeos_root,
1460 board,
1461 chrome_root=None,
1462 goma_dir=None,
1463 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001464 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001465
1466 Args:
1467 chromeos_root: chromeos tree root
1468 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001469 chrome_root: Chrome tree root. If specified, build chrome using the
1470 provided tree
1471 goma_dir: Goma installed directory to mount into the chroot. If specified,
1472 build chrome with goma.
1473 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001474 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001475
1476 def has_build_package_argument(argument):
1477 stderr_lines = []
1478 try:
1479 util.check_call(
1480 'src/scripts/build_packages',
1481 '--help',
1482 cwd=chromeos_root,
1483 stderr_callback=stderr_lines.append)
1484 except subprocess.CalledProcessError:
1485 help_output = ''.join(stderr_lines)
1486 return '--[no]%s' % argument in help_output
1487
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001488 common_env = {
1489 'USE': '-cros-debug chrome_internal',
1490 'FEATURES': 'separatedebug',
1491 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001492 stderr_lines = []
1493 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001494 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001495 env = common_env.copy()
1496 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001497 cros_sdk(
1498 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001499 './update_chroot',
1500 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001501 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001502 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001503 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001504
1505 env = common_env.copy()
1506 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001507 './build_packages',
1508 '--board',
1509 board,
1510 '--withdev',
1511 '--noworkon',
1512 '--skip_chroot_upgrade',
1513 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001514 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001515
1516 # `use_any_chrome` flag is default on and will force to use a chrome
1517 # prebuilt even if the version doesn't match.
1518
1519 # As this argument is landed in 12681, we should check if the argument
1520 # exists before adding this.
1521 if has_build_package_argument('use_any_chrome'):
1522 cmd.append('--nouse_any_chrome')
1523
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001524 if goma_dir:
1525 # Tell build_packages to start and stop goma
1526 cmd.append('--run_goma')
1527 env['USE_GOMA'] = 'true'
1528 if afdo_use:
1529 env['USE'] += ' afdo_use'
1530 cros_sdk(
1531 chromeos_root,
1532 *cmd,
1533 env=env,
1534 chrome_root=chrome_root,
1535 stderr_callback=stderr_lines.append,
1536 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001537 except subprocess.CalledProcessError as e:
1538 # Detect failures due to incompatibility between chroot and source tree. If
1539 # so, notify the caller to recreate chroot and retry.
1540 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1541 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001542 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001543
1544 # For other failures, don't know how to handle. Just bail out.
1545 raise
1546
Kuang-che Wu28980b22019-07-31 19:51:45 +08001547
1548def build_image(chromeos_root, board):
1549 """Build ChromeOS image.
1550
1551 Args:
1552 chromeos_root: chromeos tree root
1553 board: ChromeOS board name
1554
1555 Returns:
1556 image folder; relative to chromeos_root
1557 """
1558 stderr_lines = []
1559 try:
1560 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1561 cros_sdk(
1562 chromeos_root,
1563 './build_image',
1564 '--board',
1565 board,
1566 '--noenable_rootfs_verification',
1567 'test',
1568 env={
1569 'USE': '-cros-debug chrome_internal',
1570 'FEATURES': 'separatedebug',
1571 },
1572 stderr_callback=stderr_lines.append)
1573 except subprocess.CalledProcessError as e:
1574 # Detect failures due to incompatibility between chroot and source tree. If
1575 # so, notify the caller to recreate chroot and retry.
1576 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1577 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001578 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu28980b22019-07-31 19:51:45 +08001579
1580 # For other failures, don't know how to handle. Just bail out.
1581 raise
1582
1583 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1584 'latest')
1585 assert os.path.exists(image_symlink)
1586 image_name = os.readlink(image_symlink)
1587 image_folder = os.path.join(cached_images_dir, board, image_name)
1588 assert os.path.exists(
1589 os.path.join(chromeos_root, image_folder, test_image_filename))
1590 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001591
1592
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001593class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001594 """Parsed content of autotest control file.
1595
1596 Attributes:
1597 name: test name
1598 path: control file path
1599 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1600 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1601 """
1602
1603 def __init__(self, path, variables):
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001604 assert 'NAME' in variables, 'invalid control file'
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001605 self.name = variables['NAME']
1606 self.path = path
1607 self.variables = variables
1608
1609
1610def parse_autotest_control_file(path):
1611 """Parses autotest control file.
1612
1613 This only parses simple top-level string assignments.
1614
1615 Returns:
1616 AutotestControlInfo object
1617 """
1618 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001619 with open(path) as f:
1620 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001621 for stmt in code.body:
1622 # Skip if not simple "NAME = *" assignment.
1623 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1624 isinstance(stmt.targets[0], ast.Name)):
1625 continue
1626
1627 # Only support string value.
1628 if isinstance(stmt.value, ast.Str):
1629 variables[stmt.targets[0].id] = stmt.value.s
1630
1631 return AutotestControlInfo(path, variables)
1632
1633
1634def enumerate_autotest_control_files(autotest_dir):
1635 """Enumerate autotest control files.
1636
1637 Args:
1638 autotest_dir: autotest folder
1639
1640 Returns:
1641 list of paths to control files
1642 """
1643 # Where to find control files. Relative to autotest_dir.
1644 subpaths = [
1645 'server/site_tests',
1646 'client/site_tests',
1647 'server/tests',
1648 'client/tests',
1649 ]
1650
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001651 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001652 result = []
1653 for subpath in subpaths:
1654 path = os.path.join(autotest_dir, subpath)
1655 for root, dirs, files in os.walk(path):
1656
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001657 for deny in denylist:
1658 if deny in dirs:
1659 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001660
1661 for filename in files:
1662 if filename == 'control' or filename.startswith('control.'):
1663 result.append(os.path.join(root, filename))
1664
1665 return result
1666
1667
1668def get_autotest_test_info(autotest_dir, test_name):
1669 """Get metadata of given test.
1670
1671 Args:
1672 autotest_dir: autotest folder
1673 test_name: test name
1674
1675 Returns:
1676 AutotestControlInfo object. None if test not found.
1677 """
1678 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001679 try:
1680 info = parse_autotest_control_file(control_file)
1681 except SyntaxError:
1682 logger.warning('%s is not parsable, ignore', control_file)
1683 continue
1684
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001685 if info.name == test_name:
1686 return info
1687 return None
1688
1689
Kuang-che Wu9d14c162020-11-03 19:35:18 +08001690def _get_overlay_name(overlay):
1691 path = os.path.join(overlay, 'metadata', 'layout.conf')
1692 if os.path.exists(path):
1693 with open(path) as f:
1694 for line in f:
1695 m = re.search(r'repo-name\s*=\s*(\S+)', line)
1696 if m:
1697 return m.group(1)
1698
1699 path = os.path.join(overlay, 'profiles', 'repo_name')
1700 if os.path.exists(path):
1701 with open(path) as f:
1702 return f.readline().rstrip()
1703
1704 return None
1705
1706
1707def parse_chromeos_overlays(chromeos_root):
1708 # ref: chromite's lib/portage_util.py ListOverlays().
1709 overlays = {}
1710 paths = ['src/overlays', 'src/private-overlays']
1711
1712 for path in paths:
1713 path = os.path.join(chromeos_root, path, 'overlay-*')
1714 for overlay in sorted(glob.glob(path)):
1715 name = _get_overlay_name(overlay)
1716 if not name:
1717 continue
1718 # Special cases which have variant boards.
1719 if name in ['auron', 'guado', 'nyan', 'veyron']:
1720 continue
1721
1722 path = os.path.join(overlay, 'metadata', 'layout.conf')
1723 masters = []
1724 if os.path.exists(path):
1725 with open(path) as f:
1726 for line in f:
1727 m = re.search(r'masters\s*=(.*)', line)
1728 if m:
1729 masters = m.group(1).split()
1730 overlays[name] = masters
1731 return overlays
1732
1733
1734def resolve_basic_boards(overlays):
1735
1736 def normalize(name):
1737 return name.replace('-private', '')
1738
1739 def resolve(name):
1740 result = set()
1741 for parent in overlays[name]:
1742 assert parent != name, 'recursive overlays definition?'
1743 if parent not in overlays:
1744 continue
1745 for basic in resolve(parent):
1746 result.add(basic)
1747 if not result:
1748 result.add(name)
1749 return set(map(normalize, result))
1750
1751 result = {}
1752 for name in overlays:
1753 board = normalize(name)
1754 basic = resolve(name)
1755 assert len(basic) == 1
1756 basic_board = basic.pop()
1757 result[board] = basic_board
1758 return result
1759
1760
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001761def detect_branch_level(branch):
1762 """Given a branch name of manifest-internal, detect it's branch level.
1763
1764 level1: if ChromeOS version is x.0.0
1765 level2: if ChromeOS version is x.x.0
1766 level3: if ChromeOS version is x.x.x
1767 Where x is an non-zero integer.
1768
1769 Args:
1770 branch: branch name or ref name in manifest-internal
1771
1772 Returns:
1773 An integer indicates the branch level, or zero if not detectable.
1774 """
1775 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1776 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1777 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1778
1779 if re.match(level1, branch):
1780 return 1
1781 if re.match(level2, branch):
1782 return 2
1783 if re.match(level3, branch):
1784 return 3
1785 return 0
1786
1787
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001788def get_crosland_link(old, new):
1789 """Generates crosland link between two versions.
1790
1791 Args:
1792 old: ChromeOS version
1793 new: ChromeOS version
1794
1795 Returns:
1796 A crosland url.
1797 """
1798
1799 def version_to_url_parameter(ver):
1800 if is_cros_snapshot_version(ver):
1801 return snapshot_version_split(ver)[2]
1802 return version_to_short(ver)
1803
1804 old_parameter = version_to_url_parameter(old)
1805 new_parameter = version_to_url_parameter(new)
1806 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1807
1808
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001809class ChromeOSSpecManager(codechange.SpecManager):
1810 """Repo manifest related operations.
1811
1812 This class enumerates chromeos manifest files, parses them,
1813 and sync to disk state according to them.
1814 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001815
1816 def __init__(self, config):
1817 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001818 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1819 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001820 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1821 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001822 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001823 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001824 self.historical_manifest_branch_name = 'refs/heads/master'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001825 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001826 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1827 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001828
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001829 def lookup_snapshot_manifest_revisions(self, old, new):
1830 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001831
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001832 Returns:
1833 list of (timestamp, commit_id, snapshot_id):
1834 timestamp: integer unix timestamp
1835 commit_id: a string indicates commit hash
1836 snapshot_id: a string indicates snapshot id
1837 """
1838 assert is_cros_snapshot_version(old)
1839 assert is_cros_snapshot_version(new)
1840
1841 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001842 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001843 # Try to guess the commit time of a snapshot manifest, it is usually a few
1844 # minutes different between snapshot manifest commit and image.zip
1845 # generate.
1846 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001847 old_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001848 gs_path.format(board=self.config['board'], version=old)) - 86400
1849 except subprocess.CalledProcessError:
1850 old_timestamp = None
1851 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001852 new_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001853 gs_path.format(board=self.config['board'], version=new)) + 86400
1854 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1855 # we can find snapshot 5982
1856 # snapshot_id <= 5982 has different commit message format, so we need
1857 # to identify its id in different ways, see below comment for more info.
1858 new_timestamp = max(new_timestamp, 1558657989 + 1)
1859 except subprocess.CalledProcessError:
1860 new_timestamp = None
1861 result = []
1862 _, _, old_snapshot_id = snapshot_version_split(old)
1863 _, _, new_snapshot_id = snapshot_version_split(new)
1864 repo = self.manifest_internal_dir
1865 path = 'snapshot.xml'
1866 branch = 'snapshot'
1867 commits = git_util.get_history(
1868 repo,
1869 path,
1870 branch,
1871 after=old_timestamp,
1872 before=new_timestamp,
1873 with_subject=True)
1874
1875 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1876 # subject, as their subjects are all `Annealing manifest snapshot.`.
1877 # So instead we count the snapshot_id manually.
1878 count = 5982
1879 # There are two snapshot_id = 2633 in commit history, ignore the former
1880 # one.
1881 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1882 # We examine the commits in reverse order as there are some testing
1883 # commits before snapshot_id=2, this method works fine after
1884 # snapshot 2, except snapshot 2633
1885 for commit in reversed(commits):
1886 msg = commit[2]
1887 if commit[1] in ignore_list:
1888 continue
1889
1890 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1891 if match:
1892 snapshot_id = match.group(1)
1893 elif 'Annealing manifest snapshot' in msg:
1894 snapshot_id = str(count)
1895 count -= 1
1896 else:
1897 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08001898 # b/151054108: snapshot version in [29288, 29439] is broken
1899 if 29288 <= int(snapshot_id) <= 29439:
1900 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001901 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1902 result.append((commit[0], commit[1], snapshot_id))
1903 # We find commits in reversed order, now reverse it again to chronological
1904 # order.
1905 return list(reversed(result))
1906
1907 def lookup_build_timestamp(self, rev):
1908 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1909 if is_cros_full_version(rev):
1910 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001911 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001912
1913 def lookup_snapshot_build_timestamp(self, rev):
1914 assert is_cros_snapshot_version(rev)
1915 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1916
1917 def lookup_release_build_timestamp(self, rev):
1918 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001919 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001920 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1921 try:
1922 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001923 self.historical_manifest_branch_name,
1924 path)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001925 except ValueError as e:
1926 raise errors.InternalError(
1927 '%s does not have %s' %
1928 (self.historical_manifest_git_dir, path)) from e
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001929 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001930
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001931 def detect_float_spec_branch_level(self, spec):
1932 results = [
1933 detect_branch_level(branch) for branch in git_util.get_branches(
1934 self.manifest_dir, commit=spec.name)
1935 ]
1936 results = [x for x in results if x > 0]
1937 return min(results) if results else 0
1938
1939 def branch_between_float_specs(self, old_spec, new_spec):
1940 if old_spec.spec_type != codechange.SPEC_FLOAT:
1941 return False
1942 if new_spec.spec_type != codechange.SPEC_FLOAT:
1943 return False
1944
1945 level_old = self.detect_float_spec_branch_level(old_spec)
1946 level_new = self.detect_float_spec_branch_level(new_spec)
1947
1948 if not level_old or not level_new:
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001949 logger.warning('branch level detect failed, assume not branched')
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001950 return False
1951 return level_old != level_new
1952
Kuang-che Wud558a042020-06-06 02:11:00 +08001953 def _determine_float_branch(self, old, new, fixed_specs):
1954 # There is no revision tag in snapshot's xml. We know snapshot
1955 # builds are on master branch.
1956 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001957 if fixed_specs[0].revision:
1958 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001959 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
1960 else:
1961 old_branches = [master_refname]
1962
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001963 if fixed_specs[-1].revision:
1964 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001965 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001966 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08001967 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001968
Kuang-che Wud558a042020-06-06 02:11:00 +08001969 common_branches = list(set(old_branches) & set(new_branches))
1970 assert common_branches, '%s and %s are not on common branches?' % (old, new)
1971
1972 if len(common_branches) == 1:
1973 return common_branches[0]
1974
1975 # There are more than one common branches, use heuristic to tie breaking.
1976 # The heuristic is simple: choice the branch with "smallest" number.
1977 # "Smaller" means the more major branch (not branched) or branched later.
1978 #
1979 # Following is the commit graph of manifest-internal repo. It shows many
1980 # interesting cases.
1981 #
1982 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
1983 # --A--+---X--------------X------B-------X-----------> master
1984 # \
1985 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
1986 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
1987 # \ \
1988 # \ E------------> stabilize-13020.67.B
1989 # \ 83/13020.55.1
1990 # F-----X--------------------> stabilize-13020.55.B
1991 #
1992 # How to read this graph:
1993 # - Time goes from left to right. Branch names are on the right side of
1994 # arrows.
1995 # - Letters A-F are manifest commits.
1996 # - Marker X means release image build at that time, the version numbers
1997 # are labeled above the X marker.
1998 # For example,
1999 # 1) 13021.0.0 release is based on manifest A, which is on all branches
2000 # shown on the graph.
2001 # We know 13021.0.0 is on master (and R84 branch later, not shown in
2002 # this graph), not on 13020* branches.
2003 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
2004 # (R83-13020.B, 13020.67.B, and 13020.55.B).
2005 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
2006 # 13020.55.B.
2007 #
2008 # There is an important property here. Every time a new branch is created,
2009 # there will always be a commit (like C, E, and F) to fix "revision" field
2010 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
2011 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
2012 # xxxxx.B branch.
2013 #
2014 # With such property, among the branches containing the given manifest
2015 # file, the branch with "smallest" number guarantees where the release is.
2016
2017 def branch_key(s):
2018 if s == master_refname:
2019 return 0, 0, 0
2020 m = re.search(r'-(\d+)\.B$', s)
2021 if m:
2022 return int(m.group(1)), 0, 0
2023 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
2024 if m:
2025 return int(m.group(1)), int(m.group(2)), 0
2026 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
2027 if m:
2028 return int(m.group(1)), int(m.group(2)), int(m.group(3))
2029
2030 logger.warning('unexpected branch name: %s', s)
2031 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
2032
2033 common_branches.sort(key=branch_key)
2034 return common_branches[0]
2035
2036 def collect_float_spec(self, old, new, fixed_specs=None):
2037 assert fixed_specs
2038 branch = self._determine_float_branch(old, new, fixed_specs)
2039 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002040
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002041 old_timestamp = self.lookup_build_timestamp(old)
2042 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002043 # snapshot time is different from commit time
2044 # usually it's a few minutes different
2045 # 30 minutes should be safe in most cases
2046 if is_cros_snapshot_version(old):
2047 old_timestamp = old_timestamp - 1800
2048 if is_cros_snapshot_version(new):
2049 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002050
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08002051 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002052 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08002053 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002054 parser = repo_util.ManifestParser(self.manifest_dir)
2055 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002056 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002057 result.append(
2058 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
2059 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002060
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002061 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002062 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
2063 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
2064
2065 # case 1: if both are snapshot, return a list of snapshot
2066 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
2067 return self.collect_snapshot_specs(old, new)
2068
2069 # case 2: if both are release version
2070 # return a list of release version
2071 if is_cros_full_version(old) and is_cros_full_version(new):
2072 return self.collect_release_specs(old, new)
2073
2074 # case 3: return a list of release version and append a snapshot
2075 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002076 result = self.collect_release_specs(
2077 version_to_full(self.config['board'], old),
2078 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002079 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002080 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002081 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08002082 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002083 return result
2084
2085 def collect_snapshot_specs(self, old, new):
2086 assert is_cros_snapshot_version(old)
2087 assert is_cros_snapshot_version(new)
2088
2089 def guess_snapshot_version(board, snapshot_id, old, new):
2090 if old.endswith('-' + snapshot_id):
2091 return old
2092 if new.endswith('-' + snapshot_id):
2093 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08002094 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002095 'R*-{snapshot_id}-*'.format(
2096 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002097 for line in gsutil_ls(gs_path, ignore_errors=True):
2098 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
2099 if m:
2100 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002101 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002102
2103 result = []
2104 path = 'snapshot.xml'
2105 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002106 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002107 snapshot_version = guess_snapshot_version(self.config['board'],
2108 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002109 if snapshot_version:
2110 result.append(
2111 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
2112 path))
2113 else:
2114 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002115 return result
2116
2117 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002118 assert is_cros_full_version(old)
2119 assert is_cros_full_version(new)
2120 old_milestone, old_short_version = version_split(old)
2121 new_milestone, new_short_version = version_split(new)
2122
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002123 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002124 for milestone in git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002125 self.historical_manifest_git_dir, self.historical_manifest_branch_name,
2126 'buildspecs'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002127 if not milestone.isdigit():
2128 continue
2129 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
2130 continue
2131
Kuang-che Wu74768d32018-09-07 12:03:24 +08002132 files = git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002133 self.historical_manifest_git_dir,
2134 self.historical_manifest_branch_name,
Kuang-che Wu74768d32018-09-07 12:03:24 +08002135 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002136
2137 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002138 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002139 short_version, ext = os.path.splitext(fn)
2140 if ext != '.xml':
2141 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002142 if (util.is_version_lesseq(old_short_version, short_version) and
2143 util.is_version_lesseq(short_version, new_short_version) and
2144 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002145 rev = make_cros_full_version(milestone, short_version)
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002146 timestamp = git_util.get_commit_time(
2147 self.historical_manifest_git_dir,
2148 self.historical_manifest_branch_name, path)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002149 result.append(
2150 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002151
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002152 def version_key_func(spec):
2153 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002154 return util.version_key_func(short_version)
2155
2156 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002157 assert result[0].name == old
2158 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002159 return result
2160
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002161 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002162 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2163 if is_cros_full_version(rev):
2164 milestone, short_version = version_split(rev)
2165 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2166 manifest = git_util.get_file_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002167 self.historical_manifest_git_dir,
2168 self.historical_manifest_branch_name, path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002169 else:
2170 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2171 commit_id = revisions[0][1]
2172 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2173 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002174 return manifest
2175
2176 def get_manifest_file(self, rev):
2177 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002178 manifest_name = 'manifest_%s.xml' % rev
2179 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2180 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002181 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002182
2183 # workaround for b/150572399
2184 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2185 # folder .repo instead of.repo/manifests
2186 if is_cros_version_lesseq(rev, '12931.0.0'):
2187 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2188 manifest_patch_path = os.path.join(repo_path, manifest_name)
2189 with open(manifest_patch_path, 'w') as f:
2190 f.write(self.get_manifest(rev))
2191
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002192 return manifest_name
2193
2194 def parse_spec(self, spec):
2195 parser = repo_util.ManifestParser(self.manifest_dir)
2196 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002197 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002198 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002199 with open(manifest_path) as f:
2200 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002201 root = parser.parse_single_xml(content, allow_include=False)
2202 else:
2203 root = parser.parse_xml_recursive(spec.name, spec.path)
2204
2205 spec.entries = parser.process_parsed_result(root)
2206 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002207 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002208 raise ValueError('fixed spec %r has unexpected floating entries' %
2209 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002210 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002211
2212 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002213 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002214
2215 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2216 # manifest. 'repo sync -m' is not enough
2217 repo_util.init(
2218 self.config['chromeos_root'],
2219 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2220 manifest_name=manifest_name,
2221 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002222 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002223 # b/150753074: moblab is in non-default group and causes mark_as_stable
2224 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002225 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002226 )
2227
2228 # Note, don't sync with current_branch=True for chromeos. One of its
2229 # build steps (inside mark_as_stable) executes "git describe" which
2230 # needs git tag information.
2231 repo_util.sync(self.config['chromeos_root'])