wslc-wip 0.17.1 → 0.17.3

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: d462bf5f1448a0488b717c963bccd77fc7c9719d29b74b990226179fd26e75f8
4
- data.tar.gz: 7c05922ab0abc42995ed495c99704d2dbab435d8ab88ed46c9094a402052abed
3
+ metadata.gz: c00368614ae5c01c7f514957368d9533f2faf9be114cfd17c676fc70ebfec138
4
+ data.tar.gz: 86d094ead585366c91633a963d0436a0c68af10f5bda3dace5ab46485f714a4a
5
5
  SHA512:
6
- metadata.gz: 0cfc54266fde234fceac4dffc6f43968ab27123704040eaf6183dec8c54f25bc33daad08a5bd03a7552554474dc14848162dca40b0ebcde6a785c721632cc0b1
7
- data.tar.gz: f4bfd8777b2f23b9f5c688b2d5794219914df74f42e44bb3a99b5c209e32fc262f240803ff5b8c505f427e19c3ee494d035a58fb407a8769c7ef7e9d723c1d14
6
+ metadata.gz: 99cd7c6e59ccd6b64667ab7a179c240d96577f24296a93b801484f994570d6262ccc30d1535853a6cd7b4fb2a31f5e8145fa227aa637d7d9960f0aa794756a67
7
+ data.tar.gz: 13a319d2b312f709a5937711bb65d69a560e098bed809e3f5f76fc83bae419983f5f4dbb0ae541e5afa3c93b9a7fd290b9c963378b6f405cd4bb0ceb3900e274
@@ -14,37 +14,39 @@ module Wip
14
14
  @ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
15
15
  end
16
16
 
17
- def stage
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.
20
+ def stage(on_progress: nil)
18
21
  return yield @root.to_s if @ignore.empty?
19
22
 
20
23
  Dir.mktmpdir('wip-build-context-') do |dir|
21
- copy_included_files(Pathname(dir))
24
+ copy_included_files(Pathname(dir), on_progress)
22
25
  yield dir
23
26
  end
24
27
  end
25
28
 
26
29
  private
27
30
 
28
- def copy_included_files(destination)
29
- each_included_file do |relative_path|
31
+ def copy_included_files(destination, on_progress)
32
+ files = included_files
33
+ files.each_with_index do |relative_path, index|
30
34
  target = destination.join(relative_path)
31
35
  FileUtils.mkdir_p(target.dirname)
32
36
  # Keep links as links. Dereferencing a link here could copy arbitrary
33
37
  # host files outside the build context (for example, ~/.ssh/id_rsa)
34
38
  # into the staged directory and expose them to the image build.
35
39
  FileUtils.copy_entry(@root.join(relative_path), target, false, false, false)
40
+ on_progress&.call(index + 1, files.size)
36
41
  end
37
42
  end
38
43
 
39
- def each_included_file
40
- Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).each do |entry|
41
- next if %w[. ..].include?(entry)
44
+ def included_files
45
+ Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).select do |entry|
46
+ next false if %w[. ..].include?(entry)
42
47
 
43
48
  path = @root.join(entry)
44
- next unless path.file? || path.symlink?
45
- next if @ignore.ignored?(entry)
46
-
47
- yield entry
49
+ (path.file? || path.symlink?) && !@ignore.ignored?(entry)
48
50
  end
49
51
  end
50
52
  end
data/lib/wip/cli.rb CHANGED
@@ -70,9 +70,14 @@ module Wip
70
70
  extra.shift if extra.first == '--'
71
71
  settings = load_config.command('build') || {}
72
72
  context = settings['context'] || '.'
73
- BuildContext.new(context).stage do |staged_context|
73
+ warn "wip: staging build context (#{context})"
74
+ progress = StagingProgress.new
75
+ BuildContext.new(context).stage(on_progress: progress.method(:tick)) do |staged_context|
76
+ progress.finish
74
77
  execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
75
78
  end
79
+ ensure
80
+ progress&.finish
76
81
  end
77
82
 
78
83
  desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
@@ -188,7 +193,11 @@ module Wip
188
193
 
189
194
  private
190
195
 
