bundler-spinel 0.3.0 → 0.4.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 +56 -0
- data/lib/bundler/spinel/cli.rb +23 -4
- data/lib/bundler/spinel/engine.rb +11 -1
- data/lib/bundler/spinel/history.rb +26 -0
- data/lib/bundler/spinel/localizer.rb +11 -9
- data/lib/bundler/spinel/probe.rb +51 -5
- data/lib/bundler/spinel/vendorer.rb +118 -25
- data/lib/bundler/spinel/verifier.rb +25 -4
- data/lib/bundler/spinel/version.rb +8 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8518bfe1ec1303fb7116e33738abb8246bb3c7072afca1b70057523f824b4a2c
|
|
4
|
+
data.tar.gz: 5d945e9ca4fa4409f5ff325a6eae9d917469e5c0e7ceb2ef69c8c356551c0fda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff071af5cde1af69f32f92b509092ec6d535173dfbbdbe84c02940b5118abb1f7cafacc610cc26a4c3e20263426e4a7422560ba166c766ff296d391be1758e34
|
|
7
|
+
data.tar.gz: e704f7c8a825a0cdb278c604a86bb38fd9363ac0fe8fe7406c88c52e9b77e1a76cd2675036d503b3e80fe488f27f3ff1820c7803428989f7ac1930bdaff9a412
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ All notable changes to `bundler-spinel` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
5
|
project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] — 2026-06-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Probe: a require-only compile failure is the load-path limit, not
|
|
11
|
+
`analyze-failed` (spinel master b60fbd7 absorption).** Since matz/spinel's
|
|
12
|
+
#1383 fix, an unresolvable plain `require "gem/sub"` makes `spinel -c` exit
|
|
13
|
+
non-zero instead of warning and continuing. Because that idiom is
|
|
14
|
+
near-universal, the b60fbd7 corpus reprobe spuriously turned thousands of
|
|
15
|
+
previously-`clean` gems into `rejected:analyze-failed`. The probe now detects
|
|
16
|
+
a compile that failed *solely* on an unresolvable `require` (the
|
|
17
|
+
`CallNode \`require\`` form, with no real codegen error and no other
|
|
18
|
+
unsupported call) and classifies it `risky [load-path:require]` — the
|
|
19
|
+
documented no-load-path limitation a real Spinel project resolves by
|
|
20
|
+
vendoring. Real codegen errors still reject. +`test/probe_classify_test.rb`.
|
|
21
|
+
- **Engine rev detection for worktrees + frozen copies.** `Engine#rev` fell
|
|
22
|
+
back to an opaque `bin:<hash>` (a) in a git *worktree*, where `.git` is a
|
|
23
|
+
file not a directory, and (b) for a frozen/detached engine copy with no
|
|
24
|
+
`.git`. Both now resolve to `git:<short-sha>`: worktree `.git` files are
|
|
25
|
+
honored (`File.exist?`, not `File.directory?`), and a `.spinel_rev` stamp
|
|
26
|
+
(a bare HEAD sha written at freeze time) is read first. Keeps the ledger
|
|
27
|
+
key — and the history/diff tooling that joins on it — consistent across the
|
|
28
|
+
build layouts a reprobe actually uses. `bin/reprobe-corpus.sh`'s frozen
|
|
29
|
+
engines now carry the stamp.
|
|
30
|
+
- **cmake build-units on macOS (spinelgems#21).** The Vendorer now sets the
|
|
31
|
+
SDK libc++ include path (`CPLUS_INCLUDE_PATH` via `xcrun --show-sdk-path`)
|
|
32
|
+
for cmake build-unit invocations on Darwin — without it every C++ unit died
|
|
33
|
+
with `fatal error: 'array' file not found` (toy#27's Mac cold-start LIB
|
|
34
|
+
leg; fix verified on an M2). Caller-set `CPLUS_INCLUDE_PATH` is preserved
|
|
35
|
+
(SDK path appends); missing/failing `xcrun` degrades to the previous
|
|
36
|
+
behavior.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **Opt-in build-units + variant build dirs (spinelgems#20).** An `optional`
|
|
40
|
+
entry may declare `"default": "disabled"`: a plain `vendor` skips its build
|
|
41
|
+
entirely (placeholder ← `disabled_cflags`) until the consumer enables it via
|
|
42
|
+
`--with-ext NAME` / `SPINEL_EXT_ENABLE` — what a CUDA unit needs to not
|
|
43
|
+
attempt nvcc configures on a CUDA-less box. Explicit disable beats enable;
|
|
44
|
+
unknown enable names warn loud. cmake units take `"build_dir"` (default
|
|
45
|
+
`"build"`, relative-only) so two entries can build one shared source dir
|
|
46
|
+
into variant trees (`build/` vs `build-cuda/`), and source dirs are copied
|
|
47
|
+
once per vendor run — a second entry sharing `dir` no longer wipes the first
|
|
48
|
+
unit's just-built artifacts. Unblocks toy merging its staged
|
|
49
|
+
`spinel-ext-gpu.json` into `spinel-ext.json` (toy#45 Phase 3 → publish).
|
|
50
|
+
- **A gem's `sig/*.rbs` acts as the type root (spinelgems#13).** Spinel infers
|
|
51
|
+
param types from call sites (whole-program, closed-world), so an *uncalled*
|
|
52
|
+
public method widens to `int`/`poly` — the failure mode hand-written seed
|
|
53
|
+
blocks exist to patch. `--rbs` was verified to re-pin uncalled methods' param,
|
|
54
|
+
return, and ivar types, so the tooling now treats a shipped `sig/` tree as
|
|
55
|
+
authoritative: `spinel-compat verify` auto-passes `--rbs <gem>/sig` when the
|
|
56
|
+
gem ships signatures (`--rbs DIR` to override, `--no-rbs` to opt out), and
|
|
57
|
+
records an `rbs:sig` provenance tag in the verdict reasons. `vendor` copies
|
|
58
|
+
each gem's `sig/` alongside `lib/`, aggregates all of them under
|
|
59
|
+
`<into>/sig/<gem>/`, and advertises the single `--rbs <into>/sig` root in
|
|
60
|
+
`deps.rb` and the CLI output. A spinel-native gem (SpinelKit, the Tep
|
|
61
|
+
batteries) ships one standard Ruby artifact — no seed soup, no manifest keys.
|
|
62
|
+
|
|
7
63
|
## [0.3.0] — 2026-06-08
|
|
8
64
|
|
|
9
65
|
### Added
|
data/lib/bundler/spinel/cli.rb
CHANGED
|
@@ -146,7 +146,11 @@ module Bundler
|
|
|
146
146
|
smoke = (j = argv.index("--smoke")) ? argv.delete_at(j + 1).tap { argv.delete_at(j) } : nil
|
|
147
147
|
full = !!argv.delete("--full")
|
|
148
148
|
tests = !!argv.delete("--tests")
|
|
149
|
-
|
|
149
|
+
# --rbs DIR / --no-rbs: explicit type root / opt out. Default :auto —
|
|
150
|
+
# use the gem's own sig/*.rbs when shipped (spinelgems#13).
|
|
151
|
+
rbs = (k = argv.index("--rbs")) ? argv.delete_at(k + 1).tap { argv.delete_at(k) } : :auto
|
|
152
|
+
rbs = false if argv.delete("--no-rbs")
|
|
153
|
+
name = argv.shift or raise Error, "usage: spinel-compat verify NAME [VERSION] [--dir PATH] [--smoke FILE | --tests] [--full] [--rbs DIR | --no-rbs]"
|
|
150
154
|
engine = Engine.new
|
|
151
155
|
if dir
|
|
152
156
|
version = argv.shift || "path"
|
|
@@ -164,7 +168,7 @@ module Bundler
|
|
|
164
168
|
smoke = File.join(Dir.tmpdir, "__spinel_tests_#{name}.rb")
|
|
165
169
|
File.write(smoke, runner)
|
|
166
170
|
end
|
|
167
|
-
v = Verifier.new(engine, Ledger.new).verify(name, version, gem_dir, smoke: smoke && File.expand_path(smoke), full: full)
|
|
171
|
+
v = Verifier.new(engine, Ledger.new).verify(name, version, gem_dir, smoke: smoke && File.expand_path(smoke), full: full, rbs: rbs)
|
|
168
172
|
print_verdict(v)
|
|
169
173
|
File.delete(smoke) if tests && smoke && File.exist?(smoke)
|
|
170
174
|
(v.verified? || v.loaded?) ? 0 : 1
|
|
@@ -186,13 +190,26 @@ module Bundler
|
|
|
186
190
|
ext_disable << argv.delete_at(d + 1).to_s
|
|
187
191
|
argv.delete_at(d)
|
|
188
192
|
end
|
|
193
|
+
# --with-ext NAME : opt IN to a default-disabled entry, e.g. a CUDA
|
|
194
|
+
# build-unit (repeatable; also SPINEL_EXT_ENABLE). spinelgems#20.
|
|
195
|
+
ext_enable = []
|
|
196
|
+
while (w = argv.index("--with-ext"))
|
|
197
|
+
ext_enable << argv.delete_at(w + 1).to_s
|
|
198
|
+
argv.delete_at(w)
|
|
199
|
+
end
|
|
189
200
|
lock = argv.shift || "Gemfile.lock"
|
|
190
201
|
raise Error, "no #{lock}; run `bundle lock` first" unless File.exist?(lock)
|
|
191
202
|
|
|
192
|
-
res = Vendorer.new.vendor(lock, into: into, ext_overrides: ext_overrides,
|
|
203
|
+
res = Vendorer.new.vendor(lock, into: into, ext_overrides: ext_overrides,
|
|
204
|
+
ext_disable: ext_disable, ext_enable: ext_enable)
|
|
193
205
|
ext = res[:extensions].to_i
|
|
194
206
|
@out.puts "vendored #{res[:count]} gem(s)#{ext.positive? ? " (+#{ext} C ext)" : ''} -> #{res[:into]}"
|
|
195
207
|
@out.puts " require_relative \"#{res[:into]}/deps\" from your Spinel entrypoint"
|
|
208
|
+
sigs = res[:sig_gems] || []
|
|
209
|
+
unless sigs.empty?
|
|
210
|
+
@out.puts " #{sigs.size} gem(s) ship sig/*.rbs type roots -> #{res[:into]}/sig"
|
|
211
|
+
@out.puts " compile with: spinel ... --rbs #{res[:into]}/sig (spinelgems#13)"
|
|
212
|
+
end
|
|
196
213
|
0
|
|
197
214
|
end
|
|
198
215
|
|
|
@@ -469,7 +486,9 @@ module Bundler
|
|
|
469
486
|
spinel-compat probe NAME [VERSION] probe one gem, record a verdict
|
|
470
487
|
spinel-compat why NAME [--probe] legible "why doesn't this work (yet)?" report
|
|
471
488
|
spinel-compat verify NAME [--smoke F] differential CRuby-vs-Spinel run -> verified
|
|
472
|
-
|
|
489
|
+
[--rbs DIR | --no-rbs] type root: gem's sig/*.rbs (auto when shipped)
|
|
490
|
+
spinel-compat vendor [LOCK] [--into D] place deps where Spinel finds them + deps.rb (+ sig/ roots)
|
|
491
|
+
[--with-ext NAME | --no-ext NAME] opt in/out of optional C-ext units
|
|
473
492
|
spinel-compat check [LOCK] [--strict] gate a Gemfile.lock (exit 1 if rejected)
|
|
474
493
|
spinel-compat survey GEM... | --list F wholesale review -> reason histogram
|
|
475
494
|
spinel-compat serve --store DIR curated source (only vetted gems)
|
|
@@ -76,7 +76,17 @@ module Bundler
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
78
|
def compute_rev
|
|
79
|
-
|
|
79
|
+
# A frozen/detached engine copy (bin/reprobe-corpus.sh's
|
|
80
|
+
# spinel-frozen-<rev>, or a build whose .git was stripped) can carry a
|
|
81
|
+
# `.spinel_rev` stamp — a bare HEAD sha written at freeze time — so it
|
|
82
|
+
# still keys to its real git rev instead of an opaque binary hash.
|
|
83
|
+
stamp = File.join(@dir, ".spinel_rev")
|
|
84
|
+
if File.file?(stamp) && !(s = File.read(stamp).strip[0, 12].to_s).empty?
|
|
85
|
+
return "git:#{s}"
|
|
86
|
+
end
|
|
87
|
+
# `.git` is a *file* in a git worktree (a gitdir pointer), not a dir —
|
|
88
|
+
# File.exist? covers both so worktree checkouts report git:, not bin:.
|
|
89
|
+
if File.exist?(File.join(@dir, ".git"))
|
|
80
90
|
sha = capture("git", "-C", @dir, "rev-parse", "--short", "HEAD")
|
|
81
91
|
if sha && !sha.empty?
|
|
82
92
|
dirty = capture("git", "-C", @dir, "status", "--porcelain")
|
|
@@ -83,6 +83,32 @@ module Bundler
|
|
|
83
83
|
"to Ruby source lines via <code>#line</code>, on by default — the " \
|
|
84
84
|
"<a href=\"https://github.com/matz/spinel/issues/1338\">#1338</a> RFC direction. The behaviour-verified " \
|
|
85
85
|
"tier reached 144 mechanical ★ this run." },
|
|
86
|
+
{ rev: "cb23cc6", date: "2026-06-09", commit: "~23 commits — 12 harness-filed issues closed in one wave, incl. Mutex/Monitor#synchronize + Thread.new now running and carrying their block value (#1360), class-instance-vars in class methods (#1352), is_a?(IncludedModule)/ancestors (#1350), \\h/\\H regex (#1349), reopened-builtin optional defaults (#1348), bitwise/shift operator mangling (#1358/#1368), the captured-&block value/type family",
|
|
87
|
+
file: "survey-cb23cc6/compat.jsonl",
|
|
88
|
+
note: "<strong>Mutex/Thread come in from the cold.</strong> With matz/spinel#1360 landing " \
|
|
89
|
+
"(<code>Mutex#synchronize</code>/<code>Thread.new</code> now run single-threaded and carry their " \
|
|
90
|
+
"block's value), the static pre-filter no longer hard-rejects them — they're flagged " \
|
|
91
|
+
"<code>risky</code> (correct for defensive use, degenerate only for true concurrency). Re-probing " \
|
|
92
|
+
"the 8,833 gems that had been rejected on sight for <code>Mutex.new</code>/<code>Thread.new</code>, " \
|
|
93
|
+
"<strong>3,493 (39.5%) moved out of <code>rejected</code></strong> — they compile now; the static " \
|
|
94
|
+
"wall had been hiding it. The four spine gems (<code>bundler</code>/<code>rake</code>/" \
|
|
95
|
+
"<code>minitest</code>/<code>thor</code>) now reject for their <em>real</em> reason — the deep " \
|
|
96
|
+
"metaprogramming surface (<code>send</code>/<code>method_missing</code>/<code>cattr_accessor</code>) — " \
|
|
97
|
+
"not a misleading <code>hard:Mutex.new</code>. The differential re-audit also caught one regression in " \
|
|
98
|
+
"the wave: returning <code>self</code> from a reopened-builtin method " \
|
|
99
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1386\">#1386</a>, <code>to-bool</code>), filed same day." },
|
|
100
|
+
{ rev: "b60fbd7", date: "2026-06-15", commit: "636 commits — the largest wave yet. A type-inference + codegen rewrite (typed-array/poly ~222, inference/cast/box ~168, string ~124, block/yield ~101, module/singleton ~94) closing many harness-filed issues at once: reopened-builtin self (#1386), hash-block String type (#1382/#1394), circular require_relative (#1373), computed require (#1383), class-method yield (#1387), Scheduled-server boot + SIGTERM (#1369/#1384). New surfaces: value-type objects, --rbs/--emit-rbs/--emit-symbol-map.",
|
|
101
|
+
file: "survey-b60fbd7/compat.jsonl",
|
|
102
|
+
note: "<strong>The largest movement in the catalog's history.</strong> 636 upstream commits — a type-inference " \
|
|
103
|
+
"rewrite that resolved the entire <code>unresolved:<method></code> reject family (~60k records → ~0): " \
|
|
104
|
+
"<strong>~49,000 gems moved out of <code>rejected</code></strong> (−46%), +22k to <code>clean</code>. The " \
|
|
105
|
+
"wave also changed unresolvable <code>require</code> to hard-fail the compile (post-#1383); because " \
|
|
106
|
+
"<code>require \"gem/version\"</code> is near-universal that spuriously rejected thousands, so the probe " \
|
|
107
|
+
"now classifies a require-only failure as the no-load-path limit (<code>risky [load-path:require]</code>, " \
|
|
108
|
+
"9,716 gems corrected). Cost side, caught by re-verifying every ★ at this rev: <strong>~34 of 175 " \
|
|
109
|
+
"behaviour-verified gems regressed</strong> (12 miscompiles incl. <code>after_commit_action</code>, 22 " \
|
|
110
|
+
"codegen incl. bare-<code>super</code>/<code>ForwardingSuperNode</code>) — the trust tier dropped 175→136. " \
|
|
111
|
+
"Fresh-verify overrode stickiness, so the ★ count is honest, not carried." },
|
|
86
112
|
].freeze
|
|
87
113
|
|
|
88
114
|
ORDER = %w[clean risky rejected].freeze
|
|
@@ -65,17 +65,19 @@ module Bundler
|
|
|
65
65
|
file.empty? ? "line #{v['line']}" : "#{file}:#{v['line']}"
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# Resolve bisect.sh. It lives in spinel-dev (separate from
|
|
69
|
-
# engine), so there's no fixed relation to the engine dir —
|
|
70
|
-
# conventional spots, newest-intent first: an explicit override,
|
|
71
|
-
#
|
|
68
|
+
# Resolve bisect.sh. It lives in the spinel-dev tooling repo (separate from
|
|
69
|
+
# the compiler engine), so there's no fixed relation to the engine dir —
|
|
70
|
+
# probe a few conventional spots, newest-intent first: an explicit override,
|
|
71
|
+
# a sibling checkout next to the engine, the conventional ~/sites layout.
|
|
72
|
+
# The local checkout may be named spinel-tools (gx10) or spinel-dev (the
|
|
73
|
+
# GitHub repo name), so probe both.
|
|
72
74
|
def bisect_script
|
|
73
75
|
rel = "tools/value-bisect/bisect.sh"
|
|
74
|
-
candidates = [
|
|
75
|
-
|
|
76
|
-
File.expand_path(File.join(@engine.dir, "..",
|
|
77
|
-
File.expand_path("~/sites
|
|
78
|
-
|
|
76
|
+
candidates = [ENV["SPINEL_BISECT"]]
|
|
77
|
+
%w[spinel-tools spinel-dev].each do |repo|
|
|
78
|
+
candidates << File.expand_path(File.join(@engine.dir, "..", repo, rel))
|
|
79
|
+
candidates << File.expand_path("~/sites/#{repo}/#{rel}")
|
|
80
|
+
end
|
|
79
81
|
candidates.find { |c| c && File.exist?(c) }
|
|
80
82
|
end
|
|
81
83
|
end
|
data/lib/bundler/spinel/probe.rb
CHANGED
|
@@ -37,6 +37,25 @@ module Bundler
|
|
|
37
37
|
# and stdlib deps surface here. Informational, NOT a standalone reject:
|
|
38
38
|
# it's as often a probe limitation as a real incompatibility.
|
|
39
39
|
UNRESOLVED_REQUIRE = /require "([^"]+)" could not be resolved/.freeze
|
|
40
|
+
# Since the #1383 fix, an unresolvable plain `require` no longer warns +
|
|
41
|
+
# continues (exit 0) — it's emitted as an unsupported CallNode and the
|
|
42
|
+
# compile EXITS NON-ZERO. On the whole 189k corpus that turned ~thousands
|
|
43
|
+
# of previously-`clean` gems (the universal `require "gem/version"` idiom)
|
|
44
|
+
# into spurious `analyze-failed`. We detect this exact form so a
|
|
45
|
+
# require-ONLY compile failure is classified as the no-load-path
|
|
46
|
+
# limitation, not a codegen failure. (b60fbd7; see harness/findings.)
|
|
47
|
+
REQUIRE_CALL_FAIL = /unsupported call:.*CallNode `require`/.freeze
|
|
48
|
+
# A genuine codegen/compile failure — the C compiler choked, the analyzer
|
|
49
|
+
# died, or an unsupported construct OTHER than a plain require. Presence of
|
|
50
|
+
# any of these means the failure is real, not just the load-path limit.
|
|
51
|
+
HARD_COMPILE_ERROR = %r{
|
|
52
|
+
out\.c:\d+.*\berror: | # gcc error on emitted C
|
|
53
|
+
\bfatal\b |
|
|
54
|
+
\bSegmentation\ fault\b |
|
|
55
|
+
\banalyze\ failed\b |
|
|
56
|
+
unsupported\ puts\ argument | # an unsupported non-require construct
|
|
57
|
+
unsupported\ call:\ (?!.*CallNode\ `require`) # any non-require unsupported call
|
|
58
|
+
}ix.freeze
|
|
40
59
|
ANALYZE_FAILED = /\b(analyze failed|fatal)\b/i.freeze
|
|
41
60
|
|
|
42
61
|
# Spinel's analyze pass can spin for minutes on pathological inputs (no
|
|
@@ -53,15 +72,26 @@ module Bundler
|
|
|
53
72
|
# Spinel will never support (threads, Mutex, TracePoint). Metaprogramming
|
|
54
73
|
# tokens like `define_method` stay in RISK_TOKENS below — they degrade
|
|
55
74
|
# silently, so the compile signal is still the right call there.
|
|
75
|
+
# Constructs that put a gem outside the AOT closed-world model entirely —
|
|
76
|
+
# rejected from a static scan, before a compile is even attempted.
|
|
77
|
+
# Thread/Mutex lived here until matz/spinel#1360 made them *run* (single-
|
|
78
|
+
# threaded, carrying the block's value): they're now compiled + flagged
|
|
79
|
+
# `risky` (below), not hard-rejected. TracePoint/set_trace_func stay —
|
|
80
|
+
# there is no degenerate-but-correct lowering for runtime tracing.
|
|
56
81
|
HARD_REJECT_TOKENS = {
|
|
57
|
-
/\
|
|
58
|
-
/\
|
|
59
|
-
/\bMutex_m\b/ => "Mutex_m",
|
|
60
|
-
/\bTracePoint\b/ => "TracePoint"
|
|
82
|
+
/\bTracePoint\b/ => "TracePoint",
|
|
83
|
+
/\bset_trace_func\b/ => "set_trace_func"
|
|
61
84
|
}.freeze
|
|
62
85
|
|
|
63
86
|
# token => reason. Tokens Spinel cannot honour and may silently no-op.
|
|
64
87
|
RISK_TOKENS = {
|
|
88
|
+
# Thread/Mutex run single-threaded since matz/spinel#1360 — correct for
|
|
89
|
+
# defensive use (a mutex guarding state, Thread.new for a value), but
|
|
90
|
+
# degenerate for genuine concurrency: compiles, flagged, fails
|
|
91
|
+
# `check --strict`. (Demoted from HARD_REJECT after #1360.)
|
|
92
|
+
/\bThread\.(new|start|fork)\b/ => "thread",
|
|
93
|
+
/\bMutex\.new\b/ => "mutex",
|
|
94
|
+
/\bMutex_m\b/ => "mutex_m",
|
|
65
95
|
/\beval\s*\(/ => "eval",
|
|
66
96
|
/\binstance_eval\b/ => "instance_eval",
|
|
67
97
|
/\b(class|module)_eval\b/ => "class_eval",
|
|
@@ -108,6 +138,13 @@ module Bundler
|
|
|
108
138
|
elsif sig[:timed_out]
|
|
109
139
|
# Analyzer ran past the cap — pathological for Spinel, not the gem.
|
|
110
140
|
["rejected", ["analyze-timeout"]]
|
|
141
|
+
elsif sig[:require_only_fail]
|
|
142
|
+
# Compile failed solely on an unresolvable plain `require` (b60fbd7
|
|
143
|
+
# hard-fails these where cb23cc6 warned + continued). That's the
|
|
144
|
+
# no-load-path limitation, not a codegen failure — a real Spinel
|
|
145
|
+
# project vendors deps so the require resolves. Classify as the
|
|
146
|
+
# load-path limit (risky), keeping the needs: notes below.
|
|
147
|
+
["risky", ["load-path:require"]]
|
|
111
148
|
elsif sig[:analyze_failed] || !sig[:exit_ok]
|
|
112
149
|
["rejected", ["analyze-failed"]]
|
|
113
150
|
elsif !static_only_risks(risks).empty?
|
|
@@ -134,18 +171,27 @@ module Bundler
|
|
|
134
171
|
analyze_failed = false
|
|
135
172
|
exit_ok = true
|
|
136
173
|
timed_out = false
|
|
174
|
+
require_fail = false # saw the new unresolvable-require hard-fail form
|
|
175
|
+
hard_error = false # saw a genuine codegen/analyzer failure
|
|
137
176
|
Dir.mktmpdir do |tmp|
|
|
138
177
|
entries.each do |f|
|
|
139
178
|
out, ok, hit_timeout = run_spinel(f, File.join(tmp, "out.c"))
|
|
140
179
|
exit_ok &&= ok
|
|
141
180
|
timed_out ||= hit_timeout
|
|
142
181
|
analyze_failed ||= out =~ ANALYZE_FAILED ? true : false
|
|
182
|
+
require_fail ||= out =~ REQUIRE_CALL_FAIL ? true : false
|
|
183
|
+
hard_error ||= out =~ HARD_COMPILE_ERROR ? true : false
|
|
143
184
|
out.scan(UNRESOLVED_CALL) { |m| calls << m[0] }
|
|
144
185
|
out.scan(UNRESOLVED_REQUIRE) { |m| requires << m[0] }
|
|
145
186
|
end
|
|
146
187
|
end
|
|
188
|
+
# A compile that failed ONLY because a plain `require` is unresolvable
|
|
189
|
+
# (no real codegen error, no other unsupported call) is the documented
|
|
190
|
+
# no-load-path limitation, not a failure of the gem's own code.
|
|
191
|
+
require_only_fail = !exit_ok && !timed_out && require_fail && !hard_error && calls.empty?
|
|
147
192
|
{ calls: calls.uniq, requires: requires.uniq,
|
|
148
|
-
analyze_failed: analyze_failed, exit_ok: exit_ok, timed_out: timed_out
|
|
193
|
+
analyze_failed: analyze_failed, exit_ok: exit_ok, timed_out: timed_out,
|
|
194
|
+
require_only_fail: require_only_fail }
|
|
149
195
|
end
|
|
150
196
|
|
|
151
197
|
# Run `spinel -c FILE -o OUT_C` with a wall-clock cap. Returns
|
|
@@ -29,15 +29,24 @@ module Bundler
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def vendor(lockfile = "Gemfile.lock", into: "vendor/spinel", warn_incompatible: true,
|
|
32
|
-
ext_overrides: {}, ext_disable: [])
|
|
32
|
+
ext_overrides: {}, ext_disable: [], ext_enable: [])
|
|
33
33
|
parsed = Bundler::LockfileParser.new(File.read(lockfile))
|
|
34
34
|
lock_dir = File.dirname(File.expand_path(lockfile))
|
|
35
35
|
into = File.expand_path(into)
|
|
36
36
|
FileUtils.mkdir_p(into)
|
|
37
37
|
disable = (ext_disable + ENV["SPINEL_EXT_DISABLE"].to_s.split(",")).map(&:strip).reject(&:empty?)
|
|
38
|
+
enable = (ext_enable + ENV["SPINEL_EXT_ENABLE"].to_s.split(",")).map(&:strip).reject(&:empty?)
|
|
39
|
+
# Per-run state for build-units: copy-once over shared source dirs
|
|
40
|
+
# (spinelgems#20) + the optional-entry names actually seen, so a typo'd
|
|
41
|
+
# --with-ext warns loud instead of silently doing nothing.
|
|
42
|
+
@copied_build_dirs = {}
|
|
43
|
+
@ext_names_seen = []
|
|
38
44
|
|
|
39
45
|
manifest = []
|
|
40
46
|
exts = 0
|
|
47
|
+
sig_root = File.join(into, "sig")
|
|
48
|
+
FileUtils.rm_rf(sig_root)
|
|
49
|
+
sig_gems = []
|
|
41
50
|
# Topological order (dependencies before dependents), not the lockfile's
|
|
42
51
|
# alphabetical `specs` (spinelgems#19): Spinel has no load path, so
|
|
43
52
|
# deps.rb is a *flattened single load* — each gem's entrypoint
|
|
@@ -50,15 +59,20 @@ module Bundler
|
|
|
50
59
|
src = resolve_source(spec, lock_dir)
|
|
51
60
|
dest = File.join(into, name)
|
|
52
61
|
place(src, dest)
|
|
53
|
-
exts += wire_extensions(src, dest, ext_overrides, disable)
|
|
62
|
+
exts += wire_extensions(src, dest, ext_overrides, disable, enable)
|
|
63
|
+
sig_gems << name if collect_sig(dest, sig_root, name)
|
|
54
64
|
if (target = require_target(name, dest))
|
|
55
65
|
manifest << { require: target, libdir: "#{File.basename(dest)}/lib" }
|
|
56
66
|
end
|
|
57
67
|
note_compat(name, version) if warn_incompatible
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
unless (unknown = enable - @ext_names_seen).empty?
|
|
71
|
+
warn "[vendor] --with-ext/SPINEL_EXT_ENABLE names matched no optional " \
|
|
72
|
+
"entry in any vendored manifest: #{unknown.join(', ')}"
|
|
73
|
+
end
|
|
74
|
+
write_manifest(into, manifest, sig_gems)
|
|
75
|
+
{ into: into, count: manifest.size, extensions: exts, sig_gems: sig_gems }
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
# Order specs so every gem's runtime dependencies come before it — a DFS
|
|
@@ -108,12 +122,29 @@ module Bundler
|
|
|
108
122
|
def place(src, dest)
|
|
109
123
|
FileUtils.rm_rf(dest)
|
|
110
124
|
FileUtils.mkdir_p(dest)
|
|
111
|
-
|
|
125
|
+
# sig/ rides along with lib/: a gem's shipped *.rbs is its type root
|
|
126
|
+
# under Spinel (spinelgems#13) — verify --dir on the vendored copy
|
|
127
|
+
# auto-detects it, and vendor() aggregates one --rbs root from all gems.
|
|
128
|
+
%w[lib sig].each do |sub|
|
|
112
129
|
s = File.join(src, sub)
|
|
113
130
|
FileUtils.cp_r(s, dest) if File.directory?(s)
|
|
114
131
|
end
|
|
115
132
|
end
|
|
116
133
|
|
|
134
|
+
# Aggregate a vendored gem's sig/*.rbs under <into>/sig/<name>/ so the
|
|
135
|
+
# consumer build passes a single `--rbs <into>/sig` (the driver takes one
|
|
136
|
+
# DIR; spinel_rbs_extract walks it recursively). Without this, uncalled
|
|
137
|
+
# public methods in a library widen to int/poly — the seed-block failure
|
|
138
|
+
# mode spinelgems#13 retires. Returns true when the gem contributed sigs.
|
|
139
|
+
def collect_sig(dest, sig_root, name)
|
|
140
|
+
sig = File.join(dest, "sig")
|
|
141
|
+
return false if Dir[File.join(sig, "**", "*.rbs")].empty?
|
|
142
|
+
|
|
143
|
+
FileUtils.mkdir_p(sig_root)
|
|
144
|
+
FileUtils.cp_r(sig, File.join(sig_root, name))
|
|
145
|
+
true
|
|
146
|
+
end
|
|
147
|
+
|
|
117
148
|
# Build + wire any C extensions the gem declares in spinel-ext.json
|
|
118
149
|
# (OriPekelman/spinelgems#2; see docs/c-ext.md). Each entry substitutes its
|
|
119
150
|
# @PLACEHOLDER@ in the placed Ruby — so the gem's `ffi_cflags "@PLACEHOLDER@"`
|
|
@@ -125,8 +156,13 @@ module Bundler
|
|
|
125
156
|
# - any static `libs`.
|
|
126
157
|
# An `optional` entry the consumer opted out of (`name` in `disable` /
|
|
127
158
|
# SPINEL_EXT_DISABLE) substitutes its `disabled_cflags` instead (category C).
|
|
128
|
-
#
|
|
129
|
-
|
|
159
|
+
# An `optional` entry declaring `"default": "disabled"` is opt-IN
|
|
160
|
+
# (spinelgems#20 — CUDA/Metal units must not build, or fail to configure,
|
|
161
|
+
# on a default `vendor`): it is treated as opted-out unless the consumer
|
|
162
|
+
# enables it (`--with-ext NAME` / SPINEL_EXT_ENABLE). Explicit disable
|
|
163
|
+
# beats enable. Returns the count wired. A no-op for gems without the
|
|
164
|
+
# manifest.
|
|
165
|
+
def wire_extensions(src, dest, overrides, disable, enable = [])
|
|
130
166
|
manifest = File.join(src, "spinel-ext.json")
|
|
131
167
|
return 0 unless File.exist?(manifest)
|
|
132
168
|
|
|
@@ -143,7 +179,7 @@ module Bundler
|
|
|
143
179
|
sib_cflags = Hash.new { |h, k| h[k] = [] }
|
|
144
180
|
entries.each do |e|
|
|
145
181
|
next if e["source"] || !e["name"] # only CFLAGS-only siblings
|
|
146
|
-
next if e
|
|
182
|
+
next if ext_disabled?(e, disable, enable)
|
|
147
183
|
(cf = pkg_config_cflags(e)) && sib_cflags[e["name"].to_s].concat(cf)
|
|
148
184
|
sib_cflags[e["name"].to_s].concat(Array(e["cflags"])) if e["cflags"]
|
|
149
185
|
end
|
|
@@ -156,9 +192,11 @@ module Bundler
|
|
|
156
192
|
entries.each do |e|
|
|
157
193
|
placeholder = e["placeholder"]
|
|
158
194
|
name = e["name"]
|
|
195
|
+
(@ext_names_seen ||= []) << name.to_s if name && e["optional"]
|
|
159
196
|
|
|
160
|
-
#
|
|
161
|
-
|
|
197
|
+
# Opted out — explicitly (`--no-ext`), or an opt-in (`"default":
|
|
198
|
+
# "disabled"`) entry the consumer didn't enable (spinelgems#20).
|
|
199
|
+
if ext_disabled?(e, disable, enable)
|
|
162
200
|
substitute_placeholder(dest, placeholder, e["disabled_cflags"].to_s) if placeholder
|
|
163
201
|
wired += 1
|
|
164
202
|
next
|
|
@@ -225,6 +263,17 @@ module Bundler
|
|
|
225
263
|
0
|
|
226
264
|
end
|
|
227
265
|
|
|
266
|
+
# Is this optional entry off for this run? Explicit disable always wins;
|
|
267
|
+
# an opt-in entry (`"default": "disabled"`, spinelgems#20) is off unless
|
|
268
|
+
# its name is in the enable set. `default` is only meaningful on an
|
|
269
|
+
# `optional` entry with a `name`.
|
|
270
|
+
def ext_disabled?(entry, disable, enable)
|
|
271
|
+
return false unless entry["optional"] && entry["name"]
|
|
272
|
+
n = entry["name"].to_s
|
|
273
|
+
return true if disable.include?(n)
|
|
274
|
+
entry["default"].to_s == "disabled" && !enable.include?(n)
|
|
275
|
+
end
|
|
276
|
+
|
|
228
277
|
# System libs/cflags for an entry, resolved at the consumer's environment:
|
|
229
278
|
# `pkg-config --cflags --libs <name>`, else `pkg_config_fallback`, else nil
|
|
230
279
|
# (leave the placeholder so the build fails loud — we never silently drop a
|
|
@@ -300,18 +349,28 @@ module Bundler
|
|
|
300
349
|
end
|
|
301
350
|
|
|
302
351
|
ven_dir = File.join(dest, dir_rel)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
352
|
+
# Copy-once (spinelgems#20): two entries may share one source dir —
|
|
353
|
+
# toy's CPU and CUDA ggml units differ only in configure args/build_dir.
|
|
354
|
+
# The rm_rf+recopy below would wipe the first entry's just-built tree,
|
|
355
|
+
# so a dir already placed this vendor run is reused as-is. (Patches stay
|
|
356
|
+
# declarable on both entries: the stack-level detection no-ops on an
|
|
357
|
+
# already-patched copy.)
|
|
358
|
+
@copied_build_dirs ||= {}
|
|
359
|
+
unless @copied_build_dirs[ven_dir]
|
|
360
|
+
FileUtils.mkdir_p(File.dirname(ven_dir))
|
|
361
|
+
FileUtils.rm_rf(ven_dir)
|
|
362
|
+
# Source-only copy: a path:-sourced dev checkout carries build state —
|
|
363
|
+
# build*/ dirs (whose CMakeCache pins the ORIGINAL source path and makes
|
|
364
|
+
# cmake refuse the copy), the .patched sentinel, .git. ggml: 205MB with
|
|
365
|
+
# build dirs, 24MB without. Top-level name filter covers the real cases.
|
|
366
|
+
FileUtils.mkdir_p(ven_dir)
|
|
367
|
+
Dir.children(src_dir).each do |c|
|
|
368
|
+
next if c.start_with?("build") || c == ".git" || c == ".patched"
|
|
369
|
+
# stale dev objects/archives would also poison make's mtime logic
|
|
370
|
+
next if c =~ /\.(o|a|so|dylib|bundle)\z/
|
|
371
|
+
FileUtils.cp_r(File.join(src_dir, c), File.join(ven_dir, c))
|
|
372
|
+
end
|
|
373
|
+
@copied_build_dirs[ven_dir] = true
|
|
315
374
|
end
|
|
316
375
|
|
|
317
376
|
# Declared patches (toy#45: pristine vendored ggml + vendor-patches/*.patch),
|
|
@@ -360,7 +419,15 @@ module Bundler
|
|
|
360
419
|
cmds =
|
|
361
420
|
case b["tool"].to_s
|
|
362
421
|
when "cmake"
|
|
363
|
-
|
|
422
|
+
# Variant build dirs over a shared source (spinelgems#20): toy's
|
|
423
|
+
# CUDA unit configures the same vendored ggml into build-cuda/
|
|
424
|
+
# alongside the CPU unit's build/. Relative, no escape.
|
|
425
|
+
bd = (b["build_dir"] || "build").to_s
|
|
426
|
+
if bd.start_with?("/") || bd.split(File::SEPARATOR).include?("..")
|
|
427
|
+
warn "[vendor] bad build_dir for #{entry['name']}: #{bd.inspect} (relative, no ..)"
|
|
428
|
+
return nil
|
|
429
|
+
end
|
|
430
|
+
build_dir = File.join(ven_dir, bd)
|
|
364
431
|
cfg = ["cmake", "-S", ven_dir, "-B", build_dir, *Array(b["args"]).map(&:to_s)]
|
|
365
432
|
bld = ["cmake", "--build", build_dir, "-j", jobs]
|
|
366
433
|
targets = Array(b["targets"]).map(&:to_s)
|
|
@@ -374,8 +441,9 @@ module Bundler
|
|
|
374
441
|
return nil
|
|
375
442
|
end
|
|
376
443
|
|
|
444
|
+
env = build_env(b["tool"].to_s)
|
|
377
445
|
cmds.each do |cmd|
|
|
378
|
-
out, st = Open3.capture2e(*cmd)
|
|
446
|
+
out, st = Open3.capture2e(env, *cmd)
|
|
379
447
|
unless st.success?
|
|
380
448
|
warn "[vendor] build failed (#{entry['name']}): #{cmd.take(2).join(' ')} ... : " \
|
|
381
449
|
"#{out.lines.last(3).join.strip}"
|
|
@@ -397,6 +465,26 @@ module Bundler
|
|
|
397
465
|
ven_dir.start_with?(pwd) ? ven_dir.delete_prefix(pwd) : ven_dir
|
|
398
466
|
end
|
|
399
467
|
|
|
468
|
+
# Build-unit environment (spinelgems#21): on macOS, a cmake unit with C++
|
|
469
|
+
# sources can't find the stdlib headers (`fatal error: 'array' file not
|
|
470
|
+
# found`) unless CPLUS_INCLUDE_PATH carries the SDK's libc++ path —
|
|
471
|
+
# toy#27's Mac cold-start LIB leg, verified fix on the M2. Tool-side, not
|
|
472
|
+
# per-gem env: the platform knowledge belongs here, or every gem with a
|
|
473
|
+
# C++ unit repeats it. A caller-set CPLUS_INCLUDE_PATH is preserved (the
|
|
474
|
+
# SDK path appends); xcrun absent/failing degrades to no env, and the
|
|
475
|
+
# build then fails with the original loud cmake error.
|
|
476
|
+
def build_env(tool, platform: RUBY_PLATFORM)
|
|
477
|
+
return {} unless tool == "cmake" && platform.include?("darwin")
|
|
478
|
+
sdk, st = Open3.capture2e("xcrun", "--show-sdk-path")
|
|
479
|
+
sdk = sdk.strip
|
|
480
|
+
return {} unless st.success? && !sdk.empty?
|
|
481
|
+
libcxx = File.join(sdk, "usr", "include", "c++", "v1")
|
|
482
|
+
paths = [ENV["CPLUS_INCLUDE_PATH"].to_s, libcxx].reject(&:empty?)
|
|
483
|
+
{ "CPLUS_INCLUDE_PATH" => paths.join(File::PATH_SEPARATOR) }
|
|
484
|
+
rescue Errno::ENOENT
|
|
485
|
+
{}
|
|
486
|
+
end
|
|
487
|
+
|
|
400
488
|
# A placeholder that substitutes ZERO files is drift (toy#45: a gem whose
|
|
401
489
|
# ffi_cflags line moved out from under its manifest's literal-string
|
|
402
490
|
# placeholder) — warn loud. This replaces per-gem canary hacks like toy's
|
|
@@ -443,11 +531,16 @@ module Bundler
|
|
|
443
531
|
"— may not compile (run `spinel-compat check`)"
|
|
444
532
|
end
|
|
445
533
|
|
|
446
|
-
def write_manifest(into, entries)
|
|
534
|
+
def write_manifest(into, entries, sig_gems = [])
|
|
447
535
|
es = entries.compact
|
|
448
536
|
body = +"# Generated by bundler-spinel. require_relative this from a\n" \
|
|
449
537
|
"# Spinel program to pull in vendored dependencies (topo order:\n" \
|
|
450
538
|
"# every gem's dependencies are loaded before it — spinelgems#19).\n"
|
|
539
|
+
unless sig_gems.empty?
|
|
540
|
+
body << "# Type roots: #{sig_gems.join(', ')} ship sig/*.rbs, aggregated\n" \
|
|
541
|
+
"# under sig/ — compile with `--rbs #{File.join(into, 'sig')}`\n" \
|
|
542
|
+
"# so uncalled public methods keep their declared types (spinelgems#13).\n"
|
|
543
|
+
end
|
|
451
544
|
# Put each vendored gem's lib root on $LOAD_PATH so a dependent's plain
|
|
452
545
|
# `require "<depgem>"` resolves under CRuby too (the differential verify
|
|
453
546
|
# and plain-Ruby dev runs). Spinel has no load path: it ignores both the
|
|
@@ -37,13 +37,20 @@ module Bundler
|
|
|
37
37
|
# vocabulary is unchanged; the probe is tagged `verify-full` so the
|
|
38
38
|
# whole-surface signal stays distinguishable in the ledger from the
|
|
39
39
|
# entrypoint-only `verify`.
|
|
40
|
-
|
|
40
|
+
# rbs: a gem's shipped sig/*.rbs acts as the type root for the Spinel
|
|
41
|
+
# compile (spinelgems#13). `--rbs` re-pins *uncalled* public methods'
|
|
42
|
+
# param/return/ivar types that whole-program inference would otherwise
|
|
43
|
+
# widen to int/poly for lack of a call site — the failure mode hand-written
|
|
44
|
+
# seed blocks exist to patch. :auto (default) uses <dir>/sig when it
|
|
45
|
+
# contains .rbs files; a String is an explicit root; nil/false disables.
|
|
46
|
+
def verify(gem_name, version, dir, smoke: nil, full: false, rbs: :auto)
|
|
41
47
|
@engine.ensure!
|
|
48
|
+
rbs_root = resolve_rbs_root(dir, rbs)
|
|
42
49
|
harness = File.join(dir, HARNESS)
|
|
43
50
|
File.write(harness, harness_source(gem_name, dir, smoke, full))
|
|
44
51
|
|
|
45
52
|
ruby_out, ruby_err, ruby_ok = run_ruby(harness, dir)
|
|
46
|
-
spin_out, spin_err, spin_ok = run_spinel(harness)
|
|
53
|
+
spin_out, spin_err, spin_ok = run_spinel(harness, rbs_root)
|
|
47
54
|
|
|
48
55
|
verdict, reasons = classify(ruby_ok, spin_ok, ruby_out, spin_out, spin_err, behavior: !smoke.nil?)
|
|
49
56
|
# Tag *why* (the spinelgems#4 usability rubric) so a failure says what it'd
|
|
@@ -57,9 +64,14 @@ module Bundler
|
|
|
57
64
|
# and, when it pins a diverging scalar, appends `localized:<file>:<line>
|
|
58
65
|
# <var> cruby=… spinel=…`. Best-effort: nil when it can't localize.
|
|
59
66
|
if verdict == "rejected" && reasons.include?("miscompile")
|
|
67
|
+
# Note: the bisector compiles without the sig root, so localization of
|
|
68
|
+
# an rbs-seeded build is best-effort (bisect.sh has no --rbs passthrough).
|
|
60
69
|
loc = Localizer.new(@engine).localize(harness)
|
|
61
70
|
reasons += [loc] if loc
|
|
62
71
|
end
|
|
72
|
+
# Provenance: the verdict was reached with the gem's sig/ as type root —
|
|
73
|
+
# legible in the ledger/notes, ignored by the site's sticky logic.
|
|
74
|
+
reasons += ["rbs:sig"] if rbs_root
|
|
63
75
|
@ledger.record(@ledger.build(
|
|
64
76
|
gem: gem_name, version: version, rev: @engine.rev,
|
|
65
77
|
verdict: verdict, reasons: reasons, probe: full ? "verify-full" : "verify"
|
|
@@ -180,9 +192,18 @@ module Bundler
|
|
|
180
192
|
[out, err, st.success?]
|
|
181
193
|
end
|
|
182
194
|
|
|
183
|
-
def
|
|
195
|
+
def resolve_rbs_root(dir, rbs)
|
|
196
|
+
return nil if rbs.nil? || rbs == false
|
|
197
|
+
return File.expand_path(rbs) if rbs.is_a?(String)
|
|
198
|
+
sig = File.join(dir, "sig")
|
|
199
|
+
Dir[File.join(sig, "**", "*.rbs")].empty? ? nil : sig
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def run_spinel(file, rbs_root = nil)
|
|
184
203
|
bin = file.sub(/\.rb$/, ".bin")
|
|
185
|
-
|
|
204
|
+
args = [@engine.bin, file, "-o", bin]
|
|
205
|
+
args += ["--rbs", rbs_root] if rbs_root
|
|
206
|
+
_, cerr, cst = Open3.capture3(*args)
|
|
186
207
|
return ["", cerr, false] unless cst.success? && File.executable?(bin)
|
|
187
208
|
|
|
188
209
|
out, err, st = Open3.capture3(bin)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
module Bundler
|
|
2
2
|
module Spinel
|
|
3
|
-
# 0.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
3
|
+
# 0.4.0: a gem's shipped `sig/*.rbs` acts as the Spinel type root —
|
|
4
|
+
# `verify` auto-`--rbs`, `vendor` aggregates one root, retiring seed soup
|
|
5
|
+
# (#13); opt-in (`"default":"disabled"`) build-units + variant `build_dir`
|
|
6
|
+
# over a shared source for optional CUDA/Metal backends (#20); macOS SDK
|
|
7
|
+
# libc++ path for cmake build-units (#21); and probe/engine robustness for
|
|
8
|
+
# the b60fbd7 corpus reprobe (require-only → load-path limit; git rev for
|
|
9
|
+
# worktrees/frozen copies).
|
|
10
|
+
VERSION = "0.4.0"
|
|
8
11
|
end
|
|
9
12
|
end
|