use std::{
fs::{File, OpenOptions},
path::{Path, PathBuf},
thread,
time::{Duration, Instant},
};
use anyhow::{Context, Result};
use super::{
extract_tar_gz, get_platform, get_s3_bucket, get_toolchain_root, S3StorageBackend, Toolchain,
ToolchainConfig,
};
pub const RUST_NIGHTLY_VERSION: &str = "nightly-2025-05-10";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum ToolchainSource {
Explicit,
Environment,
WorkspaceFile,
Rialoman,
Installed,
}
impl std::fmt::Display for ToolchainSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Explicit => write!(f, "explicit"),
Self::Environment => write!(f, "environment"),
Self::WorkspaceFile => write!(f, "workspace-file"),
Self::Rialoman => write!(f, "manifest"),
Self::Installed => write!(f, "installed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ResolvedToolchainVersion {
pub version: String,
pub source: ToolchainSource,
pub source_path: Option<PathBuf>,
}
pub const RUST_COMMIT_HASH: &str = "dcecb99176edf2eec51613730937d21cdd5c8f6e";
const RISCV_BACKEND_PATCH: &str = include_str!("../../patches/0001-risc-v-backend.patch");
const INSTALL_LOCK_FILE_NAME: &str = ".rialo-rust-install.lock";
const INSTALL_LOCK_RETRY_INTERVAL: Duration = Duration::from_millis(200);
const INSTALL_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug, Clone)]
pub struct RialoRustToolchain {
pub(crate) config: ToolchainConfig,
}
impl RialoRustToolchain {
pub fn new() -> Result<Self> {
let (version, _) = Self::resolve_version()?;
Self::with_version(&version)
}
pub fn with_version(version: &str) -> Result<Self> {
if version == "auto" || version == "latest" {
return Self::new();
}
let toolchain_root = get_toolchain_root()?;
let install_path = toolchain_root.join(format!("rialo-rust-{}", version));
let platform = get_platform()?;
let download_url = Self::get_download_url(version, &platform)?;
Ok(Self {
config: ToolchainConfig {
name: "rialo-rust".to_string(),
version: version.to_string(),
download_url,
install_path,
checksum: None,
},
})
}
pub fn resolve_version() -> Result<(String, ToolchainSource)> {
if let Ok(version) = std::env::var("RIALO_RUST_TOOLCHAIN_VERSION") {
log::info!("Using toolchain {} from environment variable", version);
return Ok((version, ToolchainSource::Environment));
}
if let Some(version) = super::detect_rialoman_release_toolchain_version()? {
log::info!("Using toolchain {} from Rialoman release manifest", version);
return Ok((version, ToolchainSource::Rialoman));
}
if let Some(version) = Self::find_installed_version()? {
log::info!("Using installed Rialo Rust toolchain: {}", version);
return Ok((version, ToolchainSource::Installed));
}
Err(anyhow::anyhow!(
"No Rialo Rust toolchains installed. Install one with:\n \
rialoman toolchain install rialo-rust --version <VERSION>\n\n\
Or build from source with:\n \
rialoman toolchain build"
))
}
pub fn resolve_version_for_program(
program_path: &Path,
explicit_override: Option<&str>,
) -> Result<ResolvedToolchainVersion> {
let program_path = if program_path.is_absolute() {
program_path.to_path_buf()
} else {
std::env::current_dir()
.context("Failed to determine current working directory")?
.join(program_path)
};
resolve_version_for_program_with_sources(
&program_path,
explicit_override,
std::env::var("RIALO_RUST_TOOLCHAIN_VERSION").ok(),
super::detect_rialoman_release_toolchain_version()?,
Self::find_installed_version()?,
)
}
pub fn find_installed_version() -> Result<Option<String>> {
let toolchains = crate::toolchain::list_installed_toolchains()?;
for (name, version) in toolchains {
if name == "rialo-rust" {
log::debug!("Found installed Rialo Rust toolchain: {}", version);
return Ok(Some(version));
}
}
Ok(None)
}
pub fn new_from_source() -> Result<Self> {
let toolchain_root = get_toolchain_root()?;
let install_path = toolchain_root.join("rialo-rust-source");
Ok(Self {
config: ToolchainConfig {
name: "rialo-rust".to_string(),
version: RUST_NIGHTLY_VERSION.to_string(),
download_url: String::new(), install_path,
checksum: None,
},
})
}
fn get_download_url(version: &str, platform: &str) -> Result<String> {
let base_url = "https://github.com/subzerolabs/rialo-toolchains/releases/download";
let toolchain_platform = match platform {
"x86_64-apple-darwin" => "x86_64-apple-darwin",
"aarch64-apple-darwin" => "aarch64-apple-darwin",
"x86_64-unknown-linux-gnu" => "x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu" => "aarch64-unknown-linux-gnu",
_ => {
return Err(anyhow::anyhow!(
"No Rialo Rust toolchain available for platform: {platform}"
))
}
};
let archive_name = format!("rialo-rust-{version}-{toolchain_platform}.tar.gz");
let url = format!("{base_url}/{version}/{archive_name}");
Ok(url)
}
pub fn get_patch_content() -> &'static str {
RISCV_BACKEND_PATCH
}
pub fn write_patch_file(dest_dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(dest_dir)
.with_context(|| format!("Failed to create directory {}", dest_dir.display()))?;
let patch_path = dest_dir.join("0001-risc-v-backend.patch");
std::fs::write(&patch_path, RISCV_BACKEND_PATCH)
.with_context(|| format!("Failed to write patch file to {}", patch_path.display()))?;
Ok(patch_path)
}
pub(crate) fn check_rustup() -> Result<()> {
which::which("rustup")
.context("rustup not found. Please install rustup from https://rustup.rs/")?;
Ok(())
}
fn check_rustup_registration() -> Result<bool> {
let output = std::process::Command::new("rustup")
.args(["toolchain", "list"])
.output()
.context("Failed to run rustup toolchain list")?;
let output_str = String::from_utf8_lossy(&output.stdout);
Ok(output_str.lines().any(|line| line.starts_with("rialo")))
}
pub fn register_with_rustup(&self) -> Result<()> {
Self::check_rustup()?;
self.with_install_lock(|toolchain| toolchain.register_with_rustup_unlocked())
}
pub(crate) fn register_with_rustup_unlocked(&self) -> Result<()> {
log::info!("Registering toolchain with rustup as 'rialo'...");
let status = std::process::Command::new("rustup")
.args([
"toolchain",
"link",
"rialo",
self.config.install_path.to_str().unwrap(),
])
.status()
.context("Failed to link toolchain with rustup")?;
if !status.success() {
return Err(anyhow::anyhow!("Failed to register toolchain with rustup"));
}
log::info!("Toolchain registered as 'rialo'");
Ok(())
}
fn unregister_from_rustup() -> Result<()> {
if Self::check_rustup_registration()? {
log::info!("Unregistering toolchain from rustup...");
std::process::Command::new("rustup")
.args(["toolchain", "remove", "rialo"])
.status()
.context("Failed to unregister toolchain")?;
log::info!("Toolchain unregistered");
}
Ok(())
}
fn validate_toolchain_at(path: &Path) -> Result<()> {
if !path.exists() {
return Err(anyhow::anyhow!(
"Toolchain path does not exist: {}",
path.display()
));
}
let bin_path = path.join("bin");
if !bin_path.exists() {
return Err(anyhow::anyhow!(
"Toolchain bin directory not found at {}",
bin_path.display()
));
}
let rustc = bin_path.join("rustc");
let cargo = bin_path.join("cargo");
if !rustc.exists() {
return Err(anyhow::anyhow!(
"rustc not found in toolchain at {}",
bin_path.display()
));
}
if !cargo.exists() {
return Err(anyhow::anyhow!(
"cargo not found in toolchain at {}",
bin_path.display()
));
}
Ok(())
}
pub fn ensure_installed_atomically(&self) -> Result<()> {
if self.validate_if_installed()? {
return Ok(());
}
Self::check_rustup()?;
self.with_install_lock(|toolchain| toolchain.ensure_usable_unlocked())
}
pub(crate) fn with_install_lock<T>(
&self,
action: impl FnOnce(&RialoRustToolchain) -> Result<T>,
) -> Result<T> {
let _guard = Self::acquire_install_lock()?;
action(self)
}
fn acquire_install_lock() -> Result<InstallLockGuard> {
let toolchain_root = get_toolchain_root()?;
Self::acquire_install_lock_in_root(&toolchain_root)
}
fn acquire_install_lock_in_root(toolchain_root: &Path) -> Result<InstallLockGuard> {
std::fs::create_dir_all(toolchain_root)
.with_context(|| format!("Failed to create {}", toolchain_root.display()))?;
let lock_path = toolchain_root.join(INSTALL_LOCK_FILE_NAME);
InstallLockGuard::acquire(lock_path)
}
}
struct InstallLockGuard {
path: PathBuf,
file: File,
}
impl InstallLockGuard {
fn acquire(path: PathBuf) -> Result<Self> {
let deadline = Instant::now() + INSTALL_LOCK_TIMEOUT;
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&path)
.with_context(|| format!("Failed to open {}", path.display()))?;
loop {
match file.try_lock() {
Ok(()) => return Ok(Self { path, file }),
Err(std::fs::TryLockError::WouldBlock) => {
if Instant::now() >= deadline {
return Err(anyhow::anyhow!(
"Timed out waiting for toolchain install lock at {}",
path.display()
));
}
thread::sleep(INSTALL_LOCK_RETRY_INTERVAL);
}
Err(std::fs::TryLockError::Error(error)) => {
return Err(error).with_context(|| format!("Failed to lock {}", path.display()))
}
}
}
}
}
impl Drop for InstallLockGuard {
fn drop(&mut self) {
if let Err(error) = self.file.unlock() {
log::warn!(
"Failed to unlock toolchain install lock {}: {}",
self.path.display(),
error
);
}
}
}
fn resolve_version_for_program_with_sources(
program_path: &Path,
explicit_override: Option<&str>,
env_override: Option<String>,
manifest_override: Option<String>,
installed_override: Option<String>,
) -> Result<ResolvedToolchainVersion> {
if let Some(version) = normalize_toolchain_version(explicit_override) {
return Ok(ResolvedToolchainVersion {
version: version.to_string(),
source: ToolchainSource::Explicit,
source_path: None,
});
}
if let Some(version) = normalize_toolchain_version(env_override.as_deref()) {
return Ok(ResolvedToolchainVersion {
version: version.to_string(),
source: ToolchainSource::Environment,
source_path: None,
});
}
if let Some((path, version)) = find_workspace_toolchain_version(program_path)? {
return Ok(ResolvedToolchainVersion {
version,
source: ToolchainSource::WorkspaceFile,
source_path: Some(path),
});
}
if let Some(version) = normalize_toolchain_version(manifest_override.as_deref()) {
return Ok(ResolvedToolchainVersion {
version: version.to_string(),
source: ToolchainSource::Rialoman,
source_path: None,
});
}
if let Some(version) = normalize_toolchain_version(installed_override.as_deref()) {
return Ok(ResolvedToolchainVersion {
version: version.to_string(),
source: ToolchainSource::Installed,
source_path: None,
});
}
Err(anyhow::anyhow!(
"No Rialo Rust toolchains installed. Install one with:\n \
rialoman toolchain install rialo-rust --version <VERSION>\n\n\
Or build from source with:\n \
rialoman toolchain build"
))
}
fn find_workspace_toolchain_version(program_path: &Path) -> Result<Option<(PathBuf, String)>> {
for ancestor in program_path.ancestors() {
let toolchain_path = ancestor.join(".rialo-toolchain");
if !toolchain_path.exists() {
continue;
}
let toolchain_path = toolchain_path
.canonicalize()
.with_context(|| format!("Failed to canonicalize {}", toolchain_path.display()))?;
let version = std::fs::read_to_string(&toolchain_path)
.with_context(|| format!("Failed to read {}", toolchain_path.display()))?;
let version = version.trim();
if version.is_empty() {
return Err(anyhow::anyhow!(
"Empty toolchain version in {}",
toolchain_path.display()
));
}
return Ok(Some((toolchain_path, version.to_string())));
}
Ok(None)
}
fn normalize_toolchain_version(version: Option<&str>) -> Option<&str> {
match version.map(str::trim) {
Some("") | None => None,
Some("auto" | "latest") => None,
Some(version) => Some(version),
}
}
impl Default for RialoRustToolchain {
fn default() -> Self {
Self::new().expect("Failed to create default Rialo Rust toolchain")
}
}
impl Toolchain for RialoRustToolchain {
fn is_installed(&self) -> Result<bool> {
if Self::validate_toolchain_at(&self.config.install_path).is_ok() {
log::debug!(
"Found Rialo Rust toolchain at {}",
self.config.install_path.display()
);
let using_direct_cargo = std::env::var("RIALO_BUILD_CARGO_PATH")
.map(|p| !p.is_empty())
.unwrap_or(false);
if using_direct_cargo {
log::debug!("Direct cargo mode — skipping rustup registration check");
return Ok(true);
}
if !Self::check_rustup_registration()? {
log::debug!("Toolchain files exist but not registered with rustup");
return Ok(false);
}
return Ok(true);
}
Ok(false)
}
fn install(&self) -> Result<()> {
if self.is_installed()? {
log::info!(
"Rialo Rust toolchain {} is already installed at {}",
self.config.version,
self.config.install_path.display()
);
return Ok(());
}
Self::check_rustup()?;
self.with_install_lock(|toolchain| toolchain.install_unlocked())
}
fn validate(&self) -> Result<()> {
if !self.is_installed()? {
return Err(anyhow::anyhow!(
"Rialo Rust toolchain {} is not installed. Run 'rialo-build toolchain install rialo-rust' to install it.",
self.config.version
));
}
let using_direct_cargo = std::env::var("RIALO_BUILD_CARGO_PATH")
.map(|p| !p.is_empty())
.unwrap_or(false);
if using_direct_cargo {
log::info!(
"Rialo Rust toolchain {} available (direct cargo mode)",
self.config.version
);
return Ok(());
}
let output = std::process::Command::new("cargo")
.args(["+rialo", "--version"])
.output()
.context("Failed to run cargo +rialo --version. Is rustup configured correctly?")?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"cargo +rialo returned non-zero exit code. The toolchain may be corrupted."
));
}
log::info!("Rialo Rust toolchain validation:");
let version_output = String::from_utf8_lossy(&output.stdout);
log::info!(" {}", version_output.trim());
let output = std::process::Command::new("rustc")
.args(["+rialo", "--print", "target-list"])
.output()
.context("Failed to run rustc +rialo --print target-list")?;
let targets = String::from_utf8_lossy(&output.stdout);
if !targets.contains("riscv64emac-solana-solana") {
return Err(anyhow::anyhow!(
"Custom target riscv64emac-solana-solana not found in toolchain. The toolchain may be incorrectly built."
));
}
log::info!(" Custom target: riscv64emac-solana-solana");
log::info!("Toolchain is functional");
Ok(())
}
fn get_bin_path(&self) -> Result<PathBuf> {
let bin_path = self.config.install_path.join("bin");
if !bin_path.exists() {
return Err(anyhow::anyhow!(
"Toolchain bin directory not found at {}",
bin_path.display()
));
}
Ok(bin_path)
}
fn get_config(&self) -> &ToolchainConfig {
&self.config
}
}
impl RialoRustToolchain {
fn validate_if_installed(&self) -> Result<bool> {
if !self.is_installed()? {
return Ok(false);
}
match self.validate() {
Ok(()) => Ok(true),
Err(error) => {
log::warn!(
"Installed Rialo Rust toolchain {} failed validation before acquiring the shared lock: {}",
self.config.version,
error
);
Ok(false)
}
}
}
fn ensure_usable_unlocked(&self) -> Result<()> {
if !self.is_installed()? {
self.install_unlocked()?;
}
self.validate()
}
fn install_unlocked(&self) -> Result<()> {
log::info!("Installing Rialo Rust toolchain {}...", self.config.version);
let toolchain_root = get_toolchain_root()?;
std::fs::create_dir_all(&toolchain_root).with_context(|| {
format!(
"Failed to create toolchain directory {}",
toolchain_root.display()
)
})?;
let temp_dir = tempfile::tempdir()
.context("Failed to create temporary directory for toolchain download")?;
let archive_path = temp_dir.path().join("rialo-rust-toolchain.tar.gz");
self.download_with_fallback(&archive_path)
.with_context(|| {
format!(
"Failed to download Rialo Rust toolchain version {}",
self.config.version
)
})?;
std::fs::create_dir_all(&self.config.install_path).with_context(|| {
format!(
"Failed to create install directory {}",
self.config.install_path.display()
)
})?;
extract_tar_gz(&archive_path, &self.config.install_path)?;
self.register_with_rustup_unlocked()?;
log::info!(
"Rialo Rust toolchain {} installed successfully at {}",
self.config.version,
self.config.install_path.display()
);
Ok(())
}
pub fn uninstall(&self) -> Result<()> {
Self::unregister_from_rustup()?;
if self.config.install_path.exists() {
std::fs::remove_dir_all(&self.config.install_path).with_context(|| {
format!(
"Failed to remove installation directory {}",
self.config.install_path.display()
)
})?;
log::info!(
"Removed Rialo Rust toolchain from {}",
self.config.install_path.display()
);
}
Ok(())
}
fn download_with_fallback(&self, dest: &Path) -> Result<()> {
self.download_from_http(dest)
}
fn download_from_http(&self, dest: &Path) -> Result<()> {
use crate::toolchain::HttpToolchainClient;
let client =
HttpToolchainClient::new().context("Failed to create HTTP toolchain client")?;
let platform = get_platform()?;
let archive_name = format!("rialo-rust-{}-{}", self.config.version, platform);
client
.download_toolchain("rialo-rust", &self.config.version, &archive_name, dest)
.with_context(|| {
format!(
"Failed to download Rialo Rust toolchain version {}",
self.config.version
)
})
}
pub fn upload_to_s3(&self) -> Result<()> {
if !self.is_installed()? {
return Err(anyhow::anyhow!(
"Toolchain is not installed. Cannot upload to S3."
));
}
log::debug!("Packaging Rialo Rust toolchain for upload...");
let temp_dir =
tempfile::tempdir().context("Failed to create temporary directory for tarball")?;
let archive_path = temp_dir.path().join("rialo-rust-toolchain.tar.gz");
self.create_tarball(&archive_path)?;
log::info!("Uploading to S3...");
let runtime = tokio::runtime::Runtime::new()
.context("Failed to create tokio runtime for S3 upload")?;
runtime.block_on(async {
let bucket = get_s3_bucket();
let backend = S3StorageBackend::new(bucket).await?.context(
"S3 backend not available. Ensure AWS credentials are configured \
(AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).",
)?;
let platform = get_platform()?;
backend
.upload_toolchain("rialo-rust", &self.config.version, &platform, &archive_path)
.await
})
}
fn create_tarball(&self, dest: &Path) -> Result<()> {
use std::fs::File;
use flate2::{write::GzEncoder, Compression};
use tar::Builder;
log::debug!("Creating tarball at {}...", dest.display());
let tar_gz = File::create(dest)
.with_context(|| format!("Failed to create tarball file {}", dest.display()))?;
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = Builder::new(enc);
tar.append_dir_all(".", &self.config.install_path)
.with_context(|| {
format!(
"Failed to add files from {} to tarball",
self.config.install_path.display()
)
})?;
tar.finish().context("Failed to finalize tarball")?;
log::debug!("Tarball created");
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use super::*;
#[test]
fn test_create_rialo_rust_toolchain() {
let toolchain = RialoRustToolchain::with_version("v1.0.0");
assert!(
toolchain.is_ok(),
"Failed to create toolchain: {:?}",
toolchain.err()
);
let tc = toolchain.unwrap();
assert_eq!(tc.config.version, "v1.0.0");
assert_eq!(tc.config.name, "rialo-rust");
}
#[test]
fn test_toolchain_with_different_version() {
let toolchain = RialoRustToolchain::with_version("v2.5.3");
assert!(toolchain.is_ok());
let tc = toolchain.unwrap();
assert_eq!(tc.config.version, "v2.5.3");
assert_eq!(tc.config.name, "rialo-rust");
}
#[test]
fn test_patch_content_exists() {
let patch = RialoRustToolchain::get_patch_content();
assert!(!patch.is_empty());
assert!(patch.contains("riscv64emac-solana-solana"));
assert!(patch.contains("risc-v backend"));
}
#[test]
fn test_source_build_constants() {
assert_eq!(RUST_NIGHTLY_VERSION, "nightly-2025-05-10");
assert_eq!(RUST_COMMIT_HASH, "dcecb99176edf2eec51613730937d21cdd5c8f6e");
}
#[test]
fn resolve_version_for_program_prefers_explicit_override() {
let temp_dir = tempfile::tempdir().unwrap();
let program_dir = temp_dir.path().join("program");
std::fs::create_dir_all(&program_dir).unwrap();
std::fs::write(temp_dir.path().join(".rialo-toolchain"), "0.0.3\n").unwrap();
let resolved = resolve_version_for_program_with_sources(
&program_dir,
Some("1.2.3"),
Some("9.9.9".to_string()),
Some("8.8.8".to_string()),
Some("7.7.7".to_string()),
)
.unwrap();
assert_eq!(resolved.version, "1.2.3");
assert_eq!(resolved.source, ToolchainSource::Explicit);
assert!(resolved.source_path.is_none());
}
#[test]
fn resolve_version_for_program_prefers_workspace_file_over_manifest_and_installed() {
let temp_dir = tempfile::tempdir().unwrap();
let program_dir = temp_dir.path().join("program");
std::fs::create_dir_all(&program_dir).unwrap();
let toolchain_path = temp_dir.path().join(".rialo-toolchain");
std::fs::write(&toolchain_path, "0.0.3\n").unwrap();
let resolved = resolve_version_for_program_with_sources(
&program_dir,
None,
None,
Some("8.8.8".to_string()),
Some("7.7.7".to_string()),
)
.unwrap();
assert_eq!(resolved.version, "0.0.3");
assert_eq!(resolved.source, ToolchainSource::WorkspaceFile);
assert_eq!(
resolved.source_path,
Some(toolchain_path.canonicalize().unwrap())
);
}
#[test]
fn install_lock_serializes_install_and_rustup_link_step() {
let temp_dir = tempfile::tempdir().unwrap();
let first_guard =
RialoRustToolchain::acquire_install_lock_in_root(temp_dir.path()).unwrap();
let (sender, receiver) = mpsc::channel();
let lock_root = temp_dir.path().to_path_buf();
let worker = std::thread::spawn(move || {
let _guard = RialoRustToolchain::acquire_install_lock_in_root(&lock_root).unwrap();
sender.send(()).unwrap();
});
assert!(receiver.recv_timeout(Duration::from_millis(250)).is_err());
drop(first_guard);
receiver.recv_timeout(Duration::from_secs(15)).unwrap();
worker.join().unwrap();
}
}