diff --git a/README.md b/README.md index c3d97847ed0e747dfc12b1f686fa197ecc3864f5..3550f8668166a9cc076545f9ffd22a22953ffc20 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ OPTIONS: SUBCOMMANDS: new Setup and create a new smart contract project - build Compiles the smart contract - generate-metadata Generate contract metadata artifacts + build Compiles the contract, generates metadata, bundles both together in a '.contract' file + check Check that the code builds as Wasm; does not output any build artifact to the top level `target/` directory test Test the smart contract off-chain deploy Upload the smart contract code to the chain instantiate Instantiate a deployed smart contract diff --git a/metadata/lib.rs b/metadata/lib.rs index 285074151fe4ec5b97836b98b2785a529b75f427..1c1e606d707c836cc839445ebac03f9537496bf7 100644 --- a/metadata/lib.rs +++ b/metadata/lib.rs @@ -27,15 +27,16 @@ //! //! let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0)); //! let compiler = SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap()); -//! let source = Source::new([0u8; 32], language, compiler); +//! let wasm = SourceWasm::new(vec![0u8]); +//! let source = Source::new(Some(wasm), Some(CodeHash([0u8; 32])), language, compiler); //! let contract = Contract::builder() //! .name("incrementer".to_string()) //! .version(Version::new(2, 1, 0)) //! .authors(vec!["Parity Technologies ".to_string()]) //! .description("increment a value".to_string()) -//! .documentation(Url::parse("http:docs.rs/").unwrap()) -//! .repository(Url::parse("http:github.com/paritytech/ink/").unwrap()) -//! .homepage(Url::parse("http:example.com/").unwrap()) +//! .documentation(Url::parse("http://docs.rs/").unwrap()) +//! .repository(Url::parse("http://github.com/paritytech/ink/").unwrap()) +//! .homepage(Url::parse("http://example.com/").unwrap()) //! .license("Apache-2.0".to_string()) //! .build() //! .unwrap(); @@ -60,7 +61,7 @@ use url::Url; const METADATA_VERSION: &str = "0.1.0"; /// Smart contract metadata. -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct ContractMetadata { #[serde(rename = "metadataVersion")] metadata_version: semver::Version, @@ -92,29 +93,86 @@ impl ContractMetadata { abi, } } + + pub fn remove_source_wasm_attribute(&mut self) { + self.source.wasm = None; + } +} + +/// Representation of the Wasm code hash. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CodeHash(pub [u8; 32]); + +impl Serialize for CodeHash { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_as_byte_str(&self.0[..], serializer) + } } -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct Source { - #[serde(serialize_with = "serialize_as_byte_str")] - hash: [u8; 32], + #[serde(skip_serializing_if = "Option::is_none")] + hash: Option, language: SourceLanguage, compiler: SourceCompiler, + #[serde(skip_serializing_if = "Option::is_none")] + wasm: Option, } impl Source { /// Constructs a new InkProjectSource. - pub fn new(hash: [u8; 32], language: SourceLanguage, compiler: SourceCompiler) -> Self { + pub fn new( + wasm: Option, + hash: Option, + language: SourceLanguage, + compiler: SourceCompiler, + ) -> Self { Source { hash, language, compiler, + wasm, + } + } +} + +/// The bytes of the compiled Wasm smart contract. +#[derive(Clone, Debug)] +pub struct SourceWasm { + wasm: Vec, +} + +impl SourceWasm { + /// Constructs a new `SourceWasm`. + pub fn new(wasm: Vec) -> Self { + SourceWasm { wasm } + } +} + +impl Serialize for SourceWasm { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_as_byte_str(&self.wasm[..], serializer) + } +} + +impl Display for SourceWasm { + fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { + write!(f, "0x").expect("failed writing to string"); + for byte in &self.wasm { + write!(f, "{:02x}", byte).expect("failed writing to string"); } + write!(f, "") } } /// The language and version in which a smart contract is written. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct SourceLanguage { language: Language, version: Version, @@ -143,7 +201,7 @@ impl Display for SourceLanguage { } /// The language in which the smart contract is written. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Language { Ink, Solidity, @@ -161,7 +219,7 @@ impl Display for Language { } /// A compiler used to compile a smart contract. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct SourceCompiler { compiler: Compiler, version: Version, @@ -189,7 +247,7 @@ impl SourceCompiler { } /// Compilers used to compile a smart contract. -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub enum Compiler { RustC, Solang, @@ -205,7 +263,7 @@ impl Display for Compiler { } /// Metadata about a smart contract. -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct Contract { name: String, version: Version, @@ -229,7 +287,7 @@ impl Contract { } /// Additional user defined metadata, can be any valid json. -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct User { #[serde(flatten)] json: Map, @@ -463,7 +521,8 @@ mod tests { let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0)); let compiler = SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap()); - let source = Source::new([0u8; 32], language, compiler); + let wasm = SourceWasm::new(vec![0u8, 1u8, 2u8]); + let source = Source::new(Some(wasm), Some(CodeHash([0u8; 32])), language, compiler); let contract = Contract::builder() .name("incrementer".to_string()) .version(Version::new(2, 1, 0)) @@ -507,7 +566,8 @@ mod tests { "source": { "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "language": "ink! 2.1.0", - "compiler": "rustc 1.46.0-nightly" + "compiler": "rustc 1.46.0-nightly", + "wasm": "0x000102" }, "contract": { "name": "incrementer", @@ -544,7 +604,7 @@ mod tests { let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0)); let compiler = SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap()); - let source = Source::new([0u8; 32], language, compiler); + let source = Source::new(None, Some(CodeHash([0u8; 32])), language, compiler); let contract = Contract::builder() .name("incrementer".to_string()) .version(Version::new(2, 1, 0)) diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 9c8cfec2e7f42d081b14771254db03444fb6a94d..f65772e2f9058dc1c492ea410d82e0bdac999d13 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -24,7 +24,7 @@ use crate::{ crate_metadata::CrateMetadata, util, workspace::{ManifestPath, Profile, Workspace}, - UnstableFlags, Verbosity, + GenerateArtifacts, GenerationResult, OptimizationResult, UnstableFlags, Verbosity, }; use anyhow::{Context, Result}; use colored::Colorize; @@ -192,7 +192,7 @@ fn post_process_wasm(crate_metadata: &CrateMetadata) -> Result<()> { /// /// The intention is to reduce the size of bloated wasm binaries as a result of missing /// optimizations (or bugs?) between Rust and Wasm. -fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result<()> { +fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result { let mut optimized = crate_metadata.dest_wasm.clone(); optimized.set_file_name(format!("{}-opt.wasm", crate_metadata.package_name)); @@ -219,14 +219,13 @@ fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result<()> { let original_size = metadata(&crate_metadata.dest_wasm)?.len() as f64 / 1000.0; let optimized_size = metadata(&optimized)?.len() as f64 / 1000.0; - println!( - " Original wasm size: {:.1}K, Optimized: {:.1}K", - original_size, optimized_size - ); // overwrite existing destination wasm file with the optimised version std::fs::rename(&optimized, &crate_metadata.dest_wasm)?; - Ok(()) + Ok(OptimizationResult { + original_size, + optimized_size, + }) } /// Executes build of the smart-contract which produces a wasm binary that is ready for deploying. @@ -240,49 +239,81 @@ fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result<()> { pub(crate) fn execute( manifest_path: &ManifestPath, verbosity: Option, + optimize_contract: bool, + build_artifact: GenerateArtifacts, unstable_options: UnstableFlags, -) -> Result { +) -> Result { let crate_metadata = CrateMetadata::collect(manifest_path)?; - execute_with_metadata(&crate_metadata, verbosity, unstable_options) + if build_artifact == GenerateArtifacts::CodeOnly { + let (maybe_dest_wasm, maybe_optimization_result) = execute_with_crate_metadata( + &crate_metadata, + verbosity, + optimize_contract, + build_artifact, + unstable_options, + )?; + let res = GenerationResult { + dest_wasm: maybe_dest_wasm, + dest_metadata: None, + dest_bundle: None, + target_directory: crate_metadata.cargo_meta.target_directory, + optimization_result: maybe_optimization_result, + }; + return Ok(res); + } + + let res = + super::metadata::execute(&manifest_path, verbosity, build_artifact, unstable_options)?; + Ok(res) } -/// Executes build of the smart-contract which produces a wasm binary that is ready for deploying. +/// 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. /// /// # Note /// /// Uses the supplied `CrateMetadata`. If an instance is not available use [`execute_build`] -pub(crate) fn execute_with_metadata( +/// +/// Returns a tuple of `(maybe_optimized_wasm_path, maybe_optimization_result)`. +pub(crate) fn execute_with_crate_metadata( crate_metadata: &CrateMetadata, verbosity: Option, + optimize_contract: bool, + build_artifact: GenerateArtifacts, unstable_options: UnstableFlags, -) -> Result { +) -> Result<(Option, Option)> { println!( " {} {}", - "[1/3]".bold(), + format!("[1/{}]", build_artifact.steps()).bold(), "Building cargo project".bright_green().bold() ); build_cargo_project(&crate_metadata, verbosity, unstable_options)?; println!( " {} {}", - "[2/3]".bold(), + format!("[2/{}]", build_artifact.steps()).bold(), "Post processing wasm file".bright_green().bold() ); post_process_wasm(&crate_metadata)?; + if !optimize_contract { + return Ok((None, None)); + } println!( " {} {}", - "[3/3]".bold(), + format!("[3/{}]", build_artifact.steps()).bold(), "Optimizing wasm file".bright_green().bold() ); - optimize_wasm(&crate_metadata)?; - Ok(crate_metadata.dest_wasm.clone()) + let optimization_result = optimize_wasm(&crate_metadata)?; + Ok(( + Some(crate_metadata.dest_wasm.clone()), + Some(optimization_result), + )) } #[cfg(feature = "test-ci-only")] #[cfg(test)] mod tests { - use crate::{cmd, util::tests::with_tmp_dir, ManifestPath, UnstableFlags}; + use crate::{cmd, util::tests::with_tmp_dir, GenerateArtifacts, ManifestPath, UnstableFlags}; #[test] fn build_template() { @@ -290,7 +321,14 @@ mod tests { cmd::new::execute("new_project", Some(path)).expect("new project creation failed"); let manifest_path = ManifestPath::new(&path.join("new_project").join("Cargo.toml")).unwrap(); - super::execute(&manifest_path, None, UnstableFlags::default()).expect("build failed"); + super::execute( + &manifest_path, + None, + true, + GenerateArtifacts::All, + UnstableFlags::default(), + ) + .expect("build failed"); Ok(()) }) } diff --git a/src/cmd/metadata.rs b/src/cmd/metadata.rs index 15393910e3c48e5da36e52a0077fb40e2f91ed20..e80c01e4065988dc0c0018fbad433a2c2fdaa129 100644 --- a/src/cmd/metadata.rs +++ b/src/cmd/metadata.rs @@ -18,11 +18,15 @@ use crate::{ crate_metadata::CrateMetadata, util, workspace::{ManifestPath, Workspace}, - UnstableFlags, Verbosity, + GenerateArtifacts, GenerationResult, OptimizationResult, UnstableFlags, Verbosity, }; + use anyhow::Result; +use blake2::digest::{Update as _, VariableOutput as _}; +use colored::Colorize; use contract_metadata::{ - Compiler, Contract, ContractMetadata, Language, Source, SourceCompiler, SourceLanguage, User, + CodeHash, Compiler, Contract, ContractMetadata, Language, Source, SourceCompiler, + SourceLanguage, SourceWasm, User, }; use semver::Version; use std::{fs, path::PathBuf}; @@ -34,23 +38,52 @@ const METADATA_FILE: &str = "metadata.json"; struct GenerateMetadataCommand { crate_metadata: CrateMetadata, verbosity: Option, + build_artifact: GenerateArtifacts, unstable_options: UnstableFlags, } +/// Result of generating the extended contract project metadata +struct ExtendedMetadataResult { + dest_wasm: Option, + source: Source, + contract: Contract, + user: Option, + optimization_result: Option, +} + impl GenerateMetadataCommand { - pub fn exec(&self) -> Result { + pub fn exec(&self) -> Result { util::assert_channel()?; - println!(" Generating metadata"); let cargo_meta = &self.crate_metadata.cargo_meta; - let out_path = cargo_meta.target_directory.join(METADATA_FILE); - let target_dir = cargo_meta.target_directory.clone(); + let out_path_metadata = cargo_meta.target_directory.join(METADATA_FILE); + + let fname_bundle = format!("{}.contract", self.crate_metadata.package_name); + let out_path_bundle = cargo_meta.target_directory.join(fname_bundle); + + let target_directory = cargo_meta.target_directory.clone(); // build the extended contract project metadata - let (source_meta, contract_meta, user_meta) = self.extended_metadata()?; + let ExtendedMetadataResult { + dest_wasm, + source, + contract, + user, + optimization_result, + } = self.extended_metadata()?; let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> { - let target_dir_arg = format!("--target-dir={}", target_dir.to_string_lossy()); + let mut current_progress = 4; + let curr_step = match self.build_artifact { + GenerateArtifacts::MetadataOnly => 1, + _ => current_progress, + }; + println!( + " {} {}", + format!("[{}/{}]", curr_step, self.build_artifact.steps()).bold(), + "Generating metadata".bright_green().bold() + ); + let target_dir_arg = format!("--target-dir={}", target_directory.to_string_lossy()); let stdout = util::invoke_cargo( "run", &[ @@ -66,9 +99,25 @@ impl GenerateMetadataCommand { let ink_meta: serde_json::Map = serde_json::from_slice(&stdout)?; - let metadata = ContractMetadata::new(source_meta, contract_meta, user_meta, ink_meta); - let contents = serde_json::to_string_pretty(&metadata)?; - fs::write(&out_path, contents)?; + let metadata = ContractMetadata::new(source, contract, user, ink_meta); + { + let mut metadata = metadata.clone(); + metadata.remove_source_wasm_attribute(); + let contents = serde_json::to_string_pretty(&metadata)?; + fs::write(&out_path_metadata, contents)?; + current_progress += 1; + } + + if self.build_artifact == GenerateArtifacts::All { + println!( + " {} {}", + format!("[{}/{}]", current_progress, self.build_artifact.steps()).bold(), + "Generating bundle".bright_green().bold() + ); + let contents = serde_json::to_string_pretty(&metadata)?; + fs::write(&out_path_bundle, contents)?; + } + Ok(()) }; @@ -86,11 +135,22 @@ impl GenerateMetadataCommand { .using_temp(generate_metadata)?; } - Ok(out_path) + let dest_bundle = if self.build_artifact == GenerateArtifacts::All { + Some(out_path_bundle) + } else { + None + }; + Ok(GenerationResult { + dest_metadata: Some(out_path_metadata), + dest_wasm, + dest_bundle, + optimization_result, + target_directory, + }) } /// Generate the extended contract project metadata - fn extended_metadata(&self) -> Result<(Source, Contract, Option)> { + fn extended_metadata(&self) -> Result { let contract_package = &self.crate_metadata.root_package; let ink_version = &self.crate_metadata.ink_version; let rust_version = Version::parse(&rustc_version::version()?.to_string())?; @@ -107,12 +167,28 @@ impl GenerateMetadataCommand { .transpose()?; let homepage = self.crate_metadata.homepage.clone(); let license = contract_package.license.clone(); - let hash = self.wasm_hash()?; - + let (dest_wasm, hash, optimization_result) = + if self.build_artifact != GenerateArtifacts::MetadataOnly { + let (wasm, hash, optimization) = self.wasm_hash()?; + (Some(wasm), Some(hash), Some(optimization)) + } else { + (None, None, None) + }; let source = { let lang = SourceLanguage::new(Language::Ink, ink_version.clone()); let compiler = SourceCompiler::new(Compiler::RustC, rust_version); - Source::new(hash, lang, compiler) + let maybe_wasm = if self.build_artifact == GenerateArtifacts::All { + let wasm = fs::read(&self.crate_metadata.dest_wasm)?; + // The Wasm which we read must have the same hash as `source.hash` + debug_assert!({ + let expected = blake2_hash(wasm.as_slice()); + Some(expected) == hash + }); + Some(SourceWasm::new(wasm)) + } else { + None + }; + Source::new(maybe_wasm, hash, lang, compiler) }; // Required contract fields @@ -149,52 +225,71 @@ impl GenerateMetadataCommand { // user defined metadata let user = self.crate_metadata.user.clone().map(User::new); - Ok((source, contract, user)) + Ok(ExtendedMetadataResult { + dest_wasm, + source, + contract, + user, + optimization_result, + }) } - /// Compile the contract and then hash the resulting wasm - fn wasm_hash(&self) -> Result<[u8; 32]> { - super::build::execute_with_metadata( + /// Compile the contract and then hash the resulting Wasm. + /// + /// Return a tuple of `(dest_wasm, hash, optimization_result)`. + fn wasm_hash(&self) -> Result<(PathBuf, CodeHash, OptimizationResult)> { + let (maybe_dest_wasm, maybe_optimization_res) = super::build::execute_with_crate_metadata( &self.crate_metadata, self.verbosity, + true, // for the hash we always use the optimized version of the contract + self.build_artifact, self.unstable_options.clone(), )?; let wasm = fs::read(&self.crate_metadata.dest_wasm)?; - - use ::blake2::digest::{Update as _, VariableOutput as _}; - let mut output = [0u8; 32]; - let mut blake2 = blake2::VarBlake2b::new_keyed(&[], 32); - blake2.update(wasm); - blake2.finalize_variable(|result| output.copy_from_slice(result)); - Ok(output) + let dest_wasm = maybe_dest_wasm.expect("dest wasm must exist"); + let optimization_res = maybe_optimization_res.expect("optimization result must exist"); + Ok((dest_wasm, blake2_hash(wasm.as_slice()), optimization_res)) } } +/// Returns the blake2 hash of the submitted slice. +fn blake2_hash(code: &[u8]) -> CodeHash { + let mut output = [0u8; 32]; + let mut blake2 = blake2::VarBlake2b::new_keyed(&[], 32); + blake2.update(code); + blake2.finalize_variable(|result| output.copy_from_slice(result)); + CodeHash(output) +} + /// Generates a file with metadata describing the ABI of the smart-contract. /// /// It does so by generating and invoking a temporary workspace member. pub(crate) fn execute( - manifest_path: ManifestPath, + manifest_path: &ManifestPath, verbosity: Option, + build_artifact: GenerateArtifacts, unstable_options: UnstableFlags, -) -> Result { - let crate_metadata = CrateMetadata::collect(&manifest_path)?; - GenerateMetadataCommand { +) -> Result { + let crate_metadata = CrateMetadata::collect(manifest_path)?; + let res = GenerateMetadataCommand { crate_metadata, verbosity, + build_artifact, unstable_options, } - .exec() + .exec()?; + Ok(res) } #[cfg(feature = "test-ci-only")] #[cfg(test)] mod tests { + use crate::cmd::metadata::blake2_hash; use crate::{ - cmd, crate_metadata::CrateMetadata, util::tests::with_tmp_dir, ManifestPath, UnstableFlags, + cmd, crate_metadata::CrateMetadata, util::tests::with_tmp_dir, GenerateArtifacts, + ManifestPath, UnstableFlags, }; - use blake2::digest::{Update as _, VariableOutput as _}; use contract_metadata::*; use serde_json::{Map, Value}; use std::{fmt::Write, fs}; @@ -287,21 +382,27 @@ mod tests { test_manifest.write()?; let crate_metadata = CrateMetadata::collect(&test_manifest.manifest_path)?; - let metadata_file = - cmd::metadata::execute(test_manifest.manifest_path, None, UnstableFlags::default()) - .expect("generate metadata failed"); + let dest_bundle = cmd::metadata::execute( + &test_manifest.manifest_path, + None, + GenerateArtifacts::All, + UnstableFlags::default(), + )? + .dest_bundle + .expect("bundle file not found"); let metadata_json: Map = - serde_json::from_slice(&fs::read(&metadata_file)?)?; + serde_json::from_slice(&fs::read(&dest_bundle)?)?; assert!( - metadata_file.exists(), - format!("Missing metadata file '{}'", metadata_file.display()) + dest_bundle.exists(), + format!("Missing metadata file '{}'", dest_bundle.display()) ); let source = metadata_json.get("source").expect("source not found"); let hash = source.get("hash").expect("source.hash not found"); let language = source.get("language").expect("source.language not found"); let compiler = source.get("compiler").expect("source.compiler not found"); + let wasm = source.get("wasm").expect("source.wasm not found"); let contract = metadata_json.get("contract").expect("contract not found"); let name = contract.get("name").expect("contract.name not found"); @@ -331,17 +432,10 @@ mod tests { let user = metadata_json.get("user").expect("user section not found"); // calculate wasm hash - let wasm = fs::read(&crate_metadata.dest_wasm)?; - let mut output = [0u8; 32]; - let mut blake2 = blake2::VarBlake2b::new_keyed(&[], 32); - blake2.update(wasm); - blake2.finalize_variable(|result| output.copy_from_slice(result)); - - let mut expected_hash = String::new(); - write!(expected_hash, "0x").expect("failed writing to string"); - for byte in &output { - write!(expected_hash, "{:02x}", byte).expect("failed writing to string"); - } + let fs_wasm = fs::read(&crate_metadata.dest_wasm)?; + let expected_hash = blake2_hash(&fs_wasm[..]); + let expected_wasm = build_byte_str(&fs_wasm); + let expected_language = SourceLanguage::new(Language::Ink, crate_metadata.ink_version).to_string(); let expected_rustc_version = @@ -358,7 +452,8 @@ mod tests { ), ); - assert_eq!(expected_hash, hash.as_str().unwrap()); + assert_eq!(build_byte_str(&expected_hash.0[..]), hash.as_str().unwrap()); + assert_eq!(expected_wasm, wasm.as_str().unwrap()); assert_eq!(expected_language, language.as_str().unwrap()); assert_eq!(expected_compiler, compiler.as_str().unwrap()); assert_eq!(crate_metadata.package_name, name.as_str().unwrap()); @@ -377,4 +472,13 @@ mod tests { Ok(()) }) } + + fn build_byte_str(bytes: &[u8]) -> String { + let mut str = String::new(); + write!(str, "0x").expect("failed writing to string"); + for byte in bytes { + write!(str, "{:02x}", byte).expect("failed writing to string"); + } + str + } } diff --git a/src/main.rs b/src/main.rs index d6698c2bb5de60479382266a6fa175c3c24e96e6..124cf4c7d69fadbd072ee4ca6ac727a010eee492 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,6 +151,153 @@ impl TryFrom<&UnstableOptions> for UnstableFlags { } } +/// Describes which artifacts to generate +#[derive(Copy, Clone, Eq, PartialEq, Debug, StructOpt)] +#[structopt(name = "build-artifacts")] +pub enum GenerateArtifacts { + /// Generate the Wasm, the metadata and a bundled `.contract` file + #[structopt(name = "all")] + All, + /// Only the Wasm is created, generation of metadata and a bundled `.contract` file is skipped + #[structopt(name = "code-only")] + CodeOnly, + /// Only the Wasm and the metadata are generated, no bundled `.contract` file is created + #[structopt(name = "metadata-only")] + MetadataOnly, +} + +impl GenerateArtifacts { + /// Returns the number of steps required to complete a build artifact. + /// Used as output on the cli. + pub fn steps(&self) -> usize { + match self { + GenerateArtifacts::All => 5, + GenerateArtifacts::CodeOnly => 3, + GenerateArtifacts::MetadataOnly => 1, + } + } + + pub fn display(&self, result: &GenerationResult) -> String { + if self == &GenerateArtifacts::MetadataOnly { + return format!( + "\nYour contract's metadata is ready. You can find it here:\n{}", + result + .dest_metadata + .as_ref() + .expect("metadata path must exist") + .display() + .to_string() + .bold() + ); + } + + let optimization = GenerationResult::display_optimization(result); + let size_diff = format!( + "\nOriginal wasm size: {}, Optimized: {}\n\n", + format!("{:.1}K", optimization.0).bold(), + format!("{:.1}K", optimization.1).bold(), + ); + + if self == &GenerateArtifacts::CodeOnly { + let out = format!( + "{}Your contract's code is ready. You can find it here:\n{}", + size_diff, + result + .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, + result.target_directory.display().to_string().bold(), + ); + if let Some(dest_bundle) = result.dest_bundle.as_ref() { + let bundle = format!( + " - {} (code + metadata)\n", + GenerationResult::display(&dest_bundle).bold() + ); + out.push_str(&bundle); + } + if let Some(dest_wasm) = result.dest_wasm.as_ref() { + let wasm = format!( + " - {} (the contract's code)\n", + GenerationResult::display(&dest_wasm).bold() + ); + out.push_str(&wasm); + } + if let Some(dest_metadata) = result.dest_metadata.as_ref() { + let metadata = format!( + " - {} (the contract's metadata)", + GenerationResult::display(&dest_metadata).bold() + ); + out.push_str(&metadata); + } + out + } +} + +impl std::str::FromStr for GenerateArtifacts { + type Err = String; + fn from_str(artifact: &str) -> Result { + match artifact { + "all" => Ok(GenerateArtifacts::All), + "code-only" => Ok(GenerateArtifacts::CodeOnly), + "metadata-only" => Ok(GenerateArtifacts::MetadataOnly), + _ => Err("Could not parse build artifact".to_string()), + } + } +} + +/// Result of the metadata generation process. +pub struct GenerationResult { + /// Path to the resulting metadata file. + pub dest_metadata: Option, + /// Path to the resulting Wasm file. + pub dest_wasm: Option, + /// Path to the bundled file. + pub dest_bundle: 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, +} + +/// Result of the optimization process. +pub struct OptimizationResult { + /// The original Wasm size. + pub original_size: f64, + /// The Wasm size after optimizations have been applied. + pub optimized_size: f64, +} + +impl GenerationResult { + /// Returns the base name of the path. + pub fn display(path: &PathBuf) -> &str { + path.file_name() + .expect("file name must exist") + .to_str() + .expect("must be valid utf-8") + } + + /// Returns a tuple of `(original_size, optimized_size)`. + /// + /// Panics if no optimization result is available. + pub fn display_optimization(res: &GenerationResult) -> (f64, f64) { + let optimization = res + .optimization_result + .as_ref() + .expect("optimization result must exist"); + (optimization.original_size, optimization.optimized_size) + } +} + #[derive(Debug, StructOpt)] enum Command { /// Setup and create a new smart contract project @@ -162,21 +309,42 @@ enum Command { #[structopt(short, long, parse(from_os_str))] target_dir: Option, }, - /// Compiles the smart contract + /// Compiles the contract, generates metadata, bundles both together in a `.contract` file #[structopt(name = "build")] Build { /// Path to the Cargo.toml of the contract to build #[structopt(long, parse(from_os_str))] manifest_path: Option, + /// Which build artifacts to generate. + /// + /// - `all`: Generate the Wasm, the metadata and a bundled `.contract` file. + /// The metadata file includes the Wasm hash. + /// + /// - `code-only`: Only the Wasm is created, generation of metadata and a bundled + /// `.contract` file is skipped. + /// + /// - `metadata-only`: Only the metadata iis generated, neither the bundled + /// `.contract`, nor the Wasm file are created. The resulting metadata + /// does not contain the Wasm hash. + #[structopt( + long = "generate", + default_value = "all", + value_name = "all | code-only | metadata-only", + verbatim_doc_comment + )] + build_artifact: GenerateArtifacts, #[structopt(flatten)] verbosity: VerbosityFlags, #[structopt(flatten)] unstable_options: UnstableOptions, }, - /// Generate contract metadata artifacts + /// Command has been deprecated, use `cargo contract build` instead #[structopt(name = "generate-metadata")] - GenerateMetadata { - /// Path to the Cargo.toml of the contract for which to generate metadata + GenerateMetadata {}, + /// Check that the code builds as Wasm; does not output any build artifact to the top level `target/` directory + #[structopt(name = "check")] + Check { + /// Path to the Cargo.toml of the contract to build #[structopt(long, parse(from_os_str))] manifest_path: Option, #[structopt(flatten)] @@ -193,7 +361,7 @@ enum Command { Deploy { #[structopt(flatten)] extrinsic_opts: ExtrinsicOpts, - /// Path to wasm contract code, defaults to ./target/-pruned.wasm + /// Path to wasm contract code, defaults to `./target/-pruned.wasm` #[structopt(parse(from_os_str))] wasm_path: Option, }, @@ -249,35 +417,48 @@ fn exec(cmd: Command) -> Result { Command::Build { manifest_path, verbosity, + build_artifact, unstable_options, } => { let manifest_path = ManifestPath::try_from(manifest_path.as_ref())?; - let dest_wasm = cmd::build::execute( - &manifest_path, - verbosity.try_into()?, - unstable_options.try_into()?, - )?; - Ok(format!( - "\nYour contract is ready. You can find it here:\n{}", - dest_wasm.display().to_string().bold() - )) + let result = if build_artifact == &GenerateArtifacts::MetadataOnly { + cmd::metadata::execute( + &manifest_path, + verbosity.try_into()?, + *build_artifact, + unstable_options.try_into()?, + )? + } else { + cmd::build::execute( + &manifest_path, + verbosity.try_into()?, + true, + *build_artifact, + unstable_options.try_into()?, + )? + }; + + Ok(build_artifact.display(&result)) } - Command::GenerateMetadata { + Command::Check { manifest_path, verbosity, unstable_options, } => { let manifest_path = ManifestPath::try_from(manifest_path.as_ref())?; - let metadata_file = cmd::metadata::execute( - manifest_path, + let res = cmd::build::execute( + &manifest_path, verbosity.try_into()?, + false, + GenerateArtifacts::CodeOnly, unstable_options.try_into()?, )?; - Ok(format!( - "Your metadata file is ready.\nYou can find it here:\n{}", - metadata_file.display() - )) + assert!(res.dest_wasm.is_none(), "no dest_wasm should exist"); + Ok("\nYour contract's code was built successfully.".to_string()) } + Command::GenerateMetadata {} => Err(anyhow::anyhow!( + "Command deprecated, use `cargo contract build` instead" + )), Command::Test {} => Err(anyhow::anyhow!("Command unimplemented")), #[cfg(feature = "extrinsics")] Command::Deploy {