build.rs 34 KiB
Newer Older
Andrew Jones's avatar
Andrew Jones committed
// 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 <http://www.gnu.org/licenses/>.
use std::{convert::TryFrom, ffi::OsStr, fs::metadata, path::PathBuf};

#[cfg(feature = "binaryen-as-dependency")]
Hero Bird's avatar
Hero Bird committed

#[cfg(any(test, not(feature = "binaryen-as-dependency")))]
use std::{path::Path, process::Command, str};
    crate_metadata::CrateMetadata,
    maybe_println, util, validate_wasm,
    workspace::{Manifest, ManifestPath, Profile, Workspace},
    BuildArtifacts, BuildResult, OptimizationPasses, OptimizationResult, UnstableFlags,
    UnstableOptions, Verbosity, VerbosityFlags,
use anyhow::{Context, Result};
use colored::Colorize;
use parity_wasm::elements::{External, MemoryType, Module, Section};
use structopt::StructOpt;

/// This is the maximum number of pages available for a contract to allocate.
const MAX_MEMORY_PAGES: u32 = 16;

/// 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<PathBuf>,
    /// Which build artifacts to generate.
    ///
    /// - `all`: Generate the Wasm, the metadata and a bundled `<name>.contract` file.
    ///
    /// - `code-only`: Only the Wasm is created, generation of metadata and a bundled
    ///   `<name>.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 `3`
    ///
    /// - 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")]
    optimization_passes: Option<OptimizationPasses>,
}

impl BuildCommand {
    pub fn exec(&self) -> Result<BuildResult> {
        let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?;
        let unstable_flags: UnstableFlags =
            TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?;
        let 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,
                }
            }
        };

        execute(
            &manifest_path,
            verbosity,
            self.build_artifact,
            unstable_flags,
            optimization_passes,
        )
    }
}

#[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<PathBuf>,
    #[structopt(flatten)]
    verbosity: VerbosityFlags,
    #[structopt(flatten)]
    unstable_options: UnstableOptions,
}

impl CheckCommand {
    pub fn exec(&self) -> Result<BuildResult> {
        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)?;
        execute(
            &manifest_path,
            verbosity,
            BuildArtifacts::CheckOnly,
            unstable_flags,
/// 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.
///
/// 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,
    verbosity: Verbosity,
    unstable_flags: &UnstableFlags,
    // 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",
    );

    let cargo_build = |manifest_path: &ManifestPath| {
        let target_dir = &crate_metadata.target_directory;
        let args = [
            "--target=wasm32-unknown-unknown",
            "-Zbuild-std",
            "-Zbuild-std-features=panic_immediate_abort",
            "--no-default-features",
            "--release",
            &format!("--target-dir={}", target_dir.to_string_lossy()),
        ];
        util::invoke_cargo(command, &args, manifest_path.directory(), verbosity)?;
    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())?;
            .using_temp(cargo_build)?;
    // clear RUSTFLAGS
    std::env::remove_var("RUSTFLAGS");

}

/// 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.
fn strip_custom_sections(module: &mut Module) {
    module.sections_mut().retain(|section| {
        !matches!(
            section,
            Section::Custom(_) | Section::Name(_) | Section::Reloc(_)
        )
    });
}

/// 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");
    }
    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,
) -> Result<OptimizationResult> {
    let mut dest_optimized = crate_metadata.dest_wasm.clone();
    dest_optimized.set_file_name(format!("{}-opt.wasm", crate_metadata.package_name));
    let _ = do_optimization(
        crate_metadata.dest_wasm.as_os_str(),
        &dest_optimized.as_os_str(),
        optimization_passes,
    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)?;
        dest_wasm: crate_metadata.dest_wasm.clone(),
/// Optimizes the Wasm supplied as `wasm` using the `binaryen-rs` dependency.
///
/// 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`.
#[cfg(feature = "binaryen-as-dependency")]
fn do_optimization(
    optimization_level: OptimizationPasses,
) -> Result<()> {
    let mut dest_wasm_file = File::open(dest_wasm)?;
    let mut dest_wasm_file_content = Vec::new();
    dest_wasm_file.read_to_end(&mut dest_wasm_file_content)?;

    let codegen_config = binaryen::CodegenConfig {
        // Number of optimization passes (spends potentially a lot of time optimizing)
        optimization_level: optimization_level.to_passes(),
Michael Müller's avatar
Michael Müller committed
        // The default
        shrink_level: optimization_level.to_shrink(),
Michael Müller's avatar
Michael Müller committed
        // The default
    log::info!(
        "Optimization level passed to `binaryen` dependency: {}",
        codegen_config.optimization_level
    );
    log::info!(
        "Shrink level passed to `binaryen` dependency: {}",
        codegen_config.shrink_level
    );
    let mut module = binaryen::Module::read(&dest_wasm_file_content)
        .map_err(|_| anyhow::anyhow!("binaryen failed to read file content"))?;

    if optimization_level != OptimizationPasses::Zero {
        // binaryen-rs still uses the default optimization passes, even if zero
        // is passed. this is the ticket for it: https://github.com/pepyakin/binaryen-rs/issues/56.
        // we can remove the if condition here once the issue is fixed.
        module.optimize(&codegen_config);
    }

    let mut optimized_wasm_file = File::create(dest_optimized)?;
    optimized_wasm_file.write_all(&module.write())?;

    Ok(())
}

/// 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`.
#[cfg(not(feature = "binaryen-as-dependency"))]
fn do_optimization(
    optimization_level: OptimizationPasses,
    // check `wasm-opt` is installed
    let which = which::which("wasm-opt");
    if which.is_err() {
            "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()
    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 _ = check_wasm_opt_version_compatibility(wasm_opt_path)?;
    log::info!(
        "Optimization level passed to wasm-opt: {}",
        optimization_level
    );
    let output = Command::new(wasm_opt_path)
        .arg(format!("-O{}", optimization_level))
        // 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")
        .output()
        .map_err(|err| {
            anyhow::anyhow!(
                "Executing {} failed with {:?}",
                wasm_opt_path.display(),
                err
            )
        })?;
        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.
#[cfg(any(test, not(feature = "binaryen-as-dependency")))]
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
        );

    // ```sh
    // $ wasm-opt --version
    // wasm-opt version 99 (version_99-79-gc12cc3f50)
    // ```
    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
        )
    })?;
    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 '{:?}'",
    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
        );
    }
/// 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).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(())
}

/// 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(
    manifest_path: &ManifestPath,
    verbosity: Verbosity,
    build_artifact: BuildArtifacts,
    unstable_flags: UnstableFlags,
    optimization_passes: OptimizationPasses,
) -> Result<BuildResult> {
    let crate_metadata = CrateMetadata::collect(&manifest_path)?;

    assert_compatible_ink_dependencies(&manifest_path, verbosity)?;

    let build = || -> Result<OptimizationResult> {
        maybe_println!(
            " {} {}",
            format!("[1/{}]", build_artifact.steps()).bold(),
            "Building cargo project".bright_green().bold()
        );
        exec_cargo_for_wasm_target(&crate_metadata, "build", verbosity, &unstable_flags)?;

        maybe_println!(
            " {} {}",
            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)?;
        Ok(optimization_result)
    };

    let (opt_result, metadata_result) = match build_artifact {
        BuildArtifacts::CheckOnly => {
            exec_cargo_for_wasm_target(&crate_metadata, "check", 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_artifact,
#[cfg(feature = "test-ci-only")]
#[cfg(test)]
    use super::{assert_compatible_ink_dependencies, check_wasm_opt_version_compatibility};
    use crate::{
        cmd::{self, BuildCommand},
        util::tests::with_tmp_dir,
        workspace::Manifest,
        BuildArtifacts, ManifestPath, OptimizationPasses, UnstableFlags, UnstableOptions,
        Verbosity, VerbosityFlags,
    use std::{
        io::Write,
        os::unix::fs::PermissionsExt,
        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: &PathBuf,
        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");
    }
    /// Creates an executable `wasm-opt-mocked` file which outputs
    /// "wasm-opt version `version`".
    ///
    /// Returns the path to this file.
    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
    }

    fn build_code_only() {
            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();
            let res = super::execute(
                Verbosity::Default,
                BuildArtifacts::CodeOnly,
Michael Müller's avatar
Michael Müller committed
                OptimizationPasses::default(),
            // 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);
    fn check_must_not_output_contract_artifacts_in_project_dir() {
        with_tmp_dir(|path| {
            // given
            cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
            let project_dir = path.join("new_project");
            let manifest_path = ManifestPath::new(&project_dir.join("Cargo.toml")).unwrap();

            // when
            super::execute(
                &manifest_path,
                Verbosity::Default,
                BuildArtifacts::CheckOnly,
                UnstableFlags::default(),
Michael Müller's avatar
Michael Müller committed
                OptimizationPasses::default(),
                !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!"
    fn optimization_passes_from_cli_must_take_precedence_over_profile() {
        with_tmp_dir(|path| {
            // given
            cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
            let cargo_toml_path = path.join("new_project").join("Cargo.toml");
            write_optimization_passes_into_manifest(&cargo_toml_path, OptimizationPasses::Three);
            let cmd = BuildCommand {
                manifest_path: Some(cargo_toml_path),
                build_artifact: BuildArtifacts::All,
                verbosity: VerbosityFlags::default(),
                unstable_options: UnstableOptions::default(),

                // we choose zero optimization passes as the "cli" parameter
                optimization_passes: Some(OptimizationPasses::Zero),
            };

            // when
            let res = cmd.exec().expect("build failed");
            let optimization = res
                .optimization_result
                .expect("no optimization result available");

            // then
            // we have to truncate here to account for a possible small delta
            // in the floating point numbers
            let optimized_size = optimization.optimized_size.trunc();
            let original_size = optimization.original_size.trunc();
            assert!(
                optimized_size == original_size,
                "The optimized size {:?} differs from the original size {:?}",
                optimized_size,
                original_size
            );

            Ok(())
        })
    }

    #[test]
    fn optimization_passes_from_profile_must_be_used() {
        with_tmp_dir(|path| {
            // given
            cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
            let cargo_toml_path = path.join("new_project").join("Cargo.toml");
            write_optimization_passes_into_manifest(&cargo_toml_path, OptimizationPasses::Three);
            let cmd = BuildCommand {
                manifest_path: Some(cargo_toml_path),
                build_artifact: BuildArtifacts::All,
                verbosity: VerbosityFlags::default(),
                unstable_options: UnstableOptions::default(),

                // we choose no optimization passes as the "cli" parameter
                optimization_passes: None,
            };

            // when
            let res = cmd.exec().expect("build failed");
            let optimization = res
                .optimization_result
                .expect("no optimization result available");

            // then
            // we have to truncate here to account for a possible small delta
            // in the floating point numbers
            let optimized_size = optimization.optimized_size.trunc();
            let original_size = optimization.original_size.trunc();
            assert!(
                optimized_size < original_size,
                "The optimized size DOES NOT {:?} differ from the original size {:?}",
                optimized_size,
                original_size
            );

            Ok(())
        })
    }

    #[test]
    fn project_template_dependencies_must_be_ink_compatible() {
        with_tmp_dir(|path| {
            // given
            cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
            let cargo_toml_path = path.join("new_project").join("Cargo.toml");
            let manifest_path =
                ManifestPath::new(&cargo_toml_path).expect("manifest path creation failed");

            // 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_tmp_dir(|path| {
            // given
            cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
            let cargo_toml_path = path.join("new_project").join("Cargo.toml");
            let manifest_path =
                ManifestPath::new(&cargo_toml_path).expect("manifest path creation failed");

            // 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(())
        })
    }
    fn incompatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
            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_eq!(
                format!("{:?}", res),
                "Err(Your wasm-opt version is 98, but we require a version >= 99.)"
            );

            Ok(())
        })
    }

    #[test]
    fn compatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
            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(())
        })
    }

    #[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_eq!(
                format!("{:?}", res),
                "Err(Your wasm-opt version is 98, but we require a version >= 99.)"
            );

            Ok(())
        })
    }

    #[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(())
        })
    }