blob: f59f87047c8b800d227aaaff9daf6ab4798068a4 [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'
70
71
Kuang-che Wu9890ce82018-07-07 15:14:10 +080072class NeedRecreateChrootException(Exception):
73 """Failed to build ChromeOS because of chroot mismatch or corruption"""
74
75
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080076def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080077 """Determines if `s` is chromeos short version.
78
79 This function doesn't accept version number of local build.
80 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080081 return bool(re.match(re_chromeos_short_version, s))
82
83
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080084def is_cros_localbuild_version(s):
85 """Determines if `s` is chromeos local build version."""
86 return bool(re.match(re_chromeos_localbuild_version, s))
87
88
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080089def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080090 """Determines if `s` is chromeos full version.
91
92 This function doesn't accept version number of local build.
93 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080094 return bool(re.match(re_chromeos_full_version, s))
95
96
97def is_cros_version(s):
98 """Determines if `s` is chromeos version (either short or full)"""
99 return is_cros_short_version(s) or is_cros_full_version(s)
100
101
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800102def is_cros_snapshot_version(s):
103 """Determines if `s` is chromeos snapshot version"""
104 return bool(re.match(re_chromeos_snapshot_version, s))
105
106
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800107def make_cros_full_version(milestone, short_version):
108 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800109 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800110 return 'R%s-%s' % (milestone, short_version)
111
112
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800113def make_cros_snapshot_version(milestone, short_version, snapshot_id):
114 """Makes snapshot version from milestone, short_version and snapshot id"""
115 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
116
117
118def version_split(version):
119 """Splits full_version or snapshot_version into milestone and short_version"""
120 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
121 if is_cros_snapshot_version(version):
122 return snapshot_version_split(version)[0:2]
123 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800124 return milestone[1:], short_version
125
126
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800127def snapshot_version_split(snapshot_version):
128 """Splits snapshot_version into milestone, short_version and snapshot_id"""
129 assert is_cros_snapshot_version(snapshot_version)
130 milestone, shot_version, snapshot_id = snapshot_version.split('-')
131 return milestone[1:], shot_version, snapshot_id
132
133
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800134def query_snapshot_buildbucket_id(board, snapshot_version):
135 """Query buildbucket id of a snapshot"""
136 assert is_cros_snapshot_version(snapshot_version)
137 path = ('gs://chromeos-image-archive/{board}-postsubmit'
138 '/{snapshot_version}-*/image.zip')
139 output = gsutil_ls(
140 '-d',
141 path.format(board=board, snapshot_version=snapshot_version),
142 ignore_errors=True)
143 for line in output:
144 m = re.match(r'.*-postsubmit/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
145 if m:
146 return m.group(1)
147 return None
148
149
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800150def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800151 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800152 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800153 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 +0800154 return s
155
156
157def query_dut_lsb_release(host):
158 """Query /etc/lsb-release of given DUT
159
160 Args:
161 host: the DUT address
162
163 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800164 dict for keys and values of /etc/lsb-release.
165
166 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800167 errors.SshConnectionError: cannot connect to host
168 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800169 """
170 try:
Kuang-che Wu44278142019-03-04 11:33:57 +0800171 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800172 except subprocess.CalledProcessError:
Kuang-che Wu44278142019-03-04 11:33:57 +0800173 raise errors.ExternalError('unable to read /etc/lsb-release; not a DUT')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800174 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
175
176
177def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800178 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800179
180 Args:
181 host: the DUT address
182
183 Returns:
184 True if the host is a chromeos device.
185 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800186 try:
187 return query_dut_lsb_release(host).get('DEVICETYPE') in [
188 'CHROMEBASE',
189 'CHROMEBIT',
190 'CHROMEBOOK',
191 'CHROMEBOX',
192 'REFERENCE',
193 ]
194 except (errors.ExternalError, errors.SshConnectionError):
195 return False
196
197
198def is_good_dut(host):
199 if not is_dut(host):
200 return False
201
202 # Sometimes python is broken after 'cros flash'.
203 try:
204 util.ssh_cmd(host, 'python', '-c', '1')
205 return True
206 except (subprocess.CalledProcessError, errors.SshConnectionError):
207 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800208
209
210def query_dut_board(host):
211 """Query board name of a given DUT"""
212 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
213
214
215def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800216 """Query short version of a given DUT.
217
218 This function may return version of local build, which
219 is_cros_short_version() is false.
220 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800221 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
222
223
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800224def query_dut_is_snapshot(host):
225 """Query if given DUT is a snapshot version."""
226 path = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILDER_PATH', '')
227 return '-postsubmit' in path
228
229
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800230def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800231 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800232
233 Args:
234 host: DUT address
235 connect_timeout: connection timeout
236
237 Returns:
238 boot uuid
239 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800240 return util.ssh_cmd(
241 host,
242 'cat',
243 '/proc/sys/kernel/random/boot_id',
244 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800245
246
247def reboot(host):
248 """Reboot a DUT and verify"""
249 logger.debug('reboot %s', host)
250 boot_id = query_dut_boot_id(host)
251
Kuang-che Wu44278142019-03-04 11:33:57 +0800252 try:
253 util.ssh_cmd(host, 'reboot')
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800254 except errors.SshConnectionError:
255 # Depends on timing, ssh may return failure due to broken pipe, which is
256 # working as intended. Ignore such kind of errors.
Kuang-che Wu44278142019-03-04 11:33:57 +0800257 pass
Kuang-che Wu708310b2018-03-28 17:24:34 +0800258 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800259
Kuang-che Wu708310b2018-03-28 17:24:34 +0800260
261def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800262 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800263 # (dev screen short delay) or more (long delay).
264 time.sleep(15)
265 for _ in range(100):
266 try:
267 # During boot, DUT does not response and thus ssh may hang a while. So
268 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
269 # set tight limit because it's inside retry loop.
270 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
271 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800272 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800273 logger.debug('reboot not ready? sleep wait 1 sec')
274 time.sleep(1)
275
Kuang-che Wue121fae2018-11-09 16:18:39 +0800276 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800277
278
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800279def gs_release_boardpath(board):
280 """Normalizes board name for gs://chromeos-releases/
281
282 This follows behavior of PushImage() in chromite/scripts/pushimage.py
283 Note, only gs://chromeos-releases/ needs normalization,
284 gs://chromeos-image-archive does not.
285
286 Args:
287 board: ChromeOS board name
288
289 Returns:
290 normalized board name
291 """
292 return board.replace('_', '-')
293
294
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800295def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800296 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800297
298 Args:
299 args: command line arguments passed to gsutil
300 kwargs:
301 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
302 but the path not found.
303
304 Returns:
305 stdout of gsutil
306
307 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800308 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800309 subprocess.CalledProcessError: command failed
310 """
311 stderr_lines = []
312 try:
313 return util.check_output(
314 gsutil_bin, *args, stderr_callback=stderr_lines.append)
315 except subprocess.CalledProcessError as e:
316 stderr = ''.join(stderr_lines)
317 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800318 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800319 'gsutil failed due to permission. ' +
320 'Run "%s config" and follow its instruction. ' % gsutil_bin +
321 'Fill any string if it asks for project-id')
322 if kwargs.get('ignore_errors'):
323 return ''
324 raise
325 except OSError as e:
326 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800327 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800328 'Unable to run %s. gsutil is not installed or not in PATH?' %
329 gsutil_bin)
330 raise
331
332
333def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800334 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800335
336 Args:
337 args: arguments passed to 'gsutil ls'
338 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800339 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800340 exception, ex. path not found.
341
342 Returns:
343 list of 'gsutil ls' result. One element for one line of gsutil output.
344
345 Raises:
346 subprocess.CalledProcessError: gsutil failed, usually means path not found
347 """
348 return gsutil('ls', *args, **kwargs).splitlines()
349
350
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800351def gsutil_stat_update_time(*args, **kwargs):
352 """Returns the last modified time of a file or multiple files.
353
354 Args:
355 args: arguments passed to 'gsutil stat'.
356 kwargs: extra parameters for gsutil.
357
358 Returns:
359 A integer indicates the last modified timestamp.
360
361 Raises:
362 subprocess.CalledProcessError: gsutil failed, usually means path not found
363 errors.ExternalError: update time is not found
364 """
365 result = -1
366 # Currently we believe stat always returns a UTC time, and strptime also
367 # parses a UTC time by default.
368 time_format = '%a, %d %b %Y %H:%M:%S GMT'
369
370 for line in gsutil('stat', *args, **kwargs).splitlines():
371 if ':' not in line:
372 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800373 key, value = line.split(':', 1)
374 key, value = key.strip(), value.strip()
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800375 if key != 'Update time':
376 continue
377 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800378 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800379 result = max(result, unixtime)
380
381 if result == -1:
382 raise errors.ExternalError("didn't find update time")
383 return result
384
385
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800386def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800387 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800388
389 Args:
390 board: ChromeOS board name
391 short_version: ChromeOS version number in short format, ex. 9300.0.0
392
393 Returns:
394 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
395 None if failed.
396 """
397 path = gs_archive_path.format(board=board) + '/R*-' + short_version
398 for line in gsutil_ls('-d', path, ignore_errors=True):
399 m = re.search(r'/R(\d+)-', line)
400 if not m:
401 continue
402 return m.group(1)
403
404 for channel in ['canary', 'dev', 'beta', 'stable']:
405 path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800406 channel=channel,
407 boardpath=gs_release_boardpath(board),
408 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800409 for line in gsutil_ls(path, ignore_errors=True):
410 m = re.search(r'\bR(\d+)-' + short_version, line)
411 if not m:
412 continue
413 return m.group(1)
414
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800415 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800416 return None
417
418
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800419def list_board_names(chromeos_root):
420 """List board names.
421
422 Args:
423 chromeos_root: chromeos tree root
424
425 Returns:
426 list of board names
427 """
428 # Following logic is simplified from chromite/lib/portage_util.py
429 cros_list_overlays = os.path.join(chromeos_root,
430 'chromite/bin/cros_list_overlays')
431 overlays = util.check_output(cros_list_overlays).splitlines()
432 result = set()
433 for overlay in overlays:
434 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
435 name = None
436 if os.path.exists(conf_file):
437 for line in open(conf_file):
438 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
439 if m:
440 name = m.group(1)
441 break
442
443 if not name:
444 name_file = os.path.join(overlay, 'profiles', 'repo_name')
445 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800446 with open(name_file) as f:
447 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800448
449 if name:
450 name = re.sub(r'-private$', '', name)
451 result.add(name)
452
453 return list(result)
454
455
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800456def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800457 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800458
459 Args:
460 board: ChromeOS board name
461 version: ChromeOS version number in short or full format
462
463 Returns:
464 (milestone, version in short format)
465 """
466 if is_cros_short_version(version):
467 milestone = query_milestone_by_version(board, version)
468 short_version = version
469 else:
470 milestone, short_version = version_split(version)
471 return milestone, short_version
472
473
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800474def extract_major_version(version):
475 """Converts a version to its major version.
476
477 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800478 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800479
480 Returns:
481 major version number in string format
482 """
483 version = version_to_short(version)
484 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
485 return m.group(1)
486
487
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800488def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800489 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800490
491 Args:
492 version: ChromeOS version number in short or full format
493
494 Returns:
495 version number in short format
496 """
497 if is_cros_short_version(version):
498 return version
499 _, short_version = version_split(version)
500 return short_version
501
502
503def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800504 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800505
506 Args:
507 board: ChromeOS board name
508 version: ChromeOS version number in short or full format
509
510 Returns:
511 version number in full format
512 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800513 if is_cros_snapshot_version(version):
514 milestone, short_version, _ = snapshot_version_split(version)
515 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800516 if is_cros_full_version(version):
517 return version
518 milestone = query_milestone_by_version(board, version)
Kuang-che Wu0205f052019-05-23 12:48:37 +0800519 assert milestone, 'incorrect board=%s or version=%s ?' % (board, version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800520 return make_cros_full_version(milestone, version)
521
522
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800523def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800524 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800525
526 Args:
527 board: ChromeOS board
528 major_version: ChromeOS major version
529
530 Returns:
531 list of (version, gs_path):
532 version: Chrome OS snapshot version
533 gs_path: gs path of test image
534 """
535
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800536 def extract_snapshot_id(result):
537 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
538 assert m
539 return int(m.group(1))
540
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800541 short_version = '%s.0.0' % major_version
542 milestone = query_milestone_by_version(board, short_version)
543 if not milestone:
544 milestone = '*'
545
546 path = ('gs://chromeos-image-archive/{board}-postsubmit/R{milestone}-'
547 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800548 result = []
549 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800550 path.format(
551 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800552 ignore_errors=True)
553
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800554 for gs_path in sorted(output):
555 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800556 if m:
557 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800558 # we should skip if there is duplicate snapshot
559 if result and result[-1][0] == snapshot_version:
560 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800561 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800562
563 # sort by its snapshot_id
564 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800565 return result
566
567
Kuang-che Wu575dc442019-03-05 10:30:55 +0800568def list_prebuilt_from_image_archive(board):
569 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
570
571 gs://chromeos-image-archive contains only recent builds (in two years).
572 We prefer this function to list_prebuilt_from_chromeos_releases() because
573 - this is what "cros flash" supports directly.
574 - the paths have milestone information, so we don't need to do slow query
575 by ourselves.
576
577 Args:
578 board: ChromeOS board name
579
580 Returns:
581 list of (version, gs_path):
582 version: Chrome OS version in full format
583 gs_path: gs path of test image
584 """
585 result = []
586 for line in gsutil_ls(gs_archive_path.format(board=board)):
587 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
588 if m:
589 full_version = m.group(1)
590 test_image = 'chromiumos_test_image.tar.xz'
591 assert line.endswith('/')
592 gs_path = line + test_image
593 result.append((full_version, gs_path))
594 return result
595
596
597def list_prebuilt_from_chromeos_releases(board):
598 """Lists ChromeOS versions available from gs://chromeos-releases.
599
600 gs://chromeos-releases contains more builds. However, 'cros flash' doesn't
601 support it.
602
603 Args:
604 board: ChromeOS board name
605
606 Returns:
607 list of (version, gs_path):
608 version: Chrome OS version in short format
609 gs_path: gs path of test image (with wildcard)
610 """
611 result = []
612 for line in gsutil_ls(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800613 gs_release_path.format(
614 channel='*', boardpath=gs_release_boardpath(board), short_version=''),
Kuang-che Wu575dc442019-03-05 10:30:55 +0800615 ignore_errors=True):
616 m = re.match(r'gs:\S+/(\d+\.\d+\.\d+)/$', line)
617 if m:
618 short_version = m.group(1)
619 test_image = 'ChromeOS-test-R*-{short_version}-{board}.tar.xz'.format(
620 short_version=short_version, board=board)
621 gs_path = line + test_image
622 result.append((short_version, gs_path))
623 return result
624
625
Kuang-che Wue1808402020-01-06 20:27:45 +0800626def has_test_image(board, version):
627 if is_cros_snapshot_version(version):
628 return bool(query_snapshot_buildbucket_id(board, version))
629
630 full_version = version_to_full(board, version)
631 short_version = version_to_short(version)
632 paths = [
633 gs_archive_path.format(board=board) +
634 '/%s/chromiumos_test_image.tar.xz' % full_version,
635 gs_release_path.format(
636 channel='*',
637 boardpath=gs_release_boardpath(board),
638 short_version=short_version),
639 ]
640
641 for path in paths:
642 if gsutil_ls(path, ignore_errors=True):
643 return True
644 return False
645
646
Kuang-che Wu575dc442019-03-05 10:30:55 +0800647def list_chromeos_prebuilt_versions(board,
648 old,
649 new,
650 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800651 include_older_build=True,
652 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800653 """Lists ChromeOS version numbers with prebuilt between given range
654
655 Args:
656 board: ChromeOS board name
657 old: start version (inclusive)
658 new: end version (inclusive)
659 only_good_build: only if test image is available
660 include_older_build: include prebuilt in gs://chromeos-releases
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800661 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800662
663 Returns:
664 list of sorted version numbers (in full format) between [old, new] range
665 (inclusive).
666 """
667 old = version_to_short(old)
668 new = version_to_short(new)
669
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800670 rev_map = {
671 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800672 for full_version, gs_path in list_prebuilt_from_image_archive(board):
673 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800674 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800675
676 if include_older_build and old not in rev_map:
677 for short_version, gs_path in list_prebuilt_from_chromeos_releases(board):
678 if short_version not in rev_map:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800679 rev_map[short_version] = [(short_version, gs_path)]
680
681 if use_snapshot:
682 for major_version in range(
683 int(extract_major_version(old)),
684 int(extract_major_version(new)) + 1):
685 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800686 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800687 # If current version is smaller than cutover, ignore it as it might not
688 # contain enough information for continuing android and chrome bisection.
689 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
690 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800691
692 # Given the fact that snapshots are images between two release versions.
693 # Adding snapshots of 12345.0.0 should be treated as adding commits
694 # between [12345.0.0, 12346.0.0).
695 # So in the following lines we check two facts:
696 # 1) If 12346.0.0(next_short_version) is a version between old and new
697 if not util.is_direct_relative_version(next_short_version, old):
698 continue
699 if not util.is_direct_relative_version(next_short_version, new):
700 continue
701 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800702 if not util.is_direct_relative_version(short_version, old):
703 continue
704 if not util.is_direct_relative_version(short_version, new):
705 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800706
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800707 snapshots = list_snapshots_from_image_archive(board, str(major_version))
708 if snapshots:
709 # if snapshots found, we can append them after the release version,
710 # so the prebuilt image list of this version will be
711 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800712 if short_version not in rev_map:
713 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800714 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800715
716 result = []
717 for rev in sorted(rev_map, key=util.version_key_func):
718 if not util.is_direct_relative_version(new, rev):
719 continue
720 if not util.is_version_lesseq(old, rev):
721 continue
722 if not util.is_version_lesseq(rev, new):
723 continue
724
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800725 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800726
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800727 # version_to_full() and gsutil_ls() may take long time if versions are a
728 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800729
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800730 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800731 gs_result = gsutil_ls(gs_path, ignore_errors=True)
732 if not gs_result:
733 logger.warning('%s is not a good build, ignore', version)
734 continue
735 assert len(gs_result) == 1
736 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
737 if not m:
738 logger.warning('format of image path is unexpected: %s', gs_result[0])
739 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800740 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800741 elif is_cros_short_version(version):
742 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800743
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800744 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800745
746 return result
747
748
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800749def prepare_snapshot_image(chromeos_root, board, snapshot_version):
750 """Prepare chromeos snapshot image.
751
752 Args:
753 chromeos_root: chromeos tree root
754 board: ChromeOS board name
755 snapshot_version: ChromeOS snapshot version number
756
757 Returns:
758 local file path of test image relative to chromeos_root
759 """
760 assert is_cros_snapshot_version(snapshot_version)
761 milestone, short_version, snapshot_id = snapshot_version_split(
762 snapshot_version)
763 full_version = make_cros_full_version(milestone, short_version)
764 tmp_dir = os.path.join(
765 chromeos_root, 'tmp',
766 'ChromeOS-test-%s-%s-%s' % (full_version, board, snapshot_id))
767 if not os.path.exists(tmp_dir):
768 os.makedirs(tmp_dir)
769
770 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/' +
771 '{snapshot_version}-*/image.zip')
772 gs_path = gs_path.format(board=board, snapshot_version=snapshot_version)
773
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800774 full_path = os.path.join(tmp_dir, test_image_filename)
775 rel_path = os.path.relpath(full_path, chromeos_root)
776 if os.path.exists(full_path):
777 return rel_path
778
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800779 files = gsutil_ls(gs_path, ignore_errors=True)
Zheng-Jie Changeb7308f2019-12-05 14:25:05 +0800780 if len(files) >= 1:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800781 gs_path = files[0]
782 gsutil('cp', gs_path, tmp_dir)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800783 util.check_call(
784 'unzip', '-j', 'image.zip', test_image_filename, cwd=tmp_dir)
785 os.remove(os.path.join(tmp_dir, 'image.zip'))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800786 return rel_path
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800787
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800788 assert False
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800789 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800790
791
Kuang-che Wu28980b22019-07-31 19:51:45 +0800792def prepare_prebuilt_image(chromeos_root, board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800793 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800794
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800795 It searches for xbuddy image which "cros flash" can use, or fetch image to
796 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800797
798 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800799 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800800 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800801 version: ChromeOS version number in short or full format
802
803 Returns:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800804 xbuddy path or file path (relative to chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800805 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800806 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800807 full_version = version_to_full(board, version)
808 short_version = version_to_short(full_version)
809
810 image_path = None
811 gs_path = gs_archive_path.format(board=board) + '/' + full_version
812 if gsutil_ls('-d', gs_path, ignore_errors=True):
813 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
814 board=board, full_version=full_version)
815 else:
Kuang-che Wu28980b22019-07-31 19:51:45 +0800816 tmp_dir = os.path.join(chromeos_root, 'tmp',
817 'ChromeOS-test-%s-%s' % (full_version, board))
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800818 full_path = os.path.join(tmp_dir, test_image_filename)
819 rel_path = os.path.relpath(full_path, chromeos_root)
820 if os.path.exists(full_path):
821 return rel_path
822
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800823 if not os.path.exists(tmp_dir):
824 os.makedirs(tmp_dir)
825 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800826 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800827 # to fetch the image by ourselves
828 for channel in ['canary', 'dev', 'beta', 'stable']:
829 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
830 full_version=full_version, board=board)
831 gs_path = gs_release_path.format(
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800832 channel=channel,
833 boardpath=gs_release_boardpath(board),
834 short_version=short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800835 gs_path += '/' + fn
836 if gsutil_ls(gs_path, ignore_errors=True):
837 # TODO(kcwu): delete tmp
838 gsutil('cp', gs_path, tmp_dir)
839 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800840 image_path = os.path.relpath(full_path, chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800841 break
842
843 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800844 return image_path
845
846
847def cros_flash(chromeos_root,
848 host,
849 board,
850 image_path,
851 version=None,
852 clobber_stateful=False,
Kuang-che Wu155fb6e2018-11-29 16:00:41 +0800853 disable_rootfs_verification=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800854 """Flash a DUT with given ChromeOS image.
855
856 This is implemented by 'cros flash' command line.
857
858 Args:
859 chromeos_root: use 'cros flash' of which chromeos tree
860 host: DUT address
861 board: ChromeOS board name
Kuang-che Wu28980b22019-07-31 19:51:45 +0800862 image_path: chromeos image xbuddy path or file path. For relative
863 path, it should be relative to chromeos_root.
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800864 version: ChromeOS version in short or full format
865 clobber_stateful: Clobber stateful partition when performing update
866 disable_rootfs_verification: Disable rootfs verification after update
867 is completed
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800868
869 Raises:
870 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800871 """
872 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
873
874 # Reboot is necessary because sometimes previous 'cros flash' failed and
875 # entered a bad state.
876 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800877
Kuang-che Wu28980b22019-07-31 19:51:45 +0800878 # Handle relative path.
879 if '://' not in image_path and not os.path.isabs(image_path):
880 assert os.path.exists(os.path.join(chromeos_root, image_path))
881 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
882
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +0800883 args = [
884 '--debug', '--no-ping', '--send-payload-in-parallel', host, image_path
885 ]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800886 if clobber_stateful:
887 args.append('--clobber-stateful')
888 if disable_rootfs_verification:
889 args.append('--disable-rootfs-verification')
890
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800891 try:
892 cros_sdk(chromeos_root, 'cros', 'flash', *args)
893 except subprocess.CalledProcessError:
894 raise errors.ExternalError('cros flash failed')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800895
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800896 if version:
897 # In the past, cros flash may fail with returncode=0
898 # So let's have an extra check.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800899 if is_cros_snapshot_version(version):
900 builder_path = query_dut_lsb_release(host).get(
901 'CHROMEOS_RELEASE_BUILDER_PATH', '')
902 expect_prefix = '%s-postsubmit/%s-' % (board, version)
903 if not builder_path.startswith(expect_prefix):
904 raise errors.ExternalError(
905 'although cros flash succeeded, the OS builder path is '
906 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
907 else:
908 expect_version = version_to_short(version)
909 dut_version = query_dut_short_version(host)
910 if dut_version != expect_version:
911 raise errors.ExternalError(
912 'although cros flash succeeded, the OS version is unexpected: '
913 'actual=%s expect=%s' % (dut_version, expect_version))
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800914
Kuang-che Wu4a81ea72019-10-05 15:35:17 +0800915 # "cros flash" may terminate successfully but the DUT starts self-repairing
Kuang-che Wu414d67f2019-05-28 11:28:57 +0800916 # (b/130786578), so it's necessary to do sanity check.
917 if not is_good_dut(host):
918 raise errors.ExternalError(
919 'although cros flash succeeded, the DUT is in bad state')
920
921
922def cros_flash_with_retry(chromeos_root,
923 host,
924 board,
925 image_path,
926 version=None,
927 clobber_stateful=False,
928 disable_rootfs_verification=True,
929 repair_callback=None):
930 # 'cros flash' is not 100% reliable, retry if necessary.
931 for attempt in range(2):
932 if attempt > 0:
933 logger.info('will retry 60 seconds later')
934 time.sleep(60)
935
936 try:
937 cros_flash(
938 chromeos_root,
939 host,
940 board,
941 image_path,
942 version=version,
943 clobber_stateful=clobber_stateful,
944 disable_rootfs_verification=disable_rootfs_verification)
945 return True
946 except errors.ExternalError:
947 logger.exception('cros flash failed')
948 if repair_callback and not repair_callback(host):
949 logger.warning('not repaired, assume it is harmless')
950 continue
951 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800952
953
954def version_info(board, version):
955 """Query subcomponents version info of given version of ChromeOS
956
957 Args:
958 board: ChromeOS board name
959 version: ChromeOS version number in short or full format
960
961 Returns:
962 dict of component and version info, including (if available):
963 cros_short_version: ChromeOS version
964 cros_full_version: ChromeOS version
965 milestone: milestone of ChromeOS
966 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800967 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800968 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
969 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800970 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800971 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800972 milestone, short_version, _ = snapshot_version_split(version)
973 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +0800974 data = api.get(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +0800975 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800976 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800977 VERSION_KEY_MILESTONE: milestone,
978 VERSION_KEY_CROS_FULL_VERSION: version,
979 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +0800980 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
981 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
982 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800983 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800984 info = {}
985 full_version = version_to_full(board, version)
986
987 # Some boards may have only partial-metadata.json but no metadata.json.
988 # e.g. caroline R60-9462.0.0
989 # Let's try both.
990 metadata = None
991 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +0800992 path = gs_archive_path.format(
993 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800994 metadata = gsutil('cat', path, ignore_errors=True)
995 if metadata:
996 o = json.loads(metadata)
997 v = o['version']
998 board_metadata = o['board-metadata'][board]
999 info.update({
1000 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1001 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1002 VERSION_KEY_MILESTONE: v['milestone'],
1003 VERSION_KEY_CR_VERSION: v['chrome'],
1004 })
1005
1006 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001007 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001008 if 'android-branch' in v: # this appears since R58-9317.0.0
1009 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1010 elif 'android-container-branch' in board_metadata:
1011 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1012 break
1013 else:
1014 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1015 logger.error(
1016 'Note, so far no quick way to look up version info for too old builds')
1017
1018 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001019
1020
1021def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001022 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001023
1024 Args:
1025 board: ChromeOS board name
1026 version: ChromeOS version number in short or full format
1027
1028 Returns:
1029 Chrome version number
1030 """
1031 info = version_info(board, version)
1032 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001033
1034
1035def query_android_build_id(board, rev):
1036 info = version_info(board, rev)
1037 rev = info['android_build_id']
1038 return rev
1039
1040
1041def query_android_branch(board, rev):
1042 info = version_info(board, rev)
1043 rev = info['android_branch']
1044 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001045
1046
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001047def guess_chrome_version(board, rev):
1048 """Guess chrome version number.
1049
1050 Args:
1051 board: chromeos board name
1052 rev: chrome or chromeos version
1053
1054 Returns:
1055 chrome version number
1056 """
1057 if is_cros_version(rev):
1058 assert board, 'need to specify BOARD for cros version'
1059 rev = query_chrome_version(board, rev)
1060 assert cr_util.is_chrome_version(rev)
1061
1062 return rev
1063
1064
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001065def is_inside_chroot():
1066 """Returns True if we are inside chroot."""
1067 return os.path.exists('/etc/cros_chroot_version')
1068
1069
1070def cros_sdk(chromeos_root, *args, **kwargs):
1071 """Run commands inside chromeos chroot.
1072
1073 Args:
1074 chromeos_root: chromeos tree root
1075 *args: command to run
1076 **kwargs:
Kuang-che Wud4603d72018-11-29 17:51:21 +08001077 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001078 env: (dict) environment variables for the command
Kuang-che Wubcafc552019-08-15 15:27:02 +08001079 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001080 stdin: standard input file handle for the command
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001081 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001082 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001083 """
1084 envs = []
1085 for k, v in kwargs.get('env', {}).items():
1086 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1087 envs.append('%s=%s' % (k, v))
1088
1089 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1090 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001091 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001092
1093 if kwargs.get('chrome_root'):
Kuang-che Wu399d4662019-06-06 15:23:37 +08001094 prefix += ['--chrome_root', kwargs['chrome_root']]
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001095 if kwargs.get('goma_dir'):
1096 prefix += ['--goma_dir', kwargs['goma_dir']]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001097
Kuang-che Wu399d4662019-06-06 15:23:37 +08001098 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001099
Kuang-che Wu399d4662019-06-06 15:23:37 +08001100 # In addition to the output of command we are interested, cros_sdk may
1101 # generate its own messages. For example, chroot creation messages if we run
1102 # cros_sdk the first time.
1103 # This is the hack to run dummy command once, so we can get clean output for
1104 # the command we are interested.
1105 cmd = prefix + ['true']
1106 util.check_call(*cmd, cwd=chromeos_root)
1107
1108 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001109 return util.check_output(
1110 *cmd,
1111 cwd=chromeos_root,
Kuang-che Wubcafc552019-08-15 15:27:02 +08001112 log_stdout=kwargs.get('log_stdout', True),
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001113 stdin=kwargs.get('stdin'),
1114 stderr_callback=kwargs.get('stderr_callback'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001115
1116
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001117def create_chroot(chromeos_root):
1118 """Creates ChromeOS chroot.
1119
1120 Args:
1121 chromeos_root: chromeos tree root
1122 """
1123 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1124 return
1125 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1126 return
1127
1128 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1129
1130
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001131def copy_into_chroot(chromeos_root, src, dst):
1132 """Copies file into chromeos chroot.
1133
1134 Args:
1135 chromeos_root: chromeos tree root
1136 src: path outside chroot
1137 dst: path inside chroot
1138 """
1139 # chroot may be an image, so we cannot copy to corresponding path
1140 # directly.
1141 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
1142
1143
1144def exists_in_chroot(chromeos_root, path):
1145 """Determine whether a path exists in the chroot.
1146
1147 Args:
1148 chromeos_root: chromeos tree root
1149 path: path inside chroot, relative to src/scripts
1150
1151 Returns:
1152 True if a path exists
1153 """
1154 try:
Kuang-che Wuacb6efd2018-04-25 18:52:58 +08001155 cros_sdk(chromeos_root, 'test', '-e', path)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001156 except subprocess.CalledProcessError:
1157 return False
1158 return True
1159
1160
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001161def check_if_need_recreate_chroot(stdout, stderr):
1162 """Analyze build log and determine if chroot should be recreated.
1163
1164 Args:
1165 stdout: stdout output of build
1166 stderr: stderr output of build
1167
1168 Returns:
1169 the reason if chroot needs recreated; None otherwise
1170 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001171 if re.search(
1172 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001173 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001174 return 'EAPI version mismatch'
1175
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001176 if 'Chroot is too new. Consider running:' in stderr:
1177 return 'chroot version is too new'
1178
1179 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001180 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1181 return 'chroot version is too new'
1182
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001183 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1184 if "undefined reference to 'std::__1::basic_string" in stdout:
1185 return 'might be due to compiler change'
1186
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001187 # Detect failures due to file collisions.
1188 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1189 # and conflict with each other. Other possible cases are package renaming or
1190 # refactoring. Let's recreate chroot to work around them.
1191 if 'Detected file collision' in stdout:
1192 # Using wildcard between words because the text wraps to the next line
1193 # depending on length of package name and each line is prefixed with
1194 # package name.
1195 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1196 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1197 # package name (65 now).
1198 m = re.search(
1199 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1200 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1201 if m:
1202 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001203
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001204 return None
1205
1206
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001207def build_packages(chromeos_root,
1208 board,
1209 chrome_root=None,
1210 goma_dir=None,
1211 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001212 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001213
1214 Args:
1215 chromeos_root: chromeos tree root
1216 board: ChromeOS board name
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001217 chrome_root: Chrome tree root. If specified, build chrome using the
1218 provided tree
1219 goma_dir: Goma installed directory to mount into the chroot. If specified,
1220 build chrome with goma.
1221 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001222 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001223
1224 def has_build_package_argument(argument):
1225 stderr_lines = []
1226 try:
1227 util.check_call(
1228 'src/scripts/build_packages',
1229 '--help',
1230 cwd=chromeos_root,
1231 stderr_callback=stderr_lines.append)
1232 except subprocess.CalledProcessError:
1233 help_output = ''.join(stderr_lines)
1234 return '--[no]%s' % argument in help_output
1235
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001236 common_env = {
1237 'USE': '-cros-debug chrome_internal',
1238 'FEATURES': 'separatedebug',
1239 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001240 stderr_lines = []
1241 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001242 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001243 env = common_env.copy()
1244 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001245 cros_sdk(
1246 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001247 './update_chroot',
1248 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001249 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001250 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001251 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001252
1253 env = common_env.copy()
1254 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001255 './build_packages',
1256 '--board',
1257 board,
1258 '--withdev',
1259 '--noworkon',
1260 '--skip_chroot_upgrade',
1261 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001262 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001263
1264 # `use_any_chrome` flag is default on and will force to use a chrome
1265 # prebuilt even if the version doesn't match.
1266
1267 # As this argument is landed in 12681, we should check if the argument
1268 # exists before adding this.
1269 if has_build_package_argument('use_any_chrome'):
1270 cmd.append('--nouse_any_chrome')
1271
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001272 if goma_dir:
1273 # Tell build_packages to start and stop goma
1274 cmd.append('--run_goma')
1275 env['USE_GOMA'] = 'true'
1276 if afdo_use:
1277 env['USE'] += ' afdo_use'
1278 cros_sdk(
1279 chromeos_root,
1280 *cmd,
1281 env=env,
1282 chrome_root=chrome_root,
1283 stderr_callback=stderr_lines.append,
1284 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001285 except subprocess.CalledProcessError as e:
1286 # Detect failures due to incompatibility between chroot and source tree. If
1287 # so, notify the caller to recreate chroot and retry.
1288 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1289 if reason:
1290 raise NeedRecreateChrootException(reason)
1291
1292 # For other failures, don't know how to handle. Just bail out.
1293 raise
1294
Kuang-che Wu28980b22019-07-31 19:51:45 +08001295
1296def build_image(chromeos_root, board):
1297 """Build ChromeOS image.
1298
1299 Args:
1300 chromeos_root: chromeos tree root
1301 board: ChromeOS board name
1302
1303 Returns:
1304 image folder; relative to chromeos_root
1305 """
1306 stderr_lines = []
1307 try:
1308 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1309 cros_sdk(
1310 chromeos_root,
1311 './build_image',
1312 '--board',
1313 board,
1314 '--noenable_rootfs_verification',
1315 'test',
1316 env={
1317 'USE': '-cros-debug chrome_internal',
1318 'FEATURES': 'separatedebug',
1319 },
1320 stderr_callback=stderr_lines.append)
1321 except subprocess.CalledProcessError as e:
1322 # Detect failures due to incompatibility between chroot and source tree. If
1323 # so, notify the caller to recreate chroot and retry.
1324 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1325 if reason:
1326 raise NeedRecreateChrootException(reason)
1327
1328 # For other failures, don't know how to handle. Just bail out.
1329 raise
1330
1331 image_symlink = os.path.join(chromeos_root, cached_images_dir, board,
1332 'latest')
1333 assert os.path.exists(image_symlink)
1334 image_name = os.readlink(image_symlink)
1335 image_folder = os.path.join(cached_images_dir, board, image_name)
1336 assert os.path.exists(
1337 os.path.join(chromeos_root, image_folder, test_image_filename))
1338 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001339
1340
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001341class AutotestControlInfo(object):
1342 """Parsed content of autotest control file.
1343
1344 Attributes:
1345 name: test name
1346 path: control file path
1347 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
1348 DOC, ATTRIBUTES, DEPENDENCIES, etc.
1349 """
1350
1351 def __init__(self, path, variables):
1352 self.name = variables['NAME']
1353 self.path = path
1354 self.variables = variables
1355
1356
1357def parse_autotest_control_file(path):
1358 """Parses autotest control file.
1359
1360 This only parses simple top-level string assignments.
1361
1362 Returns:
1363 AutotestControlInfo object
1364 """
1365 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001366 with open(path) as f:
1367 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001368 for stmt in code.body:
1369 # Skip if not simple "NAME = *" assignment.
1370 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1371 isinstance(stmt.targets[0], ast.Name)):
1372 continue
1373
1374 # Only support string value.
1375 if isinstance(stmt.value, ast.Str):
1376 variables[stmt.targets[0].id] = stmt.value.s
1377
1378 return AutotestControlInfo(path, variables)
1379
1380
1381def enumerate_autotest_control_files(autotest_dir):
1382 """Enumerate autotest control files.
1383
1384 Args:
1385 autotest_dir: autotest folder
1386
1387 Returns:
1388 list of paths to control files
1389 """
1390 # Where to find control files. Relative to autotest_dir.
1391 subpaths = [
1392 'server/site_tests',
1393 'client/site_tests',
1394 'server/tests',
1395 'client/tests',
1396 ]
1397
1398 blacklist = ['site-packages', 'venv', 'results', 'logs', 'containers']
1399 result = []
1400 for subpath in subpaths:
1401 path = os.path.join(autotest_dir, subpath)
1402 for root, dirs, files in os.walk(path):
1403
1404 for black in blacklist:
1405 if black in dirs:
1406 dirs.remove(black)
1407
1408 for filename in files:
1409 if filename == 'control' or filename.startswith('control.'):
1410 result.append(os.path.join(root, filename))
1411
1412 return result
1413
1414
1415def get_autotest_test_info(autotest_dir, test_name):
1416 """Get metadata of given test.
1417
1418 Args:
1419 autotest_dir: autotest folder
1420 test_name: test name
1421
1422 Returns:
1423 AutotestControlInfo object. None if test not found.
1424 """
1425 for control_file in enumerate_autotest_control_files(autotest_dir):
1426 info = parse_autotest_control_file(control_file)
1427 if info.name == test_name:
1428 return info
1429 return None
1430
1431
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001432def detect_branch_level(branch):
1433 """Given a branch name of manifest-internal, detect it's branch level.
1434
1435 level1: if ChromeOS version is x.0.0
1436 level2: if ChromeOS version is x.x.0
1437 level3: if ChromeOS version is x.x.x
1438 Where x is an non-zero integer.
1439
1440 Args:
1441 branch: branch name or ref name in manifest-internal
1442
1443 Returns:
1444 An integer indicates the branch level, or zero if not detectable.
1445 """
1446 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1447 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1448 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1449
1450 if re.match(level1, branch):
1451 return 1
1452 if re.match(level2, branch):
1453 return 2
1454 if re.match(level3, branch):
1455 return 3
1456 return 0
1457
1458
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001459class ChromeOSSpecManager(codechange.SpecManager):
1460 """Repo manifest related operations.
1461
1462 This class enumerates chromeos manifest files, parses them,
1463 and sync to disk state according to them.
1464 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001465
1466 def __init__(self, config):
1467 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001468 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1469 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001470 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1471 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001472 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001473 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001474 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001475 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1476 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001477
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001478 def lookup_snapshot_manifest_revisions(self, old, new):
1479 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001480
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001481 Returns:
1482 list of (timestamp, commit_id, snapshot_id):
1483 timestamp: integer unix timestamp
1484 commit_id: a string indicates commit hash
1485 snapshot_id: a string indicates snapshot id
1486 """
1487 assert is_cros_snapshot_version(old)
1488 assert is_cros_snapshot_version(new)
1489
1490 gs_path = (
1491 'gs://chromeos-image-archive/{board}-postsubmit/{version}-*/image.zip')
1492 # Try to guess the commit time of a snapshot manifest, it is usually a few
1493 # minutes different between snapshot manifest commit and image.zip
1494 # generate.
1495 try:
1496 old_timestamp = gsutil_stat_update_time(
1497 gs_path.format(board=self.config['board'], version=old)) - 86400
1498 except subprocess.CalledProcessError:
1499 old_timestamp = None
1500 try:
1501 new_timestamp = gsutil_stat_update_time(
1502 gs_path.format(board=self.config['board'], version=new)) + 86400
1503 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1504 # we can find snapshot 5982
1505 # snapshot_id <= 5982 has different commit message format, so we need
1506 # to identify its id in different ways, see below comment for more info.
1507 new_timestamp = max(new_timestamp, 1558657989 + 1)
1508 except subprocess.CalledProcessError:
1509 new_timestamp = None
1510 result = []
1511 _, _, old_snapshot_id = snapshot_version_split(old)
1512 _, _, new_snapshot_id = snapshot_version_split(new)
1513 repo = self.manifest_internal_dir
1514 path = 'snapshot.xml'
1515 branch = 'snapshot'
1516 commits = git_util.get_history(
1517 repo,
1518 path,
1519 branch,
1520 after=old_timestamp,
1521 before=new_timestamp,
1522 with_subject=True)
1523
1524 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
1525 # subject, as their subjects are all `Annealing manifest snapshot.`.
1526 # So instead we count the snapshot_id manually.
1527 count = 5982
1528 # There are two snapshot_id = 2633 in commit history, ignore the former
1529 # one.
1530 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
1531 # We examine the commits in reverse order as there are some testing
1532 # commits before snapshot_id=2, this method works fine after
1533 # snapshot 2, except snapshot 2633
1534 for commit in reversed(commits):
1535 msg = commit[2]
1536 if commit[1] in ignore_list:
1537 continue
1538
1539 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
1540 if match:
1541 snapshot_id = match.group(1)
1542 elif 'Annealing manifest snapshot' in msg:
1543 snapshot_id = str(count)
1544 count -= 1
1545 else:
1546 continue
1547 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
1548 result.append((commit[0], commit[1], snapshot_id))
1549 # We find commits in reversed order, now reverse it again to chronological
1550 # order.
1551 return list(reversed(result))
1552
1553 def lookup_build_timestamp(self, rev):
1554 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1555 if is_cros_full_version(rev):
1556 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08001557 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001558
1559 def lookup_snapshot_build_timestamp(self, rev):
1560 assert is_cros_snapshot_version(rev)
1561 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
1562
1563 def lookup_release_build_timestamp(self, rev):
1564 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001565 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001566 path = os.path.join('buildspecs', milestone, short_version + '.xml')
1567 try:
1568 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1569 'refs/heads/master', path)
1570 except ValueError:
Kuang-che Wuce2f3be2019-10-28 19:44:54 +08001571 raise errors.InternalError(
1572 '%s does not have %s' % (self.historical_manifest_git_dir, path))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001573 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001574
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001575 def detect_float_spec_branch_level(self, spec):
1576 results = [
1577 detect_branch_level(branch) for branch in git_util.get_branches(
1578 self.manifest_dir, commit=spec.name)
1579 ]
1580 results = [x for x in results if x > 0]
1581 return min(results) if results else 0
1582
1583 def branch_between_float_specs(self, old_spec, new_spec):
1584 if old_spec.spec_type != codechange.SPEC_FLOAT:
1585 return False
1586 if new_spec.spec_type != codechange.SPEC_FLOAT:
1587 return False
1588
1589 level_old = self.detect_float_spec_branch_level(old_spec)
1590 level_new = self.detect_float_spec_branch_level(new_spec)
1591
1592 if not level_old or not level_new:
1593 logger.warning('branch level detect failed, assume master')
1594 return False
1595 return level_old != level_new
1596
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001597 def collect_float_spec(self, old, new, fixed_specs=None):
1598 assert fixed_specs
1599 branch = None
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001600 old_branches = []
1601 new_branches = []
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001602
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08001603 # There is no revision tag in snapshot's xml
1604 if fixed_specs[0].revision:
1605 old_branches = git_util.get_branches(
1606 self.manifest_dir, commit=fixed_specs[0].revision)
1607 if fixed_specs[-1].revision:
1608 new_branches = git_util.get_branches(
1609 self.manifest_dir, commit=fixed_specs[-1].revision)
1610
1611 # 1. if both are not snapshot, do AND operation
1612 # 2. if new is snapshot, branch = master
1613 # 3. if old is snapshot but new is not, respect new's branch
1614 if fixed_specs[0].revision and fixed_specs[-1].revision:
1615 branches = list(set(old_branches) & set(new_branches))
1616 elif not fixed_specs[-1].revision:
1617 branches = ['refs/remotes/origin/master']
1618 else:
1619 branches = new_branches
1620
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001621 if branches:
1622 branch = branches[0]
1623 else:
1624 logger.warning(
1625 'unable to determine float spec branch, '
1626 'old = %s, new = %s', old_branches, new_branches)
1627
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001628 old_timestamp = self.lookup_build_timestamp(old)
1629 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001630 # snapshot time is different from commit time
1631 # usually it's a few minutes different
1632 # 30 minutes should be safe in most cases
1633 if is_cros_snapshot_version(old):
1634 old_timestamp = old_timestamp - 1800
1635 if is_cros_snapshot_version(new):
1636 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001637
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001638 path = os.path.join(self.manifest_dir, 'default.xml')
1639 if not os.path.islink(path) or os.readlink(path) != 'full.xml':
Kuang-che Wue121fae2018-11-09 16:18:39 +08001640 raise errors.InternalError(
1641 'default.xml not symlink to full.xml is not supported')
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001642
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001643 result = []
1644 path = 'full.xml'
1645 parser = repo_util.ManifestParser(self.manifest_dir)
1646 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001647 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001648 result.append(
1649 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
1650 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001651
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001652 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001653 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
1654 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
1655
1656 # case 1: if both are snapshot, return a list of snapshot
1657 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
1658 return self.collect_snapshot_specs(old, new)
1659
1660 # case 2: if both are release version
1661 # return a list of release version
1662 if is_cros_full_version(old) and is_cros_full_version(new):
1663 return self.collect_release_specs(old, new)
1664
1665 # case 3: return a list of release version and append a snapshot
1666 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001667 result = self.collect_release_specs(
1668 version_to_full(self.config['board'], old),
1669 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001670 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08001671 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001672 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08001673 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001674 return result
1675
1676 def collect_snapshot_specs(self, old, new):
1677 assert is_cros_snapshot_version(old)
1678 assert is_cros_snapshot_version(new)
1679
1680 def guess_snapshot_version(board, snapshot_id, old, new):
1681 if old.endswith('-' + snapshot_id):
1682 return old
1683 if new.endswith('-' + snapshot_id):
1684 return new
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001685 gs_path = ('gs://chromeos-image-archive/{board}-postsubmit/'
1686 'R*-{snapshot_id}-*'.format(
1687 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001688 for line in gsutil_ls(gs_path, ignore_errors=True):
1689 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
1690 if m:
1691 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001692 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001693
1694 result = []
1695 path = 'snapshot.xml'
1696 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08001697 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001698 snapshot_version = guess_snapshot_version(self.config['board'],
1699 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08001700 if snapshot_version:
1701 result.append(
1702 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
1703 path))
1704 else:
1705 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001706 return result
1707
1708 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001709 assert is_cros_full_version(old)
1710 assert is_cros_full_version(new)
1711 old_milestone, old_short_version = version_split(old)
1712 new_milestone, new_short_version = version_split(new)
1713
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001714 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001715 for milestone in git_util.list_dir_from_revision(
1716 self.historical_manifest_git_dir, 'refs/heads/master', 'buildspecs'):
1717 if not milestone.isdigit():
1718 continue
1719 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
1720 continue
1721
Kuang-che Wu74768d32018-09-07 12:03:24 +08001722 files = git_util.list_dir_from_revision(
1723 self.historical_manifest_git_dir, 'refs/heads/master',
1724 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001725
1726 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001727 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001728 short_version, ext = os.path.splitext(fn)
1729 if ext != '.xml':
1730 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001731 if (util.is_version_lesseq(old_short_version, short_version) and
1732 util.is_version_lesseq(short_version, new_short_version) and
1733 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001734 rev = make_cros_full_version(milestone, short_version)
1735 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
1736 'refs/heads/master', path)
1737 result.append(
1738 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001739
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001740 def version_key_func(spec):
1741 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001742 return util.version_key_func(short_version)
1743
1744 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001745 assert result[0].name == old
1746 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001747 return result
1748
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001749 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001750 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
1751 if is_cros_full_version(rev):
1752 milestone, short_version = version_split(rev)
1753 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
1754 manifest = git_util.get_file_from_revision(
1755 self.historical_manifest_git_dir, 'refs/heads/master', path)
1756 else:
1757 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
1758 commit_id = revisions[0][1]
1759 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
1760 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001761 return manifest
1762
1763 def get_manifest_file(self, rev):
1764 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001765 manifest_name = 'manifest_%s.xml' % rev
1766 manifest_path = os.path.join(self.manifest_dir, manifest_name)
1767 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001768 f.write(self.get_manifest(rev))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001769 return manifest_name
1770
1771 def parse_spec(self, spec):
1772 parser = repo_util.ManifestParser(self.manifest_dir)
1773 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001774 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001775 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08001776 with open(manifest_path) as f:
1777 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001778 root = parser.parse_single_xml(content, allow_include=False)
1779 else:
1780 root = parser.parse_xml_recursive(spec.name, spec.path)
1781
1782 spec.entries = parser.process_parsed_result(root)
1783 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08001784 if not spec.is_static():
1785 raise ValueError(
1786 'fixed spec %r has unexpected floating entries' % spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08001787 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001788
1789 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001790 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001791
1792 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
1793 # manifest. 'repo sync -m' is not enough
1794 repo_util.init(
1795 self.config['chromeos_root'],
1796 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
1797 manifest_name=manifest_name,
1798 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001799 reference=self.config['chromeos_mirror'],
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001800 )
1801
1802 # Note, don't sync with current_branch=True for chromeos. One of its
1803 # build steps (inside mark_as_stable) executes "git describe" which
1804 # needs git tag information.
1805 repo_util.sync(self.config['chromeos_root'])