2026-05-29 23:02:57 +02:00

208 lines
7.5 KiB
Rust

use std::path::PathBuf;
// Canonical definition of the env var name. Also hardcoded as a literal in
// `lbc-common`'s `circuit_artifacts!` macro (env!() requires a literal).
const LBC_ROOT_DIR: &str = "LBC_ROOT_DIR";
#[cfg(feature = "prebuilt")]
mod prebuilt {
use std::path::{Path, PathBuf};
use ureq::{Body, http::Response};
static REPO: &str = "logos-blockchain/logos-blockchain-circuits";
static ARTIFACT_PREFIX: &str = "logos-blockchain-circuits";
fn build_artifact_name(version: &str, os: &str, arch: &str) -> String {
format!("{ARTIFACT_PREFIX}-v{version}-{os}-{arch}")
}
fn build_artifact_url(version: &str, os: &str, arch: &str) -> String {
let artifact = build_artifact_name(version, os, arch);
let artifact_tar_gz = format!("{artifact}.tar.gz");
format!("https://github.com/{REPO}/releases/download/v{version}/{artifact_tar_gz}")
}
fn fetch_artifact(version: &str, os: &str, arch: &str) -> Response<Body> {
let url = build_artifact_url(version, os, arch);
// We skip checksum verification intentionally.
// Hardcoded hashes would protect against a silently replaced release asset but
// require a two-step release (build → hash → commit → tag) which feels
// overkill for a first-party library.
ureq::get(&url).call().unwrap_or_else(|error| {
panic!(
"Failed to download a prebuilt artifact for {os}-{arch} v{version}: {error}. \
Set {ENV_VAR} to point to a local build instead.",
ENV_VAR = super::LBC_ROOT_DIR
)
})
}
fn unpack_artifact(
response: Response<Body>,
version: &str,
os: &str,
arch: &str,
output_dir: &Path,
) -> PathBuf {
let gz_decoder = flate2::read::GzDecoder::new(response.into_body().into_reader());
let mut archive = tar::Archive::new(gz_decoder);
archive
.unpack(output_dir)
.expect("Failed to unpack the downloaded archive.");
let artifact_root = output_dir.join(build_artifact_name(version, os, arch));
assert!(
artifact_root.is_dir(),
"Failed to find the unpacked artifact directory at {}.",
artifact_root.display()
);
artifact_root
}
/// Produce a lockfile for the given directory
fn get_lockfile(directory: &Path) -> fd_lock::RwLock<std::fs::File> {
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(directory.join(".lock"))
.expect("Failed to open the cache lock file.");
fd_lock::RwLock::new(file)
}
fn get_cache_dir() -> PathBuf {
dirs::cache_dir()
.expect("Could not determine the cache directory for this platform.")
.join("logos")
.join("blockchain")
}
pub fn provision_artifact() -> PathBuf {
let version = env!("CARGO_PKG_VERSION");
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let cache = get_cache_dir();
let artifact_root = cache.join(build_artifact_name(version, &os, &arch));
std::fs::create_dir_all(&cache).expect("Failed to create the cache directory.");
// Since the circuits' libraries are all contained in the same single artifact,
// each crate will try to download the same artifact.
// To avoid redundant downloads, we use a lock to ensure that only one process
// fetches the artifact while the others wait for it to complete and
// then re-check the cache.
let mut lock = get_lockfile(&cache);
let _guard = lock.write().expect("Failed to acquire cache lock.");
if artifact_root.is_dir() {
println!(
"Found a cached artifact at {}, reusing.",
artifact_root.display()
);
return artifact_root;
}
println!("No cached download found, downloading v{version} for {os}-{arch}...");
let response = fetch_artifact(version, &os, &arch);
println!("Download complete, unpacking...");
let root = unpack_artifact(response, version, &os, &arch, &cache);
println!("Ready, artifact at {}.", root.display());
root
}
}
/// Resolves the circuits root directory, either from the environment variable
/// or by downloading a prebuilt version.
///
/// If called more than once per build script (e.g. `build_circuit` +
/// `build_rapidsnark`), `cargo:rustc-env=LBC_ROOT_DIR` is emitted twice. Cargo
/// takes last-wins for duplicate keys, but this is safe here since both calls
/// resolve to the same path.
fn resolve_root() -> PathBuf {
println!("cargo:rerun-if-env-changed={LBC_ROOT_DIR}");
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=build.rs");
let root = std::env::var(LBC_ROOT_DIR).map_or_else(
|_| {
#[cfg(not(feature = "prebuilt"))]
panic!(
"{LBC_ROOT_DIR} is not set. Either:\n\
- Set {LBC_ROOT_DIR} to point at a local build, or\n\
- Enable the `prebuilt` feature to download from GitHub Releases."
);
#[cfg(feature = "prebuilt")]
{
println!(
"Environment variable '{LBC_ROOT_DIR}' is not set, falling back to prebuilt download"
);
prebuilt::provision_artifact()
}
},
|dir| {
println!("Environment variable '{LBC_ROOT_DIR}' set, using local artifact at '{dir}'");
let dir_path = PathBuf::from(dir);
assert!(
dir_path.is_dir(),
"The root directory specified in '{LBC_ROOT_DIR}' at {} does not exist.",
dir_path.display()
);
dir_path
},
);
let root_str = root
.to_str()
.expect("Failed to convert the root directory path to a string");
println!("cargo:rustc-env={LBC_ROOT_DIR}={root_str}");
root
}
pub fn build_circuit(circuit_name: &str) {
let root = resolve_root();
let circuit_dir = root.join(circuit_name);
let circuit_dir_str = circuit_dir
.to_str()
.expect("Failed to convert the circuit directory path to a string");
let lib_dir = root.join("lib");
let lib_dir_str = lib_dir
.to_str()
.expect("Failed to convert the lib directory path to a string");
println!("cargo:rerun-if-changed={circuit_dir_str}");
println!("cargo:rerun-if-changed={lib_dir_str}");
println!("cargo:rustc-link-search=native={circuit_dir_str}");
println!("cargo:rustc-link-search=native={lib_dir_str}");
println!("cargo:rustc-link-lib=static={circuit_name}");
let cpp_lib = std::env::var("CARGO_CFG_TARGET_OS").map_or_else(
|_| "stdc++",
|os| if os == "macos" { "c++" } else { "stdc++" },
);
println!("cargo:rustc-link-lib={cpp_lib}");
println!("cargo:rustc-link-lib=static=gmp");
}
pub fn build_rapidsnark() {
let root = resolve_root();
let extension = if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
".exe"
} else {
""
};
for binary in ["prover", "verifier"] {
let path = root.join(format!("{binary}{extension}"));
println!("cargo:rerun-if-changed={}", path.display());
}
}