blob: d54b5db9605088d325c18d69b4e30869499baa3a [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""ChromeOS utility.
6
7Terminology used in this module.
8 short_version: ChromeOS version number without milestone, like "9876.0.0".
9 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080010 snapshot_version: ChromeOS version number with milestone and snapshot id,
11 like "R62-9876.0.0-12345".
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080012 version: if not specified, it could be in short or full format.
13"""
14
15from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080016import ast
Kuang-che Wu72b5a572019-10-29 20:37:57 +080017import calendar
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080018import datetime
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080019import errno
20import json
21import logging
22import os
23import re
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +080024import shutil
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080025import subprocess
Kuang-che Wud558a042020-06-06 02:11:00 +080026import sys
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080027import time
28
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080029from google.protobuf import json_format
30
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080031from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080032from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080033from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080034from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080035from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080036from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080037from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080038from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080039from bisect_kit import util
40
41logger = logging.getLogger(__name__)
42
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080043re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080044re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
45re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080046re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080047
48gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
49gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080050 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080051
52# Assume gsutil is in PATH.
53gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080054
55# Since snapshots with version >= 12618.0.0 have android and chrome version
56# info.
57snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080058
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080059# current earliest buildbucket buildable versions
60# picked from https://crrev.com/c/2072618
61buildbucket_cutover_versions = [
62 '12931.0.0',
63 '12871.26.0', # R81
64 '12871.24.2', # stabilize-12871.24.B
65 '12812.10.0', # factory-excelsior-12812.B
66 '12768.14.0', # firmware-servo-12768.B
67 '12739.85.0', # R80
68 '12739.67.1', # stabilize-excelsior-12739.67.B
69 '12692.36.0', # factory-hatch-12692.B
70 '12672.104.0', # firmware-hatch-12672.B
71 '12607.110.0', # R79
72 '12607.83.2', # stabilize-quickfix-12607.83.B
73 '12587.59.0', # factory-kukui-12587.B
74 '12573.78.0', # firmware-kukui-12573.B
75 '12499.96.0', # R78
76 '12422.33.0', # firmware-mistral-12422.B
77 '12371.190.0', # R77
78 '12361.38.0', # factory-mistral-12361.B
79 '12200.65.0', # firmware-sarien-12200.B
80 '12105.128.0', # R75
81 '12033.82.0', # factory-sarien-12033.B
82]
83
Kuang-che Wub9705bd2018-06-28 17:59:18 +080084chromeos_root_inside_chroot = '/mnt/host/source'
85# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080086prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080087prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080088# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
89cached_images_dir = 'src/build/images'
90test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080091
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080092VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
93VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
94VERSION_KEY_MILESTONE = 'milestone'
95VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080096VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080097VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +080098CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080099
100
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800101class NeedRecreateChrootException(Exception):
102 """Failed to build ChromeOS because of chroot mismatch or corruption"""
103
104
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800105def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800106 """Determines if `s` is chromeos short version.
107
108 This function doesn't accept version number of local build.
109 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800110 return bool(re.match(re_chromeos_short_version, s))
111
112
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800113def is_cros_localbuild_version(s):
114 """Determines if `s` is chromeos local build version."""
115 return bool(re.match(re_chromeos_localbuild_version, s))
116
117
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800118def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800119 """Determines if `s` is chromeos full version.
120
121 This function doesn't accept version number of local build.
122 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800123 return bool(re.match(re_chromeos_full_version, s))
124
125
126def is_cros_version(s):
127 """Determines if `s` is chromeos version (either short or full)"""
128 return is_cros_short_version(s) or is_cros_full_version(s)
129
130
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800131def is_cros_snapshot_version(s):
132 """Determines if `s` is chromeos snapshot version"""
133 return bool(re.match(re_chromeos_snapshot_version, s))
134
135
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800136def is_cros_version_lesseq(ver1, ver2):
137 """Determines if ver1 is less or equal to ver2.
138
139 Args:
140 ver1: a Chrome OS version in short, full, or snapshot format.
141 ver2: a Chrome OS version in short, full, or snapshot format.
142
143 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800144 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800145 """
146 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
147 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
148
149 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
150 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
151 return ver1 <= ver2
152
153
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800154def is_buildbucket_buildable(version):
155 """Determines if a version is buildable on buildbucket."""
156 short_version = version_to_short(version)
157 # If given version is child of any cutover, then it's buildable
158 return any([
159 util.is_direct_relative_version(x, short_version) and
160 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
161 ])
162
163
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800164def make_cros_full_version(milestone, short_version):
165 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800166 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800167 return 'R%s-%s' % (milestone, short_version)
168
169
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800170def make_cros_snapshot_version(milestone, short_version, snapshot_id):
171 """Makes snapshot version from milestone, short_version and snapshot id"""
172 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
173
174
175def version_split(version):
176 """Splits full_version or snapshot_version into milestone and short_version"""
177 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
178 if is_cros_snapshot_version(version):
179 return snapshot_version_split(version)[0:2]
180 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800181 return milestone[1:], short_version
182
183
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800184def snapshot_version_split(snapshot_version):
185 """Splits snapshot_version into milestone, short_version and snapshot_id"""
186 assert is_cros_snapshot_version(snapshot_version)
187 milestone, shot_version, snapshot_id = snapshot_version.split('-')
188 return milestone[1:], shot_version, snapshot_id
189
190
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800191def query_snapshot_buildbucket_id(board, snapshot_version):
192 """Query buildbucket id of a snapshot"""
193 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800194 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800195 '/{snapshot_version}-*/image.zip')
196 output = gsutil_ls(
197 '-d',
198 path.format(board=board, snapshot_version=snapshot_version),
199 ignore_errors=True)
200 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800201 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800202 if m:
203 return m.group(1)
204 return None
205
206
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800207def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800208 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800209 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800210 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 +0800211 return s
212
213
214def query_dut_lsb_release(host):
215 """Query /etc/lsb-release of given DUT
216
217 Args:
218 host: the DUT address
219
220 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800221 dict for keys and values of /etc/lsb-release.
222
223 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800224 errors.SshConnectionError: cannot connect to host
225 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800226 """
227 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800228 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800229 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800230 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800231 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
232
233
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800234def query_dut_os_release(host):
235 """Query /etc/os-release of given DUT
236
237 Args:
238 host: the DUT address
239
240 Returns:
241 dict for keys and values of /etc/os-release.
242
243 Raises:
244 errors.SshConnectionError: cannot connect to host
245 errors.ExternalError: lsb-release file doesn't exist
246 """
247 try:
248 output = util.ssh_cmd(host, 'cat', '/etc/os-release')
249 except subprocess.CalledProcessError:
250 raise errors.ExternalError('unable to read /etc/os-release; not a DUT')
251 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
252
253
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800254def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800255 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800256
257 Args:
258 host: the DUT address
259
260 Returns:
261 True if the host is a chromeos device.
262 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800263 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800264 return query_dut_os_release(host).get('ID') in [
265 'chromiumos',
266 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800267 ]
268 except (errors.ExternalError, errors.SshConnectionError):
269 return False
270
271
272def is_good_dut(host):
273 if not is_dut(host):
274 return False
275
276 # Sometimes python is broken after 'cros flash'.
277 try:
278 util.ssh_cmd(host, 'python', '-c', '1')
279 return True
280 except (subprocess.CalledProcessError, errors.SshConnectionError):
281 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800282
283
284def query_dut_board(host):
285 """Query board name of a given DUT"""
286 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
287
288
289def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800290 """Query short version of a given DUT.
291
292 This function may return version of local build, which
293 is_cros_short_version() is false.
294 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800295 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
296
297
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800298def query_dut_prebuilt_version(host):
299 """Return a snapshot version or short version of a given DUT.
300
301 Args:
302 host: dut host
303
304 Returns:
305 Snapshot version or short version.
306 """
307 lsb_release = query_dut_lsb_release(host)
308 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
309 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH')
310 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
311 builder_path)
312 if match:
313 return match.group(1)
314 return release_version
315
316
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800317def query_dut_is_by_official_builder(host):
318 """Query if given DUT is build by buildbucket builder"""
319 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
320 '').startswith('Continuous Builder')
321
322
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800323def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800324 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800325
326 Args:
327 host: DUT address
328 connect_timeout: connection timeout
329
330 Returns:
331 boot uuid
332 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800333 return util.ssh_cmd(
334 host,
335 'cat',
336 '/proc/sys/kernel/random/boot_id',
337 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800338
339
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800340def reboot(host, force_reboot_callback=None):
341 """Reboot a DUT and verify.
342
343 Args:
344 host: DUT address
345 force_reboot_callback: powerful reboot hook (via servo). This will be
346 invoked if normal reboot failed.
347 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800348 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800349 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800350 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800351 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800352
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800353 try:
354 util.ssh_cmd(host, 'reboot')
355 except errors.SshConnectionError:
356 # Depends on timing, ssh may return failure due to broken pipe, which is
357 # working as intended. Ignore such kind of errors.
358 pass
359
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800360 wait_reboot_done(host, boot_id)
361 except (errors.SshConnectionError, errors.ExternalError):
362 if force_reboot_callback and force_reboot_callback(host):
363 wait_reboot_done(host, boot_id)
364 return
365 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800366
Kuang-che Wu708310b2018-03-28 17:24:34 +0800367
368def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800369 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800370 # (dev screen short delay) or more (long delay).
371 time.sleep(15)
372 for _ in range(100):
373 try:
374 # During boot, DUT does not response and thus ssh may hang a while. So
375 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
376 # set tight limit because it's inside retry loop.
377 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
378 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800379 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800380 logger.debug('reboot not ready? sleep wait 1 sec')
381 time.sleep(1)
382
Kuang-che Wue121fae2018-11-09 16:18:39 +0800383 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800384
385
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800386def gs_release_boardpath(board):
387 """Normalizes board name for gs://chromeos-releases/
388
389 This follows behavior of PushImage() in chromite/scripts/pushimage.py
390 Note, only gs://chromeos-releases/ needs normalization,
391 gs://chromeos-image-archive does not.
392
393 Args:
394 board: ChromeOS board name
395
396 Returns:
397 normalized board name
398 """
399 return board.replace('_', '-')
400
401
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800402def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800403 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800404
405 Args:
406 args: command line arguments passed to gsutil
407 kwargs:
408 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
409 but the path not found.
410
411 Returns:
412 stdout of gsutil
413
414 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800415 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800416 subprocess.CalledProcessError: command failed
417 """
418 stderr_lines = []
419 try:
420 return util.check_output(
421 gsutil_bin, *args, stderr_callback=stderr_lines.append)
422 except subprocess.CalledProcessError as e:
423 stderr = ''.join(stderr_lines)
424 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800425 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800426 'gsutil failed due to permission. ' +
427 'Run "%s config" and follow its instruction. ' % gsutil_bin +
428 'Fill any string if it asks for project-id')
429 if kwargs.get('ignore_errors'):
430 return ''
431 raise
432 except OSError as e:
433 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800434 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800435 'Unable to run %s. gsutil is not installed or not in PATH?' %
436 gsutil_bin)
437 raise
438
439
440def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800441 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800442
443 Args:
444 args: arguments passed to 'gsutil ls'
445 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800446 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800447 exception, ex. path not found.
448
449 Returns:
450 list of 'gsutil ls' result. One element for one line of gsutil output.
451
452 Raises:
453 subprocess.CalledProcessError: gsutil failed, usually means path not found
454 """
455 return gsutil('ls', *args, **kwargs).splitlines()
456
457
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800458def gsutil_stat_update_time(*args, **kwargs):
459 """Returns the last modified time of a file or multiple files.
460
461 Args:
462 args: arguments passed to 'gsutil stat'.
463 kwargs: extra parameters for gsutil.
464
465 Returns:
466 A integer indicates the last modified timestamp.
467
468 Raises:
469 subprocess.CalledProcessError: gsutil failed, usually means path not found
470 errors.ExternalError: update time is not found
471 """
472 result = -1
473 # Currently we believe stat always returns a UTC time, and strptime also
474 # parses a UTC time by default.
475 time_format = '%a, %d %b %Y %H:%M:%S GMT'
476
477 for line in gsutil('stat', *args, **kwargs).splitlines():
478 if ':' not in line:
479 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800480 key, value = line.split(':', 1)
481 key, value = key.strip(), value.strip()
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800482 if key != 'Update time':
483 continue
484 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800485 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800486 result = max(result, unixtime)
487
488 if result == -1:
489 raise errors.ExternalError("didn't find update time")
490 return result
491
492
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800493def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800494 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800495
496 Args:
497 board: ChromeOS board name
498 short_version: ChromeOS version number in short format, ex. 9300.0.0
499
500 Returns:
501 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
502 None if failed.
503 """
504 path = gs_archive_path.format(board=board) + '/R*-' + short_version
505 for line in gsutil_ls('-d', path, ignore_errors=True):
506 m = re.search(r'/R(\d+)-', line)
507 if not m:
508 continue
509 return m.group(1)
510
511 for channel in ['canary', 'dev', 'beta', 'stable']:
512 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800513 channel=channel,
514 boardpath=gs_release_boardpath(board),
515 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800516 for line in gsutil_ls(path, ignore_errors=True):
517 m = re.search(r'\bR(\d+)-' + short_version, line)
518 if not m:
519 continue
520 return m.group(1)
521
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800522 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800523 return None
524
525
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800526def list_board_names(chromeos_root):
527 """List board names.
528
529 Args:
530 chromeos_root: chromeos tree root
531
532 Returns:
533 list of board names
534 """
535 # Following logic is simplified from chromite/lib/portage_util.py
536 cros_list_overlays = os.path.join(chromeos_root,
537 'chromite/bin/cros_list_overlays')
538 overlays = util.check_output(cros_list_overlays).splitlines()
539 result = set()
540 for overlay in overlays:
541 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
542 name = None
543 if os.path.exists(conf_file):
544 for line in open(conf_file):
545 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
546 if m:
547 name = m.group(1)
548 break
549
550 if not name:
551 name_file = os.path.join(overlay, 'profiles', 'repo_name')
552 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800553 with open(name_file) as f:
554 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800555
556 if name:
557 name = re.sub(r'-private$', '', name)
558 result.add(name)
559
560 return list(result)
561
562
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800563def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800564 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800565
566 Args:
567 board: ChromeOS board name
568 version: ChromeOS version number in short or full format
569
570 Returns:
571 (milestone, version in short format)
572 """
573 if is_cros_short_version(version):
574 milestone = query_milestone_by_version(board, version)
575 short_version = version
576 else:
577 milestone, short_version = version_split(version)
578 return milestone, short_version
579
580
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800581def extract_major_version(version):
582 """Converts a version to its major version.
583
584 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800585 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800586
587 Returns:
588 major version number in string format
589 """
590 version = version_to_short(version)
591 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
592 return m.group(1)
593
594
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800595def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800596 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800597
598 Args:
599 version: ChromeOS version number in short or full format
600
601 Returns:
602 version number in short format
603 """
604 if is_cros_short_version(version):
605 return version
606 _, short_version = version_split(version)
607 return short_version
608
609
610def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800611 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800612
613 Args:
614 board: ChromeOS board name
615 version: ChromeOS version number in short or full format
616
617 Returns:
618 version number in full format
619 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800620 if is_cros_snapshot_version(version):
621 milestone, short_version, _ = snapshot_version_split(version)
622 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800623 if is_cros_full_version(version):
624 return version
625 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800626 if not milestone:
627 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
628 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800629 return make_cros_full_version(milestone, version)
630
631
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800632def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800633 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800634
635 Args:
636 board: ChromeOS board
637 major_version: ChromeOS major version
638
639 Returns:
640 list of (version, gs_path):
641 version: Chrome OS snapshot version
642 gs_path: gs path of test image
643 """
644
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800645 def extract_snapshot_id(result):
646 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
647 assert m
648 return int(m.group(1))
649
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800650 short_version = '%s.0.0' % major_version
651 milestone = query_milestone_by_version(board, short_version)
652 if not milestone:
653 milestone = '*'
654
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800655 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800656 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800657 result = []
658 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800659 path.format(
660 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800661 ignore_errors=True)
662
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800663 for gs_path in sorted(output):
664 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800665 if m:
666 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800667 # we should skip if there is duplicate snapshot
668 if result and result[-1][0] == snapshot_version:
669 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800670
671 # b/151054108: snapshot version in [29288, 29439] is broken
672 _, _, snapshot_id = snapshot_version_split(snapshot_version)
673 if 29288 <= int(snapshot_id) <= 29439:
674 continue
675
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800676 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800677
678 # sort by its snapshot_id
679 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800680 return result
681
682
Kuang-che Wu575dc442019-03-05 10:30:55 +0800683def list_prebuilt_from_image_archive(board):
684 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
685
686 gs://chromeos-image-archive contains only recent builds (in two years).
687 We prefer this function to list_prebuilt_from_chromeos_releases() because
688 - this is what "cros flash" supports directly.
689 - the paths have milestone information, so we don't need to do slow query
690 by ourselves.
691
692 Args:
693 board: ChromeOS board name
694
695 Returns:
696 list of (version, gs_path):
697 version: Chrome OS version in full format
698 gs_path: gs path of test image
699 """
700 result = []
701 for line in gsutil_ls(gs_archive_path.format(board=board)):
702 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
703 if m:
704 full_version = m.group(1)
705 test_image = 'chromiumos_test_image.tar.xz'
706 assert line.endswith('/')
707 gs_path = line + test_image
708 result.append((full_version, gs_path))
709 return result
710
711
712def list_prebuilt_from_chromeos_releases(board):
713 """Lists ChromeOS versions available from gs://chromeos-releases.
714
715 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
716 support it.
717
718 Args:
719 board: ChromeOS board name
720
721 Returns:
722 list of (version, gs_path):
723 version: Chrome OS version in short format
724 gs_path: gs path of test image (with wildcard)
725 """
726 result = []
727 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800728 gs_release_path.format(
729 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800730 ignore_errors=True):
731 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
732 if m:
733 short_version = m.group(1)
734 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
735 short_version=short_version, board=board)
736 gs_path = line + test_image
737 result.append((short_version, gs_path))
738 return result
739
740
Kuang-che Wue1808402020-01-06 20:27:45 +0800741def has_test_image(board, version):
742 if is_cros_snapshot_version(version):
743 return bool(query_snapshot_buildbucket_id(board, version))
744
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800745 try:
746 full_version = version_to_full(board, version)
747 except errors.ExternalError:
748 # version_to_full() is implemented by checking image, thus its failure
749 # means no image.
750 return False
Kuang-che Wue1808402020-01-06 20:27:45 +0800751 short_version = version_to_short(version)
752 paths = [
753 gs_archive_path.format(board=board) +
754 '/%s/chromiumos_test_image.tar.xz' % full_version,
755 gs_release_path.format(
756 channel='*',
757 boardpath=gs_release_boardpath(board),
758 short_version=short_version),
759 ]
760
761 for path in paths:
762 if gsutil_ls(path, ignore_errors=True):
763 return True
764 return False
765
766
Kuang-che Wu575dc442019-03-05 10:30:55 +0800767def list_chromeos_prebuilt_versions(board,
768 old,
769 new,
770 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800771 include_older_build=True,
772 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800773 """Lists ChromeOS version numbers with prebuilt between given range
774
775 Args:
776 board: ChromeOS board name
777 old: start version (inclusive)
778 new: end version (inclusive)
779 only_good_build: only if test image is available
780 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800781 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800782
783 Returns:
784 list of sorted version numbers (in full format) between [old, new] range
785 (inclusive).
786 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800787 old_short = version_to_short(old)
788 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800789
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800790 rev_map = {
791 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800792 for full_version, gs_path in list_prebuilt_from_image_archive(board):
793 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800794 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800795
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800796 if include_older_build and old_short not in rev_map:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800797 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
798 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800799 rev_map[short_version] = [(short_version, gs_path)]
800
801 if use_snapshot:
802 for major_version in range(
803 int(extract_major_version(old)),
804 int(extract_major_version(new)) + 1):
805 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800806 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800807 # If current version is smaller than cutover, ignore it as it might not
808 # contain enough information for continuing android and chrome bisection.
809 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
810 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800811
812 # Given the fact that snapshots are images between two release versions.
813 # Adding snapshots of 12345.0.0 should be treated as adding commits
814 # between [12345.0.0, 12346.0.0).
815 # So in the following lines we check two facts:
816 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800817 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800818 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800819 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800820 continue
821 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800822 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800823 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800824 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800825 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800826
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800827 snapshots = list_snapshots_from_image_archive(board, str(major_version))
828 if snapshots:
829 # if snapshots found, we can append them after the release version,
830 # so the prebuilt image list of this version will be
831 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800832 if short_version not in rev_map:
833 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800834 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800835
836 result = []
837 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800838 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800839 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800840 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800841 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800842 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800843 continue
844
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800845 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800846
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800847 # version_to_full() and gsutil_ls() may take long time if versions are a
848 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800849
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800850 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800851 gs_result = gsutil_ls(gs_path, ignore_errors=True)
852 if not gs_result:
853 logger.warning('%s is not a good build, ignore', version)
854 continue
855 assert len(gs_result) == 1
856 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
857 if not m:
858 logger.warning('format of image path is unexpected: %s', gs_result[0])
859 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800860 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800861 elif is_cros_short_version(version):
862 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800863
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800864 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
865 version, new):
866 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800867
868 return result
869
870
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800871def prepare_snapshot_image(chromeos_root, board, snapshot_version):
872 """Prepare chromeos snapshot image.
873
874 Args:
875 chromeos_root: chromeos tree root
876 board: ChromeOS board name
877 snapshot_version: ChromeOS snapshot version number
878
879 Returns:
880 local file path of test image relative to chromeos_root
881 """
882 assert is_cros_snapshot_version(snapshot_version)
883 milestone, short_version, snapshot_id = snapshot_version_split(
884 snapshot_version)
885 full_version = make_cros_full_version(milestone, short_version)
886 tmp_dir = os.path.join(
887 chromeos_root, 'tmp',
888 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
889 if not os.path.exists(tmp_dir):
890 os.makedirs(tmp_dir)
891
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800892 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/' +
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800893 '{snapshot_version}-*/image.zip')
894 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
895
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800896 full_path = os.path.join(tmp_dir, test_image_filename)
897 rel_path = os.path.relpath(full_path, chromeos_root)
898 if os.path.exists(full_path):
899 return rel_path
900
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800901 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800902 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800903 gs_path = files[0]
904 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800905 util.check_call(
906 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
907 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800908 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800909
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800910 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800911 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800912
913
Kuang-che Wu28980b22019-07-31 19:51:45 +0800914def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800915 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800916
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800917 It searches for xbuddy image which "cros flash" can use, or fetch image to
918 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800919
920 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800921 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800922 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800923 version: ChromeOS version number in short or full format
924
925 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800926 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800927 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800928 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800929 full_version = version_to_full(board, version)
930 short_version = version_to_short(full_version)
931
932 image_path = None
933 gs_path = gs_archive_path.format(board=board) + '/' + full_version
934 if gsutil_ls('-d', gs_path, ignore_errors=True):
935 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
936 board=board, full_version=full_version)
937 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800938 tmp_dir = os.path.join(chromeos_root, 'tmp',
939 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800940 full_path = os.path.join(tmp_dir, test_image_filename)
941 rel_path = os.path.relpath(full_path, chromeos_root)
942 if os.path.exists(full_path):
943 return rel_path
944
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800945 if not os.path.exists(tmp_dir):
946 os.makedirs(tmp_dir)
947 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800948 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800949 # to fetch the image by ourselves
950 for channel in ['canary', 'dev', 'beta', 'stable']:
951 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
952 full_version=full_version, board=board)
953 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800954 channel=channel,
955 boardpath=gs_release_boardpath(board),
956 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800957 gs_path += '/' + fn
958 if gsutil_ls(gs_path, ignore_errors=True):
959 # TODO(kcwu): delete tmp
960 gsutil('cp', gs_path, tmp_dir)
961 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800962 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800963 break
964
965 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800966 return image_path
967
968
969def cros_flash(chromeos_root,
970 host,
971 board,
972 image_path,
973 version=None,
974 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800975 disable_rootfs_verification=True,
976 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800977 """Flash a DUT with given ChromeOS image.
978
979 This is implemented by 'cros flash' command line.
980
981 Args:
982 chromeos_root: use 'cros flash' of which chromeos tree
983 host: DUT address
984 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800985 image_path: chromeos image xbuddy path or file path. For relative
986 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800987 version: ChromeOS version in short or full format
988 clobber_stateful: Clobber stateful partition when performing update
989 disable_rootfs_verification: Disable rootfs verification after update
990 is completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800991 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800992
993 Raises:
994 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800995 """
996 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
997
998 # Reboot is necessary because sometimes previous 'cros flash' failed and
999 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001000 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001001
Kuang-che Wu020a1182020-09-08 17:17:22 +08001002 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001003 # The service is used in jetstream boards, but not other CrOS devices.
1004 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
1005 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001006 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
1007 # For simplicity, we wait 60 seconds here, which is the timeout value of
1008 # jetstream_host.
1009 # https://chromium.googlesource.com/chromiumos/third_party/autotest
1010 # /+/master/server/hosts/jetstream_host.py#27
1011 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001012 util.ssh_cmd(host, 'stop', 'ap-update-manager')
1013 except subprocess.CalledProcessError:
1014 pass # not started; do nothing
1015
Kuang-che Wu28980b22019-07-31 19:51:45 +08001016 # Handle relative path.
1017 if '://' not in image_path and not os.path.isabs(image_path):
1018 assert os.path.exists(os.path.join(chromeos_root, image_path))
1019 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
1020
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001021 args = [
1022 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
1023 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001024 if clobber_stateful:
1025 args.append('--clobber-stateful')
1026 if disable_rootfs_verification:
1027 args.append('--disable-rootfs-verification')
1028
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001029 try:
1030 cros_sdk(chromeos_root, 'cros', 'flash', *args)
1031 except subprocess.CalledProcessError:
1032 raise errors.ExternalError('cros flash failed')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001033
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001034 if version:
1035 # In the past, cros flash may fail with returncode=0
1036 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001037 if is_cros_snapshot_version(version):
1038 builder_path = query_dut_lsb_release(host).get(
1039 'CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001040 expect_prefix = '%s-snapshot/%s-' % (board, version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001041 if not builder_path.startswith(expect_prefix):
1042 raise errors.ExternalError(
1043 'although cros flash succeeded, the OS builder path is '
1044 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1045 else:
1046 expect_version = version_to_short(version)
1047 dut_version = query_dut_short_version(host)
1048 if dut_version != expect_version:
1049 raise errors.ExternalError(
1050 'although cros flash succeeded, the OS version is unexpected: '
1051 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001052
Kuang-che Wu4a81ea72019-10-05 15:35:17 +08001053 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001054 # (b/130786578), so it's necessary to do sanity check.
1055 if not is_good_dut(host):
1056 raise errors.ExternalError(
1057 'although cros flash succeeded, the DUT is in bad state')
1058
1059
1060def cros_flash_with_retry(chromeos_root,
1061 host,
1062 board,
1063 image_path,
1064 version=None,
1065 clobber_stateful=False,
1066 disable_rootfs_verification=True,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001067 repair_callback=None,
1068 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001069 # 'cros flash' is not 100% reliable, retry if necessary.
1070 for attempt in range(2):
1071 if attempt > 0:
1072 logger.info('will retry 60 seconds later')
1073 time.sleep(60)
1074
1075 try:
1076 cros_flash(
1077 chromeos_root,
1078 host,
1079 board,
1080 image_path,
1081 version=version,
1082 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001083 disable_rootfs_verification=disable_rootfs_verification,
1084 force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001085 return True
1086 except errors.ExternalError:
1087 logger.exception('cros flash failed')
1088 if repair_callback and not repair_callback(host):
1089 logger.warning('not repaired, assume it is harmless')
1090 continue
1091 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001092
1093
1094def version_info(board, version):
1095 """Query subcomponents version info of given version of ChromeOS
1096
1097 Args:
1098 board: ChromeOS board name
1099 version: ChromeOS version number in short or full format
1100
1101 Returns:
1102 dict of component and version info, including (if available):
1103 cros_short_version: ChromeOS version
1104 cros_full_version: ChromeOS version
1105 milestone: milestone of ChromeOS
1106 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001107 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001108 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1109 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001110 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001111 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001112 milestone, short_version, _ = snapshot_version_split(version)
1113 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001114 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001115 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001116 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001117 VERSION_KEY_MILESTONE: milestone,
1118 VERSION_KEY_CROS_FULL_VERSION: version,
1119 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001120 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1121 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1122 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001123 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001124 info = {}
1125 full_version = version_to_full(board, version)
1126
1127 # Some boards may have only partial-metadata.json but no metadata.json.
1128 # e.g. caroline R60-9462.0.0
1129 # Let's try both.
1130 metadata = None
1131 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001132 path = gs_archive_path.format(
1133 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001134 metadata = gsutil('cat', path, ignore_errors=True)
1135 if metadata:
1136 o = json.loads(metadata)
1137 v = o['version']
1138 board_metadata = o['board-metadata'][board]
1139 info.update({
1140 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1141 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1142 VERSION_KEY_MILESTONE: v['milestone'],
1143 VERSION_KEY_CR_VERSION: v['chrome'],
1144 })
1145
1146 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001147 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001148 if 'android-branch' in v: # this appears since R58-9317.0.0
1149 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1150 elif 'android-container-branch' in board_metadata:
1151 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1152 break
1153 else:
1154 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1155 logger.error(
1156 'Note, so far no quick way to look up version info for too old builds')
1157
1158 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001159
1160
1161def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001162 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001163
1164 Args:
1165 board: ChromeOS board name
1166 version: ChromeOS version number in short or full format
1167
1168 Returns:
1169 Chrome version number
1170 """
1171 info = version_info(board, version)
1172 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001173
1174
1175def query_android_build_id(board, rev):
1176 info = version_info(board, rev)
1177 rev = info['android_build_id']
1178 return rev
1179
1180
1181def query_android_branch(board, rev):
1182 info = version_info(board, rev)
1183 rev = info['android_branch']
1184 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001185
1186
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001187def guess_chrome_version(board, rev):
1188 """Guess chrome version number.
1189
1190 Args:
1191 board: chromeos board name
1192 rev: chrome or chromeos version
1193
1194 Returns:
1195 chrome version number
1196 """
1197 if is_cros_version(rev):
1198 assert board, 'need to specify BOARD for cros version'
1199 rev = query_chrome_version(board, rev)
1200 assert cr_util.is_chrome_version(rev)
1201
1202 return rev
1203
1204
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001205def is_inside_chroot():
1206 """Returns True if we are inside chroot."""
1207 return os.path.exists('/etc/cros_chroot_version')
1208
1209
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001210def convert_path_outside_chroot(chromeos_root, path):
1211 """Converts path in chroot to outside.
1212
1213 Args:
1214 chromeos_root: chromeos tree root
1215 path: path inside chroot; support starting with '~/'
1216
1217 Returns:
1218 The corresponding path outside chroot assuming the chroot is mounted
1219 """
1220 if path.startswith('~/'):
1221 path = path.replace('~', '/home/' + os.environ['USER'])
1222 assert '~' not in path, 'tilde (~) character is not fully supported'
1223
1224 assert os.path.isabs(path)
1225 assert path[0] == os.sep
1226 return os.path.join(chromeos_root, 'chroot', path[1:])
1227
1228
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001229def cros_sdk(chromeos_root, *args, **kwargs):
1230 """Run commands inside chromeos chroot.
1231
1232 Args:
1233 chromeos_root: chromeos tree root
1234 *args: command to run
1235 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001236 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001237 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001238 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001239 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001240 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001241 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001242 """
1243 envs = []
1244 for k, v in kwargs.get('env', {}).items():
1245 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1246 envs.append('%s=%s' % (k, v))
1247
1248 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1249 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001250 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001251
1252 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001253 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001254 if kwargs.get('goma_dir'):
1255 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001256
Kuang-che Wu399d4662019-06-06 15:23:37 +08001257 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001258
Kuang-che Wu399d4662019-06-06 15:23:37 +08001259 # In addition to the output of command we are interested, cros_sdk may
1260 # generate its own messages. For example, chroot creation messages if we run
1261 # cros_sdk the first time.
1262 # This is the hack to run dummy command once, so we can get clean output for
1263 # the command we are interested.
1264 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001265 try:
1266 util.check_call(*cmd, cwd=chromeos_root)
1267 except subprocess.CalledProcessError:
1268 logger.exception('cros_sdk init/update failed')
1269 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001270
1271 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001272 return util.check_output(
1273 *cmd,
1274 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001275 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001276 stdin=kwargs.get('stdin'),
1277 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001278
1279
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001280def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001281 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001282
1283 Args:
1284 chromeos_root: chromeos tree root
1285 """
1286 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1287 return
1288 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1289 return
1290
1291 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1292
1293
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001294def mount_chroot(chromeos_root):
1295 """Creates ChromeOS chroot if necessary.
1296
1297 Args:
1298 chromeos_root: chromeos tree root
1299 """
1300 # An arbitrary file must exist in chroot.
1301 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1302
1303 # Not created or mounted yet.
1304 if not os.path.exists(path):
1305 create_chroot(chromeos_root)
1306 # After this command, the chroot is mounted.
1307 cros_sdk(chromeos_root, 'true')
1308 assert os.path.exists(path)
1309
1310
1311def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001312 """Copies file into chromeos chroot.
1313
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001314 The side effect is chroot created and mounted.
1315
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001316 Args:
1317 chromeos_root: chromeos tree root
1318 src: path outside chroot
1319 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001320 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001321 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001322 mount_chroot(chromeos_root)
1323 src = os.path.expanduser(src)
1324 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1325 if not overwrite and os.path.exists(dst_outside):
1326 return
1327
1328 # Haven't support directory or special files yet.
1329 assert os.path.isfile(src)
1330 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1331
1332 dirname = os.path.dirname(dst_outside)
1333 if not os.path.exists(dirname):
1334 os.makedirs(dirname)
1335 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001336
1337
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001338def prepare_chroot(chromeos_root):
1339 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001340
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001341 # Work around b/149077936:
1342 # The creds file is copied into the chroot since 12866.0.0.
1343 # But earlier versions need this file as well because of cipd ACL change.
1344 creds_path = '~/.config/chrome_infra/auth/creds.json'
1345 if os.path.exists(os.path.expanduser(creds_path)):
1346 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001347
1348
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001349def check_if_need_recreate_chroot(stdout, stderr):
1350 """Analyze build log and determine if chroot should be recreated.
1351
1352 Args:
1353 stdout: stdout output of build
1354 stderr: stderr output of build
1355
1356 Returns:
1357 the reason if chroot needs recreated; None otherwise
1358 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001359 if re.search(
1360 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001361 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001362 return 'EAPI version mismatch'
1363
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001364 if 'Chroot is too new. Consider running:' in stderr:
1365 return 'chroot version is too new'
1366
1367 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001368 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1369 return 'chroot version is too new'
1370
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001371 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1372 if "undefined reference to 'std::__1::basic_string" in stdout:
1373 return 'might be due to compiler change'
1374
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001375 # Detect failures due to file collisions.
1376 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1377 # and conflict with each other. Other possible cases are package renaming or
1378 # refactoring. Let's recreate chroot to work around them.
1379 if 'Detected file collision' in stdout:
1380 # Using wildcard between words because the text wraps to the next line
1381 # depending on length of package name and each line is prefixed with
1382 # package name.
1383 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1384 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1385 # package name (65 now).
1386 m = re.search(
1387 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1388 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1389 if m:
1390 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001391
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001392 return None
1393
1394
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001395def build_packages(chromeos_root,
1396 board,
1397 chrome_root=None,
1398 goma_dir=None,
1399 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001400 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001401
1402 Args:
1403 chromeos_root: chromeos tree root
1404 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001405 chrome_root: Chrome tree root. If specified, build chrome using the
1406 provided tree
1407 goma_dir: Goma installed directory to mount into the chroot. If specified,
1408 build chrome with goma.
1409 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001410 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001411
1412 def has_build_package_argument(argument):
1413 stderr_lines = []
1414 try:
1415 util.check_call(
1416 'src/scripts/build_packages',
1417 '--help',
1418 cwd=chromeos_root,
1419 stderr_callback=stderr_lines.append)
1420 except subprocess.CalledProcessError:
1421 help_output = ''.join(stderr_lines)
1422 return '--[no]%s' % argument in help_output
1423
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001424 common_env = {
1425 'USE': '-cros-debug chrome_internal',
1426 'FEATURES': 'separatedebug',
1427 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001428 stderr_lines = []
1429 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001430 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001431 env = common_env.copy()
1432 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001433 cros_sdk(
1434 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001435 './update_chroot',
1436 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001437 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001438 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001439 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001440
1441 env = common_env.copy()
1442 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001443 './build_packages',
1444 '--board',
1445 board,
1446 '--withdev',
1447 '--noworkon',
1448 '--skip_chroot_upgrade',
1449 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001450 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001451
1452 # `use_any_chrome` flag is default on and will force to use a chrome
1453 # prebuilt even if the version doesn't match.
1454
1455 # As this argument is landed in 12681, we should check if the argument
1456 # exists before adding this.
1457 if has_build_package_argument('use_any_chrome'):
1458 cmd.append('--nouse_any_chrome')
1459
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001460 if goma_dir:
1461 # Tell build_packages to start and stop goma
1462 cmd.append('--run_goma')
1463 env['USE_GOMA'] = 'true'
1464 if afdo_use:
1465 env['USE'] += ' afdo_use'
1466 cros_sdk(
1467 chromeos_root,
1468 *cmd,
1469 env=env,
1470 chrome_root=chrome_root,
1471 stderr_callback=stderr_lines.append,
1472 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001473 except subprocess.CalledProcessError as e:
1474 # Detect failures due to incompatibility between chroot and source tree. If
1475 # so, notify the caller to recreate chroot and retry.
1476 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1477 if reason:
1478 raise NeedRecreateChrootException(reason)
1479
1480 # For other failures, don't know how to handle. Just bail out.
1481 raise
1482
Kuang-che Wu28980b22019-07-31 19:51:45 +08001483
1484def build_image(chromeos_root, board):
1485 """Build ChromeOS image.
1486
1487 Args:
1488 chromeos_root: chromeos tree root
1489 board: ChromeOS board name
1490
1491 Returns:
1492 image folder; relative to chromeos_root
1493 """
1494 stderr_lines = []
1495 try:
1496 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1497 cros_sdk(
1498 chromeos_root,
1499 './build_image',
1500 '--board',
1501 board,
1502 '--noenable_rootfs_verification',
1503 'test',
1504 env={
1505 'USE': '-cros-debug chrome_internal',
1506 'FEATURES': 'separatedebug',
1507 },
1508 stderr_callback=stderr_lines.append)
1509 except subprocess.CalledProcessError as e:
1510 # Detect failures due to incompatibility between chroot and source tree. If
1511 # so, notify the caller to recreate chroot and retry.
1512 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1513 if reason:
1514 raise NeedRecreateChrootException(reason)
1515
1516 # For other failures, don't know how to handle. Just bail out.
1517 raise
1518
1519 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1520 'latest')
1521 assert os.path.exists(image_symlink)
1522 image_name = os.readlink(image_symlink)
1523 image_folder = os.path.join(cached_images_dir, board, image_name)
1524 assert os.path.exists(
1525 os.path.join(chromeos_root, image_folder, test_image_filename))
1526 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001527
1528
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001529class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001530 """Parsed content of autotest control file.
1531
1532 Attributes:
1533 name: test name
1534 path: control file path
1535 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1536 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1537 """
1538
1539 def __init__(self, path, variables):
1540 self.name = variables['NAME']
1541 self.path = path
1542 self.variables = variables
1543
1544
1545def parse_autotest_control_file(path):
1546 """Parses autotest control file.
1547
1548 This only parses simple top-level string assignments.
1549
1550 Returns:
1551 AutotestControlInfo object
1552 """
1553 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001554 with open(path) as f:
1555 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001556 for stmt in code.body:
1557 # Skip if not simple "NAME = *" assignment.
1558 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1559 isinstance(stmt.targets[0], ast.Name)):
1560 continue
1561
1562 # Only support string value.
1563 if isinstance(stmt.value, ast.Str):
1564 variables[stmt.targets[0].id] = stmt.value.s
1565
1566 return AutotestControlInfo(path, variables)
1567
1568
1569def enumerate_autotest_control_files(autotest_dir):
1570 """Enumerate autotest control files.
1571
1572 Args:
1573 autotest_dir: autotest folder
1574
1575 Returns:
1576 list of paths to control files
1577 """
1578 # Where to find control files. Relative to autotest_dir.
1579 subpaths = [
1580 'server/site_tests',
1581 'client/site_tests',
1582 'server/tests',
1583 'client/tests',
1584 ]
1585
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001586 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001587 result = []
1588 for subpath in subpaths:
1589 path = os.path.join(autotest_dir, subpath)
1590 for root, dirs, files in os.walk(path):
1591
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001592 for deny in denylist:
1593 if deny in dirs:
1594 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001595
1596 for filename in files:
1597 if filename == 'control' or filename.startswith('control.'):
1598 result.append(os.path.join(root, filename))
1599
1600 return result
1601
1602
1603def get_autotest_test_info(autotest_dir, test_name):
1604 """Get metadata of given test.
1605
1606 Args:
1607 autotest_dir: autotest folder
1608 test_name: test name
1609
1610 Returns:
1611 AutotestControlInfo object. None if test not found.
1612 """
1613 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001614 try:
1615 info = parse_autotest_control_file(control_file)
1616 except SyntaxError:
1617 logger.warning('%s is not parsable, ignore', control_file)
1618 continue
1619
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001620 if info.name == test_name:
1621 return info
1622 return None
1623
1624
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001625def detect_branch_level(branch):
1626 """Given a branch name of manifest-internal, detect it's branch level.
1627
1628 level1: if ChromeOS version is x.0.0
1629 level2: if ChromeOS version is x.x.0
1630 level3: if ChromeOS version is x.x.x
1631 Where x is an non-zero integer.
1632
1633 Args:
1634 branch: branch name or ref name in manifest-internal
1635
1636 Returns:
1637 An integer indicates the branch level, or zero if not detectable.
1638 """
1639 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1640 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1641 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1642
1643 if re.match(level1, branch):
1644 return 1
1645 if re.match(level2, branch):
1646 return 2
1647 if re.match(level3, branch):
1648 return 3
1649 return 0
1650
1651
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001652def get_crosland_link(old, new):
1653 """Generates crosland link between two versions.
1654
1655 Args:
1656 old: ChromeOS version
1657 new: ChromeOS version
1658
1659 Returns:
1660 A crosland url.
1661 """
1662
1663 def version_to_url_parameter(ver):
1664 if is_cros_snapshot_version(ver):
1665 return snapshot_version_split(ver)[2]
1666 return version_to_short(ver)
1667
1668 old_parameter = version_to_url_parameter(old)
1669 new_parameter = version_to_url_parameter(new)
1670 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1671
1672
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001673class ChromeOSSpecManager(codechange.SpecManager):
1674 """Repo manifest related operations.
1675
1676 This class enumerates chromeos manifest files, parses them,
1677 and sync to disk state according to them.
1678 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001679
1680 def __init__(self, config):
1681 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001682 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1683 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001684 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1685 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001686 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001687 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001688 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001689 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1690 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001691
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001692 def lookup_snapshot_manifest_revisions(self, old, new):
1693 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001694
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001695 Returns:
1696 list of (timestamp, commit_id, snapshot_id):
1697 timestamp: integer unix timestamp
1698 commit_id: a string indicates commit hash
1699 snapshot_id: a string indicates snapshot id
1700 """
1701 assert is_cros_snapshot_version(old)
1702 assert is_cros_snapshot_version(new)
1703
1704 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001705 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001706 # Try to guess the commit time of a snapshot manifest, it is usually a few
1707 # minutes different between snapshot manifest commit and image.zip
1708 # generate.
1709 try:
1710 old_timestamp = gsutil_stat_update_time(
1711 gs_path.format(board=self.config['board'], version=old)) - 86400
1712 except subprocess.CalledProcessError:
1713 old_timestamp = None
1714 try:
1715 new_timestamp = gsutil_stat_update_time(
1716 gs_path.format(board=self.config['board'], version=new)) + 86400
1717 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1718 # we can find snapshot 5982
1719 # snapshot_id <= 5982 has different commit message format, so we need
1720 # to identify its id in different ways, see below comment for more info.
1721 new_timestamp = max(new_timestamp, 1558657989 + 1)
1722 except subprocess.CalledProcessError:
1723 new_timestamp = None
1724 result = []
1725 _, _, old_snapshot_id = snapshot_version_split(old)
1726 _, _, new_snapshot_id = snapshot_version_split(new)
1727 repo = self.manifest_internal_dir
1728 path = 'snapshot.xml'
1729 branch = 'snapshot'
1730 commits = git_util.get_history(
1731 repo,
1732 path,
1733 branch,
1734 after=old_timestamp,
1735 before=new_timestamp,
1736 with_subject=True)
1737
1738 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1739 # subject, as their subjects are all `Annealing manifest snapshot.`.
1740 # So instead we count the snapshot_id manually.
1741 count = 5982
1742 # There are two snapshot_id = 2633 in commit history, ignore the former
1743 # one.
1744 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1745 # We examine the commits in reverse order as there are some testing
1746 # commits before snapshot_id=2, this method works fine after
1747 # snapshot 2, except snapshot 2633
1748 for commit in reversed(commits):
1749 msg = commit[2]
1750 if commit[1] in ignore_list:
1751 continue
1752
1753 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1754 if match:
1755 snapshot_id = match.group(1)
1756 elif 'Annealing manifest snapshot' in msg:
1757 snapshot_id = str(count)
1758 count -= 1
1759 else:
1760 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08001761 # b/151054108: snapshot version in [29288, 29439] is broken
1762 if 29288 <= int(snapshot_id) <= 29439:
1763 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001764 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1765 result.append((commit[0], commit[1], snapshot_id))
1766 # We find commits in reversed order, now reverse it again to chronological
1767 # order.
1768 return list(reversed(result))
1769
1770 def lookup_build_timestamp(self, rev):
1771 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1772 if is_cros_full_version(rev):
1773 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001774 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001775
1776 def lookup_snapshot_build_timestamp(self, rev):
1777 assert is_cros_snapshot_version(rev)
1778 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1779
1780 def lookup_release_build_timestamp(self, rev):
1781 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001782 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001783 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1784 try:
1785 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1786 'refs/heads/master', path)
1787 except ValueError:
Kuang-che Wud1b74152020-05-20 08:46:46 +08001788 raise errors.InternalError('%s does not have %s' %
1789 (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001790 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001791
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001792 def detect_float_spec_branch_level(self, spec):
1793 results = [
1794 detect_branch_level(branch) for branch in git_util.get_branches(
1795 self.manifest_dir, commit=spec.name)
1796 ]
1797 results = [x for x in results if x > 0]
1798 return min(results) if results else 0
1799
1800 def branch_between_float_specs(self, old_spec, new_spec):
1801 if old_spec.spec_type != codechange.SPEC_FLOAT:
1802 return False
1803 if new_spec.spec_type != codechange.SPEC_FLOAT:
1804 return False
1805
1806 level_old = self.detect_float_spec_branch_level(old_spec)
1807 level_new = self.detect_float_spec_branch_level(new_spec)
1808
1809 if not level_old or not level_new:
1810 logger.warning('branch level detect failed, assume master')
1811 return False
1812 return level_old != level_new
1813
Kuang-che Wud558a042020-06-06 02:11:00 +08001814 def _determine_float_branch(self, old, new, fixed_specs):
1815 # There is no revision tag in snapshot's xml. We know snapshot
1816 # builds are on master branch.
1817 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001818 if fixed_specs[0].revision:
1819 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001820 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
1821 else:
1822 old_branches = [master_refname]
1823
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001824 if fixed_specs[-1].revision:
1825 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08001826 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001827 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08001828 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001829
Kuang-che Wud558a042020-06-06 02:11:00 +08001830 common_branches = list(set(old_branches) & set(new_branches))
1831 assert common_branches, '%s and %s are not on common branches?' % (old, new)
1832
1833 if len(common_branches) == 1:
1834 return common_branches[0]
1835
1836 # There are more than one common branches, use heuristic to tie breaking.
1837 # The heuristic is simple: choice the branch with "smallest" number.
1838 # "Smaller" means the more major branch (not branched) or branched later.
1839 #
1840 # Following is the commit graph of manifest-internal repo. It shows many
1841 # interesting cases.
1842 #
1843 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
1844 # --A--+---X--------------X------B-------X-----------> master
1845 # \
1846 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
1847 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
1848 # \ \
1849 # \ E------------> stabilize-13020.67.B
1850 # \ 83/13020.55.1
1851 # F-----X--------------------> stabilize-13020.55.B
1852 #
1853 # How to read this graph:
1854 # - Time goes from left to right. Branch names are on the right side of
1855 # arrows.
1856 # - Letters A-F are manifest commits.
1857 # - Marker X means release image build at that time, the version numbers
1858 # are labeled above the X marker.
1859 # For example,
1860 # 1) 13021.0.0 release is based on manifest A, which is on all branches
1861 # shown on the graph.
1862 # We know 13021.0.0 is on master (and R84 branch later, not shown in
1863 # this graph), not on 13020* branches.
1864 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
1865 # (R83-13020.B, 13020.67.B, and 13020.55.B).
1866 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
1867 # 13020.55.B.
1868 #
1869 # There is an important property here. Every time a new branch is created,
1870 # there will always be a commit (like C, E, and F) to fix "revision" field
1871 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
1872 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
1873 # xxxxx.B branch.
1874 #
1875 # With such property, among the branches containing the given manifest
1876 # file, the branch with "smallest" number guarantees where the release is.
1877
1878 def branch_key(s):
1879 if s == master_refname:
1880 return 0, 0, 0
1881 m = re.search(r'-(\d+)\.B$', s)
1882 if m:
1883 return int(m.group(1)), 0, 0
1884 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
1885 if m:
1886 return int(m.group(1)), int(m.group(2)), 0
1887 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
1888 if m:
1889 return int(m.group(1)), int(m.group(2)), int(m.group(3))
1890
1891 logger.warning('unexpected branch name: %s', s)
1892 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
1893
1894 common_branches.sort(key=branch_key)
1895 return common_branches[0]
1896
1897 def collect_float_spec(self, old, new, fixed_specs=None):
1898 assert fixed_specs
1899 branch = self._determine_float_branch(old, new, fixed_specs)
1900 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001901
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001902 old_timestamp = self.lookup_build_timestamp(old)
1903 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001904 # snapshot time is different from commit time
1905 # usually it's a few minutes different
1906 # 30 minutes should be safe in most cases
1907 if is_cros_snapshot_version(old):
1908 old_timestamp = old_timestamp - 1800
1909 if is_cros_snapshot_version(new):
1910 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001911
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001912 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001913 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001914 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001915 parser = repo_util.ManifestParser(self.manifest_dir)
1916 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001917 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001918 result.append(
1919 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1920 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001921
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001922 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001923 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1924 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1925
1926 # case 1: if both are snapshot, return a list of snapshot
1927 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
1928 return self.collect_snapshot_specs(old, new)
1929
1930 # case 2: if both are release version
1931 # return a list of release version
1932 if is_cros_full_version(old) and is_cros_full_version(new):
1933 return self.collect_release_specs(old, new)
1934
1935 # case 3: return a list of release version and append a snapshot
1936 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001937 result = self.collect_release_specs(
1938 version_to_full(self.config['board'], old),
1939 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001940 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001941 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001942 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08001943 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001944 return result
1945
1946 def collect_snapshot_specs(self, old, new):
1947 assert is_cros_snapshot_version(old)
1948 assert is_cros_snapshot_version(new)
1949
1950 def guess_snapshot_version(board, snapshot_id, old, new):
1951 if old.endswith('-' + snapshot_id):
1952 return old
1953 if new.endswith('-' + snapshot_id):
1954 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001955 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001956 'R*-{snapshot_id}-*'.format(
1957 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001958 for line in gsutil_ls(gs_path, ignore_errors=True):
1959 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
1960 if m:
1961 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001962 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001963
1964 result = []
1965 path = 'snapshot.xml'
1966 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001967 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001968 snapshot_version = guess_snapshot_version(self.config['board'],
1969 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001970 if snapshot_version:
1971 result.append(
1972 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
1973 path))
1974 else:
1975 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001976 return result
1977
1978 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001979 assert is_cros_full_version(old)
1980 assert is_cros_full_version(new)
1981 old_milestone, old_short_version = version_split(old)
1982 new_milestone, new_short_version = version_split(new)
1983
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001984 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001985 for milestone in git_util.list_dir_from_revision(
1986 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
1987 if not milestone.isdigit():
1988 continue
1989 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
1990 continue
1991
Kuang-che Wu74768d32018-09-07 12:03:24 +08001992 files = git_util.list_dir_from_revision(
1993 self.historical_manifest_git_dir, 'refs/heads/master',
1994 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001995
1996 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001997 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001998 short_version, ext = os.path.splitext(fn)
1999 if ext != '.xml':
2000 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002001 if (util.is_version_lesseq(old_short_version, short_version) and
2002 util.is_version_lesseq(short_version, new_short_version) and
2003 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002004 rev = make_cros_full_version(milestone, short_version)
2005 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
2006 'refs/heads/master', path)
2007 result.append(
2008 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002009
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002010 def version_key_func(spec):
2011 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002012 return util.version_key_func(short_version)
2013
2014 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002015 assert result[0].name == old
2016 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002017 return result
2018
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002019 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002020 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2021 if is_cros_full_version(rev):
2022 milestone, short_version = version_split(rev)
2023 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2024 manifest = git_util.get_file_from_revision(
2025 self.historical_manifest_git_dir, 'refs/heads/master', path)
2026 else:
2027 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2028 commit_id = revisions[0][1]
2029 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2030 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002031 return manifest
2032
2033 def get_manifest_file(self, rev):
2034 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002035 manifest_name = 'manifest_%s.xml' % rev
2036 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2037 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002038 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002039
2040 # workaround for b/150572399
2041 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2042 # folder .repo instead of.repo/manifests
2043 if is_cros_version_lesseq(rev, '12931.0.0'):
2044 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2045 manifest_patch_path = os.path.join(repo_path, manifest_name)
2046 with open(manifest_patch_path, 'w') as f:
2047 f.write(self.get_manifest(rev))
2048
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002049 return manifest_name
2050
2051 def parse_spec(self, spec):
2052 parser = repo_util.ManifestParser(self.manifest_dir)
2053 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002054 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002055 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002056 with open(manifest_path) as f:
2057 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002058 root = parser.parse_single_xml(content, allow_include=False)
2059 else:
2060 root = parser.parse_xml_recursive(spec.name, spec.path)
2061
2062 spec.entries = parser.process_parsed_result(root)
2063 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002064 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002065 raise ValueError('fixed spec %r has unexpected floating entries' %
2066 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002067 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002068
2069 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002070 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002071
2072 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2073 # manifest. 'repo sync -m' is not enough
2074 repo_util.init(
2075 self.config['chromeos_root'],
2076 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2077 manifest_name=manifest_name,
2078 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002079 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002080 # b/150753074: moblab is in non-default group and causes mark_as_stable
2081 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002082 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002083 )
2084
2085 # Note, don't sync with current_branch=True for chromeos. One of its
2086 # build steps (inside mark_as_stable) executes "git describe" which
2087 # needs git tag information.
2088 repo_util.sync(self.config['chromeos_root'])