Skip to main content

rialo_build_lib/toolchain/
source_builder.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Source code building support for toolchains
5//!
6//! This module provides infrastructure for building toolchains from source,
7//! particularly the Rialo Rust toolchain from the rust-lang/rust repository.
8
9use 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/// Configuration for building a toolchain from source
20#[derive(Debug, Clone)]
21pub struct SourceBuildConfig {
22    /// URL to the source repository
23    pub source_url: String,
24    /// Git commit hash to checkout
25    pub commit_hash: String,
26    /// Path to patch files to apply
27    pub patch_files: Vec<PathBuf>,
28    /// Build system configuration
29    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/// Build system configuration
44#[derive(Debug, Clone)]
45pub enum BuildSystemConfig {
46    /// Rust bootstrap build system (x.py)
47    RustBootstrap {
48        /// Build profile (e.g., "compiler")
49        profile: String,
50        /// Target platforms to build for
51        targets: Vec<String>,
52        /// Whether to build extended tools
53        extended: bool,
54        /// Additional tools to build
55        tools: Vec<String>,
56        /// Build stage (typically 2 for compiler)
57        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, // Don't set build-stage, let bootstrap decide
70        }
71    }
72}
73
74/// Trait for toolchains that can be built from source
75pub trait SourceBuildable {
76    /// Check if building from source is supported
77    fn can_build_from_source(&self) -> bool;
78
79    /// Get the source build configuration
80    fn get_source_config(&self) -> Result<SourceBuildConfig>;
81
82    /// Build the toolchain from source
83    fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()>;
84}
85
86/// Rust source builder for the Rialo toolchain
87pub struct RustSourceBuilder {
88    /// Directory where Rust source will be cloned
89    source_dir: PathBuf,
90    /// Directory where toolchain will be installed
91    install_dir: PathBuf,
92    /// Build configuration
93    config: SourceBuildConfig,
94}
95
96impl RustSourceBuilder {
97    /// Create a new Rust source builder
98    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    /// Create a new builder with custom configuration
110    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    /// Clone the Rust source repository
122    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            // Verify we're on the right commit
131            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            // Fetch and checkout the correct commit
145            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        // Create parent directory
166        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        // Clone the repository
172        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        // Checkout specific commit
189        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    /// Apply patches to the Rust source
205    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        // First, check if we need to clean any existing patch artifacts
214        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            // Check if patch can be applied
222            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                    // Patch can be applied cleanly
234                    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                    // Patch cannot be applied - check if it's already applied
251                    if target_file.exists() {
252                        println!("  ⚠ Patch appears to be already applied");
253                        continue;
254                    } else {
255                        // Patch failed and target doesn't exist - try to reset and apply
256                        println!("  ⚠ Patch conflicts detected, resetting to clean state...");
257
258                        // Reset to HEAD to clean any partial patch application
259                        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                        // Try applying again
272                        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    /// Create config.toml for the Rust build
299    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        // Create sysconfdir
311        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        // Format targets
316        let targets_str = targets
317            .iter()
318            .map(|t| format!("\"{}\"", t))
319            .collect::<Vec<_>>()
320            .join(", ");
321
322        // Format tools
323        let tools_str = tools
324            .iter()
325            .map(|t| format!("\"{}\"", t))
326            .collect::<Vec<_>>()
327            .join(", ");
328
329        // Detect CI environment and set appropriate build stage
330        // CI requires stage 2 for safety, local builds can use stage 1 for speed
331        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        // Determine compiler paths for LLVM build
345        let (cc_path, cxx_path) = if cfg!(target_os = "macos") {
346            // Use Xcode clang on macOS
347            ("/usr/bin/clang", "/usr/bin/clang++")
348        } else {
349            // Linux: prefer clang, fallback to gcc
350            if which::which("clang").is_ok() {
351                ("clang", "clang++")
352            } else {
353                ("gcc", "g++")
354            }
355        };
356
357        // Disable CI LLVM downloads (old commits get deleted, causing 404 errors)
358        // Use static linking to simplify build and avoid installation path issues
359        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    /// Build the Rust toolchain
412    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(); // Ensure output is visible
420
421        // Set up environment variables
422        let mut cmd = std::process::Command::new("./x.py");
423        cmd.current_dir(&self.source_dir);
424        cmd.arg("build");
425
426        // Build stage and compiler paths are configured in config.toml
427        // Set compiler flags to suppress pointer type warnings
428        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    /// Install the built toolchain
452    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    /// Complete build process: clone, patch, configure, build, install
481    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        // Check if required tools are available
494        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        // Write patch to the toolchain install directory
502        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        // Check prerequisites
530        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        // Create source builder
537        let builder =
538            RustSourceBuilder::with_config(self.config.install_path.clone(), config.clone())?;
539
540        // Build
541        builder.build_complete()?;
542
543        // Register with rustup
544        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); // 0 = let bootstrap decide
579            }
580        }
581    }
582}