blob: 4806f8816c233614dcbfddc3f07067415506679c [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities for reading and manipulating chromeos_version.sh script."""
from datetime import datetime
import logging
import os
from pathlib import Path
import re
import shutil
import tempfile
from typing import Any, Optional, Union
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import git
_PUSH_BRANCH = "temp_auto_checkin_branch"
class VersionUpdateException(Exception):
"""Exception gets thrown for failing to update the version file"""
class VersionInfo:
"""Class to encapsulate the Chrome OS version info scheme.
You can instantiate this class in three ways.
1) using a version file, specifically chromeos_version.sh,
which contains the version information.
2) passing in a string with the 3 version components.
3) using a source repo and calling from_repo().
"""
# Pattern for matching build name format. Includes chrome branch hack.
VER_PATTERN = r"(\d+).(\d+).(\d+)(?:-R(\d+))*"
KEY_VALUE_PATTERN = r"%s=(\d+)\s*$"
VALID_INCR_TYPES = ("chrome_branch", "build", "branch", "patch")
DATE_TIME_FORMAT = "%Y_%m_%d_%H%M%S"
def __init__(
self,
version_string: Optional[str] = None,
chrome_branch: Optional[str] = None,
incr_type: str = "build",
version_file: Optional[Union[str, os.PathLike]] = None,
) -> None:
"""Initialize.
Args:
version_string: Optional 3 component version string to parse.
Contains:
build_number: release build number.
branch_build_number: current build number on a branch.
patch_number: patch number.
chrome_branch: If version_string specified, specify chrome_branch
i.e. 13.
incr_type: How we should increment this version -
chrome_branch|build|branch|patch
version_file: version file location.
"""
if version_file:
if isinstance(version_file, str):
version_file = Path(version_file)
self.version_file = version_file
logging.debug("Using VERSION_FILE = %s", version_file)
self._LoadFromFile()
else:
match = re.search(self.VER_PATTERN, version_string)
self.build_number = match.group(1)
self.branch_build_number = match.group(2)
self.patch_number = match.group(3)
self.patch_number_with_date_time = self.patch_number
self.chrome_branch = chrome_branch
self.version_file = None
self.incr_type = incr_type
@classmethod
def from_repo(cls, source_repo: Union[str, os.PathLike], **kwargs):
kwargs["version_file"] = Path(source_repo) / constants.VERSION_FILE
return cls(**kwargs)
def _GetDateTime(self):
return datetime.now().strftime(self.DATE_TIME_FORMAT)
def _LoadFromFile(self) -> None:
"""Read the version file and set the version components"""
with open(self.version_file, "r", encoding="utf-8") as version_fh:
for line in version_fh:
if not line.strip():
continue
match = self.FindValue("CHROME_BRANCH", line)
if match:
self.chrome_branch = match
logging.debug(
"Set the Chrome branch number to:%s", self.chrome_branch
)
continue
match = self.FindValue("CHROMEOS_BUILD", line)
if match:
self.build_number = match
logging.debug(
"Set the build version to:%s", self.build_number
)
continue
match = self.FindValue("CHROMEOS_BRANCH", line)
if match:
self.branch_build_number = match
logging.debug(
"Set the branch version to:%s", self.branch_build_number
)
continue
match = self.FindValue("CHROMEOS_PATCH", line)
if match:
self.patch_number = match
self.patch_number_with_date_time = self.patch_number
# For developer builds, append a date and time string.
if os.environ.get("CHROMEOS_OFFICIAL") != "1":
self.patch_number_with_date_time += (
f"-d{self._GetDateTime()}"
)
logging.debug(
"Set the patch version to:%s", self.patch_number
)
logging.debug(
"Set the patch version with date to:%s",
self.patch_number_with_date_time,
)
continue
logging.debug(self.VersionString())
def _PushGitChanges(
self,
git_repo: Union[str, os.PathLike],
message: str,
dry_run: bool = False,
push_to: Optional[git.RemoteRef] = None,
) -> None:
"""Push the final commit into the git repo.
Args:
git_repo: Path to the git repository.
message: Commit message.
dry_run: If true, don't actually push changes to the server.
push_to: The remote branch to push the changes to. Defaults to the
tracking branch of the current branch.
"""
if push_to is None:
push_to = git.GetTrackingBranch(
git_repo, for_checkout=False, for_push=True
)
git.RunGit(git_repo, ["add", "-A"])
# It's possible that while we are running on dry_run, someone has
# already committed our change.
try:
git.RunGit(git_repo, ["commit", "-m", message])
except cros_build_lib.RunCommandError:
if dry_run:
return
raise
logging.info(
"Pushing to branch (%s) with message: %s %s.",
push_to,
message,
" (dryrun)" if dry_run else "",
)
git.GitPush(git_repo, _PUSH_BRANCH, push_to, skip=dry_run)
def FindValue(self, key, line):
"""Given the key find the value from the line, if it finds key = value
Args:
key: key to look for
line: string to search
Returns:
None: on a non match
value: for a matching key
"""
match = re.search(self.KEY_VALUE_PATTERN % (key,), line)
return match.group(1) if match else None
def IncrementVersion(self):
"""Updates the version file by incrementing the patch component."""
if not self.incr_type or self.incr_type not in self.VALID_INCR_TYPES:
raise VersionUpdateException(
"Need to specify the part of the version to" " increment"
)
if self.incr_type == "chrome_branch":
self.chrome_branch = str(int(self.chrome_branch) + 1)
# Increment build_number for 'chrome_branch' incr_type to avoid
# crbug.com/213075.
if self.incr_type in ("build", "chrome_branch"):
self.build_number = str(int(self.build_number) + 1)
self.branch_build_number = "0"
self.patch_number = "0"
elif self.incr_type == "branch" and self.patch_number == "0":
self.branch_build_number = str(int(self.branch_build_number) + 1)
else:
self.patch_number = str(int(self.patch_number) + 1)
return self.VersionString()
def UpdateVersionFile(self, message, dry_run, push_to=None) -> None:
"""Update the version file with our current version.
Args:
message: Commit message.
dry_run: Git dryrun.
push_to: A git.RemoteRef object.
"""
if not self.version_file:
raise VersionUpdateException(
"Cannot call UpdateVersionFile without "
"an associated version_file"
)
components = (
("CHROMEOS_BUILD", self.build_number),
("CHROMEOS_BRANCH", self.branch_build_number),
("CHROMEOS_PATCH", self.patch_number),
("CHROME_BRANCH", self.chrome_branch),
)
with tempfile.NamedTemporaryFile(prefix="mvp", mode="w") as temp_fh:
with open(
self.version_file, "r", encoding="utf-8"
) as source_version_fh:
for line in source_version_fh:
for key, value in components:
line = re.sub(
self.KEY_VALUE_PATTERN % (key,),
"%s=%s\n" % (key, value),
line,
)
temp_fh.write(line)
temp_fh.flush()
repo_dir = self.version_file.parent
logging.info("Updating version file to: %s", self.VersionString())
try:
git.CreateBranch(repo_dir, _PUSH_BRANCH)
shutil.copyfile(temp_fh.name, self.version_file)
self._PushGitChanges(repo_dir, message, dry_run, push_to)
finally:
# Update to the remote version that contains our changes. This
# is needed to ensure that we don't build a release using a
# local commit.
git.CleanAndCheckoutUpstream(repo_dir)
def VersionString(self):
"""returns the version string"""
return (
f"{self.build_number}.{self.branch_build_number}."
f"{self.patch_number}"
)
def VersionStringWithDateTime(self):
"""returns the version string with date and time."""
return (
f"{self.build_number}.{self.branch_build_number}."
f"{self.patch_number_with_date_time}"
)
def VersionComponents(self):
"""Return an array of ints of the version fields for comparing."""
return [
int(x)
for x in [
self.build_number,
self.branch_build_number,
self.patch_number,
]
]
@classmethod
def VersionCompare(cls, version_string):
"""Useful method to return a comparable version of a LKGM string."""
return cls(version_string).VersionComponents()
def __lt__(self, other) -> bool:
return self.VersionComponents() < other.VersionComponents()
def __le__(self, other) -> bool:
return self.VersionComponents() <= other.VersionComponents()
def __eq__(self, other: Any) -> bool:
return self.VersionComponents() == other.VersionComponents()
def __ne__(self, other: Any) -> bool:
return self.VersionComponents() != other.VersionComponents()
def __gt__(self, other) -> bool:
return self.VersionComponents() > other.VersionComponents()
def __ge__(self, other) -> bool:
return self.VersionComponents() >= other.VersionComponents()
__hash__ = None
def BuildPrefix(self):
"""Get the build prefix to match the buildspecs in manifest-versions."""
if self.incr_type == "branch":
if self.patch_number == "0":
return "%s." % self.build_number
else:
return "%s.%s." % (self.build_number, self.branch_build_number)
# Default to build incr_type.
return ""
def __str__(self) -> str:
return "%s(%s)" % (self.__class__, self.VersionString())