feat(windows): scaffolding pass for Windows support#87
Draft
tannevaled wants to merge 6 commits into
Draft
Conversation
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.
This was referenced May 18, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — Windows support scaffolding
Windows is an official
pkgxtarget. 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).What's in this PR
standardPath()— Windows case returning baselineSystem32/System32\Wbem; introducesPATH_SEPmodule constant (a literal":"split would shredC:\drive prefixes).install_prefix()— Windows resolves to%LOCALAPPDATA%\pkgm. Mirrorspkgxdev/setup'sinstaller.ps1($env:LOCALAPPDATA\pkgx).ls()/outdated()— walkinstall_prefix().join("pkgs")on Windows instead of the hard-coded POSIX/usr/local/pkgs+~/.local/pkgspair.get_pkgx()— usesPATH_SEPand looks forpkgx.exeon Windows.symlink_with_overwrite()— Windows: hardlink for files (Deno.linkSync, no elevation needed on the same volume) withDeno.copyFileSyncas last resort; dir symlinks viaDeno.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. Thev1/v2/… aliases are a navigation convenience; canonicalv<x.y.z>paths are what stubs andmirror_directoryactually reference..cmdwrapper at<stem>.cmdthatsets the env vars then execs the real binary. Delete the.exehardlink first so PATHEXT doesn't shadow the wrapper.%in values escaped as%%(batch-file convention).Deno.chmod(0o755)guarded — throws on Windows.test-windowsjob, separate from the POSIXtestmatrix. Two smoke steps via explicitpkgx deno^2.1 runinvocation:pkgm --version(validates pkgx-on-PATH + libpkgx imports + parseArgs floor) andpkgm ls(validatesinstall_prefix()+ the ls candidate-paths branch).Design answers (modelled on what pkgx already does)
After surveying
pkgxdev/pkgx+pkgxdev/setup:%LOCALAPPDATA%\pkgmpkgxdev/setup/installer.ps1installs pkgx itself to$env:LOCALAPPDATA\pkgxinstaller.ps1writes to user-levelPATHvia[Environment]::SetEnvironmentVariable("Path", …, User)— never elevates.cmdfilespkgxdev/pkgx/crates/lib/src/utils.rsfind_program()looks for.exe → .bat → .cmdextensionspkgm.cmdwrapper installed bypkgxdev/setup/installer.ps1(mirror of the POSIX/usr/local/bin/pkgmshebang shim)installer.shalready creates/usr/local/bin/pkgmas#!/usr/bin/env -S pkgx -q! pkgm;installer.ps1would write a.cmdequivalentThe
pkgm.cmdwrapper change belongs inpkgxdev/setup, not this PR.Upstream gap discovered
While iterating, the CI's
install hyperfinesmoke step failed withError: CmdNotFound("hyperfine")despite pkgm doing the right thing on its side. The v2 dist layout (under thev2/prefix in thedist.tea.xyzbucket) 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/.ps1variant.dev_stub_text()— generates POSIX shell with[ -x /usr/local/bin/dev ] || …; needs a Windows variant (or skip dev-mode integration in v1).mklink /Jas a fallback forcreate_v_symlinkson machines without developer mode.pkgm.cmdwrapper inpkgxdev/setup— separate PR to that repo.uninstall()Windows path normalisation — needs verifying once installs actually work.Smaller open implementation questions
<name>.exe(the hardlink) and<name>.cmdwould coexist, we currently delete the.exeand 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 inbin/. Acceptable for v1.Deno.copyFileSyncfallback: only kicks in ifDeno.linkSyncfails (cross-volume etc.). Copy duplicates disk usage. Not a v1 concern.%LOCALAPPDATA%\pkgm\binon PATH: the install warns if missing but doesn't add it.pkgxdev/setup/installer.ps1adds$env:LOCALAPPDATA\pkgxto 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 withdeno fmt --check,deno lint,deno check, and./pkgm.ts i hyperfine+./pkgm.ts lssmoke runs on macOS.test-windowsshould be green via the twopkgm --version/pkgm lssmoke steps. End-to-end install can't be tested until the upstream dist gap closes.sudo-installred 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_pkgxandget_pkgx— both branches add disjoint code paths.References used
pkgxdev/setupinstaller.ps1— Windows install path + PATH manipulation.pkgxdev/pkgxcrates/lib/src/config.rs—dirs_next::data_local_dir()resolves to%LOCALAPPDATA%on Windows.pkgxdev/pkgxcrates/lib/src/utils.rs—find_program().exe/.bat/.cmdextension lookup.pkgxdev/pkgxcrates/cli/src/execve.rs—#[cfg(windows)]switch fromexecvetoCommand::spawn.dist.pkgx.devS3 listing — confirmed the 842-prefix Windows gap.