wslc-wip 0.7.4 → 0.9.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: 39d1c4da0d3b7446f90692ccf3d16ed693613edce14fafc4941d9fd51732486b
4
- data.tar.gz: bdd0f8e5b7a5a18ffd833d77844c5d0acf5638f0e0e2f2a337c0129b9cb0682a
3
+ metadata.gz: d16349112264f413d1492541731ec704d56616ccb4b05cef1f505d667a5d823d
4
+ data.tar.gz: 05f7f5819345079aa82e062ee368a3255f93bdeaa62cf55fda2621c2bd36c4ed
5
5
  SHA512:
6
- metadata.gz: c5e3824808f307422ad5e6e402309242bb884bbcc2b3d708e8f576a97100a66fda81e20e5b63922b5123160c7348987716ca951e8de37f8a4875b522cd23d471
7
- data.tar.gz: a187b673ed8efea261c2c8218e69940e1e5fd64cdc911ea2b217f2276eb72193d674c4933709ce1437f67359ac46376a3280b04c397d3f7e98d78f2492141624
6
+ metadata.gz: cdbeea98b7472f9ec6e1ccec8883bf136dfcb76164c2ba2d2ddbd63fe52f22c20f72a994a2a85527b1d9e7a7511016fe1b54ab64e678aca009530616887435f1
7
+ data.tar.gz: a6f524f41b20fe20af6262e3e22e2dafa924d37e31279ff21ed94268824587017ad94670084ae81aeb79513369a923d37011fd95bd9471a1e357551b90701769
data/README.md CHANGED
@@ -31,6 +31,7 @@ From source: `bundle install && bundle exec exe/wip version`.
31
31
  ```bash
32
32
  gem install wslc-wip
33
33
  cd my-project
34
+ wip init # writes a starter wip.yml; edit the TODOs, then:
34
35
  wip doctor
35
36
  wip build
36
37
  wip up -d
@@ -44,6 +45,7 @@ Put a `wip.yml` in your project root. Running from a subdirectory walks up to fi
44
45
 
45
46
  ```yaml
46
47
  version: 1
48
+ mode: container # or compose; picks the orchestration path `up`/`down`/`sync`/etc. take (default: container)
47
49
  wslc:
48
50
  command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
49
51
  defaults:
@@ -156,6 +158,8 @@ sync:
156
158
  command: rsync # binary that does the mirroring (default: rsync)
157
159
  options: [] # extra flags appended to the rsync invocation
158
160
  interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
161
+ mode: exec # exec (mirror inside the running container) or run (a throwaway one);
162
+ # default: exec for `mode: container`, run for `mode: compose`
159
163
  ```
160
164
 
161
165
  Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
@@ -166,8 +170,10 @@ Everything below `sync:` is optional — `sync: {}` alone already works. With it
166
170
  `dependencies:` keep mounting whatever they declare.
167
171
  - `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
168
172
  skips that step.
169
- - `wip sync` mirrors on demand `wslc exec`ing rsync inside the container when it's running, and
170
- falling back to a throwaway container with the same mounts when it isn't.
173
+ - `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
174
+ rsync inside the already-running container; `sync.mode: run` always uses a throwaway container
175
+ with the same mounts instead. Which one runs is fixed by config, not guessed at from whether a
176
+ container happens to be up.
171
177
  - `wip sync --watch [--interval N]` keeps re-syncing until Ctrl-C, so host edits reach the
172
178
  container with a short delay. Run it in a second terminal alongside `wip up -d`.
173
179
  - `wip doctor` reports the resolved source, volume, and target, and fails if the source is missing.
@@ -181,8 +187,11 @@ image already has. And the mirror is one-way (host → volume): anything the app
181
187
  `target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
182
188
  or set `delete: false`.
183
189
 