191
- def loader = ConfigLoader.new(path: options[:config])
196
+ def loader
197
+ env_file = options[:env_file] && Pathname(options[:env_file]).expand_path
198
+ ConfigLoader.new(path: options[:config], env_file: env_file)
199
+ end
200
+
192
201
  def load_config = (@load_config ||= loader.load)
193
202
  def resolver = CommandResolver.new
194
203
 
@@ -30,8 +30,11 @@ module Wip
30
30
  BUILD_KEYS = %w[context dockerfile args].freeze
31
31
  SUPPORTED_CONDITIONS = %w[service_started].freeze
32
32
 
33
- def self.load(path)
34
- raw = YAML.safe_load_file(path, aliases: true)
33
+ # env interpolates compose.yml's ${VAR} references the way `docker compose` does,
34
+ # so values like `user: ${USER_ID}:${GROUP_ID}` reach wslc already substituted
35
+ # instead of literally (see Config#compose_interpolation_env for what's passed in).
36
+ def self.load(path, env: {})
37
+ raw = VariableInterpolation.tree(YAML.safe_load_file(path, aliases: true), env)
35
38
  raise ConfigError, "#{path}: services: must be a mapping" unless raw.is_a?(Hash) && raw['services'].is_a?(Hash)
36
39
 
37
40
  new(raw['services'], path: path)
data/lib/wip/config.rb CHANGED
@@ -19,9 +19,10 @@ module Wip
19
19
  MODES = %w[container compose compose-native].freeze
20
20
  attr_reader :path
21
21
 
22
- def initialize(raw, path = nil)
22
+ def initialize(raw, path = nil, env_file = nil)
23
23
  @raw = stringify(raw)
24
24
  @path = path
25
+ @env_file = env_file
25
26
  validate!
26
27
  end
27
28
 
@@ -202,7 +203,20 @@ module Wip
202
203
  def presence(value) = value.to_s.empty? ? nil : value.to_s
203
204
 
204
205
  def parsed_compose_file
205
- @parsed_compose_file ||= ComposeFile.load(ComposeBridge.file_path(self))
206
+ @parsed_compose_file ||= ComposeFile.load(ComposeBridge.file_path(self), env: compose_interpolation_env)
207
+ end
208
+
209
+ # Compose interpolates ${VAR} references in compose.yml from the shell environment
210
+ # and a project .env file, shell values winning on conflict — mirror that here so
211
+ # `wip` doesn't need the value duplicated into wip.yml just to resolve a compose.yml
212
+ # reference. Uses the same dotenv file --env-file/DotenvLoader would (explicit
213
+ # override, else .env next to wip.yml), so compose interpolation and the env
214
+ # actually passed to containers never see two different .env files.
215
+ def compose_interpolation_env
216
+ env_file = @env_file || (path && Pathname(path).dirname.join('.env'))
217
+ return ENV.to_h unless env_file
218
+
219
+ DotenvLoader.new(env_file).load.merge(ENV.to_h)
206
220
  end
207
221
 
208
222
  def default_compose_network = path && Pathname(path).dirname.basename.to_s
@@ -8,9 +8,10 @@ module Wip
8
8
  class ConfigLoader
9
9
  FILENAME = 'wip.yml'
10
10
 
11
- def initialize(start_dir: Dir.pwd, path: nil)
11
+ def initialize(start_dir: Dir.pwd, path: nil, env_file: nil)
12
12
  @start_dir = Pathname(start_dir).expand_path
13
13
  @path = path
14
+ @env_file = env_file
14
15
  end
15
16
 
16
17
  def find
@@ -28,7 +29,7 @@ module Wip
28
29
  raise ConfigError, "wip.yml was not found (searched from #{@start_dir} to the filesystem root)" unless path&.file?
29
30
 
30
31
  data = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
31
- Config.new(data, path)
32
+ Config.new(data, path, @env_file)
32
33
  rescue Psych::Exception => e
33
34
  raise ConfigError, "Could not parse #{path}: #{e.message}"
