diff --git a/Cargo.lock b/Cargo.lock index 6023da0e8..3e74cadb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,27 +524,19 @@ dependencies = [ "anyhow", "assert_cmd", "async-std", - "blake2", - "cargo_metadata", "clap", "colored", + "contract-build", "contract-metadata", "contract-transcode", "current_platform", - "dirs", - "heck", "hex", - "impl-serde", "jsonrpsee", "pallet-contracts-primitives", "parity-scale-codec", - "parity-wasm", "predicates", - "pretty_assertions", "regex", "rust_decimal", - "rustc_version", - "semver", "serde", "serde_json", "sp-core", @@ -552,15 +544,10 @@ dependencies = [ "substrate-build-script-utils", "subxt", "tempfile", - "toml", "tracing", "tracing-subscriber 0.3.16", "url", - "wabt", - "walkdir", - "wasm-opt", "which", - "zip", ] [[package]] @@ -698,6 +685,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "contract-build" +version = "2.0.0-beta" +dependencies = [ + "anyhow", + "blake2", + "cargo_metadata", + "clap", + "colored", + "contract-metadata", + "heck", + "hex", + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "pretty_assertions", + "rustc_version", + "semver", + "serde", + "serde_json", + "tempfile", + "toml", + "tracing", + "url", + "wabt", + "walkdir", + "wasm-opt", + "which", + "zip", +] + [[package]] name = "contract-metadata" version = "2.0.0-beta" @@ -999,26 +1017,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "doc-comment" version = "0.3.3" @@ -2821,17 +2819,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.8", - "redox_syscall", - "thiserror", -] - [[package]] name = "ref-cast" version = "1.0.13" diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml new file mode 100644 index 000000000..20e4cb8f9 --- /dev/null +++ b/crates/build/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "contract-build" +version = "2.0.0-beta" +authors = ["Parity Technologies "] +edition = "2021" + +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/paritytech/cargo-contract" +documentation = "https://docs.rs/contract-metadata" +homepage = "https://www.substrate.io/" +description = "Library for building ink! smart contracts" +keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"] +include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE", "build.rs", "templates",] + +[dependencies] +anyhow = "1.0.65" +blake2 = "0.10.4" +cargo_metadata = "0.15.0" +colored = "2.0.0" +clap = { version = "4.0.15", features = ["derive", "env"] } +heck = "0.4.0" +hex = "0.4.3" +impl-serde = "0.4.0" +rustc_version = "0.4.0" +scale = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] } +toml = "0.5.9" +tracing = "0.1.37" +parity-wasm = "0.45.0" +semver = { version = "1.0.14", features = ["serde"] } +serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = "1.0.86" +tempfile = "3.3.0" +url = { version = "2.3.1", features = ["serde"] } +wasm-opt = "0.110.1" +which = "4.3.0" +zip = { version = "0.6.3", default-features = false } + +contract-metadata = { version = "2.0.0-beta", path = "../metadata" } + +[build-dependencies] +anyhow = "1.0.65" +walkdir = "2.3.2" +zip = { version = "0.6.3", default-features = false } + +[dev-dependencies] +pretty_assertions = "1.3.0" +wabt = "0.10.0" diff --git a/crates/build/LICENSE b/crates/build/LICENSE new file mode 120000 index 000000000..30cff7403 --- /dev/null +++ b/crates/build/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/build/README.md b/crates/build/README.md new file mode 100644 index 000000000..0409b3340 --- /dev/null +++ b/crates/build/README.md @@ -0,0 +1,37 @@ +# contract-build + +A crate for building [`ink!`](https://github.com/paritytech/ink) smart contracts. Used by +[`cargo-contract`](https://github.com/paritytech/cargo-contract). + +## Usage + +```rust +use contract_build::{ + ManifestPath, + Verbosity, + BuildArtifacts, + BuildMode, + Network, + OptimizationPasses, + OutputType, + UnstableFlags, +}; + +let manifest_path = ManifestPath::new("my-contract/Cargo.toml").unwrap(); + +let args = contract_build::ExecuteArgs { + manifest_path, + verbosity: Verbosity::Default, + build_mode: BuildMode::Release, + network: Network::Online, + build_artifact: BuildArtifacts::All, + unstable_flags: UnstableFlags::default(), + optimization_passes: Some(OptimizationPasses::default()), + keep_debug_symbols: false, + lint: false, + output_type: OutputType::Json, + skip_wasm_validation: false, +}; + +contract_build::execute(args); +``` \ No newline at end of file diff --git a/crates/build/build.rs b/crates/build/build.rs new file mode 100644 index 000000000..290ede0ec --- /dev/null +++ b/crates/build/build.rs @@ -0,0 +1,123 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract 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. +// +// cargo-contract 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 cargo-contract. If not, see . + +use std::{ + env, + ffi::OsStr, + fs::File, + io::{ + prelude::*, + Write, + }, + iter::Iterator, + path::{ + Path, + PathBuf, + }, +}; + +use anyhow::Result; +use walkdir::WalkDir; +use zip::{ + write::FileOptions, + CompressionMethod, + ZipWriter, +}; + +const DEFAULT_UNIX_PERMISSIONS: u32 = 0o755; + +fn main() { + let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR should be set by cargo") + .into(); + let out_dir: PathBuf = env::var("OUT_DIR") + .expect("OUT_DIR should be set by cargo") + .into(); + let res = zip_template(&manifest_dir, &out_dir); + + match res { + Ok(()) => std::process::exit(0), + Err(err) => { + eprintln!("Encountered error: {:?}", err); + std::process::exit(1) + } + } +} + +/// Creates a zip archive `template.zip` of the `new` project template in `out_dir`. +fn zip_template(manifest_dir: &Path, out_dir: &Path) -> Result<()> { + let template_dir = manifest_dir.join("templates").join("new"); + let template_dst_file = out_dir.join("template.zip"); + println!( + "Creating template zip: template_dir '{}', destination archive '{}'", + template_dir.display(), + template_dst_file.display() + ); + zip_dir(&template_dir, &template_dst_file, CompressionMethod::Stored).map(|_| { + println!( + "Done: {} written to {}", + template_dir.display(), + template_dst_file.display() + ); + }) +} + +/// Creates a zip archive at `dst_file` with the content of the `src_dir`. +fn zip_dir(src_dir: &Path, dst_file: &Path, method: CompressionMethod) -> Result<()> { + if !src_dir.exists() { + anyhow::bail!("src_dir '{}' does not exist", src_dir.display()); + } + if !src_dir.is_dir() { + anyhow::bail!("src_dir '{}' is not a directory", src_dir.display()); + } + + let file = File::create(dst_file)?; + + let walkdir = WalkDir::new(src_dir); + let it = walkdir.into_iter().filter_map(|e| e.ok()); + + let mut zip = ZipWriter::new(file); + let options = FileOptions::default() + .compression_method(method) + .unix_permissions(DEFAULT_UNIX_PERMISSIONS); + + let mut buffer = Vec::new(); + for entry in it { + let path = entry.path(); + let mut name = path.strip_prefix(src_dir)?.to_path_buf(); + + // `Cargo.toml` files cause the folder to excluded from `cargo package` so need to be renamed + if name.file_name() == Some(OsStr::new("_Cargo.toml")) { + name.set_file_name("Cargo.toml"); + } + + let file_path = name.as_os_str().to_string_lossy(); + + if path.is_file() { + zip.start_file(file_path, options)?; + let mut f = File::open(path)?; + + f.read_to_end(&mut buffer)?; + zip.write_all(&buffer)?; + buffer.clear(); + } else if !name.as_os_str().is_empty() { + zip.add_directory(file_path, options)?; + } + } + zip.finish()?; + + Ok(()) +} diff --git a/crates/build/src/args.rs b/crates/build/src/args.rs new file mode 100644 index 000000000..8c2c4580d --- /dev/null +++ b/crates/build/src/args.rs @@ -0,0 +1,226 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract 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. +// +// cargo-contract 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 cargo-contract. If not, see . + +use anyhow::Result; +use clap::Args; +use std::{ + convert::TryFrom, + fmt, +}; + +#[derive(Default, Clone, Debug, Args)] +pub struct VerbosityFlags { + /// No output printed to stdout + #[clap(long)] + quiet: bool, + /// Use verbose output + #[clap(long)] + verbose: bool, +} + +impl TryFrom<&VerbosityFlags> for Verbosity { + type Error = anyhow::Error; + + fn try_from(value: &VerbosityFlags) -> Result { + match (value.quiet, value.verbose) { + (false, false) => Ok(Verbosity::Default), + (true, false) => Ok(Verbosity::Quiet), + (false, true) => Ok(Verbosity::Verbose), + (true, true) => anyhow::bail!("Cannot pass both --quiet and --verbose flags"), + } + } +} + +/// Denotes if output should be printed to stdout. +#[derive(Clone, Copy, serde::Serialize, Eq, PartialEq)] +pub enum Verbosity { + /// Use default output + Default, + /// No output printed to stdout + Quiet, + /// Use verbose output + Verbose, +} + +impl Default for Verbosity { + fn default() -> Self { + Verbosity::Default + } +} + +impl Verbosity { + /// Returns `true` if output should be printed (i.e. verbose output is set). + pub fn is_verbose(&self) -> bool { + match self { + Verbosity::Quiet => false, + Verbosity::Default | Verbosity::Verbose => true, + } + } +} + +/// Use network connection to build contracts and generate metadata or use cached dependencies only. +#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize)] +pub enum Network { + /// Use network + Online, + /// Use cached dependencies. + Offline, +} + +impl Default for Network { + fn default() -> Network { + Network::Online + } +} + +impl fmt::Display for Network { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Online => write!(f, ""), + Self::Offline => write!(f, "--offline"), + } + } +} + +/// Describes which artifacts to generate +#[derive(Copy, Clone, Eq, PartialEq, Debug, clap::ValueEnum, serde::Serialize)] +#[clap(name = "build-artifacts")] +pub enum BuildArtifacts { + /// Generate the Wasm, the metadata and a bundled `.contract` file + #[clap(name = "all")] + All, + /// Only the Wasm is created, generation of metadata and a bundled `.contract` file is + /// skipped + #[clap(name = "code-only")] + CodeOnly, + /// No artifacts produced: runs the `cargo check` command for the Wasm target, only checks for + /// compilation errors. + #[clap(name = "check-only")] + CheckOnly, +} + +impl BuildArtifacts { + /// Returns the number of steps required to complete a build artifact. + /// Used as output on the cli. + pub fn steps(&self) -> BuildSteps { + match self { + BuildArtifacts::All => BuildSteps::new(5), + BuildArtifacts::CodeOnly => BuildSteps::new(4), + BuildArtifacts::CheckOnly => BuildSteps::new(1), + } + } +} + +impl Default for BuildArtifacts { + fn default() -> Self { + BuildArtifacts::All + } +} + +/// Track and display the current and total number of steps. +#[derive(Debug, Clone, Copy)] +pub struct BuildSteps { + pub current_step: usize, + pub total_steps: usize, +} + +impl BuildSteps { + pub fn new(total_steps: usize) -> Self { + Self { + current_step: 1, + total_steps, + } + } + + pub fn increment_current(&mut self) { + self.current_step += 1; + } +} + +impl fmt::Display for BuildSteps { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}/{}]", self.current_step, self.total_steps) + } +} + +/// The mode to build the contract in. +#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum BuildMode { + /// Functionality to output debug messages is build into the contract. + Debug, + /// The contract is build without any debugging functionality. + Release, +} + +impl Default for BuildMode { + fn default() -> BuildMode { + BuildMode::Debug + } +} + +impl fmt::Display for BuildMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Debug => write!(f, "debug"), + Self::Release => write!(f, "release"), + } + } +} + +/// The type of output to display at the end of a build. +pub enum OutputType { + /// Output build results in a human readable format. + HumanReadable, + /// Output the build results JSON formatted. + Json, +} + +impl Default for OutputType { + fn default() -> Self { + OutputType::HumanReadable + } +} + +#[derive(Default, Clone, Debug, Args)] +pub struct UnstableOptions { + /// Use the original manifest (Cargo.toml), do not modify for build optimizations + #[clap(long = "unstable-options", short = 'Z', number_of_values = 1)] + options: Vec, +} + +#[derive(Clone, Default)] +pub struct UnstableFlags { + pub original_manifest: bool, +} + +impl TryFrom<&UnstableOptions> for UnstableFlags { + type Error = anyhow::Error; + + fn try_from(value: &UnstableOptions) -> Result { + let valid_flags = ["original-manifest"]; + let invalid_flags = value + .options + .iter() + .filter(|o| !valid_flags.contains(&o.as_str())) + .collect::>(); + if !invalid_flags.is_empty() { + anyhow::bail!("Unknown unstable-options {:?}", invalid_flags) + } + Ok(UnstableFlags { + original_manifest: value.options.contains(&"original-manifest".to_owned()), + }) + } +} diff --git a/crates/cargo-contract/src/crate_metadata.rs b/crates/build/src/crate_metadata.rs similarity index 100% rename from crates/cargo-contract/src/crate_metadata.rs rename to crates/build/src/crate_metadata.rs diff --git a/crates/cargo-contract/src/cmd/build/mod.rs b/crates/build/src/lib.rs similarity index 75% rename from crates/cargo-contract/src/cmd/build/mod.rs rename to crates/build/src/lib.rs index ce877f8ef..f71314903 100644 --- a/crates/cargo-contract/src/cmd/build/mod.rs +++ b/crates/build/src/lib.rs @@ -14,34 +14,56 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . +#![doc = include_str!("../README.md")] +#![deny(unused_crate_dependencies)] + +use which as _; + +mod args; +mod crate_metadata; +pub mod metadata; +mod new; #[cfg(test)] mod tests; - -use crate::{ +pub mod util; +mod validate_wasm; +mod wasm_opt; +mod workspace; + +pub use self::{ + args::{ + BuildArtifacts, + BuildMode, + BuildSteps, + Network, + OutputType, + UnstableFlags, + UnstableOptions, + Verbosity, + VerbosityFlags, + }, crate_metadata::CrateMetadata, - maybe_println, - util, - validate_wasm, - wasm_opt::WasmOptHandler, + metadata::{ + BuildInfo, + MetadataResult, + WasmOptSettings, + }, + new::new_contract_project, + util::DEFAULT_KEY_COL_WIDTH, + wasm_opt::{ + OptimizationPasses, + OptimizationResult, + }, workspace::{ Manifest, ManifestPath, Profile, Workspace, }, - BuildArtifacts, - BuildMode, - BuildResult, - BuildSteps, - Network, - OptimizationPasses, - OptimizationResult, - OutputType, - UnstableFlags, - UnstableOptions, - Verbosity, - VerbosityFlags, }; + +use crate::wasm_opt::WasmOptHandler; + use anyhow::{ Context, Result, @@ -56,7 +78,6 @@ use parity_wasm::elements::{ }; use semver::Version; use std::{ - convert::TryFrom, path::{ Path, PathBuf, @@ -73,7 +94,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Arguments to use when executing `build` or `check` commands. #[derive(Default)] -pub(crate) struct ExecuteArgs { +pub struct ExecuteArgs { /// The location of the Cargo manifest (`Cargo.toml`) file to use. pub manifest_path: ManifestPath, pub verbosity: Verbosity, @@ -81,189 +102,112 @@ pub(crate) struct ExecuteArgs { pub network: Network, pub build_artifact: BuildArtifacts, pub unstable_flags: UnstableFlags, - pub optimization_passes: OptimizationPasses, + pub optimization_passes: Option, pub keep_debug_symbols: bool, pub lint: bool, pub output_type: OutputType, pub skip_wasm_validation: bool, } -/// Executes build of the smart contract which produces a Wasm binary that is ready for deploying. -/// -/// It does so by invoking `cargo build` and then post processing the final binary. -#[derive(Debug, clap::Args)] -#[clap(name = "build")] -pub struct BuildCommand { - /// Path to the `Cargo.toml` of the contract to build - #[clap(long, value_parser)] - manifest_path: Option, - /// By default the contract is compiled with debug functionality - /// included. This enables the contract to output debug messages, - /// but increases the contract size and the amount of gas used. - /// - /// A production contract should always be build in `release` mode! - /// Then no debug functionality is compiled into the contract. - #[clap(long = "release")] - build_release: bool, - /// Build offline - #[clap(long = "offline")] - build_offline: bool, - /// Performs linting checks during the build process - #[clap(long)] - lint: bool, - /// Which build artifacts to generate. - /// - /// - `all`: Generate the Wasm, the metadata and a bundled `.contract` file. - /// - /// - `code-only`: Only the Wasm is created, generation of metadata and a bundled - /// `.contract` file is skipped. - /// - /// - `check-only`: No artifacts produced: runs the `cargo check` command for the Wasm target, - /// only checks for compilation errors. - #[clap(long = "generate", value_enum, default_value = "all")] - build_artifact: BuildArtifacts, - #[clap(flatten)] - verbosity: VerbosityFlags, - #[clap(flatten)] - unstable_options: UnstableOptions, - /// Number of optimization passes, passed as an argument to `wasm-opt`. - /// - /// - `0`: execute no optimization passes - /// - /// - `1`: execute 1 optimization pass (quick & useful opts, useful for iteration builds) - /// - /// - `2`, execute 2 optimization passes (most opts, generally gets most perf) - /// - /// - `3`, execute 3 optimization passes (spends potentially a lot of time optimizing) - /// - /// - `4`, execute 4 optimization passes (also flatten the IR, which can take a lot more time and memory - /// but is useful on more nested / complex / less-optimized input) - /// - /// - `s`, execute default optimization passes, focusing on code size - /// - /// - `z`, execute default optimization passes, super-focusing on code size - /// - /// - The default value is `z` - /// - /// - It is possible to define the number of optimization passes in the - /// `[package.metadata.contract]` of your `Cargo.toml` as e.g. `optimization-passes = "3"`. - /// The CLI argument always takes precedence over the profile value. - #[clap(long)] - optimization_passes: Option, - /// Do not remove symbols (Wasm name section) when optimizing. - /// - /// This is useful if one wants to analyze or debug the optimized binary. - #[clap(long)] - keep_debug_symbols: bool, - - /// Export the build output in JSON format. - #[clap(long, conflicts_with = "verbose")] - output_json: bool, - /// Don't perform wasm validation checks e.g. for permitted imports. - #[clap(long)] - skip_wasm_validation: bool, +/// Result of the build process. +#[derive(serde::Serialize)] +pub struct BuildResult { + /// Path to the resulting Wasm file. + pub dest_wasm: Option, + /// Result of the metadata generation. + pub metadata_result: Option, + /// Path to the directory where output files are written to. + pub target_directory: PathBuf, + /// If existent the result of the optimization. + pub optimization_result: Option, + /// The mode to build the contract in. + pub build_mode: BuildMode, + /// Which build artifacts were generated. + pub build_artifact: BuildArtifacts, + /// The verbosity flags. + pub verbosity: Verbosity, + /// The type of formatting to use for the build output. + #[serde(skip_serializing)] + pub output_type: OutputType, } -impl BuildCommand { - pub fn exec(&self) -> Result { - let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; - let unstable_flags: UnstableFlags = - TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; - let mut verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; - - // The CLI flag `optimization-passes` overwrites optimization passes which are - // potentially defined in the `Cargo.toml` profile. - let optimization_passes = match self.optimization_passes { - Some(opt_passes) => opt_passes, - None => { - let mut manifest = Manifest::new(manifest_path.clone())?; - match manifest.get_profile_optimization_passes() { - // if no setting is found, neither on the cli nor in the profile, - // then we use the default - None => OptimizationPasses::default(), - Some(opt_passes) => opt_passes, - } - } - }; +impl BuildResult { + pub fn display(&self) -> String { + let optimization = self.display_optimization(); + let size_diff = format!( + "\nOriginal wasm size: {}, Optimized: {}\n\n", + format!("{:.1}K", optimization.0).bold(), + format!("{:.1}K", optimization.1).bold(), + ); + debug_assert!( + optimization.1 > 0.0, + "optimized file size must be greater 0" + ); - let build_mode = match self.build_release { - true => BuildMode::Release, - false => BuildMode::Debug, - }; + let build_mode = format!( + "The contract was built in {} mode.\n\n", + format!("{}", self.build_mode).to_uppercase().bold(), + ); - let network = match self.build_offline { - true => Network::Offline, - false => Network::Online, + if self.build_artifact == BuildArtifacts::CodeOnly { + let out = format!( + "{}{}Your contract's code is ready. You can find it here:\n{}", + size_diff, + build_mode, + self.dest_wasm + .as_ref() + .expect("wasm path must exist") + .display() + .to_string() + .bold() + ); + return out }; - if self.lint && matches!(network, Network::Offline) { - anyhow::bail!( - "Linting requires network access in order to download available lints" - ) + let mut out = format!( + "{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n", + size_diff, + build_mode, + self.target_directory.display().to_string().bold(), + ); + if let Some(metadata_result) = self.metadata_result.as_ref() { + let bundle = format!( + " - {} (code + metadata)\n", + util::base_name(&metadata_result.dest_bundle).bold() + ); + out.push_str(&bundle); } - - let output_type = match self.output_json { - true => OutputType::Json, - false => OutputType::HumanReadable, - }; - - // We want to ensure that the only thing in `STDOUT` is our JSON formatted string. - if matches!(output_type, OutputType::Json) { - verbosity = Verbosity::Quiet; + if let Some(dest_wasm) = self.dest_wasm.as_ref() { + let wasm = format!( + " - {} (the contract's code)\n", + util::base_name(dest_wasm).bold() + ); + out.push_str(&wasm); } - - let args = ExecuteArgs { - manifest_path, - verbosity, - build_mode, - network, - build_artifact: self.build_artifact, - unstable_flags, - optimization_passes, - keep_debug_symbols: self.keep_debug_symbols, - lint: self.lint, - output_type, - skip_wasm_validation: self.skip_wasm_validation, - }; - - execute(args) + if let Some(metadata_result) = self.metadata_result.as_ref() { + let metadata = format!( + " - {} (the contract's metadata)", + util::base_name(&metadata_result.dest_metadata).bold() + ); + out.push_str(&metadata); + } + out } -} - -#[derive(Debug, clap::Args)] -#[clap(name = "check")] -pub struct CheckCommand { - /// Path to the `Cargo.toml` of the contract to build - #[clap(long, value_parser)] - manifest_path: Option, - #[clap(flatten)] - verbosity: VerbosityFlags, - #[clap(flatten)] - unstable_options: UnstableOptions, -} - -impl CheckCommand { - pub fn exec(&self) -> Result { - let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; - let unstable_flags: UnstableFlags = - TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; - let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; - let args = ExecuteArgs { - manifest_path, - verbosity, - build_mode: BuildMode::Debug, - network: Network::default(), - build_artifact: BuildArtifacts::CheckOnly, - unstable_flags, - optimization_passes: OptimizationPasses::Zero, - keep_debug_symbols: false, - lint: false, - output_type: OutputType::default(), - skip_wasm_validation: false, - }; + /// Returns a tuple of `(original_size, optimized_size)`. + /// + /// Panics if no optimization result is available. + fn display_optimization(&self) -> (f64, f64) { + let optimization = self + .optimization_result + .as_ref() + .expect("optimization result must exist"); + (optimization.original_size, optimization.optimized_size) + } - execute(args) + /// Display the build results in a pretty formatted JSON string. + pub fn serialize_json(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) } } @@ -602,9 +546,7 @@ pub fn assert_debug_mode_supported(ink_version: &Version) -> anyhow::Result<()> /// Executes build of the smart contract which produces a Wasm binary that is ready for deploying. /// /// It does so by invoking `cargo build` and then post processing the final binary. -pub(crate) fn execute(args: ExecuteArgs) -> Result { - use crate::cmd::metadata::BuildInfo; - +pub fn execute(args: ExecuteArgs) -> Result { let ExecuteArgs { manifest_path, verbosity, @@ -619,6 +561,21 @@ pub(crate) fn execute(args: ExecuteArgs) -> Result { skip_wasm_validation, } = args; + // The CLI flag `optimization-passes` overwrites optimization passes which are + // potentially defined in the `Cargo.toml` profile. + let optimization_passes = match optimization_passes { + Some(opt_passes) => opt_passes, + None => { + let mut manifest = Manifest::new(manifest_path.clone())?; + match manifest.get_profile_optimization_passes() { + // if no setting is found, neither on the cli nor in the profile, + // then we use the default + None => OptimizationPasses::default(), + Some(opt_passes) => opt_passes, + } + } + }; + let crate_metadata = CrateMetadata::collect(&manifest_path)?; assert_compatible_ink_dependencies(&manifest_path, verbosity)?; @@ -645,8 +602,6 @@ pub(crate) fn execute(args: ExecuteArgs) -> Result { }; let build = || -> Result<(OptimizationResult, BuildInfo, BuildSteps)> { - use crate::cmd::metadata::WasmOptSettings; - let mut build_steps = maybe_lint()?; maybe_println!( @@ -737,7 +692,7 @@ pub(crate) fn execute(args: ExecuteArgs) -> Result { BuildArtifacts::All => { let (optimization_result, build_info, build_steps) = build()?; - let metadata_result = super::metadata::execute( + let metadata_result = crate::metadata::execute( &crate_metadata, optimization_result.dest_wasm.as_path(), network, @@ -768,10 +723,7 @@ pub(crate) fn execute(args: ExecuteArgs) -> Result { /// all tests which invoke the `build` command. #[cfg(test)] mod unit_tests { - use super::{ - assert_compatible_ink_dependencies, - assert_debug_mode_supported, - }; + use super::*; use crate::{ util::tests::{ with_new_contract_project, @@ -844,4 +796,50 @@ mod unit_tests { Ok(()) }) } + + #[test] + fn build_result_seralization_sanity_check() { + // given + let raw_result = r#"{ + "dest_wasm": "/path/to/contract.wasm", + "metadata_result": { + "dest_metadata": "/path/to/metadata.json", + "dest_bundle": "/path/to/contract.contract" + }, + "target_directory": "/path/to/target", + "optimization_result": { + "dest_wasm": "/path/to/contract.wasm", + "original_size": 64.0, + "optimized_size": 32.0 + }, + "build_mode": "Debug", + "build_artifact": "All", + "verbosity": "Quiet" +}"#; + + let build_result = BuildResult { + dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")), + metadata_result: Some(MetadataResult { + dest_metadata: PathBuf::from("/path/to/metadata.json"), + dest_bundle: PathBuf::from("/path/to/contract.contract"), + }), + target_directory: PathBuf::from("/path/to/target"), + optimization_result: Some(OptimizationResult { + dest_wasm: PathBuf::from("/path/to/contract.wasm"), + original_size: 64.0, + optimized_size: 32.0, + }), + build_mode: Default::default(), + build_artifact: Default::default(), + verbosity: Verbosity::Quiet, + output_type: OutputType::Json, + }; + + // when + let serialized_result = build_result.serialize_json(); + + // then + assert!(serialized_result.is_ok()); + assert_eq!(serialized_result.unwrap(), raw_result); + } } diff --git a/crates/cargo-contract/src/cmd/metadata.rs b/crates/build/src/metadata.rs similarity index 100% rename from crates/cargo-contract/src/cmd/metadata.rs rename to crates/build/src/metadata.rs diff --git a/crates/cargo-contract/src/cmd/new.rs b/crates/build/src/new.rs similarity index 53% rename from crates/cargo-contract/src/cmd/new.rs rename to crates/build/src/new.rs index 9cff6e73d..b54df08e9 100644 --- a/crates/cargo-contract/src/cmd/new.rs +++ b/crates/build/src/new.rs @@ -14,15 +14,26 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . +use anyhow::Result; +use heck::ToUpperCamelCase as _; use std::{ env, fs, - path::Path, + io::{ + Cursor, + Read, + Seek, + SeekFrom, + Write, + }, + path::{ + Path, + PathBuf, + }, }; -use anyhow::Result; - -pub(crate) fn execute

