wslc-wip 0.7.3 → 0.8.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: 3599b0a90a3528f5366a5ea007c213b60d66e47070e0ab419f267c7786587760
4
- data.tar.gz: 5afad9f0783d325bdf5ccc72f84d3b4e7308ff326dab3c5d0837e029ab5b234e
3
+ metadata.gz: afc914ac16e4359e48e682f5dab523827ad782251365abee7abd60b6eee89044
4
+ data.tar.gz: dcc97ec1bace3262bc1c419162085de6a3f5e24f0dde8babb2532ef9bdd6446c
5
5
  SHA512:
6
- metadata.gz: e57b54f645f50bc881a3702f54cb3a91ce099351047b75bd00df8aebfdea2536fb257756dd6f7d2277d629578d6afa764c7531befcd8b3b4eaf58e6174bbb818
7
- data.tar.gz: cc735ce314bd1c1f5e8f543d1a4f145dd61465cb236f8c99adc79d73104d454f00d4d79d0087daefa0be5b877eab76166ca14b3c87dff8892d469421de512e4e
6
+ metadata.gz: 526f9730c15eb6beabda31c3df3ee95cac2f8058c6da2dddf27100aadb48276bb68d66a3ede2b7383f49ecb6cd6fd45604b39ec1130058718cfbf439e7629f5a
7
+ data.tar.gz: 87a5f6c40e948fabdc79eeaf3e81cb73fac3cdb0fdd306189a72a437f459de22deeb57fbd92167411c157485d66dd82f40734f72316f46cb65fe6aa74b111511
data/README.md CHANGED
@@ -44,6 +44,7 @@ Put a `wip.yml` in your project root. Running from a subdirectory walks up to fi
44
44
 
45
45
  ```yaml
46
46
  version: 1
47
+ mode: container # or compose; picks the orchestration path `up`/`down`/`sync`/etc. take (default: container)
47
48
  wslc:
48
49
  command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
49
50
  defaults:
@@ -156,6 +157,8 @@ sync:
156
157
  command: rsync # binary that does the mirroring (default: rsync)
157
158
  options: [] # extra flags appended to the rsync invocation
158
159
  interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
160
+ mode: exec # exec (mirror inside the running container) or run (a throwaway one);
161
+ # default: exec for `mode: container`, run for `mode: compose`
159
162
  ```
160
163
 
161
164
  Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
@@ -166,8 +169,10 @@ Everything below `sync:` is optional — `sync: {}` alone already works. With it
166
169
  `dependencies:` keep mounting whatever they declare.
167
170
  - `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
168
171
  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.
172
+ - `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
173
+ rsync inside the already-running container; `sync.mode: run` always uses a throwaway container
174
+ with the same mounts instead. Which one runs is fixed by config, not guessed at from whether a
175
+ container happens to be up.
171
176
  - `wip sync --watch [--interval N]` keeps re-syncing until Ctrl-C, so host edits reach the
172
177
  container with a short delay. Run it in a second terminal alongside `wip up -d`.
173
178
  - `wip doctor` reports the resolved source, volume, and target, and fails if the source is missing.
@@ -181,8 +186,11 @@ image already has. And the mirror is one-way (host → volume): anything the app
181
186
  `target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
182
187
  or set `delete: false`.
183
188
 
184
- `sync:` is mutually exclusive with `compose:` in compose mode the compose file owns the volume
185
- layout.
189
+ `sync:` works alongside `mode: compose` too. Compose still owns the volume layout a compose
190
+ service must mount the same named volume `sync.volume` names — but wip's mirror runs as its own
191
+ throwaway container (`sync.mode` defaults to `run` and can't be set to `exec` under `mode:
192
+ compose`, since only a container wip itself booted is guaranteed to have the read-only source
193
+ mount attached).
186
194
 
187
195
  ### Compose mode
188
196
 
@@ -191,6 +199,7 @@ If your project already has a real `compose.yml`, don't duplicate it in `depende
191
199
 
