use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result};
pub mod build_script;
pub mod compilation;
pub mod config;
pub mod detection;
mod riscv_builder;
mod solana_builder;
pub mod toolchain;
pub mod venus;
pub use config::{BuildFileConfig, BuildType, CompileFlags, RiscvConfig, SourceType};
pub use detection::{detect_program_type, ProgramType};
pub use riscv_builder::RiscvBuilder;
pub use solana_builder::SolanaBuilder;
pub use toolchain::{
BuildSystemConfig, DownloadSource, GnuRiscvToolchain, RialoRustToolchain, RustSourceBuilder,
S3StorageBackend, SourceBuildConfig, SourceBuildable, Toolchain, ToolchainConfig,
ToolchainType,
};
pub use venus::{build_venus_workflow, is_venus_workflow};
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub program_path: PathBuf,
pub output_dir: PathBuf,
pub target_dir: PathBuf,
}
pub fn validate_program_path(path: &std::path::Path) -> Result<()> {
if !path.exists() {
return Err(anyhow::anyhow!(
"Program path does not exist: {}",
path.display()
));
}
if !path.is_dir() {
return Err(anyhow::anyhow!(
"Program path is not a directory: {}",
path.display()
));
}
Ok(())
}
const NESTED_CARGO_ENV_VARS_TO_REMOVE: &[&str] = &[
"CARGO",
"CARGO_MAKEFLAGS",
"CARGO_BUILD_RUSTFLAGS",
"CARGO_ENCODED_RUSTFLAGS",
"RUSTC",
"RUSTDOC",
"RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"RUSTUP_TOOLCHAIN",
];
pub fn sanitize_nested_cargo_env(command: &mut Command) {
for key in NESTED_CARGO_ENV_VARS_TO_REMOVE {
command.env_remove(key);
}
}
pub fn workspace_root_for_program(program_path: &Path) -> Result<PathBuf> {
let program_path = resolve_program_directory(program_path)?;
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(program_path.join("Cargo.toml"))
.no_deps()
.exec()
.with_context(|| {
format!(
"Failed to load Cargo metadata for {}",
program_path.display()
)
})?;
Ok(metadata.workspace_root.as_std_path().to_path_buf())
}
pub fn resolve_target_dir_for_program(
program_path: &Path,
target_dir_override: Option<&Path>,
) -> Result<PathBuf> {
let program_path = resolve_program_directory(program_path)?;
let workspace_root = workspace_root_for_program(&program_path)?;
resolve_target_dir_with_inputs(
&workspace_root,
target_dir_override,
env::var_os("CARGO_TARGET_DIR"),
)
}
fn resolve_target_dir_with_inputs(
workspace_root: &Path,
target_dir_override: Option<&Path>,
inherited_target_dir: Option<OsString>,
) -> Result<PathBuf> {
if let Some(target_dir_override) = target_dir_override {
return resolve_user_path(target_dir_override);
}
if let Some(inherited_target_dir) = inherited_target_dir.filter(|value| !value.is_empty()) {
let inherited_target_dir = PathBuf::from(inherited_target_dir);
if inherited_target_dir.is_absolute() {
return Ok(inherited_target_dir);
}
return Ok(workspace_root.join(inherited_target_dir));
}
Ok(workspace_root.join("target"))
}
fn resolve_program_directory(program_path: &Path) -> Result<PathBuf> {
let program_path = resolve_user_path(program_path)?;
validate_program_path(&program_path)?;
program_path
.canonicalize()
.with_context(|| format!("Failed to canonicalize {}", program_path.display()))
}
pub(crate) fn resolve_user_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
Ok(env::current_dir()
.context("Failed to determine current working directory")?
.join(path))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RiscvTarget {
Rv32i,
Rv32im,
Rv64gc,
#[default]
RialoCustom,
}
impl RiscvTarget {
pub fn as_target_triple(&self) -> &str {
match self {
RiscvTarget::Rv32i => "riscv32i-unknown-none-elf",
RiscvTarget::Rv32im => "riscv32im-unknown-none-elf",
RiscvTarget::Rv64gc => "riscv64gc-unknown-none-elf",
RiscvTarget::RialoCustom => "riscv64emac-solana-solana",
}
}
pub fn as_march(&self) -> &str {
match self {
RiscvTarget::Rv32i => "rv32i",
RiscvTarget::Rv32im => "rv32im",
RiscvTarget::Rv64gc => "rv64gc",
RiscvTarget::RialoCustom => "rv64gc", }
}
pub fn as_mabi(&self) -> &str {
match self {
RiscvTarget::Rv32i | RiscvTarget::Rv32im => "ilp32",
RiscvTarget::Rv64gc | RiscvTarget::RialoCustom => "lp64d",
}
}
pub fn requires_rialo_toolchain(&self) -> bool {
matches!(self, RiscvTarget::RialoCustom)
}
}
#[derive(Debug, Clone)]
pub enum BuilderConfig {
Solana {},
Riscv {
toolchain_version: Option<String>,
target: RiscvTarget,
},
}
#[derive(Debug, serde::Serialize)]
pub struct BuildResult {
pub package_name: String,
pub output_dir: PathBuf,
pub program_binary: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub program_keypair: Option<PathBuf>,
}
pub trait ProgramBuilder {
fn validate(&self) -> Result<()>;
fn build(&self, config: &BuildConfig) -> Result<BuildResult>;
}
pub fn create_builder(builder_config: &BuilderConfig) -> Result<Box<dyn ProgramBuilder>> {
match builder_config {
BuilderConfig::Solana {} => Ok(Box::new(SolanaBuilder::default())),
BuilderConfig::Riscv {
toolchain_version,
target,
} => {
let builder = if let Some(version) = toolchain_version {
RiscvBuilder::with_version(version, *target)?
} else {
RiscvBuilder::new(*target)?
};
Ok(Box::new(builder))
}
}
}
pub fn build_program(config: &BuildConfig) -> Result<BuildResult> {
let builder = create_builder(&BuilderConfig::Solana {})?;
builder.validate()?;
builder.build(config)
}
pub fn auto_detect_builder(program_path: &std::path::Path) -> Result<BuilderConfig> {
let file_config = BuildFileConfig::from_directory(program_path)?;
if let Some(config) = &file_config {
if let Some(build_type) = config.build_type {
match build_type {
BuildType::Solana => return Ok(BuilderConfig::Solana {}),
BuildType::Riscv => {
let target = config
.riscv
.as_ref()
.and_then(|r| r.target)
.unwrap_or_default();
let toolchain_version = config
.riscv
.as_ref()
.and_then(|r| r.toolchain_version.clone());
return Ok(BuilderConfig::Riscv {
toolchain_version,
target,
});
}
BuildType::Auto => {
}
}
}
}
let program_type = detect_program_type(program_path)?;
match program_type {
ProgramType::Solana => Ok(BuilderConfig::Solana {}),
ProgramType::RiscvC | ProgramType::RiscvRust => {
let target = file_config
.as_ref()
.and_then(|c| c.riscv.as_ref())
.and_then(|r| r.target)
.unwrap_or_default();
let toolchain_version = file_config
.as_ref()
.and_then(|c| c.riscv.as_ref())
.and_then(|r| r.toolchain_version.clone());
Ok(BuilderConfig::Riscv {
toolchain_version,
target,
})
}
}
}
pub fn build_program_auto(config: &BuildConfig) -> Result<BuildResult> {
let builder_config = auto_detect_builder(&config.program_path)?;
let builder = create_builder(&builder_config)?;
builder.validate()?;
builder.build(config)
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, ffi::OsString, path::PathBuf, process::Command};
use super::{
resolve_target_dir_with_inputs, sanitize_nested_cargo_env, workspace_root_for_program,
};
#[test]
fn resolve_target_dir_prefers_explicit_override() {
let workspace = create_workspace().unwrap();
let explicit_target_dir = workspace.root.join("explicit-target");
let target_dir = resolve_target_dir_with_inputs(
&workspace.root,
Some(explicit_target_dir.as_path()),
Some(OsString::from("ignored-by-override")),
)
.unwrap();
assert_eq!(target_dir, explicit_target_dir);
}
#[test]
fn resolve_target_dir_honors_absolute_inherited_target_dir() {
let workspace = create_workspace().unwrap();
let absolute_target_dir = workspace.root.join("absolute-target");
let target_dir = resolve_target_dir_with_inputs(
&workspace.root,
None,
Some(absolute_target_dir.clone().into_os_string()),
)
.unwrap();
assert_eq!(target_dir, absolute_target_dir);
}
#[test]
fn resolve_target_dir_normalizes_relative_inherited_target_dir_against_workspace_root() {
let workspace = create_workspace().unwrap();
let target_dir = resolve_target_dir_with_inputs(
&workspace.root,
None,
Some(OsString::from("target-rel")),
)
.unwrap();
assert_eq!(target_dir, workspace.root.join("target-rel"));
}
#[test]
fn resolve_target_dir_falls_back_to_workspace_target_directory() {
let workspace = create_workspace().unwrap();
let target_dir = resolve_target_dir_with_inputs(&workspace.root, None, None).unwrap();
assert_eq!(target_dir, workspace.root.join("target"));
}
#[test]
fn workspace_root_for_program_uses_cargo_metadata() {
let workspace = create_workspace().unwrap();
let workspace_root = workspace_root_for_program(&workspace.program_dir).unwrap();
assert_eq!(workspace_root, workspace.root.canonicalize().unwrap());
}
#[test]
fn sanitize_nested_cargo_env_removes_only_problematic_vars() {
let mut command = Command::new("cargo");
command.env("HOME", "/tmp/rialo-home");
command.env("RUSTC", "bad-rustc");
command.env("RUSTUP_TOOLCHAIN", "bad-toolchain");
command.env("CARGO_MAKEFLAGS", "bad-jobserver");
sanitize_nested_cargo_env(&mut command);
let envs: BTreeMap<OsString, Option<OsString>> = command
.get_envs()
.map(|(key, value)| (key.to_os_string(), value.map(|value| value.to_os_string())))
.collect();
assert_eq!(
envs.get(&OsString::from("HOME")),
Some(&Some(OsString::from("/tmp/rialo-home")))
);
assert_eq!(envs.get(&OsString::from("RUSTC")), Some(&None));
assert_eq!(envs.get(&OsString::from("RUSTUP_TOOLCHAIN")), Some(&None));
assert_eq!(envs.get(&OsString::from("CARGO_MAKEFLAGS")), Some(&None));
}
fn create_workspace() -> anyhow::Result<TestWorkspace> {
let root = tempfile::tempdir()?;
let root_path = root.path().to_path_buf();
let program_dir = root_path.join("program");
let src_dir = program_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
std::fs::write(
root_path.join("Cargo.toml"),
"[workspace]\nmembers = [\"program\"]\nresolver = \"2\"\n",
)?;
std::fs::write(
program_dir.join("Cargo.toml"),
"[package]\nname = \"example-program\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)?;
std::fs::write(src_dir.join("lib.rs"), "pub fn example() {}\n")?;
Ok(TestWorkspace {
_root: root,
root: root_path,
program_dir,
})
}
struct TestWorkspace {
_root: tempfile::TempDir,
root: PathBuf,
program_dir: PathBuf,
}
}