Skip to content

Windows port, rebased onto upstream #3569 (CI only, do not merge) - #8

Open
PeterNeiss wants to merge 106 commits into
upstream-masterfrom
windows-rebase
Open

PeterNeiss wants to merge 106 commits into
upstream-masterfrom
windows-rebase

Conversation

@PeterNeiss

Copy link
Copy Markdown
Owner

This PR only exists to run the Windows CI on the rebased port. Do not merge it. upstream-master is a copy of thought-machine/please master at fa553360 (thought-machine#3569), so the diff here is exactly what the Windows port adds on top of upstream.

The branch replays the fork's 103 commits onto thought-machine#3569 (the PR merge commits were dropped, flattening the history) and adds two commits on top.

Conflicts resolved during the rebase

  • Upstream's new signatures. Outputs, toolPath and similar now take the graph; the fork's slash-separated path fixes are kept on top of them.
  • Arcat warning. It now follows upstream's new parse.InitParser(state, &r) in Run.
  • The //pkg/... walk. It moved into RecursiveParse with the same backslash bug; it now uses packageNameOf.
  • Parse-claim fix 22a3a3fa: dropped. It patched SyncParsePackage, which Convert build parallelism to a more synchronous model thought-machine/please#3569 removed, and the only caller of cmap.Delete went with it. The commit now contains only its two-subrepo regression test, test/subrepo/nested_subrepo_probe, and has a new message.
  • Arcat tests. Convert build parallelism to a more synchronous model thought-machine/please#3569 deleted src/parse/parse_step_test.go, so they moved to a new //src/parse:internal_package_test, and test/windows/BUILD points at that target.

New commits

  • Windows test list. The test list in test/windows/BUILD points at the new parse test target.
  • Release bookkeeping. VERSION goes back to 17.33.0, the fork's 18.0.0 and 18.0.1 ChangeLog entries are dropped, and so is its milestone page. Upstream owns releases; the README and FAQ text about Windows stays.

Verified locally

  • //src:please builds, and so does the windows_amd64 cross-build.
  • Linux unit tests: 877 run, 870 passed, 7 skipped.
  • Wine pass: 36 targets, 827 tests, 819 passed, 8 skipped.
  • The end-to-end tests and lint are still running.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AaLmkJrkt7eRJC9uPXvbKe

Peter Neiss and others added 30 commits September 14, 2026 19:48
Scopes a Windows port with C++ builds as the driving use case, developed and
tested on Linux via cross-compilation. Ten milestones across two repos
(please + please-build/cc-rules), with five recorded decisions.

M0 was measured rather than predicted, and the results reshaped the plan:

- Compile blockers are 5 sites in 4 packages, not the ~11 the source survey
  suggested. They are layered, not parallel: src/process is a dependency of
  nearly everything, so a single `go build ./...` leaves 30 of 51 packages
  unchecked.
- syscall.Exec, syscall.Chdir and the signal constants all compile on Windows
  (Go ships stubs returning EWINDOWS). They fail at runtime instead, which is
  harder to catch, not easier.
- go-flags uses '/' as its option delimiter on Windows, which breaks Please's
  entire label syntax: //pkg:target parses as option /pkg with argument
  target. Fixed by -tags forceposix (D5).
- src/output/shell_output.go reaches into cmd.SysProcAttr from outside
  src/process -- an abstraction leak the survey missed.
- busybox-w64 ships a bash applet but rejects --noprofile/--norc, unlike
  Linux busybox. Configurable ShellArgs is a requirement, not a hedge.

With those addressed, please.exe parses BUILD files and runs builds under
Wine, including the find|sort|tr pipeline the cc rules depend on. M1 is
re-estimated from 2-3 weeks to 1-2.

probe/m1-skeleton.patch records the minimal changes used to get there. It is
not an implementation -- its lock_windows.go is a no-op that would corrupt
concurrent builds.

No source files are touched by this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Two changes needed before anything else can be tested on Windows.

src/cli/logging.go used path.Dir on logFile, which is a real filesystem path,
not a build label. On Windows that returns "." for any backslash-separated
path, so MkdirAll creates the wrong directory and plz dies at startup with
"Error opening log file: ... Path not found". filepath.Dir is correct on every
platform; this happened to be harmless on POSIX.

.plzconfig_windows_amd64 sets BuildTags = forceposix for the go plugin.
go-flags uses '/' as its option delimiter and ':' as its name/argument
delimiter on Windows, so //pkg:target parses as option /pkg with argument
target and //... is rejected outright -- every command taking a build label is
broken without this. Note go_binary has no tags parameter; the go plugin reads
CONFIG.GO.BUILD_TAGS, so this has to be config rather than a per-target edit,
and scoping it to the arch config keeps it off other platforms.

The file also carries the MinGW toolchain settings, empty defaultldflags
(-lpthread and -ldl are both wrong there), and disables xattrs and the sandbox.

Docs updated: D5 previously described forceposix as a BUILD-file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
These are the four things that stopped plz compiling for GOOS=windows, plus
the shell flags needed to make a build actually run there.

Process control. Windows has no process group that descendants inherit, so
killing a tree needs a job object: every process assigned to one dies together
on TerminateJobObject, and JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE covers the case
where we exit abnormally without cleaning up, which is what Pdeathsig gives us
on Linux. SIGTERM maps to a Ctrl-Break on the console process group, keeping
the existing graceful-then-forceful sequence in killProcess intact. There is
an unavoidable race assigning the job after Start(); closing it would need
CREATE_SUSPENDED, which os/exec gives us no way to do. Noted in the code.

File locking. LockFileEx replaces flock. It locks a byte range rather than a
file, so we take a single byte far past any content and leave the PID written
in the lock file readable by other processes -- that is what produces the
"process N has already acquired the lock" message. Unlike flock it cannot
convert between shared and exclusive atomically, so we drop and re-take;
acquireRepoLock only changes mode at startup so this isn't contended.

src/output/shell_output.go was reaching into cmd.SysProcAttr.Setpgid from
outside src/process. Replaced with process.ShareParentProcessGroup, so the
platform detail stays in one package.

clean's ForkExec becomes a detached exec.Command, with DETACHED_PROCESS on
Windows so the async delete isn't killed with our console.

Shell flags are now platform-specific: busybox, which is what we will ship as
the Windows shell, rejects --noprofile and --norc outright. It reads no
profile or rc files anyway, so nothing is lost. Note this is about the shell
being invoked, not the host -- remote execution always talks to a real bash on
the worker, so it keeps the full flag set via a new RemoteBashCommand.

lock_test.go now uses the portable constants, which lets the core tests
cross-compile. All twelve lock tests pass under Wine, including both mode
transitions and the non-blocking contention case, so LockFileEx is genuinely
excluding rather than silently succeeding.

Linux behaviour is unchanged throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Tracker updated for the process, locking, clean and shell-args work.

Also documents a trap that produced a false pass during M0: rm -rf plz-out is
not enough to force a cold build under Wine, because Please's directory cache
lives in the Wine prefix under AppData/Local/please. A build can look like it
succeeded while replaying artifacts from an earlier, differently-built binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
None of these stopped the Windows build: Go's syscall package ships Windows
stubs for Exec and Chdir, and defines the signal constants, so every one of
them compiled and would have failed only at runtime. That makes them easy to
miss, so they are covered by running the binary rather than by the compiler.

process.ExecReplace replaces the five syscall.Exec calls (plz op, plz tool,
plz run, plz update, please_shim). On Unix it is still syscall.Exec. On
Windows there is no way to replace a process image, so it runs the command as
a child and exits with its status; stdout passthrough and exit codes 0 and 3
were verified under Wine.

That difference has a consequence at the update call site. On Unix the exec
drops the repo lock for us, because Go opens files O_CLOEXEC. On Windows we
stay alive as the new process's parent, so the exclusive lock update holds
would deadlock the binary it just launched. The lock is now released
explicitly before handing over, which is what already happened implicitly
elsewhere.

Signal handling is now per-platform. Windows only ever delivers Ctrl-C as
os.Interrupt and a synthesised SIGTERM; SIGHUP, SIGQUIT and SIGABRT are
defined but never sent. The 128+signum exit convention is a shell idiom with
no meaning there, so it reports a plain failure instead.

core.LookPath was searching PATH entries split on a literal ":" and matching
exact filenames, so on Windows it would neither split C:\foo correctly nor
find bash.exe when asked for bash. It now uses the platform list separator via
fs.SplitPathList -- promoted from the private helper that already existed for
the FreeBSD fallback -- and tries the PATHEXT candidates from
fs.ExecutableNames. Note the other ":"-splitting sites are untouched; those
are a separate pass.

Xattrs now default off on Windows, which has no equivalent; the fallback that
writes separate files already existed. github.com/pkg/xattr needs no build tag
since it ships xattr_unsupported.go.

toExitError uses ExitError.ExitCode() rather than casting Sys() to a
syscall.WaitStatus. Equivalent on Unix, including the -1 for a signalled
process, and portable. The comment it replaces conceded there wasn't a good
way to do this.

Linux behaviour is unchanged throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Also corrects two things the design docs got wrong. isExecutable's 0111 check
is only reachable on the FreeBSD code path, so it needed no Windows work at
all; the real gap was core.LookPath, which neither split PATH correctly nor
knew about PATHEXT. And fs.SplitPathList was promoted during M1 rather than
M2, because LookPath needed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
The previous two commits were only ever checked with go build directly, which
does not exercise the BUILD files at all. Going through plz found three
problems.

golang.org/x/sys/windows cannot be depended on unconditionally. go_repo
generates the subrepo's BUILD files using the host build context, so on Linux
no target is produced for that package at all -- the sources are extracted,
but there is no BUILD file and the dependency fails to resolve. The deps are
now guarded with is_platform(os = "windows"), which is already the idiom used
in src/BUILD.plz. go_library filters srcs by build constraint, so the
_windows.go files are dropped on other platforms and the dependency genuinely
isn't needed there.

src/tool and tools/please_shim import src/process now but never declared it.

src/core's go_test had filter_srcs = False, with a comment referring to
something that no longer exists. That is harmless while a package has no
platform-specific sources and fatal once it does: the internal test compiles
the package sources alongside the test sources, so lock_other.go and
lock_windows.go were both fed to the compiler and every symbol collided.
Removing it fixes the build and all 281 tests in the package still pass.

Verified: plz build //src:please, plz build --arch windows_amd64 //src:please,
and plz test //src/... --exclude=e2e (837 tests, 835 passed, 2 skipped). The
cross-built binary parses labels and runs cold-cache builds under Wine, so
forceposix is reaching the compiler through the real BUILD path.

Note the windows_amd64 Go toolchain hash turns out not to be needed: Go
cross-compiles from the host toolchain, so there is nothing to download.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
The windows_amd64 Go toolchain hash is struck off: Go cross-compiles from the
host toolchain, so there is nothing to fetch. M1's exit criterion is met, and
the M0 CI job's command already passes -- only the wiring is outstanding.

Adds two risks found by actually running plz. go_repo generates third-party
BUILD files with the host build context, so a Windows-only package like
x/sys/windows has no target on Linux and cannot be depended on
unconditionally. And verifying with go build rather than plz hides real
breakage -- one pass through plz found three bugs.

Also softens the arcat "hard gate": it did not block parsing or genrules under
Wine, so it bites later than the plan implied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
PATH-style lists were split and joined on a literal ":" in six places, which
on Windows would neither parse C:\foo nor produce a list anything can read.
They now go through fs.SplitPathList and os.PathListSeparator. This is a no-op
on Unix, where the separator is ":" anyway.

src/remote/action.go is deliberately asymmetric: it splits with the local
separator, because the value was built locally, but still joins with ":",
because the worker on the other end is a POSIX machine. Same reasoning as
RemoteBashCommand. Remote execution from a Windows host is not otherwise
addressed here.

fs.ExpandHomePath read $HOME directly and its regex assumed ":" separated
PATH entries and "/" separated paths. It now uses os.UserHomeDir and builds
the pattern from the platform separators, accepting either slash on Windows.
The Unix pattern is byte-for-byte what it was.

MachineConfigFileName was /etc/please/plzconfig; on Windows it resolves under
ProgramData. DefaultPath is empty there rather than /usr/local/bin and
friends: Windows has no equivalent directory holding build tools, so there is
nothing honest to point at and users configure [build] path instead. Both
became vars rather than consts, which is why SandboxDir did too.

Build actions get USERPROFILE, TEMP and TMP alongside HOME and TMPDIR, but
only on Windows -- native tools read those, and adding them unconditionally
would change every target hash on Unix for no benefit. Verified under Wine
that all five point at the action's tmp dir.

Hash check: within a single working directory, the only targets whose hash
changes are the dependency cone of the files edited here. cmap, cli, metrics
and assets are untouched. Note that comparing hashes across two working
directories is not a valid check -- they differ for unrelated reasons.

837 tests pass; the windows_amd64 cross-build still runs builds under Wine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Comparing plz hash output between a git worktree and the main repo is not a
valid regression check: the same commit hashes differently in two working
directories, producing dozens of false positives. Stash and unstash in place
instead.

Also confirms the forward-slash normalisation is still needed -- a genrule
under Wine sees HOME with backslashes, which survives echo but will break any
command that treats backslash as an escape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Build commands are shell strings, and a backslash is an escape character to
much of what runs in them. Expanding a variable is safe -- echo and printf
'%s' both round-trip a Windows path unharmed -- but passing one to anything
that interprets its arguments is not. Under Wine:

    cmd = "echo placeholder | sed -e \"s#placeholder#$TMP_DIR#\" > $OUT"

turned Z:\...\plz-out\tmp\sedtest._build into Z:<TAB>mp...sedtest._build:
\t became a literal tab and every other backslash was eaten. That is not a
hypothetical shape -- the C/C++ rules build their link line with sed, which is
the primary use case for this port.

Win32, MinGW and busybox all accept forward slashes, so the environment now
uses them throughout. Normalising here rather than at each of the twenty-odd
places a path is written means new ones cannot quietly regress it.

It runs before withUserProvidedEnv deliberately: values the user wrote
themselves are left exactly as written, since they may not be paths at all.

No-op on platforms whose separator is already a forward slash, so Linux
behaviour and Linux build environments are unchanged. The hashes that move are
exactly the dependency cone of build_env.go; cmap, cli, metrics, assets and
version do not.

The test asserts the invariant on every platform. It is trivially true on
Unix, so it was checked by neutering the normalisation and confirming it fails
under Wine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
The original note was right that this was the highest-risk detail but wrong
about the mechanism. Shell variable expansion does not reprocess escapes, so
echo and printf round-trip a Windows path fine; it is commands that interpret
their own arguments, like sed, that destroy it. Replaces the speculation with
the measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
…lege

glob() returned nothing at all on Windows, which is fatal for a build system
whose BUILD files are full of it -- including this repo's own.

The cause is a separator mismatch. The walk goes through io/fs, whose paths are
always slash-separated whatever the host, but patternToMatcher built the
pattern with filepath.Join, so on Windows it produced globdir\*.txt and matched
nothing against globdir/a.txt. toRegexString has the same assumption baked in,
hardcoding / in its character classes.

builtInGlob.Match had a subtler version of the same problem: filepath.Match
treats the separator as a backslash on Windows, so * would have matched across
/ once the pattern was fixed, and a single-star glob would have wrongly
recursed into subdirectories.

Both now use path rather than filepath, which is what matching io/fs paths
calls for. This is a no-op on Unix, where the two are the same. Verified under
Wine: globdir/*.txt matches two files and correctly excludes the one in a
subdirectory, and globdir/**/*.txt matches all three.

Note the raw "/" handling elsewhere in glob.go and in fs/sort.go is correct
for exactly the same reason, and was left alone.

Separately: creating a symlink on Windows needs Developer Mode or
SeCreateSymbolicLinkPrivilege, which an ordinary user does not have.
CopyOrLinkFile now falls back to copying what the link points at, warning
once, since for populating plz-out the content is what matters. And RemoveAll
clears the read-only attribute on files as well as directories there, because
that is what actually stops a delete on Windows.

838 tests pass. Hash changes are confined to the fs and core dependency cone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
The interesting finding was not on the list: glob() matched nothing at all on
Windows, because the pattern was built with filepath while the walk yields
io/fs paths, which are always slash-separated.

That also inverts two entries the design doc got wrong. The raw "/" handling
in fs/sort.go and parts of glob.go is correct precisely because io/fs paths
are always "/", so those needed no change. Whether filepath or path is right
depends on whether the value is an OS path or an io/fs path -- the opposite
call from the logging.go fix in M1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
A cc_library + cc_binary + cc_shared_object triple now cross-builds from Linux
to lib.a, prog.exe and libshared.dll, and prog.exe runs under Wine and links
the static library correctly. The same targets still produce prog and
libshared.so on Linux, and all 12 of cc-rules' own tests pass there.

D1 is confirmed rather than assumed. Two MinGW toolchains -- WinLibs 16.2.0
under Wine and Ubuntu's 13 cross-compiler -- both match please_cc's existing
GCC and GNU ld matchers, and the Clang matcher correctly does not, so no new
matchers are needed. Ubuntu reports "13-win32", giving a bare "13", which
MustParseVersion and Compare both handle.

Three things the experiments taught that the design did not anticipate:

- Module-level CONFIG does not see the target architecture. Defining the
  suffix as a constant silently had no effect, because these build defs are
  subincluded and CONFIG.OS there reflects the host. It has to be a function.
- A repeatable config key cannot be cleared by assigning empty:
  "defaultldflags =" yields [""], which becomes a bare -Wl, and the linker
  fails with "cannot find : Invalid argument".
- -lpthread is fine on MinGW; only -ldl had to go.

The changes live in another repo, so they are recorded as a patch under
probe/ with instructions to reproduce, until they can be upstreamed.

Still open: please_cc has no windows_amd64 release (it is fetched prebuilt per
platform), and UnitTest++ needs its Win32 sources, which blocks cc_test but
not cc_library or cc_binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Upgrades the severity of the arcat item and explains what the tool actually
does, having tripped over it trying to run the M6 end-to-end test.

arcat is Please's built-in archive toolkit -- extract, tar, and ar -- so rules
never depend on the host having tar, zip, ar or unzip. The reason it matters
more than "a hash to add" is that plugin_repo extracts the plugin zip with it,
and every language plugin is delivered that way. Without it Windows cannot
load the cc rules at all, and cc_library then needs it again for .a archives.
Parsing and simple genrules work without it, which is why the first assessment
understated it.

The port itself is easy. arcat is six Go files with no syscall, x/sys/unix or
cgo usage; it cross-compiles to PE32+, and under Wine both critical paths work
-- arcat x extracts a zip, and arcat ar -r produces an archive that MinGW
links into a working exe. The work is publishing a windows_amd64 release and
recording its hash, not porting code.

One unrelated snag found on the way: arcat's go.mod says go 1.17 while the
code uses generics, so it fails to build on any platform with a modern
toolchain. One-line upstream fix.

M6's headline test is blocked on that release plus Wine having no working DNS
here, so the plugin cannot be fetched from inside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Two bugs found by driving a real C++ build with plz.exe under Wine. Both are
the same mistake in different places: treating an io/fs path, or a path that
may still hold backslashes, as though it were already in the host's form.

buildFileName joined the package name and BUILD file with filepath.Join and
then handed the result to iofs.Stat. io/fs paths are always slash-separated,
so on Windows it looked for a single file whose name contained a backslash and
found nothing. The effect is that no package below the top level parses at
all: //sub:target and //sub/nested:deep both fail, and so does every plugin,
since the plugin subrepo's build_defs live in a subdirectory. Only the root
package worked, because filepath.Join("", "BUILD") has no separator to get
wrong -- which is why earlier testing missed it.

toolPath prepends ./ to a bare filename so the shell runs it rather than
searching PATH, deciding via strings.Contains(path, "/"). On Windows the path
may still be backslash-separated at that point, so an absolute path looked
bare and became "./Z:/tmp/.../please_cc.exe". It now checks both separators.

Note this is the inverse of the fix in src/cli/logging.go, where filepath was
the right answer. Which one is correct depends on whether the value is an OS
path or an io/fs path, and that distinction is worth checking rather than
assuming.

With these, the full chain works under Wine: plz.exe extracts the cc plugin
with arcat.exe, runs build actions through busybox, identifies the toolchain
with please_cc.exe, compiles and links with MinGW g++.exe, and the resulting
hello.exe runs and prints correctly.

838 tests pass and the windows_amd64 cross-build is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
wine plz.exe now builds a C++ binary through an entirely Windows toolchain --
arcat.exe extracts the plugin, busybox runs the build actions, please_cc.exe
identifies the compiler, MinGW g++.exe compiles and links -- and the result
runs.

Records the two bugs that had to be fixed to get there, both of which were
invisible to every earlier test. The package-lookup one is the more alarming:
no package below the top level parsed on Windows at all, and it went unnoticed
because the root package is the one case where the buggy join cannot produce a
wrong separator.

Also notes two things a Windows user will hit immediately: .plzconfig rejects
unquoted backslashes, and with DefaultPath empty on Windows nothing builds
until [build] path is configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Build actions are shell strings, and every local call site hardcoded "bash".
Windows ships nothing that can run one, so [build] shell and [build] shellargs
now select the shell, defaulting per-platform: bash with --noprofile --norc on
Unix, and the bundled busybox's bash applet on Windows, which rejects those two
flags and has no startup files to suppress anyway.

BashCommand becomes a method on Executor so it carries the shell with it.
RemoteBashCommand is untouched: the remote worker is a real bash whatever we
happen to be running on, so it keeps the full flag set. The cmd cache's two
"sh -c" sites and the shell that plz build --shell opens follow the same config;
the latter was a third hardcoded shell that earlier passes missed.

Resolution matters more than it looks. A shell that is on Please's own PATH is
still left for the OS to find, exactly as before, but a name that isn't there
falls back to the build path, which has Please's install directory prepended.
Without that the bundling would be pointless, because nothing puts that
directory on a Windows user's PATH.

busybox-w64 is vendored as a remote_file with a pinned hash and installed
alongside please.exe. It is GPL-2.0, which .plzconfig rejected outright, so that
is accepted now with a note: Please execs busybox rather than linking it, so the
two are separately distributed works and the release carries the licence.

Also gated off Windows: tarball(xzip = True), since busybox's xz decompresses
only, and please_sandbox, which is built on Linux namespaces.

Verified under Wine with no configuration and nothing on the PATH: a genrule
running "cat $SRCS | sort > $OUT" builds through the bundled shell, and fails
correctly when it is moved away. Target hashes on Linux are byte-identical
before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Records the five things this milestone turned up. The one worth remembering is
that resolving the shell on $PATH alone would have made bundling it pointless:
nothing puts Please's install directory on a Windows user's PATH, so the default
would never have been found.

Also notes that the busybox bash applet form behaves identically to the renamed
bash.exe that M0 tested, which is why ShellArgs selects the applet rather than
the packaging renaming the binary and shadowing a user's real bash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Makes `plz build --arch windows_amd64 //package:release_files` produce something a
Windows user can actually unzip and run, and wires the CI job that will do it.

The release is a .zip, since Windows has no guaranteed tar, holding please.exe,
busybox.exe, build_langserver.exe and a plz.cmd shim. The shim replaces the
`ln -sf please plz` that install.sh does on Unix, because symlinks on Windows
need Developer Mode. The xz tarballs are gated off Windows and the zip stands in
for them; please_sandbox is gated off too, being Linux namespaces throughout.

The .exe suffix had to be asked for per target. go_binary names its output after
the rule, so //src:please produced a file called `please`, which cmd will not run
and LookPath will not find. The general fix belongs in the go plugin.

Self-update needed two changes beyond that. Symlinking is replaced by a
per-platform linkFile: Windows hard-links, which needs no privilege on NTFS. And
because a running executable can be neither deleted nor written over, and the
file being replaced is usually the Please doing the replacing, a file that cannot
be removed is renamed aside to .stale, which the next run sweeps up.

pleasew.ps1 is the PowerShell counterpart of pleasew, written to a repo by
plz init alongside it - a repo is often worked on from more than one platform, so
picking by host would leave a Linux developer no way to set one up for their
Windows colleagues. There is no PowerShell on the Linux host, so it has been
reviewed but not executed anywhere yet.

Verified by extracting the zip as a user would and running plz.cmd under Wine: it
builds a genrule with no configuration at all, the shim finding please.exe and
please.exe finding busybox.exe beside it. `query alltargets //...` works too,
which is the forceposix smoke test.

Still outstanding for the milestone: arcat has no windows_amd64 release, so no
plugin can load there yet, and the builder image needs pushing before the CI job
can run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Everything in the release pipeline is built and wired; what is left is a
dependency on another repo. arcat has no windows_amd64 release, and since every
language plugin is delivered as a zip that arcat extracts, no plugin can load on
Windows until there is one. That is now the single thing between here and the
exit criterion.

Records that the .exe suffix had to be asked for per target rather than coming
from the go plugin, and that plz init now writes both wrapper scripts on every
platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Running the unit tests under Wine for the first time turned up four bugs that
share a cause: code that operates on repo-relative or label-derived paths was
using filepath, whose separator on Windows is a backslash. Every one is a
correctness bug, and none of them shows up on Linux, where the two are the same.

Globs crossed package boundaries. isBuildFile called filepath.Base on a path
that came from io/fs, which is always slash-separated, so on Windows it compared
the whole path against "BUILD" and never matched. No subpackage was ever
detected, and a glob in one package would happily swallow files belonging to
another.

The initial package was wrong whenever plz ran from a subdirectory. getRepoRoot
returned it with backslashes, which are illegal in a package name, so the label
failed validation and Please walked up until something parsed - usually the repo
root. Relative labels like path/to:thingy failed outright for the same reason.

$(location), $(exe), $(worker) and tool paths expanded with backslashes. These
go straight into a shell command, where a backslash is an escape character; the
design notes measured sed silently turning \t into a tab. The environment was
already normalised, but these are not environment values.

The last one changes a decision rather than fixing a slip. plz-out paths are now
built with path rather than filepath, so they are slash-separated everywhere.
The design doc argued for normalising only at the environment boundary on the
grounds it was the smaller change; that turned out to leave the replacements
above broken, and the tests already assumed slashes throughout. Win32 accepts
either separator, so nothing is given up. This is a no-op on Unix.

Also makes the tests that were asserting Unix semantics say what they mean:
home paths through os.UserHomeDir rather than $HOME, path lists split on
os.PathListSeparator rather than a colon, and LookPath looking for a tool the
test wrote itself rather than the bash the host is assumed to have. TestSymlink
skips on Windows, where creating one needs Developer Mode and Wine reports
success while producing a link it can't stat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Until now everything about the port's runtime behaviour was checked by hand.
This makes it a test target: wine_go_test runs a Go test binary cross-built for
Windows, and wine_plz_test runs the cross-built please.exe against a small repo
laid out the way the release is, with busybox.exe beside it and nothing on the
PATH.

Four targets to start: the src/core and src/fs unit tests, the shell smoke test
from the design notes - a build action with a pipe and a redirect, so the
bundled shell has to work - and a query, because a dropped forceposix tag breaks
every build label and is invisible in Please's own source.

Finding them at all needed one thing recorded here rather than fixed: Go's exec
package on Windows will not run a file whose name has no extension in PATHEXT,
even when handed its full path. The go plugin names test binaries after the
rule, so the macro copies each one to a .exe before running it. Without that,
any test whose subject re-execs itself fails obscurely.

They are labelled wine and excluded from the other passes, because building them
means cross-compiling the Go standard library for another platform - too much to
impose on someone who wanted the unit tests. test.sh runs them as a third pass
where wine is installed and says so where it isn't, and the CI job is blocking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Running the unit tests under Wine properly for the first time found four
correctness bugs, all the same shape: filepath used on paths that are
slash-separated by definition. M2 recorded the inverse of that lesson and fixed
the producers; these were the consumers it missed.

The decision that changed: normalising path separators only at the environment
boundary, which 02-shell-and-build-actions.md chose on the grounds it was the
smaller change. It isn't, because $(location) and friends are not environment
values. plz-out paths are slash-separated throughout now.

Also corrects the testing strategy's prediction about symlinks under Wine. It
assumed Wine grants the privilege unconditionally so the copy fallback goes
untested. Wine actually reports success and produces a link it cannot stat, so
Wine tells us nothing either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Setting [sandbox] build or test on Windows used to produce "Can't find sandbox
tool please_sandbox on the path", which invites you to install something that
does not exist and cannot. The defaults were already false, so the milestone's
real work was this message.

Please now says sandboxing is not implemented on the platform and that actions
will run without isolation, and builds an executor that does not claim to
sandbox rather than one that silently doesn't.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
The event loop compares the paths it recorded against the ones fsnotify reports
back. Ours are slash-separated, coming from build labels; fsnotify on Windows
reports backslashes. Nothing ever matched, so every event was discarded as
belonging to a file we weren't watching and the watch simply never fired.

It fails silently, because a discarded event looks exactly like an unrelated
file changing, and it is logged at a level nobody runs at. Both sides go through
watchKey now.

The design notes had this down as documenting fsnotify's Windows limits. It
isn't a limit, it's a bug on our side, and the test that guards it only means
anything when run on Windows, so it goes in the Wine job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
translateOS needed nothing - windows already passes through the default branch,
so that is recorded rather than changed. The src/watch item was down as
documenting fsnotify's Windows limits and was a silent bug on our side instead.

Also notes that the go plugin's .exe naming now blocks more than it looked like:
plz run fails on any go_binary until it lands, because Go's exec on Windows will
not run a file with no PATHEXT extension even given its full path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Four more of the same shape as the last batch, all found by pointing the Wine
job at more packages.

output_dirs produced doubled paths. copyOutDir strips the temp directory off a
path to get an output name, comparing a filepath.Join result against a
slash-separated TmpDir. Neither prefix matched on Windows, so the whole path
survived as the output name and moveOutputs then joined the temp directory onto
a path that already contained it.

JS coverage file names were never sanitised, because the paths from the coverage
file were compared against filepath.Dir of a plz-out directory. Coverage came
out attributed to absolute build paths rather than source files. Coverage by
directory had backslashed keys for the same reason, which neither read correctly
nor matched anything configured.

file:// URLs could not name a Windows path at all. RFC 8089 puts a slash before
the drive letter, so file:///C:/foo arrives as /C:/foo, which filepath.IsAbs
rejects. No remote_file with a local URL could work.

Two tests also had to stop writing to the real home directory. They left a
read-only file at ~/secret, which on Windows the next run cannot replace; they
now point the home directory at somewhere they own. The tests that need working
symlinks or Unix permission bits skip on Windows and say why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
//src/build, //src/test, //src/cli and //src/watch join //src/core and //src/fs:
428 tests, 424 passing and 4 skipped. //src/build is the valuable one, since it
runs real build actions and so covers the process layer and the bundled shell as
well as whatever it is nominally about. wine_go_test grows a needs_shell option
that puts busybox next to the test binary, the way an install has it.

Two things about the harness itself, both found by tests failing for reasons
that had nothing to do with Please. Wine ships a hosts file with the localhost
line commented out, so anything resolving it hangs until it gives up - three
tests were each burning fifteen seconds. And ~ resolves inside the shared
prefix, so anything a test writes there leaks into the next run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkSQhgMeCVmuWqbGAP7oVM
Peter Neiss and others added 30 commits September 14, 2026 19:54
809 tests now run on a real Windows machine, up from 800, and the two that still
skip there are skipped on every platform and always were.

Getting from seven skips to two found two real failures that Wine had been passing
for months. plz run handed cmd.exe a forward-slashed path, which it reads as a
switch, so it could not launch an sh_binary at all. And every link: label silently
became a warning, because buildLinks passed os.Symlink straight through while
CopyOrLinkFile had had a fallback all along.

That is now a standing note in its own right: a skip hides a bug better than a
missing test does. Both of these sat behind runtime.GOOS == "windows" skips that
looked perfectly reasonable when they were written.

Ctrl-Break stays open, with the reason it is harder than it looks: KillProcess
gives the graceful path 30ms before terminating the job regardless, so a test
asserting graceful shutdown races that timer on a CI machine, and a flaky test in
a blocking job is worse than no test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
The Windows fixes for all four plugins are on GitHub now, so the machinery that
existed only because they were not can go. That was more than a pin: a
local-checkout switch in plugins/BUILD, a vendoring script, a gitignored archive
directory, a bundled payload in the release, a file:// plugin repo default in the
binary, and eight tests that only existed when a checkout was configured.

Pinned to commit SHAs rather than to the windows branch. A branch archive changes
whenever it is pushed to, which would silently move every build hash that reaches
it and leave the cache serving something else.

The gain is larger than the deletion. Those eight tests are now unconditional -
both pex tests, the DLL test, the cc_test, the sh_binary test - so they run in CI
on every change instead of only on the one machine that had the checkouts. The two
//test/export failures go too: they only ever failed because .plzconfig.local was
present, which it no longer needs to be.

Four workarounds this repo carried because the old pins lacked the fixes are gone
with it: the out = "please.exe" overrides in src/BUILD.plz and
//tools/build_langserver, and the cc toolchain and defaultldflags lines in
.plzconfig_windows_amd64. The plugins work all of that out themselves now, so
keeping them would be second-guessing a plugin that is right.

PexTool stays. It builds please_pex from the plugin's source because no published
release carries the Windows preamble, and the fork publishes no releases at all.

arcat stays in the release too, and has to: it is built in-tree from the module
proxy, and without it a Windows plz cannot extract a downloaded plugin. What goes
with the bundling is please_go.exe, please_cc.exe and please_pex.exe, which were
built inside the checkouts and cannot be built from here - cross-compiling a
plugin's own tool collides on subrepo names. So a native Windows plz can fetch and
parse plugins but not build a Go, C++ or Python target until those three have
releases to download. Cross-building from Linux is unaffected. That is now the
third item in what to pick up next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
A native Windows plz could not build a Go, C++ or Python target, because
please_go, please_cc and please_pex have no windows_amd64 release to fetch and
cannot be built from the repo using the plugin - cross-compiling a plugin's own
tool collides on subrepo names. Please's release carried copies for a while, but
that only helped people who had that release.

All three are now published from the forks and downloaded like any other platform.
Only windows_amd64 is redirected there; everything else still comes from
please-build, so when upstream publishes its own the redirect goes away and
nothing else changes. Verified by fetching each asset and checking the hash the
BUILD file pins.

Each download needs an explicit out on Windows: the asset name carries the version
and platform, which leaves it with no extension in PATHEXT, and Go's exec will not
run such a file even when handed its full path.

PexTool stays in .plzconfig_windows_amd64 and now has a narrower reason. The
Windows please_pex has the preamble, but a Linux host cross-building uses the
Linux one, and that is still the upstream build without it. Publishing a Linux
please_pex from the fork is what would retire the override; it is now the third
thing to pick up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
The last of M9. Everything technical was done; what was left was the part that
decides whether anyone outside this repo can use it.

docs/faq.html said Windows was not supported natively, which had been the honest
answer for years and stopped being true. It now says what is supported and names
the two things that genuinely behave differently: there is no build sandbox,
because Please's is built on Linux namespaces and Windows has no equivalent, and
real-time virus scanning holds freshly written files open, which Windows treats as
a reason to refuse deleting them.

get_plz.ps1 is the counterpart to get_plz.sh, served and signed from the same
bucket by the same release script, and run with irm ... | iex since Windows has no
shell to curl into. It is deliberately parallel to the sh version; the differences
are all forced by the platform - a .zip rather than a tarball, a plz.cmd shim
rather than a symlink, and hard-links rather than symlinks to link the install up
a level.

The changelog entry leads with the feature and then lists the bugs a real Windows
machine found, because those are what someone upgrading will recognise: plz clean
failing every time, plz hanging outside a repo, link: labels silently doing
nothing.

Also corrects two statements in the state of play that had gone stale within the
hour: plz run on an sh_binary is fixed and tested rather than open, and nothing is
blocked on push access any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
.plzconfig_windows_amd64 pointed PexTool at the plugin's own source, because the
released Linux please_pex has no Windows preamble and a Linux host cross-compiling
uses the Linux tool. A .pex is a zip with an executable stub in front, so what came
out was an ELF-prefixed file Windows would not run.

The fork now publishes a Linux please_pex that carries the preamble, so the
override is gone and the pex tests pass without it.

That leaves .plzconfig_windows_amd64 with nothing but genuine platform facts: the
forceposix build tag, because go-flags reads / as an option delimiter and would
break every build label; no extended attributes; and no sandbox. Every entry that
was compensating for a plugin has now been fixed in the plugin instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
The Windows support was real and unreachable at the same time. Upstream publishes
to a GCS bucket from a CircleCI job that only runs on thought-machine/please, so
nothing this branch produced could be downloaded by pleasew.ps1, get_plz.ps1 or
plz update. A working port nobody can install is not a working port.

The release workflow builds the same artifacts and publishes them as a GitHub
Release on this fork. The asset names carry the platform, which is what
gen_release.py already does for the GitHub half of an upstream release, so the two
agree on names even though they disagree on paths.

Both installers now understand either layout and pick by looking at the base URL:
a release keeps everything under one tag with the platform in the filename, the
bucket keeps a directory per platform and version. Setting [please]
downloadlocation, or PLZ_DOWNLOAD_BASE for get_plz.ps1, switches back to the
bucket. That is what makes this revertible in one line if upstream ever starts
publishing Windows builds.

Numbered 18.0.0 rather than 17.34.0. Native support for a new operating system is
not a patch, and this fork's numbering is its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnBWtZhT4ReP23CYYbfkRa
Nothing anywhere had ever executed a line of the codelabs at
https://please.build/codelabs.html, on any platform. This adds a blocking
codelabs job to the Windows workflow that replays all eight on windows-latest
with the release zip, the way a reader would.

The steps are extracted from docs/codelabs/*.md rather than transcribed, so the
check cannot drift from the published pages. A block no rule can classify is an
error, and //test/windows/codelab_script/script:script_test checks that against
the real codelabs on Linux. What the Markdown cannot say lives in
test/windows/codelab_steps.conf, each stanza with a reason and pinned to the
text it was decided about. No codelab is edited.

Four failures are listed ahead of the first run, from facts checked directly:
plz init plugin writes upstream plugin_repo targets whose please_go and
please_pex have no windows_amd64 release, Puku has none either, and a bash
environment prefix is not PowerShell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The first native run showed the harness manufacturing a failure. plz init
plugin go already writes GoTool, the Go codelab's fragment sets it again, and
appending the fragment verbatim left a plugin section with a repeated key,
which Please refuses. A reader edits the key instead, so the runner now does
too: a key the section has is replaced, a new key joins its section, and a new
section is appended. Subsection names stay case-sensitive.

Also from that run: a transcript's output is attached to the command it
follows rather than to the block's last command, and a failing command's
errors are plain text rather than CLIXML.

Two genuine Windows failures join the known list. genrule's plz run of a
#!/bin/bash tool fails because Windows runs nothing by shebang, and plz_query's
cloned repo has no please_go on Windows and asks golang.org for a Go 1.20
.tar.gz that does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
With fragments merged key by key, the second run left one failure nobody had
listed, and it is not a Windows failure. plz init plugin go generates a
go_stdlib in third_party/go/BUILD and points STDLib at it; the Go codelabs'
own third_party/go/BUILD holds only a go_toolchain, so following them drops the
stdlib and every Go build fails to find //third_party/go:std. The Kubernetes
codelab stops there, and go_intro hits it ahead of its Windows failures.

The state of play now says what the runs found: only using_plugins can be
followed to its end on Windows, and every entry in the known-failures list
carries the log line behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
… the forks

Windows has no #! mechanism, so plz run could not start a script built the Unix
way, which is how the genrule codelab builds its word-count tool: "%1 is not a
valid Win32 application". plz run now hands an extensionless #!/bin/sh or
#!/bin/bash script to the shell build actions already use. Build actions needed
nothing: busybox reads the #! line itself, which a new smoke-repo target checks
under Wine and on windows-latest.

plz init plugin wrote please-build plugin_repos, whose tools have no Windows
release. go, cc, shell and python now come from the PeterNeiss forks at the
revisions plugins/BUILD pins, with no call to GitHub's API; a test keeps the two
lists in step. An explicit --owner or --version still looks up tags, now only
vX.Y.Z tags, highest first, across every page, with GITHUB_TOKEN when set and a
message that names the rate limit. Previously the first tag listed won, which for
python-rules is wheel_resolver-v2.1.0.

plz init plugin go also overwrote GoTool with the stdlib's label when a go_stdlib
already existed, leaving STDLib pointing at nothing.

The codelab check accepts PowerShell blocks, for the Windows forms to come, and
skips tree -a, which is tree.com on Windows and exits 0 on a switch it rejects.
The genrule entry leaves the known failures with this fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
go-rules and python-rules move to fork commits that build on a Windows host,
here and in plz init plugin's pins. go_toolchain downloads Go's .zip there and
names its tools with .exe (52d9e0f); the build interpreter defaults to python,
since a Windows install has no python3 (eecfd15).

The Go codelab's third_party/go/BUILD now declares the go_stdlib its .plzconfig
points at; it replaced the one plz init plugin go generates, on every platform.
The Go and Python codelabs give a Windows form of [build] path, PassEnv = PATH,
since the default path there is empty, and config.html says so too. The Python
codelab's numpy moves to 1.26.4, the first with wheels for Python 3.12. plz_query
clones the PeterNeiss fork of please-codelabs, which pins the same go-rules fork.

Their three entries leave the known failures with these fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
Puku published no Windows build, so plz puku failed with a 404. The PeterNeiss
fork now publishes puku-1.17.1-windows_amd64 from the upstream v1.17.1 source,
and the codelab's remote_file downloads from the fork on Windows, as puku.exe,
and from please-build everywhere else. puku-version moves to 1.17.1 to match.

Its bash-only lines, GODEBUG=... and GOTOOLCHAIN=local ..., gain PowerShell forms,
and [build] path gains its Windows form, PassEnv = PATH. The replay runs those
forms and skips the bash ones, which is what a Windows reader does too.

Both Puku entries leave the known failures; only the out-of-scope k8s one remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
…hout go.mod

The fourth native run of the codelabs reached further and found three more
things, none of them in the codelabs' text.

plz test //src/... panicked on Windows with "Invalid package name: src\". The
walk for ... returns paths with backslashes, and only "/" was trimmed from the
directory before it became a package name.

Go on Windows finds its build cache under LOCALAPPDATA, which build actions did
not have, so every go command in one failed with "GOCACHE is not defined and
%LocalAppData% is not defined". LOCALAPPDATA and APPDATA now point into the
build's directory, as HOME, USERPROFILE and TEMP already did.

go-rules moves to 9c26bfd, which installs the standard library with
GO111MODULE=off. In module mode it read the repo's go.mod from inside the build
directory, and a go.mod from a newer host Go stopped the Go 1.20 toolchain cold.

The genrule codelab's default tool, plain wc, has no Windows counterpart on the
build path; that is recorded in the known failures with the log line. A test
helper also gains the t.Helper() lint asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
go-rules 2906dda names cc as a tool on a Windows host only where cgo or an
external link needs it, since tools resolve before anything runs and no cc exists
there by default. It also moves to please_go 1.24.0-windows.1, which trims
go_repo package paths on Windows instead of looking under a doubled source root.

python-rules fe3f124 moves to please_pex 3.0.2-windows.1, which reads its
embedded test runners by slash-separated path; every python_test on Windows
failed to build without it.

Both Windows tool builds are released under their own versions rather than
replacing assets that earlier commits pin by hash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
7166e39 passes -extld "$TOOLS_LD" to the Go linker only when it names cc as a
tool. Naming cc only where Windows needs it had left the flag behind, and the
build shell stopped on the unset variable: "TOOLS_LD: parameter not set".

It also moves to please_go 1.24.0-windows.2, which slash-joins go_repo subrepo
names and label packages on Windows. With backslashes they named no subrepo, and
the plz_query and Puku codelabs failed with "Subrepo third_party\go\... is not
defined". The Puku fork makes the same fix to the labels puku itself writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The codelabs job never started on 0671e3c: docker info exits non-zero when the daemon is not
running, and the pwsh wrapper fails a step on the last native exit code. The step only reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
… Windows

arcat's AddInitPyFiles looked for each directory's __init__.py under a backslash-separated
name, never found it, and wrote an empty one beside it. zipimport normalises separators and
loaded the empty one, so every python_test built on Windows failed with "module 'xmlrunner'
has no attribute 'XMLTestRunner'" - the Python codelab's plz test stopped there. The Wine pex
tests could not see it, since their pexes are assembled on Linux.

A second patch makes it, and shouldInclude, use path rather than filepath on member names.
//test/windows:arcat_init_py_test runs the Windows arcat under Wine against a nested package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
A rule written for Unix names tools like wc and expects them on the build path. Windows has no
such directory, so the genrule codelab's word_count stopped with "wc not found in path". The
bundled busybox has wc built in, and picks its applet from the name it is started under, so a
bare tool name found nowhere else now resolves to a copy of busybox named for it, under
plz-out/busybox. Its content is busybox's, so the tool hash follows a busybox upgrade.

That path is the first system tool inside the repo, which found a hasher bug: it trimmed only
a / after the repo root, leaving \plz-out\... - absolute again, to the root of the drive.

Checked under Wine by //test/windows:applet_tool_test, and natively by the probes. The genrule
codelab's known failure goes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
…ted name

The same fault as arcat's, in please_pex's own copy of the zip writer. python-rules 9cd3f0d
downloads please_pex 3.0.2-windows.2 on Windows, which carries the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The codelab told readers to run please_go get, which go-rules removed (thought-machine#306); it fails with
"Unknown command get" on every platform. go get and go list -m all give the same list of
modules for the go_repo rules that follow, and Puku is pointed to for keeping them in sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
go-rules 883d615 downloads please_go 1.24.0-windows.3, whose package_info slash-joins import
paths: every go_repo package failed on Windows with "Cannot determine export file path", which
stopped the plz query and Puku codelabs. It also finds the stdlib archives for a toolchain's
importconfig from inside GOROOT, whose backslashes were escapes in the sed pattern that trimmed
it, so a compile against it could not import math/rand.

python-rules c10d10a downloads please_pex 3.0.2-windows.3, which locks the cache a non-zip-safe
pex explodes into with msvcrt. portalocker needs pywin32 on Windows, and the Python codelab's
numpy binary stopped with "No module named 'win32con'".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The block that adds the go_repo rules is the whole of third_party/go/BUILD, and it held only
those, so following it deleted the toolchain and stdlib the codelab set up earlier. plz test
then failed with "//third_party/go:std doesn't exist".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
please_go 1.24.0-windows.4 normalises both paths before trimming a go_repo's source root in
package_info; 1.24.0-windows.3 slash-joined the import path but still left the root on it.

please_pex 3.0.2-windows.4 deletes the read-only files an uncached, non-zip-safe pex extracted.
The Python codelab's numpy binary ran and then failed on f2py.exe with "Access is denied".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The codelab's labelled BUILD file named //third_party/go:assert, but the repo it clones names
that go_repo testify, so plz test failed with "doesn't contain target assert" on every
platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
please_pex 3.0.2-windows.5 leaves the extension modules a running pex still has loaded when it
cleans up on Windows, which will not delete them; the Python codelab's numpy binary ran and
then failed on a .pyd with "Access is denied".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
The codelabs job on windows-latest passes with one known failure left, k8s, which is outside the
Windows work. The ChangeLog records what that took; the design docs mark M10 done, record the
decision it left open, and describe the busybox applet fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Djqj7h7hQ1Y9Gj99s4DLm9
…with slashes

--shell had never been exercised on Windows. Under Wine it works, with and without =run, for
builds and tests: busybox opens in the target's directory with its environment set. The one
defect was the directory printed above it, which for a test came out backslash-separated and so
could not be pasted into that shell.

Adds Wine tests for both forms and native probes for the Windows job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaLmkJrkt7eRJC9uPXvbKe
Upstream thought-machine#3569 deleted parse_step_test.go along with the queueing code it
tested. The arcat tests the Windows port had added to it now live in
internal_package_test, so the Wine and native runs take that target instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaLmkJrkt7eRJC9uPXvbKe
The fork cut 18.0.0 and 18.0.1 itself, because it could not publish through
upstream's release pipeline. Upstream owns the version number and writes its
ChangeLog at release time, so this series goes back to 17.33.0 and drops the
fork's two ChangeLog entries and its 18.0.0 milestone page.

The Windows support itself is unchanged, including the README and FAQ, which
describe it rather than a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaLmkJrkt7eRJC9uPXvbKe
plz update on Windows fetched only the bare please binary. Everything else a
Windows install ships - busybox, which runs build actions there, arcat and the
plz.cmd shim - stayed at whatever version was first installed, silently getting
staler with each update. A Windows release is a zip of all of it, so plz update
now downloads that and unpacks it, stripping the top-level please/ directory
the way the tarball path does and refusing any entry that would land outside
the version directory. The existing checksum and signature checks apply to the
zip unchanged.

The download takes the OS as a parameter, so the tests fetch the Windows
release from any platform; cross-built under Wine and on the native job, the
existing download tests take the zip path as well.

plz update still only knows the bucket layout. The fork's GitHub releases are
shaped differently, which the state-of-play doc now records.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaLmkJrkt7eRJC9uPXvbKe
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