wslc-wip 1.1.0 → 1.1.1

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: e2bffbab317006bdd799443ef1dc7e16d9527219261f91c9758504ae85aef638
4
- data.tar.gz: b1e0f8f72d44878564573fcc8076af18defe14375d1bee4cd4946c72139bcc1e
3
+ metadata.gz: 49996dea1b8ae083cceb0caaafc0b8ddcc6de0a3ab8a7f5f4f856156948840d3
4
+ data.tar.gz: cca5758770d9f4364427c1ae7519ee8026c598b1002206fc9af8f22f5ffc0a64
5
5
  SHA512:
6
- metadata.gz: d16eda21cb080e2dd7decc2522c4dac7c99536f3c720c1de7d64f0065edd7487c633450db9eeb687b12138b2763d3093628e0d169b82d7896c781e7c8298bb3c
7
- data.tar.gz: 5f668930355e17877237892da08dc440630f2015253b072c715e324c38045598765c58cb84d9657d7769527d3f27774f8d4f03f6bec2cc1bb785bca9a308cce2
6
+ metadata.gz: df63da254bbbb1b6dedf5e56b5a78d8fbb09fd7d6dfa41ae0b3b58db600f07e8703ea3d729a4bb125e8d1a29e498ce8c3e4504e93d74ad51ce37b7d4a3bad0db
7
+ data.tar.gz: 8b8607bdda225610b7c7389210c16a04764aa95ba45748f9bcd3c384f9b6eb3ccb371342cea89a12749d7c4226f9fe6b94729c191513690b40168b339d21dd7b
data/README.md CHANGED
@@ -244,8 +244,10 @@ top level (`networks:`, `volumes:`, `configs:`, `secrets:`, ...) is the one exce
244
244
  real Compose tools, not by `wip`, so `wip` silently ignores it rather than rejecting an otherwise
245
245
  valid compose.yml over sections it doesn't need to look at:
246
246
 
247
- - Per service: `image`, `build` (string or `{context:, dockerfile:}`; resolved relative to
248
- `compose.yml`, not wherever `wip` is invoked from), `command` (shell or exec form), `environment`
247
+ - Per service: `image`, `build` (string or `{context:, dockerfile:, args:, shadow_context:}`;
248
+ `context` is resolved relative to `compose.yml`, not wherever `wip` is invoked from; `dockerfile`
249
+ stays relative to `context` itself — see [.dockerignore](#dockerignore) for what `shadow_context`
250
+ does), `command` (shell or exec form), `environment`
249
251
  (mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
250
252
  isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
251
253
  long-syntax mappings), `working_dir`, `user`, `depends_on` (ordering only — a `condition:` other
@@ -272,13 +274,14 @@ to load a different file instead.
272
274
 
273
275
  `wip build` reads `.dockerignore` from the build context and excludes anything it matches before
274
276
  handing the context to `wslc build`, since `wslc` sends the context as-is otherwise. Set
275
- `commands.build.shadow_context` in `wip.yml` to a directory on the Windows filesystem to enable a
276
- persistent shadow context for projects outside `/mnt/<drive>`. The first build copies every
277
- included file; later builds only copy added or changed files and remove deleted or newly ignored
278
- files. Without this setting the optimization is disabled, and it only applies under WSL2 on WSL1
279
- (or anywhere else) the context is handed to `wslc build` directly. Projects already on `/mnt/c` (or
280
- another mounted Windows drive) also continue to build directly even when the setting is present.
281
- The path must live outside the build context itself, or `wip build` refuses it.
277
+ `commands.build.shadow_context` in `wip.yml` (or `build.shadow_context` on a compose-native
278
+ `build:` service in `compose.yml`) to a directory on the Windows filesystem to enable a persistent
279
+ shadow context for projects outside `/mnt/<drive>`. The first build copies every included file;
280
+ later builds only copy added or changed files and remove deleted or newly ignored files. Without
281
+ this setting the optimization is disabled, and it only applies under WSL2 on WSL1 (or anywhere
282
+ else) the context is handed to `wslc build` directly. Projects already on `/mnt/c` (or another
283
+ mounted Windows drive) also continue to build directly even when the setting is present. The path
284
+ must live outside the build context itself, or `wip build`/`wip up` refuses it.
282
285
 
283
286
  ### Source sync
284
287
 
@@ -30,6 +30,11 @@ module Wip
30
30
  end
31
31
  end
32
32
 
33
+ # Whether the upcoming stage will use the Windows-side shadow directory
34
+ # (configured shadow_root, on WSL2, with a context outside /mnt) rather
35
+ # than staging in place or under a WSL-side tmpdir.
36
+ def shadow? = shadow_required?
37
+
33
38
  private
34
39
 
35
40
  # A shadow root under the context would itself be walked by included_files
data/lib/wip/cli.rb CHANGED
@@ -76,8 +76,7 @@ module Wip
76
76
  build_context = BuildContext.new(context, shadow_root: settings['shadow_context'])
77
77
  build_context.stage(on_progress: progress.method(:tick)) do |staged_context|
78
78
  progress.finish
79
- built = builder.build(settings: settings.merge('context' => staged_context), extra: extra)
80
- execute(built, interactive: tty?(true))
79
+ run_staged_build(build_context, staged_context, settings, extra)
81
80
  end
82
81
  ensure
83
82
  progress&.finish
@@ -248,10 +247,12 @@ module Wip
248
247
  execute(exec_target(command, interactive: interactive), interactive: tty?(interactive))
249
248
  end
250
249
 
251
- def execute(command, interactive: false, exit_on_failure: true)
250
+ def execute(command, interactive: false, exit_on_failure: true, chdir: nil)
252
251
  runner = CommandRunner.new(debug: debug?)
252
+ run_options = { interactive: interactive }
253
+ run_options[:chdir] = chdir if chdir
253
254
  code = reporter.step("running: #{CommandDisplay.for_debug(command)}", live: !interactive) do
254
- runner.run(command, interactive: interactive)
255
+ runner.run(command, **run_options)
255
256
  end
256
257
  exit(code) if exit_on_failure && !code.zero?
257
258
  code
@@ -322,15 +323,23 @@ module Wip
322
323
  extra = compose_build_extra(spec)
323
324
  warn "wip: building service '#{name}' (tag: #{spec['tag']}) from #{spec['context']}"
324
325
  progress = StagingProgress.new
325
- BuildContext.new(spec['context']).stage(on_progress: progress.method(:tick)) do |staged_context|
326
+ build_context = BuildContext.new(spec['context'], shadow_root: spec['shadow_context'])
327
+ build_context.stage(on_progress: progress.method(:tick)) do |staged_context|
326
328
  progress.finish
327
- settings = { 'context' => staged_context, 'tag' => spec['tag'] }
328
- execute(builder.build(settings: settings, extra: extra), interactive: tty?(true))
329
+ run_staged_build(build_context, staged_context, { 'tag' => spec['tag'] }, extra)
329
330
  end
330
331
  ensure
331
332
  progress&.finish
332
333
  end
333
334
 
335
+ # wslc build crashes (ERROR_UNHANDLED_EXCEPTION) when handed an absolute
336
+ # context path; running from inside the context and passing "." avoids it.
337
+ def run_staged_build(build_context, staged_context, settings, extra)
338
+ warn "wip: using shadow build context at #{staged_context}" if build_context.shadow?
339
+ built = builder.build(settings: settings.merge('context' => '.'), extra: extra)
340
+ execute(built, interactive: tty?(true), chdir: staged_context)
341
+ end
342
+
334
343
  def build_extra_options(extra)
335
344
  extra = extra.dup
336
345
  extra.shift if extra.first == '--'
@@ -356,7 +365,9 @@ module Wip
356
365
 
357
366
  Dir.mktmpdir('wip-sync-build-') do |dir|
358
367
  File.write(File.join(dir, 'Dockerfile'), settings.build['dockerfile'])
359
- execute(builder.sync_build(dir))
368
+ # wslc build crashes (ERROR_UNHANDLED_EXCEPTION) when handed an absolute
369
+ # context path; running from inside the context and passing "." avoids it.
370
+ execute(builder.sync_build('.'), chdir: dir)
360
371
  end
361
372
  end
362
373
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'pty'
5
+ require 'io/console'
4
6
 
5
7
  module Wip
6
8
  # Executes a built command, pumping its I/O and returning the exit status.
@@ -14,13 +16,14 @@ module Wip
14
16
  @debug = debug
15
17
  end
16
18
 
17
- def run(command, env: {}, interactive: false)
19
+ def run(command, env: {}, interactive: false, chdir: nil)
18
20
  @stderr.puts "+ #{CommandDisplay.for_debug(command)}" if @debug
19
- return run_attached(command, env) if interactive
21
+ return run_attached(command, env, chdir) if interactive
20
22
 
23
+ opts = chdir ? { chdir: chdir } : {}
21
24
  captured = +''
22
25
  status = nil
23
- Open3.popen3(env, *command) do |input, output, error, wait|
26
+ Open3.popen3(env, *command, opts) do |input, output, error, wait|
24
27
  input.close
25
28
  threads = [pump(output, @stdout, captured), pump(error, @stderr, captured)]
26
29
  threads.each(&:join)
@@ -52,15 +55,84 @@ module Wip
52
55
 
53
56
  # Piping stdin/stdout/stderr (as `run` does above) closes the child's
54
57
  # stdin immediately, which breaks anything that reads from the terminal
55
- # (a shell, `rails console`, ...). Inherit the real file descriptors
56
- # instead so the child gets a genuine TTY.
57
- def run_attached(command, env)
58
- pid = Process.spawn(env, *command)
59
- _, status = Process.wait2(pid)
58
+ # (a shell, `rails console`, ...). Run it behind a pseudo-terminal instead
59
+ # of inheriting wip's real fds directly: a pty still gives the child a
60
+ # genuine controlling terminal (job control, Ctrl-C -> SIGINT, isatty-gated
61
+ # rendering all work the same as direct inheritance), but routes output
62
+ # through wip first, so report_hint can still see it — inherited fds go
63
+ # straight to the terminal and wip never would. wip's own terminal is
64
+ # switched to raw mode for the duration so only the pty's line discipline
65
+ # echoes input; without that, every keystroke would echo twice.
66
+ def run_attached(command, env, chdir)
67
+ opts = chdir ? { chdir: chdir } : {}
68
+ captured = +''
69
+ status = nil
70
+ PTY.spawn(env, *command, opts) do |output, input, pid|
71
+ sync_winsize(output)
72
+ with_winsize_sync(output) { with_raw_stdin { pump_attached(output, input, captured) } }
73
+ _, status = Process.wait2(pid)
74
+ end
75
+ report_hint(captured) unless status.success?
60
76
  exitstatus(status)
61
77
  rescue Errno::ENOENT => e
62
78
  @stderr.puts e.message
63
79
  127
80
+ rescue Interrupt
81
+ @stderr.puts "\nwip: interrupted"
82
+ 130
83
+ end
84
+
85
+ def pump_attached(output, input, captured)
86
+ stdin_thread = forward_stdin(input)
87
+ loop do
88
+ chunk = output.readpartial(4096)
89
+ @stdout.write(chunk)
90
+ captured << chunk
91
+ end
92
+ rescue Errno::EIO, IOError
93
+ # the pty's slave side closed when the child exited
94
+ ensure
95
+ stdin_thread.kill
96
+ end
97
+
98
+ # Keeps the child's pty sized to wip's real terminal so full-screen
99
+ # programs (an editor, `less`, ...) render correctly. A non-tty @stdout
100
+ # (piped output, tests) has no size to read, so the pty keeps its default.
101
+ def sync_winsize(output)
102
+ output.winsize = @stdout.winsize if @stdout.respond_to?(:winsize) && @stdout.tty?
103
+ end
104
+
105
+ # The child's pty only gets the terminal size wip had at spawn time
106
+ # (sync_winsize, above) — it's a separate pty from wip's own real one, so
107
+ # later resizes of wip's terminal don't reach it on their own. Trap
108
+ # SIGWINCH for the duration to re-sync it live, restoring whatever handler
109
+ # was already installed (if any) once the command finishes.
110
+ def with_winsize_sync(output)
111
+ return yield unless @stdout.respond_to?(:winsize) && @stdout.tty?
112
+
113
+ previous = Signal.trap('WINCH') { sync_winsize(output) }
114
+ yield
115
+ ensure
116
+ Signal.trap('WINCH', previous) if previous
117
+ end
118
+
119
+ # A non-tty @stdin (piped input, tests) has no raw mode to switch to, so
120
+ # it's forwarded to the child as-is.
121
+ def with_raw_stdin(&)
122
+ return yield unless @stdin.respond_to?(:raw) && @stdin.tty?
123
+
124
+ @stdin.raw(&)
125
+ end
126
+
127
+ def forward_stdin(input)
128
+ Thread.new do
129
+ loop do
130
+ chunk = @stdin.readpartial(4096)
131
+ input.write(chunk)
132
+ end
133
+ rescue IOError, Errno::EIO, Errno::EBADF
134
+ nil
135
+ end
64
136
  end
65
137
 
66
138
  # An Interrupt during the main thread's `join` closes these pipes out from
@@ -24,7 +24,7 @@ module Wip
24
24
  # service already shares one project network (config.rb); and `wslc run`/`exec` has no
25
25
  # restart-policy or capability flag to forward `restart:`/`cap_add:` to.
26
26
  IGNORED_SERVICE_KEYS = %w[tty stdin_open networks restart cap_add].freeze
27
- BUILD_KEYS = %w[context dockerfile args].freeze
27
+ BUILD_KEYS = %w[context dockerfile args shadow_context].freeze
28
28
  SUPPORTED_CONDITIONS = %w[service_started].freeze
29
29
  LIST_HINT = 'only supports short syntax ("host:container"), not long-syntax mappings'
30
30
 
@@ -140,9 +140,11 @@ module Wip
140
140
  unless unknown.empty?
141
141
 
142
142
  context = resolve_context(presence(value['context']) || '.')
143
- dockerfile = presence(value['dockerfile'])
144
- { 'context' => context, 'dockerfile' => dockerfile && Pathname(context).join(dockerfile).to_s,
145
- 'args' => normalize_kv(name, value['args'], 'build.args') }.compact
143
+ # Kept relative to context (not resolved against it) so `-f` still finds it once
144
+ # `wip up`/`wip build` chdir into a staged or shadowed copy of that context.
145
+ { 'context' => context, 'dockerfile' => presence(value['dockerfile']),
146
+ 'args' => normalize_kv(name, value['args'], 'build.args'),
147
+ 'shadow_context' => presence(value['shadow_context']) }.compact
146
148
  end
147
149
 
148
150
  # build.context is relative to compose.yml's own directory (Compose's own rule),
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '1.1.0'
4
+ VERSION = '1.1.1'
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: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors