flowrs-engine/crates/flowrs-appshell/build.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

170 lines
6.8 KiB
Rust

//! What the runtime bundle (`bundle`, which `export` turns on) is made from: the flowRS commit
//! it names (`runtime::REV`), and the source of the crates it carries.
//!
//! **The commit.** An exported project carries flowRS's runtime crates in `vendor/`, copied
//! from the source this exporter was built from, and its `vendor/README.md` names that commit.
//! The revision is `FLOWRS_EXPORT_REV` when the build sets it, else this checkout's HEAD. A
//! HEAD that changes nothing over its parent (the commit a build host makes of a clean
//! worktree it snapshots) names the parent, the commit that is in flowRS's history.
//!
//! **The source.** Every text file of the crates the bundle may carry (their sources, assets,
//! manifests, licences, notices and provenance; not their tests, examples, benches, fuzzers,
//! fixtures or eval corpora), the workspace's manifest and its `Cargo.lock`, compiled in with
//! `include_str!` (`$OUT_DIR/runtime_sources.rs`). Which of them go into `vendor/`, and how the
//! manifests are rewritten to stand alone, is `runtime.rs`'s to decide, where it is tested;
//! this script only lists files. A generated project, which links the shell alone, runs
//! nothing here.
use std::path::{Path, PathBuf};
use std::process::Command;
/// The crates the bundle may carry: `runtime::RULES` names the same, and a test holds the two
/// together.
const CRATES: &[&str] = &[
"flowrs-openui",
"flowrs-app",
"flowrs-appshell",
"flowrs-math",
"flowrs-dsp",
"flowrs-sequence",
"flowrs-record",
"flowrs-results",
"flowrs-station",
"flowrs-transport",
"flowrs-hal",
"flowrs-device",
];
/// Directories at a crate's top level that never go into a vendored crate.
const SKIP_DIRS: &[&str] = &[
"target", "tests", "benches", "examples", "fuzz", "eval", "fixtures",
];
fn git(dir: &Path, args: &[&str]) -> Option<String> {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
(!s.is_empty()).then_some(s)
}
fn rev(dir: &Path) -> String {
let given = std::env::var("FLOWRS_EXPORT_REV")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let rev = given.or_else(|| {
// Rebuild when HEAD moves: the worktree's HEAD, the branch it names, packed refs.
if let Some(git_dir) = git(dir, &["rev-parse", "--absolute-git-dir"]) {
println!("cargo:rerun-if-changed={git_dir}/HEAD");
if let Some(common) = git(dir, &["rev-parse", "--git-common-dir"]) {
let common = if Path::new(&common).is_absolute() {
common
} else {
dir.join(&common).to_string_lossy().into_owned()
};
println!("cargo:rerun-if-changed={common}/packed-refs");
if let Some(head_ref) = git(dir, &["symbolic-ref", "-q", "HEAD"]) {
println!("cargo:rerun-if-changed={common}/{head_ref}");
}
}
}
let head = git(dir, &["rev-parse", "HEAD"])?;
let tree = git(dir, &["rev-parse", "HEAD^{tree}"]);
let parent_tree = git(dir, &["rev-parse", "HEAD~1^{tree}"]);
match (tree, parent_tree) {
(Some(t), Some(p)) if t == p => git(dir, &["rev-parse", "HEAD~1"]).or(Some(head)),
_ => Some(head),
}
});
// Without git and without the variable: said so, in the README of every project.
rev.unwrap_or_else(|| "unknown".into())
}
/// Every text file under `dir`, as paths relative to `root`, sorted. Hidden entries and the
/// skipped top-level directories are left out; a file that is not UTF-8 is too (no crate the
/// bundle carries compiles one in).
fn files(root: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
let mut entries: Vec<_> = entries.filter_map(Result::ok).collect();
entries.sort_by_key(|e| e.file_name());
for e in entries {
let name = e.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
continue;
}
let path = e.path();
let rel = path
.strip_prefix(root)
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
if path.is_dir() {
if dir == root && SKIP_DIRS.contains(&name.as_str()) {
continue;
}
files(root, &path, out);
} else if std::fs::read_to_string(&path).is_ok() {
out.push((rel, path));
}
}
}
fn main() {
println!("cargo:rerun-if-env-changed=FLOWRS_EXPORT_REV");
println!("cargo:rerun-if-changed=build.rs");
if std::env::var_os("CARGO_FEATURE_BUNDLE").is_none() {
return;
}
let dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
println!("cargo:rustc-env=FLOWRS_EXPORT_REV={}", rev(&dir));
let crates_dir = dir.join("..");
let root = dir.join("../..");
let mut src = String::from(
"/// Each file the bundle may carry: (crate, path within it, contents).\n\
pub(crate) static SOURCES: &[(&str, &str, &str)] = &[\n",
);
for name in CRATES {
let crate_dir = crates_dir.join(name);
println!("cargo:rerun-if-changed={}", crate_dir.display());
let mut found = Vec::new();
files(&crate_dir, &crate_dir, &mut found);
for (rel, path) in found {
let abs = std::fs::canonicalize(&path).unwrap_or(path);
src.push_str(&format!(
" ({name:?}, {rel:?}, include_str!({:?})),\n",
abs.to_string_lossy()
));
}
}
src.push_str("];\n");
// The public engine carries a lock pruned to its own crates; what an export pins is what
// flowRS's whole workspace pins, so there the full lock travels beside the lock data.
let full_lock = dir.join("data/export-cargo.lock");
let lockfile = if full_lock.exists() {
full_lock
} else {
root.join("Cargo.lock")
};
for (constant, file, path) in [
("WORKSPACE_MANIFEST", "Cargo.toml", root.join("Cargo.toml")),
("LOCKFILE", "Cargo.lock", lockfile),
] {
println!("cargo:rerun-if-changed={}", path.display());
let abs = std::fs::canonicalize(&path).unwrap_or(path);
src.push_str(&format!(
"/// The workspace's `{file}`.\npub(crate) static {constant}: &str = include_str!({:?});\n",
abs.to_string_lossy()
));
}
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap_or_default());
std::fs::write(out.join("runtime_sources.rs"), src).expect("write runtime_sources.rs");
}