newton-core 0.4.16

newton protocol core sdk
//! Build script for newton-prover-core
//!
//! This script:
//! 1. Extracts version from Cargo.toml at build time
//! 2. Injects it into Rust code via environment variables
//! 3. Generates a Solidity file with version constants for contracts
//! 4. Embeds deployment JSONs so binaries are self-contained

use std::{env, fs, path::PathBuf};

fn main() {
    // Get the package version from Cargo
    let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");

    // Derive MIN_COMPATIBLE as MAJOR.MINOR.0 (patch is ignored for compatibility)
    let min_compatible = derive_min_compatible(&version);

    // Make version available to Rust code at compile time
    println!("cargo:rustc-env=PROTOCOL_VERSION={}", version);
    println!("cargo:rustc-env=MIN_COMPATIBLE_VERSION={}", min_compatible);

    // Generate Solidity version file
    generate_solidity_version(&version, &min_compatible);

    // Embed deployment JSONs into the binary
    generate_embedded_deployments();

    // Expose workspace root for config file discovery in dev/test builds
    let manifest_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
    let workspace_root = manifest_path
        .parent()
        .and_then(|p| p.parent())
        .expect("Failed to find workspace root");
    println!("cargo:rustc-env=WORKSPACE_ROOT={}", workspace_root.display());

    // Re-run if Cargo.toml changes
    println!("cargo:rerun-if-changed=../../Cargo.toml");
    println!("cargo:rerun-if-changed=Cargo.toml");
    println!("cargo:rerun-if-changed=../../contracts/script/deployments");
}

/// Derive the minimum compatible version by zeroing the patch component.
/// e.g., "0.1.3" -> "0.1.0", "1.5.7" -> "1.5.0"
fn derive_min_compatible(version: &str) -> String {
    let parts: Vec<&str> = version.split('.').collect();
    if parts.len() == 3 {
        format!("{}.{}.0", parts[0], parts[1])
    } else {
        // Fallback: use the version as-is if format is unexpected
        version.to_string()
    }
}

/// Generate a Rust source file that embeds all deployment JSONs via `include_str!`.
///
/// Produces `$OUT_DIR/embedded_deployments.rs` with a function:
/// ```ignore
/// pub fn get_embedded_deployment(category: &str, chain_id: u64, env: &str) -> Option<&'static str>
/// ```
fn generate_embedded_deployments() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
    let workspace_root = PathBuf::from(&manifest_dir)
        .parent()
        .and_then(|p| p.parent())
        .expect("Failed to find workspace root")
        .to_path_buf();

    let deployments_dir = workspace_root.join("contracts").join("script").join("deployments");

    let categories = ["core", "newton-prover", "policy", "newton-cross-chain"];
    let mut arms = Vec::new();

    let manifest_path = PathBuf::from(&manifest_dir);

    for category in &categories {
        let cat_dir = deployments_dir.join(category);
        if !cat_dir.exists() {
            continue;
        }
        let entries: Vec<_> = fs::read_dir(&cat_dir)
            .unwrap_or_else(|e| panic!("Failed to read {:?}: {}", cat_dir, e))
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
            .collect();

        for entry in entries {
            let filename = entry.file_name();
            let filename_str = filename.to_string_lossy();
            // Parse "{chain_id}-{env}.json"
            let stem = filename_str.strip_suffix(".json").unwrap();
            let parts: Vec<&str> = stem.splitn(2, '-').collect();
            if parts.len() != 2 {
                continue;
            }
            let chain_id = parts[0];
            let env_name = parts[1];

            let abs_path = entry.path();
            let rel_path = abs_path
                .strip_prefix(&workspace_root)
                .map(|p| format!("../../{}", p.to_string_lossy().replace('\\', "/")))
                .unwrap_or_else(|_| abs_path.to_string_lossy().to_string());

            // Per-file rerun-if-changed so edits to individual JSONs trigger rebuild
            println!("cargo:rerun-if-changed={}", abs_path.to_string_lossy());

            arms.push(format!(
                r#"        ("{cat}", {chain_id}, "{env_name}") => Some(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/{rel}"))),"#,
                cat = category,
                chain_id = chain_id,
                env_name = env_name,
                rel = rel_path,
            ));
        }
    }

    let generated = format!(
        r#"/// Auto-generated by build.rs — do not edit.
/// Returns embedded deployment JSON for the given category, chain_id, and env.
pub fn get_embedded_deployment(category: &str, chain_id: u64, env: &str) -> Option<&'static str> {{
    match (category, chain_id, env) {{
{arms}
        _ => None,
    }}
}}
"#,
        arms = arms.join("\n"),
    );

    let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
    let out_path = PathBuf::from(&out_dir).join("embedded_deployments.rs");

    let should_write = if out_path.exists() {
        fs::read_to_string(&out_path).map_or(true, |existing| existing != generated)
    } else {
        true
    };

    if should_write {
        fs::write(&out_path, generated).expect("Failed to write embedded_deployments.rs");
    }
}

fn generate_solidity_version(version: &str, min_compatible: &str) {
    let solidity_content = format!(
        r#"// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;

// @title Protocol Version Constants
// @notice This file is auto-generated by the Rust build system from Cargo.toml
// @dev DO NOT EDIT MANUALLY - Changes will be overwritten on next build
// @dev To update versions, modify the workspace version in the root Cargo.toml
// @dev MIN_COMPATIBLE version uses MAJOR.MINOR.0 (patch is ignored for compatibility)

// Protocol version following SemVer 2.0.0 (MAJOR.MINOR.PATCH)
string constant PROTOCOL_VERSION = "{}";

// Minimum compatible factory version (MAJOR.MINOR.0)
string constant MIN_COMPATIBLE_VERSION = "{}";
"#,
        version, min_compatible
    );

    let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
    let manifest_path = PathBuf::from(&manifest_dir);
    let workspace_root = manifest_path
        .parent()
        .and_then(|p| p.parent())
        .expect("Failed to find workspace root");

    let output_path = workspace_root
        .join("contracts")
        .join("src")
        .join("libraries")
        .join("ProtocolVersion.sol");

    // Only write if content has changed to avoid invalidating caches
    let should_write = if output_path.exists() {
        match fs::read_to_string(&output_path) {
            Ok(existing) => existing != solidity_content,
            Err(_) => true,
        }
    } else {
        true
    };

    if should_write {
        fs::write(&output_path, solidity_content).unwrap_or_else(|e| {
            eprintln!(
                "Warning: Failed to write Solidity version file to {:?}: {}",
                output_path, e
            );
        });
        println!("cargo:warning=Generated Solidity version file: {:?}", output_path);
    }
}