|
28 | 28 | import shutil |
29 | 29 | import subprocess |
30 | 30 | import sys |
| 31 | +import tempfile |
31 | 32 | import tomllib |
32 | 33 | from pathlib import Path |
33 | 34 |
|
@@ -100,32 +101,76 @@ def _is_dir(d: Path) -> bool: |
100 | 101 | return False |
101 | 102 |
|
102 | 103 |
|
103 | | -def lookup_path() -> str: |
| 104 | +def lookup_path(extra: list[Path] | None = None) -> str: |
104 | 105 | """PATH augmented with the tool-managed bin dirs (for the presence check).""" |
105 | 106 | parts = os.environ.get("PATH", "").split(os.pathsep) |
106 | 107 | parts += [str(d) for d in EXTRA_BIN_DIRS if _is_dir(d)] |
| 108 | + parts += [str(d) for d in (extra or []) if _is_dir(d)] |
107 | 109 | return os.pathsep.join(parts) |
108 | 110 |
|
109 | 111 |
|
110 | | -def verify_one(dep_id: str, arg: str, binaries: list[str], command: str) -> bool: |
111 | | - """Run one install command; report whether its binaries are reachable.""" |
| 112 | +def build_command(installer: str, template: str, arg: str) -> tuple[str, list[Path]]: |
| 113 | + """Render a recipe's install command and any extra bin dirs to look in. |
| 114 | +
|
| 115 | + Mirrors `Installer::command()`; `.rstrip()` matches its bare `aikup install` |
| 116 | + (empty arg ⇒ latest, no trailing space). |
| 117 | +
|
| 118 | + For `nix`, each recipe is redirected into its own throwaway profile. A real |
| 119 | + user installs ONE tool, but this gate installs every nix recipe into a |
| 120 | + single environment, where two packages that ship the same file collide |
| 121 | + (e.g. rustup and cargo both provide `cargo.bash`). Per-recipe profiles |
| 122 | + remove that false conflict; the nixpkgs attr installed is exactly what |
| 123 | + `doctor` prints — only the profile location differs, in the same spirit as |
| 124 | + the EXTRA_BIN_DIRS PATH augmentation above. |
| 125 | + """ |
| 126 | + command = template.format(arg=arg).rstrip() |
| 127 | + extra: list[Path] = [] |
| 128 | + if installer == "nix": |
| 129 | + profile = Path(tempfile.mkdtemp(prefix="nixprof-")) / "profile" |
| 130 | + command = command.replace( |
| 131 | + "nix profile install ", |
| 132 | + f"nix profile install --profile {profile} ", |
| 133 | + 1, |
| 134 | + ) |
| 135 | + extra.append(profile / "bin") |
| 136 | + return command, extra |
| 137 | + |
| 138 | + |
| 139 | +def verify_one( |
| 140 | + dep_id: str, binaries: list[str], command: str, extra_bin_dirs: list[Path] |
| 141 | +) -> bool: |
| 142 | + """Run one install command; report whether its binaries are reachable. |
| 143 | +
|
| 144 | + Success is defined by the binary being reachable afterwards, NOT by the |
| 145 | + install command's exit code. Some recipes exit non-zero for reasons that |
| 146 | + don't reflect a broken recipe — e.g. brew's rustup post-install step fails |
| 147 | + on a runner that already ships a Rust toolchain, yet `rustup` is installed |
| 148 | + and on PATH. We surface a non-zero exit as a warning but only fail when a |
| 149 | + declared binary is actually missing. |
| 150 | + """ |
112 | 151 | print(f"::group::{dep_id} via {command}", flush=True) |
113 | | - ok = True |
114 | | - try: |
115 | | - subprocess.run(command, shell=True, check=True) |
116 | | - except subprocess.CalledProcessError as e: |
117 | | - print(f" ✗ install command exited {e.returncode}", flush=True) |
118 | | - ok = False |
| 152 | + install_rc = subprocess.run(command, shell=True).returncode |
119 | 153 |
|
120 | | - path = lookup_path() |
| 154 | + path = lookup_path(extra_bin_dirs) |
| 155 | + missing = False |
121 | 156 | for binary in binaries: |
122 | 157 | if shutil.which(binary, path=path): |
123 | 158 | print(f" ✓ {binary} on PATH", flush=True) |
124 | 159 | else: |
125 | 160 | print(f" ✗ {binary} NOT found after install", flush=True) |
126 | | - ok = False |
| 161 | + missing = True |
| 162 | + |
| 163 | + if install_rc != 0: |
| 164 | + if missing: |
| 165 | + print(f" ✗ install command exited {install_rc}", flush=True) |
| 166 | + else: |
| 167 | + print( |
| 168 | + f" ! install command exited {install_rc}, but binaries are " |
| 169 | + f"present — treating as OK", |
| 170 | + flush=True, |
| 171 | + ) |
127 | 172 | print("::endgroup::", flush=True) |
128 | | - return ok |
| 173 | + return not missing |
129 | 174 |
|
130 | 175 |
|
131 | 176 | def main() -> int: |
@@ -155,8 +200,8 @@ def main() -> int: |
155 | 200 |
|
156 | 201 | failures: list[str] = [] |
157 | 202 | for dep_id, arg, binaries in methods: |
158 | | - command = template.format(arg=arg) |
159 | | - if not verify_one(dep_id, arg, binaries, command): |
| 203 | + command, extra_bin_dirs = build_command(args.installer, template, arg) |
| 204 | + if not verify_one(dep_id, binaries, command, extra_bin_dirs): |
160 | 205 | failures.append(dep_id) |
161 | 206 |
|
162 | 207 | print() |
|
0 commit comments