Skip to content

fix(npm): detect GNU tar silent failure on Windows installer#134

Merged
jonathanpopham merged 1 commit intomainfrom
fix/windows-gnu-tar-silent-failure
Apr 15, 2026
Merged

fix(npm): detect GNU tar silent failure on Windows installer#134
jonathanpopham merged 1 commit intomainfrom
fix/windows-gnu-tar-silent-failure

Conversation

@jonathanpopham
Copy link
Copy Markdown
Contributor

@jonathanpopham jonathanpopham commented Apr 15, 2026

Summary

Fixes #133. The Windows installer failure on clean machines was two bugs compounding:

  1. GNU tar silent failure. npm postinstall runs under Git Bash on Windows. PATH resolves tar to GNU tar, which parses C:\... as rsh host:path, prints Cannot connect to C: resolve failed, and exits 0 with nothing extracted. The existing try/catch PowerShell fallback never fired because tar didn't throw.
  2. Defender scan-lock on the freshly-downloaded zip. Even when PowerShell did run, Expand-Archive kept failing with The process cannot access the file ... because it is being used by another process. The existing retry loop silently swallowed all 10 retries and returned, so the caller never knew.

Changes

  • Call the native Windows bsdtar explicitly at %SystemRoot%\System32\tar.exe (Windows 10+). It handles both zip format and C:\ paths and isn't blocked by Defender the way Expand-Archive is.
  • Keep PowerShell Expand-Archive as a fallback for older Windows; bump retries 10→20 and — critically — throw $lastErr so exhausted retries surface instead of silently returning.
  • Always verify tmpDir is non-empty after each extraction attempt; tools that exit 0 without writing anything (GNU tar) now get detected and fall through to the next attempt.
  • Wait for the download stream's close event (not just finish) so the fd is actually released before extraction runs.

Test plan

  • node --test npm/install.test.js — 7 tests pass, covering: first-extractor success, fallback on throw, fallback on silent-empty exit, throw-on-exhaustion for both modes, PS retry-loop shape, and path propagation
  • End-to-end on Windows 11 (Git Bash, Node v22.13.1): ran the postinstall against the real v0.6.5 release zip — downloaded, extracted via native bsdtar, ./bin/supermodel.exe --version prints 0.6.5
  • Verify in CI on Linux/mac (non-Windows branches unchanged; tar.gz path untouched)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • More reliable archive installation: waits for files to be fully written before extraction, verifies extracted files exist, and surfaces meaningful errors when extraction fails.
    • If an extractor appears to succeed but produces no files, the installer now automatically falls back to an alternate extraction method.
  • Improvements

    • On Windows, prefers the native archiver when available and uses a PowerShell fallback with retries for robust extraction.
  • Tests

    • Updated tests to cover fallback behavior, retry logic, and post-extract verification.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b30ca703-d5dc-4296-a10c-c3122361aef9

📥 Commits

Reviewing files that changed from the base of the PR and between 7e45459 and 4337945.

📒 Files selected for processing (2)
  • npm/install.js
  • npm/install.test.js
✅ Files skipped from review due to trivial changes (1)
  • npm/install.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • npm/install.js

Walkthrough

download() now waits for file descriptor release before proceeding. extractZip() on Windows prefers native tar.exe, falls back to a PowerShell Expand-Archive with a bounded retry loop, and verifies extraction by reading the temp dir — if no files are produced the fallback runs or the last error is thrown.

Changes

Cohort / File(s) Summary
Installer logic
npm/install.js
download() now invokes callback after the write stream emits close. extractZip() reordered extractors to prefer native tar.exe on Windows, added PowerShell Expand-Archive fallback with retry loop, and performs post-run readdirSync(tmpDir) to require at least one entry; failures throw the last captured error.
Tests
npm/install.test.js
Replaced real-archive creation with fake extraction artifacts; added/adjusted tests to assert extractor ordering, fallback behavior when tar succeeds but produces no files, retries in PowerShell command, command argument checks, and failure modes when extractors fail or produce no files.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Installer as Install.js
    participant Tar as tar.exe / tar (shell)
    participant PowerShell as PowerShell Expand-Archive
    participant FS as TempDir (readdirSync)

    Client->>Installer: start download()
    Installer->>Installer: write stream -> wait for 'close'
    Installer->>Tar: attempt extraction (prefer native tar.exe on Windows)
    Tar-->>Installer: exit code (0/err)
    Installer->>FS: readdirSync(tmpDir)
    alt tmpDir has files
        Installer-->>Client: success (stop)
    else tmpDir empty or read error
        Installer->>PowerShell: run Expand-Archive with retry loop
        PowerShell-->>Installer: exit/result
        Installer->>FS: readdirSync(tmpDir)
        alt tmpDir has files
            Installer-->>Client: success
        else
            Installer-->>Client: throw last error / no-files error
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A download waits till streams say "close",
tar tries first where Windows path woes pose.
If nothing appears, PowerShell will try,
looping, then checking the temp folder's eye.
Errors bubble up — we catch, then recompose.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: detecting when GNU tar silently fails on Windows when interpreting paths as remote hosts, which is the core issue addressed.
Description check ✅ Passed The description provides a clear explanation of both bugs, the specific changes made, and a comprehensive test plan with both unit and manual testing details.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #133: detects silent tar failure by verifying tmpDir is non-empty, implements the fallback to PowerShell Expand-Archive, adds error throwing on exhaustion, and includes comprehensive tests.
Out of Scope Changes check ✅ Passed All changes directly address the Windows installer failure: stream handling fix, tar/PowerShell extraction logic, post-extraction verification, and corresponding unit tests are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-gnu-tar-silent-failure

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
npm/install.test.js (1)

