An independent C++23 implementation of GNU Stow's symlink-farm behavior,
with a GNU Stow 2.4.1 compatibility baseline. It builds native stow and
chkstow executables with Meson/Ninja. No Perl interpreter, generated Perl
files, or original source directory is required at runtime.
Requirements: a C++23 compiler and standard library supporting std::expected
(tested with GCC 13), Meson >= 1.3, Ninja, pkg-config, and PCRE2 development
headers (libpcre2-8). Python 3 is needed only for tests.
meson setup build
meson compile -C build
meson test -C build --print-errorlogs
./build/stow --helpOptional comparison with a local GNU Stow installation:
meson configure build -Dreference_stow=/path/to/stow/bin/stow
meson test -C build --print-errorlogsThe differential tests set PERL5LIB for a source-tree reference themselves.
They use isolated temporary trees, never the caller's home or real target.
One explicit GNU 2.4.1 stale-plan regression is tested separately: if a
batch unstow hits its exact invalid target diagnostic without modifying
the tree, a separate oracle serializes the reference's unstow phase.
The native result is compared against that intended state, not against
the reference's failed invocation. Such cases are reported in the test log.
Ordinary scenarios require matching exit statuses and tree states directly.
meson setup build-release --buildtype=release --prefix=/usr/local
meson compile -C build-release
meson install -C build-releaseInstallation is deliberately not performed automatically: installing stow
can replace an existing GNU Stow command. Use --prefix or DESTDIR for an
isolated installation. Disable tests with -Dtests=false for a runtime-only
build. For sanitizer testing:
meson setup build-sanitize -Db_sanitize=address,undefined -Db_pie=false \
-Dcpp_args=-fno-pie -Dcpp_link_args=-no-pie
meson test -C build-sanitize --print-errorlogs# Packages live in ~/dotfiles/editor and ~/dotfiles/shell.
stow -d ~/dotfiles -t ~ --dotfiles editor shell
stow -d ~/dotfiles -t ~ -n -v --dotfiles -R editor
stow -d ~/dotfiles -t ~ --dotfiles -D shell
chkstow -t ~ --badlinksBoth stow and target directories must already exist. Package operands are
single directory names, not paths; trailing slashes are accepted.
STOW_DIR supplies the default stow directory. The default target is the
parent of the supplied stow directory path (before resolving directory
symlinks). Source and target roots are then canonicalized.
All principal options are implemented: -S, -D, -R, mixed actions,
--simulate, --adopt, --dotfiles, --no-folding, --compat,
--ignore, --defer, --override, verbosity, help, and version.
Unstows are planned before stows, regardless of the order of action groups.
Repeated options and bundled short options are supported. Verbosity >= 1
prints planned operations to stderr; >= 2 also prints resolved directories.
Levels 3–5 are accepted but do not reproduce Perl's internal debug traces.
~/.stowrc is read before ./.stowrc. Each line uses shell-style quoting,
but no shell is executed. CLI scalar settings override configuration;
regex lists accumulate. Configuration package operands are ignored.
Only configuration --dir/--target values expand $VAR, ${VAR}, and
~/~user; command-line paths are literal after the calling shell's
expansion. Undefined environment variables are errors.
Ignore precedence is package .stow-local-ignore, then
~/.stow-global-ignore, then built-in GNU Stow defaults. Custom files
replace, not append to, lower-priority lists. A top-level
.stow-local-ignore is always ignored. Ignore-file patterns without /
match whole basenames; patterns containing / match path segments.
CLI --ignore is suffix-anchored; defer/override are prefix-anchored.
Dotfile translation happens after ignore matching.
--adopt overwrites the corresponding regular package file with the
target file, then replaces the target file with a symlink. Back up or
version-control package contents before using it. Simulation does not
move or overwrite any file. Adoption works across filesystems.
This is a behavioral rewrite, not a line-by-line Perl translation. Source and regression tests establish the contract; temporary-tree tests exercise the native programs; differential tests compare exit status and complete filesystem trees against the supplied reference after each operation.
The core is one deep module with a small interface:
plan(Request) -> Plan, inspect tasks()/conflicts(), then execute().
CLI parsing and diagnostics remain outside the filesystem algorithm.
Options, Request, Task, and Conflict are value types; operation and
node kinds are scoped enums. PCRE2 allocations have RAII ownership.
CLI parsing returns std::expected; I/O failures use exceptions, caught
and reported only at command entry points.
The planner uses a read-only filesystem view with an in-memory overlay.
Future links, removed entries, and newly created directories are visible
to subsequent planning. A newly unfolded directory masks the old symlink's
children, preventing accidental traversal into package contents. Ownership
uses normalized path components, never textual prefix matching.
Other source directories are recognized through .stow markers.
Absent directory targets are normally folded into one relative symlink. When packages share a directory, an owned folded link is unfolded and both packages' entries are replanned. Unstowing only removes owned links; nonempty directories containing a single remaining package can refold. Empty real directories remain in place, as in GNU Stow. No persistent ownership database is used.
Planning never writes. A conflict anywhere aborts the entire invocation,
including otherwise valid packages and adoption moves. Before execution,
all observed node kinds, link text, regular-file size/mtime, and directory
listings are rechecked. Execution verifies real directory parents, removes
only symlinks or empty directories, and never recursively deletes targets.
Simulation cannot be bypassed by calling Plan::execute().
Normal CLI, configuration, ownership, folding/unfolding, cleanup, dotfile, and package-management behavior targets GNU Stow 2.4.1. Output text and operation counts are not byte-for-byte compatible; the native planner can retain redundant intermediate operations rather than Perl's task cancellation. The Perl module interface is not provided.
Safety differences are intentional:
- Empty,
.and..package names and targets inside a stow/source directory are rejected. - Unreadable or malformed configuration/ignore files and invalid regexes are explicit failures; a bad ignore file is never silently discarded.
- Source/target directory nesting is limited to 256 levels, and followed symlink chains to 64; detected cycles fail rather than looping.
.stow/.nonstowtarget directories and the current stow directory are protected during both node and directory traversal.- Adoption accepts only regular files and rejects source-directory symlink traversal; it does not overwrite symlink destinations or special files.
- Refolding refuses renamed links (including translated
dot-*entries) that would change visible names when replaced by a directory symlink. - A package file overlapping a real target directory is left alone during unstow instead of triggering the Perl implementation's traversal error.
- Batch unstows use the planned directory state, fixing GNU 2.4.1's
unstow_contents() called with invalid targeterror when another package has already scheduled that physical directory for removal.
PCRE2 covers common Perl regex syntax, including lookarounds, but it is not the Perl regex runtime: embedded Perl code and Perl-specific features are not supported. These fail explicitly at compilation.
chkstow preserves the historical text-based --list inference and its
limitation when the source directory is not named stow. It does not
follow directory symlinks. .stow and .notstowed trees are skipped.
It additionally supports help/version and returns failure for invalid
options or unreadable targets.
Not an atomic filesystem transaction: planning conflicts cause zero writes, but an I/O error during execution can leave completed operations in place. There is no rollback of adoption overwrites. Preflight checks reduce stale-plan risk but are not a locking mechanism or protection against hostile concurrent filesystem mutation. Run against trees you control, without concurrent writers; this is not a privileged helper.
GPL-3.0-or-later; see COPYING and NOTICE.