use crate::*;
use leo_ast::DiGraph;
use leo_errors::{CliError, PackageError, Result, UtilError};
use leo_span::Symbol;
use indexmap::{IndexMap, map::Entry};
use snarkvm::prelude::anyhow;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub enum ProgramData {
Bytecode(String),
SourcePath {
directory: PathBuf,
source: PathBuf,
},
}
#[derive(Clone, Debug)]
pub struct Package {
pub base_directory: PathBuf,
pub compilation_units: Vec<CompilationUnit>,
pub manifest: Manifest,
pub dep_graph: DiGraph<Symbol>,
}
impl Package {
pub fn outputs_directory(&self) -> PathBuf {
self.base_directory.join(OUTPUTS_DIRECTORY)
}
pub fn imports_directory(&self) -> PathBuf {
self.base_directory.join(IMPORTS_DIRECTORY)
}
pub fn build_directory(&self) -> PathBuf {
self.base_directory.join(BUILD_DIRECTORY)
}
pub fn source_directory(&self) -> PathBuf {
self.base_directory.join(SOURCE_DIRECTORY)
}
pub fn tests_directory(&self) -> PathBuf {
self.base_directory.join(TESTS_DIRECTORY)
}
pub fn initialize<P: AsRef<Path>>(package_name: &str, path: P, is_library: bool) -> Result<PathBuf> {
Self::initialize_impl(package_name, path.as_ref(), is_library)
}
fn initialize_impl(package_name: &str, path: &Path, is_library: bool) -> Result<PathBuf> {
let package_name = if is_library {
if !crate::is_valid_library_name(package_name) {
return Err(CliError::invalid_package_name("library", package_name).into());
}
package_name.to_string()
} else {
let program_name =
if package_name.ends_with(".aleo") { package_name.to_string() } else { format!("{package_name}.aleo") };
if !crate::is_valid_program_name(&program_name) {
return Err(CliError::invalid_package_name("program", &program_name).into());
}
program_name
};
let path = path.canonicalize().map_err(|e| PackageError::failed_path(path.display(), e))?;
let full_path = path.join(package_name.strip_suffix(".aleo").unwrap_or(&package_name));
if full_path.exists() {
return Err(
PackageError::failed_to_initialize_package(package_name, &path, "Directory already exists").into()
);
}
std::fs::create_dir(&full_path)
.map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
std::env::set_current_dir(&full_path)
.map_err(|e| PackageError::failed_to_initialize_package(&package_name, &full_path, e))?;
const GITIGNORE_TEMPLATE: &str = ".env\n*.avm\n*.prover\n*.verifier\noutputs/\n";
const GITIGNORE_FILENAME: &str = ".gitignore";
let gitignore_path = full_path.join(GITIGNORE_FILENAME);
std::fs::write(gitignore_path, GITIGNORE_TEMPLATE).map_err(PackageError::io_error_gitignore_file)?;
let manifest = Manifest {
program: package_name.clone(),
version: "0.1.0".to_string(),
description: String::new(),
license: "MIT".to_string(),
leo: env!("CARGO_PKG_VERSION").to_string(),
dependencies: None,
dev_dependencies: None,
};
let manifest_path = full_path.join(MANIFEST_FILENAME);
manifest.write_to_file(manifest_path)?;
let source_path = full_path.join(SOURCE_DIRECTORY);
std::fs::create_dir(&source_path)
.map_err(|e| PackageError::failed_to_create_source_directory(source_path.display(), e))?;
let name_no_aleo = package_name.strip_suffix(".aleo").unwrap_or(&package_name);
if is_library {
let lib_path = source_path.join("lib.leo");
std::fs::write(&lib_path, lib_template(name_no_aleo)).map_err(|e| {
UtilError::util_file_io_error(format_args!("Failed to write `{}`", lib_path.display()), e)
})?;
let tests_path = full_path.join(TESTS_DIRECTORY);
std::fs::create_dir(&tests_path)
.map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
std::fs::write(&test_file_path, lib_test_template(name_no_aleo)).map_err(|e| {
UtilError::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
})?;
} else {
let main_path = source_path.join(MAIN_FILENAME);
std::fs::write(&main_path, main_template(name_no_aleo)).map_err(|e| {
UtilError::util_file_io_error(format_args!("Failed to write `{}`", main_path.display()), e)
})?;
let tests_path = full_path.join(TESTS_DIRECTORY);
std::fs::create_dir(&tests_path)
.map_err(|e| PackageError::failed_to_create_source_directory(tests_path.display(), e))?;
let test_file_path = tests_path.join(format!("test_{name_no_aleo}.leo"));
std::fs::write(&test_file_path, test_template(name_no_aleo)).map_err(|e| {
UtilError::util_file_io_error(format_args!("Failed to write `{}`", test_file_path.display()), e)
})?;
}
Ok(full_path)
}
pub fn from_directory_no_graph<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
home_path: Q,
network: Option<NetworkName>,
endpoint: Option<&str>,
) -> Result<Self> {
Self::from_directory_impl(
path.as_ref(),
home_path.as_ref(),
false,
false,
false,
false,
network,
endpoint,
)
}
pub fn from_directory<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
home_path: Q,
no_cache: bool,
no_local: bool,
network: Option<NetworkName>,
endpoint: Option<&str>,
) -> Result<Self> {
Self::from_directory_impl(
path.as_ref(),
home_path.as_ref(),
true,
false,
no_cache,
no_local,
network,
endpoint,
)
}
pub fn from_directory_with_tests<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
home_path: Q,
no_cache: bool,
no_local: bool,
network: Option<NetworkName>,
endpoint: Option<&str>,
) -> Result<Self> {
Self::from_directory_impl(
path.as_ref(),
home_path.as_ref(),
true,
true,
no_cache,
no_local,
network,
endpoint,
)
}
pub fn test_files(&self) -> impl Iterator<Item = PathBuf> {
let path = self.tests_directory();
let data: Vec<PathBuf> = Self::files_with_extension(&path, "leo").collect();
data.into_iter()
}
pub fn import_files(&self) -> impl Iterator<Item = PathBuf> {
let path = self.imports_directory();
let data: Vec<PathBuf> = Self::files_with_extension(&path, "aleo").collect();
data.into_iter()
}
fn files_with_extension(path: &Path, extension: &'static str) -> impl Iterator<Item = PathBuf> {
path.read_dir()
.ok()
.into_iter()
.flatten()
.flat_map(|maybe_filename| maybe_filename.ok())
.filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
.flat_map(move |entry| {
let path = entry.path();
if path.extension().is_some_and(|e| e == extension) { Some(path) } else { None }
})
}
#[allow(clippy::too_many_arguments)]
fn from_directory_impl(
path: &Path,
home_path: &Path,
build_graph: bool,
with_tests: bool,
no_cache: bool,
no_local: bool,
network: Option<NetworkName>,
endpoint: Option<&str>,
) -> Result<Self> {
let map_err = |path: &Path, err| {
UtilError::util_file_io_error(format_args!("Trying to find path at {}", path.display()), err)
};
let path = path.canonicalize().map_err(|err| map_err(path, err))?;
let manifest = Manifest::read_from_file(path.join(MANIFEST_FILENAME))?;
let (compilation_units, digraph) = if build_graph {
let home_path = home_path.canonicalize().map_err(|err| map_err(home_path, err))?;
let mut map: IndexMap<Symbol, (Dependency, CompilationUnit)> = IndexMap::new();
let mut digraph = DiGraph::<Symbol>::new(Default::default());
let first_dependency = Dependency {
name: manifest.program.clone(),
location: Location::Local,
path: Some(path.clone()),
edition: None,
};
let test_dependencies: Vec<Dependency> = if with_tests {
let tests_directory = path.join(TESTS_DIRECTORY);
let mut test_dependencies: Vec<Dependency> = Self::files_with_extension(&tests_directory, "leo")
.map(|path| Dependency {
name: format!("{}.aleo", crate::filename_no_leo_extension(&path).unwrap()),
edition: None,
location: Location::Test,
path: Some(path.to_path_buf()),
})
.collect();
if let Some(deps) = manifest.dev_dependencies.as_ref() {
test_dependencies.extend(deps.iter().cloned());
}
test_dependencies
} else {
Vec::new()
};
for dependency in test_dependencies.into_iter().chain(std::iter::once(first_dependency.clone())) {
Self::graph_build(
&home_path,
network,
endpoint,
&first_dependency,
dependency,
&mut map,
&mut digraph,
no_cache,
no_local,
)?;
}
let ordered_dependency_symbols =
digraph.post_order().map_err(|_| UtilError::circular_dependency_error())?;
(
ordered_dependency_symbols.into_iter().map(|symbol| map.swap_remove(&symbol).unwrap().1).collect(),
digraph,
)
} else {
(Vec::new(), DiGraph::default())
};
Ok(Package { base_directory: path, compilation_units, manifest, dep_graph: digraph })
}
#[allow(clippy::too_many_arguments)]
fn graph_build(
home_path: &Path,
network: Option<NetworkName>,
endpoint: Option<&str>,
main_program: &Dependency,
new: Dependency,
map: &mut IndexMap<Symbol, (Dependency, CompilationUnit)>,
graph: &mut DiGraph<Symbol>,
no_cache: bool,
no_local: bool,
) -> Result<()> {
let name_symbol = symbol(&new.name)?;
let dependencies = map.clone().into_iter().map(|(name, (dep, _))| (name, dep)).collect();
let unit = match map.entry(name_symbol) {
Entry::Occupied(occupied) => {
let existing_dep = &occupied.get().0;
assert_eq!(new.name, existing_dep.name);
if new.location != existing_dep.location
|| new.path != existing_dep.path
|| new.edition != existing_dep.edition
{
return Err(PackageError::conflicting_dependency(existing_dep, new).into());
}
return Ok(());
}
Entry::Vacant(vacant) => {
let unit = match (new.path.as_ref(), new.location) {
(Some(path), Location::Local) if !no_local => {
if path.extension().and_then(|p| p.to_str()) == Some("aleo") && path.is_file() {
CompilationUnit::from_aleo_path(name_symbol, path, &dependencies)?
} else {
CompilationUnit::from_package_path(name_symbol, path)?
}
}
(Some(path), Location::Test) => {
CompilationUnit::from_test_path(path, main_program.clone())?
}
(_, Location::Network) | (Some(_), Location::Local) => {
let Some(endpoint) = endpoint else {
return Err(anyhow!("An endpoint must be provided to fetch network dependencies.").into());
};
let Some(network) = network else {
return Err(anyhow!("A network must be provided to fetch network dependencies.").into());
};
CompilationUnit::fetch(name_symbol, new.edition, home_path, network, endpoint, no_cache)?
}
_ => return Err(anyhow!("Invalid dependency data for {} (path must be given).", new.name).into()),
};
vacant.insert((new, unit.clone()));
unit
}
};
graph.add_node(name_symbol);
for dependency in unit.dependencies.iter() {
let dependency_symbol = symbol(&dependency.name)?;
graph.add_edge(name_symbol, dependency_symbol);
Self::graph_build(
home_path,
network,
endpoint,
main_program,
dependency.clone(),
map,
graph,
no_cache,
no_local,
)?;
}
Ok(())
}
}
fn main_template(name: &str) -> String {
format!(
r#"// The '{name}' program.
program {name}.aleo {{
// This is the constructor for the program.
// The constructor allows you to manage program upgrades.
// It is called when the program is deployed or upgraded.
// It is currently configured to **prevent** upgrades.
// Other configurations include:
// - @admin(address="aleo1...")
// - @checksum(mapping="credits.aleo/fixme", key="0field")
// - @custom
// For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
@noupgrade
constructor() {{}}
fn main(public a: u32, b: u32) -> u32 {{
let c: u32 = a + b;
return c;
}}
}}
"#
)
}
fn test_template(name: &str) -> String {
format!(
r#"// The 'test_{name}' test program.
import {name}.aleo;
program test_{name}.aleo {{
@test
@should_fail
fn test_main_fails() {{
let result: u32 = {name}.aleo::main(2u32, 3u32);
assert_eq(result, 3u32);
}}
@noupgrade
constructor() {{}}
}}
"#
)
}
fn lib_template(name: &str) -> String {
format!(
r#"// The '{name}' library.
// Returns the identity of x.
fn example(x: u32) -> u32 {{
return x;
}}
"#
)
}
fn lib_test_template(name: &str) -> String {
format!(
r#"// The 'test_{name}' test program.
program test_{name}.aleo {{
@test
fn test_example() {{
assert_eq({name}::example(42u32), 42u32);
}}
@noupgrade
constructor() {{}}
}}
"#
)
}