rialo-build-lib 0.11.2

Shared library for Rialo program building logic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Source code building support for toolchains
//!
//! This module provides infrastructure for building toolchains from source,
//! particularly the Rialo Rust toolchain from the rust-lang/rust repository.

use std::path::PathBuf;

use anyhow::{Context, Result};

use super::{
    get_platform, get_toolchain_root,
    rialo_rust::{RUST_COMMIT_HASH, RUST_NIGHTLY_VERSION},
    RialoRustToolchain,
};

/// Configuration for building a toolchain from source
#[derive(Debug, Clone)]
pub struct SourceBuildConfig {
    /// URL to the source repository
    pub source_url: String,
    /// Git commit hash to checkout
    pub commit_hash: String,
    /// Path to patch files to apply
    pub patch_files: Vec<PathBuf>,
    /// Build system configuration
    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(),
        }
    }
}

/// Build system configuration
#[derive(Debug, Clone)]
pub enum BuildSystemConfig {
    /// Rust bootstrap build system (x.py)
    RustBootstrap {
        /// Build profile (e.g., "compiler")
        profile: String,
        /// Target platforms to build for
        targets: Vec<String>,
        /// Whether to build extended tools
        extended: bool,
        /// Additional tools to build
        tools: Vec<String>,
        /// Build stage (typically 2 for compiler)
        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, // Don't set build-stage, let bootstrap decide
        }
    }
}

/// Trait for toolchains that can be built from source
pub trait SourceBuildable {
    /// Check if building from source is supported
    fn can_build_from_source(&self) -> bool;

    /// Get the source build configuration
    fn get_source_config(&self) -> Result<SourceBuildConfig>;

    /// Build the toolchain from source
    fn build_from_source(&self, config: &SourceBuildConfig) -> Result<()>;
}

/// Rust source builder for the Rialo toolchain
pub struct RustSourceBuilder {
    /// Directory where Rust source will be cloned
    source_dir: PathBuf,
    /// Directory where toolchain will be installed
    install_dir: PathBuf,
    /// Build configuration
    config: SourceBuildConfig,
}

impl RustSourceBuilder {
    /// Create a new Rust source builder
    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(),
        })
    }

    /// Create a new builder with custom configuration
    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,
        })
    }

    /// Clone the Rust source repository
    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);

            // Verify we're on the right commit
            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(());
            }

            // Fetch and checkout the correct commit
            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...");

        // Create parent directory
        if let Some(parent) = self.source_dir.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create directory {}", parent.display()))?;
        }

        // Clone the repository
        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"));
        }

        // Checkout specific commit
        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(())
    }

    /// Apply patches to the Rust source
    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());

        // First, check if we need to clean any existing patch artifacts
        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());

            // Check if patch can be applied
            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() => {
                    // Patch can be applied cleanly
                    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(_) => {
                    // Patch cannot be applied - check if it's already applied
                    if target_file.exists() {
                        println!("  ⚠ Patch appears to be already applied");
                        continue;
                    } else {
                        // Patch failed and target doesn't exist - try to reset and apply
                        println!("  ⚠ Patch conflicts detected, resetting to clean state...");

                        // Reset to HEAD to clean any partial patch application
                        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"
                            ));
                        }

                        // Try applying again
                        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(())
    }

    /// Create config.toml for the Rust build
    pub fn create_config_toml(&self) -> Result<()> {
        println!("Creating config.toml...");

        let BuildSystemConfig::RustBootstrap {
            profile,
            targets,
            extended,
            tools,
            build_stage: _,
        } = &self.config.build_config;

        // Create sysconfdir
        let sysconfdir = self.install_dir.join("sysconfdir");
        std::fs::create_dir_all(&sysconfdir)
            .with_context(|| format!("Failed to create sysconfdir {}", sysconfdir.display()))?;

        // Format targets
        let targets_str = targets
            .iter()
            .map(|t| format!("\"{}\"", t))
            .collect::<Vec<_>>()
            .join(", ");

        // Format tools
        let tools_str = tools
            .iter()
            .map(|t| format!("\"{}\"", t))
            .collect::<Vec<_>>()
            .join(", ");

        // Detect CI environment and set appropriate build stage
        // CI requires stage 2 for safety, local builds can use stage 1 for speed
        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
        };

        // Determine compiler paths for LLVM build
        let (cc_path, cxx_path) = if cfg!(target_os = "macos") {
            // Use Xcode clang on macOS
            ("/usr/bin/clang", "/usr/bin/clang++")
        } else {
            // Linux: prefer clang, fallback to gcc
            if which::which("clang").is_ok() {
                ("clang", "clang++")
            } else {
                ("gcc", "g++")
            }
        };

        // Disable CI LLVM downloads (old commits get deleted, causing 404 errors)
        // Use static linking to simplify build and avoid installation path issues
        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(())
    }

    /// Build the Rust toolchain
    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(); // Ensure output is visible

        // Set up environment variables
        let mut cmd = std::process::Command::new("./x.py");
        cmd.current_dir(&self.source_dir);
        cmd.arg("build");

        // Build stage and compiler paths are configured in config.toml
        // Set compiler flags to suppress pointer type warnings
        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(())
    }

    /// Install the built toolchain
    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(())
    }

    /// Complete build process: clone, patch, configure, build, install
    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 {
        // Check if required tools are available
        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> {
        // Write patch to the toolchain install directory
        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!();

        // Check prerequisites
        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."
            ));
        }

        // Create source builder
        let builder =
            RustSourceBuilder::with_config(self.config.install_path.clone(), config.clone())?;

        // Build
        builder.build_complete()?;

        // Register with rustup
        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); // 0 = let bootstrap decide
            }
        }
    }
}