wslc-wip 0.4.3 → 0.5.0

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: be995c6781c440a7fd84dd3c3bbbddd6799154b59694edce5aa653d80c7d6e53
4
- data.tar.gz: 7fb1638a125b2bce92212e8ff525a38e31e2d8aa4386e3e74e379420d4df9c91
3
+ metadata.gz: '0845dfb67f17323b8a99e6f2f92ef06c17bc03fd1316680cf5f43f2640d73da3'
4
+ data.tar.gz: 6eea9bb520aed0fff5b401b71a20dccdade6b50d9a71403037e59e4e5b83ab2e
5
5
  SHA512:
6
- metadata.gz: 539721780baa7f366f215581629a761c85e396823e05ff08a3da97c2d0de7808d1470c959240867a5a0ab762f4441ea8b2cb22c92f0063712a18c6b0f80dc4fe
7
- data.tar.gz: fc00ceceeeb01cd63ac931ff2ffeea1f64848f38a1222b60b8c5de7a501e914a3820d61abbd7d437ebd412e2046958223edab7b46d582b5cb6ef693badf5cc3e
6
+ metadata.gz: 1b522d206ba3070244f51bda9d4e144dfe95df63ab01bb92b35d0fe0e84a29035c997d7b061c2ec23286c8bd9f586c69e276b7304abd1c8ad81fe808293599b5
7
+ data.tar.gz: d7fc237820edfcc734f55775e027133c5f9877c24b16f5b1ca6aa98304b35fdea5825a179ff7f64b4d22fe2634d52d11761777284b68a86e76faa56432d33230
data/README.md CHANGED
@@ -103,6 +103,22 @@ commands:
103
103
  credential, or auth. Keep real secrets out of the config file and in your runtime environment
104
104
  instead.
105
105
 
