blob: 2842c34a30df4d0502c699bd994eaf1c8165d445 [file] [log] [blame] [edit]
# Copyright 2021 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Uprev Chrome OS LTS kernel.
Update Chrome OS manifest commit ids to the latest LTS commit ids used in
kernel repo.
Assumptions:
1. The user has a full chromiumos checkout.
2. XML structure follows //project/{name|path|revision}.
3. Kernel repos have name="chromiumos/third_party/kernel".
4. Each kernel repo revision is unique.
"""
import datetime
import logging
import os
from pathlib import Path
import re
from typing import Dict, List, Optional
import xml.etree.ElementTree as ET
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import uri_lib
def _parse_iso_date_str(date_str: str) -> datetime.datetime:
"""Transform ISO date string into a datetime.
Args:
date_str: ISO date string.
Returns:
A datetime object representing the same time as date_str.
"""
try:
return datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S %z')
except TypeError:
return None
class LtsKernelUprev():
"""Class to represent upreving LTS kernels."""
KERNEL_PROJECT_NAME = 'chromiumos/third_party/kernel'
def __init__(self, release_milestone: str, buildroot: Path):
"""Initialize the LtsKernelUprev class.
Args:
release_milestone: Milestone to use for LTS kernel uprev.
buildroot: Path to chromiumos checkout.
"""
self.release_milestone = release_milestone
self.buildroot = buildroot
self.kernel_repo_path = buildroot / 'src/third_party/kernel'
self.manifest_repo_path = buildroot / 'manifest-internal'
self.full_manifest_path = self.manifest_repo_path / 'full.xml'
def get_branch_name(self) -> str:
"""Determine branch stem using the kernel repo path and release milestone.
Returns:
The stem of branches in the kernel repo which contain the release
milestone, or '' if no milestone branches are found.
"""
kernel_upstream_path = self.kernel_repo_path / 'upstream'
release_branches = cros_build_lib.run(
[
'git',
'for-each-ref',
'--format',
'%(refname:lstrip=-1)',
f'refs/remotes/cros/release-{self.release_milestone}-*',
],
capture_output=True,
encoding='utf-8',
cwd=kernel_upstream_path).stdout.splitlines()
return self._get_branch_stem(release_branches)
def _get_branch_stem(self, branches: List[str]) -> str:
"""Determine branch stem given a list of branches.
Args:
branches: All branches containing the release milestone (e.g. ['remotes/
cros/release-R96-14268.B-chromeos-4.4', 'remotes/cros/release-R96-14268.
B-chromeos-5.10']).
Returns:
The stem of branches in the kernel repo which contain the release
milestone (e.g. 'release-R96-14268.B-chromeos-').
"""
# Select any branch because they all share the same prefix.
try:
branch = branches[0].strip()
except IndexError:
return None
# Remove remote prefix.
branch = os.path.basename(branch)
# Find and remove the kernel version.
branch_kernel_version = branch.split('-')[-1]
branch_stem = branch.replace(branch_kernel_version, '')
return branch_stem
def find_new_kernel_commit_ids(
self, branch_stem: str, manifest_tree: ET.ElementTree) -> Dict[str, str]:
"""Determine latest commit id for each kernel repo.
Args:
branch_stem: The stem of branches for the release milestone.
manifest_tree: Representation of manifest XML hierarchy.
Returns:
Mapping of strings to find -> replace in XML manifest.
"""
replace_mapping = {}
for kernel_element in manifest_tree.getroot().findall('.//project'):
if kernel_element.attrib['name'] == self.KERNEL_PROJECT_NAME:
kernel_path = self.buildroot / kernel_element.attrib['path']
orig_kernel_version = os.path.basename(kernel_path)
# Check if this is a test branch (e.g. v5.10-arcvm).
if re.match(r'^v[0-9\.]+-[a-zA-Z]+$', orig_kernel_version):
logging.info('Leaving test branch unchanged: %s', orig_kernel_version)
replace_mapping[orig_kernel_version] = {
'original_revision': kernel_element.attrib['revision'],
'new_revision': kernel_element.attrib['revision'],
'original_date_str': None,
'new_date_str': None,
}
continue
# Remove first 'v' from repo kernel version (e.g. v5.10) to get
# branch-style kernel version (e.g. 5.10).
kernel_version = orig_kernel_version.replace('v', '', 1)
branch_name = f'{branch_stem}{kernel_version}'
if not os.path.exists(kernel_path):
logging.warning('Could not find "%s". Skipping.', kernel_path)
continue
try:
branch_tag = cros_build_lib.run(
['git', 'describe', f'remotes/cros/{branch_name}'],
capture_output=True,
encoding='utf-8',
cwd=kernel_path).stdout.strip()
except cros_build_lib.RunCommandError as e:
logging.warning(
'No tag in "%s" for branch "remotes/cros/%s: %s". Skipping.',
kernel_path, branch_name, e)
continue
if not branch_tag:
logging.warning('No branch found for "%s": "%s"', kernel_path,
branch_name)
continue
branch_commit_id = git.GetGitRepoRevision(
kernel_path, branch=branch_tag)
orig_rev = kernel_element.attrib['revision']
kernel_element.attrib['revision'] = branch_commit_id
if orig_rev != branch_commit_id:
orig_date_str = self.get_commit_date(kernel_path, orig_rev)
new_date_str = self.get_commit_date(kernel_path, branch_commit_id)
replace_mapping[orig_kernel_version] = {
'original_revision': orig_rev,
'new_revision': branch_commit_id,
'original_date_str': orig_date_str,
'new_date_str': new_date_str,
}
logging.info('Version %s: replace "%s" with "%s"', kernel_version,
orig_rev, branch_commit_id)
return replace_mapping
def get_commit_date(self, kernel_repo: str, commit_id: str) -> str:
"""Determine date of a given commit.
Args:
kernel_repo: Path to repo containing commit id.
commit_id: Unique id representing a commit in kernel_repo.
Returns:
Date of commit.
"""
if not git.IsSHA1(commit_id):
logging.warning(
'Error checking date of "%s": not a valid SHA1, skipping.', commit_id)
return None
cmd = [
'git',
'show',
'--no-patch',
'--no-notes',
'--pretty=%cd',
'--date=iso',
commit_id,
]
return cros_build_lib.run(
cmd, capture_output=True, encoding='utf-8',
cwd=kernel_repo).stdout.strip('\n')
def remove_invalid_revisions(self, replace_mapping: Dict[str, str]):
"""Remove commit id revisions that are invalid by modifying dict.
Revisions are invalid if the replacement commit id occurred before the
original commit id.
Args:
replace_mapping: Mapping from original to new commit id.
"""
invalid_revisions = []
for kernel_version, revisions in replace_mapping.items():
orig_date_str = revisions['original_date_str']
new_date_str = revisions['new_date_str']
orig_date = _parse_iso_date_str(orig_date_str)
new_date = _parse_iso_date_str(new_date_str)
if orig_date and new_date and orig_date > new_date:
kernel_path = self.kernel_repo_path / kernel_version
logging.warning(
'Skipping "%s": commit id "%s" (%s) more recent than new commit id '
'"%s" (%s)', kernel_path, revisions['original_revision'],
orig_date_str, revisions['new_revision'], new_date_str)
invalid_revisions.append(kernel_version)
for rev in invalid_revisions:
del replace_mapping[rev]
def pretty_update_xml(self, replace_mapping: Dict[str, str],
full_manifest_path: str):
"""Use replace_mapping to update manifest XML retaining formatting.
Args:
replace_mapping: Mapping of strings to find -> replace in XML manifest.
full_manifest_path: Path to XML manifest.
"""
manifest_str = osutils.ReadFile(full_manifest_path)
for revisions in replace_mapping.values():
orig_str = f'revision="{revisions["original_revision"]}"'
new_str = f'revision="{revisions["new_revision"]}"'
manifest_str = manifest_str.replace(orig_str, new_str)
osutils.WriteFile(full_manifest_path, manifest_str)
def create_cl(self, commit_message: str) -> str:
"""Create and upload a CL in the manifest repo.
Args:
commit_message: CL commit message.
Returns:
URI of uploaded CL.
"""
try:
git.AddPath(self.full_manifest_path)
git.Commit(self.manifest_repo_path, commit_message)
git_stdout = git.UploadCL(
self.manifest_repo_path, 'cros-internal', 'main',
capture_output=True).stdout
return uri_lib.ShortenUri(git.GetUrlFromRemoteOutput(git_stdout))
except cros_build_lib.RunCommandError as e:
cros_build_lib.Die(f'Error creating CL: {e}')
def get_parser() -> commandline.ArgumentParser:
"""Creates the argparse parser.
Returns:
Argument parser.
"""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
'--release', required=True, help='Release milestone, e.g. R96')
parser.add_argument(
'--buildroot',
default=constants.SOURCE_ROOT,
type='path',
help='Path to chromiumos checkout')
return parser
def parse_args(argv: List[str]) -> commandline.ArgumentNamespace:
"""Parse and validate CLI arguments.
Args:
argv: Arguments passed via CLI.
Returns:
Validated argument namespace.
"""
parser = get_parser()
options = parser.parse_args(argv)
if not re.match(r'^R[0-9]+$', options.release):
cros_build_lib.Die(
'Please provide a release milestone of the format R[release number] '
'e.g. R96')
options.buildroot = Path(options.buildroot)
options.Freeze()
return options
def main(argv: List[str]) -> Optional[int]:
options = parse_args(argv)
lts = LtsKernelUprev(options.release, options.buildroot)
# Determine branch stem based on kernel repo and release milestone.
logging.info('Using release "%s" and chromiumos checkout at "%s"',
lts.release_milestone, lts.buildroot)
# Ensure kernel refs are up-to-date.
git.RunGit(lts.kernel_repo_path / 'upstream', ['fetch', 'cros'])
branch_stem = lts.get_branch_name()
if not branch_stem:
cros_build_lib.Die(
f'Could not determine a branch stem for repo "{lts.kernel_repo_path}" '
f'and release "{lts.release_milestone}".')
logging.info('Using branch stem "%s"', branch_stem)
# Ensure manifest repo is up-to-date.
git.SyncPushBranch(lts.manifest_repo_path, 'cros-internal',
'refs/remotes/cros-internal/main')
git.RunGit(lts.manifest_repo_path, ['fetch', 'cros-internal'])
# Read manifest XML.
try:
manifest_tree = ET.parse(lts.full_manifest_path)
except ET.ParseError as e:
cros_build_lib.Die(f'Error parsing XML: {e}')
# Find new commit ids for each kernel repo, ensuring they are newer than the
# existing commit ids.
replace_mapping = lts.find_new_kernel_commit_ids(branch_stem, manifest_tree)
lts.remove_invalid_revisions(replace_mapping)
# Using built-in XML functionality causes formatting changes.
# Use string.replace to preserve formatting.
lts.pretty_update_xml(replace_mapping, lts.full_manifest_path)
# If available, add date details for manifest commit id changes to give
# context in the CL.
commit_details = ''
for kernel_version, revs in replace_mapping.items():
if revs['original_date_str'] and revs['new_date_str']:
commit_details += (
f'\nKernel {kernel_version}: Replaced commit id from '
f'"{revs["original_date"]}" with commit id from "revs["new_date"]"')
commit_message = f"""LTS: update kernel commit ids for LTS branches
This CL was created automatically. For more details see go/
chromeos-lts-kernel-uprev.
{commit_details}
BUG=None
TEST=CQ"""
uploaded_cl_uri = lts.create_cl(commit_message)
if uploaded_cl_uri:
logging.info('Successfully uploaded CL: %s', uploaded_cl_uri)