mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-04 22:18:03 +00:00
fix(install): find bun at standard install locations when PATH misses it (#762)
Fixes #707. shutil.which first, then ~/.bun/bin and the usual Homebrew, Linuxbrew, ~/.local/bin and /usr/local/bin locations, bun.exe on Windows, executable check on POSIX; the resolved path is used for every bun call and its directory is prepended to PATH for bun's own children.
This commit is contained in:
1 parent
a813602972
commit
87d1857139
2 files changed
+91
-4
No files matched your search
@@ -121,5 +121,58 @@ class InstallPortalClis(unittest.TestCase):
|
||||
self.assertIn(f"would install {cli.parent.name}", proc.stdout)
|
||||
|
||||
|
||||
class FindBun(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name)
|
||||
|
||||
def test_find_bun_prefers_path(self):
|
||||
import shutil
|
||||
orig_which = shutil.which
|
||||
shutil.which = lambda name: "/custom/bin/bun" if name == "bun" else None
|
||||
self.addCleanup(setattr, shutil, "which", orig_which)
|
||||
|
||||
self.assertEqual(install_portal_clis.find_bun(), "/custom/bin/bun")
|
||||
|
||||
def test_find_bun_falls_back_to_candidate_directory(self):
|
||||
import shutil
|
||||
orig_which = shutil.which
|
||||
shutil.which = lambda name: None
|
||||
self.addCleanup(setattr, shutil, "which", orig_which)
|
||||
|
||||
binary = "bun.exe" if sys.platform == "win32" else "bun"
|
||||
fake_bin = self.root / ".bun" / "bin"
|
||||
fake_bin.mkdir(parents=True)
|
||||
fake_bun = fake_bin / binary
|
||||
fake_bun.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
if sys.platform != "win32":
|
||||
fake_bun.chmod(0o755)
|
||||
|
||||
found = install_portal_clis.find_bun(candidate_dirs=(str(fake_bin),))
|
||||
self.assertEqual(found, str(fake_bun))
|
||||
|
||||
def test_find_bun_returns_none_when_missing(self):
|
||||
import shutil
|
||||
orig_which = shutil.which
|
||||
shutil.which = lambda name: None
|
||||
self.addCleanup(setattr, shutil, "which", orig_which)
|
||||
|
||||
self.assertIsNone(install_portal_clis.find_bun(candidate_dirs=(str(self.root / "empty"),)))
|
||||
|
||||
def test_missing_bun_message_suggests_export(self):
|
||||
output = io.StringIO()
|
||||
orig_find = install_portal_clis.find_bun
|
||||
install_portal_clis.find_bun = lambda *args, **kwargs: None
|
||||
self.addCleanup(setattr, install_portal_clis, "find_bun", orig_find)
|
||||
|
||||
with redirect_stdout(output):
|
||||
rc = install_portal_clis.main([])
|
||||
self.assertEqual(rc, 1)
|
||||
text = output.getvalue()
|
||||
self.assertIn("bun was not found on PATH", text)
|
||||
self.assertIn("export PATH=", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -30,6 +30,7 @@ install failed.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -96,6 +97,30 @@ def install(cli_dirs: list[Path], bun: str, dry_run: bool, root: Path = ROOT, ru
|
||||
return 0
|
||||
|
||||
|
||||
_BUN_CANDIDATE_DIRS = (
|
||||
"~/.bun/bin", # default for bun.sh installer (curl -fsSL https://bun.sh/install | bash)
|
||||
"/opt/homebrew/bin", # Homebrew on Apple Silicon macOS
|
||||
"/usr/local/bin", # Homebrew on Intel macOS / standard Linux
|
||||
"/home/linuxbrew/.linuxbrew/bin", # Linuxbrew system
|
||||
"~/.linuxbrew/bin", # Linuxbrew user
|
||||
"~/.local/bin", # pipx / user local
|
||||
"/usr/bin", # distro packages
|
||||
)
|
||||
|
||||
|
||||
def find_bun(candidate_dirs: tuple[str, ...] = _BUN_CANDIDATE_DIRS) -> str | None:
|
||||
"""Return `bun` on PATH, or at a standard install location."""
|
||||
found = shutil.which("bun")
|
||||
if found:
|
||||
return found
|
||||
binary = "bun.exe" if sys.platform == "win32" else "bun"
|
||||
for dir_path in candidate_dirs:
|
||||
candidate = Path(dir_path).expanduser() / binary
|
||||
if candidate.is_file() and (sys.platform == "win32" or os.access(candidate, os.X_OK)):
|
||||
return str(candidate)
|
||||
return None
|
||||
|
||||
|
||||
def _force_utf8_output() -> None:
|
||||
"""Write UTF-8 whatever the host's default encoding is (a fork's portal name may not be ASCII)."""
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
@@ -104,20 +129,29 @@ def _force_utf8_output() -> None:
|
||||
reconfigure(encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def main(argv=None) -> int:
|
||||
_force_utf8_output()
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--dry-run", action="store_true", help="list what would be installed, run nothing")
|
||||
args = ap.parse_args()
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
cli_dirs = discover()
|
||||
if not cli_dirs:
|
||||
print("No portal CLIs found under .agents/skills/*/cli/ - nothing to install.")
|
||||
return 0
|
||||
bun = shutil.which("bun")
|
||||
bun = find_bun()
|
||||
if not bun and not args.dry_run:
|
||||
print("bun is not on PATH. Install it from https://bun.sh, open a new shell, and re-run.")
|
||||
print(
|
||||
"bun was not found on PATH and was not found at any of the usual install locations.\n"
|
||||
' - installed via bun.sh? export PATH="$HOME/.bun/bin:$PATH"\n'
|
||||
" - installed via Homebrew/Linuxbrew? brew install bun\n"
|
||||
" - installed elsewhere? reinstall to a directory on PATH"
|
||||
)
|
||||
return 1
|
||||
if bun:
|
||||
bun_dir = str(Path(bun).parent)
|
||||
if bun_dir not in os.environ.get("PATH", "").split(os.pathsep):
|
||||
os.environ["PATH"] = bun_dir + os.pathsep + os.environ.get("PATH", "")
|
||||
return install(cli_dirs, bun or "bun", args.dry_run)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user