build.rs 53.6 KiB
Newer Older
// 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 <http://www.gnu.org/licenses/>.
    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 semver::Version;
use std::{
    convert::TryFrom,
    ffi::OsStr,
    fs::metadata,

/// 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,
achimcc's avatar
achimcc committed
    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<PathBuf>,
    /// 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,
achimcc's avatar
achimcc committed
    /// Build offline
    #[clap(long = "--offline")]
achimcc's avatar
achimcc committed
    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 `<name>.contract` file.
    ///
    /// - `code-only`: Only the Wasm is created, generation of metadata and a bundled
    ///   `<name>.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.
    optimization_passes: Option<OptimizationPasses>,
    /// Do not remove symbols (Wasm name section) when optimizing.
    ///
    /// This is useful if one wants to analyze or debug the optimized binary.
    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<BuildResult> {
        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,
        };
achimcc's avatar
achimcc committed
        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,
            build_mode,
achimcc's avatar
achimcc committed
            network,
            build_artifact: self.build_artifact,
            optimization_passes,
            keep_debug_symbols: self.keep_debug_symbols,
            skip_linting: self.skip_linting,
#[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<PathBuf>,
    #[clap(flatten)]
    verbosity: VerbosityFlags,
    #[clap(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)?;

        let args = ExecuteArgs {
            manifest_path,
            build_mode: BuildMode::Debug,
achimcc's avatar
achimcc committed
            network: Network::default(),
            build_artifact: BuildArtifacts::CheckOnly,
            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,
achimcc's avatar
achimcc committed
    network: Network,
    verbosity: Verbosity,
    unstable_flags: &UnstableFlags,
    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,
achimcc's avatar
achimcc committed
        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)?;
    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()?;
            .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());
    }
/// 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<P: AsRef<Path>>(path: P) -> Result<Module> {
    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<OptimizationResult> {
    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)?;
        dest_wasm: crate_metadata.dest_wasm.clone(),
/// 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`.
    optimization_level: OptimizationPasses,
    keep_debug_symbols: bool,
    // 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\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()
        .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 mut command = Command::new(wasm_opt_path);
    command
        .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");
    if keep_debug_symbols {
        command.arg("-g");
    }
Michael Müller's avatar
Michael Müller committed
    log::info!("Invoking wasm-opt with {:?}", command);
    let output = command.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.
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 '{:?}'",
    log::info!(
        "The wasm-opt version output is '{}', which was parsed to '{}'",
        version_stdout,
        version_number
    );
            "Your wasm-opt version is {}, but we require a version >= 99.{}",
            version_number,
            github_note,
/// 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
                )
/// 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<BuildResult> {
    let ExecuteArgs {
        manifest_path,
        verbosity,
        build_mode,
achimcc's avatar
achimcc committed
        network,
        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<OptimizationResult> {
        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,
achimcc's avatar
achimcc committed
            network,
            verbosity,
            &unstable_flags,
        )?;

        maybe_println!(
            " {} {}",
            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,
achimcc's avatar
achimcc committed
                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(),
achimcc's avatar
achimcc committed
                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,
#[cfg(feature = "test-ci-only")]
#[cfg(test)]
    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,
        },
        BuildArtifacts,
        BuildMode,
        ManifestPath,
        OptimizationPasses,
        OutputType,
        UnstableOptions,
        Verbosity,
        VerbosityFlags,
    use semver::Version;
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    /// 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: AsRef<Path>>(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) {