leo_lang/cli/commands/
build.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19use leo_ast::{NetworkName, Stub};
20use leo_compiler::{AstSnapshots, Compiler, CompilerOptions};
21use leo_errors::{CliError, UtilError};
22use leo_package::{Manifest, Package};
23use leo_span::Symbol;
24
25use snarkvm::prelude::{CanaryV0, Itertools, MainnetV0, Program, TestnetV0};
26
27use indexmap::IndexMap;
28use std::path::Path;
29
30impl From<BuildOptions> for CompilerOptions {
31    fn from(options: BuildOptions) -> Self {
32        Self {
33            ast_spans_enabled: options.enable_ast_spans,
34            ast_snapshots: if options.enable_all_ast_snapshots {
35                AstSnapshots::All
36            } else {
37                AstSnapshots::Some(options.ast_snapshots.into_iter().collect())
38            },
39            initial_ast: options.enable_all_ast_snapshots | options.enable_initial_ast_snapshot,
40        }
41    }
42}
43
44/// Compile and build program command.
45#[derive(Parser, Debug)]
46pub struct LeoBuild {
47    #[clap(flatten)]
48    pub(crate) options: BuildOptions,
49    #[clap(flatten)]
50    pub(crate) env_override: EnvOptions,
51}
52
53impl Command for LeoBuild {
54    type Input = ();
55    type Output = Package;
56
57    fn log_span(&self) -> Span {
58        tracing::span!(tracing::Level::INFO, "Leo")
59    }
60
61    fn prelude(&self, _: Context) -> Result<Self::Input> {
62        Ok(())
63    }
64
65    fn apply(self, context: Context, _: Self::Input) -> Result<Self::Output> {
66        // Build the program.
67        handle_build(&self, context)
68    }
69}
70
71// A helper function to handle the build command.
72fn handle_build(command: &LeoBuild, context: Context) -> Result<<LeoBuild as Command>::Output> {
73    // Get the package path and home directory.
74    let package_path = context.dir()?;
75    let home_path = context.home()?;
76
77    // Get the network, defaulting to `TestnetV0` if none is specified.
78    let network = match get_network(&command.env_override.network) {
79        Ok(network) => network,
80        Err(_) => {
81            println!("⚠️ No network specified, defaulting to 'testnet'.");
82            NetworkName::TestnetV0
83        }
84    };
85
86    // Get the endpoint, if it is provided.
87    let endpoint = match get_endpoint(&command.env_override.endpoint) {
88        Ok(endpoint) => endpoint,
89        Err(_) => {
90            println!("⚠️ No endpoint specified, defaulting to '{}'.", DEFAULT_ENDPOINT);
91            DEFAULT_ENDPOINT.to_string()
92        }
93    };
94
95    let package = if command.options.build_tests {
96        Package::from_directory_with_tests(
97            &package_path,
98            &home_path,
99            command.options.no_cache,
100            command.options.no_local,
101            Some(network),
102            Some(&endpoint),
103        )?
104    } else {
105        Package::from_directory(
106            &package_path,
107            &home_path,
108            command.options.no_cache,
109            command.options.no_local,
110            Some(network),
111            Some(&endpoint),
112        )?
113    };
114
115    // Check the manifest for the compiler version.
116    // If it does not match, warn the user and continue.
117    if package.manifest.leo != env!("CARGO_PKG_VERSION") {
118        tracing::warn!(
119            "The Leo compiler version in the manifest ({}) does not match the current version ({}).",
120            package.manifest.leo,
121            env!("CARGO_PKG_VERSION")
122        );
123    }
124
125    let outputs_directory = package.outputs_directory();
126    let build_directory = package.build_directory();
127    let imports_directory = package.imports_directory();
128    let source_directory = package.source_directory();
129    let main_source_path = source_directory.join("main.leo");
130
131    for dir in [&outputs_directory, &build_directory, &imports_directory] {
132        std::fs::create_dir_all(dir).map_err(|err| {
133            UtilError::util_file_io_error(format_args!("Couldn't create directory {}", dir.display()), err)
134        })?;
135    }
136
137    // Initialize error handler.
138    let handler = Handler::default();
139
140    let mut stubs: IndexMap<Symbol, Stub> = IndexMap::new();
141
142    for program in package.programs.iter() {
143        let (bytecode, build_path) = match &program.data {
144            leo_package::ProgramData::Bytecode(bytecode) => {
145                // This was a network dependency or local .aleo dependency, and we have its bytecode.
146                (bytecode.clone(), imports_directory.join(format!("{}.aleo", program.name)))
147            }
148            leo_package::ProgramData::SourcePath { directory, source } => {
149                // This is a local dependency, so we must compile it.
150                let build_path = if source == &main_source_path {
151                    build_directory.join("main.aleo")
152                } else {
153                    imports_directory.join(format!("{}.aleo", program.name))
154                };
155                // Load the manifest in local dependency.
156                let source_dir = directory.join("src");
157                let bytecode = compile_leo_source_directory(
158                    source, // entry file
159                    &source_dir,
160                    program.name,
161                    program.is_test,
162                    &outputs_directory,
163                    &handler,
164                    command.options.clone(),
165                    stubs.clone(),
166                    network,
167                )?;
168                (bytecode, build_path)
169            }
170        };
171
172        // Write the .aleo file.
173        std::fs::write(build_path, &bytecode).map_err(CliError::failed_to_load_instructions)?;
174
175        // Track the Stub.
176        let stub = match network {
177            NetworkName::MainnetV0 => leo_disassembler::disassemble_from_str::<MainnetV0>(program.name, &bytecode),
178            NetworkName::TestnetV0 => leo_disassembler::disassemble_from_str::<TestnetV0>(program.name, &bytecode),
179            NetworkName::CanaryV0 => leo_disassembler::disassemble_from_str::<CanaryV0>(program.name, &bytecode),
180        }?;
181        stubs.insert(program.name, stub);
182    }
183
184    // SnarkVM expects to find a `program.json` file in the build directory, so make
185    // a bogus one.
186    let build_manifest_path = build_directory.join(leo_package::MANIFEST_FILENAME);
187    let fake_manifest = Manifest {
188        program: package.manifest.program.clone(),
189        version: "0.1.0".to_string(),
190        description: String::new(),
191        license: String::new(),
192        leo: env!("CARGO_PKG_VERSION").to_string(),
193        dependencies: None,
194        dev_dependencies: None,
195    };
196    fake_manifest.write_to_file(build_manifest_path)?;
197
198    Ok(package)
199}
200
201/// Compiles a Leo file. Writes and returns the compiled bytecode.
202#[allow(clippy::too_many_arguments)]
203fn compile_leo_source_directory(
204    entry_file_path: &Path,
205    source_directory: &Path,
206    program_name: Symbol,
207    is_test: bool,
208    output_path: &Path,
209    handler: &Handler,
210    options: BuildOptions,
211    stubs: IndexMap<Symbol, Stub>,
212    network: NetworkName,
213) -> Result<String> {
214    // Create a new instance of the Leo compiler.
215    let mut compiler = Compiler::new(
216        Some(program_name.to_string()),
217        is_test,
218        handler.clone(),
219        output_path.to_path_buf(),
220        Some(options.into()),
221        stubs,
222        network,
223    );
224
225    // Compile the Leo program into Aleo instructions.
226    let bytecode = compiler.compile_from_directory(entry_file_path, source_directory)?;
227
228    // Check the program size limit.
229    use leo_package::MAX_PROGRAM_SIZE;
230    let program_size = bytecode.len();
231
232    if program_size > MAX_PROGRAM_SIZE {
233        return Err(leo_errors::LeoError::UtilError(UtilError::program_size_limit_exceeded(
234            program_name,
235            program_size,
236            MAX_PROGRAM_SIZE,
237        )));
238    }
239
240    // Get the AVM bytecode.
241    let checksum: String = match network {
242        NetworkName::MainnetV0 => Program::<MainnetV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
243        NetworkName::TestnetV0 => Program::<TestnetV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
244        NetworkName::CanaryV0 => Program::<CanaryV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
245    };
246
247    tracing::info!("    {} statements before dead code elimination.", compiler.statements_before_dce);
248    tracing::info!("    {} statements after dead code elimination.", compiler.statements_after_dce);
249    tracing::info!("    The program checksum is: '[{checksum}]'.");
250
251    tracing::info!("✅ Compiled '{program_name}.aleo' into Aleo instructions.");
252    Ok(bytecode)
253}