34
35
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
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.
8
+ class StagingProgress
9
+ def initialize(out: $stderr, interval: 0.5)
10
+ @out = out
11
+ @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
16
+ end
17
+
18
+ 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}"
25
+ end
26
+
27
+ # Idempotent: a caller can't know in advance whether tick ever fired.
28
+ def finish
29
+ return unless @printed
30
+
31
+ @printed = false
32
+ @out.puts
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Interpolates ${VAR} references the way `docker compose` does when reading
5
+ # compose.yml: ${VAR}, ${VAR:-default}/${VAR-default}, bare $VAR, and $$ as an
6
+ # escaped literal dollar sign. ${VAR:?err}/${VAR:+alt} aren't recognized by
7
+ # PATTERN at all, so — unlike an unset $VAR/${VAR}, which this resolves to an
8
+ # empty string — they pass through completely unchanged, "${...}" and all.
9
+ module VariableInterpolation
10
+ PATTERN = /\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)((:-|-)([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/
11
+
12
+ def self.call(text, env)
13
+ text.gsub(PATTERN) do |matched|
14
+ next '$' if matched == '$$'
15
+
16
+ name, _, operator, default, bare_name = Regexp.last_match.captures
17
+ resolve(env[name || bare_name], operator, default)
18
+ end
19
+ end
20
+
21
+ # Walks an already-parsed YAML structure and interpolates string values only —
22
+ # real Compose interpolates YAML values, never mapping keys (its docs call this
23
+ # out explicitly), and doing this after parsing means a substituted value can't
24
+ # introduce YAML syntax (e.g. a literal "#" turning into a comment marker).
25
+ #
26
+ # `seen` tracks Hash/Array objects on the current recursion path (by identity,
27
+ # not #==) so a self-referential YAML alias — ComposeFile.load parses with
28
+ # aliases: true — raises instead of recursing until SystemStackError. The same
29
+ # object reached again from a *different* branch (an anchor reused, not a
30
+ # cycle) is fine: it's removed from `seen` once its own subtree finishes.
31
+ def self.tree(value, env, seen = {}.compare_by_identity)
32
+ case value
33
+ when String then call(value, env)
34
+ when Hash, Array then walk_container(value, env, seen)
35
+ else value
36
+ end
37
+ end
38
+
39
+ def self.walk_container(value, env, seen)
40
+ raise ConfigError, 'compose.yml contains a self-referential YAML alias' if seen.key?(value)
41
+
42
+ seen[value] = true
43
+ begin
44
+ value.is_a?(Hash) ? value.transform_values { |v| tree(v, env, seen) } : value.map { |v| tree(v, env, seen) }
45
+ ensure
46
+ seen.delete(value)
47
+ end
48
+ end
49
+ private_class_method :walk_container
50
+
51
+ def self.resolve(value, operator, default)
52
+ case operator
53
+ when ':-' then value.nil? || value.empty? ? default.to_s : value
54
+ when '-' then value.nil? ? default.to_s : value
55
+ else value.to_s
56
+ end
57
+ end
58
+ private_class_method :resolve
59
+ end
60
+ 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.1'
4
+ VERSION = '0.17.3'
5
5
  end
data/lib/wip.rb CHANGED
@@ -9,10 +9,12 @@ require_relative 'wip/environment'
9
9
  require_relative 'wip/dotenv_loader'
10
10
  require_relative 'wip/docker_ignore'
11
11
  require_relative 'wip/build_context'
12
+ require_relative 'wip/staging_progress'
12
13
  require_relative 'wip/command_resolver'
13
14
  require_relative 'wip/error_interpreter'
14
15
  require_relative 'wip/command_builder'
15
16
  require_relative 'wip/compose_bridge'
17
+ require_relative 'wip/variable_interpolation'
16
18
  require_relative 'wip/compose_file'
17
19
  require_relative 'wip/initializer'
18
20
  require_relative 'wip/command_display'
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.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -52,7 +52,9 @@ files:
52
52
  - lib/wip/errors.rb
53
53
  - lib/wip/initializer.rb
54
54
  - lib/wip/resource_monitor.rb
55
+ - lib/wip/staging_progress.rb
55
56
  - lib/wip/sync_settings.rb
57
+ - lib/wip/variable_interpolation.rb
56
58
  - lib/wip/version.rb
57
59
  homepage: https://wslc-wip.slidict.com/
58
60
  licenses: