wslc-wip 0.18.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f4750c407f5fe6be96d5e079e090d8d8a9aea434479bbbfcfe628db1ba1b993
4
- data.tar.gz: e3a71ba96464b059bcfb04e48c4512848f56564efa91aa0a05f6efbf4380e8aa
3
+ metadata.gz: e2bffbab317006bdd799443ef1dc7e16d9527219261f91c9758504ae85aef638
4
+ data.tar.gz: b1e0f8f72d44878564573fcc8076af18defe14375d1bee4cd4946c72139bcc1e
5
5
  SHA512:
6
- metadata.gz: adf6898a0e0bb8f15cc755fd64f6d7a86abfcadb915c2cf934171276ea80a76671fb423c57778c89b120684a4db211dc540f6e87e3d154b9c989e24bdbfe2904
7
- data.tar.gz: a871127bfbc45d40c43e08efe282e134b2e021df7af7c9209fe60c443e1158b13a8b7f7328664c453fe1ffce30c51b3ee8cc77d2b39a78c92298de303e8be7bc
6
+ metadata.gz: d16eda21cb080e2dd7decc2522c4dac7c99536f3c720c1de7d64f0065edd7487c633450db9eeb687b12138b2763d3093628e0d169b82d7896c781e7c8298bb3c
7
+ data.tar.gz: 5f668930355e17877237892da08dc440630f2015253b072c715e324c38045598765c58cb84d9657d7769527d3f27774f8d4f03f6bec2cc1bb785bca9a308cce2
data/README.md CHANGED
@@ -16,6 +16,40 @@ commands into a single `wip.yml`, and forwards them to `wslc.exe` / `wslc` as sa
16
16
 
17
17
  > **Status:** early release. Expect to track WSLC's own interface as it evolves.
18
18
 
