Windows runtime baseline
Configure Windows paths, symlinks, native commands, PowerShell and Registry preferences.
The initial baseline targets Windows 11 x64 with CPython 3.9–3.14. Start Etch
from a checkout with python -S etch; no third-party runtime packages, POSIX
shell, elevation, or Developer Mode are needed for validation, facts, planning,
and diagnostics of a profile without platform-specific actions.
The built-in os fact is windows, arch normalizes AMD64 to x86_64
and ARM64 to aarch64, and distro is unavailable outside Linux. Select
Windows modules or actions with 'when': {'os': 'windows'}. ARM64 normalization
is defined but native Windows ARM64 is not yet qualified in CI.
Paths use the host Python path rules. Module-relative executable paths accept
Windows backslashes or forward slashes; executable discovery uses Python’s
shutil.which, including Windows PATHEXT for bare command names. Explicit
paths must include the executable suffix (for example bin/tool.exe) to work
on every supported Python version. Use native executable probes;
PowerShell invocation is described below; batch-file execution is unsupported.
Home expansion follows Python’s Windows USERPROFILE/HOMEDRIVE/HOMEPATH
behavior rather than assuming the Unix HOME variable. Python configuration
strings must escape backslashes or use forward slashes.
This is a bounded Windows contract. Do not assume POSIX shell commands, sudo, executable permission bits, Homebrew, or Unix-only reference profiles work on Windows. Windows Server and WSL are outside the native baseline; WSL follows Linux rules.
Consumer launcher on PATH
The minimal example includes etch.cmd and etch.ps1,
plus an etch module that links both launchers into ~/.local/bin and adds it to
Windows user PATH. Copy the example layout, initialize vendor/etch, and run
.\etch.cmd plan --profile developer, then .\etch.cmd apply --profile developer.
Open a new terminal to pick up PATH changes. The links resolve back to your
consumer checkout even when invoked from an unrelated directory.
Before apply, the launcher actually creates and removes temporary file and
directory symlinks using Python, checking the current session rather than assuming
that elevation or a Developer Mode registry value grants access. Failure stops
before Etch applies actions and recommends Developer Mode for non-admin use.
plan, doctor, and help do not require this capability test. The launcher does
not enable Developer Mode or elevate. An elevated-session success does not grant
normal sessions privileges; target-directory ACLs still apply after a probe passes.
Filesystem contract
create makes directories and is a no-op when the directory exists. link
creates actual file or directory symlinks, with the directory type declared to
Windows explicitly. It never substitutes a junction, hard link, or copied file.
Enable Windows Developer Mode or use an account with the Create symbolic links
privilege. A privilege error explains that requirement; Etch does not elevate
itself. Replacement creates the candidate link before replacing the old link,
so failed creation preserves the old destination.
Destinations expand the native user home and resolve relative to the repository;
assets resolve inside their module. Absolute drive and UNC paths use native
Python semantics. Reject drive-relative (C:foo) and root-relative (\foo)
paths, which otherwise depend on the process drive. Forward slashes or escaped
backslashes work. Windows path comparisons are case-insensitive, and conflicting
owners fail even when destination spellings differ by case. The final destination
component remains the object managed, rather than its resolved symlink target.
Case-sensitive Windows directories are outside this initial contract.
clean removes only receipt-backed symlinks that are broken or explicitly
obsolete, immediately beneath selected real directories. It does not remove
regular files, directories, unowned links, junctions, or other reparse points.
Junctions are not managed destinations or cleanup roots, and receipt directories
cannot be redirected by junctions. Existing parent aliases resolve to their
actual location before ownership checks. There is no recursive cleanup.
Windows CI exercises file/directory symlinks, creation, cleanup, path conflicts, privilege diagnostics and idempotence on every supported Python version (3.9–3.14). Hosted Windows runners must permit symlink creation; those integration checks deliberately fail rather than skip when the required capability is unavailable.
PowerShell and command execution
Etch prefers PowerShell 7 (pwsh) on PATH and falls back to Windows
PowerShell 5.1 (powershell.exe) when it is unavailable. It never falls back to
cmd.exe or a POSIX shell. If neither interpreter is available, planning fails
with installation guidance. Etch respects the machine’s
execution policy and its providers do not add ExecutionPolicy Bypass. The optional
consumer .cmd launcher sets a process-only policy for its trusted bootstrap;
child provider processes inherit that session policy. Group Policy still takes
precedence, and user/machine registry policy is not changed.
Use a module-owned .ps1 file with the existing script action:
{
'script': {
'path': 'scripts/configure.ps1',
'args': ['two words', '$HOME; literal text'],
'env': {'MY_SETTING': 'value'},
'check': {'file_exists': 'configured.txt'},
},
'when': {'os': 'windows'},
}Etch invokes pwsh -NoProfile -NonInteractive -File and passes script arguments
as separate process arguments, without constructing a command string. .ps1
files must exist inside the module but do not need Unix executable bits.
Script names and arguments can contain spaces; dollar signs and semicolons in
arguments are literal. PowerShell’s script parameter binding still applies.
A string shell.command intentionally evaluates PowerShell code on Windows
through pwsh -NoProfile -NonInteractive -Command. Only use trusted code there;
pass variable data through script arguments or environment values. An argument
list shell.command runs a native .exe or .com directly with shell=False.
Batch .cmd/.bat files are rejected to avoid implicit command-shell expansion.
POSIX string commands continue to use /bin/sh -c on macOS/Linux.
Every child runs from the module directory with inherited environment plus the
configured values and existing ETCH_* context. Output is captured by default, with a bounded diagnostic tail on failure.
Use apply --verbose for live output; quiet suppresses verbose streaming.
Interactive input or elevation requests keep live output for prompts. Input is disabled unless stdin is enabled; PowerShell
remains without interactive prompts. Timeouts and nonzero exits fail the action. Checks retain
the existing skip/idempotence behavior. sudo is unsupported on Windows: Etch
never launches an elevation prompt or changes the account automatically.
Downloaded installers
Windows installer actions download a PowerShell script to a temporary .ps1
file, close it, verify any configured SHA-256, and execute it with -File.
The default interpreter is pwsh, with the same 5.1 fallback as script actions.
Set 'shell': 'powershell.exe' to explicitly select the built-in interpreter.
Other Windows installer forms, including native EXE/MSI and batch installers,
are rejected; no file type is guessed from the URL.
{
'installer': {
'url': 'https://example.com/install.ps1',
'sha256': 'replace-with-the-published-sha256-digest',
'args': ['argument with spaces'],
'check': {'file_exists': 'installed.txt'},
},
'when': {'os': 'windows'},
}Replace the placeholder digest with 64 hexadecimal characters from a trusted
source. Installer arguments and environment follow the command contract above.
Planning resolves interpreter availability without downloading or executing.
TLS verification stays enabled by default; module-owned custom CA files and
explicit verification overrides retain their existing semantics. No execution
policy override is added by the installer provider, and Etch does not elevate.
When started through the consumer .cmd launcher, the installer inherits its
process-only execution policy.
Temporary artifacts are removed after success, nonzero exit, hash failure, or timeout. Script-created files and other installer side effects are not rolled back. Checks skip already satisfied installers before interpreter resolution or network access.
Registry preferences
Use the optional Windows Registry reference plugin
for typed current-user preferences. It uses native Python winreg access,
shows current and desired values in plans, and leaves undeclared values intact.
System hives, deletion, elevation and automatic restarts are unsupported.
Continuous integration coverage
Every pull request runs the Windows regression and native provider suites on
CPython 3.9–3.14, alongside the full Linux and macOS suites. Windows checks include
configuration validation and formatting, plugin loading, conditions, dependency
graphs, fact caching, provider contracts, output, concurrent execution, partial
failure recovery, planning efficiency, and the native consumer profile.
Real local Git repositories exercise update and submodules update, including
autostash conflicts, local-edit preservation, nested submodules and pinned revisions.
Only tests for the POSIX consumer shell launcher are skipped in those Git suites;
Windows consumers invoke the Python entrypoint directly. These checks are required
by the Project gate.
The import suites exercise real Git object acquisition, pinned copies, structural
validation, path boundaries, publication cleanup, and native timeout cleanup.
Import requires Git for Windows on PATH; use the Python entrypoint, for example
python -S vendor/etch/etch import github:owner/dotfiles --path modules/git --repo C:/dotfiles.
Imported actions still need to support Windows or have appropriate OS conditions.
Windows coverage is still a selected suite rather than full test discovery. Unix-specific installer, Homebrew and macOS preference integrations remain covered on their target hosts.