bundler-spinel 0.2.1 → 0.3.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 +46 -0
- data/lib/bundler/spinel/cli.rb +58 -0
- data/lib/bundler/spinel/history.rb +58 -1
- data/lib/bundler/spinel/load_bearing.rb +116 -0
- data/lib/bundler/spinel/site.rb +23 -5
- data/lib/bundler/spinel/vendorer.rb +214 -7
- data/lib/bundler/spinel/version.rb +5 -5
- data/lib/bundler/spinel/why.rb +219 -0
- data/lib/bundler/spinel.rb +1 -0
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f60b276e969f601d5931d3df6b6e7177820f58609d18e1082e49784e4acf7ce3
|
|
4
|
+
data.tar.gz: 9a5b89828d86ab07cacbb3d85a63ff7167d0cecc83e96138ca4de28e19c51083
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60290945a8663c019299e28553af1c5bbd6766f8606a1b8675142653b1cd2692eae7e3eb9d7d243fbbfa05765c58f44eee74fb9ebb1852606bf2ecff9ce7ec9b
|
|
7
|
+
data.tar.gz: fb8b90c9cec7bda764f34625fd3643c27facb676c9a951551fd2a53a1c365e782acd0bd7d318a21c50062ea37c9f41c3105761ab01974d87fa51bfc329a90102
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ 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.3.0] — 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`spinel-compat vendor` build-units (spinelgems#14).** `spinel-ext.json` now
|
|
11
|
+
supports a `build` entry — a declared native build (`tool: cmake | make`, with
|
|
12
|
+
`dir`, `args`, `targets`, `artifacts`, and `patches`) run **inside the
|
|
13
|
+
consumer's vendor tree**, with link flags expanded relative to it (`{dir}` and
|
|
14
|
+
cross-entry `{dir:NAME}`). This lets a heavy-native gem — toy's vendored ggml
|
|
15
|
+
CMake build plus its tinynn shims — vendor **self-contained and relocatable**,
|
|
16
|
+
the same end state tep's small per-`.c` shims already had. A
|
|
17
|
+
`SPINEL_EXT_<PLACEHOLDER>` override substitutes prebuilt flags and skips the
|
|
18
|
+
build. Declared `patches` apply with stack-level already-applied detection, so
|
|
19
|
+
a `path:`-sourced dev checkout (already patched) vendors cleanly. Proven
|
|
20
|
+
end-to-end: a consumer vendors toy, the archives build in-tree, a
|
|
21
|
+
Spinel-compiled program links and runs, with zero absolute paths and survival
|
|
22
|
+
across a project move. Strictly narrower than `extconf.rb` (no free-form
|
|
23
|
+
shell; declared artifacts) — the Spinel analogue of a gemspec `extensions:`.
|
|
24
|
+
- **`spinel-compat vendor` handles transitive gem→gem dependencies
|
|
25
|
+
(spinelgems#19).** Vendoring a gem that depends on another vendored gem now
|
|
26
|
+
works: `deps.rb` is emitted in **topological order** (every gem's runtime
|
|
27
|
+
dependencies load before it, via a DFS over `spec.dependencies` with a stable
|
|
28
|
+
alphabetical tiebreak and a cycle guard) instead of the lockfile's alphabetical
|
|
29
|
+
order, and it prepends each vendored gem's `lib` to `$LOAD_PATH` so a
|
|
30
|
+
dependent's plain `require "<depgem>"` resolves under CRuby too. Spinel (no load
|
|
31
|
+
path) ignores both and relies on the topo-ordered `require_relative`s, so the
|
|
32
|
+
one `deps.rb` is correct under both runtimes — verified identical output on a
|
|
33
|
+
two-gem fixture. Unblocks `tep` → `spinel_kit` (the new stdlib-surface gem)
|
|
34
|
+
on the clean `gem "spinel_kit"` + vendor path.
|
|
35
|
+
- **`spinel-compat why <gem>` (spinelgems#12).** A legible "why doesn't this gem
|
|
36
|
+
work (yet)?" report: a plain-English cause, a category (native C-ext / Spinel
|
|
37
|
+
limitation / fixable compiler bug / dependency-blocked / metaprogramming), the
|
|
38
|
+
concrete evidence (the CRuby-vs-Spinel diff, the unresolved calls, the missing
|
|
39
|
+
require, the hard construct), and — most usefully — whether the verdict is
|
|
40
|
+
**terminal** (needs an upstream port or compiler feature) or **fixable** (a
|
|
41
|
+
tracked compiler bug that can graduate). Reads the dominant-rev ledger entry
|
|
42
|
+
like the catalog; `--probe` / `--dir` explains a fresh live probe instead.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- **`spinel-ext.json` wiring now warns on manifest drift.** A declared
|
|
46
|
+
placeholder that matches no vendored `.rb` emits a loud warning at vendor time
|
|
47
|
+
— replacing the per-gem "cflags canary" constants consumers maintained by hand.
|
|
48
|
+
- **Catalog/site rendering** advanced across several engine-rev reprobes (signals
|
|
49
|
+
lead every verdict tier; the download floor is off by default; human
|
|
50
|
+
attestations earn a verified rung; a load-bearing-gems roadmap page). These
|
|
51
|
+
affect the rendered spinelgems.org catalog, not the plugin's gating behaviour.
|
|
52
|
+
|
|
7
53
|
## [0.2.1] — 2026-06-01
|
|
8
54
|
|
|
9
55
|
### Fixed
|
data/lib/bundler/spinel/cli.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Bundler
|
|
|
21
21
|
when "install-engine" then cmd_install_engine(argv)
|
|
22
22
|
when "init" then cmd_init(argv)
|
|
23
23
|
when "probe" then cmd_probe(argv)
|
|
24
|
+
when "why" then cmd_why(argv)
|
|
24
25
|
when "verify" then cmd_verify(argv)
|
|
25
26
|
when "vendor" then cmd_vendor(argv)
|
|
26
27
|
when "check" then cmd_check(argv)
|
|
@@ -29,6 +30,7 @@ module Bundler
|
|
|
29
30
|
when "build-site" then cmd_build_site(argv)
|
|
30
31
|
when "build-db" then cmd_build_db(argv)
|
|
31
32
|
when "build-history" then cmd_build_history(argv)
|
|
33
|
+
when "build-load-bearing" then cmd_build_load_bearing(argv)
|
|
32
34
|
when "server" then cmd_server(argv)
|
|
33
35
|
when "ledger" then cmd_ledger(argv)
|
|
34
36
|
when "diff" then cmd_diff(argv)
|
|
@@ -91,6 +93,54 @@ module Bundler
|
|
|
91
93
|
v.rejected? ? 1 : 0
|
|
92
94
|
end
|
|
93
95
|
|
|
96
|
+
# why NAME [VERSION] [--rev REV] [--probe] [--dir PATH]
|
|
97
|
+
# Legible "why doesn't this gem work (yet)?" report (spinelgems#12).
|
|
98
|
+
# Reads the recorded verdict (dominant rev, like the catalog) by default;
|
|
99
|
+
# --probe / --dir explains a fresh live probe instead.
|
|
100
|
+
def cmd_why(argv)
|
|
101
|
+
require_relative "why"
|
|
102
|
+
dir = (i = argv.index("--dir")) ? argv.delete_at(i + 1).tap { argv.delete_at(i) } : nil
|
|
103
|
+
rev = (k = argv.index("--rev")) ? argv.delete_at(k + 1).tap { argv.delete_at(k) } : nil
|
|
104
|
+
live = !!argv.delete("--probe") || !!dir
|
|
105
|
+
name = argv.shift or raise Error, "usage: spinel-compat why NAME [VERSION] [--rev REV] [--probe] [--dir PATH]"
|
|
106
|
+
version = argv.shift
|
|
107
|
+
|
|
108
|
+
if live
|
|
109
|
+
engine = Engine.new
|
|
110
|
+
src = dir ? File.expand_path(dir) : GemFetcher.new.fetch(name, version || latest_version(name))
|
|
111
|
+
v = Probe.new(engine, Ledger.new).probe(name, version || "path", src)
|
|
112
|
+
Why.new(out: @out).report(v, source: "live probe")
|
|
113
|
+
else
|
|
114
|
+
v = ledger_pick(name, version: version, rev: rev)
|
|
115
|
+
unless v
|
|
116
|
+
@out.puts " no recorded verdict for #{name}#{version ? " #{version}" : ''}. " \
|
|
117
|
+
"Run `spinel-compat why #{name} --probe` to evaluate it now."
|
|
118
|
+
return 1
|
|
119
|
+
end
|
|
120
|
+
Why.new(out: @out).report(v, source: "ledger #{v.rev}")
|
|
121
|
+
end
|
|
122
|
+
0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The authoritative ledger entry for a gem: a specific --rev if asked,
|
|
126
|
+
# else the entry at the ledger's dominant rev (the rev most rows share —
|
|
127
|
+
# the same target the catalog renders), else the most recent seen.
|
|
128
|
+
def ledger_pick(name, version: nil, rev: nil)
|
|
129
|
+
entries = []
|
|
130
|
+
rev_counts = Hash.new(0)
|
|
131
|
+
Ledger.new.each do |v|
|
|
132
|
+
rev_counts[v.rev] += 1
|
|
133
|
+
next unless v.gem == name
|
|
134
|
+
next if version && v.version != version
|
|
135
|
+
entries << v
|
|
136
|
+
end
|
|
137
|
+
return nil if entries.empty?
|
|
138
|
+
return entries.select { |v| v.rev == rev }.last if rev
|
|
139
|
+
|
|
140
|
+
target = rev_counts.max_by { |_, c| c }&.first
|
|
141
|
+
entries.select { |v| v.rev == target }.last || entries.last
|
|
142
|
+
end
|
|
143
|
+
|
|
94
144
|
def cmd_verify(argv)
|
|
95
145
|
dir = (i = argv.index("--dir")) ? argv.delete_at(i + 1).tap { argv.delete_at(i) } : nil
|
|
96
146
|
smoke = (j = argv.index("--smoke")) ? argv.delete_at(j + 1).tap { argv.delete_at(j) } : nil
|
|
@@ -308,6 +358,13 @@ module Bundler
|
|
|
308
358
|
0
|
|
309
359
|
end
|
|
310
360
|
|
|
361
|
+
def cmd_build_load_bearing(argv)
|
|
362
|
+
out = (j = argv.index("--out")) ? argv[j + 1] : raise(Error, "build-load-bearing needs --out FILE")
|
|
363
|
+
f = LoadBearing.new.build_html(File.expand_path(out))
|
|
364
|
+
@out.puts "built load-bearing -> #{f}"
|
|
365
|
+
0
|
|
366
|
+
end
|
|
367
|
+
|
|
311
368
|
# Serve the static site + (with --store) the Compact Index from one process
|
|
312
369
|
# — what the deploy host runs. Port defaults to $PORT (Upsun) then 9292.
|
|
313
370
|
def cmd_server(argv)
|
|
@@ -410,6 +467,7 @@ module Bundler
|
|
|
410
467
|
spinel-compat install-engine [REV] fetch+build the Spinel compiler -> ~/.cache/spinel
|
|
411
468
|
spinel-compat init [DIR] scaffold a Spinel+Tep project (Gemfile, app.rb, bin/build)
|
|
412
469
|
spinel-compat probe NAME [VERSION] probe one gem, record a verdict
|
|
470
|
+
spinel-compat why NAME [--probe] legible "why doesn't this work (yet)?" report
|
|
413
471
|
spinel-compat verify NAME [--smoke F] differential CRuby-vs-Spinel run -> verified
|
|
414
472
|
spinel-compat vendor [LOCK] [--into D] place deps where Spinel finds them + deps.rb
|
|
415
473
|
spinel-compat check [LOCK] [--strict] gate a Gemfile.lock (exit 1 if rejected)
|
|
@@ -26,6 +26,63 @@ module Bundler
|
|
|
26
26
|
"(<code>e2e010c</code>); together with the new <code>instance_methods</code> const-fold " \
|
|
27
27
|
"(<a href=\"https://github.com/matz/spinel/issues/1073\">#1073</a>) and <code>transpose</code>/map " \
|
|
28
28
|
"specializations, the brass cluster and thousands more moved out of <code>rejected</code>." },
|
|
29
|
+
{ rev: "95557f5", date: "2026-06-02", commit: "module/class-body side effects + lexical const refs (#1256), Regexp.last_match(n) (#1257), preserve Float-in-Hash (#1258), Struct typing, JSON.generate, alias, +14 more",
|
|
30
|
+
file: "survey-95557f5/compat.jsonl",
|
|
31
|
+
note: "<strong>The biggest single jump yet.</strong> 22 upstream commits — including fixes for three " \
|
|
32
|
+
"issues this harness filed (<a href=\"https://github.com/matz/spinel/issues/1256\">#1256</a> module-body, " \
|
|
33
|
+
"<a href=\"https://github.com/matz/spinel/issues/1257\">#1257</a> <code>Regexp.last_match</code>, " \
|
|
34
|
+
"<a href=\"https://github.com/matz/spinel/issues/1258\">#1258</a> Float-in-Hash) plus Struct typing, " \
|
|
35
|
+
"<code>alias</code>, <code>JSON.generate</code> for records and more — moved <strong>20,175</strong> gems " \
|
|
36
|
+
"out of <code>rejected</code>, among them <code>rspec</code>, <code>globalid</code>, " \
|
|
37
|
+
"<code>mini_portile2</code> and <code>coffee-rails</code>." },
|
|
38
|
+
{ rev: "a782696", date: "2026-06-03", commit: "StringScanner unscan/check + Error, Time#to_s + puts-nil, Dir.exist? + alias_method dispatch, missing int-hash keys as nil, RBS extractor heterogeneous-union→poly, subclass-initialize poly unification (13 commits)",
|
|
39
|
+
file: "survey-a782696/compat.jsonl",
|
|
40
|
+
note: "<strong>A consolidation rev.</strong> The base verdict mix is essentially flat after the previous " \
|
|
41
|
+
"jump — 13 upstream commits of correctness fixes (<code>StringScanner</code>, <code>Time#to_s</code>, " \
|
|
42
|
+
"<code>Dir.exist?</code>/<code>alias_method</code>, and the RBS-extractor union→poly change) graduated a " \
|
|
43
|
+
"small set of gems — <code>google-adwords-api</code>, <code>libdatadog</code>, <code>random_user_agent</code>, " \
|
|
44
|
+
"<code>twitter_username_extractor</code> and the <code>redcar-*</code> cluster — while a couple regressed " \
|
|
45
|
+
"and were caught by the re-probe. The bigger story this rev was off the catalog: the harness found " \
|
|
46
|
+
"<code>spinel_analyze</code> consuming 100+ GB on a cluster of auto-generated API-SDK gems (a compiler " \
|
|
47
|
+
"memory blow-up, filed upstream)." },
|
|
48
|
+
{ rev: "9c0a5f0", date: "2026-06-04", commit: "79 commits — incl. fixes for 6 harness-filed issues: stdlib-class-in-ivar (#1305), reopen-Object (#1306), lambda/proc branch-local (#1315), &blk+block_given? (#1316), inject(&:sym) (#1317), ignored-require constant (#1273)",
|
|
49
|
+
file: "survey-9c0a5f0/compat.jsonl",
|
|
50
|
+
note: "<strong>The harness loop paying off.</strong> matz landed fixes for <strong>six</strong> issues this " \
|
|
51
|
+
"harness filed the day before — all common idioms: <code>block_given?</code> with a named " \
|
|
52
|
+
"<code>&blk</code> (<a href=\"https://github.com/matz/spinel/issues/1316\">#1316</a>), " \
|
|
53
|
+
"<code>inject(&:+)</code> (<a href=\"https://github.com/matz/spinel/issues/1317\">#1317</a>), " \
|
|
54
|
+
"reopening <code>class Object</code> (<a href=\"https://github.com/matz/spinel/issues/1306\">#1306</a>), " \
|
|
55
|
+
"a stdlib class held in an instance variable " \
|
|
56
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1305\">#1305</a>), and a branch-assigned local inside a " \
|
|
57
|
+
"lambda/proc (<a href=\"https://github.com/matz/spinel/issues/1315\">#1315</a>). <strong>3,487</strong> gems " \
|
|
58
|
+
"moved out of <code>rejected</code> (110.3k→106.8k). The one feature ruled out of scope — aliasing the " \
|
|
59
|
+
"regexp special globals (<a href=\"https://github.com/matz/spinel/issues/1307\">#1307</a>) — now fails with a " \
|
|
60
|
+
"clear diagnostic instead of bad C." },
|
|
61
|
+
{ rev: "5c9790c", date: "2026-06-05", commit: "17 commits — fixes for 3 harness-filed typed-collection issues: Hash#fetch on int_int_hash (#1329), Array#join on poly_array (#1332), Class-in-collection→poly (#1337); plus regex line-anchoring/gsub-buffer + first-class string type",
|
|
62
|
+
file: "survey-5c9790c/compat.jsonl",
|
|
63
|
+
note: "<strong>Typed-collection coverage.</strong> matz fixed three issues this harness filed hours earlier — all " \
|
|
64
|
+
"the same shape: a method that exists on the generic path but was missing on a <em>specialized</em> " \
|
|
65
|
+
"collection. <code>Hash#fetch</code> on an int→int hash " \
|
|
66
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1329\">#1329</a>), <code>Array#join</code> on a mixed " \
|
|
67
|
+
"<code>poly_array</code> (<a href=\"https://github.com/matz/spinel/issues/1332\">#1332</a>), and a " \
|
|
68
|
+
"<code>Class</code> value stored in a Hash/Array now typed as poly instead of int " \
|
|
69
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1337\">#1337</a>, which had broken every options-hash " \
|
|
70
|
+
"carrying an exception class). The compile+scan base barely moves on fixes like these — they're " \
|
|
71
|
+
"full-surface/runtime, so the graduation shows in the behaviour-verified tier." },
|
|
72
|
+
{ rev: "57af7f9", date: "2026-06-07", commit: "~40 commits — 9 harness-filed issues closed in one wave: the ecosystem-spine front doors (alias→attr_reader #1356, &:sym-after-positional parse #1359), unary operator mangling (#1357), sp_sym_intern link (#1355), plus typed-collection nil steps (#801/#1180) and #line / --emit-symbol-map diagnostics (the #1338 RFC direction)",
|
|
73
|
+
file: "survey-57af7f9/compat.jsonl",
|
|
74
|
+
note: "<strong>The spine-gems wave.</strong> Auditing why <code>bundler</code>/<code>rake</code>/" \
|
|
75
|
+
"<code>minitest</code>/<code>thor</code> reject found two shallow front doors — " \
|
|
76
|
+
"<code>alias</code> to an <code>attr_reader</code>-generated method " \
|
|
77
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1356\">#1356</a>, rake + thor) and " \
|
|
78
|
+
"<code>&:sym</code> after a positional argument mis-parsed as a hash literal " \
|
|
79
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1359\">#1359</a>, bundler + minitest) — and matz closed " \
|
|
80
|
+
"both within a day, alongside 7 more harness filings. All four spine gems now compile past their old " \
|
|
81
|
+
"blockers into distinct second-tier issues " \
|
|
82
|
+
"(<a href=\"https://github.com/matz/spinel/issues/1368\">#1368</a> et al.). C compile errors now map back " \
|
|
83
|
+
"to Ruby source lines via <code>#line</code>, on by default — the " \
|
|
84
|
+
"<a href=\"https://github.com/matz/spinel/issues/1338\">#1338</a> RFC direction. The behaviour-verified " \
|
|
85
|
+
"tier reached 144 mechanical ★ this run." },
|
|
29
86
|
].freeze
|
|
30
87
|
|
|
31
88
|
ORDER = %w[clean risky rejected].freeze
|
|
@@ -133,7 +190,7 @@ module Bundler
|
|
|
133
190
|
<title>#{h title}</title><link rel="stylesheet" href="/assets/style.css"></head>
|
|
134
191
|
<body>
|
|
135
192
|
<header><a class="brand" href="/">#{gem}SpinelGems</a>
|
|
136
|
-
<nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/history.html">History</a>
|
|
193
|
+
<nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/load-bearing.html">Load-bearing</a> <a href="/history.html">History</a>
|
|
137
194
|
<a href="https://github.com/OriPekelman/spinelgems">GitHub</a></nav></header>
|
|
138
195
|
<main>
|
|
139
196
|
#{body}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require_relative "site"
|
|
3
|
+
|
|
4
|
+
module Bundler
|
|
5
|
+
module Spinel
|
|
6
|
+
# The "build-it-first" roadmap page: which gems, if made to compile, unblock
|
|
7
|
+
# the most of the ecosystem. Reads the committed
|
|
8
|
+
# harness/load-bearing/targets.tsv (transitive load-bearing + buildability
|
|
9
|
+
# impact, precomputed from the local dependency graph) and renders a clear,
|
|
10
|
+
# status-coloured table. Static, committed in site/ like the history page.
|
|
11
|
+
class LoadBearing
|
|
12
|
+
DATA = File.expand_path("../../../harness/load-bearing/targets.tsv", __dir__)
|
|
13
|
+
GLYPH = Site::GLYPH
|
|
14
|
+
|
|
15
|
+
# Buildability snapshot @ 95557f5 (from harness/load-bearing/buildability.rb).
|
|
16
|
+
BUILDABLE = 50_688
|
|
17
|
+
BLOCKED = 29_139
|
|
18
|
+
REJECTED = 110_256
|
|
19
|
+
|
|
20
|
+
def initialize(data = DATA) = (@data = data)
|
|
21
|
+
|
|
22
|
+
def build_html(out)
|
|
23
|
+
rows = File.exist?(@data) ? File.readlines(@data)[1..].map { |l| l.chomp.split("\t") } : []
|
|
24
|
+
compiler = rows.select { |r| r[7] == "compiler" }.sort_by { |r| -r[1].to_i }
|
|
25
|
+
File.write(out, page("Load-bearing gems — SpinelGems", body(compiler, rows)))
|
|
26
|
+
out
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def body(compiler, all)
|
|
32
|
+
b = +""
|
|
33
|
+
b << "<h1>Load-bearing gems</h1>\n"
|
|
34
|
+
b << %(<p class="lede">A gem matters to the ecosystem by how many gems pull it in )
|
|
35
|
+
b << %(<em>transitively</em> — directly, or as a dependency of a dependency, turtles all )
|
|
36
|
+
b << %(the way down. If Spinel can't compile a load-bearing gem, nothing above it can ship )
|
|
37
|
+
b << %(either. This is the build-it-first roadmap.</p>\n)
|
|
38
|
+
|
|
39
|
+
b << %(<div class="stat-row">\n)
|
|
40
|
+
b << stat("buildable", BUILDABLE, "whole dependency tree compiles")
|
|
41
|
+
b << stat("blocked", BLOCKED, "compiles itself — but a dependency is rejected")
|
|
42
|
+
b << stat("rejected", REJECTED, "doesn't compile")
|
|
43
|
+
b << %(</div>\n)
|
|
44
|
+
b << %(<p class="note">~<strong>#{fmt(BLOCKED)}</strong> gems compile on their own but )
|
|
45
|
+
b << %(can't actually be used because something beneath them is rejected. Fixing a load-bearing )
|
|
46
|
+
b << %(blocker flows <em>up</em> the tree — its dependents become buildable too.</p>\n)
|
|
47
|
+
|
|
48
|
+
b << %(<h2>Build-it-first targets</h2>\n)
|
|
49
|
+
b << %(<p class="sub">Ranked by <strong>impact</strong> — how many blocked gems become )
|
|
50
|
+
b << %(buildable if this one alone is fixed. Filtered to <strong>compiler-fixable</strong> )
|
|
51
|
+
b << %(failures: the native (C-extension) and heavy-metaprogramming (Rails-shaped) clusters )
|
|
52
|
+
b << %(are deliberately set aside as not the first target here. Each row carries two ways to )
|
|
53
|
+
b << %(fix it — a focused Spinel issue, or, for a small library, a PR to the gem itself.</p>\n)
|
|
54
|
+
|
|
55
|
+
b << %(<table id="catalog"><thead><tr>)
|
|
56
|
+
b << %(<th class="num">impact</th><th>gem</th><th>status</th><th class="num">load-bearing</th>)
|
|
57
|
+
b << %(<th>failure</th><th class="num">lib size</th><th>fix</th></tr></thead><tbody>\n)
|
|
58
|
+
compiler.first(120).each do |r|
|
|
59
|
+
gem, sole, _reach, transit, _dl, verdict, ftype, _ach, files, loc = r
|
|
60
|
+
fi = files.to_i
|
|
61
|
+
strat = fi.positive? && fi <= 12 ? %(<span class="badge human">lib PR</span> small — #{fi} files) :
|
|
62
|
+
%(<span class="badge rubric">Spinel issue</span>)
|
|
63
|
+
b << %(<tr>)
|
|
64
|
+
b << %(<td class="num"><strong>#{fmt sole}</strong></td>)
|
|
65
|
+
b << %(<td class="g">#{h gem}</td>)
|
|
66
|
+
b << %(<td class="v #{verdict}">#{GLYPH[verdict] || '?'} #{h verdict}</td>)
|
|
67
|
+
b << %(<td class="num">#{fmt transit}</td>)
|
|
68
|
+
b << %(<td><span class="badge rubric">#{h ftype}</span></td>)
|
|
69
|
+
b << %(<td class="num">#{fi.positive? ? "#{fi}f / #{loc}L" : "—"}</td>)
|
|
70
|
+
b << %(<td>#{strat}</td>)
|
|
71
|
+
b << %(</tr>\n)
|
|
72
|
+
end
|
|
73
|
+
b << "</tbody></table>\n"
|
|
74
|
+
|
|
75
|
+
nat = all.count { |r| r[7] == "native" }; mp = all.count { |r| r[7] == "metaprog" }
|
|
76
|
+
b << %(<p class="meta">Set aside as not-first-target: <strong>#{nat}</strong> native )
|
|
77
|
+
b << %(C-extension blockers (need FFI/ext vendoring) and <strong>#{mp}</strong> heavy-metaprogramming )
|
|
78
|
+
b << %(blockers (the Rails ecosystem). Method + data: )
|
|
79
|
+
b << %(<a href="https://github.com/OriPekelman/spinelgems/blob/main/docs/load-bearing-gems.md">docs/load-bearing-gems.md</a> · )
|
|
80
|
+
b << %(<a href="https://github.com/OriPekelman/spinelgems/blob/main/harness/load-bearing/">harness/load-bearing/</a>. )
|
|
81
|
+
b << %(Built locally from the gem cache's dependency graph; impact is per engine revision.</p>\n)
|
|
82
|
+
b
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def stat(cls, n, label)
|
|
86
|
+
%( <div class="stat #{cls}"><b>#{fmt n}</b><span>#{label}</span></div>\n)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fmt(n)
|
|
90
|
+
n = n.to_i
|
|
91
|
+
n >= 1000 ? "#{(n / 1000.0).round(1)}k" : n.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def h(s) = CGI.escapeHTML(s.to_s)
|
|
95
|
+
|
|
96
|
+
def page(title, body)
|
|
97
|
+
gem = %(<svg class="gem" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h12l4 6-10 13L2 9z" fill="#7b2d8e"/><path d="M6 3 2 9l10 13z" fill="#5a1f6b" opacity=".55"/><path d="M18 3l4 6-10 13z" fill="#b14fc4"/><path d="M6 3h12l-6 6z" fill="#d98ee8"/></svg>)
|
|
98
|
+
<<~HTML
|
|
99
|
+
<!doctype html>
|
|
100
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
102
|
+
<title>#{h title}</title><link rel="stylesheet" href="/assets/style.css"></head>
|
|
103
|
+
<body>
|
|
104
|
+
<header><a class="brand" href="/">#{gem}SpinelGems</a>
|
|
105
|
+
<nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/load-bearing.html">Load-bearing</a>
|
|
106
|
+
<a href="/history.html">History</a> <a href="https://github.com/OriPekelman/spinelgems">GitHub</a></nav></header>
|
|
107
|
+
<main>
|
|
108
|
+
#{body}
|
|
109
|
+
</main>
|
|
110
|
+
#{Site::FOOTER_HTML}
|
|
111
|
+
</body></html>
|
|
112
|
+
HTML
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/bundler/spinel/site.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Bundler
|
|
|
52
52
|
|
|
53
53
|
# One-line semantics per verdict — used as the lede on each per-verdict page.
|
|
54
54
|
BLURB = {
|
|
55
|
-
"verified" => "<strong>Full surface</strong> compiles and a behaviour smoke matches CRuby under a Spinel-compiled harness — every <code>lib/</code> file force-required (no <code>autoload</code> masking, no missing-dependency rescue), not just the entrypoint. The only verdict to trust where it matters. A constant/VERSION-only smoke that loads the entrypoint but leaves the gem's real code behind <code>autoload</code> is <em>not</em> enough — that overstated usability, so the bar was tightened to whole-surface. Sticky across engine revisions until a re-run catches a regression.",
|
|
55
|
+
"verified" => "<strong>Full surface</strong> compiles and a behaviour smoke matches CRuby under a Spinel-compiled harness — every <code>lib/</code> file force-required (no <code>autoload</code> masking, no missing-dependency rescue), not just the entrypoint. The only verdict to trust where it matters. A constant/VERSION-only smoke that loads the entrypoint but leaves the gem's real code behind <code>autoload</code> is <em>not</em> enough — that overstated usability, so the bar was tightened to whole-surface. Sticky across engine revisions until a re-run catches a regression. <strong>Or</strong>: for a gem the mechanical probe can't rank — a Spinel-<em>native</em> program rather than a <code>require</code>-library (e.g. <code>tep</code>, the translator that compiles this very site) — a <span class=\"badge human\">👤 human</span> attestation of real production use is the verification (the strongest signal we carry); a fresh behaviour failure still overrides it.",
|
|
56
56
|
"loaded" => "Compiles and loads identically under CRuby and Spinel via a require-only differential. Logic untested — a gem can load fine and still silently miscompile in the code paths the require-only smoke doesn't exercise. Weaker than <strong>verified</strong>; not a trust signal.",
|
|
57
57
|
"clean" => "Compiles clean (cheap static lower bound). No behaviour was exercised — the survey doesn't run the gem. Massively overstates compatibility; the harness is the trustworthy check.",
|
|
58
58
|
"risky" => "Compiles, but the source uses constructs Spinel degrades silently (<code>eval</code>, <code>define_method</code>, …). Allowed by default; fails under <code>spinel-compat check --strict</code>.",
|
|
@@ -313,6 +313,17 @@ module Bundler
|
|
|
313
313
|
current_entries[v.gem] << v if v.rev == target_rev
|
|
314
314
|
end
|
|
315
315
|
|
|
316
|
+
# A human attestation of real production use is the strongest trust
|
|
317
|
+
# signal we carry — stronger than a hand smoke — and for a gem the
|
|
318
|
+
# mechanical probe *can't* rank, it's the only applicable verification.
|
|
319
|
+
# The require-probe assumes "gem = library you require"; a Spinel-native
|
|
320
|
+
# program like tep (a translator/framework, FFI + no CRuby runtime) can't
|
|
321
|
+
# pass it though it demonstrably works (it compiles this very site). So a
|
|
322
|
+
# human-attested (gem,version) earns ★ exactly like a verify-full pass —
|
|
323
|
+
# unless a fresh *behaviour* probe (verify/verify-full) contradicts the
|
|
324
|
+
# human's claim (handled by the `behaviour_rejected` guard below).
|
|
325
|
+
attestations.each_key { |k| ever_verified << k }
|
|
326
|
+
|
|
316
327
|
current_entries.map do |name, vs|
|
|
317
328
|
# Within the current rev, pick the *strongest* signal — not the
|
|
318
329
|
# most recent. A rejected (compile error or harness miscompile)
|
|
@@ -403,6 +414,12 @@ module Bundler
|
|
|
403
414
|
# candidates.tsv / compat.jsonl for machine consumers).
|
|
404
415
|
def verdict_page_html(verdict, all_rs, counts)
|
|
405
416
|
full = all_rs.select { |r| r.verdict == verdict }
|
|
417
|
+
# Signals-first in every tier: a gem with a human attestation and/or
|
|
418
|
+
# passing own-tests outranks an unsignaled one (most-trusted on top),
|
|
419
|
+
# then by downloads — matching the dynamic /catalog order_by. all_rs is
|
|
420
|
+
# already downloads-sorted, so the stable sort keeps downloads as the
|
|
421
|
+
# tiebreak. Keeps signal-bearing low-download gems (e.g. tep) at the top.
|
|
422
|
+
full = full.sort_by.with_index { |r, i| [-((r.human ? 1 : 0) + (r.tests ? 1 : 0)), i] }
|
|
406
423
|
capped = (verdict == "rejected") && full.size > REJECTED_CAP
|
|
407
424
|
shown = capped ? full.first(REJECTED_CAP) : full
|
|
408
425
|
|
|
@@ -424,7 +441,7 @@ module Bundler
|
|
|
424
441
|
|
|
425
442
|
body << %(<div class="filters">\n)
|
|
426
443
|
body << %( <input id="q" type="search" placeholder="filter by gem name…" autocomplete="off">\n)
|
|
427
|
-
body << %( <label class="floor"><input type="checkbox" id="floor"
|
|
444
|
+
body << %( <label class="floor"><input type="checkbox" id="floor"> )
|
|
428
445
|
body << %(hide low-signal gems (< #{fmt_n MIN_DOWNLOADS} downloads)</label>\n)
|
|
429
446
|
body << %(</div>\n)
|
|
430
447
|
|
|
@@ -432,7 +449,7 @@ module Bundler
|
|
|
432
449
|
body << %(<th class="num">downloads</th><th>updated</th><th>description</th></tr></thead><tbody>\n)
|
|
433
450
|
shown.each do |r|
|
|
434
451
|
gem_cell = r.homepage ? %(<a href="#{h r.homepage}" rel="noopener nofollow">#{h r.gem}</a>) : h(r.gem)
|
|
435
|
-
body << %(<tr data-gem="#{h r.gem.downcase}" data-dl="#{r.downloads}">)
|
|
452
|
+
body << %(<tr data-gem="#{h r.gem.downcase}" data-dl="#{r.downloads}" data-sig="#{r.human || r.tests ? 1 : 0}">)
|
|
436
453
|
body << %(<td class="v #{r.verdict}" title="#{h r.notes}">#{GLYPH[r.verdict]} #{r.verdict}</td>)
|
|
437
454
|
body << %(<td class="sig">#{signals_html(r)}</td>)
|
|
438
455
|
body << %(<td class="g">#{gem_cell} <span class="ver">#{h r.version}</span></td>)
|
|
@@ -459,7 +476,7 @@ module Bundler
|
|
|
459
476
|
<body>
|
|
460
477
|
<header>
|
|
461
478
|
<a class="brand" href="/"><svg class="gem" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h12l4 6-10 13L2 9z" fill="#7b2d8e"/><path d="M6 3 2 9l10 13z" fill="#5a1f6b" opacity=".55"/><path d="M18 3l4 6-10 13z" fill="#b14fc4"/><path d="M6 3h12l-6 6z" fill="#d98ee8"/></svg>SpinelGems</a>
|
|
462
|
-
<nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/history.html">History</a>
|
|
479
|
+
<nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/load-bearing.html">Load-bearing</a> <a href="/history.html">History</a>
|
|
463
480
|
<a href="https://github.com/OriPekelman/spinelgems">GitHub</a></nav>
|
|
464
481
|
</header>
|
|
465
482
|
<main>
|
|
@@ -528,7 +545,8 @@ module Bundler
|
|
|
528
545
|
const hideLow = floor.checked;
|
|
529
546
|
for (const tr of rows) {
|
|
530
547
|
const okQ = !term || tr.dataset.gem.includes(term);
|
|
531
|
-
|
|
548
|
+
// signal-bearing rows (👤/✪) are never hidden by the floor
|
|
549
|
+
const okF = !hideLow || tr.dataset.sig === '1' || (+tr.dataset.dl) >= FLOOR;
|
|
532
550
|
tr.style.display = (okQ && okF) ? '' : 'none';
|
|
533
551
|
}
|
|
534
552
|
}
|
|
@@ -38,14 +38,22 @@ module Bundler
|
|
|
38
38
|
|
|
39
39
|
manifest = []
|
|
40
40
|
exts = 0
|
|
41
|
-
|
|
41
|
+
# Topological order (dependencies before dependents), not the lockfile's
|
|
42
|
+
# alphabetical `specs` (spinelgems#19): Spinel has no load path, so
|
|
43
|
+
# deps.rb is a *flattened single load* — each gem's entrypoint
|
|
44
|
+
# require_relative'd once, in order. A dependent loaded before its
|
|
45
|
+
# dependency would reference not-yet-defined constants. tep→spinel_kit
|
|
46
|
+
# is the first real case (it sorted right only by alphabetical luck).
|
|
47
|
+
topo_sort(parsed.specs).each do |spec|
|
|
42
48
|
name = spec.name
|
|
43
49
|
version = spec.version.to_s
|
|
44
50
|
src = resolve_source(spec, lock_dir)
|
|
45
51
|
dest = File.join(into, name)
|
|
46
52
|
place(src, dest)
|
|
47
53
|
exts += wire_extensions(src, dest, ext_overrides, disable)
|
|
48
|
-
|
|
54
|
+
if (target = require_target(name, dest))
|
|
55
|
+
manifest << { require: target, libdir: "#{File.basename(dest)}/lib" }
|
|
56
|
+
end
|
|
49
57
|
note_compat(name, version) if warn_incompatible
|
|
50
58
|
end
|
|
51
59
|
|
|
@@ -53,6 +61,30 @@ module Bundler
|
|
|
53
61
|
{ into: into, count: manifest.size, extensions: exts }
|
|
54
62
|
end
|
|
55
63
|
|
|
64
|
+
# Order specs so every gem's runtime dependencies come before it — a DFS
|
|
65
|
+
# post-order over `spec.dependencies`, with an alphabetical tiebreak for
|
|
66
|
+
# determinism and a visiting-set guard so a dependency cycle degrades to
|
|
67
|
+
# *some* stable order instead of looping. Deps not present in this lockset
|
|
68
|
+
# (stdlib/default gems) are skipped. (spinelgems#19)
|
|
69
|
+
def topo_sort(specs)
|
|
70
|
+
by_name = specs.each_with_object({}) { |s, h| h[s.name] = s }
|
|
71
|
+
ordered = []
|
|
72
|
+
state = {} # name => :visiting | :done
|
|
73
|
+
visit = lambda do |spec|
|
|
74
|
+
st = state[spec.name]
|
|
75
|
+
return if st == :done || st == :visiting
|
|
76
|
+
state[spec.name] = :visiting
|
|
77
|
+
spec.dependencies.sort_by(&:name).each do |dep|
|
|
78
|
+
dn = dep.respond_to?(:name) ? dep.name : dep.to_s
|
|
79
|
+
visit.call(by_name[dn]) if by_name[dn]
|
|
80
|
+
end
|
|
81
|
+
state[spec.name] = :done
|
|
82
|
+
ordered << spec
|
|
83
|
+
end
|
|
84
|
+
specs.sort_by(&:name).each { |s| visit.call(s) }
|
|
85
|
+
ordered
|
|
86
|
+
end
|
|
87
|
+
|
|
56
88
|
# path:/git: lockfile sources (toy ↔ tep is the headline case)
|
|
57
89
|
# point at a local tree; we don't go through `gem fetch`. For GEM
|
|
58
90
|
# sources we fall back to the cache-backed RubyGems fetcher.
|
|
@@ -117,6 +149,10 @@ module Bundler
|
|
|
117
149
|
end
|
|
118
150
|
|
|
119
151
|
wired = 0
|
|
152
|
+
# name -> vendored build dir, for cross-entry {dir:NAME} link expansion
|
|
153
|
+
# (toy#45: tinynn's link line needs both its own dir and ggml's).
|
|
154
|
+
# Entries are processed in manifest order, so referenced units come first.
|
|
155
|
+
built_dirs = {}
|
|
120
156
|
entries.each do |e|
|
|
121
157
|
placeholder = e["placeholder"]
|
|
122
158
|
name = e["name"]
|
|
@@ -128,6 +164,35 @@ module Bundler
|
|
|
128
164
|
next
|
|
129
165
|
end
|
|
130
166
|
|
|
167
|
+
# Build-unit entry (spinelgems#14): a declared native build (cmake|make)
|
|
168
|
+
# producing archives *inside the consumer's vendor tree*, with `link`
|
|
169
|
+
# flags expanded relative to it ({dir} -> the vendored build dir). This
|
|
170
|
+
# is the heavy-native analogue of `source` per-.c entries — nokogiri's
|
|
171
|
+
# mini_portile2 precedent, Spinel-shaped. It replaces the per-consumer
|
|
172
|
+
# post-vendor absolute-path rewrite hooks (toy's prep/post_vendor_toy.rb)
|
|
173
|
+
# that made vendored trees non-relocatable and toy unpublishable.
|
|
174
|
+
# A consumer override (SPINEL_EXT_<PLACEHOLDER> / --ext) supplies the
|
|
175
|
+
# full replacement flags and skips the build (prebuilt escape hatch).
|
|
176
|
+
if e["build"]
|
|
177
|
+
if placeholder && (ov = overrides[placeholder] || ENV[ext_env_key(placeholder)])
|
|
178
|
+
substitute_placeholder(dest, placeholder, ov.to_s)
|
|
179
|
+
wired += 1
|
|
180
|
+
next
|
|
181
|
+
end
|
|
182
|
+
ven_dir = build_unit(src, dest, e) or next # build failed (warned)
|
|
183
|
+
built_dirs[name.to_s] = ven_dir if name
|
|
184
|
+
if placeholder
|
|
185
|
+
parts = Array(e["link"]).map do |t|
|
|
186
|
+
t.gsub("{dir}", ven_dir)
|
|
187
|
+
.gsub(/\{dir:([^}]+)\}/) { built_dirs[$1] || "{dir:#{$1}}" }
|
|
188
|
+
end
|
|
189
|
+
parts.concat(Array(e["libs"]))
|
|
190
|
+
substitute_placeholder(dest, placeholder, parts.join(" ").strip, name: name)
|
|
191
|
+
end
|
|
192
|
+
wired += 1
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
|
|
131
196
|
# Compile / place the .o (or take a prebuilt override path). Both forms
|
|
132
197
|
# need this; post-#1011 const-fold form skips the substitution below.
|
|
133
198
|
obj = nil
|
|
@@ -214,11 +279,141 @@ module Bundler
|
|
|
214
279
|
nil
|
|
215
280
|
end
|
|
216
281
|
|
|
217
|
-
|
|
282
|
+
# Build-unit (spinelgems#14): copy the gem's declared build dir into the
|
|
283
|
+
# vendor tree, run the declared tool there, verify the declared artifacts.
|
|
284
|
+
# Returns the vendored dir path (project-relative when `into` was given
|
|
285
|
+
# relative — the usual case — so substituted -L flags stay relocatable
|
|
286
|
+
# with the consumer project) or nil on failure (warned, entry skipped).
|
|
287
|
+
#
|
|
288
|
+
# The tool surface is deliberately constrained to cmake|make with declared
|
|
289
|
+
# args/targets/artifacts — no free-form shell. extconf.rb is precedent for
|
|
290
|
+
# arbitrary install-time code in gems, but there's no need to copy that
|
|
291
|
+
# mistake into spinel-ext.json: a declarative unit stays auditable and the
|
|
292
|
+
# detector-inferable, consumer-side philosophy survives.
|
|
293
|
+
def build_unit(src, dest, entry)
|
|
294
|
+
b = entry["build"]
|
|
295
|
+
dir_rel = b["dir"].to_s
|
|
296
|
+
src_dir = File.join(src, dir_rel)
|
|
297
|
+
unless File.directory?(src_dir)
|
|
298
|
+
warn "[vendor] build dir not found for #{entry['name'] || entry['placeholder']}: #{dir_rel}"
|
|
299
|
+
return nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
ven_dir = File.join(dest, dir_rel)
|
|
303
|
+
FileUtils.mkdir_p(File.dirname(ven_dir))
|
|
304
|
+
FileUtils.rm_rf(ven_dir)
|
|
305
|
+
# Source-only copy: a path:-sourced dev checkout carries build state —
|
|
306
|
+
# build*/ dirs (whose CMakeCache pins the ORIGINAL source path and makes
|
|
307
|
+
# cmake refuse the copy), the .patched sentinel, .git. ggml: 205MB with
|
|
308
|
+
# build dirs, 24MB without. Top-level name filter covers the real cases.
|
|
309
|
+
FileUtils.mkdir_p(ven_dir)
|
|
310
|
+
Dir.children(src_dir).each do |c|
|
|
311
|
+
next if c.start_with?("build") || c == ".git" || c == ".patched"
|
|
312
|
+
# stale dev objects/archives would also poison make's mtime logic
|
|
313
|
+
next if c =~ /\.(o|a|so|dylib|bundle)\z/
|
|
314
|
+
FileUtils.cp_r(File.join(src_dir, c), File.join(ven_dir, c))
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Declared patches (toy#45: pristine vendored ggml + vendor-patches/*.patch),
|
|
318
|
+
# applied into the COPY before configure — mini_portile's patch_files
|
|
319
|
+
# precedent. Globs resolve against the gem root; patch files are data,
|
|
320
|
+
# which keeps the no-free-form-shell property of the schema.
|
|
321
|
+
# `git apply` (not patch(1)): strict, no fuzz — patch(1) happily
|
|
322
|
+
# *re*-applies hunks fuzzily onto an already-patched tree. Works in
|
|
323
|
+
# non-repo dirs too (gem-shipped trees have no .git).
|
|
324
|
+
#
|
|
325
|
+
# Patches form an ordered STACK (later ones rewrite earlier ones'
|
|
326
|
+
# hunks), so already-applied detection is stack-level, not per-patch:
|
|
327
|
+
# a pristine tree forward-applies the FIRST patch; a fully-patched
|
|
328
|
+
# working tree (path:-sourced dev checkout, toy's `.patched` flow)
|
|
329
|
+
# reverse-applies the LAST. Anything else is genuine drift — fail.
|
|
330
|
+
patches = Array(entry["build"]["patches"])
|
|
331
|
+
.flat_map { |g| Dir[File.join(src, g.to_s)].sort }
|
|
332
|
+
.map { |p| File.expand_path(p) }
|
|
333
|
+
unless patches.empty?
|
|
334
|
+
_, pristine = Open3.capture2e("git", "-C", ven_dir, "apply", "--check", patches.first)
|
|
335
|
+
if pristine.success?
|
|
336
|
+
patches.each do |abs|
|
|
337
|
+
out, st = Open3.capture2e("git", "-C", ven_dir, "apply", abs)
|
|
338
|
+
unless st.success?
|
|
339
|
+
warn "[vendor] patch failed (#{entry['name']}): #{File.basename(abs)}: #{out.lines.last(2).join.strip}"
|
|
340
|
+
return nil
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
else
|
|
344
|
+
_, stacked = Open3.capture2e("git", "-C", ven_dir, "apply", "--reverse", "--check", patches.last)
|
|
345
|
+
unless stacked.success?
|
|
346
|
+
warn "[vendor] patches for #{entry['name']} neither apply (pristine) nor " \
|
|
347
|
+
"reverse-apply (already patched) — source tree drifted from the patch set"
|
|
348
|
+
return nil
|
|
349
|
+
end
|
|
350
|
+
# already-patched working tree: nothing to do
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
jobs = begin
|
|
355
|
+
require "etc"
|
|
356
|
+
Etc.nprocessors.to_s
|
|
357
|
+
rescue StandardError
|
|
358
|
+
"4"
|
|
359
|
+
end
|
|
360
|
+
cmds =
|
|
361
|
+
case b["tool"].to_s
|
|
362
|
+
when "cmake"
|
|
363
|
+
build_dir = File.join(ven_dir, "build")
|
|
364
|
+
cfg = ["cmake", "-S", ven_dir, "-B", build_dir, *Array(b["args"]).map(&:to_s)]
|
|
365
|
+
bld = ["cmake", "--build", build_dir, "-j", jobs]
|
|
366
|
+
targets = Array(b["targets"]).map(&:to_s)
|
|
367
|
+
bld.push("--target", *targets) unless targets.empty?
|
|
368
|
+
[cfg, bld]
|
|
369
|
+
when "make"
|
|
370
|
+
[["make", "-C", ven_dir, "-j", jobs,
|
|
371
|
+
*Array(b["args"]).map(&:to_s), *Array(b["targets"]).map(&:to_s)]]
|
|
372
|
+
else
|
|
373
|
+
warn "[vendor] unknown build tool #{b['tool'].inspect} for #{entry['name']} (cmake|make)"
|
|
374
|
+
return nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
cmds.each do |cmd|
|
|
378
|
+
out, st = Open3.capture2e(*cmd)
|
|
379
|
+
unless st.success?
|
|
380
|
+
warn "[vendor] build failed (#{entry['name']}): #{cmd.take(2).join(' ')} ... : " \
|
|
381
|
+
"#{out.lines.last(3).join.strip}"
|
|
382
|
+
return nil
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
missing = Array(b["artifacts"]).reject { |a| File.exist?(File.join(ven_dir, a.to_s)) }
|
|
387
|
+
unless missing.empty?
|
|
388
|
+
warn "[vendor] build for #{entry['name']} succeeded but artifacts missing: #{missing.join(', ')}"
|
|
389
|
+
return nil
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Project-relative {dir} when the vendor tree lives under the consumer's
|
|
393
|
+
# cwd (the normal `--into vendor/spinel` case) — substituted -L flags
|
|
394
|
+
# then survive moving the whole project, not just deleting the gem's
|
|
395
|
+
# source checkout. Compile from the project root (the documented flow).
|
|
396
|
+
pwd = Dir.pwd + File::SEPARATOR
|
|
397
|
+
ven_dir.start_with?(pwd) ? ven_dir.delete_prefix(pwd) : ven_dir
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# A placeholder that substitutes ZERO files is drift (toy#45: a gem whose
|
|
401
|
+
# ffi_cflags line moved out from under its manifest's literal-string
|
|
402
|
+
# placeholder) — warn loud. This replaces per-gem canary hacks like toy's
|
|
403
|
+
# CURRENT_FFI_CFLAGS lockstep constant with a systemic vendor-time check.
|
|
404
|
+
def substitute_placeholder(dest, placeholder, repl, name: nil)
|
|
405
|
+
hits = 0
|
|
218
406
|
Dir[File.join(dest, "**", "*.rb")].each do |f|
|
|
219
407
|
body = File.read(f)
|
|
220
|
-
|
|
408
|
+
next unless body.include?(placeholder)
|
|
409
|
+
File.write(f, body.gsub(placeholder, repl))
|
|
410
|
+
hits += 1
|
|
221
411
|
end
|
|
412
|
+
if hits.zero?
|
|
413
|
+
warn "[vendor] #{name || 'ext'}: placeholder matched NO vendored .rb — " \
|
|
414
|
+
"manifest drift? (#{placeholder.length > 60 ? placeholder[0, 57] + '...' : placeholder})"
|
|
415
|
+
end
|
|
416
|
+
hits
|
|
222
417
|
end
|
|
223
418
|
|
|
224
419
|
# @TEP_SPHTTP_O@ -> SPINEL_EXT_TEP_SPHTTP_O
|
|
@@ -248,10 +443,22 @@ module Bundler
|
|
|
248
443
|
"— may not compile (run `spinel-compat check`)"
|
|
249
444
|
end
|
|
250
445
|
|
|
251
|
-
def write_manifest(into,
|
|
446
|
+
def write_manifest(into, entries)
|
|
447
|
+
es = entries.compact
|
|
252
448
|
body = +"# Generated by bundler-spinel. require_relative this from a\n" \
|
|
253
|
-
"# Spinel program to pull in vendored dependencies (
|
|
254
|
-
|
|
449
|
+
"# Spinel program to pull in vendored dependencies (topo order:\n" \
|
|
450
|
+
"# every gem's dependencies are loaded before it — spinelgems#19).\n"
|
|
451
|
+
# Put each vendored gem's lib root on $LOAD_PATH so a dependent's plain
|
|
452
|
+
# `require "<depgem>"` resolves under CRuby too (the differential verify
|
|
453
|
+
# and plain-Ruby dev runs). Spinel has no load path: it ignores both the
|
|
454
|
+
# $LOAD_PATH lines and the inter-gem `require`, and instead loads
|
|
455
|
+
# everything via the topo-ordered require_relatives below — so the same
|
|
456
|
+
# deps.rb is correct for both runtimes. (spinelgems#19, gap 2)
|
|
457
|
+
es.map { |e| e[:libdir] }.uniq.each do |d|
|
|
458
|
+
body << %{$LOAD_PATH.unshift(File.expand_path(#{d.inspect}, __dir__))\n}
|
|
459
|
+
end
|
|
460
|
+
body << "\n"
|
|
461
|
+
es.each { |e| body << %{require_relative "#{e[:require]}"\n} }
|
|
255
462
|
File.write(File.join(into, "deps.rb"), body)
|
|
256
463
|
end
|
|
257
464
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Bundler
|
|
2
2
|
module Spinel
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
VERSION = "0.
|
|
3
|
+
# 0.3.0: `spinel-compat vendor` grows build-units (cmake/make native deps
|
|
4
|
+
# built inside the consumer's vendor tree — heavy-native gems like toy's
|
|
5
|
+
# ggml vendor self-contained + relocatable, #14), and a new
|
|
6
|
+
# `spinel-compat why <gem>` legible diagnostic (#12).
|
|
7
|
+
VERSION = "0.3.0"
|
|
8
8
|
end
|
|
9
9
|
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Spinel
|
|
5
|
+
# `spinel-compat why <gem>` — a legible "why doesn't this gem work (yet)?"
|
|
6
|
+
# report (spinelgems#12). Turns a recorded (or freshly probed) Verdict into
|
|
7
|
+
# plain English: the cause, a category (Spinel limitation vs fixable compiler
|
|
8
|
+
# bug vs native C-ext vs metaprogramming vs dependency-blocked), the specific
|
|
9
|
+
# evidence, and what it would take — including whether the verdict is
|
|
10
|
+
# TERMINAL (won't improve without an upstream/compiler change) or FIXABLE.
|
|
11
|
+
#
|
|
12
|
+
# The data is already in the ledger (the rubric tag, the spinel warnings
|
|
13
|
+
# distilled into `reasons`, the static `risks`); this assembles it instead
|
|
14
|
+
# of making a user grep C output. Where deeper localization helps, it points
|
|
15
|
+
# at `why --probe` (live compiler output) / spinel-dev doctor for deeper localization.
|
|
16
|
+
class Why
|
|
17
|
+
# rubric/risk/reason signal -> structured explanation. :terminal is
|
|
18
|
+
# :native — won't work without a Spinel-native port (terminal here)
|
|
19
|
+
# :limitation— a Spinel feature gap (improves when the compiler grows it)
|
|
20
|
+
# :bug — a fixable Spinel compiler bug (file/track upstream)
|
|
21
|
+
# :dep — blocked by a dependency (improves when the dep does)
|
|
22
|
+
# :ok — already usable; nothing to fix
|
|
23
|
+
EXPLAIN = {
|
|
24
|
+
"c-extension" => {
|
|
25
|
+
category: "native (C extension)", terminal: :native,
|
|
26
|
+
cause: "ships a C extension; Spinel is whole-program AOT and never dlopens a .so.",
|
|
27
|
+
take: "port the extension to Spinel's ffi_cflags/ffi_func DSL (tep/SpinelKit pattern), " \
|
|
28
|
+
"or consume a pure-Ruby alternative. The CRuby .so cannot be vendored.",
|
|
29
|
+
},
|
|
30
|
+
"needs-dep" => {
|
|
31
|
+
category: "dependency / not self-contained", terminal: :dep,
|
|
32
|
+
cause: "fails under CRuby in the harness too — it needs an external gem, TLS, or network the probe doesn't provide.",
|
|
33
|
+
take: "vendor the missing dependency (must itself be Spinel-compatible) or smoke only the offline surface.",
|
|
34
|
+
},
|
|
35
|
+
"load-path" => {
|
|
36
|
+
category: "Spinel limitation (load path)", terminal: :limitation,
|
|
37
|
+
cause: %(Spinel ignored a plain `require "gem/part"` (it has no load path), so the gem's real classes never compiled.),
|
|
38
|
+
take: "restructure the gem to require_relative its own files (Spinel inlines those), or wait on load-path support.",
|
|
39
|
+
},
|
|
40
|
+
"needs-stdlib" => {
|
|
41
|
+
category: "Spinel limitation (stdlib surface)", terminal: :limitation,
|
|
42
|
+
cause: "requires a standard-library feature Spinel doesn't ship.",
|
|
43
|
+
take: "use a Spinel-safe shim for that surface (SpinelKit consolidates these: JSON/Logger/…), or wait on stdlib coverage.",
|
|
44
|
+
},
|
|
45
|
+
"codegen" => {
|
|
46
|
+
category: "compiler bug (codegen)", terminal: :bug,
|
|
47
|
+
cause: "ordinary Ruby produced a C compile error — a fixable Spinel codegen bug, not a limitation of your code.",
|
|
48
|
+
take: "file/track a matz/spinel issue. `why <gem> --probe` shows the live compiler error; spinel-dev doctor " \
|
|
49
|
+
"localizes it to a file:line, and the harness usually has a minimal reproducer already.",
|
|
50
|
+
},
|
|
51
|
+
"miscompile" => {
|
|
52
|
+
category: "compiler bug (silent miscompile)", terminal: :bug,
|
|
53
|
+
cause: "it compiles and runs, but the output diverges from CRuby — the most dangerous failure, silently wrong.",
|
|
54
|
+
take: "file a matz/spinel issue with the diff below; spinel-dev doctor + value-bisection localize it to a file:line + variable.",
|
|
55
|
+
},
|
|
56
|
+
"unsupported" => {
|
|
57
|
+
category: "unsupported call (often metaprogramming)", terminal: :bug,
|
|
58
|
+
cause: "Spinel could not resolve a call and silently emitted 0 — typically dynamic dispatch (send/define_method/extend).",
|
|
59
|
+
take: "if it's a small codegen gap, file a matz/spinel issue; if it's deep metaprogramming, the surface is currently unsupported.",
|
|
60
|
+
},
|
|
61
|
+
"build-error" => {
|
|
62
|
+
category: "build/run error", terminal: :bug,
|
|
63
|
+
cause: "the Spinel build or run failed for a reason outside the other buckets.",
|
|
64
|
+
take: "inspect the reasons below; `why <gem> --probe` re-runs the compiler and surfaces the raw error line.",
|
|
65
|
+
},
|
|
66
|
+
"smoke-error" => {
|
|
67
|
+
category: "inconclusive (smoke broken under CRuby)", terminal: :dep,
|
|
68
|
+
cause: "the behaviour smoke didn't run cleanly under plain CRuby, so no Spinel conclusion can be drawn.",
|
|
69
|
+
take: "fix the smoke (a self-contained example of the gem's API), then re-verify.",
|
|
70
|
+
},
|
|
71
|
+
"analyze-oom" => {
|
|
72
|
+
category: "compiler bug (analyzer OOM)", terminal: :bug,
|
|
73
|
+
cause: "the Spinel analyzer exhausts memory on this gem (matz/spinel#1302); it's blacklisted from probing.",
|
|
74
|
+
take: "terminal until matz/spinel#1302 lands; tracked there with reproducers.",
|
|
75
|
+
},
|
|
76
|
+
# Static pre-filter rejections (probe=static): a hard construct found by
|
|
77
|
+
# source scan before any compile. Thread/Mutex compile now but misbehave
|
|
78
|
+
# (matz/spinel#1360); TracePoint/ObjectSpace are out of the AOT model.
|
|
79
|
+
"hard-construct" => {
|
|
80
|
+
category: "Spinel limitation (runtime construct)", terminal: :limitation,
|
|
81
|
+
cause: "uses a construct the static filter rejects before compiling (threads/mutexes/tracing).",
|
|
82
|
+
take: "Thread/Mutex are single-thread-degradable (matz/spinel#1360); TracePoint/ObjectSpace/set_trace_func " \
|
|
83
|
+
"are outside the closed-world AOT model. Often the gem works once that one construct is shimmed.",
|
|
84
|
+
},
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
POSITIVE = {
|
|
88
|
+
"verified" => "compiles, and a behaviour smoke runs identically under CRuby and a Spinel-compiled binary. " \
|
|
89
|
+
"Trustworthy — the only verdict that earns a curated-source slot.",
|
|
90
|
+
"loaded" => "compiles and loads identically to CRuby, but no behaviour smoke has exercised its logic at this rev — " \
|
|
91
|
+
"so a silent miscompile in that logic is still possible. Run `verify --smoke <file>` to lift it to verified.",
|
|
92
|
+
"clean" => "compiles clean and uses no dynamic constructs Spinel degrades — but it has only been compiled, not run. " \
|
|
93
|
+
"Run `verify` (require-only → loaded) or `verify --smoke` (behaviour → verified) to confirm it works.",
|
|
94
|
+
"risky" => "compiles, but the source uses dynamic constructs Spinel degrades silently — allowed by default, " \
|
|
95
|
+
"rejected under `check --strict`. Whether it actually works depends on whether those paths run; " \
|
|
96
|
+
"a behaviour smoke (`verify --smoke`) is the only way to know.",
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
USABLE = %w[verified loaded clean risky].freeze
|
|
100
|
+
|
|
101
|
+
def initialize(out: $stdout)
|
|
102
|
+
@out = out
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Render the report for a Verdict (from the ledger or a live probe).
|
|
106
|
+
def report(v, source: "ledger")
|
|
107
|
+
@out.puts
|
|
108
|
+
@out.puts "spinel-compat why #{v.gem} (#{v.version} @ #{v.rev || 'unknown rev'}, via #{source})"
|
|
109
|
+
@out.puts
|
|
110
|
+
|
|
111
|
+
glyph = { "verified" => "★", "loaded" => "○", "clean" => "✓", "risky" => "~", "rejected" => "✗" }[v.verdict] || "?"
|
|
112
|
+
line "verdict", "#{glyph} #{v.verdict}"
|
|
113
|
+
|
|
114
|
+
if USABLE.include?(v.verdict)
|
|
115
|
+
positive(v, glyph)
|
|
116
|
+
else
|
|
117
|
+
negative(v)
|
|
118
|
+
end
|
|
119
|
+
@out.puts
|
|
120
|
+
v
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def positive(v, _glyph)
|
|
126
|
+
line "meaning", POSITIVE[v.verdict] if POSITIVE[v.verdict]
|
|
127
|
+
# risky/clean can still carry dynamic-construct risks worth surfacing.
|
|
128
|
+
dyn = dynamic_risks(v)
|
|
129
|
+
line "watch", "uses #{dyn.join(', ')} — Spinel may degrade these silently; a behaviour smoke confirms real behaviour." unless dyn.empty?
|
|
130
|
+
unmet = blocking_deps(v)
|
|
131
|
+
line "deps", "depends on #{unmet.join(', ')} (declared `needs:`) — vendor those too." unless unmet.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def negative(v)
|
|
135
|
+
tag = rubric_tag(v) || static_tag(v)
|
|
136
|
+
info = EXPLAIN[tag] || EXPLAIN["build-error"]
|
|
137
|
+
|
|
138
|
+
line "category", info[:category]
|
|
139
|
+
line "cause", info[:cause]
|
|
140
|
+
|
|
141
|
+
detail = evidence(v, tag)
|
|
142
|
+
line "detail", detail unless detail.empty?
|
|
143
|
+
|
|
144
|
+
unmet = blocking_deps(v)
|
|
145
|
+
line "blocked-by", "rejected/unmet dependencies: #{unmet.join(', ')}" unless unmet.empty?
|
|
146
|
+
|
|
147
|
+
line "terminal?", terminal_line(info[:terminal])
|
|
148
|
+
line "what it'd take", info[:take]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A static-probe rejection has no rubric tag; its reason IS the signal.
|
|
152
|
+
def static_tag(v)
|
|
153
|
+
reasons = v.reasons + v.risks
|
|
154
|
+
return "analyze-oom" if reasons.any? { |r| r.include?("analyze-oom") }
|
|
155
|
+
return "c-extension" if reasons.include?("c-extension")
|
|
156
|
+
return "hard-construct" if reasons.any? { |r| r.start_with?("hard:") }
|
|
157
|
+
return "unsupported" if reasons.any? { |r| r.start_with?("unresolved:") }
|
|
158
|
+
return "codegen" if reasons.any? { |r| r =~ /out\.c:\d+:\d+: *error:/ }
|
|
159
|
+
return "needs-dep" if reasons.any? && reasons.all? { |r| r.start_with?("needs:") }
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# --- signal extraction ---------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def rubric_tag(v)
|
|
166
|
+
v.reasons.grep(/\Arubric:/).first&.sub("rubric:", "")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
DYNAMIC = %w[define_method instance_eval class_eval module_eval eval send method_missing
|
|
171
|
+
const_set define_singleton_method].freeze
|
|
172
|
+
|
|
173
|
+
def dynamic_risks(v)
|
|
174
|
+
v.risks.select { |r| DYNAMIC.include?(r) || DYNAMIC.any? { |d| r.include?(d) } }.uniq
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def blocking_deps(v)
|
|
178
|
+
(v.reasons + v.risks).grep(/\Aneeds:/).map { |r| r.sub("needs:", "") }.uniq
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The concrete evidence lines for a rejection, by tag.
|
|
182
|
+
def evidence(v, tag)
|
|
183
|
+
case tag
|
|
184
|
+
when "miscompile"
|
|
185
|
+
v.reasons.grep(/\Adiff:/).first.to_s.sub("diff:", "CRuby vs Spinel ").strip
|
|
186
|
+
when "unsupported"
|
|
187
|
+
calls = v.reasons.grep(/\Aunresolved:/).map { |r| r.sub("unresolved:", "") }
|
|
188
|
+
calls.empty? ? "" : "unresolved: #{calls.first(8).join(', ')}#{calls.size > 8 ? " (+#{calls.size - 8} more)" : ''}"
|
|
189
|
+
when "load-path", "needs-stdlib"
|
|
190
|
+
miss = v.reasons.grep(/could not be resolved|no .+\.rb/).first
|
|
191
|
+
miss ? miss.strip : ""
|
|
192
|
+
when "codegen", "build-error"
|
|
193
|
+
v.reasons.grep(/out\.c:|error:|fatal/i).first.to_s.strip
|
|
194
|
+
when "hard-construct"
|
|
195
|
+
v.reasons.grep(/\Ahard:/).map { |r| r.sub("hard:", "") }.join(", ")
|
|
196
|
+
when "c-extension"
|
|
197
|
+
"an ext/ directory with C/C++ sources (compiled by mkmf under CRuby)"
|
|
198
|
+
else
|
|
199
|
+
# fall back to the most informative non-rubric reason
|
|
200
|
+
v.reasons.reject { |r| r.start_with?("rubric:") }.first.to_s.strip
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def terminal_line(kind)
|
|
205
|
+
case kind
|
|
206
|
+
when :native then "TERMINAL here — needs a Spinel-native port, not a catalog/compiler change."
|
|
207
|
+
when :limitation then "improves when Spinel grows the feature (a limitation, not a per-gem bug)."
|
|
208
|
+
when :bug then "FIXABLE — a compiler bug; the catalog tracks it and it can graduate on a Spinel fix."
|
|
209
|
+
when :dep then "conditional — unblocks when the dependency (or the smoke) is resolved."
|
|
210
|
+
else "—"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def line(label, text)
|
|
215
|
+
@out.puts format(" %-14s %s", label, text)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/bundler/spinel.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-spinel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ori Pekelman
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: A Bundler plugin + CLI that probes whether gems compile under Spinel
|
|
14
13
|
and gates `bundle lock` on a forward-compatible, engine-rev-keyed compatibility
|
|
@@ -37,6 +36,7 @@ files:
|
|
|
37
36
|
- lib/bundler/spinel/gem_fetcher.rb
|
|
38
37
|
- lib/bundler/spinel/history.rb
|
|
39
38
|
- lib/bundler/spinel/ledger.rb
|
|
39
|
+
- lib/bundler/spinel/load_bearing.rb
|
|
40
40
|
- lib/bundler/spinel/localizer.rb
|
|
41
41
|
- lib/bundler/spinel/platform.rb
|
|
42
42
|
- lib/bundler/spinel/probe.rb
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- lib/bundler/spinel/vendorer.rb
|
|
49
49
|
- lib/bundler/spinel/verifier.rb
|
|
50
50
|
- lib/bundler/spinel/version.rb
|
|
51
|
+
- lib/bundler/spinel/why.rb
|
|
51
52
|
- plugins.rb
|
|
52
53
|
homepage: https://github.com/OriPekelman/spinelgems
|
|
53
54
|
licenses:
|
|
@@ -57,7 +58,6 @@ metadata:
|
|
|
57
58
|
bug_tracker_uri: https://github.com/OriPekelman/spinelgems/issues
|
|
58
59
|
changelog_uri: https://github.com/OriPekelman/spinelgems/blob/main/CHANGELOG.md
|
|
59
60
|
rubygems_mfa_required: 'true'
|
|
60
|
-
post_install_message:
|
|
61
61
|
rdoc_options: []
|
|
62
62
|
require_paths:
|
|
63
63
|
- lib
|
|
@@ -72,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '0'
|
|
74
74
|
requirements: []
|
|
75
|
-
rubygems_version: 3.
|
|
76
|
-
signing_key:
|
|
75
|
+
rubygems_version: 3.6.9
|
|
77
76
|
specification_version: 4
|
|
78
77
|
summary: Resolution-time gem-compatibility gating for the Spinel Ruby AOT compiler
|
|
79
78
|
test_files: []
|