diff --git a/.circleci/config.yml b/.circleci/config.yml index eb801f834d..5ce58312b2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -186,18 +186,18 @@ jobs: export LEO=/home/circleci/project/project/bin/leo ./project/.circleci/leo-setup.sh - leo-add-remove: - docker: - - image: cimg/rust:1.54.0 - resource_class: xlarge - steps: - - attach_workspace: - at: /home/circleci/project/ - - run: - name: leo add & remove - command: | - export LEO=/home/circleci/project/project/bin/leo - ./project/.circleci/leo-add-remove.sh + # leo-add-remove: + # docker: + # - image: cimg/rust:1.54.0 + # resource_class: xlarge + # steps: + # - attach_workspace: + # at: /home/circleci/project/ + # - run: + # name: leo add & remove + # command: | + # export LEO=/home/circleci/project/project/bin/leo + # ./project/.circleci/leo-add-remove.sh leo-check-constraints: docker: @@ -270,9 +270,9 @@ workflows: - leo-setup: requires: - leo-executable - - leo-add-remove: - requires: - - leo-executable + # - leo-add-remove: + # requires: + # - leo-executable - leo-check-constraints: requires: - leo-executable diff --git a/.circleci/r b/.circleci/r deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Cargo.lock b/Cargo.lock index 1aa878be32..a67fd805ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1320,6 +1320,7 @@ dependencies = [ "console", "dirs", "from-pest", + "indexmap", "lazy_static", "leo-ast", "leo-compiler", @@ -1359,6 +1360,7 @@ version = "1.5.3" name = "leo-package" version = "1.5.3" dependencies = [ + "indexmap", "lazy_static", "leo-errors", "serde", diff --git a/Cargo.toml b/Cargo.toml index fb3a68e932..852e27b752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,10 @@ version = "0.14.0" [dependencies.from-pest] version = "0.3.1" +[dependencies.indexmap] +version = "1.7" +features = ["serde"] + [dependencies.lazy_static] version = "1.4.0" diff --git a/compiler/src/compiler.rs b/compiler/src/compiler.rs index 7e8c825fb4..c7b27872b8 100644 --- a/compiler/src/compiler.rs +++ b/compiler/src/compiler.rs @@ -23,6 +23,7 @@ pub use leo_asg::{new_context, AsgContext as Context, AsgContext}; use leo_asg::{Asg, AsgPass, Program as AsgProgram}; use leo_ast::{Input, MainInput, Program as AstProgram}; use leo_errors::{CompilerError, Result}; +use leo_imports::ImportParser; use leo_input::LeoInputParser; use leo_package::inputs::InputPairs; use leo_parser::parse_ast; @@ -34,6 +35,7 @@ use snarkvm_r1cs::{ConstraintSynthesizer, ConstraintSystem, SynthesisError}; use sha2::{Digest, Sha256}; use std::{ + collections::HashMap, fs, marker::PhantomData, path::{Path, PathBuf}, @@ -62,6 +64,7 @@ pub struct Compiler<'a, F: PrimeField, G: GroupType> { context: AsgContext<'a>, asg: Option>, options: CompilerOptions, + imports_map: HashMap, ast_snapshot_options: AstSnapshotOptions, _engine: PhantomData, _group: PhantomData, @@ -77,6 +80,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { output_directory: PathBuf, context: AsgContext<'a>, options: Option, + imports_map: HashMap, ast_snapshot_options: Option, ) -> Self { Self { @@ -88,6 +92,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { asg: None, context, options: options.unwrap_or_default(), + imports_map, ast_snapshot_options: ast_snapshot_options.unwrap_or_default(), _engine: PhantomData, _group: PhantomData, @@ -107,6 +112,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { output_directory: PathBuf, context: AsgContext<'a>, options: Option, + imports_map: HashMap, ast_snapshot_options: Option, ) -> Result { let mut compiler = Self::new( @@ -115,6 +121,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { output_directory, context, options, + imports_map, ast_snapshot_options, ); @@ -146,6 +153,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { state_path: &Path, context: AsgContext<'a>, options: Option, + imports_map: HashMap, ast_snapshot_options: Option, ) -> Result { let mut compiler = Self::new( @@ -154,6 +162,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { output_directory, context, options, + imports_map, ast_snapshot_options, ); @@ -239,7 +248,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { ast.to_json_file(self.output_directory.clone(), "initial_ast.json")?; } - // Preform canonicalization of AST always. + // Perform canonicalization of AST always. ast.canonicalize()?; if self.ast_snapshot_options.canonicalized { @@ -256,7 +265,7 @@ impl<'a, F: PrimeField, G: GroupType> Compiler<'a, F, G> { let asg = Asg::new( self.context, &self.program, - &mut leo_imports::ImportParser::new(self.main_file_path.clone()), + &mut ImportParser::new(self.main_file_path.clone(), self.imports_map.clone()), )?; if self.ast_snapshot_options.type_inferenced { diff --git a/compiler/src/option.rs b/compiler/src/option.rs index 0dc5ccca51..f163cec5ad 100644 --- a/compiler/src/option.rs +++ b/compiler/src/option.rs @@ -17,7 +17,7 @@ /// /// Toggles compiler optimizations on the program. /// -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CompilerOptions { pub constant_folding_enabled: bool, pub dead_code_elimination_enabled: bool, diff --git a/compiler/src/test.rs b/compiler/src/test.rs index 244fa58961..bb8c688799 100644 --- a/compiler/src/test.rs +++ b/compiler/src/test.rs @@ -14,7 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use leo_asg::*; use leo_ast::{Ast, Program}; @@ -50,6 +53,7 @@ fn new_compiler(path: PathBuf, theorem_options: Option) -> E output_dir, make_test_context(), None, + HashMap::new(), theorem_options, ) } diff --git a/errors/src/cli/cli_errors.rs b/errors/src/cli/cli_errors.rs index 09a060439b..28f8b9b77e 100644 --- a/errors/src/cli/cli_errors.rs +++ b/errors/src/cli/cli_errors.rs @@ -157,7 +157,7 @@ create_errors!( /// For when the CLI cannot find the manifest file. @backtraced - mainifest_file_not_found { + manifest_file_not_found { args: (), msg: "Package manifest not found, try running `leo init`", help: None, @@ -379,6 +379,27 @@ create_errors!( msg: format!("Old release version {} {}", current, latest), help: None, } + + @backtraced + dependencies_are_not_installed { + args: (), + msg: "dependencies are not installed, please run `leo fetch` first", + help: None, + } + + @backtraced + recursive_dependency_found { + args: (message: impl Display), + msg: format!("recursive dependency found \n{}", message), + help: None, + } + + @backtraced + unable_to_read_imported_dependency_manifest { + args: (), + msg: "unable to parse imported dependency's manifest", + help: None, + } ); impl CliError { diff --git a/errors/src/package/package_errors.rs b/errors/src/package/package_errors.rs index fcda83e6b9..b51bb00f58 100644 --- a/errors/src/package/package_errors.rs +++ b/errors/src/package/package_errors.rs @@ -497,4 +497,66 @@ create_errors!( msg: format!("invalid project name {}", package), help: None, } + + @backtraced + failed_to_create_lock_file { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed creating manifest file `{}` {}", filename, error), + help: None, + } + + /// For when getting the lock file metadata failed. + @backtraced + failed_to_get_lock_file_metadata { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed getting lock file metadata `{}` {}", filename, error), + help: None, + } + + /// For when opening the lock file failed. + @backtraced + failed_to_open_lock_file { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed openining lock file `{}` {}", filename, error), + help: None, + } + + /// For when parsing the lock file failed. + @backtraced + failed_to_parse_lock_file { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed parsing lock file `{}` {}", filename, error), + help: None, + } + + /// For when reading the lock file failed. + @backtraced + failed_to_read_lock_file { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed reading lock file `{}` {}", filename, error), + help: None, + } + + /// For when writing the lock file failed. + @backtraced + failed_to_write_lock_file { + args: (filename: impl Display, error: impl ErrorArg), + msg: format!("failed writing lock file `{}` {}", filename, error), + help: None, + } + + /// For when the lock file has an IO error. + @backtraced + io_error_lock_file { + args: (error: impl ErrorArg), + msg: format!("IO error lock file from the provided file path - {}", error), + help: None, + } + + @backtraced + failed_to_serialize_lock_file { + args: (error: impl ErrorArg), + msg: format!("serialization failed: {}", error), + help: None, + } ); diff --git a/imports/src/parser/import_parser.rs b/imports/src/parser/import_parser.rs index 0212800d1a..106992551a 100644 --- a/imports/src/parser/import_parser.rs +++ b/imports/src/parser/import_parser.rs @@ -18,7 +18,7 @@ use leo_asg::{AsgContext, ImportResolver, Program}; use leo_errors::{ImportError, LeoError, Result, Span}; use indexmap::{IndexMap, IndexSet}; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; /// Stores imported packages. /// @@ -29,14 +29,16 @@ pub struct ImportParser<'a> { program_path: PathBuf, partial_imports: IndexSet, imports: IndexMap>, + pub imports_map: HashMap, } impl<'a> ImportParser<'a> { - pub fn new(program_path: PathBuf) -> Self { + pub fn new(program_path: PathBuf, imports_map: HashMap) -> Self { ImportParser { program_path, partial_imports: Default::default(), imports: Default::default(), + imports_map, } } } diff --git a/imports/src/parser/parse_package.rs b/imports/src/parser/parse_package.rs index 3f2079598b..0a2700c7ac 100644 --- a/imports/src/parser/parse_package.rs +++ b/imports/src/parser/parse_package.rs @@ -104,10 +104,19 @@ impl<'a> ImportParser<'a> { .collect::, std::io::Error>>() .map_err(|error| ImportError::directory_error(error, error_path, span))?; + // Keeping backward compatibilty for existing packages. + // If index_map contains key, use it or try to access directly. + // TODO: Remove when migration is possible. + let package_name = self + .imports_map + .get(package_name) + .unwrap_or(&package_name.to_string()) + .clone(); + // Check if the imported package name is in the imports directory. let matched_import_entry = entries .into_iter() - .find(|entry| entry.file_name().into_string().unwrap().eq(package_name)); + .find(|entry| entry.file_name().into_string().unwrap().eq(&package_name)); // Check if the package name was found in both the source and imports directory. match (matched_source_entry, matched_import_entry) { diff --git a/leo/commands/build.rs b/leo/commands/build.rs index 214f56a6f3..f38c2adb48 100644 --- a/leo/commands/build.rs +++ b/leo/commands/build.rs @@ -56,9 +56,9 @@ pub struct BuildOptions { impl Default for BuildOptions { fn default() -> Self { Self { - disable_constant_folding: true, - disable_code_elimination: true, - disable_all_optimizations: true, + disable_constant_folding: false, + disable_code_elimination: false, + disable_all_optimizations: false, enable_all_ast_snapshots: false, enable_initial_ast_snapshot: false, enable_canonicalized_ast_snapshot: false, @@ -69,10 +69,10 @@ impl Default for BuildOptions { impl From for CompilerOptions { fn from(options: BuildOptions) -> Self { - if !options.disable_all_optimizations { + if options.disable_all_optimizations { CompilerOptions { - constant_folding_enabled: true, - dead_code_elimination_enabled: true, + constant_folding_enabled: false, + dead_code_elimination_enabled: false, } } else { CompilerOptions { @@ -123,7 +123,14 @@ impl Command for Build { fn apply(self, context: Context, _: Self::Input) -> Result { let path = context.dir()?; - let package_name = context.manifest()?.get_package_name(); + let manifest = context.manifest().map_err(|_| CliError::manifest_file_not_found())?; + let package_name = manifest.get_package_name(); + let imports_map = manifest.get_imports_map().unwrap_or_default(); + + // Error out if there are dependencies but no lock file found. + if !imports_map.is_empty() && !context.lock_file_exists()? { + return Err(CliError::dependencies_are_not_installed().into()); + } // Sanitize the package path to the root directory. let mut package_path = path.clone(); @@ -159,6 +166,12 @@ impl Command for Build { // Log compilation of files to console tracing::info!("Compiling main program... ({:?})", main_file_path); + let imports_map = if context.lock_file_exists()? { + context.lock_file()?.to_import_map() + } else { + Default::default() + }; + // Load the program at `main_file_path` let program = Compiler::::parse_program_with_input( package_name.clone(), @@ -170,6 +183,7 @@ impl Command for Build { &state_path, thread_leaked_context(), Some(self.compiler_options.clone().into()), + imports_map, Some(self.compiler_options.into()), )?; diff --git a/leo/commands/package/add.rs b/leo/commands/package/add.rs index e5e7a4635b..0a27de6b97 100644 --- a/leo/commands/package/add.rs +++ b/leo/commands/package/add.rs @@ -14,6 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . +// !!!!!!!!!!!!!!!!!!!!!!!!!!!! +// COMMAND TEMPORARILY DISABLED +// !!!!!!!!!!!!!!!!!!!!!!!!!!!! + use crate::{api::Fetch, commands::Command, context::Context}; use leo_errors::{CliError, Result}; use leo_package::imports::{ImportsDirectory, IMPORTS_DIRECTORY_NAME}; @@ -21,6 +25,7 @@ use leo_package::imports::{ImportsDirectory, IMPORTS_DIRECTORY_NAME}; use std::{ fs::{create_dir_all, File}, io::{Read, Write}, + path::PathBuf, }; use structopt::StructOpt; use tracing::Span; @@ -76,7 +81,7 @@ impl Add { impl Command for Add { type Input = (); - type Output = (); + type Output = PathBuf; fn log_span(&self) -> Span { tracing::span!(tracing::Level::INFO, "Adding") @@ -88,18 +93,20 @@ impl Command for Add { fn apply(self, context: Context, _: Self::Input) -> Result { // Check that a manifest exists for the current package. - context.manifest().map_err(|_| CliError::mainifest_file_not_found())?; + context.manifest().map_err(|_| CliError::manifest_file_not_found())?; let (author, package_name) = self .try_read_arguments() .map_err(CliError::cli_bytes_conversion_error)?; + tracing::info!("Package: {}/{}", &author, &package_name); + // Attempt to fetch the package. let reader = { let fetch = Fetch { - author, + author: author.clone(), package_name: package_name.clone(), - version: self.version, + version: self.version.clone(), }; let bytes = context .api @@ -114,7 +121,14 @@ impl Command for Add { { ImportsDirectory::create(&path)?; path.push(IMPORTS_DIRECTORY_NAME); - path.push(package_name); + + // Dumb compatibility hack. + // TODO: Remove once `leo add` functionality is discussed. + if self.version.is_some() { + path.push(format!("{}-{}@{}", author, package_name, self.version.unwrap())); + } else { + path.push(package_name.clone()); + } create_dir_all(&path).map_err(CliError::cli_io_error)?; }; @@ -142,8 +156,8 @@ impl Command for Add { } } - tracing::info!("Successfully added a package"); + tracing::info!("Successfully added package {}/{}", author, package_name); - Ok(()) + Ok(path) } } diff --git a/leo/commands/package/fetch.rs b/leo/commands/package/fetch.rs new file mode 100644 index 0000000000..07c29fb579 --- /dev/null +++ b/leo/commands/package/fetch.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2019-2021 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::{ + commands::{package::Add, Command}, + context::{create_context, Context}, +}; + +use leo_package::root::{ + lock_file::{LockFile, Package}, + Dependency, +}; + +use indexmap::{set::IndexSet, IndexMap}; +use leo_errors::{CliError, Result}; +use structopt::StructOpt; +use tracing::span::Span; + +/// Pull dependencies specified in Leo toml. +#[derive(StructOpt, Debug)] +#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] +pub struct Fetch {} + +impl Command for Fetch { + /// Names of dependencies in the current branch of a dependency tree. + type Input = IndexSet; + type Output = (); + + fn log_span(&self) -> Span { + tracing::span!(tracing::Level::INFO, "Fetching") + } + + fn prelude(&self, context: Context) -> Result { + let package_name = context.manifest()?.get_package_name(); + + let mut set = IndexSet::new(); + set.insert(package_name); + + Ok(set) + } + + fn apply(self, context: Context, tree: Self::Input) -> Result { + let dependencies = context + .manifest() + .map_err(|_| CliError::manifest_file_not_found())? + .get_package_dependencies(); + + // If program has no dependencies in the Leo.toml, exit with success. + let dependencies = match dependencies { + Some(dependencies) => dependencies, + None => return Ok(()), + }; + + if dependencies.is_empty() { + return Ok(()); + } + + let mut lock_file = LockFile::new(); + self.add_dependencies(context.clone(), tree, &mut lock_file, dependencies)?; + lock_file.write_to(&context.dir()?)?; + + Ok(()) + } +} + +impl Fetch { + /// Pulls dependencies and fills in the lock file. Also checks for + /// recursive dependencies with dependency tree. + fn add_dependencies( + &self, + context: Context, + mut tree: IndexSet, + lock_file: &mut LockFile, + dependencies: IndexMap, + ) -> Result<()> { + // Go through each dependency in Leo.toml and add it to the imports. + // While adding, pull dependencies of this package as well and check for recursion. + for (import_name, dependency) in dependencies.into_iter() { + let mut package = Package::from(&dependency); + package.import_name = Some(import_name); + + // Pull the dependency first. + let path = Add::new( + None, + Some(package.author.clone()), + Some(package.name.clone()), + Some(package.version.clone()), + ) + .apply(context.clone(), ())?; + + // Try inserting a new dependency to the branch. If not inserted, + // then fail because this dependency was added on a higher level. + if !tree.insert(package.name.clone()) { + // Pretty format for the message - show dependency structure. + let mut message: Vec = tree + .into_iter() + .enumerate() + .map(|(i, val)| format!("{}└─{}", " ".repeat(i * 2), val)) + .collect(); + + message.push(format!("{}└─{} (FAILURE)", " ".repeat(message.len() * 2), package.name)); + + return Err(CliError::recursive_dependency_found(message.join("\n")).into()); + } + + // Check imported dependency's dependencies. + let imported_dependencies = create_context(path, None)? + .manifest() + .map_err(|_| CliError::unable_to_read_imported_dependency_manifest())? + .get_package_dependencies(); + + if let Some(dependencies) = imported_dependencies { + if !dependencies.is_empty() { + // Fill in the lock file with imported dependency and information about its dependencies. + package.add_dependencies(&dependencies); + lock_file.add_package(package); + + // Recursively call this method for imported program. + self.add_dependencies(context.clone(), tree.clone(), lock_file, dependencies)?; + + continue; + } + } + + // If there are no dependencies for the new import, add a single record. + lock_file.add_package(package); + } + + Ok(()) + } +} diff --git a/leo/commands/package/mod.rs b/leo/commands/package/mod.rs index ce8c637f59..cd1c3ccd69 100644 --- a/leo/commands/package/mod.rs +++ b/leo/commands/package/mod.rs @@ -20,6 +20,9 @@ pub use add::Add; pub mod clone; pub use clone::Clone; +pub mod fetch; +pub use fetch::Fetch; + pub mod login; pub use login::Login; diff --git a/leo/commands/package/remove.rs b/leo/commands/package/remove.rs index 6d8ba229f9..06907265ac 100644 --- a/leo/commands/package/remove.rs +++ b/leo/commands/package/remove.rs @@ -14,6 +14,10 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . +// !!!!!!!!!!!!!!!!!!!!!!!!!!!! +// COMMAND TEMPORARILY DISABLED +// !!!!!!!!!!!!!!!!!!!!!!!!!!!! + use crate::{commands::Command, context::Context}; use leo_errors::Result; use leo_package::LeoPackage; diff --git a/leo/commands/test.rs b/leo/commands/test.rs index 773ac53488..aa7cef20e3 100644 --- a/leo/commands/test.rs +++ b/leo/commands/test.rs @@ -109,6 +109,7 @@ impl Command for Test { output_directory.clone(), thread_leaked_context(), Some(self.compiler_options.clone().into()), + std::collections::HashMap::new(), Some(self.compiler_options.clone().into()), )?; diff --git a/leo/context.rs b/leo/context.rs index d7c7beaeed..49bdf54d1e 100644 --- a/leo/context.rs +++ b/leo/context.rs @@ -16,7 +16,7 @@ use crate::{api::Api, config}; use leo_errors::{CliError, Result}; -use leo_package::root::Manifest; +use leo_package::root::{LockFile, Manifest}; use std::{convert::TryFrom, env::current_dir, path::PathBuf}; @@ -41,10 +41,20 @@ impl Context { } } - /// Get package manifest for current context + /// Get package manifest for current context. pub fn manifest(&self) -> Result { Ok(Manifest::try_from(self.dir()?.as_path())?) } + + /// Get lock file for current context. + pub fn lock_file(&self) -> Result { + Ok(LockFile::try_from(self.dir()?.as_path())?) + } + + /// Check if lock file exists. + pub fn lock_file_exists(&self) -> Result { + Ok(LockFile::exists_at(&self.dir()?)) + } } /// Create a new context for the current directory. diff --git a/leo/main.rs b/leo/main.rs index 38a25f722c..eb500655af 100644 --- a/leo/main.rs +++ b/leo/main.rs @@ -22,7 +22,7 @@ pub mod logger; pub mod updater; use commands::{ - package::{Add, Clone, Login, Logout, Publish, Remove}, + package::{Clone, Fetch, Login, Logout, Publish}, Build, Clean, Command, Deploy, Init, Lint, New, Prove, Run, Setup, Test, Update, Watch, }; use leo_errors::Result; @@ -119,10 +119,15 @@ enum CommandOpts { command: Test, }, - #[structopt(about = "Import a package from the Aleo Package Manager")] - Add { + // #[structopt(about = "Import a package from the Aleo Package Manager")] + // Add { + // #[structopt(flatten)] + // command: Add, + // }, + #[structopt(about = "Pull dependencies from Aleo Package Manager")] + Fetch { #[structopt(flatten)] - command: Add, + command: Fetch, }, #[structopt(about = "Clone a package from the Aleo Package Manager")] @@ -149,12 +154,11 @@ enum CommandOpts { command: Publish, }, - #[structopt(about = "Uninstall a package from the current package")] - Remove { - #[structopt(flatten)] - command: Remove, - }, - + // #[structopt(about = "Uninstall a package from the current package")] + // Remove { + // #[structopt(flatten)] + // command: Remove, + // }, #[structopt(about = "Lints the Leo files in the package (*)")] Lint { #[structopt(flatten)] @@ -204,13 +208,13 @@ fn run_with_args(opt: Opt) -> Result<()> { CommandOpts::Watch { command } => command.try_execute(context), CommandOpts::Update { command } => command.try_execute(context), - CommandOpts::Add { command } => command.try_execute(context), + // CommandOpts::Add { command } => command.try_execute(context), + CommandOpts::Fetch { command } => command.try_execute(context), CommandOpts::Clone { command } => command.try_execute(context), CommandOpts::Login { command } => command.try_execute(context), CommandOpts::Logout { command } => command.try_execute(context), CommandOpts::Publish { command } => command.try_execute(context), - CommandOpts::Remove { command } => command.try_execute(context), - + // CommandOpts::Remove { command } => command.try_execute(context), CommandOpts::Lint { command } => command.try_execute(context), CommandOpts::Deploy { command } => command.try_execute(context), } @@ -231,6 +235,7 @@ mod cli_tests { use crate::{run_with_args, Opt}; use leo_errors::{CliError, Result}; + use snarkvm_utilities::Write; use std::path::PathBuf; use structopt::StructOpt; use test_dir::{DirBuilder, FileType, TestDir}; @@ -347,6 +352,7 @@ mod cli_tests { } #[test] + #[ignore] fn test_import() { let dir = testdir("test"); let path = dir.path("test"); @@ -388,4 +394,32 @@ mod cli_tests { assert!(run_cmd("leo test -f examples/silly-sudoku/src/lib.leo", path).is_ok()); assert!(run_cmd("leo test -f examples/silly-sudoku/src/main.leo", path).is_ok()); } + + #[test] + fn test_install() { + let dir = testdir("test"); + let path = dir.path("test"); + + assert!(run_cmd("leo new install", &Some(path.clone())).is_ok()); + + let install_path = &Some(path.join("install")); + + let mut file = std::fs::OpenOptions::new() + .write(true) + .append(true) + .open(path.join("install/Leo.toml")) + .unwrap(); + + assert!(run_cmd("leo fetch", install_path).is_ok()); + assert!(file + .write_all( + br#" + sudoku = {author = "justice-league", package = "u8u32", version = "0.1.0"} + "# + ) + .is_ok()); + + assert!(run_cmd("leo fetch", install_path).is_ok()); + assert!(run_cmd("leo build", install_path).is_ok()); + } } diff --git a/package/Cargo.toml b/package/Cargo.toml index 2e79acfb60..48a1ca3f2c 100644 --- a/package/Cargo.toml +++ b/package/Cargo.toml @@ -21,6 +21,10 @@ edition = "2018" path = "../errors" version = "1.5.3" +[dependencies.indexmap] +version = "1.7" +features = ["serde"] + [dependencies.serde] version = "1.0" features = [ "derive" ] diff --git a/package/README.md b/package/README.md index dc16bf3844..aa62f7c2a7 100644 --- a/package/README.md +++ b/package/README.md @@ -4,6 +4,27 @@ [![Authors](https://img.shields.io/badge/authors-Aleo-orange.svg)](../AUTHORS) [![License](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE.md) -This module defines the structure of a Leo project package. +## Description -Each file in this directory mirrors a corresponding file generated in a new Leo project package. +This module defines the structure of a Leo project package. And describes behavior of package internals, such +as Leo Manifest (Leo.toml), Lock File (Leo.lock), source files and imports. + +Mainly used by Leo binary. + +## Structure + +Each directory in this crate mirrors a corresponding file generated in a new Leo project package: + +```bash +package/src +├── errors # crate level error definitions +├── imports # program imports management +├── inputs # program inputs directory +├── outputs # program outputs directory +├── root # program root: Leo.toml, Leo.lock +└── source # source files directory +``` + +## Testing + +Package features functional tests in the `tests/` directory. diff --git a/package/src/root/lock_file.rs b/package/src/root/lock_file.rs new file mode 100644 index 0000000000..82b00c5f8f --- /dev/null +++ b/package/src/root/lock_file.rs @@ -0,0 +1,160 @@ +// Copyright (C) 2019-2021 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::root::Dependency; +use leo_errors::{PackageError, Result}; + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + collections::HashMap, + convert::TryFrom, + fmt::{self, Display}, + fs::File, + io::{Read, Write}, + path::Path, +}; + +pub const LOCKFILE_FILENAME: &str = "Leo.lock"; + +/// Lock-file struct, contains all information about imported dependencies +/// and their relationships. +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct LockFile { + pub package: Vec, +} + +/// Single dependency record. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Package { + pub name: String, + pub version: String, + pub author: String, + pub import_name: Option, + #[serde(skip_serializing_if = "IndexMap::is_empty", default)] + pub dependencies: IndexMap, +} + +impl LockFile { + pub fn new() -> Self { + LockFile { package: vec![] } + } + + /// Check if LockFile exists in a directory. + pub fn exists_at(path: &Path) -> bool { + let mut path = Cow::from(path); + if path.is_dir() { + path.to_mut().push(LOCKFILE_FILENAME); + } + path.exists() + } + + /// Add Package record to the lock file. Chainable. + pub fn add_package(&mut self, package: Package) -> &mut Self { + self.package.push(package); + self + } + + /// Print LockFile as toml. + pub fn to_string(&self) -> Result { + Ok(toml::to_string(self).map_err(PackageError::failed_to_serialize_lock_file)?) + } + + /// Form a HashMap of kind: + /// ``` imported_name => package_name ``` + /// for all imported packages. + pub fn to_import_map(&self) -> HashMap { + let mut result = HashMap::new(); + for package in self.package.iter() { + match &package.import_name { + Some(name) => result.insert(name.clone(), package.to_string()), + None => result.insert(package.name.clone(), package.to_string()), + }; + } + + result + } + + /// Write Leo.lock to the given location. + pub fn write_to(self, path: &Path) -> Result<()> { + let mut path = Cow::from(path); + if path.is_dir() { + path.to_mut().push(LOCKFILE_FILENAME); + } + + File::create(&path) + .map_err(|error| PackageError::failed_to_create_lock_file(LOCKFILE_FILENAME, error))? + .write_all(self.to_string()?.as_bytes()) + .map_err(|error| PackageError::failed_to_write_lock_file(LOCKFILE_FILENAME, error))?; + + Ok(()) + } +} + +impl TryFrom<&Path> for LockFile { + type Error = PackageError; + + fn try_from(path: &Path) -> Result { + let mut path = Cow::from(path); + if path.is_dir() { + path.to_mut().push(LOCKFILE_FILENAME); + } + + let mut file = File::open(path.clone()) + .map_err(|error| PackageError::failed_to_open_lock_file(LOCKFILE_FILENAME, error))?; + let size = file + .metadata() + .map_err(|error| PackageError::failed_to_get_lock_file_metadata(LOCKFILE_FILENAME, error))? + .len() as usize; + + let mut buffer = String::with_capacity(size); + file.read_to_string(&mut buffer) + .map_err(|error| PackageError::failed_to_read_lock_file(LOCKFILE_FILENAME, error))?; + + toml::from_str(&buffer).map_err(|error| PackageError::failed_to_parse_lock_file(LOCKFILE_FILENAME, error)) + } +} + +impl Package { + /// Fill dependencies from Leo Manifest data. + pub fn add_dependencies(&mut self, dependencies: &IndexMap) { + for (import_name, dependency) in dependencies.iter() { + self.dependencies + .insert(import_name.clone(), Package::from(dependency).to_string()); + } + } +} + +impl Display for Package { + /// Form an path identifier for a package. It is the path under which package is stored + /// inside the `imports/` directory. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}-{}@{}", self.author, self.name, self.version) + } +} + +impl From<&Dependency> for Package { + fn from(dependency: &Dependency) -> Package { + Package { + name: dependency.package.clone(), + author: dependency.author.clone(), + version: dependency.version.clone(), + dependencies: Default::default(), + import_name: None, + } + } +} diff --git a/package/src/root/manifest.rs b/package/src/root/manifest.rs index fc08033cff..c72624bc96 100644 --- a/package/src/root/manifest.rs +++ b/package/src/root/manifest.rs @@ -17,9 +17,11 @@ use crate::package::Package; use leo_errors::{PackageError, Result}; +use indexmap::IndexMap; use serde::Deserialize; use std::{ borrow::Cow, + collections::HashMap, convert::TryFrom, fs::File, io::{Read, Write}, @@ -34,10 +36,18 @@ pub struct Remote { pub author: String, } +#[derive(Clone, Debug, Deserialize)] +pub struct Dependency { + pub author: String, + pub version: String, + pub package: String, +} + #[derive(Deserialize)] pub struct Manifest { pub project: Package, pub remote: Option, + pub dependencies: Option>, } impl Manifest { @@ -45,6 +55,7 @@ impl Manifest { Ok(Self { project: Package::new(package_name)?, remote: author.map(|author| Remote { author }), + dependencies: Some(IndexMap::::new()), }) } @@ -72,6 +83,27 @@ impl Manifest { self.project.description.clone() } + pub fn get_package_dependencies(&self) -> Option> { + self.dependencies.clone() + } + + /// Get HashMap of kind: + /// import name => import directory + /// Which then used in AST/ASG to resolve import paths. + pub fn get_imports_map(&self) -> Option> { + self.dependencies.clone().map(|dependencies| { + dependencies + .into_iter() + .map(|(name, dependency)| { + ( + name, + format!("{}-{}@{}", dependency.author, dependency.package, dependency.version), + ) + }) + .collect() + }) + } + pub fn get_package_license(&self) -> Option { self.project.license.clone() } @@ -109,6 +141,14 @@ license = "MIT" [remote] author = "{author}" # Add your Aleo Package Manager username or team name. + +[target] +curve = "bls12_377" +proving_system = "groth16" + +[dependencies] +# Define dependencies here in format: +# name = {{ package = "package-name", author = "author", version = "version" }} "#, name = self.project.name, author = author diff --git a/package/src/root/mod.rs b/package/src/root/mod.rs index 6b90439ca1..3a6c2b95a0 100644 --- a/package/src/root/mod.rs +++ b/package/src/root/mod.rs @@ -17,6 +17,9 @@ pub mod gitignore; pub use self::gitignore::*; +pub mod lock_file; +pub use self::lock_file::*; + pub mod manifest; pub use self::manifest::*; diff --git a/package/src/root/zip.rs b/package/src/root/zip.rs index bf95a9aaf7..b40811ae47 100644 --- a/package/src/root/zip.rs +++ b/package/src/root/zip.rs @@ -156,10 +156,8 @@ impl ZipFile { /// Check if the file path should be included in the package zip file. fn is_included(path: &Path) -> bool { - // excluded directories: `output`, `imports` - if path.ends_with(OUTPUTS_DIRECTORY_NAME.trim_end_matches('/')) - | path.ends_with(IMPORTS_DIRECTORY_NAME.trim_end_matches('/')) - { + // DO NOT include `imports` and `outputs` directories. + if path.starts_with(IMPORTS_DIRECTORY_NAME) || path.starts_with(OUTPUTS_DIRECTORY_NAME) { return false; }