blob: ef49507733da2d60b2bf2cfad6b6d99187d16148 [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Find LKGM or older latest version of ChromeOS image for a board
This module reads //chromeos/CHROMEOS_LKGM file in a chrome checkout to
determine what the current LKGM version is.
"""
import logging
import os
from typing import Optional
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import gs
from chromite.lib import osutils
from chromite.lib import path_util
class Error(Exception):
"""Base class for the errors happened upon finding ChromeOS image version"""
class NoChromiumSrcDir(Error):
"""Error thrown when no chromium src dir is found."""
def __init__(self, path):
super().__init__(f"No chromium src dir found in {path}")
class MissingLkgmFile(Error):
"""Error thrown when we cannot get the version from CHROMEOS_LKGM."""
def __init__(self, path):
super().__init__(f"Cannot parse CHROMEOS_LKGM file: {path}")
def GetChromeLkgm(chrome_src_dir: str = "") -> Optional[str]:
"""Get the CHROMEOS LKGM checked into the Chrome tree.
Args:
chrome_src_dir: chrome source directory.
Returns:
Version number in format '10171.0.0'.
"""
if not chrome_src_dir:
chrome_src_dir = path_util.DetermineCheckout().chrome_src_dir
if not chrome_src_dir:
return None
lkgm_file = os.path.join(chrome_src_dir, constants.PATH_TO_CHROME_LKGM)
version = osutils.ReadFile(lkgm_file).rstrip()
logging.debug("Read LKGM version from %s: %s", lkgm_file, version)
return version
class ChromeOSVersionFinder:
"""Finds LKGM or latest version of ChromeOS image for a board"""
def __init__(
self,
cache_dir,
board,
fallback_versions,
chrome_src=None,
use_external_config=None,
):
"""Create a new object
Args:
cache_dir: The toplevel cache dir to use.
board: The board to manage the SDK for.
fallback_versions: number of older versions to be considered
chrome_src: The location of the chrome checkout. If unspecified, the
cwd is presumed to be within a chrome checkout.
use_external_config: When identifying the configuration for a board,
force usage of the external configuration if both external and
internal are available.
"""
self.cache_dir = cache_dir
self.board = board
if use_external_config or not self._HasInternalConfig():
self.config_name = f"{board}-{config_lib.CONFIG_TYPE_PUBLIC}"
self.gs_base = f"gs://chromiumos-image-archive/{self.config_name}"
else:
self.config_name = f"{board}-{config_lib.CONFIG_TYPE_RELEASE}"
self.gs_base = f"gs://chromeos-image-archive/{self.config_name}"
self.gs_ctx = gs.GSContext(cache_dir=cache_dir, init_boto=False)
self.fallback_versions = fallback_versions
self.chrome_src = chrome_src
def _HasInternalConfig(self):
"""Determines if the SDK we need is provided by an internal builder.
A given board can have a public and/or an internal builder that
publishes its Simple Chrome SDK. e.g. "amd64-generic" only has a public
builder, "scarlet" only has an internal builder, "octopus" has both. So
if we haven't explicitly passed "--use-external-config", we need to
figure out if we want to use a public or internal builder.
The configs inside gs://chromeos-build-release-console are the proper
source of truth for what boards have public or internal builders.
However, the ACLs on that bucket make it difficult for some folk to
inspect it. So we instead simply assume that everything but the
"*-generic" boards have internal configs.
TODO(b/241964080): Inspect gs://chromeos-build-release-console here
instead if/when ACLs on that bucket are opened up.
Returns:
True if there's an internal builder available that publishes SDKs
for the board.
"""
return "generic" not in self.board
def _GetFullVersionFromStorage(self, version_file):
"""Cat |version_file| in google storage.
Args:
version_file: google storage path of the version file.
Returns:
Version number in the format 'R30-3929.0.0' or None.
"""
try:
# If the version doesn't exist in google storage,
# which isn't unlikely, don't waste time on retries.
full_version = self.gs_ctx.Cat(
version_file, retries=0, encoding="utf-8"
)
assert full_version.startswith("R")
return full_version
except (gs.GSNoSuchKey, gs.GSCommandError):
return None
def _GetFullVersionFromRecentLatest(self, version):
"""Gets the full version number from a recent LATEST- file.
If LATEST-{version} does not exist, we need to look for a recent
LATEST- file to get a valid full version from.
Args:
version: The version number to look backwards from. If version is
not a canary version (ending in .0.0), returns None.
Returns:
Version number in the format 'R30-3929.0.0' or None.
"""
# If version does not end in .0.0 it is not a canary so fail.
if not version.endswith(".0.0"):
return None
version_base = int(version.split(".")[0])
version_base_min = max(version_base - self.fallback_versions, 0)
for v in range(version_base - 1, version_base_min, -1):
version_file = f"{self.gs_base}/LATEST-{v}.0.0"
logging.info("Trying: %s", version_file)
full_version = self._GetFullVersionFromStorage(version_file)
if full_version is not None:
logging.info(
"Using cros version from most recent LATEST file: %s -> %s",
version_file,
full_version,
)
return full_version
logging.warning(
"No recent LATEST file found from %s.0.0 to %s.0.0",
version_base_min,
version_base,
)
return None
def GetFullVersionFromLatest(self, version):
"""Gets the full version number from the LATEST-{version} file.
Args:
version: The version number or branch to look at.
Returns:
Version number in the format 'R30-3929.0.0' or None.
"""
version_file = f"{self.gs_base}/LATEST-{version}"
full_version = self._GetFullVersionFromStorage(version_file)
if full_version is None:
logging.warning("No LATEST file matching SDK version %s", version)
return self._GetFullVersionFromRecentLatest(version)
return full_version