boringbuilder 0.1.0.alpha.1
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 +7 -0
- data/AGENTS.md +38 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +131 -0
- data/SECURITY.md +25 -0
- data/SOUL.md +40 -0
- data/STYLE.md +33 -0
- data/docs/artifacts.md +106 -0
- data/docs/building.md +175 -0
- data/docs/caching.md +53 -0
- data/exe/boringbuilder +6 -0
- data/lib/boringbuilder/artifact.rb +87 -0
- data/lib/boringbuilder/boring_cache.rb +126 -0
- data/lib/boringbuilder/build_progress.rb +90 -0
- data/lib/boringbuilder/builder.rb +33 -0
- data/lib/boringbuilder/cli.rb +234 -0
- data/lib/boringbuilder/config_file.rb +40 -0
- data/lib/boringbuilder/configuration.rb +165 -0
- data/lib/boringbuilder/errors.rb +9 -0
- data/lib/boringbuilder/exporter.rb +144 -0
- data/lib/boringbuilder/exporters/asset.rb +7 -0
- data/lib/boringbuilder/exporters/boring_cache.rb +83 -0
- data/lib/boringbuilder/exporters/local.rb +27 -0
- data/lib/boringbuilder/exporters/receipt.rb +11 -0
- data/lib/boringbuilder/exporters/resolver.rb +57 -0
- data/lib/boringbuilder/exporters.rb +7 -0
- data/lib/boringbuilder/initializer.rb +180 -0
- data/lib/boringbuilder/mise.rb +88 -0
- data/lib/boringbuilder/pipeline.rb +156 -0
- data/lib/boringbuilder/project.rb +264 -0
- data/lib/boringbuilder/project_plan.rb +78 -0
- data/lib/boringbuilder/rails_build.rb +6 -0
- data/lib/boringbuilder/railtie.rb +9 -0
- data/lib/boringbuilder/result.rb +33 -0
- data/lib/boringbuilder/ruby_application.rb +142 -0
- data/lib/boringbuilder/ruby_build.rb +212 -0
- data/lib/boringbuilder/runtime.rb +89 -0
- data/lib/boringbuilder/tasks/boringbuilder.rake +40 -0
- data/lib/boringbuilder/version.rb +5 -0
- data/lib/boringbuilder.rb +45 -0
- metadata +108 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
class RubyApplication
|
|
5
|
+
attr_reader :root, :configuration
|
|
6
|
+
|
|
7
|
+
def initialize(root, configuration)
|
|
8
|
+
@root = root
|
|
9
|
+
@configuration = configuration
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ruby?
|
|
13
|
+
root.join("Gemfile").file?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rails?
|
|
17
|
+
ruby? && root.join("config/application.rb").file?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def hanami?
|
|
21
|
+
ruby? && root.join("config/app.rb").file?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def framework
|
|
25
|
+
return unless ruby?
|
|
26
|
+
return :rails if rails?
|
|
27
|
+
return :hanami if hanami?
|
|
28
|
+
return :rack if rack?
|
|
29
|
+
|
|
30
|
+
:ruby
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def path
|
|
34
|
+
rails? ? "/rails" : "/app"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user
|
|
38
|
+
rails? ? "rails" : "app"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def runtime_environment
|
|
42
|
+
environment = {
|
|
43
|
+
"PORT" => port.to_s,
|
|
44
|
+
"RACK_ENV" => "production"
|
|
45
|
+
}
|
|
46
|
+
environment.merge!(rails_environment) if rails?
|
|
47
|
+
environment.merge!("HANAMI_ENV" => "production", "HANAMI_PORT" => port.to_s) if hanami?
|
|
48
|
+
environment
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def runtime_command
|
|
52
|
+
configuration.command ? explicit_command : detected_command
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def runtime_entrypoint
|
|
56
|
+
return [] if configuration.command || procfile_command
|
|
57
|
+
return %w[/rails/bin/docker-entrypoint] if rails? && docker_entrypoint?
|
|
58
|
+
return %w[/rails/bin/rails] if rails?
|
|
59
|
+
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def runtime_port
|
|
64
|
+
port
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def web?
|
|
68
|
+
rails? || hanami? || rack? || !procfile_command.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def detected_command
|
|
74
|
+
return procfile_command if procfile_command
|
|
75
|
+
return rails_command if rails?
|
|
76
|
+
return hanami_command if hanami?
|
|
77
|
+
return rack_command if rack?
|
|
78
|
+
return %w[bundle exec rake] if root.join("Rakefile").file?
|
|
79
|
+
|
|
80
|
+
[]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def explicit_command
|
|
84
|
+
command = configuration.command
|
|
85
|
+
return command.map(&:to_s) if command.is_a?(Array)
|
|
86
|
+
|
|
87
|
+
["/bin/sh", "-lc", command.to_s]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rails_command
|
|
91
|
+
return %w[./bin/thrust ./bin/rails server] if thrust?
|
|
92
|
+
|
|
93
|
+
docker_entrypoint? ? %w[./bin/rails server] : %w[server]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def hanami_command
|
|
97
|
+
return %w[bundle exec puma -C config/puma.rb] if root.join("config/puma.rb").file?
|
|
98
|
+
return rack_command if rack?
|
|
99
|
+
|
|
100
|
+
[]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def rack_command
|
|
104
|
+
["bundle", "exec", "rackup", "config.ru", "--host", "0.0.0.0", "--port", port.to_s]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def procfile_command
|
|
108
|
+
return @procfile_command if defined?(@procfile_command)
|
|
109
|
+
|
|
110
|
+
path = root.join("Procfile")
|
|
111
|
+
@procfile_command = if path.file?
|
|
112
|
+
command = path.each_line.filter_map { |line| line[/\Aweb:\s*(.+)\s*\z/, 1] }.first
|
|
113
|
+
["/bin/sh", "-lc", command] if command
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rack?
|
|
118
|
+
root.join("config.ru").file?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def docker_entrypoint?
|
|
122
|
+
root.join("bin/docker-entrypoint").file?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def thrust?
|
|
126
|
+
root.join("bin/thrust").file?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def port
|
|
130
|
+
configuration.port || (rails? && thrust? ? 80 : 3000)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def rails_environment
|
|
134
|
+
{
|
|
135
|
+
"NODE_ENV" => "production",
|
|
136
|
+
"RAILS_ENV" => "production",
|
|
137
|
+
"RAILS_LOG_TO_STDOUT" => "true",
|
|
138
|
+
"RAILS_SERVE_STATIC_FILES" => "true"
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
class RubyBuild
|
|
5
|
+
DEFAULT_BUILD_IMAGE = Mise::DEFAULT_IMAGE
|
|
6
|
+
DEFAULT_RUNTIME_IMAGE = BoringCache::CLI_IMAGE
|
|
7
|
+
BUILD_PACKAGES = %w[
|
|
8
|
+
build-essential
|
|
9
|
+
curl
|
|
10
|
+
git
|
|
11
|
+
libpq-dev
|
|
12
|
+
libyaml-dev
|
|
13
|
+
pkg-config
|
|
14
|
+
].freeze
|
|
15
|
+
RUNTIME_PACKAGES = %w[
|
|
16
|
+
curl
|
|
17
|
+
libpq5
|
|
18
|
+
].freeze
|
|
19
|
+
BUNDLE_ENVIRONMENT = {
|
|
20
|
+
"BUNDLE_CLEAN" => "true",
|
|
21
|
+
"BUNDLE_DEPLOYMENT" => "1",
|
|
22
|
+
"BUNDLE_IGNORE_CONFIG" => "true",
|
|
23
|
+
"BUNDLE_JOBS" => "1",
|
|
24
|
+
"BUNDLE_PATH" => "/usr/local/bundle",
|
|
25
|
+
"BUNDLE_WITHOUT" => "development:test"
|
|
26
|
+
}.freeze
|
|
27
|
+
BUNDLE_INSTALL_COMMAND = ["sh", "-c", "bundle install && bundle clean --force"].freeze
|
|
28
|
+
BUNDLE_FILES = %w[Gemfile Gemfile.lock .ruby-version].freeze
|
|
29
|
+
BUNDLE_CONFIG = <<~YAML
|
|
30
|
+
---
|
|
31
|
+
BUNDLE_PATH: "vendor/bundle"
|
|
32
|
+
BUNDLE_WITHOUT: "development:test"
|
|
33
|
+
BUNDLE_JOBS: "1"
|
|
34
|
+
BUNDLE_CLEAN: "true"
|
|
35
|
+
YAML
|
|
36
|
+
|
|
37
|
+
attr_reader :project, :client, :progress
|
|
38
|
+
|
|
39
|
+
def initialize(project, client, progress: BuildProgress.silent)
|
|
40
|
+
@project = project
|
|
41
|
+
@client = client
|
|
42
|
+
@progress = progress
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def container
|
|
46
|
+
builder = with_environment(build_image_container, build_environment)
|
|
47
|
+
builder = install_packages(builder, (BUILD_PACKAGES + configuration.build_packages).uniq)
|
|
48
|
+
builder = create_application_user(builder)
|
|
49
|
+
builder = install_toolchain(builder)
|
|
50
|
+
builder = install_gems(builder)
|
|
51
|
+
builder = builder.with_directory(application_path, project.source(client), owner: user_owner)
|
|
52
|
+
builder = builder.with_workdir(application_path).with_exec(%w[bundle check])
|
|
53
|
+
builder = precompile(builder)
|
|
54
|
+
builder = write_bundle_config(builder)
|
|
55
|
+
builder = progress.step("Build #{project.framework.to_s.capitalize} application") { builder.sync }
|
|
56
|
+
|
|
57
|
+
runtime_container(builder)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def configuration
|
|
63
|
+
project.configuration
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def application_path
|
|
67
|
+
project.application_path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def user_owner
|
|
71
|
+
"#{project.application_user}:#{project.application_user}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def image_options
|
|
75
|
+
configuration.platform ? { platform: configuration.platform } : {}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_image_container
|
|
79
|
+
address = configuration.base_image || DEFAULT_BUILD_IMAGE
|
|
80
|
+
client.container(image_options).from(address)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def runtime_image_container
|
|
84
|
+
address = configuration.base_image || DEFAULT_RUNTIME_IMAGE
|
|
85
|
+
client.container(image_options).from(address)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_environment
|
|
89
|
+
BUNDLE_ENVIRONMENT
|
|
90
|
+
.merge(project.runtime_environment)
|
|
91
|
+
.merge("SECRET_KEY_BASE_DUMMY" => "1")
|
|
92
|
+
.merge(configuration.build_environment)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def runtime_environment
|
|
96
|
+
BUNDLE_ENVIRONMENT.merge(project.runtime_environment)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def with_environment(container, environment)
|
|
100
|
+
environment.reduce(container) do |result, (name, value)|
|
|
101
|
+
result.with_env_variable(name, value)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def install_toolchain(container)
|
|
106
|
+
toolchain = Mise.new(project, pipeline)
|
|
107
|
+
container = toolchain.with_project_metadata(container, at: application_path)
|
|
108
|
+
toolchain.install(container, tools: { ruby: project.ruby_version }, workdir: application_path, only: :ruby)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def install_gems(container)
|
|
112
|
+
container = BUNDLE_FILES.reduce(container.with_workdir(application_path)) do |result, name|
|
|
113
|
+
path = project.root.join(name)
|
|
114
|
+
path.file? ? result.with_file("#{application_path}/#{name}", project.source_file(client, name)) : result
|
|
115
|
+
end
|
|
116
|
+
container = pipeline.run(
|
|
117
|
+
container,
|
|
118
|
+
BUNDLE_INSTALL_COMMAND,
|
|
119
|
+
cache: "bundle",
|
|
120
|
+
at: "/usr/local/bundle/cache",
|
|
121
|
+
entry: "bundler",
|
|
122
|
+
name: "Install gems"
|
|
123
|
+
)
|
|
124
|
+
container.with_exec(["find", "/usr/local/bundle", "-path", "*/cache/*.gem", "-type", "f", "-delete"])
|
|
125
|
+
.with_exec([
|
|
126
|
+
"sh", "-c",
|
|
127
|
+
"find /usr/local/bundle -path '*/bundler/gems/*/.git' -type d -prune -exec rm -rf '{}' +"
|
|
128
|
+
])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def write_bundle_config(container)
|
|
132
|
+
container.with_workdir(application_path)
|
|
133
|
+
.with_exec(%w[mkdir -p .bundle])
|
|
134
|
+
.with_new_file("#{application_path}/.bundle/config", BUNDLE_CONFIG)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def pipeline
|
|
138
|
+
@pipeline ||= Pipeline.new(project, client, progress: progress)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def runtime_container(builder)
|
|
142
|
+
container = with_environment(runtime_image_container, Mise::ENVIRONMENT.merge(runtime_environment))
|
|
143
|
+
container = install_packages(container, (RUNTIME_PACKAGES + configuration.runtime_packages).uniq)
|
|
144
|
+
container = create_application_user(container)
|
|
145
|
+
container = container.with_directory("/mise/installs", builder.directory("/mise/installs"))
|
|
146
|
+
.with_directory("/usr/local/bundle", builder.directory("/usr/local/bundle"),
|
|
147
|
+
owner: user_owner)
|
|
148
|
+
.with_directory(application_path, builder.directory(application_path), owner: user_owner)
|
|
149
|
+
.with_workdir(application_path)
|
|
150
|
+
.with_exec(%w[mise reshim])
|
|
151
|
+
container = runtime_metadata(container).with_user(project.application_user)
|
|
152
|
+
progress.step("Assemble runtime image") { container.sync }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def install_packages(container, packages)
|
|
156
|
+
container
|
|
157
|
+
.with_exec(%w[apt-get update])
|
|
158
|
+
.with_exec(["apt-get", "install", "-y", "--no-install-recommends", *packages])
|
|
159
|
+
.with_exec(%w[rm -rf /var/lib/apt/lists])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def create_application_user(container)
|
|
163
|
+
name = project.application_user
|
|
164
|
+
container
|
|
165
|
+
.with_exec(["groupadd", "--system", "--gid", "1000", name])
|
|
166
|
+
.with_exec(["useradd", name, "--uid", "1000", "--gid", "1000", "--create-home", "--shell", "/bin/bash"])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def precompile(container)
|
|
170
|
+
container = container.with_exec(%w[bin/rails assets:precompile]) if project.rails? && project.assets?
|
|
171
|
+
if project.bootsnap?
|
|
172
|
+
paths = project.rails? ? %w[app/ lib/] : %w[app/ lib/ config/]
|
|
173
|
+
container = container.with_exec(["bundle", "exec", "bootsnap", "precompile", "--gemfile", *paths])
|
|
174
|
+
end
|
|
175
|
+
return container unless project.rails?
|
|
176
|
+
|
|
177
|
+
normalize_rails_output(container)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def normalize_rails_output(container)
|
|
181
|
+
ruby = <<~RUBY.strip
|
|
182
|
+
path = "public/assets/.manifest.json"
|
|
183
|
+
if File.file?(path)
|
|
184
|
+
manifest = JSON.parse(File.binread(path))
|
|
185
|
+
File.binwrite(path, JSON.generate(manifest.sort.to_h))
|
|
186
|
+
end
|
|
187
|
+
RUBY
|
|
188
|
+
container
|
|
189
|
+
.with_exec(["ruby", "-rjson", "-e", ruby])
|
|
190
|
+
.with_exec(%w[rm -f tmp/local_secret.txt tmp/cache/bootsnap/load-path-cache])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def runtime_metadata(container)
|
|
194
|
+
entrypoint = project.runtime_entrypoint
|
|
195
|
+
command = project.runtime_command
|
|
196
|
+
container = container.with_entrypoint(entrypoint) unless entrypoint.empty?
|
|
197
|
+
container = container.with_default_args(command) unless command.empty?
|
|
198
|
+
return container unless project.web?
|
|
199
|
+
|
|
200
|
+
port = project.runtime_port
|
|
201
|
+
container = container.with_exposed_port(port, description: "Ruby HTTP server")
|
|
202
|
+
return container unless project.rails?
|
|
203
|
+
|
|
204
|
+
container.with_docker_healthcheck(
|
|
205
|
+
["curl", "--fail", "--silent", "http://localhost:#{port}/up"],
|
|
206
|
+
interval: "30s",
|
|
207
|
+
timeout: "5s",
|
|
208
|
+
retries: 3
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module BoringBuilder
|
|
7
|
+
class Runtime
|
|
8
|
+
Probe = Data.define(:success, :message) do
|
|
9
|
+
alias_method :success?, :success
|
|
10
|
+
end
|
|
11
|
+
RUNTIME_COMMANDS = {
|
|
12
|
+
apple: %w[container system status],
|
|
13
|
+
docker: %w[docker version]
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :requested
|
|
17
|
+
|
|
18
|
+
def initialize(requested, probe: nil, host_os: RUBY_PLATFORM)
|
|
19
|
+
@requested = requested.to_sym
|
|
20
|
+
@probe = probe || method(:probe_command)
|
|
21
|
+
@host_os = host_os
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def resolve
|
|
25
|
+
return session_runtime if dagger_session?
|
|
26
|
+
return :custom if custom_runner?
|
|
27
|
+
return ensure_available!(requested) unless requested == :auto
|
|
28
|
+
|
|
29
|
+
failures = []
|
|
30
|
+
runtime_order.each do |runtime|
|
|
31
|
+
result = @probe.call(runtime, RUNTIME_COMMANDS.fetch(runtime))
|
|
32
|
+
return runtime if result.success?
|
|
33
|
+
|
|
34
|
+
failures << "#{runtime}: #{result.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
raise BoringBuilder::RuntimeError, <<~MESSAGE.strip
|
|
38
|
+
No supported container runtime is ready. Start Apple Container with `container system start`
|
|
39
|
+
or start Docker, then try again. Probes: #{failures.join('; ')}
|
|
40
|
+
MESSAGE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dagger_runtime(resolved = resolve)
|
|
44
|
+
%i[session custom].include?(resolved) ? :auto : resolved
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def ensure_available!(runtime)
|
|
50
|
+
result = @probe.call(runtime, RUNTIME_COMMANDS.fetch(runtime))
|
|
51
|
+
return runtime if result.success?
|
|
52
|
+
|
|
53
|
+
hint = runtime == :apple ? "container system start" : "start the Docker daemon"
|
|
54
|
+
raise BoringBuilder::RuntimeError,
|
|
55
|
+
"#{runtime.to_s.capitalize} runtime is not ready: #{result.message}. Try `#{hint}`."
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def runtime_order
|
|
59
|
+
@host_os.include?("darwin") ? %i[apple docker] : %i[docker apple]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dagger_session?
|
|
63
|
+
ENV.fetch("DAGGER_SESSION_PORT", nil) && ENV.fetch("DAGGER_SESSION_TOKEN", nil)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def custom_runner?
|
|
67
|
+
ENV.fetch("_EXPERIMENTAL_DAGGER_RUNNER_HOST", nil) && requested == :auto
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def session_runtime
|
|
71
|
+
runner_host = ENV.fetch("_EXPERIMENTAL_DAGGER_RUNNER_HOST", nil)
|
|
72
|
+
match = runner_host&.match(%r{\Aimage\+(apple|docker)://})
|
|
73
|
+
return match[1].to_sym if match
|
|
74
|
+
return :custom if runner_host
|
|
75
|
+
|
|
76
|
+
:session
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def probe_command(_runtime, command)
|
|
80
|
+
stdout, stderr, status = Timeout.timeout(5) { Open3.capture3(*command) }
|
|
81
|
+
message = [stderr, stdout].map(&:strip).reject(&:empty?).first || "command exited #{status.exitstatus}"
|
|
82
|
+
Probe.new(success: status.success?, message: message)
|
|
83
|
+
rescue Errno::ENOENT
|
|
84
|
+
Probe.new(success: false, message: "#{command.first} is not installed")
|
|
85
|
+
rescue Timeout::Error
|
|
86
|
+
Probe.new(success: false, message: "probe timed out")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :boringbuilder do
|
|
4
|
+
desc "Build the Rails application with Dagger"
|
|
5
|
+
task build: :environment do
|
|
6
|
+
options = {}
|
|
7
|
+
{
|
|
8
|
+
runtime: "BORINGBUILDER_RUNTIME",
|
|
9
|
+
platform: "BORINGBUILDER_PLATFORM",
|
|
10
|
+
format: "BORINGBUILDER_FORMAT",
|
|
11
|
+
output: "BORINGBUILDER_OUTPUT",
|
|
12
|
+
publish: "BORINGBUILDER_PUSH",
|
|
13
|
+
load: "BORINGBUILDER_LOAD"
|
|
14
|
+
}.each do |option, environment_name|
|
|
15
|
+
value = ENV.fetch(environment_name, nil)
|
|
16
|
+
options[option] = value if value && !value.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
paths: "BORINGBUILDER_PATHS",
|
|
21
|
+
files: "BORINGBUILDER_FILES",
|
|
22
|
+
host_paths: "BORINGBUILDER_HOST_PATHS"
|
|
23
|
+
}.each do |option, environment_name|
|
|
24
|
+
values = ENV.fetch(environment_name, "").split(",").reject(&:empty?)
|
|
25
|
+
options[option] = values unless values.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result = BoringBuilder.build(root: Rails.root, **options)
|
|
29
|
+
puts "Built with Dagger on #{result.runtime}"
|
|
30
|
+
puts "Exported #{result.format}: #{result.path}" if result.exported?
|
|
31
|
+
puts "Image: #{result.reference}" if result.published?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc "Check the configured container runtime"
|
|
35
|
+
task :doctor do
|
|
36
|
+
requested = ENV.fetch("BORINGBUILDER_RUNTIME", "auto")
|
|
37
|
+
runtime = BoringBuilder::Runtime.new(requested).resolve
|
|
38
|
+
puts "Dagger runtime ready: #{runtime}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "boringbuilder/version"
|
|
4
|
+
require_relative "boringbuilder/errors"
|
|
5
|
+
require_relative "boringbuilder/artifact"
|
|
6
|
+
require_relative "boringbuilder/configuration"
|
|
7
|
+
require_relative "boringbuilder/config_file"
|
|
8
|
+
require_relative "boringbuilder/initializer"
|
|
9
|
+
require_relative "boringbuilder/build_progress"
|
|
10
|
+
require_relative "boringbuilder/boring_cache"
|
|
11
|
+
require_relative "boringbuilder/mise"
|
|
12
|
+
require_relative "boringbuilder/pipeline"
|
|
13
|
+
require_relative "boringbuilder/runtime"
|
|
14
|
+
require_relative "boringbuilder/ruby_application"
|
|
15
|
+
require_relative "boringbuilder/project"
|
|
16
|
+
require_relative "boringbuilder/exporters"
|
|
17
|
+
require_relative "boringbuilder/project_plan"
|
|
18
|
+
require_relative "boringbuilder/ruby_build"
|
|
19
|
+
require_relative "boringbuilder/rails_build"
|
|
20
|
+
require_relative "boringbuilder/exporter"
|
|
21
|
+
require_relative "boringbuilder/result"
|
|
22
|
+
require_relative "boringbuilder/builder"
|
|
23
|
+
require_relative "boringbuilder/cli"
|
|
24
|
+
require_relative "boringbuilder/railtie" if defined?(Rails::Railtie)
|
|
25
|
+
|
|
26
|
+
module BoringBuilder
|
|
27
|
+
class << self
|
|
28
|
+
def configure(&)
|
|
29
|
+
ConfigFile.configure(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configuration(root: Dir.pwd, config: :auto, **options)
|
|
33
|
+
configuration = Configuration.new(root: root)
|
|
34
|
+
config_path = ConfigFile.resolve(configuration.root, config)
|
|
35
|
+
ConfigFile.load(config_path, configuration) if config_path
|
|
36
|
+
configuration.apply(options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build(root: Dir.pwd, config: :auto, **)
|
|
40
|
+
configuration = configuration(root: root, config: config, **)
|
|
41
|
+
yield configuration if block_given?
|
|
42
|
+
Builder.new(configuration).build
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: boringbuilder
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.alpha.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BoringCache
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dagger_ruby
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.10'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.10'
|
|
27
|
+
description: BoringBuilder turns application source into filesystem artifacts and
|
|
28
|
+
container images. Rails is first class, while Mise-powered Dagger Ruby recipes support
|
|
29
|
+
any workload.
|
|
30
|
+
email:
|
|
31
|
+
- oss@boringcache.com
|
|
32
|
+
executables:
|
|
33
|
+
- boringbuilder
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- AGENTS.md
|
|
38
|
+
- CHANGELOG.md
|
|
39
|
+
- LICENSE
|
|
40
|
+
- README.md
|
|
41
|
+
- SECURITY.md
|
|
42
|
+
- SOUL.md
|
|
43
|
+
- STYLE.md
|
|
44
|
+
- docs/artifacts.md
|
|
45
|
+
- docs/building.md
|
|
46
|
+
- docs/caching.md
|
|
47
|
+
- exe/boringbuilder
|
|
48
|
+
- lib/boringbuilder.rb
|
|
49
|
+
- lib/boringbuilder/artifact.rb
|
|
50
|
+
- lib/boringbuilder/boring_cache.rb
|
|
51
|
+
- lib/boringbuilder/build_progress.rb
|
|
52
|
+
- lib/boringbuilder/builder.rb
|
|
53
|
+
- lib/boringbuilder/cli.rb
|
|
54
|
+
- lib/boringbuilder/config_file.rb
|
|
55
|
+
- lib/boringbuilder/configuration.rb
|
|
56
|
+
- lib/boringbuilder/errors.rb
|
|
57
|
+
- lib/boringbuilder/exporter.rb
|
|
58
|
+
- lib/boringbuilder/exporters.rb
|
|
59
|
+
- lib/boringbuilder/exporters/asset.rb
|
|
60
|
+
- lib/boringbuilder/exporters/boring_cache.rb
|
|
61
|
+
- lib/boringbuilder/exporters/local.rb
|
|
62
|
+
- lib/boringbuilder/exporters/receipt.rb
|
|
63
|
+
- lib/boringbuilder/exporters/resolver.rb
|
|
64
|
+
- lib/boringbuilder/initializer.rb
|
|
65
|
+
- lib/boringbuilder/mise.rb
|
|
66
|
+
- lib/boringbuilder/pipeline.rb
|
|
67
|
+
- lib/boringbuilder/project.rb
|
|
68
|
+
- lib/boringbuilder/project_plan.rb
|
|
69
|
+
- lib/boringbuilder/rails_build.rb
|
|
70
|
+
- lib/boringbuilder/railtie.rb
|
|
71
|
+
- lib/boringbuilder/result.rb
|
|
72
|
+
- lib/boringbuilder/ruby_application.rb
|
|
73
|
+
- lib/boringbuilder/ruby_build.rb
|
|
74
|
+
- lib/boringbuilder/runtime.rb
|
|
75
|
+
- lib/boringbuilder/tasks/boringbuilder.rake
|
|
76
|
+
- lib/boringbuilder/version.rb
|
|
77
|
+
homepage: https://github.com/boringcache/builder
|
|
78
|
+
licenses:
|
|
79
|
+
- MIT
|
|
80
|
+
metadata:
|
|
81
|
+
allowed_push_host: https://rubygems.org
|
|
82
|
+
homepage_uri: https://github.com/boringcache/builder
|
|
83
|
+
source_code_uri: https://github.com/boringcache/builder/tree/v0.1.0.alpha.1
|
|
84
|
+
documentation_uri: https://github.com/boringcache/builder#readme
|
|
85
|
+
changelog_uri: https://github.com/boringcache/builder/blob/main/CHANGELOG.md
|
|
86
|
+
bug_tracker_uri: https://github.com/boringcache/builder/issues
|
|
87
|
+
security_policy_uri: https://github.com/boringcache/builder/security/policy
|
|
88
|
+
rubygems_mfa_required: 'true'
|
|
89
|
+
post_install_message:
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: 3.2.0
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.3.1
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 3.4.19
|
|
105
|
+
signing_key:
|
|
106
|
+
specification_version: 4
|
|
107
|
+
summary: Build deployment artifacts and images with Dagger
|
|
108
|
+
test_files: []
|