Windows mvp - #712
Windows mvp#712
Conversation
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It modifies shared plib library code and the workspace-wide get_binary_path test-harness resolution, so its blast radius spans every crate's tests and warrants human verification across build/CI configurations.
Review effort: Balanced
Findings: None
What changed in this PR
This PR introduces a "minimum viable" Windows (MSVC) port for a subset of the workspace — the gettext-rs, plib, and posixutils-xform crates — enabling cksum, compress, uuencode, and uudecode to build and pass tests on Windows. The approach ports whole crates at a time, gating inherently Unix functionality behind #[cfg(unix)] and supplying Windows equivalents (read-only attribute as the POSIX owner-write bit, no umask/chown/hard-link count, no SIGPIPE). It also hardens the shared test harness and adds a dedicated Windows CI job.
Changes:
- Cross-platform file metadata handling in
xform(mode ↔ read-only attribute,FileTimes-based time preservation, link-count andNAME_MAXshims) and#[cfg(unix)]gating acrossplibmodules,locale,io,diag, andgettext-rs. - Reworked
plib::testing::get_binary_pathto locate binaries from the running test executable (current_exe,depsparent,EXE_SUFFIX) instead of reconstructing the target/profile path. - New Windows CI job + cross-target clippy lint,
WINDOWS_CRATESenv,.gitattributesfor LF normalization, and Windows docs inREADME.md/CLAUDE.md.
| File | Description |
|---|---|
| xform/uuencode.rs | Split mode formatting into format_mode/mode_of/new_file_mode with Windows read-only-attribute handling |
| xform/uudecode.rs | Cross-platform is_writable and set_mode (read-only attribute on Windows) |
| xform/compress.rs | FileMetadata refactor using fs::Permissions/FileTimes, cfg-gated link_count/name_max, reordered apply_to |
| xform/tests/uue/mod.rs | Cross-platform test helpers (running_as_root, set_read_only), cfg-gated mode logic |
| xform/tests/compress/mod.rs | Gated Unix-only symlink/metadata tests; decompress_command helper for Windows |
| plib/src/testing.rs | get_binary_path now derived from current_exe; read_until_full/assert_dies_by_sigpipe cfg-gated |
| plib/src/lib.rs | #[cfg(unix)] gating of Unix-only modules |
| plib/src/locale.rs | Gated libc-backed locale helpers; extracted locale_yesexpr |
| plib/src/io.rs | Gated SIGPIPE/atomic-write/std-fd helpers; Windows no-ops |
| plib/src/diag.rs | strerror_r path gated to Unix; Windows uses Rust's own message |
| plib/tests/write_atomic_umask.rs | Whole-file #![cfg(unix)] gate |
| gettext-rs/src/lib.rs | LC_MESSAGES gated to Unix, falls back to LC_ALL on Windows |
| gettext-rs/src/catalog.rs | NLSPATH split via env::split_paths for platform separators |
| .github/workflows/TestingCI.yml | WINDOWS_CRATES, Windows target clippy lint, Windows build/test job |
| README.md / CLAUDE.md | Document Windows subset, behavior, and build/test commands |
| .gitattributes | Force LF checkout for byte-compared fixtures, with CRLF-sensitive exceptions |
The implementation is consistently gated, removed imports are correctly localized into their #[cfg(unix)] blocks, the apply_to reordering (times before permissions) is sound, and the is_affirmative/FileMetadata refactors preserve existing semantics. I did not find concrete code defects.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The specifier loop and the pointer-qualifier loop each matched `_ if let Some(m) = super::cv_qualifier_modifier(name_id)`. `if let` guards were stabilised only recently, and these two were the only reason the workspace needed a Rust newer than 1.88. Each loop now asks cv_qualifier_modifier first and continues; the qualifier spellings are disjoint from every arm before them, and nothing follows the match in either loop body, so the parse is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.84 was declared but no longer built anything: clap 4.6 and clap_lex 1.1 are edition 2024 and declare rust-version 1.85, and ipp 5.4 (lp's IPP client) uses let chains, stable from 1.88, without declaring a rust-version at all. With the cc `if let` guards rewritten, 1.88 checks the whole workspace, every target, and the Windows crates for x86_64-pc-windows-msvc. Cargo.toml, clippy.toml's msrv, CLAUDE.md and the Copilot instructions all say 1.88.0. Clippy at that msrv asks for the APIs it makes available: 21 `x % n == 0` tests on unsigned integers become `x.is_multiple_of(n)` (stable 1.87), and dd's byte swap walks `as_chunks_mut::<2>()` (stable 1.88) rather than chunks_exact_mut(2). Each is the same computation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The platform jobs build with the latest stable, so the declared rust-version had drifted unnoticed to a release that could not build the workspace at all. A new msrv job reads rust-version from Cargo.toml -- the one place it is set -- installs that toolchain, and checks every target of the workspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`LC_MESSAGES` is POSIX, not ISO C, and Windows has none: to_libc's unconditional `libc::LC_MESSAGES` was the first compile error of every crate in the workspace on x86_64-pc-windows-msvc. It is now Unix-only, and Windows takes the existing LC_ALL fallback arm. NLSPATH was split on ':', which on Windows cuts a template at its drive letter. It is now split as the platform splits PATH (std::env::split_paths: ':' on Unix, ';' on Windows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table plib is the base of every utility crate, and nothing in it compiled on x86_64-pc-windows-msvc. The modules with no Windows meaning -- users, groups, utmpx, syslog, ttys, priorities, umask/mode strings, POSIX regex, temporary files, exec, the test(1) evaluator, SCCS files -- are now #[cfg(unix)]. The rest is portable: - diag::io_error_text: strerror_r is Unix-only; Windows uses Rust's own error text, which there is already the system's FormatMessage. - io: restore_sigpipe does nothing on Windows, which has no SIGPIPE (a write to a closed pipe is an error); ensure_std_fds_open does nothing there, since handles are not reused by number. write_atomic* and SigPipeIgnored, which exist for Unix modes and signals, are Unix-only. - locale: the wide-character functions (wcwidth, iswprint, towlower, mbrtowc -- MSVC has no wcwidth and a 16-bit wchar_t), radix_char and strftime are Unix-only. is_affirmative keeps the locale's YESEXPR on Unix and the POSIX locale's leading y/Y, its documented fallback, on Windows. - testing: get_binary_path finds the binaries beside the running test executable (cargo puts it in <bin dir>/deps) and appends the platform's executable suffix, so it holds for --target builds and .exe names as well as for the target directory and coverage layouts it used to reconstruct from environment variables. get_target_dir and get_profile, which did that reconstructing, had no other caller and are deleted. Linux and macOS behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cksum needed nothing beyond a portable plib. The other three spoke Unix
file attributes directly; each now keeps its Unix behaviour and takes
the Windows meaning of the same thing there:
- uuencode: the header mode comes from mode_of, the file's mode on Unix
and its read-only attribute seen as the owner-write bit (0444 or
0644) on Windows; with no file, new_file_mode is 0666 less the umask
on Unix and 0644 on Windows, which has no umask.
- uudecode: set_mode applies the header's bits on Unix and makes the
file read-only on Windows exactly when the header withholds owner
write; the access(W_OK) check is "not read-only" on Windows.
- compress: the preserved metadata keeps fs::Permissions, which is
portable, and the times as fs::FileTimes, set with File::set_times on
every platform in place of a hand-built utimensat (futimens on Unix,
still to the nanosecond). They are set through the output's write
handle before it is dropped -- a reopen would be refused under a
umask that leaves the output without owner write, and the times
silently lost -- and before the mode, which comes last. chown and the
hard-link warning stay Unix-only (stable Rust has no Windows link
count), and {NAME_MAX} is NTFS's 255 on Windows, which has no
pathconf.
compress copies the input's read-only attribute onto its output, and
Windows will not delete a read-only file wherever
FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE is not honoured (Wine, older
Windows): the input could not be removed, nor the output in the
back-out, leaving both behind. Every removal now goes through one
remove_file, which on Windows clears the attribute first and restores
it if the removal still fails; on Unix it is the plain fs::remove_file.
Two tests pin the rules this changes: the times survive a 0277 umask
under compress and compress -d (Unix), and a read-only input is
compressed and removed (every platform).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The xform test binary did not compile on Windows. What it tests is now portable except where the subject is Unix-only: - uue: the expected header digits come from the file's mode on Unix and its read-only attribute on Windows, as uuencode writes them; the read-only target test sets that attribute through set_read_only, which also clears it for cleanup (Windows will not delete a read-only file); "running as root" is a Unix question. - compress: the binary round trips decompress with `uncompress` on Unix and `compress -d` on Windows, where xform/build.rs makes no argv[0] aliases; the four tests of the zcat and uncompress aliases themselves are Unix-only, as are the five that need plib::tmp (the -v output and mode/time preservation tests). 62 of the 72 run on Windows (x86_64-pc-windows-gnu under Wine); all 72 still run on Unix. .gitattributes checks text out with LF on every platform: the fixtures are compared byte for byte, and a Windows checkout would otherwise turn them into CRLF. The diff fixtures and display/test.mdoc, committed with the CRLF they test, are never converted; no committed file changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m for Windows on Linux A new `windows` job builds and tests, with MSVC, the crates named in the workflow's WINDOWS_CRATES -- gettext-rs, plib and posixutils-xform (cksum, compress, uuencode, uudecode). It is required like the other platform jobs, and runs under bash so the crate list expands to separate arguments. The lint job also installs the x86_64-pc-windows-msvc target and runs clippy over the same crates for it, so code that only compiles on Windows is linted on every push without waiting on a Windows runner. The msrv job also checks the same crates for that target with the declared minimum Rust. Utilities are ported a whole crate at a time because `cargo test -p` builds every binary in the crate; a crate joins WINDOWS_CRATES once all of its binaries and tests are portable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README gains a Windows section: the xform utilities build and are tested there, the whole workspace does not, what a file mode, compress's metadata and the zcat/uncompress aliases mean on Windows, how a crate joins WINDOWS_CRATES (gated with #[cfg(unix)], never stubbed), and how to run the Windows tests on Linux under Wine. CLAUDE.md's platform line points at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text crate is the next one to build on Windows, and it classifies, case-maps, measures and decodes characters through plib::locale, whose character functions existed only on Unix. The MSVC CRT cannot stand in for the Unix C library: its wchar_t is 16 bits, so a character outside the Basic Multilingual Plane is not one wide character, and it has no wcwidth. On Windows these functions now use Rust's Unicode semantics with input decoded as UTF-8, keeping each public function's signature and contract so callers are unchanged: - ASCII answers exactly as the POSIX locale does. Above ASCII each predicate takes the closest Unicode property, documented on the function; digit and xdigit stay ASCII-only as POSIX defines them, and digits of other scripts count as alpha so alnum stays the union of alpha and digit, as glibc does. - to_lower/to_upper take a one-character Unicode mapping and leave a character with a multi-character mapping unchanged. - wcwidth_char gives 0 for NUL, the combining-only blocks, zero-width spaces and variation selectors, -1 for other controls, and 2 for East Asian Wide and Fullwidth (Kuhn's ranges plus the main emoji blocks). - mb_char_slices and MbDecoder decode UTF-8 with the same one-byte fallback for an invalid or incomplete sequence; MbDecoder carries a split sequence into the next chunk. - radix_char is ".". strftime stays Unix-only: plib has no date library to replace localtime_r and LC_TIME. Each public function keeps one shared ASCII-versus-wide split, with per-platform private helpers answering each half. The tests split the same way: platform-neutral tests (an all-ASCII sweep of every class against the POSIX definitions, ASCII slicing and decoding, collation) run everywhere, the libc-locale tests stay Unix-only, and new Windows tests cover non-ASCII classification, case mapping, widths, invalid UTF-8 and split sequences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text crate's grep, sed, nl, csplit and tr use plib::regex, which wraps
the C library's regcomp/regexec. The Windows C runtime has none, so on a
Windows target plib now compiles musl's POSIX regex (TRE) from
plib/vendor/musl-regex with the cc crate and links it under plib_-prefixed
names. Unix keeps the C library's regex, unchanged.
Vendored from musl 1.2.6 (musl-1.2.6.tar.gz, SHA-256
d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a, signature
verified against musl's key): src/regex/{regcomp.c,regexec.c,regerror.c,
tre-mem.c,tre.h} (byte-identical to 1.2.5), src/ctype/iswctype.c, an adapted
include/regex.h, and COPYRIGHT. musl is MIT; the TRE-derived files are
Ville Laurikari's, 2-clause BSD, license text in each file.
Local changes, each marked "plib:" and listed in the vendor README:
- shim headers for musl internals (hidden, CHARCLASS_NAME_MAX, RE_DUP_MAX,
LCTRANS_CUR) and plib_ renames of every external symbol;
- regoff_t is ptrdiff_t, and ALIGN no longer assumes long is pointer-sized
(on 64-bit Windows it truncated pointers and under-aligned blocks);
- TRE_CHAR_MAX is capped at a 16-bit WCHAR_MAX;
- [[:class:]] uses musl's wctype/iswctype rather than the runtime's, whose
msvcrt version has no "blank";
- regexec's pmatch[restrict] is spelled as a pointer for MSVC;
- a musl bug: the backtracking matcher (any pattern with a back-reference)
assumed the lookahead character was one byte, so after a multibyte
character back-references compared from the wrong byte. In UTF-8,
\(a\)\1 did not match "üaa". 226 of 4000 generated patterns disagreed
with their ASCII transliteration before the fix, none after.
plib/build.rs compiles the C only for a Windows target (CARGO_CFG_TARGET_OS,
since the script runs on the host), and only when a C compiler for it
exists: Cargo does not tell a build script whether it is checking or
building, and the Linux lint job's clippy for x86_64-pc-windows-msvc has no
MSVC compiler. cc is a plain build-dependency, since a target-cfg one is
matched against the host.
plib::regex keeps one wrapper; only the FFI declarations differ by
platform. diag::init_locale now also selects setlocale(LC_ALL, ".UTF-8") on
Windows so the runtime's mbtowc, and so the regex, reads UTF-8.
New tests cover back-references, leftmost-longest, bracket classes,
REG_ICASE, REG_NOTBOL and a UTF-8 pattern. The UTF-8 one returns early
under the -gnu targets (Wine), whose msvcrt has no UTF-8 locale.
plib::regex's UTF-8 test asserts the multibyte back-reference cases
only off Apple platforms: there plib uses Apple's libc regex, which
comes from the same TRE code and keeps the flaw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
io: add open_terminal_input and open_terminal_output, which open what a prompt reads its answer from and writes its question to. POSIX takes such answers from /dev/tty rather than standard input (pr -p, patch's questions), and several utilities spell that path out by hand. Windows has no /dev/tty; the same thing there is the console, whose input and screen buffers open as CONIN$ and CONOUT$. One pair of constants holds the difference, so a caller has one rule on both platforms, and fails the open the same way when the process has no terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pr: ask std::io::IsTerminal whether standard output is a terminal instead of libc::isatty, the same question on Unix. The -p pause reads its newline through plib::io::open_terminal_input, so it waits on the console on Windows. The SIGINT handler is unchanged: the Windows C runtime has the same signal, raise and fflush. patch: the file-name and yes/no prompts share one helper that writes the question with plib::io::open_terminal_output and reads the answer with open_terminal_input, rather than opening /dev/tty by hand twice. The dangerous-name check now refuses any name with a root or a drive prefix rather than asking is_absolute: identical on Unix, but on Windows "/etc/passwd" and "C:file" are not is_absolute and would have escaped the directory being patched. csplit: the signals that remove created files are a per-platform list. Windows has no SIGHUP or SIGQUIT; its keyboard quit (Ctrl-Break) arrives as SIGBREAK, which the libc crate does not declare. It also has no sigaction, so the handler is installed with the C runtime's signal(), which resets to the default before the handler runs; the handler re-raises with the default either way. The handler itself is unchanged (libc::unlink is the CRT's _unlink there). sort: numeric_conv reads localeconv on Unix as before. The libc crate has no localeconv for Windows, and plib takes numbers in the POSIX locale's form there, so -n uses plib::locale::radix_char and no thousands grouping. diff: directory-loop detection keys on a per-platform DirId: (dev, ino) on Unix as before; on Windows, where stable Rust does not expose the volume serial and file index, the canonical path, which resolves symlinks and junctions and is unique since Windows has no directory hard links. Special files are named by a special_kind helper; Windows has no FIFOs or devices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows C runtime reads no locale environment variable: setlocale(LC_ALL, "") is always the user's regional setting. So LC_ALL=C had no effect on Windows, and a utility collated, classified and decoded multibyte input by the regional setting whatever the user asked for -- sort and comm ordered lines by it, and tr's character classes and case mapping followed Unicode even under LC_ALL=C, unlike Unix. init_locale now resolves each category's POSIX value (LC_ALL, else LC_<category>, else LANG, the first set and non-empty) with one helper, and maps it as glibc would: value LC_CTYPE other categories C, POSIX CRT "C"; ASCII, byte per char CRT "C" C.UTF-8, POSIX.utf8 CRT ".UTF-8"; Unicode, UTF-8 CRT "C" (byte order) anything else, unset CRT ".UTF-8"; Unicode, UTF-8 the user's locale Any other value keeps the user's locale, since Windows locale names are not POSIX ones. The LC_CTYPE row also sets a process-wide mode in plib::locale, which the Windows halves of the predicates, to_lower/to_upper, wcwidth_char, mb_char_slices and MbDecoder consult: in the C mode nothing above ASCII is in a class, case-maps or has a width, and every byte is one character (above ASCII undecodable), as in glibc's C locale. A program starts in the C mode, as a Unix program starts in "C". Unix is unchanged. plib::testing::utf8_locale() returns "C.UTF-8" on Windows without running `locale -a`, which Windows does not have: plib supports UTF-8 on every Windows system, and C.UTF-8 means there what it means under glibc. locale_matching() returns None on Windows, since no name other than a C locale selects the locale it names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text test binaries did not compile on Windows. What they test is now portable, except where the subject itself is Unix: - diff: the fixture expectations spell paths with `/`; diff joins a directory operand and an entry with the platform separator, so the checker converts them (no fixture contains a `/`), and the per-file header test builds its paths with Path::join. Gated to Unix: the three symlink tests (a Windows symlink needs a privilege tests do not have), the two that make a file unreadable with mode 000 (no Windows meaning), and the SIGPIPE test. - grep, sed: the open-failure diagnostics are the system's own text, which differs on Windows; the expectations now come from the same failing open in the test, through plib::diag::io_error_text. - pr, uniq, unexpand: three tests used plib::tmp (Unix-only) only for a scratch directory; they write one file under std::env::temp_dir. - wc: the SIGPIPE test is Unix-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posixutils-text joins WINDOWS_CRATES: all 22 of its utilities build and their tests pass on Windows, so the windows job builds and tests it with MSVC, the lint job clippies it for x86_64-pc-windows-msvc, and the msrv job checks it there with the declared minimum Rust. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a crate README's Windows section lists both ported crates -- xform and text, built natively with MSVC -- and everything that behaves differently there: modes, compress's metadata and aliases, Unicode classification with the POSIX locale variables, musl regex's U+FFFF limit, sort -n's decimal point, diff's special files and loop identity, console prompts and csplit's signals. WINDOWS.md is the repeatable process for "make crate X build on Windows": the rules (whole crate, gate never stub, Unix unchanged, one helper per rule), the table of what each Unix concept means on Windows, the steps from survey through plib, sources, tests, verification (with the Wine recipe and its limits), CI wiring and a bisectable commit series. README points porters to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No description provided.