use super::*;
use leo_ast::{NetworkName, NodeBuilder, Program, Stub};
use leo_compiler::{AstSnapshots, Compiled, Compiler, CompilerOptions};
use leo_errors::{CliError, UtilError};
use leo_package::{ABI_FILENAME, BUILD_DIRECTORY, Manifest, 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::{
path::{Path, PathBuf},
rc::Rc,
};
struct ProgramForValidation {
bytecode: String,
path: PathBuf,
is_leo_compiled: bool,
}
impl From<BuildOptions> for CompilerOptions {
fn from(options: BuildOptions) -> Self {
Self {
ast_spans_enabled: options.enable_ast_spans,
ast_snapshots: if options.enable_all_ast_snapshots {
AstSnapshots::All
} else {
AstSnapshots::Some(options.ast_snapshots.into_iter().collect())
},
initial_ast: options.enable_all_ast_snapshots | options.enable_initial_ast_snapshot,
}
}
}
#[derive(Parser, Debug)]
pub struct LeoBuild {
#[clap(flatten)]
pub(crate) options: BuildOptions,
#[clap(flatten)]
pub(crate) env_override: EnvOptions,
}
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> {
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 package = if command.options.build_tests {
Package::from_directory_with_tests(
&package_path,
&home_path,
command.options.no_cache,
command.options.no_local,
Some(network),
Some(&endpoint),
)?
} else {
Package::from_directory(
&package_path,
&home_path,
command.options.no_cache,
command.options.no_local,
Some(network),
Some(&endpoint),
)?
};
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 outputs_directory = package.outputs_directory();
let build_directory = package.build_directory();
let imports_directory = package.imports_directory();
let source_directory = package.source_directory();
let main_source_path = source_directory.join("main.leo");
let is_library = package.compilation_units.last().map(|p| p.kind.is_library()).unwrap_or(false);
if !is_library {
for dir in [&outputs_directory, &build_directory, &imports_directory] {
std::fs::create_dir_all(dir).map_err(|err| {
UtilError::util_file_io_error(format_args!("Couldn't create directory {}", dir.display()), err)
})?;
}
}
let handler = Handler::default();
let node_builder = Rc::new(NodeBuilder::default());
let mut stubs: IndexMap<Symbol, Stub> = IndexMap::new();
let mut compiled_programs: IndexMap<String, ProgramForValidation> = IndexMap::new();
for unit in &package.compilation_units {
match &unit.data {
leo_package::ProgramData::Bytecode(bytecode) => {
let build_path = imports_directory.join(format!("{}", unit.name));
std::fs::write(&build_path, bytecode).map_err(CliError::failed_to_load_instructions)?;
let stub = match network {
NetworkName::MainnetV0 => leo_disassembler::disassemble_from_str::<MainnetV0>(unit.name, bytecode),
NetworkName::TestnetV0 => leo_disassembler::disassemble_from_str::<TestnetV0>(unit.name, bytecode),
NetworkName::CanaryV0 => leo_disassembler::disassemble_from_str::<CanaryV0>(unit.name, bytecode),
}?;
stubs.insert(unit.name, stub.into());
compiled_programs.entry(unit.name.to_string()).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(|| {
UtilError::failed_to_open_file(format_args!(
"Failed to find directory for test {}",
source.display()
))
})?
.to_path_buf()
} else {
directory.join("src")
};
if source == &main_source_path || unit.kind.is_test() {
let compiled = compile_leo_source_directory(
source, &source_dir,
unit.name,
unit.kind.is_test(),
&outputs_directory,
&handler,
&node_builder,
command.options.clone(),
stubs.clone(),
network,
)?;
let primary_path = if source == &main_source_path {
build_directory.join("main.aleo")
} else {
imports_directory.join(format!("{}", unit.name))
};
std::fs::write(&primary_path, &compiled.primary.bytecode)
.map_err(CliError::failed_to_load_instructions)?;
for import in &compiled.imports {
let import_path = imports_directory.join(&import.name);
std::fs::write(&import_path, &import.bytecode)
.map_err(CliError::failed_to_load_instructions)?;
let import_abi_path = imports_directory.join(format!("{}.abi.json", import.name));
let import_abi_json = serde_json::to_string_pretty(&import.abi)
.map_err(|e| CliError::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&import_abi_path, import_abi_json).map_err(CliError::failed_to_write_abi)?;
compiled_programs.entry(import.name.clone()).or_insert(ProgramForValidation {
bytecode: import.bytecode.clone(),
path: import_path,
is_leo_compiled: true,
});
}
compiled_programs.entry(unit.name.to_string()).or_insert(ProgramForValidation {
bytecode: compiled.primary.bytecode.clone(),
path: primary_path,
is_leo_compiled: true,
});
if source == &main_source_path {
let abi_path = build_directory.join(ABI_FILENAME);
let abi_json = serde_json::to_string_pretty(&compiled.primary.abi)
.map_err(|e| CliError::failed_to_serialize_abi(e.to_string()))?;
std::fs::write(&abi_path, abi_json).map_err(CliError::failed_to_write_abi)?;
tracing::info!("✅ Generated ABI at '{BUILD_DIRECTORY}/{ABI_FILENAME}'.");
}
}
if unit.kind.is_library() {
let library = parse_leo_source_directory_library(
source,
&source_dir,
unit.name,
&handler,
&node_builder,
command.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 {
let leo_program = parse_leo_source_directory(
source,
&source_dir,
unit.name,
&handler,
&node_builder,
command.options.clone(),
network,
)?;
stubs.insert(unit.name, leo_program.into());
}
}
}
}
validate_compiled_programs(&compiled_programs, network)?;
if !is_library {
let build_manifest_path = build_directory.join(leo_package::MANIFEST_FILENAME);
let fake_manifest = Manifest {
program: package.manifest.program.clone(),
version: "0.1.0".to_string(),
description: String::new(),
license: String::new(),
leo: env!("CARGO_PKG_VERSION").to_string(),
dependencies: None,
dev_dependencies: None,
};
fake_manifest.write_to_file(build_manifest_path)?;
}
Ok(package)
}
#[allow(clippy::too_many_arguments)]
fn compile_leo_source_directory(
entry_file_path: &Path,
source_directory: &Path,
program_name: Symbol,
is_test: bool,
output_path: &Path,
handler: &Handler,
node_builder: &Rc<NodeBuilder>,
options: BuildOptions,
stubs: IndexMap<Symbol, Stub>,
network: NetworkName,
) -> Result<Compiled> {
println!();
tracing::info!("🔨 Compiling '{program_name}'");
let mut compiler = Compiler::new(
Some(program_name.to_string()),
is_test,
handler.clone(),
Rc::clone(node_builder),
output_path.to_path_buf(),
Some(options.into()),
stubs,
network,
);
let compiled = 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(leo_errors::LeoError::UtilError(UtilError::program_size_limit_exceeded(
program_name,
program_size,
MAX_PROGRAM_SIZE,
)));
}
let checksum: String = match network {
NetworkName::MainnetV0 => SvmProgram::<MainnetV0>::from_str(primary_bytecode)?.to_checksum().iter().join(", "),
NetworkName::TestnetV0 => SvmProgram::<TestnetV0>::from_str(primary_bytecode)?.to_checksum().iter().join(", "),
NetworkName::CanaryV0 => SvmProgram::<CanaryV0>::from_str(primary_bytecode)?.to_checksum().iter().join(", "),
};
tracing::info!(" {} statements before dead code elimination.", compiler.statements_before_dce);
tracing::info!(" {} statements after dead code elimination.", compiler.statements_after_dce);
tracing::info!(" The program checksum is: '[{checksum}]'.");
let (size_kb, max_kb, warning) = format_program_size(program_size, MAX_PROGRAM_SIZE);
tracing::info!(" Program size: {size_kb:.2} KB / {max_kb:.2} KB");
if let Some(msg) = warning {
tracing::warn!("⚠️ Program '{program_name}' is {msg}.");
}
tracing::info!("✅ Compiled '{program_name}' into Aleo instructions.");
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);
}
Ok(compiled)
}
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,
) -> Result<Program> {
let mut compiler = Compiler::new(
Some(program_name.to_string()),
false,
handler.clone(),
Rc::clone(node_builder),
std::path::PathBuf::default(),
Some(options.into()),
IndexMap::new(),
network,
);
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 mut process = SvmProcess::<N>::load()
.map_err(|e| CliError::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| CliError::failed_to_parse_aleo_file(name, e))?;
let checksum = program.to_checksum().iter().join(", ");
process.add_program_with_edition(&program, LOCAL_PROGRAM_DEFAULT_EDITION).map_err(|e| {
if *is_leo_compiled {
CliError::generated_invalid_bytecode(name, path.display(), &checksum, e)
} else {
CliError::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),
std::path::PathBuf::default(),
Some(options.into()),
IndexMap::new(),
network,
);
compiler.parse_library_from_directory(library_name, entry_file_path, source_directory)
}