wslc-wip 0.17.5 → 0.17.7
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 +2 -2
- data/lib/wip/cli.rb +21 -3
- data/lib/wip/command_builder.rb +3 -0
- data/lib/wip/compose_file.rb +33 -21
- data/lib/wip/config.rb +13 -0
- data/lib/wip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f6c715d39600201d79b4181892f546555b4ccb48fb6046815c2dd27d2e4d203
|
|
4
|
+
data.tar.gz: 4aab099d2fa77a875c8037fb865739d9858d59abda28ae4d3bda83d85b2ac7aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75beec87e7d2b2d4b9a2e71f72a0d126d4e407840a3c9d4fc06a63af80d5fe7d0b4ae68cfbaaf85b9fe749356f9f455953118110cc2c955db5bb152c9eed5dab
|
|
7
|
+
data.tar.gz: 3ac4feb3c65bef872f8b86027ecff32f9f3891a6f3c630b481167e87fb18e7b17a1fed631a23731a9445275e37feb521cb2a0ecf98adf82d7e0a7df9f515536d
|
data/README.md
CHANGED
|
@@ -370,8 +370,8 @@ valid compose.yml over sections it doesn't need to look at:
|
|
|
370
370
|
| `wip version` | wip's version, plus WSLC's if it can be detected |
|
|
371
371
|
| `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
|
|
372
372
|
| `wip config` | Print the effective configuration (secrets masked) |
|
|
373
|
-
| `wip build --
|
|
374
|
-
| `wip up [-d] [--no-sync]` | Start the primary `dependencies:` entry (`container:` names which one) and its sidecars (creating any that are missing, on `network:` if set). `-d` runs the main container in the background; with `sync:` configured, the source is mirrored into the volume first unless `--no-sync` |
|
|
373
|
+
| `wip build [--no-cache] [-- OPTIONS]` | Build the image from the `build` definition. `wslc build` reuses matching local layers by default; `--no-cache` disables that. |
|
|
374
|
+
| `wip up [-d] [--no-sync] [--no-cache]` | Start the primary `dependencies:` entry (`container:` names which one) and its sidecars (creating any that are missing, on `network:` if set). `-d` runs the main container in the background; with `sync:` configured, the source is mirrored into the volume first unless `--no-sync` |
|
|
375
375
|
| `wip down` | Stop and remove the primary container and its sidecar `dependencies:` |
|
|
376
376
|
| `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
|
|
377
377
|
| `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (mode: compose `exec`s into `compose.service` instead — see "Compose mode" above) |
|
data/lib/wip/cli.rb
CHANGED
|
@@ -66,8 +66,9 @@ module Wip
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
desc 'build [OPTIONS]', 'Build the configured image'
|
|
69
|
+
option :no_cache, type: :boolean, default: false, desc: 'Build without using cached layers'
|
|
69
70
|
def build(*extra)
|
|
70
|
-
extra
|
|
71
|
+
extra = build_extra_options(extra)
|
|
71
72
|
settings = load_config.command('build') || {}
|
|
72
73
|
context = Pathname(load_config.path).dirname.join(settings['context'] || '.').to_s
|
|
73
74
|
warn "wip: staging build context (#{context})"
|
|
@@ -84,6 +85,7 @@ module Wip
|
|
|
84
85
|
desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
|
|
85
86
|
option :detach, type: :boolean, default: false, aliases: '-d'
|
|
86
87
|
option :sync, type: :boolean, default: true, desc: 'Mirror the source into the sync volume first (--no-sync skips)'
|
|
88
|
+
option :no_cache, type: :boolean, default: false, desc: 'Build compose-native images without cached layers'
|
|
87
89
|
def up
|
|
88
90
|
if load_config.compose?
|
|
89
91
|
sync_before_boot if options[:sync]
|
|
@@ -308,8 +310,7 @@ module Wip
|
|
|
308
310
|
# Stages spec['context'] the same way `wip build` does, so a .dockerignore next to
|
|
309
311
|
# a compose-native service's build: is honored and progress is reported here too.
|
|
310
312
|
def build_compose_image(spec)
|
|
311
|
-
extra = spec
|
|
312
|
-
spec['args']&.each { |key, value| extra.push('--build-arg', "#{key}=#{value}") }
|
|
313
|
+
extra = compose_build_extra(spec)
|
|
313
314
|
warn "wip: staging build context (#{spec['context']})"
|
|
314
315
|
progress = StagingProgress.new
|
|
315
316
|
BuildContext.new(spec['context']).stage(on_progress: progress.method(:tick)) do |staged_context|
|
|
@@ -321,6 +322,23 @@ module Wip
|
|
|
321
322
|
progress&.finish
|
|
322
323
|
end
|
|
323
324
|
|
|
325
|
+
def build_extra_options(extra)
|
|
326
|
+
extra = extra.dup
|
|
327
|
+
extra.shift if extra.first == '--'
|
|
328
|
+
with_no_cache_option(extra)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def compose_build_extra(spec)
|
|
332
|
+
extra = spec['dockerfile'] ? ['-f', spec['dockerfile']] : []
|
|
333
|
+
spec['args']&.each { |key, value| extra.push('--build-arg', "#{key}=#{value}") }
|
|
334
|
+
with_no_cache_option(extra)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def with_no_cache_option(extra)
|
|
338
|
+
extra.unshift('--no-cache') if options[:no_cache] && !extra.include?('--no-cache')
|
|
339
|
+
extra
|
|
340
|
+
end
|
|
341
|
+
|
|
324
342
|
# Builds sync.build's image once per invocation (not per --watch tick, which
|
|
325
343
|
# would pay Docker build-cache-lookup overhead on every mirror for no reason).
|
|
326
344
|
# A no-op unless sync.build is configured.
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -136,6 +136,9 @@ module Wip
|
|
|
136
136
|
[@wslc, 'remove', '-f', name.to_s]
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# `wslc build` reuses matching local layers by default (like `docker build`
|
|
140
|
+
# without `--pull`) — it has no `--cache-from` flag to ask for that explicitly,
|
|
141
|
+
# and passing one is a hard error (unrecognized argument).
|
|
139
142
|
def build(settings:, extra: [])
|
|
140
143
|
values = primary_values.merge(settings)
|
|
141
144
|
context = values['context'] || '.'
|
data/lib/wip/compose_file.rb
CHANGED
|
@@ -15,20 +15,18 @@ module Wip
|
|
|
15
15
|
# "Compose mode (native)") — once wslc ships that support, or a
|
|
16
16
|
# compose-for-wslc tool reliably supports `run`.
|
|
17
17
|
class ComposeFile
|
|
18
|
-
Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :user, :depends_on,
|
|
18
|
+
Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :user, :depends_on, :profiles,
|
|
19
19
|
keyword_init: true)
|
|
20
20
|
|
|
21
|
-
SERVICE_KEYS = %w[image build command environment ports volumes working_dir user depends_on].freeze
|
|
21
|
+
SERVICE_KEYS = %w[image build command environment ports volumes working_dir user depends_on profiles].freeze
|
|
22
22
|
# Real Compose keys that read as meaningful here but have nothing to map onto: TTY/stdin
|
|
23
|
-
# allocation is
|
|
24
|
-
# service
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
# real `docker compose up` starts by default, which doesn't apply here since wip never starts
|
|
28
|
-
# "every service" — only `compose.service` and whatever it reaches via `depends_on`.
|
|
29
|
-
IGNORED_SERVICE_KEYS = %w[tty stdin_open networks restart cap_add profiles].freeze
|
|
23
|
+
# allocation is decided per invocation (CommandBuilder#tty?), not per service; every
|
|
24
|
+
# service already shares one project network (config.rb); and `wslc run`/`exec` has no
|
|
25
|
+
# restart-policy or capability flag to forward `restart:`/`cap_add:` to.
|
|
26
|
+
IGNORED_SERVICE_KEYS = %w[tty stdin_open networks restart cap_add].freeze
|
|
30
27
|
BUILD_KEYS = %w[context dockerfile args].freeze
|
|
31
28
|
SUPPORTED_CONDITIONS = %w[service_started].freeze
|
|
29
|
+
LIST_HINT = 'only supports short syntax ("host:container"), not long-syntax mappings'
|
|
32
30
|
|
|
33
31
|
# env interpolates compose.yml's ${VAR} references the way `docker compose` does,
|
|
34
32
|
# so values like `user: ${USER_ID}:${GROUP_ID}` reach wslc already substituted
|
|
@@ -53,9 +51,13 @@ module Wip
|
|
|
53
51
|
|
|
54
52
|
def service_names_in_dependency_order = @order.dup
|
|
55
53
|
|
|
54
|
+
# True if `name` is gated behind profiles: (wip has no --profile flag); false for an unknown name.
|
|
55
|
+
def profiled?(name) = @services[name.to_s]&.profiles&.any? || false
|
|
56
|
+
|
|
56
57
|
# name => {context:, dockerfile:, tag:} for every service with a build:.
|
|
58
|
+
# Profile-gated services are skipped — see #startable_order.
|
|
57
59
|
def build_specs
|
|
58
|
-
|
|
60
|
+
startable_order.filter_map do |name|
|
|
59
61
|
service = @services.fetch(name)
|
|
60
62
|
next unless service.build
|
|
61
63
|
|
|
@@ -66,7 +68,7 @@ module Wip
|
|
|
66
68
|
# Shaped like Config::DEPENDENCY_DEFAULTS expects: image/command/env/ports/volumes/workdir,
|
|
67
69
|
# in dependency order so callers iterating sidecars start them before their dependents.
|
|
68
70
|
def to_dependencies_hash
|
|
69
|
-
|
|
71
|
+
startable_order.to_h do |name|
|
|
70
72
|
service = @services.fetch(name)
|
|
71
73
|
[name, { 'image' => service.build ? image_tag(name, service) : service.image, 'command' => service.command,
|
|
72
74
|
'env' => service.env, 'ports' => service.ports, 'volumes' => service.volumes,
|
|
@@ -76,6 +78,10 @@ module Wip
|
|
|
76
78
|
|
|
77
79
|
private
|
|
78
80
|
|
|
81
|
+
# A profile-gated service (see #profiled?) is never among the ones wip starts
|
|
82
|
+
# on its own, but still participates in #topological_order/depends_on validation.
|
|
83
|
+
def startable_order = @order.reject { |name| profiled?(name) }
|
|
84
|
+
|
|
79
85
|
# A service naming both `build:` and `image:` builds via the former and tags the
|
|
80
86
|
# result with the latter (real Compose's own rule for that combination), instead
|
|
81
87
|
# of the auto-generated tag a build:-only service gets.
|
|
@@ -96,7 +102,8 @@ module Wip
|
|
|
96
102
|
|
|
97
103
|
def normalize_service_lists(name, entry)
|
|
98
104
|
{ ports: normalize_list(name, entry['ports'], 'ports'),
|
|
99
|
-
volumes: normalize_list(name, entry['volumes'], 'volumes')
|
|
105
|
+
volumes: normalize_list(name, entry['volumes'], 'volumes'),
|
|
106
|
+
profiles: normalize_list(name, entry['profiles'], 'profiles', hint: 'must be an array of strings') }
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
def image_or_build(name, entry)
|
|
@@ -129,10 +136,8 @@ module Wip
|
|
|
129
136
|
|
|
130
137
|
def normalize_build_mapping(name, value)
|
|
131
138
|
unknown = value.keys.map(&:to_s) - BUILD_KEYS
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"#{@path}: services.#{name}.build has unsupported key(s): #{unknown.join(', ')}"
|
|
135
|
-
end
|
|
139
|
+
raise ConfigError, "#{@path}: services.#{name}.build has unsupported key(s): #{unknown.join(', ')}" \
|
|
140
|
+
unless unknown.empty?
|
|
136
141
|
|
|
137
142
|
context = resolve_context(presence(value['context']) || '.')
|
|
138
143
|
dockerfile = presence(value['dockerfile'])
|
|
@@ -179,13 +184,12 @@ module Wip
|
|
|
179
184
|
end
|
|
180
185
|
end
|
|
181
186
|
|
|
182
|
-
def normalize_list(name, value, key)
|
|
187
|
+
def normalize_list(name, value, key, hint: LIST_HINT)
|
|
183
188
|
return [] unless value
|
|
184
189
|
raise ConfigError, "#{@path}: services.#{name}.#{key} must be an array" unless value.is_a?(Array)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
end
|
|
190
|
+
|
|
191
|
+
nested = value.any? { |item| item.is_a?(Hash) || item.is_a?(Array) }
|
|
192
|
+
raise ConfigError, "#{@path}: services.#{name}.#{key} #{hint}" if nested
|
|
189
193
|
|
|
190
194
|
value.map(&:to_s)
|
|
191
195
|
end
|
|
@@ -209,10 +213,18 @@ module Wip
|
|
|
209
213
|
dep.to_s
|
|
210
214
|
end
|
|
211
215
|
|
|
216
|
+
# Also rejects a startable (unprofiled) service depending on a profile-gated one —
|
|
217
|
+
# with no profile activation, real Compose treats that as an invalid model, since
|
|
218
|
+
# the dependency would silently never start.
|
|
212
219
|
def validate_depends_on!
|
|
213
220
|
@services.each do |name, service|
|
|
214
221
|
service.depends_on.each do |dep|
|
|
215
222
|
raise ConfigError, "#{@path}: services.#{name} depends_on unknown service '#{dep}'" unless @services.key?(dep)
|
|
223
|
+
next if service.profiles.any? || !profiled?(dep)
|
|
224
|
+
|
|
225
|
+
raise ConfigError, "#{@path}: services.#{name} depends_on '#{dep}', gated behind profiles: " \
|
|
226
|
+
"(#{@services.fetch(dep).profiles.join(', ')}) wip never activates " \
|
|
227
|
+
'(no --profile flag)'
|
|
216
228
|
end
|
|
217
229
|
end
|
|
218
230
|
end
|
data/lib/wip/config.rb
CHANGED
|
@@ -204,6 +204,19 @@ module Wip
|
|
|
204
204
|
|
|
205
205
|
def parsed_compose_file
|
|
206
206
|
@parsed_compose_file ||= ComposeFile.load(ComposeBridge.file_path(self), env: compose_interpolation_env)
|
|
207
|
+
.tap { |file| validate_compose_service_startable!(file) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# compose.service is what wip always starts by default (container:) — wip has no
|
|
211
|
+
# --profile flag to activate one, so naming a profile-gated service here would
|
|
212
|
+
# otherwise fail later with a misleading "No dependencies.<name> entry" instead
|
|
213
|
+
# of pointing at the real cause.
|
|
214
|
+
def validate_compose_service_startable!(file)
|
|
215
|
+
return unless file.profiled?(compose_service)
|
|
216
|
+
|
|
217
|
+
raise ConfigError, "compose.service '#{compose_service}' is gated behind profiles: in compose.yml, " \
|
|
218
|
+
'but wip has no --profile flag to activate one — pick a service with no profiles: ' \
|
|
219
|
+
'or remove profiles: from it'
|
|
207
220
|
end
|
|
208
221
|
|
|
209
222
|
# Compose interpolates ${VAR} references in compose.yml from the shell environment
|
data/lib/wip/version.rb
CHANGED