use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::PathBuf;
const VEROVIO_VERSION: &str = "5.7.0";
const VEROVIO_TARBALL_SHA256: &str =
"bf7483504ddbf2d7ff59ae53b547e6347f89f82583559bf264d97b3624279d5e";
fn get_download_url() -> String {
format!(
"https://github.com/rism-digital/verovio/archive/refs/tags/version-{}.tar.gz",
VEROVIO_VERSION
)
}
const VEROVIOXIDE_VERSION: &str = "0.1.0";
fn get_release_base_url() -> String {
format!(
"https://github.com/oxur/verovioxide/releases/download/v{}",
VEROVIOXIDE_VERSION
)
}
fn get_prebuilt_url(target: &str) -> String {
let lib_name = get_prebuilt_lib_name(target);
format!("{}/{}", get_release_base_url(), lib_name)
}
fn get_hashes_url() -> String {
format!("{}/hashes.json", get_release_base_url())
}
fn get_prebuilt_lib_name(target: &str) -> String {
if target.contains("windows") && target.contains("msvc") {
format!("verovio-{}-{}.lib", VEROVIO_VERSION, target)
} else {
format!("libverovio-{}-{}.a", VEROVIO_VERSION, target)
}
}
const SUPPORTED_TARGETS: &[&str] = &[
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
];
fn fetch_prebuilt_sha256(target: &str) -> Result<String, String> {
let url = get_hashes_url();
let cache_dir = get_cache_dir();
std::fs::create_dir_all(&cache_dir)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
let hashes_cache_path = cache_dir.join(format!("hashes-v{}.json", VEROVIOXIDE_VERSION));
let hashes_json = if hashes_cache_path.exists() {
std::fs::read_to_string(&hashes_cache_path)
.map_err(|e| format!("Failed to read cached hashes: {}", e))?
} else {
println!("cargo:warning=Downloading hash manifest from: {}", url);
let response = ureq::get(&url).call().map_err(|e| {
format!(
"Failed to download hash manifest: {}\n\n\
This likely means prebuilt binaries haven't been released yet for v{}.\n\
Use the 'bundled' feature to compile from source:\n\
cargo build --no-default-features --features bundled",
e, VEROVIOXIDE_VERSION
)
})?;
if response.status() != 200 {
return Err(format!(
"HTTP error downloading hash manifest: status {}\n\n\
Prebuilt binaries may not be available for v{}.\n\
Use the 'bundled' feature to compile from source.",
response.status(),
VEROVIOXIDE_VERSION
));
}
let json = response
.into_string()
.map_err(|e| format!("Failed to read hash manifest: {}", e))?;
let _ = std::fs::write(&hashes_cache_path, &json);
json
};
let search_key = format!("\"{}\"", target);
if let Some(key_pos) = hashes_json.find(&search_key) {
let after_key = &hashes_json[key_pos + search_key.len()..];
if let Some(colon_pos) = after_key.find(':') {
let after_colon = &after_key[colon_pos + 1..];
if let Some(quote_start) = after_colon.find('"') {
let value_start = &after_colon[quote_start + 1..];
if let Some(quote_end) = value_start.find('"') {
let hash = &value_start[..quote_end];
return Ok(hash.to_string());
}
}
}
}
Err(format!(
"No prebuilt library available for target '{}'. \n\
Supported targets: {}.\n\n\
Use the 'bundled' feature instead to compile from source:\n\
cargo build --no-default-features --features bundled",
target,
SUPPORTED_TARGETS.join(", ")
))
}
fn download_prebuilt() -> Result<PathBuf, String> {
let target = std::env::var("TARGET").map_err(|_| "TARGET environment variable not set")?;
if !SUPPORTED_TARGETS.contains(&target.as_str()) {
return Err(format!(
"Target '{}' is not supported for prebuilt binaries.\n\
Supported targets: {}.\n\n\
Use the 'bundled' feature instead to compile from source:\n\
cargo build --no-default-features --features bundled",
target,
SUPPORTED_TARGETS.join(", ")
));
}
let cache_dir = get_cache_dir();
std::fs::create_dir_all(&cache_dir)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
let lib_name = get_prebuilt_lib_name(&target);
let lib_path = cache_dir.join(&lib_name);
let expected_hash = fetch_prebuilt_sha256(&target)?;
if lib_path.exists() {
match verify_sha256(&lib_path, &expected_hash) {
Ok(HashVerification::Match) => {
println!(
"cargo:warning=Using cached prebuilt library: {}",
lib_path.display()
);
return Ok(lib_path);
}
_ => {
let _ = std::fs::remove_file(&lib_path);
}
}
}
let url = get_prebuilt_url(&target);
println!(
"cargo:warning=Downloading prebuilt Verovio library from: {}",
url
);
download_file(&url, &lib_path)?;
match verify_sha256(&lib_path, &expected_hash) {
Ok(HashVerification::Match) => {
println!("cargo:warning=Prebuilt library verified successfully");
Ok(lib_path)
}
Ok(HashVerification::Mismatch { actual }) => {
let _ = std::fs::remove_file(&lib_path);
Err(format!(
"SHA256 hash mismatch for prebuilt library.\n\
Expected: {}\n\
Actual: {}\n\n\
The prebuilt binary may be corrupted or tampered with.\n\
Try again or use --features bundled to compile from source.",
expected_hash, actual
))
}
Err(e) => {
let _ = std::fs::remove_file(&lib_path);
Err(format!("Failed to verify prebuilt library: {}", e))
}
}
}
fn get_cache_dir() -> PathBuf {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let workspace_root = manifest_dir
.parent() .and_then(|p| p.parent()) .expect("Failed to find workspace root from CARGO_MANIFEST_DIR");
workspace_root.join("target").join("verovio-cache")
}
fn get_cached_library_path() -> PathBuf {
let cache_dir = get_cache_dir();
if cfg!(target_os = "windows") && cfg!(target_env = "msvc") {
cache_dir.join("verovio.lib")
} else {
cache_dir.join("libverovio.a")
}
}
fn get_cached_source_dir() -> PathBuf {
get_cache_dir().join("verovio-source")
}
fn should_use_cache() -> bool {
if std::env::var("CARGO_FEATURE_FORCE_REBUILD").is_ok() {
println!("cargo:warning=force-rebuild feature enabled, recompiling Verovio");
return false;
}
let cached_lib = get_cached_library_path();
if cached_lib.exists() {
println!(
"cargo:warning=Using cached Verovio library from {}",
cached_lib.display()
);
true
} else {
println!(
"cargo:warning=No cached Verovio library found at {}, compiling from source",
cached_lib.display()
);
false
}
}
fn emit_link_directives(search_path: &std::path::Path) {
println!("cargo:rustc-link-search=native={}", search_path.display());
println!("cargo:rustc-link-lib=static=verovio");
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-lib=c++");
} else if cfg!(target_os = "linux") {
println!("cargo:rustc-link-lib=stdc++");
} else if cfg!(target_os = "windows") {
if cfg!(target_env = "gnu") {
println!("cargo:rustc-link-lib=stdc++");
}
}
}
fn cache_compiled_library(out_dir: &std::path::Path) {
let cache_dir = get_cache_dir();
if let Err(e) = std::fs::create_dir_all(&cache_dir) {
println!(
"cargo:warning=Failed to create cache directory: {}. Caching disabled.",
e
);
return;
}
let lib_name = if cfg!(target_os = "windows") && cfg!(target_env = "msvc") {
"verovio.lib"
} else {
"libverovio.a"
};
let source = out_dir.join(lib_name);
let dest = cache_dir.join(lib_name);
if source.exists() {
match std::fs::copy(&source, &dest) {
Ok(_) => println!("cargo:warning=Cached Verovio library to {}", dest.display()),
Err(e) => println!(
"cargo:warning=Failed to cache library: {}. Future builds may recompile.",
e
),
}
} else {
println!(
"cargo:warning=Compiled library not found at {}, caching skipped",
source.display()
);
}
}
enum HashVerification {
Match,
Mismatch { actual: String },
}
fn verify_sha256(path: &std::path::Path, expected_hash: &str) -> Result<HashVerification, String> {
let data =
std::fs::read(path).map_err(|e| format!("Failed to read file for hashing: {}", e))?;
let mut hasher = Sha256::new();
hasher.update(&data);
let result = hasher.finalize();
let actual_hash = format!("{:x}", result);
if actual_hash == expected_hash {
Ok(HashVerification::Match)
} else {
Ok(HashVerification::Mismatch {
actual: actual_hash,
})
}
}
fn download_file(url: &str, dest: &std::path::Path) -> Result<(), String> {
println!("cargo:warning=Downloading Verovio source from: {}", url);
println!("cargo:warning=This may take a moment on first build...");
let response = ureq::get(url)
.call()
.map_err(|e| format!("Failed to download Verovio source: {}", e))?;
if response.status() != 200 {
return Err(format!(
"HTTP error downloading Verovio source: status {}",
response.status()
));
}
let mut reader = response.into_reader();
let mut data = Vec::new();
std::io::Read::read_to_end(&mut reader, &mut data)
.map_err(|e| format!("Failed to read download response: {}", e))?;
std::fs::write(dest, &data).map_err(|e| format!("Failed to write downloaded file: {}", e))?;
Ok(())
}
fn extract_tarball(
tarball_path: &std::path::Path,
dest_dir: &std::path::Path,
) -> Result<(), String> {
let file =
std::fs::File::open(tarball_path).map_err(|e| format!("Failed to open tarball: {}", e))?;
let gz_decoder = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(gz_decoder);
archive
.unpack(dest_dir)
.map_err(|e| format!("Failed to extract tarball: {}", e))?;
Ok(())
}
fn discover_verovio_source() -> Result<PathBuf, String> {
if let Ok(env_path) = std::env::var("VEROVIO_SOURCE_DIR") {
let path = PathBuf::from(&env_path);
if path.exists() && path.join("src").exists() {
println!(
"cargo:warning=Using Verovio source from VEROVIO_SOURCE_DIR: {}",
path.display()
);
return Ok(path);
} else {
return Err(format!(
"VEROVIO_SOURCE_DIR is set to '{}' but it doesn't appear to be a valid Verovio source directory. \
Expected to find a 'src' subdirectory.",
env_path
));
}
}
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let submodule_path = manifest_dir.join("../../verovio");
if let Ok(canonical) = dunce::canonicalize(&submodule_path) {
if canonical.join("src").exists() {
println!(
"cargo:warning=Using Verovio source from local submodule: {}",
canonical.display()
);
return Ok(canonical);
}
}
let cache_dir = get_cache_dir();
let source_cache_dir = get_cached_source_dir();
let extracted_dir = source_cache_dir.join(format!("verovio-version-{}", VEROVIO_VERSION));
if extracted_dir.exists() && extracted_dir.join("src").exists() {
println!(
"cargo:warning=Using cached Verovio source from: {}",
extracted_dir.display()
);
return Ok(extracted_dir);
}
println!("cargo:warning=Verovio source not found locally, downloading from GitHub...");
std::fs::create_dir_all(&cache_dir)
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
std::fs::create_dir_all(&source_cache_dir)
.map_err(|e| format!("Failed to create source cache directory: {}", e))?;
let tarball_path = source_cache_dir.join(format!("verovio-{}.tar.gz", VEROVIO_VERSION));
let url = get_download_url();
download_file(&url, &tarball_path)?;
println!("cargo:warning=Verifying download integrity...");
match verify_sha256(&tarball_path, VEROVIO_TARBALL_SHA256) {
Ok(HashVerification::Match) => {
println!("cargo:warning=SHA256 hash verified successfully");
}
Ok(HashVerification::Mismatch { actual }) => {
let _ = std::fs::remove_file(&tarball_path);
return Err(format!(
"SHA256 hash mismatch for downloaded Verovio source.\n\n\
Expected: {}\n\
Actual: {}\n\n\
This could indicate:\n\
1. A corrupted download - try again\n\
2. The VEROVIO_TARBALL_SHA256 constant needs updating for version {}\n\
3. A supply chain attack (unlikely but verify manually)\n\n\
To update the hash, run:\n\
curl -sL {} | shasum -a 256\n\n\
Or set VEROVIO_SOURCE_DIR to use a local copy.",
VEROVIO_TARBALL_SHA256,
actual,
VEROVIO_VERSION,
get_download_url()
));
}
Err(e) => {
let _ = std::fs::remove_file(&tarball_path);
return Err(format!("Failed to verify download hash: {}", e));
}
}
println!("cargo:warning=Extracting Verovio source...");
extract_tarball(&tarball_path, &source_cache_dir)?;
if extracted_dir.exists() && extracted_dir.join("src").exists() {
println!(
"cargo:warning=Verovio source extracted to: {}",
extracted_dir.display()
);
let _ = std::fs::remove_file(&tarball_path);
Ok(extracted_dir)
} else {
Err(format!(
"Extraction completed but expected directory not found: {}\n\
Please report this issue or set VEROVIO_SOURCE_DIR to use a local copy.",
extracted_dir.display()
))
}
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=VEROVIO_SOURCE_DIR");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let prebuilt_enabled = std::env::var("CARGO_FEATURE_PREBUILT").is_ok();
let bundled_enabled = std::env::var("CARGO_FEATURE_BUNDLED").is_ok();
if prebuilt_enabled {
match download_prebuilt() {
Ok(lib_path) => {
let lib_dir = lib_path.parent().expect("library path has no parent");
emit_link_directives(lib_dir);
return;
}
Err(e) => {
if bundled_enabled {
println!(
"cargo:warning=Prebuilt download failed, falling back to bundled compilation: {}",
e
);
} else {
panic!(
"\n\nFailed to download prebuilt Verovio library:\n\n{}\n\n\
To resolve this, you can:\n\
1. Use --features bundled to compile from source instead\n\
2. Check your network connection\n\
3. Verify the target platform is supported\n\n",
e
);
}
}
}
}
if !bundled_enabled {
return;
}
if should_use_cache() {
let cache_dir = get_cache_dir();
emit_link_directives(&cache_dir);
return;
}
let verovio_dir = match discover_verovio_source() {
Ok(path) => path,
Err(e) => {
panic!(
"\n\nFailed to locate Verovio source:\n\n{}\n\n\
To resolve this, you can:\n\
1. Set VEROVIO_SOURCE_DIR environment variable to your local Verovio source\n\
2. Initialize the git submodule: git submodule update --init\n\
3. Ensure you have network access to download from GitHub\n\n",
e
);
}
};
println!(
"cargo:rerun-if-changed={}",
verovio_dir.join("tools/c_wrapper.cpp").display()
);
println!(
"cargo:rerun-if-changed={}",
verovio_dir.join("tools/c_wrapper.h").display()
);
let git_commit_h = verovio_dir.join("include/vrv/git_commit.h");
if !git_commit_h.exists() {
let mut file = std::fs::File::create(&git_commit_h).expect("Failed to create git_commit.h");
writeln!(
file,
"////////////////////////////////////////////////////////"
)
.unwrap();
writeln!(
file,
"/// Git commit version file generated at compilation ///"
)
.unwrap();
writeln!(
file,
"////////////////////////////////////////////////////////"
)
.unwrap();
writeln!(file).unwrap();
writeln!(file, "#define GIT_COMMIT \"\"").unwrap();
writeln!(file).unwrap();
}
let mut build = cc::Build::new();
build.cpp(true).std("c++20");
let include_dirs = [
"include",
"include/vrv",
"include/crc",
"include/midi",
"include/hum",
"include/json",
"include/pugi",
"include/zip",
"libmei/dist",
"libmei/addons",
];
for dir in &include_dirs {
build.include(verovio_dir.join(dir));
}
if cfg!(target_os = "windows") {
build.include(verovio_dir.join("include/win32"));
}
build.define("NO_DARMS_SUPPORT", None);
build.define("NO_RUNTIME", None);
build.define("RESOURCE_DIR", "\"/usr/local/share/verovio\"");
if !cfg!(target_env = "msvc") {
build
.flag("-Wall")
.flag("-W")
.flag("-pedantic")
.flag("-Wno-unused-parameter")
.flag("-Wno-dollar-in-identifier-extension")
.flag("-Wno-conversion")
.flag("-Wno-float-conversion")
.flag("-Wno-missing-braces")
.flag("-Wno-missing-field-initializers")
.flag("-Wno-overloaded-virtual")
.flag("-Wno-shadow")
.flag("-Wno-sign-conversion")
.flag("-Wno-trigraphs")
.flag("-Wno-unknown-pragmas")
.flag("-Wno-unused-label");
} else {
build.flag("/bigobj").flag("/W2").flag("/wd4244");
build.define("NO_PAE_SUPPORT", None);
build.define("USE_PAE_OLD_PARSER", None);
}
let mut sources: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(verovio_dir.join("src")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp")
&& path.file_name().is_some_and(|name| name != "main.cpp")
{
sources.push(path);
}
}
if let Ok(entries) = std::fs::read_dir(verovio_dir.join("src/hum")) {
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp") {
sources.push(path);
}
}
}
for entry in std::fs::read_dir(verovio_dir.join("src/midi")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp") {
sources.push(path);
}
}
for entry in std::fs::read_dir(verovio_dir.join("src/crc")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp") {
sources.push(path);
}
}
sources.push(verovio_dir.join("src/json/jsonxx.cc"));
sources.push(verovio_dir.join("src/pugi/pugixml.cpp"));
for entry in std::fs::read_dir(verovio_dir.join("libmei/dist")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp") {
sources.push(path);
}
}
for entry in std::fs::read_dir(verovio_dir.join("libmei/addons")).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "cpp") {
sources.push(path);
}
}
sources.push(verovio_dir.join("tools/c_wrapper.cpp"));
for source in &sources {
build.file(source);
}
build.compile("verovio");
cache_compiled_library(&out_dir);
emit_link_directives(&out_dir);
}