//! 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 { 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"); }