Skip to content

feat(windows): scaffolding pass for Windows support#87

Draft
tannevaled wants to merge 6 commits into
pkgxdev:mainfrom
tannevaled:feat/windows-support
Draft

feat(windows): scaffolding pass for Windows support#87
tannevaled wants to merge 6 commits into
pkgxdev:mainfrom
tannevaled:feat/windows-support

Conversation

@tannevaled
Copy link
Copy Markdown
Contributor

@tannevaled tannevaled commented May 18, 2026

Draft — Windows support scaffolding

Windows is an official pkgx target. This draft is a scaffolding pass that lets pkgm itself run on Windows; the open design questions raised in the original draft are now closed (see "Design answers" below).

End-to-end install on Windows is blocked upstream. A probe of all 842 top-level prefixes in dist.pkgx.dev (the bucket pkgx pulls package distributables from) finds zero packages with a windows/ subdir. So pkgm install <anything> on Windows hits Error: CmdNotFound("...") from libpkgx regardless of how complete pkgm's Windows port is. The CI smoke test exercises everything pkgm can verify without a working install (--version, ls on a fresh tree); end-to-end install will pass once upstream ships Windows artifacts.

What's in this PR

  • standardPath() — Windows case returning baseline System32 / System32\Wbem; introduces PATH_SEP module constant (a literal ":" split would shred C:\ drive prefixes).
  • install_prefix() — Windows resolves to %LOCALAPPDATA%\pkgm. Mirrors pkgxdev/setup's installer.ps1 ($env:LOCALAPPDATA\pkgx).
  • ls() / outdated() — walk install_prefix().join("pkgs") on Windows instead of the hard-coded POSIX /usr/local/pkgs + ~/.local/pkgs pair.
  • get_pkgx() — uses PATH_SEP and looks for pkgx.exe on Windows.
  • symlink_with_overwrite() — Windows: hardlink for files (Deno.linkSync, no elevation needed on the same volume) with Deno.copyFileSync as last resort; dir symlinks via Deno.symlinkSync({ type: "dir" }) for the rare directory call site (relies on developer-mode being enabled — true on GHA Windows runners).
  • create_v_symlinks() — early-return on Windows. The v1/v2/… aliases are a navigation convenience; canonical v<x.y.z> paths are what stubs and mirror_directory actually reference.
  • Stub emission — when runtime env is present on Windows, emit a .cmd wrapper at <stem>.cmd that sets the env vars then execs the real binary. Delete the .exe hardlink first so PATHEXT doesn't shadow the wrapper. % in values escaped as %% (batch-file convention).
  • Deno.chmod(0o755) guarded — throws on Windows.
  • CI — new test-windows job, separate from the POSIX test matrix. Two smoke steps via explicit pkgx deno^2.1 run invocation: pkgm --version (validates pkgx-on-PATH + libpkgx imports + parseArgs floor) and pkgm ls (validates install_prefix() + the ls candidate-paths branch).

Design answers (modelled on what pkgx already does)

After surveying pkgxdev/pkgx + pkgxdev/setup:

Question Answer Where pkgx does it
System-wide install prefix None — per-user only, %LOCALAPPDATA%\pkgm pkgxdev/setup/installer.ps1 installs pkgx itself to $env:LOCALAPPDATA\pkgx
Elevation model No UAC. Per-user only. installer.ps1 writes to user-level PATH via [Environment]::SetEnvironmentVariable("Path", …, User) — never elevates
Stub format .cmd files pkgxdev/pkgx/crates/lib/src/utils.rs find_program() looks for .exe → .bat → .cmd extensions
Entrypoint pkgm.cmd wrapper installed by pkgxdev/setup/installer.ps1 (mirror of the POSIX /usr/local/bin/pkgm shebang shim) installer.sh already creates /usr/local/bin/pkgm as #!/usr/bin/env -S pkgx -q! pkgm; installer.ps1 would write a .cmd equivalent

The pkgm.cmd wrapper change belongs in pkgxdev/setup, not this PR.

Upstream gap discovered

While iterating, the CI's install hyperfine smoke step failed with Error: CmdNotFound("hyperfine") despite pkgm doing the right thing on its side. The v2 dist layout (under the v2/ prefix in the dist.tea.xyz bucket) shows 15 of 88 packages have Windows builds today — they're all the toolchain layer (bun.sh, cmake.org, curl.se, deno.land, git-scm.org, go.dev, libarchive.org, nasm.us, ninja-build.org, openssl.org, perl.org, python.org, rust-lang.org, sqlite.org, zlib.net). No application-layer packages (hyperfine, gum, fd, ripgrep, etc.) have Windows artifacts.

