Skip to content

Fix Windows relative bind mounts rejected for extended-length path prefix#51

Merged
jssblck merged 1 commit into
mainfrom
jssblck/admiring-cohen-dd568c
Jul 1, 2026
Merged

Fix Windows relative bind mounts rejected for extended-length path prefix#51
jssblck merged 1 commit into
mainfrom
jssblck/admiring-cohen-dd568c

Conversation

@jssblck

@jssblck jssblck commented Jul 1, 2026

Copy link
Copy Markdown
Member

Why

On Windows, a service with a relative bind mount (volume=./seed:/docker-entrypoint-initdb.d) fails at eph up (issue #44):

Error: failed to create container
Caused by:
    Docker responded with status code 500: \?\C%!(EXTRA string=is not a valid Windows path)

Workspace::from_path canonicalized with std, which on Windows returns the extended-length \?\C:\... form and stores it in Workspace::path. A relative bind resolves against that root, so eph forwarded \?\C:\... to Docker as the bind source, which the daemon rejects. (The %!(EXTRA ...) garble is an upstream moby fmt artifact, reproducible with a bare docker run -v.) The prefix also leaked into eph check and eph status output.

What Changed

  • Canonicalize with dunce::canonicalize in Workspace::from_path and Watch::new. It returns a plain C:\... path whenever one exists (the common case), keeping \?\ only for paths that genuinely require it. This fixes bind sources, display output, and compose/build current_dir at the source. On Unix it is std::fs::canonicalize (no behavior change).
  • resolve_volume_spec is now fallible. A boundary guard (reject_verbatim_bind_source) rejects any host source still carrying \?\ (the genuine long-path fallback) with an actionable message, instead of handing Docker a source it only rejects cryptically. The relative join uses to_str() so a non-UTF-8 path errors rather than silently mounting a lossy source.
  • Documented the fix and the workspace-id churn in the troubleshooting guide.

Designed and reviewed in collaboration with Codex (gpt-5.5).

Testing

  • cargo test --lib (96 pass, including two new regression tests: a clean Windows root resolves without the prefix, and a verbatim root is rejected), cargo test --doc (7 pass), cargo clippy --all-targets (clean).
  • Live on Windows 11 with Docker Desktop 29.5.3: reproduced the issue's exact .eph (named pgdata + volume=./seed:/docker-entrypoint-initdb.d). eph up now succeeds (was a 500). docker inspect shows a clean bind source, and the seed table created by the bind-mounted init script exists in postgres. eph check / eph status print C:\Users\... with no \?\.

User-Facing Or Compatibility Impact

  • Relative bind mounts now work on native Windows.
  • eph check / eph status no longer show the \?\ prefix.
  • Windows-only breaking change: the workspace id is derived from the path string, so dropping \?\ changes the id. Containers, named volumes, and the state directory created by an older build live under the previous id and are orphaned; they need manual docker cleanup. Named volumes did work before, so this only matters for services already running under an older build. Documented in the troubleshooting guide.
  • Absolute Windows bind sources (volume=C:\data:/x) remain unsupported: they route to the named-volume branch and the drive-letter colon breaks splitn. That is a separate pre-existing bug, out of scope here; a real volume-spec parser is the proper fix.

Fixes #44

…efix

On Windows, std's canonicalize returns the extended-length `\?\C:\...`
form. Workspace::from_path stored that in Workspace::path, so a relative
bind mount resolved onto it and was forwarded to Docker as `\?\C:\...`,
which the daemon rejects (with a garbled `%!(EXTRA ...)` moby fmt
artifact). The prefix also leaked into `eph check` and `eph status`.

Canonicalize with dunce instead: it yields a plain `C:\...` path whenever
one exists, fixing bind sources, display output, and compose/build
current_dir in one place. On Unix it is std::fs::canonicalize. As a
boundary guard, resolve_volume_spec is now fallible and rejects any host
source that still carries `\?\` (the genuine long-path fallback) with an
actionable message instead of forwarding a source Docker only rejects
cryptically. The relative join uses to_str so a non-UTF-8 path errors
rather than mounting a lossy source.

Windows-only breaking change: the workspace id is derived from the path,
so dropping `\?\` changes it. Containers, volumes, and state under the
old id are orphaned and need manual cleanup (documented in the
troubleshooting guide).

Fixes #44

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jssblck jssblck merged commit f7d770b into main Jul 1, 2026
19 checks passed
@jssblck jssblck deleted the jssblck/admiring-cohen-dd568c branch July 1, 2026 18:20
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.

Windows: relative bind-mount source passed to Docker as \?\ path, rejected as invalid

1 participant