use std::path::PathBuf;
use anyhow::{Context, Result};
use super::{
get_platform, get_toolchain_root,
rialo_rust::{RUST_COMMIT_HASH, RUST_NIGHTLY_VERSION},
RialoRustToolchain,
};
#[derive(Debug, Clone)]
pub struct SourceBuildConfig {
pub source_url: String,
pub commit_hash: String,
pub patch_files: Vec<PathBuf>,
pub build_config: BuildSystemConfig,
}
impl Default for SourceBuildConfig {
fn default() -> Self {
Self {
source_url: "https://github.com/rust-lang/rust".to_string(),
commit_hash: RUST_COMMIT_HASH.to_string(),
patch_files: Vec::new(),
build_config: BuildSystemConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub enum BuildSystemConfig {
RustBootstrap {
profile: String,
targets: Vec<String>,
extended: bool,
tools: Vec<String>,
build_stage: u32,
},
}
impl Default for BuildSystemConfig {
fn default() -> Self {
let platform = get_platform().unwrap_or_else(|_| "unknown".to_string());
Self::RustBootstrap {
profile: "compiler".to_string(),
targets: vec![platform, "riscv64emac-solana-solana".to_string()],
extended: true,
tools: vec!["cargo".to_string()],
build_stage: 0, }
}
}
pub trait SourceBuildable {
fn can_build_from_source(&self) -> bool;
fn get_source_config(&self) -> Result<SourceBuildConfig>;
fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()>;
}
pub struct RustSourceBuilder {
source_dir: PathBuf,
install_dir: PathBuf,
config: SourceBuildConfig,
}
impl RustSourceBuilder {
pub fn new(install_dir: PathBuf) -> Result<Self> {
let toolchain_root = get_toolchain_root()?;
let source_dir = toolchain_root.parent().unwrap().join("rust-src/rust");
Ok(Self {
source_dir,
install_dir,
config: SourceBuildConfig::default(),
})
}
pub fn with_config(install_dir: PathBuf, config: SourceBuildConfig) -> Result<Self> {
let toolchain_root = get_toolchain_root()?;
let source_dir = toolchain_root.parent().unwrap().join("rust-src/rust");
Ok(Self {
source_dir,
install_dir,
config,
})
}
pub fn clone_source(&self) -> Result<()> {
if self.source_dir.exists() {
println!(
"Rust source directory already exists at {}",
self.source_dir.display()
);
println!("Checking out commit {}...", self.config.commit_hash);
let output = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["rev-parse", "HEAD"])
.output()
.context("Failed to get current git commit")?;
let current_commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
if current_commit == self.config.commit_hash {
println!("✅ Already on correct commit");
return Ok(());
}
println!("Fetching latest changes...");
std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["fetch"])
.status()
.context("Failed to fetch git changes")?;
std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["checkout", &self.config.commit_hash])
.status()
.context("Failed to checkout commit")?;
println!("✅ Checked out commit {}", self.config.commit_hash);
return Ok(());
}
println!("Cloning Rust repository from {}...", self.config.source_url);
println!("This may take several minutes...");
if let Some(parent) = self.source_dir.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
}
let source_dir_str = self.source_dir.to_str().ok_or_else(|| {
anyhow::anyhow!(
"Invalid source directory path: {}",
self.source_dir.display()
)
})?;
let status = std::process::Command::new("git")
.args(["clone", &self.config.source_url, source_dir_str])
.status()
.context("Failed to clone Rust repository")?;
if !status.success() {
return Err(anyhow::anyhow!("Git clone failed"));
}
println!("Checking out commit {}...", self.config.commit_hash);
let status = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["checkout", &self.config.commit_hash])
.status()
.context("Failed to checkout commit")?;
if !status.success() {
return Err(anyhow::anyhow!("Git checkout failed"));
}
println!("✅ Rust source ready at {}", self.source_dir.display());
Ok(())
}
pub fn apply_patches(&self) -> Result<()> {
if self.config.patch_files.is_empty() {
println!("No patches to apply");
return Ok(());
}
println!("Applying {} patches...", self.config.patch_files.len());
let target_file = self
.source_dir
.join("compiler/rustc_target/src/spec/targets/riscv64emac_solana_solana.rs");
for patch_file in &self.config.patch_files {
println!("Applying patch: {}", patch_file.display());
let patch_path_str = patch_file.to_str().ok_or_else(|| {
anyhow::anyhow!("Invalid patch file path: {}", patch_file.display())
})?;
let check_status = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["apply", "--check", patch_path_str])
.output();
match check_status {
Ok(output) if output.status.success() => {
let apply_status = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["apply", patch_path_str])
.status()
.context("Failed to apply patch")?;
if !apply_status.success() {
return Err(anyhow::anyhow!(
"Failed to apply patch {}",
patch_file.display()
));
}
println!(" ✅ Patch applied successfully");
}
Ok(_) => {
if target_file.exists() {
println!(" ⚠ Patch appears to be already applied");
continue;
} else {
println!(" ⚠ Patch conflicts detected, resetting to clean state...");
let reset_status = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["reset", "--hard", "HEAD"])
.status()
.context("Failed to reset git repository")?;
if !reset_status.success() {
return Err(anyhow::anyhow!(
"Failed to reset repository to clean state"
));
}
let apply_status = std::process::Command::new("git")
.current_dir(&self.source_dir)
.args(["apply", patch_path_str])
.status()
.context("Failed to apply patch after reset")?;
if !apply_status.success() {
return Err(anyhow::anyhow!(
"Failed to apply patch {} even after reset",
patch_file.display()
));
}
println!(" ✅ Patch applied successfully after reset");
}
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to check patch: {}", e));
}
}
}
println!("✅ All patches applied");
Ok(())
}
pub fn create_config_toml(&self) -> Result<()> {
println!("Creating config.toml...");
let BuildSystemConfig::RustBootstrap {
profile,
targets,
extended,
tools,
build_stage: _,
} = &self.config.build_config;
let sysconfdir = self.install_dir.join("sysconfdir");
std::fs::create_dir_all(&sysconfdir)
.with_context(|| format!("Failed to create sysconfdir {}", sysconfdir.display()))?;
let targets_str = targets
.iter()
.map(|t| format!("\"{}\"", t))
.collect::<Vec<_>>()
.join(", ");
let tools_str = tools
.iter()
.map(|t| format!("\"{}\"", t))
.collect::<Vec<_>>()
.join(", ");
let is_ci = std::env::var("CI").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
|| std::env::var("GITLAB_CI").is_ok()
|| std::env::var("CIRCLECI").is_ok();
let build_stage = if is_ci {
println!("CI environment detected: using build-stage = 2");
2
} else {
println!("Local environment: using build-stage = 1 for faster builds");
1
};
let (cc_path, cxx_path) = if cfg!(target_os = "macos") {
("/usr/bin/clang", "/usr/bin/clang++")
} else {
if which::which("clang").is_ok() {
("clang", "clang++")
} else {
("gcc", "g++")
}
};
let config_content = format!(
r#"profile = "{profile}"
change-id = 137215
[build]
host = ["{host}"]
target = [{targets_str}]
docs = false
extended = {extended}
tools = [{tools_str}]
build-stage = {build_stage}
[install]
prefix = "{install_prefix}"
sysconfdir = "{sysconfdir}"
[llvm]
download-ci-llvm = false
link-shared = false
ccache = false
[llvm.build-config]
CMAKE_BUILD_TYPE = "Release"
CMAKE_C_COMPILER = "{cc_path}"
CMAKE_CXX_COMPILER = "{cxx_path}"
CMAKE_ASM_COMPILER = "{cc_path}"
[rust]
lld = true
incremental = true
debug-assertions = false
"#,
profile = profile,
host = get_platform()?,
targets_str = targets_str,
extended = extended,
tools_str = tools_str,
build_stage = build_stage,
cc_path = cc_path,
cxx_path = cxx_path,
install_prefix = self.install_dir.display(),
sysconfdir = sysconfdir.display(),
);
let config_path = self.source_dir.join("config.toml");
std::fs::write(&config_path, config_content)
.with_context(|| format!("Failed to write config.toml to {}", config_path.display()))?;
println!("✅ config.toml created");
Ok(())
}
pub fn build(&self) -> Result<()> {
use std::io::Write;
println!("Building Rust toolchain...");
println!("⚠️ This will take 30-60 minutes depending on your system");
println!("Build progress: stage0 → stage1 → stage2");
println!();
let _ = std::io::stdout().flush();
let mut cmd = std::process::Command::new("./x.py");
cmd.current_dir(&self.source_dir);
cmd.arg("build");
cmd.env("CFLAGS", "-Wno-error=incompatible-pointer-types");
cmd.env("CXXFLAGS", "-Wno-error=incompatible-pointer-types");
println!("Starting build...");
let _ = std::io::stdout().flush();
let status = cmd.status().context("Failed to start build")?;
if !status.success() {
eprintln!("❌ Build failed!");
let _ = std::io::stderr().flush();
return Err(anyhow::anyhow!(
"Build failed with exit code: {:?}\nCheck the output above for errors.\nSource directory: {}",
status.code(),
self.source_dir.display()
));
}
println!("✅ Build completed successfully");
let _ = std::io::stdout().flush();
Ok(())
}
pub fn install(&self) -> Result<()> {
use std::io::Write;
println!("Installing toolchain to {}...", self.install_dir.display());
println!("This will copy the built toolchain to the install directory...");
let _ = std::io::stdout().flush();
let status = std::process::Command::new("./x.py")
.current_dir(&self.source_dir)
.arg("install")
.status()
.context("Failed to start install")?;
if !status.success() {
eprintln!("❌ Installation failed!");
let _ = std::io::stderr().flush();
return Err(anyhow::anyhow!(
"Installation failed with exit code: {:?}\nCheck the output above for errors.\nInstall directory: {}",
status.code(),
self.install_dir.display()
));
}
println!("✅ Toolchain installed to {}", self.install_dir.display());
let _ = std::io::stdout().flush();
Ok(())
}
pub fn build_complete(&self) -> Result<()> {
self.clone_source()?;
self.apply_patches()?;
self.create_config_toml()?;
self.build()?;
self.install()?;
Ok(())
}
}
impl SourceBuildable for RialoRustToolchain {
fn can_build_from_source(&self) -> bool {
which::which("git").is_ok()
&& which::which("python3").is_ok()
&& which::which("cmake").is_ok()
&& which::which("rustup").is_ok()
}
fn get_source_config(&self) -> Result<SourceBuildConfig> {
let patch_dir = self.config.install_path.join("patches");
std::fs::create_dir_all(&patch_dir)
.with_context(|| format!("Failed to create patch directory {}", patch_dir.display()))?;
let patch_path = Self::write_patch_file(&patch_dir)?;
let config = SourceBuildConfig {
patch_files: vec![patch_path],
..Default::default()
};
Ok(config)
}
fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()> {
Self::check_rustup()?;
self.with_install_lock(|toolchain| toolchain.build_from_source_unlocked(config))
}
}
impl RialoRustToolchain {
fn build_from_source_unlocked(&self, config: &SourceBuildConfig) -> Result<()> {
println!("Building Rialo Rust toolchain from source...");
println!("Version: {}", RUST_NIGHTLY_VERSION);
println!("Commit: {}", RUST_COMMIT_HASH);
println!();
if !self.can_build_from_source() {
return Err(anyhow::anyhow!(
"Missing required tools for source build. Please ensure git, python3, cmake, and rustup are installed."
));
}
let builder =
RustSourceBuilder::with_config(self.config.install_path.clone(), config.clone())?;
builder.build_complete()?;
self.register_with_rustup_unlocked()?;
println!();
println!("✅ Rialo Rust toolchain built and installed successfully");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_build_config_default() {
let config = SourceBuildConfig::default();
assert_eq!(config.source_url, "https://github.com/rust-lang/rust");
assert_eq!(config.commit_hash, RUST_COMMIT_HASH);
}
#[test]
fn test_build_system_config_default() {
let config = BuildSystemConfig::default();
match config {
BuildSystemConfig::RustBootstrap {
profile,
targets,
extended,
tools,
build_stage,
} => {
assert_eq!(profile, "compiler");
assert!(targets.contains(&"riscv64emac-solana-solana".to_string()));
assert!(extended);
assert!(tools.contains(&"cargo".to_string()));
assert_eq!(build_stage, 0); }
}
}
}