wslc-wip 0.17.3 → 0.17.5

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: c00368614ae5c01c7f514957368d9533f2faf9be114cfd17c676fc70ebfec138
4
- data.tar.gz: 86d094ead585366c91633a963d0436a0c68af10f5bda3dace5ab46485f714a4a
3
+ metadata.gz: ae440c584eee01b5fca703f10fe051f457cd24534a54b8d2c76fe485d33fc256
4
+ data.tar.gz: '0078b9598b5084d7b62ff7cf3aa85589c61739801bdae6d8bfa76e3218459fde'
5
5
  SHA512:
6
- metadata.gz: 99cd7c6e59ccd6b64667ab7a179c240d96577f24296a93b801484f994570d6262ccc30d1535853a6cd7b4fb2a31f5e8145fa227aa637d7d9960f0aa794756a67
7
- data.tar.gz: 13a319d2b312f709a5937711bb65d69a560e098bed809e3f5f76fc83bae419983f5f4dbb0ae541e5afa3c93b9a7fd290b9c963378b6f405cd4bb0ceb3900e274
6
+ metadata.gz: 8d92d054446d0b517db26312afa196e0f4486181ea3c6254d7eecb11a4bd239c5b0a48ff28804d3c2806d1acfc0f9beeaebcce5dcacdd73855b3ed629386774d
7
+ data.tar.gz: 0b19afc749ee393752613e27306b56559e8ff5c31d29f0248ddc2bee05e0ba4b327725cef1a371b9f24f4fc9fef85c5972e1a005e041003100bd2e44a2c9f664
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'find'
3
4
  require 'fileutils'
4
5
  require 'pathname'
5
6
  require 'tmpdir'
@@ -14,9 +15,8 @@ module Wip
14
15
  @ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
15
16
  end
16
17
 
17
- # on_progress fires once per file copied, as `|count, total|` — so a caller
18
- # can show that staging a large context (thousands of small files can take
19
- # tens of seconds) is still moving instead of looking hung.
18
+ # on_progress fires before copying and after each file, as `|count, total|`,
19
+ # so a caller can report elapsed progress even while copying one large file.
20
20
  def stage(on_progress: nil)
21
21
  return yield @root.to_s if @ignore.empty?
22
22
 
@@ -30,6 +30,7 @@ module Wip
30
30
 
31
31
  def copy_included_files(destination, on_progress)
32
32
  files = included_files
33
+ on_progress&.call(0, files.size)
33
34
  files.each_with_index do |relative_path, index|
34
35
  target = destination.join(relative_path)
35
36
  FileUtils.mkdir_p(target.dirname)
@@ -41,13 +42,31 @@ module Wip
41
42
  end
42
43
  end
43
44
 
45
+ # Walks the tree by hand (rather than Dir.glob-ing it all up front) so an
46
+ # ignored directory — node_modules, vendor/bundle, a multi-gigabyte
47
+ # storage/ — is never descended into just to be thrown away afterward.
44
48
  def included_files
45
- Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).select do |entry|
46
- next false if %w[. ..].include?(entry)
49
+ result = []
50
+ Find.find(@root.to_s) do |path|
51
+ next if path == @root.to_s
47
52
 
48
- path = @root.join(entry)
49
- (path.file? || path.symlink?) && !@ignore.ignored?(entry)
53
+ entry = Pathname(path).relative_path_from(@root).to_s
54
+ result << entry if visit?(path, entry)
50
55
  end
56
+ result
57
+ end
58
+
59
+ # Returns whether `path` belongs in the staged context, pruning the walk
60
+ # when it's an ignored directory so nothing under it is even visited —
61
+ # unless a later negated rule could still re-include something beneath it.
62
+ def visit?(path, entry)
63
+ ignored = @ignore.ignored?(entry)
64
+ if !File.symlink?(path) && File.directory?(path)
65
+ Find.prune if ignored && @ignore.prunable?(entry)
66
+ return false
67
+ end
68
+
69
+ !ignored && (File.file?(path) || File.symlink?(path))
51
70
  end
