rialo-build-lib 0.10.1

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

//! Build configuration file support

use std::path::Path;

use anyhow::{Context, Result};

use crate::RiscvTarget;

/// Build configuration from rialo-build.toml
#[derive(Debug, Clone, Default)]
pub struct BuildFileConfig {
    /// Build type (solana, riscv, or auto)
    pub build_type: Option<BuildType>,
    /// RISC-V specific configuration
    pub riscv: Option<RiscvConfig>,
}

/// Build type specified in config
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildType {
    /// Solana program
    Solana,
    /// RISC-V program
    Riscv,
    /// Auto-detect
    Auto,
}

/// RISC-V build configuration
#[derive(Debug, Clone)]
pub struct RiscvConfig {
    /// Target architecture
    pub target: Option<RiscvTarget>,
    /// Toolchain version
    pub toolchain_version: Option<String>,
    /// Source type (c or rust)
    pub source_type: Option<SourceType>,
    /// Compile flags
    pub compile_flags: Option<CompileFlags>,
}

/// Source type for RISC-V programs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceType {
    /// C program
    C,
    /// Rust program
    Rust,
}

/// Compile flags for C programs
#[derive(Debug, Clone)]
pub struct CompileFlags {
    /// Optimization level (O0, O1, O2, O3, Os, Oz)
    pub optimization: Option<String>,
    /// Architecture (march flag)
    pub march: Option<String>,
    /// ABI (mabi flag)
    pub mabi: Option<String>,
    /// Additional flags
    pub additional_flags: Vec<String>,
}

impl BuildFileConfig {
    /// Load configuration from a file
    pub fn from_file(path: &Path) -> Result<Self> {
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read config file {}", path.display()))?;

        Self::parse_str(&content)
    }

    /// Parse configuration from a string
    pub fn parse_str(content: &str) -> Result<Self> {
        let toml_value: toml::Value =
            toml::from_str(content).context("Failed to parse TOML configuration")?;

        let mut config = BuildFileConfig::default();

        // Parse [build] section
        if let Some(build_section) = toml_value.get("build") {
            if let Some(build_type) = build_section.get("type") {
                if let Some(type_str) = build_type.as_str() {
                    config.build_type = Some(parse_build_type(type_str)?);
                }
            }
        }

        // Parse [riscv] section
        if let Some(riscv_section) = toml_value.get("riscv") {
            config.riscv = Some(parse_riscv_config(riscv_section)?);
        }

        Ok(config)
    }

    /// Try to load configuration from a directory
    /// Returns None if no configuration file is found
    pub fn from_directory(dir: &Path) -> Result<Option<Self>> {
        let config_path = dir.join("rialo-build.toml");

        if !config_path.exists() {
            return Ok(None);
        }

        Ok(Some(Self::from_file(&config_path)?))
    }
}

fn parse_build_type(s: &str) -> Result<BuildType> {
    match s.to_lowercase().as_str() {
        "solana" => Ok(BuildType::Solana),
        "riscv" => Ok(BuildType::Riscv),
        "auto" => Ok(BuildType::Auto),
        _ => Err(anyhow::anyhow!(
            "Invalid build type: {}. Must be 'solana', 'riscv', or 'auto'",
            s
        )),
    }
}

fn parse_riscv_config(value: &toml::Value) -> Result<RiscvConfig> {
    let mut config = RiscvConfig {
        target: None,
        toolchain_version: None,
        source_type: None,
        compile_flags: None,
    };

    if let Some(target_str) = value.get("target").and_then(|v| v.as_str()) {
        config.target = Some(parse_riscv_target(target_str)?);
    }

    if let Some(version) = value.get("toolchain_version").and_then(|v| v.as_str()) {
        config.toolchain_version = Some(version.to_string());
    }

    if let Some(source_type_str) = value.get("source_type").and_then(|v| v.as_str()) {
        config.source_type = Some(parse_source_type(source_type_str)?);
    }

    if let Some(compile_flags_section) = value.get("compile_flags") {
        config.compile_flags = Some(parse_compile_flags(compile_flags_section)?);
    }

    Ok(config)
}

fn parse_riscv_target(s: &str) -> Result<RiscvTarget> {
    match s.to_lowercase().as_str() {
        "rv32i" => Ok(RiscvTarget::Rv32i),
        "rv32im" => Ok(RiscvTarget::Rv32im),
        "rv64gc" => Ok(RiscvTarget::Rv64gc),
        _ => Err(anyhow::anyhow!(
            "Invalid RISC-V target: {}. Must be 'rv32i', 'rv32im', or 'rv64gc'",
            s
        )),
    }
}

fn parse_source_type(s: &str) -> Result<SourceType> {
    match s.to_lowercase().as_str() {
        "c" => Ok(SourceType::C),
        "rust" => Ok(SourceType::Rust),
        _ => Err(anyhow::anyhow!(
            "Invalid source type: {}. Must be 'c' or 'rust'",
            s
        )),
    }
}

fn parse_compile_flags(value: &toml::Value) -> Result<CompileFlags> {
    let mut flags = CompileFlags {
        optimization: None,
        march: None,
        mabi: None,
        additional_flags: Vec::new(),
    };

    if let Some(opt) = value.get("optimization").and_then(|v| v.as_str()) {
        flags.optimization = Some(opt.to_string());
    }

    if let Some(march) = value.get("march").and_then(|v| v.as_str()) {
        flags.march = Some(march.to_string());
    }

    if let Some(mabi) = value.get("mabi").and_then(|v| v.as_str()) {
        flags.mabi = Some(mabi.to_string());
    }

    if let Some(additional) = value.get("additional_flags").and_then(|v| v.as_array()) {
        for flag in additional {
            if let Some(flag_str) = flag.as_str() {
                flags.additional_flags.push(flag_str.to_string());
            }
        }
    }

    Ok(flags)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_basic_config() {
        let config_str = r#"
[build]
type = "riscv"

[riscv]
target = "rv64gc"
toolchain_version = "13.2.0"
"#;

        let config = BuildFileConfig::parse_str(config_str).unwrap();
        assert_eq!(config.build_type, Some(BuildType::Riscv));
        assert!(config.riscv.is_some());

        let riscv = config.riscv.unwrap();
        assert_eq!(riscv.target, Some(RiscvTarget::Rv64gc));
        assert_eq!(riscv.toolchain_version, Some("13.2.0".to_string()));
    }

    #[test]
    fn test_parse_compile_flags() {
        let config_str = r#"
[build]
type = "riscv"

[riscv]
target = "rv32im"

[riscv.compile_flags]
optimization = "O2"
march = "rv32im"
mabi = "ilp32"
additional_flags = ["-static", "-nostdlib"]
"#;

        let config = BuildFileConfig::parse_str(config_str).unwrap();
        let riscv = config.riscv.unwrap();
        let flags = riscv.compile_flags.unwrap();

        assert_eq!(flags.optimization, Some("O2".to_string()));
        assert_eq!(flags.march, Some("rv32im".to_string()));
        assert_eq!(flags.mabi, Some("ilp32".to_string()));
        assert_eq!(flags.additional_flags.len(), 2);
    }

    #[test]
    fn test_parse_auto_type() {
        let config_str = r#"
[build]
type = "auto"
"#;

        let config = BuildFileConfig::parse_str(config_str).unwrap();
        assert_eq!(config.build_type, Some(BuildType::Auto));
    }
}