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 = get_endpoint(&command.env_override.endpoint).ok();
88
89    let package = if command.options.build_tests {
90        Package::from_directory_with_tests(
91            &package_path,
92            &home_path,
93            command.options.no_cache,
94            command.options.no_local,
95            Some(network),
96            endpoint.as_deref(),
97        )?
98    } else {
99        Package::from_directory(
100            &package_path,
101            &home_path,
102            command.options.no_cache,
103            command.options.no_local,
104            Some(network),
105            endpoint.as_deref(),
106        )?
107    };
108
109    // Check the manifest for the compiler version.
110    // If it does not match, warn the user and continue.
111    if package.manifest.leo != env!("CARGO_PKG_VERSION") {
112        tracing::warn!(
113            "The Leo compiler version in the manifest ({}) does not match the current version ({}).",
114            package.manifest.leo,
115            env!("CARGO_PKG_VERSION")
116        );
117    }
118
119    let outputs_directory = package.outputs_directory();
120    let build_directory = package.build_directory();
121    let imports_directory = package.imports_directory();
122    let source_directory = package.source_directory();
123    let main_source_path = source_directory.join("main.leo");
124
125    for dir in [&outputs_directory, &build_directory, &imports_directory] {
126        std::fs::create_dir_all(dir).map_err(|err| {
127            UtilError::util_file_io_error(format_args!("Couldn't create directory {}", dir.display()), err)
128        })?;
129    }
130
131    // Initialize error handler.
132    let handler = Handler::default();
133
134    let mut stubs: IndexMap<Symbol, Stub> = IndexMap::new();
135
136    for program in package.programs.iter() {
137        let (bytecode, build_path) = match &program.data {
138            leo_package::ProgramData::Bytecode(bytecode) => {
139                // This was a network dependency or local .aleo dependency, and we have its bytecode.
140                (bytecode.clone(), imports_directory.join(format!("{}.aleo", program.name)))
141            }
142            leo_package::ProgramData::SourcePath { directory, source } => {
143                // This is a local dependency, so we must compile it.
144                let build_path = if source == &main_source_path {
145                    build_directory.join("main.aleo")
146                } else {
147                    imports_directory.join(format!("{}.aleo", program.name))
148                };
149                // Load the manifest in local dependency.
150                let source_dir = directory.join("src");
151                let bytecode = compile_leo_source_directory(
152                    source, // entry file
153                    &source_dir,
154                    program.name,
155                    program.is_test,
156                    &outputs_directory,
157                    &handler,
158                    command.options.clone(),
159                    stubs.clone(),
160                    network,
161                )?;
162                (bytecode, build_path)
163            }
164        };
165
166        // Write the .aleo file.
167        std::fs::write(build_path, &bytecode).map_err(CliError::failed_to_load_instructions)?;
168
169        // Track the Stub.
170        let stub = match network {
171            NetworkName::MainnetV0 => leo_disassembler::disassemble_from_str::<MainnetV0>(program.name, &bytecode),
172            NetworkName::TestnetV0 => leo_disassembler::disassemble_from_str::<TestnetV0>(program.name, &bytecode),
173            NetworkName::CanaryV0 => leo_disassembler::disassemble_from_str::<CanaryV0>(program.name, &bytecode),
174        }?;
175        stubs.insert(program.name, stub);
176    }
177
178    // SnarkVM expects to find a `program.json` file in the build directory, so make
179    // a bogus one.
180    let build_manifest_path = build_directory.join(leo_package::MANIFEST_FILENAME);
181    let fake_manifest = Manifest {
182        program: package.manifest.program.clone(),
183        version: "0.1.0".to_string(),
184        description: String::new(),
185        license: String::new(),
186        leo: env!("CARGO_PKG_VERSION").to_string(),
187        dependencies: None,
188        dev_dependencies: None,
189    };
190    fake_manifest.write_to_file(build_manifest_path)?;
191
192    Ok(package)
193}
194
195/// Compiles a Leo file. Writes and returns the compiled bytecode.
196#[allow(clippy::too_many_arguments)]
197fn compile_leo_source_directory(
198    entry_file_path: &Path,
199    source_directory: &Path,
200    program_name: Symbol,
201    is_test: bool,
202    output_path: &Path,
203    handler: &Handler,
204    options: BuildOptions,
205    stubs: IndexMap<Symbol, Stub>,
206    network: NetworkName,
207) -> Result<String> {
208    // Create a new instance of the Leo compiler.
209    let mut compiler = Compiler::new(
210        Some(program_name.to_string()),
211        is_test,
212        handler.clone(),
213        output_path.to_path_buf(),
214        Some(options.into()),
215        stubs,
216        network,
217    );
218
219    // Compile the Leo program into Aleo instructions.
220    let bytecode = compiler.compile_from_directory(entry_file_path, source_directory)?;
221
222    // Check the program size limit.
223    use leo_package::MAX_PROGRAM_SIZE;
224    let program_size = bytecode.len();
225
226    if program_size > MAX_PROGRAM_SIZE {
227        return Err(leo_errors::LeoError::UtilError(UtilError::program_size_limit_exceeded(
228            program_name,
229            program_size,
230            MAX_PROGRAM_SIZE,
231        )));
232    }
233
234    // Get the AVM bytecode.
235    let checksum: String = match network {
236        NetworkName::MainnetV0 => Program::<MainnetV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
237        NetworkName::TestnetV0 => Program::<TestnetV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
238        NetworkName::CanaryV0 => Program::<CanaryV0>::from_str(&bytecode)?.to_checksum().iter().join(", "),
239    };
240
241    tracing::info!("    {} statements before dead code elimination.", compiler.statements_before_dce);
242    tracing::info!("    {} statements after dead code elimination.", compiler.statements_after_dce);
243    tracing::info!("    The program checksum is: '[{checksum}]'.");
244
245    tracing::info!("✅ Compiled '{program_name}.aleo' into Aleo instructions.");
246    Ok(bytecode)
247}