-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
204 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
## Rust + Nix FFI | ||
|
||
https://github.com/srid/devour-flake introduced the idea of defining "functions" in Nix flake, that can be called from any external process. The flakes `inputs` acts as "arguments" taken by this function, with the flake's package output acting as its `output`. | ||
|
||
This package, `nix_rs::flake::functions`, provides the Rust FFI adapter to work with such Nix functions in Rust, using simpler API. | ||
|
||
In effect, this generalizes devour-flake to be able to define such functions. See [`devour_flake.rs`] for an example. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
//! Calling Nix functions (defined in a flake) from Rust, as if to provide FFI. | ||
// | ||
// This model provides a simpler alternative to Flake Schemas, but it can also do more than Flake Schemas can (such as building derivations). | ||
|
||
use super::url::FlakeUrl; | ||
use crate::command::NixCmd; | ||
use serde::{Deserialize, Serialize}; | ||
use std::{ffi::OsString, os::unix::ffi::OsStringExt, path::PathBuf, process::Stdio}; | ||
use tokio::io::{AsyncBufReadExt, BufReader}; | ||
|
||
/// Trait for flake functions | ||
pub trait FlakeFn { | ||
/// Input type, corresponding to flake inputs | ||
/// | ||
/// A field named `flake` will be treated special (extra args' --override-inputs operates on this flake) | ||
type Input; | ||
/// Output generated by building the flake fn | ||
type Output; | ||
|
||
/// Get the flake URL referencing this function | ||
fn flake() -> &'static FlakeUrl; | ||
|
||
/// Initialize the type after reading from Nix build | ||
fn init(out: &mut Self::Output); | ||
|
||
/// Call the flake function, taking `Self::Input`, returning `Self::Output` | ||
fn call( | ||
nixcmd: &NixCmd, | ||
// Whther to avoid the --override-input noise suppression. | ||
verbose: bool, | ||
// Extra arguments to pass to `nix build` | ||
// | ||
// --override-input is treated specially, to account for the flake input named `flake` (as defined in `Self::Input`) | ||
extra_args: Vec<String>, | ||
// The input arguments to the flake function. | ||
input: Self::Input, | ||
) -> impl std::future::Future<Output = Result<Self::Output, Error>> + Send | ||
where | ||
Self::Input: Serialize + Send + Sync, | ||
Self::Output: Sync + for<'de> Deserialize<'de>, | ||
{ | ||
async move { | ||
let mut cmd = nixcmd.command(); | ||
cmd.args([ | ||
"build", | ||
Self::flake(), | ||
"-L", | ||
"--no-link", | ||
"--print-out-paths", | ||
]); | ||
|
||
let input_vec = to_vec(&input); | ||
for (k, v) in input_vec { | ||
cmd.arg("--override-input"); | ||
cmd.arg(k); | ||
cmd.arg(v); | ||
} | ||
|
||
cmd.args(transform_override_inputs(&extra_args)); | ||
|
||
crate::command::trace_cmd(&cmd); | ||
|
||
let mut output_fut = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; | ||
let stderr_handle = output_fut.stderr.take().unwrap(); | ||
tokio::spawn(async move { | ||
// Suppress --override-input noise, since we expect these to be present. | ||
let mut reader = BufReader::new(stderr_handle).lines(); | ||
while let Some(line) = reader.next_line().await.expect("read stderr") { | ||
if !verbose { | ||
if line.starts_with("• Added input") { | ||
// Consume the input logging itself | ||
reader.next_line().await.expect("read stderr"); | ||
continue; | ||
} else if line | ||
.starts_with("warning: not writing modified lock file of flake") | ||
{ | ||
continue; | ||
} | ||
} | ||
eprintln!("{}", line); | ||
} | ||
}); | ||
let output = output_fut.wait_with_output().await?; | ||
if output.status.success() { | ||
let drv_out = | ||
PathBuf::from(OsString::from_vec(output.stdout.trim_ascii_end().into())); | ||
let mut v: Self::Output = serde_json::from_reader(std::fs::File::open(drv_out)?)?; | ||
Self::init(&mut v); | ||
Ok(v) | ||
} else { | ||
Err(Error::NixBuildFailed(output.status.code())) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Transform `--override-input` arguments to use `flake/` prefix, which | ||
/// devour_flake expects. | ||
/// | ||
/// NOTE: This assumes that Input struct contains a field named exactly "flake" referring to the flake. We should probably be smart about this. | ||
fn transform_override_inputs(args: &[String]) -> Vec<String> { | ||
let mut new_args = Vec::with_capacity(args.len()); | ||
let mut iter = args.iter().peekable(); | ||
|
||
while let Some(arg) = iter.next() { | ||
new_args.push(arg.clone()); | ||
if arg == "--override-input" { | ||
if let Some(next_arg) = iter.next() { | ||
new_args.push(format!("flake/{}", next_arg)); | ||
} | ||
} | ||
} | ||
|
||
new_args | ||
} | ||
|
||
/// Convert a struct of uniform value types (Option allowed, however) into a vector of fields. The value should be of String kind. | ||
fn to_vec<T>(value: &T) -> Vec<(String, String)> | ||
where | ||
T: Serialize, | ||
{ | ||
let map = serde_json::to_value(value) | ||
.unwrap() | ||
.as_object() | ||
.unwrap_or_else(|| panic!("Bad struct for FlakeFn")) | ||
.clone(); | ||
|
||
map.into_iter() | ||
.filter_map(|(k, v)| v.as_str().map(|v| (k, v.to_string()))) | ||
.collect() | ||
} | ||
|
||
/// Errors associated with `FlakeFn::call` | ||
#[derive(thiserror::Error, Debug)] | ||
pub enum Error { | ||
/// IO error | ||
#[error("IO error: {0}")] | ||
IOError(#[from] std::io::Error), | ||
|
||
/// Non-zero exit code | ||
#[error("`nix build` failed; exit code: {0:?}")] | ||
NixBuildFailed(Option<i32>), | ||
|
||
/// JSON error | ||
#[error("JSON error: {0}")] | ||
JSONError(#[from] serde_json::Error), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
pub mod command; | ||
pub mod eval; | ||
pub mod functions; | ||
pub mod metadata; | ||
pub mod outputs; | ||
pub mod schema; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.