use std::{
collections::{BTreeSet, HashMap, HashSet},
env, fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use cargo_metadata::{Metadata, Package, PackageId};
use crate::compilation;
const ARTIFACT_ENV_VAR: &str = "RIALO_BUILD_ARTIFACT_FILE";
pub fn setup_polkavm_artifact_build() -> PolkaVmArtifactBuild {
PolkaVmArtifactBuild::default()
}
#[derive(Debug, Default)]
pub struct PolkaVmArtifactBuild {
program_path: Option<PathBuf>,
toolchain_version_override: Option<String>,
}
#[derive(Debug)]
pub struct BuildScriptResult {
pub artifact_path: PathBuf,
}
impl PolkaVmArtifactBuild {
pub fn program_path(mut self, program_path: impl Into<PathBuf>) -> Self {
self.program_path = Some(program_path.into());
self
}
pub fn toolchain_version(mut self, version: impl Into<String>) -> Self {
self.toolchain_version_override = Some(version.into());
self
}
pub fn run(self) -> Result<BuildScriptResult> {
emit_rerun_if_env_changed("CARGO_TARGET_DIR");
emit_rerun_if_env_changed("RIALO_RUST_TOOLCHAIN_VERSION");
let manifest_dir = env_path("CARGO_MANIFEST_DIR")?;
let out_dir = env_path("OUT_DIR")?;
let relative_program_path = self
.program_path
.context("PolkaVM artifact build requires a program path")?;
let program_path = manifest_dir.join(relative_program_path);
emit_rerun_if_changed_for_local_package_graph(&program_path)?;
let base_target_dir = crate::resolve_target_dir_for_program(&program_path, None)?;
let target_dir_override = base_target_dir.join("rialo-build");
let result = compilation::compile_program(&compilation::CompileProgramRequest {
program_path: program_path.clone(),
output_dir: out_dir.clone(),
target_dir_override: Some(target_dir_override),
toolchain_version_override: self.toolchain_version_override,
})?;
if let Some(source_path) = result.resolved_toolchain.source_path.as_deref() {
emit_rerun_if_changed(source_path);
}
let artifact_file_name = result
.program_binary
.file_name()
.context("Compiled program artifact has no file name")?;
let artifact_path = out_dir.join(artifact_file_name);
fs::copy(&result.program_binary, &artifact_path).with_context(|| {
format!(
"Failed to copy {} to {}",
result.program_binary.display(),
artifact_path.display()
)
})?;
write_stable_polkavm_artifact(
&program_path,
&result.package_name,
&result.program_binary,
artifact_file_name,
)?;
println!(
"cargo:rustc-env={ARTIFACT_ENV_VAR}={}",
artifact_file_name.to_string_lossy()
);
Ok(BuildScriptResult { artifact_path })
}
}
fn write_stable_polkavm_artifact(
program_path: &Path,
package_name: &str,
source_binary: &Path,
artifact_file_name: &std::ffi::OsStr,
) -> Result<()> {
let stable_dir = program_path
.join("target")
.join("rialo-build")
.join(format!("{package_name}-riscv"));
fs::create_dir_all(&stable_dir).with_context(|| {
format!(
"Failed to create stable artifact directory {}",
stable_dir.display()
)
})?;
let stable_path = stable_dir.join(artifact_file_name);
let tmp_path = stable_dir.join(format!("{}.tmp", artifact_file_name.to_string_lossy()));
let _ = fs::remove_file(&tmp_path);
fs::copy(source_binary, &tmp_path).with_context(|| {
format!(
"Failed to copy {} to {}",
source_binary.display(),
tmp_path.display()
)
})?;
fs::rename(&tmp_path, &stable_path).with_context(|| {
format!(
"Failed to rename {} to {}",
tmp_path.display(),
stable_path.display()
)
})?;
println!("Wrote stable PolkaVM artifact to {}", stable_path.display());
Ok(())
}
fn env_path(var_name: &str) -> Result<PathBuf> {
let value = env::var_os(var_name)
.with_context(|| format!("{var_name} is not available in this build script"))?;
Ok(PathBuf::from(value))
}
fn emit_rerun_if_env_changed(var_name: &str) {
println!("cargo:rerun-if-env-changed={var_name}");
}
fn emit_rerun_if_changed(path: &Path) {
println!("cargo:rerun-if-changed={}", path.display());
}
fn emit_rerun_if_changed_for_local_package_graph(program_path: &Path) -> Result<()> {
for path in local_package_graph_watch_paths(program_path)? {
emit_rerun_if_changed(&path);
}
Ok(())
}
fn local_package_graph_watch_paths(program_path: &Path) -> Result<Vec<PathBuf>> {
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(program_path.join("Cargo.toml"))
.exec()
.with_context(|| {
format!(
"Failed to load Cargo metadata for {}",
program_path.display()
)
})?;
let root_package = root_package(&metadata, program_path)?;
let local_package_ids = local_package_closure(&metadata, &root_package.id)?;
let mut watched_paths = BTreeSet::new();
watched_paths.insert(metadata.workspace_root.as_std_path().join("Cargo.toml"));
let lockfile = metadata.workspace_root.as_std_path().join("Cargo.lock");
if lockfile.exists() {
watched_paths.insert(lockfile);
}
for package in metadata
.packages
.iter()
.filter(|package| package.source.is_none() && local_package_ids.contains(&package.id))
{
watched_paths.insert(package.manifest_path.as_std_path().to_path_buf());
let package_dir = package
.manifest_path
.as_std_path()
.parent()
.context("Package manifest path has no parent directory")?;
let src_dir = package_dir.join("src");
if src_dir.exists() {
watched_paths.insert(src_dir);
}
let build_script = package_dir.join("build.rs");
if build_script.exists() {
watched_paths.insert(build_script);
}
}
Ok(watched_paths.into_iter().collect())
}
fn root_package<'a>(metadata: &'a Metadata, program_path: &Path) -> Result<&'a Package> {
let manifest_path = program_path
.join("Cargo.toml")
.canonicalize()
.with_context(|| format!("Failed to canonicalize {}", program_path.display()))?;
metadata
.packages
.iter()
.find(|package| {
package
.manifest_path
.as_std_path()
.canonicalize()
.ok()
.as_ref()
== Some(&manifest_path)
})
.with_context(|| format!("Failed to locate package for {}", program_path.display()))
}
fn local_package_closure(metadata: &Metadata, root_id: &PackageId) -> Result<HashSet<PackageId>> {
let resolve = metadata
.resolve
.as_ref()
.context("Cargo metadata did not include a resolved package graph")?;
let edges: HashMap<&PackageId, Vec<&PackageId>> = resolve
.nodes
.iter()
.map(|node| (&node.id, node.deps.iter().map(|dep| &dep.pkg).collect()))
.collect();
let package_lookup: HashMap<&PackageId, &Package> = metadata
.packages
.iter()
.map(|package| (&package.id, package))
.collect();
let mut visited = HashSet::new();
let mut stack = vec![root_id.clone()];
while let Some(package_id) = stack.pop() {
if !visited.insert(package_id.clone()) {
continue;
}
for dependency_id in edges.get(&package_id).into_iter().flatten() {
let Some(package) = package_lookup.get(dependency_id) else {
continue;
};
if package.source.is_none() {
stack.push((*dependency_id).clone());
}
}
}
Ok(visited)
}
#[cfg(test)]
mod tests {
use std::fs;
use super::local_package_graph_watch_paths;
#[test]
fn local_package_graph_watch_paths_include_local_packages_and_workspace_files() {
let tempdir = tempfile::tempdir().expect("create tempdir");
let workspace_root = tempdir.path();
fs::write(
workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["app", "dep"]
resolver = "2"
"#,
)
.expect("write workspace Cargo.toml");
fs::create_dir_all(workspace_root.join("app/src")).expect("create app/src");
fs::write(
workspace_root.join("app/Cargo.toml"),
r#"[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
dep = { path = "../dep" }
"#,
)
.expect("write app Cargo.toml");
fs::write(workspace_root.join("app/src/lib.rs"), "pub fn app() {}\n")
.expect("write app lib.rs");
fs::create_dir_all(workspace_root.join("dep/src")).expect("create dep/src");
fs::write(
workspace_root.join("dep/Cargo.toml"),
r#"[package]
name = "dep"
version = "0.1.0"
edition = "2021"
"#,
)
.expect("write dep Cargo.toml");
fs::write(workspace_root.join("dep/src/lib.rs"), "pub fn dep() {}\n")
.expect("write dep lib.rs");
fs::write(workspace_root.join("dep/build.rs"), "fn main() {}\n")
.expect("write dep build.rs");
fs::write(workspace_root.join("Cargo.lock"), "").expect("write Cargo.lock");
let watched_paths = local_package_graph_watch_paths(&workspace_root.join("app"))
.expect("collect watched paths");
assert!(watched_paths.contains(&workspace_root.join("Cargo.toml")));
assert!(watched_paths.contains(&workspace_root.join("Cargo.lock")));
assert!(watched_paths.contains(&workspace_root.join("app/Cargo.toml")));
assert!(watched_paths.contains(&workspace_root.join("app/src")));
assert!(watched_paths.contains(&workspace_root.join("dep/Cargo.toml")));
assert!(watched_paths.contains(&workspace_root.join("dep/src")));
assert!(watched_paths.contains(&workspace_root.join("dep/build.rs")));
}
}