Per the maintainer's reply on pkgxdev/pkgx#607: the manifest infrastructure that produced these v2 Windows builds was rolled back about a year ago, and there's currently no pipeline producing new Windows artifacts. So pkgm install <app> on Windows is blocked on reviving (or replacing) that work upstream — not on anything pkgm itself can fix.

This PR is therefore "pkgm Windows-ready, waiting for the upstream build pipeline to come back online".

What's deliberately NOT in this PR

  • shim() — also POSIX-shebang-based, needs .cmd/.ps1 variant.
  • dev_stub_text() — generates POSIX shell with [ -x /usr/local/bin/dev ] || …; needs a Windows variant (or skip dev-mode integration in v1).
  • Junctions via mklink /J as a fallback for create_v_symlinks on machines without developer mode.
  • pkgm.cmd wrapper in pkgxdev/setup — separate PR to that repo.
  • uninstall() Windows path normalisation — needs verifying once installs actually work.

Smaller open implementation questions

  1. Stub shadowing: when both <name>.exe (the hardlink) and <name>.cmd would coexist, we currently delete the .exe and write only the .cmd (for pkgs with runtime env). Pkgs without runtime env keep the hardlinked .exe — meaning users with both env-wrapped and non-env pkgs see mixed extensions in bin/. Acceptable for v1.
  2. Deno.copyFileSync fallback: only kicks in if Deno.linkSync fails (cross-volume etc.). Copy duplicates disk usage. Not a v1 concern.
  3. %LOCALAPPDATA%\pkgm\bin on PATH: the install warns if missing but doesn't add it. pkgxdev/setup/installer.ps1 adds $env:LOCALAPPDATA\pkgx to the user PATH — pkgm install would benefit from the same treatment, also belongs upstream.

Test plan

lint, test (ubuntu-latest), test (macos-latest) should be green — verified locally with deno fmt --check, deno lint, deno check, and ./pkgm.ts i hyperfine + ./pkgm.ts ls smoke runs on macOS.

test-windows should be green via the two pkgm --version / pkgm ls smoke steps. End-to-end install can't be tested until the upstream dist gap closes.

sudo-install red is expected on this branch — that job's regressions are addressed in #86 and are independent of the Windows port.

Relationship to #86

Independent. Small conflicts expected in query_pkgx and get_pkgx — both branches add disjoint code paths.

References used

  • pkgxdev/setup installer.ps1 — Windows install path + PATH manipulation.
  • pkgxdev/pkgx crates/lib/src/config.rsdirs_next::data_local_dir() resolves to %LOCALAPPDATA% on Windows.
  • pkgxdev/pkgx crates/lib/src/utils.rsfind_program() .exe/.bat/.cmd extension lookup.
  • pkgxdev/pkgx crates/cli/src/execve.rs#[cfg(windows)] switch from execve to Command::spawn.
  • dist.pkgx.dev S3 listing — confirmed the 842-prefix Windows gap.

tannevaled and others added 5 commits May 18, 2026 17:31
First-pass changes to let pkgm at least start on Windows so CI can
surface real gaps instead of failing at "/usr/local doesn't exist":

