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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +141 -14
  4. data/lib/beni/build_config.rb +45 -0
  5. data/lib/beni/builder.rb +141 -0
  6. data/lib/beni/configuration.rb +12 -0
  7. data/lib/beni/dsl/context.rb +108 -0
  8. data/lib/beni/dsl/definition_context.rb +49 -0
  9. data/lib/beni/dsl/target_context.rb +35 -0
  10. data/lib/beni/dsl.rb +25 -0
  11. data/lib/beni/selected_toolchain.rb +11 -0
  12. data/lib/beni/target.rb +9 -0
  13. data/lib/beni/tasks.rb +159 -0
  14. data/lib/beni/toolchain_definition.rb +9 -0
  15. data/lib/beni/vendor/checksum.rb +68 -0
  16. data/lib/beni/vendor/downloader.rb +78 -0
  17. data/lib/beni/vendor/tarball.rb +72 -0
  18. data/lib/beni/vendor/toolchain.rb +107 -0
  19. data/lib/beni/vendor/toolchains/wasi.rake +30 -0
  20. data/lib/beni/vendor.rb +139 -0
  21. data/lib/beni/version.rb +1 -1
  22. data/lib/beni.rb +9 -1
  23. data/sig/beni/build_config.rbs +9 -0
  24. data/sig/beni/builder.rbs +39 -0
  25. data/sig/beni/configuration.rbs +10 -0
  26. data/sig/beni/dsl/context.rbs +39 -0
  27. data/sig/beni/dsl/definition_context.rbs +19 -0
  28. data/sig/beni/dsl/target_context.rbs +15 -0
  29. data/sig/beni/dsl.rbs +5 -0
  30. data/sig/beni/selected_toolchain.rbs +9 -0
  31. data/sig/beni/target.rbs +8 -0
  32. data/sig/beni/tasks.rbs +36 -0
  33. data/sig/beni/toolchain_definition.rbs +9 -0
  34. data/sig/beni/vendor/checksum.rbs +22 -0
  35. data/sig/beni/vendor/downloader.rbs +26 -0
  36. data/sig/beni/vendor/tarball.rbs +24 -0
  37. data/sig/beni/vendor/toolchain.rbs +33 -0
  38. data/sig/beni/vendor.rbs +20 -0
  39. data/sig/beni.rbs +3 -1
  40. data/sig/patches/open-uri.rbs +10 -0
  41. metadata +59 -9
  42. data/Rakefile +0 -12
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Beni
4
- VERSION = "0.0.0"
4
+ VERSION = "0.1.0"
5
5
  end