use std::path::Path;
use anyhow::{Context, Result};
use super::{BuildConfig, BuildResult, CompileFlags, ProgramBuilder, RiscvTarget};
use crate::{
sanitize_nested_cargo_env,
toolchain::{GnuRiscvToolchain, RialoRustToolchain, Toolchain},
};
fn link_elf_to_polkavm(elf_bytes: &[u8]) -> Result<Vec<u8>> {
let mut config = polkavm_linker::Config::default();
config.set_strip(true);
config.set_optimize(true);
config.set_min_stack_size(0x40000);
config.set_opt_level(polkavm_linker::OptLevel::O1);
polkavm_linker::program_from_elf(config, elf_bytes)
.map_err(|e| anyhow::anyhow!("PolkaVM linking failed: {}", e))
}
pub struct RiscvBuilder {
gnu_toolchain: GnuRiscvToolchain,
rust_toolchain: Option<RialoRustToolchain>,
target: RiscvTarget,
compile_flags: Option<CompileFlags>,
}
impl RiscvBuilder {
pub fn new(target: RiscvTarget) -> Result<Self> {
let gnu_toolchain = GnuRiscvToolchain::new()?;
let rust_toolchain = if target.requires_rialo_toolchain() {
Some(RialoRustToolchain::new()?)
} else {
None
};
Ok(Self {
gnu_toolchain,
rust_toolchain,
target,
compile_flags: None,
})
}
pub fn with_version(version: &str, target: RiscvTarget) -> Result<Self> {
let gnu_toolchain = GnuRiscvToolchain::with_version(version)?;
let rust_toolchain = if target.requires_rialo_toolchain() {
Some(RialoRustToolchain::with_version(version)?)
} else {
None
};
Ok(Self {
gnu_toolchain,
rust_toolchain,
target,
compile_flags: None,
})
}
pub fn with_compile_flags(mut self, flags: CompileFlags) -> Self {
self.compile_flags = Some(flags);
self
}
fn has_c_source_files(dir: &Path) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) {
if ext == "c" || ext == "S" {
return true;
}
}
}
}
false
}
fn detect_program_type(&self, dir: &Path) -> Result<ProgramType> {
let cargo_toml = dir.join("Cargo.toml");
let has_cargo_toml = cargo_toml.exists();
let has_c_files = Self::has_c_source_files(dir);
let src_dir = dir.join("src");
let has_c_in_src = src_dir.exists() && Self::has_c_source_files(&src_dir);
if has_cargo_toml {
Ok(ProgramType::Rust)
} else if has_c_files || has_c_in_src {
Ok(ProgramType::C)
} else {
Err(anyhow::anyhow!(
"Could not detect program type. Directory must contain either Cargo.toml (Rust) or .c/.S files (C)"
))
}
}
fn build_c_program(&self, config: &BuildConfig) -> Result<BuildResult> {
println!("Building C program for RISC-V...");
let program_name = config
.program_path
.file_name()
.context("Failed to get program directory name")?
.to_str()
.context("Failed to convert directory name to string")?;
let output_dir = config.output_dir.join(format!("{program_name}-riscv"));
std::fs::create_dir_all(&output_dir).with_context(|| {
format!("Failed to create output directory {}", output_dir.display())
})?;
let mut source_files = Vec::new();
let src_dir = config.program_path.join("src");
let search_dirs = if src_dir.exists() {
vec![&config.program_path, &src_dir]
} else {
vec![&config.program_path]
};
for dir in search_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext == "c" || ext == "S" {
source_files.push(path);
}
}
}
}
}
if source_files.is_empty() {
return Err(anyhow::anyhow!(
"No C source files (.c or .S) found in {}",
config.program_path.display()
));
}
println!("Found {} source files", source_files.len());
let bin_path = self.gnu_toolchain.get_bin_path()?;
let gcc_path = bin_path.join(format!("{}-gcc", self.gnu_toolchain.get_tool_prefix()));
let output_elf = output_dir.join(format!("{program_name}.elf"));
let mut command = std::process::Command::new(&gcc_path);
if let Some(flags) = &self.compile_flags {
if let Some(march) = &flags.march {
command.args(["-march", march]);
} else {
command.args(["-march", self.target.as_march()]);
}
if let Some(mabi) = &flags.mabi {
command.args(["-mabi", mabi]);
} else {
command.args(["-mabi", self.target.as_mabi()]);
}
command
.arg("-static")
.arg("-nostdlib")
.arg("-nostartfiles")
.arg("-fno-common")
.arg("-fvisibility=hidden");
if let Some(opt) = &flags.optimization {
command.arg(format!("-{}", opt));
} else {
command.arg("-O2");
}
for flag in &flags.additional_flags {
command.arg(flag);
}
} else {
command
.args(["-march", self.target.as_march()])
.args(["-mabi", self.target.as_mabi()])
.arg("-static")
.arg("-nostdlib")
.arg("-nostartfiles")
.arg("-fno-common")
.arg("-fvisibility=hidden")
.arg("-O2");
}
command.arg("-o").arg(&output_elf);
for source in &source_files {
command.arg(source);
}
let linker_script = config.program_path.join("link.ld");
if linker_script.exists() {
command.arg("-T").arg(&linker_script);
}
println!("Compiling with: {:?}", command);
let status = command.status().with_context(|| {
format!(
"Failed to execute gcc at {}.\n\
\n\
Is the GNU RISC-V toolchain installed?\n\
Run: rialo-build toolchain install gnu-riscv\n\
\n\
Note: If you're building Rust programs, you don't need the GNU toolchain.\n\
Use auto-detection instead: rialo-build --program-path /path/to/program",
gcc_path.display()
)
})?;
if !status.success() {
return Err(anyhow::anyhow!(
"Compilation failed. Check the error output above for details.\n\
\n\
Common issues:\n\
- Missing or incorrect C source code syntax\n\
- Incompatible compiler flags for target architecture\n\
- Missing linker script (link.ld)\n\
\n\
For Rust programs, use auto-detection instead:\n\
rialo-build --program-path /path/to/program"
));
}
println!("✅ C program compiled successfully");
if self.target.requires_rialo_toolchain() {
println!("Converting ELF to PolkaVM blob...");
let elf_bytes = std::fs::read(&output_elf)
.context("Failed to read ELF file for PolkaVM linking")?;
let polkavm_bytes =
link_elf_to_polkavm(&elf_bytes).context("Failed to link ELF to PolkaVM blob")?;
let artifact_name = program_name.replace('-', "_");
let polkavm_path = output_dir.join(format!("{artifact_name}.polkavm"));
std::fs::write(&polkavm_path, &polkavm_bytes).with_context(|| {
format!("Failed to write PolkaVM blob to {}", polkavm_path.display())
})?;
println!("✅ PolkaVM blob created: {}", polkavm_path.display());
Ok(BuildResult {
package_name: program_name.to_string(),
output_dir: output_dir.clone(),
program_binary: polkavm_path,
program_keypair: None,
})
} else {
Ok(BuildResult {
package_name: program_name.to_string(),
output_dir: output_dir.clone(),
program_binary: output_elf,
program_keypair: None,
})
}
}
fn build_rust_program(&self, config: &BuildConfig) -> Result<BuildResult> {
println!("Building Rust program for RISC-V...");
let use_rialo_toolchain = self.target.requires_rialo_toolchain();
if use_rialo_toolchain {
let rust_toolchain = self.rust_toolchain.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Rialo Rust toolchain not initialized for target {}",
self.target.as_target_triple()
)
})?;
let using_direct_cargo = std::env::var("RIALO_BUILD_CARGO_PATH")
.map(|p| !p.is_empty())
.unwrap_or(false);
if !using_direct_cargo {
rust_toolchain.ensure_installed_atomically()?;
}
self.build_rust_with_rialo_toolchain(config)
} else {
self.build_rust_with_gnu_toolchain(config)
}
}
fn build_rust_with_rialo_toolchain(&self, config: &BuildConfig) -> Result<BuildResult> {
let direct_cargo = std::env::var("RIALO_BUILD_CARGO_PATH")
.ok()
.filter(|p| !p.is_empty());
let use_direct = direct_cargo.is_some();
let toolchain_label = if use_direct {
format!("direct ({})", direct_cargo.as_deref().unwrap_or_default())
} else {
"cargo +rialo".to_string()
};
println!("Using Rialo custom Rust toolchain ({})", toolchain_label);
println!("Target: {}", self.target.as_target_triple());
let package_name = get_package_name(&config.program_path)?;
let output_dir = config.output_dir.join(format!("{package_name}-riscv"));
std::fs::create_dir_all(&output_dir).with_context(|| {
format!("Failed to create output directory {}", output_dir.display())
})?;
let absolute_target_dir = if config.target_dir.is_absolute() {
config.target_dir.clone()
} else {
std::env::current_dir()
.context("Failed to get current directory")?
.join(&config.target_dir)
};
let mut command = if let Some(ref cargo_path) = direct_cargo {
let abs_cargo = if std::path::Path::new(cargo_path).is_absolute() {
std::path::PathBuf::from(cargo_path)
} else {
std::env::current_dir()
.context("Failed to get current working directory")?
.join(cargo_path)
};
std::process::Command::new(abs_cargo)
} else {
let mut cmd = std::process::Command::new("cargo");
cmd.arg("+rialo");
cmd
};
command.current_dir(&config.program_path);
let target_triple = self.target.as_target_triple();
command
.arg("build")
.arg("--release")
.arg("--target")
.arg(target_triple)
.arg("--target-dir")
.arg(&absolute_target_dir);
if has_implementation_feature(&config.program_path)? {
command
.arg("--features")
.arg(rialo_venus_dsl::generate::constants::IMPLEMENTATION_FEATURE);
}
sanitize_nested_cargo_env(&mut command);
command.env("RUSTUP_TOOLCHAIN", "rialo");
let cargo_display = if use_direct {
direct_cargo.as_deref().unwrap_or("cargo")
} else {
"cargo +rialo"
};
println!(
"Building with: {} build --release --target {}",
cargo_display, target_triple
);
let status = command
.status()
.context("Failed to execute cargo +rialo build")?;
if !status.success() {
return Err(anyhow::anyhow!("Cargo build failed"));
}
let target_dir = absolute_target_dir.join(target_triple).join("release");
let binary_name = package_name.replace('-', "_");
let source_elf = target_dir.join(&binary_name).with_extension("elf");
if !source_elf.exists() {
return Err(anyhow::anyhow!(
"Built ELF not found at {}. Expected cargo to produce a binary.",
source_elf.display()
));
}
println!("✅ Rust program compiled successfully with Rialo toolchain");
if self.target.requires_rialo_toolchain() {
println!("Converting ELF to PolkaVM blob...");
let elf_bytes = std::fs::read(&source_elf)
.context("Failed to read ELF file for PolkaVM linking")?;
let polkavm_bytes =
link_elf_to_polkavm(&elf_bytes).context("Failed to link ELF to PolkaVM blob")?;
let polkavm_path = output_dir.join(format!("{binary_name}.polkavm"));
std::fs::write(&polkavm_path, &polkavm_bytes).with_context(|| {
format!("Failed to write PolkaVM blob to {}", polkavm_path.display())
})?;
println!("✅ PolkaVM blob created: {}", polkavm_path.display());
Ok(BuildResult {
package_name,
output_dir: output_dir.clone(),
program_binary: polkavm_path,
program_keypair: None,
})
} else {
let output_elf = output_dir.join(format!("{binary_name}.elf"));
std::fs::copy(&source_elf, &output_elf).with_context(|| {
format!(
"Failed to copy {} to {}",
source_elf.display(),
output_elf.display()
)
})?;
Ok(BuildResult {
package_name,
output_dir: output_dir.clone(),
program_binary: output_elf,
program_keypair: None,
})
}
}
fn build_rust_with_gnu_toolchain(&self, config: &BuildConfig) -> Result<BuildResult> {
println!("Using GNU RISC-V toolchain with cargo");
let package_name = get_package_name(&config.program_path)?;
let output_dir = config.output_dir.join(format!("{package_name}-riscv"));
std::fs::create_dir_all(&output_dir).with_context(|| {
format!("Failed to create output directory {}", output_dir.display())
})?;
let absolute_target_dir = if config.target_dir.is_absolute() {
config.target_dir.clone()
} else {
std::env::current_dir()
.context("Failed to get current directory")?
.join(&config.target_dir)
};
let bin_path = self.gnu_toolchain.get_bin_path()?;
let tool_prefix = self.gnu_toolchain.get_tool_prefix();
let mut command = std::process::Command::new("cargo");
command.current_dir(&config.program_path);
let target_triple = self.target.as_target_triple();
let env_prefix = target_triple.replace('-', "_").to_uppercase();
command
.env(
format!("CC_{}", env_prefix),
bin_path.join(format!("{}-gcc", tool_prefix)),
)
.env(
format!("AR_{}", env_prefix),
bin_path.join(format!("{}-ar", tool_prefix)),
)
.env("CARGO_BUILD_TARGET", target_triple);
command
.arg("build")
.arg("--release")
.arg("--target")
.arg(target_triple)
.arg("--target-dir")
.arg(&absolute_target_dir);
command.arg("-Z").arg("build-std=core,alloc");
println!("Building with cargo: {:?}", command);
let status = command.status().context("Failed to execute cargo build")?;
if !status.success() {
return Err(anyhow::anyhow!("Cargo build failed"));
}
let target_dir = absolute_target_dir.join(target_triple).join("release");
let binary_name = package_name.replace('-', "_");
let source_elf = target_dir.join(&binary_name).with_extension("elf");
if !source_elf.exists() {
return Err(anyhow::anyhow!(
"Built ELF not found at {}. Expected cargo to produce a binary.",
source_elf.display()
));
}
println!("✅ Rust program compiled successfully with GNU toolchain");
if self.target.requires_rialo_toolchain() {
println!("Converting ELF to PolkaVM blob...");
let elf_bytes = std::fs::read(&source_elf)
.context("Failed to read ELF file for PolkaVM linking")?;
let polkavm_bytes =
link_elf_to_polkavm(&elf_bytes).context("Failed to link ELF to PolkaVM blob")?;
let polkavm_path = output_dir.join(format!("{binary_name}.polkavm"));
std::fs::write(&polkavm_path, &polkavm_bytes).with_context(|| {
format!("Failed to write PolkaVM blob to {}", polkavm_path.display())
})?;
println!("✅ PolkaVM blob created: {}", polkavm_path.display());
Ok(BuildResult {
package_name,
output_dir: output_dir.clone(),
program_binary: polkavm_path,
program_keypair: None,
})
} else {
let output_elf = output_dir.join(format!("{binary_name}.elf"));
std::fs::copy(&source_elf, &output_elf).with_context(|| {
format!(
"Failed to copy {} to {}",
source_elf.display(),
output_elf.display()
)
})?;
Ok(BuildResult {
package_name,
output_dir: output_dir.clone(),
program_binary: output_elf,
program_keypair: None,
})
}
}
}
impl ProgramBuilder for RiscvBuilder {
fn validate(&self) -> Result<()> {
if !self.target.requires_rialo_toolchain() {
self.gnu_toolchain.validate()?;
}
if let Some(ref rust_toolchain) = self.rust_toolchain {
if rust_toolchain.is_installed()? {
rust_toolchain.validate()?;
} else {
println!("Note: Rialo Rust toolchain not installed. It will be automatically installed when building Rust programs.");
}
}
Ok(())
}
fn build(&self, config: &BuildConfig) -> Result<BuildResult> {
crate::validate_program_path(&config.program_path)?;
let program_type = self.detect_program_type(&config.program_path)?;
match program_type {
ProgramType::C => self.build_c_program(config),
ProgramType::Rust => self.build_rust_program(config),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProgramType {
C,
Rust,
}
fn get_package_name(dir: &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(),
})
}
fn has_implementation_feature(dir: &Path) -> Result<bool> {
let cargo_toml_path = dir.join("Cargo.toml");
if !cargo_toml_path.exists() {
anyhow::bail!("Cargo.toml not found at: {}", cargo_toml_path.display());
}
let manifest = cargo_toml::Manifest::from_path(cargo_toml_path)?;
Ok(manifest
.features
.contains_key(rialo_venus_dsl::generate::constants::IMPLEMENTATION_FEATURE))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_riscv_target_conversions() {
assert_eq!(
RiscvTarget::Rv64gc.as_target_triple(),
"riscv64gc-unknown-none-elf"
);
assert_eq!(RiscvTarget::Rv64gc.as_march(), "rv64gc");
assert_eq!(RiscvTarget::Rv64gc.as_mabi(), "lp64d");
assert_eq!(
RiscvTarget::Rv32i.as_target_triple(),
"riscv32i-unknown-none-elf"
);
assert_eq!(RiscvTarget::Rv32i.as_march(), "rv32i");
assert_eq!(RiscvTarget::Rv32i.as_mabi(), "ilp32");
}
#[test]
fn test_default_target() {
assert_eq!(RiscvTarget::default(), RiscvTarget::RialoCustom);
}
}