- standardPath(): add Windows case returning the baseline System32 /
  System32\Wbem dirs joined with `;`. Introduces PATH_SEP module
  constant used wherever we tokenise $PATH (a literal `:` split would
  shred `C:\` drive prefixes on Windows).
- install_prefix(): on Windows, skip the /usr/local probe and return
  Path.home()/.local. System-wide installs under %ProgramFiles% need
  UAC modelling — out of scope here.
- get_pkgx(): use PATH_SEP and look for `pkgx.exe` on Windows.
- install() PATH-contains check: use PATH_SEP.
- Stub writing: skip the Deno.chmod(0o755) on Windows (it throws).
  The stub content is still POSIX shell — proper `.cmd`/`.ps1` stub
  emission is a follow-up.
- CI: add windows-latest to the `test` matrix with
  `strategy.fail-fast: false`. The existing `continue-on-error: true`
  keeps the matrix overall green; the Windows leg surfaces what's
  still broken so we can iterate.

Open design questions deliberately left for the draft-PR description:
- System-wide install prefix on Windows (%ProgramFiles%? user-only?)
- Elevation model (UAC? refuse?)
- Stub format (.cmd, .ps1, or compiled wrapper?)
- libpkgx + pkgxdev/setup Windows-readiness verification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surveyed pkgxdev/pkgx + pkgxdev/setup for prior art and applied:

- install_prefix() on Windows now resolves to %LOCALAPPDATA%\pkgm
  (was Path.home()/.local). Mirrors pkgxdev/setup/installer.ps1
  which installs pkgx to $env:LOCALAPPDATA\pkgx, and matches
  libpkgx config.rs falling back to dirs_next::data_local_dir() on
  Windows. Keeps the per-user, no-UAC posture pkgx already adopts.

- ls()/outdated() walk install_prefix().join("pkgs") on Windows
  instead of the hard-coded POSIX pair, so installed pkgs are
  actually found there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more pieces of the Windows port, all aligned with how pkgx
itself handles the same primitives:

- symlink_with_overwrite(): on Windows, files fall back to
  Deno.linkSync (hardlinks need no elevation as long as src/dst live
  on the same volume — true with the new install_prefix). Plain copy
  is the last resort for cross-volume. Directories still go through
  Deno.symlinkSync({ type: "dir" }), which uses developer-mode-style
  dir symlinks (enabled on GHA Windows runners; non-elevated
  machines without dev-mode skip create_v_symlinks anyway, below).

- create_v_symlinks(): early-return on Windows. The v1/v2/...
  major-version aliases are a navigation convenience — installed
  pkgs are still accessible via the canonical v<x.y.z> path, which
  is what mirror_directory + the stub wrappers actually reference.
  Adding junctions (mklink /J via subprocess) is a follow-up if
  someone needs them.

- install() stub loop: when runtime_env is set on Windows, emit a
  .cmd wrapper at <stem>.cmd that `set`s the env vars then execs the
  real binary from the pkg cache. We delete the original .exe
  hardlink first so PATHEXT doesn't shadow the .cmd. `%` is the only
  in-quotes special char inside `set "K=V"`, escaped as `%%` (batch-
  file convention). Matches pkgx's find_program() looking up .exe →
  .bat → .cmd.

POSIX legs verified locally: deno fmt/lint/check clean, `./pkgm.ts i
hyperfine` + `./pkgm.ts ls` still produce the expected output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous "windows-latest in test matrix" approach fails at step 1
(\`./pkgm.ts i git\`) because Windows doesn't interpret shebangs.
Guarding ~25 POSIX-style steps with \`if: runner.os != 'Windows'\`
would be noise; the existing job stays POSIX-only.

Add a focused \`test-windows\` job that invokes pkgm via the explicit
form a user without the (not-yet-existing) \`pkgm.cmd\` wrapper would
use:

  pkgx deno^2.1 run --ext=ts --allow-... ./pkgm.ts <args>

Two smoke steps:

1. \`pkgm --version\` — floor test that pkgx is on PATH, deno can run
   the script, libpkgx imports resolve on Windows, parseArgs reaches
   the version arm.
2. \`pkgm i hyperfine\` — exercises the install end-to-end. Expected
   to surface either pantry-resolution / hardlink-fallback / .cmd-
   emission gaps. continue-on-error: true while iterating.

Verifies the expected cache landing site is %LOCALAPPDATA%\pkgm\pkgs.
The "install hyperfine" smoke step on \`test-windows\` failed with
\`Error: CmdNotFound("hyperfine")\`. Investigation: hyperfine has no
Windows build in \`dist.pkgx.dev\`. Exhaustive probe of all 842 top-
level prefixes in the bucket found zero packages with a \`windows/\`
subdir — pkgx-on-Windows can pull \`pkgx.exe\` itself but no other
package. \`pkgm install\` on Windows is blocked on upstream shipping
Windows artifacts, independent of pkgm's port.

Drop the install step (it can't pass), keep the \`--version\` floor,
and add a \`pkgm ls\` smoke that exercises install_prefix() (now
%LOCALAPPDATA%\pkgm) and the new Windows candidate-paths branch in
ls() without depending on a real install.

Job no longer needs \`continue-on-error: true\` — both remaining
steps are deterministically achievable on Windows today.
The previous comment claimed "a probe of all 842 top-level prefixes
in dist.pkgx.dev finds zero packages with a windows/ subdir". That
probe was on the v1 root layout and missed the v2/ hierarchy, where
15 toolchain-layer packages do have Windows builds today (bun,
cmake, curl, deno, git, go, libarchive, nasm, ninja, openssl, perl,
python, rust, sqlite, zlib). jhheider clarified on pkgxdev/pkgx#607
that the manifest pipeline producing those was rolled back about a
year ago, so no new Windows builds are shipping — but it's not a
"zero artifacts" situation, and the path forward is reviving the
build pipeline rather than starting from scratch.

No functional change; only the explanatory comment is updated.
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.

1 participant