blueprint_build_utils/
lib.rs

1use std::io::{Read, Write};
2
3use blueprint_std::{
4    env, fs,
5    path::{Path, PathBuf},
6    process::Command,
7};
8
9/// Build the Smart contracts at the specified directories.
10///
11/// This function will automatically rerun the build if changes are detected in the `src`
12/// directory within any of the directories specified. Due to this, it is recommended to
13/// ensure that you only pass in directories that contain the `src` directory and won't be
14/// modified by anything else in the build script (otherwise, the build will always rerun).
15///
16/// # Panics
17///
18/// - If the Cargo Manifest directory is not found.
19/// - If the `forge` executable is not found.
20/// - If the `foundry.toml` file is not found in any of the specified directories
21pub fn build_contracts(contract_dirs: Vec<&str>) {
22    // Get the project root directory
23    let root = workspace_or_manifest_dir();
24
25    // Try to find the `forge` executable dynamically
26    let forge_executable = find_forge_executable();
27
28    for dir in contract_dirs {
29        let full_path = root.join(dir).canonicalize().unwrap_or_else(|_| {
30            println!(
31                "Directory not found or inaccessible: {}",
32                root.join(dir).display()
33            );
34            root.join(dir)
35        });
36
37        if full_path.exists() {
38            if full_path != root.join("./contracts") {
39                // Check if foundry.toml exists and add evm_version if needed
40                let foundry_toml_path = full_path.join("foundry.toml");
41
42                // We need to pin the evm_version of each foundry.toml with the same version so contracts are all consistent
43                if foundry_toml_path.exists() {
44                    // Read the existing foundry.toml
45                    let mut content = String::new();
46                    std::fs::File::open(&foundry_toml_path)
47                        .expect("Failed to open foundry.toml")
48                        .read_to_string(&mut content)
49                        .expect("Failed to read foundry.toml");
50
51                    // Only add evm_version if it's not already there
52                    if !content.contains("evm_version") {
53                        // Find the [profile.default] section
54                        if let Some(pos) = content.find("[profile.default]") {
55                            // Insert evm_version after the section header
56                            let mut new_content = content.clone();
57                            let insert_pos = content[pos..]
58                                .find('\n')
59                                .map_or(content.len(), |p| p + pos + 1);
60                            new_content.insert_str(insert_pos, "    evm_version = \"shanghai\"\n");
61
62                            // Write the modified content back
63                            std::fs::write(&foundry_toml_path, new_content)
64                                .expect("Failed to write to foundry.toml");
65                        } else {
66                            // If [profile.default] section doesn't exist, append it
67                            let mut file = std::fs::OpenOptions::new()
68                                .append(true)
69                                .open(&foundry_toml_path)
70                                .expect("Failed to open foundry.toml for appending");
71
72                            file.write_all(b"\n[profile.default]\nevm_version = \"shanghai\"\n")
73                                .expect("Failed to append to foundry.toml");
74                        }
75                    }
76                } else {
77                    panic!("Failed to read dependency foundry.toml");
78                }
79            }
80
81            // Run forge build with explicit EVM version
82            let status = Command::new(&forge_executable)
83                .current_dir(&full_path)
84                .arg("build")
85                .arg("--evm-version")
86                .arg("shanghai")
87                .arg("--use")
88                .arg("0.8.27")
89                .status()
90                .expect("Failed to execute Forge build");
91
92            assert!(
93                status.success(),
94                "Forge build failed for directory: {}",
95                full_path.display()
96            );
97        } else {
98            panic!(
99                "Directory not found or does not exist: {}",
100                full_path.display()
101            );
102        }
103    }
104}
105
106fn is_directory_empty(path: &Path) -> bool {
107    fs::read_dir(path)
108        .map(|mut i| i.next().is_none())
109        .unwrap_or(true)
110}
111
112fn workspace_or_manifest_dir() -> PathBuf {
113    let dir = env::var("CARGO_WORKSPACE_DIR")
114        .or_else(|_| env::var("CARGO_MANIFEST_DIR"))
115        .expect("neither CARGO_WORKSPACE_DIR nor CARGO_MANIFEST_DIR is set");
116    PathBuf::from(dir)
117}
118
119/// Run soldeer's 'install' command if the dependencies directory exists and is not empty.
120///
121/// # Panics
122/// - If the Cargo Manifest directory is not found.
123/// - If the `forge` executable is not found.
124/// - If forge's `soldeer` is not installed.
125pub fn soldeer_install() {
126    // Get the project root directory
127    let root = workspace_or_manifest_dir();
128
129    // Check if the dependencies directory exists and is not empty
130    let dependencies_dir = root.join("dependencies");
131    if !dependencies_dir.exists() || is_directory_empty(&dependencies_dir) {
132        let forge_executable = find_forge_executable();
133
134        println!("Populating dependencies directory");
135        let status = Command::new(&forge_executable)
136            .current_dir(&root)
137            .args(["soldeer", "install"])
138            .status()
139            .expect("Failed to execute 'forge soldeer install'");
140
141        assert!(status.success(), "'forge soldeer install' failed");
142    } else {
143        println!("Dependencies directory exists or is not empty. Skipping soldeer install.");
144    }
145}
146
147/// Run soldeer's `update` command to populate the `dependencies` directory.
148///
149/// # Panics
150/// - If the Cargo Manifest directory is not found.
151/// - If the `forge` executable is not found.
152/// - If forge's `soldeer` is not installed.
153pub fn soldeer_update() {
154    // Get the project root directory
155    let root = workspace_or_manifest_dir();
156
157    // Try to find the `forge` executable dynamically
158    let forge_executable = find_forge_executable();
159
160    let status = Command::new(&forge_executable)
161        .current_dir(&root)
162        .args(["soldeer", "update", "-d"])
163        .status()
164        .expect("Failed to execute 'forge soldeer update'");
165
166    assert!(status.success(), "'forge soldeer update' failed");
167}
168
169/// Returns a string with the path to the `forge` executable.
170///
171/// # Panics
172/// - If the `forge` executable is not found i.e., if Foundry is not installed.
173#[must_use]
174pub fn find_forge_executable() -> String {
175    // Try to find the `forge` executable dynamically
176    match Command::new("which").arg("forge").output() {
177        Ok(output) => {
178            let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
179            assert!(
180                !path.is_empty(),
181                "Forge executable not found. Make sure Foundry is installed."
182            );
183            path
184        }
185        Err(e) => panic!("Failed to find `forge` executable: {e}"),
186    }
187}