rialo_build_lib/toolchain/
source_builder.rs1use std::path::PathBuf;
10
11use anyhow::{Context, Result};
12
13use super::{
14 get_platform, get_toolchain_root,
15 rialo_rust::{RUST_COMMIT_HASH, RUST_NIGHTLY_VERSION},
16 RialoRustToolchain,
17};
18
19#[derive(Debug, Clone)]
21pub struct SourceBuildConfig {
22 pub source_url: String,
24 pub commit_hash: String,
26 pub patch_files: Vec<PathBuf>,
28 pub build_config: BuildSystemConfig,
30}
31
32impl Default for SourceBuildConfig {
33 fn default() -> Self {
34 Self {
35 source_url: "https://github.com/rust-lang/rust".to_string(),
36 commit_hash: RUST_COMMIT_HASH.to_string(),
37 patch_files: Vec::new(),
38 build_config: BuildSystemConfig::default(),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub enum BuildSystemConfig {
46 RustBootstrap {
48 profile: String,
50 targets: Vec<String>,
52 extended: bool,
54 tools: Vec<String>,
56 build_stage: u32,
58 },
59}
60
61impl Default for BuildSystemConfig {
62 fn default() -> Self {
63 let platform = get_platform().unwrap_or_else(|_| "unknown".to_string());
64 Self::RustBootstrap {
65 profile: "compiler".to_string(),
66 targets: vec![platform, "riscv64emac-solana-solana".to_string()],
67 extended: true,
68 tools: vec!["cargo".to_string()],
69 build_stage: 0, }
71 }
72}
73
74pub trait SourceBuildable {
76 fn can_build_from_source(&self) -> bool;
78
79 fn get_source_config(&self) -> Result<SourceBuildConfig>;
81
82 fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()>;
84}
85
86pub struct RustSourceBuilder {
88 source_dir: PathBuf,
90 install_dir: PathBuf,
92 config: SourceBuildConfig,
94}
95
96impl RustSourceBuilder {
97 pub fn new(install_dir: PathBuf) -> Result<Self> {
99 let toolchain_root = get_toolchain_root()?;
100 let source_dir = toolchain_root.parent().unwrap().join("rust-src/rust");
101
102 Ok(Self {
103 source_dir,
104 install_dir,
105 config: SourceBuildConfig::default(),
106 })
107 }
108
109 pub fn with_config(install_dir: PathBuf, config: SourceBuildConfig) -> Result<Self> {
111 let toolchain_root = get_toolchain_root()?;
112 let source_dir = toolchain_root.parent().unwrap().join("rust-src/rust");
113
114 Ok(Self {
115 source_dir,
116 install_dir,
117 config,
118 })
119 }
120
121 pub fn clone_source(&self) -> Result<()> {
123 if self.source_dir.exists() {
124 println!(
125 "Rust source directory already exists at {}",
126 self.source_dir.display()
127 );
128 println!("Checking out commit {}...", self.config.commit_hash);
129
130 let output = std::process::Command::new("git")
132 .current_dir(&self.source_dir)
133 .args(["rev-parse", "HEAD"])
134 .output()
135 .context("Failed to get current git commit")?;
136
137 let current_commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
138
139 if current_commit == self.config.commit_hash {
140 println!("✅ Already on correct commit");
141 return Ok(());
142 }
143
144 println!("Fetching latest changes...");
146 std::process::Command::new("git")
147 .current_dir(&self.source_dir)
148 .args(["fetch"])
149 .status()
150 .context("Failed to fetch git changes")?;
151
152 std::process::Command::new("git")
153 .current_dir(&self.source_dir)
154 .args(["checkout", &self.config.commit_hash])
155 .status()
156 .context("Failed to checkout commit")?;
157
158 println!("✅ Checked out commit {}", self.config.commit_hash);
159 return Ok(());
160 }
161
162 println!("Cloning Rust repository from {}...", self.config.source_url);
163 println!("This may take several minutes...");
164
165 if let Some(parent) = self.source_dir.parent() {
167 std::fs::create_dir_all(parent)
168 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
169 }
170
171 let source_dir_str = self.source_dir.to_str().ok_or_else(|| {
173 anyhow::anyhow!(
174 "Invalid source directory path: {}",
175 self.source_dir.display()
176 )
177 })?;
178
179 let status = std::process::Command::new("git")
180 .args(["clone", &self.config.source_url, source_dir_str])
181 .status()
182 .context("Failed to clone Rust repository")?;
183
184 if !status.success() {
185 return Err(anyhow::anyhow!("Git clone failed"));
186 }
187
188 println!("Checking out commit {}...", self.config.commit_hash);
190 let status = std::process::Command::new("git")
191 .current_dir(&self.source_dir)
192 .args(["checkout", &self.config.commit_hash])
193 .status()
194 .context("Failed to checkout commit")?;
195
196 if !status.success() {
197 return Err(anyhow::anyhow!("Git checkout failed"));
198 }
199
200 println!("✅ Rust source ready at {}", self.source_dir.display());
201 Ok(())
202 }
203
204 pub fn apply_patches(&self) -> Result<()> {
206 if self.config.patch_files.is_empty() {
207 println!("No patches to apply");
208 return Ok(());
209 }
210
211 println!("Applying {} patches...", self.config.patch_files.len());
212
213 let target_file = self
215 .source_dir
216 .join("compiler/rustc_target/src/spec/targets/riscv64emac_solana_solana.rs");
217
218 for patch_file in &self.config.patch_files {
219 println!("Applying patch: {}", patch_file.display());
220
221 let patch_path_str = patch_file.to_str().ok_or_else(|| {
223 anyhow::anyhow!("Invalid patch file path: {}", patch_file.display())
224 })?;
225
226 let check_status = std::process::Command::new("git")
227 .current_dir(&self.source_dir)
228 .args(["apply", "--check", patch_path_str])
229 .output();
230
231 match check_status {
232 Ok(output) if output.status.success() => {
233 let apply_status = std::process::Command::new("git")
235 .current_dir(&self.source_dir)
236 .args(["apply", patch_path_str])
237 .status()
238 .context("Failed to apply patch")?;
239
240 if !apply_status.success() {
241 return Err(anyhow::anyhow!(
242 "Failed to apply patch {}",
243 patch_file.display()
244 ));
245 }
246
247 println!(" ✅ Patch applied successfully");
248 }
249 Ok(_) => {
250 if target_file.exists() {
252 println!(" ⚠ Patch appears to be already applied");
253 continue;
254 } else {
255 println!(" ⚠ Patch conflicts detected, resetting to clean state...");
257
258 let reset_status = std::process::Command::new("git")
260 .current_dir(&self.source_dir)
261 .args(["reset", "--hard", "HEAD"])
262 .status()
263 .context("Failed to reset git repository")?;
264
265 if !reset_status.success() {
266 return Err(anyhow::anyhow!(
267 "Failed to reset repository to clean state"
268 ));
269 }
270
271 let apply_status = std::process::Command::new("git")
273 .current_dir(&self.source_dir)
274 .args(["apply", patch_path_str])
275 .status()
276 .context("Failed to apply patch after reset")?;
277
278 if !apply_status.success() {
279 return Err(anyhow::anyhow!(
280 "Failed to apply patch {} even after reset",
281 patch_file.display()
282 ));
283 }
284
285 println!(" ✅ Patch applied successfully after reset");
286 }
287 }
288 Err(e) => {
289 return Err(anyhow::anyhow!("Failed to check patch: {}", e));
290 }
291 }
292 }
293
294 println!("✅ All patches applied");
295 Ok(())
296 }
297
298 pub fn create_config_toml(&self) -> Result<()> {
300 println!("Creating config.toml...");
301
302 let BuildSystemConfig::RustBootstrap {
303 profile,
304 targets,
305 extended,
306 tools,
307 build_stage: _,
308 } = &self.config.build_config;
309
310 let sysconfdir = self.install_dir.join("sysconfdir");
312 std::fs::create_dir_all(&sysconfdir)
313 .with_context(|| format!("Failed to create sysconfdir {}", sysconfdir.display()))?;
314
315 let targets_str = targets
317 .iter()
318 .map(|t| format!("\"{}\"", t))
319 .collect::<Vec<_>>()
320 .join(", ");
321
322 let tools_str = tools
324 .iter()
325 .map(|t| format!("\"{}\"", t))
326 .collect::<Vec<_>>()
327 .join(", ");
328
329 let is_ci = std::env::var("CI").is_ok()
332 || std::env::var("GITHUB_ACTIONS").is_ok()
333 || std::env::var("GITLAB_CI").is_ok()
334 || std::env::var("CIRCLECI").is_ok();
335
336 let build_stage = if is_ci {
337 println!("CI environment detected: using build-stage = 2");
338 2
339 } else {
340 println!("Local environment: using build-stage = 1 for faster builds");
341 1
342 };
343
344 let (cc_path, cxx_path) = if cfg!(target_os = "macos") {
346 ("/usr/bin/clang", "/usr/bin/clang++")
348 } else {
349 if which::which("clang").is_ok() {
351 ("clang", "clang++")
352 } else {
353 ("gcc", "g++")
354 }
355 };
356
357 let config_content = format!(
360 r#"profile = "{profile}"
361change-id = 137215
362
363[build]
364host = ["{host}"]
365target = [{targets_str}]
366docs = false
367extended = {extended}
368tools = [{tools_str}]
369build-stage = {build_stage}
370
371[install]
372prefix = "{install_prefix}"
373sysconfdir = "{sysconfdir}"
374
375[llvm]
376download-ci-llvm = false
377link-shared = false
378ccache = false
379
380[llvm.build-config]
381CMAKE_BUILD_TYPE = "Release"
382CMAKE_C_COMPILER = "{cc_path}"
383CMAKE_CXX_COMPILER = "{cxx_path}"
384CMAKE_ASM_COMPILER = "{cc_path}"
385
386[rust]
387lld = true
388incremental = true
389debug-assertions = false
390"#,
391 profile = profile,
392 host = get_platform()?,
393 targets_str = targets_str,
394 extended = extended,
395 tools_str = tools_str,
396 build_stage = build_stage,
397 cc_path = cc_path,
398 cxx_path = cxx_path,
399 install_prefix = self.install_dir.display(),
400 sysconfdir = sysconfdir.display(),
401 );
402
403 let config_path = self.source_dir.join("config.toml");
404 std::fs::write(&config_path, config_content)
405 .with_context(|| format!("Failed to write config.toml to {}", config_path.display()))?;
406
407 println!("✅ config.toml created");
408 Ok(())
409 }
410
411 pub fn build(&self) -> Result<()> {
413 use std::io::Write;
414
415 println!("Building Rust toolchain...");
416 println!("⚠️ This will take 30-60 minutes depending on your system");
417 println!("Build progress: stage0 → stage1 → stage2");
418 println!();
419 let _ = std::io::stdout().flush(); let mut cmd = std::process::Command::new("./x.py");
423 cmd.current_dir(&self.source_dir);
424 cmd.arg("build");
425
426 cmd.env("CFLAGS", "-Wno-error=incompatible-pointer-types");
429 cmd.env("CXXFLAGS", "-Wno-error=incompatible-pointer-types");
430
431 println!("Starting build...");
432 let _ = std::io::stdout().flush();
433
434 let status = cmd.status().context("Failed to start build")?;
435
436 if !status.success() {
437 eprintln!("❌ Build failed!");
438 let _ = std::io::stderr().flush();
439 return Err(anyhow::anyhow!(
440 "Build failed with exit code: {:?}\nCheck the output above for errors.\nSource directory: {}",
441 status.code(),
442 self.source_dir.display()
443 ));
444 }
445
446 println!("✅ Build completed successfully");
447 let _ = std::io::stdout().flush();
448 Ok(())
449 }
450
451 pub fn install(&self) -> Result<()> {
453 use std::io::Write;
454
455 println!("Installing toolchain to {}...", self.install_dir.display());
456 println!("This will copy the built toolchain to the install directory...");
457 let _ = std::io::stdout().flush();
458
459 let status = std::process::Command::new("./x.py")
460 .current_dir(&self.source_dir)
461 .arg("install")
462 .status()
463 .context("Failed to start install")?;
464
465 if !status.success() {
466 eprintln!("❌ Installation failed!");
467 let _ = std::io::stderr().flush();
468 return Err(anyhow::anyhow!(
469 "Installation failed with exit code: {:?}\nCheck the output above for errors.\nInstall directory: {}",
470 status.code(),
471 self.install_dir.display()
472 ));
473 }
474
475 println!("✅ Toolchain installed to {}", self.install_dir.display());
476 let _ = std::io::stdout().flush();
477 Ok(())
478 }
479
480 pub fn build_complete(&self) -> Result<()> {
482 self.clone_source()?;
483 self.apply_patches()?;
484 self.create_config_toml()?;
485 self.build()?;
486 self.install()?;
487 Ok(())
488 }
489}
490
491impl SourceBuildable for RialoRustToolchain {
492 fn can_build_from_source(&self) -> bool {
493 which::which("git").is_ok()
495 && which::which("python3").is_ok()
496 && which::which("cmake").is_ok()
497 && which::which("rustup").is_ok()
498 }
499
500 fn get_source_config(&self) -> Result<SourceBuildConfig> {
501 let patch_dir = self.config.install_path.join("patches");
503 std::fs::create_dir_all(&patch_dir)
504 .with_context(|| format!("Failed to create patch directory {}", patch_dir.display()))?;
505
506 let patch_path = Self::write_patch_file(&patch_dir)?;
507
508 let config = SourceBuildConfig {
509 patch_files: vec![patch_path],
510 ..Default::default()
511 };
512
513 Ok(config)
514 }
515
516 fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()> {
517 Self::check_rustup()?;
518 self.with_install_lock(|toolchain| toolchain.build_from_source_unlocked(config))
519 }
520}
521
522impl RialoRustToolchain {
523 fn build_from_source_unlocked(&self, config: &SourceBuildConfig) -> Result<()> {
524 println!("Building Rialo Rust toolchain from source...");
525 println!("Version: {}", RUST_NIGHTLY_VERSION);
526 println!("Commit: {}", RUST_COMMIT_HASH);
527 println!();
528
529 if !self.can_build_from_source() {
531 return Err(anyhow::anyhow!(
532 "Missing required tools for source build. Please ensure git, python3, cmake, and rustup are installed."
533 ));
534 }
535
536 let builder =
538 RustSourceBuilder::with_config(self.config.install_path.clone(), config.clone())?;
539
540 builder.build_complete()?;
542
543 self.register_with_rustup_unlocked()?;
545
546 println!();
547 println!("✅ Rialo Rust toolchain built and installed successfully");
548 Ok(())
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn test_source_build_config_default() {
558 let config = SourceBuildConfig::default();
559 assert_eq!(config.source_url, "https://github.com/rust-lang/rust");
560 assert_eq!(config.commit_hash, RUST_COMMIT_HASH);
561 }
562
563 #[test]
564 fn test_build_system_config_default() {
565 let config = BuildSystemConfig::default();
566 match config {
567 BuildSystemConfig::RustBootstrap {
568 profile,
569 targets,
570 extended,
571 tools,
572 build_stage,
573 } => {
574 assert_eq!(profile, "compiler");
575 assert!(targets.contains(&"riscv64emac-solana-solana".to_string()));
576 assert!(extended);
577 assert!(tools.contains(&"cargo".to_string()));
578 assert_eq!(build_stage, 0); }
580 }
581 }
582}