wslc-wip 0.17.0 → 0.17.2

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: 0b6d4bd2dc0f6ceb899efeaebe7378c6b043915b2d640bf4efff7501f5ca4852
4
- data.tar.gz: e5d6c0ed4b89bacaa374482bc7e815a5b04883b4d817809b6dbb0a91c9801ae3
3
+ metadata.gz: ffab59f2ac2df1c7a566825b0ee97eeff0b515396b42f3b95b1067b361d3aa8c
4
+ data.tar.gz: 9500d0b3842aa86f2aaabdc41b89986cad0c66fa0b765b130c6b37679212b985
5
5
  SHA512:
6
- metadata.gz: aed82d2db66e8036c55986181ce73fc032a1785c3ac3a314aadcc2ef7a8b91f6280dc31cbc209aa97dd25ad5eed536013954ca593b61ed966792004209ebcb2f
7
- data.tar.gz: 94ee0961eddd267ca8fc8e8f1e198dbd4793801b15c2512539ff55c6cb5223c5ae7e331807030f7eca8d5264b38527db6f4e993ae61fb14d4ef6bdbf26eaf1ad
6
+ metadata.gz: 96991898c1e0b423ad14151ee6b7db24bc38c8ea84b7f5213c69d14d83786288faa5b2d1f71c94382611c0f4c569aa939333e66a3e050095b45a1d50b6ef4bb6
7
+ data.tar.gz: 963d62e619fab36835541421ce7f1642a98e063ba916a0c466311d6138ddce53bf6cae3660dbdadd779272defe81898fa153d9be17a661c11be4c93bff8179e5
data/lib/wip/cli.rb CHANGED
@@ -70,6 +70,7 @@ module Wip
70
70
  extra.shift if extra.first == '--'
71
71
  settings = load_config.command('build') || {}
72
72
  context = settings['context'] || '.'
73
+ warn "wip: staging build context (#{context})"
73
74
  BuildContext.new(context).stage do |staged_context|
74
75
  execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
75
76
  end
@@ -188,7 +189,11 @@ module Wip
188
189
 
189
190
  private
190
191
 
191
- def loader = ConfigLoader.new(path: options[:config])
192
+ def loader
193
+ env_file = options[:env_file] && Pathname(options[:env_file]).expand_path
194
+ ConfigLoader.new(path: options[:config], env_file: env_file)
195
+ end
196
+
192
197
  def load_config = (@load_config ||= loader.load)
193
198
  def resolver = CommandResolver.new
194
199
 
@@ -26,8 +26,7 @@ module Wip
26
26
  threads.each(&:join)
27
27
  status = wait.value
28
28
  end
29
- hint = @interpreter.interpret(captured)
30
- @stderr.puts("\n#{hint}") if !status.success? && hint
29
+ report_hint(captured) unless status.success?
31
30
  status.exitstatus
32
31
  rescue Errno::ENOENT => e
33
32
  @stderr.puts e.message
@@ -39,6 +38,11 @@ module Wip
39
38
 
40
39
  private
41
40
 
41
+ def report_hint(captured)
42
+ hint = @interpreter.interpret(captured.force_encoding(Encoding::UTF_8).scrub)
43
+ @stderr.puts("\n#{hint}") if hint
44
+ end
45
+
42
46
  # Piping stdin/stdout/stderr (as `run` does above) closes the child's
43
47
  # stdin immediately, which breaks anything that reads from the terminal
44
48
  # (a shell, `rails console`, ...). Inherit the real file descriptors
@@ -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
@@ -122,23 +122,7 @@ module Wip
122
122
  # optional; exec (default, mirrors into the running container) | run (always a fresh container)
123
123
  mode: exec
124
124
 
125
- # image/build below always cover `wip up`'s pre-boot mirror (a throwaway container, since
126
- # the primary one isn't running yet — falls back to the primary image if neither is set).
127
- # With mode: exec above (the default here), `wip sync`/`wip sync --watch` afterward run
128
- # rsync inside the already-running primary container instead, so its own image needs
129
- # rsync installed too — image/build below aren't read for those.
130
-
131
- # image the pre-boot (and, under sync.mode: run, every) mirror container runs
132
- # image: your/image:tag
133
-
134
- # alternative to image — let wip build a small dedicated mirror image itself
135
- # build:
136
- # dockerfile: |
137
- # FROM alpine:latest
138
- # RUN apk add --no-cache rsync
139
-
140
- # optional (default: wip-sync-<container>:latest)
141
- # tag: wip-sync-app:latest
125
+ # sync.image/sync.build required for mode: compose and used by sync.mode: run; see README "Source sync"
142
126
  YAML
143
127
  end
144
128
 
@@ -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.0'
4
+ VERSION = '0.17.2'
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/variable_interpolation'
16
17
  require_relative 'wip/compose_file'
17
18
  require_relative 'wip/initializer'
18
19
  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.0
4
+ version: 0.17.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -53,6 +53,7 @@ files:
53
53
  - lib/wip/initializer.rb
54
54
  - lib/wip/resource_monitor.rb
55
55
  - lib/wip/sync_settings.rb
56
+ - lib/wip/variable_interpolation.rb
56
57
  - lib/wip/version.rb
57
58
  homepage: https://wslc-wip.slidict.com/
58
59
  licenses: