use super::*;
use leo_ast::{NetworkName, NodeBuilder, Program, Stub, TypeInterner};
use leo_compiler::{Compiled, Compiler, CompilerOptions};
use leo_package::{ABI_FILENAME, Package};
use leo_span::Symbol;
use snarkvm::prelude::{CanaryV0, MainnetV0, Process as SvmProcess, Program as SvmProgram, TestnetV0};
use indexmap::IndexMap;
use itertools::Itertools;
use std::{
collections::HashSet,
path::{Path, PathBuf},
rc::Rc,
};
enum DisassembleProcess {
Mainnet(SvmProcess<MainnetV0>),
Testnet(SvmProcess<TestnetV0>),
Canary(SvmProcess<CanaryV0>),
}
struct ProgramForValidation {
bytecode: String,
path: PathBuf,
is_leo_compiled: bool,
}
impl From<BuildOptions> for CompilerOptions {
fn from(options: BuildOptions) -> Self {
Self { no_std: options.no_std }
}
}
#[derive(Parser, Debug)]
pub struct LeoBuild {
#[clap(flatten)]
pub(crate) options: BuildOptions,
#[clap(flatten)]
pub(crate) env_override: EnvOptions,
#[clap(skip)]
pub(crate) rename: Option<String>,
}
impl Command for LeoBuild {
type Input = ();
type Output = Package;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, _: Context) -> Result<Self::Input> {
Ok(())
}
fn apply(self, context: Context, _: Self::Input) -> Result<Self::Output> {
match context.resolve_targets()? {
Some((_, targets)) => {
let mut last_package = None;
for target in &targets {
let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
if targets.len() > 1 {
println!("\n--- workspace member '{member_name}' ---");
}
let member_ctx = context.with_path(target.clone());
last_package = Some(handle_build(&self, member_ctx)?);
}
last_package.ok_or_else(|| crate::errors::custom("No workspace members found.").into())
}
None => handle_build(&self, context),
}
}
}
fn handle_build(command: &LeoBuild, context: Context) -> Result<<LeoBuild as Command>::Output> {
let package_path = context.dir()?;
let home_path = context.home()?;
let network = match get_network(&command.env_override.network) {
Ok(network) => network,
Err(_) => {
println!("⚠️ No network specified, defaulting to 'testnet'.");
NetworkName::TestnetV0
}
};
let endpoint = match get_endpoint(&command.env_override.endpoint) {
Ok(endpoint) => endpoint,
Err(_) => {
println!("⚠️ No endpoint specified, defaulting to '{}'.", DEFAULT_ENDPOINT);
DEFAULT_ENDPOINT.to_string()
}
};
let mut package = if command.options.build_tests {
Package::from_directory_with_tests(
&package_path,
&home_path,
command.options.no_cache,
command.options.no_local,
command.options.offline,
Some(network),
Some(&endpoint),
command.env_override.network_retries,
)?
} else {
Package::from_directory(
&package_path,
&home_path,
command.options.no_cache,
command.options.no_local,
command.options.offline,
Some(network),
Some(&endpoint),
command.env_override.network_retries,
)?
};
if package.manifest.leo != env!("CARGO_PKG_VERSION") {
tracing::warn!(
"The Leo compiler version in the manifest ({}) does not match the current version ({}).",
package.manifest.leo,
env!("CARGO_PKG_VERSION")
);
}
let build_directory = package.build_directory();
let source_directory = package.source_directory();
let main_source_path = source_directory.join("main.leo");
let primary_name = package.primary_unit().map(|p| p.name);
let rename_target = apply_rename(command, &mut package, primary_name)?;
std::fs::create_dir_all(&build_directory).map_err(|err| {
crate::errors::util_file_io_error(format_args!("Couldn't create directory {}", build_directory.display()), err)
})?;
remove_legacy_build_artifacts(&build_directory);
let handler = Handler::default();
let node_builder = Rc::new(NodeBuilder::default());
let mut build_options = command.options.clone();
build_options.no_std = package.manifest.no_std;
let mut stubs: IndexMap<Symbol, Stub> = IndexMap::new();
if !build_options.no_std {
let std_stub = Compiler::build_std_stub(
handler.clone(),
Rc::clone(&node_builder),
network,
Rc::new(TypeInterner::default()),
)?;
stubs.insert(Symbol::intern(leo_std::library_name()), std_stub);
}
let mut compiled_programs: IndexMap<String, ProgramForValidation> = IndexMap::new();
let mut written: HashSet<String> = HashSet::new();
let mut disassemble_process = match network {
NetworkName::MainnetV0 => DisassembleProcess::Mainnet(SvmProcess::<MainnetV0>::load().map_err(|e| {
crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
})?),
NetworkName::TestnetV0 => DisassembleProcess::Testnet(SvmProcess::<TestnetV0>::load().map_err(|e| {
crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
})?),
NetworkName::CanaryV0 => DisassembleProcess::Canary(SvmProcess::<CanaryV0>::load().map_err(|e| {
crate::errors::custom(format!("Failed to initialize snarkVM process for disassembler validation: {e}"))
})?),
};
for unit in &package.compilation_units {
let unit_name = unit.name.to_string();
let unit_key = leo_package::bare_unit_name(&unit_name).to_string();
match &unit.data {
leo_package::ProgramData::Bytecode(bytecode) => {
let build_path = package.unit_bytecode_path(&unit_name);
if written.insert(unit_key.clone()) {
ensure_parent_dir(&build_path)?;
std::fs::write(&build_path, bytecode).map_err(crate::errors::failed_to_load_instructions)?;
}
let stub = match &mut disassemble_process {
DisassembleProcess::Mainnet(p) => {
leo_disassembler::disassemble_from_str::<MainnetV0>(unit.name, bytecode, p)
}
DisassembleProcess::Testnet(p) => {
leo_disassembler::disassemble_from_str::<TestnetV0>(unit.name, bytecode, p)
}
DisassembleProcess::Canary(p) => {
leo_disassembler::disassemble_from_str::<CanaryV0>(unit.name, bytecode, p)
}
}?;
stubs.insert(unit.name, stub.into());
compiled_programs.entry(unit_key.clone()).or_insert(ProgramForValidation {
bytecode: bytecode.clone(),
path: build_path,
is_leo_compiled: false,
});
}
leo_package::ProgramData::SourcePath { directory, source } => {
let source_dir = if unit.kind.is_test() {
source
.parent()
.ok_or_else(|| {
crate::errors::failed_to_open_file(format_args!(
"Failed to find directory for test {}",
source.display()
))
})?
.to_path_buf()
} else {
directory.join("src")
};
let is_main = source == &main_source_path;
if is_main || unit.kind.is_test() {
let compiled = compile_leo_source_directory(
source, &source_dir,
unit.name,
unit.kind.is_test(),
&handler,
&node_builder,
build_options.clone(),
stubs.clone(),
network,
if is_main { rename_target.clone() } else { None },
)?;
let primary_path = package.unit_bytecode_path(&unit_name);
if written.insert(unit_key.clone()) {
ensure_parent_dir(&primary_path)?;
std::fs::write(&primary_path, &compiled.primary.bytecode)
.map_err(crate::errors::failed_to_load_instructions)?;
if is_main {
let abi_path = package.unit_abi_path(&unit_name);
let abi_json = serde_json::to_string_pretty(&compiled.primary.abi)
.map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&abi_path, abi_json).map_err(crate::errors::failed_to_write_abi)?;
tracing::info!("✅ Generated ABI for program '{unit_name}'.");
let interfaces_directory = package.unit_interfaces_directory(&unit_name);
write_interface_abis(&interfaces_directory, &compiled.interfaces)?;
}
}
for import in &compiled.imports {
let import_path = package.unit_bytecode_path(&import.name);
let import_key = leo_package::bare_unit_name(&import.name).to_string();
if written.insert(import_key.clone()) {
ensure_parent_dir(&import_path)?;
std::fs::write(&import_path, &import.bytecode)
.map_err(crate::errors::failed_to_load_instructions)?;
let import_abi_path = package.unit_abi_path(&import.name);
let import_abi_json = serde_json::to_string_pretty(&import.abi)
.map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&import_abi_path, import_abi_json)
.map_err(crate::errors::failed_to_write_abi)?;
}
compiled_programs.entry(import_key).or_insert(ProgramForValidation {
bytecode: import.bytecode.clone(),
path: import_path,
is_leo_compiled: true,
});
}
compiled_programs.entry(unit_key.clone()).or_insert(ProgramForValidation {
bytecode: compiled.primary.bytecode.clone(),
path: primary_path,
is_leo_compiled: true,
});
}
if unit.kind.is_library() {
let library = if primary_name == Some(unit.name) {
let (lib, interfaces) = build_leo_source_directory_library(
source,
&source_dir,
unit.name,
&handler,
&node_builder,
build_options.clone(),
stubs.clone(),
network,
)?;
let interfaces_directory = package.unit_interfaces_directory(&unit_name);
write_interface_abis(&interfaces_directory, &interfaces)?;
lib
} else {
parse_leo_source_directory_library(
source,
&source_dir,
unit.name,
&handler,
&node_builder,
build_options.clone(),
network,
)?
};
handler.last_err()?;
let mut library_stub: Stub = library.into();
for node in package.dep_graph.nodes() {
if package.dep_graph.neighbors(node).any(|dep| dep == &unit.name) {
library_stub.add_parent(*node);
}
}
stubs.insert(unit.name, library_stub);
} else if !unit.kind.is_test() {
let leo_program = parse_leo_source_directory(
source,
&source_dir,
unit.name,
&handler,
&node_builder,
build_options.clone(),
network,
if is_main { rename_target.clone() } else { None },
)?;
stubs.insert(unit.name, leo_program.into());
}
}
}
}
for unit in &package.compilation_units {
if !unit.kind.is_program() || unit.kind.is_test() {
continue;
}
let leo_package::ProgramData::SourcePath { directory, source } = &unit.data else { continue };
let unit_name = unit.name.to_string();
let unit_key = leo_package::bare_unit_name(&unit_name).to_string();
if !written.insert(unit_key.clone()) {
continue;
}
let source_dir = directory.join("src");
let compiled = compile_leo_source_directory(
source,
&source_dir,
unit.name,
false,
&handler,
&node_builder,
build_options.clone(),
stubs.clone(),
network,
None,
)?;
let primary_path = package.unit_bytecode_path(&unit_name);
ensure_parent_dir(&primary_path)?;
std::fs::write(&primary_path, &compiled.primary.bytecode)
.map_err(crate::errors::failed_to_load_instructions)?;
let abi_path = package.unit_abi_path(&unit_name);
let abi_json = serde_json::to_string_pretty(&compiled.primary.abi)
.map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&abi_path, abi_json).map_err(crate::errors::failed_to_write_abi)?;
let interfaces_directory = package.unit_interfaces_directory(&unit_name);
write_interface_abis(&interfaces_directory, &compiled.interfaces)?;
compiled_programs.entry(unit_key).or_insert(ProgramForValidation {
bytecode: compiled.primary.bytecode.clone(),
path: primary_path,
is_leo_compiled: true,
});
}
validate_compiled_programs(&compiled_programs, network)?;
Ok(package)
}
fn apply_rename(command: &LeoBuild, package: &mut Package, primary_name: Option<Symbol>) -> Result<Option<String>> {
let Some(requested) = &command.rename else {
return Ok(None);
};
if command.options.build_tests {
return Err(crate::errors::custom("`--rename` cannot be combined with `--build-tests`.").into());
}
let renamed = leo_package::canonicalize_program_name(requested);
if !leo_package::is_valid_program_name(&renamed) {
return Err(crate::errors::custom(format!(
"Invalid program name '{requested}' for `--rename`; expected a valid Aleo program name."
))
.into());
}
let Some(original) = primary_name else {
return Err(crate::errors::custom("`--rename` requires a primary program to rename.").into());
};
let renamed_bare = leo_package::bare_unit_name(&renamed);
if renamed_bare == leo_package::bare_unit_name(&original.to_string()) {
return Err(crate::errors::custom(format!(
"`--rename` target '{renamed}' is identical to the program's current name."
))
.into());
}
if package
.compilation_units
.iter()
.any(|unit| unit.name != original && leo_package::bare_unit_name(&unit.name.to_string()) == renamed_bare)
{
return Err(crate::errors::custom(format!(
"`--rename` target '{renamed}' conflicts with an existing program or dependency in this package; choose a different name."
))
.into());
}
let renamed_symbol = Symbol::intern(&renamed);
for unit in package.compilation_units.iter_mut() {
if !unit.kind.is_test() && unit.name == original {
unit.name = renamed_symbol;
}
}
Ok(Some(renamed))
}
fn collect_checksums<N: snarkvm::prelude::Network>(program: &SvmProgram<N>) -> BuildOutput {
let mut function_checksums = IndexMap::with_capacity(program.functions().len() + program.views().len());
for (name, function) in program.functions() {
function_checksums.insert(name.to_string(), function.to_checksum().iter().map(|b| **b).collect());
}
for (name, view) in program.views() {
function_checksums.insert(name.to_string(), view.to_checksum().iter().map(|b| **b).collect());
}
BuildOutput {
program: program.id().to_string(),
program_checksum: program.to_checksum().iter().map(|b| **b).collect(),
function_checksums,
}
}
fn program_checksums(network: NetworkName, bytecode: &str) -> Result<BuildOutput> {
Ok(match network {
NetworkName::MainnetV0 => collect_checksums(&SvmProgram::<MainnetV0>::from_str(bytecode)?),
NetworkName::TestnetV0 => collect_checksums(&SvmProgram::<TestnetV0>::from_str(bytecode)?),
NetworkName::CanaryV0 => collect_checksums(&SvmProgram::<CanaryV0>::from_str(bytecode)?),
})
}
pub fn build_output(package: &Package, network: Option<NetworkName>) -> Result<BuildOutput> {
let network = get_network(&network).unwrap_or(NetworkName::TestnetV0);
let unit =
package.primary_unit().ok_or_else(|| crate::errors::custom("No primary program found in the package."))?;
let name = unit.name.to_string();
let bytecode = std::fs::read_to_string(package.unit_bytecode_path(&name)).map_err(|err| {
crate::errors::util_file_io_error(format_args!("Trying to read compiled bytecode for `{name}`"), err)
})?;
program_checksums(network, &bytecode)
}
#[allow(clippy::too_many_arguments)]
fn compile_leo_source_directory(
entry_file_path: &Path,
source_directory: &Path,
program_name: Symbol,
is_test: bool,
handler: &Handler,
node_builder: &Rc<NodeBuilder>,
options: BuildOptions,
stubs: IndexMap<Symbol, Stub>,
network: NetworkName,
rename: Option<String>,
) -> Result<Compiled> {
if !is_test {
println!();
}
tracing::info!("🔨 Compiling '{program_name}'");
let print_checksums = options.checksums;
let mut compiler = Compiler::new(
Some(program_name.to_string()),
is_test,
handler.clone(),
Rc::clone(node_builder),
Some(options.into()),
stubs,
network,
);
compiler.rename = rename;
let compiled = if is_test {
compiler.compile_from_file(entry_file_path)?
} else {
compiler.compile_from_directory(entry_file_path, source_directory)?
};
let primary_bytecode = &compiled.primary.bytecode;
use leo_package::MAX_PROGRAM_SIZE;
let program_size = primary_bytecode.len();
if program_size > MAX_PROGRAM_SIZE {
return Err(crate::errors::program_size_limit_exceeded(program_name, program_size, MAX_PROGRAM_SIZE).into());
}
if print_checksums {
let checksums = program_checksums(network, primary_bytecode)?;
let format = |bytes: &[u8]| bytes.iter().map(|b| format!("{b}u8")).join(", ");
tracing::info!(" The program checksum is: '[{}]'.", format(&checksums.program_checksum));
for (name, function_checksum) in &checksums.function_checksums {
tracing::info!(" `{name}` function checksum is: '[{}]'.", format(function_checksum));
}
}
let (size_kb, max_kb, warning) = format_program_size(program_size, MAX_PROGRAM_SIZE);
if let Some(msg) = warning {
tracing::warn!("⚠️ Program '{program_name}' is {msg}.");
}
if !is_test {
tracing::info!(" Program size: {size_kb:.2} KB / {max_kb:.2} KB");
tracing::info!("✅ Compiled '{program_name}' into Aleo instructions.");
}
if print_checksums {
for import in &compiled.imports {
let dep_checksum: String = match network {
NetworkName::MainnetV0 => {
SvmProgram::<MainnetV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
}
NetworkName::TestnetV0 => {
SvmProgram::<TestnetV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
}
NetworkName::CanaryV0 => {
SvmProgram::<CanaryV0>::from_str(&import.bytecode)?.to_checksum().iter().join(", ")
}
};
tracing::info!(" Import '{}': checksum = '[{dep_checksum}]'", import.name);
}
}
if !is_test {
for import in &compiled.imports {
let import_size = import.bytecode.len();
let (size_kb, max_kb, _warning) = format_program_size(import_size, MAX_PROGRAM_SIZE);
tracing::info!(" Import '{}': program size: {size_kb:.2} KB / {max_kb:.2} KB", import.name);
}
}
Ok(compiled)
}
#[allow(clippy::too_many_arguments)]
fn parse_leo_source_directory(
entry_file_path: &Path,
source_directory: &Path,
program_name: Symbol,
handler: &Handler,
node_builder: &Rc<NodeBuilder>,
options: BuildOptions,
network: NetworkName,
rename: Option<String>,
) -> Result<Program> {
let mut compiler = Compiler::new(
Some(program_name.to_string()),
false,
handler.clone(),
Rc::clone(node_builder),
Some(options.into()),
IndexMap::new(),
network,
);
compiler.rename = rename;
compiler.parse_program_from_directory(entry_file_path, source_directory)
}
fn validate_compiled_programs(programs: &IndexMap<String, ProgramForValidation>, network: NetworkName) -> Result<()> {
match network {
NetworkName::MainnetV0 => validate_compiled_programs_inner::<MainnetV0>(programs),
NetworkName::TestnetV0 => validate_compiled_programs_inner::<TestnetV0>(programs),
NetworkName::CanaryV0 => validate_compiled_programs_inner::<CanaryV0>(programs),
}
}
fn validate_compiled_programs_inner<N: snarkvm::prelude::Network>(
programs: &IndexMap<String, ProgramForValidation>,
) -> Result<()> {
let process = SvmProcess::<N>::load().map_err(|e| {
crate::errors::custom(format!("Failed to initialize snarkVM process for bytecode validation: {e}"))
})?;
for (name, ProgramForValidation { bytecode, path, is_leo_compiled }) in programs {
let program =
SvmProgram::<N>::from_str(bytecode).map_err(|e| crate::errors::failed_to_parse_aleo_file(name, e))?;
let checksum = program.to_checksum().iter().join(", ");
process.lock().add_program_with_edition(&program, LOCAL_PROGRAM_DEFAULT_EDITION).map_err(|e| {
if *is_leo_compiled {
crate::errors::generated_invalid_bytecode(name, path.display(), &checksum, e)
} else {
crate::errors::custom(format!(
"snarkVM rejected external program '{name}' during build validation: {e}"
))
}
})?;
}
Ok(())
}
fn parse_leo_source_directory_library(
entry_file_path: &Path,
source_directory: &Path,
library_name: Symbol,
handler: &Handler,
node_builder: &Rc<NodeBuilder>,
options: BuildOptions,
network: NetworkName,
) -> Result<leo_ast::Library> {
let mut compiler = Compiler::new(
Some(library_name.to_string()),
false,
handler.clone(),
Rc::clone(node_builder),
Some(options.into()),
IndexMap::new(),
network,
);
compiler.parse_library_from_directory(library_name, entry_file_path, source_directory)
}
#[allow(clippy::too_many_arguments)]
fn build_leo_source_directory_library(
entry_file_path: &Path,
source_directory: &Path,
library_name: Symbol,
handler: &Handler,
node_builder: &Rc<NodeBuilder>,
options: BuildOptions,
stubs: IndexMap<Symbol, Stub>,
network: NetworkName,
) -> Result<(leo_ast::Library, Vec<leo_abi::interfaces::CompiledInterface>)> {
println!();
tracing::info!("🔨 Building library '{library_name}'");
let mut compiler = Compiler::new(
Some(library_name.to_string()),
false,
handler.clone(),
Rc::clone(node_builder),
Some(options.into()),
stubs,
network,
);
let library = compiler.build_library_from_directory(library_name, entry_file_path, source_directory)?;
let interfaces = compiler.generate_library_interface_abis();
tracing::info!("✅ Validated '{library_name}'.");
Ok((library, interfaces))
}
fn write_interface_abis(interfaces_dir: &Path, interfaces: &[leo_abi::interfaces::CompiledInterface]) -> Result<()> {
if interfaces_dir.exists() {
std::fs::remove_dir_all(interfaces_dir).map_err(crate::errors::failed_to_write_abi)?;
}
if interfaces.is_empty() {
return Ok(());
}
for ci in interfaces {
let mut file_path = match &ci.owner {
leo_abi::interfaces::InterfaceOwner::Local => interfaces_dir.to_path_buf(),
leo_abi::interfaces::InterfaceOwner::External { owner_program } => interfaces_dir.join(owner_program),
};
for seg in &ci.abi.path[..ci.abi.path.len().saturating_sub(1)] {
file_path.push(seg);
}
std::fs::create_dir_all(&file_path).map_err(crate::errors::failed_to_write_abi)?;
file_path.push(format!("{}.json", ci.abi.name));
let json =
serde_json::to_string_pretty(&ci.abi).map_err(|e| crate::errors::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&file_path, json).map_err(crate::errors::failed_to_write_abi)?;
}
tracing::info!("✅ Generated {} interface ABI(s).", interfaces.len());
Ok(())
}
fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
crate::errors::util_file_io_error(format_args!("Couldn't create directory {}", parent.display()), err)
})?;
}
Ok(())
}
fn remove_legacy_build_artifacts(build_directory: &Path) {
let is_legacy =
build_directory.join("main.aleo").exists() || build_directory.join(leo_package::MANIFEST_FILENAME).exists();
if !is_legacy {
return;
}
for file in ["main.aleo", ABI_FILENAME, leo_package::MANIFEST_FILENAME] {
let _ = std::fs::remove_file(build_directory.join(file));
}
for dir in ["imports", "interfaces"] {
let _ = std::fs::remove_dir_all(build_directory.join(dir));
}
}