52
71
  end
53
72
  end
data/lib/wip/cli.rb CHANGED
@@ -69,12 +69,13 @@ module Wip
69
69
  def build(*extra)
70
70
  extra.shift if extra.first == '--'
71
71
  settings = load_config.command('build') || {}
72
- context = settings['context'] || '.'
72
+ context = Pathname(load_config.path).dirname.join(settings['context'] || '.').to_s
73
73
  warn "wip: staging build context (#{context})"
74
74
  progress = StagingProgress.new
75
75
  BuildContext.new(context).stage(on_progress: progress.method(:tick)) do |staged_context|
76
76
  progress.finish
77
- execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
77
+ built = builder.build(settings: settings.merge('context' => staged_context), extra: extra)
78
+ execute(built, interactive: tty?(true))
78
79
  end
79
80
  ensure
80
81
  progress&.finish
@@ -301,11 +302,23 @@ module Wip
301
302
  # `wip up` invocation, mirroring ensure_sync_image's "build once, not every tick"
302
303
  # approach. A no-op for mode: container/compose and for build:-less services.
303
304
  def ensure_compose_images
304
- load_config.compose_build_specs.each_value do |spec|
305
- extra = spec['dockerfile'] ? ['-f', spec['dockerfile']] : []
306
- spec['args']&.each { |key, value| extra.push('--build-arg', "#{key}=#{value}") }
307
- execute(builder.build(settings: { 'context' => spec['context'], 'tag' => spec['tag'] }, extra: extra))
305
+ load_config.compose_build_specs.each_value { |spec| build_compose_image(spec) }
306
+ end
307
+
308
+ # Stages spec['context'] the same way `wip build` does, so a .dockerignore next to
309
+ # a compose-native service's build: is honored and progress is reported here too.
310
+ def build_compose_image(spec)
311
+ extra = spec['dockerfile'] ? ['-f', spec['dockerfile']] : []
312
+ spec['args']&.each { |key, value| extra.push('--build-arg', "#{key}=#{value}") }
313
+ warn "wip: staging build context (#{spec['context']})"
314
+ progress = StagingProgress.new
315
+ BuildContext.new(spec['context']).stage(on_progress: progress.method(:tick)) do |staged_context|
316
+ progress.finish
317
+ settings = { 'context' => staged_context, 'tag' => spec['tag'] }
318
+ execute(builder.build(settings: settings, extra: extra), interactive: tty?(true))
308
319
  end
320
+ ensure
321
+ progress&.finish
309
322
  end
310
323
 
311
324
  # Builds sync.build's image once per invocation (not per --watch tick, which
@@ -27,7 +27,7 @@ module Wip
27
27
  status = wait.value
28
28
  end
29
29
  report_hint(captured) unless status.success?
30
- status.exitstatus
30
+ exitstatus(status)
31
31
  rescue Errno::ENOENT => e
32
32
  @stderr.puts e.message
33
33
  127
@@ -38,6 +38,13 @@ module Wip
38
38
 
39
39
  private
40
40
 
41
+ # exitstatus is nil when the process was killed by a signal instead of
42
+ # exiting normally; fall back to the conventional 128+signal shell code
43
+ # so callers always get a comparable integer.
44
+ def exitstatus(status)
45
+ status.exitstatus || (128 + status.termsig)
46
+ end
47
+
41
48
  def report_hint(captured)
42
49
  hint = @interpreter.interpret(captured.force_encoding(Encoding::UTF_8).scrub)
43
50
  @stderr.puts("\n#{hint}") if hint
@@ -50,7 +57,7 @@ module Wip
50
57
  def run_attached(command, env)
51
58
  pid = Process.spawn(env, *command)
52
59
  _, status = Process.wait2(pid)
53
- status.exitstatus
60
+ exitstatus(status)
54
61
  rescue Errno::ENOENT => e
55
62
  @stderr.puts e.message
56
63
  127
@@ -30,8 +30,26 @@ module Wip
30
30
  end
