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 +4 -4
- data/lib/wip/build_context.rb +13 -11
- data/lib/wip/cli.rb +11 -2
- data/lib/wip/compose_file.rb +5 -2
- data/lib/wip/config.rb +16 -2
- data/lib/wip/config_loader.rb +3 -2
- data/lib/wip/staging_progress.rb +35 -0
- data/lib/wip/variable_interpolation.rb +60 -0
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c00368614ae5c01c7f514957368d9533f2faf9be114cfd17c676fc70ebfec138
|
|
4
|
+
data.tar.gz: 86d094ead585366c91633a963d0436a0c68af10f5bda3dace5ab46485f714a4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99cd7c6e59ccd6b64667ab7a179c240d96577f24296a93b801484f994570d6262ccc30d1535853a6cd7b4fb2a31f5e8145fa227aa637d7d9960f0aa794756a67
|
|
7
|
+
data.tar.gz: 13a319d2b312f709a5937711bb65d69a560e098bed809e3f5f76fc83bae419983f5f4dbb0ae541e5afa3c93b9a7fd290b9c963378b6f405cd4bb0ceb3900e274
|
data/lib/wip/build_context.rb
CHANGED
|
@@ -14,37 +14,39 @@ module Wip
|
|
|
14
14
|
@ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
|
|
15
15
|
end
|
|
16
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.
|
|
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
|
-
|
|
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
|
|
40
|
-
Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
data/lib/wip/compose_file.rb
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
data/lib/wip/config_loader.rb
CHANGED
|
@@ -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
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.
|
|
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:
|