184
- `sync:` is mutually exclusive with `compose:` in compose mode the compose file owns the volume
185
- layout.
190
+ `sync:` works alongside `mode: compose` too. Compose still owns the volume layout a compose
191
+ service must mount the same named volume `sync.volume` names — but wip's mirror runs as its own
192
+ throwaway container (`sync.mode` defaults to `run` and can't be set to `exec` under `mode:
193
+ compose`, since only a container wip itself booted is guaranteed to have the read-only source
194
+ mount attached).
186
195
 
187
196
  ### Compose mode
188
197
 
@@ -191,6 +200,7 @@ If your project already has a real `compose.yml`, don't duplicate it in `depende
191
200
 
192
201
  ```yaml
193
202
  version: 1
203
+ mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
194
204
  compose:
195
205
  service: app # required: which compose service wip run/exec/NAME target
196
206
  command: wslc-compose # required: the compose-for-wslc binary/path you have installed
@@ -229,6 +239,7 @@ found, its version, and which compose file `wip` resolved.
229
239
 
230
240
  | Command | Description |
231
241
  |---|---|
242
+ | `wip init [--force]` | Write a starter `wip.yml`: `mode: compose` if a `compose.yml`/`docker-compose.yml` is found next to it, `mode: container` otherwise. Refuses to overwrite an existing `wip.yml` unless `--force` |
232
243
  | `wip version` | wip's version, plus WSLC's if it can be detected |
233
244
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
234
245
  | `wip config` | Print the effective configuration (secrets masked) |
data/lib/wip/cli.rb CHANGED
@@ -39,6 +39,17 @@ module Wip
39
39
  nil
40
40
  end
41
41
 
42
+ desc 'init', 'Create a starter wip.yml (detects an existing compose.yml/docker-compose.yml)'
43
+ option :force, type: :boolean, default: false, desc: 'Overwrite an existing wip.yml'
44
+ def init
45
+ path = Pathname(options[:config] || ConfigLoader::FILENAME).expand_path
46
+ raise Error, "#{path} already exists (use --force to overwrite)" if path.file? && !options[:force]
47
+
48
+ initializer = Initializer.new(dir: path.dirname)
49
+ path.write(initializer.call)
50
+ warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose' : 'container'})"
51
+ end
52
+
42
53
  desc 'doctor', 'Diagnose the development environment'
43
54
  def doctor
44
55
  results = Doctor.new(loader: loader).call
@@ -271,15 +282,14 @@ module Wip
271
282
  "run it with `wip dispatch #{name}`"
272
283
  end
273
284
 
274
- # Inside the running container the mirror is a plain `exec`; otherwise it
275
- # takes a throwaway container that mounts the same source and volume.
285
+ # sync.mode picks the path explicitly (default: exec for mode: container,
286
+ # run for mode: compose) rather than probing whether a container happens
287
+ # to be running under a matching name.
276
288
  def run_sync(exit_on_failure: true)
277
- command = container_running? ? builder.sync_exec : builder.sync_run
289
+ command = sync_settings!.exec? ? builder.sync_exec : builder.sync_run
278
290
  execute(command, exit_on_failure: exit_on_failure)
279
291
  end
280
292
 
281
- def container_running? = resource_exists?(builder.find_running)
282
-
283
293
  def sync_before_boot
284
294
  settings = load_config.sync
285
295
  return unless settings
@@ -49,13 +49,9 @@ module Wip
49
49
  [@wslc, 'list', '--all', '--filter', "name=#{container}", '--format', 'json']
50
50
  end
51
51
 
52
- def find_running
53
- container = required(@config.defaults, 'container')
54
- [@wslc, 'list', '--filter', "name=#{container}", '--format', 'json']
55
- end
56
-
57
- # Mirrors into the volume from a throwaway container, for when the app
58
- # container isn't running yet (or at all) — e.g. just before `up` boots it.
52
+ # Mirrors into the volume from a throwaway container. Used for sync.mode:
53
+ # run (compose's default, and mode: container's fallback for when the app
54
+ # container isn't running yet — e.g. just before `up` boots it).
59
55
  def sync_run
60
56
  sync = required_sync
61
57
  command = [@wslc, 'run', '--rm']
@@ -63,8 +59,10 @@ module Wip
63
59
  command.push(required(@config.defaults, 'image')).concat(sync.mirror_command)
64
60
  end
65
61
 
66
- # Mirrors from inside the running container, which already has both the
67
- # read-only source mount and the volume attached.
62
+ # Mirrors from inside the already-running container. Only valid for
63
+ # sync.mode: exec (mode: container's default), since only a container wip
64
+ # itself booted is guaranteed to have both the read-only source mount and
65
+ # the volume attached.
68
66
  def sync_exec
69
67
  [@wslc, 'exec', required(@config.defaults, 'container'), *required_sync.mirror_command]
70
68
  end
data/lib/wip/config.rb CHANGED
@@ -6,6 +6,10 @@ module Wip
6
6
  DEFAULTS = { 'container' => 'app', 'workdir' => '/app', 'interactive' => false,
7
7
  'remove' => true, 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
8
8
  SECRET_PATTERN = /token|password|secret|credential|auth/i
9
+ # Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
10
+ # inferred from a `compose:` block's presence, so a config reader doesn't have
11
+ # to know that rule to predict which mode wip runs in.
12
+ MODES = %w[container compose].freeze
9
13
  attr_reader :path
10
14
 
11
15
  def initialize(raw, path = nil)
@@ -20,8 +24,9 @@ module Wip
20
24
  def up_command = @raw.dig('up', 'command')
21
25
  def dependencies = @raw['dependencies'] || {}
22
26
  def network = defaults['network']
27
+ def mode = @raw['mode'] || 'container'
23
28
  def compose = @raw['compose']
24
- def compose? = !!compose
29
+ def compose? = mode == 'compose'
25
30
  def compose_service = compose && compose['service']
26
31
  def compose_file = compose && compose['file']
27
32
  def compose_project = compose && compose['project']
@@ -50,8 +55,8 @@ module Wip
50
55
 
51
56
  def to_h(redact: true)
52
57
  value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
53
- 'up' => { 'command' => up_command }, 'dependencies' => dependencies, 'compose' => compose,
54
- 'sync' => sync&.to_h,
58
+ 'up' => { 'command' => up_command }, 'mode' => mode, 'dependencies' => dependencies,
59
+ 'compose' => compose, 'sync' => sync&.to_h,
55
60
  'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
56
61
  redact ? redact_secrets(value) : value
57
62
  end
@@ -62,6 +67,7 @@ module Wip
62
67
  raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
63
68
  raise ConfigError, 'up must be a mapping' if @raw.key?('up') && !@raw['up'].is_a?(Hash)
64
69
 
70
+ validate_mode!
65
71
  validate_commands!
66
72
  validate_dependencies!
67
73
  validate_compose!
@@ -70,12 +76,20 @@ module Wip
70
76
 
71
77
  def build_sync
72
78
  SyncSettings.new(@raw['sync'], base: path && File.dirname(path.to_s),
73
- workdir: defaults['workdir'], container: defaults['container'])
79
+ workdir: defaults['workdir'], container: defaults['container'],
80
+ compose: compose?)
74
81
  end
75
82
 
83
+ def validate_mode!
84
+ raise ConfigError, "mode must be one of #{MODES.join(', ')}" unless MODES.include?(mode)
85
+ raise ConfigError, 'mode: compose requires a compose: block' if compose? && !@raw.key?('compose')
86
+ raise ConfigError, 'a compose: block requires mode: compose' if @raw.key?('compose') && !compose?
87
+ end
88
+
89
+ # Forces SyncSettings to build (and so run its own validation, including
90
+ # sync.mode vs. mode) at load time instead of on first access.
76
91
  def validate_sync!
77
- return unless sync
78
- raise ConfigError, 'sync is mutually exclusive with compose' if compose?
92
+ sync if @raw.key?('sync')
79
93
  end
80
94
 
81
95
  def validate_commands!
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Builds a starter wip.yml. Detects an existing compose file next to the
5
+ # target (the same filenames ComposeBridge auto-detects) to decide between
6
+ # mode: compose and mode: container, since that's the one decision `wip
7
+ # init` can't leave to a placeholder.
8
+ class Initializer
9
+ CONTAINER_TEMPLATE = <<~YAML
10
+ version: 1
11
+ mode: container
12
+
13
+ defaults:
14
+ container: app # TODO: name of the container wip creates
15
+ image: your/image:tag # TODO: image to run
16
+ workdir: /app
17
+
18
+ sync: {} # optional; mirrors the source into a named volume instead of bind-mounting it live
19
+ YAML
20
+
21
+ def initialize(dir: Dir.pwd)
22
+ @dir = dir
23
+ end
24
+
25
+ def compose? = !!compose_file
26
+
27
+ def call
28
+ compose? ? compose_template : CONTAINER_TEMPLATE
29
+ end
30
+
31
+ private
32
+
33
+ def compose_file
34
+ @compose_file ||= ComposeBridge::FILENAMES.find { |name| File.file?(File.join(@dir, name)) }
35
+ end
36
+
37
+ def compose_template
38
+ <<~YAML
39
+ version: 1
40
+ mode: compose
41
+
42
+ compose:
43
+ service: app # TODO: which service in #{compose_file} wip run/exec/NAME target
44
+ command: wslc-compose # TODO: the compose-for-wslc binary/path you have installed
45
+ YAML
46
+ end
47
+ end
48
+ end
@@ -26,19 +26,26 @@ module Wip
26
26
  BASE_OPTIONS = %w[-r -l -t --whole-file].freeze
27
27
  # Trailing mount options wslc/docker accept after the container path.
28
28
  VOLUME_MODES = %w[ro rw z Z cached delegated consistent].freeze
29
+ # exec mirrors inside the already-running, wip-managed container (fast,
30
+ # but only correct when wip itself created that container with the sync
31
+ # mounts attached). run always mirrors from a disposable container, since
32
+ # compose owns its own services' mounts and never guarantees that shape.
33
+ SYNC_MODES = %w[exec run].freeze
29
34
 
30
- attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval
35
+ attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval, :mode
31
36
 
32
- def initialize(raw, base: nil, workdir: nil, container: nil)
37
+ def initialize(raw, base: nil, workdir: nil, container: nil, compose: false)
33
38
  raise ConfigError, 'sync must be a mapping' unless raw.is_a?(Hash)
34
39
 
35
40
  @base = base
36
41
  assign_paths(raw, workdir: workdir, container: container)
37
42
  assign_mirror(raw)
43
+ assign_mode(raw, compose: compose)
38
44
  validate!
39
45
  end
40
46
 
41
47
  def delete? = !!@delete
48
+ def exec? = mode == 'exec'
42
49
 
43
50
  # Expanded against the wip.yml directory so the mirror covers the same tree
44
51
  # no matter which subdirectory wip was invoked from.
@@ -69,7 +76,7 @@ module Wip
69
76
  def to_h
70
77
  { 'source' => source, 'target' => target, 'mount' => mount, 'volume' => volume,
71
78
  'delete' => delete?, 'exclude' => exclude, 'command' => binary, 'options' => extra_options,
72
- 'interval' => interval }
79
+ 'interval' => interval, 'mode' => mode }
73
80
  end
