1use 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#[derive(Debug, Clone)]
35pub struct BuildConfig {
36 pub program_path: PathBuf,
38 pub output_dir: PathBuf,
40 pub target_dir: PathBuf,
42}
43
44pub 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
78pub 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
86pub 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
103pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum RiscvTarget {
172 Rv32i,
174 Rv32im,
176 Rv64gc,
178 #[default]
180 RialoCustom,
181}
182
183impl RiscvTarget {
184 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 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", }
202 }
203
204 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 pub fn requires_rialo_toolchain(&self) -> bool {
214 matches!(self, RiscvTarget::RialoCustom)
215 }
216}
217
218#[derive(Debug, Clone)]
220pub enum BuilderConfig {
221 Solana {},
223 Riscv {
225 toolchain_version: Option<String>,
227 target: RiscvTarget,
229 },
230}
231
232#[derive(Debug, serde::Serialize)]
234pub struct BuildResult {
235 pub package_name: String,
237 pub output_dir: PathBuf,
239 pub program_binary: PathBuf,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub program_keypair: Option<PathBuf>,
244}
245
246pub trait ProgramBuilder {
248 fn validate(&self) -> Result<()>;
250 fn build(&self, config: &BuildConfig) -> Result<BuildResult>;
252}
253
254pub 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
272pub fn build_program(config: &BuildConfig) -> Result<BuildResult> {
274 let builder = create_builder(&BuilderConfig::Solana {})?;
275 builder.validate()?;
276 builder.build(config)
277}
278
279pub fn auto_detect_builder(program_path: &std::path::Path) -> Result<BuilderConfig> {
286 let file_config = BuildFileConfig::from_directory(program_path)?;
288
289 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 }
314 }
315 }
316 }
317
318 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 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
344pub 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}