Skip to main content

blueprint_chain_setup_tangle/
foundry.rs

1use blueprint_std::{
2    collections::VecDeque,
3    io::{BufRead, BufReader},
4    process::{Command, Stdio},
5    sync::mpsc,
6    thread,
7};
8use color_eyre::eyre::Result;
9use dialoguer::console::style;
10use indicatif::{ProgressBar, ProgressStyle};
11
12pub struct FoundryToolchain {
13    pub forge: Forge,
14}
15
16/// Trait for checking if a command is installed.
17trait CommandInstalled {
18    /// Returns true if the command is installed.
19    fn is_installed(&self) -> bool;
20}
21
22impl CommandInstalled for Command {
23    fn is_installed(&self) -> bool {
24        let cmd = self.get_program();
25        if cfg!(target_os = "windows") {
26            Command::new("where")
27                .arg(cmd)
28                .output()
29                .is_ok_and(|v| v.status.success())
30        } else {
31            Command::new("which")
32                .arg(cmd)
33                .output()
34                .is_ok_and(|v| v.status.success())
35        }
36    }
37}
38
39impl Default for FoundryToolchain {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl FoundryToolchain {
46    pub fn new() -> Self {
47        Self {
48            forge: Forge::new(),
49        }
50    }
51    pub fn check_installed_or_exit(&self) {
52        fn foundry_installation_instructions() {
53            eprintln!("Please install Foundry, follow https://getfoundry.sh/ for instructions.");
54        }
55        if !self.forge.is_installed() {
56            eprintln!("Forge is not installed.");
57            foundry_installation_instructions();
58        }
59    }
60}
61
62pub struct Forge {
63    /// Default template to use if none specified
64    pub default_template: String,
65    /// Default commit to use if none specified
66    pub default_commit: String,
67}
68
69impl Default for Forge {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl Forge {
76    /// Creates a new Forge instance with default settings.
77    pub fn new() -> Self {
78        Self {
79            default_template: "foundry-rs/forge-template".to_string(),
80            default_commit: "main".to_string(),
81        }
82    }
83
84    /// Returns the version of Forge.
85    pub fn version(&self) -> Result<String> {
86        let output = Command::new("forge").arg("--version").output()?;
87        Ok(String::from_utf8(output.stdout)?
88            .trim()
89            .replace("forge", ""))
90    }
91
92    /// Returns true if Forge is installed.
93    pub fn is_installed(&self) -> bool {
94        Command::new("forge").is_installed()
95    }
96
97    /// Install dependencies with live progress updates.
98    /// Shows real-time output from forge and tracks progress.
99    pub fn install_dependencies(&self) -> Result<()> {
100        // Start the command with inherited stdio to show output directly
101        let status = Command::new("forge")
102            .args(["soldeer", "update", "-d"])
103            .stdout(Stdio::inherit())
104            .stderr(Stdio::inherit())
105            .status()?;
106
107        if !status.success() {
108            return Err(color_eyre::eyre::eyre!(
109                "Failed to install dependencies. Check the output above for details."
110            ));
111        }
112
113        Ok(())
114    }
115
116    /// Build the contracts with progress tracking.
117    pub fn build(&self) -> Result<()> {
118        let spinner = ProgressBar::new_spinner();
119        spinner.set_style(
120            ProgressStyle::default_spinner()
121                .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈")
122                .template("{spinner:.green} {msg}")
123                .unwrap(),
124        );
125
126        spinner.set_message("Building contracts...");
127
128        let mut child = Command::new("forge")
129            .arg("build")
130            .stdout(Stdio::piped())
131            .stderr(Stdio::piped())
132            .spawn()?;
133
134        let stdout = child.stdout.take().expect("Failed to capture stdout");
135        let stderr = child.stderr.take().expect("Failed to capture stderr");
136
137        // Create readers for both stdout and stderr
138        let stdout_reader = BufReader::new(stdout);
139        let stderr_reader = BufReader::new(stderr);
140
141        // Create channels for output communication
142        let (tx, rx) = mpsc::channel();
143        let tx_stderr = tx.clone();
144
145        // Keep a buffer of recent output lines
146        let mut output_buffer = VecDeque::with_capacity(100);
147
148        // Spawn thread to read stdout
149        thread::spawn(move || {
150            for line in stdout_reader.lines().map_while(Result::ok) {
151                let _ = tx.send(line);
152            }
153        });
154
155        // Spawn thread to read stderr
156        thread::spawn(move || {
157            for line in stderr_reader.lines().map_while(Result::ok) {
158                let _ = tx_stderr.send(line);
159            }
160        });
161
162        // Update spinner and show output while command runs
163        while child.try_wait()?.is_none() {
164            spinner.tick();
165
166            // Check for new output
167            while let Ok(line) = rx.try_recv() {
168                // Only add non-duplicate lines
169                if !output_buffer.contains(&line) {
170                    output_buffer.push_back(line);
171
172                    // Keep only the last 100 lines
173                    if output_buffer.len() > 100 {
174                        output_buffer.pop_front();
175                    }
176
177                    // Print just the new line
178                    spinner.suspend(|| {
179                        println!("{}", style(&output_buffer[output_buffer.len() - 1]).dim());
180                    });
181                }
182            }
183
184            thread::sleep(std::time::Duration::from_millis(100));
185        }
186
187        let status = child.wait()?;
188
189        if !status.success() {
190            spinner.finish_with_message("❌ Failed to build contracts");
191            return Err(color_eyre::eyre::eyre!(
192                "Failed to build contracts. Check the output above for details."
193            ));
194        }
195
196        spinner.finish_with_message("✨ Contracts built successfully!");
197        Ok(())
198    }
199}