rialo-build-lib 0.11.2

Shared library for Rialo program building logic
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{Context, Result};

use super::{BuildConfig, BuildResult, ProgramBuilder};

/// Solana-based builder for Rialo programs
pub struct SolanaBuilder {}

impl SolanaBuilder {
    /// Create a new Solana builder
    pub fn new() -> Self {
        Self {}
    }
}

impl Default for SolanaBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl ProgramBuilder for SolanaBuilder {
    fn validate(&self) -> Result<()> {
        // Check if Solana CLI is installed
        if !check_solana_installed() {
            return Err(anyhow::anyhow!(
                "Solana CLI not found. Please install Solana CLI to enable program compilation."
            ));
        }
        Ok(())
    }

    fn build(&self, config: &BuildConfig) -> Result<BuildResult> {
        // Validate program path exists
        crate::validate_program_path(&config.program_path)?;

        // Get the package name for logging
        let package_name = match get_package_name(&config.program_path) {
            Ok(name) => name,
            Err(e) => {
                println!("cargo:warning=Failed to get package name: {e}");
                "<Unknown>".into()
            }
        };

        // Create program-specific output directory
        let dir_name = config
            .program_path
            .file_name()
            .context("Failed to get directory name")?
            .to_str()
            .context("Failed to convert directory name to string")?;

        // Construct the output path
        let out_dir = config.output_dir.join(format!("{dir_name}-program"));

        // Create the directory if it doesn't exist
        std::fs::create_dir_all(&out_dir)
            .with_context(|| format!("Failed to create output directory {}", out_dir.display()))?;

        // Canonicalize the path
        let out_dir = out_dir.canonicalize().with_context(|| {
            format!(
                "Failed to canonicalize output directory {}",
                out_dir.display()
            )
        })?;

        // Build the program directly to the output directory
        let mut command = std::process::Command::new("cargo");
        command
            .current_dir(&config.program_path)
            .arg("build-sbf")
            .arg("--sbf-out-dir")
            .arg(&out_dir);

        // Check if the implementation feature is present in the Cargo.toml
        if has_implementation_feature(&config.program_path)? {
            command
                .arg("--features")
                .arg(rialo_venus_dsl::generate::constants::IMPLEMENTATION_FEATURE);
        }

        command
            .arg("--")
            .arg("--target-dir")
            .arg(&config.target_dir);

        let build_status = command
            .status()
            .with_context(|| "Failed to execute cargo build-sbf")?;

        if !build_status.success() {
            return Err(anyhow::anyhow!("cargo build-sbf execution failed"));
        }

        // Clean up temporary artifacts in the program directory
        cleanup_temps(&config.program_path)?;

        Ok(BuildResult {
            package_name: package_name.clone(),
            output_dir: out_dir.clone(),
            program_binary: out_dir.join(format!("{package_name}.so")),
            program_keypair: Some(out_dir.join(format!("{package_name}-keypair.json"))),
        })
    }
}

/// Check if Solana CLI is installed
fn check_solana_installed() -> bool {
    which::which("solana").is_ok()
}

/// Check if the implementation feature is present in the Cargo.toml
fn has_implementation_feature(dir: &std::path::Path) -> Result<bool> {
    let cargo_toml_path = dir.join("Cargo.toml");
    if !cargo_toml_path.exists() {
        return Err(anyhow::anyhow!(
            "Cargo.toml not found at: {}",
            cargo_toml_path.display()
        ));
    }

    let manifest = cargo_toml::Manifest::from_path(cargo_toml_path)?;
    let features = manifest.features;
    let has_implementation_feature =
        features.contains_key(rialo_venus_dsl::generate::constants::IMPLEMENTATION_FEATURE);
    Ok(has_implementation_feature)
}

/// Get the package name from Cargo.toml in the specified directory
fn get_package_name(dir: &std::path::Path) -> Result<String> {
    let dir = dir
        .canonicalize()
        .with_context(|| format!("Failed to canonicalize directory {}", dir.display()))?;
    let manifest_path = dir.join("Cargo.toml");

    let metadata = cargo_metadata::MetadataCommand::new()
        .manifest_path(&manifest_path)
        .no_deps()
        .exec()
        .with_context(|| format!("Failed to parse Cargo.toml at {}", manifest_path.display()))?;

    Ok(match metadata.workspace_default_packages().first() {
        Some(p) => p.name.clone(),
        None => "<unknown>".into(),
    })
}

/// Clean up temporary build files and artifacts
fn cleanup_temps(dir: &std::path::Path) -> Result<()> {
    // Clean up temporary build directories
    let paths_to_clean = ["target", "elf"];

    for path_name in paths_to_clean.iter() {
        let path = dir.join(path_name);
        if path.exists() {
            if let Err(e) = std::fs::remove_dir_all(&path) {
                // Log warning but don't fail the build
                eprintln!("Warning: Failed to remove {}: {}", path.display(), e);
            }
        }
    }

    // Clean up any .so and .json files in the root directory
    cleanup_files(dir, &[".so", ".json"])?;
    Ok(())
}

/// Clean up files with specific extensions in a directory
fn cleanup_files(dir: &std::path::Path, extensions: &[&str]) -> Result<()> {
    if !dir.exists() {
        return Ok(());
    }

    match std::fs::read_dir(dir) {
        Ok(entries) => {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() {
                    if let Some(ext) = path.extension() {
                        let ext_str = ext.to_string_lossy();
                        if extensions.iter().any(|e| *e == format!(".{ext_str}")) {
                            if let Err(e) = std::fs::remove_file(&path) {
                                eprintln!("Warning: Failed to remove {}: {}", path.display(), e);
                            }
                        }
                    }
                }
            }
        }
        Err(e) => {
            eprintln!("Warning: Failed to read directory {}: {}", dir.display(), e);
        }
    }
    Ok(())
}