130-132: Optional: tighten the fallback assertion a bit more.

You already assert Expand-Archive; adding a powershell check here would make the intent of this specific path even clearer.

Small assertion add-on
     assert.equal(commands.length, 2, "should fall back to PowerShell when tar produced no files");
     assert.ok(commands[0].startsWith("tar"), "first call should be tar");
+    assert.ok(commands[1].includes("powershell"), "second call should be PowerShell");
     assert.ok(commands[1].includes("Expand-Archive"), "fallback should use Expand-Archive");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@npm/install.test.js` around lines 130 - 132, The test asserts that the
fallback uses Expand-Archive but doesn't explicitly verify PowerShell; update
the assertion in npm/install.test.js that checks the fallback (the commands
array used in the failing-tar path) to also assert that commands[1] includes a
case-insensitive "powershell" token (or startsWith/contains "powershell
-NoProfile -Command") so the intent is explicit; modify the assertion alongside
the existing commands[1].includes("Expand-Archive") check to include the
powershell check for clearer intent.
npm/install.js (1)

58-60: Consider validating the expected artifact instead of only entries.length > 0.

Line 60 can report success if tmpDir already contains unrelated files. A slightly safer check is verifying supermodel.exe exists after tar (or comparing before/after directory state).

Suggested hardening
-    let entries = [];
-    try { entries = fs.readdirSync(tmpDir); } catch { /* missing dir => failure */ }
-    tarOk = entries.length > 0;
+    try {
+      tarOk = fs.existsSync(path.join(tmpDir, "supermodel.exe"));
+    } catch {
+      tarOk = false;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@npm/install.js` around lines 58 - 60, The current success check sets tarOk
based solely on entries.length > 0 which can be fooled by unrelated files;
instead verify the expected artifact (e.g., presence of "supermodel.exe") in
tmpDir after extraction: after reading tmpDir with fs.readdirSync (entries) set
tarOk = entries.includes("supermodel.exe") (or perform a direct
fs.existsSync(path.join(tmpDir, "supermodel.exe"))) and optionally record the
before/after state to ensure the file was newly created rather than
pre-existing; update the logic that sets tarOk and any error handling to rely on
this explicit artifact check rather than a non-empty directory test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@npm/install.js`:
- Around line 58-60: The current success check sets tarOk based solely on
entries.length > 0 which can be fooled by unrelated files; instead verify the
expected artifact (e.g., presence of "supermodel.exe") in tmpDir after
extraction: after reading tmpDir with fs.readdirSync (entries) set tarOk =
entries.includes("supermodel.exe") (or perform a direct
fs.existsSync(path.join(tmpDir, "supermodel.exe"))) and optionally record the
before/after state to ensure the file was newly created rather than
pre-existing; update the logic that sets tarOk and any error handling to rely on
this explicit artifact check rather than a non-empty directory test.

In `@npm/install.test.js`:
- Around line 130-132: The test asserts that the fallback uses Expand-Archive
but doesn't explicitly verify PowerShell; update the assertion in
npm/install.test.js that checks the fallback (the commands array used in the
failing-tar path) to also assert that commands[1] includes a case-insensitive
"powershell" token (or startsWith/contains "powershell -NoProfile -Command") so
the intent is explicit; modify the assertion alongside the existing
commands[1].includes("Expand-Archive") check to include the powershell check for
clearer intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ec562f0-789c-4174-b17b-5a3a6c15871d

📥 Commits

Reviewing files that changed from the base of the PR and between fbaeaeb and 7e45459.

📒 Files selected for processing (2)
  • npm/install.js
  • npm/install.test.js

@jonathanpopham jonathanpopham force-pushed the fix/windows-gnu-tar-silent-failure branch from 7e45459 to 4337945 Compare April 15, 2026 17:50
Real failure chain on Windows was two-layered:

1. GNU tar (resolved from PATH under Git Bash, npm's postinstall shell)
   parses `C:\...` as rsh host:path, prints "Cannot connect to C: resolve
   failed", and exits 0 with nothing extracted. The try/catch PowerShell
   fallback never fired because tar didn't throw.

2. Even when the fallback did run, Windows Defender held a scan-lock on
   the freshly-downloaded .exe-containing zip; Expand-Archive kept
   failing with "The process cannot access the file ... because it is
   being used by another process". The existing retry loop silently
   swallowed all 10 retries and returned, so the caller never knew
   extraction failed.

Fix:

- Call the native Windows bsdtar directly at %SystemRoot%\System32\tar.exe
  (Windows 10+). bsdtar handles both zip format and `C:\` paths natively
  and isn't blocked by Defender the way Expand-Archive is.
- Keep PowerShell Expand-Archive as a fallback for older Windows, with
  the retry loop bumped from 10 → 20 attempts and — critically — a
  `throw $lastErr` so exhausted retries surface instead of silently
  returning.
- Always verify `tmpDir` is non-empty after extraction; if a tool exits 0
  without writing anything, treat that as failure and try the next one.
- Wait for the download stream's `close` event, not just `finish`, so
  the fd is truly released before any extractor runs.

Tests rewritten to cover: first-extractor success, fallback on throw,
fallback on silent-empty success, throw-on-exhaustion for both failure
modes, PS retry-loop shape, and path propagation.

Closes #133
@jonathanpopham jonathanpopham merged commit ae3497f into main Apr 15, 2026
6 of 7 checks passed
@jonathanpopham jonathanpopham deleted the fix/windows-gnu-tar-silent-failure branch April 15, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows install fails: tar misinterprets C:\ path as remote host, supermodel.exe never extracted

1 participant