Skip to main content

leo_lang/cli/commands/
build.rs

1// Copyright (C) 2019-2026 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, NodeBuilder, Program, Stub, TypeInterner};
20use leo_compiler::{Compiled, Compiler, CompilerOptions};
21use leo_package::{ABI_FILENAME, Package};
22use leo_span::Symbol;
23
24use snarkvm::prelude::{CanaryV0, MainnetV0, Process as SvmProcess, Program as SvmProgram, TestnetV0};
25
26use indexmap::IndexMap;
27use itertools::Itertools;
28use std::{
29    collections::HashSet,
30    path::{Path, PathBuf},
31    rc::Rc,
32};
33
34/// Network-typed `Process` used during the disassemble loop. The variants let
35/// a single loop body reuse a process across iterations without triplicating
36/// the body for each network. Held for the duration of the build's stub
37/// resolution, then dropped (a separate `Process` is loaded later for the
38/// post-build bulk validation in `validate_compiled_programs_inner`).
39enum DisassembleProcess {
40    Mainnet(SvmProcess<MainnetV0>),
41    Testnet(SvmProcess<TestnetV0>),
42    Canary(SvmProcess<CanaryV0>),
43}
44
45/// A program queued for bytecode validation after the build.
46struct ProgramForValidation {
47    /// The Aleo bytecode.
48    bytecode: String,
49    /// Path to the bytecode file on disk, used for error reporting.
50    path: PathBuf,
51    /// Whether the program was compiled from Leo source (`true`) or loaded as external bytecode (`false`).
52    is_leo_compiled: bool,
53}
54
55impl From<BuildOptions> for CompilerOptions {
56    fn from(options: BuildOptions) -> Self {
57        Self { no_std: options.no_std }
58    }
59}
60
61/// Compile and build program command.
62#[derive(Parser, Debug)]
63pub struct LeoBuild {
64    #[clap(flatten)]
65    pub(crate) options: BuildOptions,
66    #[clap(flatten)]
67    pub(crate) env_override: EnvOptions,
68    /// Recompile the primary program under a different on-chain name. Set internally
69    /// by `leo deploy --rename`; not exposed as a build flag.
70    #[clap(skip)]
71    pub(crate) rename: Option<String>,
72}
73
74impl Command for LeoBuild {
75    type Input = ();
76    type Output = Package;
77
78    fn log_span(&self) -> Span {
79        tracing::span!(tracing::Level::INFO, "Leo")
80    }
81
82    fn prelude(&self, _: Context) -> Result<Self::Input> {
83        Ok(())
84    }
85
86    fn apply(self, context: Context, _: Self::Input) -> Result<Self::Output> {
87        match context.resolve_targets()? {
88            Some((_, targets)) => {
89                let mut last_package = None;
90                for target in &targets {
91                    let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
92                    if targets.len() > 1 {
93                        println!("\n--- workspace member '{member_name}' ---");
94                    }
95                    let member_ctx = context.with_path(target.clone());
96                    last_package = Some(handle_build(&self, member_ctx)?);
97                }
98                last_package.ok_or_else(|| crate::errors::custom("No workspace members found.").into())
99            }
100            None => handle_build(&self, context),
101        }
102    }
103}
104
105// A helper function to handle the build command.
106fn handle_build(command: &LeoBuild, context: Context) -> Result<<LeoBuild as Command>::Output> {
107    // Get the package path and home directory.
108    let package_path = context.dir()?;
109    let home_path = context.home()?;
110
111    // Get the network, defaulting to `TestnetV0` if none is specified.
112    let network = match get_network(&command.env_override.network) {
113        Ok(network) => network,
114        Err(_) => {
115            println!("⚠️ No network specified, defaulting to 'testnet'.");
116            NetworkName::TestnetV0
117        }
118    };
119
120    // Get the endpoint, if it is provided.
121    let endpoint = match get_endpoint(&command.env_override.endpoint) {
122        Ok(endpoint) => endpoint,
123        Err(_) => {
124            println!("⚠️ No endpoint specified, defaulting to '{}'.", DEFAULT_ENDPOINT);
125            DEFAULT_ENDPOINT.to_string()
126        }
127    };
128
129    let mut package = if command.options.build_tests {
130        Package::from_directory_with_tests(
131            &package_path,
132            &home_path,
133            command.options.no_cache,
134            command.options.no_local,
135            command.options.offline,
136            Some(network),
137            Some(&endpoint),
138            command.env_override.network_retries,
139        )?
140    } else {
141        Package::from_directory(
142            &package_path,
143            &home_path,
144            command.options.no_cache,
145            command.options.no_local,
146            command.options.offline,
147            Some(network),
148            Some(&endpoint),
149            command.env_override.network_retries,
150        )?
151    };
152
153    // Check the manifest for the compiler version.
154    // If it does not match, warn the user and continue.
155    if package.manifest.leo != env!("CARGO_PKG_VERSION") {
156        tracing::warn!(
157            "The Leo compiler version in the manifest ({}) does not match the current version ({}).",
158            package.manifest.leo,
159            env!("CARGO_PKG_VERSION")
160        );
161    }
162
163    let build_directory = package.build_directory();
164    let source_directory = package.source_directory();
165    let main_source_path = source_directory.join("main.leo");
166
167    // Resolve via the manifest so this isn't a test unit under `--build-tests`.
168    let primary_name = package.primary_unit().map(|p| p.name);
169
170    // `leo deploy --rename`: recompile the primary program under a different on-chain name.
171    let rename_target = apply_rename(command, &mut package, primary_name)?;
172
173    std::fs::create_dir_all(&build_directory).map_err(|err| {
174        crate::errors::util_file_io_error(format_args!("Couldn't create directory {}", build_directory.display()), err)
175    })?;
176    // Clear artifacts from the pre-flat-layout build directory so they don't
177    // linger beside the new per-program directories after an upgrade.
178    remove_legacy_build_artifacts(&build_directory);
179
180    // Initialize error handler.
181    let handler = Handler::default();
182    let node_builder = Rc::new(NodeBuilder::default());
183
184    // Manifest opt-out for the implicit `std` library. Propagated to every
185    // unit's `Compiler` via `CompilerOptions::no_std`.
186    let mut build_options = command.options.clone();
187    build_options.no_std = package.manifest.no_std;
188
189    let mut stubs: IndexMap<Symbol, Stub> = IndexMap::new();
190
191    // Prebuild the implicit `std` library once. Every per-unit `Compiler` clones `stubs`,
192    // so the prebuilt stub is shared across the build instead of recompiled per unit.
193    // `inject_std_library` short-circuits when it finds this entry.
194    if !build_options.no_std {
195        let std_stub = Compiler::build_std_stub(
196            handler.clone(),
197            Rc::clone(&node_builder),
198            network,
199            Rc::new(TypeInterner::default()),
200        )?;
201        stubs.insert(Symbol::intern(leo_std::library_name()), std_stub);
202    }
203
204    // All programs to validate through snarkVM's bytecode validator, in dependency order
205    // (imports must be loaded before the programs that depend on them).
206    let mut compiled_programs: IndexMap<String, ProgramForValidation> = IndexMap::new();
207
208    // Tracks compilation units whose build artifacts have already been written this
209    // build, so each unit is written exactly once - by its first (most authoritative)
210    // compilation. The package's own program is compiled before its tests, so its
211    // primary build is kept rather than a test's re-derived import copy.
212    let mut written: HashSet<String> = HashSet::new();
213
214    // Hoist a typed `Process` for the active network so bytecode dependencies are
215    // validated through `Process::add_program` *before* `disassemble` runs on them
216    // (issue #29399 — `from_function_core` panics on shapes the grammar accepts).
217    // `add_program` is contextual, so the same process must be reused across the
218    // loop in topological order; the enum lets the network-typed process live
219    // across iterations without triplicating the loop body.
220    let mut disassemble_process = match network {
221        NetworkName::MainnetV0 => DisassembleProcess::Mainnet(SvmProcess::<MainnetV0>::load().map_err(|e| {
222            crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
223        })?),
224        NetworkName::TestnetV0 => DisassembleProcess::Testnet(SvmProcess::<TestnetV0>::load().map_err(|e| {
225            crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
226        })?),
227        NetworkName::CanaryV0 => DisassembleProcess::Canary(SvmProcess::<CanaryV0>::load().map_err(|e| {
228            crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
229        })?),
230    };
231
232    for unit in &package.compilation_units {
233        let unit_name = unit.name.to_string();
234        // Normalize key so `"foo.aleo"` and `"foo"` don't collide on the same dir.
235        let unit_key = leo_package::bare_unit_name(&unit_name).to_string();
236        match &unit.data {
237            leo_package::ProgramData::Bytecode(bytecode) => {
238                // This was a network dependency or local .aleo dependency, and we have its bytecode.
239                let build_path = package.unit_bytecode_path(&unit_name);
240
241                // Write the .aleo file into the program's own build directory.
242                if written.insert(unit_key.clone()) {
243                    ensure_parent_dir(&build_path)?;
244                    std::fs::write(&build_path, bytecode).map_err(crate::errors::failed_to_load_instructions)?;
245                }
246
247                // Track the stub. Validates via `Process::add_program` and disassembles in
248                // one step; the hoisted process accumulates dependencies across iterations.
249                let stub = match &mut disassemble_process {
250                    DisassembleProcess::Mainnet(p) => {
251                        leo_disassembler::disassemble_from_str::<MainnetV0>(unit.name, bytecode, p)
252                    }
253                    DisassembleProcess::Testnet(p) => {
254                        leo_disassembler::disassemble_from_str::<TestnetV0>(unit.name, bytecode, p)
255                    }
256                    DisassembleProcess::Canary(p) => {
257                        leo_disassembler::disassemble_from_str::<CanaryV0>(unit.name, bytecode, p)
258                    }
259                }?;
260
261                stubs.insert(unit.name, stub.into());
262
263                compiled_programs.entry(unit_key.clone()).or_insert(ProgramForValidation {
264                    bytecode: bytecode.clone(),
265                    path: build_path,
266                    is_leo_compiled: false,
267                });
268            }
269
270            leo_package::ProgramData::SourcePath { directory, source } => {
271                // This is a local dependency, so we must compile or parse it.
272                let source_dir = if unit.kind.is_test() {
273                    source
274                        .parent()
275                        .ok_or_else(|| {
276                            crate::errors::failed_to_open_file(format_args!(
277                                "Failed to find directory for test {}",
278                                source.display()
279                            ))
280                        })?
281                        .to_path_buf()
282                } else {
283                    directory.join("src")
284                };
285
286                let is_main = source == &main_source_path;
287                if is_main || unit.kind.is_test() {
288                    // Compile the program (main or test).
289                    let compiled = compile_leo_source_directory(
290                        source, // entry file
291                        &source_dir,
292                        unit.name,
293                        unit.kind.is_test(),
294                        &handler,
295                        &node_builder,
296                        build_options.clone(),
297                        stubs.clone(),
298                        network,
299                        if is_main { rename_target.clone() } else { None },
300                    )?;
301
302                    // Write this unit's compiled bytecode. ABI and interface ABIs are
303                    // emitted only for the main program; tests deliberately skip them.
304                    let primary_path = package.unit_bytecode_path(&unit_name);
305                    if written.insert(unit_key.clone()) {
306                        ensure_parent_dir(&primary_path)?;
307                        std::fs::write(&primary_path, &compiled.primary.bytecode)
308                            .map_err(crate::errors::failed_to_load_instructions)?;
309                        if is_main {
310                            let abi_path = package.unit_abi_path(&unit_name);
311                            let abi_json = serde_json::to_string_pretty(&compiled.primary.abi)
312                                .map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
313                            std::fs::write(&abi_path, abi_json).map_err(crate::errors::failed_to_write_abi)?;
314                            tracing::info!("✅ Generated ABI for program '{unit_name}'.");
315                            let interfaces_directory = package.unit_interfaces_directory(&unit_name);
316                            write_interface_abis(&interfaces_directory, &compiled.interfaces)?;
317                        }
318                    }
319
320                    // Write each import's bytecode and ABI into its own build directory.
321                    for import in &compiled.imports {
322                        let import_path = package.unit_bytecode_path(&import.name);
323                        let import_key = leo_package::bare_unit_name(&import.name).to_string();
324                        if written.insert(import_key.clone()) {
325                            ensure_parent_dir(&import_path)?;
326                            std::fs::write(&import_path, &import.bytecode)
327                                .map_err(crate::errors::failed_to_load_instructions)?;
328
329                            let import_abi_path = package.unit_abi_path(&import.name);
330                            let import_abi_json = serde_json::to_string_pretty(&import.abi)
331                                .map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
332                            std::fs::write(&import_abi_path, import_abi_json)
333                                .map_err(crate::errors::failed_to_write_abi)?;
334                        }
335
336                        // Queue import for validation.
337                        compiled_programs.entry(import_key).or_insert(ProgramForValidation {
338                            bytecode: import.bytecode.clone(),
339                            path: import_path,
340                            is_leo_compiled: true,
341                        });
342                    }
343                    // Queue the primary program.
344                    compiled_programs.entry(unit_key.clone()).or_insert(ProgramForValidation {
345                        bytecode: compiled.primary.bytecode.clone(),
346                        path: primary_path,
347                        is_leo_compiled: true,
348                    });
349                }
350
351                if unit.kind.is_library() {
352                    // The primary library runs the full frontend (name validation through static
353                    // analysis) so type errors, undefined names, and interface cycles are caught
354                    // even when no downstream program consumes the library. Non-primary library
355                    // dependencies are parsed only; their semantics are validated when their own
356                    // `leo build` is run.
357                    let library = if primary_name == Some(unit.name) {
358                        let (lib, interfaces) = build_leo_source_directory_library(
359                            source,
360                            &source_dir,
361                            unit.name,
362                            &handler,
363                            &node_builder,
364                            build_options.clone(),
365                            stubs.clone(),
366                            network,
367                        )?;
368
369                        // Write interface ABIs for the primary library.
370                        let interfaces_directory = package.unit_interfaces_directory(&unit_name);
371                        write_interface_abis(&interfaces_directory, &interfaces)?;
372
373                        lib
374                    } else {
375                        parse_leo_source_directory_library(
376                            source,
377                            &source_dir,
378                            unit.name,
379                            &handler,
380                            &node_builder,
381                            build_options.clone(),
382                            network,
383                        )?
384                    };
385                    // Bail out if any errors were collected (parse errors produce ErrExpression
386                    // nodes that would panic in later passes, and frontend-pass errors surface here).
387                    handler.last_err()?;
388                    // Compute parents from dep_graph
389                    let mut library_stub: Stub = library.into();
390                    for node in package.dep_graph.nodes() {
391                        if package.dep_graph.neighbors(node).any(|dep| dep == &unit.name) {
392                            library_stub.add_parent(*node);
393                        }
394                    }
395                    stubs.insert(unit.name, library_stub);
396                } else if !unit.kind.is_test() {
397                    // Build a stub for the primary program and intermediate dependencies so other
398                    // programs can resolve their imports; the primary's stub adopts the rename too,
399                    // or its parse would fail the name check. Tests are excluded: nothing imports a
400                    // test, so it needs no stub.
401                    let leo_program = parse_leo_source_directory(
402                        source,
403                        &source_dir,
404                        unit.name,
405                        &handler,
406                        &node_builder,
407                        build_options.clone(),
408                        network,
409                        if is_main { rename_target.clone() } else { None },
410                    )?;
411
412                    stubs.insert(unit.name, leo_program.into());
413                }
414            }
415        }
416    }
417
418    // Ensure every program unit has on-disk bytecode for downstream commands; the
419    // parse-only path above skips manifest deps the main source doesn't `import`.
420    for unit in &package.compilation_units {
421        if !unit.kind.is_program() || unit.kind.is_test() {
422            continue;
423        }
424        let leo_package::ProgramData::SourcePath { directory, source } = &unit.data else { continue };
425        let unit_name = unit.name.to_string();
426        let unit_key = leo_package::bare_unit_name(&unit_name).to_string();
427        if !written.insert(unit_key.clone()) {
428            continue;
429        }
430        let source_dir = directory.join("src");
431        let compiled = compile_leo_source_directory(
432            source,
433            &source_dir,
434            unit.name,
435            false,
436            &handler,
437            &node_builder,
438            build_options.clone(),
439            stubs.clone(),
440            network,
441            // Dependencies are never renamed; only the primary deploy target is.
442            None,
443        )?;
444        let primary_path = package.unit_bytecode_path(&unit_name);
445        ensure_parent_dir(&primary_path)?;
446        std::fs::write(&primary_path, &compiled.primary.bytecode)
447            .map_err(crate::errors::failed_to_load_instructions)?;
448        let abi_path = package.unit_abi_path(&unit_name);
449        let abi_json = serde_json::to_string_pretty(&compiled.primary.abi)
450            .map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
451        std::fs::write(&abi_path, abi_json).map_err(crate::errors::failed_to_write_abi)?;
452        let interfaces_directory = package.unit_interfaces_directory(&unit_name);
453        write_interface_abis(&interfaces_directory, &compiled.interfaces)?;
454        compiled_programs.entry(unit_key).or_insert(ProgramForValidation {
455            bytecode: compiled.primary.bytecode.clone(),
456            path: primary_path,
457            is_leo_compiled: true,
458        });
459    }
460
461    // Validate generated bytecode through snarkVM's type checker.
462    validate_compiled_programs(&compiled_programs, network)?;
463
464    Ok(package)
465}
466
467/// Resolves `leo deploy --rename` and applies it to `package`.
468///
469/// Returns `Ok(None)` when no rename was requested. Otherwise validates the
470/// requested name, rejects conflicts, and rewrites the primary unit's name in
471/// `package` so the build artifacts, the deploy read path, and the compiled
472/// bytecode all share the renamed identity; the canonical `.aleo`-suffixed name
473/// to compile under is returned. Programs that import the original name are
474/// intentionally not redirected to the renamed copy.
475fn apply_rename(command: &LeoBuild, package: &mut Package, primary_name: Option<Symbol>) -> Result<Option<String>> {
476    let Some(requested) = &command.rename else {
477        return Ok(None);
478    };
479
480    // `--rename` rewrites a single primary program, so it cannot apply to a test build:
481    // tests keep their original names and would dangle against the renamed primary.
482    if command.options.build_tests {
483        return Err(crate::errors::custom("`--rename` cannot be combined with `--build-tests`.").into());
484    }
485
486    let renamed = leo_package::canonicalize_program_name(requested);
487    if !leo_package::is_valid_program_name(&renamed) {
488        return Err(crate::errors::custom(format!(
489            "Invalid program name '{requested}' for `--rename`; expected a valid Aleo program name."
490        ))
491        .into());
492    }
493
494    let Some(original) = primary_name else {
495        return Err(crate::errors::custom("`--rename` requires a primary program to rename.").into());
496    };
497
498    // Compare on the bare name: a local primary's name is bare while the target is
499    // `.aleo`-suffixed, so a direct symbol comparison would never flag a no-op rename.
500    let renamed_bare = leo_package::bare_unit_name(&renamed);
501    if renamed_bare == leo_package::bare_unit_name(&original.to_string()) {
502        return Err(crate::errors::custom(format!(
503            "`--rename` target '{renamed}' is identical to the program's current name."
504        ))
505        .into());
506    }
507
508    // Reject renaming onto a name already used by another unit or dependency. Build
509    // artifacts are keyed by bare unit name, so a collision would silently discard the
510    // renamed program and deploy the colliding unit's bytecode instead. Compare on the
511    // bare name to match that keying.
512    if package
513        .compilation_units
514        .iter()
515        .any(|unit| unit.name != original && leo_package::bare_unit_name(&unit.name.to_string()) == renamed_bare)
516    {
517        return Err(crate::errors::custom(format!(
518            "`--rename` target '{renamed}' conflicts with an existing program or dependency in this package; choose a different name."
519        ))
520        .into());
521    }
522
523    let renamed_symbol = Symbol::intern(&renamed);
524    for unit in package.compilation_units.iter_mut() {
525        if !unit.kind.is_test() && unit.name == original {
526            unit.name = renamed_symbol;
527        }
528    }
529
530    Ok(Some(renamed))
531}
532
533/// Collects the program checksum and each entry/view function checksum (the `std::prog::function_checksum` targets).
534fn collect_checksums<N: snarkvm::prelude::Network>(program: &SvmProgram<N>) -> BuildOutput {
535    let mut function_checksums = IndexMap::with_capacity(program.functions().len() + program.views().len());
536    for (name, function) in program.functions() {
537        function_checksums.insert(name.to_string(), function.to_checksum().iter().map(|b| **b).collect());
538    }
539    for (name, view) in program.views() {
540        function_checksums.insert(name.to_string(), view.to_checksum().iter().map(|b| **b).collect());
541    }
542    BuildOutput {
543        program: program.id().to_string(),
544        program_checksum: program.to_checksum().iter().map(|b| **b).collect(),
545        function_checksums,
546    }
547}
548
549/// Computes the checksums for compiled bytecode. Checksums are network-independent, so `network` only
550/// selects the `Program` type to parse into.
551fn program_checksums(network: NetworkName, bytecode: &str) -> Result<BuildOutput> {
552    Ok(match network {
553        NetworkName::MainnetV0 => collect_checksums(&SvmProgram::<MainnetV0>::from_str(bytecode)?),
554        NetworkName::TestnetV0 => collect_checksums(&SvmProgram::<TestnetV0>::from_str(bytecode)?),
555        NetworkName::CanaryV0 => collect_checksums(&SvmProgram::<CanaryV0>::from_str(bytecode)?),
556    })
557}
558
559/// Builds the `leo build` JSON output from the primary program's compiled bytecode on disk.
560pub fn build_output(package: &Package, network: Option<NetworkName>) -> Result<BuildOutput> {
561    let network = get_network(&network).unwrap_or(NetworkName::TestnetV0);
562    let unit =
563        package.primary_unit().ok_or_else(|| crate::errors::custom("No primary program found in the package."))?;
564    let name = unit.name.to_string();
565    let bytecode = std::fs::read_to_string(package.unit_bytecode_path(&name)).map_err(|err| {
566        crate::errors::util_file_io_error(format_args!("Trying to read compiled bytecode for `{name}`"), err)
567    })?;
568    program_checksums(network, &bytecode)
569}
570
571/// Compiles a Leo file. Writes and returns the compiled bytecode and ABI.
572#[allow(clippy::too_many_arguments)]
573fn compile_leo_source_directory(
574    entry_file_path: &Path,
575    source_directory: &Path,
576    program_name: Symbol,
577    is_test: bool,
578    handler: &Handler,
579    node_builder: &Rc<NodeBuilder>,
580    options: BuildOptions,
581    stubs: IndexMap<Symbol, Stub>,
582    network: NetworkName,
583    rename: Option<String>,
584) -> Result<Compiled> {
585    if !is_test {
586        println!();
587    }
588    tracing::info!("🔨 Compiling '{program_name}'");
589    // Capture before `options` is consumed by the conversion below.
590    let print_checksums = options.checksums;
591    // Create a new instance of the Leo compiler.
592    let mut compiler = Compiler::new(
593        Some(program_name.to_string()),
594        is_test,
595        handler.clone(),
596        Rc::clone(node_builder),
597        Some(options.into()),
598        stubs,
599        network,
600    );
601    // When set, recompile the program scope under this on-chain name (`leo deploy --rename`).
602    compiler.rename = rename;
603
604    // Compile the Leo program into Aleo instructions. A test is a single standalone file:
605    // its siblings in `tests/` are independent test programs, not modules to fold in.
606    let compiled = if is_test {
607        compiler.compile_from_file(entry_file_path)?
608    } else {
609        compiler.compile_from_directory(entry_file_path, source_directory)?
610    };
611    let primary_bytecode = &compiled.primary.bytecode;
612
613    // Check the program size limit for each bytecode.
614    use leo_package::MAX_PROGRAM_SIZE;
615    let program_size = primary_bytecode.len();
616
617    if program_size > MAX_PROGRAM_SIZE {
618        return Err(crate::errors::program_size_limit_exceeded(program_name, program_size, MAX_PROGRAM_SIZE).into());
619    }
620
621    if print_checksums {
622        let checksums = program_checksums(network, primary_bytecode)?;
623        let format = |bytes: &[u8]| bytes.iter().map(|b| format!("{b}u8")).join(", ");
624        tracing::info!("    The program checksum is: '[{}]'.", format(&checksums.program_checksum));
625        for (name, function_checksum) in &checksums.function_checksums {
626            tracing::info!("      `{name}` function checksum is: '[{}]'.", format(function_checksum));
627        }
628    }
629
630    let (size_kb, max_kb, warning) = format_program_size(program_size, MAX_PROGRAM_SIZE);
631    if let Some(msg) = warning {
632        tracing::warn!("⚠️  Program '{program_name}' is {msg}.");
633    }
634
635    if !is_test {
636        tracing::info!("    Program size: {size_kb:.2} KB / {max_kb:.2} KB");
637        tracing::info!("✅ Compiled '{program_name}' into Aleo instructions.");
638    }
639
640    // Print import checksums only when explicitly requested.
641    if print_checksums {
642        for import in &compiled.imports {
643            // Compute checksum depending on network.
644            let dep_checksum: String = match network {
645                NetworkName::MainnetV0 => {
646                    SvmProgram::<MainnetV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
647                }
648                NetworkName::TestnetV0 => {
649                    SvmProgram::<TestnetV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
650                }
651                NetworkName::CanaryV0 => {
652                    SvmProgram::<CanaryV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
653                }
654            };
655
656            tracing::info!("    Import '{}': checksum = '[{dep_checksum}]'", import.name);
657        }
658    }
659
660    // Import sizes track the primary program size rather than the checksums:
661    // both answer "will this fit on chain", so they print together.
662    if !is_test {
663        for import in &compiled.imports {
664            let import_size = import.bytecode.len();
665            let (size_kb, max_kb, _warning) = format_program_size(import_size, MAX_PROGRAM_SIZE);
666            tracing::info!("    Import '{}': program size: {size_kb:.2} KB / {max_kb:.2} KB", import.name);
667        }
668    }
669
670    Ok(compiled)
671}
672
673/// Parses a Leo file into an AST without generating bytecode.
674#[allow(clippy::too_many_arguments)]
675fn parse_leo_source_directory(
676    entry_file_path: &Path,
677    source_directory: &Path,
678    program_name: Symbol,
679    handler: &Handler,
680    node_builder: &Rc<NodeBuilder>,
681    options: BuildOptions,
682    network: NetworkName,
683    rename: Option<String>,
684) -> Result<Program> {
685    // Create a new instance of the Leo compiler.
686    let mut compiler = Compiler::new(
687        Some(program_name.to_string()),
688        false,
689        handler.clone(),
690        Rc::clone(node_builder),
691        Some(options.into()),
692        IndexMap::new(),
693        network,
694    );
695    // When set, rewrite the parsed program scope under this on-chain name so the stub
696    // matches the renamed identity (`leo deploy --rename`).
697    compiler.rename = rename;
698
699    // Parse the Leo program into an AST.
700    compiler.parse_program_from_directory(entry_file_path, source_directory)
701}
702
703/// Validates compiled Aleo bytecode by loading all programs into a snarkVM `Process`.
704/// Note that the programs must be provided in dependency order.
705fn validate_compiled_programs(programs: &IndexMap<String, ProgramForValidation>, network: NetworkName) -> Result<()> {
706    match network {
707        NetworkName::MainnetV0 => validate_compiled_programs_inner::<MainnetV0>(programs),
708        NetworkName::TestnetV0 => validate_compiled_programs_inner::<TestnetV0>(programs),
709        NetworkName::CanaryV0 => validate_compiled_programs_inner::<CanaryV0>(programs),
710    }
711}
712
713/// Network-generic implementation of [`validate_compiled_programs`].
714fn validate_compiled_programs_inner<N: snarkvm::prelude::Network>(
715    programs: &IndexMap<String, ProgramForValidation>,
716) -> Result<()> {
717    let process = SvmProcess::<N>::load().map_err(|e| {
718        crate::errors::custom(format!("Failed to initialize snarkVM process for bytecode validation: {e}"))
719    })?;
720
721    for (name, ProgramForValidation { bytecode, path, is_leo_compiled }) in programs {
722        let program =
723            SvmProgram::<N>::from_str(bytecode).map_err(|e| crate::errors::failed_to_parse_aleo_file(name, e))?;
724
725        let checksum = program.to_checksum().iter().join(", ");
726
727        process.lock().add_program_with_edition(&program, LOCAL_PROGRAM_DEFAULT_EDITION).map_err(|e| {
728            if *is_leo_compiled {
729                crate::errors::generated_invalid_bytecode(name, path.display(), &checksum, e)
730            } else {
731                crate::errors::custom(format!(
732                    "snarkVM rejected external program '{name}' during build validation: {e}"
733                ))
734            }
735        })?;
736    }
737
738    Ok(())
739}
740
741/// Parses a Leo file into an AST without generating bytecode.
742fn parse_leo_source_directory_library(
743    entry_file_path: &Path,
744    source_directory: &Path,
745    library_name: Symbol,
746    handler: &Handler,
747    node_builder: &Rc<NodeBuilder>,
748    options: BuildOptions,
749    network: NetworkName,
750) -> Result<leo_ast::Library> {
751    // Create a new instance of the Leo compiler.
752    let mut compiler = Compiler::new(
753        Some(library_name.to_string()),
754        false,
755        handler.clone(),
756        Rc::clone(node_builder),
757        Some(options.into()),
758        IndexMap::new(),
759        network,
760    );
761
762    // Parse the Leo program into an AST.
763    compiler.parse_library_from_directory(library_name, entry_file_path, source_directory)
764}
765
766/// Builds a library by running all frontend passes. Does not generate bytecode.
767/// Returns the validated library and any interface ABIs defined in it.
768#[allow(clippy::too_many_arguments)]
769fn build_leo_source_directory_library(
770    entry_file_path: &Path,
771    source_directory: &Path,
772    library_name: Symbol,
773    handler: &Handler,
774    node_builder: &Rc<NodeBuilder>,
775    options: BuildOptions,
776    stubs: IndexMap<Symbol, Stub>,
777    network: NetworkName,
778) -> Result<(leo_ast::Library, Vec<leo_abi::interfaces::CompiledInterface>)> {
779    // Print a newline for better formatting.
780    println!();
781    tracing::info!("🔨 Building library '{library_name}'");
782
783    let mut compiler = Compiler::new(
784        Some(library_name.to_string()),
785        false,
786        handler.clone(),
787        Rc::clone(node_builder),
788        Some(options.into()),
789        stubs,
790        network,
791    );
792
793    let library = compiler.build_library_from_directory(library_name, entry_file_path, source_directory)?;
794    let interfaces = compiler.generate_library_interface_abis();
795
796    tracing::info!("✅ Validated '{library_name}'.");
797
798    Ok((library, interfaces))
799}
800
801/// Writes interface ABI JSON files to the interfaces directory.
802fn write_interface_abis(interfaces_dir: &Path, interfaces: &[leo_abi::interfaces::CompiledInterface]) -> Result<()> {
803    // Remove stale files from a previous build (renamed/deleted interfaces).
804    if interfaces_dir.exists() {
805        std::fs::remove_dir_all(interfaces_dir).map_err(crate::errors::failed_to_write_abi)?;
806    }
807    if interfaces.is_empty() {
808        return Ok(());
809    }
810    for ci in interfaces {
811        let mut file_path = match &ci.owner {
812            leo_abi::interfaces::InterfaceOwner::Local => interfaces_dir.to_path_buf(),
813            leo_abi::interfaces::InterfaceOwner::External { owner_program } => interfaces_dir.join(owner_program),
814        };
815        // Module path segments (everything except the final name).
816        for seg in &ci.abi.path[..ci.abi.path.len().saturating_sub(1)] {
817            file_path.push(seg);
818        }
819        std::fs::create_dir_all(&file_path).map_err(crate::errors::failed_to_write_abi)?;
820        file_path.push(format!("{}.json", ci.abi.name));
821        let json =
822            serde_json::to_string_pretty(&ci.abi).map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
823        std::fs::write(&file_path, json).map_err(crate::errors::failed_to_write_abi)?;
824    }
825    tracing::info!("✅ Generated {} interface ABI(s).", interfaces.len());
826    Ok(())
827}
828
829/// Ensure the parent directory of `path` exists, creating it if necessary.
830fn ensure_parent_dir(path: &Path) -> Result<()> {
831    if let Some(parent) = path.parent() {
832        std::fs::create_dir_all(parent).map_err(|err| {
833            crate::errors::util_file_io_error(format_args!("Couldn't create directory {}", parent.display()), err)
834        })?;
835    }
836    Ok(())
837}
838
839/// Remove artifacts left by the pre-flat-layout build directory (`build/main.aleo`,
840/// `build/imports/`, etc.) so they don't linger beside the new per-program
841/// directories after an upgrade. Best-effort: missing entries are ignored.
842///
843/// Gated on a top-level legacy artifact so we don't wipe a perfectly valid new-layout
844/// per-unit directory for a user program literally named `imports` or `interfaces`.
845fn remove_legacy_build_artifacts(build_directory: &Path) {
846    let is_legacy =
847        build_directory.join("main.aleo").exists() || build_directory.join(leo_package::MANIFEST_FILENAME).exists();
848    if !is_legacy {
849        return;
850    }
851    for file in ["main.aleo", ABI_FILENAME, leo_package::MANIFEST_FILENAME] {
852        let _ = std::fs::remove_file(build_directory.join(file));
853    }
854    for dir in ["imports", "interfaces"] {
855        let _ = std::fs::remove_dir_all(build_directory.join(dir));
856    }
857}