bundler-spinel 0.1.0 → 0.2.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 +43 -0
- data/README.md +2 -2
- data/lib/bundler/spinel/engine_installer.rb +29 -15
- data/lib/bundler/spinel/localizer.rb +83 -0
- data/lib/bundler/spinel/verifier.rb +10 -0
- data/lib/bundler/spinel/version.rb +1 -1
- data/lib/bundler/spinel.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12d700f606c1cf8b69ae4fe623c16293bfbc560fe09b59c8fadeb4529a8a02f6
|
|
4
|
+
data.tar.gz: e08a61f93c03930e4c85b0e1effbaf3dfb2e05f3bd1d1979459fe4e667de4f4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3390679f31399277e342e4853530a15d9017aa8de9cbb822dc933420953e4badc97ca497302cbfa8acf29dc6cb1380f2508c515b7e00aa1e5a0eaca502a64e73
|
|
7
|
+
data.tar.gz: ccf7ac613f132909ae9598f01be97c14c6f4fc12dcd70a5352c2ea4f62beb2303589463039c7c98d8787918131323c83cbe2f101ababae9e2161afa6a64c8a5e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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.2.0] — 2026-06-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`verify` self-localizes a miscompile.** When the differential smoke diverges
|
|
11
|
+
(CRuby and Spinel both run but disagree on stdout), `verify` now runs the
|
|
12
|
+
value-bisection harness (`spinel-dev tools/value-bisect/bisect.sh --json`) on
|
|
13
|
+
the still-on-disk harness and, when it pins the first scalar local to part
|
|
14
|
+
ways, appends a `localized:<file>:<line> <var> cruby=… spinel=…` reason. This
|
|
15
|
+
upgrades a bare `diff:L2 cruby=… spinel=…` ("the outputs differ") into a line
|
|
16
|
+
to look at ("`x` is wrong here"). Strictly best-effort and non-fatal: if the
|
|
17
|
+
harness can't be found (it's a separate repo — override with `SPINEL_BISECT`,
|
|
18
|
+
else probed next to the engine and at `~/sites/spinel-dev`) or can't attribute
|
|
19
|
+
the divergence to a traced scalar, the verdict is returned unchanged.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **`init` scaffold uses the published tep gem.** Now that tep is on RubyGems
|
|
23
|
+
(`tep 0.11.1`), the generated `Gemfile` emits plain `gem "tep", ">= 0.11.1"`
|
|
24
|
+
instead of the `git:`-fallback comment. Docs (`README`, `docs/adoption.md`)
|
|
25
|
+
updated to the published-gem form. spinelgems#10.
|
|
26
|
+
- **`init`'s `bin/build` made self-sufficient + correct.** A clean-room run
|
|
27
|
+
showed the old `bundle install && ./bin/build` flow couldn't run: the
|
|
28
|
+
`engine: spinel` marker makes `bundle install` refuse under CRuby (by design),
|
|
29
|
+
and `bundle lock` + `vendor` place tep's *lib* but never install the `tep`
|
|
30
|
+
*CLI*. `bin/build` now `gem install`s tep if absent, uses `bundle lock` (not
|
|
31
|
+
install), exports `SPINEL` (so tep finds the provisioned engine), then
|
|
32
|
+
provisions + vendors + compiles; the `init` next-steps reflect this.
|
|
33
|
+
**The full onboarding now runs end-to-end** — validated in a clean `ruby:3.3`
|
|
34
|
+
container: `gem install bundler-spinel` → `init` → `./bin/build` → `./app`
|
|
35
|
+
serves a Spinel-compiled Tep app (needs `tep ≥ 0.11.1`, which builds its C
|
|
36
|
+
helpers on demand). spinelgems#10.
|
|
37
|
+
|
|
38
|
+
## [0.1.1] — 2026-06-01
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- **`init` scaffold produced an unlockable Gemfile.** It wrote the engine git SHA
|
|
42
|
+
into `engine_version:`, which bundler parses as a Gem version requirement —
|
|
43
|
+
`bundle lock` died with `Illformed requirement [...]`. The scaffold now writes a
|
|
44
|
+
version-literal `engine_version: "0.0.0"` (advisory) and pins the real revision
|
|
45
|
+
in a `SPINEL_PIN` file, which `install-engine` already reads. Caught by a
|
|
46
|
+
clean-room `ruby:3.3` Docker run of the full onboarding flow.
|
|
47
|
+
|
|
7
48
|
## [0.1.0] — 2026-06-01
|
|
8
49
|
|
|
9
50
|
First non-prerelease. Installable without `--pre`. Closes the onboarding gap
|
|
@@ -57,5 +98,7 @@ and the ledger format may all change before `0.0.1`. Install with `--pre`.
|
|
|
57
98
|
- Wholesale `survey` with a thread-safe ledger, a per-compile wall-clock
|
|
58
99
|
timeout (`analyze-timeout` reject reason), and a ledger-based report.
|
|
59
100
|
|
|
101
|
+
[0.2.0]: https://github.com/OriPekelman/spinelgems/releases/tag/v0.2.0
|
|
102
|
+
[0.1.1]: https://github.com/OriPekelman/spinelgems/releases/tag/v0.1.1
|
|
60
103
|
[0.1.0]: https://github.com/OriPekelman/spinelgems/releases/tag/v0.1.0
|
|
61
104
|
[0.0.1.pre]: https://github.com/OriPekelman/spinelgems/releases/tag/v0.0.1.pre
|
data/README.md
CHANGED
|
@@ -37,8 +37,8 @@ limited and still growing. So we surveyed the ecosystem and publish a
|
|
|
37
37
|
source "https://rubygems.org"
|
|
38
38
|
ruby "3.3.0", engine: "spinel", engine_version: "0.0.0"
|
|
39
39
|
|
|
40
|
-
gem "tep"
|
|
41
|
-
gem "
|
|
40
|
+
gem "tep" # published on RubyGems
|
|
41
|
+
gem "some_unreleased_lib", git: "…" # or a sibling via path:/git:
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`bundle lock` resolves normally (it ignores the engine marker); the marker guards
|
|
@@ -160,7 +160,12 @@ module Bundler
|
|
|
160
160
|
def init(dir, out: $stdout, rev: nil)
|
|
161
161
|
FileUtils.mkdir_p(dir)
|
|
162
162
|
engine_rev = rev || EngineInstaller::DEFAULT_REV
|
|
163
|
-
write(out, File.join(dir, "Gemfile"), gemfile
|
|
163
|
+
write(out, File.join(dir, "Gemfile"), gemfile)
|
|
164
|
+
# The engine revision is a git SHA — NOT a valid `engine_version:` (bundler
|
|
165
|
+
# parses that as a Gem version requirement and `bundle lock` errors on a
|
|
166
|
+
# SHA). So the rev lives in a SPINEL_PIN file, which `install-engine`
|
|
167
|
+
# reads; the Gemfile keeps a version-literal advisory marker.
|
|
168
|
+
write(out, File.join(dir, "SPINEL_PIN"), "#{engine_rev}\n")
|
|
164
169
|
write(out, File.join(dir, "app.rb"), app_rb)
|
|
165
170
|
bin = File.join(dir, "bin", "build")
|
|
166
171
|
FileUtils.mkdir_p(File.dirname(bin))
|
|
@@ -171,9 +176,11 @@ module Bundler
|
|
|
171
176
|
out.puts "Scaffolded a Spinel + Tep project in #{dir}/"
|
|
172
177
|
out.puts "Next:"
|
|
173
178
|
out.puts " cd #{dir}" unless File.expand_path(dir) == Dir.pwd
|
|
174
|
-
out.puts "
|
|
175
|
-
out.puts " ./bin/build # provisions Spinel, vendors deps, compiles app.rb"
|
|
179
|
+
out.puts " ./bin/build # ensures tep, resolves + vendors deps, provisions Spinel, compiles"
|
|
176
180
|
out.puts " ./app -p 4567 # run the native binary"
|
|
181
|
+
out.puts ""
|
|
182
|
+
out.puts "(The `engine: spinel` Gemfile marker makes `bundle install` refuse to run"
|
|
183
|
+
out.puts " under CRuby by design — bin/build uses `bundle lock` + `spinel-compat vendor`.)"
|
|
177
184
|
end
|
|
178
185
|
|
|
179
186
|
def write(out, path, body)
|
|
@@ -185,17 +192,20 @@ module Bundler
|
|
|
185
192
|
end
|
|
186
193
|
end
|
|
187
194
|
|
|
188
|
-
def gemfile
|
|
195
|
+
def gemfile
|
|
189
196
|
<<~RUBY
|
|
190
197
|
source "https://rubygems.org"
|
|
191
198
|
|
|
192
199
|
# Spinel is the engine: code here is compiled ahead-of-time to a native
|
|
193
|
-
# binary, not run on CRuby.
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
# binary, not run on CRuby. `engine_version` is advisory and must be a
|
|
201
|
+
# version literal (bundler parses it as a Gem requirement); the actual
|
|
202
|
+
# engine revision install-engine builds is pinned in ./SPINEL_PIN.
|
|
203
|
+
ruby "3.3.0", engine: "spinel", engine_version: "0.0.0"
|
|
204
|
+
|
|
205
|
+
# The web framework — Sinatra-style, compiles via Spinel. >= 0.11.1 builds
|
|
206
|
+
# its C helpers on demand (needed for `gem install tep` without `make`).
|
|
207
|
+
# (For an unreleased sibling instead: gem "tep", git: "https://github.com/OriPekelman/tep.git")
|
|
208
|
+
gem "tep", ">= 0.11.1"
|
|
199
209
|
RUBY
|
|
200
210
|
end
|
|
201
211
|
|
|
@@ -212,12 +222,16 @@ module Bundler
|
|
|
212
222
|
def build_sh
|
|
213
223
|
<<~SH
|
|
214
224
|
#!/usr/bin/env bash
|
|
215
|
-
#
|
|
216
|
-
#
|
|
225
|
+
# From a fresh checkout to a native binary. The Gemfile's `engine: spinel`
|
|
226
|
+
# marker makes `bundle install` refuse to run under CRuby, so we resolve
|
|
227
|
+
# with `bundle lock` and place deps with `spinel-compat vendor` instead.
|
|
217
228
|
set -e
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
command -v tep >/dev/null 2>&1 || gem install tep # the tep build CLI (a compile-time tool)
|
|
230
|
+
[ -f Gemfile.lock ] || bundle lock # resolve deps (NOT `bundle install`)
|
|
231
|
+
spinel-compat install-engine # fetch+build the pinned engine (cached)
|
|
232
|
+
export SPINEL="${SPINEL:-$HOME/.cache/spinel/current/spinel}" # tell tep where the engine is
|
|
233
|
+
spinel-compat vendor # place deps where Spinel follows them
|
|
234
|
+
tep build app.rb -o app # compile -> ./app
|
|
221
235
|
echo "built ./app — run it with: ./app -p 4567"
|
|
222
236
|
SH
|
|
223
237
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Bundler
|
|
5
|
+
module Spinel
|
|
6
|
+
# Upgrades a bare `miscompile` verdict (CRuby and Spinel agree to run but
|
|
7
|
+
# disagree on stdout) into a *located* one: the (file, line, variable) where
|
|
8
|
+
# a scalar local first diverges — via the value-bisection harness that lives
|
|
9
|
+
# in the spinel-dev repo (tools/value-bisect/bisect.sh).
|
|
10
|
+
#
|
|
11
|
+
# Why: a miscompile reason of `diff:L2 cruby="42" spinel="0"` says two outputs
|
|
12
|
+
# differ but not *why*. The bisector traces every scalar local under both
|
|
13
|
+
# CRuby and a Spinel --debug build and reports the first to part ways, turning
|
|
14
|
+
# that into `localized:foo.rb:12 total cruby=42 spinel=0` — a line to look at.
|
|
15
|
+
#
|
|
16
|
+
# Strictly best-effort and non-fatal. If the harness can't be found (it's a
|
|
17
|
+
# separate repo), can't run (the engine dir is a bare binary with no compiler
|
|
18
|
+
# sources / no lldb), or can't pin a scalar (the divergence is in output
|
|
19
|
+
# formatting, not a traced local), verify still returns the miscompile verdict
|
|
20
|
+
# unchanged — this only ever *adds* a `localized:` reason when it has one.
|
|
21
|
+
class Localizer
|
|
22
|
+
def initialize(engine)
|
|
23
|
+
@engine = engine
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Run the bisector on `harness` (a self-contained .rb; require_relative'd
|
|
27
|
+
# files are traced too) and return a short reason string, or nil when
|
|
28
|
+
# localization isn't possible or didn't pin a value.
|
|
29
|
+
def localize(harness)
|
|
30
|
+
script = bisect_script or return nil
|
|
31
|
+
# SPINEL_DIR points the bisector at the same compiler the engine uses;
|
|
32
|
+
# it shells out to that checkout's spinel_analyze.rb / spinel_codegen.rb.
|
|
33
|
+
# bisect.sh exits 1 *on divergence* (the case we want), so the exit code
|
|
34
|
+
# is not a success signal — parse stdout regardless. stdout carries only
|
|
35
|
+
# the single JSON object in --json mode; progress goes to stderr.
|
|
36
|
+
out, _err, _st = Open3.capture3(
|
|
37
|
+
{ "SPINEL_DIR" => @engine.dir }, "sh", script, "--json", harness
|
|
38
|
+
)
|
|
39
|
+
line = out.lines.map(&:strip).reject(&:empty?).last or return nil
|
|
40
|
+
verdict = begin
|
|
41
|
+
JSON.parse(line)
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
return nil
|
|
44
|
+
end
|
|
45
|
+
format_verdict(verdict)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def format_verdict(v)
|
|
53
|
+
case v["verdict"]
|
|
54
|
+
when "diverge"
|
|
55
|
+
"localized:#{loc(v)} #{v['variable']} cruby=#{v['cruby']} spinel=#{v['spinel']}"
|
|
56
|
+
when "crash"
|
|
57
|
+
"localized:crash@#{loc(v)} signal=#{v['signal']}"
|
|
58
|
+
end
|
|
59
|
+
# Any other verdict (ok / exit-differ / abort) means the bisector couldn't
|
|
60
|
+
# attribute the divergence to a traced scalar; leave the verdict un-enriched.
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def loc(v)
|
|
64
|
+
file = v["file"].to_s
|
|
65
|
+
file.empty? ? "line #{v['line']}" : "#{file}:#{v['line']}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Resolve bisect.sh. It lives in spinel-dev (separate from the compiler
|
|
69
|
+
# engine), so there's no fixed relation to the engine dir — probe a few
|
|
70
|
+
# conventional spots, newest-intent first: an explicit override, a sibling
|
|
71
|
+
# spinel-dev checkout next to the engine, the conventional ~/sites layout.
|
|
72
|
+
def bisect_script
|
|
73
|
+
rel = "tools/value-bisect/bisect.sh"
|
|
74
|
+
candidates = [
|
|
75
|
+
ENV["SPINEL_BISECT"],
|
|
76
|
+
File.expand_path(File.join(@engine.dir, "..", "spinel-dev", rel)),
|
|
77
|
+
File.expand_path("~/sites/spinel-dev/#{rel}")
|
|
78
|
+
]
|
|
79
|
+
candidates.find { |c| c && File.exist?(c) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "open3"
|
|
2
|
+
require_relative "localizer"
|
|
2
3
|
|
|
3
4
|
module Bundler
|
|
4
5
|
module Spinel
|
|
@@ -50,6 +51,15 @@ module Bundler
|
|
|
50
51
|
unless verdict == "verified" || verdict == "loaded" || verdict == "clean"
|
|
51
52
|
reasons = ["rubric:#{rubric(ruby_ok, ruby_err, spin_ok, spin_err, ruby_out, spin_out, gem_name)}"] + reasons
|
|
52
53
|
end
|
|
54
|
+
# Self-localize a miscompile: a `diff:L2 cruby=… spinel=…` reason says
|
|
55
|
+
# the outputs differ but not where the value went wrong. The bisector
|
|
56
|
+
# traces the still-on-disk harness (require_relative'd gem files included)
|
|
57
|
+
# and, when it pins a diverging scalar, appends `localized:<file>:<line>
|
|
58
|
+
# <var> cruby=… spinel=…`. Best-effort: nil when it can't localize.
|
|
59
|
+
if verdict == "rejected" && reasons.include?("miscompile")
|
|
60
|
+
loc = Localizer.new(@engine).localize(harness)
|
|
61
|
+
reasons += [loc] if loc
|
|
62
|
+
end
|
|
53
63
|
@ledger.record(@ledger.build(
|
|
54
64
|
gem: gem_name, version: version, rev: @engine.rev,
|
|
55
65
|
verdict: verdict, reasons: reasons, probe: full ? "verify-full" : "verify"
|
data/lib/bundler/spinel.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "spinel/ledger"
|
|
|
10
10
|
require_relative "spinel/gem_fetcher"
|
|
11
11
|
require_relative "spinel/probe"
|
|
12
12
|
require_relative "spinel/verifier"
|
|
13
|
+
require_relative "spinel/localizer"
|
|
13
14
|
require_relative "spinel/vendorer"
|
|
14
15
|
require_relative "spinel/survey"
|
|
15
16
|
require_relative "spinel/checker"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-spinel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ori Pekelman
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- lib/bundler/spinel/gem_fetcher.rb
|
|
38
38
|
- lib/bundler/spinel/history.rb
|
|
39
39
|
- lib/bundler/spinel/ledger.rb
|
|
40
|
+
- lib/bundler/spinel/localizer.rb
|
|
40
41
|
- lib/bundler/spinel/platform.rb
|
|
41
42
|
- lib/bundler/spinel/probe.rb
|
|
42
43
|
- lib/bundler/spinel/proxy.rb
|