makepad/libs/relax/build_stamp.rs
Admin b22a714840 makepad run: the Rust JIT (libs/relax) and makepad-remote
The JIT that runs makepad apps from source: lexer, front end, type checker,
lazy borrow checker (Gandalf; `--check-first` opts in), codegen, session and
warm/cold caches keyed by compiler stamp, provided crates bound late,
startup images, live patching. User-facing name is `makepad run` (binary
`makepad`, MAKEPAD_RUN_* env, ~/.makepad-run); libs/relax stays the internal
library name. `makepad check` reports rustc-style diagnostics with rustc
error codes and checks a whole workspace (`--workspace`). Runs on macOS,
Linux, Windows and Android. makepad-remote: one tool schema for apps and the
WM (app/1): grabs, captures, traces, frame profiles and tweaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

159 lines
7.2 KiB
Rust

//! Build provenance, shared by the build scripts of the makepad loader and relax-compiler
//! (#[path]; each declares `json` and `srchash` at its root): MAKEPAD_RUN_PROVENANCE =
//! "fastrust <hash> relax <hash>", content hashes of the source files of the path packages the
//! binary compiles, from `cargo metadata` of the package (its library-dependency closure), split into
//! Relax's own (under libs/relax and tools/run) and the makepad crates it uses. Only file contents count:
//! never git state, mtimes or the checkout's location. A commit, checkout or `git add` neither
//! reruns the script nor changes the stamp; the stamp is in the binary, whose bytes are the
//! build id in every Relax cache key.
//! Reruns: every file the hashes read (a deleted one too) and the workspace manifest (it places
//! the workspace dependencies). Nothing else of the workspace or the repository.
use crate::json;
use crate::srchash::{self, SrcPkg};
use std::path::Path;
use std::process::Command;
/// srchash.rs reads through Relax's VFS (`crate::vfs`); a build script has only the disk.
pub mod vfs {
pub fn read(p: impl AsRef<std::path::Path>) -> std::io::Result<Vec<u8>> {
std::fs::read(p)
}
pub fn is_file(p: impl AsRef<std::path::Path>) -> bool {
p.as_ref().is_file()
}
pub fn read_dir(p: impl AsRef<std::path::Path>) -> std::io::Result<Vec<(String, bool)>> {
let mut v: Vec<(String, bool)> = std::fs::read_dir(p)?
.flatten()
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.file_type().map(|t| t.is_dir()).unwrap_or(false)))
.collect();
v.sort();
Ok(v)
}
}
/// The resolved package graph of the workspace holding the package at `dir` (its resolved
/// features and library dependencies); `root` is that package.
pub struct Graph {
pub pkgs: Vec<SrcPkg>,
pub root: usize,
}
pub fn graph(dir: &Path) -> Graph {
let target = std::env::var("TARGET").unwrap_or_default();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let manifest = dir.join("Cargo.toml");
let out = Command::new(cargo)
.args(["metadata", "--format-version", "1", "--offline", "--filter-platform", &target, "--manifest-path"])
.arg(&manifest)
.output()
.expect("cargo metadata");
assert!(out.status.success(), "cargo metadata: {}", String::from_utf8_lossy(&out.stderr));
let m = json::parse(&out.stdout).expect("cargo metadata json");
let packages = m.get("packages").expect("packages").arr();
let nodes = m.get("resolve").expect("resolve").get("nodes").expect("nodes").arr();
let ids: Vec<&str> = nodes.iter().map(|n| n.get("id").unwrap().str()).collect();
let canon = |p: &Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
let own = canon(&manifest);
let mut pkgs = Vec::new();
let mut root = None;
for n in nodes {
let id = n.get("id").unwrap().str();
let p = packages.iter().find(|p| p.get("id").unwrap().str() == id).expect("package of node");
let manifest = p.get("manifest_path").unwrap().str();
if canon(Path::new(manifest)) == own {
root = Some(pkgs.len());
}
let pdir = Path::new(manifest).parent().unwrap().to_string_lossy().into_owned();
let features = n.get("features").map(|f| f.arr().iter().map(|x| x.str().to_string()).collect()).unwrap_or_default();
// library dependencies (proc macros among them); build- and dev-dependencies are not
// compiled into the crate
let deps = n
.get("deps")
.map(|d| {
d.arr()
.iter()
.filter(|d| d.get("dep_kinds").map_or(true, |k| k.arr().iter().any(|k| matches!(k.get("kind"), None | Some(json::Json::Null)))))
.filter_map(|d| ids.iter().position(|i| *i == d.get("pkg").unwrap().str()))
.collect()
})
.unwrap_or_default();
let proc_macro = p.get("targets").map_or(false, |t| t.arr().iter().any(|t| t.get("kind").map_or(false, |k| k.arr().iter().any(|k| k.str() == "proc-macro"))));
pkgs.push(SrcPkg { name: p.get("name").unwrap().str().to_string(), version: p.get("version").unwrap().str().to_string(), dir: pdir, features, deps, proc_macro });
}
if let Some(ws) = m.get("workspace_root") {
println!("cargo:rerun-if-changed={}/Cargo.toml", ws.str());
}
Graph { pkgs, root: root.unwrap_or_else(|| panic!("{} is not in its cargo metadata", manifest.display())) }
}
impl Graph {
/// The packages `root` reaches, itself included.
pub fn closure(&self, root: usize) -> Vec<usize> {
let mut seen = vec![false; self.pkgs.len()];
let mut stack = vec![root];
let mut out = Vec::new();
while let Some(i) = stack.pop() {
if std::mem::replace(&mut seen[i], true) {
continue;
}
out.push(i);
stack.extend(self.pkgs[i].deps.iter().copied());
}
out
}
/// Prints MAKEPAD_RUN_PROVENANCE for the package's closure and watches every file it hashes:
/// each path package's files as srchash.rs lists them (nested packages, `target` and hidden
/// entries left out) but for its tests, benches and examples, which the binary does not
/// compile. `extra` joins Relax's own part: inputs the binary depends on outside those
/// files (the compiler's std digest).
pub fn provenance(&self, relax_roots: &[&Path], extra: &str) -> String {
let relax_roots: Vec<std::path::PathBuf> = relax_roots.iter().map(|r| r.canonicalize().unwrap_or_else(|_| r.to_path_buf())).collect();
let mut relax = vec![{ let mut h = Fnv::new(); h.bytes(extra.as_bytes()); h.0 }];
let mut host = Vec::new();
for i in self.closure(self.root) {
let p = &self.pkgs[i];
if p.dir.contains("/.cargo/") {
continue;
}
let mut files = Vec::new();
srchash::files(Path::new(&p.dir), "", &mut files);
files.retain(|(rel, _)| !["tests/", "benches/", "examples/"].iter().any(|d| rel.starts_with(d)));
files.sort();
let mut h = Fnv::new();
h.bytes(p.name.as_bytes());
for (rel, f) in &files {
println!("cargo:rerun-if-changed={}", f.display());
h.bytes(rel.as_bytes());
h.bytes(&std::fs::read(f).expect("read package file"));
}
let dir = Path::new(&p.dir).canonicalize().unwrap_or_else(|_| p.dir.clone().into());
if relax_roots.iter().any(|r| dir.starts_with(r)) { relax.push(h.0) } else { host.push(h.0) }
}
format!("makepad {} jit {}", stamp(host), stamp(relax))
}
}
/// FNV-1a, each piece length-prefixed.
struct Fnv(u64);
impl Fnv {
fn new() -> Fnv {
Fnv(0xcbf2_9ce4_8422_2325)
}
fn bytes(&mut self, b: &[u8]) {
for x in (b.len() as u64).to_le_bytes().iter().chain(b) {
self.0 ^= *x as u64;
self.0 = self.0.wrapping_mul(0x100_0000_01b3);
}
}
}
fn stamp(mut parts: Vec<u64>) -> String {
parts.sort_unstable();
let mut h = Fnv::new();
for x in parts {
h.bytes(&x.to_le_bytes());
}
format!("{:012x}", h.0 >> 16)
}