wslc-wip 0.17.6 → 0.18.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: adf27a8bafbb2482bfe25b393e3623a39fd0c8d77f92a7c6e6e60286506c8f17
4
- data.tar.gz: 77d180849706ea0915e82c03a7bfcae6d5ee0c581a87db808e3eceacdb0b8210
3
+ metadata.gz: 44cc7065b814848fed40e76cd95c640a7722a3b56834c76be1382f99fc8881bf
4
+ data.tar.gz: fc400d2a47818ec0a70d0bc5afba7b13afe93bbfbc54728c1446b844351873c9
5
5
  SHA512:
6
- metadata.gz: f894b61d72a792604221a0bc41c5bef018cb2257ee7469d958f17f96fd5de57e52f2a1665b74e51bfbbd3e6cea6ba9f684f42a4ab64747462098c4742b7cd350
7
- data.tar.gz: 68201cce4009583ffee442f06858d930a9d618a399553fbf04523281c8012630e0c8bedc1517012b5f9afca9896612d42829206303943b5548118d0a2d960d17
6
+ metadata.gz: bf14d601c1e35a60deb8d242b072d76f87fdd40e14381e9f890d2f0f6235daf43eddab7692fce1e90d1dc199892b3b935f2452d3d101cffc39b8767798115275
7
+ data.tar.gz: 2a995ceea7b96839811d2de3f99619174bc215b78303b2061ca9c95995d62e992949fbfd1cf39e03e9ea3314a2f036a9149dcd0c78ebdfa3939b8bc17e86a6d1
data/README.md CHANGED
@@ -370,8 +370,9 @@ 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 [--no-cache] [-- OPTIONS]` | Build the image from the `build` definition. By default, wip passes the target tag as `--cache-from` so previous layers can be reused; `--no-cache` disables that cache path. |
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
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
+ | `wip stop` | Stop the primary container and its sidecar `dependencies:` without removing them |
375
376
  | `wip down` | Stop and remove the primary container and its sidecar `dependencies:` |
376
377
  | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
377
378
  | `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
@@ -119,14 +119,22 @@ module Wip
119
119
  warn "\nwip: sync stopped"
120
120
  end
121
121
 
122
+ desc 'stop', 'Stop the configured container and its dependencies without removing them'
123
+ def stop
124
+ return execute(compose_bridge.stop, exit_on_failure: false) if load_config.compose?
125
+
126
+ execute(builder.stop, exit_on_failure: false)
127
+ sidecar_names.each do |name|
128
+ execute(builder.dependency_stop(name), exit_on_failure: false)
129
+ end
130
+ end
131
+
122
132
  desc 'down', 'Stop and remove the configured container and its dependencies'
123
133
  def down
124
134
  return execute(compose_bridge.down, exit_on_failure: false) if load_config.compose?
125
135
 
126
- execute(builder.down, exit_on_failure: false)
127
136
  execute(builder.remove, exit_on_failure: false)
128
137
  sidecar_names.each do |name|
129
- execute(builder.dependency_down(name), exit_on_failure: false)
130
138
  execute(builder.dependency_remove(name), exit_on_failure: false)
131
139
  end
132
140
  end
@@ -84,10 +84,6 @@ module Wip
84
84
  [@wslc, 'exec', required_container, *required_sync.mirror_command]
85
85
  end
86
86
 
87
- def down
88
- [@wslc, 'stop', required_container]
89
- end
90
-
91
87
  # Only reachable under mode: compose-native — mode: compose delegates `wip logs`
92
88
  # to the external compose command's own `logs` (ComposeBridge#logs) instead.
93
89
  # Single container only, mirroring `wslc`/docker's own `logs`: there's no
@@ -98,6 +94,10 @@ module Wip
98
94
  command.push(name.to_s)
99
95
  end
100
96
 
97
+ def stop
98
+ [@wslc, 'stop', required_container]
99
+ end
100
+
101
101
  def remove
102
102
  [@wslc, 'remove', '-f', required_container]
103
103
  end
@@ -128,7 +128,7 @@ module Wip
128
128
  [@wslc, 'list', '--all', '--filter', "name=#{name}", '--format', 'json']
129
129
  end
130
130
 
131
- def dependency_down(name)
131
+ def dependency_stop(name)
132
132
  [@wslc, 'stop', name.to_s]
133
133
  end
134
134
 
@@ -136,14 +136,16 @@ 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'] || '.'
142
145
  tag = values['tag'] || values['image']
143
146
  raise ConfigError, 'Build image/tag must not be empty' if tag.to_s.empty?
144
147
 
145
- cache_options = extra.include?('--no-cache') ? [] : ['--cache-from', tag]
146
- [@wslc, 'build', '-t', tag, *cache_options, *extra, context]
148
+ [@wslc, 'build', '-t', tag, *extra, context]
147
149
  end
148
150
 
149
151
  def custom(name, arguments)
@@ -45,6 +45,8 @@ module Wip
45
45
  command
46
46
  end
47
47
 
48
+ def stop = base.push('stop')
49
+
48
50
  def down = base.push('down')
49
51
 
50
52
  def exec(service, arguments, interactive: true)
@@ -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 already decided per invocation (see CommandBuilder#tty?), not fixed per
24
- # service; there's no per-service network to join beyond the one project network `network`
25
- # (config.rb) already puts every service on; `wslc run`/`exec` has no restart-policy or
26
- # capability flag to forward `restart:`/`cap_add:` to; and `profiles:` gates which services a
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
- @order.filter_map do |name|
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
- @order.to_h do |name|
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
- if unknown.any?
133
- raise ConfigError,
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
- if value.any?(Hash)
186
- raise ConfigError, "#{@path}: services.#{name}.#{key} only supports short syntax (\"host:container\"), " \
187
- 'not long-syntax mappings'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '0.17.6'
4
+ VERSION = '0.18.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.17.6
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors