// Copyright 2018-2022 Parity Technologies (UK) Ltd.
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// cargo-contract is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with cargo-contract. If not, see .
use crate::{
crate_metadata::CrateMetadata,
maybe_println,
util,
validate_wasm,
workspace::{
Manifest,
ManifestPath,
Profile,
Workspace,
},
BuildArtifacts,
BuildMode,
BuildResult,
Network,
OptimizationPasses,
OptimizationResult,
OutputType,
UnstableFlags,
UnstableOptions,
Verbosity,
VerbosityFlags,
};
use anyhow::{
Context,
Result,
};
use colored::Colorize;
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,
};
/// This is the maximum number of pages available for a contract to allocate.
const MAX_MEMORY_PAGES: u32 = 16;
/// 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,
network: Network,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
optimization_passes: OptimizationPasses,
keep_debug_symbols: bool,
skip_linting: 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, clap::Args)]
#[clap(name = "build")]
pub struct BuildCommand {
/// Path to the `Cargo.toml` of the contract to build
#[clap(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.
#[clap(long = "--release")]
build_release: bool,
/// Build offline
#[clap(long = "--offline")]
build_offline: bool,
/// Skips linting checks during the build process
#[clap(long = "--skip-linting")]
skip_linting: bool,
/// Which build artifacts to generate.
///
/// - `all`: Generate the Wasm, the metadata and a bundled `.contract` file.
///
/// - `code-only`: Only the Wasm is created, generation of metadata and a bundled
/// `.contract` file is skipped.
///
/// - `check-only`: No artifacts produced: runs the `cargo check` command for the Wasm target,
/// only checks for compilation errors.
#[clap(long = "generate", arg_enum, default_value = "all")]
build_artifact: BuildArtifacts,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(flatten)]
unstable_options: UnstableOptions,
/// Number of optimization passes, passed as an argument to `wasm-opt`.
///
/// - `0`: execute no optimization passes
///
/// - `1`: execute 1 optimization pass (quick & useful opts, useful for iteration builds)
///
/// - `2`, execute 2 optimization passes (most opts, generally gets most perf)
///
/// - `3`, execute 3 optimization passes (spends potentially a lot of time optimizing)
///
/// - `4`, execute 4 optimization passes (also flatten the IR, which can take a lot more time and memory
/// but is useful on more nested / complex / less-optimized input)
///
/// - `s`, execute default optimization passes, focusing on code size
///
/// - `z`, execute default optimization passes, super-focusing on code size
///
/// - The default value is `z`
///
/// - It is possible to define the number of optimization passes in the
/// `[package.metadata.contract]` of your `Cargo.toml` as e.g. `optimization-passes = "3"`.
/// The CLI argument always takes precedence over the profile value.
#[clap(long)]
optimization_passes: Option,
/// Do not remove symbols (Wasm name section) when optimizing.
///
/// This is useful if one wants to analyze or debug the optimized binary.
#[clap(long)]
keep_debug_symbols: bool,
/// Export the build output in JSON format.
#[clap(long, conflicts_with = "verbose")]
output_json: bool,
}
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 network = match self.build_offline {
true => Network::Offline,
false => Network::Online,
};
let output_type = match self.output_json {
true => OutputType::Json,
false => OutputType::HumanReadable,
};
// We want to ensure that the only thing in `STDOUT` is our JSON formatted string.
if matches!(output_type, OutputType::Json) {
verbosity = Verbosity::Quiet;
}
let args = ExecuteArgs {
manifest_path,
verbosity,
build_mode,
network,
build_artifact: self.build_artifact,
unstable_flags,
optimization_passes,
keep_debug_symbols: self.keep_debug_symbols,
skip_linting: self.skip_linting,
output_type,
};
execute(args)
}
}
#[derive(Debug, clap::Args)]
#[clap(name = "check")]
pub struct CheckCommand {
/// Path to the `Cargo.toml` of the contract to build
#[clap(long, parse(from_os_str))]
manifest_path: Option,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(flatten)]
unstable_options: UnstableOptions,
}
impl CheckCommand {
pub fn exec(&self) -> Result {
let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?;
let unstable_flags: UnstableFlags =
TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?;
let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?;
let args = ExecuteArgs {
manifest_path,
verbosity,
build_mode: BuildMode::Debug,
network: Network::default(),
build_artifact: BuildArtifacts::CheckOnly,
unstable_flags,
optimization_passes: OptimizationPasses::Zero,
keep_debug_symbols: false,
skip_linting: 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 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
///
/// The original `Cargo.toml` will be amended to remove the `rlib` crate type in order to minimize
/// the final Wasm binary size.
///
/// Preferred default `[profile.release]` settings will be added if they are missing, existing
/// user-defined settings will be preserved.
///
/// The `[workspace]` will be added if it is missing to ignore `workspace` from parent `Cargo.toml`.
///
/// To disable this and use the original `Cargo.toml` as is then pass the `-Z original_manifest` flag.
fn exec_cargo_for_wasm_target(
crate_metadata: &CrateMetadata,
command: &str,
build_mode: BuildMode,
network: Network,
verbosity: Verbosity,
unstable_flags: &UnstableFlags,
) -> Result<()> {
util::assert_channel()?;
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",
&target_dir,
];
if network == Network::Offline {
args.push("--offline");
}
if build_mode == BuildMode::Debug {
args.push("--features=ink_env/ink-debug");
} else {
args.push("-Zbuild-std-features=panic_immediate_abort");
}
let env = vec![(
"RUSTFLAGS",
Some("-C link-arg=-zstack-size=65536 -C link-arg=--import-memory -Clinker-plugin-lto"),
)];
util::invoke_cargo(command, &args, manifest_path.directory(), verbosity, env)?;
Ok(())
};
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()
);
cargo_build(&crate_metadata.manifest_path)?;
} else {
Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
manifest
.with_removed_crate_type("rlib")?
.with_profile_release_defaults(Profile::default_contract_release())?
.with_workspace()?;
Ok(())
})?
.using_temp(cargo_build)?;
}
Ok(())
}
/// Executes `cargo dylint` with the ink! linting driver that is built during
/// the `build.rs`.
///
/// We create a temporary folder, extract the linting driver there and run
/// `cargo dylint` with it.
fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Result<()> {
check_dylint_requirements(crate_metadata.manifest_path.directory())?;
let tmp_dir = tempfile::Builder::new()
.prefix("cargo-contract-dylint_")
.tempdir()?;
log::debug!("Using temp workspace at '{}'", tmp_dir.path().display());
let driver = include_bytes!(concat!(env!("OUT_DIR"), "/ink-dylint-driver.zip"));
crate::util::unzip(driver, tmp_dir.path().to_path_buf(), None)?;
let manifest_path = crate_metadata.manifest_path.cargo_arg()?;
let args = vec!["--lib", "ink_linting", &manifest_path];
let tmp_dir_path = tmp_dir.path().as_os_str().to_string_lossy();
let env = vec![
("DYLINT_LIBRARY_PATH", Some(tmp_dir_path.as_ref())),
// For tests we need to set the `DYLINT_DRIVER_PATH` to a tmp folder,
// otherwise tests running in parallel will try to write to the same
// file at the same time which will result in a `Text file busy` error.
#[cfg(test)]
("DYLINT_DRIVER_PATH", Some(tmp_dir_path.as_ref())),
// We need to remove the `CARGO_TARGET_DIR` environment variable in
// case `cargo dylint` is invoked.
//
// This is because the ink! dylint driver crate found in `dylint` uses a
// fixed Rust toolchain via the `ink_linting/rust-toolchain` file. By
// removing this env variable we avoid issues with different Rust toolchains
// interfering with each other.
("CARGO_TARGET_DIR", None),
// There are generally problems with having a custom `rustc` wrapper, while
// executing `dylint` (which has a custom linker). Especially for `sccache`
// there is this bug: https://github.com/mozilla/sccache/issues/1000.
// Until we have a justification for leaving the wrapper we should unset it.
("RUSTC_WRAPPER", None),
];
let working_dir = crate_metadata
.manifest_path
.directory()
.unwrap_or_else(|| Path::new("."))
.canonicalize()?;
let verbosity = if verbosity == Verbosity::Verbose {
// `dylint` is verbose by default, it doesn't have a `--verbose` argument,
Verbosity::Default
} else {
verbosity
};
util::invoke_cargo("dylint", &args, Some(working_dir), verbosity, env)?;
Ok(())
}
/// Checks if all requirements for `dylint` are installed.
///
/// We require only an installed version of `cargo-dylint` here and don't
/// check for an installed version of `dylint-link`. This is because
/// `dylint-link` is only required for the `dylint` driver build process
/// in `build.rs`.
///
/// This function takes a `_working_dir` which is only used for unit tests.
fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> {
let execute_cmd = |cmd: &mut Command| {
// when testing this function we set the `PATH` to the `working_dir`
// so that we can have mocked binaries in there which are executed
// instead of the real ones.
#[cfg(test)]
{
let default_dir = PathBuf::from(".");
let working_dir = _working_dir.unwrap_or(default_dir.as_path());
let path_env = std::env::var("PATH").unwrap();
let path_env = format!("{}:{}", working_dir.to_string_lossy(), path_env);
cmd.env("PATH", path_env);
}
cmd.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|err| {
log::debug!("Error spawning `{:?}`", cmd);
err
})?
.wait()
.map(|res| res.success())
.map_err(|err| {
log::debug!("Error waiting for `{:?}`: {:?}", cmd, err);
err
})
};
// when testing this function we should never fall back to a `cargo` specified
// in the env variable, as this would mess with the mocked binaries.
#[cfg(not(test))]
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
#[cfg(test)]
let cargo = "cargo";
if !execute_cmd(Command::new(cargo).arg("dylint").arg("--version"))? {
anyhow::bail!("cargo-dylint was not found!\n\
Make sure it is installed and the binary is in your PATH environment.\n\n\
You can install it by executing `cargo install cargo-dylint`."
.to_string()
.bright_yellow());
}
Ok(())
}
/// Ensures the Wasm memory import of a given module has the maximum number of pages.
///
/// Iterates over the import section, finds the memory import entry if any and adjusts the maximum
/// limit.
fn ensure_maximum_memory_pages(
module: &mut Module,
maximum_allowed_pages: u32,
) -> Result<()> {
let mem_ty = module
.import_section_mut()
.and_then(|section| {
section.entries_mut().iter_mut().find_map(|entry| {
match entry.external_mut() {
External::Memory(ref mut mem_ty) => Some(mem_ty),
_ => None,
}
})
})
.context(
"Memory import is not found. Is --import-memory specified in the linker args",
)?;
if let Some(requested_maximum) = mem_ty.limits().maximum() {
// The module already has maximum, check if it is within the limit bail out.
if requested_maximum > maximum_allowed_pages {
anyhow::bail!(
"The wasm module requires {} pages. The maximum allowed number of pages is {}",
requested_maximum,
maximum_allowed_pages,
);
}
} else {
let initial = mem_ty.limits().initial();
*mem_ty = MemoryType::new(initial, Some(MAX_MEMORY_PAGES));
}
Ok(())
}
/// 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::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 = 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 `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
));
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.
///
/// 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\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()
);
}
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());
check_wasm_opt_version_compatibility(wasm_opt_path)?;
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(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() {
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(())
}
/// 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 mut cmd_res = Command::new(wasm_opt_path).arg("--version").output();
// The following condition is a workaround for a spurious CI failure:
// ```
// Executing `"/tmp/cargo-contract.test.GGnC0p/wasm-opt-mocked" --version` failed with
// Os { code: 26, kind: ExecutableFileBusy, message: "Text file busy" }
// ```
if cmd_res.is_err() && format!("{:?}", cmd_res).contains("ExecutableFileBusy") {
std::thread::sleep(std::time::Duration::from_secs(1));
cmd_res = Command::new(wasm_opt_path).arg("--version").output();
}
let res = cmd_res.map_err(|err| {
anyhow::anyhow!(
"Executing `{:?} --version` failed with {:?}",
wasm_opt_path.display(),
err
)
})?;
if !res.status.success() {
let err = str::from_utf8(&res.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
);
}
// ```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(&res.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(())
}
/// Asserts that the contract's dependencies are compatible to the ones used in ink!.
///
/// This function utilizes `cargo tree`, which takes semver into consideration.
///
/// 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: 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, vec![])
.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.
pub(crate) fn execute(args: ExecuteArgs) -> Result {
let ExecuteArgs {
manifest_path,
verbosity,
build_mode,
network,
build_artifact,
unstable_flags,
optimization_passes,
keep_debug_symbols,
skip_linting,
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 {
if skip_linting {
maybe_println!(
verbosity,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Skip ink! linting rules".bright_yellow().bold()
);
} else {
maybe_println!(
verbosity,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Checking ink! linting rules".bright_green().bold()
);
exec_cargo_dylint(&crate_metadata, verbosity)?;
}
maybe_println!(
verbosity,
" {} {}",
format!("[2/{}]", build_artifact.steps()).bold(),
"Building cargo project".bright_green().bold()
);
exec_cargo_for_wasm_target(
&crate_metadata,
"build",
build_mode,
network,
verbosity,
&unstable_flags,
)?;
maybe_println!(
verbosity,
" {} {}",
format!("[3/{}]", build_artifact.steps()).bold(),
"Post processing wasm file".bright_green().bold()
);
post_process_wasm(&crate_metadata)?;
maybe_println!(
verbosity,
" {} {}",
format!("[4/{}]", 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 => {
if skip_linting {
maybe_println!(
verbosity,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Skip ink! linting rules".bright_yellow().bold()
);
} else {
maybe_println!(
verbosity,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Checking ink! linting rules".bright_green().bold()
);
exec_cargo_dylint(&crate_metadata, verbosity)?;
}
maybe_println!(
verbosity,
" {} {}",
format!("[2/{}]", build_artifact.steps()).bold(),
"Executing `cargo check`".bright_green().bold()
);
exec_cargo_for_wasm_target(
&crate_metadata,
"check",
BuildMode::Release,
network,
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(),
network,
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_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 file at `path` with the content `content`.
///
/// Currently works only on `unix`.
#[cfg(unix)]
fn create_executable(path: &Path, content: &str) {
{
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes())
.expect("writing of executable failed");
}
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o777))
.expect("setting permissions failed");
}
/// 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 content = format!("#!/bin/sh\necho \"wasm-opt version {}\"", version);
create_executable(&path, &content);
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,
build_offline: 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,
skip_linting: 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,
build_offline: false,
verbosity: VerbosityFlags::default(),
unstable_options: UnstableOptions::default(),
// we choose no optimization passes as the "cli" parameter
optimization_passes: None,
keep_debug_symbols: false,
skip_linting: 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 incompatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
with_tmp_dir(|path| {
// 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());
// this println is here to debug a spuriously failing CI at the following assert.
eprintln!("error: {:?}", res);
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,
build_offline: false,
verbosity: VerbosityFlags::default(),
unstable_options: UnstableOptions::default(),
optimization_passes: None,
keep_debug_symbols: false,
skip_linting: 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(())
})
}
// This test has to be ignored until the next ink! rc.
// Before that we don't have the `__ink_dylint_…` markers available
// to actually run `dylint`.
#[test]
#[ignore]
fn dylint_must_find_issue() {
with_new_contract_project(|manifest_path| {
// given
let contract = r#"
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod fail_mapping_01 {
use ink_storage::{traits::SpreadAllocate, Mapping};
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct MyContract {
balances: Mapping,
}
impl MyContract {
#[ink(constructor)]
pub fn new() -> Self {
Self {
balances: Default::default(),
}
}
/// Returns the total token supply.
#[ink(message)]
pub fn get(&self) {
// ...
}
}
}"#;
let project_dir = manifest_path.directory().expect("directory must exist");
let lib = project_dir.join("lib.rs");
std::fs::write(&lib, contract)?;
let args = crate::cmd::build::ExecuteArgs {
manifest_path,
build_artifact: BuildArtifacts::CheckOnly,
..Default::default()
};
// when
let res = super::execute(args);
// then
match res {
Err(err) => {
eprintln!("err: {:?}", err);
assert!(err.to_string().contains(
"help: add an `initialize_contract` function in this constructor"
));
}
_ => panic!("build succeeded, but must fail!"),
};
Ok(())
})
}
#[cfg(unix)]
#[test]
fn missing_cargo_dylint_installation_must_be_detected() {
with_new_contract_project(|manifest_path| {
// given
let manifest_dir = manifest_path.directory().unwrap();
// mock existing `dylint-link` binary
create_executable(&manifest_dir.join("dylint-link"), "#!/bin/sh\nexit 0");
// mock a non-existing `cargo dylint` installation.
create_executable(&manifest_dir.join("cargo"), "#!/bin/sh\nexit 1");
// when
let args = crate::cmd::build::ExecuteArgs {
manifest_path,
..Default::default()
};
let res = super::execute(args).map(|_| ()).unwrap_err();
// then
assert!(format!("{:?}", res).contains("cargo-dylint was not found!"));
Ok(())
})
}
}