diff --git a/Cargo.lock b/Cargo.lock index 0f1f638fbcf485b14aa90650afb06a550aa27664..8d358f37e5b232072a944fbc896cd4fc5ecb4b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,19 @@ dependencies = [ "radium", ] +[[package]] +name = "blake2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ce5b6108f8e154604bd4eb76a2f726066c3464d5a552a4229262a18c9bb471" +dependencies = [ + "byte-tools", + "byteorder", + "crypto-mac 0.8.0", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "blake2-rfc" version = "0.2.18" @@ -272,7 +285,7 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", ] [[package]] @@ -350,6 +363,7 @@ dependencies = [ "anyhow", "assert_matches", "async-std", + "blake2", "cargo-xbuild", "cargo_metadata", "colored", @@ -363,6 +377,8 @@ dependencies = [ "pretty_assertions", "pwasm-utils", "rustc_version", + "semver 0.10.0", + "serde", "serde_json", "sp-core", "structopt", @@ -401,7 +417,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46e3374c604fb39d1a2f35ed5e4a4e30e60d01fab49446e08f1b3e9a90aef202" dependencies = [ - "semver", + "semver 0.9.0", "serde", "serde_derive", "serde_json", @@ -566,10 +582,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" dependencies = [ - "generic-array", + "generic-array 0.12.3", "subtle 1.0.0", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.2", + "subtle 2.2.2", +] + [[package]] name = "ctor" version = "0.1.15" @@ -587,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" dependencies = [ "byteorder", - "digest", + "digest 0.8.1", "rand_core 0.5.1", "subtle 2.2.2", "zeroize", @@ -622,7 +648,16 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.2", ] [[package]] @@ -970,6 +1005,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1090,8 +1135,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" dependencies = [ - "crypto-mac", - "digest", + "crypto-mac 0.7.0", + "digest 0.8.1", ] [[package]] @@ -1100,8 +1145,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e570451493f10f6581b48cdd530413b63ea9e780f544bfd3bdcaa0d89d1a7b" dependencies = [ - "digest", - "generic-array", + "digest 0.8.1", + "generic-array 0.12.3", "hmac", ] @@ -1505,7 +1550,7 @@ checksum = "1fc1e2c808481a63dc6da2074752fdd4336a3c8fcc68b83db6f1fd5224ae7962" dependencies = [ "arrayref", "crunchy", - "digest", + "digest 0.8.1", "hmac-drbg", "rand 0.7.3", "sha2", @@ -1647,7 +1692,7 @@ checksum = "f75db05d738947aa5389863aadafbcf2e509d7ba099dc2ddcdf4fc66bf7a9e03" dependencies = [ "blake2b_simd", "blake2s_simd", - "digest", + "digest 0.8.1", "sha-1", "sha2", "sha3", @@ -1943,7 +1988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "006c038a43a45995a9670da19e67600114740e8511d4333bf97a56e66a7542d9" dependencies = [ "byteorder", - "crypto-mac", + "crypto-mac 0.7.0", ] [[package]] @@ -2375,7 +2420,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", ] [[package]] @@ -2486,6 +2531,16 @@ dependencies = [ "serde", ] +[[package]] +name = "semver" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" +dependencies = [ + "semver-parser", + "serde", +] + [[package]] name = "semver-parser" version = "0.7.0" @@ -2536,7 +2591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2554,7 +2609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2567,7 +2622,7 @@ checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" dependencies = [ "block-buffer", "byte-tools", - "digest", + "digest 0.8.1", "keccak", "opaque-debug", ] @@ -3531,6 +3586,7 @@ dependencies = [ "idna 0.2.0", "matches", "percent-encoding 2.1.0", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9326786460c04aeae68f801be114204984f4480a..9a676db88a5e928d05df90afb1fe7c7fb2b72628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,12 @@ colored = "1.9" toml = "0.5.4" cargo-xbuild = "0.5.32" rustc_version = "0.2.3" +blake2 = "0.9.0" +semver = { version = "0.10.0", features = ["serde"] } +serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = "1.0" tempfile = "3.1.0" +url = { version = "2.1.1", features = ["serde"] } # dependencies for optional extrinsics feature async-std = { version = "=1.5.0", optional = true } @@ -40,7 +44,6 @@ sp-core = { version = "2.0.0-rc3", optional = true } subxt = { version = "0.9.0", package = "substrate-subxt", optional = true } futures = { version = "0.3.2", optional = true } hex = { version = "0.4.0", optional = true } -url = { version = "2.1.1", optional = true } [build-dependencies] anyhow = "1.0" @@ -54,5 +57,13 @@ wabt = "0.9.2" [features] default = [] -extrinsics = ["sp-core", "subxt", "async-std", "futures", "hex", "url"] + +# Enable this for (experimental) commands to deploy, instantiate and call contracts. +# +# Disabled by default +extrinsics = ["sp-core", "subxt", "async-std", "futures", "hex"] + +# Enable this to execute long running tests, which usually are only run on the CI server +# +# Disabled by default test-ci-only = [] diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 7f7dd4839411017678d93963745b6a78407974da..7848b30131e8351deb416eb15c7672887b3c5173 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -17,79 +17,23 @@ use std::{ fs::metadata, io::{self, Write}, - path::{Path, PathBuf}, + path::PathBuf, process::Command, }; use crate::{ + crate_metadata::CrateMetadata, util, workspace::{ManifestPath, Profile, Workspace}, UnstableFlags, Verbosity, }; use anyhow::{Context, Result}; -use cargo_metadata::Package; use colored::Colorize; use parity_wasm::elements::{External, MemoryType, Module, Section}; /// This is the maximum number of pages available for a contract to allocate. const MAX_MEMORY_PAGES: u32 = 16; -/// Relevant metadata obtained from Cargo.toml. -#[derive(Debug)] -pub struct CrateMetadata { - manifest_path: ManifestPath, - cargo_meta: cargo_metadata::Metadata, - package_name: String, - root_package: Package, - original_wasm: PathBuf, - pub dest_wasm: PathBuf, -} - -impl CrateMetadata { - pub fn target_dir(&self) -> &Path { - self.cargo_meta.target_directory.as_path() - } -} - -/// Parses the contract manifest and returns relevant metadata. -pub fn collect_crate_metadata(manifest_path: &ManifestPath) -> Result { - let (metadata, root_package_id) = crate::util::get_cargo_metadata(manifest_path)?; - - // Find the root package by id in the list of packages. It is logical error if the root - // package is not found in the list. - let root_package = metadata - .packages - .iter() - .find(|package| package.id == root_package_id) - .expect("The package is not found in the `cargo metadata` output") - .clone(); - - // Normalize the package name. - let package_name = root_package.name.replace("-", "_"); - - // {target_dir}/wasm32-unknown-unknown/release/{package_name}.wasm - let mut original_wasm = metadata.target_directory.clone(); - original_wasm.push("wasm32-unknown-unknown"); - original_wasm.push("release"); - original_wasm.push(package_name.clone()); - original_wasm.set_extension("wasm"); - - // {target_dir}/{package_name}.wasm - let mut dest_wasm = metadata.target_directory.clone(); - dest_wasm.push(package_name.clone()); - dest_wasm.set_extension("wasm"); - - let crate_metadata = CrateMetadata { - manifest_path: manifest_path.clone(), - cargo_meta: metadata, - root_package: root_package.clone(), - package_name, - original_wasm, - dest_wasm, - }; - Ok(crate_metadata) -} - /// Builds the project in the specified directory, defaults to the current directory. /// /// Uses [`cargo-xbuild`](https://github.com/rust-osdev/cargo-xbuild) for maximum optimization of @@ -125,7 +69,7 @@ fn build_cargo_project( let xbuild = |manifest_path: &ManifestPath| { let manifest_path = Some(manifest_path); let target = Some("wasm32-unknown-unknown"); - let target_dir = crate_metadata.target_dir(); + let target_dir = &crate_metadata.cargo_meta.target_directory; let other_args = [ "--no-default-features", "--release", @@ -169,6 +113,9 @@ fn build_cargo_project( .using_temp(xbuild)?; } + // clear RUSTFLAGS + std::env::remove_var("RUSTFLAGS"); + Ok(()) } @@ -293,58 +240,67 @@ fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result<()> { /// Executes build of the smart-contract which produces a wasm binary that is ready for deploying. /// -/// It does so by invoking build by cargo and then post processing the final binary. -pub(crate) fn execute_build( - manifest_path: ManifestPath, +/// It does so by invoking `cargo build` and then post processing the final binary. +/// +/// # Note +/// +/// Collects the contract crate's metadata using the supplied manifest (`Cargo.toml`) path. Use +/// [`execute_build_with_metadata`] if an instance is already available. +pub(crate) fn execute( + manifest_path: &ManifestPath, verbosity: Option, unstable_options: UnstableFlags, -) -> Result { - println!( - " {} {}", - "[1/4]".bold(), - "Collecting crate metadata".bright_green().bold() - ); - let crate_metadata = collect_crate_metadata(&manifest_path)?; +) -> Result { + let crate_metadata = CrateMetadata::collect(manifest_path)?; + execute_with_metadata(&crate_metadata, verbosity, unstable_options) +} + +/// 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( + crate_metadata: &CrateMetadata, + verbosity: Option, + unstable_options: UnstableFlags, +) -> Result { println!( " {} {}", - "[2/4]".bold(), + "[1/3]".bold(), "Building cargo project".bright_green().bold() ); build_cargo_project(&crate_metadata, verbosity, unstable_options)?; println!( " {} {}", - "[3/4]".bold(), + "[2/3]".bold(), "Post processing wasm file".bright_green().bold() ); post_process_wasm(&crate_metadata)?; println!( " {} {}", - "[4/4]".bold(), + "[3/3]".bold(), "Optimizing wasm file".bright_green().bold() ); optimize_wasm(&crate_metadata)?; - - Ok(format!( - "\nYour contract is ready. You can find it here:\n{}", - crate_metadata.dest_wasm.display().to_string().bold() - )) + Ok(crate_metadata.dest_wasm.clone()) } #[cfg(feature = "test-ci-only")] #[cfg(test)] mod tests { - use crate::{ - cmd::execute_new, util::tests::with_tmp_dir, workspace::ManifestPath, UnstableFlags, - }; + use crate::{cmd, util::tests::with_tmp_dir, workspace::ManifestPath, UnstableFlags}; #[test] fn build_template() { with_tmp_dir(|path| { - execute_new("new_project", Some(path)).expect("new project creation failed"); + 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_build(manifest_path, None, UnstableFlags::default()) - .expect("build failed"); - }); + super::execute(&manifest_path, None, UnstableFlags::default()).expect("build failed"); + Ok(()) + }) } } diff --git a/src/cmd/deploy.rs b/src/cmd/deploy.rs index 3056c8d30cd748ac57f6c4a8d7741f300cfea2b4..11fbc1215184de86fd8694d492f394f18b473d2d 100644 --- a/src/cmd/deploy.rs +++ b/src/cmd/deploy.rs @@ -20,7 +20,7 @@ use anyhow::{Context, Result}; use sp_core::H256; use subxt::{contracts::*, ClientBuilder, DefaultNodeRuntime}; -use crate::{cmd::build, ExtrinsicOpts}; +use crate::{crate_metadata, ExtrinsicOpts}; /// Load the wasm blob from the specified path. /// @@ -28,7 +28,10 @@ use crate::{cmd::build, ExtrinsicOpts}; fn load_contract_code(path: Option<&PathBuf>) -> Result> { let contract_wasm_path = match path { Some(path) => path.clone(), - None => build::collect_crate_metadata(&Default::default())?.dest_wasm, + None => { + let metadata = crate_metadata::CrateMetadata::collect(&Default::default())?; + metadata.dest_wasm + } }; log::info!("Contract code path: {}", contract_wasm_path.display()); let mut data = Vec::new(); @@ -102,6 +105,7 @@ mod tests { let result = execute_deploy(&extrinsic_opts, Some(&wasm_path)); assert_matches!(result, Ok(_)); - }); + Ok(()) + }) } } diff --git a/src/cmd/instantiate.rs b/src/cmd/instantiate.rs index 20af3f628e4b00dee44f2b241518cb4a7211aee2..32577dda26150d3e44e99f77608f82c48ba5075b 100644 --- a/src/cmd/instantiate.rs +++ b/src/cmd/instantiate.rs @@ -92,6 +92,7 @@ mod tests { ); assert_matches!(result, Ok(_)); - }); + Ok(()) + }) } } diff --git a/src/cmd/metadata.rs b/src/cmd/metadata.rs deleted file mode 100644 index b1a032041c06639447a303a14c68f09d7a4217d4..0000000000000000000000000000000000000000 --- a/src/cmd/metadata.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2018-2020 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 crate::{ - util, - workspace::{ManifestPath, Workspace}, - UnstableFlags, Verbosity, -}; -use anyhow::Result; - -const METADATA_FILE: &str = "metadata.json"; - -/// 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_generate_metadata( - original_manifest_path: ManifestPath, - verbosity: Option, - unstable_options: UnstableFlags, -) -> Result { - util::assert_channel()?; - println!(" Generating metadata"); - - let (metadata, root_package_id) = crate::util::get_cargo_metadata(&original_manifest_path)?; - - let out_path = metadata.target_directory.join(METADATA_FILE); - let out_path_display = format!("{}", out_path.display()); - - let target_dir = metadata.target_directory.clone(); - - let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> { - let target_dir_arg = format!("--target-dir={}", target_dir.to_string_lossy()); - util::invoke_cargo( - "run", - &[ - "--package", - "metadata-gen", - &manifest_path.cargo_arg(), - &target_dir_arg, - "--release", - // "--no-default-features", // Breaks builds for MacOS (linker errors), we should investigate this issue asap! - ], - original_manifest_path.directory(), - verbosity, - ) - }; - - if unstable_options.original_manifest { - generate_metadata(&original_manifest_path)?; - } else { - Workspace::new(&metadata, &root_package_id)? - .with_root_package_manifest(|manifest| { - manifest - .with_added_crate_type("rlib")? - .with_profile_release_lto(false)?; - Ok(()) - })? - .with_metadata_gen_package()? - .using_temp(generate_metadata)?; - } - - Ok(format!( - "Your metadata file is ready.\nYou can find it here:\n{}", - out_path_display - )) -} - -#[cfg(feature = "test-ci-only")] -#[cfg(test)] -mod tests { - use crate::{ - cmd::{execute_generate_metadata, execute_new}, - util::tests::with_tmp_dir, - workspace::ManifestPath, - UnstableFlags, - }; - - #[test] - fn generate_metadata() { - env_logger::try_init().ok(); - with_tmp_dir(|path| { - execute_new("new_project", Some(path)).expect("new project creation failed"); - let working_dir = path.join("new_project"); - let manifest_path = ManifestPath::new(working_dir.join("Cargo.toml")).unwrap(); - let message = execute_generate_metadata(manifest_path, None, UnstableFlags::default()) - .expect("generate metadata failed"); - println!("{}", message); - - let mut metadata_file = working_dir; - metadata_file.push("target"); - metadata_file.push("metadata.json"); - assert!( - metadata_file.exists(), - format!("Missing metadata file '{}'", metadata_file.display()) - ) - }); - } -} diff --git a/src/cmd/metadata/contract.rs b/src/cmd/metadata/contract.rs new file mode 100644 index 0000000000000000000000000000000000000000..dc273db09df6f5ba5fc429b9dd2d5eeb32190052 --- /dev/null +++ b/src/cmd/metadata/contract.rs @@ -0,0 +1,241 @@ +// Copyright 2018-2020 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 core::fmt::{Display, Formatter, Result as DisplayResult, Write}; +use semver::Version; +use serde::{Serialize, Serializer}; +use serde_json::{Map, Value}; +use url::Url; + +const METADATA_VERSION: &str = "0.1.0"; + +/// An entire ink! project for metadata file generation purposes. +#[derive(Debug, Serialize)] +pub struct ContractMetadata { + metadata_version: semver::Version, + source: Source, + contract: Contract, + #[serde(skip_serializing_if = "Option::is_none")] + user: Option, + /// Raw JSON of the metadata generated by the ink! contract itself + #[serde(flatten)] + ink: Map, +} + +impl ContractMetadata { + /// Construct new contract metadata + pub fn new( + source: Source, + contract: Contract, + user: Option, + ink: Map, + ) -> Self { + let metadata_version = semver::Version::parse(METADATA_VERSION) + .expect("METADATA_VERSION is a valid semver string"); + + Self { + metadata_version, + source, + contract, + user, + ink, + } + } +} + +#[derive(Debug, Serialize)] +pub struct Source { + #[serde(serialize_with = "serialize_as_byte_str")] + hash: [u8; 32], + language: SourceLanguage, + compiler: SourceCompiler, +} + +impl Source { + /// Constructs a new InkProjectSource. + pub fn new(hash: [u8; 32], language: SourceLanguage, compiler: SourceCompiler) -> Self { + Source { + hash, + language, + compiler, + } + } +} + +/// The language and version in which a smart contract is written. +#[derive(Debug)] +pub struct SourceLanguage { + language: Language, + version: Version, +} + +impl SourceLanguage { + /// Constructs a new SourceLanguage. + pub fn new(language: Language, version: Version) -> Self { + SourceLanguage { language, version } + } +} + +impl Serialize for SourceLanguage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl Display for SourceLanguage { + fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { + write!(f, "{} {}", self.language, self.version) + } +} + +/// The language in which the smart contract is written. +#[derive(Debug)] +pub enum Language { + Ink, + Solidity, + AssemblyScript, +} + +impl Display for Language { + fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { + match self { + Self::Ink => write!(f, "ink!"), + Self::Solidity => write!(f, "Solidity"), + Self::AssemblyScript => write!(f, "AssemblyScript"), + } + } +} + +/// A compiler used to compile a smart contract. +#[derive(Debug)] +pub struct SourceCompiler { + compiler: Compiler, + version: Version, +} + +impl Display for SourceCompiler { + fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { + write!(f, "{} {}", self.compiler, self.version) + } +} + +impl Serialize for SourceCompiler { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl SourceCompiler { + pub fn new(compiler: Compiler, version: Version) -> Self { + SourceCompiler { compiler, version } + } +} + +/// Compilers used to compile a smart contract. +#[derive(Debug, Serialize)] +pub enum Compiler { + RustC, + Solang, +} + +impl Display for Compiler { + fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { + match self { + Self::RustC => write!(f, "rustc"), + Self::Solang => write!(f, "solang"), + } + } +} + +/// Metadata about a smart contract. +#[derive(Debug, Serialize)] +pub struct Contract { + name: String, + version: Version, + authors: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + documentation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + repository: Option, + #[serde(skip_serializing_if = "Option::is_none")] + homepage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + license: Option, +} + +impl Contract { + /// Constructs a new Contract. + pub fn new( + name: String, + version: Version, + authors: Vec, + description: Option, + documentation: Option, + repository: Option, + homepage: Option, + license: Option, + ) -> Self { + Contract { + name, + version, + authors, + description, + documentation, + repository, + homepage, + license, + } + } +} + +/// Additional user defined metadata, can be any valid json. +#[derive(Debug, Serialize)] +pub struct User { + #[serde(flatten)] + json: Map, +} + +impl User { + /// Constructs a new InkProjectUser + pub fn new(json: Map) -> Self { + User { json } + } +} + +/// Serializes the given bytes as byte string. +fn serialize_as_byte_str(bytes: &[u8], serializer: S) -> Result +where + S: serde::Serializer, +{ + if bytes.is_empty() { + // Return empty string without prepended `0x`. + return serializer.serialize_str(""); + } + let mut hex = String::with_capacity(bytes.len() * 2 + 2); + write!(hex, "0x").expect("failed writing to string"); + for byte in bytes { + write!(hex, "{:02x}", byte).expect("failed writing to string"); + } + serializer.serialize_str(&hex) +} diff --git a/src/cmd/metadata/mod.rs b/src/cmd/metadata/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..9a62dc0464f3357744e6d53f321ef2e85e35c038 --- /dev/null +++ b/src/cmd/metadata/mod.rs @@ -0,0 +1,366 @@ +// Copyright 2018-2020 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 . + +mod contract; + +use crate::{ + crate_metadata::CrateMetadata, + util, + workspace::{ManifestPath, Workspace}, + UnstableFlags, Verbosity, +}; +use anyhow::Result; +use contract::{ + Compiler, Contract, ContractMetadata, Language, Source, SourceCompiler, SourceLanguage, User, +}; +use semver::Version; +use std::{fs, path::PathBuf}; +use url::Url; + +const METADATA_FILE: &str = "metadata.json"; + +/// Executes the metadata generation process +struct GenerateMetadataCommand { + crate_metadata: CrateMetadata, + verbosity: Option, + unstable_options: UnstableFlags, +} + +impl GenerateMetadataCommand { + 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(); + + // build the extended contract project metadata + let (source_meta, contract_meta, user_meta) = self.extended_metadata()?; + + let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> { + let target_dir_arg = format!("--target-dir={}", target_dir.to_string_lossy()); + let stdout = util::invoke_cargo( + "run", + &[ + "--package", + "metadata-gen", + &manifest_path.cargo_arg(), + &target_dir_arg, + "--release", + ], + self.crate_metadata.manifest_path.directory(), + self.verbosity, + )?; + + 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)?; + Ok(()) + }; + + if self.unstable_options.original_manifest { + generate_metadata(&self.crate_metadata.manifest_path)?; + } else { + Workspace::new(&cargo_meta, &self.crate_metadata.root_package.id)? + .with_root_package_manifest(|manifest| { + manifest + .with_added_crate_type("rlib")? + .with_profile_release_lto(false)?; + Ok(()) + })? + .with_metadata_gen_package()? + .using_temp(generate_metadata)?; + } + + Ok(out_path) + } + + /// Generate the extended contract project metadata + fn extended_metadata(&self) -> Result<(Source, Contract, Option)> { + 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())?; + let contract_name = contract_package.name.clone(); + let contract_version = Version::parse(&contract_package.version.to_string())?; + let contract_authors = contract_package.authors.clone(); + // optional + let description = contract_package.description.clone(); + let documentation = self.crate_metadata.documentation.clone(); + let repository = contract_package + .repository + .as_ref() + .map(|repo| Url::parse(&repo)) + .transpose()?; + let homepage = self.crate_metadata.homepage.clone(); + let license = contract_package.license.clone(); + let hash = self.wasm_hash()?; + + let source = { + let lang = SourceLanguage::new(Language::Ink, ink_version.clone()); + let compiler = SourceCompiler::new(Compiler::RustC, rust_version); + Source::new(hash, lang, compiler) + }; + + // Required contract fields + let contract = Contract::new( + contract_name, + contract_version, + contract_authors, + description, + documentation, + repository, + homepage, + license, + ); + + // user defined metadata + let user = self.crate_metadata.user.clone().map(User::new); + + Ok((source, contract, user)) + } + + /// Compile the contract and then hash the resulting wasm + fn wasm_hash(&self) -> Result<[u8; 32]> { + super::build::execute_with_metadata( + &self.crate_metadata, + self.verbosity, + 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) + } +} + +/// 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, + verbosity: Option, + unstable_options: UnstableFlags, +) -> Result { + let crate_metadata = CrateMetadata::collect(&manifest_path)?; + GenerateMetadataCommand { + crate_metadata, + verbosity, + unstable_options, + } + .exec() +} + +#[cfg(feature = "test-ci-only")] +#[cfg(test)] +mod tests { + use crate::{ + cmd::{self, metadata::contract::*}, + crate_metadata::CrateMetadata, + util::tests::with_tmp_dir, + workspace::ManifestPath, + UnstableFlags, + }; + use blake2::digest::{Update as _, VariableOutput as _}; + use serde_json::{Map, Value}; + use std::{fmt::Write, fs}; + use toml::value; + + struct TestContractManifest { + toml: value::Table, + manifest_path: ManifestPath, + } + + impl TestContractManifest { + fn new(manifest_path: ManifestPath) -> anyhow::Result { + Ok(Self { + toml: toml::from_slice(&fs::read(&manifest_path)?)?, + manifest_path, + }) + } + + fn package_mut(&mut self) -> anyhow::Result<&mut value::Table> { + self.toml + .get_mut("package") + .ok_or(anyhow::anyhow!("package section not found"))? + .as_table_mut() + .ok_or(anyhow::anyhow!("package section should be a table")) + } + + /// Add a key/value to the `[package.metadata.contract.user]` section + fn add_user_metadata_value( + &mut self, + key: &'static str, + value: value::Value, + ) -> anyhow::Result<()> { + self.package_mut()? + .entry("metadata") + .or_insert(value::Value::Table(Default::default())) + .as_table_mut() + .ok_or(anyhow::anyhow!("metadata section should be a table"))? + .entry("contract") + .or_insert(value::Value::Table(Default::default())) + .as_table_mut() + .ok_or(anyhow::anyhow!( + "metadata.contract section should be a table" + ))? + .entry("user") + .or_insert(value::Value::Table(Default::default())) + .as_table_mut() + .ok_or(anyhow::anyhow!( + "metadata.contract.user section should be a table" + ))? + .insert(key.into(), value); + Ok(()) + } + + fn add_package_value( + &mut self, + key: &'static str, + value: value::Value, + ) -> anyhow::Result<()> { + self.package_mut()?.insert(key.into(), value); + Ok(()) + } + + fn write(&self) -> anyhow::Result<()> { + let toml = toml::to_string(&self.toml)?; + fs::write(&self.manifest_path, toml).map_err(Into::into) + } + } + + #[test] + fn generate_metadata() { + env_logger::try_init().ok(); + with_tmp_dir(|path| { + cmd::new::execute("new_project", Some(path)).expect("new project creation failed"); + let working_dir = path.join("new_project"); + let manifest_path = ManifestPath::new(working_dir.join("Cargo.toml"))?; + + // add optional metadata fields + let mut test_manifest = TestContractManifest::new(manifest_path)?; + test_manifest.add_package_value("description", "contract description".into())?; + test_manifest.add_package_value("documentation", "http://documentation.com".into())?; + test_manifest.add_package_value("repository", "http://repository.com".into())?; + test_manifest.add_package_value("homepage", "http://homepage.com".into())?; + test_manifest.add_package_value("license", "Apache-2.0".into())?; + test_manifest + .add_user_metadata_value("some-user-provided-field", "and-its-value".into())?; + test_manifest.add_user_metadata_value( + "more-user-provided-fields", + vec!["and", "their", "values"].into(), + )?; + 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 metadata_json: Map = + serde_json::from_slice(&fs::read(&metadata_file)?)?; + + assert!( + metadata_file.exists(), + format!("Missing metadata file '{}'", metadata_file.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 contract = metadata_json.get("contract").expect("contract not found"); + let name = contract.get("name").expect("contract.name not found"); + let version = contract.get("version").expect("contract.version not found"); + let authors = contract + .get("authors") + .expect("contract.authors not found") + .as_array() + .expect("contract.authors is an array") + .iter() + .map(|author| author.as_str().expect("author is a string")) + .collect::>(); + let description = contract + .get("description") + .expect("contract.description not found"); + let documentation = contract + .get("documentation") + .expect("contract.documentation not found"); + let repository = contract + .get("repository") + .expect("contract.repository not found"); + let homepage = contract + .get("homepage") + .expect("contract.homepage not found"); + let license = contract.get("license").expect("contract.license not found"); + + 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 expected_language = + SourceLanguage::new(Language::Ink, crate_metadata.ink_version).to_string(); + let expected_rustc_version = + semver::Version::parse(&rustc_version::version()?.to_string())?; + let expected_compiler = + SourceCompiler::new(Compiler::RustC, expected_rustc_version).to_string(); + let mut expected_user_metadata = serde_json::Map::new(); + expected_user_metadata + .insert("some-user-provided-field".into(), "and-its-value".into()); + expected_user_metadata.insert( + "more-user-provided-fields".into(), + serde_json::Value::Array( + vec!["and".into(), "their".into(), "values".into()].into(), + ), + ); + + assert_eq!(expected_hash, hash.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()); + assert_eq!( + crate_metadata.root_package.version.to_string(), + version.as_str().unwrap() + ); + assert_eq!(crate_metadata.root_package.authors, authors); + assert_eq!("contract description", description.as_str().unwrap()); + assert_eq!("http://documentation.com/", documentation.as_str().unwrap()); + assert_eq!("http://repository.com/", repository.as_str().unwrap()); + assert_eq!("http://homepage.com/", homepage.as_str().unwrap()); + assert_eq!("Apache-2.0", license.as_str().unwrap()); + assert_eq!(&expected_user_metadata, user.as_object().unwrap()); + + Ok(()) + }) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9d9352010fa7f8da0f1e689f36efb1344d3662ef..6a7191d6d10eb59dedc4eceed4b61a2124a7fc29 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -14,16 +14,13 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -mod build; +pub mod build; #[cfg(feature = "extrinsics")] mod deploy; #[cfg(feature = "extrinsics")] mod instantiate; -mod metadata; -mod new; +pub mod metadata; +pub mod new; -pub(crate) use self::{ - build::execute_build, metadata::execute_generate_metadata, new::execute_new, -}; #[cfg(feature = "extrinsics")] pub(crate) use self::{deploy::execute_deploy, instantiate::execute_instantiate}; diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 90a8de542410f0c3956f27d4fd04a224f7a4c158..39d79453029bc0ebf41f74b8a3135d3dc1c84509 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -17,18 +17,23 @@ use std::{ env, fs, io::{Cursor, Read, Seek, SeekFrom, Write}, - path::PathBuf, + path::Path, }; use anyhow::Result; use heck::CamelCase as _; -pub(crate) fn execute_new(name: &str, dir: Option<&PathBuf>) -> Result { +pub(crate) fn execute

(name: &str, dir: Option

) -> Result +where + P: AsRef, +{ if name.contains('-') { anyhow::bail!("Contract names cannot contain hyphens"); } - let out_dir = dir.unwrap_or(&env::current_dir()?).join(name); + let out_dir = dir + .map_or(env::current_dir()?, |p| p.as_ref().to_path_buf()) + .join(name); if out_dir.join("Cargo.toml").exists() { anyhow::bail!("A Cargo package already exists in {}", name); } @@ -97,32 +102,34 @@ pub(crate) fn execute_new(name: &str, dir: Option<&PathBuf>) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::{cmd::execute_new, util::tests::with_tmp_dir}; + use crate::{cmd, util::tests::with_tmp_dir}; #[test] fn rejects_hyphenated_name() { with_tmp_dir(|path| { - let result = execute_new("rejects-hyphenated-name", Some(path)); + let result = cmd::new::execute("rejects-hyphenated-name", Some(path)); assert_eq!( format!("{:?}", result), r#"Err(Contract names cannot contain hyphens)"# - ) - }); + ); + Ok(()) + }) } #[test] fn contract_cargo_project_already_exists() { with_tmp_dir(|path| { let name = "test_contract_cargo_project_already_exists"; - let _ = execute_new(name, Some(path)); - let result = execute_new(name, Some(path)); + let _ = execute(name, Some(path)); + let result = cmd::new::execute(name, Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( result.err().unwrap().to_string(), "A Cargo package already exists in test_contract_cargo_project_already_exists" ); - }); + Ok(()) + }) } #[test] @@ -132,13 +139,14 @@ mod tests { let dir = path.join(name); fs::create_dir_all(&dir).unwrap(); fs::File::create(dir.join(".gitignore")).unwrap(); - let result = execute_new(name, Some(path)); + let result = cmd::new::execute(name, Some(path)); assert!(result.is_err(), "Should fail"); assert_eq!( result.err().unwrap().to_string(), "New contract file .gitignore already exists" ); - }); + Ok(()) + }) } } diff --git a/src/crate_metadata.rs b/src/crate_metadata.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc312e3e836444b9083d60ca7a4be1c4ab912480 --- /dev/null +++ b/src/crate_metadata.rs @@ -0,0 +1,152 @@ +// Copyright 2018-2020 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 crate::workspace::ManifestPath; +use anyhow::{Context, Result}; +use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand, Package}; +use semver::Version; +use serde_json::{Map, Value}; +use std::{fs, path::PathBuf}; +use toml::value; +use url::Url; + +/// Relevant metadata obtained from Cargo.toml. +#[derive(Debug)] +pub struct CrateMetadata { + pub manifest_path: ManifestPath, + pub cargo_meta: cargo_metadata::Metadata, + pub package_name: String, + pub root_package: Package, + pub original_wasm: PathBuf, + pub dest_wasm: PathBuf, + pub ink_version: Version, + pub documentation: Option, + pub homepage: Option, + pub user: Option>, +} + +impl CrateMetadata { + /// Parses the contract manifest and returns relevant metadata. + pub fn collect(manifest_path: &ManifestPath) -> Result { + let (metadata, root_package) = get_cargo_metadata(manifest_path)?; + + // Normalize the package name. + let package_name = root_package.name.replace("-", "_"); + + // {target_dir}/wasm32-unknown-unknown/release/{package_name}.wasm + let mut original_wasm = metadata.target_directory.clone(); + original_wasm.push("wasm32-unknown-unknown"); + original_wasm.push("release"); + original_wasm.push(package_name.clone()); + original_wasm.set_extension("wasm"); + + // {target_dir}/{package_name}.wasm + let mut dest_wasm = metadata.target_directory.clone(); + dest_wasm.push(package_name.clone()); + dest_wasm.set_extension("wasm"); + + let ink_version = metadata + .packages + .iter() + .find_map(|package| { + if package.name == "ink_lang" { + Some( + Version::parse(&package.version.to_string()) + .expect("Invalid ink_lang version string"), + ) + } else { + None + } + }) + .ok_or(anyhow::anyhow!("No 'ink_lang' dependency found"))?; + + let (documentation, homepage, user) = get_cargo_toml_metadata(manifest_path)?; + + let crate_metadata = CrateMetadata { + manifest_path: manifest_path.clone(), + cargo_meta: metadata, + root_package, + package_name, + original_wasm, + dest_wasm, + ink_version, + documentation, + homepage, + user, + }; + Ok(crate_metadata) + } +} + +/// Get the result of `cargo metadata`, together with the root package id. +fn get_cargo_metadata(manifest_path: &ManifestPath) -> Result<(CargoMetadata, Package)> { + let mut cmd = MetadataCommand::new(); + let metadata = cmd + .manifest_path(manifest_path) + .exec() + .context("Error invoking `cargo metadata`")?; + let root_package_id = metadata + .resolve + .as_ref() + .and_then(|resolve| resolve.root.as_ref()) + .context("Cannot infer the root project id")? + .clone(); + // Find the root package by id in the list of packages. It is logical error if the root + // package is not found in the list. + let root_package = metadata + .packages + .iter() + .find(|package| package.id == root_package_id) + .expect("The package is not found in the `cargo metadata` output") + .clone(); + Ok((metadata, root_package)) +} + +/// Read extra metadata not available via `cargo metadata` directly from `Cargo.toml` +fn get_cargo_toml_metadata( + manifest_path: &ManifestPath, +) -> Result<(Option, Option, Option>)> { + let toml = fs::read_to_string(manifest_path)?; + let toml: value::Table = toml::from_str(&toml)?; + + let get_url = |field_name| -> Result> { + toml.get("package") + .ok_or(anyhow::anyhow!("package section not found"))? + .get(field_name) + .and_then(|v| v.as_str()) + .map(Url::parse) + .transpose() + .context(format!("{} should be a valid URL", field_name)) + .map_err(Into::into) + }; + + let documentation = get_url("documentation")?; + let homepage = get_url("homepage")?; + + let user = toml + .get("package") + .and_then(|v| v.get("metadata")) + .and_then(|v| v.get("contract")) + .and_then(|v| v.get("user")) + .and_then(|v| v.as_table()) + .map(|v| { + // convert user defined section from toml to json + serde_json::to_string(v).and_then(|json| serde_json::from_str(&json)) + }) + .transpose()?; + + Ok((documentation, homepage, user)) +} diff --git a/src/main.rs b/src/main.rs index 51d4d3be0f86eeb04bb0ed07a6993c4f9d9b2577..1e721a4ba99e522c98d42af8efefc2d0594a64f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ // along with cargo-contract. If not, see . mod cmd; +mod crate_metadata; mod util; mod workspace; @@ -98,6 +99,7 @@ struct VerbosityFlags { verbose: bool, } +#[derive(Clone, Copy)] enum Verbosity { Quiet, Verbose, @@ -123,7 +125,7 @@ struct UnstableOptions { options: Vec, } -#[derive(Default)] +#[derive(Clone, Default)] struct UnstableFlags { original_manifest: bool, } @@ -235,23 +237,36 @@ fn main() { fn exec(cmd: Command) -> Result { match &cmd { - Command::New { name, target_dir } => cmd::execute_new(name, target_dir.as_ref()), + Command::New { name, target_dir } => cmd::new::execute(name, target_dir.as_ref()), Command::Build { verbosity, unstable_options, - } => cmd::execute_build( - Default::default(), - verbosity.try_into()?, - unstable_options.try_into()?, - ), + } => { + let manifest_path = Default::default(); + 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() + )) + } Command::GenerateMetadata { verbosity, unstable_options, - } => cmd::execute_generate_metadata( - Default::default(), - verbosity.try_into()?, - unstable_options.try_into()?, - ), + } => { + let metadata_file = cmd::metadata::execute( + Default::default(), + verbosity.try_into()?, + unstable_options.try_into()?, + )?; + Ok(format!( + "Your metadata file is ready.\nYou can find it here:\n{}", + metadata_file.display() + )) + } Command::Test {} => Err(anyhow::anyhow!("Command unimplemented")), #[cfg(feature = "extrinsics")] Command::Deploy { diff --git a/src/util.rs b/src/util.rs index 246c89f5cb5346f99b1aeed3cc49fd3a40ca64bd..84b2b89488d57e4cfbb68a4e4500e7c4b2b01004 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,28 +14,11 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use crate::{workspace::ManifestPath, Verbosity}; +use crate::Verbosity; use anyhow::{Context, Result}; -use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand, PackageId}; use rustc_version::Channel; use std::{ffi::OsStr, path::Path, process::Command}; -/// Get the result of `cargo metadata`, together with the root package id. -pub fn get_cargo_metadata(manifest_path: &ManifestPath) -> Result<(CargoMetadata, PackageId)> { - let mut cmd = MetadataCommand::new(); - let metadata = cmd - .manifest_path(manifest_path) - .exec() - .context("Error invoking `cargo metadata`")?; - let root_package_id = metadata - .resolve - .as_ref() - .and_then(|resolve| resolve.root.as_ref()) - .context("Cannot infer the root project id")? - .clone(); - Ok((metadata, root_package_id)) -} - /// Check whether the current rust channel is valid: `nightly` is recommended. pub fn assert_channel() -> Result<()> { let meta = rustc_version::version_meta()?; @@ -53,12 +36,14 @@ pub fn assert_channel() -> Result<()> { } /// Run cargo with the supplied args +/// +/// If successful, returns the stdout bytes pub(crate) fn invoke_cargo( command: &str, args: I, working_dir: Option

, verbosity: Option, -) -> Result<()> +) -> Result> where I: IntoIterator + std::fmt::Debug, S: AsRef, @@ -79,25 +64,40 @@ where None => &mut cmd, }; - let status = cmd - .status() + log::info!("invoking cargo: {:?}", cmd); + + let child = cmd + // capture the stdout to return from this function as bytes + .stdout(std::process::Stdio::piped()) + .spawn() .context(format!("Error executing `{:?}`", cmd))?; + let output = child.wait_with_output()?; - if status.success() { - Ok(()) + if output.status.success() { + Ok(output.stdout) } else { - anyhow::bail!("`{:?}` failed with exit code: {:?}", cmd, status.code()); + anyhow::bail!( + "`{:?}` failed with exit code: {:?}", + cmd, + output.status.code() + ); } } #[cfg(test)] pub mod tests { - use std::path::PathBuf; - use tempfile::TempDir; + use std::path::Path; - pub fn with_tmp_dir(f: F) { - let tmp_dir = TempDir::new().expect("temporary directory creation failed"); + pub fn with_tmp_dir(f: F) + where + F: FnOnce(&Path) -> anyhow::Result<()>, + { + let tmp_dir = tempfile::Builder::new() + .prefix("cargo-contract.test.") + .tempdir() + .expect("temporary directory creation failed"); - f(&tmp_dir.into_path()); + // catch test panics in order to clean up temp dir which will be very large + f(tmp_dir.path()).expect("Error executing test with tmp dir") } } diff --git a/src/workspace/manifest.rs b/src/workspace/manifest.rs index 7b8507542905de5a4d062e821ab112734101d4b2..b96a8b67c9d6a02597ef0c134e92a5b5799739ff 100644 --- a/src/workspace/manifest.rs +++ b/src/workspace/manifest.rs @@ -361,20 +361,16 @@ impl Manifest { .as_str() .ok_or(anyhow::anyhow!("[lib] name should be a string"))?; - let get_dependency = |name| -> Result<&value::Table> { - self.toml - .get("dependencies") - .ok_or(anyhow::anyhow!("[dependencies] section not found"))? - .get(name) - .ok_or(anyhow::anyhow!("{} dependency not found", name))? - .as_table() - .ok_or(anyhow::anyhow!("{} dependency should be a table", name)) - }; - - let ink_lang = get_dependency("ink_lang")?; - let ink_metadata = get_dependency("ink_metadata")?; - - metadata::generate_package(dir, name, ink_lang.clone(), ink_metadata.clone())?; + let ink_metadata = self + .toml + .get("dependencies") + .ok_or(anyhow::anyhow!("[dependencies] section not found"))? + .get("ink_metadata") + .ok_or(anyhow::anyhow!("{} dependency not found", name))? + .as_table() + .ok_or(anyhow::anyhow!("{} dependency should be a table", name))?; + + metadata::generate_package(dir, name, ink_metadata.clone())?; } let updated_toml = toml::to_string(&self.toml)?; diff --git a/src/workspace/metadata.rs b/src/workspace/metadata.rs index 2a6d69b7cc9de3994e20756657e1123a9e6f8b2f..0f82bf3ad574806ed99d0585ad66ab583cfb672e 100644 --- a/src/workspace/metadata.rs +++ b/src/workspace/metadata.rs @@ -18,7 +18,8 @@ use anyhow::Result; use std::{fs, path::Path}; use toml::value; -/// Generates a cargo workspace package which will be invoked to generate contract metadata. +/// Generates a cargo workspace package `metadata-gen` which will be invoked via `cargo run` to +/// generate contract metadata. /// /// # Note /// @@ -27,7 +28,6 @@ use toml::value; pub(super) fn generate_package>( target_dir: P, contract_package_name: &str, - ink_lang_dependency: value::Table, mut ink_metadata_dependency: value::Table, ) -> Result<()> { let dir = target_dir.as_ref(); @@ -61,7 +61,6 @@ pub(super) fn generate_package>( ink_metadata_dependency.remove("optional"); // add ink dependencies copied from contract manifest - deps.insert("ink_lang".into(), ink_lang_dependency.into()); deps.insert("ink_metadata".into(), ink_metadata_dependency.into()); let cargo_toml = toml::to_string(&cargo_toml)?; diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 8df3d14bf7f41bc0cb7a8d8c8bd2a19424f6ed5d..6489a126eadc5c2e7f8f0fbcf167f7c9166bf08e 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -162,7 +162,7 @@ impl Workspace { F: FnOnce(&ManifestPath) -> Result<()>, { let tmp_dir = tempfile::Builder::new() - .prefix(".cargo-contract_") + .prefix("cargo-contract_") .tempdir()?; log::debug!("Using temp workspace at '{}'", tmp_dir.path().display()); let new_paths = self.write(&tmp_dir)?; diff --git a/templates/new/_Cargo.toml b/templates/new/_Cargo.toml index 451c54f1a64bbbf1202a7fdb1ae1a394800c3d0f..df71d79d9b837ca0e3dbee7bee044c1b06ff6d40 100644 --- a/templates/new/_Cargo.toml +++ b/templates/new/_Cargo.toml @@ -11,7 +11,7 @@ ink_core = { git = "https://github.com/paritytech/ink", branch = "master", packa ink_lang = { git = "https://github.com/paritytech/ink", branch = "master", package = "ink_lang", default-features = false } scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } -scale-info = { version = "0.2", default-features = false, features = ["derive"], optional = true } +scale-info = { version = "0.3", default-features = false, features = ["derive"], optional = true } [lib] name = "{{name}}" diff --git a/templates/tools/generate-metadata/main.rs b/templates/tools/generate-metadata/main.rs index a8ecb518a3574ad86bf607de8a329975b4f9a98b..66ecdde260fdbe2b71457e3f7dd0f0d5e153a097 100644 --- a/templates/tools/generate-metadata/main.rs +++ b/templates/tools/generate-metadata/main.rs @@ -5,9 +5,8 @@ extern "Rust" { } fn main() -> Result<(), std::io::Error> { - let ink_project = unsafe { __ink_generate_metadata() }; - let contents = serde_json::to_string_pretty(&ink_project)?; - std::fs::create_dir("target").ok(); - std::fs::write("target/metadata.json", contents)?; + let metadata = unsafe { __ink_generate_metadata() }; + let contents = serde_json::to_string_pretty(&metadata)?; + print!("{}", contents); Ok(()) }