bundler-spinel 0.0.1.pre

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fedb059cdc7900cd98ccfff408a28e1fb4e4eaad68d9e7afe48c9d967cb97b71
4
+ data.tar.gz: a1d6fbfb31256d58cacab1cc120290455bd38de869295504a1c6dcdee96ea8da
5
+ SHA512:
6
+ metadata.gz: acd75d23500e6af2d3e55c116bc0f5d82e8a4c8b1912049a349967e9cc920134ffe4820862abeeb6a753445f0a668cf26a9248c6aed4b983ea0dff7c262c13ee
7
+ data.tar.gz: 1bd2fa39872c215534c919f6a8c05cf01cf6dc30e5cce162d470d8d0ec3d750a09e06bc3a404d05f1ac4195a0c86232ca6e93f9d761e5d99771603361fd58756
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,164 @@
1
+ # bundler-spinel — architecture
2
+
3
+ Make Spinel gem-incompatibility fail **at `bundle lock` time**, not at compile
4
+ time (where Spinel may silently emit a no-op instead of an error) — and make
5
+ that verdict **forward-compatible**, so a gem rejected today flips to accepted
6
+ the moment a future Spinel learns the feature it needed.
7
+
8
+ ## The problem it solves
9
+
10
+ Spinel is a whole-program Ruby→C AOT compiler with no gems, no eval, no
11
+ metaprogramming. Two facts make naive dependency management dangerous:
12
+
13
+ 1. **Bundler can't gate compatibility.** There is no `required_ruby_engine`
14
+ gemspec field; `required_ruby_version` is engine-blind; you can't fabricate a
15
+ platform variant for a gem you don't publish. The Gemfile `engine: "spinel"`
16
+ directive is a *post-resolution* check — `bundle lock` ignores it (exit 0);
17
+ only `bundle install` fires the guard (exit 18). So resolution can't reject
18
+ an incompatible gem.
19
+
20
+ 2. **Spinel doesn't fail loudly.** On unsupported Ruby it prints
21
+ `warning: ... cannot resolve call to 'eval' ... (emitting 0)` and degrades
22
+ the call to a no-op, exiting 0. Worse, some constructs (`define_method`,
23
+ silent miscompiles like local-var-name collapse / Int-0-as-nil) produce no
24
+ warning at all. So "it compiled" ≠ "it works."
25
+
26
+ This tool moves the decision to resolution time and grounds it in actually
27
+ running Spinel over the gem source.
28
+
29
+ ## The ledger — single source of truth
30
+
31
+ `ledger/compat.jsonl`, append-only, one line per **`(gem, version, engine_rev)`**:
32
+
33
+ ```json
34
+ {"gem":"rake","version":"13.4.2","rev":"git:0adca86+dirty",
35
+ "verdict":"rejected","reasons":["analyze-failed"],
36
+ "risks":["eval","method_missing","needs:rbconfig"],"probe":"compile+scan","at":"…"}
37
+ ```
38
+
39
+ **`engine_rev` is the forward-compat key.** Spinel ships no version string
40
+ (`spinel --version` prints usage; `git describe` is a bare SHA), so we key on the
41
+ git revision of the Spinel checkout, falling back to a binary content-hash.
42
+ Upgrade Spinel → new rev → cache miss → automatic re-probe. No hand-maintained
43
+ blocklist; a `rejected` verdict is always "rejected *as of this rev, because of
44
+ these named features*," never "rejected forever." `spinel-compat reprobe` sweeps
45
+ known gems under the current rev to surface what newly passes.
46
+
47
+ ## The verdict ladder
48
+
49
+ Spinel's failure modes aren't exit codes, so one signal isn't enough:
50
+
51
+ | Verdict | Earned by | Trusted by |
52
+ |---|---|---|
53
+ | `rejected` | unsupported **call** (`cannot resolve call to 'X'` → `unresolved:X`), or `analyze failed` / non-zero exit | gate fails the lock |
54
+ | `risky` | compiles clean, but static scan found constructs Spinel degrades silently (`define_method`, `eval`, `send`, `method_missing`, C-ext…) | gate allows; `--strict` fails |
55
+ | `clean` | compiles clean, no risky constructs | gate allows |
56
+ | `verified` | `clean` **and** the gem's own tests pass through a Spinel-compiled harness | curated whitelist + platform badge |
57
+
58
+ Two probe signals feed this:
59
+
60
+ - **Compile signal** (`spinel -c` over the gem's lib entrypoints) — parses stderr.
61
+ `cannot resolve call to 'X'` is the gold signal: precise and forward-compatible.
62
+ - **Static risk scan** — catches constructs that are silently degraded (no
63
+ warning) or hidden behind dead-code elimination.
64
+
65
+ ### Honest limitations (why `verified` must exist)
66
+
67
+ - **No load path.** Spinel resolves plain `require "x"` only against
68
+ `<spinel>/lib`. A gem's internal `require "gem/part"` and its stdlib requires
69
+ don't resolve, so the compile probe doesn't fully follow multi-file gems
70
+ (recorded as `needs:X` notes, not rejections). The compile signal is a **lower
71
+ bound** on problems. `require_relative`-based gems probe well; plain-`require`
72
+ gems under-probe.
73
+ - **Silent miscompiles** (var-name collapse, Int-0-as-nil) emit no warning and
74
+ exit 0. The static scan can't see them either.
75
+
76
+ So `clean` means "no problem *found cheaply*," not "correct." Only `verified` —
77
+ compiling the gem's test suite and running it — is trustworthy, which is exactly
78
+ why the curated whitelist and the platform badge require it.
79
+
80
+ ## Placement — "make it work" (the primary job)
81
+
82
+ Independent of the ledger: `spinel-compat vendor` reads a `Gemfile.lock`, copies
83
+ each gem's `lib/` into `vendor/spinel/<name>/`, and writes `vendor/spinel/deps.rb`
84
+ (a `require_relative` manifest in lock order). A Spinel program does
85
+ `require_relative "vendor/spinel/deps"`. This is the reusable form of the
86
+ hand-vendoring projects do today, and the reason the convention is usable at all
87
+ given Spinel has no load path. Gating is layered on top but advisory — placement
88
+ and compatibility are different jobs.
89
+
90
+ ## Gating consumers, all views over the ledger
91
+
92
+ ### 1. Lock-time gate — `bundle spinel-lock` (BUILT)
93
+ `bundle lock` (resolves, ignoring the engine directive), then `check` resolves a
94
+ verdict for every locked gem (ledger hit, or probe-on-miss) and exits non-zero on
95
+ any `rejected`. The headline: resolution-time failure with feature-named reasons.
96
+ Also the **backstop** for consumer #2's leak (below): it re-checks every gem in
97
+ the *resulting* lock against the ledger regardless of where it resolved from.
98
+
99
+ ### 1b. The `verified` rung — `spinel-compat verify` (BUILT)
100
+ Differential testing: run a smoke program once under CRuby and once Spinel-
101
+ compiled, diff stdout. The only signal that catches Spinel's **silent
102
+ miscompiles** — they emit no warning and exit 0, so the cheap probe calls them
103
+ `clean`, but a differential run diverges. Proven: a method doing `h[k].nil? ? -1
104
+ : v` over a stored `0` is `clean` to the probe but caught as `rejected:miscompile`
105
+ by verify (`L2 cruby="-1" spinel="0"` — the Int-0-as-nil footgun). The smoke is
106
+ the unit of trust, so `verified` is opt-in and smoke-supplied.
107
+
108
+ ### 2. Curated source / proxy — `spinel-compat serve` (BUILT, MVP)
109
+ A Compact Index source serving only vetted gems from a local store of .gem
110
+ artifacts (`--min verified|clean|risky`). Proven end-to-end: `bundle lock` against
111
+ it resolves a vetted gem (exit 0) and fails on an absent one (exit 7, "could not
112
+ find gem … in repository … or installed locally"). The "whitelist" is not a file:
113
+ it's the acceptable-verdict subset of the ledger at the pinned rev. `path:`/`git:`
114
+ siblings (`gem "tep", path: …`) are the degenerate one-gem curated source and
115
+ probe in place via `probe --dir`.
116
+
117
+ **Leak caveat (important):** Bundler *also* considers locally-installed gems
118
+ ("…or installed locally"). In a polluted environment an unvetted-but-installed
119
+ gem can resolve and even be mis-attributed to the source. So the curated source
120
+ gates a **clean** environment (CI); pair it with consumer #1 as a backstop for
121
+ dev machines. Read-through filtering of upstream rubygems is a documented
122
+ extension; empirically the third-party ecosystem is ~all-rejected today, so the
123
+ local-store mode is what carries weight.
124
+
125
+ ### 3. Platform-variant opt-in (designed — `platform.rb` stub)
126
+ The bridge from "our ledger says verified" to "stock Bundler selects it,"
127
+ reusing the same machinery JRuby uses with the `java` platform. A `verified` gem
128
+ is republished (to the curated source) under a `spinel` platform token — likely
129
+ `<cpu>-spinel-<engine_rev>` so the badge is rev-scoped — and Bundler's normal
130
+ platform resolution prefers it. Opt-in because earning the badge means someone
131
+ ran the gem's tests through a Spinel-compiled harness.
132
+
133
+ ## Dogfooding — the curated source served by Tep
134
+
135
+ The MVP proxy is CRuby/WEBrick (fast to prove Bundler resolves against it). The
136
+ target is to serve the same Compact Index endpoints from **Tep** — the Sinatra-
137
+ flavoured framework that *itself compiles via Spinel*. Then the Spinel
138
+ dependency-manager's source is a Spinel program: the system vets its own
139
+ substrate. Tep already has the pieces (`sphttp` server, routing, an HTTP client
140
+ for upstream fetch); the open questions are Spinel support for the bits the proxy
141
+ needs — MD5/SHA256 (digest), and reading the JSONL ledger — which are themselves
142
+ good probe targets. Serving `/names`, `/versions`, `/info/<gem>`, `/gems/<file>`
143
+ is plain text + file bytes, well within Tep's range.
144
+
145
+ ## Layout
146
+
147
+ ```
148
+ lib/bundler/spinel/
149
+ engine.rb # locate compiler, derive forward-compat engine rev
150
+ ledger.rb # append-only JSONL verdict store, keyed on (gem,version,rev)
151
+ gem_fetcher.rb # gem fetch + unpack (source, not install), cached
152
+ probe.rb # compile signal + static risk scan -> verdict
153
+ verifier.rb # differential CRuby-vs-Spinel smoke -> verified
154
+ vendorer.rb # place lockfile deps where Spinel finds them + deps.rb
155
+ survey.rb # parallel wholesale review -> reason histogram
156
+ checker.rb # Gemfile.lock -> per-gem verdict -> pass/fail gate
157
+ cli.rb # `spinel-compat` dispatcher
158
+ command.rb # Bundler plugin command (bundle spinel-lock / -check)
159
+ proxy.rb # STUB: curated source
160
+ platform.rb # STUB: platform-variant opt-in
161
+ exe/spinel-compat # standalone CLI
162
+ plugins.rb # Bundler plugin entry
163
+ ledger/compat.jsonl
164
+ ```
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to `bundler-spinel` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.0.1.pre] — 2026-05-26
8
+
9
+ First pre-release. **Experimental** — the CLI surface, the verdict vocabulary,
10
+ and the ledger format may all change before `0.0.1`. Install with `--pre`.
11
+
12
+ ### Added
13
+ - Initial public release: the Gemfile convention for Spinel projects, the
14
+ `spinel-compat` CLI (`vendor`, `check`, `probe`, `verify`, `survey`,
15
+ `serve`, `ledger`, `reprobe`), and the `spinel-lock` / `spinel-check`
16
+ Bundler plugin commands.
17
+ - Forward-compatible, engine-rev-keyed compatibility ledger.
18
+ - Wholesale `survey` with a thread-safe ledger, a per-compile wall-clock
19
+ timeout (`analyze-timeout` reject reason), and a ledger-based report.
20
+
21
+ [0.0.1.pre]: https://github.com/OriPekelman/spinelgems/releases/tag/v0.0.1.pre
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ori Pekelman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # bundler-spinel
2
+
3
+ > ⚠️ **Pre-release / experimental** (`0.0.1.pre`). The CLI surface, the verdict
4
+ > vocabulary, and the ledger format may all change before `0.0.1`. Install with
5
+ > `--pre`. (`spinelgems.org` is not live yet.)
6
+
7
+ **Use a standard `Gemfile` for your [Spinel](https://github.com/matz/spinel)
8
+ project — for now.** Spinel-compiled projects have no shared way to declare or
9
+ exchange dependencies, so each vendors by hand. Rather than design a package
10
+ manager, borrow a format everyone already knows and revisit later. See
11
+ [RFC.md](RFC.md) for the proposal.
12
+
13
+ `bundler-spinel` is a small Bundler plugin that makes that practical in two ways:
14
+
15
+ 1. **Makes it work** — places resolved dependencies where Spinel can actually
16
+ find them. Spinel has no load path and inlines `require_relative`, so a dep
17
+ has to be *placed* and wired. `spinel-compat vendor` does that from a lockfile.
18
+ 2. **Gates** — Spinel silently emits a no-op for unsupported Ruby (exit 0), so
19
+ "it compiled" ≠ "it works". The plugin probes gems and flags incompatible ones
20
+ at `bundle lock` time, with reasons that name the missing feature — nicer than
21
+ a silent miscompile. Verdicts are forward-compatible (keyed on the Spinel rev).
22
+
23
+ ## The Gemfile convention
24
+
25
+ ```ruby
26
+ source "https://rubygems.org"
27
+ ruby "3.3.0", engine: "spinel", engine_version: "0.0.0"
28
+
29
+ gem "tep", git: "https://…/tep.git" # siblings via path:/git: (replaces rsync)
30
+ gem "some_pure_ruby_lib"
31
+ ```
32
+
33
+ `bundle lock` resolves normally (it ignores the engine marker); the marker guards
34
+ `bundle install` (exit 18 under CRuby). Nothing here is novel — that's the point.
35
+
36
+ ## Quick start
37
+
38
+ ```sh
39
+ # 1. make it work — place deps where Spinel finds them
40
+ bundle lock
41
+ exe/spinel-compat vendor # -> vendor/spinel/<gem>/lib + vendor/spinel/deps.rb
42
+ # then `require_relative "vendor/spinel/deps"` from your Spinel entrypoint
43
+
44
+ # 2. gate — flag what Spinel can't compile, early
45
+ exe/spinel-compat check Gemfile.lock # exit 1 if any gem is rejected
46
+ ```
47
+
48
+ As a Bundler plugin:
49
+
50
+ ```sh
51
+ bundle plugin install bundler-spinel --git https://github.com/OriPekelman/spinelgems.git # or --path .
52
+ bundle spinel-lock # bundle lock, then report incompatible gems
53
+ bundle spinel-check # gate an existing Gemfile.lock
54
+ ```
55
+
56
+ ## The rest of the toolbelt
57
+
58
+ ```sh
59
+ exe/spinel-compat engine # detected compiler + engine rev
60
+ exe/spinel-compat probe rake [--dir P] # probe one gem (or a local/sibling dir)
61
+ exe/spinel-compat verify NAME --smoke F # differential CRuby-vs-Spinel run -> verified
62
+ exe/spinel-compat survey --list F # wholesale review -> reason histogram
63
+ exe/spinel-compat serve --store DIR # curated source (only vetted gems)
64
+ exe/spinel-compat ledger / reprobe # inspect / re-probe under current rev
65
+ ```
66
+
67
+ Verdicts: `✓ clean` · `★ verified` · `~ risky` · `✗ rejected`.
68
+
69
+ ## Environment
70
+
71
+ - `SPINEL_DIR` — path to the Spinel checkout (default `~/spinel`; falls back to a `spinel` on `PATH`).
72
+ - `SPINEL_COMPAT_LEDGER` — ledger path (default `ledger/compat.jsonl`).
73
+
74
+ ## Status
75
+
76
+ Working: the Gemfile convention, `vendor` (placement), the lock-time gate +
77
+ Bundler plugin, the probe + forward-compat ledger, the `verified` differential
78
+ harness, the curated source (`serve`), and the wholesale `survey`.
79
+
80
+ The probe is a **lower bound** — Spinel's lack of a load path means multi-file
81
+ plain-`require` gems under-probe, and silent miscompiles are invisible to it.
82
+ Trust `verified` (smoke runs identically under CRuby and Spinel), not `clean`,
83
+ where it matters. Empirically most third-party gems reject today, so the weight
84
+ is on your own vetted gems and `path:`/`git:` siblings — not a rubygems mirror.
data/RFC.md ADDED
@@ -0,0 +1,120 @@
1
+ # RFC: Use a Gemfile for Spinel projects (for now)
2
+
3
+ **Status:** draft (for discussion)
4
+ **Author:** Ori Pekelman
5
+ **Prototype:** [`bundler-spinel`](./README.md) — working, see [ARCHITECTURE.md](./ARCHITECTURE.md)
6
+
7
+ ## Summary
8
+
9
+ Spinel-compiled projects have no shared way to declare or exchange dependencies,
10
+ so each one vendors by hand. This RFC proposes a deliberately small, temporary
11
+ answer that requires **no new design**:
12
+
13
+ 1. **Declare dependencies in a standard `Gemfile`.** Mark the project with
14
+ `ruby "3.x", engine: "spinel", engine_version: "0.0.0"`. That's the whole
15
+ convention. It buys a familiar manifest, `path:`/`git:` sources for sibling
16
+ projects, and Bundler's resolver + lockfile — for free.
17
+
18
+ 2. **A small Bundler plugin** ([`bundler-spinel`](./README.md)) that does two
19
+ things: **(a) makes it work** — places resolved dependencies where Spinel can
20
+ actually find them; and **(b) gates** — flags gems Spinel can't compile early,
21
+ so the experience is nicer than a silent miscompile.
22
+
23
+ The point is to **postpone the big discussion**. Spinel isn't near a release, and
24
+ there's no expectation it adopts any particular dependency-management design. But
25
+ projects need *some* interop today — so let's borrow a format everyone knows and
26
+ revisit later, rather than design a package manager now.
27
+
28
+ ## Motivation
29
+
30
+ Interop is already happening, ad-hoc. Roundhouse (a separate author's project)
31
+ vendors part of Tep; our own projects carry a mix of rsync'd copies and hand-
32
+ maintained concatenation. Every project reinvents "get this other code into a
33
+ shape Spinel will compile." A shared, boring convention removes that duplication
34
+ without anyone committing to a long-term model.
35
+
36
+ Two facts make a thin tool worthwhile (both verified — Bundler 2.7.2 / CRuby):
37
+
38
+ - **Bundler already does the right thing with the engine marker.** `bundle lock`
39
+ ignores `engine: "spinel"` and resolves normally (exit 0); only `bundle
40
+ install` fires the guard ("Your Ruby engine is ruby, but your Gemfile specified
41
+ spinel", exit 18). So the convention costs nothing and fails loudly in the
42
+ right place.
43
+
44
+ - **Spinel doesn't fail loudly on unsupported Ruby.** It prints
45
+ `warning: … cannot resolve call to 'eval' … (emitting 0)` and degrades the call
46
+ to a no-op, exiting **0**; some constructs (and silent miscompiles) warn not at
47
+ all. So "it compiled" ≠ "it works" — which is why a little gating help is worth
48
+ having.
49
+
50
+ ## Proposal 1 — the Gemfile convention
51
+
52
+ A Spinel project's `Gemfile`:
53
+
54
+ ```ruby
55
+ source "https://rubygems.org"
56
+ ruby "3.3.0", engine: "spinel", engine_version: "0.0.0"
57
+
58
+ gem "tep", git: "https://…/tep.git" # sibling projects via path:/git:
59
+ gem "some_pure_ruby_lib" # third-party, if it compiles
60
+ ```
61
+
62
+ Nothing here is novel — that's the point. `bundle lock` produces a normal
63
+ lockfile; siblings resolve through `path:`/`git:` sources (replacing rsync /
64
+ manual vendoring); third-party gems resolve from rubygems.org. The engine marker
65
+ documents the target and guards `bundle install`.
66
+
67
+ ## Proposal 2 — the Bundler plugin
68
+
69
+ ### (a) Make it work — placement
70
+
71
+ Spinel has no load path (plain `require "x"` resolves only against
72
+ `<spinel>/lib`) and inlines `require_relative`. So a resolved dependency has to
73
+ be *placed* where Spinel will follow it. The plugin vendors each locked gem's
74
+ `lib/` into a project dir and generates a `require_relative` manifest:
75
+
76
+ ```
77
+ spinel-compat vendor # reads Gemfile.lock
78
+ # -> vendor/spinel/<gem>/lib/… and vendor/spinel/deps.rb
79
+ ```
80
+
81
+ A Spinel program then just `require_relative "vendor/spinel/deps"`. This is the
82
+ reusable form of what projects already do by hand (concatenation scripts, partial
83
+ vendoring).
84
+
85
+ ### (b) Gating — a nicer experience
86
+
87
+ Because Spinel silently no-ops unsupported Ruby, the plugin probes gems (compile
88
+ + static scan, optionally a differential CRuby-vs-Spinel run) and flags ones that
89
+ won't work — at `bundle lock` time, with reasons that name the missing feature:
90
+
91
+ ```
92
+ bundle spinel-lock # bundle lock, then report incompatible gems
93
+ ```
94
+
95
+ Verdicts are **forward-compatible**: each is keyed on the Spinel revision (Spinel
96
+ ships no version, so we key on the git rev / binary hash, scoped by platform).
97
+ Upgrade Spinel → re-probe → a gem rejected today clears the moment the feature it
98
+ needed lands. No hand-maintained blocklist.
99
+
100
+ Gating is advisory by design — placement and compatibility are different jobs.
101
+ The plugin makes the failure visible and early; it doesn't try to be a wall.
102
+
103
+ ## Open questions (for discussion, not asks)
104
+
105
+ These are things we ran into, offered as discussion points — not feature
106
+ requests, and not assuming Spinel wants any of them:
107
+
108
+ - **Signalling unsupported constructs.** Today they're silent no-ops at exit 0.
109
+ Would a non-zero exit or a structured (e.g. JSON) diagnostic — perhaps behind a
110
+ `--check`/`--strict` flag — be in scope or interest? It's the difference
111
+ between inferring compatibility from stderr and reading it from a contract.
112
+ - **A stable engine identity.** We key verdicts on a git rev / binary hash for
113
+ now. If Spinel ever wanted to expose a version/rev, the ledger and the
114
+ `engine_version:` marker could line up — but there's no rush.
115
+ - **Resolving `require` beyond `<spinel>/lib`.** A load-path notion would let
116
+ multi-file gems and stdlib shims resolve (and would make probing more
117
+ accurate). Possibly useful well beyond this tool; possibly out of scope.
118
+
119
+ None of these are needed for the convention or the plugin to be useful today.
120
+ They're just where the seams are, if there's ever appetite to smooth them.
data/exe/spinel-compat ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/bundler/spinel/cli"
3
+
4
+ exit Bundler::Spinel::CLI.new.run(ARGV)
@@ -0,0 +1,58 @@
1
+ require "bundler"
2
+ require "bundler/lockfile_parser"
3
+
4
+ module Bundler
5
+ module Spinel
6
+ # The resolution-time gate. Reads a Gemfile.lock, resolves a verdict for
7
+ # every locked gem (from the ledger, or by probing on a cache miss), and
8
+ # decides pass/fail. This is what turns Spinel's compile-time-or-never
9
+ # failure into a `bundle lock`-time failure.
10
+ class Checker
11
+ Result = Struct.new(:verdict, :rejected, :risky, :probed, keyword_init: true)
12
+
13
+ def initialize(engine: Engine.new, ledger: Ledger.new)
14
+ @engine = engine
15
+ @ledger = ledger
16
+ @fetcher = GemFetcher.new
17
+ @probe = Probe.new(@engine, @ledger)
18
+ end
19
+
20
+ # strict: treat `risky` as a failure too.
21
+ def check(lockfile = "Gemfile.lock", strict: false)
22
+ @engine.ensure!
23
+ parsed = Bundler::LockfileParser.new(File.read(lockfile))
24
+ rejected = []
25
+ risky = []
26
+ verdicts = []
27
+
28
+ parsed.specs.each do |spec|
29
+ # path:/git: sources (e.g. a sibling like tep) probe in place; the
30
+ # GEM source fetches. We only have name+version here, so prototype
31
+ # handles the rubygems case; path/git probing is a documented TODO.
32
+ v = verdict_for(spec.name, spec.version.to_s)
33
+ next unless v # skipped (unfetchable / TODO source)
34
+
35
+ verdicts << v
36
+ rejected << v if v.rejected?
37
+ risky << v if v.risky?
38
+ end
39
+
40
+ ok = rejected.empty? && (!strict || risky.empty?)
41
+ Result.new(verdict: ok, rejected: rejected, risky: risky, probed: verdicts)
42
+ end
43
+
44
+ private
45
+
46
+ def verdict_for(name, version)
47
+ cached = @ledger.lookup(name, version, @engine.rev)
48
+ return cached if cached
49
+
50
+ dir = @fetcher.fetch(name, version)
51
+ @probe.probe(name, version, dir)
52
+ rescue Error => e
53
+ warn "[spinel-compat] skipped #{name} #{version}: #{e.message}"
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end