Wire quire.ci via IntoLua and register_module
Replaces the ad-hoc install methods on Registration and Runtime with
the mlua primitives built for this exact pattern. Runtime keeps a
RuntimeHandle newtype because the orphan rule blocks impl IntoLua
for Rc<Runtime> directly.
Assisted-by: Claude Opus 4.7 via Claude Code
diff --git a/src/ci/lua.rs b/src/ci/lua.rs
index ba3d796..9cf4659 100644
--- a/src/ci/lua.rs
+++ b/src/ci/lua.rs
@@ -11,7 +11,7 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
-use mlua::{Lua, LuaSerdeExt};
+use mlua::{IntoLua, Lua, LuaSerdeExt};
use super::pipeline::{Job, ValidationError};
use crate::Result;
@@ -31,14 +31,13 @@ pub(super) fn parse(
let src = Rc::new(source.to_string());
fennel.eval_raw(source, name, |lua| {
- let module = Registration {
- jobs: jobs.clone(),
- source: src.clone(),
- }
- .install(lua)?;
- let loaded: mlua::Table = lua.globals().get::<mlua::Table>("package")?.get("loaded")?;
- loaded.set("quire.ci", module)?;
- Ok(())
+ lua.register_module(
+ "quire.ci",
+ Registration {
+ jobs: jobs.clone(),
+ source: src.clone(),
+ },
+ )
})?;
Ok(jobs.take())
@@ -47,10 +46,11 @@ pub(super) fn parse(
/// The registration-time module exposed to Fennel scripts via
/// `(require :quire.ci)`.
///
-/// `install` stows the registration sink on the Lua VM via
-/// `set_app_data` (so `register_job` can find it) and returns the
-/// `quire.ci` table — `{job}` for now. Runtime primitives (`sh`,
-/// `secret`) live on the per-execution [`Runtime`] handle, not here.
+/// Converted into a Lua table via [`IntoLua`]: stows itself on the
+/// VM as app data (so `register_job` can find the registration sink)
+/// and returns a table whose only entry is `job`. Runtime primitives
+/// (`sh`, `secret`) live on the per-execution [`Runtime`] handle, not
+/// here.
///
/// ```fennel
/// (local ci (require :quire.ci))
@@ -63,15 +63,12 @@ struct Registration {
source: Rc<String>,
}
-impl Registration {
- /// Install on `lua` as app data and return the `quire.ci` table.
- /// `register_job` errors at call time if the registration isn't
- /// installed first.
- fn install(self, lua: &Lua) -> mlua::Result<mlua::Table> {
+impl IntoLua for Registration {
+ fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
lua.set_app_data(self);
let table = lua.create_table()?;
table.set("job", lua.create_function(register_job)?)?;
- Ok(table)
+ table.into_lua(lua)
}
}
@@ -99,16 +96,15 @@ fn register_job(
/// Per-execution runtime: holds the secrets exposed to the job, the
/// current-job cursor, and the per-job captured `sh` outputs.
///
-/// `Run::execute` constructs an `Rc<Runtime>`, calls [`install`] to
-/// stow it on the Lua VM as app data and produce the handle table,
-/// and updates `current_job` around each `run_fn` call. `(sh …)` and
-/// `(secret …)` look the runtime back up via app data.
+/// Wrap an `Rc<Runtime>` in [`RuntimeHandle`] and convert it via
+/// [`IntoLua`] to install it on the Lua VM (sets app data, returns
+/// the handle table passed into each `run_fn`). The newtype is
+/// required because the orphan rule forbids `impl IntoLua` directly
+/// on `Rc<Runtime>`.
///
/// Outside a run, no runtime is installed; in that case `(sh …)`
/// runs the command but doesn't record (the cursor lookup misses).
/// `(secret …)` requires a runtime — without one, calls error.
-///
-/// [`install`]: Runtime::install
#[derive(Debug)]
pub(super) struct Runtime {
secrets: HashMap<String, SecretString>,
@@ -127,16 +123,6 @@ impl Runtime {
}
}
- /// Install on `lua` as app data and return the runtime handle —
- /// the table passed as the sole argument to each `run_fn`.
- pub(super) fn install(self: Rc<Self>, lua: &Lua) -> mlua::Result<mlua::Table> {
- lua.set_app_data(self);
- let table = lua.create_table()?;
- table.set("sh", lua.create_function(run_sh)?)?;
- table.set("secret", lua.create_function(lookup_secret)?)?;
- Ok(table)
- }
-
/// Mark `id` as the currently executing job. `(sh …)` invocations
/// from this job's `run_fn` will record output under `id`.
pub(super) fn enter_job(&self, id: &str) {
@@ -156,6 +142,20 @@ impl Runtime {
}
}
+/// `IntoLua` carrier for an `Rc<Runtime>`. Stows the Rc on the VM as
+/// app data and returns the handle table — `{sh, secret}` for now.
+pub(super) struct RuntimeHandle(pub Rc<Runtime>);
+
+impl IntoLua for RuntimeHandle {
+ fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
+ lua.set_app_data(self.0);
+ let table = lua.create_table()?;
+ table.set("sh", lua.create_function(run_sh)?)?;
+ table.set("secret", lua.create_function(lookup_secret)?)?;
+ table.into_lua(lua)
+ }
+}
+
/// Body of `(secret name)`. Errors as a Lua error if the runtime
/// isn't installed, the name is undeclared, or the file form fails to
/// read.
@@ -314,9 +314,9 @@ mod tests {
/// Install a runtime with the given secrets on `pipeline`'s VM and
/// return the runtime handle. Mirrors what `Run::execute` does so
/// tests can drive a `run_fn` directly.
- fn rt(pipeline: &Pipeline, secrets: HashMap<String, SecretString>) -> mlua::Table {
- Rc::new(Runtime::new(secrets))
- .install(pipeline.fennel().lua())
+ fn rt(pipeline: &Pipeline, secrets: HashMap<String, SecretString>) -> mlua::Value {
+ RuntimeHandle(Rc::new(Runtime::new(secrets)))
+ .into_lua(pipeline.fennel().lua())
.expect("install runtime")
}
diff --git a/src/ci/run.rs b/src/ci/run.rs
index 0cb4918..8c8d6d7 100644
--- a/src/ci/run.rs
+++ b/src/ci/run.rs
@@ -10,8 +10,9 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use jiff::Timestamp;
+use mlua::IntoLua;
-use super::lua::{Runtime, ShOutput};
+use super::lua::{Runtime, RuntimeHandle, ShOutput};
use super::pipeline::Pipeline;
use crate::secret::SecretString;
use crate::{Error, Result};
@@ -275,10 +276,8 @@ impl Run {
secrets: HashMap<String, SecretString>,
) -> Result<()> {
self.runtime = Rc::new(Runtime::new(secrets));
- let rt_table = self
- .runtime
- .clone()
- .install(pipeline.fennel().lua())
+ let rt_value = RuntimeHandle(self.runtime.clone())
+ .into_lua(pipeline.fennel().lua())
.expect("install runtime on Lua VM");
let order: Vec<String> = pipeline
@@ -295,7 +294,7 @@ impl Run {
.expect("topo_order returned a job id not in pipeline");
self.runtime.enter_job(&job.id);
- let result = job.run_fn.call::<mlua::Value>(rt_table.clone());
+ let result = job.run_fn.call::<mlua::Value>(rt_value.clone());
self.runtime.leave_job();
if let Err(e) = result {