Skip to main content

rialo_build_lib/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    env,
6    ffi::OsString,
7    path::{Path, PathBuf},
8    process::Command,
9};
10
11use anyhow::{Context, Result};
12
13pub mod build_script;
14pub mod compilation;
15pub mod config;
16pub mod detection;
17mod riscv_builder;
18mod solana_builder;
19pub mod toolchain;
20pub mod venus;
21
22pub use config::{BuildFileConfig, BuildType, CompileFlags, RiscvConfig, SourceType};
23pub use detection::{detect_program_type, ProgramType};
24pub use riscv_builder::RiscvBuilder;
25pub use solana_builder::SolanaBuilder;
26pub use toolchain::{
27    BuildSystemConfig, DownloadSource, GnuRiscvToolchain, RialoRustToolchain, RustSourceBuilder,
28    S3StorageBackend, SourceBuildConfig, SourceBuildable, Toolchain, ToolchainConfig,
29    ToolchainType,
30};
31pub use venus::{build_venus_workflow, is_venus_workflow};
32
33/// Configuration for building a Rialo program
34#[derive(Debug, Clone)]
35pub struct BuildConfig {
36    /// Path to the program directory to build
37    pub program_path: PathBuf,
38    /// Output directory for the built artifacts
39    pub output_dir: PathBuf,
40    /// Target directory for cargo build artifacts
41    pub target_dir: PathBuf,
42}
43
44/// Validate that a program path exists and is a directory
45///
46/// Returns an error if the path does not exist or is not a directory.
47/// This is used to provide consistent error messages across all builders.
48pub fn validate_program_path(path: &std::path::Path) -> Result<()> {
49    if !path.exists() {
50        return Err(anyhow::anyhow!(
51            "Program path does not exist: {}",
52            path.display()
53        ));
54    }
55
56    if !path.is_dir() {
57        return Err(anyhow::anyhow!(
58            "Program path is not a directory: {}",
59            path.display()
60        ));
61    }
62
63    Ok(())
64}
65
66const NESTED_CARGO_ENV_VARS_TO_REMOVE: &[&str] = &[
67    "CARGO",
68    "CARGO_MAKEFLAGS",
69    "CARGO_BUILD_RUSTFLAGS",
70    "CARGO_ENCODED_RUSTFLAGS",
71    "RUSTC",
72    "RUSTDOC",
73    "RUSTC_WRAPPER",
74    "RUSTC_WORKSPACE_WRAPPER",
75    "RUSTUP_TOOLCHAIN",
76];
77
78/// Remove environment variables inherited from an outer Cargo invocation that
79/// can corrupt a nested Cargo/rustup toolchain build.
80pub fn sanitize_nested_cargo_env(command: &mut Command) {
81    for key in NESTED_CARGO_ENV_VARS_TO_REMOVE {
82        command.env_remove(key);
83    }
84}
85
86/// Resolve the workspace root for a Cargo program directory.
87pub fn workspace_root_for_program(program_path: &Path) -> Result<PathBuf> {
88    let program_path = resolve_program_directory(program_path)?;
89    let metadata = cargo_metadata::MetadataCommand::new()
90        .manifest_path(program_path.join("Cargo.toml"))
91        .no_deps()
92        .exec()
93        .with_context(|| {
94            format!(
95                "Failed to load Cargo metadata for {}",
96                program_path.display()
97            )
98        })?;
99
100    Ok(metadata.workspace_root.as_std_path().to_path_buf())
101}
102
103/// Resolve the target directory for a Cargo program with shared CLI/workspace
104/// semantics.
105///
106/// Resolution order:
107/// 1. Explicit override
108/// 2. `CARGO_TARGET_DIR`
109/// 3. `<workspace_root>/target`
110///
111/// Explicit overrides keep CLI semantics: relative paths are resolved against
112/// the current working directory. Inherited `CARGO_TARGET_DIR` keeps Cargo
113/// workspace semantics: relative paths are resolved against the program's
114/// workspace root.
115pub fn resolve_target_dir_for_program(
116    program_path: &Path,
117    target_dir_override: Option<&Path>,
118) -> Result<PathBuf> {
119    let program_path = resolve_program_directory(program_path)?;
120    let workspace_root = workspace_root_for_program(&program_path)?;
121
122    resolve_target_dir_with_inputs(
123        &workspace_root,
124        target_dir_override,
125        env::var_os("CARGO_TARGET_DIR"),
126    )
127}
128
129fn resolve_target_dir_with_inputs(
130    workspace_root: &Path,
131    target_dir_override: Option<&Path>,
132    inherited_target_dir: Option<OsString>,
133) -> Result<PathBuf> {
134    if let Some(target_dir_override) = target_dir_override {
135        return resolve_user_path(target_dir_override);
136    }
137
138    if let Some(inherited_target_dir) = inherited_target_dir.filter(|value| !value.is_empty()) {
139        let inherited_target_dir = PathBuf::from(inherited_target_dir);
140        if inherited_target_dir.is_absolute() {
141            return Ok(inherited_target_dir);
142        }
143
144        return Ok(workspace_root.join(inherited_target_dir));
145    }
146
147    Ok(workspace_root.join("target"))
148}
149
150fn resolve_program_directory(program_path: &Path) -> Result<PathBuf> {
151    let program_path = resolve_user_path(program_path)?;
152    validate_program_path(&program_path)?;
153
154    program_path
155        .canonicalize()
156        .with_context(|| format!("Failed to canonicalize {}", program_path.display()))
157}
158
159pub(crate) fn resolve_user_path(path: &Path) -> Result<PathBuf> {
160    if path.is_absolute() {
161        return Ok(path.to_path_buf());
162    }
163
164    Ok(env::current_dir()
165        .context("Failed to determine current working directory")?
166        .join(path))
167}
168
169/// RISC-V target architecture
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum RiscvTarget {
172    /// RV32I base integer instruction set
173    Rv32i,
174    /// RV32IM with integer multiply/divide
175    Rv32im,
176    /// RV64GC general purpose (includes IMAFD + Zicsr + Zifencei + C extensions)
177    Rv64gc,
178    /// Rialo custom target (riscv64emac-solana-solana) with custom Rust toolchain
179    #[default]
180    RialoCustom,
181}
182
183impl RiscvTarget {
184    /// Get the target triple string for cargo
185    pub fn as_target_triple(&self) -> &str {
186        match self {
187            RiscvTarget::Rv32i => "riscv32i-unknown-none-elf",
188            RiscvTarget::Rv32im => "riscv32im-unknown-none-elf",
189            RiscvTarget::Rv64gc => "riscv64gc-unknown-none-elf",
190            RiscvTarget::RialoCustom => "riscv64emac-solana-solana",
191        }
192    }
193
194    /// Get the -march flag for gcc
195    pub fn as_march(&self) -> &str {
196        match self {
197            RiscvTarget::Rv32i => "rv32i",
198            RiscvTarget::Rv32im => "rv32im",
199            RiscvTarget::Rv64gc => "rv64gc",
200            RiscvTarget::RialoCustom => "rv64gc", // Fallback for C compilation
201        }
202    }
203
204    /// Get the -mabi flag for gcc
205    pub fn as_mabi(&self) -> &str {
206        match self {
207            RiscvTarget::Rv32i | RiscvTarget::Rv32im => "ilp32",
208            RiscvTarget::Rv64gc | RiscvTarget::RialoCustom => "lp64d",
209        }
210    }
211
212    /// Check if this target requires the Rialo custom Rust toolchain
213    pub fn requires_rialo_toolchain(&self) -> bool {
214        matches!(self, RiscvTarget::RialoCustom)
215    }
216}
217
218/// Builder-specific configuration
219#[derive(Debug, Clone)]
220pub enum BuilderConfig {
221    /// Configuration for the Solana builder
222    Solana {},
223    /// Configuration for the RISC-V builder
224    Riscv {
225        /// Toolchain version (optional, uses default if not specified)
226        toolchain_version: Option<String>,
227        /// Target architecture
228        target: RiscvTarget,
229    },
230}
231
232/// Result of a build operation
233#[derive(Debug, serde::Serialize)]
234pub struct BuildResult {
235    /// The package name that was built
236    pub package_name: String,
237    /// The output directory where artifacts were placed
238    pub output_dir: PathBuf,
239    /// The program binary file
240    pub program_binary: PathBuf,
241    /// The program keypair file (optional, Solana-specific)
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub program_keypair: Option<PathBuf>,
244}
245
246/// Trait for building Rialo programs
247pub trait ProgramBuilder {
248    /// Validate that the builder can be used (e.g., check dependencies)
249    fn validate(&self) -> Result<()>;
250    /// Build a program using the given configuration
251    fn build(&self, config: &BuildConfig) -> Result<BuildResult>;
252}
253
254/// Create a builder based on the builder config
255pub fn create_builder(builder_config: &BuilderConfig) -> Result<Box<dyn ProgramBuilder>> {
256    match builder_config {
257        BuilderConfig::Solana {} => Ok(Box::new(SolanaBuilder::default())),
258        BuilderConfig::Riscv {
259            toolchain_version,
260            target,
261        } => {
262            let builder = if let Some(version) = toolchain_version {
263                RiscvBuilder::with_version(version, *target)?
264            } else {
265                RiscvBuilder::new(*target)?
266            };
267            Ok(Box::new(builder))
268        }
269    }
270}
271
272/// Build a single Rialo program using the default builder
273pub fn build_program(config: &BuildConfig) -> Result<BuildResult> {
274    let builder = create_builder(&BuilderConfig::Solana {})?;
275    builder.validate()?;
276    builder.build(config)
277}
278
279/// Automatically detect the builder configuration based on the program directory
280///
281/// This function will:
282/// 1. Look for a rialo-build.toml configuration file
283/// 2. If not found or set to "auto", detect the program type
284/// 3. Return the appropriate BuilderConfig
285pub fn auto_detect_builder(program_path: &std::path::Path) -> Result<BuilderConfig> {
286    // Try to load configuration file
287    let file_config = BuildFileConfig::from_directory(program_path)?;
288
289    // If we have a config file with explicit build type, use it
290    if let Some(config) = &file_config {
291        if let Some(build_type) = config.build_type {
292            match build_type {
293                BuildType::Solana => return Ok(BuilderConfig::Solana {}),
294                BuildType::Riscv => {
295                    let target = config
296                        .riscv
297                        .as_ref()
298                        .and_then(|r| r.target)
299                        .unwrap_or_default();
300
301                    let toolchain_version = config
302                        .riscv
303                        .as_ref()
304                        .and_then(|r| r.toolchain_version.clone());
305
306                    return Ok(BuilderConfig::Riscv {
307                        toolchain_version,
308                        target,
309                    });
310                }
311                BuildType::Auto => {
312                    // Continue to auto-detection
313                }
314            }
315        }
316    }
317
318    // Auto-detect based on program contents
319    let program_type = detect_program_type(program_path)?;
320
321    match program_type {
322        ProgramType::Solana => Ok(BuilderConfig::Solana {}),
323        ProgramType::RiscvC | ProgramType::RiscvRust => {
324            // Use config file settings if available, otherwise use defaults
325            let target = file_config
326                .as_ref()
327                .and_then(|c| c.riscv.as_ref())
328                .and_then(|r| r.target)
329                .unwrap_or_default();
330
331            let toolchain_version = file_config
332                .as_ref()
333                .and_then(|c| c.riscv.as_ref())
334                .and_then(|r| r.toolchain_version.clone());
335
336            Ok(BuilderConfig::Riscv {
337                toolchain_version,
338                target,
339            })
340        }
341    }
342}
343
344/// Build a program with automatic builder detection
345pub fn build_program_auto(config: &BuildConfig) -> Result<BuildResult> {
346    let builder_config = auto_detect_builder(&config.program_path)?;
347    let builder = create_builder(&builder_config)?;
348    builder.validate()?;
349    builder.build(config)
350}
351
352#[cfg(test)]
353mod tests {
354    use std::{collections::BTreeMap, ffi::OsString, path::PathBuf, process::Command};
355
356    use super::{
357        resolve_target_dir_with_inputs, sanitize_nested_cargo_env, workspace_root_for_program,
358    };
359
360    #[test]
361    fn resolve_target_dir_prefers_explicit_override() {
362        let workspace = create_workspace().unwrap();
363        let explicit_target_dir = workspace.root.join("explicit-target");
364
365        let target_dir = resolve_target_dir_with_inputs(
366            &workspace.root,
367            Some(explicit_target_dir.as_path()),
368            Some(OsString::from("ignored-by-override")),
369        )
370        .unwrap();
371
372        assert_eq!(target_dir, explicit_target_dir);
373    }
374
375    #[test]
376    fn resolve_target_dir_honors_absolute_inherited_target_dir() {
377        let workspace = create_workspace().unwrap();
378        let absolute_target_dir = workspace.root.join("absolute-target");
379
380        let target_dir = resolve_target_dir_with_inputs(
381            &workspace.root,
382            None,
383            Some(absolute_target_dir.clone().into_os_string()),
384        )
385        .unwrap();
386
387        assert_eq!(target_dir, absolute_target_dir);
388    }
389
390    #[test]
391    fn resolve_target_dir_normalizes_relative_inherited_target_dir_against_workspace_root() {
392        let workspace = create_workspace().unwrap();
393
394        let target_dir = resolve_target_dir_with_inputs(
395            &workspace.root,
396            None,
397            Some(OsString::from("target-rel")),
398        )
399        .unwrap();
400
401        assert_eq!(target_dir, workspace.root.join("target-rel"));
402    }
403
404    #[test]
405    fn resolve_target_dir_falls_back_to_workspace_target_directory() {
406        let workspace = create_workspace().unwrap();
407
408        let target_dir = resolve_target_dir_with_inputs(&workspace.root, None, None).unwrap();
409
410        assert_eq!(target_dir, workspace.root.join("target"));
411    }
412
413    #[test]
414    fn workspace_root_for_program_uses_cargo_metadata() {
415        let workspace = create_workspace().unwrap();
416
417        let workspace_root = workspace_root_for_program(&workspace.program_dir).unwrap();
418
419        assert_eq!(workspace_root, workspace.root.canonicalize().unwrap());
420    }
421
422    #[test]
423    fn sanitize_nested_cargo_env_removes_only_problematic_vars() {
424        let mut command = Command::new("cargo");
425        command.env("HOME", "/tmp/rialo-home");
426        command.env("RUSTC", "bad-rustc");
427        command.env("RUSTUP_TOOLCHAIN", "bad-toolchain");
428        command.env("CARGO_MAKEFLAGS", "bad-jobserver");
429
430        sanitize_nested_cargo_env(&mut command);
431
432        let envs: BTreeMap<OsString, Option<OsString>> = command
433            .get_envs()
434            .map(|(key, value)| (key.to_os_string(), value.map(|value| value.to_os_string())))
435            .collect();
436
437        assert_eq!(
438            envs.get(&OsString::from("HOME")),
439            Some(&Some(OsString::from("/tmp/rialo-home")))
440        );
441        assert_eq!(envs.get(&OsString::from("RUSTC")), Some(&None));
442        assert_eq!(envs.get(&OsString::from("RUSTUP_TOOLCHAIN")), Some(&None));
443        assert_eq!(envs.get(&OsString::from("CARGO_MAKEFLAGS")), Some(&None));
444    }
445
446    fn create_workspace() -> anyhow::Result<TestWorkspace> {
447        let root = tempfile::tempdir()?;
448        let root_path = root.path().to_path_buf();
449        let program_dir = root_path.join("program");
450        let src_dir = program_dir.join("src");
451
452        std::fs::create_dir_all(&src_dir)?;
453        std::fs::write(
454            root_path.join("Cargo.toml"),
455            "[workspace]\nmembers = [\"program\"]\nresolver = \"2\"\n",
456        )?;
457        std::fs::write(
458            program_dir.join("Cargo.toml"),
459            "[package]\nname = \"example-program\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
460        )?;
461        std::fs::write(src_dir.join("lib.rs"), "pub fn example() {}\n")?;
462
463        Ok(TestWorkspace {
464            _root: root,
465            root: root_path,
466            program_dir,
467        })
468    }
469
470    struct TestWorkspace {
471        _root: tempfile::TempDir,
472        root: PathBuf,
473        program_dir: PathBuf,
474    }
475}