blob: 5b4ba18135d1775163f7052993b9736c2861094c [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
24import subprocess
25import time
26
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080027from google.protobuf import json_format
28
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080029from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080030from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080031from bisect_kit import codechange
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080032from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080033from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080034from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080035from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080036from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080037from bisect_kit import util
38
39logger = logging.getLogger(__name__)
40
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080041re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080042re_chromeos_localbuild_version = r'^\d+\.\d+\.\d{4}_\d\d_\d\d_\d{4}$'
43re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080044re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080045
46gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
47gs_release_path = (
Kuang-che Wu80bf6a52019-05-31 12:48:06 +080048 'gs://chromeos-releases/{channel}-channel/{boardpath}/{short_version}')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080049
50# Assume gsutil is in PATH.
51gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080052
53# Since snapshots with version >= 12618.0.0 have android and chrome version
54# info.
55snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080056
Kuang-che Wub9705bd2018-06-28 17:59:18 +080057chromeos_root_inside_chroot = '/mnt/host/source'
58# relative to chromeos_root
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080059prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu28980b22019-07-31 19:51:45 +080060# Relative to chromeos root. Images are cached_images_dir/$board/$image_name.
61cached_images_dir = 'src/build/images'
62test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wub9705bd2018-06-28 17:59:18 +080063
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080064VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
65VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
66VERSION_KEY_MILESTONE = 'milestone'
67VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080068VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080069VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +080070CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080071
72
Kuang-che Wu9890ce82018-07-07 15:14:10 +080073class NeedRecreateChrootException(Exception):
74 """Failed to build ChromeOS because of chroot mismatch or corruption"""
75
76
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080077def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080078 """Determines if `s` is chromeos short version.
79
80 This function doesn't accept version number of local build.
81 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080082 return bool(re.match(re_chromeos_short_version, s))
83
84
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080085def is_cros_localbuild_version(s):
86 """Determines if `s` is chromeos local build version."""
87 return bool(re.match(re_chromeos_localbuild_version, s))
88
89
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080090def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080091 """Determines if `s` is chromeos full version.
92
93 This function doesn't accept version number of local build.
94 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080095 return bool(re.match(re_chromeos_full_version, s))
96
97
98def is_cros_version(s):
99 """Determines if `s` is chromeos version (either short or full)"""
100 return is_cros_short_version(s) or is_cros_full_version(s)
101
102
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800103def is_cros_snapshot_version(s):
104 """Determines if `s` is chromeos snapshot version"""
105 return bool(re.match(re_chromeos_snapshot_version, s))
106
107
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800108def make_cros_full_version(milestone, short_version):
109 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800110 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800111 return 'R%s-%s' % (milestone, short_version)
112
113
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800114def make_cros_snapshot_version(milestone, short_version, snapshot_id):
115 """Makes snapshot version from milestone, short_version and snapshot id"""
116 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
117
118
119def version_split(version):
120 """Splits full_version or snapshot_version into milestone and short_version"""
121 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
122 if is_cros_snapshot_version(version):
123 return snapshot_version_split(version)[0:2]
124 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800125 return milestone[1:], short_version
126
127
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800128def snapshot_version_split(snapshot_version):
129 """Splits snapshot_version into milestone, short_version and snapshot_id"""
130 assert is_cros_snapshot_version(snapshot_version)
131 milestone, shot_version, snapshot_id = snapshot_version.split('-')
132 return milestone[1:], shot_version, snapshot_id
133
134
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800135def query_snapshot_buildbucket_id(board, snapshot_version):
136 """Query buildbucket id of a snapshot"""
137 assert is_cros_snapshot_version(snapshot_version)
138 path = ('gs://chromeos-image-archive/{board}-postsubmit'
139 '/{snapshot_version}-*/image.zip')
140 output = gsutil_ls(
141 '-d',
142 path.format(board=board, snapshot_version=snapshot_version),
143 ignore_errors=True)
144 for line in output:
145 m = re.match(r'.*-postsubmit/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
146 if m:
147 return m.group(1)
148 return None
149
150
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800151def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800152 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800153 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800154 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 +0800155 return s
156
157
158def query_dut_lsb_release(host):
159 """Query /etc/lsb-release of given DUT
160
161 Args:
162 host: the DUT address
163
164 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800165 dict for keys and values of /etc/lsb-release.
166
167 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800168 errors.SshConnectionError: cannot connect to host
169 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800170 """
171 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800172 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800173 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800174 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800175 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
176
177
178def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800179 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800180
181 Args:
182 host: the DUT address
183
184 Returns:
185 True if the host is a chromeos device.
186 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800187 try:
188 return query_dut_lsb_release(host).get('DEVICETYPE') in [
189 'CHROMEBASE',
190 'CHROMEBIT',
191 'CHROMEBOOK',
192 'CHROMEBOX',
193 'REFERENCE',
194 ]
195 except (errors.ExternalError, errors.SshConnectionError):
196 return False
197
198
199def is_good_dut(host):
200 if not is_dut(host):
201 return False
202
203 # Sometimes python is broken after 'cros flash'.
204 try:
205 util.ssh_cmd(host, 'python', '-c', '1')
206 return True
207 except (subprocess.CalledProcessError, errors.SshConnectionError):
208 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800209
210
211def query_dut_board(host):
212 """Query board name of a given DUT"""
213 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
214
215
216def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800217 """Query short version of a given DUT.
218
219 This function may return version of local build, which
220 is_cros_short_version() is false.
221 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800222 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
223
224
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800225def query_dut_is_snapshot(host):
226 """Query if given DUT is a snapshot version."""
227 path = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILDER_PATH', '')
228 return '-postsubmit' in path
229
230
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800231def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800232 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800233
234 Args:
235 host: DUT address
236 connect_timeout: connection timeout
237
238 Returns:
239 boot uuid
240 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800241 return util.ssh_cmd(
242 host,
243 'cat',
244 '/proc/sys/kernel/random/boot_id',
245 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800246
247
248def reboot(host):
249 """Reboot a DUT and verify"""
250 logger.debug('reboot %s', host)
251 boot_id = query_dut_boot_id(host)
252
Kuang-che Wu44278142019-03-04 11:33:57 +0800253 try:
254 util.ssh_cmd(host, 'reboot')
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800255 except errors.SshConnectionError:
256 # Depends on timing, ssh may return failure due to broken pipe, which is
257 # working as intended. Ignore such kind of errors.
Kuang-che Wu44278142019-03-04 11:33:57 +0800258 pass
Kuang-che Wu708310b2018-03-28 17:24:34 +0800259 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800260
Kuang-che Wu708310b2018-03-28 17:24:34 +0800261
262def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800263 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800264 # (dev screen short delay) or more (long delay).
265 time.sleep(15)
266 for _ in range(100):
267 try:
268 # During boot, DUT does not response and thus ssh may hang a while. So
269 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
270 # set tight limit because it's inside retry loop.
271 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
272 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800273 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800274 logger.debug('reboot not ready? sleep wait 1 sec')
275 time.sleep(1)
276
Kuang-che Wue121fae2018-11-09 16:18:39 +0800277 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800278
279
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800280def gs_release_boardpath(board):
281 """Normalizes board name for gs://chromeos-releases/
282
283 This follows behavior of PushImage() in chromite/scripts/pushimage.py
284 Note, only gs://chromeos-releases/ needs normalization,
285 gs://chromeos-image-archive does not.
286
287 Args:
288 board: ChromeOS board name
289
290 Returns:
291 normalized board name
292 """
293 return board.replace('_', '-')
294
295
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800296def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800297 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800298
299 Args:
300 args: command line arguments passed to gsutil
301 kwargs:
302 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
303 but the path not found.
304
305 Returns:
306 stdout of gsutil
307
308 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800309 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800310 subprocess.CalledProcessError: command failed
311 """
312 stderr_lines = []
313 try:
314 return util.check_output(
315 gsutil_bin, *args, stderr_callback=stderr_lines.append)
316 except subprocess.CalledProcessError as e:
317 stderr = ''.join(stderr_lines)
318 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800319 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800320 'gsutil failed due to permission. ' +
321 'Run "%s config" and follow its instruction. ' % gsutil_bin +
322 'Fill any string if it asks for project-id')
323 if kwargs.get('ignore_errors'):
324 return ''
325 raise
326 except OSError as e:
327 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800328 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800329 'Unable to run %s. gsutil is not installed or not in PATH?' %
330 gsutil_bin)
331 raise
332
333
334def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800335 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800336
337 Args:
338 args: arguments passed to 'gsutil ls'
339 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800340 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800341 exception, ex. path not found.
342
343 Returns:
344 list of 'gsutil ls' result. One element for one line of gsutil output.
345
346 Raises:
347 subprocess.CalledProcessError: gsutil failed, usually means path not found
348 """
349 return gsutil('ls', *args, **kwargs).splitlines()
350
351
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800352def gsutil_stat_update_time(*args, **kwargs):
353 """Returns the last modified time of a file or multiple files.
354
355 Args:
356 args: arguments passed to 'gsutil stat'.
357 kwargs: extra parameters for gsutil.
358
359 Returns:
360 A integer indicates the last modified timestamp.
361
362 Raises:
363 subprocess.CalledProcessError: gsutil failed, usually means path not found
364 errors.ExternalError: update time is not found
365 """
366 result = -1
367 # Currently we believe stat always returns a UTC time, and strptime also
368 # parses a UTC time by default.
369 time_format = '%a, %d %b %Y %H:%M:%S GMT'
370
371 for line in gsutil('stat', *args, **kwargs).splitlines():
372 if ':' not in line:
373 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800374 key, value = line.split(':', 1)
375 key, value = key.strip(), value.strip()
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800376 if key != 'Update time':
377 continue
378 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800379 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800380 result = max(result, unixtime)
381
382 if result == -1:
383 raise errors.ExternalError("didn't find update time")
384 return result
385
386
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800387def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800388 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800389
390 Args:
391 board: ChromeOS board name
392 short_version: ChromeOS version number in short format, ex. 9300.0.0
393
394 Returns:
395 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
396 None if failed.
397 """
398 path = gs_archive_path.format(board=board) + '/R*-' + short_version
399 for line in gsutil_ls('-d', path, ignore_errors=True):
400 m = re.search(r'/R(\d+)-', line)
401 if not m:
402 continue
403 return m.group(1)
404
405 for channel in ['canary', 'dev', 'beta', 'stable']:
406 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800407 channel=channel,
408 boardpath=gs_release_boardpath(board),
409 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800410 for line in gsutil_ls(path, ignore_errors=True):
411 m = re.search(r'\bR(\d+)-' + short_version, line)
412 if not m:
413 continue
414 return m.group(1)
415
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800416 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800417 return None
418
419
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800420def list_board_names(chromeos_root):
421 """List board names.
422
423 Args:
424 chromeos_root: chromeos tree root
425
426 Returns:
427 list of board names
428 """
429 # Following logic is simplified from chromite/lib/portage_util.py
430 cros_list_overlays = os.path.join(chromeos_root,
431 'chromite/bin/cros_list_overlays')
432 overlays = util.check_output(cros_list_overlays).splitlines()
433 result = set()
434 for overlay in overlays:
435 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
436 name = None
437 if os.path.exists(conf_file):
438 for line in open(conf_file):
439 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
440 if m:
441 name = m.group(1)
442 break
443
444 if not name:
445 name_file = os.path.join(overlay, 'profiles', 'repo_name')
446 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800447 with open(name_file) as f:
448 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800449
450 if name:
451 name = re.sub(r'-private$', '', name)
452 result.add(name)
453
454 return list(result)
455
456
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800457def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800458 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800459
460 Args:
461 board: ChromeOS board name
462 version: ChromeOS version number in short or full format
463
464 Returns:
465 (milestone, version in short format)
466 """
467 if is_cros_short_version(version):
468 milestone = query_milestone_by_version(board, version)
469 short_version = version
470 else:
471 milestone, short_version = version_split(version)
472 return milestone, short_version
473
474
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800475def extract_major_version(version):
476 """Converts a version to its major version.
477
478 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800479 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800480
481 Returns:
482 major version number in string format
483 """
484 version = version_to_short(version)
485 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
486 return m.group(1)
487
488
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800489def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800490 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800491
492 Args:
493 version: ChromeOS version number in short or full format
494
495 Returns:
496 version number in short format
497 """
498 if is_cros_short_version(version):
499 return version
500 _, short_version = version_split(version)
501 return short_version
502
503
504def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800505 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800506
507 Args:
508 board: ChromeOS board name
509 version: ChromeOS version number in short or full format
510
511 Returns:
512 version number in full format
513 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800514 if is_cros_snapshot_version(version):
515 milestone, short_version, _ = snapshot_version_split(version)
516 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800517 if is_cros_full_version(version):
518 return version
519 milestone = query_milestone_by_version(board, version)
Kuang-che Wu0205f052019-05-23 12:48:37 +0800520 assert milestone, 'incorrect board=%s or version=%s ?' % (board, version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800521 return make_cros_full_version(milestone, version)
522
523
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800524def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800525 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800526
527 Args:
528 board: ChromeOS board
529 major_version: ChromeOS major version
530
531 Returns:
532 list of (version, gs_path):
533 version: Chrome OS snapshot version
534 gs_path: gs path of test image
535 """
536
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800537 def extract_snapshot_id(result):
538 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
539 assert m
540 return int(m.group(1))
541
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800542 short_version = '%s.0.0' % major_version
543 milestone = query_milestone_by_version(board, short_version)
544 if not milestone:
545 milestone = '*'
546
547 path = ('gs://chromeos-image-archive/{board}-postsubmit/R{milestone}-'
548 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800549 result = []
550 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800551 path.format(
552 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800553 ignore_errors=True)
554
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800555 for gs_path in sorted(output):
556 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800557 if m:
558 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800559 # we should skip if there is duplicate snapshot
560 if result and result[-1][0] == snapshot_version:
561 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800562 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800563
564 # sort by its snapshot_id
565 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800566 return result
567
568
Kuang-che Wu575dc442019-03-05 10:30:55 +0800569def list_prebuilt_from_image_archive(board):
570 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
571
572 gs://chromeos-image-archive contains only recent builds (in two years).
573 We prefer this function to list_prebuilt_from_chromeos_releases() because
574 - this is what "cros flash" supports directly.
575 - the paths have milestone information, so we don't need to do slow query
576 by ourselves.
577
578 Args:
579 board: ChromeOS board name
580
581 Returns:
582 list of (version, gs_path):
583 version: Chrome OS version in full format
584 gs_path: gs path of test image
585 """
586 result = []
587 for line in gsutil_ls(gs_archive_path.format(board=board)):
588 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
589 if m:
590 full_version = m.group(1)
591 test_image = 'chromiumos_test_image.tar.xz'
592 assert line.endswith('/')
593 gs_path = line + test_image
594 result.append((full_version, gs_path))
595 return result
596
597
598def list_prebuilt_from_chromeos_releases(board):
599 """Lists ChromeOS versions available from gs://chromeos-releases.
600
601 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
602 support it.
603
604 Args:
605 board: ChromeOS board name
606
607 Returns:
608 list of (version, gs_path):
609 version: Chrome OS version in short format
610 gs_path: gs path of test image (with wildcard)
611 """
612 result = []
613 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800614 gs_release_path.format(
615 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800616 ignore_errors=True):
617 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
618 if m:
619 short_version = m.group(1)
620 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
621 short_version=short_version, board=board)
622 gs_path = line + test_image
623 result.append((short_version, gs_path))
624 return result
625
626
Kuang-che Wue1808402020-01-06 20:27:45 +0800627def has_test_image(board, version):
628 if is_cros_snapshot_version(version):
629 return bool(query_snapshot_buildbucket_id(board, version))
630
631 full_version = version_to_full(board, version)
632 short_version = version_to_short(version)
633 paths = [
634 gs_archive_path.format(board=board) +
635 '/%s/chromiumos_test_image.tar.xz' % full_version,
636 gs_release_path.format(
637 channel='*',
638 boardpath=gs_release_boardpath(board),
639 short_version=short_version),
640 ]
641
642 for path in paths:
643 if gsutil_ls(path, ignore_errors=True):
644 return True
645 return False
646
647
Kuang-che Wu575dc442019-03-05 10:30:55 +0800648def list_chromeos_prebuilt_versions(board,
649 old,
650 new,
651 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800652 include_older_build=True,
653 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800654 """Lists ChromeOS version numbers with prebuilt between given range
655
656 Args:
657 board: ChromeOS board name
658 old: start version (inclusive)
659 new: end version (inclusive)
660 only_good_build: only if test image is available
661 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800662 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800663
664 Returns:
665 list of sorted version numbers (in full format) between [old, new] range
666 (inclusive).
667 """
668 old = version_to_short(old)
669 new = version_to_short(new)
670
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800671 rev_map = {
672 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800673 for full_version, gs_path in list_prebuilt_from_image_archive(board):
674 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800675 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800676
677 if include_older_build and old not in rev_map:
678 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
679 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800680 rev_map[short_version] = [(short_version, gs_path)]
681
682 if use_snapshot:
683 for major_version in range(
684 int(extract_major_version(old)),
685 int(extract_major_version(new)) + 1):
686 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800687 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800688 # If current version is smaller than cutover, ignore it as it might not
689 # contain enough information for continuing android and chrome bisection.
690 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
691 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800692
693 # Given the fact that snapshots are images between two release versions.
694 # Adding snapshots of 12345.0.0 should be treated as adding commits
695 # between [12345.0.0, 12346.0.0).
696 # So in the following lines we check two facts:
697 # 1) If 12346.0.0(next_short_version) is a version between old and new
698 if not util.is_direct_relative_version(next_short_version, old):
699 continue
700 if not util.is_direct_relative_version(next_short_version, new):
701 continue
702 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800703 if not util.is_direct_relative_version(short_version, old):
704 continue
705 if not util.is_direct_relative_version(short_version, new):
706 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800707
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800708 snapshots = list_snapshots_from_image_archive(board, str(major_version))
709 if snapshots:
710 # if snapshots found, we can append them after the release version,
711 # so the prebuilt image list of this version will be
712 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800713 if short_version not in rev_map:
714 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800715 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800716
717 result = []
718 for rev in sorted(rev_map, key=util.version_key_func):
719 if not util.is_direct_relative_version(new, rev):
720 continue
721 if not util.is_version_lesseq(old, rev):
722 continue
723 if not util.is_version_lesseq(rev, new):
724 continue
725
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800726 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800727
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800728 # version_to_full() and gsutil_ls() may take long time if versions are a
729 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800730
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800731 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800732 gs_result = gsutil_ls(gs_path, ignore_errors=True)
733 if not gs_result:
734 logger.warning('%s is not a good build, ignore', version)
735 continue
736 assert len(gs_result) == 1
737 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
738 if not m:
739 logger.warning('format of image path is unexpected: %s', gs_result[0])
740 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800741 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800742 elif is_cros_short_version(version):
743 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800744
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800745 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800746
747 return result
748
749
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800750def prepare_snapshot_image(chromeos_root, board, snapshot_version):
751 """Prepare chromeos snapshot image.
752
753 Args:
754 chromeos_root: chromeos tree root
755 board: ChromeOS board name
756 snapshot_version: ChromeOS snapshot version number
757
758 Returns:
759 local file path of test image relative to chromeos_root
760 """
761 assert is_cros_snapshot_version(snapshot_version)
762 milestone, short_version, snapshot_id = snapshot_version_split(
763 snapshot_version)
764 full_version = make_cros_full_version(milestone, short_version)
765 tmp_dir = os.path.join(
766 chromeos_root, 'tmp',
767 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
768 if not os.path.exists(tmp_dir):
769 os.makedirs(tmp_dir)
770
771 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/' +
772 '{snapshot_version}-*/image.zip')
773 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
774
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800775 full_path = os.path.join(tmp_dir, test_image_filename)
776 rel_path = os.path.relpath(full_path, chromeos_root)
777 if os.path.exists(full_path):
778 return rel_path
779
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800780 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800781 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800782 gs_path = files[0]
783 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800784 util.check_call(
785 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
786 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800787 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800788
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800789 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800790 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800791
792
Kuang-che Wu28980b22019-07-31 19:51:45 +0800793def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800794 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800795
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800796 It searches for xbuddy image which "cros flash" can use, or fetch image to
797 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800798
799 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800800 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800801 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800802 version: ChromeOS version number in short or full format
803
804 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800805 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800806 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800807 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800808 full_version = version_to_full(board, version)
809 short_version = version_to_short(full_version)
810
811 image_path = None
812 gs_path = gs_archive_path.format(board=board) + '/' + full_version
813 if gsutil_ls('-d', gs_path, ignore_errors=True):
814 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
815 board=board, full_version=full_version)
816 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800817 tmp_dir = os.path.join(chromeos_root, 'tmp',
818 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800819 full_path = os.path.join(tmp_dir, test_image_filename)
820 rel_path = os.path.relpath(full_path, chromeos_root)
821 if os.path.exists(full_path):
822 return rel_path
823
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800824 if not os.path.exists(tmp_dir):
825 os.makedirs(tmp_dir)
826 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800827 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800828 # to fetch the image by ourselves
829 for channel in ['canary', 'dev', 'beta', 'stable']:
830 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
831 full_version=full_version, board=board)
832 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800833 channel=channel,
834 boardpath=gs_release_boardpath(board),
835 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800836 gs_path += '/' + fn
837 if gsutil_ls(gs_path, ignore_errors=True):
838 # TODO(kcwu): delete tmp
839 gsutil('cp', gs_path, tmp_dir)
840 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800841 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800842 break
843
844 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800845 return image_path
846
847
848def cros_flash(chromeos_root,
849 host,
850 board,
851 image_path,
852 version=None,
853 clobber_stateful=False,
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800854 disable_rootfs_verification=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800855 """Flash a DUT with given ChromeOS image.
856
857 This is implemented by 'cros flash' command line.
858
859 Args:
860 chromeos_root: use 'cros flash' of which chromeos tree
861 host: DUT address
862 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800863 image_path: chromeos image xbuddy path or file path. For relative
864 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800865 version: ChromeOS version in short or full format
866 clobber_stateful: Clobber stateful partition when performing update
867 disable_rootfs_verification: Disable rootfs verification after update
868 is completed
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800869
870 Raises:
871 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800872 """
873 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
874
875 # Reboot is necessary because sometimes previous 'cros flash' failed and
876 # entered a bad state.
877 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800878
Kuang-che Wu28980b22019-07-31 19:51:45 +0800879 # Handle relative path.
880 if '://' not in image_path and not os.path.isabs(image_path):
881 assert os.path.exists(os.path.join(chromeos_root, image_path))
882 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
883
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800884 args = [
885 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
886 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800887 if clobber_stateful:
888 args.append('--clobber-stateful')
889 if disable_rootfs_verification:
890 args.append('--disable-rootfs-verification')
891
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800892 try:
893 cros_sdk(chromeos_root, 'cros', 'flash', *args)
894 except subprocess.CalledProcessError:
895 raise errors.ExternalError('cros flash failed')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800896
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800897 if version:
898 # In the past, cros flash may fail with returncode=0
899 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800900 if is_cros_snapshot_version(version):
901 builder_path = query_dut_lsb_release(host).get(
902 'CHROMEOS_RELEASE_BUILDER_PATH', '')
903 expect_prefix = '%s-postsubmit/%s-' % (board, version)
904 if not builder_path.startswith(expect_prefix):
905 raise errors.ExternalError(
906 'although cros flash succeeded, the OS builder path is '
907 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
908 else:
909 expect_version = version_to_short(version)
910 dut_version = query_dut_short_version(host)
911 if dut_version != expect_version:
912 raise errors.ExternalError(
913 'although cros flash succeeded, the OS version is unexpected: '
914 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800915
Kuang-che Wu4a81ea72019-10-05 15:35:17 +0800916 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800917 # (b/130786578), so it's necessary to do sanity check.
918 if not is_good_dut(host):
919 raise errors.ExternalError(
920 'although cros flash succeeded, the DUT is in bad state')
921
922
923def cros_flash_with_retry(chromeos_root,
924 host,
925 board,
926 image_path,
927 version=None,
928 clobber_stateful=False,
929 disable_rootfs_verification=True,
930 repair_callback=None):
931 # 'cros flash' is not 100% reliable, retry if necessary.
932 for attempt in range(2):
933 if attempt > 0:
934 logger.info('will retry 60 seconds later')
935 time.sleep(60)
936
937 try:
938 cros_flash(
939 chromeos_root,
940 host,
941 board,
942 image_path,
943 version=version,
944 clobber_stateful=clobber_stateful,
945 disable_rootfs_verification=disable_rootfs_verification)
946 return True
947 except errors.ExternalError:
948 logger.exception('cros flash failed')
949 if repair_callback and not repair_callback(host):
950 logger.warning('not repaired, assume it is harmless')
951 continue
952 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800953
954
955def version_info(board, version):
956 """Query subcomponents version info of given version of ChromeOS
957
958 Args:
959 board: ChromeOS board name
960 version: ChromeOS version number in short or full format
961
962 Returns:
963 dict of component and version info, including (if available):
964 cros_short_version: ChromeOS version
965 cros_full_version: ChromeOS version
966 milestone: milestone of ChromeOS
967 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800968 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800969 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
970 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800971 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800972 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800973 milestone, short_version, _ = snapshot_version_split(version)
974 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800975 data = api.get(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +0800976 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800977 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800978 VERSION_KEY_MILESTONE: milestone,
979 VERSION_KEY_CROS_FULL_VERSION: version,
980 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +0800981 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
982 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
983 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800984 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800985 info = {}
986 full_version = version_to_full(board, version)
987
988 # Some boards may have only partial-metadata.json but no metadata.json.
989 # e.g. caroline R60-9462.0.0
990 # Let's try both.
991 metadata = None
992 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +0800993 path = gs_archive_path.format(
994 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800995 metadata = gsutil('cat', path, ignore_errors=True)
996 if metadata:
997 o = json.loads(metadata)
998 v = o['version']
999 board_metadata = o['board-metadata'][board]
1000 info.update({
1001 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1002 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1003 VERSION_KEY_MILESTONE: v['milestone'],
1004 VERSION_KEY_CR_VERSION: v['chrome'],
1005 })
1006
1007 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001008 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001009 if 'android-branch' in v: # this appears since R58-9317.0.0
1010 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1011 elif 'android-container-branch' in board_metadata:
1012 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1013 break
1014 else:
1015 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1016 logger.error(
1017 'Note, so far no quick way to look up version info for too old builds')
1018
1019 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001020
1021
1022def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001023 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001024
1025 Args:
1026 board: ChromeOS board name
1027 version: ChromeOS version number in short or full format
1028
1029 Returns:
1030 Chrome version number
1031 """
1032 info = version_info(board, version)
1033 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001034
1035
1036def query_android_build_id(board, rev):
1037 info = version_info(board, rev)
1038 rev = info['android_build_id']
1039 return rev
1040
1041
1042def query_android_branch(board, rev):
1043 info = version_info(board, rev)
1044 rev = info['android_branch']
1045 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001046
1047
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001048def guess_chrome_version(board, rev):
1049 """Guess chrome version number.
1050
1051 Args:
1052 board: chromeos board name
1053 rev: chrome or chromeos version
1054
1055 Returns:
1056 chrome version number
1057 """
1058 if is_cros_version(rev):
1059 assert board, 'need to specify BOARD for cros version'
1060 rev = query_chrome_version(board, rev)
1061 assert cr_util.is_chrome_version(rev)
1062
1063 return rev
1064
1065
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001066def is_inside_chroot():
1067 """Returns True if we are inside chroot."""
1068 return os.path.exists('/etc/cros_chroot_version')
1069
1070
1071def cros_sdk(chromeos_root, *args, **kwargs):
1072 """Run commands inside chromeos chroot.
1073
1074 Args:
1075 chromeos_root: chromeos tree root
1076 *args: command to run
1077 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001078 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001079 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001080 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001081 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001082 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001083 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001084 """
1085 envs = []
1086 for k, v in kwargs.get('env', {}).items():
1087 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1088 envs.append('%s=%s' % (k, v))
1089
1090 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1091 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001092 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001093
1094 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001095 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001096 if kwargs.get('goma_dir'):
1097 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001098
Kuang-che Wu399d4662019-06-06 15:23:37 +08001099 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001100
Kuang-che Wu399d4662019-06-06 15:23:37 +08001101 # In addition to the output of command we are interested, cros_sdk may
1102 # generate its own messages. For example, chroot creation messages if we run
1103 # cros_sdk the first time.
1104 # This is the hack to run dummy command once, so we can get clean output for
1105 # the command we are interested.
1106 cmd = prefix + ['true']
1107 util.check_call(*cmd, cwd=chromeos_root)
1108
1109 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001110 return util.check_output(
1111 *cmd,
1112 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001113 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001114 stdin=kwargs.get('stdin'),
1115 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001116
1117
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001118def create_chroot(chromeos_root):
1119 """Creates ChromeOS chroot.
1120
1121 Args:
1122 chromeos_root: chromeos tree root
1123 """
1124 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1125 return
1126 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1127 return
1128
1129 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1130
1131
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001132def copy_into_chroot(chromeos_root, src, dst):
1133 """Copies file into chromeos chroot.
1134
1135 Args:
1136 chromeos_root: chromeos tree root
1137 src: path outside chroot
1138 dst: path inside chroot
1139 """
1140 # chroot may be an image, so we cannot copy to corresponding path
1141 # directly.
1142 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
1143
1144
1145def exists_in_chroot(chromeos_root, path):
1146 """Determine whether a path exists in the chroot.
1147
1148 Args:
1149 chromeos_root: chromeos tree root
1150 path: path inside chroot, relative to src/scripts
1151
1152 Returns:
1153 True if a path exists
1154 """
1155 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +08001156 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001157 except subprocess.CalledProcessError:
1158 return False
1159 return True
1160
1161
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001162def check_if_need_recreate_chroot(stdout, stderr):
1163 """Analyze build log and determine if chroot should be recreated.
1164
1165 Args:
1166 stdout: stdout output of build
1167 stderr: stderr output of build
1168
1169 Returns:
1170 the reason if chroot needs recreated; None otherwise
1171 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001172 if re.search(
1173 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001174 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001175 return 'EAPI version mismatch'
1176
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001177 if 'Chroot is too new. Consider running:' in stderr:
1178 return 'chroot version is too new'
1179
1180 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001181 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1182 return 'chroot version is too new'
1183
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001184 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1185 if "undefined reference to 'std::__1::basic_string" in stdout:
1186 return 'might be due to compiler change'
1187
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001188 # Detect failures due to file collisions.
1189 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1190 # and conflict with each other. Other possible cases are package renaming or
1191 # refactoring. Let's recreate chroot to work around them.
1192 if 'Detected file collision' in stdout:
1193 # Using wildcard between words because the text wraps to the next line
1194 # depending on length of package name and each line is prefixed with
1195 # package name.
1196 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1197 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1198 # package name (65 now).
1199 m = re.search(
1200 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1201 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1202 if m:
1203 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001204
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001205 return None
1206
1207
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001208def build_packages(chromeos_root,
1209 board,
1210 chrome_root=None,
1211 goma_dir=None,
1212 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001213 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001214
1215 Args:
1216 chromeos_root: chromeos tree root
1217 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001218 chrome_root: Chrome tree root. If specified, build chrome using the
1219 provided tree
1220 goma_dir: Goma installed directory to mount into the chroot. If specified,
1221 build chrome with goma.
1222 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001223 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001224
1225 def has_build_package_argument(argument):
1226 stderr_lines = []
1227 try:
1228 util.check_call(
1229 'src/scripts/build_packages',
1230 '--help',
1231 cwd=chromeos_root,
1232 stderr_callback=stderr_lines.append)
1233 except subprocess.CalledProcessError:
1234 help_output = ''.join(stderr_lines)
1235 return '--[no]%s' % argument in help_output
1236
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001237 common_env = {
1238 'USE': '-cros-debug chrome_internal',
1239 'FEATURES': 'separatedebug',
1240 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001241 stderr_lines = []
1242 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001243 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001244 env = common_env.copy()
1245 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001246 cros_sdk(
1247 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001248 './update_chroot',
1249 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001250 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001251 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001252 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001253
1254 env = common_env.copy()
1255 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001256 './build_packages',
1257 '--board',
1258 board,
1259 '--withdev',
1260 '--noworkon',
1261 '--skip_chroot_upgrade',
1262 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001263 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001264
1265 # `use_any_chrome` flag is default on and will force to use a chrome
1266 # prebuilt even if the version doesn't match.
1267
1268 # As this argument is landed in 12681, we should check if the argument
1269 # exists before adding this.
1270 if has_build_package_argument('use_any_chrome'):
1271 cmd.append('--nouse_any_chrome')
1272
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001273 if goma_dir:
1274 # Tell build_packages to start and stop goma
1275 cmd.append('--run_goma')
1276 env['USE_GOMA'] = 'true'
1277 if afdo_use:
1278 env['USE'] += ' afdo_use'
1279 cros_sdk(
1280 chromeos_root,
1281 *cmd,
1282 env=env,
1283 chrome_root=chrome_root,
1284 stderr_callback=stderr_lines.append,
1285 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001286 except subprocess.CalledProcessError as e:
1287 # Detect failures due to incompatibility between chroot and source tree. If
1288 # so, notify the caller to recreate chroot and retry.
1289 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1290 if reason:
1291 raise NeedRecreateChrootException(reason)
1292
1293 # For other failures, don't know how to handle. Just bail out.
1294 raise
1295
Kuang-che Wu28980b22019-07-31 19:51:45 +08001296
1297def build_image(chromeos_root, board):
1298 """Build ChromeOS image.
1299
1300 Args:
1301 chromeos_root: chromeos tree root
1302 board: ChromeOS board name
1303
1304 Returns:
1305 image folder; relative to chromeos_root
1306 """
1307 stderr_lines = []
1308 try:
1309 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1310 cros_sdk(
1311 chromeos_root,
1312 './build_image',
1313 '--board',
1314 board,
1315 '--noenable_rootfs_verification',
1316 'test',
1317 env={
1318 'USE': '-cros-debug chrome_internal',
1319 'FEATURES': 'separatedebug',
1320 },
1321 stderr_callback=stderr_lines.append)
1322 except subprocess.CalledProcessError as e:
1323 # Detect failures due to incompatibility between chroot and source tree. If
1324 # so, notify the caller to recreate chroot and retry.
1325 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1326 if reason:
1327 raise NeedRecreateChrootException(reason)
1328
1329 # For other failures, don't know how to handle. Just bail out.
1330 raise
1331
1332 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1333 'latest')
1334 assert os.path.exists(image_symlink)
1335 image_name = os.readlink(image_symlink)
1336 image_folder = os.path.join(cached_images_dir, board, image_name)
1337 assert os.path.exists(
1338 os.path.join(chromeos_root, image_folder, test_image_filename))
1339 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001340
1341
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001342class AutotestControlInfo(object):
1343 """Parsed content of autotest control file.
1344
1345 Attributes:
1346 name: test name
1347 path: control file path
1348 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1349 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1350 """
1351
1352 def __init__(self, path, variables):
1353 self.name = variables['NAME']
1354 self.path = path
1355 self.variables = variables
1356
1357
1358def parse_autotest_control_file(path):
1359 """Parses autotest control file.
1360
1361 This only parses simple top-level string assignments.
1362
1363 Returns:
1364 AutotestControlInfo object
1365 """
1366 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001367 with open(path) as f:
1368 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001369 for stmt in code.body:
1370 # Skip if not simple "NAME = *" assignment.
1371 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1372 isinstance(stmt.targets[0], ast.Name)):
1373 continue
1374
1375 # Only support string value.
1376 if isinstance(stmt.value, ast.Str):
1377 variables[stmt.targets[0].id] = stmt.value.s
1378
1379 return AutotestControlInfo(path, variables)
1380
1381
1382def enumerate_autotest_control_files(autotest_dir):
1383 """Enumerate autotest control files.
1384
1385 Args:
1386 autotest_dir: autotest folder
1387
1388 Returns:
1389 list of paths to control files
1390 """
1391 # Where to find control files. Relative to autotest_dir.
1392 subpaths = [
1393 'server/site_tests',
1394 'client/site_tests',
1395 'server/tests',
1396 'client/tests',
1397 ]
1398
1399 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
1400 result = []
1401 for subpath in subpaths:
1402 path = os.path.join(autotest_dir, subpath)
1403 for root, dirs, files in os.walk(path):
1404
1405 for black in blacklist:
1406 if black in dirs:
1407 dirs.remove(black)
1408
1409 for filename in files:
1410 if filename == 'control' or filename.startswith('control.'):
1411 result.append(os.path.join(root, filename))
1412
1413 return result
1414
1415
1416def get_autotest_test_info(autotest_dir, test_name):
1417 """Get metadata of given test.
1418
1419 Args:
1420 autotest_dir: autotest folder
1421 test_name: test name
1422
1423 Returns:
1424 AutotestControlInfo object. None if test not found.
1425 """
1426 for control_file in enumerate_autotest_control_files(autotest_dir):
1427 info = parse_autotest_control_file(control_file)
1428 if info.name == test_name:
1429 return info
1430 return None
1431
1432
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001433def detect_branch_level(branch):
1434 """Given a branch name of manifest-internal, detect it's branch level.
1435
1436 level1: if ChromeOS version is x.0.0
1437 level2: if ChromeOS version is x.x.0
1438 level3: if ChromeOS version is x.x.x
1439 Where x is an non-zero integer.
1440
1441 Args:
1442 branch: branch name or ref name in manifest-internal
1443
1444 Returns:
1445 An integer indicates the branch level, or zero if not detectable.
1446 """
1447 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1448 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1449 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1450
1451 if re.match(level1, branch):
1452 return 1
1453 if re.match(level2, branch):
1454 return 2
1455 if re.match(level3, branch):
1456 return 3
1457 return 0
1458
1459
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001460def get_crosland_link(old, new):
1461 """Generates crosland link between two versions.
1462
1463 Args:
1464 old: ChromeOS version
1465 new: ChromeOS version
1466
1467 Returns:
1468 A crosland url.
1469 """
1470
1471 def version_to_url_parameter(ver):
1472 if is_cros_snapshot_version(ver):
1473 return snapshot_version_split(ver)[2]
1474 return version_to_short(ver)
1475
1476 old_parameter = version_to_url_parameter(old)
1477 new_parameter = version_to_url_parameter(new)
1478 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1479
1480
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001481class ChromeOSSpecManager(codechange.SpecManager):
1482 """Repo manifest related operations.
1483
1484 This class enumerates chromeos manifest files, parses them,
1485 and sync to disk state according to them.
1486 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001487
1488 def __init__(self, config):
1489 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001490 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1491 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001492 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1493 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001494 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001495 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001496 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001497 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1498 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001499
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001500 def lookup_snapshot_manifest_revisions(self, old, new):
1501 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001502
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001503 Returns:
1504 list of (timestamp, commit_id, snapshot_id):
1505 timestamp: integer unix timestamp
1506 commit_id: a string indicates commit hash
1507 snapshot_id: a string indicates snapshot id
1508 """
1509 assert is_cros_snapshot_version(old)
1510 assert is_cros_snapshot_version(new)
1511
1512 gs_path = (
1513 'gs://chromeos-image-archive/{board}-postsubmit/{version}-*/image.zip')
1514 # Try to guess the commit time of a snapshot manifest, it is usually a few
1515 # minutes different between snapshot manifest commit and image.zip
1516 # generate.
1517 try:
1518 old_timestamp = gsutil_stat_update_time(
1519 gs_path.format(board=self.config['board'], version=old)) - 86400
1520 except subprocess.CalledProcessError:
1521 old_timestamp = None
1522 try:
1523 new_timestamp = gsutil_stat_update_time(
1524 gs_path.format(board=self.config['board'], version=new)) + 86400
1525 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1526 # we can find snapshot 5982
1527 # snapshot_id <= 5982 has different commit message format, so we need
1528 # to identify its id in different ways, see below comment for more info.
1529 new_timestamp = max(new_timestamp, 1558657989 + 1)
1530 except subprocess.CalledProcessError:
1531 new_timestamp = None
1532 result = []
1533 _, _, old_snapshot_id = snapshot_version_split(old)
1534 _, _, new_snapshot_id = snapshot_version_split(new)
1535 repo = self.manifest_internal_dir
1536 path = 'snapshot.xml'
1537 branch = 'snapshot'
1538 commits = git_util.get_history(
1539 repo,
1540 path,
1541 branch,
1542 after=old_timestamp,
1543 before=new_timestamp,
1544 with_subject=True)
1545
1546 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1547 # subject, as their subjects are all `Annealing manifest snapshot.`.
1548 # So instead we count the snapshot_id manually.
1549 count = 5982
1550 # There are two snapshot_id = 2633 in commit history, ignore the former
1551 # one.
1552 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1553 # We examine the commits in reverse order as there are some testing
1554 # commits before snapshot_id=2, this method works fine after
1555 # snapshot 2, except snapshot 2633
1556 for commit in reversed(commits):
1557 msg = commit[2]
1558 if commit[1] in ignore_list:
1559 continue
1560
1561 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1562 if match:
1563 snapshot_id = match.group(1)
1564 elif 'Annealing manifest snapshot' in msg:
1565 snapshot_id = str(count)
1566 count -= 1
1567 else:
1568 continue
1569 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1570 result.append((commit[0], commit[1], snapshot_id))
1571 # We find commits in reversed order, now reverse it again to chronological
1572 # order.
1573 return list(reversed(result))
1574
1575 def lookup_build_timestamp(self, rev):
1576 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1577 if is_cros_full_version(rev):
1578 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001579 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001580
1581 def lookup_snapshot_build_timestamp(self, rev):
1582 assert is_cros_snapshot_version(rev)
1583 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1584
1585 def lookup_release_build_timestamp(self, rev):
1586 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001587 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001588 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1589 try:
1590 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1591 'refs/heads/master', path)
1592 except ValueError:
Kuang-che Wuce2f3be2019-10-28 19:44:54 +08001593 raise errors.InternalError(
1594 '%s does not have %s' % (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001595 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001596
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001597 def detect_float_spec_branch_level(self, spec):
1598 results = [
1599 detect_branch_level(branch) for branch in git_util.get_branches(
1600 self.manifest_dir, commit=spec.name)
1601 ]
1602 results = [x for x in results if x > 0]
1603 return min(results) if results else 0
1604
1605 def branch_between_float_specs(self, old_spec, new_spec):
1606 if old_spec.spec_type != codechange.SPEC_FLOAT:
1607 return False
1608 if new_spec.spec_type != codechange.SPEC_FLOAT:
1609 return False
1610
1611 level_old = self.detect_float_spec_branch_level(old_spec)
1612 level_new = self.detect_float_spec_branch_level(new_spec)
1613
1614 if not level_old or not level_new:
1615 logger.warning('branch level detect failed, assume master')
1616 return False
1617 return level_old != level_new
1618
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001619 def collect_float_spec(self, old, new, fixed_specs=None):
1620 assert fixed_specs
1621 branch = None
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001622 old_branches = []
1623 new_branches = []
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001624
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001625 # There is no revision tag in snapshot's xml
1626 if fixed_specs[0].revision:
1627 old_branches = git_util.get_branches(
1628 self.manifest_dir, commit=fixed_specs[0].revision)
1629 if fixed_specs[-1].revision:
1630 new_branches = git_util.get_branches(
1631 self.manifest_dir, commit=fixed_specs[-1].revision)
1632
1633 # 1. if both are not snapshot, do AND operation
1634 # 2. if new is snapshot, branch = master
1635 # 3. if old is snapshot but new is not, respect new's branch
1636 if fixed_specs[0].revision and fixed_specs[-1].revision:
1637 branches = list(set(old_branches) & set(new_branches))
1638 elif not fixed_specs[-1].revision:
1639 branches = ['refs/remotes/origin/master']
1640 else:
1641 branches = new_branches
1642
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001643 if branches:
1644 branch = branches[0]
1645 else:
1646 logger.warning(
1647 'unable to determine float spec branch, '
1648 'old = %s, new = %s', old_branches, new_branches)
1649
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001650 old_timestamp = self.lookup_build_timestamp(old)
1651 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001652 # snapshot time is different from commit time
1653 # usually it's a few minutes different
1654 # 30 minutes should be safe in most cases
1655 if is_cros_snapshot_version(old):
1656 old_timestamp = old_timestamp - 1800
1657 if is_cros_snapshot_version(new):
1658 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001659
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001660 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001661 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08001662 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001663 parser = repo_util.ManifestParser(self.manifest_dir)
1664 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001665 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001666 result.append(
1667 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1668 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001669
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001670 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001671 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1672 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1673
1674 # case 1: if both are snapshot, return a list of snapshot
1675 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
1676 return self.collect_snapshot_specs(old, new)
1677
1678 # case 2: if both are release version
1679 # return a list of release version
1680 if is_cros_full_version(old) and is_cros_full_version(new):
1681 return self.collect_release_specs(old, new)
1682
1683 # case 3: return a list of release version and append a snapshot
1684 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001685 result = self.collect_release_specs(
1686 version_to_full(self.config['board'], old),
1687 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001688 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001689 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001690 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08001691 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001692 return result
1693
1694 def collect_snapshot_specs(self, old, new):
1695 assert is_cros_snapshot_version(old)
1696 assert is_cros_snapshot_version(new)
1697
1698 def guess_snapshot_version(board, snapshot_id, old, new):
1699 if old.endswith('-' + snapshot_id):
1700 return old
1701 if new.endswith('-' + snapshot_id):
1702 return new
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001703 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/'
1704 'R*-{snapshot_id}-*'.format(
1705 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001706 for line in gsutil_ls(gs_path, ignore_errors=True):
1707 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
1708 if m:
1709 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001710 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001711
1712 result = []
1713 path = 'snapshot.xml'
1714 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001715 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001716 snapshot_version = guess_snapshot_version(self.config['board'],
1717 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001718 if snapshot_version:
1719 result.append(
1720 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
1721 path))
1722 else:
1723 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001724 return result
1725
1726 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001727 assert is_cros_full_version(old)
1728 assert is_cros_full_version(new)
1729 old_milestone, old_short_version = version_split(old)
1730 new_milestone, new_short_version = version_split(new)
1731
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001732 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001733 for milestone in git_util.list_dir_from_revision(
1734 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
1735 if not milestone.isdigit():
1736 continue
1737 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
1738 continue
1739
Kuang-che Wu74768d32018-09-07 12:03:24 +08001740 files = git_util.list_dir_from_revision(
1741 self.historical_manifest_git_dir, 'refs/heads/master',
1742 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001743
1744 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001745 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001746 short_version, ext = os.path.splitext(fn)
1747 if ext != '.xml':
1748 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001749 if (util.is_version_lesseq(old_short_version, short_version) and
1750 util.is_version_lesseq(short_version, new_short_version) and
1751 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001752 rev = make_cros_full_version(milestone, short_version)
1753 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1754 'refs/heads/master', path)
1755 result.append(
1756 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001757
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001758 def version_key_func(spec):
1759 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001760 return util.version_key_func(short_version)
1761
1762 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001763 assert result[0].name == old
1764 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001765 return result
1766
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001767 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001768 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1769 if is_cros_full_version(rev):
1770 milestone, short_version = version_split(rev)
1771 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
1772 manifest = git_util.get_file_from_revision(
1773 self.historical_manifest_git_dir, 'refs/heads/master', path)
1774 else:
1775 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
1776 commit_id = revisions[0][1]
1777 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
1778 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001779 return manifest
1780
1781 def get_manifest_file(self, rev):
1782 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001783 manifest_name = 'manifest_%s.xml' % rev
1784 manifest_path = os.path.join(self.manifest_dir, manifest_name)
1785 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001786 f.write(self.get_manifest(rev))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001787 return manifest_name
1788
1789 def parse_spec(self, spec):
1790 parser = repo_util.ManifestParser(self.manifest_dir)
1791 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001792 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001793 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08001794 with open(manifest_path) as f:
1795 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001796 root = parser.parse_single_xml(content, allow_include=False)
1797 else:
1798 root = parser.parse_xml_recursive(spec.name, spec.path)
1799
1800 spec.entries = parser.process_parsed_result(root)
1801 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08001802 if not spec.is_static():
1803 raise ValueError(
1804 'fixed spec %r has unexpected floating entries' % spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001805 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001806
1807 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001808 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001809
1810 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
1811 # manifest. 'repo sync -m' is not enough
1812 repo_util.init(
1813 self.config['chromeos_root'],
1814 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
1815 manifest_name=manifest_name,
1816 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001817 reference=self.config['chromeos_mirror'],
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001818 )
1819
1820 # Note, don't sync with current_branch=True for chromeos. One of its
1821 # build steps (inside mark_as_stable) executes "git describe" which
1822 # needs git tag information.
1823 repo_util.sync(self.config['chromeos_root'])