beni 0.0.0 → 0.1.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 +54 -0
- data/README.md +141 -14
- data/lib/beni/build_config.rb +45 -0
- data/lib/beni/builder.rb +141 -0
- data/lib/beni/configuration.rb +12 -0
- data/lib/beni/dsl/context.rb +108 -0
- data/lib/beni/dsl/definition_context.rb +49 -0
- data/lib/beni/dsl/target_context.rb +35 -0
- data/lib/beni/dsl.rb +25 -0
- data/lib/beni/selected_toolchain.rb +11 -0
- data/lib/beni/target.rb +9 -0
- data/lib/beni/tasks.rb +159 -0
- data/lib/beni/toolchain_definition.rb +9 -0
- data/lib/beni/vendor/checksum.rb +68 -0
- data/lib/beni/vendor/downloader.rb +78 -0
- data/lib/beni/vendor/tarball.rb +72 -0
- data/lib/beni/vendor/toolchain.rb +107 -0
- data/lib/beni/vendor/toolchains/wasi.rake +30 -0
- data/lib/beni/vendor.rb +139 -0
- data/lib/beni/version.rb +1 -1
- data/lib/beni.rb +9 -1
- data/sig/beni/build_config.rbs +9 -0
- data/sig/beni/builder.rbs +39 -0
- data/sig/beni/configuration.rbs +10 -0
- data/sig/beni/dsl/context.rbs +39 -0
- data/sig/beni/dsl/definition_context.rbs +19 -0
- data/sig/beni/dsl/target_context.rbs +15 -0
- data/sig/beni/dsl.rbs +5 -0
- data/sig/beni/selected_toolchain.rbs +9 -0
- data/sig/beni/target.rbs +8 -0
- data/sig/beni/tasks.rbs +36 -0
- data/sig/beni/toolchain_definition.rbs +9 -0
- data/sig/beni/vendor/checksum.rbs +22 -0
- data/sig/beni/vendor/downloader.rbs +26 -0
- data/sig/beni/vendor/tarball.rbs +24 -0
- data/sig/beni/vendor/toolchain.rbs +33 -0
- data/sig/beni/vendor.rbs +20 -0
- data/sig/beni.rbs +3 -1
- data/sig/patches/open-uri.rbs +10 -0
- metadata +59 -9
- data/Rakefile +0 -12
data/lib/beni/target.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Beni
|
|
4
|
+
# A +target <name>+ declaration (SPEC.md Terminology: target
|
|
5
|
+
# declaration) — names one build target to verify and holds the
|
|
6
|
+
# toolchain references its block declared.
|
|
7
|
+
class Target < Data.define(:name, :references)
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/beni/tasks.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rake/tasklib"
|
|
5
|
+
|
|
6
|
+
require_relative "../beni"
|
|
7
|
+
|
|
8
|
+
module Beni
|
|
9
|
+
# Rake task library exposing the +beni:*+ namespace. Add to a Rakefile:
|
|
10
|
+
#
|
|
11
|
+
# require "beni/tasks"
|
|
12
|
+
#
|
|
13
|
+
# Beni::Tasks.new
|
|
14
|
+
#
|
|
15
|
+
# or with declarations (a custom config cross-building to wasm32-wasip1):
|
|
16
|
+
#
|
|
17
|
+
# Beni::Tasks.new do
|
|
18
|
+
# build_config "build_config/mruby.rb"
|
|
19
|
+
#
|
|
20
|
+
# target :host
|
|
21
|
+
# target :wasi do
|
|
22
|
+
# toolchain "wasi-sdk"
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# The block is the declarative DSL from SPEC.md, run on +DSL::Context+:
|
|
27
|
+
# scalar settings (+version+ / +build_config+ / +vendor_dir+), target
|
|
28
|
+
# declarations carrying toolchain references, and top-level toolchain
|
|
29
|
+
# definitions overriding a built-in pair. Every malformed declaration
|
|
30
|
+
# raises here — no task defined, nothing downloaded.
|
|
31
|
+
#
|
|
32
|
+
# Defined tasks:
|
|
33
|
+
#
|
|
34
|
+
# rake beni:build — fetch toolchains + build libmruby.a per target
|
|
35
|
+
# rake beni:clean — remove mruby build trees (keeps source)
|
|
36
|
+
# rake beni:config — generate the upstream default build config
|
|
37
|
+
# rake beni:vendor:setup — download & unpack the selected toolchains
|
|
38
|
+
# rake beni:vendor:clean — remove unpacked toolchains (keeps tarball cache)
|
|
39
|
+
# rake beni:vendor:clobber — remove the vendor tree entirely
|
|
40
|
+
class Tasks < Rake::TaskLib
|
|
41
|
+
# The resolved declarations — exposed so consumers and tests can
|
|
42
|
+
# inspect what the task definitions were wired from.
|
|
43
|
+
attr_reader :configuration
|
|
44
|
+
|
|
45
|
+
def initialize(&block)
|
|
46
|
+
super()
|
|
47
|
+
context = DSL::Context.new
|
|
48
|
+
context.instance_exec(&block) if block
|
|
49
|
+
@configuration = context.configuration
|
|
50
|
+
define
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def builder
|
|
56
|
+
@builder ||= Builder.new(
|
|
57
|
+
vendor_dir: configuration.vendor_dir,
|
|
58
|
+
build_config: configuration.build_config,
|
|
59
|
+
targets: configuration.targets
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The selected toolchains as Vendor pipeline values, each carrying
|
|
64
|
+
# its resolved version and checksum.
|
|
65
|
+
def vendor_toolchains
|
|
66
|
+
@vendor_toolchains ||= configuration.toolchains.map do |selected|
|
|
67
|
+
Vendor.public_send(
|
|
68
|
+
Vendor::TOOLCHAIN_FACTORIES.fetch(selected.name),
|
|
69
|
+
vendor_dir: configuration.vendor_dir, version: selected.version, sha256: selected.sha256
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def define
|
|
75
|
+
namespace :beni do
|
|
76
|
+
define_vendor_namespace
|
|
77
|
+
define_build_task
|
|
78
|
+
define_clean_task
|
|
79
|
+
define_config_task
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def define_vendor_namespace
|
|
84
|
+
namespace :vendor do
|
|
85
|
+
vendor_toolchains.each { |toolchain| define_toolchain_tasks(toolchain) }
|
|
86
|
+
define_vendor_setup_task
|
|
87
|
+
define_vendor_clean_tasks
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def define_vendor_setup_task
|
|
92
|
+
desc "Fetch and unpack the selected vendor toolchains (#{vendor_toolchains.map(&:name).join(" + ")})"
|
|
93
|
+
task setup: vendor_toolchains.map { |toolchain| "setup:#{toolchain.task_name}" } do
|
|
94
|
+
Vendor.stage_wasi_toolchain_file(vendor_dir: configuration.vendor_dir) if wasi_sdk_selected?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def wasi_sdk_selected?
|
|
99
|
+
configuration.toolchains.any? { |toolchain| toolchain.name == "wasi-sdk" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def define_vendor_clean_tasks
|
|
103
|
+
desc "Remove unpacked vendor toolchains (keeps cached tarballs)"
|
|
104
|
+
task :clean do
|
|
105
|
+
vendor_toolchains.each { |toolchain| FileUtils.rm_rf(toolchain.final_dir) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
desc "Remove #{configuration.vendor_dir} entirely (unpacked trees and cached tarballs)"
|
|
109
|
+
task :clobber do
|
|
110
|
+
FileUtils.rm_rf(configuration.vendor_dir)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def define_toolchain_tasks(toolchain)
|
|
115
|
+
file toolchain.tarball_path do
|
|
116
|
+
toolchain.fetch
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
namespace :setup do
|
|
120
|
+
desc "Download and unpack #{toolchain.name} #{toolchain.version_label} into #{toolchain.final_dir}"
|
|
121
|
+
task toolchain.task_name => toolchain.tarball_path do
|
|
122
|
+
toolchain.install
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def define_build_task
|
|
128
|
+
desc "Build vendored mruby for #{configuration.targets.join(" + ")} " \
|
|
129
|
+
"(produces #{builder.libmruby_paths.join(", ")})"
|
|
130
|
+
task build: "vendor:setup" do
|
|
131
|
+
builder.ensure_built
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def define_clean_task
|
|
136
|
+
desc "Remove mruby's build trees (keeps vendored mruby source)"
|
|
137
|
+
task :clean do
|
|
138
|
+
builder.clean
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def define_config_task
|
|
143
|
+
desc "Generate mruby's upstream default build config at the `build_config` declaration's path"
|
|
144
|
+
task :config do
|
|
145
|
+
dest = configuration.build_config
|
|
146
|
+
raise Error, "[beni] beni:config requires a `build_config` declaration naming the file to generate" unless dest
|
|
147
|
+
|
|
148
|
+
BuildConfig.generate(dest, mruby_dir: builder.mruby_dir, version: mruby_version)
|
|
149
|
+
puts "[beni] generated #{dest} — edit it to define further targets"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# mruby's selected version — always present, `mruby` is selected in
|
|
154
|
+
# every resolution.
|
|
155
|
+
def mruby_version
|
|
156
|
+
configuration.toolchains.to_h { |toolchain| [toolchain.name, toolchain.version] }.fetch("mruby")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Beni
|
|
4
|
+
# A top-level +toolchain <name>+ block (SPEC.md Terminology: toolchain
|
|
5
|
+
# definition) — carries the +version+ and +sha256+ pair that replaces
|
|
6
|
+
# the named toolchain's built-in pair.
|
|
7
|
+
class ToolchainDefinition < Data.define(:name, :version, :sha256)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Beni
|
|
6
|
+
module Vendor
|
|
7
|
+
# SHA256 verification for vendored tarballs. One instance per +(path,
|
|
8
|
+
# expected_sha)+ pair; reuse is not supported and not needed by
|
|
9
|
+
# +Beni::Tasks+. Operates in two modes:
|
|
10
|
+
#
|
|
11
|
+
# * Explicit expected hash (a built-in pair entry or a consumer
|
|
12
|
+
# override) — must match exactly; mismatch raises.
|
|
13
|
+
# * Trust-on-first-use (TOFU) — when +expected_sha+ is +nil+ or empty,
|
|
14
|
+
# the actual hash is pinned to a +.sha256+ sidecar next to the
|
|
15
|
+
# tarball. Subsequent runs compare against the pinned value and
|
|
16
|
+
# raise on drift.
|
|
17
|
+
#
|
|
18
|
+
# Public contract is the single +#verify_or_pin+ entry point; the two
|
|
19
|
+
# branches and the digest helper are internal.
|
|
20
|
+
class Checksum
|
|
21
|
+
def initialize(path, expected_sha)
|
|
22
|
+
@path = path
|
|
23
|
+
@expected_sha = expected_sha
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Verify the tarball against +expected_sha+ (if non-empty) or TOFU-pin
|
|
27
|
+
# against the +.sha256+ sidecar. Returns the computed SHA256 hex digest
|
|
28
|
+
# on success. Raises +Beni::Error+ on mismatch (explicit mode) or drift
|
|
29
|
+
# (TOFU mode); both error messages carry a +[beni]+ prefix for CI log
|
|
30
|
+
# grepping.
|
|
31
|
+
def verify_or_pin
|
|
32
|
+
actual = sha256
|
|
33
|
+
sidecar = "#{@path}.sha256"
|
|
34
|
+
expected? ? verify_against_expected(actual, sidecar) : verify_or_pin_sidecar(actual, sidecar)
|
|
35
|
+
actual
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def expected?
|
|
41
|
+
!@expected_sha.to_s.empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sha256
|
|
45
|
+
Digest::SHA256.file(@path).hexdigest
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_against_expected(actual, sidecar)
|
|
49
|
+
unless actual == @expected_sha
|
|
50
|
+
raise Error, "[beni] checksum mismatch for #{File.basename(@path)}: " \
|
|
51
|
+
"expected #{@expected_sha}, got #{actual}"
|
|
52
|
+
end
|
|
53
|
+
File.write(sidecar, "#{actual}\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def verify_or_pin_sidecar(actual, sidecar)
|
|
57
|
+
if File.exist?(sidecar)
|
|
58
|
+
pinned = File.read(sidecar).strip
|
|
59
|
+
return if actual == pinned
|
|
60
|
+
|
|
61
|
+
raise Error, "[beni] checksum drift for #{File.basename(@path)}: " \
|
|
62
|
+
"pinned #{pinned}, got #{actual}"
|
|
63
|
+
end
|
|
64
|
+
File.write(sidecar, "#{actual}\n")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
require "net/http" # eager — TRANSIENT_ERRORS names Net::* at class-eval
|
|
6
|
+
|
|
7
|
+
module Beni
|
|
8
|
+
module Vendor
|
|
9
|
+
# Tarball download with exponential-backoff retry on transient network
|
|
10
|
+
# failures. One instance per +(url, dest)+ pair; reuse is not supported
|
|
11
|
+
# and not needed by +Beni::Tasks+.
|
|
12
|
+
#
|
|
13
|
+
# Public contract is the single +#download+ entry point; +TRANSIENT_ERRORS+
|
|
14
|
+
# and +MAX_RETRIES+ are exposed as tunable knobs but the retry mechanics
|
|
15
|
+
# themselves are internal.
|
|
16
|
+
class Downloader
|
|
17
|
+
# Retry attempts wait +1 << attempt+ seconds (2 + 4 + 8 = 14s total)
|
|
18
|
+
# — enough to ride out a GitHub archive 502 / TCP read timeout.
|
|
19
|
+
MAX_RETRIES = 3
|
|
20
|
+
|
|
21
|
+
# Transient network errors retried by the internal +with_retry+ wrapper.
|
|
22
|
+
# +OpenURI::HTTPError+ is narrowed to 5xx; 4xx (URL typo, deleted repo)
|
|
23
|
+
# bypasses the retry path.
|
|
24
|
+
TRANSIENT_ERRORS = [
|
|
25
|
+
OpenURI::HTTPError, Net::ReadTimeout, Net::OpenTimeout,
|
|
26
|
+
Errno::ECONNRESET, SocketError
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
def initialize(url, dest)
|
|
30
|
+
@url = url
|
|
31
|
+
@dest = dest
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch +url+ into +dest+ atomically via a +.part+ sidecar, retrying
|
|
35
|
+
# transient failures with exponential backoff. Permanent failures
|
|
36
|
+
# (4xx, DNS resolution failure on non-network condition) surface
|
|
37
|
+
# immediately. Raises whatever the underlying +URI#open+ raises after
|
|
38
|
+
# the retry budget is exhausted.
|
|
39
|
+
def download
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(@dest))
|
|
41
|
+
tmp = "#{@dest}.part"
|
|
42
|
+
with_retry { fetch(tmp) }
|
|
43
|
+
File.rename(tmp, @dest)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Stream the URL body into +tmp+ — the seam between the retry
|
|
49
|
+
# policy and the network; tests override this to script outcomes
|
|
50
|
+
# per attempt.
|
|
51
|
+
def fetch(tmp)
|
|
52
|
+
URI.parse(@url).open("rb") { |io| File.open(tmp, "wb") { |f| IO.copy_stream(io, f) } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_retry
|
|
56
|
+
attempts = 0
|
|
57
|
+
begin
|
|
58
|
+
yield
|
|
59
|
+
rescue *TRANSIENT_ERRORS => e
|
|
60
|
+
raise if permanent?(e) || (attempts += 1) > MAX_RETRIES
|
|
61
|
+
|
|
62
|
+
warn_and_sleep(e, attempts)
|
|
63
|
+
retry
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def permanent?(error)
|
|
68
|
+
error.is_a?(OpenURI::HTTPError) && !error.message.match?(/\A5\d\d\b/)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def warn_and_sleep(error, attempt)
|
|
72
|
+
warn "[beni] retry #{attempt}/#{MAX_RETRIES} after #{error.class}: " \
|
|
73
|
+
"#{error.message.lines.first&.strip}"
|
|
74
|
+
sleep(1 << attempt)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Beni
|
|
6
|
+
module Vendor
|
|
7
|
+
# Unpacks a vendored tarball into +final_dir+, idempotent on a
|
|
8
|
+
# +.beni-version+ marker stamped inside the tree. One instance per
|
|
9
|
+
# +(tarball, top_level_dir, final_dir, version)+ configuration; reuse is
|
|
10
|
+
# not supported and not needed by +Beni::Tasks+.
|
|
11
|
+
#
|
|
12
|
+
# A version mismatch (toolchain bump) forces a clean re-extract, so the
|
|
13
|
+
# unpacked tree never lags the pinned version. Public contract is the
|
|
14
|
+
# single +#prepare+ entry point; the staging-directory step is internal.
|
|
15
|
+
class Tarball
|
|
16
|
+
# Marker stamped inside +final_dir+ after a successful unpack; a matching
|
|
17
|
+
# value short-circuits +#prepare+, a mismatch forces re-extract.
|
|
18
|
+
VERSION_MARKER = ".beni-version"
|
|
19
|
+
|
|
20
|
+
def initialize(tarball:, top_level_dir:, final_dir:, version:)
|
|
21
|
+
@tarball = tarball
|
|
22
|
+
@top_level_dir = top_level_dir
|
|
23
|
+
@final_dir = final_dir
|
|
24
|
+
@version = version
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Extract the tarball into a staging sibling of +final_dir+, then
|
|
28
|
+
# atomically move the +top_level_dir+ subtree into place and stamp the
|
|
29
|
+
# version marker. A no-op when the stamped version already matches.
|
|
30
|
+
# Raises if the tarball does not contain the expected +top_level_dir+
|
|
31
|
+
# root. The staging tree is removed whether or not extraction succeeds.
|
|
32
|
+
def prepare
|
|
33
|
+
return if installed_version == @version
|
|
34
|
+
|
|
35
|
+
staging = "#{@final_dir}.staging"
|
|
36
|
+
begin
|
|
37
|
+
extract_to_staging(staging)
|
|
38
|
+
promote(staging)
|
|
39
|
+
ensure
|
|
40
|
+
FileUtils.rm_rf(staging)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Version recorded by the last successful unpack, or +nil+ when the tree
|
|
47
|
+
# is absent or predates version stamping (forcing a re-extract).
|
|
48
|
+
def installed_version
|
|
49
|
+
marker = File.join(@final_dir, VERSION_MARKER)
|
|
50
|
+
File.read(marker).strip if File.exist?(marker)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_to_staging(staging)
|
|
54
|
+
FileUtils.rm_rf(staging)
|
|
55
|
+
FileUtils.mkdir_p(staging)
|
|
56
|
+
system("tar", "-xzf", @tarball, "-C", staging, exception: true)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Move the expected +top_level_dir+ subtree out of +staging+ into
|
|
60
|
+
# +final_dir+ and stamp the version marker.
|
|
61
|
+
def promote(staging)
|
|
62
|
+
src = File.join(staging, @top_level_dir)
|
|
63
|
+
raise Error, "[beni] expected #{src} after extracting #{@tarball}, missing" unless File.directory?(src)
|
|
64
|
+
|
|
65
|
+
FileUtils.rm_rf(@final_dir)
|
|
66
|
+
FileUtils.mkdir_p(File.dirname(@final_dir))
|
|
67
|
+
FileUtils.mv(src, @final_dir)
|
|
68
|
+
File.write(File.join(@final_dir, VERSION_MARKER), "#{@version}\n")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Beni
|
|
4
|
+
module Vendor
|
|
5
|
+
# Declarative value object describing a tarball-style vendored
|
|
6
|
+
# toolchain. Captures the +(remote, cache, unpacked)+ triple anchored
|
|
7
|
+
# on +vendor_dir+, and exposes the three pipeline stages (+#fetch+,
|
|
8
|
+
# +#verify+, +#install+) that +Beni::Tasks+ wires into +file+ /
|
|
9
|
+
# +task+ declarations.
|
|
10
|
+
#
|
|
11
|
+
# Adding a new tarball-based vendor artifact is a single factory
|
|
12
|
+
# method in +Beni::Vendor+; the rake DSL loop in +Beni::Tasks+
|
|
13
|
+
# picks it up automatically.
|
|
14
|
+
#
|
|
15
|
+
# Fields:
|
|
16
|
+
#
|
|
17
|
+
# * +name+ — display name; also the basename of the unpacked
|
|
18
|
+
# tree under +vendor_dir+ and the base for the
|
|
19
|
+
# +setup:<name>+ task identifier.
|
|
20
|
+
# * +version_label+ — version string; printed in the download log and
|
|
21
|
+
# stamped into +final_dir+ as the idempotency key
|
|
22
|
+
# that detects a bump and forces a re-extract.
|
|
23
|
+
# * +base_url+ — remote URL prefix; resolved through
|
|
24
|
+
# +Beni::Vendor.base_url_for+ so test fixtures
|
|
25
|
+
# can override via +BENI_VENDOR_BASE_URL+.
|
|
26
|
+
# * +tarball_name+ — filename joined to both +base_url+ (download)
|
|
27
|
+
# and the +.cache+ directory (cache location).
|
|
28
|
+
# * +top_level_dir+ — the single top-level directory produced when
|
|
29
|
+
# the tarball is extracted; passed through to
|
|
30
|
+
# +Tarball#prepare+ under the same name.
|
|
31
|
+
# * +vendor_dir+ — root of the vendor tree; anchors +final_dir+
|
|
32
|
+
# and +tarball_path+.
|
|
33
|
+
# * +expected_sha256+ — the toolchain's selected checksum (resolved
|
|
34
|
+
# by +Beni::Vendor+ from the built-in pair or a
|
|
35
|
+
# consumer override); +nil+ falls to TOFU
|
|
36
|
+
# sidecar pinning in +Checksum#verify_or_pin+.
|
|
37
|
+
class Toolchain <
|
|
38
|
+
Data.define(:name, :version_label, :base_url, :tarball_name, :top_level_dir, :vendor_dir, :expected_sha256)
|
|
39
|
+
# Symbol used to identify the +setup:<task_name>+ rake task. Dashes
|
|
40
|
+
# in +name+ are not valid in rake task identifiers, so we map them
|
|
41
|
+
# to underscores at this single seam.
|
|
42
|
+
def task_name
|
|
43
|
+
name.tr("-", "_").to_sym
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolved download URL. Honours the +BENI_VENDOR_BASE_URL+ test
|
|
47
|
+
# fixture override at call time (not at construction time), so a
|
|
48
|
+
# test can flip the env var after the Toolchain is built.
|
|
49
|
+
def url
|
|
50
|
+
"#{Vendor.base_url_for(base_url)}/#{tarball_name}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Destination under +vendor_dir+ where the unpacked tree is moved.
|
|
54
|
+
def final_dir
|
|
55
|
+
File.join(vendor_dir, name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Local cache path for the downloaded tarball. Lives under
|
|
59
|
+
# +vendor_dir/.cache+ (the cache moves with the vendor tree).
|
|
60
|
+
def tarball_path
|
|
61
|
+
File.join(vendor_dir, ".cache", tarball_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Download the tarball into +tarball_path+ and verify its SHA256.
|
|
65
|
+
# Intended as the body of the +file tarball_path+ rake task; the
|
|
66
|
+
# task's mtime-based caching avoids re-downloading on a cache hit.
|
|
67
|
+
# A tarball failing verification deliberately stays cached: a
|
|
68
|
+
# checksum mismatch is an abort condition, never a re-download
|
|
69
|
+
# trigger, and the cache-hit path re-fails it the same way.
|
|
70
|
+
def fetch
|
|
71
|
+
puts "[beni] downloading #{name} #{version_label} from #{url}"
|
|
72
|
+
downloader.download
|
|
73
|
+
verify
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Recompute the cached tarball's SHA256 and check it against
|
|
77
|
+
# +expected_sha256+ (or pin via TOFU sidecar). Idempotent — safe
|
|
78
|
+
# to call from both +file+ and +setup+ task bodies when the latter
|
|
79
|
+
# depends on the former.
|
|
80
|
+
def verify
|
|
81
|
+
Checksum.new(tarball_path, expected_sha256).verify_or_pin
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Verify the cached tarball, then unpack it into +final_dir+ via
|
|
85
|
+
# +Tarball#prepare+. A no-op when the version stamped under +final_dir+
|
|
86
|
+
# already matches +version_label+.
|
|
87
|
+
def install
|
|
88
|
+
verify
|
|
89
|
+
Tarball.new(
|
|
90
|
+
tarball: tarball_path,
|
|
91
|
+
top_level_dir: top_level_dir,
|
|
92
|
+
final_dir: final_dir,
|
|
93
|
+
version: version_label
|
|
94
|
+
).prepare
|
|
95
|
+
puts "[beni] #{name} ready at #{final_dir}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# The network boundary — tests override this to script the
|
|
101
|
+
# download without opening a connection.
|
|
102
|
+
def downloader
|
|
103
|
+
Downloader.new(url, tarball_path)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# beni's wasm32-wasip1 cross-compile toolchain — the wasi toolchain
|
|
4
|
+
# file, staged into the mruby tree's +tasks/toolchains/+ by
|
|
5
|
+
# +beni:vendor:setup+ whenever wasi-sdk is selected. A build config
|
|
6
|
+
# activates it with +conf.toolchain :wasi+.
|
|
7
|
+
MRuby::Toolchain.new(:wasi) do |conf, _params|
|
|
8
|
+
# WASI_SDK_PATH overrides the wasi-sdk root; the default is the
|
|
9
|
+
# vendor tree this mruby source is staged in (MRUBY_ROOT's parent).
|
|
10
|
+
wasi_sdk = ENV["WASI_SDK_PATH"] || File.join(File.expand_path("..", MRUBY_ROOT), "wasi-sdk")
|
|
11
|
+
bin = File.join(wasi_sdk, "bin")
|
|
12
|
+
target_flags = ["--target=wasm32-wasi", "--sysroot=#{File.join(wasi_sdk, "share", "wasi-sysroot")}"]
|
|
13
|
+
# setjmp/longjmp via the wasm exception-handling mechanism: all
|
|
14
|
+
# three flags must be present at both compile and link stages.
|
|
15
|
+
sjlj_flags = ["-mllvm", "-wasm-enable-sjlj", "-mllvm", "-wasm-use-legacy-eh=false"]
|
|
16
|
+
|
|
17
|
+
conf.toolchain :clang
|
|
18
|
+
conf.cc.command = File.join(bin, "clang")
|
|
19
|
+
conf.cxx.command = File.join(bin, "clang++")
|
|
20
|
+
conf.linker.command = File.join(bin, "clang")
|
|
21
|
+
conf.archiver.command = File.join(bin, "llvm-ar")
|
|
22
|
+
# GNU archive format: llvm-ar defaults to the Darwin format on
|
|
23
|
+
# macOS hosts, which can overflow on many long wasm member paths.
|
|
24
|
+
conf.archiver.archive_options = "--format=gnu rs %<outfile>s %<objs>s"
|
|
25
|
+
|
|
26
|
+
[conf.cc, conf.cxx, conf.linker].each do |tool|
|
|
27
|
+
tool.flags << target_flags << sjlj_flags
|
|
28
|
+
end
|
|
29
|
+
conf.linker.libraries << "setjmp"
|
|
30
|
+
end
|
data/lib/beni/vendor.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "vendor/downloader"
|
|
4
|
+
require_relative "vendor/checksum"
|
|
5
|
+
require_relative "vendor/tarball"
|
|
6
|
+
require_relative "vendor/toolchain"
|
|
7
|
+
|
|
8
|
+
module Beni
|
|
9
|
+
# Vendor toolchain façade. Owns the release-vendored toolchain pins and
|
|
10
|
+
# the factory methods that build declarative +Toolchain+ values anchored
|
|
11
|
+
# on a caller-supplied +vendor_dir+; +Beni::Tasks+ calls the factories
|
|
12
|
+
# with each selected toolchain's resolved +(version, sha256)+ to wire
|
|
13
|
+
# +file+ / +task+ declarations. Network download lives in
|
|
14
|
+
# +Vendor::Downloader+, SHA256 verification in +Vendor::Checksum+,
|
|
15
|
+
# tarball extraction in +Vendor::Tarball+, and the per-toolchain
|
|
16
|
+
# pipeline composition in +Vendor::Toolchain+.
|
|
17
|
+
#
|
|
18
|
+
# Honors +BENI_VENDOR_BASE_URL+ to point downloads at a local fixture
|
|
19
|
+
# during tests.
|
|
20
|
+
module Vendor
|
|
21
|
+
# ---- Built-in pairs ----------------------------------------------------
|
|
22
|
+
# The version and checksum pair this release vendors per toolchain.
|
|
23
|
+
# wasi-sdk ships one tarball per build platform, so its checksum is
|
|
24
|
+
# keyed by +WASI_SDK_PLATFORM+ (values from the GitHub release asset
|
|
25
|
+
# digests); mruby's source archive is host-agnostic with a single
|
|
26
|
+
# checksum.
|
|
27
|
+
#
|
|
28
|
+
# wasi-sdk: must be >= 26 for native wasm32-wasip1 setjmp/longjmp
|
|
29
|
+
# support. 33's +libc.a+ supplies +__wasi_init_tp+, which Rust's
|
|
30
|
+
# wasm32-wasip1 +crt1-command.o+ references from 1.96 onward. Keep in
|
|
31
|
+
# lockstep with the channel in +rust-toolchain.toml+ — in both this
|
|
32
|
+
# repo and kobako.
|
|
33
|
+
BUILT_IN_PAIRS = {
|
|
34
|
+
"mruby" => {
|
|
35
|
+
version: "4.0.0",
|
|
36
|
+
sha256: "e2ea271dbed14e9f2b33df773ae447b747dbc242ce2675022c0a57efea85a7b4"
|
|
37
|
+
},
|
|
38
|
+
"wasi-sdk" => {
|
|
39
|
+
version: "33.0",
|
|
40
|
+
sha256: {
|
|
41
|
+
"arm64-linux" => "4f98ee738c7abb45c81a94d1461fc53cc569d1cd01498951c8184d841a027844",
|
|
42
|
+
"arm64-macos" => "85c997a2665ead91673b5bb88b7d0df3fc8900df3bfa244f720d478187bbdc78",
|
|
43
|
+
"x86_64-linux" => "0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce",
|
|
44
|
+
"x86_64-macos" => "18f3f201ba9734e6a4455b0b6410690395a55e9ffa9f6f5066f66083a94b93b3"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Transitive toolchain dependencies, folded into the selected set at
|
|
50
|
+
# task-definition time: referencing wasi-sdk implies mruby.
|
|
51
|
+
DEPENDENCIES = { "wasi-sdk" => %w[mruby] }.freeze
|
|
52
|
+
|
|
53
|
+
# ---- Platform detection (wasi-sdk only; mruby tarball is host-agnostic).
|
|
54
|
+
# +x86_64-linux+ is both the most common host triple and the safest
|
|
55
|
+
# fallback for unrecognised ones, so we collapse both cases into the
|
|
56
|
+
# +else+ branch rather than carrying an explicit +when+ that would
|
|
57
|
+
# duplicate the default.
|
|
58
|
+
WASI_SDK_PLATFORM =
|
|
59
|
+
case RUBY_PLATFORM
|
|
60
|
+
when /arm64-darwin|aarch64-darwin/ then "arm64-macos"
|
|
61
|
+
when /x86_64-darwin/ then "x86_64-macos"
|
|
62
|
+
when /aarch64-linux|arm64-linux/ then "arm64-linux"
|
|
63
|
+
else "x86_64-linux"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Known toolchain names mapped to their factory methods — the name
|
|
67
|
+
# domain the DSL validates against and +Beni::Tasks+ dispatches on.
|
|
68
|
+
TOOLCHAIN_FACTORIES = {
|
|
69
|
+
"mruby" => :mruby,
|
|
70
|
+
"wasi-sdk" => :wasi_sdk
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
module_function
|
|
74
|
+
|
|
75
|
+
def wasi_sdk(vendor_dir:, version: nil, sha256: nil)
|
|
76
|
+
version ||= BUILT_IN_PAIRS.fetch("wasi-sdk").fetch(:version)
|
|
77
|
+
# The release tag carries the major version only; tarballs carry the
|
|
78
|
+
# full version plus the platform.
|
|
79
|
+
Toolchain.new(
|
|
80
|
+
name: "wasi-sdk",
|
|
81
|
+
version_label: "#{version} (#{WASI_SDK_PLATFORM})",
|
|
82
|
+
base_url: "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-#{version.split(".").first}",
|
|
83
|
+
tarball_name: "wasi-sdk-#{version}-#{WASI_SDK_PLATFORM}.tar.gz",
|
|
84
|
+
top_level_dir: "wasi-sdk-#{version}-#{WASI_SDK_PLATFORM}",
|
|
85
|
+
vendor_dir: vendor_dir,
|
|
86
|
+
expected_sha256: sha256 || built_in_sha256("wasi-sdk", version)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def mruby(vendor_dir:, version: nil, sha256: nil)
|
|
91
|
+
version ||= BUILT_IN_PAIRS.fetch("mruby").fetch(:version)
|
|
92
|
+
Toolchain.new(
|
|
93
|
+
name: "mruby",
|
|
94
|
+
version_label: version,
|
|
95
|
+
base_url: "https://github.com/mruby/mruby/archive/refs/tags",
|
|
96
|
+
tarball_name: "#{version}.tar.gz",
|
|
97
|
+
top_level_dir: "mruby-#{version}",
|
|
98
|
+
vendor_dir: vendor_dir,
|
|
99
|
+
expected_sha256: sha256 || built_in_sha256("mruby", version)
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# When BENI_VENDOR_BASE_URL is set, all tarballs are fetched from that
|
|
104
|
+
# base URL (test fixture). The base URL must serve files named exactly
|
|
105
|
+
# +tarball_name+ for each toolchain.
|
|
106
|
+
def base_url_for(default)
|
|
107
|
+
override = ENV.fetch("BENI_VENDOR_BASE_URL", nil)
|
|
108
|
+
return default if override.nil? || override.empty?
|
|
109
|
+
|
|
110
|
+
override.chomp("/")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The built-in checksum for +name+ at +version+: the build platform's
|
|
114
|
+
# entry when the toolchain's checksums are platform-keyed, +nil+ for
|
|
115
|
+
# any version other than the vendored one — mruby's TOFU pinning path.
|
|
116
|
+
def built_in_sha256(name, version)
|
|
117
|
+
pair = BUILT_IN_PAIRS.fetch(name)
|
|
118
|
+
return nil unless version == pair.fetch(:version)
|
|
119
|
+
|
|
120
|
+
checksum = pair.fetch(:sha256)
|
|
121
|
+
checksum.is_a?(Hash) ? checksum.fetch(WASI_SDK_PLATFORM) : checksum
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Write the gem-shipped wasi toolchain file into the staged mruby
|
|
125
|
+
# source, where mruby's +conf.toolchain :wasi+ resolves it from.
|
|
126
|
+
# Idempotent and always overwriting, so a re-extracted tree or an
|
|
127
|
+
# older beni's copy converges on this release's definition. Returns
|
|
128
|
+
# the staged path.
|
|
129
|
+
def stage_wasi_toolchain_file(vendor_dir:)
|
|
130
|
+
# `__dir__ || "."`: __dir__ is only nil under eval, which never
|
|
131
|
+
# loads this file; the fallback satisfies steep's String? typing.
|
|
132
|
+
source = File.expand_path("vendor/toolchains/wasi.rake", __dir__ || ".")
|
|
133
|
+
target = File.join(vendor_dir, "mruby", "tasks", "toolchains", "wasi.rake")
|
|
134
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
135
|
+
FileUtils.cp(source, target)
|
|
136
|
+
target
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/beni/version.rb
CHANGED