Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scaffold scarb package command, implementing --list arg #657

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions scarb/src/bin/scarb/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pub enum Command {
Metadata(MetadataArgs),
/// Create a new Scarb package at <PATH>.
New(NewArgs),
/// Assemble the local package into a distributable tarball.
#[command(after_help = "\
This command will create distributable, compressed `.tar.zst` 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.
Expand Down Expand Up @@ -304,6 +310,17 @@ pub struct TestArgs {
pub args: Vec<OsString>,
}

/// 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)]
Expand Down
2 changes: 2 additions & 0 deletions scarb/src/bin/scarb/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down
67 changes: 67 additions & 0 deletions scarb/src/bin/scarb/commands/package.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();

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<PackageName, Vec<Utf8PathBuf>>);

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<S: Serializer>(self, _ser: S) -> Result<S::Ok, S::Error>
where
Self: Sized,
{
todo!("JSON output is not implemented yet.")
}
}
2 changes: 2 additions & 0 deletions scarb/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -21,6 +22,7 @@ mod fmt;
mod manifest;
mod metadata;
mod new;
mod package;
mod resolve;
mod scripts;
mod subcommands;
Expand Down
185 changes: 185 additions & 0 deletions scarb/src/ops/package.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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<ArchiveFile>;

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<dyn FnOnce() -> Result<Vec<u8>>>),
}

pub fn package(
packages: &[PackageId],
opts: &PackageOpts,
ws: &Workspace<'_>,
) -> Result<Vec<FileLockGuard>> {
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_list(
packages: &[PackageId],
opts: &PackageOpts,
ws: &Workspace<'_>,
) -> Result<BTreeMap<PackageName, Vec<Utf8PathBuf>>> {
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<FileLockGuard> {
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<Vec<Utf8PathBuf>> {
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<ArchiveRecipe> {
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({
let pkg = pkg.clone();
Box::new(|| 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<ArchiveRecipe> {
// TODO(mkaput): Implement this properly.
let mut recipe = vec![ArchiveFile {
path: DEFAULT_SOURCE_PATH.into(),
contents: ArchiveFileContents::OnDisk(pkg.root().join(DEFAULT_SOURCE_PATH)),
}];

// Add reserved files if they exist in source. They will be rejected later on.
for &file in RESERVED_FILES {
let path = pkg.root().join(file);
if path.exists() {
recipe.push(ArchiveFile {
path: file.into(),
contents: ArchiveFileContents::OnDisk(path),
});
}
}

Ok(recipe)
}

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<Vec<u8>> {
// TODO(mkaput): Implement this properly.
Ok("[package]".to_string().into_bytes())
}
65 changes: 65 additions & 0 deletions scarb/tests/package.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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
"#});
}