Skip to content
Browse documentation

Windows runtime baseline

Configure Windows paths, symlinks, native commands, PowerShell and Registry preferences.

EtchReference

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.