diff --git a/.circleci/config.yml b/.circleci/config.yml index d758e2dadd..5bbb00055f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -138,6 +138,32 @@ jobs: export LEO=/home/circleci/project/project/bin/leo ./project/.circleci/leo-login-logout.sh + leo-clone: + docker: + - image: cimg/rust:1.50.0 + resource_class: xlarge + steps: + - attach_workspace: + at: /home/circleci/project/ + - run: + name: leo clone + command: | + export LEO=/home/circleci/project/project/bin/leo + ./project/.circleci/leo-clone.sh + + leo-publish: + docker: + - image: cimg/rust:1.50.0 + resource_class: xlarge + steps: + - attach_workspace: + at: /home/circleci/project/ + - run: + name: leo publish + command: | + export LEO=/home/circleci/project/project/bin/leo + ./project/.circleci/leo-publish.sh + workflows: version: 2 main-workflow: @@ -160,4 +186,10 @@ workflows: - rust-stable - leo-login-logout: requires: - - rust-stable \ No newline at end of file + - rust-stable + - leo-clone: + requires: + - rust-stable + - leo-publish: + requires: + - rust-stable diff --git a/.circleci/leo-clone.sh b/.circleci/leo-clone.sh new file mode 100755 index 0000000000..c625b8d355 --- /dev/null +++ b/.circleci/leo-clone.sh @@ -0,0 +1,18 @@ +# leo clone + +# Clone the test-app package. +export PACKAGE="$ALEO_PM_USERNAME/test-app" +$LEO clone $PACKAGE + +# Assert that the 'test-app' folder is not empty + +cd test-app || exit 1 +if [ "$(ls -A $DIR)" ]; then + echo "$DIR is not empty" +else + echo "$DIR is empty" + exit 1 +fi + +ls -la +$LEO run diff --git a/.circleci/leo-publish.sh b/.circleci/leo-publish.sh new file mode 100755 index 0000000000..f41109dffd --- /dev/null +++ b/.circleci/leo-publish.sh @@ -0,0 +1,56 @@ +# leo login, publish and logout + +# Login +$LEO login -u "$ALEO_PM_USERNAME" -p "$ALEO_PM_PASSWORD" + +# Clone the test-app package. +export PACKAGE="$ALEO_PM_USERNAME/test-app" +$LEO clone $PACKAGE +cd test-app || exit 1 + +# Fetch the current Leo package version number. +# +# 1. Print out the Leo.toml file. +# 2. Search for a line with the word "version". +# 3. Isolate that into a single line. +# 4. Split the line from the '=' sign and keep the right-hand side. +# 5. Remove the quotes around the version number. +# 6. Trim any excess whitespace. +export CURRENT=$(cat Leo.toml \ +| grep version \ +| head -1 \ +| awk -F= '{ print $2 }' \ +| sed 's/[",]//g' \ +| xargs) + +# Increment the current Leo package version number by 1. +# +# 1. Print out the Leo.toml file. +# 2. Search for a line with the word "version". +# 3. Isolate that into a single line. +# 4. Split the line from the '=' sign and keep the right-hand side. +# 5. Remove the quotes around the version number. +# 6. Trim any excess whitespace. +# 7. Increment the version number by 1 (on the semver patch). +# +# https://stackoverflow.com/questions/8653126/how-to-increment-version-number-in-a-shell-script +export UPDATED=$(cat Leo.toml \ +| grep version \ +| head -1 \ +| awk -F= '{ print $2 }' \ +| sed 's/[",]//g' \ +| xargs \ +| awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') + +# Write the updated Leo package version number to the Leo.toml file. +export TOML=$(cat Leo.toml | sed "s/$CURRENT/$UPDATED/g") +echo "$TOML" > Leo.toml + +# Run the package to confirm the manifest remains well-formed. +$LEO run + +# Publish the package to Aleo.pm +$LEO publish + +# Logout +$LEO logout diff --git a/leo/commands/package/add.rs b/leo/commands/package/add.rs index 0b97c67bb4..d70f7be8b1 100644 --- a/leo/commands/package/add.rs +++ b/leo/commands/package/add.rs @@ -25,7 +25,7 @@ use std::{ use structopt::StructOpt; use tracing::Span; -/// Add package from Aleo Package Manager +/// Add a package from Aleo Package Manager #[derive(StructOpt, Debug)] #[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] pub struct Add { @@ -58,7 +58,7 @@ impl Add { } /// Try to parse author/package string from self.remote - pub fn try_read_arguments(&self) -> Result<(String, String)> { + fn try_read_arguments(&self) -> Result<(String, String)> { if let Some(val) = &self.remote { let v: Vec<&str> = val.split('/').collect(); if v.len() == 2 { @@ -91,42 +91,41 @@ impl Command for Add { } fn apply(self, context: Context, _: Self::Input) -> Result { - // checking that manifest exists... + // Check that a manifest exists for the current package. if context.manifest().is_err() { - return Err(anyhow!("Package Manifest not found, try running leo init or leo new")); + return Err(anyhow!("Package manifest not found, try running `leo init`")); }; let (author, package_name) = match self.try_read_arguments() { Ok((author, package)) => (author, package), Err(err) => return Err(err), }; - let version = self.version; - // build request body (Options are skipped when sealizing) - let fetch = Fetch { - author, - package_name: package_name.clone(), - version, + // Attempt to fetch the package. + let reader = { + let fetch = Fetch { + author, + package_name: package_name.clone(), + version: self.version, + }; + let bytes = context.api.run_route(fetch)?.bytes()?; + std::io::Cursor::new(bytes) }; - let bytes = context.api.run_route(fetch)?.bytes()?; + // Construct the directory structure. let mut path = context.dir()?; - { - // setup directory structure since request was success ImportsDirectory::create(&path)?; path.push(IMPORTS_DIRECTORY_NAME); path.push(package_name); create_dir_all(&path)?; }; - let reader = std::io::Cursor::new(bytes); - + // Proceed to unzip and parse the fetched bytes. let mut zip_archive = match zip::ZipArchive::new(reader) { Ok(zip) => zip, Err(error) => return Err(anyhow!(error)), }; - for i in 0..zip_archive.len() { let file = match zip_archive.by_index(i) { Ok(file) => file, diff --git a/leo/commands/package/clone.rs b/leo/commands/package/clone.rs new file mode 100644 index 0000000000..37d829aaf4 --- /dev/null +++ b/leo/commands/package/clone.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::{api::Fetch, commands::Command, context::Context}; + +use anyhow::{anyhow, Result}; +use std::{ + borrow::Cow, + fs::{self, File}, + io::{Read, Write}, + path::Path, +}; +use structopt::StructOpt; +use tracing::Span; + +/// Clone a package from Aleo Package Manager +#[derive(StructOpt, Debug)] +#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)] +pub struct Clone { + #[structopt(name = "REMOTE")] + remote: Option, + + #[structopt(name = "author", help = "Specify a package author", long = "author", short = "a")] + author: Option, + + #[structopt(name = "package", help = "Specify a package name", long = "package", short = "p")] + package: Option, + + #[structopt(name = "version", help = "Specify a package version", long = "version", short = "v")] + version: Option, +} + +impl Clone { + pub fn new( + remote: Option, + author: Option, + package: Option, + version: Option, + ) -> Self { + Self { + remote, + author, + package, + version, + } + } + + /// Try to parse author/package string from self.remote + fn try_read_arguments(&self) -> Result<(String, String)> { + if let Some(val) = &self.remote { + let v: Vec<&str> = val.split('/').collect(); + if v.len() == 2 { + Ok((v[0].to_string(), v[1].to_string())) + } else { + Err(anyhow!( + "Incorrect argument, please use --help for information on command use" + )) + } + } else if let (Some(author), Some(package)) = (&self.author, &self.package) { + Ok((author.clone(), package.clone())) + } else { + Err(anyhow!( + "Incorrect argument, please use --help for information on command use" + )) + } + } + + /// Creates a directory at the provided path with the given directory name. + fn create_directory(path: &Path, directory_name: &str) -> Result<()> { + let mut path = Cow::from(path); + + // Check that the path ends in the directory name. + // If it does not, proceed to append the directory name to the path. + if path.is_dir() && !path.ends_with(directory_name) { + path.to_mut().push(directory_name); + } + + Ok(fs::create_dir_all(&path)?) + } +} + +impl Command for Clone { + type Input = (); + type Output = (); + + fn log_span(&self) -> Span { + tracing::span!(tracing::Level::INFO, "Cloning") + } + + fn prelude(&self) -> Result { + Ok(()) + } + + fn apply(self, context: Context, _: Self::Input) -> Result { + let (author, package_name) = match self.try_read_arguments() { + Ok((author, package)) => (author, package), + Err(err) => return Err(err), + }; + + // Attempt to fetch the package. + let reader = { + let fetch = Fetch { + author, + package_name: package_name.clone(), + version: self.version, + }; + let bytes = context.api.run_route(fetch)?.bytes()?; + std::io::Cursor::new(bytes) + }; + + // Construct the directory structure. + let mut path = context.dir()?; + path.push(package_name.clone()); + Self::create_directory(&path, &package_name)?; + + // Proceed to unzip and parse the fetched bytes. + let mut zip_archive = match zip::ZipArchive::new(reader) { + Ok(zip) => zip, + Err(error) => return Err(anyhow!(error)), + }; + for i in 0..zip_archive.len() { + let file = match zip_archive.by_index(i) { + Ok(file) => file, + Err(error) => return Err(anyhow!(error)), + }; + + let file_name = file.name(); + + let mut file_path = path.clone(); + file_path.push(file_name); + + if file_name.ends_with('/') { + fs::create_dir_all(file_path)?; + } else { + if let Some(parent_directory) = path.parent() { + fs::create_dir_all(parent_directory)?; + } + + File::create(file_path)?.write_all(&file.bytes().map(|e| e.unwrap()).collect::>())?; + } + } + + tracing::info!("Successfully cloned {}", package_name); + + Ok(()) + } +} diff --git a/leo/commands/package/mod.rs b/leo/commands/package/mod.rs index f3b292715f..ce8c637f59 100644 --- a/leo/commands/package/mod.rs +++ b/leo/commands/package/mod.rs @@ -17,6 +17,9 @@ pub mod add; pub use add::Add; +pub mod clone; +pub use clone::Clone; + pub mod login; pub use login::Login; diff --git a/leo/main.rs b/leo/main.rs index 6fe3cf92ac..f42cd412f6 100644 --- a/leo/main.rs +++ b/leo/main.rs @@ -23,7 +23,7 @@ pub mod synthesizer; pub mod updater; use commands::{ - package::{Add, Login, Logout, Publish, Remove}, + package::{Add, Clone, Login, Logout, Publish, Remove}, Build, Clean, Command, @@ -121,12 +121,18 @@ enum CommandOpts { command: Test, }, - #[structopt(about = "Install a package from the Aleo Package Manager")] + #[structopt(about = "Import a package from the Aleo Package Manager")] Add { #[structopt(flatten)] command: Add, }, + #[structopt(about = "Clone a package from the Aleo Package Manager")] + Clone { + #[structopt(flatten)] + command: Clone, + }, + #[structopt(about = "Login to the Aleo Package Manager")] Login { #[structopt(flatten)] @@ -189,6 +195,7 @@ fn main() { CommandOpts::Update { command } => command.try_execute(), CommandOpts::Add { command } => command.try_execute(), + CommandOpts::Clone { command } => command.try_execute(), CommandOpts::Login { command } => command.try_execute(), CommandOpts::Logout { command } => command.try_execute(), CommandOpts::Publish { command } => command.try_execute(),