Skip to content

Commit

Permalink
Scaffold scarb package command, implementing --list arg
Browse files Browse the repository at this point in the history
commit-id:cd0036df
  • Loading branch information
mkaput committed Sep 27, 2023
1 parent a25f452 commit cfb970a
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 0 deletions.
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
194 changes: 194 additions & 0 deletions scarb/src/ops/package.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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<'a> = Vec<ArchiveFile<'a>>;

struct ArchiveFile<'a> {
/// 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<'a>,
}

enum ArchiveFileContents<'a> {
/// 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>> + 'a>),
}

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_one(
package: PackageId,
opts: &PackageOpts,
ws: &Workspace<'_>,
) -> Result<FileLockGuard> {
before_package(ws)?;
package_one_impl(package, opts, ws)
}

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, ws)?;

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, ws)?;
Ok(recipe.into_iter().map(|f| f.path).collect())
}

fn prepare_archive_recipe<'a>(
pkg: &'a Package,
ws: &'a Workspace<'_>,
) -> Result<ArchiveRecipe<'a>> {
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(|| normalize_manifest(pkg, ws))),
});

// 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, _ws: &Workspace<'_>) -> 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
"#});
}

0 comments on commit cfb970a

Please sign in to comment.