1# Copyright 2023 The ChromiumOS Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""A collection of tools used by the PGO scripts here.""" 6 7import contextlib 8import logging 9import os 10from pathlib import Path 11import re 12import shlex 13import subprocess 14import sys 15import tempfile 16from typing import Any, Dict, Generator, IO, List, Optional, Union 17 18 19Command = List[Union[str, Path]] 20 21 22def run( 23 command: Command, 24 cwd: Optional[Path] = None, 25 check: bool = True, 26 extra_env: Optional[Dict[str, str]] = None, 27 stdout: Union[IO[Any], int, None] = None, 28 stderr: Union[IO[Any], int, None] = None, 29) -> subprocess.CompletedProcess: 30 """Convenient wrapper around subprocess.run.""" 31 if extra_env: 32 env = dict(os.environ) 33 env.update(extra_env) 34 else: 35 env = None 36 37 if logging.getLogger().isEnabledFor(logging.DEBUG): 38 c = shlex.join(str(x) for x in command) 39 dir_extra = f" in {cwd}" if cwd is not None else "" 40 logging.debug("Running `%s`%s", c, dir_extra) 41 42 return subprocess.run( 43 command, 44 check=check, 45 cwd=cwd, 46 env=env, 47 encoding="utf-8", 48 stdin=subprocess.DEVNULL, 49 stdout=stdout, 50 stderr=stderr, 51 ) 52 53 54def installed_llvm_has_pgo_generate_enabled() -> bool: 55 """Returns whether the currently-installed LLVM has USE=pgo_generate.""" 56 equery_output = run( 57 ["equery", "--no-color", "--no-pipe", "u", "sys-devel/llvm"], 58 stdout=subprocess.PIPE, 59 ).stdout 60 61 # The output of `equery` is in the format: 62 # `${default_state_if_emerged} ${state_of_installed_pkg} llvm_pgo_generate` 63 # 64 # The relevant bit is the second. 65 r = re.compile(r"^ [+-] ([+-]) llvm_pgo_generate\s", re.MULTILINE) 66 results = r.findall(equery_output) 67 if not results: 68 raise ValueError( 69 "No llvm_pgo_generate line found in USE for sys-devel/llvm" 70 ) 71 72 if len(results) > 1: 73 raise ValueError( 74 "Multiple llvm_pgo_generate line found in USE for sys-devel/llvm" 75 ) 76 77 return results[0] == "+" 78 79 80def quickpkg_llvm() -> Path: 81 """Runs quickpkg to generate an LLVM binpkg.""" 82 if installed_llvm_has_pgo_generate_enabled(): 83 # If you do want this, feel free to find this check and bypass it. 84 # There's nothing _inherently wrong_ with using a +pgo_generate LLVM. 85 # It'll just take *a lot* of extra time (2.5x+) for no reason. If you 86 # want to start fresh: 87 # ``` 88 # sudo rm -rf /var/lib/portage/pkgs/sys-devel/llvm*tbz2 && \ 89 # sudo emerge -G sys-devel/llvm 90 # ``` 91 raise ValueError( 92 "Base LLVM version has pgo_generate enabled; this is " 93 "almost definitely not what you want. You can " 94 "quickly restore to a non-pgo_generate LLVM by " 95 "running `sudo emerge -G sys-devel/llvm`." 96 ) 97 98 logging.info("Building binpackage for existing sys-devel/llvm installation") 99 quickpkg_result = run( 100 ["quickpkg", "sys-devel/llvm"], stdout=subprocess.PIPE 101 ).stdout 102 # We have to scrape for the package's name, since the package generated is 103 # for the _installed_ version of LLVM, which might not match the current 104 # ebuild's version. 105 matches = re.findall( 106 r"Building package for sys-devel/(llvm-[^ ]+) ", quickpkg_result 107 ) 108 if len(matches) != 1: 109 raise ValueError( 110 f"Couldn't determine LLVM version from {quickpkg_result!r};" 111 f"candidates: {matches}" 112 ) 113 114 llvm_ver = matches[0] 115 pkg = Path("/var/lib/portage/pkgs/sys-devel", llvm_ver + ".tbz2") 116 assert pkg.exists(), f"expected binpkg at {pkg} not found" 117 return pkg 118 119 120def generate_quickpkg_restoration_command(quickpkg_path: Path) -> Command: 121 """Returns a command you can run to restore the quickpkg'ed package.""" 122 package_ver = quickpkg_path.stem 123 category = quickpkg_path.parent.name 124 return ["sudo", "emerge", "--usepkgonly", f"={category}/{package_ver}"] 125 126 127def is_in_chroot() -> bool: 128 """Returns whether this script was invoked inside of the chroot.""" 129 return Path("/etc/cros_chroot_version").exists() 130 131 132def exit_if_not_in_chroot(): 133 """Calls sys.exit if this script was not run inside of the chroot.""" 134 if not is_in_chroot(): 135 sys.exit("Run me inside of the chroot.") 136 137 138def exit_if_in_chroot(): 139 """Calls sys.exit if this script was run inside of the chroot.""" 140 if is_in_chroot(): 141 sys.exit("Run me outside of the chroot.") 142 143 144@contextlib.contextmanager 145def temporary_file(prefix: Optional[str] = None) -> Generator[Path, None, None]: 146 """Yields a temporary file name, and cleans it up on exit. 147 148 This differs from NamedTemporaryFile in that it doesn't keep a handle to 149 the file open. This is useful if the temporary file is intended to be 150 passed to another process (e.g., through a flag), since that other process 151 might/might not overwite the file, leading to subtle bugs about reusing the 152 File-like aspects of NamedTemporaryFile. 153 """ 154 fd, tmp = tempfile.mkstemp(prefix=prefix) 155 tmp_path = Path(tmp) 156 try: 157 os.close(fd) 158 yield tmp_path 159 finally: 160 tmp_path.unlink(missing_ok=True) 161