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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3b827b40a0385e917ba034d28ced80adccc7b0370d4779d1ed3fb2826307846
4
- data.tar.gz: 398ad4b15f5158b20fef7bf228ec2868a486e141bcce14c201a403b65baa42f9
3
+ metadata.gz: c331de89e1d8d6996c41ba290800042b985d932ffc9c0ef0d957753144c195b0
4
+ data.tar.gz: 3d4e9478ee1b7d4e5fcd91f0f47b49d750e491b062580ad8be902ca68548b35b
5
5
  SHA512:
6
- metadata.gz: ca47ed907af19136e59beabc0277cdc55850d57f6593b41b5d7086ebd7bb89d9506b07eacee92b9c617e9f99089dad1237b686affc70fd3a65e1e81d1c4e3e64
7
- data.tar.gz: fca25b38eadd6f2c5087a736387ba85b232ac3332c1900ab3d313b24ba9e7f96a9f0a952c0d0a9ae494bb4e0da70e9d8bc98a47465b6a28ee368f1ff0fa56d79
6
+ metadata.gz: '093f8eeb0ab8d91e94c01a74808716d875c112d9d0351b31cb98d4db27891147f6b11cca04fc655e4f50f0bca90dbca3be1c81b907b4bacbdd4419ee58050f56'
7
+ data.tar.gz: 384eeb4227d265d66c4eaa4a35aab0a1988ad99af1fce770e73a0faf9c32d2a3e15444d4cea9f1b579064a430e57f2a9f26015ef1cf1583a0bd96ece6fdede41
data/CHANGELOG.md ADDED
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-06-06)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * require Ruby >= 3.3
9
+ * **beni-sys:** align bindgen with the archive via libmruby.flags.mak
10
+ * **beni:** default the build to mruby's upstream config, not the repo's
11
+
12
+ ### Features
13
+
14
+ * add Beni::Builder driving mruby's own rake ([2be29d3](https://github.com/elct9620/beni/commit/2be29d3d1fa353804413b92203758232796fad3b))
15
+ * add Beni::Tasks exposing the beni:* rake namespace ([2833c23](https://github.com/elct9620/beni/commit/2833c23a7a01efea185cfcd165f4499e5ffc0cf7))
16
+ * add RBS type checking and Claude Code quality hooks ([707e692](https://github.com/elct9620/beni/commit/707e6920f26b21ff4a67f8a31691fc21e1d5dd81))
17
+ * **beni-sys:** align bindgen with the archive via libmruby.flags.mak ([8d019dc](https://github.com/elct9620/beni/commit/8d019dc40fb0c6f7cf5b4139ac27b6a3b7d1459a))
18
+ * **beni:** compile the full wrapper API surface in placeholder mode ([00a429d](https://github.com/elct9620/beni/commit/00a429dfe524b7daaeb72765841136e2e92ed83a))
19
+ * **beni:** generate self-contained build configs via rake beni:config ([51c04c2](https://github.com/elct9620/beni/commit/51c04c214a1cc5b2766b68a1e54937cf00a5a69d))
20
+ * **beni:** register typed Rust functions with method! and seal the panic boundary ([b1706a2](https://github.com/elct9620/beni/commit/b1706a2aa2aa9c8550846ea2e4d139720ec1aa34))
21
+ * **beni:** ship Ruby surfaces as Gem implementations ([37b6260](https://github.com/elct9620/beni/commit/37b6260b235b4510d145ce8dd933e2f62bf265d8))
22
+ * **beni:** surface mruby rejections as Err through magnus-shaped handles ([ccd2809](https://github.com/elct9620/beni/commit/ccd280974700e3c3943e541c4bf72260eaad74be))
23
+ * **config:** generate the build config from the staged upstream default ([5429c54](https://github.com/elct9620/beni/commit/5429c542ceb58456be8b529c1422d4efe18ded07))
24
+ * **dsl:** add the declarative configuration vocabulary and resolution ([a8cbe48](https://github.com/elct9620/beni/commit/a8cbe483e7ea877ae59277056ffae6f282c5fe54))
25
+ * port kobako's rake chain as the compile-verification harness ([a0e69f2](https://github.com/elct9620/beni/commit/a0e69f20609fb293b317f4ee206169dd3f543f37))
26
+ * port the vendor pipeline into Beni::Vendor ([4387e6b](https://github.com/elct9620/beni/commit/4387e6be8432975ebf69cdcd2c35bd02d38060d4))
27
+ * ship the mruby build configs with the gem ([88d79d4](https://github.com/elct9620/beni/commit/88d79d4bbc4f28c12a767ae078d8ea6775e0fdc3))
28
+ * **sys:** make archive discovery env-driven with no vendor fallback ([389d608](https://github.com/elct9620/beni/commit/389d6088644559ac3f53505b4fe9bfcc5f25b298))
29
+ * **tasks:** switch Beni::Tasks to the declarative DSL ([7eaf07f](https://github.com/elct9620/beni/commit/7eaf07f8f2b6b57f6cb93aa37edebe680ee0b7b9))
30
+ * **test:** add default_host consumer-scenario harness ([9d7d6d3](https://github.com/elct9620/beni/commit/9d7d6d3b339bcda266643ce3535efeb9b720cf0d))
31
+ * **test:** exercise the generate-config chain as a consumer scenario ([2df1490](https://github.com/elct9620/beni/commit/2df1490ee19b161d9d8fb171d69669c7016c8fdd))
32
+ * **vendor:** stage the wasi toolchain file into the mruby tree ([e081b69](https://github.com/elct9620/beni/commit/e081b694dfbe45b35ac58e45eb22fd5fe2997447))
33
+ * **vendor:** vendor built-in version-checksum pairs and inject selection into toolchains ([b3edf10](https://github.com/elct9620/beni/commit/b3edf103351814f8e67d7d2a3f4fbf5fff6ec223))
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * **beni:** gate Mrb's repr(transparent) on mruby_linked, not wasm32 ([7d8c949](https://github.com/elct9620/beni/commit/7d8c9499e1365e6bb2c6551d1e918738163db1cb))
39
+ * **beni:** prefix the remaining runtime errors for CI log grepping ([ffd799a](https://github.com/elct9620/beni/commit/ffd799a67bcdc1ca9dbb92d106c14f333afe14b8))
40
+ * **beni:** treat init-failed mruby states and missing configs as errors ([e4c0bf5](https://github.com/elct9620/beni/commit/e4c0bf5c406717c7129901a11d5fb2791b33bf3e))
41
+ * **beni:** type mrb_get_args count out-params as mrb_int, not c_int ([b21ee22](https://github.com/elct9620/beni/commit/b21ee225577d6b2bba8176836e426e7e25611b93))
42
+ * **gemspec:** give source_code_uri its own URL ([3d3da60](https://github.com/elct9620/beni/commit/3d3da6092d0c7f222558a822f3b7c5c1660d0ff0))
43
+ * **gemspec:** ship only the consumer-facing files and link the changelog ([176f18a](https://github.com/elct9620/beni/commit/176f18a77fb01f18502a0e0b6c1b237470e0d0fc))
44
+ * **release:** pin the first release to 0.1.0 and drop the dead lock updaters ([5bd6106](https://github.com/elct9620/beni/commit/5bd61065d61f3e00cfbd6918d7dd803f2e9e11b4))
45
+
46
+
47
+ ### Miscellaneous Chores
48
+
49
+ * require Ruby >= 3.3 ([de636e8](https://github.com/elct9620/beni/commit/de636e87163474f84f19e7163f22c51a1ec9dc22))
50
+
51
+
52
+ ### Code Refactoring
53
+
54
+ * **beni:** default the build to mruby's upstream config, not the repo's ([0cafae1](https://github.com/elct9620/beni/commit/0cafae18e17350c0cbbd5d0048c1131e450cd573))
data/README.md CHANGED
@@ -1,20 +1,131 @@
1
1
  # Beni
2
2
 
3
- mruby toolchain monorepo a Ruby gem that manages the mruby build chain and
4
- Rust crates that bind the mruby C API, extracted from the
5
- [kobako](https://github.com/elct9620/kobako) project.
6
-
7
- > **Status**: under active development. The published `0.0.0` versions reserve
8
- > the package names while the build chain and binding crates are extracted
9
- > from kobako; no usable API ships yet.
3
+ beni gives Rust developers a magnus-like experience for mruby: a Ruby gem
4
+ manages the mruby build chain, and Rust crates expose a safe, typed API over
5
+ the resulting `libmruby.a`. Extracted from the
6
+ [kobako](https://github.com/elct9620/kobako) project; APIs follow 0.x semver
7
+ semantics and may still evolve between minor versions.
10
8
 
11
9
  ## Packages
12
10
 
11
+ All three packages release in lockstep under a single version.
12
+
13
13
  | Package | Registry | Role |
14
14
  |---|---|---|
15
- | `beni` gem | rubygems.org | mruby dependency manager vendors mruby source, builds `libmruby.a`, future mrbgem management |
15
+ | `beni` gem | rubygems.org | Rake tasks + DSL config that download mruby and build `libmruby.a` |
16
16
  | `beni-sys` crate | crates.io | bindgen FFI surface over the mruby C API |
17
- | `beni` crate | crates.io | typed Rust wrapper over `beni-sys` (magnus analog) |
17
+ | `beni` crate | crates.io | safe typed wrapper over `beni-sys`, aligned with magnus idioms |
18
+
19
+ ## Getting started
20
+
21
+ ### Build `libmruby.a` with the gem
22
+
23
+ Add `beni` to your Gemfile and install the task library in your Rakefile:
24
+
25
+ ```ruby
26
+ require "beni/tasks"
27
+
28
+ Beni::Tasks.new
29
+ ```
30
+
31
+ ```bash
32
+ rake beni:build
33
+ ```
34
+
35
+ This downloads the pinned mruby release, builds it with mruby's untouched
36
+ upstream default config, and stages `vendor/mruby/build/host/lib/` with
37
+ `libmruby.a` and its `libmruby.flags.mak` compile-flags sidecar — everything
38
+ the crates need.
39
+
40
+ To tune the build, declare a config path and generate the seed:
41
+
42
+ ```ruby
43
+ Beni::Tasks.new do
44
+ build_config "build_config/mruby.rb"
45
+ end
46
+ ```
47
+
48
+ `rake beni:config` writes a self-contained copy of the upstream default
49
+ config to that path. The file is yours to edit — add targets, gems, or
50
+ defines; beni never rewrites it.
51
+
52
+ ### Embed mruby from Rust
53
+
54
+ Add the `beni` crate to your Cargo.toml, then point archive discovery at
55
+ the vendor tree the gem staged:
56
+
57
+ ```bash
58
+ BENI_VENDOR_DIR=$PWD/vendor cargo build
59
+ ```
60
+
61
+ A crate ships its Ruby surface as a `Gem` and installs it during
62
+ interpreter setup:
63
+
64
+ ```rust
65
+ use beni::{method, Error, Gem, Module, Mrb, Value};
66
+
67
+ fn answer(_mrb: &Mrb, _self: Value) -> i32 {
68
+ 42
69
+ }
70
+
71
+ struct WidgetGem;
72
+
73
+ impl Gem for WidgetGem {
74
+ fn init(mrb: &Mrb) -> Result<(), Error> {
75
+ let widget = mrb.define_class(c"Widget", mrb.object_class())?;
76
+ widget.define_method(mrb, c"answer", method!(answer, 0))?;
77
+ Ok(())
78
+ }
79
+ }
80
+
81
+ fn main() {
82
+ let mrb = Mrb::open().expect("mruby interpreter");
83
+ mrb.init_gem::<WidgetGem>().expect("Widget surface");
84
+ // Widget#answer now returns 42 to any Ruby code the interpreter runs.
85
+ }
86
+ ```
87
+
88
+ With no archive discovery variable set, a host build compiles in
89
+ placeholder mode: `cargo check` passes, no FFI surface is exported, and
90
+ `Mrb::open` returns an error — so `beni` is safe to take as a transitive
91
+ dependency. Any C API the typed wrapper does not cover stays reachable
92
+ through the unsafe `beni::sys` escape hatch.
93
+
94
+ ### Cross-compile for wasm32-wasip1
95
+
96
+ Declare a `wasi` target referencing the `wasi-sdk` toolchain:
97
+
98
+ ```ruby
99
+ Beni::Tasks.new do
100
+ build_config "build_config/mruby.rb"
101
+
102
+ target :host
103
+ target :wasi do
104
+ toolchain "wasi-sdk"
105
+ end
106
+ end
107
+ ```
108
+
109
+ and append the cross build to the generated config:
110
+
111
+ ```ruby
112
+ MRuby::CrossBuild.new("wasi") do |conf|
113
+ conf.toolchain :wasi
114
+ end
115
+ ```
116
+
117
+ `conf.toolchain :wasi` resolves to the wasi toolchain file
118
+ `beni:vendor:setup` stages into the mruby tree whenever `wasi-sdk` is
119
+ selected — the cross-compile settings ship with beni and update with it.
120
+ After `rake beni:build`, name the staged archive and the wasi-sdk root
121
+ explicitly for the cargo side (a cross-compiled cargo target never reads
122
+ the vendor tree on its own):
123
+
124
+ ```bash
125
+ MRUBY_LIB_DIR=$PWD/vendor/mruby/build/wasi/lib \
126
+ WASI_SDK_PATH=$PWD/vendor/wasi-sdk \
127
+ cargo build --target wasm32-wasip1
128
+ ```
18
129
 
19
130
  ## Toolchain
20
131
 
@@ -28,12 +139,28 @@ are unaffected by the pairing.
28
139
 
29
140
  ## Development
30
141
 
31
- After checking out the repo, run `bin/setup` to install dependencies. Then,
32
- run `rake test` to run the tests. You can also run `bin/console` for an
33
- interactive prompt that will allow you to experiment.
142
+ After checking out the repo, run `bin/setup` to install dependencies, then
143
+ `bundle exec rake` for the default gate (tests + RuboCop + Steep). The repo
144
+ dogfoods its own gem: the Rakefile wires `Beni::Tasks` with the validation
145
+ config `build_config/mruby.rb` (host + wasi targets), and a repo-local rake
146
+ chain verifies the crates compile against a real `libmruby.a` on both the
147
+ host target and wasm32-wasip1:
148
+
149
+ ```bash
150
+ bundle exec rake rust:verify # beni:build + check/test (host) + check (wasm32)
151
+ ```
152
+
153
+ Behavior contracts live in `SPEC.md` — the source of truth the
154
+ implementation follows.
155
+
156
+ ## Releasing
34
157
 
35
- The Rust crates live under `crates/` in a Cargo workspace at the repo root;
36
- `cargo check --workspace` covers both.
158
+ Releases are cut by release-please: merging the release PR tags the
159
+ version and publishes the gem and both crates in lockstep through OIDC
160
+ trusted publishing. One-time cleanup: after 0.1.0 ships, remove the
161
+ `release-as` line (and the then-stale `last-release-sha`) from
162
+ `release-please-config.json` — left in place it pins every subsequent
163
+ release to 0.1.0.
37
164
 
38
165
  ## Contributing
39
166
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Beni
6
+ # Generates the consumer's editable mruby build config: a verbatim
7
+ # copy of the staged mruby source's own +build_config/default.rb+ —
8
+ # the configured version's upstream default. Exposed as
9
+ # +rake beni:config+ by +Beni::Tasks+; the generated file belongs to
10
+ # the consuming project and seeds further customization.
11
+ module BuildConfig
12
+ module_function
13
+
14
+ # Copy the staged source's default config to +dest+, creating
15
+ # missing parent directories. Requires +version+'s mruby source
16
+ # staged under +mruby_dir+ and refuses to clobber an existing
17
+ # (likely hand-tuned) config. Returns +dest+.
18
+ def generate(dest, mruby_dir:, version:)
19
+ raise Error, "[beni] #{dest} already exists — delete it first to regenerate" if File.exist?(dest)
20
+
21
+ source = staged_default_config(mruby_dir, version)
22
+ FileUtils.mkdir_p(File.dirname(dest))
23
+ FileUtils.cp(source, dest)
24
+ dest
25
+ end
26
+
27
+ # The staged source's upstream default config path, verified to be
28
+ # +version+'s: the +.beni-version+ marker +Vendor::Tarball#prepare+
29
+ # stamps must match, so a stale tree never seeds a config for the
30
+ # wrong release.
31
+ def staged_default_config(mruby_dir, version)
32
+ source = File.join(mruby_dir, "build_config", "default.rb")
33
+ return source if staged_version(mruby_dir) == version && File.exist?(source)
34
+
35
+ raise Error,
36
+ "[beni] mruby #{version}'s source is not staged at #{mruby_dir} — " \
37
+ "run `rake beni:vendor:setup` first"
38
+ end
39
+
40
+ def staged_version(mruby_dir)
41
+ marker = File.join(mruby_dir, Vendor::Tarball::VERSION_MARKER)
42
+ File.read(marker).strip if File.exist?(marker)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "rbconfig"
5
+
6
+ module Beni
7
+ # Drives the vendored mruby tree's build to produce the per-target
8
+ # +libmruby.a+ archives, by running mruby's own +Rakefile+ — the
9
+ # documented build entry point (doc/guides/compile.md). With no
10
+ # +build_config+, mruby falls back to its own
11
+ # +build_config/default.rb+; an explicit config is wired in via the
12
+ # +MRUBY_CONFIG+ env var, which mruby resolves as an absolute path.
13
+ class Builder
14
+ # mruby's anonymous +MRuby::Build.new+ names its target "host"
15
+ # (lib/mruby/build.rb), so the upstream default config produces
16
+ # +build/host/lib/libmruby.a+. A custom build config with
17
+ # different target names supplies its own list via +Beni::Tasks+.
18
+ DEFAULT_TARGETS = %w[host].freeze
19
+
20
+ attr_reader :vendor_dir, :build_config, :targets
21
+
22
+ def initialize(vendor_dir:, build_config: nil, targets: DEFAULT_TARGETS)
23
+ @vendor_dir = vendor_dir
24
+ @build_config = build_config
25
+ @targets = targets
26
+ end
27
+
28
+ def mruby_dir
29
+ File.join(vendor_dir, "mruby")
30
+ end
31
+
32
+ def libmruby_path(target)
33
+ File.join(mruby_dir, "build", target, "lib", "libmruby.a")
34
+ end
35
+
36
+ def libmruby_paths
37
+ targets.map { |target| libmruby_path(target) }
38
+ end
39
+
40
+ # True when every target's artifacts — +libmruby.a+ plus the
41
+ # +libmruby.flags.mak+ sidecar +beni-sys+ parses for ABI alignment
42
+ # — are already present, letting callers skip the build without
43
+ # spawning a subprocess. An archive without its sidecar (e.g. a
44
+ # tree built before flags.mak joined the contract) triggers a
45
+ # rebuild, which is incremental and only emits the missing file.
46
+ def built?
47
+ artifact_paths.all? { |path| File.exist?(path) }
48
+ end
49
+
50
+ # Idempotent build entry point for +rake beni:build+: skip with a
51
+ # note when every artifact is already present, otherwise build and
52
+ # report readiness. A declared config that does not exist aborts
53
+ # before the skip check — stale artifacts must not mask it.
54
+ def ensure_built
55
+ check_build_config!
56
+ if built?
57
+ puts "[beni] libmruby.a already present for #{targets.join(" + ")} — skipping"
58
+ return
59
+ end
60
+
61
+ build
62
+ puts "[beni] libmruby.a ready for #{targets.join(" + ")}"
63
+ end
64
+
65
+ # Run mruby's rake and raise unless every target's +libmruby.a+
66
+ # exists afterwards. Alongside the default task, each target's
67
+ # +libmruby.flags.mak+ file task is requested explicitly — mruby's
68
+ # embedder interface recording the exact compile flags, which
69
+ # +beni-sys+'s build script parses to keep bindgen's view of the
70
+ # ABI aligned with the archive. (The file task is defined per
71
+ # target but not part of mruby's default products.) The underlying
72
+ # build is make-style incremental, so re-running on a partially
73
+ # built tree only compiles what is missing.
74
+ def build
75
+ check_build_config!
76
+ cmd = [RbConfig.ruby, "-S", "rake", "default", *flags_mak_paths]
77
+ puts "[beni] cd #{mruby_dir} && #{env.map { |k, v| "#{k}=#{v}" }.join(" ")} #{cmd.join(" ")}"
78
+ run_mruby_rake(env, cmd)
79
+ verify_artifacts!
80
+ end
81
+
82
+ # Remove each target's build tree (keeps the vendored mruby source).
83
+ def clean
84
+ targets.each do |target|
85
+ dir = File.join(mruby_dir, "build", target)
86
+ FileUtils.rm_rf(dir)
87
+ puts "[beni] removed #{dir}"
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # Every artifact the build must leave behind: the per-target
94
+ # archive and its flags.mak sidecar.
95
+ def artifact_paths
96
+ libmruby_paths + flags_mak_paths
97
+ end
98
+
99
+ # Spawn mruby's rake with the parent environment plus the +env+
100
+ # overlay. Extracted as a seam so tests can fake the subprocess
101
+ # while observing the full env + cmd contract.
102
+ def run_mruby_rake(env, cmd)
103
+ system(env, *cmd, chdir: mruby_dir, exception: true)
104
+ end
105
+
106
+ # Environment for the mruby build subprocess. +BENI_VENDOR_DIR+
107
+ # lets build configs resolve the vendor tree without knowing where
108
+ # the consuming project lives on disk. +MRUBY_CONFIG+ is only set
109
+ # for an explicit config — absent, mruby falls back to its own
110
+ # +build_config/default.rb+ (lib/mruby/build.rb#mruby_config_path).
111
+ def env
112
+ env = { "BENI_VENDOR_DIR" => vendor_dir }
113
+ env["MRUBY_CONFIG"] = build_config if build_config
114
+ env
115
+ end
116
+
117
+ # Per-target +libmruby.flags.mak+ file-task paths, matching the
118
+ # task names mruby defines in tasks/libmruby.rake (absolute,
119
+ # anchored on the default +build/<target>+ layout).
120
+ def flags_mak_paths
121
+ targets.map { |target| File.join(mruby_dir, "build", target, "lib", "libmruby.flags.mak") }
122
+ end
123
+
124
+ # The declared config belongs to the consumer; a path that does
125
+ # not exist is a configuration error named before anything spawns.
126
+ def check_build_config!
127
+ return if build_config.nil? || File.exist?(build_config)
128
+
129
+ raise Error, "[beni] build config #{build_config} does not exist"
130
+ end
131
+
132
+ # Report every missing artifact at once, so a multi-target build
133
+ # failure shows the whole gap instead of one path per run.
134
+ def verify_artifacts!
135
+ missing = artifact_paths.reject { |path| File.exist?(path) }
136
+ return if missing.empty?
137
+
138
+ raise Error, "[beni] build completed but artifacts are missing:\n #{missing.join("\n ")}"
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ # Resolution output of the Tasks DSL — immutable, defaults applied.
5
+ # The only input the task-definition phase reads: +vendor_dir+ fully
6
+ # resolved (declaration > +BENI_VENDOR_DIR+ > +vendor/+), +build_config+
7
+ # as an absolute path or +nil+ for mruby's untouched upstream default,
8
+ # +targets+ as the declared set (or +["host"]+), and +toolchains+ as
9
+ # the reference-driven selection.
10
+ class Configuration < Data.define(:vendor_dir, :build_config, :targets, :toolchains)
11
+ end
12
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ module DSL
5
+ # Top-level vocabulary of the +Beni::Tasks.new+ block. Collects the
6
+ # scalar settings, target declarations, and toolchain definitions —
7
+ # validating each declaration eagerly — and resolves them into the
8
+ # +Configuration+ the task-definition phase consumes.
9
+ class Context
10
+ def initialize
11
+ @settings = {}
12
+ @targets = {}
13
+ @definitions = {}
14
+ end
15
+
16
+ def version(value)
17
+ declare_scalar(:version, value)
18
+ end
19
+
20
+ def build_config(path)
21
+ declare_scalar(:build_config, path)
22
+ end
23
+
24
+ def vendor_dir(path)
25
+ declare_scalar(:vendor_dir, path)
26
+ end
27
+
28
+ # A +target <name>+ declaration; the optional block holds the
29
+ # target's toolchain references.
30
+ def target(name, &block)
31
+ key = name.to_s
32
+ raise Error, "duplicate `target` declaration #{key.inspect}" if @targets.key?(key)
33
+
34
+ # @type var references: Array[String]
35
+ references = block ? TargetContext.collect(&block) : []
36
+ @targets[key] = Target.new(name: key, references: references)
37
+ end
38
+
39
+ # A top-level +toolchain <name>+ — always a definition, so the
40
+ # block is part of the grammar and +mruby+ is never definable.
41
+ def toolchain(name, &block)
42
+ raise Error, "top-level `toolchain #{name.inspect}` must carry a definition block" unless block
43
+ if name == "mruby"
44
+ raise Error, "a toolchain definition never names \"mruby\" — select it with the `version` setting"
45
+ end
46
+
47
+ DSL.assert_known_toolchain!(name)
48
+ raise Error, "duplicate toolchain definition #{name.inspect}" if @definitions.key?(name)
49
+
50
+ @definitions[name] = DefinitionContext.collect(name, &block)
51
+ end
52
+
53
+ # Resolve the collected declarations (SPEC.md Behaviors: selection
54
+ # is reference-driven; defaults fall to the built-in pairs).
55
+ def configuration
56
+ Configuration.new(
57
+ vendor_dir: resolved_vendor_dir,
58
+ build_config: resolved_build_config,
59
+ targets: resolved_targets,
60
+ toolchains: selected_names.map { |name| selected_toolchain(name) }
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def declare_scalar(key, value)
67
+ raise Error, "duplicate `#{key}` declaration" if @settings.key?(key)
68
+
69
+ @settings[key] = value
70
+ end
71
+
72
+ def resolved_vendor_dir
73
+ File.expand_path(@settings[:vendor_dir] || ENV.fetch("BENI_VENDOR_DIR", nil) || "vendor")
74
+ end
75
+
76
+ def resolved_build_config
77
+ path = @settings[:build_config]
78
+ path && File.expand_path(path)
79
+ end
80
+
81
+ def resolved_targets
82
+ return Builder::DEFAULT_TARGETS.dup if @targets.empty?
83
+
84
+ @targets.keys
85
+ end
86
+
87
+ # References plus their transitive dependencies; +mruby+ is always
88
+ # selected and leads the set so it stages first.
89
+ def selected_names
90
+ references = @targets.values.flat_map(&:references)
91
+ dependencies = references.flat_map { |name| Vendor::DEPENDENCIES.fetch(name, []) }
92
+ (%w[mruby] + references + dependencies).uniq
93
+ end
94
+
95
+ def selected_toolchain(name)
96
+ definition = @definitions[name]
97
+ return SelectedToolchain.new(name: name, version: definition.version, sha256: definition.sha256) if definition
98
+
99
+ version = name == "mruby" ? mruby_version : Vendor::BUILT_IN_PAIRS.fetch(name).fetch(:version)
100
+ SelectedToolchain.new(name: name, version: version, sha256: Vendor.built_in_sha256(name, version))
101
+ end
102
+
103
+ def mruby_version
104
+ @settings[:version] || Vendor::BUILT_IN_PAIRS.fetch("mruby").fetch(:version)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ module DSL
5
+ # Vocabulary inside a top-level +toolchain <name> do … end+ block —
6
+ # the +(version, sha256)+ pair and nothing else. Both fields are
7
+ # required exactly once; the check runs when the block returns.
8
+ class DefinitionContext
9
+ # Run +block+ on a fresh context and return the collected
10
+ # ToolchainDefinition.
11
+ def self.collect(name, &)
12
+ context = new(name)
13
+ context.instance_exec(&)
14
+ context.to_definition
15
+ end
16
+
17
+ def initialize(name)
18
+ @name = name
19
+ @version = nil
20
+ @sha256 = nil
21
+ end
22
+
23
+ def version(value)
24
+ raise Error, "duplicate `version` in toolchain #{@name.inspect} definition" if @version
25
+
26
+ @version = value
27
+ end
28
+
29
+ def sha256(value)
30
+ raise Error, "duplicate `sha256` in toolchain #{@name.inspect} definition" if @sha256
31
+
32
+ @sha256 = value
33
+ end
34
+
35
+ # The collected definition; raises when the block left +version+
36
+ # or +sha256+ undeclared.
37
+ def to_definition
38
+ version = @version
39
+ sha256 = @sha256
40
+ if version.nil? || sha256.nil?
41
+ missing = [("version" if version.nil?), ("sha256" if sha256.nil?)].compact
42
+ raise Error, "toolchain #{@name.inspect} definition missing #{missing.join(" and ")}"
43
+ end
44
+
45
+ ToolchainDefinition.new(name: @name, version: version, sha256: sha256)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ module DSL
5
+ # Vocabulary inside a +target <name> do … end+ block — block-less
6
+ # toolchain references only. References are set-semantic: repeats
7
+ # collapse, and referencing +mruby+ is legal redundancy.
8
+ class TargetContext
9
+ # Run +block+ on a fresh context and return the collected
10
+ # reference names.
11
+ def self.collect(&)
12
+ context = new
13
+ context.instance_exec(&)
14
+ context.references
15
+ end
16
+
17
+ attr_reader :references
18
+
19
+ def initialize
20
+ @references = []
21
+ end
22
+
23
+ def toolchain(name, &block)
24
+ if block
25
+ raise Error,
26
+ "`toolchain #{name.inspect}` inside a target block must not carry a block — " \
27
+ "definitions live at the top level"
28
+ end
29
+ DSL.assert_known_toolchain!(name)
30
+
31
+ @references << name unless @references.include?(name)
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/beni/dsl.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ # Declarative configuration vocabulary for +Beni::Tasks+. Three
5
+ # contexts each expose exactly the declarations legal at their
6
+ # position — top level (+Context+), inside a target block
7
+ # (+TargetContext+), and inside a toolchain definition block
8
+ # (+DefinitionContext+) — so a malformed declaration fails in the
9
+ # method itself, at definition time.
10
+ module DSL
11
+ module_function
12
+
13
+ # A toolchain name outside the Vendor registry fails at the
14
+ # declaration that names it, never mid-build.
15
+ def assert_known_toolchain!(name)
16
+ return if Vendor::TOOLCHAIN_FACTORIES.key?(name)
17
+
18
+ raise Error, "unknown toolchain #{name.inspect} (known: #{Vendor::TOOLCHAIN_FACTORIES.keys.join(", ")})"
19
+ end
20
+ end
21
+ end
22
+
23
+ require_relative "dsl/context"
24
+ require_relative "dsl/target_context"
25
+ require_relative "dsl/definition_context"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Beni
4
+ # One selected toolchain with its resolved +(version, sha256)+ pair.
5
+ # +sha256+ is concrete at resolution time — a definition's declared
6
+ # value or the built-in checksum +Beni::Vendor+ resolves. +nil+ has
7
+ # exactly one reachable meaning: mruby at a non-default version, where
8
+ # verification falls to the TOFU sidecar path.
9
+ class SelectedToolchain < Data.define(:name, :version, :sha256)
10
+ end
11
+ end