106
+ ### .env
107
+
108
+ Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
109
+ per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
110
+ passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
111
+ custom commands. `.env` only fills in keys that aren't already set by `defaults.env` or a
112
+ command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH` to load a
113
+ different file instead.
114
+
115
+ ### .dockerignore
116
+
117
+ `wip build` reads `.dockerignore` from the build context and stages a filtered copy of the
118
+ context (skipping anything it matches) before handing it to `wslc build`, since `wslc` sends the
119
+ context as-is otherwise. If there's no `.dockerignore`, the original context directory is used
120
+ directly with no copying.
121
+
106
122
  ### Dependency containers
107
123
 
108
124
  If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+ require 'tmpdir'
6
+
7
+ module Wip
8
+ # Stages a build context into a scratch directory with anything matched by
9
+ # .dockerignore left out, since wslc build (unlike `docker build`) sends the
10
+ # context as-is instead of filtering it itself.
11
+ class BuildContext
12
+ def initialize(context, ignore: nil)
13
+ @root = Pathname(context).expand_path
14
+ @ignore = ignore || DockerIgnore.load(@root.join('.dockerignore'))
15
+ end
16
+
17
+ def stage
18
+ return yield @root.to_s if @ignore.empty?
19
+
20
+ Dir.mktmpdir('wip-build-context-') do |dir|
21
+ copy_included_files(Pathname(dir))
22
+ yield dir
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def copy_included_files(destination)
29
+ each_included_file do |relative_path|
30
+ target = destination.join(relative_path)
31
+ FileUtils.mkdir_p(target.dirname)
32
+ FileUtils.cp(@root.join(relative_path), target)
33
+ end
34
+ end
35
+
36
+ def each_included_file
37
+ Dir.glob('**/*', File::FNM_DOTMATCH, base: @root.to_s).each do |entry|
38
+ next if %w[. ..].include?(entry)
39
+
40
+ path = @root.join(entry)
41
+ next unless path.file?
42
+ next if @ignore.ignored?(entry)
43
+
44
+ yield entry
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/wip/cli.rb CHANGED
@@ -9,6 +9,7 @@ module Wip
9
9
  # Thor-based command-line interface for wip.
10
10
  class CLI < Thor
11
11
  class_option :config, type: :string, desc: 'Path to wip.yml'
12
+ class_option :env_file, type: :string, desc: 'Path to a dotenv file (default: .env next to wip.yml)'
12
13
  class_option :debug, type: :boolean, default: false, desc: 'Print progress and timing for each step'
13
14
  class_option :debug_log, type: :string, desc: 'Where --debug snapshots go: a file path, or "-" for inline'
14
15
  default_task :dispatch
@@ -52,7 +53,11 @@ module Wip
52
53
  desc 'build [OPTIONS]', 'Build the configured image'
53
54
  def build(*extra)
54
55
  extra.shift if extra.first == '--'
55
- execute(builder.build(settings: load_config.command('build') || {}, extra: extra))
56
+ settings = load_config.command('build') || {}
57
+ context = settings['context'] || load_config.defaults['context'] || '.'
58
+ BuildContext.new(context).stage do |staged_context|
59
+ execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
60
+ end
56
61
  end
57
62
 
58
63
  desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
@@ -118,7 +123,16 @@ module Wip
118
123
  def loader = ConfigLoader.new(path: options[:config])
119
124
  def load_config = (@load_config ||= loader.load)
120
125
  def resolver = CommandResolver.new
121
- def builder = CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config)
126
+
127
+ def dotenv_path
128
+ options[:env_file] ? Pathname(options[:env_file]).expand_path : Pathname(load_config.path).dirname.join('.env')
129
+ end
130
+
131
+ def dotenv = @dotenv ||= DotenvLoader.new(dotenv_path).load
132
+
133
+ def builder
134
+ CommandBuilder.new(wslc: resolver.resolve(load_config.wslc_command), config: load_config, dotenv: dotenv)
135
+ end
122
136
 
123
137
  def execute(command, interactive: false, exit_on_failure: true)
124
138
  runner = CommandRunner.new(debug: debug?)
@@ -5,10 +5,11 @@ require 'shellwords'
5
5
  module Wip
6
6
  # Builds the argument arrays for wslc build/exec/run/custom invocations.
7
7
  class CommandBuilder
8
- def initialize(wslc:, config:, environment: Environment.new)
8
+ def initialize(wslc:, config:, environment: Environment.new, dotenv: {})
9
9
  @wslc = wslc
10
10
  @config = config
11
11
  @environment = environment
12
+ @dotenv = dotenv
12
13
  end
13
14
 
14
15
  def exec(arguments, settings: {}, interactive: true)
@@ -115,7 +116,7 @@ module Wip
115
116
  def options(values, include_container: false, include_publish: true)
116
117
  result = []
117
118
  result.push('-w', values['workdir']) unless values['workdir'].to_s.empty?
118
- values.fetch('env', {}).each { |key, value| result.push('-e', "#{key}=#{value}") }
119
+ merged_env(values).each { |key, value| result.push('-e', "#{key}=#{value}") }
119
120
  if include_publish
120
121
  Array(values['ports']).each { |port| result.push('-p', port.to_s) }
121
122
  Array(values['volumes']).each { |volume| result.push('-v', volume.to_s) }
@@ -124,6 +125,9 @@ module Wip
124
125
  result
125
126
  end
126
127
 
128
+ # .env supplies defaults; env set in wip.yml (defaults or per-command) wins on conflict.
129
+ def merged_env(values) = @dotenv.merge(values.fetch('env', {}))
130
+
127
131
  def required(values, key)
128
132
  value = values[key]
129
133
  raise ConfigError, "Configured #{key} must not be empty" if value.to_s.empty?
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Wip
6
+ # Parses a .dockerignore file and decides whether a build-context-relative
7
+ # path should be excluded, following the same pattern rules as the Docker
8
+ # CLI (gitignore-like globs, later rules override earlier ones, `!` negates).
9
+ class DockerIgnore
10
+ Rule = Struct.new(:pattern, :negate)
11
+
12
+ FNMATCH_FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
13
+
14
+ def self.load(path)
15
+ path = Pathname(path)
16
+ return new([]) unless path.file?
17
+
18
+ new(path.readlines)
19
+ end
20
+
21
+ def initialize(lines)
22
+ @rules = lines.filter_map { |line| parse(line) }
23
+ end
24
+
25
+ def empty? = @rules.empty?
26
+
27
+ def ignored?(relative_path)
28
+ @rules.reduce(false) do |ignored, rule|
29
+ matches?(rule.pattern, relative_path) ? !rule.negate : ignored
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def parse(line)
36
+ line = line.strip
37
+ return nil if line.empty? || line.start_with?('#')
38
+
39
+ negate = line.start_with?('!')
40
+ pattern = negate ? line[1..] : line
41
+ pattern = pattern.delete_suffix('/')
42
+ anchored = pattern.start_with?('/')
43
+ pattern = pattern.delete_prefix('/')
44
+ pattern = "**/#{pattern}" if !anchored && !pattern.include?('/')
45
+ Rule.new(pattern, negate)
46
+ end
47
+
48
+ # A match on a directory component also excludes everything under it,
49
+ # so every prefix of the path (not just the full path) is tested.
50
+ def matches?(pattern, relative_path)
51
+ components = relative_path.split('/')
52
+ (1..components.size).any? do |i|
53
+ File.fnmatch(pattern, components[0...i].join('/'), FNMATCH_FLAGS)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Wip
6
+ # Parses a .env file the way `docker compose` does, so values don't have to
7
+ # be duplicated into wip.yml just to reach the container as -e flags.
8
+ class DotenvLoader
9
+ LINE_PATTERN = /\A(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)\z/
10
+
11
+ def initialize(path)
12
+ @path = Pathname(path)
13
+ end
14
+
15
+ def load
16
+ return {} unless @path.file?
17
+
18
+ @path.readlines.each_with_object({}) do |line, env|
19
+ line = line.strip
20
+ next if line.empty? || line.start_with?('#')
21
+
22
+ match = LINE_PATTERN.match(line)
23
+ next unless match
24
+
25
+ env[match[1]] = unquote(match[2])
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def unquote(value)
32
+ value = value.strip
33
+ return value[1..-2] if value.start_with?('"') && value.end_with?('"') && value.length >= 2
34
+ return value[1..-2] if value.start_with?("'") && value.end_with?("'") && value.length >= 2
35
+
36
+ value.split(/\s+#/, 2).first.to_s.strip
37
+ end
38
+ end
39
+ 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.4.3'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/wip.rb CHANGED
@@ -5,6 +5,9 @@ require_relative 'wip/errors'
5
5
  require_relative 'wip/config'
6
6
  require_relative 'wip/config_loader'
7
7
  require_relative 'wip/environment'
8
+ require_relative 'wip/dotenv_loader'
9
+ require_relative 'wip/docker_ignore'
10
+ require_relative 'wip/build_context'
8
11
  require_relative 'wip/command_resolver'
9
12
  require_relative 'wip/error_interpreter'
10
13
  require_relative 'wip/command_builder'
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.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -33,6 +33,7 @@ files:
33
33
  - README.md
34
34
  - exe/wip
35
35
  - lib/wip.rb
36
+ - lib/wip/build_context.rb
36
37
  - lib/wip/cli.rb
37
38
  - lib/wip/command_builder.rb
38
39
  - lib/wip/command_display.rb
@@ -41,7 +42,9 @@ files:
41
42
  - lib/wip/config.rb
42
43
  - lib/wip/config_loader.rb
43
44
  - lib/wip/debug_reporter.rb
45
+ - lib/wip/docker_ignore.rb
44
46
  - lib/wip/doctor.rb
47
+ - lib/wip/dotenv_loader.rb
45
48
  - lib/wip/environment.rb
46
49
  - lib/wip/error_interpreter.rb
47
50
  - lib/wip/errors.rb