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