xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/setup_for_workon.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2024 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Sets up src/third_party/llvm-project for cros workon from an LLVM ebuild."""
7
8import argparse
9import dataclasses
10import logging
11from pathlib import Path
12import re
13import shlex
14import subprocess
15import sys
16from typing import List, Union
17
18import git_llvm_rev
19
20
21@dataclasses.dataclass(frozen=True)
22class LLVMSourceDir:
23    """An LLVM source dir, with convenient additional accessors."""
24
25    path: Path
26
27    def cros_workon_subdir(self):
28        """Returns the subdir used for communicating with the ebuild."""
29        return self.path / ".ebuild"
30
31
32def apply_patches(
33    llvm_dir: LLVMSourceDir,
34    patch_manager: Path,
35    patch_metadata_file: Path,
36    current_rev: git_llvm_rev.Rev,
37) -> None:
38    """Applies patches using `patch_manager` to `llvm_dir`."""
39    subprocess.run(
40        [
41            patch_manager,
42            f"--svn_version={current_rev.number}",
43            f"--src_path={llvm_dir.path}",
44            f"--patch_metadata_file={patch_metadata_file}",
45        ],
46        check=True,
47        stdin=subprocess.DEVNULL,
48    )
49
50
51def find_ebuild_in_dir(ebuild_dir: Path) -> Path:
52    """Returns the path to a 9999 ebuild in `ebuild_dir`; raises if none."""
53    candidates = list(ebuild_dir.glob("*-9999.ebuild"))
54    if len(candidates) != 1:
55        raise ValueError(
56            f"Expected exactly one 9999 ebuild in {ebuild_dir}; found "
57            f"{candidates}"
58        )
59    return candidates[0]
60
61
62def write_gentoo_cmake_hack(llvm_dir: LLVMSourceDir, ebuild_dir: Path) -> None:
63    """Modifies cmake files in LLVM so cmake.eclass doesn't modify them."""
64    # Upstream's `cmake.eclass` will try to override "dangerous" configurations
65    # that override Gentoo settings. There's no way to skip this override, but
66    # it _does_ have logic to detect if it has already run & skips all
67    # modifications in that case. Since LLVM has no such "dangerous" settings,
68    # and the `9999` ebuild never "goes live," it's safe to skip these.
69
70    # The file to modify is the 'main' cmake file, which is determined based on
71    # `CMAKE_USE_DIR`. Parsing that out isn't _too_ painful, so try it.
72    ebuild_path = find_ebuild_in_dir(ebuild_dir)
73    ebuild_contents = ebuild_path.read_text(encoding="utf-8")
74    cmake_use_dir_re = re.compile(
75        # Use string concatenation rather than re.VERBOSE, since this regex
76        # goes in an error message on failure, and that's _really_ hard to
77        # read.
78        r"^\s*"
79        # While these all use `export`, it's not strictly required by
80        # cmake.eclass.
81        r"(?:export\s+)?" r'CMAKE_USE_DIR="\$\{S\}/([^"]+)"',
82        re.MULTILINE,
83    )
84    cmake_use_dirs = cmake_use_dir_re.findall(ebuild_contents)
85    if len(cmake_use_dirs) != 1:
86        raise ValueError(
87            f"Expected to find 1 match of {cmake_use_dir_re} in "
88            f"{ebuild_path}; found {len(cmake_use_dirs)}"
89        )
90
91    cmake_file = llvm_dir.path / cmake_use_dirs[0] / "CMakeLists.txt"
92    special_marker = "<<< Gentoo configuration >>>"
93    if special_marker in cmake_file.read_text(encoding="utf-8"):
94        return
95
96    with cmake_file.open("a", encoding="utf-8") as f:
97        f.write(f"\n# HACK from setup_from_workon.py:\n# {special_marker}")
98
99
100def write_patch_application_stamp(
101    llvm_dir: LLVMSourceDir, package_name: str
102) -> None:
103    """Writes a stamp file to note that patches have been applied."""
104    stamp_path = (
105        llvm_dir.cros_workon_subdir()
106        / "stamps"
107        / "patches_applied"
108        / package_name
109    )
110    stamp_path.parent.mkdir(parents=True, exist_ok=True)
111    stamp_path.touch()
112
113
114def main(argv: List[str]) -> None:
115    logging.basicConfig(
116        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
117        "%(message)s",
118        level=logging.INFO,
119    )
120
121    my_dir = Path(__file__).resolve().parent
122    parser = argparse.ArgumentParser(
123        description=__doc__,
124        formatter_class=argparse.RawDescriptionHelpFormatter,
125    )
126    parser.add_argument(
127        "--llvm-dir",
128        type=lambda x: LLVMSourceDir(path=Path(x)),
129        default=LLVMSourceDir(path=my_dir.parent.parent / "llvm-project"),
130        help="Path containing a directory with llvm sources.",
131    )
132    parser.add_argument(
133        "--ebuild-dir",
134        type=Path,
135        help="""
136        Directory of the ebuild we're trying to set up. If this isn't
137        specified, `--package` should be specified, and this will be
138        autodetected. Example: ${cros_overlay}/sys-devel/llvm.
139        """,
140    )
141    parser.add_argument(
142        "--clean-llvm",
143        action="store_true",
144        help="""
145        If passed, a series of commands will be run to reset the LLVM directory
146        to HEAD prior to applying patches. **This flag deletes all staged
147        unstaged changes, and deletes all untracked files**.
148        """,
149    )
150    parser.add_argument(
151        "--package",
152        help="""
153        Name of the package to set up for, in the form '${CATEGORY}/${PN}'.
154        This must be provided unless `--ebuild-dir` is provided. Example:
155        sys-devel/llvm.
156        """,
157    )
158    parser.add_argument(
159        "--no-commit",
160        dest="commit",
161        action="store_false",
162        help="Don't create a commit with all changes applied.",
163    )
164    parser.add_argument(
165        "--workon-board",
166        dest="workon_board",
167        help="""
168        Ensure cros workon for the given board after applying changes.
169        Set to 'host' for working on the host system, and not a board.
170        """,
171    )
172
173    checkout_group = parser.add_mutually_exclusive_group(required=True)
174    checkout_group.add_argument(
175        "--checkout",
176        help="""
177        If specified, the llvm directory will be checked out to the given SHA.
178        """,
179    )
180    # The value of this isn't used anywhere; it's just used as an explicit
181    # nudge for checking LLVM out.
182    checkout_group.add_argument(
183        "--no-checkout",
184        action="store_true",
185        help="""
186        Don't check llvm-project out to anything special before running. Useful
187        if you'd like to, for example, workon multiple LLVM projects
188        simultaneously and have already called `setup_for_workon` on another
189        one.
190        """,
191    )
192    opts = parser.parse_args(argv)
193
194    ebuild_dir = opts.ebuild_dir
195    package_name = opts.package
196    if not ebuild_dir and not package_name:
197        parser.error(
198            "At least one of --ebuild-dir or --package must be specified."
199        )
200
201    if not ebuild_dir:
202        # All of these are in chromiumos-overlay, so just use that as a basis.
203        ebuild_dir = my_dir.parent.parent / "chromiumos-overlay" / package_name
204        logging.info("Ebuild directory is %s.", ebuild_dir)
205    elif not package_name:
206        package_name = f"{ebuild_dir.parent.name}/{ebuild_dir.name}"
207        logging.info("Package is %s.", package_name)
208
209    git_housekeeping_commands: List[List[Union[Path, str]]] = []
210    if opts.clean_llvm:
211        git_housekeeping_commands += (
212            ["git", "clean", "-fd", "."],
213            ["git", "reset", "--hard", "HEAD"],
214        )
215
216    if opts.checkout is not None:
217        git_housekeeping_commands.append(
218            ["git", "checkout", "--quiet", opts.checkout],
219        )
220
221    for cmd in git_housekeeping_commands:
222        subprocess.run(
223            cmd,
224            cwd=opts.llvm_dir.path,
225            check=True,
226            stdin=subprocess.DEVNULL,
227        )
228
229    rev = git_llvm_rev.translate_sha_to_rev(
230        git_llvm_rev.LLVMConfig(
231            remote="cros",
232            dir=opts.llvm_dir.path,
233        ),
234        subprocess.run(
235            ["git", "rev-parse", "HEAD"],
236            check=True,
237            cwd=opts.llvm_dir.path,
238            stdin=subprocess.DEVNULL,
239            encoding="utf-8",
240            stdout=subprocess.PIPE,
241        ).stdout.strip(),
242    )
243
244    logging.info("Applying patches...")
245    files_dir = ebuild_dir / "files"
246    apply_patches(
247        opts.llvm_dir,
248        patch_manager=files_dir / "patch_manager" / "patch_manager.py",
249        patch_metadata_file=files_dir / "PATCHES.json",
250        current_rev=rev,
251    )
252    write_patch_application_stamp(opts.llvm_dir, package_name)
253    write_gentoo_cmake_hack(opts.llvm_dir, ebuild_dir)
254
255    if opts.commit:
256        subprocess.run(
257            ["git", "add", "."],
258            check=True,
259            cwd=opts.llvm_dir.path,
260            stdin=subprocess.DEVNULL,
261        )
262        subprocess.run(
263            [
264                "git",
265                "commit",
266                "--message",
267                "Patches applied and markers added.",
268            ],
269            check=True,
270            cwd=opts.llvm_dir.path,
271            stdin=subprocess.DEVNULL,
272        )
273
274    if not opts.workon_board:
275        logging.warning(
276            "Didn't ensure 'workon' for any board or host."
277            " Make sure you've called 'cros workon [...] start %s'"
278            " before building!",
279            package_name,
280        )
281        return
282
283    if opts.workon_board == "host":
284        cmd = ["cros", "workon", "--host", "start", package_name]
285    else:
286        cmd = [
287            "cros",
288            "workon",
289            f"-b={opts.workon_board}",
290            "start",
291            package_name,
292        ]
293    subprocess.run(cmd, check=True, stdin=subprocess.DEVNULL)
294    logging.info(
295        "Successfully workon-ed: %s", shlex.join([str(c) for c in cmd])
296    )
297
298
299if __name__ == "__main__":
300    main(sys.argv[1:])
301