diff --git a/Cargo.lock b/Cargo.lock index 7827509a4..e5566937c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4603,6 +4603,7 @@ dependencies = [ "cairo-lang-test-plugin", "cairo-lang-utils", "camino", + "cargo_metadata", "clap", "convert_case", "create-output-dir", diff --git a/scarb/Cargo.toml b/scarb/Cargo.toml index 0f9cf52f5..cb0cc9392 100644 --- a/scarb/Cargo.toml +++ b/scarb/Cargo.toml @@ -85,6 +85,7 @@ walkdir.workspace = true which.workspace = true windows-sys.workspace = true zstd.workspace = true +cargo_metadata.workspace = true [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { workspace = true, default-features = true } diff --git a/scarb/src/compiler/plugin/mod.rs b/scarb/src/compiler/plugin/mod.rs index 54c0681c6..30838566f 100644 --- a/scarb/src/compiler/plugin/mod.rs +++ b/scarb/src/compiler/plugin/mod.rs @@ -31,7 +31,7 @@ pub fn fetch_cairo_plugin(package: &Package, ws: &Workspace<'_>) -> Result<()> { let props: CairoPluginProps = target.props()?; // No need to fetch for buildin plugins. if !props.builtin { - proc_macro::fetch_package(package, ws)?; + proc_macro::fetch_crate(package, ws)?; } Ok(()) } diff --git a/scarb/src/compiler/plugin/proc_macro/compilation.rs b/scarb/src/compiler/plugin/proc_macro/compilation.rs index 74dd17d87..451bdf3b8 100644 --- a/scarb/src/compiler/plugin/proc_macro/compilation.rs +++ b/scarb/src/compiler/plugin/proc_macro/compilation.rs @@ -1,15 +1,20 @@ use crate::compiler::ProcMacroCompilationUnit; use crate::core::{Config, Package, Workspace}; use crate::flock::Filesystem; +use crate::ops::PackageOpts; use crate::process::exec_piping; -use anyhow::Result; +use crate::CARGO_MANIFEST_FILE_NAME; +use anyhow::{anyhow, Context, Result}; use camino::Utf8PathBuf; +use cargo_metadata::MetadataCommand; +use indoc::formatdoc; use libloading::library_filename; use ra_ap_toolchain::Tool; use scarb_ui::{Message, OutputFormat}; use serde::{Serialize, Serializer}; use serde_json::value::RawValue; use std::fmt::Display; +use std::fs; use std::process::Command; use tracing::trace_span; @@ -20,7 +25,7 @@ pub trait SharedLibraryProvider { /// Location of Cargo `target` directory. fn target_path(&self, config: &Config) -> Filesystem; /// Location of the shared library for the package. - fn shared_lib_path(&self, config: &Config) -> Utf8PathBuf; + fn shared_lib_path(&self, config: &Config) -> Result; } impl SharedLibraryProvider for Package { @@ -36,17 +41,20 @@ impl SharedLibraryProvider for Package { .into_child("target") } - fn shared_lib_path(&self, config: &Config) -> Utf8PathBuf { - let lib_name = library_filename(self.id.name.to_string()); + fn shared_lib_path(&self, config: &Config) -> Result { + let lib_name = + get_cargo_library_name(self, config).context("could not resolve library name")?; + let lib_name = library_filename(lib_name); let lib_name = lib_name .into_string() .expect("library name must be valid UTF-8"); // Defines the shared library path inside the target directory, as: // `/(..)/target/release/[lib].[so|dll|dylib]` - self.target_path(config) + Ok(self + .target_path(config) .into_child(PROC_MACRO_BUILD_PROFILE) .path_unchecked() - .join(lib_name) + .join(lib_name)) } } @@ -60,10 +68,96 @@ pub fn check_unit(unit: ProcMacroCompilationUnit, ws: &Workspace<'_>) -> Result< run_cargo(CargoAction::Check, &package, ws) } -pub fn fetch_package(package: &Package, ws: &Workspace<'_>) -> Result<()> { +fn get_cargo_package_name(package: &Package) -> Result { + let cargo_toml_path = package.root().join(CARGO_MANIFEST_FILE_NAME); + + let cargo_toml: toml::Value = toml::from_str( + &fs::read_to_string(cargo_toml_path).context("could not read `Cargo.toml`")?, + ) + .context("could not convert `Cargo.toml` to toml")?; + + let package_section = cargo_toml + .get("package") + .ok_or_else(|| anyhow!("could not get `package` section from Cargo.toml"))?; + + let package_name = package_section + .get("name") + .ok_or_else(|| anyhow!("could not get `name` field from Cargo.toml"))? + .as_str() + .ok_or_else(|| anyhow!("could not convert package name to string"))?; + + Ok(package_name.to_string()) +} + +fn get_cargo_library_name(package: &Package, config: &Config) -> Result { + let metadata = MetadataCommand::new() + .cargo_path(Tool::Cargo.path()) + .current_dir(package.root()) + .exec() + .context("could not get Cargo metadata")?; + + let cargo_package_name = get_cargo_package_name(package)?; + + if cargo_package_name != package.id.name.to_string() { + config.ui().warn(formatdoc!( + r#" + package name differs between Cargo and Scarb manifest + cargo: `{cargo_name}`, scarb: `{scarb_name}` + this might become an error in future Scarb releases + "#, + cargo_name = cargo_package_name, + scarb_name = package.id.name, + )); + } + + let package = metadata + .packages + .iter() + .find(|pkg| pkg.name == cargo_package_name) + .ok_or_else(|| anyhow!("could not get `{cargo_package_name}` package from metadata"))?; + + let cdylib_target = package + .targets + .iter() + .find(|target| target.kind.contains(&"cdylib".to_string())) + .ok_or_else(|| anyhow!("no target of `cdylib` kind found in package"))?; + + Ok(cdylib_target.name.clone()) +} + +fn get_cargo_package_version(package: &Package) -> Result { + let metadata = MetadataCommand::new() + .cargo_path(Tool::Cargo.path()) + .current_dir(package.root()) + .exec() + .context("could not get Cargo metadata")?; + + let cargo_package_name = get_cargo_package_name(package)?; + + let package = metadata + .packages + .iter() + .find(|pkg| pkg.name == cargo_package_name) + .ok_or_else(|| anyhow!("could not get `{cargo_package_name}` package from metadata"))?; + + Ok(package.version.to_string()) +} + +pub fn get_crate_archive_basename(package: &Package) -> Result { + let package_name = get_cargo_package_name(package)?; + let package_version = get_cargo_package_version(package)?; + + Ok(format!("{}-{}", package_name, package_version)) +} + +pub fn fetch_crate(package: &Package, ws: &Workspace<'_>) -> Result<()> { run_cargo(CargoAction::Fetch, package, ws) } +pub fn package_crate(package: &Package, opts: &PackageOpts, ws: &Workspace<'_>) -> Result<()> { + run_cargo(CargoAction::Package(opts.clone()), package, ws) +} + fn run_cargo(action: CargoAction, package: &Package, ws: &Workspace<'_>) -> Result<()> { let cmd = CargoCommand { action, @@ -73,6 +167,7 @@ fn run_cargo(action: CargoAction, package: &Package, ws: &Workspace<'_>) -> Resu .target_path(ws.config()) .path_unchecked() .to_path_buf(), + config: ws.config(), }; { let _ = trace_span!("proc_macro").enter(); @@ -81,17 +176,20 @@ fn run_cargo(action: CargoAction, package: &Package, ws: &Workspace<'_>) -> Resu Ok(()) } +#[derive(Clone)] enum CargoAction { Build, Check, Fetch, + Package(PackageOpts), } -struct CargoCommand { +struct CargoCommand<'c> { current_dir: Utf8PathBuf, target_dir: Utf8PathBuf, output_format: OutputFormat, action: CargoAction, + config: &'c Config, } enum CargoOutputFormat { @@ -117,17 +215,31 @@ impl From for CargoOutputFormat { } } -impl From for Command { - fn from(args: CargoCommand) -> Self { +impl<'c> From> for Command { + fn from(args: CargoCommand<'c>) -> Self { let mut cmd = Command::new(Tool::Cargo.path()); cmd.current_dir(args.current_dir); match args.action { CargoAction::Fetch => cmd.arg("fetch"), CargoAction::Build => cmd.arg("build"), CargoAction::Check => cmd.arg("check"), + CargoAction::Package(_) => cmd.arg("package"), }; + if args.config.offline() { + cmd.arg("--offline"); + } match args.action { CargoAction::Fetch => (), + CargoAction::Package(ref opts) => { + cmd.arg("--target-dir"); + cmd.arg(args.target_dir); + if !opts.check_metadata { + cmd.arg("--no-metadata"); + } + if opts.allow_dirty { + cmd.arg("--allow-dirty"); + } + } _ => { cmd.arg("--release"); cmd.arg("--message-format"); diff --git a/scarb/src/compiler/plugin/proc_macro/ffi.rs b/scarb/src/compiler/plugin/proc_macro/ffi.rs index 2b415f0a9..bb358ade9 100644 --- a/scarb/src/compiler/plugin/proc_macro/ffi.rs +++ b/scarb/src/compiler/plugin/proc_macro/ffi.rs @@ -62,7 +62,9 @@ impl Debug for ProcMacroInstance { impl ProcMacroInstance { /// Load shared library pub fn try_new(package: Package, config: &Config) -> Result { - let lib_path = package.shared_lib_path(config); + let lib_path = package + .shared_lib_path(config) + .context("could not resolve shared library path")?; let plugin = unsafe { Plugin::try_new(lib_path.to_path_buf())? }; Ok(Self { expansions: unsafe { Self::load_expansions(&plugin, package.id)? }, diff --git a/scarb/src/compiler/plugin/proc_macro/mod.rs b/scarb/src/compiler/plugin/proc_macro/mod.rs index e4419b22a..888c012fc 100644 --- a/scarb/src/compiler/plugin/proc_macro/mod.rs +++ b/scarb/src/compiler/plugin/proc_macro/mod.rs @@ -2,6 +2,6 @@ pub mod compilation; mod ffi; mod host; -pub use compilation::{check_unit, compile_unit, fetch_package}; +pub use compilation::{check_unit, compile_unit, fetch_crate}; pub use ffi::*; pub use host::*; diff --git a/scarb/src/core/manifest/toml_manifest.rs b/scarb/src/core/manifest/toml_manifest.rs index ccb17b2f5..8bd32a5db 100644 --- a/scarb/src/core/manifest/toml_manifest.rs +++ b/scarb/src/core/manifest/toml_manifest.rs @@ -46,7 +46,7 @@ pub struct TomlManifest { pub dependencies: Option>, pub dev_dependencies: Option>, pub lib: Option>, - pub cairo_plugin: Option>, + pub cairo_plugin: Option>, pub test: Option>>, pub target: Option>>>, pub cairo: Option, @@ -294,6 +294,12 @@ pub struct TomlLibTargetParams { pub sierra_text: Option, } +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TomlCairoPluginTargetParams { + pub builtin: Option, +} + pub type TomlExternalTargetParams = BTreeMap; #[derive(Debug, Default, Deserialize, Serialize, Clone)] diff --git a/scarb/src/core/publishing/manifest_normalization.rs b/scarb/src/core/publishing/manifest_normalization.rs index 09e033c91..14e41e0ac 100644 --- a/scarb/src/core/publishing/manifest_normalization.rs +++ b/scarb/src/core/publishing/manifest_normalization.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Result}; use camino::Utf8PathBuf; use indoc::formatdoc; +use crate::core::{TomlCairoPluginTargetParams, TomlTarget}; use crate::{ core::{ DepKind, DependencyVersionReq, DetailedTomlDependency, ManifestDependency, MaybeWorkspace, @@ -29,10 +30,7 @@ pub fn prepare_manifest_for_publish(pkg: &Package) -> Result { .collect() }); - let cairo_plugin = match pkg.target(&TargetKind::CAIRO_PLUGIN) { - None => None, - Some(_) => todo!("Packaging Cairo plugins is not implemented yet."), - }; + let cairo_plugin = generate_cairo_plugin(pkg); Ok(TomlManifest { package, @@ -146,3 +144,16 @@ fn generate_dependency(dep: &ManifestDependency) -> Result { }, }))) } + +fn generate_cairo_plugin(pkg: &Package) -> Option> { + let target = pkg.target(&TargetKind::CAIRO_PLUGIN)?; + let params = target.props::().ok()?; + + Some(TomlTarget { + name: Some(target.name.clone()), + source_path: None, + params: TomlCairoPluginTargetParams { + builtin: params.builtin.and_then(|b| b.then_some(true)), + }, + }) +} diff --git a/scarb/src/core/publishing/source.rs b/scarb/src/core/publishing/source.rs index 8ec730ec3..2416bb232 100644 --- a/scarb/src/core/publishing/source.rs +++ b/scarb/src/core/publishing/source.rs @@ -4,7 +4,10 @@ use ignore::{DirEntry, WalkBuilder}; use crate::core::Package; use crate::internal::fsx::PathBufUtf8Ext; -use crate::{DEFAULT_TARGET_DIR_NAME, LOCK_FILE_NAME, MANIFEST_FILE_NAME, SCARB_IGNORE_FILE_NAME}; +use crate::{ + CARGO_LOCK_FILE_NAME, CARGO_MANIFEST_FILE_NAME, DEFAULT_TARGET_DIR_NAME, LOCK_FILE_NAME, + MANIFEST_FILE_NAME, SCARB_IGNORE_FILE_NAME, +}; /// List all files relevant to building this package inside this source. /// @@ -52,11 +55,15 @@ fn push_worktree_files(pkg: &Package, ret: &mut Vec) -> Result<()> return false; } - // Skip `Scarb.toml`, `Scarb.lock` and `target` directory. + // Skip `Scarb.toml`, `Scarb.lock`, 'Cargo.toml`, 'Cargo.lock', and `target` directory. if entry.depth() == 1 && ({ let f = entry.file_name(); - f == MANIFEST_FILE_NAME || f == LOCK_FILE_NAME || f == DEFAULT_TARGET_DIR_NAME + f == MANIFEST_FILE_NAME + || f == LOCK_FILE_NAME + || f == CARGO_MANIFEST_FILE_NAME + || f == CARGO_LOCK_FILE_NAME + || f == DEFAULT_TARGET_DIR_NAME }) { return false; diff --git a/scarb/src/lib.rs b/scarb/src/lib.rs index 485deb94d..673632428 100644 --- a/scarb/src/lib.rs +++ b/scarb/src/lib.rs @@ -40,3 +40,5 @@ pub const STARKNET_PLUGIN_NAME: &str = "starknet"; pub const TEST_PLUGIN_NAME: &str = "cairo_test"; pub const TEST_ASSERTS_PLUGIN_NAME: &str = "assert_macros"; pub const CAIRO_RUN_PLUGIN_NAME: &str = "cairo_run"; +pub const CARGO_MANIFEST_FILE_NAME: &str = "Cargo.toml"; +pub const CARGO_LOCK_FILE_NAME: &str = "Cargo.lock"; diff --git a/scarb/src/ops/package.rs b/scarb/src/ops/package.rs index fc58d5df9..0d017327d 100644 --- a/scarb/src/ops/package.rs +++ b/scarb/src/ops/package.rs @@ -13,19 +13,23 @@ use scarb_ui::components::Status; use scarb_ui::{HumanBytes, HumanCount}; use serde::Serialize; +use crate::compiler::plugin::proc_macro::compilation::{ + get_crate_archive_basename, package_crate, SharedLibraryProvider, +}; use crate::core::publishing::manifest_normalization::prepare_manifest_for_publish; use crate::core::publishing::source::list_source_files; -use crate::core::{Config, Package, PackageId, PackageName, TargetKind, Workspace}; +use crate::core::{Config, Package, PackageId, PackageName, Target, TargetKind, Workspace}; use crate::flock::{FileLockGuard, Filesystem}; use crate::internal::restricted_names; use crate::{ - ops, DEFAULT_LICENSE_FILE_NAME, DEFAULT_README_FILE_NAME, MANIFEST_FILE_NAME, - VCS_INFO_FILE_NAME, + ops, CARGO_MANIFEST_FILE_NAME, DEFAULT_LICENSE_FILE_NAME, DEFAULT_README_FILE_NAME, + MANIFEST_FILE_NAME, VCS_INFO_FILE_NAME, }; const VERSION: u8 = 1; const VERSION_FILE_NAME: &str = "VERSION"; const ORIGINAL_MANIFEST_FILE_NAME: &str = "Scarb.orig.toml"; +const ORIGINAL_CARGO_MANIFEST_FILE_NAME: &str = "Cargo.orig.toml"; const RESERVED_FILES: &[&str] = &[ VERSION_FILE_NAME, @@ -137,6 +141,14 @@ fn extract_vcs_info(repo: PackageRepository, opts: &PackageOpts) -> Result bool { + target + .params + .get("builtin") + .and_then(|v| v.as_bool()) + .unwrap_or(false) +} + #[tracing::instrument(level = "trace", skip(opts, ws))] fn package_one_impl( pkg_id: PackageId, @@ -153,7 +165,7 @@ fn package_one_impl( check_metadata(pkg, ws.config())?; } - let recipe = prepare_archive_recipe(pkg, opts)?; + let recipe = prepare_archive_recipe(pkg, opts, ws)?; let num_files = recipe.len(); // Package up and test a temporary tarball and only move it to the final location if it actually @@ -170,7 +182,7 @@ fn package_one_impl( let uncompressed_size = tar(pkg_id, recipe, &mut dst, ws)?; - let mut dst = if opts.verify { + let mut dst = if opts.verify && !pkg.manifest.targets.iter().any(is_builtin) { run_verify(pkg, dst, ws, opts.features.clone()) .context("failed to verify package tarball")? } else { @@ -205,17 +217,21 @@ fn list_one_impl( ws: &Workspace<'_>, ) -> Result> { let pkg = ws.fetch_package(&pkg_id)?; - let recipe = prepare_archive_recipe(pkg, opts)?; + let recipe = prepare_archive_recipe(pkg, opts, ws)?; Ok(recipe.into_iter().map(|f| f.path).collect()) } -fn prepare_archive_recipe(pkg: &Package, opts: &PackageOpts) -> Result { +fn prepare_archive_recipe( + pkg: &Package, + opts: &PackageOpts, + ws: &Workspace<'_>, +) -> Result { ensure!( - pkg.manifest.targets.iter().any(|x| x.is_lib()), + pkg.is_lib() || pkg.is_cairo_plugin(), formatdoc!( r#" - cannot archive package `{package_name}` without a `lib` target - help: add `[lib]` section to package manifest + cannot archive package `{package_name}` without a `lib` or `cairo-plugin` target + help: consider adding `[lib]` section to package manifest --> Scarb.toml + [lib] "#, @@ -246,6 +262,43 @@ fn prepare_archive_recipe(pkg: &Package, opts: &PackageOpts) -> Result = LazyLock::new(|| { - let path = fsx::canonicalize( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../plugins/") - .join("cairo-lang-macro"), - ) - .unwrap(); - serde_json::to_string(&path).unwrap() -}); - -struct CairoPluginProjectBuilder { - project: ProjectBuilder, - name: String, - src: HashMap, - deps: Vec, -} - -impl CairoPluginProjectBuilder { - pub fn start() -> Self { - Self { - project: ProjectBuilder::start(), - name: Default::default(), - src: Default::default(), - deps: Default::default(), - } - } - - pub fn scarb_project(mut self, mutate: impl FnOnce(ProjectBuilder) -> ProjectBuilder) -> Self { - self.project = mutate(self.project); - self - } - - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self.project = self.project.name(name.to_string()); - self - } - - pub fn src(mut self, path: impl Into, source: impl ToString) -> Self { - self.src.insert(path.into(), source.to_string()); - self - } - - pub fn lib_rs(self, source: impl ToString) -> Self { - self.src("src/lib.rs", source.to_string()) - } - - pub fn add_dep(mut self, dep: impl ToString) -> Self { - self.deps.push(dep.to_string()); - self - } - - fn render_manifest(&self) -> String { - let macro_lib_path = CAIRO_LANG_MACRO_PATH.to_string(); - let deps = self.deps.join("\n"); - let name = self.name.clone(); - formatdoc! {r#" - [package] - name = "{name}" - version = "0.1.0" - edition = "2021" - publish = false - - [lib] - crate-type = ["cdylib"] - - [dependencies] - cairo-lang-macro = {{ path = {macro_lib_path}}} - {deps} - "#} - } - - pub fn just_code(&self, t: &impl PathChild) { - t.child("Cargo.toml") - .write_str(self.render_manifest().as_str()) - .unwrap(); - for (path, source) in &self.src { - t.child(path).write_str(source).unwrap(); - } - } - - pub fn build(&self, t: &impl PathChild) { - self.project.just_manifest(t); - self.just_code(t); - } -} - -impl Default for CairoPluginProjectBuilder { - fn default() -> Self { - let default_name = "some"; - let default_code = indoc! {r#" - use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro}; - - #[attribute_macro] - pub fn some(_attr: TokenStream, token_stream: TokenStream) -> ProcMacroResult { - ProcMacroResult::new(token_stream) - } - "#}; - Self::start() - .name(default_name) - .scarb_project(|b| { - b.name(default_name) - .version("1.0.0") - .manifest_extra(r#"[cairo-plugin]"#) - }) - .lib_rs(default_code) - } -} #[test] fn compile_cairo_plugin() { diff --git a/scarb/tests/package.rs b/scarb/tests/package.rs index 12dd49db7..9e4c0d131 100644 --- a/scarb/tests/package.rs +++ b/scarb/tests/package.rs @@ -11,14 +11,15 @@ use assert_fs::TempDir; use indoc::{formatdoc, indoc}; use itertools::Itertools; use scarb::DEFAULT_TARGET_DIR_NAME; -use test_case::test_case; - +use scarb_build_metadata::CAIRO_VERSION; +use scarb_test_support::cairo_plugin_project_builder::CairoPluginProjectBuilder; use scarb_test_support::command::Scarb; use scarb_test_support::fsx::unix_paths_to_os_lossy; use scarb_test_support::gitx; use scarb_test_support::project_builder::{Dep, DepBuilder, ProjectBuilder}; use scarb_test_support::registry::local::LocalRegistry; use scarb_test_support::workspace_builder::WorkspaceBuilder; +use test_case::test_case; struct PackageChecker { actual_files: HashMap, @@ -482,6 +483,159 @@ fn workspace() { .name_and_version("workspace_dep", "1.0.0"); } +#[test] +fn cairo_plugin() { + let t = TempDir::new().unwrap(); + CairoPluginProjectBuilder::default().build(&t); + + Scarb::quick_snapbox() + .arg("package") + .arg("--no-metadata") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&t) + .assert() + .success() + .stdout_matches(indoc! {r#" + [..] Packaging some v1.0.0 [..] + [..]warn: package name or version differs between Cargo manifest and Scarb manifest + [..]Scarb manifest: `some-1.0.0`, Cargo manifest: `some-0.1.0` + [..]this might become an error in future Scarb releases + + [..] Verifying some-1.0.0.tar.zst + [..] Compiling some v1.0.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Packaged [..] files, [..] ([..] compressed) + "#}); + + PackageChecker::assert(&t.child("target/package/some-1.0.0.tar.zst")) + .name_and_version("some", "1.0.0") + .contents(&[ + "VERSION", + "Scarb.orig.toml", + "Scarb.toml", + "Cargo.orig.toml", + "Cargo.toml", + "src/lib.rs", + ]) + .file_eq("VERSION", "1") + .file_eq_path("src/lib.rs", t.child("src/lib.rs")) + .file_eq_path("Scarb.orig.toml", t.child("Scarb.toml")) + .file_eq_nl( + "Scarb.toml", + indoc! {r#" + # Code generated by scarb package -p some; DO NOT EDIT. + # + # When uploading packages to the registry Scarb will automatically + # "normalize" Scarb.toml files for maximal compatibility + # with all versions of Scarb and also rewrite `path` dependencies + # to registry dependencies. + # + # If you are reading this file be aware that the original Scarb.toml + # will likely look very different (and much more reasonable). + # See Scarb.orig.toml for the original contents. + + [package] + name = "some" + version = "1.0.0" + edition = "2023_01" + + [dependencies] + + [cairo-plugin] + name = "some" + "#}, + ) + .file_matches_nl( + "Cargo.orig.toml", + indoc! {r#" + [package] + name = "some" + version = "0.1.0" + edition = "2021" + publish = false + + [lib] + crate-type = ["cdylib"] + + [dependencies] + cairo-lang-macro = { path = "[..]cairo-lang-macro", version = "0.1.0" } + "#}, + ) + .file_matches( + "Cargo.toml", + indoc! {r#" + # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO + # + ... + "#}, + ); +} + +#[test] +fn builtin_cairo_plugin() { + let t = TempDir::new().unwrap(); + CairoPluginProjectBuilder::start() + .name("assert_macros") + .scarb_project(|b| { + b.name("assert_macros") + .version(CAIRO_VERSION) + .manifest_package_extra("no-core = true") + .manifest_extra(indoc! {r#" + [cairo-plugin] + builtin = true + "#}) + }) + .build(&t); + + Scarb::quick_snapbox() + .arg("package") + .arg("--no-metadata") + .current_dir(&t) + .assert() + .success() + .stdout_matches(formatdoc! {r#" + [..]Packaging assert_macros v{CAIRO_VERSION} ([..]Scarb.toml) + [..]Packaged [..] files, [..] ([..] compressed) + "#}); + + PackageChecker::assert(&t.child(format!( + "target/package/assert_macros-{CAIRO_VERSION}.tar.zst" + ))) + .name_and_version("assert_macros", CAIRO_VERSION) + .contents(&["VERSION", "Scarb.orig.toml", "Scarb.toml"]) + .file_eq("VERSION", "1") + .file_eq_path("Scarb.orig.toml", t.child("Scarb.toml")) + .file_eq_nl( + "Scarb.toml", + formatdoc! {r#" + # Code generated by scarb package -p assert_macros; DO NOT EDIT. + # + # When uploading packages to the registry Scarb will automatically + # "normalize" Scarb.toml files for maximal compatibility + # with all versions of Scarb and also rewrite `path` dependencies + # to registry dependencies. + # + # If you are reading this file be aware that the original Scarb.toml + # will likely look very different (and much more reasonable). + # See Scarb.orig.toml for the original contents. + + [package] + name = "assert_macros" + version = "{CAIRO_VERSION}" + edition = "2023_01" + no-core = true + + [dependencies] + + [cairo-plugin] + name = "assert_macros" + builtin = true + "#} + .as_str(), + ); +} + #[test] fn clean_repo() { let t = TempDir::new().unwrap(); @@ -1221,7 +1375,7 @@ fn ignore_whitelist_pattern() { } #[test] -fn no_lib_target() { +fn no_target() { let t = TempDir::new().unwrap(); ProjectBuilder::start() .name("foo") @@ -1239,8 +1393,8 @@ fn no_lib_target() { .failure() .stdout_matches(indoc! {r#" [..] Packaging foo v1.0.0 [..] - error: cannot archive package `foo` without a `lib` target - help: add `[lib]` section to package manifest + error: cannot archive package `foo` without a `lib` or `cairo-plugin` target + help: consider adding `[lib]` section to package manifest --> Scarb.toml + [lib] "#}); diff --git a/utils/scarb-test-support/src/cairo_plugin_project_builder.rs b/utils/scarb-test-support/src/cairo_plugin_project_builder.rs new file mode 100644 index 000000000..bc8fae976 --- /dev/null +++ b/utils/scarb-test-support/src/cairo_plugin_project_builder.rs @@ -0,0 +1,117 @@ +use crate::fsx; +use crate::project_builder::ProjectBuilder; +use assert_fs::fixture::{FileWriteStr, PathChild}; +use camino::Utf8PathBuf; +use indoc::{formatdoc, indoc}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::LazyLock; + +static CAIRO_LANG_MACRO_PATH: LazyLock = LazyLock::new(|| { + let path = fsx::canonicalize( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../plugins/") + .join("cairo-lang-macro"), + ) + .unwrap(); + serde_json::to_string(&path).unwrap() +}); + +pub struct CairoPluginProjectBuilder { + project: ProjectBuilder, + name: String, + src: HashMap, + deps: Vec, +} + +impl CairoPluginProjectBuilder { + pub fn start() -> Self { + Self { + project: ProjectBuilder::start(), + name: Default::default(), + src: Default::default(), + deps: Default::default(), + } + } + + pub fn scarb_project(mut self, mutate: impl FnOnce(ProjectBuilder) -> ProjectBuilder) -> Self { + self.project = mutate(self.project); + self + } + + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self.project = self.project.name(name.to_string()); + self + } + + pub fn src(mut self, path: impl Into, source: impl ToString) -> Self { + self.src.insert(path.into(), source.to_string()); + self + } + + pub fn lib_rs(self, source: impl ToString) -> Self { + self.src("src/lib.rs", source.to_string()) + } + + pub fn add_dep(mut self, dep: impl ToString) -> Self { + self.deps.push(dep.to_string()); + self + } + + fn render_manifest(&self) -> String { + let macro_lib_path = CAIRO_LANG_MACRO_PATH.to_string(); + let deps = self.deps.join("\n"); + let name = self.name.clone(); + formatdoc! {r#" + [package] + name = "{name}" + version = "0.1.0" + edition = "2021" + publish = false + + [lib] + crate-type = ["cdylib"] + + [dependencies] + cairo-lang-macro = {{ path = {macro_lib_path}, version = "0.1.0" }} + {deps} + "#} + } + + pub fn just_code(&self, t: &impl PathChild) { + t.child("Cargo.toml") + .write_str(self.render_manifest().as_str()) + .unwrap(); + for (path, source) in &self.src { + t.child(path).write_str(source).unwrap(); + } + } + + pub fn build(&self, t: &impl PathChild) { + self.project.just_manifest(t); + self.just_code(t); + } +} + +impl Default for CairoPluginProjectBuilder { + fn default() -> Self { + let default_name = "some"; + let default_code = indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro}; + + #[attribute_macro] + pub fn some(_attr: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + ProcMacroResult::new(token_stream) + } + "#}; + Self::start() + .name(default_name) + .scarb_project(|b| { + b.name(default_name) + .version("1.0.0") + .manifest_extra(r#"[cairo-plugin]"#) + }) + .lib_rs(default_code) + } +} diff --git a/utils/scarb-test-support/src/lib.rs b/utils/scarb-test-support/src/lib.rs index ca8ffc2f3..f914b5073 100644 --- a/utils/scarb-test-support/src/lib.rs +++ b/utils/scarb-test-support/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cairo_plugin_project_builder; pub mod cargo; pub mod command; pub mod contracts;