From 9dd469596feb00e31fbfcd616b346f94d51ea625 Mon Sep 17 00:00:00 2001 From: Marek Kaput Date: Fri, 8 Sep 2023 15:49:32 +0200 Subject: [PATCH] Scaffold `scarb package` command, implementing `--list` arg commit-id:cd0036df --- scarb/src/bin/scarb/args.rs | 17 +++ scarb/src/bin/scarb/commands/mod.rs | 2 + scarb/src/bin/scarb/commands/package.rs | 67 +++++++++ scarb/src/ops/mod.rs | 2 + scarb/src/ops/package.rs | 179 ++++++++++++++++++++++++ scarb/tests/package.rs | 63 +++++++++ 6 files changed, 330 insertions(+) create mode 100644 scarb/src/bin/scarb/commands/package.rs create mode 100644 scarb/src/ops/package.rs create mode 100644 scarb/tests/package.rs diff --git a/scarb/src/bin/scarb/args.rs b/scarb/src/bin/scarb/args.rs index d37856db6..16806967a 100644 --- a/scarb/src/bin/scarb/args.rs +++ b/scarb/src/bin/scarb/args.rs @@ -150,6 +150,12 @@ pub enum Command { Metadata(MetadataArgs), /// Create a new Scarb package at . New(NewArgs), + /// Assemble the local package into a distributable tarball. + #[command(after_help = "\ + This command will create distributable, compressed `.tar.zstd` archives containing source \ + codes of selected packages. Resulting files will be placed in `target/package` directory. + ")] + Package(PackageArgs), /// Run arbitrary package scripts. Run(ScriptsRunnerArgs), /// Execute all unit and integration tests of a local package. @@ -300,6 +306,17 @@ pub struct TestArgs { pub args: Vec, } +/// Arguments accepted by the `package` command. +#[derive(Parser, Clone, Debug)] +pub struct PackageArgs { + /// Print files included in a package without making one. + #[arg(short, long)] + pub list: bool, + + #[command(flatten)] + pub packages_filter: PackagesFilter, +} + /// Git reference specification arguments. #[derive(Parser, Clone, Debug)] #[group(requires = "git", multiple = false)] diff --git a/scarb/src/bin/scarb/commands/mod.rs b/scarb/src/bin/scarb/commands/mod.rs index b0e7c41f0..d96f27cf2 100644 --- a/scarb/src/bin/scarb/commands/mod.rs +++ b/scarb/src/bin/scarb/commands/mod.rs @@ -19,6 +19,7 @@ pub mod init; pub mod manifest_path; pub mod metadata; pub mod new; +pub mod package; pub mod remove; pub mod run; pub mod test; @@ -41,6 +42,7 @@ pub fn run(command: Command, config: &mut Config) -> Result<()> { ManifestPath => manifest_path::run(config), Metadata(args) => metadata::run(args, config), New(args) => new::run(args, config), + Package(args) => package::run(args, config), Remove(args) => remove::run(args, config), Run(args) => run::run(args, config), Test(args) => test::run(args, config), diff --git a/scarb/src/bin/scarb/commands/package.rs b/scarb/src/bin/scarb/commands/package.rs new file mode 100644 index 000000000..58a191d33 --- /dev/null +++ b/scarb/src/bin/scarb/commands/package.rs @@ -0,0 +1,67 @@ +use std::collections::BTreeMap; + +use anyhow::Result; +use camino::Utf8PathBuf; +use serde::Serializer; + +use scarb::core::{Config, PackageName}; +use scarb::ops; +use scarb::ops::PackageOpts; +use scarb_ui::Message; + +use crate::args::PackageArgs; + +#[tracing::instrument(skip_all, level = "info")] +pub fn run(args: PackageArgs, config: &Config) -> Result<()> { + let ws = ops::read_workspace(config.manifest_path(), config)?; + let packages = args + .packages_filter + .match_many(&ws)? + .into_iter() + .map(|p| p.id) + .collect::>(); + + let opts = PackageOpts; + + if args.list { + let result = ops::package_list(&packages, &opts, &ws)?; + ws.config().ui().print(ListMessage(result)); + } else { + ops::package(&packages, &opts, &ws)?; + } + + Ok(()) +} + +struct ListMessage(BTreeMap>); + +impl Message for ListMessage { + fn print_text(self) + where + Self: Sized, + { + let mut first = true; + let single = self.0.len() == 1; + for (package, files) in self.0 { + if !single { + if !first { + println!(); + } + println!("{package}:",); + } + + for file in files { + println!("{file}"); + } + + first = false; + } + } + + fn structured(self, _ser: S) -> Result + where + Self: Sized, + { + todo!("JSON output is not implemented yet.") + } +} diff --git a/scarb/src/ops/mod.rs b/scarb/src/ops/mod.rs index dbe17d9af..36aa58b00 100644 --- a/scarb/src/ops/mod.rs +++ b/scarb/src/ops/mod.rs @@ -9,6 +9,7 @@ pub use fmt::*; pub use manifest::*; pub use metadata::*; pub use new::*; +pub use package::*; pub use resolve::*; pub use scripts::*; pub use subcommands::*; @@ -25,3 +26,4 @@ mod resolve; mod scripts; mod subcommands; mod workspace; +mod package; diff --git a/scarb/src/ops/package.rs b/scarb/src/ops/package.rs new file mode 100644 index 000000000..4008f27fa --- /dev/null +++ b/scarb/src/ops/package.rs @@ -0,0 +1,179 @@ +use std::collections::BTreeMap; + +use anyhow::{ensure, Result}; +use camino::Utf8PathBuf; + +use scarb_ui::components::Status; + +use crate::core::{Package, PackageId, PackageName, Workspace}; +use crate::flock::FileLockGuard; +use crate::{ops, DEFAULT_SOURCE_PATH, MANIFEST_FILE_NAME}; + +const VERSION: u8 = 1; +const VERSION_FILE_NAME: &str = "VERSION"; +const ORIGINAL_MANIFEST_FILE_NAME: &str = "Scarb.orig.toml"; + +const RESERVED_FILES: &[&str] = &[VERSION_FILE_NAME, ORIGINAL_MANIFEST_FILE_NAME]; + +pub struct PackageOpts; + +/// A listing of files to include in the archive, without actually building it yet. +/// +/// This struct is used to facilitate both building the package, and listing its contents without +/// actually making it. +type ArchiveRecipe = Vec; + +struct ArchiveFile { + /// The relative path in the archive (not including top-level package name directory). + path: Utf8PathBuf, + #[allow(dead_code)] + /// The contents of the file. + contents: ArchiveFileContents, +} + +enum ArchiveFileContents { + /// Absolute path to the file on disk to add to the archive. + OnDisk(Utf8PathBuf), + /// Generate file contents automatically. + /// + /// This variant stores a closure, so that file generation can be deferred to the very moment + /// it is needed. + /// For example, when listing package contents, we do not have files contents. + Generated(Box Result>>), +} + +pub fn package( + packages: &[PackageId], + opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result> { + before_package(ws)?; + + packages + .iter() + .map(|pkg| { + let pkg_name = pkg.to_string(); + let message = Status::new("Packaging", &pkg_name); + if packages.len() <= 1 { + ws.config().ui().verbose(message); + } else { + ws.config().ui().print(message); + } + + package_one_impl(*pkg, opts, ws) + }) + .collect() +} + +pub fn package_one( + package: PackageId, + opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result { + before_package(ws)?; + package_one_impl(package, opts, ws) +} + +pub fn package_list( + packages: &[PackageId], + opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result>> { + packages + .iter() + .map(|pkg| Ok((pkg.name.clone(), list_one_impl(*pkg, opts, ws)?))) + .collect() +} + +fn before_package(ws: &Workspace<'_>) -> Result<()> { + ops::resolve_workspace(ws)?; + Ok(()) +} + +fn package_one_impl( + pkg_id: PackageId, + _opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result { + let pkg = ws.fetch_package(&pkg_id)?; + + // TODO(mkaput): Check metadata + + // TODO(#643): Check dirty in VCS (but do not do it when listing!). + + let _recipe = prepare_archive_recipe(pkg)?; + + todo!("Actual packaging is not implemented yet.") +} + +fn list_one_impl( + pkg_id: PackageId, + _opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result> { + let pkg = ws.fetch_package(&pkg_id)?; + let recipe = prepare_archive_recipe(pkg)?; + Ok(recipe.into_iter().map(|f| f.path).collect()) +} + +fn prepare_archive_recipe(pkg: &Package) -> Result { + let mut recipe = source_files(pkg)?; + + check_no_reserved_files(&recipe)?; + + // Add normalized manifest file. + recipe.push(ArchiveFile { + path: MANIFEST_FILE_NAME.into(), + contents: ArchiveFileContents::Generated(Box::new({ + let pkg = pkg.clone(); + || normalize_manifest(pkg) + })), + }); + + // Add original manifest file. + recipe.push(ArchiveFile { + path: ORIGINAL_MANIFEST_FILE_NAME.into(), + contents: ArchiveFileContents::OnDisk(pkg.manifest_path().to_owned()), + }); + + // Add archive version file. + recipe.push(ArchiveFile { + path: VERSION_FILE_NAME.into(), + contents: ArchiveFileContents::Generated(Box::new(|| Ok(VERSION.to_string().into_bytes()))), + }); + + // Sort archive files alphabetically, putting the version file first. + recipe.sort_unstable_by_key(|f| { + let priority = if f.path == VERSION_FILE_NAME { 0 } else { 1 }; + (priority, f.path.clone()) + }); + + Ok(recipe) +} + +fn source_files(pkg: &Package) -> Result { + // TODO(mkaput): Implement this properly. + Ok(vec![ArchiveFile { + path: DEFAULT_SOURCE_PATH.into(), + contents: ArchiveFileContents::OnDisk(pkg.root().join(DEFAULT_SOURCE_PATH)), + }]) +} + +fn check_no_reserved_files(recipe: &ArchiveRecipe) -> Result<()> { + let mut found = Vec::new(); + for file in recipe { + if RESERVED_FILES.contains(&file.path.as_str()) { + found.push(file.path.as_str()); + } + } + ensure!( + found.is_empty(), + "invalid inclusion of reserved files in package: {}", + found.join(", ") + ); + Ok(()) +} + +fn normalize_manifest(_pkg: Package) -> Result> { + todo!("Manifest normalization is not implemented yet.") +} diff --git a/scarb/tests/package.rs b/scarb/tests/package.rs new file mode 100644 index 000000000..d275756cf --- /dev/null +++ b/scarb/tests/package.rs @@ -0,0 +1,63 @@ +use assert_fs::fixture::PathChild; +use assert_fs::TempDir; +use indoc::indoc; + +use scarb_test_support::command::Scarb; +use scarb_test_support::project_builder::ProjectBuilder; +use scarb_test_support::workspace_builder::WorkspaceBuilder; + +#[test] +fn list_simple() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start() + .name("foo") + .version("1.0.0") + .build(&t); + + Scarb::quick_snapbox() + .arg("package") + .arg("--list") + .current_dir(&t) + .assert() + .success() + .stdout_eq(indoc! {r#" + VERSION + Scarb.orig.toml + Scarb.toml + src/lib.cairo + "#}); +} + +#[test] +fn list_workspace() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start().name("first").build(&t.child("first")); + ProjectBuilder::start() + .name("second") + .build(&t.child("second")); + WorkspaceBuilder::start() + // Trick to test if packages are sorted alphabetically by name in the output. + .add_member("second") + .add_member("first") + .build(&t); + + Scarb::quick_snapbox() + .arg("package") + .arg("--list") + .current_dir(&t) + .assert() + .success() + .stdout_eq(indoc! {r#" + first: + VERSION + Scarb.orig.toml + Scarb.toml + src/lib.cairo + + second: + VERSION + Scarb.orig.toml + Scarb.toml + src/lib.cairo + "#}); +}