(name: &str, dir: Option

) -> Result<()> +/// Creates a new contract project from the template. +pub fn new_contract_project

(name: &str, dir: Option

) -> Result<()> where P: AsRef, { @@ -53,7 +64,71 @@ where let template = include_bytes!(concat!(env!("OUT_DIR"), "/template.zip")); - crate::util::unzip(template, out_dir, Some(name))?; + unzip(template, out_dir, Some(name))?; + + Ok(()) +} + +// Unzips the file at `template` to `out_dir`. +// +// In case `name` is set the zip file is treated as if it were a template for a new +// contract. Replacements in `Cargo.toml` for `name`-placeholders are attempted in +// that case. +fn unzip(template: &[u8], out_dir: PathBuf, name: Option<&str>) -> Result<()> { + let mut cursor = Cursor::new(Vec::new()); + cursor.write_all(template)?; + cursor.seek(SeekFrom::Start(0))?; + + let mut archive = zip::ZipArchive::new(cursor)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = out_dir.join(file.name()); + + if (*file.name()).ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut outfile = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(outpath.clone()) + .map_err(|e| { + if e.kind() == std::io::ErrorKind::AlreadyExists { + anyhow::anyhow!("File {} already exists", file.name(),) + } else { + anyhow::anyhow!(e) + } + })?; + + if let Some(name) = name { + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let contents = contents.replace("{{name}}", name); + let contents = + contents.replace("{{camel_name}}", &name.to_upper_camel_case()); + outfile.write_all(contents.as_bytes())?; + } else { + let mut v = Vec::new(); + file.read_to_end(&mut v)?; + outfile.write_all(v.as_slice())?; + } + } + + // Get and set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } Ok(()) } @@ -61,15 +136,12 @@ where #[cfg(test)] mod tests { use super::*; - use crate::util::tests::{ - with_new_contract_project, - with_tmp_dir, - }; + use crate::util::tests::with_tmp_dir; #[test] fn rejects_hyphenated_name() { - with_new_contract_project(|manifest_path| { - let result = execute("rejects-hyphenated-name", Some(manifest_path)); + with_tmp_dir(|path| { + let result = new_contract_project("rejects-hyphenated-name", Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( result.err().unwrap().to_string(), @@ -81,8 +153,8 @@ mod tests { #[test] fn rejects_name_with_period() { - with_new_contract_project(|manifest_path| { - let result = execute("../xxx", Some(manifest_path)); + with_tmp_dir(|path| { + let result = new_contract_project("../xxx", Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( result.err().unwrap().to_string(), @@ -94,8 +166,8 @@ mod tests { #[test] fn rejects_name_beginning_with_number() { - with_new_contract_project(|manifest_path| { - let result = execute("1xxx", Some(manifest_path)); + with_tmp_dir(|path| { + let result = new_contract_project("1xxx", Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( result.err().unwrap().to_string(), @@ -109,8 +181,8 @@ mod tests { fn contract_cargo_project_already_exists() { with_tmp_dir(|path| { let name = "test_contract_cargo_project_already_exists"; - let _ = execute(name, Some(path)); - let result = execute(name, Some(path)); + let _ = new_contract_project(name, Some(path)); + let result = new_contract_project(name, Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( @@ -128,7 +200,7 @@ mod tests { let dir = path.join(name); fs::create_dir_all(&dir).unwrap(); fs::File::create(dir.join(".gitignore")).unwrap(); - let result = execute(name, Some(path)); + let result = new_contract_project(name, Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( diff --git a/crates/cargo-contract/src/cmd/build/tests.rs b/crates/build/src/tests.rs similarity index 75% rename from crates/cargo-contract/src/cmd/build/tests.rs rename to crates/build/src/tests.rs index 7851f8dd9..582bf294b 100644 --- a/crates/cargo-contract/src/cmd/build/tests.rs +++ b/crates/build/src/tests.rs @@ -15,18 +15,14 @@ // along with cargo-contract. If not, see . use crate::{ - cmd::{ - build::load_module, - BuildCommand, - }, util::tests::TestContractManifest, BuildArtifacts, BuildMode, + ExecuteArgs, ManifestPath, OptimizationPasses, OutputType, - UnstableOptions, - VerbosityFlags, + Verbosity, }; use anyhow::Result; use contract_metadata::*; @@ -38,18 +34,24 @@ use std::{ ffi::OsStr, fmt::Write, fs, - path::Path, + path::{ + Path, + PathBuf, + }, }; macro_rules! build_tests { ( $($fn:ident),* ) => { #[test] - fn build_tests() { - crate::util::tests::with_tmp_dir(|tmp_dir| { - let ctx = crate::util::tests::BuildTestContext::new(tmp_dir, "build_test")?; - $( ctx.run_test(stringify!($fn), $fn)?; )* - Ok(()) - }) + fn build_tests() -> Result<()> { + let tmp_dir = ::tempfile::Builder::new() + .prefix("cargo-contract-build.test.") + .tempdir() + .expect("temporary directory creation failed"); + + let ctx = crate::tests::BuildTestContext::new(tmp_dir.path(), "build_test")?; + $( ctx.run_test(stringify!($fn), $fn)?; )* + Ok(()) } } } @@ -76,7 +78,7 @@ build_tests!( ); fn build_code_only(manifest_path: &ManifestPath) -> Result<()> { - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_mode: BuildMode::Release, build_artifact: BuildArtifacts::CodeOnly, @@ -117,7 +119,7 @@ fn check_must_not_output_contract_artifacts_in_project_dir( ) -> Result<()> { // given let project_dir = manifest_path.directory().expect("directory must exist"); - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_artifact: BuildArtifacts::CheckOnly, lint: false, @@ -147,24 +149,22 @@ fn optimization_passes_from_cli_must_take_precedence_over_profile( test_manifest.set_profile_optimization_passes(OptimizationPasses::Three)?; test_manifest.write()?; - let cmd = BuildCommand { - manifest_path: Some(manifest_path.as_ref().into()), + let args = ExecuteArgs { + manifest_path: manifest_path.clone(), + verbosity: Verbosity::Default, + build_mode: Default::default(), + network: Default::default(), build_artifact: BuildArtifacts::All, - build_release: false, - build_offline: false, - verbosity: VerbosityFlags::default(), - unstable_options: UnstableOptions::default(), - - // we choose zero optimization passes as the "cli" parameter + unstable_flags: Default::default(), optimization_passes: Some(OptimizationPasses::Zero), keep_debug_symbols: false, lint: false, - output_json: false, + output_type: OutputType::Json, skip_wasm_validation: false, }; // when - let res = cmd.exec().expect("build failed"); + let res = crate::execute(args).expect("build failed"); let optimization = res .optimization_result .expect("no optimization result available"); @@ -189,24 +189,23 @@ fn optimization_passes_from_profile_must_be_used( test_manifest.set_profile_optimization_passes(OptimizationPasses::Three)?; test_manifest.write()?; - let cmd = BuildCommand { - manifest_path: Some(manifest_path.as_ref().into()), + let args = ExecuteArgs { + manifest_path: manifest_path.clone(), + verbosity: Verbosity::Default, + build_mode: Default::default(), + network: Default::default(), build_artifact: BuildArtifacts::All, - build_release: false, - build_offline: false, - verbosity: VerbosityFlags::default(), - unstable_options: UnstableOptions::default(), - - // we choose no optimization passes as the "cli" parameter + unstable_flags: Default::default(), + // no optimization passes specified. optimization_passes: None, keep_debug_symbols: false, lint: false, - output_json: false, + output_type: OutputType::Json, skip_wasm_validation: false, }; // when - let res = cmd.exec().expect("build failed"); + let res = crate::execute(args).expect("build failed"); let optimization = res .optimization_result .expect("no optimization result available"); @@ -234,20 +233,20 @@ fn contract_lib_name_different_from_package_name_must_build( manifest.write()?; // when - let cmd = BuildCommand { - manifest_path: Some(manifest_path.as_ref().into()), + let args = ExecuteArgs { + manifest_path: manifest_path.clone(), + verbosity: Verbosity::Default, + build_mode: Default::default(), + network: Default::default(), build_artifact: BuildArtifacts::All, - build_release: false, - build_offline: false, - verbosity: VerbosityFlags::default(), - unstable_options: UnstableOptions::default(), - optimization_passes: None, + unstable_flags: Default::default(), + optimization_passes: Some(OptimizationPasses::Zero), keep_debug_symbols: false, lint: false, - output_json: false, + output_type: OutputType::HumanReadable, skip_wasm_validation: false, }; - let res = cmd.exec().expect("build failed"); + let res = crate::execute(args).expect("build failed"); // then assert_eq!( @@ -262,7 +261,7 @@ fn contract_lib_name_different_from_package_name_must_build( fn building_template_in_debug_mode_must_work(manifest_path: &ManifestPath) -> Result<()> { // given - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_mode: BuildMode::Debug, lint: false, @@ -281,7 +280,7 @@ fn building_template_in_release_mode_must_work( manifest_path: &ManifestPath, ) -> Result<()> { // given - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_mode: BuildMode::Release, lint: false, @@ -311,7 +310,7 @@ fn building_contract_with_source_file_in_subfolder_must_work( manifest.set_lib_path("srcfoo/lib.rs")?; manifest.write()?; - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_artifact: BuildArtifacts::CheckOnly, lint: false, @@ -327,7 +326,7 @@ fn building_contract_with_source_file_in_subfolder_must_work( } fn keep_debug_symbols_in_debug_mode(manifest_path: &ManifestPath) -> Result<()> { - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_mode: BuildMode::Debug, build_artifact: BuildArtifacts::CodeOnly, @@ -345,7 +344,7 @@ fn keep_debug_symbols_in_debug_mode(manifest_path: &ManifestPath) -> Result<()> } fn keep_debug_symbols_in_release_mode(manifest_path: &ManifestPath) -> Result<()> { - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), build_mode: BuildMode::Release, build_artifact: BuildArtifacts::CodeOnly, @@ -364,7 +363,7 @@ fn keep_debug_symbols_in_release_mode(manifest_path: &ManifestPath) -> Result<() fn build_with_json_output_works(manifest_path: &ManifestPath) -> Result<()> { // given - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), output_type: OutputType::Json, lint: false, @@ -395,7 +394,7 @@ fn missing_cargo_dylint_installation_must_be_detected( let _tmp1 = create_executable(&manifest_dir.join("cargo"), "#!/bin/sh\nexit 1"); // when - let args = crate::cmd::build::ExecuteArgs { + let args = ExecuteArgs { manifest_path: manifest_path.clone(), lint: true, ..Default::default() @@ -439,13 +438,13 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { fs::create_dir_all(final_contract_wasm_path.parent().unwrap()).unwrap(); fs::write(final_contract_wasm_path, "TEST FINAL WASM BLOB").unwrap(); - let mut args = crate::cmd::build::ExecuteArgs { + let mut args = ExecuteArgs { lint: false, ..Default::default() }; args.manifest_path = manifest_path.clone(); - let build_result = crate::cmd::build::execute(args)?; + let build_result = crate::execute(args)?; let dest_bundle = build_result .metadata_result .expect("Metadata should be generated") @@ -495,7 +494,7 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { // calculate wasm hash let fs_wasm = fs::read(&crate_metadata.dest_wasm)?; - let expected_hash = crate::cmd::metadata::blake2_hash(&fs_wasm[..]); + let expected_hash = crate::metadata::blake2_hash(&fs_wasm[..]); let expected_wasm = build_byte_str(&fs_wasm); let expected_language = @@ -545,8 +544,95 @@ fn build_byte_str(bytes: &[u8]) -> String { } fn has_debug_symbols>(p: P) -> bool { - load_module(p) + crate::load_module(p) .unwrap() .custom_sections() .any(|e| e.name() == "name") } + +/// Enables running a group of tests sequentially, each starting with the original template +/// contract, but maintaining the target directory so compilation artifacts are maintained across +/// each test. +pub struct BuildTestContext { + template_dir: PathBuf, + working_dir: PathBuf, +} + +impl BuildTestContext { + /// Create a new `BuildTestContext`, running the `new` command to create a blank contract + /// template project for testing the build process. + pub fn new(tmp_dir: &Path, working_project_name: &str) -> Result { + crate::new_contract_project(working_project_name, Some(tmp_dir)) + .expect("new project creation failed"); + let working_dir = tmp_dir.join(working_project_name); + + let template_dir = tmp_dir.join(format!("{}_template", working_project_name)); + + fs::rename(&working_dir, &template_dir)?; + copy_dir_all(&template_dir, &working_dir)?; + + Ok(Self { + template_dir, + working_dir, + }) + } + + /// Run the supplied test. Test failure will print the error to `stdout`, and this will still + /// return `Ok(())` in order that subsequent tests will still be run. + /// + /// The test may modify the contracts project files (e.g. Cargo.toml, lib.rs), so after + /// completion those files are reverted to their original state for the next test. + /// + /// Importantly, the `target` directory is maintained so as to avoid recompiling all of the + /// dependencies for each test. + pub fn run_test( + &self, + name: &str, + test: impl FnOnce(&ManifestPath) -> Result<()>, + ) -> Result<()> { + println!("Running {}", name); + let manifest_path = ManifestPath::new(self.working_dir.join("Cargo.toml"))?; + match test(&manifest_path) { + Ok(()) => (), + Err(err) => { + println!("{} FAILED: {:?}", name, err); + } + } + // revert to the original template files, but keep the `target` dir from the previous run. + self.remove_all_except_target_dir()?; + copy_dir_all(&self.template_dir, &self.working_dir)?; + Ok(()) + } + + /// Deletes all files and folders in project dir (except the `target` directory) + fn remove_all_except_target_dir(&self) -> Result<()> { + for entry in fs::read_dir(&self.working_dir)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + // remove all except the target dir + if entry.file_name() != "target" { + fs::remove_dir_all(entry.path())? + } + } else { + fs::remove_file(entry.path())? + } + } + Ok(()) + } +} + +/// Copy contents of `src` to `dst` recursively. +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> Result<()> { + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} diff --git a/crates/cargo-contract/src/util/mod.rs b/crates/build/src/util/mod.rs similarity index 68% rename from crates/cargo-contract/src/util/mod.rs rename to crates/build/src/util/mod.rs index 159f79fd8..70fb3c987 100644 --- a/crates/cargo-contract/src/util/mod.rs +++ b/crates/build/src/util/mod.rs @@ -22,23 +22,11 @@ use anyhow::{ Context, Result, }; -use heck::ToUpperCamelCase as _; use rustc_version::Channel; use semver::Version; use std::{ ffi::OsStr, - fs, - io::{ - Cursor, - Read, - Seek, - SeekFrom, - Write, - }, - path::{ - Path, - PathBuf, - }, + path::Path, process::Command, }; @@ -85,7 +73,7 @@ pub(crate) fn rust_toolchain() -> Result { /// vector. /// /// If successful, returns the stdout bytes. -pub(crate) fn invoke_cargo( +pub fn invoke_cargo( command: &str, args: I, working_dir: Option

, @@ -191,67 +179,3 @@ macro_rules! name_value_println { $crate::name_value_println!($name, $value, $crate::DEFAULT_KEY_COL_WIDTH) }; } - -// Unzips the file at `template` to `out_dir`. -// -// In case `name` is set the zip file is treated as if it were a template for a new -// contract. Replacements in `Cargo.toml` for `name`-placeholders are attempted in -// that case. -pub fn unzip(template: &[u8], out_dir: PathBuf, name: Option<&str>) -> Result<()> { - let mut cursor = Cursor::new(Vec::new()); - cursor.write_all(template)?; - cursor.seek(SeekFrom::Start(0))?; - - let mut archive = zip::ZipArchive::new(cursor)?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let outpath = out_dir.join(file.name()); - - if (*file.name()).ends_with('/') { - fs::create_dir_all(&outpath)?; - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(p)?; - } - } - let mut outfile = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(outpath.clone()) - .map_err(|e| { - if e.kind() == std::io::ErrorKind::AlreadyExists { - anyhow::anyhow!("File {} already exists", file.name(),) - } else { - anyhow::anyhow!(e) - } - })?; - - if let Some(name) = name { - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let contents = contents.replace("{{name}}", name); - let contents = - contents.replace("{{camel_name}}", &name.to_upper_camel_case()); - outfile.write_all(contents.as_bytes())?; - } else { - let mut v = Vec::new(); - file.read_to_end(&mut v)?; - outfile.write_all(v.as_slice())?; - } - } - - // Get and set permissions - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; - } - } - } - - Ok(()) -} diff --git a/crates/cargo-contract/src/util/tests.rs b/crates/build/src/util/tests.rs similarity index 66% rename from crates/cargo-contract/src/util/tests.rs rename to crates/build/src/util/tests.rs index 6ce295178..1e143752d 100644 --- a/crates/cargo-contract/src/util/tests.rs +++ b/crates/build/src/util/tests.rs @@ -14,10 +14,7 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use crate::{ - ManifestPath, - OptimizationPasses, -}; +use crate::ManifestPath; use anyhow::{ Context, Result, @@ -55,7 +52,7 @@ where { with_tmp_dir(|tmp_dir| { let project_name = "new_project"; - crate::cmd::new::execute(project_name, Some(tmp_dir)) + crate::new_contract_project(project_name, Some(tmp_dir)) .expect("new project creation failed"); let working_dir = tmp_dir.join(project_name); let manifest_path = ManifestPath::new(working_dir.join("Cargo.toml"))?; @@ -114,109 +111,6 @@ pub fn create_executable(path: &Path, content: &str) -> MockGuard { guard } -/// Init a tracing subscriber for logging in tests. -/// -/// Be aware that this enables `TRACE` by default. It also ignores any error -/// while setting up the logger. -/// -/// The logs are not shown by default, logs are only shown when the test fails -/// or if [`nocapture`](https://doc.rust-lang.org/cargo/commands/cargo-test.html#display-options) -/// is being used. -#[cfg(any(feature = "integration-tests", feature = "test-ci-only"))] -pub fn init_tracing_subscriber() { - let _ = tracing_subscriber::fmt() - .with_max_level(tracing::Level::TRACE) - .with_test_writer() - .try_init(); -} - -/// Enables running a group of tests sequentially, each starting with the original template -/// contract, but maintaining the target directory so compilation artifacts are maintained across -/// each test. -pub struct BuildTestContext { - template_dir: PathBuf, - working_dir: PathBuf, -} - -impl BuildTestContext { - /// Create a new `BuildTestContext`, running the `new` command to create a blank contract - /// template project for testing the build process. - pub fn new(tmp_dir: &Path, working_project_name: &str) -> Result { - crate::cmd::new::execute(working_project_name, Some(tmp_dir)) - .expect("new project creation failed"); - let working_dir = tmp_dir.join(working_project_name); - - let template_dir = tmp_dir.join(format!("{}_template", working_project_name)); - - fs::rename(&working_dir, &template_dir)?; - copy_dir_all(&template_dir, &working_dir)?; - - Ok(Self { - template_dir, - working_dir, - }) - } - - /// Run the supplied test. Test failure will print the error to `stdout`, and this will still - /// return `Ok(())` in order that subsequent tests will still be run. - /// - /// The test may modify the contracts project files (e.g. Cargo.toml, lib.rs), so after - /// completion those files are reverted to their original state for the next test. - /// - /// Importantly, the `target` directory is maintained so as to avoid recompiling all of the - /// dependencies for each test. - pub fn run_test( - &self, - name: &str, - test: impl FnOnce(&ManifestPath) -> Result<()>, - ) -> Result<()> { - println!("Running {}", name); - let manifest_path = ManifestPath::new(self.working_dir.join("Cargo.toml"))?; - match test(&manifest_path) { - Ok(()) => (), - Err(err) => { - println!("{} FAILED: {:?}", name, err); - } - } - // revert to the original template files, but keep the `target` dir from the previous run. - self.remove_all_except_target_dir()?; - copy_dir_all(&self.template_dir, &self.working_dir)?; - Ok(()) - } - - /// Deletes all files and folders in project dir (except the `target` directory) - fn remove_all_except_target_dir(&self) -> Result<()> { - for entry in fs::read_dir(&self.working_dir)? { - let entry = entry?; - let ty = entry.file_type()?; - if ty.is_dir() { - // remove all except the target dir - if entry.file_name() != "target" { - fs::remove_dir_all(entry.path())? - } - } else { - fs::remove_file(entry.path())? - } - } - Ok(()) - } -} - -/// Copy contents of `src` to `dst` recursively. -fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> Result<()> { - fs::create_dir_all(&dst)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - if ty.is_dir() { - copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; - } else { - fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; - } - } - Ok(()) -} - /// Modify a contracts `Cargo.toml` for testing purposes pub struct TestContractManifest { toml: value::Table, @@ -272,10 +166,13 @@ impl TestContractManifest { } /// Set `optimization-passes` in `[package.metadata.contract]` - pub fn set_profile_optimization_passes( + pub fn set_profile_optimization_passes

( &mut self, - passes: OptimizationPasses, - ) -> Result> { + passes: P, + ) -> Result> + where + P: ToString, + { Ok(self .toml .entry("package") diff --git a/crates/cargo-contract/src/validate_wasm.rs b/crates/build/src/validate_wasm.rs similarity index 100% rename from crates/cargo-contract/src/validate_wasm.rs rename to crates/build/src/validate_wasm.rs diff --git a/crates/cargo-contract/src/wasm_opt.rs b/crates/build/src/wasm_opt.rs similarity index 53% rename from crates/cargo-contract/src/wasm_opt.rs rename to crates/build/src/wasm_opt.rs index cc5734c9b..64461aba3 100644 --- a/crates/cargo-contract/src/wasm_opt.rs +++ b/crates/build/src/wasm_opt.rs @@ -14,17 +14,14 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use crate::{ - OptimizationPasses, - OptimizationResult, -}; - use anyhow::Result; use wasm_opt::OptimizationOptions; use std::{ + fmt, fs::metadata, path::PathBuf, + str, }; /// A helpful struct for interacting with Binaryen's `wasm-opt` tool. @@ -95,3 +92,89 @@ impl WasmOptHandler { }) } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum OptimizationPasses { + Zero, + One, + Two, + Three, + Four, + S, + Z, +} + +impl fmt::Display for OptimizationPasses { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let out = match self { + OptimizationPasses::Zero => "0", + OptimizationPasses::One => "1", + OptimizationPasses::Two => "2", + OptimizationPasses::Three => "3", + OptimizationPasses::Four => "4", + OptimizationPasses::S => "s", + OptimizationPasses::Z => "z", + }; + write!(f, "{}", out) + } +} + +impl Default for OptimizationPasses { + fn default() -> OptimizationPasses { + OptimizationPasses::Z + } +} + +impl str::FromStr for OptimizationPasses { + type Err = anyhow::Error; + + fn from_str(input: &str) -> std::result::Result { + // We need to replace " here, since the input string could come + // from either the CLI or the `Cargo.toml` profile section. + // If it is from the profile it could e.g. be "3" or 3. + let normalized_input = input.replace('"', "").to_lowercase(); + match normalized_input.as_str() { + "0" => Ok(OptimizationPasses::Zero), + "1" => Ok(OptimizationPasses::One), + "2" => Ok(OptimizationPasses::Two), + "3" => Ok(OptimizationPasses::Three), + "4" => Ok(OptimizationPasses::Four), + "s" => Ok(OptimizationPasses::S), + "z" => Ok(OptimizationPasses::Z), + _ => anyhow::bail!("Unknown optimization passes for option {}", input), + } + } +} + +impl From for OptimizationPasses { + fn from(str: String) -> Self { + ::from_str(&str).expect("conversion failed") + } +} + +impl From for OptimizationOptions { + fn from(passes: OptimizationPasses) -> OptimizationOptions { + match passes { + OptimizationPasses::Zero => OptimizationOptions::new_opt_level_0(), + OptimizationPasses::One => OptimizationOptions::new_opt_level_1(), + OptimizationPasses::Two => OptimizationOptions::new_opt_level_2(), + OptimizationPasses::Three => OptimizationOptions::new_opt_level_3(), + OptimizationPasses::Four => OptimizationOptions::new_opt_level_4(), + OptimizationPasses::S => OptimizationOptions::new_optimize_for_size(), + OptimizationPasses::Z => { + OptimizationOptions::new_optimize_for_size_aggressively() + } + } + } +} + +/// Result of the optimization process. +#[derive(serde::Serialize)] +pub struct OptimizationResult { + /// The path of the optimized Wasm file. + pub dest_wasm: PathBuf, + /// The original Wasm size. + pub original_size: f64, + /// The Wasm size after optimizations have been applied. + pub optimized_size: f64, +} diff --git a/crates/cargo-contract/src/workspace/manifest.rs b/crates/build/src/workspace/manifest.rs similarity index 100% rename from crates/cargo-contract/src/workspace/manifest.rs rename to crates/build/src/workspace/manifest.rs diff --git a/crates/cargo-contract/src/workspace/metadata.rs b/crates/build/src/workspace/metadata.rs similarity index 93% rename from crates/cargo-contract/src/workspace/metadata.rs rename to crates/build/src/workspace/metadata.rs index bb97ada80..00aae12c8 100644 --- a/crates/cargo-contract/src/workspace/metadata.rs +++ b/crates/build/src/workspace/metadata.rs @@ -40,8 +40,8 @@ pub(super) fn generate_package>( dir.display() ); - let cargo_toml = include_str!("../../templates/tools/generate-metadata/_Cargo.toml"); - let main_rs = include_str!("../../templates/tools/generate-metadata/main.rs"); + let cargo_toml = include_str!("../../templates/generate-metadata/_Cargo.toml"); + let main_rs = include_str!("../../templates/generate-metadata/main.rs"); let mut cargo_toml: value::Table = toml::from_str(cargo_toml)?; let deps = cargo_toml diff --git a/crates/cargo-contract/src/workspace/mod.rs b/crates/build/src/workspace/mod.rs similarity index 100% rename from crates/cargo-contract/src/workspace/mod.rs rename to crates/build/src/workspace/mod.rs diff --git a/crates/cargo-contract/src/workspace/profile.rs b/crates/build/src/workspace/profile.rs similarity index 100% rename from crates/cargo-contract/src/workspace/profile.rs rename to crates/build/src/workspace/profile.rs diff --git a/crates/cargo-contract/templates/tools/generate-metadata/_Cargo.toml b/crates/build/templates/generate-metadata/_Cargo.toml similarity index 100% rename from crates/cargo-contract/templates/tools/generate-metadata/_Cargo.toml rename to crates/build/templates/generate-metadata/_Cargo.toml diff --git a/crates/cargo-contract/templates/tools/generate-metadata/main.rs b/crates/build/templates/generate-metadata/main.rs similarity index 100% rename from crates/cargo-contract/templates/tools/generate-metadata/main.rs rename to crates/build/templates/generate-metadata/main.rs diff --git a/crates/cargo-contract/templates/new/.gitignore b/crates/build/templates/new/.gitignore similarity index 100% rename from crates/cargo-contract/templates/new/.gitignore rename to crates/build/templates/new/.gitignore diff --git a/crates/cargo-contract/templates/new/_Cargo.toml b/crates/build/templates/new/_Cargo.toml similarity index 100% rename from crates/cargo-contract/templates/new/_Cargo.toml rename to crates/build/templates/new/_Cargo.toml diff --git a/crates/cargo-contract/templates/new/lib.rs b/crates/build/templates/new/lib.rs similarity index 100% rename from crates/cargo-contract/templates/new/lib.rs rename to crates/build/templates/new/lib.rs diff --git a/crates/cargo-contract/Cargo.toml b/crates/cargo-contract/Cargo.toml index 9481ecfc9..70638a427 100644 --- a/crates/cargo-contract/Cargo.toml +++ b/crates/cargo-contract/Cargo.toml @@ -14,34 +14,25 @@ description = "Setup and deployment tool for developing Wasm based smart contrac keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"] categories = ["command-line-utilities", "development-tools::build-utils", "development-tools::cargo-plugins"] include = [ - "Cargo.toml", "src/**/*.rs", "README.md", "LICENSE", "build.rs", "templates", "src/**/*.scale", + "Cargo.toml", "src/**/*.rs", "README.md", "LICENSE", "build.rs", "src/**/*.scale", ] [dependencies] +contract-build = { version = "2.0.0-beta", path = "../build" } +contract-metadata = { version = "2.0.0-beta", path = "../metadata" } +contract-transcode = { version = "2.0.0-beta", path = "../transcode" } + anyhow = "1.0.66" clap = { version = "4.0.29", features = ["derive", "env"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } -heck = "0.4.0" -zip = { version = "0.6.3", default-features = false } -parity-wasm = "0.45.0" -cargo_metadata = "0.15.2" scale = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] } which = "4.3.0" colored = "2.0.0" -toml = "0.5.9" -rustc_version = "0.4.0" -blake2 = "0.10.5" -contract-metadata = { version = "2.0.0-beta", path = "../metadata" } -transcode = { package = "contract-transcode", version = "2.0.0-beta", path = "../transcode" } -semver = { version = "1.0.14", features = ["serde"] } serde = { version = "1.0.148", default-features = false, features = ["derive"] } serde_json = "1.0.89" -tempfile = "3.3.0" url = { version = "2.3.1", features = ["serde"] } -impl-serde = "0.4.0" rust_decimal = "1.27" -wasm-opt = "0.110.2" # dependencies for extrinsics (deploying and calling a contract) async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } @@ -54,20 +45,15 @@ jsonrpsee = { version = "0.16.1", features = ["ws-client"] } [build-dependencies] anyhow = "1.0.66" -dirs = "4.0.0" -zip = { version = "0.6.3", default-features = false } -walkdir = "2.3.2" substrate-build-script-utils = "3.0.0" current_platform = "0.2.0" which = "4.3.0" -regex = "1.7.0" [dev-dependencies] assert_cmd = "2.0.6" -pretty_assertions = "1.3.0" -wabt = "0.10.0" regex = "1.7.0" predicates = "2.1.3" +tempfile = "3.3.0" [features] # This `std` feature is required for testing using an inline contract's metadata, because `ink!` annotates the metadata diff --git a/crates/cargo-contract/build.rs b/crates/cargo-contract/build.rs index 069c79ebb..290a4c29d 100644 --- a/crates/cargo-contract/build.rs +++ b/crates/cargo-contract/build.rs @@ -16,117 +16,14 @@ use std::{ borrow::Cow, - env, - ffi::OsStr, - fs::File, - io::{ - prelude::*, - Write, - }, - iter::Iterator, - path::{ - Path, - PathBuf, - }, process::Command, }; -use anyhow::Result; -use walkdir::WalkDir; -use zip::{ - write::FileOptions, - CompressionMethod, - ZipWriter, -}; - use substrate_build_script_utils::rerun_if_git_head_changed; -const DEFAULT_UNIX_PERMISSIONS: u32 = 0o755; - fn main() { generate_cargo_keys(); rerun_if_git_head_changed(); - - let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR") - .expect("CARGO_MANIFEST_DIR should be set by cargo") - .into(); - let out_dir: PathBuf = env::var("OUT_DIR") - .expect("OUT_DIR should be set by cargo") - .into(); - let res = zip_template(&manifest_dir, &out_dir); - - match res { - Ok(()) => std::process::exit(0), - Err(err) => { - eprintln!("Encountered error: {:?}", err); - std::process::exit(1) - } - } -} - -/// Creates a zip archive `template.zip` of the `new` project template in `out_dir`. -fn zip_template(manifest_dir: &Path, out_dir: &Path) -> Result<()> { - let template_dir = manifest_dir.join("templates").join("new"); - let template_dst_file = out_dir.join("template.zip"); - println!( - "Creating template zip: template_dir '{}', destination archive '{}'", - template_dir.display(), - template_dst_file.display() - ); - zip_dir(&template_dir, &template_dst_file, CompressionMethod::Stored).map(|_| { - println!( - "Done: {} written to {}", - template_dir.display(), - template_dst_file.display() - ); - }) -} - -/// Creates a zip archive at `dst_file` with the content of the `src_dir`. -fn zip_dir(src_dir: &Path, dst_file: &Path, method: CompressionMethod) -> Result<()> { - if !src_dir.exists() { - anyhow::bail!("src_dir '{}' does not exist", src_dir.display()); - } - if !src_dir.is_dir() { - anyhow::bail!("src_dir '{}' is not a directory", src_dir.display()); - } - - let file = File::create(dst_file)?; - - let walkdir = WalkDir::new(src_dir); - let it = walkdir.into_iter().filter_map(|e| e.ok()); - - let mut zip = ZipWriter::new(file); - let options = FileOptions::default() - .compression_method(method) - .unix_permissions(DEFAULT_UNIX_PERMISSIONS); - - let mut buffer = Vec::new(); - for entry in it { - let path = entry.path(); - let mut name = path.strip_prefix(src_dir)?.to_path_buf(); - - // `Cargo.toml` files cause the folder to excluded from `cargo package` so need to be renamed - if name.file_name() == Some(OsStr::new("_Cargo.toml")) { - name.set_file_name("Cargo.toml"); - } - - let file_path = name.as_os_str().to_string_lossy(); - - if path.is_file() { - zip.start_file(file_path, options)?; - let mut f = File::open(path)?; - - f.read_to_end(&mut buffer)?; - zip.write_all(&buffer)?; - buffer.clear(); - } else if !name.as_os_str().is_empty() { - zip.add_directory(file_path, options)?; - } - } - zip.finish()?; - - Ok(()) } /// Generate the `cargo:` key output diff --git a/crates/cargo-contract/src/cmd/build.rs b/crates/cargo-contract/src/cmd/build.rs new file mode 100644 index 000000000..b237c3b02 --- /dev/null +++ b/crates/cargo-contract/src/cmd/build.rs @@ -0,0 +1,192 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract 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. +// +// cargo-contract 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 cargo-contract. If not, see . + +use anyhow::Result; +use contract_build::{ + BuildArtifacts, + BuildMode, + BuildResult, + ExecuteArgs, + ManifestPath, + Network, + OptimizationPasses, + OutputType, + UnstableFlags, + UnstableOptions, + Verbosity, + VerbosityFlags, +}; +use std::{ + convert::TryFrom, + path::PathBuf, +}; + +/// Executes build of the smart contract which produces a Wasm binary that is ready for deploying. +/// +/// It does so by invoking `cargo build` and then post processing the final binary. +#[derive(Debug, clap::Args)] +#[clap(name = "build")] +pub struct BuildCommand { + /// Path to the `Cargo.toml` of the contract to build + #[clap(long, value_parser)] + manifest_path: Option, + /// By default the contract is compiled with debug functionality + /// included. This enables the contract to output debug messages, + /// but increases the contract size and the amount of gas used. + /// + /// A production contract should always be build in `release` mode! + /// Then no debug functionality is compiled into the contract. + #[clap(long = "release")] + build_release: bool, + /// Build offline + #[clap(long = "offline")] + build_offline: bool, + /// Performs linting checks during the build process + #[clap(long)] + lint: bool, + /// Which build artifacts to generate. + /// + /// - `all`: Generate the Wasm, the metadata and a bundled `.contract` file. + /// + /// - `code-only`: Only the Wasm is created, generation of metadata and a bundled + /// `.contract` file is skipped. + /// + /// - `check-only`: No artifacts produced: runs the `cargo check` command for the Wasm target, + /// only checks for compilation errors. + #[clap(long = "generate", value_enum, default_value = "all")] + build_artifact: BuildArtifacts, + #[clap(flatten)] + verbosity: VerbosityFlags, + #[clap(flatten)] + unstable_options: UnstableOptions, + /// Number of optimization passes, passed as an argument to `wasm-opt`. + /// + /// - `0`: execute no optimization passes + /// + /// - `1`: execute 1 optimization pass (quick & useful opts, useful for iteration builds) + /// + /// - `2`, execute 2 optimization passes (most opts, generally gets most perf) + /// + /// - `3`, execute 3 optimization passes (spends potentially a lot of time optimizing) + /// + /// - `4`, execute 4 optimization passes (also flatten the IR, which can take a lot more time and memory + /// but is useful on more nested / complex / less-optimized input) + /// + /// - `s`, execute default optimization passes, focusing on code size + /// + /// - `z`, execute default optimization passes, super-focusing on code size + /// + /// - The default value is `z` + /// + /// - It is possible to define the number of optimization passes in the + /// `[package.metadata.contract]` of your `Cargo.toml` as e.g. `optimization-passes = "3"`. + /// The CLI argument always takes precedence over the profile value. + #[clap(long)] + optimization_passes: Option, + /// Do not remove symbols (Wasm name section) when optimizing. + /// + /// This is useful if one wants to analyze or debug the optimized binary. + #[clap(long)] + keep_debug_symbols: bool, + /// Export the build output in JSON format. + #[clap(long, conflicts_with = "verbose")] + output_json: bool, + /// Don't perform wasm validation checks e.g. for permitted imports. + #[clap(long)] + skip_wasm_validation: bool, +} + +impl BuildCommand { + pub fn exec(&self) -> Result { + let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; + let unstable_flags: UnstableFlags = + TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; + let mut verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; + + let build_mode = match self.build_release { + true => BuildMode::Release, + false => BuildMode::Debug, + }; + + let network = match self.build_offline { + true => Network::Offline, + false => Network::Online, + }; + + let output_type = match self.output_json { + true => OutputType::Json, + false => OutputType::HumanReadable, + }; + + // We want to ensure that the only thing in `STDOUT` is our JSON formatted string. + if matches!(output_type, OutputType::Json) { + verbosity = Verbosity::Quiet; + } + + let args = ExecuteArgs { + manifest_path, + verbosity, + build_mode, + network, + build_artifact: self.build_artifact, + unstable_flags, + optimization_passes: self.optimization_passes, + keep_debug_symbols: self.keep_debug_symbols, + lint: self.lint, + output_type, + skip_wasm_validation: self.skip_wasm_validation, + }; + + contract_build::execute(args) + } +} + +#[derive(Debug, clap::Args)] +#[clap(name = "check")] +pub struct CheckCommand { + /// Path to the `Cargo.toml` of the contract to build + #[clap(long, value_parser)] + manifest_path: Option, + #[clap(flatten)] + verbosity: VerbosityFlags, + #[clap(flatten)] + unstable_options: UnstableOptions, +} + +impl CheckCommand { + pub fn exec(&self) -> Result { + let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; + let unstable_flags: UnstableFlags = + TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; + let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; + + let args = ExecuteArgs { + manifest_path, + verbosity, + build_mode: BuildMode::Debug, + network: Network::default(), + build_artifact: BuildArtifacts::CheckOnly, + unstable_flags, + optimization_passes: Some(OptimizationPasses::Zero), + keep_debug_symbols: false, + lint: false, + output_type: OutputType::default(), + skip_wasm_validation: false, + }; + + contract_build::execute(args) + } +} diff --git a/crates/cargo-contract/src/cmd/decode.rs b/crates/cargo-contract/src/cmd/decode.rs index 1b8a68ddd..773f690cb 100644 --- a/crates/cargo-contract/src/cmd/decode.rs +++ b/crates/cargo-contract/src/cmd/decode.rs @@ -14,17 +14,17 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use crate::{ - crate_metadata::CrateMetadata, - util::decode_hex, - DEFAULT_KEY_COL_WIDTH, -}; +use crate::DEFAULT_KEY_COL_WIDTH; use anyhow::{ Context, Result, }; use colored::Colorize as _; -use transcode::ContractMessageTranscoder; +use contract_build::{ + util, + CrateMetadata, +}; +use contract_transcode::ContractMessageTranscoder; #[derive(Debug, Clone, clap::Args)] #[clap( @@ -56,17 +56,17 @@ impl DecodeCommand { let decoded_data = match self.r#type { DataType::Event => { transcoder.decode_contract_event( - &mut &decode_hex(&self.data).context(ERR_MSG)?[..], + &mut &util::decode_hex(&self.data).context(ERR_MSG)?[..], )? } DataType::Message => { transcoder.decode_contract_message( - &mut &decode_hex(&self.data).context(ERR_MSG)?[..], + &mut &util::decode_hex(&self.data).context(ERR_MSG)?[..], )? } DataType::Constructor => { transcoder.decode_contract_constructor( - &mut &decode_hex(&self.data).context(ERR_MSG)?[..], + &mut &util::decode_hex(&self.data).context(ERR_MSG)?[..], )? } }; diff --git a/crates/cargo-contract/src/cmd/extrinsics/call.rs b/crates/cargo-contract/src/cmd/extrinsics/call.rs index c7659f325..c103e9196 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/call.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/call.rs @@ -39,18 +39,19 @@ use crate::{ events::DisplayEvents, ErrorVariant, }, - name_value_println, DEFAULT_KEY_COL_WIDTH, }; +use contract_build::name_value_println; + use anyhow::{ anyhow, Result, }; +use contract_transcode::Value; use pallet_contracts_primitives::ContractExecResult; use scale::Encode; use sp_weights::Weight; -use transcode::Value; use std::fmt::Debug; use subxt::{ diff --git a/crates/cargo-contract/src/cmd/extrinsics/events.rs b/crates/cargo-contract/src/cmd/extrinsics/events.rs index 5fcdb2cfb..5120eab64 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/events.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/events.rs @@ -20,12 +20,10 @@ use super::{ DefaultConfig, TokenMetadata, }; -use crate::{ - Verbosity, - DEFAULT_KEY_COL_WIDTH, -}; +use crate::DEFAULT_KEY_COL_WIDTH; use colored::Colorize as _; -use transcode::{ +use contract_build::Verbosity; +use contract_transcode::{ ContractMessageTranscoder, TranscoderBuilder, Value, diff --git a/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs b/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs index 1e8ada9fd..aa301500e 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs @@ -39,9 +39,6 @@ use crate::{ ErrorVariant, TokenMetadata, }, - name_value_println, - util::decode_hex, - Verbosity, DEFAULT_KEY_COL_WIDTH, }; use anyhow::{ @@ -49,6 +46,11 @@ use anyhow::{ Context, Result, }; +use contract_build::{ + name_value_println, + util::decode_hex, + Verbosity, +}; use pallet_contracts_primitives::ContractInstantiateResult; diff --git a/crates/cargo-contract/src/cmd/extrinsics/integration_tests.rs b/crates/cargo-contract/src/cmd/extrinsics/integration_tests.rs index ea96085c9..185faa9ac 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/integration_tests.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/integration_tests.rs @@ -121,6 +121,22 @@ impl ContractsNodeProcess { } } +/// Init a tracing subscriber for logging in tests. +/// +/// Be aware that this enables `TRACE` by default. It also ignores any error +/// while setting up the logger. +/// +/// The logs are not shown by default, logs are only shown when the test fails +/// or if [`nocapture`](https://doc.rust-lang.org/cargo/commands/cargo-test.html#display-options) +/// is being used. +#[cfg(any(feature = "integration-tests", feature = "test-ci-only"))] +pub fn init_tracing_subscriber() { + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .with_test_writer() + .try_init(); +} + /// Sanity test the whole lifecycle of: /// new -> build -> upload -> instantiate -> call /// @@ -136,7 +152,7 @@ impl ContractsNodeProcess { #[ignore] #[async_std::test] async fn build_upload_instantiate_call() { - crate::util::tests::init_tracing_subscriber(); + init_tracing_subscriber(); let tmp_dir = tempfile::Builder::new() .prefix("cargo-contract.cli.test.") diff --git a/crates/cargo-contract/src/cmd/extrinsics/mod.rs b/crates/cargo-contract/src/cmd/extrinsics/mod.rs index 3eed57c10..116ec7745 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/mod.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/mod.rs @@ -46,12 +46,12 @@ use std::{ path::PathBuf, }; -use crate::{ - crate_metadata::CrateMetadata, +use crate::DEFAULT_KEY_COL_WIDTH; +use contract_build::{ name_value_println, + CrateMetadata, Verbosity, VerbosityFlags, - DEFAULT_KEY_COL_WIDTH, }; use pallet_contracts_primitives::ContractResult; use scale::{ @@ -78,10 +78,10 @@ pub use balance::{ TokenMetadata, }; pub use call::CallCommand; +pub use contract_transcode::ContractMessageTranscoder; pub use error::ErrorVariant; pub use instantiate::InstantiateCommand; pub use subxt::PolkadotConfig as DefaultConfig; -pub use transcode::ContractMessageTranscoder; pub use upload::UploadCommand; type Balance = u128; diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 31a73208f..c88b2d004 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -16,8 +16,6 @@ pub mod build; pub mod decode; -pub mod metadata; -pub mod new; pub mod test; pub(crate) use self::{ diff --git a/crates/cargo-contract/src/cmd/test.rs b/crates/cargo-contract/src/cmd/test.rs index 827b2ebec..3dcb7a48e 100644 --- a/crates/cargo-contract/src/cmd/test.rs +++ b/crates/cargo-contract/src/cmd/test.rs @@ -14,15 +14,15 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use crate::{ +use anyhow::Result; +use colored::Colorize; +use contract_build::{ maybe_println, util, - workspace::ManifestPath, + ManifestPath, Verbosity, VerbosityFlags, }; -use anyhow::Result; -use colored::Colorize; use std::{ convert::TryFrom, path::PathBuf, @@ -83,25 +83,33 @@ pub(crate) fn execute( #[cfg(feature = "test-ci-only")] #[cfg(test)] mod tests_ci_only { - use crate::{ - util::tests::with_new_contract_project, + use contract_build::{ + ManifestPath, Verbosity, }; use regex::Regex; #[test] fn passing_tests_yield_stdout() { - with_new_contract_project(|manifest_path| { - let ok_output_pattern = - Regex::new(r"test result: ok. \d+ passed; 0 failed; \d+ ignored") - .expect("regex pattern compilation failed"); + let tmp_dir = tempfile::Builder::new() + .prefix("cargo-contract.test.") + .tempdir() + .expect("temporary directory creation failed"); + + let project_name = "test_project"; + contract_build::new_contract_project(project_name, Some(&tmp_dir)) + .expect("new project creation failed"); + let working_dir = tmp_dir.path().join(project_name); + let manifest_path = ManifestPath::new(working_dir.join("Cargo.toml")) + .expect("invalid manifest path"); - let res = super::execute(&manifest_path, Verbosity::Default) - .expect("test execution failed"); + let ok_output_pattern = + Regex::new(r"test result: ok. \d+ passed; 0 failed; \d+ ignored") + .expect("regex pattern compilation failed"); - assert!(ok_output_pattern.is_match(&String::from_utf8_lossy(&res.stdout))); + let res = super::execute(&manifest_path, Verbosity::Default) + .expect("test execution failed"); - Ok(()) - }) + assert!(ok_output_pattern.is_match(&String::from_utf8_lossy(&res.stdout))); } } diff --git a/crates/cargo-contract/src/main.rs b/crates/cargo-contract/src/main.rs index b4aa1f4dc..cc911cacb 100644 --- a/crates/cargo-contract/src/main.rs +++ b/crates/cargo-contract/src/main.rs @@ -17,40 +17,28 @@ #![deny(unused_crate_dependencies)] mod cmd; -mod crate_metadata; -mod util; -mod validate_wasm; -mod wasm_opt; -mod workspace; -use self::{ - cmd::{ - metadata::MetadataResult, - BuildCommand, - CallCommand, - CheckCommand, - DecodeCommand, - ErrorVariant, - InstantiateCommand, - TestCommand, - UploadCommand, - }, +use self::cmd::{ + BuildCommand, + CallCommand, + CheckCommand, + DecodeCommand, + ErrorVariant, + InstantiateCommand, + TestCommand, + UploadCommand, +}; +use contract_build::{ + name_value_println, util::DEFAULT_KEY_COL_WIDTH, - workspace::ManifestPath, + OutputType, }; - use std::{ - convert::TryFrom, - fmt::{ - Display, - Formatter, - Result as DisplayResult, - }, + fmt::Display, path::PathBuf, str::FromStr, }; -use ::wasm_opt::OptimizationOptions; use anyhow::{ anyhow, Error, @@ -68,12 +56,12 @@ use colored::Colorize; // in order to satisfy the `unused_crate_dependencies` lint. #[cfg(test)] use assert_cmd as _; - #[cfg(test)] use predicates as _; - #[cfg(test)] use regex as _; +#[cfg(test)] +use tempfile as _; // Only used on windows. use which as _; @@ -106,398 +94,6 @@ impl FromStr for HexData { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub enum OptimizationPasses { - Zero, - One, - Two, - Three, - Four, - S, - Z, -} - -impl Display for OptimizationPasses { - fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { - let out = match self { - OptimizationPasses::Zero => "0", - OptimizationPasses::One => "1", - OptimizationPasses::Two => "2", - OptimizationPasses::Three => "3", - OptimizationPasses::Four => "4", - OptimizationPasses::S => "s", - OptimizationPasses::Z => "z", - }; - write!(f, "{}", out) - } -} - -impl Default for OptimizationPasses { - fn default() -> OptimizationPasses { - OptimizationPasses::Z - } -} - -impl FromStr for OptimizationPasses { - type Err = Error; - - fn from_str(input: &str) -> std::result::Result { - // We need to replace " here, since the input string could come - // from either the CLI or the `Cargo.toml` profile section. - // If it is from the profile it could e.g. be "3" or 3. - let normalized_input = input.replace('"', "").to_lowercase(); - match normalized_input.as_str() { - "0" => Ok(OptimizationPasses::Zero), - "1" => Ok(OptimizationPasses::One), - "2" => Ok(OptimizationPasses::Two), - "3" => Ok(OptimizationPasses::Three), - "4" => Ok(OptimizationPasses::Four), - "s" => Ok(OptimizationPasses::S), - "z" => Ok(OptimizationPasses::Z), - _ => anyhow::bail!("Unknown optimization passes for option {}", input), - } - } -} - -impl From for OptimizationPasses { - fn from(str: String) -> Self { - OptimizationPasses::from_str(&str).expect("conversion failed") - } -} - -impl From for OptimizationOptions { - fn from(passes: OptimizationPasses) -> OptimizationOptions { - match passes { - OptimizationPasses::Zero => OptimizationOptions::new_opt_level_0(), - OptimizationPasses::One => OptimizationOptions::new_opt_level_1(), - OptimizationPasses::Two => OptimizationOptions::new_opt_level_2(), - OptimizationPasses::Three => OptimizationOptions::new_opt_level_3(), - OptimizationPasses::Four => OptimizationOptions::new_opt_level_4(), - OptimizationPasses::S => OptimizationOptions::new_optimize_for_size(), - OptimizationPasses::Z => { - OptimizationOptions::new_optimize_for_size_aggressively() - } - } - } -} - -#[derive(Default, Clone, Debug, Args)] -pub struct VerbosityFlags { - /// No output printed to stdout - #[clap(long)] - quiet: bool, - /// Use verbose output - #[clap(long)] - verbose: bool, -} - -/// Denotes if output should be printed to stdout. -#[derive(Clone, Copy, serde::Serialize, Eq, PartialEq)] -pub enum Verbosity { - /// Use default output - Default, - /// No output printed to stdout - Quiet, - /// Use verbose output - Verbose, -} - -impl Default for Verbosity { - fn default() -> Self { - Verbosity::Default - } -} - -impl Verbosity { - /// Returns `true` if output should be printed (i.e. verbose output is set). - pub(crate) fn is_verbose(&self) -> bool { - match self { - Verbosity::Quiet => false, - Verbosity::Default | Verbosity::Verbose => true, - } - } -} - -impl TryFrom<&VerbosityFlags> for Verbosity { - type Error = Error; - - fn try_from(value: &VerbosityFlags) -> Result { - match (value.quiet, value.verbose) { - (false, false) => Ok(Verbosity::Default), - (true, false) => Ok(Verbosity::Quiet), - (false, true) => Ok(Verbosity::Verbose), - (true, true) => anyhow::bail!("Cannot pass both --quiet and --verbose flags"), - } - } -} - -#[derive(Default, Clone, Debug, Args)] -struct UnstableOptions { - /// Use the original manifest (Cargo.toml), do not modify for build optimizations - #[clap(long = "unstable-options", short = 'Z', number_of_values = 1)] - options: Vec, -} - -#[derive(Clone, Default)] -struct UnstableFlags { - original_manifest: bool, -} - -impl TryFrom<&UnstableOptions> for UnstableFlags { - type Error = Error; - - fn try_from(value: &UnstableOptions) -> Result { - let valid_flags = ["original-manifest"]; - let invalid_flags = value - .options - .iter() - .filter(|o| !valid_flags.contains(&o.as_str())) - .collect::>(); - if !invalid_flags.is_empty() { - anyhow::bail!("Unknown unstable-options {:?}", invalid_flags) - } - Ok(UnstableFlags { - original_manifest: value.options.contains(&"original-manifest".to_owned()), - }) - } -} - -/// Describes which artifacts to generate -#[derive(Copy, Clone, Eq, PartialEq, Debug, clap::ValueEnum, serde::Serialize)] -#[clap(name = "build-artifacts")] -pub enum BuildArtifacts { - /// Generate the Wasm, the metadata and a bundled `.contract` file - #[clap(name = "all")] - All, - /// Only the Wasm is created, generation of metadata and a bundled `.contract` file is - /// skipped - #[clap(name = "code-only")] - CodeOnly, - /// No artifacts produced: runs the `cargo check` command for the Wasm target, only checks for - /// compilation errors. - #[clap(name = "check-only")] - CheckOnly, -} - -impl BuildArtifacts { - /// Returns the number of steps required to complete a build artifact. - /// Used as output on the cli. - pub fn steps(&self) -> BuildSteps { - match self { - BuildArtifacts::All => BuildSteps::new(5), - BuildArtifacts::CodeOnly => BuildSteps::new(4), - BuildArtifacts::CheckOnly => BuildSteps::new(1), - } - } -} - -impl Default for BuildArtifacts { - fn default() -> Self { - BuildArtifacts::All - } -} - -/// Track and display the current and total number of steps. -#[derive(Debug, Clone, Copy)] -pub struct BuildSteps { - pub current_step: usize, - pub total_steps: usize, -} - -impl BuildSteps { - pub fn new(total_steps: usize) -> Self { - Self { - current_step: 1, - total_steps, - } - } - - pub fn increment_current(&mut self) { - self.current_step += 1; - } -} - -impl Display for BuildSteps { - fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { - write!(f, "[{}/{}]", self.current_step, self.total_steps) - } -} - -/// The mode to build the contract in. -#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum BuildMode { - /// Functionality to output debug messages is build into the contract. - Debug, - /// The contract is build without any debugging functionality. - Release, -} - -impl Default for BuildMode { - fn default() -> BuildMode { - BuildMode::Debug - } -} - -impl Display for BuildMode { - fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { - match self { - Self::Debug => write!(f, "debug"), - Self::Release => write!(f, "release"), - } - } -} - -/// Use network connection to build contracts and generate metadata or use cached dependencies only. -#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize)] -pub enum Network { - /// Use network - Online, - /// Use cached dependencies. - Offline, -} - -impl Default for Network { - fn default() -> Network { - Network::Online - } -} - -impl Display for Network { - fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { - match self { - Self::Online => write!(f, ""), - Self::Offline => write!(f, "--offline"), - } - } -} - -/// The type of output to display at the end of a build. -pub enum OutputType { - /// Output build results in a human readable format. - HumanReadable, - /// Output the build results JSON formatted. - Json, -} - -impl Default for OutputType { - fn default() -> Self { - OutputType::HumanReadable - } -} - -/// Result of the metadata generation process. -#[derive(serde::Serialize)] -pub struct BuildResult { - /// Path to the resulting Wasm file. - pub dest_wasm: Option, - /// Result of the metadata generation. - pub metadata_result: Option, - /// Path to the directory where output files are written to. - pub target_directory: PathBuf, - /// If existent the result of the optimization. - pub optimization_result: Option, - /// The mode to build the contract in. - pub build_mode: BuildMode, - /// Which build artifacts were generated. - pub build_artifact: BuildArtifacts, - /// The verbosity flags. - pub verbosity: Verbosity, - /// The type of formatting to use for the build output. - #[serde(skip_serializing)] - pub output_type: OutputType, -} - -/// Result of the optimization process. -#[derive(serde::Serialize)] -pub struct OptimizationResult { - /// The path of the optimized Wasm file. - pub dest_wasm: PathBuf, - /// The original Wasm size. - pub original_size: f64, - /// The Wasm size after optimizations have been applied. - pub optimized_size: f64, -} - -impl BuildResult { - pub fn display(&self) -> String { - let optimization = self.display_optimization(); - let size_diff = format!( - "\nOriginal wasm size: {}, Optimized: {}\n\n", - format!("{:.1}K", optimization.0).bold(), - format!("{:.1}K", optimization.1).bold(), - ); - debug_assert!( - optimization.1 > 0.0, - "optimized file size must be greater 0" - ); - - let build_mode = format!( - "The contract was built in {} mode.\n\n", - format!("{}", self.build_mode).to_uppercase().bold(), - ); - - if self.build_artifact == BuildArtifacts::CodeOnly { - let out = format!( - "{}{}Your contract's code is ready. You can find it here:\n{}", - size_diff, - build_mode, - self.dest_wasm - .as_ref() - .expect("wasm path must exist") - .display() - .to_string() - .bold() - ); - return out - }; - - let mut out = format!( - "{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n", - size_diff, - build_mode, - self.target_directory.display().to_string().bold(), - ); - if let Some(metadata_result) = self.metadata_result.as_ref() { - let bundle = format!( - " - {} (code + metadata)\n", - util::base_name(&metadata_result.dest_bundle).bold() - ); - out.push_str(&bundle); - } - if let Some(dest_wasm) = self.dest_wasm.as_ref() { - let wasm = format!( - " - {} (the contract's code)\n", - util::base_name(dest_wasm).bold() - ); - out.push_str(&wasm); - } - if let Some(metadata_result) = self.metadata_result.as_ref() { - let metadata = format!( - " - {} (the contract's metadata)", - util::base_name(&metadata_result.dest_metadata).bold() - ); - out.push_str(&metadata); - } - out - } - - /// Returns a tuple of `(original_size, optimized_size)`. - /// - /// Panics if no optimization result is available. - fn display_optimization(&self) -> (f64, f64) { - let optimization = self - .optimization_result - .as_ref() - .expect("optimization result must exist"); - (optimization.original_size, optimization.optimized_size) - } - - /// Display the build results in a pretty formatted JSON string. - pub fn serialize_json(&self) -> Result { - Ok(serde_json::to_string_pretty(self)?) - } -} - #[derive(Debug, Subcommand)] enum Command { /// Setup and create a new smart contract project @@ -549,7 +145,7 @@ fn main() { fn exec(cmd: Command) -> Result<()> { match &cmd { Command::New { name, target_dir } => { - cmd::new::execute(name, target_dir.as_ref())?; + contract_build::new_contract_project(name, target_dir.as_ref())?; println!("Created contract {}", name); Ok(()) } @@ -618,54 +214,3 @@ fn format_err(err: E) -> Error { format!("{}", err).bright_red() ) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_result_seralization_sanity_check() { - // given - let raw_result = r#"{ - "dest_wasm": "/path/to/contract.wasm", - "metadata_result": { - "dest_metadata": "/path/to/metadata.json", - "dest_bundle": "/path/to/contract.contract" - }, - "target_directory": "/path/to/target", - "optimization_result": { - "dest_wasm": "/path/to/contract.wasm", - "original_size": 64.0, - "optimized_size": 32.0 - }, - "build_mode": "Debug", - "build_artifact": "All", - "verbosity": "Quiet" -}"#; - - let build_result = BuildResult { - dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")), - metadata_result: Some(MetadataResult { - dest_metadata: PathBuf::from("/path/to/metadata.json"), - dest_bundle: PathBuf::from("/path/to/contract.contract"), - }), - target_directory: PathBuf::from("/path/to/target"), - optimization_result: Some(OptimizationResult { - dest_wasm: PathBuf::from("/path/to/contract.wasm"), - original_size: 64.0, - optimized_size: 32.0, - }), - build_mode: Default::default(), - build_artifact: Default::default(), - verbosity: Verbosity::Quiet, - output_type: OutputType::Json, - }; - - // when - let serialized_result = build_result.serialize_json(); - - // then - assert!(serialized_result.is_ok()); - assert_eq!(serialized_result.unwrap(), raw_result); - } -} diff --git a/crates/metadata/Cargo.toml b/crates/metadata/Cargo.toml index 2028fb382..8afd40cd7 100644 --- a/crates/metadata/Cargo.toml +++ b/crates/metadata/Cargo.toml @@ -16,7 +16,7 @@ include = ["Cargo.toml", "*.rs", "LICENSE"] [dependencies] impl-serde = "0.4.0" semver = { version = "1.0.14", features = ["serde"] } -serde = { version = "1.0.148", default-features = false, features = ["derive"] } +serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1.0.89" url = { version = "2.3.1", features = ["serde"] } anyhow = "1.0.66"