diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 18f7afd5d2..01c43753b0 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -541,7 +541,14 @@ function Get-PinnedGit { # System32; a GNU tar earlier on PATH (Cygwin/MSYS) reads C:\ as a # remote host, so never resolve it from PATH. $inboxTar = Join-Path $env:SystemRoot 'System32\tar.exe' - Invoke-Native { & $inboxTar -xf $tarPath -C $extractDir } + # MSYS ships these as symlinks into /proc. Without symlink rights (not + # elevated, no Developer Mode) tar cannot create them and fails the + # whole extract. Skip exactly the links pm's own extractor skips + # (pm/store.py extract_tar git_msys) so any other failure still fails. + # '^' anchors bsdtar's otherwise any-path-component match. + $msysProcLinks = @('dev/fd', 'dev/stdin', 'dev/stdout', 'dev/stderr', 'etc/mtab') + $excludes = foreach ($link in $msysProcLinks) { '--exclude'; "^$link" } + Invoke-Native { & $inboxTar @excludes -xf $tarPath -C $extractDir } if ($LASTEXITCODE) { Fail "failed to extract pinned git archive" } # Layout: Git-/cmd\git.exe — flatten the single wrapper dir. $inner = @(Get-ChildItem $extractDir) diff --git a/tests/pm/test_store_extract.py b/tests/pm/test_store_extract.py index eabb2bca39..d4ec65d7ba 100644 --- a/tests/pm/test_store_extract.py +++ b/tests/pm/test_store_extract.py @@ -92,6 +92,33 @@ def test_git_tar_skips_only_msys_proc_links_and_rejects_other_unsafe_entries(tmp assert not (tmp_path / "escaped").exists() +def test_install_ps1_bootstrap_skips_the_same_msys_links_as_pm(tmp_path): + """The pre-PM bootstrap's tar.exe excludes must stay the links PM skips.""" + import re + from pathlib import Path + from pm.packages import Git + + installer = Path(__file__).resolve().parents[2] / "scripts" / "install.ps1" + listed = re.search(r"\$msysProcLinks = @\(([^)]*)\)", installer.read_text(encoding="utf-8")).group(1) + excluded = set(re.findall(r"'([^']+)'", listed)) + # The pinned Git-for-Windows archives' symlinks (tar -tvf, 2.53.0.windows.3). + links = {"dev/fd": "/proc/self/fd", "dev/stdin": "/proc/self/fd/0", "dev/stdout": "/proc/self/fd/1", + "dev/stderr": "/proc/self/fd/2", "etc/mtab": "/proc/mounts"} + archive = tmp_path / "git.tar.bz2" + with tarfile.open(archive, "w:bz2") as tf: + for name, target in links.items(): + info = tarfile.TarInfo(name) + info.type, info.linkname = tarfile.SYMTYPE, target + tf.addfile(info) + binary = tarfile.TarInfo("cmd/git.exe") + binary.size = 1 + tf.addfile(binary, io.BytesIO(b"x")) + dest = tmp_path / "out" + Git().unpack(archive, dest, "win32-x64") + skipped = {name for name in links if not os.path.lexists(dest / name)} + assert excluded == skipped == set(links) + + def test_target_uses_shared_native_arch(monkeypatch): from hermes_platform.host import facts from pm import store