+
+> `cargo-contract` is a CLI tool which helps you develop smart contracts in Parity's ink!. ink! is a Rust [eDSL](https://wiki.haskell.org/Embedded_domain_specific_language) which allows you to write smart contracts for blockchains built on the [Substrate](https://github.com/paritytech/substrate) framework.
+
+
+More relevant links:
+* Talk to us on [Element][b2] or [Discord][c2]
+* [`ink!`](https://github.com/paritytech/ink) ‒ The main ink! repository with smart contract examples
+* [Canvas UI](https://paritytech.github.io/canvas-ui/#/upload) ‒ Frontend for contract deployment and interaction
+* [Substrate Contracts Node](https://github.com/paritytech/substrate-contracts-node) ‒ Simple Substrate blockchain which includes smart contract functionality
+
+<<<<<<< HEAD
A CLI tool for helping setting up and managing WebAssembly smart contracts in !ink, Solidity (_not yet! WIP._) and WebAssembly text format. Supports t3rn composable contracts.
**This is a fork of [`cargo-contracts`](https://github.com/paritytech/cargo-contracts). The fork extends the smart contract languages with Solidity and WASM text format. It also adds the features of composable contract builds, deployment and execution via t3rn gateways.**
+=======
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
## Installation
-- **Prerequisites**
+* Step 1: `rustup component add rust-src`.
- - **rust-src**: `rustup component add rust-src`
- - **wasm-opt**: https://github.com/WebAssembly/binaryen#tools
+* Step 2: Install `binaryen` in a version >= 99:
+<<<<<<< HEAD
- **Install from source**
- `cargo build --features extrinsics`
- **Install from remote repo**
@@ -22,10 +59,37 @@ A CLI tool for helping setting up and managing WebAssembly smart contracts in !i
**You can now use the compiler as a command line tool: `cargo t3rn-contract`**
+=======
+ * [Debian/Ubuntu](https://tracker.debian.org/pkg/binaryen): `apt-get install binaryen`
+ * [Homebrew](https://formulae.brew.sh/formula/binaryen): `brew install binaryen`
+ * [Arch Linux](https://archlinux.org/packages/community/x86_64/binaryen/): `pacman -S binaryen`
+ * Windows: [binary releases are available](https://github.com/WebAssembly/binaryen/releases)
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
-## Usage
+ There's only an old version in your distributions package manager? Just use a
+ [binary release](https://github.com/WebAssembly/binaryen/releases).
+
+* Step 3: `cargo install --force cargo-contract`
+
+### Installation using Docker Image
+
+If you prefer to use Docker instead we have a Docker image
+[available on the Docker Hub](https://hub.docker.com/r/paritytech/contracts-ci-linux):
+
+```bash
+# Pull the latest stable image.
+docker pull paritytech/contracts-ci-linux:production
+# Create a new contract in your current directory.
+docker run --rm -it -v $(pwd):/sources paritytech/contracts-ci-linux:production \
+ cargo +nightly contract new --target-dir /sources my_contract
+
+# Build the contract. This will create the contract file under
+# `my_contract/target/ink/my_contract.contract`.
+docker run --rm -it -v $(pwd):/sources paritytech/contracts-ci-linux:production \
+ cargo +nightly contract build --manifest-path=/sources/my_contract/Cargo.toml
```
+<<<<<<< HEAD
cargo-t3rn-contract 0.3.0
Utilities to develop Wasm smart contracts.
@@ -52,26 +116,62 @@ SUBCOMMANDS:
instantiate Instantiate a deployed smart contract
help Prints this message or the help of the given subcommand(s)
```
+=======
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
+
+If you want to reproduce other steps of CI process you can use the following
+[guide](https://github.com/paritytech/scripts#reproduce-ci-locally).
-## `build` requires the `nightly` toolchain
+## Usage
-`cargo contract build` must be run using the `nightly` toolchain. If you have
-[`rustup`](https://github.com/rust-lang/rustup) installed, the simplest way to do so is `cargo +nightly contract build`.
-To avoid having to add `+nightly` you can also create a `rust-toolchain` file in your local directory containing
-`nightly`. Read more about how to [specify the rustup toolchain](https://github.com/rust-lang/rustup#override-precedence).
+You can always use `cargo contract help` to print information on available
+commands and their usage.
-## Features
+For each command there is also a `--help` flag with info on additional parameters,
+e.g. `cargo contract new --help`.
-The `deploy` and `instantiate` subcommands are **disabled by default**, since they are not fully stable yet and increase the build time.
+##### `cargo contract new my_contract`
-If you want to try them, you need to enable the `extrinsics` feature:
+Creates an initial smart contract with some scaffolding code into a new
+folder `my_contract` .
+<<<<<<< HEAD
`cargo install --git https://github.com/MaciejBaj/cargo-contract cargo-t3rn-contract --features extrinsics --force`
+=======
+The contract contains the source code for the [`Flipper`](https://github.com/paritytech/ink/blob/master/examples/flipper/lib.rs)
+contract, which is about the simplest "smart" contract you can build ‒ a `bool` which gets flipped
+from `true` to `false` through the `flip()` function.
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
-Once they are stable and the compilation time is acceptable, we will consider removing the `extrinsics` feature.
+##### `cargo +nightly contract build`
-## License
+Compiles the contract into optimized WebAssembly bytecode, generates metadata for it,
+and bundles both together in a `.contract` file, which you can use for
+deploying the contract on-chain.
+
+`cargo contract build` must be run using the `nightly` toolchain. If you have
+[`rustup`](https://github.com/rust-lang/rustup) installed, the simplest way to
+do so is `cargo +nightly contract build`.
+<<<<<<< HEAD
The entire code within this repository is licensed under the [GPLv3](LICENSE). Please [contact Parity](https://www.parity.io/contact/) if you have questions about the licensing of this product.
+=======
+To avoid having to always add `+nightly` you can also set `nightly` as the default
+toolchain of a directory by executing `rustup override set nightly` in it.
+
+##### `cargo contract check`
+
+Checks that the code builds as WebAssembly. This command does not output any `.contract`
+artifact to the `target/` directory.
+
+##### `cargo contract test`
+
+Runs test suites defined for a smart contract off-chain.
+
+## License
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
+The entire code within this repository is licensed under the [GPLv3](LICENSE).
+Please [contact us](https://www.parity.io/contact/) if you have questions about
+the licensing of our products.
diff --git a/build.rs b/build.rs
index 2687128ee..dc79aa3f4 100644
--- a/build.rs
+++ b/build.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -15,18 +15,23 @@
// along with cargo-contract. If not, see .
use std::{
+ borrow::Cow,
env,
ffi::OsStr,
fs::File,
io::{prelude::*, Write},
iter::Iterator,
- path::PathBuf,
+ path::{Path, PathBuf},
+ process::Command,
};
use anyhow::Result;
use walkdir::WalkDir;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
+use platforms::{TARGET_ARCH, TARGET_ENV, TARGET_OS};
+use substrate_build_script_utils::rerun_if_git_head_changed;
+
const DEFAULT_UNIX_PERMISSIONS: u32 = 0o755;
fn main() {
@@ -46,6 +51,9 @@ fn main() {
dst_file.display()
);
+ generate_cargo_keys();
+ rerun_if_git_head_changed();
+
std::process::exit(
match zip_dir(&template_dir, &dst_file, CompressionMethod::Stored) {
Ok(_) => {
@@ -64,7 +72,7 @@ fn main() {
);
}
-fn zip_dir(src_dir: &PathBuf, dst_file: &PathBuf, method: CompressionMethod) -> Result<()> {
+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());
}
@@ -92,18 +100,71 @@ fn zip_dir(src_dir: &PathBuf, dst_file: &PathBuf, method: CompressionMethod) ->
name.set_file_name("Cargo.toml");
}
+ let file_path = name.as_os_str().to_string_lossy();
+
if path.is_file() {
- zip.start_file_from_path(name.as_path(), options)?;
+ 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().len() != 0 {
- zip.add_directory_from_path(name.as_path(), options)?;
+ } else if !name.as_os_str().is_empty() {
+ zip.add_directory(file_path, options)?;
}
}
zip.finish()?;
Ok(())
}
+
+/// Generate the `cargo:` key output
+fn generate_cargo_keys() {
+ let output = Command::new("git")
+ .args(&["rev-parse", "--short", "HEAD"])
+ .output();
+
+ let commit = match output {
+ Ok(o) if o.status.success() => {
+ let sha = String::from_utf8_lossy(&o.stdout).trim().to_owned();
+ Cow::from(sha)
+ }
+ Ok(o) => {
+ println!("cargo:warning=Git command failed with status: {}", o.status);
+ Cow::from("unknown")
+ }
+ Err(err) => {
+ println!("cargo:warning=Failed to execute git command: {}", err);
+ Cow::from("unknown")
+ }
+ };
+
+ println!(
+ "cargo:rustc-env=CARGO_CONTRACT_CLI_IMPL_VERSION={}",
+ get_version(&commit)
+ )
+}
+
+fn get_version(impl_commit: &str) -> String {
+ let commit_dash = if impl_commit.is_empty() { "" } else { "-" };
+
+ format!(
+ "{}{}{}-{}",
+ std::env::var("CARGO_PKG_VERSION").unwrap_or_default(),
+ commit_dash,
+ impl_commit,
+ get_platform(),
+ )
+}
+
+fn get_platform() -> String {
+ let env_dash = if TARGET_ENV.is_some() { "-" } else { "" };
+
+ format!(
+ "{}-{}{}{}",
+ TARGET_ARCH.as_str(),
+ TARGET_OS.as_str(),
+ env_dash,
+ TARGET_ENV.map(|x| x.as_str()).unwrap_or(""),
+ )
+}
diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml
new file mode 100644
index 000000000..4fa06dcd6
--- /dev/null
+++ b/metadata/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "contract-metadata"
+version = "0.3.0"
+authors = ["Parity Technologies "]
+edition = "2018"
+
+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 defining metadata for smart contracts on substrate"
+keywords = ["parity", "blockchain"]
+include = ["Cargo.toml", "*.rs", "LICENSE"]
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+semver = { version = "1.0.4", features = ["serde"] }
+serde = { version = "1.0.130", default-features = false, features = ["derive"] }
+serde_json = "1.0.68"
+url = { version = "2.2.2", features = ["serde"] }
+
+[dev-dependencies]
+pretty_assertions = "1.0.0"
diff --git a/metadata/LICENSE b/metadata/LICENSE
new file mode 100644
index 000000000..6b0b1270f
--- /dev/null
+++ b/metadata/LICENSE
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/metadata/README.md b/metadata/README.md
new file mode 100644
index 000000000..57a396c2a
--- /dev/null
+++ b/metadata/README.md
@@ -0,0 +1,7 @@
+# Contract Metadata
+
+Defines types for the extended metadata of smart contracts targeting [substrate](https://github.com/paritytech/substrate).
+
+Currently part of [`cargo-contract`](https://github.com/paritytech/cargo-contract), the build tool for smart
+ contracts written in [ink!](https://github.com/paritytech/ink).
+
diff --git a/metadata/lib.rs b/metadata/lib.rs
new file mode 100644
index 000000000..90eebbf13
--- /dev/null
+++ b/metadata/lib.rs
@@ -0,0 +1,651 @@
+// Copyright 2018-2021 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 .
+
+//! Type definitions for creating and serializing metadata for smart contracts targeting
+//! Substrate's contracts pallet.
+//!
+//! # Example
+//!
+//! ```
+//! # use contract_metadata::*;
+//! # use semver::Version;
+//! # use url::Url;
+//! # use serde_json::{Map, Value};
+//!
+//! 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 wasm = SourceWasm::new(vec![0u8]);
+//! let source = Source::new(Some(wasm), 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())
+//! .license("Apache-2.0".to_string())
+//! .build()
+//! .unwrap();
+//! // user defined raw json
+//! let user_json: Map = Map::new();
+//! let user = User::new(user_json);
+//! // contract abi raw json generated by contract compilation
+//! let abi_json: Map = Map::new();
+//!
+//! let metadata = ContractMetadata::new(source, contract, Some(user), abi_json);
+//!
+//! // serialize to json
+//! let json = serde_json::to_value(&metadata).unwrap();
+//! ```
+
+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";
+
+/// Smart contract metadata.
+#[derive(Clone, Debug, Serialize)]
+pub struct ContractMetadata {
+ #[serde(rename = "metadataVersion")]
+ metadata_version: semver::Version,
+ source: Source,
+ contract: Contract,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ user: Option,
+ /// Raw JSON of the contract abi metadata, generated during contract compilation.
+ #[serde(flatten)]
+ abi: Map,
+}
+
+impl ContractMetadata {
+ /// Construct new contract metadata.
+ pub fn new(
+ source: Source,
+ contract: Contract,
+ user: Option,
+ abi: Map,
+ ) -> Self {
+ let metadata_version = semver::Version::parse(METADATA_VERSION)
+ .expect("METADATA_VERSION is a valid semver string");
+
+ Self {
+ metadata_version,
+ source,
+ contract,
+ user,
+ 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(Clone, Debug, Serialize)]
+pub struct Source {
+ hash: CodeHash,
+ language: SourceLanguage,
+ compiler: SourceCompiler,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ wasm: Option,
+}
+
+impl Source {
+ /// Constructs a new InkProjectSource.
+ pub fn new(
+ wasm: Option,
+ hash: CodeHash,
+ 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(Clone, 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(Clone, 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(Clone, 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(Clone, 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(Clone, 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 {
+ pub fn builder() -> ContractBuilder {
+ ContractBuilder::default()
+ }
+}
+
+/// Additional user defined metadata, can be any valid json.
+#[derive(Clone, Debug, Serialize)]
+pub struct User {
+ #[serde(flatten)]
+ json: Map,
+}
+
+impl User {
+ /// Constructs new user metadata.
+ pub fn new(json: Map) -> Self {
+ User { json }
+ }
+}
+
+/// Builder for contract metadata
+#[derive(Default)]
+pub struct ContractBuilder {
+ name: Option,
+ version: Option,
+ authors: Option>,
+ description: Option,
+ documentation: Option,
+ repository: Option,
+ homepage: Option,
+ license: Option,
+}
+
+impl ContractBuilder {
+ /// Set the contract name (required)
+ pub fn name(&mut self, name: S) -> &mut Self
+ where
+ S: AsRef,
+ {
+ if self.name.is_some() {
+ panic!("name has already been set")
+ }
+ self.name = Some(name.as_ref().to_string());
+ self
+ }
+
+ /// Set the contract version (required)
+ pub fn version(&mut self, version: Version) -> &mut Self {
+ if self.version.is_some() {
+ panic!("version has already been set")
+ }
+ self.version = Some(version);
+ self
+ }
+
+ /// Set the contract version (required)
+ pub fn authors(&mut self, authors: I) -> &mut Self
+ where
+ I: IntoIterator,
+ S: AsRef,
+ {
+ if self.authors.is_some() {
+ panic!("authors has already been set")
+ }
+
+ let authors = authors
+ .into_iter()
+ .map(|s| s.as_ref().to_string())
+ .collect::>();
+
+ if authors.is_empty() {
+ panic!("must have at least one author")
+ }
+
+ self.authors = Some(authors);
+ self
+ }
+
+ /// Set the contract description (optional)
+ pub fn description(&mut self, description: S) -> &mut Self
+ where
+ S: AsRef,
+ {
+ if self.description.is_some() {
+ panic!("description has already been set")
+ }
+ self.description = Some(description.as_ref().to_string());
+ self
+ }
+
+ /// Set the contract documentation url (optional)
+ pub fn documentation(&mut self, documentation: Url) -> &mut Self {
+ if self.documentation.is_some() {
+ panic!("documentation is already set")
+ }
+ self.documentation = Some(documentation);
+ self
+ }
+
+ /// Set the contract repository url (optional)
+ pub fn repository(&mut self, repository: Url) -> &mut Self {
+ if self.repository.is_some() {
+ panic!("repository is already set")
+ }
+ self.repository = Some(repository);
+ self
+ }
+
+ /// Set the contract homepage url (optional)
+ pub fn homepage(&mut self, homepage: Url) -> &mut Self {
+ if self.homepage.is_some() {
+ panic!("homepage is already set")
+ }
+ self.homepage = Some(homepage);
+ self
+ }
+
+ /// Set the contract license (optional)
+ pub fn license(&mut self, license: S) -> &mut Self
+ where
+ S: AsRef,
+ {
+ if self.license.is_some() {
+ panic!("license has already been set")
+ }
+ self.license = Some(license.as_ref().to_string());
+ self
+ }
+
+ /// Finalize construction of the [`ContractMetadata`].
+ ///
+ /// Returns an `Err` if any required fields missing.
+ pub fn build(&self) -> Result {
+ let mut required = Vec::new();
+
+ if let (Some(name), Some(version), Some(authors)) =
+ (&self.name, &self.version, &self.authors)
+ {
+ Ok(Contract {
+ name: name.to_string(),
+ version: version.clone(),
+ authors: authors.to_vec(),
+ description: self.description.clone(),
+ documentation: self.documentation.clone(),
+ repository: self.repository.clone(),
+ homepage: self.homepage.clone(),
+ license: self.license.clone(),
+ })
+ } else {
+ if self.name.is_none() {
+ required.push("name");
+ }
+ if self.version.is_none() {
+ required.push("version")
+ }
+ if self.authors.is_none() {
+ required.push("authors")
+ }
+ Err(format!(
+ "Missing required non-default fields: {}",
+ required.join(", ")
+ ))
+ }
+ }
+}
+
+/// 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)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use pretty_assertions::assert_eq;
+ use serde_json::json;
+
+ #[test]
+ fn builder_fails_with_missing_required_fields() {
+ let missing_name = Contract::builder()
+ // .name("incrementer".to_string())
+ .version(Version::new(2, 1, 0))
+ .authors(vec!["Parity Technologies ".to_string()])
+ .build();
+
+ assert_eq!(
+ missing_name.unwrap_err(),
+ "Missing required non-default fields: name"
+ );
+
+ let missing_version = Contract::builder()
+ .name("incrementer".to_string())
+ // .version(Version::new(2, 1, 0))
+ .authors(vec!["Parity Technologies ".to_string()])
+ .build();
+
+ assert_eq!(
+ missing_version.unwrap_err(),
+ "Missing required non-default fields: version"
+ );
+
+ let missing_authors = Contract::builder()
+ .name("incrementer".to_string())
+ .version(Version::new(2, 1, 0))
+ // .authors(vec!["Parity Technologies ".to_string()])
+ .build();
+
+ assert_eq!(
+ missing_authors.unwrap_err(),
+ "Missing required non-default fields: authors"
+ );
+
+ let missing_all = Contract::builder()
+ // .name("incrementer".to_string())
+ // .version(Version::new(2, 1, 0))
+ // .authors(vec!["Parity Technologies ".to_string()])
+ .build();
+
+ assert_eq!(
+ missing_all.unwrap_err(),
+ "Missing required non-default fields: name, version, authors"
+ );
+ }
+
+ #[test]
+ fn json_with_optional_fields() {
+ 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 wasm = SourceWasm::new(vec![0u8, 1u8, 2u8]);
+ let source = Source::new(Some(wasm), 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())
+ .license("Apache-2.0".to_string())
+ .build()
+ .unwrap();
+
+ let user_json = json! {
+ {
+ "more-user-provided-fields": [
+ "and",
+ "their",
+ "values"
+ ],
+ "some-user-provided-field": "and-its-value"
+ }
+ };
+ let user = User::new(user_json.as_object().unwrap().clone());
+ let abi_json = json! {
+ {
+ "spec": {},
+ "storage": {},
+ "types": []
+ }
+ }
+ .as_object()
+ .unwrap()
+ .clone();
+
+ let metadata = ContractMetadata::new(source, contract, Some(user), abi_json);
+ let json = serde_json::to_value(&metadata).unwrap();
+
+ let expected = json! {
+ {
+ "metadataVersion": "0.1.0",
+ "source": {
+ "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "language": "ink! 2.1.0",
+ "compiler": "rustc 1.46.0-nightly",
+ "wasm": "0x000102"
+ },
+ "contract": {
+ "name": "incrementer",
+ "version": "2.1.0",
+ "authors": [
+ "Parity Technologies "
+ ],
+ "description": "increment a value",
+ "documentation": "http://docs.rs/",
+ "repository": "http://github.com/paritytech/ink/",
+ "homepage": "http://example.com/",
+ "license": "Apache-2.0",
+ },
+ "user": {
+ "more-user-provided-fields": [
+ "and",
+ "their",
+ "values"
+ ],
+ "some-user-provided-field": "and-its-value"
+ },
+ // these fields are part of the flattened raw json for the contract ABI
+ "spec": {},
+ "storage": {},
+ "types": []
+ }
+ };
+
+ assert_eq!(json, expected);
+ }
+
+ #[test]
+ fn json_excludes_optional_fields() {
+ 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(None, 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()])
+ .build()
+ .unwrap();
+ let abi_json = json! {
+ {
+ "spec": {},
+ "storage": {},
+ "types": []
+ }
+ }
+ .as_object()
+ .unwrap()
+ .clone();
+
+ let metadata = ContractMetadata::new(source, contract, None, abi_json);
+ let json = serde_json::to_value(&metadata).unwrap();
+
+ let expected = json! {
+ {
+ "metadataVersion": "0.1.0",
+ "source": {
+ "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "language": "ink! 2.1.0",
+ "compiler": "rustc 1.46.0-nightly"
+ },
+ "contract": {
+ "name": "incrementer",
+ "version": "2.1.0",
+ "authors": [
+ "Parity Technologies "
+ ],
+ },
+ // these fields are part of the flattened raw json for the contract ABI
+ "spec": {},
+ "storage": {},
+ "types": []
+ }
+ };
+
+ assert_eq!(json, expected);
+ }
+}
diff --git a/scripts/pre_cache.sh b/scripts/pre_cache.sh
new file mode 100755
index 000000000..d295f4031
--- /dev/null
+++ b/scripts/pre_cache.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+set -u
+
+# if there is no directory for this $CI_COMMIT_REF_NAME/$CI_JOB_NAME
+# create such directory and
+# copy recursively all the files from the newest dir which has $CI_JOB_NAME, if it exists
+
+# caches are in /ci-cache/${CI_PROJECT_NAME}/${2}/${CI_COMMIT_REF_NAME}/${CI_JOB_NAME}
+
+function prepopulate {
+ if [[ ! -d $1 ]]; then
+ mkdir -p "$1";
+ FRESH_CACHE=$(find "/ci-cache/$CI_PROJECT_NAME/$2" -mindepth 2 -maxdepth 2 \
+ -type d -name "$CI_JOB_NAME" -not -path "$1" -exec stat --printf="%Y\t%n\n" {} \; \
+ |sort -n -r |head -1 |cut -f2);
+ if [[ -d "$FRESH_CACHE" ]]; then
+ echo "____Using" "$FRESH_CACHE" "to prepopulate the cache____";
+ time cp -rf "$FRESH_CACHE" "$1";
+ else
+ echo "_____No such $2 dir, proceeding from scratch_____";
+ fi
+ else
+ echo "____No need to prepopulate $2 cache____";
+ fi
+}
+
+# CARGO_HOME cache was moved to the same "project/cache_type/branch_name/job_name" level as
+# CARGO_TARGET_DIR because of frequent weird data-race issues. This just means that the same cache that
+# would have been used for the entire pipeline will be duplicated for the each job.
+prepopulate "$CARGO_HOME" cargo
+prepopulate "$CARGO_TARGET_DIR" targets
diff --git a/src/cmd/build.rs b/src/cmd/build.rs
index 58b4e00a9..676a95d98 100644
--- a/src/cmd/build.rs
+++ b/src/cmd/build.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -14,30 +14,206 @@
// You should have received a copy of the GNU General Public License
// along with cargo-contract. If not, see .
-use std::{
- fs::metadata,
- io::{self, Write},
- path::PathBuf,
- process::Command,
-};
-
use crate::{
crate_metadata::CrateMetadata,
- util,
- workspace::{ManifestPath, Profile, Workspace},
- UnstableFlags, Verbosity,
+ maybe_println, util, validate_wasm,
+ workspace::{Manifest, ManifestPath, Profile, Workspace},
+ BuildArtifacts, BuildMode, BuildResult, OptimizationPasses, OptimizationResult, OutputType,
+ UnstableFlags, UnstableOptions, Verbosity, VerbosityFlags,
};
use anyhow::{Context, Result};
use colored::Colorize;
-use parity_wasm::elements::{External, MemoryType, Module, Section};
+use parity_wasm::elements::{External, Internal, MemoryType, Module, Section};
+use regex::Regex;
+use semver::Version;
+use std::{
+ convert::TryFrom,
+ ffi::OsStr,
+ fs::metadata,
+ path::{Path, PathBuf},
+ process::Command,
+ str,
+};
+use structopt::StructOpt;
/// This is the maximum number of pages available for a contract to allocate.
const MAX_MEMORY_PAGES: u32 = 16;
-/// Builds the project in the specified directory, defaults to the current directory.
+/// Arguments to use when executing `build` or `check` commands.
+#[derive(Default)]
+pub(crate) struct ExecuteArgs {
+ /// The location of the Cargo manifest (`Cargo.toml`) file to use.
+ pub(crate) manifest_path: ManifestPath,
+ verbosity: Verbosity,
+ build_mode: BuildMode,
+ build_artifact: BuildArtifacts,
+ unstable_flags: UnstableFlags,
+ optimization_passes: OptimizationPasses,
+ keep_debug_symbols: bool,
+ output_type: OutputType,
+}
+
+/// 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, StructOpt)]
+#[structopt(name = "build")]
+pub struct BuildCommand {
+ /// Path to the Cargo.toml of the contract to build
+ #[structopt(long, parse(from_os_str))]
+ 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.
+ #[structopt(long = "--release")]
+ build_release: 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.
+ #[structopt(
+ long = "generate",
+ default_value = "all",
+ value_name = "all | code-only",
+ verbatim_doc_comment
+ )]
+ build_artifact: BuildArtifacts,
+ #[structopt(flatten)]
+ verbosity: VerbosityFlags,
+ #[structopt(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.
+ #[structopt(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.
+ #[structopt(long)]
+ keep_debug_symbols: bool,
+
+ /// Export the build output in JSON format.
+ #[structopt(long, conflicts_with = "verbose")]
+ output_json: 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)?;
+
+ // 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,
+ }
+ }
+ };
+
+ let build_mode = match self.build_release {
+ true => BuildMode::Release,
+ false => BuildMode::Debug,
+ };
+
+ 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,
+ build_artifact: self.build_artifact,
+ unstable_flags,
+ optimization_passes,
+ keep_debug_symbols: self.keep_debug_symbols,
+ output_type,
+ };
+
+ execute(args)
+ }
+}
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "check")]
+pub struct CheckCommand {
+ /// Path to the Cargo.toml of the contract to build
+ #[structopt(long, parse(from_os_str))]
+ manifest_path: Option,
+ #[structopt(flatten)]
+ verbosity: VerbosityFlags,
+ #[structopt(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,
+ build_artifact: BuildArtifacts::CheckOnly,
+ unstable_flags,
+ optimization_passes: OptimizationPasses::Zero,
+ keep_debug_symbols: false,
+ output_type: OutputType::default(),
+ };
+
+ execute(args)
+ }
+}
+
+/// Executes the supplied cargo command on 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
-/// the resulting Wasm binary.
+/// Uses the unstable cargo feature [`build-std`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std)
+/// to build the standard library with [`panic_immediate_abort`](https://github.com/johnthagen/min-sized-rust#remove-panic-string-formatting-with-panic_immediate_abort)
+/// which reduces the size of the Wasm binary by not including panic strings and formatting code.
///
/// # Cargo.toml optimizations
///
@@ -48,63 +224,56 @@ const MAX_MEMORY_PAGES: u32 = 16;
/// user-defined settings will be preserved.
///
/// To disable this and use the original `Cargo.toml` as is then pass the `-Z original_manifest` flag.
-fn build_cargo_project(
+fn exec_cargo_for_wasm_target(
crate_metadata: &CrateMetadata,
- verbosity: Option,
- unstable_options: UnstableFlags,
+ command: &str,
+ build_mode: BuildMode,
+ verbosity: Verbosity,
+ unstable_flags: &UnstableFlags,
) -> Result<()> {
util::assert_channel()?;
- // set RUSTFLAGS, read from environment var by cargo-xbuild
+ // set linker args via RUSTFLAGS.
+ // Currently will override user defined RUSTFLAGS from .cargo/config. See https://github.com/paritytech/cargo-contract/issues/98.
std::env::set_var(
"RUSTFLAGS",
- "-C link-arg=-z -C link-arg=stack-size=65536 -C link-arg=--import-memory",
+ "-C link-arg=-zstack-size=65536 -C link-arg=--import-memory",
);
- let verbosity = verbosity.map(|v| match v {
- Verbosity::Verbose => xargo_lib::Verbosity::Verbose,
- Verbosity::Quiet => xargo_lib::Verbosity::Quiet,
- });
-
- let xbuild = |manifest_path: &ManifestPath| {
- let manifest_path = Some(manifest_path);
- let target = Some("wasm32-unknown-unknown");
- let target_dir = &crate_metadata.cargo_meta.target_directory;
- let other_args = [
+ let cargo_build = |manifest_path: &ManifestPath| {
+ let target_dir = &crate_metadata.target_directory;
+ let target_dir = format!("--target-dir={}", target_dir.to_string_lossy());
+ let mut args = vec![
+ "--target=wasm32-unknown-unknown",
+ "-Zbuild-std",
"--no-default-features",
"--release",
- &format!("--target-dir={}", target_dir.to_string_lossy()),
+ &target_dir,
];
- let args = xargo_lib::Args::new(target, manifest_path, verbosity, &other_args)
- .map_err(|e| anyhow::anyhow!("{}", e))
- .context("Creating xargo args")?;
-
- let config = xargo_lib::Config {
- sysroot_path: target_dir.join("sysroot"),
- memcpy: false,
- panic_immediate_abort: true,
- };
-
- let exit_status = xargo_lib::build(args, "build", Some(config))
- .map_err(|e| anyhow::anyhow!("{}", e))
- .context("Building with xbuild")?;
- if !exit_status.success() {
- anyhow::bail!("xbuild failed with status {}", exit_status)
+ if build_mode == BuildMode::Debug {
+ args.push("--features=ink_env/ink-debug");
+ } else {
+ args.push("-Zbuild-std-features=panic_immediate_abort");
}
+<<<<<<< HEAD
println!("exit_status {:?}", exit_status);
+=======
+ util::invoke_cargo(command, &args, manifest_path.directory(), verbosity)?;
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
Ok(())
};
- if unstable_options.original_manifest {
- println!(
+ if unstable_flags.original_manifest {
+ maybe_println!(
+ verbosity,
"{} {}",
"warning:".yellow().bold(),
"with 'original-manifest' enabled, the contract binary may not be of optimal size."
.bold()
);
- xbuild(&crate_metadata.manifest_path)?;
+ cargo_build(&crate_metadata.manifest_path)?;
} else {
Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
@@ -113,7 +282,7 @@ fn build_cargo_project(
.with_profile_release_defaults(Profile::default_contract_release())?;
Ok(())
})?
- .using_temp(xbuild)?;
+ .using_temp(cargo_build)?;
}
// clear RUSTFLAGS
@@ -160,149 +329,916 @@ fn ensure_maximum_memory_pages(module: &mut Module, maximum_allowed_pages: u32)
/// Strips all custom sections.
///
/// Presently all custom sections are not required so they can be stripped safely.
+/// The name section is already stripped by `wasm-opt`.
fn strip_custom_sections(module: &mut Module) {
module.sections_mut().retain(|section| match section {
- Section::Custom(_) => false,
- Section::Name(_) => false,
Section::Reloc(_) => false,
+ Section::Custom(custom) if custom.name() != "name" => false,
_ => true,
- });
+ })
+}
+
+/// A contract should export nothing but the "call" and "deploy" functions.
+///
+/// Any elements not referenced by these exports become orphaned and are removed by `wasm-opt`.
+fn strip_exports(module: &mut Module) {
+ if let Some(section) = module.export_section_mut() {
+ section.entries_mut().retain(|entry| {
+ matches!(entry.internal(), Internal::Function(_))
+ && (entry.field() == "call" || entry.field() == "deploy")
+ })
+ }
+}
+
+/// Load and parse a wasm file from disk.
+fn load_module>(path: P) -> Result {
+ let path = path.as_ref();
+ parity_wasm::deserialize_file(path).context(format!(
+ "Loading of wasm module at '{}' failed",
+ path.display(),
+ ))
}
/// Performs required post-processing steps on the wasm artifact.
fn post_process_wasm(crate_metadata: &CrateMetadata) -> Result<()> {
// Deserialize wasm module from a file.
let mut module =
- parity_wasm::deserialize_file(&crate_metadata.original_wasm).context(format!(
- "Loading original wasm file '{}'",
- crate_metadata.original_wasm.display()
- ))?;
-
- // Perform optimization.
- //
- // In practice only tree-shaking is performed, i.e transitively removing all symbols that are
- // NOT used by the specified entrypoints.
- if pwasm_utils::optimize(&mut module, ["call", "deploy"].to_vec()).is_err() {
- anyhow::bail!("Optimizer failed");
- }
+ load_module(&crate_metadata.original_wasm).context("Loading of original wasm failed")?;
+
+ strip_exports(&mut module);
ensure_maximum_memory_pages(&mut module, MAX_MEMORY_PAGES)?;
strip_custom_sections(&mut module);
+ validate_wasm::validate_import_section(&module)?;
+
+ debug_assert!(
+ !module.clone().to_bytes().unwrap().is_empty(),
+ "resulting wasm size of post processing must be > 0"
+ );
+
parity_wasm::serialize_to_file(&crate_metadata.dest_wasm, module)?;
Ok(())
}
-/// Attempts to perform optional wasm optimization using `wasm-opt`.
+/// Attempts to perform optional wasm optimization using `binaryen`.
///
/// 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,
+ optimization_passes: OptimizationPasses,
+ keep_debug_symbols: bool,
+) -> Result {
+ let mut dest_optimized = crate_metadata.dest_wasm.clone();
+ dest_optimized.set_file_name(format!(
+ "{}-opt.wasm",
+ crate_metadata.contract_artifact_name
+ ));
+ let _ = do_optimization(
+ crate_metadata.dest_wasm.as_os_str(),
+ dest_optimized.as_os_str(),
+ optimization_passes,
+ keep_debug_symbols,
+ )?;
+
+ if !dest_optimized.exists() {
+ return Err(anyhow::anyhow!(
+ "Optimization failed, optimized wasm output file `{}` not found.",
+ dest_optimized.display()
+ ));
+ }
+
+ let original_size = metadata(&crate_metadata.dest_wasm)?.len() as f64 / 1000.0;
+ let optimized_size = metadata(&dest_optimized)?.len() as f64 / 1000.0;
+
+ // overwrite existing destination wasm file with the optimised version
+ std::fs::rename(&dest_optimized, &crate_metadata.dest_wasm)?;
+ Ok(OptimizationResult {
+ dest_wasm: crate_metadata.dest_wasm.clone(),
+ original_size,
+ optimized_size,
+ })
+}
+
+/// Optimizes the Wasm supplied as `crate_metadata.dest_wasm` using
+/// the `wasm-opt` binary.
+///
+/// The supplied `optimization_level` denotes the number of optimization passes,
+/// resulting in potentially a lot of time spent optimizing.
///
-/// This step depends on the `wasm-opt` tool being installed. If it is not the build will still
-/// succeed, and the user will be encouraged to install it for further optimizations.
-fn optimize_wasm(crate_metadata: &CrateMetadata) -> Result<()> {
- // check `wasm-opt` installed
- if which::which("wasm-opt").is_err() {
- println!(
- "{}",
- "wasm-opt is not installed. Install this tool on your system in order to \n\
- reduce the size of your contract's Wasm binary. \n\
- See https://github.com/WebAssembly/binaryen#tools"
+/// If successful, the optimized wasm is written to `dest_optimized`.
+fn do_optimization(
+ dest_wasm: &OsStr,
+ dest_optimized: &OsStr,
+ optimization_level: OptimizationPasses,
+ keep_debug_symbols: bool,
+) -> Result<()> {
+ // check `wasm-opt` is installed
+ let which = which::which("wasm-opt");
+ if which.is_err() {
+ anyhow::bail!(
+ "wasm-opt not found! Make sure the binary is in your PATH environment.\n\
+ We use this tool to optimize the size of your contract's Wasm binary.\n\n\
+ wasm-opt is part of the binaryen package. You can find detailed\n\
+ installation instructions on https://github.com/WebAssembly/binaryen#tools.\n\n\
+
+ There are ready-to-install packages for many platforms:\n\
+ * Debian/Ubuntu: apt-get install binaryen\n\
+ * Homebrew: brew install binaryen\n\
+ * Arch Linux: pacman -S binaryen\n\
+ * Windows: binary releases at https://github.com/WebAssembly/binaryen/releases"
+ .to_string()
.bright_yellow()
);
- return Ok(());
}
+ let wasm_opt_path = which
+ .as_ref()
+ .expect("we just checked if which returned an err; qed")
+ .as_path();
+ log::info!("Path to wasm-opt executable: {}", wasm_opt_path.display());
- let mut optimized = crate_metadata.dest_wasm.clone();
- optimized.set_file_name(format!("{}-opt.wasm", crate_metadata.package_name));
+ let _ = check_wasm_opt_version_compatibility(wasm_opt_path)?;
- let output = Command::new("wasm-opt")
- .arg(crate_metadata.dest_wasm.as_os_str())
- .arg("-O3") // execute -O3 optimization passes (spends potentially a lot of time optimizing)
+ log::info!(
+ "Optimization level passed to wasm-opt: {}",
+ optimization_level
+ );
+ let mut command = Command::new(wasm_opt_path);
+ command
+ .arg(dest_wasm)
+ .arg(format!("-O{}", optimization_level))
.arg("-o")
- .arg(optimized.as_os_str())
- .output()?;
+ .arg(dest_optimized)
+ // the memory in our module is imported, `wasm-opt` needs to be told that
+ // the memory is initialized to zeroes, otherwise it won't run the
+ // memory-packing pre-pass.
+ .arg("--zero-filled-memory");
+ if keep_debug_symbols {
+ command.arg("-g");
+ }
+ log::info!("Invoking wasm-opt with {:?}", command);
+ let output = command.output().map_err(|err| {
+ anyhow::anyhow!(
+ "Executing {} failed with {:?}",
+ wasm_opt_path.display(),
+ err
+ )
+ })?;
if !output.status.success() {
- // Dump the output streams produced by wasm-opt into the stdout/stderr.
- io::stdout().write_all(&output.stdout)?;
- io::stderr().write_all(&output.stderr)?;
- anyhow::bail!("wasm-opt optimization failed");
+ let err = str::from_utf8(&output.stderr)
+ .expect("Cannot convert stderr output of wasm-opt to string")
+ .trim();
+ anyhow::bail!(
+ "The wasm-opt optimization failed.\n\n\
+ The error which wasm-opt returned was: \n{}",
+ err
+ );
}
+ Ok(())
+}
- 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
- );
+/// Checks if the wasm-opt binary under `wasm_opt_path` returns a version
+/// compatible with `cargo-contract`.
+///
+/// Currently this must be a version >= 99.
+fn check_wasm_opt_version_compatibility(wasm_opt_path: &Path) -> Result<()> {
+ let cmd = Command::new(wasm_opt_path)
+ .arg("--version")
+ .output()
+ .map_err(|err| {
+ anyhow::anyhow!(
+ "Executing `{:?} --version` failed with {:?}",
+ wasm_opt_path.display(),
+ err
+ )
+ })?;
+ if !cmd.status.success() {
+ let err = str::from_utf8(&cmd.stderr)
+ .expect("Cannot convert stderr output of wasm-opt to string")
+ .trim();
+ anyhow::bail!(
+ "Getting version information from wasm-opt failed.\n\
+ The error which wasm-opt returned was: \n{}",
+ err
+ );
+ }
- // overwrite existing destination wasm file with the optimised version
- std::fs::rename(&optimized, &crate_metadata.dest_wasm)?;
+ // ```sh
+ // $ wasm-opt --version
+ // wasm-opt version 99 (version_99-79-gc12cc3f50)
+ // ```
+ let github_note = "\n\n\
+ If you tried installing from your system package manager the best\n\
+ way forward is to download a recent binary release directly:\n\n\
+ https://github.com/WebAssembly/binaryen/releases\n\n\
+ Make sure that the `wasm-opt` file from that release is in your `PATH`.";
+ let version_stdout = str::from_utf8(&cmd.stdout)
+ .expect("Cannot convert stdout output of wasm-opt to string")
+ .trim();
+ let re = Regex::new(r"wasm-opt version (\d+)").expect("invalid regex");
+ let captures = re.captures(version_stdout).ok_or_else(|| {
+ anyhow::anyhow!(
+ "Unable to extract version information from '{}'.\n\
+ Your wasm-opt version is most probably too old. Make sure you use a version >= 99.{}",
+ version_stdout,
+ github_note,
+ )
+ })?;
+ let version_number: u32 = captures
+ .get(1) // first capture group is at index 1
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "Unable to extract version number from '{:?}'",
+ version_stdout
+ )
+ })?
+ .as_str()
+ .parse()
+ .map_err(|err| {
+ anyhow::anyhow!(
+ "Parsing version number failed with '{:?}' for '{:?}'",
+ err,
+ version_stdout
+ )
+ })?;
+
+ log::info!(
+ "The wasm-opt version output is '{}', which was parsed to '{}'",
+ version_stdout,
+ version_number
+ );
+ if version_number < 99 {
+ anyhow::bail!(
+ "Your wasm-opt version is {}, but we require a version >= 99.{}",
+ version_number,
+ github_note,
+ );
+ }
Ok(())
}
-/// 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.
+/// Asserts that the contract's dependencies are compatible to the ones used in ink!.
///
-/// # Note
+/// This function utilizes `cargo tree`, which takes semver into consideration.
///
-/// 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(
+/// Hence this function only returns an `Err` if it is a proper mismatch according
+/// to semantic versioning. This means that either:
+/// - the major version mismatches, differences in the minor/patch version
+/// are not considered incompatible.
+/// - or if the version starts with zero (i.e. `0.y.z`) a mismatch in the minor
+/// version is already considered incompatible.
+fn assert_compatible_ink_dependencies(
manifest_path: &ManifestPath,
- verbosity: Option,
- unstable_options: UnstableFlags,
-) -> Result {
- let crate_metadata = CrateMetadata::collect(manifest_path)?;
- execute_with_metadata(&crate_metadata, verbosity, unstable_options)
+ verbosity: Verbosity,
+) -> Result<()> {
+ for dependency in ["parity-scale-codec", "scale-info"].iter() {
+ let args = ["-i", dependency, "--duplicates"];
+ let _ = util::invoke_cargo("tree", &args, manifest_path.directory(), verbosity).map_err(
+ |_| {
+ anyhow::anyhow!(
+ "Mismatching versions of `{}` were found!\n\
+ Please ensure that your contract and your ink! dependencies use a compatible \
+ version of this package.",
+ dependency
+ )
+ },
+ )?;
+ }
+ Ok(())
+}
+
+/// Checks whether the supplied `ink_version` already contains the debug feature.
+///
+/// This feature was introduced in `3.0.0-rc4` with `ink_env/ink-debug`.
+pub fn assert_debug_mode_supported(ink_version: &Version) -> anyhow::Result<()> {
+ log::info!("Contract version: {:?}", ink_version);
+ let minimum_version = Version::parse("3.0.0-rc4").expect("parsing version failed");
+ if ink_version < &minimum_version {
+ anyhow::bail!(
+ "Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
+ );
+ }
+ Ok(())
}
/// 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!(
- " {} {}",
- "[1/3]".bold(),
- "Building cargo project".bright_green().bold()
- );
- build_cargo_project(&crate_metadata, verbosity, unstable_options)?;
- println!(
- " {} {}",
- "[2/3]".bold(),
- "Post processing wasm file".bright_green().bold()
- );
- post_process_wasm(&crate_metadata)?;
- println!(
- " {} {}",
- "[3/3]".bold(),
- "Optimizing wasm file".bright_green().bold()
- );
- optimize_wasm(&crate_metadata)?;
- Ok(crate_metadata.dest_wasm.clone())
+pub(crate) fn execute(args: ExecuteArgs) -> Result {
+ let ExecuteArgs {
+ manifest_path,
+ verbosity,
+ build_mode,
+ build_artifact,
+ unstable_flags,
+ optimization_passes,
+ keep_debug_symbols,
+ output_type,
+ } = args;
+
+ let crate_metadata = CrateMetadata::collect(&manifest_path)?;
+
+ assert_compatible_ink_dependencies(&manifest_path, verbosity)?;
+ if build_mode == BuildMode::Debug {
+ assert_debug_mode_supported(&crate_metadata.ink_version)?;
+ }
+
+ let build = || -> Result {
+ maybe_println!(
+ verbosity,
+ " {} {}",
+ format!("[1/{}]", build_artifact.steps()).bold(),
+ "Building cargo project".bright_green().bold()
+ );
+ exec_cargo_for_wasm_target(
+ &crate_metadata,
+ "build",
+ build_mode,
+ verbosity,
+ &unstable_flags,
+ )?;
+
+ maybe_println!(
+ verbosity,
+ " {} {}",
+ format!("[2/{}]", build_artifact.steps()).bold(),
+ "Post processing wasm file".bright_green().bold()
+ );
+ post_process_wasm(&crate_metadata)?;
+
+ maybe_println!(
+ verbosity,
+ " {} {}",
+ format!("[3/{}]", build_artifact.steps()).bold(),
+ "Optimizing wasm file".bright_green().bold()
+ );
+ let optimization_result =
+ optimize_wasm(&crate_metadata, optimization_passes, keep_debug_symbols)?;
+
+ Ok(optimization_result)
+ };
+
+ let (opt_result, metadata_result) = match build_artifact {
+ BuildArtifacts::CheckOnly => {
+ exec_cargo_for_wasm_target(
+ &crate_metadata,
+ "check",
+ BuildMode::Release,
+ verbosity,
+ &unstable_flags,
+ )?;
+ (None, None)
+ }
+ BuildArtifacts::CodeOnly => {
+ let optimization_result = build()?;
+ (Some(optimization_result), None)
+ }
+ BuildArtifacts::All => {
+ let optimization_result = build()?;
+
+ let metadata_result = super::metadata::execute(
+ &crate_metadata,
+ optimization_result.dest_wasm.as_path(),
+ verbosity,
+ build_artifact.steps(),
+ &unstable_flags,
+ )?;
+ (Some(optimization_result), Some(metadata_result))
+ }
+ };
+ let dest_wasm = opt_result.as_ref().map(|r| r.dest_wasm.clone());
+
+ Ok(BuildResult {
+ dest_wasm,
+ metadata_result,
+ target_directory: crate_metadata.target_directory,
+ optimization_result: opt_result,
+ build_mode,
+ build_artifact,
+ verbosity,
+ output_type,
+ })
}
#[cfg(feature = "test-ci-only")]
#[cfg(test)]
-mod tests {
- use crate::{cmd, util::tests::with_tmp_dir, workspace::ManifestPath, UnstableFlags};
+mod tests_ci_only {
+ use super::{
+ assert_compatible_ink_dependencies, assert_debug_mode_supported,
+ check_wasm_opt_version_compatibility,
+ };
+ use crate::{
+ cmd::{build::load_module, BuildCommand},
+ util::tests::{with_new_contract_project, with_tmp_dir},
+ workspace::Manifest,
+ BuildArtifacts, BuildMode, ManifestPath, OptimizationPasses, OutputType, UnstableOptions,
+ Verbosity, VerbosityFlags,
+ };
+ use semver::Version;
+ #[cfg(unix)]
+ use std::os::unix::fs::PermissionsExt;
+ use std::{
+ ffi::OsStr,
+ io::Write,
+ path::{Path, PathBuf},
+ };
+
+ /// Modifies the `Cargo.toml` under the supplied `cargo_toml_path` by
+ /// setting `optimization-passes` in `[package.metadata.contract]` to `passes`.
+ fn write_optimization_passes_into_manifest(cargo_toml_path: &Path, passes: OptimizationPasses) {
+ let manifest_path =
+ ManifestPath::new(cargo_toml_path).expect("manifest path creation failed");
+ let mut manifest = Manifest::new(manifest_path.clone()).expect("manifest creation failed");
+ manifest
+ .set_profile_optimization_passes(passes)
+ .expect("setting `optimization-passes` in profile failed");
+ manifest
+ .write(&manifest_path)
+ .expect("writing manifest failed");
+ }
+
+ fn has_debug_symbols>(p: P) -> bool {
+ load_module(p)
+ .unwrap()
+ .custom_sections()
+ .any(|e| e.name() == "name")
+ }
+
+ /// Creates an executable `wasm-opt-mocked` file which outputs
+ /// "wasm-opt version `version`".
+ ///
+ /// Returns the path to this file.
+ ///
+ /// Currently works only on `unix`.
+ #[cfg(unix)]
+ fn mock_wasm_opt_version(tmp_dir: &Path, version: &str) -> PathBuf {
+ let path = tmp_dir.join("wasm-opt-mocked");
+ {
+ let mut file = std::fs::File::create(&path).unwrap();
+ let version = format!("#!/bin/sh\necho \"wasm-opt version {}\"", version);
+ file.write_all(version.as_bytes())
+ .expect("writing wasm-opt-mocked failed");
+ }
+ std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o777))
+ .expect("setting permissions failed");
+ path
+ }
+
+ #[test]
+ fn build_code_only() {
+ with_new_contract_project(|manifest_path| {
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_mode: BuildMode::Release,
+ build_artifact: BuildArtifacts::CodeOnly,
+ ..Default::default()
+ };
+
+ let res = super::execute(args).expect("build failed");
+
+ // our ci has set `CARGO_TARGET_DIR` to cache artifacts.
+ // this dir does not include `/target/` as a path, hence
+ // we can't match for e.g. `foo_project/target/ink`.
+ //
+ // we also can't match for `/ink` here, since this would match
+ // for `/ink` being the root path.
+ assert!(res.target_directory.ends_with("ink"));
+
+ assert!(
+ res.metadata_result.is_none(),
+ "CodeOnly should not generate the metadata"
+ );
+
+ let optimized_size = res.optimization_result.unwrap().optimized_size;
+ assert!(optimized_size > 0.0);
+
+ // our optimized contract template should always be below 3k.
+ assert!(optimized_size < 3.0);
+
+ // we specified that debug symbols should be removed
+ // original code should have some but the optimized version should have them removed
+ assert!(!has_debug_symbols(&res.dest_wasm.unwrap()));
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn check_must_not_output_contract_artifacts_in_project_dir() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let project_dir = manifest_path.directory().expect("directory must exist");
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path: manifest_path.clone(),
+ build_artifact: BuildArtifacts::CheckOnly,
+ ..Default::default()
+ };
+
+ // when
+ super::execute(args).expect("build failed");
+
+ // then
+ assert!(
+ !project_dir.join("target/ink/new_project.contract").exists(),
+ "found contract artifact in project directory!"
+ );
+ assert!(
+ !project_dir.join("target/ink/new_project.wasm").exists(),
+ "found wasm artifact in project directory!"
+ );
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn optimization_passes_from_cli_must_take_precedence_over_profile() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ write_optimization_passes_into_manifest(
+ manifest_path.as_ref(),
+ OptimizationPasses::Three,
+ );
+ let cmd = BuildCommand {
+ manifest_path: Some(manifest_path.into()),
+ build_artifact: BuildArtifacts::All,
+ build_release: false,
+ verbosity: VerbosityFlags::default(),
+ unstable_options: UnstableOptions::default(),
+
+ // we choose zero optimization passes as the "cli" parameter
+ optimization_passes: Some(OptimizationPasses::Zero),
+ keep_debug_symbols: false,
+ output_json: false,
+ };
+
+ // when
+ let res = cmd.exec().expect("build failed");
+ let optimization = res
+ .optimization_result
+ .expect("no optimization result available");
+
+ // then
+ // The size does not exactly match the original size even without optimization
+ // passed because there is still some post processing happening.
+ let size_diff = optimization.original_size - optimization.optimized_size;
+ assert!(
+ 0.0 < size_diff && size_diff < 10.0,
+ "The optimized size savings are larger than allowed or negative: {}",
+ size_diff,
+ );
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn optimization_passes_from_profile_must_be_used() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ write_optimization_passes_into_manifest(
+ manifest_path.as_ref(),
+ OptimizationPasses::Three,
+ );
+ let cmd = BuildCommand {
+ manifest_path: Some(manifest_path.into()),
+ build_artifact: BuildArtifacts::All,
+ build_release: false,
+ verbosity: VerbosityFlags::default(),
+ unstable_options: UnstableOptions::default(),
+
+ // we choose no optimization passes as the "cli" parameter
+ optimization_passes: None,
+ keep_debug_symbols: false,
+ output_json: false,
+ };
+
+ // when
+ let res = cmd.exec().expect("build failed");
+ let optimization = res
+ .optimization_result
+ .expect("no optimization result available");
+
+ // then
+ // The size does not exactly match the original size even without optimization
+ // passed because there is still some post processing happening.
+ let size_diff = optimization.original_size - optimization.optimized_size;
+ assert!(
+ size_diff > (optimization.original_size / 2.0),
+ "The optimized size savings are too small: {}",
+ size_diff,
+ );
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn project_template_dependencies_must_be_ink_compatible() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ // the manifest path
+
+ // when
+ let res = assert_compatible_ink_dependencies(&manifest_path, Verbosity::Default);
+
+ // then
+ assert!(res.is_ok());
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn detect_mismatching_parity_scale_codec_dependencies() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ // the manifest path
+
+ // at the time of writing this test ink! already uses `parity-scale-codec`
+ // in a version > 2, hence 1 is an incompatible version.
+ let mut manifest = Manifest::new(manifest_path.clone())?;
+ manifest
+ .set_dependency_version("scale", "1.0.0")
+ .expect("setting `scale` version failed");
+ manifest
+ .write(&manifest_path)
+ .expect("writing manifest failed");
+
+ // when
+ let res = assert_compatible_ink_dependencies(&manifest_path, Verbosity::Default);
+
+ // then
+ assert!(res.is_err());
+ Ok(())
+ })
+ }
+ #[cfg(unix)]
#[test]
- fn build_template() {
+ fn incompatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
with_tmp_dir(|path| {
- 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");
+ // given
+ let path = mock_wasm_opt_version(path, "98 (version_13-79-gc12cc3f50)");
+
+ // when
+ let res = check_wasm_opt_version_compatibility(&path);
+
+ // then
+ assert!(res.is_err());
+ assert!(
+ format!("{:?}", res).starts_with(
+ "Err(Your wasm-opt version is 98, but we require a version >= 99."
+ ),
+ "Expected a different output, found {:?}",
+ res
+ );
+
+ Ok(())
+ })
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn compatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
+ with_tmp_dir(|path| {
+ // given
+ let path = mock_wasm_opt_version(path, "99 (version_99-79-gc12cc3f50");
+
+ // when
+ let res = check_wasm_opt_version_compatibility(&path);
+
+ // then
+ assert!(res.is_ok());
+
+ Ok(())
+ })
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn incompatible_wasm_opt_version_must_be_detected_if_installed_as_package() {
+ with_tmp_dir(|path| {
+ // given
+ let path = mock_wasm_opt_version(path, "98");
+
+ // when
+ let res = check_wasm_opt_version_compatibility(&path);
+
+ // then
+ assert!(res.is_err());
+ assert!(format!("{:?}", res)
+ .starts_with("Err(Your wasm-opt version is 98, but we require a version >= 99."));
+
+ Ok(())
+ })
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn compatible_wasm_opt_version_must_be_detected_if_installed_as_package() {
+ with_tmp_dir(|path| {
+ // given
+ let path = mock_wasm_opt_version(path, "99");
+
+ // when
+ let res = check_wasm_opt_version_compatibility(&path);
+
+ // then
+ assert!(res.is_ok());
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn contract_lib_name_different_from_package_name_must_build() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let mut manifest =
+ Manifest::new(manifest_path.clone()).expect("manifest creation failed");
+ let _ = manifest
+ .set_lib_name("some_lib_name")
+ .expect("setting lib name failed");
+ let _ = manifest
+ .set_package_name("some_package_name")
+ .expect("setting pacakge name failed");
+ manifest
+ .write(&manifest_path)
+ .expect("writing manifest failed");
+
+ // when
+ let cmd = BuildCommand {
+ manifest_path: Some(manifest_path.into()),
+ build_artifact: BuildArtifacts::All,
+ build_release: false,
+ verbosity: VerbosityFlags::default(),
+ unstable_options: UnstableOptions::default(),
+ optimization_passes: None,
+ keep_debug_symbols: false,
+ output_json: false,
+ };
+ let res = cmd.exec().expect("build failed");
+
+ // then
+ assert_eq!(
+ res.dest_wasm
+ .expect("`dest_wasm` does not exist")
+ .file_name(),
+ Some(OsStr::new("some_lib_name.wasm"))
+ );
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ pub fn debug_mode_must_be_compatible() {
+ let _ =
+ assert_debug_mode_supported(&Version::parse("3.0.0-rc4").expect("parsing must work"))
+ .expect("debug mode must be compatible");
+ let _ =
+ assert_debug_mode_supported(&Version::parse("4.0.0-rc1").expect("parsing must work"))
+ .expect("debug mode must be compatible");
+ let _ = assert_debug_mode_supported(&Version::parse("5.0.0").expect("parsing must work"))
+ .expect("debug mode must be compatible");
+ }
+
+ #[test]
+ pub fn debug_mode_must_be_incompatible() {
+ let res =
+ assert_debug_mode_supported(&Version::parse("3.0.0-rc3").expect("parsing must work"))
+ .expect_err("assertion must fail");
+ assert_eq!(
+ res.to_string(),
+ "Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
+ );
+ }
+
+ #[test]
+ fn building_template_in_debug_mode_must_work() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_mode: BuildMode::Debug,
+ ..Default::default()
+ };
+
+ // when
+ let res = super::execute(args);
+
+ // then
+ assert!(res.is_ok(), "building template in debug mode failed!");
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn building_template_in_release_mode_must_work() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_mode: BuildMode::Release,
+ ..Default::default()
+ };
+
+ // when
+ let res = super::execute(args);
+
+ // then
+ assert!(res.is_ok(), "building template in release mode failed!");
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn building_contract_with_source_file_in_subfolder_must_work() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let path = manifest_path.directory().expect("dir must exist");
+ let old_lib_path = path.join(Path::new("lib.rs"));
+ let new_lib_path = path.join(Path::new("srcfoo")).join(Path::new("lib.rs"));
+ let new_dir_path = path.join(Path::new("srcfoo"));
+ std::fs::create_dir_all(new_dir_path).expect("creating dir must work");
+ std::fs::rename(old_lib_path, new_lib_path).expect("moving file must work");
+
+ let mut manifest =
+ Manifest::new(manifest_path.clone()).expect("creating manifest must work");
+ manifest
+ .set_lib_path("srcfoo/lib.rs")
+ .expect("setting lib path must work");
+ manifest.write(&manifest_path).expect("writing must work");
+
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_artifact: BuildArtifacts::CheckOnly,
+ ..Default::default()
+ };
+
+ // when
+ let res = super::execute(args);
+
+ // then
+ assert!(res.is_ok(), "building contract failed!");
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn keep_debug_symbols_in_debug_mode() {
+ with_new_contract_project(|manifest_path| {
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_mode: BuildMode::Debug,
+ build_artifact: BuildArtifacts::CodeOnly,
+ keep_debug_symbols: true,
+ ..Default::default()
+ };
+
+ let res = super::execute(args).expect("build failed");
+
+ // we specified that debug symbols should be kept
+ assert!(has_debug_symbols(&res.dest_wasm.unwrap()));
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn keep_debug_symbols_in_release_mode() {
+ with_new_contract_project(|manifest_path| {
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ build_mode: BuildMode::Release,
+ build_artifact: BuildArtifacts::CodeOnly,
+ keep_debug_symbols: true,
+ ..Default::default()
+ };
+
+ let res = super::execute(args).expect("build failed");
+
+ // we specified that debug symbols should be kept
+ assert!(has_debug_symbols(&res.dest_wasm.unwrap()));
+
+ Ok(())
+ })
+ }
+
+ #[test]
+ fn build_with_json_output_works() {
+ with_new_contract_project(|manifest_path| {
+ // given
+ let args = crate::cmd::build::ExecuteArgs {
+ manifest_path,
+ output_type: OutputType::Json,
+ ..Default::default()
+ };
+
+ // when
+ let res = super::execute(args).expect("build failed");
+
+ // then
+ assert!(res.serialize_json().is_ok());
Ok(())
})
}
diff --git a/src/cmd/deploy.rs b/src/cmd/deploy.rs
index 70ad04819..eac3d542e 100644
--- a/src/cmd/deploy.rs
+++ b/src/cmd/deploy.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -66,7 +66,7 @@ pub(crate) fn execute_deploy(
let events = cli.put_code_and_watch(&signer, &code).await?;
let code_stored = events
.code_stored()?
- .ok_or(anyhow::anyhow!("Failed to find CodeStored event"))?;
+ .context("Failed to find CodeStored event")?;
Ok(code_stored.code_hash)
})
diff --git a/src/cmd/instantiate.rs b/src/cmd/instantiate.rs
index b94643666..6983bee11 100644
--- a/src/cmd/instantiate.rs
+++ b/src/cmd/instantiate.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -14,10 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with cargo-contract. If not, see .
+<<<<<<< HEAD
use anyhow::Result;
use subxt::{
balances::Balances, contracts::*, system::System, ClientBuilder, ContractsTemplateRuntime,
};
+=======
+use anyhow::{Context, Result};
+use subxt::{balances::Balances, contracts::*, system::System, ClientBuilder, DefaultNodeRuntime};
+>>>>>>> 8e86572b4b4ed2442de131c8e3506dee219fb0b7
use crate::{ExtrinsicOpts, HexData};
@@ -45,7 +50,7 @@ pub(crate) fn execute_instantiate(
.await?;
let instantiated = events
.instantiated()?
- .ok_or(anyhow::anyhow!("Failed to find Instantiated event"))?;
+ .context("Failed to find Instantiated event")?;
Ok(instantiated.contract)
})
diff --git a/src/cmd/metadata.rs b/src/cmd/metadata.rs
new file mode 100644
index 000000000..92bb3c3b0
--- /dev/null
+++ b/src/cmd/metadata.rs
@@ -0,0 +1,424 @@
+// Copyright 2018-2021 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::{
+ crate_metadata::CrateMetadata,
+ maybe_println, util,
+ workspace::{ManifestPath, Workspace},
+ UnstableFlags, Verbosity,
+};
+
+use anyhow::Result;
+use blake2::digest::{Update as _, VariableOutput as _};
+use colored::Colorize;
+use contract_metadata::{
+ CodeHash, Compiler, Contract, ContractMetadata, Language, Source, SourceCompiler,
+ SourceLanguage, SourceWasm, User,
+};
+use semver::Version;
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+use url::Url;
+
+const METADATA_FILE: &str = "metadata.json";
+
+/// Metadata generation result.
+#[derive(serde::Serialize)]
+pub struct MetadataResult {
+ /// Path to the resulting metadata file.
+ pub dest_metadata: PathBuf,
+ /// Path to the bundled file.
+ pub dest_bundle: PathBuf,
+}
+
+/// Result of generating the extended contract project metadata
+struct ExtendedMetadataResult {
+ source: Source,
+ contract: Contract,
+ user: Option,
+}
+
+/// 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(
+ crate_metadata: &CrateMetadata,
+ final_contract_wasm: &Path,
+ verbosity: Verbosity,
+ total_steps: usize,
+ unstable_options: &UnstableFlags,
+) -> Result {
+ util::assert_channel()?;
+
+ let target_directory = crate_metadata.target_directory.clone();
+ let out_path_metadata = target_directory.join(METADATA_FILE);
+
+ let fname_bundle = format!("{}.contract", crate_metadata.contract_artifact_name);
+ let out_path_bundle = target_directory.join(fname_bundle);
+
+ // build the extended contract project metadata
+ let ExtendedMetadataResult {
+ source,
+ contract,
+ user,
+ } = extended_metadata(crate_metadata, final_contract_wasm)?;
+
+ let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> {
+ let mut current_progress = 4;
+ maybe_println!(
+ verbosity,
+ " {} {}",
+ format!("[{}/{}]", current_progress, total_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",
+ &[
+ "--package",
+ "metadata-gen",
+ &manifest_path.cargo_arg(),
+ &target_dir_arg,
+ "--release",
+ ],
+ crate_metadata.manifest_path.directory(),
+ verbosity,
+ )?;
+
+ let ink_meta: serde_json::Map = serde_json::from_slice(&stdout)?;
+ 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;
+ }
+
+ maybe_println!(
+ verbosity,
+ " {} {}",
+ format!("[{}/{}]", current_progress, total_steps).bold(),
+ "Generating bundle".bright_green().bold()
+ );
+ let contents = serde_json::to_string(&metadata)?;
+ fs::write(&out_path_bundle, contents)?;
+
+ Ok(())
+ };
+
+ if unstable_options.original_manifest {
+ generate_metadata(&crate_metadata.manifest_path)?;
+ } else {
+ Workspace::new(&crate_metadata.cargo_meta, &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(crate_metadata.manifest_path.absolute_directory()?)?
+ .using_temp(generate_metadata)?;
+ }
+
+ Ok(MetadataResult {
+ dest_metadata: out_path_metadata,
+ dest_bundle: out_path_bundle,
+ })
+}
+
+/// Generate the extended contract project metadata
+fn extended_metadata(
+ crate_metadata: &CrateMetadata,
+ final_contract_wasm: &Path,
+) -> Result {
+ let contract_package = &crate_metadata.root_package;
+ let ink_version = &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 = crate_metadata.documentation.clone();
+ let repository = contract_package
+ .repository
+ .as_ref()
+ .map(|repo| Url::parse(repo))
+ .transpose()?;
+ let homepage = crate_metadata.homepage.clone();
+ let license = contract_package.license.clone();
+ let source = {
+ let lang = SourceLanguage::new(Language::Ink, ink_version.clone());
+ let compiler = SourceCompiler::new(Compiler::RustC, rust_version);
+ let wasm = fs::read(final_contract_wasm)?;
+ let hash = blake2_hash(wasm.as_slice());
+ Source::new(Some(SourceWasm::new(wasm)), hash, lang, compiler)
+ };
+
+ // Required contract fields
+ let mut builder = Contract::builder();
+ builder
+ .name(contract_name)
+ .version(contract_version)
+ .authors(contract_authors);
+
+ if let Some(description) = description {
+ builder.description(description);
+ }
+
+ if let Some(documentation) = documentation {
+ builder.documentation(documentation);
+ }
+
+ if let Some(repository) = repository {
+ builder.repository(repository);
+ }
+
+ if let Some(homepage) = homepage {
+ builder.homepage(homepage);
+ }
+
+ if let Some(license) = license {
+ builder.license(license);
+ }
+
+ let contract = builder
+ .build()
+ .map_err(|err| anyhow::anyhow!("Invalid contract metadata builder state: {}", err))?;
+
+ // user defined metadata
+ let user = crate_metadata.user.clone().map(User::new);
+
+ Ok(ExtendedMetadataResult {
+ source,
+ contract,
+ user,
+ })
+}
+
+/// 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)
+}
+
+#[cfg(feature = "test-ci-only")]
+#[cfg(test)]
+mod tests {
+ use crate::cmd::metadata::blake2_hash;
+ use crate::{
+ cmd, crate_metadata::CrateMetadata, util::tests::with_new_contract_project, ManifestPath,
+ };
+ use anyhow::Context;
+ use contract_metadata::*;
+ 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")
+ .context("package section not found")?
+ .as_table_mut()
+ .context("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()
+ .context("metadata section should be a table")?
+ .entry("contract")
+ .or_insert(value::Value::Table(Default::default()))
+ .as_table_mut()
+ .context("metadata.contract section should be a table")?
+ .entry("user")
+ .or_insert(value::Value::Table(Default::default()))
+ .as_table_mut()
+ .context("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_new_contract_project(|manifest_path| {
+ // 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)?;
+
+ // usually this file will be produced by a previous build step
+ let final_contract_wasm_path = &crate_metadata.dest_wasm;
+ 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::default();
+ args.manifest_path = test_manifest.manifest_path;
+
+ let build_result = cmd::build::execute(args)?;
+ let dest_bundle = build_result
+ .metadata_result
+ .expect("Metadata should be generated")
+ .dest_bundle;
+
+ let metadata_json: Map =
+ serde_json::from_slice(&fs::read(&dest_bundle)?)?;
+
+ assert!(
+ dest_bundle.exists(),
+ "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");
+ 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 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 =
+ 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()]),
+ );
+
+ 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.contract_artifact_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(())
+ })
+ }
+
+ 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/cmd/metadata/contract.rs b/src/cmd/metadata/contract.rs
deleted file mode 100644
index dc273db09..000000000
--- a/src/cmd/metadata/contract.rs
+++ /dev/null
@@ -1,241 +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 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
deleted file mode 100644
index 9a62dc046..000000000
--- a/src/cmd/metadata/mod.rs
+++ /dev/null
@@ -1,366 +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 .
-
-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 00881e55d..5d3456d0b 100644
--- a/src/cmd/mod.rs
+++ b/src/cmd/mod.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -24,7 +24,12 @@ pub mod deploy;
mod instantiate;
pub mod metadata;
pub mod new;
+pub mod test;
+pub(crate) use self::{
+ build::{BuildCommand, CheckCommand},
+ test::TestCommand,
+};
#[cfg(feature = "extrinsics")]
pub(crate) use self::{
call::call_regular_contract, call::execute_call, call::execute_contract_call,
diff --git a/src/cmd/new.rs b/src/cmd/new.rs
index 39d794530..23eccd409 100644
--- a/src/cmd/new.rs
+++ b/src/cmd/new.rs
@@ -1,4 +1,4 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+// Copyright 2018-2021 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
@@ -23,12 +23,21 @@ use std::{
use anyhow::Result;
use heck::CamelCase as _;
-pub(crate) fn execute