74
81
 
75
82
  private
@@ -89,6 +96,15 @@ module Wip
89
96
  @interval = raw.key?('interval') ? raw['interval'] : DEFAULT_INTERVAL
90
97
  end
91
98
 
99
+ def assign_mode(raw, compose:)
100
+ @mode = presence(raw['mode']) || (compose ? 'run' : 'exec')
101
+ raise ConfigError, "sync.mode must be one of #{SYNC_MODES.join(', ')}" unless SYNC_MODES.include?(@mode)
102
+ return unless compose && exec?
103
+
104
+ raise ConfigError, 'sync.mode: exec needs mode: container (compose owns its services’ mounts, ' \
105
+ 'so it can’t guarantee the running container has the sync mounts attached)'
106
+ end
107
+
92
108
  def validate!
93
109
  raise ConfigError, 'sync.target must be an absolute path' unless @target.start_with?('/')
94
110
  raise ConfigError, 'sync.mount must be an absolute path' unless @mount.start_with?('/')
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.7.4'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/wip.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'wip/command_resolver'
13
13
  require_relative 'wip/error_interpreter'
14
14
  require_relative 'wip/command_builder'
15
15
  require_relative 'wip/compose_bridge'
16
+ require_relative 'wip/initializer'
16
17
  require_relative 'wip/command_display'
17
18
  require_relative 'wip/command_runner'
18
19
  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.7.4
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -49,6 +49,7 @@ files:
49
49
  - lib/wip/environment.rb
50
50
  - lib/wip/error_interpreter.rb
51
51
  - lib/wip/errors.rb
52
+ - lib/wip/initializer.rb
52
53
  - lib/wip/resource_monitor.rb
53
54
  - lib/wip/sync_settings.rb
54
55
  - lib/wip/version.rb