192
200
  ```yaml
193
201
  version: 1
202
+ mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
194
203
  compose:
195
204
  service: app # required: which compose service wip run/exec/NAME target
196
205
  command: wslc-compose # required: the compose-for-wslc binary/path you have installed
data/lib/wip/cli.rb CHANGED
@@ -271,15 +271,14 @@ module Wip
271
271
  "run it with `wip dispatch #{name}`"
272
272
  end
273
273
 
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.
274
+ # sync.mode picks the path explicitly (default: exec for mode: container,
275
+ # run for mode: compose) rather than probing whether a container happens
276
+ # to be running under a matching name.
276
277
  def run_sync(exit_on_failure: true)
277
- command = container_running? ? builder.sync_exec : builder.sync_run
278
+ command = sync_settings!.exec? ? builder.sync_exec : builder.sync_run
278
279
  execute(command, exit_on_failure: exit_on_failure)
279
280
  end
280
281
 
281
- def container_running? = resource_exists?(builder.find_running)
282
-
283
282
  def sync_before_boot
284
283
  settings = load_config.sync
285
284
  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!
@@ -16,24 +16,36 @@ module Wip
16
16
  DEFAULT_TARGET = '/app'
17
17
  DEFAULT_BINARY = 'rsync'
18
18
  DEFAULT_INTERVAL = 2
19
- # -a preserves modes and timestamps so file watchers and bundler see a
20
- # faithful copy rather than a tree that looks freshly written every sync.
21
- BASE_OPTIONS = %w[-a].freeze
19
+ # Minimal set for a fast local-to-local mirror: -r walks the tree, -l
20
+ # keeps symlinks as symlinks, -t preserves mtimes so re-syncs can quick-
21
+ # check (size+mtime) instead of re-transferring unchanged files, and
22
+ # --whole-file skips the delta-transfer checksum pass that only pays off
23
+ # over a slow network. Owner/group/perm preservation (-o -g -p, part of
24
+ # -a) is left out since both sides are the same user; add them back via
25
+ # sync.options if a project needs them.
26
+ BASE_OPTIONS = %w[-r -l -t --whole-file].freeze
22
27
  # Trailing mount options wslc/docker accept after the container path.
23
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
24
34
 
25
- attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval
35
+ attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval, :mode
26
36
 
27
- def initialize(raw, base: nil, workdir: nil, container: nil)
37
+ def initialize(raw, base: nil, workdir: nil, container: nil, compose: false)
28
38
  raise ConfigError, 'sync must be a mapping' unless raw.is_a?(Hash)
29
39
 
30
40
  @base = base
31
41
  assign_paths(raw, workdir: workdir, container: container)
32
42
  assign_mirror(raw)
43
+ assign_mode(raw, compose: compose)
33
44
  validate!
34
45
  end
35
46
 
36
47
  def delete? = !!@delete
48
+ def exec? = mode == 'exec'
37
49
 
38
50
  # Expanded against the wip.yml directory so the mirror covers the same tree
39
51
  # no matter which subdirectory wip was invoked from.
@@ -64,7 +76,7 @@ module Wip
64
76
  def to_h
65
77
  { 'source' => source, 'target' => target, 'mount' => mount, 'volume' => volume,
66
78
  'delete' => delete?, 'exclude' => exclude, 'command' => binary, 'options' => extra_options,
67
- 'interval' => interval }
79
+ 'interval' => interval, 'mode' => mode }
68
80
  end
69
81
 
70
82
  private
@@ -84,6 +96,15 @@ module Wip
84
96
  @interval = raw.key?('interval') ? raw['interval'] : DEFAULT_INTERVAL
85
97
  end
86
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
+
87
108
  def validate!
88
109
  raise ConfigError, 'sync.target must be an absolute path' unless @target.start_with?('/')
89
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.3'
4
+ VERSION = '0.8.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.7.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors