ace-lab 0.15.1 → 0.16.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/docs/runtime-bundle.md +43 -0
- data/lib/ace/lab/atoms/install_schema.rb +9 -3
- data/lib/ace/lab/cli/commands/install.rb +1 -1
- data/lib/ace/lab/molecules/gemset_provisioner.rb +92 -185
- data/lib/ace/lab/organisms/install.rb +3 -32
- data/lib/ace/lab/organisms/verify.rb +3 -0
- data/lib/ace/lab/version.rb +1 -1
- metadata +22 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78e9b81907693f3c427e477a6f2a17052d1e72c9f13bb3b0fd505854d9c6e222
|
|
4
|
+
data.tar.gz: beac210464ec9f958652ad7b817a6c6452e90dca7acf320b0c652a957bb2e0bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07de910a64b118ae1335c7e220fe5d21b9cba2b82050f170d6094297d507cb57a20e87f3af76a7ad66f9d5607889b06c462433e430cb76d38c4ebe669ef8d5f2
|
|
7
|
+
data.tar.gz: 7518f23fc8caefbfd2bd813887d18716898aaee27c80078131ee0ca8a4c902e3d5538d0bb0bdcc4827731fa46eb16c99c587df701041440f5808104fc0c44295
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Install the complete runtime with one Bundler invocation and a persistent Gemfile/lockfile; restore the previous bundle inputs after a failed update. Keep diagnostics explicit, verify locked activation, and expose configured executable links.
|
|
15
|
+
|
|
10
16
|
## [0.15.1] - 2026-11-11
|
|
11
17
|
|
|
12
18
|
### Fixed
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Installed lab runtime
|
|
2
|
+
|
|
3
|
+
The lab's operational ACE bundle is separate from every project checkout and its
|
|
4
|
+
Gemfile. RubyGems/Bundler owns dependency resolution. `ace-lab install` performs
|
|
5
|
+
one `bundle install` for the declared gem set; success does not trigger a second
|
|
6
|
+
installation audit. `ace-lab verify` is an explicit diagnostic command.
|
|
7
|
+
|
|
8
|
+
Example for Ruby 3.4:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
gemset:
|
|
12
|
+
gem_home: /opt/ace/runtime/bundle/ruby/3.4.0
|
|
13
|
+
bin_dir: /usr/local/bin
|
|
14
|
+
gems:
|
|
15
|
+
- name: ace-lab
|
|
16
|
+
version: 0.16.0
|
|
17
|
+
- name: ace-herdr
|
|
18
|
+
version: 0.11.0
|
|
19
|
+
- name: ace-overseer
|
|
20
|
+
version: 0.30.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`gem_home` is Bundler's standard `<bundle-root>/<ruby-engine>/<ruby-ABI>`
|
|
24
|
+
installation directory. The installer writes one Gemfile and Bundler configuration
|
|
25
|
+
at `<bundle-root>`, retaining the lockfile across updates. Bundler generates
|
|
26
|
+
binstubs with the installing Ruby's absolute shebang. Optional `bin_dir` exposes
|
|
27
|
+
those binstubs through normal executable links. Unrelated executable paths are
|
|
28
|
+
not overwritten.
|
|
29
|
+
|
|
30
|
+
A failed update restores the previous Gemfile, lockfile and bundle configuration.
|
|
31
|
+
Already installed sibling versions remain on disk; the lockfile selects the
|
|
32
|
+
runtime. Diagnostics of a bundled runtime use frozen, read-only `bundle check`.
|
|
33
|
+
|
|
34
|
+
For updates, change the manifest pins and run `ace-lab install` once. To rerun
|
|
35
|
+
installation of an unchanged runtime directly:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
cd /opt/ace/runtime/bundle
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
No project Gemfile is changed. Native Herdr, Node, Pi and Forgejo are provisioned
|
|
43
|
+
separately; a gem update does not reinstall the VM or recreate its repositories.
|
|
@@ -31,7 +31,7 @@ module Ace
|
|
|
31
31
|
DEFAULT_MODE = "0755"
|
|
32
32
|
DEFAULT_KEY_TYPE = "ed25519"
|
|
33
33
|
KEY_TYPES = [DEFAULT_KEY_TYPE].freeze
|
|
34
|
-
GEMSET_KEYS = %w[gem_home gems extensions].freeze
|
|
34
|
+
GEMSET_KEYS = %w[gem_home gems extensions bin_dir].freeze
|
|
35
35
|
GEM_KEYS = %w[name version].freeze
|
|
36
36
|
GEM_NAME_PATTERN = /\A[A-Za-z0-9._-]+\z/
|
|
37
37
|
EXTENSION_KEYS = %w[gem path account].freeze
|
|
@@ -420,8 +420,14 @@ module Ace
|
|
|
420
420
|
raise_invalid "gemset gem_home is required and must be an absolute path"
|
|
421
421
|
end
|
|
422
422
|
|
|
423
|
-
{"gem_home" => gem_home.strip, "gems" => normalize_gems(raw["gems"]),
|
|
424
|
-
|
|
423
|
+
normalized = {"gem_home" => gem_home.strip, "gems" => normalize_gems(raw["gems"]),
|
|
424
|
+
"extensions" => normalize_extensions(raw["extensions"], raw["gems"], usernames)}
|
|
425
|
+
if raw.key?("bin_dir")
|
|
426
|
+
bin_dir = required_string(raw, "gemset", "bin_dir")
|
|
427
|
+
raise_invalid "gemset bin_dir must be an absolute path" unless absolute_path?(bin_dir)
|
|
428
|
+
normalized["bin_dir"] = bin_dir
|
|
429
|
+
end
|
|
430
|
+
normalized
|
|
425
431
|
end
|
|
426
432
|
|
|
427
433
|
# Harness extension projections: each entry names a DECLARED
|
|
@@ -103,7 +103,7 @@ module Ace
|
|
|
103
103
|
# Final install verification stage: the readiness verdict with
|
|
104
104
|
# every failed check named (adapters and tools included).
|
|
105
105
|
def render_verify(verify_report)
|
|
106
|
-
return ["Verify: skipped (#{verify_report['message']})"] if verify_report["status"]
|
|
106
|
+
return ["Verify: skipped (#{verify_report['message']})"] if %w[skipped not_run].include?(verify_report["status"])
|
|
107
107
|
|
|
108
108
|
counts = verify_report["counts"] || {}
|
|
109
109
|
verdict = verify_report["ready"] ? "ready" : "NOT READY"
|
|
@@ -3,30 +3,15 @@
|
|
|
3
3
|
require_relative "../errors"
|
|
4
4
|
require "open3"
|
|
5
5
|
require "fileutils"
|
|
6
|
-
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "rbconfig"
|
|
7
8
|
|
|
8
9
|
module Ace
|
|
9
10
|
module Lab
|
|
10
11
|
module Molecules
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# present is a no-op (`exists`), a missing gem is installed
|
|
15
|
-
# (`installed`), a gem present at a different version is updated
|
|
16
|
-
# (the declared version installed alongside — RubyGems keeps
|
|
17
|
-
# versions side by side; verify judges the declared set). Probes
|
|
18
|
-
# and installs run the `gem` executable with GEM_HOME/GEM_PATH
|
|
19
|
-
# pinned to the target runtime through the SAME argv runner seam
|
|
20
|
-
# the other prepare molecules use (POSIX `env`, never a shell
|
|
21
|
-
# string, ADR-031) — so the lab runtime stays isolated from the
|
|
22
|
-
# installer's own gems and the runner stays injectable for
|
|
23
|
-
# dry-run and tests. The runtime directory itself is created only
|
|
24
|
-
# with administrator privileges (the same seam
|
|
25
|
-
# AccountProvisioner gates creation with) and a symlink or
|
|
26
|
-
# non-directory at the declared path is refused (the
|
|
27
|
-
# DirectoryProvisioner takeover posture). Every failure is a
|
|
28
|
-
# classified per-gem item with an actionable message; command
|
|
29
|
-
# failures surface only the exit status and the first stderr line.
|
|
12
|
+
# Install the declared runtime as one Bundler bundle. Dependency
|
|
13
|
+
# resolution belongs to Bundler; read-only probes below are used only
|
|
14
|
+
# by explicit diagnostics.
|
|
30
15
|
class GemsetProvisioner
|
|
31
16
|
attr_reader :runner, :privileged, :dry_run
|
|
32
17
|
|
|
@@ -46,20 +31,71 @@ module Ace
|
|
|
46
31
|
|
|
47
32
|
gem_home = runtime["gem_home"]
|
|
48
33
|
runtime_failure = ensure_runtime(gem_home)
|
|
34
|
+
if runtime_failure
|
|
35
|
+
return gems.map { |gem| base(gem, gem_home).merge("status" => "failed", "message" => runtime_failure) }
|
|
36
|
+
end
|
|
37
|
+
if dry_run
|
|
38
|
+
return gems.map { |gem| base(gem, gem_home).merge("status" => "would_install", "message" => nil) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
scope = File.join(RUBY_ENGINE, RbConfig::CONFIG.fetch("ruby_version"))
|
|
42
|
+
unless gem_home.end_with?("/#{scope}")
|
|
43
|
+
raise Ace::Lab::InvalidConfigurationError,
|
|
44
|
+
"gemset gem_home must end in /#{scope} for the Bundler runtime"
|
|
45
|
+
end
|
|
46
|
+
bundle_root = File.dirname(File.dirname(gem_home))
|
|
47
|
+
gemfile = File.join(bundle_root, "Gemfile")
|
|
48
|
+
raise Ace::Lab::InvalidConfigurationError, "runtime Gemfile must not be a symlink" if File.symlink?(gemfile)
|
|
49
|
+
content = "source #{Gem.sources.first.to_s.dump}\n" + gems.map do |gem|
|
|
50
|
+
"gem #{gem.fetch('name').dump}, #{gem.fetch('version').dump}\n"
|
|
51
|
+
end.join
|
|
52
|
+
settings_dir = File.join(bundle_root, ".bundle")
|
|
53
|
+
settings_path = File.join(settings_dir, "config")
|
|
54
|
+
if File.symlink?(settings_dir) || File.symlink?(settings_path) || File.symlink?("#{gemfile}.lock")
|
|
55
|
+
raise Ace::Lab::InvalidConfigurationError, "runtime bundle configuration must not be a symlink"
|
|
56
|
+
end
|
|
57
|
+
previous = [gemfile, "#{gemfile}.lock", settings_path].to_h do |path|
|
|
58
|
+
[path, File.file?(path) ? [File.binread(path), File.stat(path).mode & 0o777] : nil]
|
|
59
|
+
end
|
|
60
|
+
begin
|
|
61
|
+
File.write(gemfile, content) unless File.file?(gemfile) && File.read(gemfile) == content
|
|
62
|
+
FileUtils.mkdir_p(settings_dir)
|
|
63
|
+
File.write(settings_path, YAML.dump(
|
|
64
|
+
"BUNDLE_PATH" => bundle_root, "BUNDLE_BIN" => File.join(gem_home, "bin"),
|
|
65
|
+
"BUNDLE_SHEBANG" => Gem.ruby))
|
|
66
|
+
result = @runner.call(runtime_argv(gem_home,
|
|
67
|
+
["BUNDLE_GEMFILE=#{gemfile}", "BUNDLE_USER_CONFIG=#{File::NULL}", "bundle", "install"]))
|
|
68
|
+
failure = command_failure("bundle install", result) unless result["status"] == "ok"
|
|
69
|
+
install_entrypoints(gem_home, runtime["bin_dir"]) unless failure
|
|
70
|
+
rescue StandardError, Interrupt
|
|
71
|
+
restore_bundle(previous)
|
|
72
|
+
raise
|
|
73
|
+
end
|
|
74
|
+
restore_bundle(previous) if failure
|
|
49
75
|
gems.map do |gem|
|
|
50
|
-
|
|
51
|
-
base(gem, gem_home).merge("status" => "failed", "message" => runtime_failure)
|
|
52
|
-
else
|
|
53
|
-
report = converge(base(gem, gem_home), gem, gem_home)
|
|
54
|
-
if %w[exists installed updated].include?(report["status"])
|
|
55
|
-
converge_post_presence(report, gem, gem_home)
|
|
56
|
-
else
|
|
57
|
-
report
|
|
58
|
-
end
|
|
59
|
-
end
|
|
76
|
+
base(gem, gem_home).merge("status" => failure ? "failed" : "installed", "message" => failure)
|
|
60
77
|
end
|
|
61
78
|
end
|
|
62
79
|
|
|
80
|
+
# A configured bundle controls activation even when another version
|
|
81
|
+
# is installed beside it. Bundler owns checking its locked closure.
|
|
82
|
+
def bundle_status(name:, version:, gem_home:)
|
|
83
|
+
gemfile = File.join(File.dirname(File.dirname(gem_home)), "Gemfile")
|
|
84
|
+
return nil unless File.file?(gemfile)
|
|
85
|
+
lock = "#{gemfile}.lock"
|
|
86
|
+
return [:failed, "runtime bundle lock is missing; rerun ace-lab install"] unless File.file?(lock)
|
|
87
|
+
require "bundler"
|
|
88
|
+
specs = Bundler::LockfileParser.new(File.read(lock)).specs
|
|
89
|
+
unless specs.any? { |spec| spec.name == name && spec.version.to_s == version }
|
|
90
|
+
return [:failed, "runtime bundle does not lock #{name} at #{version}; rerun ace-lab install"]
|
|
91
|
+
end
|
|
92
|
+
result = @runner.call(runtime_argv(gem_home,
|
|
93
|
+
["BUNDLE_GEMFILE=#{gemfile}", "BUNDLE_USER_CONFIG=#{File::NULL}", "BUNDLE_FROZEN=true", "bundle", "check", "--dry-run"]))
|
|
94
|
+
result["status"] == "ok" ? [:ok, nil] : [:failed, command_failure("bundle check", result)]
|
|
95
|
+
rescue Bundler::BundlerError, SystemCallError => error
|
|
96
|
+
[:failed, "runtime bundle is unavailable (#{error.class}); rerun ace-lab install"]
|
|
97
|
+
end
|
|
98
|
+
|
|
63
99
|
# Public READ-ONLY probe (L8.5): the local versions of `name`
|
|
64
100
|
# inside the target runtime — the exact probe convergence runs,
|
|
65
101
|
# exposed so the readiness verifier reuses the same logic
|
|
@@ -135,6 +171,31 @@ module Ace
|
|
|
135
171
|
|
|
136
172
|
private
|
|
137
173
|
|
|
174
|
+
def restore_bundle(previous)
|
|
175
|
+
previous.each do |path, saved|
|
|
176
|
+
if saved
|
|
177
|
+
File.binwrite(path, saved.first)
|
|
178
|
+
File.chmod(saved.last, path)
|
|
179
|
+
else
|
|
180
|
+
File.unlink(path) if File.file?(path)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def install_entrypoints(gem_home, directory)
|
|
186
|
+
return unless directory
|
|
187
|
+
FileUtils.mkdir_p(directory)
|
|
188
|
+
Dir.glob(File.join(gem_home, "bin", "ace-*")).each do |source|
|
|
189
|
+
target = File.join(directory, File.basename(source))
|
|
190
|
+
if File.symlink?(target)
|
|
191
|
+
next if File.expand_path(File.readlink(target), directory) == source
|
|
192
|
+
raise Ace::Lab::InvalidConfigurationError, "executable already belongs to another runtime: #{target}"
|
|
193
|
+
end
|
|
194
|
+
raise Ace::Lab::InvalidConfigurationError, "executable already exists: #{target}" if File.exist?(target)
|
|
195
|
+
File.symlink(source, target)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
138
199
|
def base(gem, gem_home)
|
|
139
200
|
{"name" => gem["name"], "version" => gem["version"], "gem_home" => gem_home}
|
|
140
201
|
end
|
|
@@ -163,147 +224,6 @@ module Ace
|
|
|
163
224
|
"gem runtime '#{gem_home}' could not be created (#{e.class.name}); rerun ace-lab prepare"
|
|
164
225
|
end
|
|
165
226
|
|
|
166
|
-
def converge(base, gem, gem_home)
|
|
167
|
-
kind, versions_or_message = runtime_versions(name: gem["name"], gem_home: gem_home)
|
|
168
|
-
# Prepare's fail-closed posture: an unprobeable runtime (no
|
|
169
|
-
# `gem` executable — the :unknown probe kind) fails the item in
|
|
170
|
-
# wet AND dry runs, exactly as the wet run would fail; verify
|
|
171
|
-
# maps the same probe kind to its offline `unknown` posture.
|
|
172
|
-
return base.merge("status" => "failed", "message" => versions_or_message) if kind != :ok
|
|
173
|
-
|
|
174
|
-
versions = versions_or_message
|
|
175
|
-
return base.merge("status" => "exists", "message" => nil) if versions.include?(gem["version"])
|
|
176
|
-
|
|
177
|
-
status = versions.empty? ? "install" : "update"
|
|
178
|
-
return base.merge("status" => "would_#{status}", "message" => nil) if @dry_run
|
|
179
|
-
|
|
180
|
-
failure = install(gem, gem_home)
|
|
181
|
-
return base.merge("status" => "failed", "message" => failure) if failure
|
|
182
|
-
|
|
183
|
-
if status == "install"
|
|
184
|
-
base.merge("status" => "installed", "message" => nil)
|
|
185
|
-
else
|
|
186
|
-
base.merge("status" => "updated", "message" => nil)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Post-presence convergence (task 8xs.t.pu0.0): the declared
|
|
191
|
-
# version being present is necessary but not sufficient.
|
|
192
|
-
# 1. NEWER versions of the declared gem sitting beside the
|
|
193
|
-
# declared one would win RubyGems activation and take over
|
|
194
|
-
# the runtime's CLI — the manifest is the single owner of
|
|
195
|
-
# the declared set, so convergence removes them from the
|
|
196
|
-
# target runtime.
|
|
197
|
-
# 2. The dependency closure must actually resolve in the
|
|
198
|
-
# runtime: a gem whose version already figures in
|
|
199
|
-
# `gem list` can still miss a transitive runtime
|
|
200
|
-
# dependency (the manually-repaired `json` class of gap);
|
|
201
|
-
# missing dependencies are converged by installing them
|
|
202
|
-
# at the parent's requirement, and an unsatisfiable
|
|
203
|
-
# dependency is a classified failure — never readiness.
|
|
204
|
-
# A dry run predicts both without writing (would_uninstall /
|
|
205
|
-
# would_install) and stops recursion at the prediction.
|
|
206
|
-
def converge_post_presence(report, gem, gem_home)
|
|
207
|
-
removal = converge_newer_versions(gem, gem_home)
|
|
208
|
-
return report.merge("status" => "failed", "message" => removal) if removal.is_a?(String)
|
|
209
|
-
report["removed_newer_versions"] = removal
|
|
210
|
-
|
|
211
|
-
closure = converge_closure(gem, gem_home)
|
|
212
|
-
return report.merge("status" => "failed", "message" => closure) if closure.is_a?(String)
|
|
213
|
-
report["closure"] = closure
|
|
214
|
-
permissions = GemPayloadPermissions.new(gem_home: gem_home, dry_run: @dry_run,
|
|
215
|
-
privileged: @privileged.call).call(name: gem["name"], version: gem["version"])
|
|
216
|
-
report["permissions"] = permissions
|
|
217
|
-
report
|
|
218
|
-
rescue GemPayloadPermissions::Error => e
|
|
219
|
-
report.merge("status" => "failed", "message" => e.message)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# @return [Array<Hash>, String] per-version removal reports or a
|
|
223
|
-
# classified failure message
|
|
224
|
-
def converge_newer_versions(gem, gem_home)
|
|
225
|
-
kind, versions_or_message = runtime_versions(name: gem["name"], gem_home: gem_home)
|
|
226
|
-
return versions_or_message if kind != :ok
|
|
227
|
-
|
|
228
|
-
declared = Gem::Version.new(gem["version"])
|
|
229
|
-
newer = versions_or_message.select { |version| Gem::Version.new(version) > declared }
|
|
230
|
-
return [] if newer.empty?
|
|
231
|
-
|
|
232
|
-
return newer.map { |version| {"version" => version, "status" => "would_uninstall"} } if @dry_run
|
|
233
|
-
|
|
234
|
-
newer.map do |version|
|
|
235
|
-
result = @runner.call(runtime_argv(gem_home,
|
|
236
|
-
["gem", "uninstall", gem["name"], "--version", version, "-x", "--force"]))
|
|
237
|
-
if result["status"] != "ok"
|
|
238
|
-
break command_failure("gem uninstall #{gem["name"]} #{version}", result)
|
|
239
|
-
end
|
|
240
|
-
{"version" => version, "status" => "uninstalled"}
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Breadth-first walk of the gem's RUNTIME dependency closure in
|
|
245
|
-
# the target runtime. `gem dependency` output is plain text (no
|
|
246
|
-
# marshaled objects), development dependencies are ignored.
|
|
247
|
-
# @return [Array<Hash>, String] per-dependency convergence
|
|
248
|
-
# actions or a classified failure message
|
|
249
|
-
def converge_closure(gem, gem_home)
|
|
250
|
-
queue = [[gem["name"], gem["version"]]]
|
|
251
|
-
seen = {}
|
|
252
|
-
actions = []
|
|
253
|
-
until queue.empty?
|
|
254
|
-
name, version = queue.shift
|
|
255
|
-
next if seen[[name, version]]
|
|
256
|
-
|
|
257
|
-
seen[[name, version]] = true
|
|
258
|
-
deps = runtime_dependencies(name: name, version: version, gem_home: gem_home)
|
|
259
|
-
return deps if deps.is_a?(String)
|
|
260
|
-
|
|
261
|
-
deps.each do |dep|
|
|
262
|
-
kind, versions_or_message = runtime_versions(name: dep["name"], gem_home: gem_home)
|
|
263
|
-
return versions_or_message if kind != :ok
|
|
264
|
-
|
|
265
|
-
satisfying = satisfying_versions(dep, versions_or_message)
|
|
266
|
-
if satisfying.empty?
|
|
267
|
-
if @dry_run
|
|
268
|
-
actions << {"name" => dep["name"], "requirement" => dep["requirement"],
|
|
269
|
-
"status" => "would_install"}
|
|
270
|
-
next # recursion stops at the prediction
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
failure = install_dependency(dep, gem_home)
|
|
274
|
-
return failure if failure
|
|
275
|
-
|
|
276
|
-
kind, versions_or_message = runtime_versions(name: dep["name"], gem_home: gem_home)
|
|
277
|
-
return versions_or_message if kind != :ok
|
|
278
|
-
satisfying = satisfying_versions(dep, versions_or_message)
|
|
279
|
-
if satisfying.empty?
|
|
280
|
-
return "dependency '#{dep["name"]} (#{dep["requirement"]})' required by gem " \
|
|
281
|
-
"'#{name}' is not satisfiable in runtime '#{gem_home}' after install; " \
|
|
282
|
-
"check the gem source or the manifest declaration"
|
|
283
|
-
end
|
|
284
|
-
actions << {"name" => dep["name"], "requirement" => dep["requirement"],
|
|
285
|
-
"status" => "installed"}
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
chosen = satisfying.max_by { |v| Gem::Version.new(v) }
|
|
289
|
-
queue << [dep["name"], chosen]
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
actions
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
# Installs a missing dependency at the parent requirement: the
|
|
296
|
-
# requirement string the runtime's own resolver would accept
|
|
297
|
-
# (`gem install name -v "~> 2.7"`).
|
|
298
|
-
# @return [String, nil] classified failure, nil on success
|
|
299
|
-
def install_dependency(dep, gem_home)
|
|
300
|
-
result = @runner.call(runtime_argv(gem_home,
|
|
301
|
-
["gem", "install", dep["name"], "--version", dep["requirement"], "--no-document"]))
|
|
302
|
-
return nil if result["status"] == "ok"
|
|
303
|
-
|
|
304
|
-
command_failure("gem install #{dep["name"]}", result)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
227
|
# The runtime (non-development) dependencies of an installed
|
|
308
228
|
# gem, from `gem dependency <name> --version <v>` plain-text
|
|
309
229
|
# output: "dep (~> 2.0, runtime)" lines under the gem header.
|
|
@@ -344,19 +264,6 @@ module Ace
|
|
|
344
264
|
end
|
|
345
265
|
end
|
|
346
266
|
|
|
347
|
-
# Installs the EXACT declared version into the runtime. Sources
|
|
348
|
-
# are the gem command's own defaults — fetching from the
|
|
349
|
-
# configured gem source is provisioning network access, the same
|
|
350
|
-
# envelope as cloning a declared repository remote.
|
|
351
|
-
# @return [String, nil] the classified failure, nil on success
|
|
352
|
-
def install(gem, gem_home)
|
|
353
|
-
result = @runner.call(runtime_argv(gem_home,
|
|
354
|
-
["gem", "install", gem["name"], "--version", gem["version"], "--no-document"]))
|
|
355
|
-
return nil if result["status"] == "ok"
|
|
356
|
-
|
|
357
|
-
command_failure("gem install", result)
|
|
358
|
-
end
|
|
359
|
-
|
|
360
267
|
# Environment variables that would drag the INVOKING process's
|
|
361
268
|
# bundler context into the gem subprocess: RUBYOPT carries
|
|
362
269
|
# `-rbundler/setup` under `bundle exec ace-lab` (the documented
|
|
@@ -375,7 +282,7 @@ module Ace
|
|
|
375
282
|
# sees ONLY the target runtime.
|
|
376
283
|
def runtime_argv(gem_home, argv)
|
|
377
284
|
["env"] +
|
|
378
|
-
BUNDLER_ENV_KEYS.flat_map { |key| ["-u", key] } +
|
|
285
|
+
(BUNDLER_ENV_KEYS + ENV.keys.grep(/\ABUNDLER?_/)).uniq.flat_map { |key| ["-u", key] } +
|
|
379
286
|
["GEM_HOME=#{gem_home}", "GEM_PATH=#{gem_home}"] + argv
|
|
380
287
|
end
|
|
381
288
|
|
|
@@ -115,11 +115,11 @@ module Ace
|
|
|
115
115
|
"(trusted grants are configured only for a converged install)"}
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
verify_report =
|
|
118
|
+
verify_report = {"ready" => nil, "status" => "not_run",
|
|
119
|
+
"message" => "Diagnostics are explicit: run ace-lab verify when needed."}
|
|
119
120
|
|
|
120
121
|
{
|
|
121
|
-
"ok" => prepare_ok && projects_ok && grants["status"] != "failed"
|
|
122
|
-
verify_stage_ok?(verify_report),
|
|
122
|
+
"ok" => prepare_ok && projects_ok && grants["status"] != "failed",
|
|
123
123
|
"dry_run" => effective_dry_run,
|
|
124
124
|
"machine" => manifest["machine"],
|
|
125
125
|
"manifest" => manifest_path,
|
|
@@ -135,35 +135,6 @@ module Ace
|
|
|
135
135
|
|
|
136
136
|
private
|
|
137
137
|
|
|
138
|
-
# Final install verification (LAB-3): read-only, over the
|
|
139
|
-
# converged artifacts. A wet run's readiness gates `ok`; a dry
|
|
140
|
-
# run has nothing converged to verify and reports the stage
|
|
141
|
-
# skipped instead of a guaranteed failure.
|
|
142
|
-
def verify_stage(manifest, dry_run)
|
|
143
|
-
return {"ready" => nil, "status" => "skipped",
|
|
144
|
-
"message" => "verification runs on the converged lab; dry-run predicts the " \
|
|
145
|
-
"steps above only"} if dry_run
|
|
146
|
-
|
|
147
|
-
verify_organism.call
|
|
148
|
-
rescue Ace::Lab::InvalidConfigurationError => e
|
|
149
|
-
{"ready" => false, "status" => "failed",
|
|
150
|
-
"message" => "final verification could not run: #{e.message}"}
|
|
151
|
-
rescue StandardError => e
|
|
152
|
-
{"ready" => false, "status" => "failed",
|
|
153
|
-
"message" => "final verification crashed: #{e.class}: #{e.message}"}
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def verify_stage_ok?(report)
|
|
157
|
-
report["ready"] != false
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def verify_organism
|
|
161
|
-
@verify || Verify.new(
|
|
162
|
-
manifest_path: manifest_path, runner: @runner, account_lookup: @account_lookup,
|
|
163
|
-
authorization_path: grants_molecule.authorization_path, platform: platform
|
|
164
|
-
)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
138
|
# The project's target executor account: the owner of the
|
|
168
139
|
# declared directory the project lives in (longest prefix
|
|
169
140
|
# match; a directory declared AS the exact project path matches
|
|
@@ -613,6 +613,9 @@ module Ace
|
|
|
613
613
|
reason = gem_version_reason(gem, versions, gem_home)
|
|
614
614
|
return [FAIL, reason] if reason
|
|
615
615
|
|
|
616
|
+
bundle = provisioner.bundle_status(name: gem["name"], version: gem["version"], gem_home: gem_home)
|
|
617
|
+
return [bundle.first == :ok ? PASS : FAIL, bundle.last] if bundle
|
|
618
|
+
|
|
616
619
|
newer_kind, newer_or_message = provisioner.newer_versions(
|
|
617
620
|
name: gem["name"], declared_version: gem["version"], gem_home: gem_home
|
|
618
621
|
)
|
data/lib/ace/lab/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
@@ -9,6 +9,26 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 2026-11-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.6'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '2.6'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3'
|
|
12
32
|
- !ruby/object:Gem::Dependency
|
|
13
33
|
name: ace-support-cli
|
|
14
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -85,20 +105,6 @@ dependencies:
|
|
|
85
105
|
- - "~>"
|
|
86
106
|
- !ruby/object:Gem::Version
|
|
87
107
|
version: '0.14'
|
|
88
|
-
- !ruby/object:Gem::Dependency
|
|
89
|
-
name: bundler
|
|
90
|
-
requirement: !ruby/object:Gem::Requirement
|
|
91
|
-
requirements:
|
|
92
|
-
- - "~>"
|
|
93
|
-
- !ruby/object:Gem::Version
|
|
94
|
-
version: '2.0'
|
|
95
|
-
type: :development
|
|
96
|
-
prerelease: false
|
|
97
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
-
requirements:
|
|
99
|
-
- - "~>"
|
|
100
|
-
- !ruby/object:Gem::Version
|
|
101
|
-
version: '2.0'
|
|
102
108
|
- !ruby/object:Gem::Dependency
|
|
103
109
|
name: minitest
|
|
104
110
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -172,6 +178,7 @@ files:
|
|
|
172
178
|
- LICENSE
|
|
173
179
|
- README.md
|
|
174
180
|
- Rakefile
|
|
181
|
+
- docs/runtime-bundle.md
|
|
175
182
|
- docs/usage.md
|
|
176
183
|
- exe/ace-lab
|
|
177
184
|
- lib/ace/lab.rb
|