wslc-wip 0.4.3 → 0.6.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 +4 -4
- data/README.md +112 -5
- data/lib/wip/build_context.rb +48 -0
- data/lib/wip/cli.rb +72 -16
- data/lib/wip/command_builder.rb +6 -2
- data/lib/wip/command_resolver.rb +17 -10
- data/lib/wip/compose_bridge.rb +70 -0
- data/lib/wip/config.rb +17 -1
- data/lib/wip/docker_ignore.rb +57 -0
- data/lib/wip/doctor.rb +38 -7
- data/lib/wip/dotenv_loader.rb +39 -0
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +4 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c125a123c76462cbb6b78fe0ead5ea400b8fd0f7a8c825421985547190603d02
|
|
4
|
+
data.tar.gz: 0762dbbf6480a84d9ef58d9481d1398672b5d2ef7e8d644d39be655558704022
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b91868415f3069167ddd479616d9bd2937d3684af2e7c6228789b198cd7af952e9d2705fe363877ef05a6831a7f5a8e9a203e073623bb0c4b46102f2771429c5
|
|
7
|
+
data.tar.gz: 88b8d3fdec972776b0bdefedd5753ebcb4a025cb9bf54122ad7eb723e663f4cac64dff5a4ec201bd45b5959b9f44ba6801e8441419ff72f388631eb7eee80f67
|
data/README.md
CHANGED
|
@@ -103,6 +103,22 @@ commands:
|
|
|
103
103
|
credential, or auth. Keep real secrets out of the config file and in your runtime environment
|
|
104
104
|
instead.
|
|
105
105
|
|
|
106
|
+
### .env
|
|
107
|
+
|
|
108
|
+
Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
|
|
109
|
+
per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
|
|
110
|
+
passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
|
|
111
|
+
custom commands. `.env` only fills in keys that aren't already set by `defaults.env` or a
|
|
112
|
+
command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH` to load a
|
|
113
|
+
different file instead.
|
|
114
|
+
|
|
115
|
+
### .dockerignore
|
|
116
|
+
|
|
117
|
+
`wip build` reads `.dockerignore` from the build context and stages a filtered copy of the
|
|
118
|
+
context (skipping anything it matches) before handing it to `wslc build`, since `wslc` sends the
|
|
119
|
+
context as-is otherwise. If there's no `.dockerignore`, the original context directory is used
|
|
120
|
+
directly with no copying.
|
|
121
|
+
|
|
106
122
|
### Dependency containers
|
|
107
123
|
|
|
108
124
|
If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
|
|
@@ -113,6 +129,47 @@ the same way Compose's service names resolve. `wip down` tears the main containe
|
|
|
113
129
|
dependencies down (the network itself is left in place). Each dependency entry accepts `image`
|
|
114
130
|
(required), `command`, `env`, `ports`, `volumes`, and `workdir` — the same shape as `defaults`.
|
|
115
131
|
|
|
132
|
+
### Compose mode
|
|
133
|
+
|
|
134
|
+
If your project already has a real `compose.yml`, don't duplicate it in `dependencies:` — point
|
|
135
|
+
`wip` at it instead:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
version: 1
|
|
139
|
+
compose:
|
|
140
|
+
service: app # required: which compose service wip run/exec/NAME target
|
|
141
|
+
command: wslc-compose # required: the compose-for-wslc binary/path you have installed
|
|
142
|
+
file: compose.yml # optional; auto-detected next to wip.yml otherwise
|
|
143
|
+
project: myapp # optional; omitted lets the compose tool pick its own default
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`compose:` is mutually exclusive with `dependencies:`/`defaults.network` — pick one orchestration
|
|
147
|
+
path per project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc`
|
|
148
|
+
CLI rather than reimplementing Compose itself.
|
|
149
|
+
|
|
150
|
+
`wslc` itself has no native Compose support yet (tracked upstream in
|
|
151
|
+
[microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
|
|
152
|
+
independent third-party tools fill the gap — for example
|
|
153
|
+
[bacarndiaye/wslc-compose](https://github.com/bacarndiaye/wslc-compose) (Python) and
|
|
154
|
+
[inuyume/wslc-compose](https://github.com/inuyume/wslc-compose) (Go), among others. `wslc` is new
|
|
155
|
+
and still evolving, so expect more of these to show up (and existing ones to change) over time.
|
|
156
|
+
`wip` deliberately doesn't pick a winner or default to any of them (unlike `wslc.command`, which
|
|
157
|
+
defaults to `auto` and searches for `wslc.exe`/`wslc`): `compose.command` is required and treats
|
|
158
|
+
every implementation equally — set it to whichever binary name or absolute path you've installed.
|
|
159
|
+
Whichever one you use needs to understand `-f FILE [-p PROJECT] up|down|exec|logs`, the subset of
|
|
160
|
+
the Compose CLI vocabulary `wip` drives. `wip doctor` reports whether the configured command is
|
|
161
|
+
found, its version, and which compose file `wip` resolved.
|
|
162
|
+
|
|
163
|
+
- `wip up`/`wip down` delegate straight to `<compose command> up -d`/`down`.
|
|
164
|
+
- `wip exec`/`wip NAME` (custom `commands:`) run inside `compose.service`.
|
|
165
|
+
- `wip shell` also goes through the bridge: unless `commands.shell` is defined in `wip.yml`, it
|
|
166
|
+
`exec`s `bash` against `compose.service`, falling back to `sh`.
|
|
167
|
+
- `wip logs [-f] [SERVICE...]` is only available in compose mode.
|
|
168
|
+
- `wip run` has no ephemeral-container equivalent in this exec-only vocabulary, so it falls back
|
|
169
|
+
to `exec` against the already-running `compose.service` (wip warns when this happens).
|
|
170
|
+
- `commands:` entries with `type: run`/`type: build` aren't supported in compose mode — use your
|
|
171
|
+
compose tool's own `build`/`up --build` directly; compose owns builds for its own services.
|
|
172
|
+
|
|
116
173
|
## Commands
|
|
117
174
|
|
|
118
175
|
| Command | Description |
|
|
@@ -124,8 +181,9 @@ dependencies down (the network itself is left in place). Each dependency entry a
|
|
|
124
181
|
| `wip up [-d]` | Start `defaults.container` and its `dependencies` (creating any that are missing, on `defaults.network` if set). `-d` runs the main container in the background |
|
|
125
182
|
| `wip down` | Stop and remove `defaults.container` and its `dependencies` |
|
|
126
183
|
| `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
|
|
127
|
-
| `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container |
|
|
184
|
+
| `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (compose mode: `exec`s into `compose.service` instead) |
|
|
128
185
|
| `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
|
|
186
|
+
| `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose mode only) |
|
|
129
187
|
| `wip NAME ARGS...` | Run `commands.NAME`, appending any extra arguments |
|
|
130
188
|
|
|
131
189
|
TTY allocation is decided by combining the command's config, the CLI option, and whether both
|
|
@@ -193,6 +251,23 @@ When `pull access denied` (or similar) is detected, wip suggests how to log in:
|
|
|
193
251
|
wslc registry login -u <username> docker.io
|
|
194
252
|
```
|
|
195
253
|
|
|
254
|
+
### Slow boot when the app directory is bind-mounted
|
|
255
|
+
|
|
256
|
+
wslc containers run in their own VM, so a bind-mounted host directory (`.:/app`) is always shared
|
|
257
|
+
in over virtiofs, even when the host path is already on WSL's native filesystem. Frameworks that
|
|
258
|
+
scan large directory trees at startup (Ruby's Zeitwerk autoloader, for example) issue many small
|
|
259
|
+
per-file stat/open calls, and each one is a round trip through that layer — CPU on the Windows
|
|
260
|
+
side can look busy while almost no data is actually transferred, and the process can appear hung
|
|
261
|
+
for minutes with barely any resource usage to show for it.
|
|
262
|
+
|
|
263
|
+
If a debug log shows a boot-time command "stuck" with low CPU/mem/IO in `resource_monitor`'s
|
|
264
|
+
output, this is worth checking before assuming the app itself is broken. The workaround is to stop
|
|
265
|
+
bind-mounting the source live and instead mirror it into a named volume: mount the host path
|
|
266
|
+
read-only (`.:/host-src:ro`), add a named volume for `/app`, and have the container's entrypoint
|
|
267
|
+
`rsync` from the read-only mount into the volume before boot, then keep syncing in the background
|
|
268
|
+
(e.g. every couple of seconds) so edits still show up with a short delay. The app then only ever
|
|
269
|
+
touches fast native storage once it's running.
|
|
270
|
+
|
|
196
271
|
### CPU architecture mismatch
|
|
197
272
|
|
|
198
273
|
Check the image and publish a multi-arch (amd64/arm64) image:
|
|
@@ -227,10 +302,42 @@ checklist.
|
|
|
227
302
|
|
|
228
303
|
## Not in the initial release
|
|
229
304
|
|
|
230
|
-
Full Compose compatibility
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
305
|
+
Full Compose compatibility isn't reimplemented in `wip` itself, but is available by delegating to
|
|
306
|
+
a third-party compose-for-`wslc` tool — see [Compose mode](#compose-mode). A resident/daemon
|
|
307
|
+
process, a GUI, PowerShell-specific tuning, direct registry API/manifest parsing, self-update, and
|
|
308
|
+
plugins are all unimplemented.
|
|
309
|
+
|
|
310
|
+
## Roadmap
|
|
311
|
+
|
|
312
|
+
`wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
|
|
313
|
+
named commands (`commands:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
|
|
314
|
+
sidecar services via `dependencies:` + `defaults.network`. Fuller Compose semantics
|
|
315
|
+
(`depends_on` ordering/health checks, log aggregation, named volumes, profiles, scaling) are now
|
|
316
|
+
handled by delegating to a separate compose-for-`wslc` tool in [compose mode](#compose-mode)
|
|
317
|
+
rather than being reimplemented in `wip` itself. Which of those actually work is entirely up to
|
|
318
|
+
the external tool you point `compose.command` at — `wip` only forwards
|
|
319
|
+
`-f FILE [-p PROJECT] up|down|exec|logs`, so treat that list as what Compose offers, not as
|
|
320
|
+
something `wip` guarantees. See that section for what compose mode covers
|
|
321
|
+
and its current limitations (`run`, and `commands:` of type `run`/`build`). What's still planned
|
|
322
|
+
for `wip`, roughly in priority order:
|
|
323
|
+
|
|
324
|
+
1. **`wip provision`** — a dip-style one-shot bootstrap hook (build → up deps → install deps →
|
|
325
|
+
create/migrate/seed DB) so a new contributor can go from `git clone` to a working environment
|
|
326
|
+
in two commands (`wip provision && wip up`).
|
|
327
|
+
2. **Config file merging** — `--config` currently accepts one file; support layering
|
|
328
|
+
(`wip.yml` + `wip.override.yml`, or a `WIP_CONFIG` list) for dev/CI/debug variants without
|
|
329
|
+
duplicating the whole file, plus a `wip config --resolved` view of the merged result.
|
|
330
|
+
3. **Bind-mount boot time (`rails c`, `bundle`, ...)** — commands like `wip rails c` still start
|
|
331
|
+
noticeably slower than the equivalent under `docker compose`, mostly from WSL2 bind-mounted
|
|
332
|
+
(`.:/app`-style) volumes doing many small reads for gems/`node_modules` (use `--debug` to
|
|
333
|
+
confirm it's disk I/O and not `wip`'s own overhead). This isn't really `wip`'s responsibility —
|
|
334
|
+
the fix has to come from either the mount layer or the project's own volume layout. We're also
|
|
335
|
+
hoping for improvements on the `wslc` side itself (faster bind-mount/cache behavior); `wip`
|
|
336
|
+
will pick those up for free as soon as they land.
|
|
337
|
+
|
|
338
|
+
Each of these should stay additive to the existing `wip.yml` shape — no breaking changes to
|
|
339
|
+
`defaults`, `commands`, `dependencies`, or `compose` are planned. A resident daemon and a GUI
|
|
340
|
+
remain out of scope; see "Not in the initial release" above.
|
|
234
341
|
|
|
235
342
|
## License
|
|
236
343
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
# Stages a build context into a scratch directory with anything matched by
|
|
9
|
+
# .dockerignore left out, since wslc build (unlike `docker build`) sends the
|
|
10
|
+
# context as-is instead of filtering it itself.
|
|
11
|
+
class BuildContext
|
|
12
|
+
def initialize(context, ignore: nil)
|
|
13
|
+
@root = Pathname(context).expand_path
|
|
14
|
+
@ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stage
|
|
18
|
+
return yield @root.to_s if @ignore.empty?
|
|
19
|
+
|
|
20
|
+
Dir.mktmpdir('wip-build-context-') do |dir|
|
|
21
|
+
copy_included_files(Pathname(dir))
|
|
22
|
+
yield dir
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def copy_included_files(destination)
|
|
29
|
+
each_included_file do |relative_path|
|
|
30
|
+
target = destination.join(relative_path)
|
|
31
|
+
FileUtils.mkdir_p(target.dirname)
|
|
32
|
+
FileUtils.cp(@root.join(relative_path), target)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each_included_file
|
|
37
|
+
Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).each do |entry|
|
|
38
|
+
next if %w[. ..].include?(entry)
|
|
39
|
+
|
|
40
|
+
path = @root.join(entry)
|
|
41
|
+
next unless path.file?
|
|
42
|
+
next if @ignore.ignored?(entry)
|
|
43
|
+
|
|
44
|
+
yield entry
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/wip/cli.rb
CHANGED
|
@@ -4,11 +4,13 @@ require 'thor'
|
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'stringio'
|
|
7
|
+
require 'shellwords'
|
|
7
8
|
|
|
8
9
|
module Wip
|
|
9
10
|
# Thor-based command-line interface for wip.
|
|
10
11
|
class CLI < Thor
|
|
11
12
|
class_option :config, type: :string, desc: 'Path to wip.yml'
|
|
13
|
+
class_option :env_file, type: :string, desc: 'Path to a dotenv file (default: .env next to wip.yml)'
|
|
12
14
|
class_option :debug, type: :boolean, default: false, desc: 'Print progress and timing for each step'
|
|
13
15
|
class_option :debug_log, type: :string, desc: 'Where --debug snapshots go: a file path, or "-" for inline'
|
|
14
16
|
default_task :dispatch
|
|
@@ -52,12 +54,20 @@ module Wip
|
|
|
52
54
|
desc 'build [OPTIONS]', 'Build the configured image'
|
|
53
55
|
def build(*extra)
|
|
54
56
|
extra.shift if extra.first == '--'
|
|
55
|
-
|
|
57
|
+
settings = load_config.command('build') || {}
|
|
58
|
+
context = settings['context'] || load_config.defaults['context'] || '.'
|
|
59
|
+
BuildContext.new(context).stage do |staged_context|
|
|
60
|
+
execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
|
|
61
|
+
end
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
|
|
59
65
|
option :detach, type: :boolean, default: false, aliases: '-d'
|
|
60
66
|
def up
|
|
67
|
+
if load_config.compose?
|
|
68
|
+
return execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
|
|
69
|
+
end
|
|
70
|
+
|
|
61
71
|
ensure_network
|
|
62
72
|
load_config.dependencies.each_key { |name| ensure_dependency(name) }
|
|
63
73
|
ensure_container
|
|
@@ -65,6 +75,8 @@ module Wip
|
|
|
65
75
|
|
|
66
76
|
desc 'down', 'Stop and remove the configured container and its dependencies'
|
|
67
77
|
def down
|
|
78
|
+
return execute(compose_bridge.down, exit_on_failure: false) if load_config.compose?
|
|
79
|
+
|
|
68
80
|
execute(builder.down, exit_on_failure: false)
|
|
69
81
|
execute(builder.remove, exit_on_failure: false)
|
|
70
82
|
load_config.dependencies.each_key do |name|
|
|
@@ -76,41 +88,51 @@ module Wip
|
|
|
76
88
|
desc 'exec COMMAND...', 'Execute a command in the running container'
|
|
77
89
|
option :interactive, type: :boolean, default: true
|
|
78
90
|
def exec(*command)
|
|
79
|
-
|
|
80
|
-
execute(builder.exec(command, interactive: options[:interactive]), interactive: tty)
|
|
91
|
+
execute(exec_target(command, interactive: options[:interactive]), interactive: tty?(options[:interactive]))
|
|
81
92
|
end
|
|
82
93
|
|
|
83
94
|
desc 'run COMMAND...', 'Run a command in a new container'
|
|
84
95
|
option :interactive, type: :boolean, default: true
|
|
85
96
|
def run_command(*command)
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
if load_config.compose?
|
|
98
|
+
warn "wip: compose mode has no ephemeral 'run'; executing in the running " \
|
|
99
|
+
"'#{load_config.compose_service}' service instead"
|
|
100
|
+
return execute(exec_target(command, interactive: options[:interactive]),
|
|
101
|
+
interactive: tty?(options[:interactive]))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
execute(builder.run(command, interactive: options[:interactive]), interactive: tty?(options[:interactive]))
|
|
88
105
|
end
|
|
89
106
|
map 'run' => :run_command
|
|
90
107
|
|
|
91
108
|
desc 'shell', 'Open a shell in the configured container'
|
|
92
109
|
def shell_command
|
|
93
110
|
configured = load_config.command('shell')
|
|
94
|
-
if configured
|
|
95
|
-
tty = builder.tty?(configured.fetch('interactive', false))
|
|
96
|
-
return execute(builder.custom('shell', []), interactive: tty)
|
|
97
|
-
end
|
|
111
|
+
return dispatch('shell') if configured
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
code = execute(builder.exec(['bash'], settings: { 'interactive' => true }, interactive: true),
|
|
101
|
-
interactive: tty, exit_on_failure: false)
|
|
113
|
+
code = execute(exec_target(['bash'], interactive: true), interactive: tty?(true), exit_on_failure: false)
|
|
102
114
|
return if code.zero?
|
|
103
115
|
|
|
104
|
-
execute(
|
|
116
|
+
execute(exec_target(['sh'], interactive: true), interactive: tty?(true))
|
|
105
117
|
end
|
|
106
118
|
map 'shell' => :shell_command
|
|
107
119
|
|
|
120
|
+
desc 'logs [SERVICE...]', 'Follow logs from compose services (compose mode only)'
|
|
121
|
+
option :follow, type: :boolean, default: true, aliases: '-f'
|
|
122
|
+
def logs(*services)
|
|
123
|
+
raise ConfigError, '`wip logs` is only available in compose mode' unless load_config.compose?
|
|
124
|
+
|
|
125
|
+
execute(compose_bridge.logs(services: services, follow: options[:follow]), interactive: true)
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
desc 'dispatch COMMAND [ARGS...]', 'Run a command defined in wip.yml'
|
|
109
129
|
def dispatch(name = nil, *arguments)
|
|
110
130
|
raise ConfigError, 'A command is required' unless name
|
|
111
131
|
|
|
112
132
|
values = load_config.command(name) || raise(ConfigError, "Unknown command: #{name}")
|
|
113
|
-
|
|
133
|
+
return dispatch_compose(name, values, arguments) if load_config.compose?
|
|
134
|
+
|
|
135
|
+
execute(builder.custom(name, arguments), interactive: tty?(values.fetch('interactive', false)))
|
|
114
136
|
end
|
|
115
137
|
|
|
116
138
|
private
|
|
@@ -118,7 +140,41 @@ module Wip
|
|
|
118
140
|
def loader = ConfigLoader.new(path: options[:config])
|
|
119
141
|
def load_config = (@load_config ||= loader.load)
|
|
120
142
|
def resolver = CommandResolver.new
|
|
121
|
-
|
|
143
|
+
|
|
144
|
+
def dotenv_path
|
|
145
|
+
options[:env_file] ? Pathname(options[:env_file]).expand_path : Pathname(load_config.path).dirname.join('.env')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def dotenv = @dotenv ||= DotenvLoader.new(dotenv_path).load
|
|
149
|
+
|
|
150
|
+
def builder
|
|
151
|
+
CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config, dotenv: dotenv)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def compose_bridge = @compose_bridge ||= ComposeBridge.for(load_config)
|
|
155
|
+
|
|
156
|
+
def tty?(requested) = requested && Environment.new.interactive?
|
|
157
|
+
|
|
158
|
+
def exec_target(arguments, interactive:)
|
|
159
|
+
if load_config.compose?
|
|
160
|
+
return compose_bridge.exec(load_config.compose_service, arguments,
|
|
161
|
+
interactive: interactive)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
builder.exec(arguments, interactive: interactive)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def dispatch_compose(name, values, arguments)
|
|
168
|
+
type = values['type'] || 'exec'
|
|
169
|
+
unless type == 'exec'
|
|
170
|
+
raise ConfigError, "commands.#{name}: type '#{type}' is not supported in compose mode " \
|
|
171
|
+
'(use `wslc-compose build`/`up --build` directly)'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
command = Shellwords.split(values['command'].to_s) + arguments
|
|
175
|
+
interactive = values.fetch('interactive', false)
|
|
176
|
+
execute(exec_target(command, interactive: interactive), interactive: tty?(interactive))
|
|
177
|
+
end
|
|
122
178
|
|
|
123
179
|
def execute(command, interactive: false, exit_on_failure: true)
|
|
124
180
|
runner = CommandRunner.new(debug: debug?)
|
|
@@ -174,7 +230,7 @@ module Wip
|
|
|
174
230
|
|
|
175
231
|
def ensure_container
|
|
176
232
|
container = load_config.defaults['container']
|
|
177
|
-
interactive =
|
|
233
|
+
interactive = tty?(!options[:detach])
|
|
178
234
|
if resource_exists?(builder.find)
|
|
179
235
|
warn "wip: starting existing container '#{container}'"
|
|
180
236
|
execute(builder.start(detach: options[:detach]), interactive: interactive)
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -5,10 +5,11 @@ require 'shellwords'
|
|
|
5
5
|
module Wip
|
|
6
6
|
# Builds the argument arrays for wslc build/exec/run/custom invocations.
|
|
7
7
|
class CommandBuilder
|
|
8
|
-
def initialize(wslc:, config:, environment: Environment.new)
|
|
8
|
+
def initialize(wslc:, config:, environment: Environment.new, dotenv: {})
|
|
9
9
|
@wslc = wslc
|
|
10
10
|
@config = config
|
|
11
11
|
@environment = environment
|
|
12
|
+
@dotenv = dotenv
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def exec(arguments, settings: {}, interactive: true)
|
|
@@ -115,7 +116,7 @@ module Wip
|
|
|
115
116
|
def options(values, include_container: false, include_publish: true)
|
|
116
117
|
result = []
|
|
117
118
|
result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
|
|
118
|
-
values
|
|
119
|
+
merged_env(values).each { |key, value| result.push('-e', "#{key}=#{value}") }
|
|
119
120
|
if include_publish
|
|
120
121
|
Array(values['ports']).each { |port| result.push('-p', port.to_s) }
|
|
121
122
|
Array(values['volumes']).each { |volume| result.push('-v', volume.to_s) }
|
|
@@ -124,6 +125,9 @@ module Wip
|
|
|
124
125
|
result
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# .env supplies defaults; env set in wip.yml (defaults or per-command) wins on conflict.
|
|
129
|
+
def merged_env(values) = @dotenv.merge(values.fetch('env', {}))
|
|
130
|
+
|
|
127
131
|
def required(values, key)
|
|
128
132
|
value = values[key]
|
|
129
133
|
raise ConfigError, "Configured #{key} must not be empty" if value.to_s.empty?
|
data/lib/wip/command_resolver.rb
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wip
|
|
4
|
-
# Locates
|
|
4
|
+
# Locates an executable on the current system, trying a list of candidates.
|
|
5
5
|
class CommandResolver
|
|
6
6
|
CANDIDATES = ['wslc.exe', 'wslc', '/mnt/c/Windows/System32/wslc.exe'].freeze
|
|
7
|
+
DEFAULT_INSTALL_HINT = <<~HINT.chomp
|
|
8
|
+
Install or update the WSL container tooling, then run:
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
wip doctor
|
|
11
|
+
HINT
|
|
12
|
+
|
|
13
|
+
def initialize(path: ENV.fetch('PATH', ''), executable: nil, candidates: CANDIDATES, label: 'WSLC',
|
|
14
|
+
install_hint: DEFAULT_INSTALL_HINT)
|
|
9
15
|
@path = path
|
|
10
16
|
@executable = executable || method(:executable?)
|
|
17
|
+
@candidates = candidates
|
|
18
|
+
@label = label
|
|
19
|
+
@install_hint = install_hint
|
|
11
20
|
end
|
|
12
21
|
|
|
13
22
|
def resolve(configured = 'auto')
|
|
14
23
|
return configured if configured != 'auto' && @executable.call(configured)
|
|
15
|
-
return raise_not_found if configured != 'auto'
|
|
24
|
+
return raise_not_found([configured]) if configured != 'auto'
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
@candidates.find { |candidate| @executable.call(candidate) } || raise_not_found(@candidates)
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
private
|
|
@@ -25,16 +34,14 @@ module Wip
|
|
|
25
34
|
@path.split(File::PATH_SEPARATOR).any? { |directory| File.executable?(File.join(directory, command)) }
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
def raise_not_found
|
|
37
|
+
def raise_not_found(attempted)
|
|
29
38
|
raise CommandNotFoundError, <<~MESSAGE
|
|
30
|
-
|
|
39
|
+
#{@label} was not found.
|
|
31
40
|
|
|
32
41
|
Checked:
|
|
33
|
-
#{
|
|
34
|
-
|
|
35
|
-
Install or update the WSL container tooling, then run:
|
|
42
|
+
#{attempted.join("\n ")}
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
#{@install_hint}
|
|
38
45
|
MESSAGE
|
|
39
46
|
end
|
|
40
47
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wip
|
|
4
|
+
# Builds argument arrays for wslc-compose invocations, delegating orchestration
|
|
5
|
+
# to a real compose.yml instead of wip's own dependencies:/network handling.
|
|
6
|
+
class ComposeBridge
|
|
7
|
+
FILENAMES = %w[compose.yml compose.yaml docker-compose.yml docker-compose.yaml].freeze
|
|
8
|
+
# No default candidates: wip doesn't favor any one compose-for-wslc implementation.
|
|
9
|
+
# compose.command must name the one you've installed.
|
|
10
|
+
INSTALL_HINT = <<~HINT.chomp
|
|
11
|
+
wip doesn't bundle or pin a compose-for-wslc implementation — install one and set
|
|
12
|
+
compose.command in wip.yml to its binary name or path, e.g.:
|
|
13
|
+
|
|
14
|
+
https://github.com/bacarndiaye/wslc-compose
|
|
15
|
+
https://github.com/inuyume/wslc-compose
|
|
16
|
+
HINT
|
|
17
|
+
|
|
18
|
+
def self.for(config, resolver: CommandResolver.new(candidates: [], label: 'compose command',
|
|
19
|
+
install_hint: INSTALL_HINT))
|
|
20
|
+
new(compose_command: resolver.resolve(config.compose_command), file: file_path(config),
|
|
21
|
+
project: config.compose_project)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.file_path(config)
|
|
25
|
+
base = Pathname(config.path).dirname
|
|
26
|
+
configured = config.compose_file
|
|
27
|
+
# Relative compose.file is resolved against wip.yml, not the current directory,
|
|
28
|
+
# so `wip` behaves the same from any subdirectory (matching auto-detection below).
|
|
29
|
+
return base.join(configured).expand_path if configured
|
|
30
|
+
|
|
31
|
+
FILENAMES.map { |name| base.join(name) }.find(&:file?) ||
|
|
32
|
+
raise(ConfigError, "compose mode: no compose file found next to #{config.path} " \
|
|
33
|
+
"(looked for #{FILENAMES.join(', ')})")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(compose_command:, file:, project: nil)
|
|
37
|
+
@compose_command = compose_command
|
|
38
|
+
@file = file
|
|
39
|
+
@project = project
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def up(detach: true)
|
|
43
|
+
command = base.push('up')
|
|
44
|
+
command << '-d' if detach
|
|
45
|
+
command
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def down = base.push('down')
|
|
49
|
+
|
|
50
|
+
def exec(service, arguments, interactive: true)
|
|
51
|
+
command = base.push('exec')
|
|
52
|
+
command << '-T' unless interactive
|
|
53
|
+
command.push(service.to_s).concat(arguments)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def logs(services: [], follow: true)
|
|
57
|
+
command = base.push('logs')
|
|
58
|
+
command << '-f' if follow
|
|
59
|
+
command.concat(services.map(&:to_s))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def base
|
|
65
|
+
command = [@compose_command, '-f', @file.to_s]
|
|
66
|
+
command.push('-p', @project) if @project
|
|
67
|
+
command
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/wip/config.rb
CHANGED
|
@@ -20,6 +20,12 @@ module Wip
|
|
|
20
20
|
def up_command = @raw.dig('up', 'command')
|
|
21
21
|
def dependencies = @raw['dependencies'] || {}
|
|
22
22
|
def network = defaults['network']
|
|
23
|
+
def compose = @raw['compose']
|
|
24
|
+
def compose? = !!compose
|
|
25
|
+
def compose_service = compose && compose['service']
|
|
26
|
+
def compose_file = compose && compose['file']
|
|
27
|
+
def compose_project = compose && compose['project']
|
|
28
|
+
def compose_command = compose && compose['command']
|
|
23
29
|
|
|
24
30
|
def command(name)
|
|
25
31
|
entry = commands[name.to_s]
|
|
@@ -37,7 +43,7 @@ module Wip
|
|
|
37
43
|
|
|
38
44
|
def to_h(redact: true)
|
|
39
45
|
value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
|
|
40
|
-
'up' => { 'command' => up_command }, 'dependencies' => dependencies,
|
|
46
|
+
'up' => { 'command' => up_command }, 'dependencies' => dependencies, 'compose' => compose,
|
|
41
47
|
'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
|
|
42
48
|
redact ? redact_secrets(value) : value
|
|
43
49
|
end
|
|
@@ -50,6 +56,7 @@ module Wip
|
|
|
50
56
|
|
|
51
57
|
validate_commands!
|
|
52
58
|
validate_dependencies!
|
|
59
|
+
validate_compose!
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
def validate_commands!
|
|
@@ -64,6 +71,15 @@ module Wip
|
|
|
64
71
|
dependencies.each { |name, entry| validate_dependency!(name, entry) }
|
|
65
72
|
end
|
|
66
73
|
|
|
74
|
+
def validate_compose!
|
|
75
|
+
return unless @raw.key?('compose')
|
|
76
|
+
raise ConfigError, 'compose must be a mapping' unless compose.is_a?(Hash)
|
|
77
|
+
raise ConfigError, 'compose.service must not be empty' if compose_service.to_s.empty?
|
|
78
|
+
raise ConfigError, 'compose.command must not be empty' if compose_command.to_s.empty?
|
|
79
|
+
raise ConfigError, 'compose is mutually exclusive with dependencies' if dependencies.any?
|
|
80
|
+
raise ConfigError, 'compose is mutually exclusive with defaults.network' if network
|
|
81
|
+
end
|
|
82
|
+
|
|
67
83
|
def validate_command!(name, entry)
|
|
68
84
|
raise ConfigError, "commands.#{name} must be a mapping" unless entry.is_a?(Hash)
|
|
69
85
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Parses a .dockerignore file and decides whether a build-context-relative
|
|
7
|
+
# path should be excluded, following the same pattern rules as the Docker
|
|
8
|
+
# CLI (gitignore-like globs, later rules override earlier ones, `!` negates).
|
|
9
|
+
class DockerIgnore
|
|
10
|
+
Rule = Struct.new(:pattern, :negate)
|
|
11
|
+
|
|
12
|
+
FNMATCH_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
|
13
|
+
|
|
14
|
+
def self.load(path)
|
|
15
|
+
path = Pathname(path)
|
|
16
|
+
return new([]) unless path.file?
|
|
17
|
+
|
|
18
|
+
new(path.readlines)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(lines)
|
|
22
|
+
@rules = lines.filter_map { |line| parse(line) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def empty? = @rules.empty?
|
|
26
|
+
|
|
27
|
+
def ignored?(relative_path)
|
|
28
|
+
@rules.reduce(false) do |ignored, rule|
|
|
29
|
+
matches?(rule.pattern, relative_path) ? !rule.negate : ignored
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse(line)
|
|
36
|
+
line = line.strip
|
|
37
|
+
return nil if line.empty? || line.start_with?('#')
|
|
38
|
+
|
|
39
|
+
negate = line.start_with?('!')
|
|
40
|
+
pattern = negate ? line[1..] : line
|
|
41
|
+
pattern = pattern.delete_suffix('/')
|
|
42
|
+
anchored = pattern.start_with?('/')
|
|
43
|
+
pattern = pattern.delete_prefix('/')
|
|
44
|
+
pattern = "**/#{pattern}" if !anchored && !pattern.include?('/')
|
|
45
|
+
Rule.new(pattern, negate)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A match on a directory component also excludes everything under it,
|
|
49
|
+
# so every prefix of the path (not just the full path) is tested.
|
|
50
|
+
def matches?(pattern, relative_path)
|
|
51
|
+
components = relative_path.split('/')
|
|
52
|
+
(1..components.size).any? do |i|
|
|
53
|
+
File.fnmatch(pattern, components[0...i].join('/'), FNMATCH_FLAGS)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/wip/doctor.rb
CHANGED
|
@@ -7,10 +7,13 @@ module Wip
|
|
|
7
7
|
class Doctor
|
|
8
8
|
Result = Data.define(:level, :message)
|
|
9
9
|
|
|
10
|
-
def initialize(loader:, resolver: CommandResolver.new, environment: Environment.new
|
|
10
|
+
def initialize(loader:, resolver: CommandResolver.new, environment: Environment.new,
|
|
11
|
+
compose_resolver: CommandResolver.new(candidates: [], label: 'compose command',
|
|
12
|
+
install_hint: ComposeBridge::INSTALL_HINT))
|
|
11
13
|
@loader = loader
|
|
12
14
|
@resolver = resolver
|
|
13
15
|
@environment = environment
|
|
16
|
+
@compose_resolver = compose_resolver
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def call
|
|
@@ -19,8 +22,8 @@ module Wip
|
|
|
19
22
|
results << interop_result unless @environment.windows?
|
|
20
23
|
results << Result.new(:ok, "Architecture: #{@environment.architecture}")
|
|
21
24
|
config = load_config(results)
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
check_wslc(config, results) if config
|
|
26
|
+
check_compose(config, results) if config&.compose?
|
|
24
27
|
results << result(command_available?('git') ? :ok : :warn, 'Git is available',
|
|
25
28
|
'Git is not available to the WSLC build environment')
|
|
26
29
|
results
|
|
@@ -41,7 +44,7 @@ module Wip
|
|
|
41
44
|
|
|
42
45
|
def load_config(results)
|
|
43
46
|
config = @loader.load
|
|
44
|
-
invalid = %w[container image].select { |key| config.defaults[key].to_s.empty? }
|
|
47
|
+
invalid = config.compose? ? [] : %w[container image].select { |key| config.defaults[key].to_s.empty? }
|
|
45
48
|
results << Result.new(invalid.empty? ? :ok : :fail,
|
|
46
49
|
invalid.empty? ? 'Loaded wip.yml' : "Empty defaults: #{invalid.join(', ')}")
|
|
47
50
|
config
|
|
@@ -50,6 +53,11 @@ module Wip
|
|
|
50
53
|
nil
|
|
51
54
|
end
|
|
52
55
|
|
|
56
|
+
def check_wslc(config, results)
|
|
57
|
+
command = resolve(config, results)
|
|
58
|
+
check_version(command, results) if command
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
def resolve(config, results)
|
|
54
62
|
command = @resolver.resolve(config.wslc_command)
|
|
55
63
|
results << Result.new(:ok, "Found #{command}")
|
|
@@ -59,12 +67,35 @@ module Wip
|
|
|
59
67
|
nil
|
|
60
68
|
end
|
|
61
69
|
|
|
62
|
-
def check_version(command, results)
|
|
70
|
+
def check_version(command, results, label: 'WSLC')
|
|
63
71
|
_output, status = Open3.capture2e(command, 'version')
|
|
64
72
|
results << Result.new(status.success? ? :ok : :fail,
|
|
65
|
-
status.success? ?
|
|
73
|
+
status.success? ? "#{label} is available" : "#{label} version failed")
|
|
66
74
|
rescue Errno::ENOENT
|
|
67
|
-
results << Result.new(:fail,
|
|
75
|
+
results << Result.new(:fail, "#{label} version failed")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_compose(config, results)
|
|
79
|
+
command = resolve_compose(config, results)
|
|
80
|
+
check_version(command, results, label: 'compose command') if command
|
|
81
|
+
check_compose_file(config, results)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_compose(config, results)
|
|
85
|
+
command = @compose_resolver.resolve(config.compose_command)
|
|
86
|
+
results << Result.new(:ok, "Found #{command}")
|
|
87
|
+
command
|
|
88
|
+
rescue CommandNotFoundError => e
|
|
89
|
+
results << Result.new(:fail, e.message)
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def check_compose_file(config, results)
|
|
94
|
+
path = ComposeBridge.file_path(config)
|
|
95
|
+
results << result(path.file? ? :ok : :fail, "Found compose file #{path}",
|
|
96
|
+
"Compose file not found: #{path}")
|
|
97
|
+
rescue ConfigError => e
|
|
98
|
+
results << Result.new(:fail, e.message)
|
|
68
99
|
end
|
|
69
100
|
|
|
70
101
|
def result(condition, ok_message, fail_message)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Wip
|
|
6
|
+
# Parses a .env file the way `docker compose` does, so values don't have to
|
|
7
|
+
# be duplicated into wip.yml just to reach the container as -e flags.
|
|
8
|
+
class DotenvLoader
|
|
9
|
+
LINE_PATTERN = /\A(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = Pathname(path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load
|
|
16
|
+
return {} unless @path.file?
|
|
17
|
+
|
|
18
|
+
@path.readlines.each_with_object({}) do |line, env|
|
|
19
|
+
line = line.strip
|
|
20
|
+
next if line.empty? || line.start_with?('#')
|
|
21
|
+
|
|
22
|
+
match = LINE_PATTERN.match(line)
|
|
23
|
+
next unless match
|
|
24
|
+
|
|
25
|
+
env[match[1]] = unquote(match[2])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def unquote(value)
|
|
32
|
+
value = value.strip
|
|
33
|
+
return value[1..-2] if value.start_with?('"') && value.end_with?('"') && value.length >= 2
|
|
34
|
+
return value[1..-2] if value.start_with?("'") && value.end_with?("'") && value.length >= 2
|
|
35
|
+
|
|
36
|
+
value.split(/\s+#/, 2).first.to_s.strip
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/wip/version.rb
CHANGED
data/lib/wip.rb
CHANGED
|
@@ -5,9 +5,13 @@ require_relative 'wip/errors'
|
|
|
5
5
|
require_relative 'wip/config'
|
|
6
6
|
require_relative 'wip/config_loader'
|
|
7
7
|
require_relative 'wip/environment'
|
|
8
|
+
require_relative 'wip/dotenv_loader'
|
|
9
|
+
require_relative 'wip/docker_ignore'
|
|
10
|
+
require_relative 'wip/build_context'
|
|
8
11
|
require_relative 'wip/command_resolver'
|
|
9
12
|
require_relative 'wip/error_interpreter'
|
|
10
13
|
require_relative 'wip/command_builder'
|
|
14
|
+
require_relative 'wip/compose_bridge'
|
|
11
15
|
require_relative 'wip/command_display'
|
|
12
16
|
require_relative 'wip/command_runner'
|
|
13
17
|
require_relative 'wip/resource_monitor'
|
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wip contributors
|
|
@@ -33,15 +33,19 @@ files:
|
|
|
33
33
|
- README.md
|
|
34
34
|
- exe/wip
|
|
35
35
|
- lib/wip.rb
|
|
36
|
+
- lib/wip/build_context.rb
|
|
36
37
|
- lib/wip/cli.rb
|
|
37
38
|
- lib/wip/command_builder.rb
|
|
38
39
|
- lib/wip/command_display.rb
|
|
39
40
|
- lib/wip/command_resolver.rb
|
|
40
41
|
- lib/wip/command_runner.rb
|
|
42
|
+
- lib/wip/compose_bridge.rb
|
|
41
43
|
- lib/wip/config.rb
|
|
42
44
|
- lib/wip/config_loader.rb
|
|
43
45
|
- lib/wip/debug_reporter.rb
|
|
46
|
+
- lib/wip/docker_ignore.rb
|
|
44
47
|
- lib/wip/doctor.rb
|
|
48
|
+
- lib/wip/dotenv_loader.rb
|
|
45
49
|
- lib/wip/environment.rb
|
|
46
50
|
- lib/wip/error_interpreter.rb
|
|
47
51
|
- lib/wip/errors.rb
|