19
+ ## Contents
20
+
21
+ - [Which mode should you use?](#which-mode-should-you-use)
22
+ - [Requirements & installation](#requirements--installation)
23
+ - [Quick start](#quick-start)
24
+ - [Configuration](#configuration)
25
+ - [Container mode](#container-mode)
26
+ - [Compose mode](#compose-mode)
27
+ - [Compose mode (native)](#compose-mode-native)
28
+ - [.env](#env)
29
+ - [.dockerignore](#dockerignore)
30
+ - [Source sync](#source-sync)
31
+ - [Commands](#commands)
32
+ - [doctor](#doctor)
33
+ - [Common errors](#common-errors)
34
+ - [FAQ](#faq)
35
+ - [Development](#development)
36
+ - [Contributing](#contributing)
37
+ - [Roadmap](#roadmap)
38
+ - [License](#license)
39
+
40
+ ## Which mode should you use?
41
+
42
+ `wip.yml` runs in one of three modes, set with `mode:`. Pick the one that matches your project:
43
+
44
+ | Situation | Use |
45
+ |---|---|
46
+ | No `compose.yml` — wip manages containers directly | [`mode: container`](#container-mode) (default) |
47
+ | Have `compose.yml`, don't want to install a third-party tool | [`mode: compose-native`](#compose-mode-native) |
48
+ | Have `compose.yml` and already use/prefer a third-party compose-for-`wslc` tool | [`mode: compose`](#compose-mode) |
49
+
50
+ `wip init` picks `compose-native` automatically when it finds a `compose.yml`/`docker-compose.yml`
51
+ next to it, and `container` otherwise — see [Commands](#commands).
52
+
19
53
  ## Requirements & installation
20
54
 
21
55
  Ruby 3.2+, WSL2, and Microsoft WSLC.
@@ -28,6 +62,9 @@ From source: `bundle install && bundle exec exe/wip version`.
28
62
 
29
63
  ## Quick start
30
64
 
65
+ This walks through `mode: container` (the default). Already have a `compose.yml`? See
66
+ [Which mode should you use?](#which-mode-should-you-use) first.
67
+
31
68
  ```bash
32
69
  gem install wslc-wip
33
70
  cd my-project
@@ -38,19 +75,19 @@ wip up -d
38
75
  wip rails console
39
76
  ```
40
77
 
41
- ## Full configuration example
78
+ ## Configuration
42
79
 
43
80
  Put a `wip.yml` in your project root. Running from a subdirectory walks up to find it, or pass
44
81
  `--config PATH` to point at one explicitly.
45
82
 
83
+ ### Container mode
84
+
46
85
  ```yaml
47
86
  version: 1
48
- mode: container # default. This example is container mode end-to-end — see "Compose mode" below
49
- # for mode: compose, which replaces container:/network:/dependencies: with a
50
- # compose: block instead; the two don't mix within one wip.yml.
87
+ mode: container # default
51
88
  wslc:
52
89
  command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
53
- container: app # required once dependencies: has entries; which one `up`/`exec`/`run`/`build`/`commands:`
90
+ container: app # required once dependencies: has entries; which one `up`/`exec`/`run`/`build`/`interaction:`
54
91
  # target. No default — a project must say which entry is the primary one explicitly.
55
92
  network: app-tier # optional; shared by every dependencies: entry so containers can resolve each other by name
56
93
  dependencies:
@@ -76,7 +113,7 @@ dependencies:
76
113
  env:
77
114
  MYSQL_ROOT_PASSWORD: password
78
115
  MYSQL_DATABASE: development
79
- commands:
116
+ interaction:
80
117
  rails:
81
118
  type: exec
82
119
  command: bin/rails
@@ -101,6 +138,7 @@ commands:
101
138
  type: build
102
139
  context: .
103
140
  tag: slidict/slidict:development
141
+ shadow_context: /mnt/c/Users/me/AppData/Local/wip/build-contexts
104
142
  sync: # optional; mirror the source into a named volume instead of bind-mounting it live
105
143
  exclude:
106
144
  - .git
@@ -112,23 +150,12 @@ sync: # optional; mirror the source into a named volume instead of bind-mounting
112
150
  credential, or auth. Keep real secrets out of the config file and in your runtime environment
113
151
  instead.
114
152
 
115
- ### .env
153
+ `interaction:` can also be spelled `commands:` — the same block under a different name, e.g. for
154
+ projects that already use `commands:`. `wip config` prints the effective config using `commands:`.
155
+ The two are aliases for the same feature, not separate ones: pick whichever name you like, but declaring both
156
+ `commands:` and `interaction:` in the same `wip.yml` is a `ConfigError`.
116
157
 
117
- Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
118
- per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
119
- passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
120
- custom commands. `.env` only fills in keys that aren't already set by the primary container's
121
- `env` or a command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH`
122
- to load a different file instead.
123
-
124
- ### .dockerignore
125
-
126
- `wip build` reads `.dockerignore` from the build context and stages a filtered copy of the
127
- context (skipping anything it matches) before handing it to `wslc build`, since `wslc` sends the
128
- context as-is otherwise. If there's no `.dockerignore`, the original context directory is used
129
- directly with no copying.
130
-
131
- ### Dependency containers
158
+ #### Dependency containers
132
159
 
133
160
  `dependencies:` holds every container uniformly — the primary one `container:` points at and any
134
161
  sidecar services (a database, Redis, ...) alongside it. Each entry accepts `image` (required),
@@ -141,10 +168,118 @@ entry by name first (creating `network:` beforehand if it doesn't exist and set)
141
168
  starts the primary one — so `bin/rails c` (or anything else run inside it) can reach
142
169
  `development.mysql`/`redis`/etc. by their dependency name, the same way Compose's service names
143
170
  resolve. `wip down` tears the primary container and all sidecars down (the network itself is left
144
- in place). Only the primary container is a target for `exec`/`run`/`build`/`commands:` — sidecars
171
+ in place). Only the primary container is a target for `exec`/`run`/`build`/`interaction:` — sidecars
145
172
  are only ever started and stopped, matching Compose's own service-vs-you-exec-into-one-of-them
146
173
  split.
147
174
 
175
+ ### Compose mode
176
+
177
+ If your project already has a real `compose.yml`, don't duplicate it in `dependencies:` — point
178
+ `wip` at it instead:
179
+
180
+ ```yaml
181
+ version: 1
182
+ mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
183
+ compose:
184
+ service: app # required: which compose service wip run/exec/NAME target
185
+ command: wslc-compose # required: the compose-for-wslc binary/path you have installed
186
+ file: compose.yml # optional; auto-detected next to wip.yml otherwise
187
+ project: myapp # optional; omitted lets the compose tool pick its own default
188
+ ```
189
+
190
+ `compose:` is mutually exclusive with `dependencies:`/`network` — pick one orchestration path per
191
+ project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc` CLI rather
192
+ than reimplementing Compose itself.
193
+
194
+ `wslc` itself has no native Compose support yet (tracked upstream in
195
+ [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
196
+ independent third-party tools fill the gap — for example
197
+ [bacarndiaye/wslc-compose](https://github.com/bacarndiaye/wslc-compose) (Python) and
198
+ [inuyume/wslc-compose](https://github.com/inuyume/wslc-compose) (Go), among others. `wslc` is new
199
+ and still evolving, so expect more of these to show up (and existing ones to change) over time.
200
+ `wip` deliberately doesn't pick a winner or default to any of them (unlike `wslc.command`, which
201
+ defaults to `auto` and searches for `wslc.exe`/`wslc`): `compose.command` is required and treats
202
+ every implementation equally — set it to whichever binary name or absolute path you've installed.
203
+ Whichever one you use needs to understand `-f FILE [-p PROJECT] up|down|exec|logs`, the subset of
204
+ the Compose CLI vocabulary `wip` drives. `wip doctor` reports whether the configured command is
205
+ found, its version, and which compose file `wip` resolved.
206
+
207
+ - `wip up`/`wip down` delegate straight to `<compose command> up -d`/`down`.
208
+ - `wip exec`/`wip NAME` (custom `interaction:`) run inside `compose.service`.
209
+ - `wip shell` also goes through the bridge: unless `interaction.shell` is defined in `wip.yml`, it
210
+ `exec`s `bash` against `compose.service`, falling back to `sh`.
211
+ - `wip logs [-f] [SERVICE...]` is only available in compose mode.
212
+ - `wip run` has no ephemeral-container equivalent in this exec-only vocabulary, so it falls back
213
+ to `exec` against the already-running `compose.service` (wip warns when this happens).
214
+ - `interaction:` entries with `type: run`/`type: build` aren't supported in compose mode — use your
215
+ compose tool's own `build`/`up --build` directly; compose owns builds for its own services.
216
+
217
+ ### Compose mode (native)
218
+
219
+ Don't want to install a third-party compose-for-`wslc` tool at all? `mode: compose-native` parses
220
+ `compose.yml` itself and drives `wslc` directly, the same way `mode: container` +`dependencies:`
221
+ already does — no external binary, and `wip run` gets a real ephemeral `wslc run --rm` instead of
222
+ the `exec` fallback above:
223
+
224
+ ```yaml
225
+ version: 1
226
+ mode: compose-native
227
+
228
+ compose:
229
+ service: app # required: which compose service wip run/exec/NAME target
230
+ file: compose.yml # optional; auto-detected next to wip.yml otherwise
231
+ project: myapp # optional; also names wip's own project network (defaults to the wip.yml
232
+ # directory's name) so services can reach each other by name
233
+ ```
234
+
235
+ There's no `compose.command` here (no external binary to name), and no top-level `container:` —
236
+ `compose.service` already names it.
237
+
238
+ This is explicitly a stopgap for as long as `wslc` itself has no native Compose support (tracked
239
+ upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) and third-party
240
+ compose-for-`wslc` tools stay incomplete. It only understands a minimal subset of the Compose spec.
241
+ Within `services.<name>:`, anything outside that subset is a load-time `ConfigError` naming the
242
+ offending key, rather than silently ignored — but everything *outside* `services:` at the document's
243
+ top level (`networks:`, `volumes:`, `configs:`, `secrets:`, ...) is the one exception: it's read by
244
+ real Compose tools, not by `wip`, so `wip` silently ignores it rather than rejecting an otherwise
245
+ valid compose.yml over sections it doesn't need to look at:
246
+
247
+ - Per service: `image`, `build` (string or `{context:, dockerfile:}`; resolved relative to
248
+ `compose.yml`, not wherever `wip` is invoked from), `command` (shell or exec form), `environment`
249
+ (mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
250
+ isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
251
+ long-syntax mappings), `working_dir`, `user`, `depends_on` (ordering only — a `condition:` other
252
+ than `service_started` is rejected, since there's no health-check support). `tty`, `stdin_open`,
253
+ and `networks` are accepted but silently ignored: TTY/stdin allocation is already decided per
254
+ invocation (see "TTY allocation" below), not fixed per service, and every service already shares
255
+ the one project network `compose.project` sets up.
256
+ - `wip logs` takes at most one `SERVICE` (defaulting to `compose.service`) — `wslc logs`, like
257
+ `docker logs`, follows a single container, unlike a real compose tool's multi-service view.
258
+ - `sync:` behaves exactly like `mode: container`'s (falls back to the primary service's own image,
259
+ defaults to `sync.mode: exec`) — none of the external bridge's `sync.image`/`sync.build`
260
+ requirement applies, since wip itself boots every container here.
261
+
262
+ ### .env
263
+
264
+ Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
265
+ per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
266
+ passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
267
+ custom commands. `.env` only fills in keys that aren't already set by the primary container's
268
+ `env` or a command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH`
269
+ to load a different file instead.
270
+
271
+ ### .dockerignore
272
+
273
+ `wip build` reads `.dockerignore` from the build context and excludes anything it matches before
274
+ handing the context to `wslc build`, since `wslc` sends the context as-is otherwise. Set
275
+ `commands.build.shadow_context` in `wip.yml` to a directory on the Windows filesystem to enable a
276
+ persistent shadow context for projects outside `/mnt/<drive>`. The first build copies every
277
+ included file; later builds only copy added or changed files and remove deleted or newly ignored
278
+ files. Without this setting the optimization is disabled, and it only applies under WSL2 — on WSL1
279
+ (or anywhere else) the context is handed to `wslc build` directly. Projects already on `/mnt/c` (or
280
+ another mounted Windows drive) also continue to build directly even when the setting is present.
281
+ The path must live outside the build context itself, or `wip build` refuses it.
282
+
148
283
  ### Source sync
149
284
 
150
285
  Bind-mounting the app directory (`.:/app`) is what usually makes a container boot crawl under
@@ -221,7 +356,7 @@ Everything below `sync:` is optional — `sync: {}` alone already works. With it
221
356
  - With `sync.build` configured, `wip build`s that image once per `wip up`/`wip sync` invocation
222
357
  (including once before a `--watch` loop starts, not on every tick) before mirroring with it.
223
358
 
224
- Like every built-in command, `wip sync` takes precedence over a `commands:` entry of the same
359
+ Like every built-in command, `wip sync` takes precedence over an `interaction:` entry of the same
225
360
  name; wip says so and points at `wip dispatch sync`, which still runs yours.
226
361
 
227
362
  Two things to keep in mind. The mirror runs `rsync` *inside* the container, so the image needs it
@@ -275,93 +410,6 @@ and uses the result, tagged `wip-sync-<container>:latest` by default (`build.tag
275
410
  Prefer managing the image yourself instead? Build and tag it however you like, then set `sync.image`
276
411
  to that tag directly — `sync.build`'s tag wins if both are set, so don't configure both at once.
277
412
 
278
- ### Compose mode
279
-
280
- If your project already has a real `compose.yml`, don't duplicate it in `dependencies:` — point
281
- `wip` at it instead:
282
-
283
- ```yaml
284
- version: 1
285
- mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
286
- compose:
287
- service: app # required: which compose service wip run/exec/NAME target
288
- command: wslc-compose # required: the compose-for-wslc binary/path you have installed
289
- file: compose.yml # optional; auto-detected next to wip.yml otherwise
290
- project: myapp # optional; omitted lets the compose tool pick its own default
291
- ```
292
-
293
- `compose:` is mutually exclusive with `dependencies:`/`network` — pick one orchestration path per
294
- project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc` CLI rather
295
- than reimplementing Compose itself.
296
-
297
- `wslc` itself has no native Compose support yet (tracked upstream in
298
- [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
299
- independent third-party tools fill the gap — for example
300
- [bacarndiaye/wslc-compose](https://github.com/bacarndiaye/wslc-compose) (Python) and
301
- [inuyume/wslc-compose](https://github.com/inuyume/wslc-compose) (Go), among others. `wslc` is new
302
- and still evolving, so expect more of these to show up (and existing ones to change) over time.
303
- `wip` deliberately doesn't pick a winner or default to any of them (unlike `wslc.command`, which
304
- defaults to `auto` and searches for `wslc.exe`/`wslc`): `compose.command` is required and treats
305
- every implementation equally — set it to whichever binary name or absolute path you've installed.
306
- Whichever one you use needs to understand `-f FILE [-p PROJECT] up|down|exec|logs`, the subset of
307
- the Compose CLI vocabulary `wip` drives. `wip doctor` reports whether the configured command is
308
- found, its version, and which compose file `wip` resolved.
309
-
310
- - `wip up`/`wip down` delegate straight to `<compose command> up -d`/`down`.
311
- - `wip exec`/`wip NAME` (custom `commands:`) run inside `compose.service`.
312
- - `wip shell` also goes through the bridge: unless `commands.shell` is defined in `wip.yml`, it
313
- `exec`s `bash` against `compose.service`, falling back to `sh`.
314
- - `wip logs [-f] [SERVICE...]` is only available in compose mode.
315
- - `wip run` has no ephemeral-container equivalent in this exec-only vocabulary, so it falls back
316
- to `exec` against the already-running `compose.service` (wip warns when this happens).
317
- - `commands:` entries with `type: run`/`type: build` aren't supported in compose mode — use your
318
- compose tool's own `build`/`up --build` directly; compose owns builds for its own services.
319
-
320
- ### Compose mode (native)
321
-
322
- Don't want to install a third-party compose-for-`wslc` tool at all? `mode: compose-native` parses
323
- `compose.yml` itself and drives `wslc` directly, the same way `mode: container` +`dependencies:`
324
- already does — no external binary, and `wip run` gets a real ephemeral `wslc run --rm` instead of
325
- the `exec` fallback above:
326
-
327
- ```yaml
328
- version: 1
329
- mode: compose-native
330
-
331
- compose:
332
- service: app # required: which compose service wip run/exec/NAME target
333
- file: compose.yml # optional; auto-detected next to wip.yml otherwise
334
- project: myapp # optional; also names wip's own project network (defaults to the wip.yml
335
- # directory's name) so services can reach each other by name
336
- ```
337
-
338
- There's no `compose.command` here (no external binary to name), and no top-level `container:` —
339
- `compose.service` already names it.
340
-
341
- This is explicitly a stopgap for as long as `wslc` itself has no native Compose support (tracked
342
- upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) and third-party
343
- compose-for-`wslc` tools stay incomplete. It only understands a minimal subset of the Compose spec.
344
- Within `services.<name>:`, anything outside that subset is a load-time `ConfigError` naming the
345
- offending key, rather than silently ignored — but everything *outside* `services:` at the document's
346
- top level (`networks:`, `volumes:`, `configs:`, `secrets:`, ...) is the one exception: it's read by
347
- real Compose tools, not by `wip`, so `wip` silently ignores it rather than rejecting an otherwise
348
- valid compose.yml over sections it doesn't need to look at:
349
-
350
- - Per service: `image`, `build` (string or `{context:, dockerfile:}`; resolved relative to
351
- `compose.yml`, not wherever `wip` is invoked from), `command` (shell or exec form), `environment`
352
- (mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
353
- isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
354
- long-syntax mappings), `working_dir`, `user`, `depends_on` (ordering only — a `condition:` other
355
- than `service_started` is rejected, since there's no health-check support). `tty`, `stdin_open`,
356
- and `networks` are accepted but silently ignored: TTY/stdin allocation is already decided per
357
- invocation (see "TTY allocation" below), not fixed per service, and every service already shares
358
- the one project network `compose.project` sets up.
359
- - `wip logs` takes at most one `SERVICE` (defaulting to `compose.service`) — `wslc logs`, like
360
- `docker logs`, follows a single container, unlike a real compose tool's multi-service view.
361
- - `sync:` behaves exactly like `mode: container`'s (falls back to the primary service's own image,
362
- defaults to `sync.mode: exec`) — none of the external bridge's `sync.image`/`sync.build`
363
- requirement applies, since wip itself boots every container here.
364
-
365
413
  ## Commands
366
414
 
367
415
  | Command | Description |
@@ -379,7 +427,7 @@ valid compose.yml over sections it doesn't need to look at:
379
427
  | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
380
428
  | `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose modes only; mode: compose-native takes at most one `SERVICE`) |
381
429
  | `wip sync [-w] [--interval N]` | Mirror the source into the sync volume once, or keep re-syncing with `--watch` (needs `sync:`) |
382
- | `wip NAME ARGS...` | Run `commands.NAME`, appending any extra arguments |
430
+ | `wip NAME ARGS...` | Run `interaction.NAME`, appending any extra arguments |
383
431
 
384
432
  TTY allocation is decided by combining the command's config, the CLI option, and whether both
385
433
  stdin and stdout are real TTYs.
@@ -474,6 +522,74 @@ docker buildx build \
474
522
  --push .
475
523
  ```
476
524
 
525
+ ## FAQ
526
+
527
+ **Which mode should I start with?**
528
+ Pick whichever `mode:` fits your project — see [Which mode should you use?](#which-mode-should-you-use)
529
+ for the breakdown.
530
+
531
+ **Can I use `dependencies:` and `compose:` together?**
532
+ No — `compose:` is mutually exclusive with `dependencies:`/`network`. Pick one orchestration path
533
+ per project; see [Configuration](#configuration).
534
+
535
+ **What's the difference between `mode: compose` and `mode: compose-native`?**
536
+ `compose` delegates to a third-party compose-for-`wslc` binary you install yourself
537
+ (`compose.command`); `compose-native` parses `compose.yml` itself and drives `wslc` directly, no
538
+ external tool required. `compose-native`'s Compose coverage isn't frozen at whatever it handles
539
+ today — it keeps growing until `wslc` ships native Compose support of its own. See
540
+ [Compose mode](#compose-mode) and [Compose mode (native)](#compose-mode-native).
541
+
542
+ **`dependencies:` already gives me sidecar containers — why would I need `compose-native` too?**
543
+ `dependencies:` and `compose-native` aren't really alternatives to each other — they're for two
544
+ different starting points. No `compose.yml`? Declare containers directly in `wip.yml`'s own shape
545
+ with `dependencies:`. Already have a `compose.yml`? Reusing it is where `compose-native` and
546
+ `mode: compose` both come in, so the real comparison is between those two, not against
547
+ `dependencies:`: `mode: compose` reuses it too, but only by delegating to a third-party
548
+ compose-for-`wslc` binary you install yourself; `compose-native` reuses the same `compose.yml`
549
+ without installing anything external, parsing it and driving `wslc` directly — and gets a real
550
+ `wslc run --rm` for `wip run` instead of the `exec` fallback `mode: compose` falls back to.
551
+ `mode: compose`'s coverage is whatever the external tool you point it at supports; `compose-native`'s
552
+ is maintained in this repo and actively extended, not treated as a permanent ceiling. See
553
+ [Which mode should you use?](#which-mode-should-you-use) for the full picture.
554
+
555
+ **What happens to `compose-native` once `wslc` gets official Compose support?**
556
+ `compose-native` exists to close the gap for as long as `wslc` has no native Compose support of its
557
+ own (tracked upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and
558
+ we intend to keep extending its Compose coverage until that lands — see
559
+ [Compose mode (native)](#compose-mode-native) and [Roadmap](#roadmap). `wip.yml`'s shape (`mode:`,
560
+ `compose:`) isn't planned to change for existing `container`/`compose`/`compose-native` setups, so
561
+ whatever we do once `wslc` catches up won't require rewriting your config.
562
+
563
+ **Is `sync:` required?**
564
+ No, it's entirely optional. Add it if boot times feel slow with a bind-mounted app directory — see
565
+ [Slow boot when the app directory is bind-mounted](#slow-boot-when-the-app-directory-is-bind-mounted).
566
+
567
+ **How does `wip` actually fix the slow bind-mount boot problem?**
568
+ The slowness comes from `.:/app`-style bind mounts going through virtiofs, where frameworks that
569
+ stat/open many small files at startup (e.g. Ruby's Zeitwerk) pay a round trip per file. A `sync:`
570
+ block moves the app off that path entirely: the host source is mounted read-only, the app itself
571
+ runs off a named volume (fast native storage inside the VM), and `wip` mirrors the read-only
572
+ source into that volume with `rsync` — once before boot, and on demand afterward via `wip sync`
573
+ (or continuously with `wip sync --watch`). Since the app never touches the bind mount directly, its
574
+ own file access is no longer paying the virtiofs cost; the trade-off is a one-way, slightly-delayed
575
+ mirror instead of an always-live view of host edits. See [Source sync](#source-sync) for the full
576
+ config and [Slow boot when the app directory is bind-mounted](#slow-boot-when-the-app-directory-is-bind-mounted)
577
+ for the root cause.
578
+
579
+ **Is it safe to put passwords/secrets in `wip.yml`?**
580
+ `wip config` masks any key matching token/password/secret/credential/auth when printing, but the
581
+ raw file itself is not encrypted. Keep real secrets in your runtime environment or `.env` instead
582
+ of committing them in `wip.yml` — but `.env` is only safe if it's actually untracked: add `.env` to
583
+ `.gitignore` (and confirm with `git check-ignore .env`) before putting anything sensitive in it.
584
+
585
+ **`wslc.exe`/`wslc` isn't found — what do I do?**
586
+ See [WSLC not found](#wslc-not-found) under Common errors.
587
+
588
+ **I'm migrating from `dip` — do I have to rename `interaction:`?**
589
+ No, `wip.yml` accepts `interaction:` as-is — it's the primary spelling, same as in `dip`. `commands:`
590
+ also works as an alias if you prefer it, but not both in the same file (that's a `ConfigError`). See
591
+ [Container mode](#container-mode).
592
+
477
593
  ## Development
478
594
 
479
595
  ```bash
@@ -486,7 +602,8 @@ bundle exec rake
486
602
  ```
487
603
 
488
604
  The test suite doesn't need WSLC — the resolution, build, and execution layers are all
489
- swappable. GitHub Actions runs RSpec and RuboCop on Ruby 3.2, 3.3, 3.4, and 4.0.
605
+ swappable. This project uses RuboCop for Ruby style and static analysis; `bundle exec rake` runs
606
+ both RSpec and RuboCop. GitHub Actions checks them on Ruby 3.2, 3.3, 3.4, and 4.0.
490
607
 
491
608
  ## Contributing
492
609
 
@@ -494,19 +611,10 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/slidict
494
611
  [CONTRIBUTING.md](CONTRIBUTING.md) for commit conventions, versioning policy, and the PR
495
612
  checklist.
496
613
 
497
- ## Not in the initial release
498
-
499
- Full Compose compatibility isn't reimplemented in `wip` itself — `mode: compose-native` parses
500
- `compose.yml` and drives `wslc` directly, but only covers a minimal subset (see
501
- [Compose mode (native)](#compose-mode-native)); full parity is otherwise available by delegating
502
- to a third-party compose-for-`wslc` tool (see [Compose mode](#compose-mode)). A resident/daemon
503
- process, a GUI, PowerShell-specific tuning, direct registry API/manifest parsing, self-update, and
504
- plugins are all unimplemented.
505
-
506
614
  ## Roadmap
507
615
 
508
616
  `wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
509
- named commands (`commands:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
617
+ named commands (`interaction:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
510
618
  sidecar services via `dependencies:` + `network:`. Rather than waiting on `wslc`'s own Compose
511
619
  support ([microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) or an external
512
620
  compose-for-`wslc` tool staying complete, `mode: compose-native` (see
@@ -520,8 +628,11 @@ compose-for-`wslc` tool in [compose mode](#compose-mode) — which of those actu
520
628
  entirely up to the external tool you point `compose.command` at; `wip` only forwards
521
629
  `-f FILE [-p PROJECT] up|down|exec|logs`, so treat that list as what Compose offers, not as
522
630
  something `wip` guarantees. See that section for what compose mode covers
523
- and its current limitations (`run`, and `commands:` of type `run`/`build`). What's still planned
524
- for `wip`, roughly in priority order:
631
+ and its current limitations (`run`, and `interaction:` of type `run`/`build`).
632
+
633
+ Beyond Compose parity, a resident/daemon process, a GUI, PowerShell-specific tuning, direct
634
+ registry API/manifest parsing, self-update, and plugins are all unimplemented and not currently
635
+ planned. What's still planned for `wip`, roughly in priority order:
525
636
 
526
637
  1. **`wip provision`** — a dip-style one-shot bootstrap hook (build → up deps → install deps →
527
638
  create/migrate/seed DB) so a new contributor can go from `git clone` to a working environment
@@ -541,7 +652,7 @@ for `wip`, roughly in priority order:
541
652
 
542
653
  Each of these should stay additive to the existing `wip.yml` shape — no breaking changes to
543
654
  `container`, `network`, `commands`, `dependencies`, or `compose` are planned. A resident daemon
544
- and a GUI remain out of scope; see "Not in the initial release" above.
655
+ and a GUI remain out of scope.
545
656
 
546
657
  ## License
547
658
 
@@ -1,33 +1,175 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'find'
3
+ require 'digest'
4
4
  require 'fileutils'
5
+ require 'find'
6
+ require 'json'
5
7
  require 'pathname'
6
8
  require 'tmpdir'
7
9
 
8
10
  module Wip
9
- # Stages a build context into a scratch directory with anything matched by
10
- # .dockerignore left out, since wslc build (unlike `docker build`) sends the
11
- # context as-is instead of filtering it itself.
11
+ # Filters build contexts for wslc and keeps WSL-hosted sources in a fast,
12
+ # persistent Windows-side shadow directory.
12
13
  class BuildContext
13
- def initialize(context, ignore: nil)
14
+ def initialize(context, ignore: nil, environment: Environment.new, shadow_root: nil)
14
15
  @root = Pathname(context).expand_path
15
16
  @ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
17
+ @environment = environment
18
+ @shadow_root = validated_shadow_root(shadow_root) if shadow_root
16
19
  end
17
20
 
18
21
  # on_progress fires before copying and after each file, as `|count, total|`,
19
22
  # so a caller can report elapsed progress even while copying one large file.
20
- def stage(on_progress: nil)
21
- return yield @root.to_s if @ignore.empty?
23
+ def stage(on_progress: nil, &block)
24
+ return stage_shadow(on_progress, &block) if shadow_required?
25
+ return block.call(@root.to_s) if @ignore.empty?
22
26
 
23
27
  Dir.mktmpdir('wip-build-context-') do |dir|
24
28
  copy_included_files(Pathname(dir), on_progress)
25
- yield dir
29
+ block.call(dir)
26
30
  end
27
31
  end
28
32
 
29
33
  private
30
34
 
35
+ # A shadow root under the context would itself be walked by included_files
36
+ # on the next build and copied into itself at ever-deeper paths, so the
37
+ # cache would grow without bound and the build would eventually fail.
38
+ def validated_shadow_root(shadow_root)
39
+ root = Pathname(shadow_root).expand_path
40
+ if root == @root || root.to_s.start_with?("#{@root}/")
41
+ raise ConfigError, "shadow_context (#{root}) must not be inside the build context (#{@root})"
42
+ end
43
+
44
+ root
45
+ end
46
+
47
+ def shadow_required?
48
+ return false unless @shadow_root
49
+
50
+ @environment.wsl2? && !@root.to_s.match?(%r{\A/mnt/[a-z](?:/|\z)}i)
51
+ end
52
+
53
+ # Keep one stable Windows-side context per source path. Its manifest lives
54
+ # beside (rather than inside) the context so it is never sent to wslc.
55
+ def stage_shadow(on_progress)
56
+ key = Digest::SHA256.hexdigest(@root.to_s)
57
+ cache = @shadow_root.join(key)
58
+ context = cache.join('context')
59
+ FileUtils.mkdir_p(cache)
60
+
61
+ File.open(cache.join('lock'), File::RDWR | File::CREAT, 0o600) do |lock|
62
+ lock.flock(File::LOCK_EX)
63
+ synchronize_shadow(context, cache.join('manifest.json'), on_progress)
64
+ # Keep the shadow immutable until wslc has finished reading it.
65
+ yield context.to_s
66
+ end
67
+ end
68
+
69
+ def synchronize_shadow(context, manifest_path, on_progress)
70
+ current = included_files.to_h { |entry| [entry, fingerprint(@root.join(entry))] }
71
+ previous = previous_manifest(context, manifest_path)
72
+ changed = current.keys.reject { |entry| current[entry] == previous[entry] }
73
+ removed = previous.keys - current.keys
74
+
75
+ apply_shadow_changes(context, changed, removed, on_progress)
76
+ FileUtils.mkdir_p(context)
77
+ write_manifest(manifest_path, current)
78
+ end
79
+
80
+ # A context we can't describe is a context we can't update incrementally:
81
+ # with no manifest there is no way to tell which of its entries are stale,
82
+ # so it gets discarded and rebuilt rather than left holding deleted or
83
+ # newly ignored files.
84
+ def previous_manifest(context, manifest_path)
85
+ return {} unless context.directory?
86
+
87
+ manifest = load_manifest(manifest_path)
88
+ return manifest if manifest
89
+
90
+ FileUtils.rm_rf(context)
91
+ {}
92
+ end
93
+
94
+ def apply_shadow_changes(context, changed, removed, on_progress)
95
+ total = changed.size + removed.size
96
+ on_progress&.call(0, total)
97
+
98
+ removed.each_with_index do |entry, index|
99
+ FileUtils.rm_rf(context.join(entry))
100
+ prune_empty_parents(context.join(entry).dirname, context)
101
+ on_progress&.call(index + 1, total)
102
+ end
103
+ changed.each_with_index do |entry, index|
104
+ copy_entry_atomically(@root.join(entry), context.join(entry))
105
+ on_progress&.call(removed.size + index + 1, total)
106
+ end
107
+ end
108
+
109
+ # Returns nil — not an empty manifest — when the manifest is missing,
110
+ # unreadable, or not a manifest at all, so callers can tell "nothing was
111
+ # synced yet" apart from "we no longer know what was synced".
112
+ def load_manifest(path)
113
+ manifest = JSON.parse(path.read)
114
+ manifest if manifest.is_a?(Hash)
115
+ rescue JSON::ParserError, SystemCallError
116
+ nil
117
+ end
118
+
119
+ def write_manifest(path, contents)
120
+ temporary = Pathname("#{path}.tmp-#{Process.pid}")
121
+ temporary.write(JSON.generate(contents))
122
+ File.rename(temporary, path)
123
+ ensure
124
+ FileUtils.rm_f(temporary) if temporary
125
+ end
126
+
127
+ def fingerprint(path)
128
+ stat = path.lstat
129
+ if stat.symlink?
130
+ { 'type' => 'link', 'target' => path.readlink.to_s }
131
+ else
132
+ { 'type' => 'file', 'size' => stat.size, 'mtime_ns' => stat.mtime.nsec + (stat.mtime.to_i * 1_000_000_000),
133
+ 'mode' => stat.mode }
134
+ end
135
+ end
136
+
137
+ # preserve: true keeps the source mode, so an executable stays executable
138
+ # even when the shadow lives on a DrvFs mount whose fmask would otherwise
139
+ # strip the bit and break a `RUN ./script` in the image build.
140
+ def copy_entry_atomically(source, target)
141
+ FileUtils.mkdir_p(target.dirname)
142
+ temporary = target.dirname.join(".#{target.basename}.wip-#{Process.pid}")
143
+ FileUtils.rm_rf(temporary)
144
+ FileUtils.copy_entry(source, temporary, true, false, false)
145
+ replace_atomically(temporary, target)
146
+ ensure
147
+ FileUtils.rm_rf(temporary) if temporary
148
+ end
149
+
150
+ # rename replaces an existing entry in a single step, so an interrupted
151
+ # update leaves the previous copy in place instead of no copy at all. Only
152
+ # a target rename can't overwrite — a directory where a file now lives, or
153
+ # a filesystem without overwrite semantics — needs the unsafe fallback.
154
+ def replace_atomically(temporary, target)
155
+ File.rename(temporary, target)
156
+ rescue Errno::EISDIR, Errno::ENOTDIR, Errno::ENOTEMPTY, Errno::EEXIST, Errno::EPERM, Errno::EACCES
157
+ FileUtils.rm_rf(target)
158
+ File.rename(temporary, target)
159
+ end
160
+
161
+ def prune_empty_parents(directory, root)
162
+ while empty_descendant?(directory, root)
163
+ directory.rmdir
164
+ directory = directory.dirname
165
+ end
166
+ end
167
+
168
+ def empty_descendant?(directory, root)
169
+ directory != root && directory.to_s.start_with?("#{root}/") &&
170
+ directory.directory? && directory.children.empty?
171
+ end
172
+
31
173
  def copy_included_files(destination, on_progress)
32
174
  files = included_files
33
175
  on_progress&.call(0, files.size)
@@ -37,7 +179,7 @@ module Wip
37
179
  # Keep links as links. Dereferencing a link here could copy arbitrary
38
180
  # host files outside the build context (for example, ~/.ssh/id_rsa)
39
181
  # into the staged directory and expose them to the image build.
40
- FileUtils.copy_entry(@root.join(relative_path), target, false, false, false)
182
+ FileUtils.copy_entry(@root.join(relative_path), target, true, false, false)
41
183
  on_progress&.call(index + 1, files.size)
42
184
  end
43
185
  end
data/lib/wip/cli.rb CHANGED
@@ -73,7 +73,8 @@ module Wip
73
73
  context = Pathname(load_config.path).dirname.join(settings['context'] || '.').to_s
74
74
  warn "wip: staging build context (#{context})"
75
75
  progress = StagingProgress.new
76
- BuildContext.new(context).stage(on_progress: progress.method(:tick)) do |staged_context|
76
+ build_context = BuildContext.new(context, shadow_root: settings['shadow_context'])
77
+ build_context.stage(on_progress: progress.method(:tick)) do |staged_context|
77
78
  progress.finish
78
79
  built = builder.build(settings: settings.merge('context' => staged_context), extra: extra)
79
80
  execute(built, interactive: tty?(true))
data/lib/wip/config.rb CHANGED
@@ -27,7 +27,18 @@ module Wip
27
27
  end
28
28
 
29
29
  def wslc_command = @raw.dig('wslc', 'command') || 'auto'
30
- def commands = @raw['commands'] || {}
30
+
31
+ # interaction: is dip's name for the same concept — accepted as an alias so a
32
+ # dip.yml can be renamed to wip.yml with fewer edits. The two are mutually
33
+ # exclusive (validate_commands!) rather than merged, so a project doesn't end up
34
+ # with the same command split across both keys.
35
+ def commands
36
+ return @raw['commands'] if @raw.key?('commands')
37
+ return @raw['interaction'] if @raw.key?('interaction')
38
+
39
+ {}
40
+ end
41
+
31
42
  # Raw dependencies: block as written in wip.yml. Under compose-native mode this stays
32
43
  # empty by construction (validate_compose! forbids combining the two) — #dependencies
33
44
  # below is what callers actually want, since it's synthesized from compose.yml there.
@@ -126,6 +137,8 @@ module Wip
126
137
  end
127
138
 
128
139
  def validate_commands!
140
+ raise ConfigError, 'commands is mutually exclusive with interaction — pick one' \
141
+ if @raw.key?('commands') && @raw.key?('interaction')
129
142
  raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
130
143
 
131
144
  commands.each { |name, entry| validate_command!(name, entry) }
@@ -175,9 +188,22 @@ module Wip
175
188
  type = entry['type'] || (name == 'build' ? 'build' : 'exec')
176
189
  raise ConfigError, "Invalid command type for #{name}: #{type}" unless %w[exec run build].include?(type)
177
190
 
191
+ validate_shadow_context!(name, entry, type)
192
+
178
193
  entry['env']&.transform_values!(&:to_s)
179
194
  end
180
195
 
196
+ # shadow_context names a Windows-side mirror of the build context, so it only
197
+ # means anything on a build command — and only as a real path.
198
+ def validate_shadow_context!(name, entry, type)
199
+ return unless entry.key?('shadow_context')
200
+
201
+ shadow_context = entry['shadow_context']
202
+ return if type == 'build' && shadow_context.is_a?(String) && !shadow_context.empty?
203
+
204
+ raise ConfigError, "commands.#{name}.shadow_context must be a non-empty path for a build command"
205
+ end
206
+
181
207
  def validate_dependency!(name, entry)
182
208
  raise ConfigError, "dependencies.#{name} must be a mapping" unless entry.is_a?(Hash)
183
209
  raise ConfigError, "dependencies.#{name} must set image" if entry['image'].to_s.empty?
@@ -31,17 +31,18 @@ module Wip
31
31
  # example uses.
32
32
  FALLBACK_EXCLUDES = %w[.git tmp/ node_modules/].freeze
33
33
 
34
- # Appended to both templates after dependencies/compose. commands: itself is left
35
- # commented since an empty commands: {} is indistinguishable from omitting the key.
34
+ # Appended to both templates after dependencies/compose. interaction: itself is left
35
+ # commented since an empty interaction: {} is indistinguishable from omitting the key.
36
36
  COMMANDS_EXAMPLE = <<~YAML.chomp
37
37
  # optional; custom subcommands, e.g. `wip test` — see README
38
- # commands:
38
+ # (interaction: is the primary key; commands: is accepted as an alias — the two are
39
+ # mutually exclusive, so pick one)
40
+ # interaction:
39
41
  # test:
40
42
  # optional; exec (default, runs in the running container) | run (fresh container) | build
41
43
  # type: exec
42
44
 
43
- # TODO
44
- # command: bundle exec rspec
45
+ # command: ls
45
46
  YAML
46
47
 
47
48
  def initialize(dir: Dir.pwd, template: nil)
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '0.18.2'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wslc-wip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors