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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BoringBuilder
|
|
6
|
+
class Mise
|
|
7
|
+
DEFAULT_IMAGE = BoringCache::BUILD_IMAGE
|
|
8
|
+
CACHE_PATH = "/mise/cache"
|
|
9
|
+
INSTALL_COMMAND = %w[mise install].freeze
|
|
10
|
+
METADATA_FILES = %w[mise.toml .mise.toml .tool-versions mise.lock].freeze
|
|
11
|
+
ENVIRONMENT = {
|
|
12
|
+
"MISE_CACHE_DIR" => CACHE_PATH,
|
|
13
|
+
"MISE_DATA_DIR" => "/mise",
|
|
14
|
+
"MISE_INSTALLS_DIR" => "/mise/installs",
|
|
15
|
+
"MISE_SHIMS_DIR" => "/mise/shims",
|
|
16
|
+
"PATH" => "/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :project, :pipeline
|
|
20
|
+
|
|
21
|
+
def initialize(project, pipeline)
|
|
22
|
+
@project = project
|
|
23
|
+
@pipeline = pipeline
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def container(tools: {}, image: DEFAULT_IMAGE, workdir: project.application_path)
|
|
27
|
+
base = with_project_metadata(pipeline.container(image), at: workdir)
|
|
28
|
+
install(base, tools: tools, workdir: workdir).with_directory(workdir, pipeline.source)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def install(container, tools:, workdir:, only: nil)
|
|
32
|
+
container = with_environment(container)
|
|
33
|
+
container = container.with_workdir(workdir)
|
|
34
|
+
.with_new_file("/etc/mise/config.toml", system_config(tools))
|
|
35
|
+
command = [*INSTALL_COMMAND, *Array(only).map(&:to_s)]
|
|
36
|
+
pipeline.run(
|
|
37
|
+
container,
|
|
38
|
+
command,
|
|
39
|
+
cache: "mise",
|
|
40
|
+
at: CACHE_PATH,
|
|
41
|
+
entry: "mise",
|
|
42
|
+
workdir: workdir,
|
|
43
|
+
name: "Prepare Mise toolchain"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def with_project_metadata(container, at: project.application_path)
|
|
48
|
+
METADATA_FILES.reduce(container) do |result, name|
|
|
49
|
+
path = project.root.join(name)
|
|
50
|
+
path.file? ? result.with_file("#{at}/#{name}", project.source_file(pipeline.client, name)) : result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def with_environment(container)
|
|
55
|
+
ENVIRONMENT.reduce(container) do |result, (name, value)|
|
|
56
|
+
result.with_env_variable(name, value)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def system_config(tools)
|
|
63
|
+
declarations = normalize_tools(tools).sort.map do |name, version|
|
|
64
|
+
"#{JSON.generate(name)} = #{JSON.generate(version)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
<<~TOML
|
|
68
|
+
[tools]
|
|
69
|
+
#{declarations.join("\n")}
|
|
70
|
+
|
|
71
|
+
[settings]
|
|
72
|
+
paranoid = true
|
|
73
|
+
TOML
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_tools(tools)
|
|
77
|
+
tools.to_h.each_with_object({}) do |(name, version), normalized|
|
|
78
|
+
name = name.to_s
|
|
79
|
+
version = version.to_s
|
|
80
|
+
raise ConfigurationError, "Mise tool names and versions cannot be empty" if name.empty? || version.empty?
|
|
81
|
+
|
|
82
|
+
normalized[name] = version
|
|
83
|
+
end
|
|
84
|
+
rescue NoMethodError, TypeError
|
|
85
|
+
raise ConfigurationError, "Mise tools must be a name-to-version map"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
class Pipeline
|
|
5
|
+
attr_reader :project, :client, :progress
|
|
6
|
+
|
|
7
|
+
def initialize(project, client, environment: ENV, progress: BuildProgress.silent)
|
|
8
|
+
@project = project
|
|
9
|
+
@client = client
|
|
10
|
+
@environment = environment
|
|
11
|
+
@progress = progress
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
container = yield self
|
|
16
|
+
raise ConfigurationError, "The custom pipeline must return a Dagger container" unless container
|
|
17
|
+
|
|
18
|
+
sync_step(container, "Build application")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def container(image = nil)
|
|
22
|
+
result = client.container(container_options)
|
|
23
|
+
image ? result.from(image) : result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def source
|
|
27
|
+
project.source(client)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mise(tools: {}, image: Mise::DEFAULT_IMAGE, workdir: project.application_path)
|
|
31
|
+
Mise.new(project, self).container(tools: tools, image: image, workdir: workdir)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run(container, command, cache:, at: nil, entry: nil, workdir: project.application_path, sharing: :LOCKED,
|
|
35
|
+
name: "Run cached step", **options)
|
|
36
|
+
arguments = Array(command).map(&:to_s)
|
|
37
|
+
mounts = normalize_mounts(cache, at, entry)
|
|
38
|
+
validate_step!(arguments, mounts)
|
|
39
|
+
|
|
40
|
+
remote_cache = BoringCache.new(project, client, environment: @environment)
|
|
41
|
+
container = container.with_workdir(workdir.to_s)
|
|
42
|
+
|
|
43
|
+
result, executed = if remote_cache.enabled?
|
|
44
|
+
container = remote_cache.prepare(container, workdir: workdir)
|
|
45
|
+
command = if entry
|
|
46
|
+
remote_cache.wrap(arguments, entry: entry.to_s)
|
|
47
|
+
else
|
|
48
|
+
remote_cache.wrap_mounts(arguments, mounts)
|
|
49
|
+
end
|
|
50
|
+
command_result = execute(container, command, options)
|
|
51
|
+
final_result = entry ? command_result : remove_directories(command_result, mounts.values)
|
|
52
|
+
[final_result, command_result]
|
|
53
|
+
else
|
|
54
|
+
run_with_local_cache(container, arguments, mounts, sharing, options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sync_step(result, name, output: -> { command_output(executed) })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def exec(container, command, name:, workdir: project.application_path, **options)
|
|
61
|
+
arguments = Array(command).map(&:to_s)
|
|
62
|
+
raise ConfigurationError, "step command cannot be empty" if arguments.empty?
|
|
63
|
+
|
|
64
|
+
result = execute(container.with_workdir(workdir.to_s), arguments, options)
|
|
65
|
+
sync_step(result, name, output: -> { command_output(result) })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def step(name, container)
|
|
69
|
+
result = yield container
|
|
70
|
+
raise ConfigurationError, "The pipeline step must return a Dagger container" unless result
|
|
71
|
+
|
|
72
|
+
sync_step(result, name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def container_options
|
|
78
|
+
project.configuration.platform ? { platform: project.configuration.platform } : {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_mounts(cache, at, entry)
|
|
82
|
+
return { cache.to_s => at.to_s } unless cache.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
raise ConfigurationError, "at and entry apply only to a single cache" if at || entry
|
|
85
|
+
|
|
86
|
+
cache.to_h { |name, path| [name.to_s, path.to_s] }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def mount_directly(container, mounts, sharing)
|
|
90
|
+
mounts.reduce(container) do |step, (name, path)|
|
|
91
|
+
step.with_mounted_cache(path, cache_volume(name), sharing: sharing)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_with_local_cache(container, arguments, mounts, sharing, options)
|
|
96
|
+
executed = execute(mount_directly(container, mounts, sharing), arguments, options)
|
|
97
|
+
[remove_local_mounts(executed, mounts), executed]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def execute(container, command, options)
|
|
101
|
+
options.empty? ? container.with_exec(command) : container.with_exec(command, options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def command_output(container)
|
|
105
|
+
[container.stdout, container.stderr].map(&:strip).reject(&:empty?).join("\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def remove_local_mounts(container, mounts)
|
|
109
|
+
mounts.reduce(container) do |step, (_name, path)|
|
|
110
|
+
step.chain_operation("withoutMount", { "path" => path })
|
|
111
|
+
.with_exec(["rm", "-rf", path])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remove_directories(container, paths)
|
|
116
|
+
container.with_exec(["rm", "-rf", *paths])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cache_volume(name)
|
|
120
|
+
client.cache_volume("boringbuilder-#{project.app_name}-#{name}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sync_step(container, name, output: nil)
|
|
124
|
+
return container if container.equal?(@last_synced_container)
|
|
125
|
+
|
|
126
|
+
@last_synced_container = progress.step(name) do
|
|
127
|
+
progress.write { output.call } if output
|
|
128
|
+
container.sync
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_step!(arguments, mounts)
|
|
133
|
+
raise ConfigurationError, "cached command cannot be empty" if arguments.empty?
|
|
134
|
+
raise ConfigurationError, "at is required for a single cache" if mounts.values == [""]
|
|
135
|
+
raise ConfigurationError, "at least one cache is required" if mounts.empty?
|
|
136
|
+
|
|
137
|
+
mounts.each do |name, path|
|
|
138
|
+
validate_cache_name!(name)
|
|
139
|
+
validate_cache_path!(path)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_cache_name!(name)
|
|
144
|
+
return if name.match?(/\A[a-zA-Z0-9][a-zA-Z0-9._-]*\z/)
|
|
145
|
+
|
|
146
|
+
raise ConfigurationError, "cache name may contain only letters, numbers, '.', '_', and '-'"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_cache_path!(path)
|
|
150
|
+
clean = Pathname.new(path).cleanpath.to_s
|
|
151
|
+
return if path.start_with?("/") && path != "/" && clean == path
|
|
152
|
+
|
|
153
|
+
raise ConfigurationError, "cache path must be a clean absolute path other than '/'"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
class Project
|
|
5
|
+
DEFAULT_EXCLUDES = %w[
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
.git
|
|
9
|
+
coverage
|
|
10
|
+
dist
|
|
11
|
+
log
|
|
12
|
+
node_modules
|
|
13
|
+
tmp
|
|
14
|
+
vendor/bundle
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :configuration
|
|
18
|
+
|
|
19
|
+
def initialize(configuration)
|
|
20
|
+
@configuration = configuration
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate!
|
|
24
|
+
configuration.validate!
|
|
25
|
+
validate_lockfile!
|
|
26
|
+
validate_application!
|
|
27
|
+
validate_native_javascript!
|
|
28
|
+
Exporters::Resolver.new(self, nil).validate!
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def container(client, progress: BuildProgress.silent)
|
|
33
|
+
return Pipeline.new(self, client, progress: progress).build(&configuration.pipeline) if custom_pipeline?
|
|
34
|
+
|
|
35
|
+
builder = rails? ? RailsBuild : RubyBuild
|
|
36
|
+
builder.new(self, client, progress: progress).container
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def plan
|
|
40
|
+
ProjectPlan.new(self).to_h
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def root
|
|
44
|
+
configuration.root
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def app_name
|
|
48
|
+
@app_name ||= root.basename.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def rails?
|
|
52
|
+
application.rails?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ruby?
|
|
56
|
+
application.ruby?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hanami?
|
|
60
|
+
application.hanami?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def framework
|
|
64
|
+
application.framework
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def locked?
|
|
68
|
+
root.join("Gemfile.lock").file?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ruby_version
|
|
72
|
+
@ruby_version ||= mise_ruby_version || tool_versions_ruby_version || ruby_version_file ||
|
|
73
|
+
gemfile_ruby_version || locked_ruby_version || RUBY_VERSION
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assets?
|
|
77
|
+
return configuration.assets unless configuration.assets.nil?
|
|
78
|
+
|
|
79
|
+
root.join("app/assets").directory? || root.join("config/manifest.js").file?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def bootsnap?
|
|
83
|
+
return configuration.bootsnap unless configuration.bootsnap.nil?
|
|
84
|
+
|
|
85
|
+
root.join("Gemfile.lock").read.match?(/^ bootsnap \(/)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def artifact_paths
|
|
89
|
+
artifact.entries.filter_map { |entry| entry.source if entry.origin == :container }.map(&:to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def application_path
|
|
93
|
+
application.path
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def application_user
|
|
97
|
+
application.user
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def runtime_environment
|
|
101
|
+
application.runtime_environment
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def runtime_command
|
|
105
|
+
application.runtime_command
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def runtime_entrypoint
|
|
109
|
+
application.runtime_entrypoint
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def runtime_port
|
|
113
|
+
application.runtime_port
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def web?
|
|
117
|
+
application.web?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def artifact
|
|
121
|
+
@artifact ||= begin
|
|
122
|
+
configured = configuration.artifact
|
|
123
|
+
add_configured_entries(configured)
|
|
124
|
+
add_default_entries(configured) if configured.empty?
|
|
125
|
+
configured
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def output_path
|
|
130
|
+
return configuration.output if configuration.output
|
|
131
|
+
|
|
132
|
+
suffix = {
|
|
133
|
+
directory: "rootfs",
|
|
134
|
+
tar: "tar",
|
|
135
|
+
tar_zst: "tar.zst",
|
|
136
|
+
oci: "oci.tar",
|
|
137
|
+
docker: "docker.tar"
|
|
138
|
+
}.fetch(configuration.format)
|
|
139
|
+
platform = configuration.platform&.tr("/", "-") || "native"
|
|
140
|
+
root.join("dist", "#{app_name}-#{platform}.#{suffix}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def source(client)
|
|
144
|
+
client.host.directory(root.to_s, exclude: DEFAULT_EXCLUDES, gitignore: true)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def source_file(client, path)
|
|
148
|
+
client.host.file(root.join(path).to_s)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def custom_pipeline?
|
|
152
|
+
!configuration.pipeline.nil?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def validate_lockfile!
|
|
158
|
+
return if custom_pipeline? || !ruby? || locked?
|
|
159
|
+
|
|
160
|
+
raise ConfigurationError, "Gemfile.lock is required for a reproducible Ruby build"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_application!
|
|
164
|
+
return if custom_pipeline? || ruby?
|
|
165
|
+
|
|
166
|
+
raise ConfigurationError, "No conventional Ruby application or custom pipeline found in #{root}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def add_configured_entries(artifact)
|
|
170
|
+
configuration.paths.each do |mapping|
|
|
171
|
+
source, destination = parse_mapping(mapping)
|
|
172
|
+
artifact.directory(source, at: destination)
|
|
173
|
+
end
|
|
174
|
+
configuration.files.each do |mapping|
|
|
175
|
+
source, destination = parse_mapping(mapping)
|
|
176
|
+
artifact.file(source, at: destination)
|
|
177
|
+
end
|
|
178
|
+
configuration.host_paths.each do |mapping|
|
|
179
|
+
source, destination = parse_mapping(mapping, destination_required: true)
|
|
180
|
+
artifact.host_path(source, at: destination)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def add_default_entries(artifact)
|
|
185
|
+
paths = custom_pipeline? ? ["/"] : [application_path, "/usr/local", "/mise"]
|
|
186
|
+
paths.each { |path| artifact.directory(path) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def parse_mapping(mapping, destination_required: false)
|
|
190
|
+
source, destination = mapping.to_s.split("=", 2)
|
|
191
|
+
if source.to_s.empty? || (destination_required && destination.to_s.empty?)
|
|
192
|
+
raise ConfigurationError, "Artifact mapping must be SOURCE=DESTINATION: #{mapping.inspect}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
[source, destination || source]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def validate_native_javascript!
|
|
199
|
+
return if custom_pipeline? || !root.join("package.json").file?
|
|
200
|
+
|
|
201
|
+
raise ConfigurationError, <<~MESSAGE.strip
|
|
202
|
+
package.json was found. Define config/boringbuilder.rb with a custom Dagger pipeline that installs the
|
|
203
|
+
application's JavaScript runtime.
|
|
204
|
+
MESSAGE
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def application
|
|
208
|
+
@application ||= RubyApplication.new(root, configuration)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def ruby_version_file
|
|
212
|
+
path = root.join(".ruby-version")
|
|
213
|
+
return unless path.file?
|
|
214
|
+
|
|
215
|
+
normalize_ruby_version(path.read.strip)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def mise_ruby_version
|
|
219
|
+
path = %w[mise.toml .mise.toml].map { |name| root.join(name) }.find(&:file?)
|
|
220
|
+
return unless path
|
|
221
|
+
|
|
222
|
+
tools_section = false
|
|
223
|
+
path.each_line do |line|
|
|
224
|
+
value = line.strip.sub(/\s+#.*\z/, "")
|
|
225
|
+
if value.start_with?("[")
|
|
226
|
+
tools_section = value == "[tools]"
|
|
227
|
+
next
|
|
228
|
+
end
|
|
229
|
+
next unless tools_section
|
|
230
|
+
|
|
231
|
+
version = value[/\Aruby\s*=\s*["']([^"']+)["']\z/, 1]
|
|
232
|
+
return normalize_ruby_version(version) if version
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def tool_versions_ruby_version
|
|
239
|
+
path = root.join(".tool-versions")
|
|
240
|
+
return unless path.file?
|
|
241
|
+
|
|
242
|
+
version = path.each_line.filter_map { |line| line[/\Aruby\s+(\S+)/, 1] }.first
|
|
243
|
+
normalize_ruby_version(version) if version
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def gemfile_ruby_version
|
|
247
|
+
path = root.join("Gemfile")
|
|
248
|
+
return unless path.file?
|
|
249
|
+
|
|
250
|
+
version = path.read[/^\s*ruby\s+["']([^"']+)["']/, 1]
|
|
251
|
+
normalize_ruby_version(version) if version
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def normalize_ruby_version(version)
|
|
255
|
+
version.to_s.delete_prefix("ruby-")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def locked_ruby_version
|
|
259
|
+
return unless locked?
|
|
260
|
+
|
|
261
|
+
root.join("Gemfile.lock").read[/^RUBY VERSION\n\s+ruby ([^\s]+)/, 1]&.sub(/p\d+\z/, "")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
class ProjectPlan
|
|
5
|
+
attr_reader :project
|
|
6
|
+
|
|
7
|
+
def initialize(project)
|
|
8
|
+
@project = project
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
identity.merge(build, artifact, delivery)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def identity
|
|
18
|
+
{
|
|
19
|
+
app: project.app_name,
|
|
20
|
+
root: project.root.to_s,
|
|
21
|
+
strategy: strategy,
|
|
22
|
+
framework: project.framework,
|
|
23
|
+
ruby_version: ruby_version
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build
|
|
28
|
+
{
|
|
29
|
+
platform: configuration.platform,
|
|
30
|
+
runtime: configuration.runtime,
|
|
31
|
+
format: configuration.format,
|
|
32
|
+
command: command,
|
|
33
|
+
port: port,
|
|
34
|
+
cache: BoringCache.new(project, nil).mode
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def artifact
|
|
39
|
+
{
|
|
40
|
+
output: project.output_path.to_s,
|
|
41
|
+
artifact: project.artifact.to_a,
|
|
42
|
+
artifact_paths: project.artifact_paths,
|
|
43
|
+
exporters: Exporters::Resolver.new(project, nil).plan
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delivery
|
|
48
|
+
{
|
|
49
|
+
publish: configuration.publish,
|
|
50
|
+
load: configuration.load
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def configuration
|
|
55
|
+
project.configuration
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def strategy
|
|
59
|
+
return "custom" if project.custom_pipeline?
|
|
60
|
+
|
|
61
|
+
project.framework.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ruby_version
|
|
65
|
+
project.ruby? ? project.ruby_version : nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def command
|
|
69
|
+
project.custom_pipeline? ? nil : project.runtime_command
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def port
|
|
73
|
+
return if project.custom_pipeline? || !project.web?
|
|
74
|
+
|
|
75
|
+
project.runtime_port
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringBuilder
|
|
4
|
+
Result = Data.define(:format, :path, :reference, :runtime, :platform, :artifact_id, :artifact_name) do
|
|
5
|
+
def initialize(format:, path:, reference:, runtime:, platform:, artifact_id: nil, artifact_name: nil)
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def exported?
|
|
10
|
+
!path.nil?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def published?
|
|
14
|
+
!reference.nil?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def artifact_published?
|
|
18
|
+
!artifact_id.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{
|
|
23
|
+
format: format,
|
|
24
|
+
path: path,
|
|
25
|
+
reference: reference,
|
|
26
|
+
runtime: runtime,
|
|
27
|
+
platform: platform,
|
|
28
|
+
artifact_id: artifact_id,
|
|
29
|
+
artifact_name: artifact_name
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|