blob: 20bc7b23202b109384c6dad4e52323efa0492b7d [file] [log] [blame]
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Sets up src/third_party/llvm-project for cros workon from an LLVM ebuild."""
import argparse
import dataclasses
import logging
from pathlib import Path
import shlex
import subprocess
from typing import Iterable, List, Optional, Union
from cros_utils import cros_paths
from llvm_tools import cros_llvm_repo
from llvm_tools import git_llvm_rev
from llvm_tools import llvm_project_base_commit
@dataclasses.dataclass(frozen=True)
class LLVMSourceDir:
"""An LLVM source dir, with convenient additional accessors."""
path: Path
def cros_workon_subdir(self):
"""Returns the subdir used for communicating with the ebuild."""
return self.path / ".ebuild"
def apply_patches(
llvm_dir: LLVMSourceDir,
patch_manager: Path,
patch_metadata_file: Path,
current_rev: git_llvm_rev.Rev,
continue_on_failure: bool = False,
) -> None:
"""Applies patches using `patch_manager` to `llvm_dir`."""
cmd: List[Union[str, Path]] = [
patch_manager,
f"--svn_version={current_rev.number}",
f"--src_path={llvm_dir.path}",
f"--patch_metadata_file={patch_metadata_file}",
]
if continue_on_failure:
cmd.append("--failure_mode=continue")
subprocess.run(
cmd,
check=True,
stdin=subprocess.DEVNULL,
)
def write_patch_application_stamp(
llvm_dir: LLVMSourceDir, package_name: str
) -> None:
"""Writes a stamp file to note that patches have been applied."""
stamp_path = (
llvm_dir.cros_workon_subdir()
/ "stamps"
/ "patches_applied"
/ package_name
)
stamp_path.parent.mkdir(parents=True, exist_ok=True)
stamp_path.touch()
def try_rev_parse(git_dir: Path, sha: str) -> Optional[str]:
"""Returns the `git rev-parse` output for `sha`.
Returns:
None if `sha` can't be resolved, or isn't an object in `git_dir`.
"""
result = subprocess.run(
# Note that the `^{object}` suffix is needed so `git rev-parse
# --verify` actually looks for an object with the given SHA.
["git", "rev-parse", "--verify", sha + "^{object}"],
check=False,
cwd=git_dir,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
encoding="utf-8",
)
if result.returncode:
return None
return result.stdout.strip()
def fetch_and_resolve_sha(llvm_project_dir: Path, sha: str) -> str:
"""Runs `git rev-parse` on `sha`. If that fails, tries fetching the repo."""
if r := try_rev_parse(llvm_project_dir, sha):
return r
logging.info("Couldn't resolve SHA %s locally; fetching upstream...", sha)
cros_llvm_repo.fetch_upstream(llvm_project_dir)
if r := try_rev_parse(llvm_project_dir, sha):
return r
raise ValueError(f"SHA {sha} can't be resolved in {llvm_project_dir}")
def _search_overlays_for_ebuild_dir(
package_name: str, overlays: Iterable[Path]
) -> Path:
for overlay in overlays:
potential_path = overlay / package_name
potential_ebuild = potential_path / f"{potential_path.name}-9999.ebuild"
if potential_ebuild.is_file():
return potential_path
raise ValueError(f"Could not find 9999 ebuild for {package_name}")
def main(argv: List[str]) -> None:
cros_root = cros_paths.script_chromiumos_checkout_or_exit()
logging.basicConfig(
format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
"%(message)s",
level=logging.INFO,
)
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--llvm-dir",
type=lambda x: LLVMSourceDir(path=Path(x)),
default=LLVMSourceDir(path=cros_root / cros_paths.LLVM_PROJECT),
help="Path containing a directory with llvm sources.",
)
parser.add_argument(
"--ebuild-dir",
type=Path,
help="""
Directory of the ebuild we're trying to set up. If this isn't
specified, `--package` should be specified, and this will be
autodetected. Example: ${cros_overlay}/sys-devel/llvm.
""",
)
parser.add_argument(
"--clean-llvm",
action="store_true",
help="""
If passed, a series of commands will be run to reset the LLVM directory
to HEAD prior to applying patches. **This flag deletes all staged
unstaged changes, and deletes all untracked files**.
""",
)
parser.add_argument(
"--package",
help="""
Name of the package to set up for, in the form '${CATEGORY}/${PN}'.
This must be provided unless `--ebuild-dir` is provided. Example:
sys-devel/llvm.
""",
)
parser.add_argument(
"--no-commit",
dest="commit",
action="store_false",
help="Don't create a commit with all changes applied.",
)
parser.add_argument(
"--continue-on-failure",
action="store_true",
help="Keep applying later patches even if an earlier patch fails.",
)
parser.add_argument(
"--workon-board",
dest="workon_board",
help="""
Ensure cros workon for the given board after applying changes.
Set to 'host' for working on the host system, and not a board.
""",
)
checkout_group = parser.add_mutually_exclusive_group(required=True)
checkout_group.add_argument(
"--checkout",
help="""
If specified, the llvm directory will be checked out to the given SHA.
""",
)
# The value of this isn't used anywhere; it's just used as an explicit
# nudge for checking LLVM out.
checkout_group.add_argument(
"--no-checkout",
action="store_true",
help="""
Don't check llvm-project out to anything special before running. Useful
if you'd like to, for example, workon multiple LLVM projects
simultaneously and have already called `setup_for_workon` on another
one.
""",
)
opts = parser.parse_args(argv)
ebuild_dir = opts.ebuild_dir
package = opts.package
if not ebuild_dir and not package:
parser.error(
"At least one of --ebuild-dir or --package must be specified."
)
if not ebuild_dir:
# All of these are either in toolchains-overlay or chromiumos-overlay.
overlays = (
cros_root / d
for d in (
cros_paths.TOOLCHAINS_OVERLAY,
cros_paths.CHROMIUMOS_OVERLAY,
)
)
ebuild_dir = _search_overlays_for_ebuild_dir(package, overlays)
logging.info("Ebuild directory is %s.", ebuild_dir)
elif not package:
package = f"{ebuild_dir.parent.name}/{ebuild_dir.name}"
logging.info("Package is %s.", package)
git_housekeeping_commands: List[List[Union[Path, str]]] = []
if opts.clean_llvm:
git_housekeeping_commands.extend(
(
["git", "clean", "-fd", "."],
["git", "reset", "--hard", "HEAD"],
)
)
if opts.checkout is not None:
sha = fetch_and_resolve_sha(opts.llvm_dir.path, opts.checkout)
git_housekeeping_commands.append(
["git", "checkout", "--quiet", sha],
)
for cmd in git_housekeeping_commands:
subprocess.run(
cmd,
cwd=opts.llvm_dir.path,
check=True,
stdin=subprocess.DEVNULL,
)
rev = git_llvm_rev.translate_sha_to_rev(
git_llvm_rev.LLVMConfig(
remote="cros",
dir=opts.llvm_dir.path,
),
subprocess.run(
["git", "rev-parse", "HEAD"],
check=True,
cwd=opts.llvm_dir.path,
stdin=subprocess.DEVNULL,
encoding="utf-8",
stdout=subprocess.PIPE,
).stdout.strip(),
)
logging.info("Applying patches...")
files_dir = ebuild_dir / "files"
apply_patches(
opts.llvm_dir,
patch_manager=files_dir / "patch_manager" / "patch_manager.py",
patch_metadata_file=files_dir / "PATCHES.json",
current_rev=rev,
continue_on_failure=opts.continue_on_failure,
)
# Assume that the package format is <category>/<name>
_, package_name = package.split("/")
write_patch_application_stamp(opts.llvm_dir, package_name)
logging.info("Applying Gentoo CMakeLists.txt hack...")
llvm_project_base_commit.write_all_gentoo_cmake_hacks(
opts.llvm_dir.path,
chromiumos_overlay=cros_root / cros_paths.CHROMIUMOS_OVERLAY,
)
if opts.commit:
subprocess.run(
["git", "add", "."],
check=True,
cwd=opts.llvm_dir.path,
stdin=subprocess.DEVNULL,
)
subprocess.run(
[
"git",
"commit",
"--message",
"Patches applied and markers added.",
],
check=True,
cwd=opts.llvm_dir.path,
stdin=subprocess.DEVNULL,
)
if not opts.workon_board:
logging.warning(
"Didn't ensure 'workon' for any board or host."
" Make sure you've called 'cros workon [...] start %s'"
" before building!",
package,
)
return
if opts.workon_board == "host":
cmd = ["cros", "workon", "--host", "start", package]
else:
cmd = [
"cros",
"workon",
f"-b={opts.workon_board}",
"start",
package,
]
subprocess.run(cmd, check=True, stdin=subprocess.DEVNULL)
logging.info(
"Successfully workon-ed: %s", shlex.join([str(c) for c in cmd])
)