31
31
  end
32
32
 
33
+ # Whether a walker can stop descending into an ignored directory without
34
+ # risking a miss: false if some later negated rule (e.g. `!node_modules/pkg`
35
+ # after `node_modules`) could still match something beneath it.
36
+ def prunable?(relative_path)
37
+ dir_components = relative_path.split('/')
38
+ @rules.none? { |rule| rule.negate && targets_descendant?(rule.pattern, dir_components) }
39
+ end
40
+
33
41
  private
34
42
 
43
+ def targets_descendant?(pattern, dir_components)
44
+ pattern_components = pattern.split('/')
45
+ return true if pattern_components.first == '**'
46
+ return false if pattern_components.size <= dir_components.size
47
+
48
+ pattern_components.first(dir_components.size).each_with_index.all? do |component, i|
49
+ File.fnmatch(component, dir_components[i], FNMATCH_FLAGS)
50
+ end
51
+ end
52
+
35
53
  def parse(line)
36
54
  line = line.strip
37
55
  return nil if line.empty? || line.start_with?('#')
data/lib/wip/doctor.rb CHANGED
@@ -151,6 +151,8 @@ module Wip
151
151
  condition == :ok ? ok_message : fail_message)
152
152
  end
153
153
 
154
- def command_available?(name) = ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) }
154
+ def command_available?(name)
155
+ ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) }
156
+ end
155
157
  end
156
158
  end
@@ -1,35 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- # Prints a self-overwriting "copying build context files: N/total" line, at
5
- # most every half second, while a slow, silent step (staging a large build
6
- # context file by file can take tens of seconds) is still moving so it
7
- # doesn't read as hung.
4
+ # Prints a self-overwriting "copying build context files: N/total" line
5
+ # every half second while a slow, silent step is running. Reporting runs on
6
+ # its own thread so a single large file does not make progress appear hung.
8
7
  class StagingProgress
9
8
  def initialize(out: $stderr, interval: 0.5)
10
9
  @out = out
11
10
  @interval = interval
12
- # Backdated so the very first tick always prints immediately, rather
13
- # than waiting a full interval before the first sign of life.
14
- @last = Process.clock_gettime(Process::CLOCK_MONOTONIC) - interval
15
- @printed = false
11
+ @mutex = Mutex.new
12
+ @condition = ConditionVariable.new
13
+ @worker = nil
14
+ @stopped = false
16
15
  end
17
16
 
18
17
  def tick(count, total)
19
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
- return if now - @last < @interval && count != total
21
-
22
- @last = now
23
- @printed = true
24
- @out.print "\rwip: copying build context files: #{count}/#{total}"
18
+ @mutex.synchronize do
19
+ @count = count
20
+ @total = total
21
+ print_progress if @worker.nil? || count == total
22
+ unless @worker
23
+ @stopped = false
24
+ @worker = Thread.new { report_on_interval }
25
+ end
26
+ end
25
27
  end
26
28
 
27
29
  # Idempotent: a caller can't know in advance whether tick ever fired.
28
30
  def finish
29
- return unless @printed
31
+ worker = @mutex.synchronize do
32
+ return unless @worker
33
+
34
+ @stopped = true
35
+ @condition.broadcast
36
+ @worker
37
+ end
38
+ worker.join
39
+
40
+ @mutex.synchronize do
41
+ @worker = nil
42
+ @out.puts
43
+ @out.flush
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def report_on_interval
50
+ @mutex.synchronize do
51
+ until @stopped
52
+ @condition.wait(@mutex, @interval)
53
+ print_progress unless @stopped
54
+ end
55
+ end
56
+ end
30
57
 
31
- @printed = false
32
- @out.puts
58
+ def print_progress
59
+ @out.print "\rwip: copying build context files: #{@count}/#{@total}"
60
+ @out.flush
33
61
  end
34
62
  end
35
63
  end
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.3'
4
+ VERSION = '0.17.5'
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.3
4
+ version: 0.17.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors