contract_build/
lib.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17#![doc = include_str!("../README.md")]
18#![deny(unused_crate_dependencies)]
19
20use contract_metadata::{
21    compatibility::check_contract_ink_compatibility,
22    ContractMetadata,
23};
24use which as _;
25
26mod args;
27mod crate_metadata;
28mod docker;
29pub mod metadata;
30mod new;
31mod post_process_wasm;
32#[cfg(test)]
33mod tests;
34pub mod util;
35mod validate_wasm;
36mod wasm_opt;
37mod workspace;
38
39#[deprecated(since = "2.0.2", note = "Use MetadataArtifacts instead")]
40pub use self::metadata::MetadataArtifacts as MetadataResult;
41
42pub use self::{
43    args::{
44        BuildArtifacts,
45        BuildMode,
46        Features,
47        Network,
48        OutputType,
49        Target,
50        UnstableFlags,
51        UnstableOptions,
52        Verbosity,
53        VerbosityFlags,
54    },
55    crate_metadata::CrateMetadata,
56    metadata::{
57        BuildInfo,
58        MetadataArtifacts,
59        WasmOptSettings,
60    },
61    new::new_contract_project,
62    post_process_wasm::{
63        load_module,
64        post_process_wasm,
65    },
66    util::DEFAULT_KEY_COL_WIDTH,
67    wasm_opt::{
68        OptimizationPasses,
69        OptimizationResult,
70    },
71    workspace::{
72        Lto,
73        Manifest,
74        ManifestPath,
75        OptLevel,
76        PanicStrategy,
77        Profile,
78        Workspace,
79    },
80};
81
82use crate::wasm_opt::WasmOptHandler;
83pub use docker::{
84    docker_build,
85    ImageVariant,
86};
87
88use anyhow::{
89    Context,
90    Result,
91};
92use colored::Colorize;
93use semver::Version;
94use std::{
95    fs,
96    path::{
97        Path,
98        PathBuf,
99    },
100    process::Command,
101    str,
102};
103use strum::IntoEnumIterator;
104
105/// This is the default maximum number of pages available for a contract to allocate.
106pub const DEFAULT_MAX_MEMORY_PAGES: u64 = 16;
107
108/// Version of the currently executing `cargo-contract` binary.
109const VERSION: &str = env!("CARGO_PKG_VERSION");
110
111/// Configuration of the linting module.
112///
113/// Ensure it is kept up-to-date when updating `cargo-contract`.
114pub(crate) mod linting {
115    /// Toolchain used to build ink_linting:
116    /// https://github.com/use-ink/ink/blob/master/linting/rust-toolchain.toml
117    pub const TOOLCHAIN_VERSION: &str = "nightly-2024-09-05";
118    /// Git repository with ink_linting libraries
119    pub const GIT_URL: &str = "https://github.com/use-ink/ink/";
120    /// Git revision number of the linting crate
121    pub const GIT_REV: &str = "5ec034ca05e1239371e1d1c904d7580b375da9ca";
122}
123
124/// Arguments to use when executing `build` or `check` commands.
125#[derive(Clone)]
126pub struct ExecuteArgs {
127    /// The location of the Cargo manifest (`Cargo.toml`) file to use.
128    pub manifest_path: ManifestPath,
129    pub verbosity: Verbosity,
130    pub build_mode: BuildMode,
131    pub features: Features,
132    pub network: Network,
133    pub build_artifact: BuildArtifacts,
134    pub unstable_flags: UnstableFlags,
135    pub optimization_passes: Option<OptimizationPasses>,
136    pub keep_debug_symbols: bool,
137    pub extra_lints: bool,
138    pub output_type: OutputType,
139    pub skip_wasm_validation: bool,
140    pub target: Target,
141    pub max_memory_pages: u64,
142    pub image: ImageVariant,
143}
144
145impl Default for ExecuteArgs {
146    fn default() -> Self {
147        Self {
148            manifest_path: Default::default(),
149            verbosity: Default::default(),
150            build_mode: Default::default(),
151            features: Default::default(),
152            network: Default::default(),
153            build_artifact: Default::default(),
154            unstable_flags: Default::default(),
155            optimization_passes: Default::default(),
156            keep_debug_symbols: Default::default(),
157            extra_lints: Default::default(),
158            output_type: Default::default(),
159            skip_wasm_validation: Default::default(),
160            target: Default::default(),
161            max_memory_pages: DEFAULT_MAX_MEMORY_PAGES,
162            image: Default::default(),
163        }
164    }
165}
166
167/// Result of the build process.
168#[derive(serde::Serialize, serde::Deserialize)]
169pub struct BuildResult {
170    /// Path to the resulting Wasm file.
171    pub dest_wasm: Option<PathBuf>,
172    /// Result of the metadata generation.
173    pub metadata_result: Option<MetadataArtifacts>,
174    /// Path to the directory where output files are written to.
175    pub target_directory: PathBuf,
176    /// If existent the result of the optimization.
177    pub optimization_result: Option<OptimizationResult>,
178    /// The mode to build the contract in.
179    pub build_mode: BuildMode,
180    /// Which build artifacts were generated.
181    pub build_artifact: BuildArtifacts,
182    /// The verbosity flags.
183    pub verbosity: Verbosity,
184    /// Image used for the verifiable build
185    pub image: Option<String>,
186    /// The type of formatting to use for the build output.
187    #[serde(skip_serializing, skip_deserializing)]
188    pub output_type: OutputType,
189}
190
191impl BuildResult {
192    pub fn display(&self) -> String {
193        let opt_size_diff = if let Some(ref opt_result) = self.optimization_result {
194            let size_diff = format!(
195                "\nOriginal wasm size: {}, Optimized: {}\n\n",
196                format!("{:.1}K", opt_result.original_size).bold(),
197                format!("{:.1}K", opt_result.optimized_size).bold(),
198            );
199            debug_assert!(
200                opt_result.optimized_size > 0.0,
201                "optimized file size must be greater 0"
202            );
203            size_diff
204        } else {
205            "\n".to_string()
206        };
207
208        let build_mode = format!(
209            "The contract was built in {} mode.\n\n",
210            format!("{}", self.build_mode).to_uppercase().bold(),
211        );
212
213        if self.build_artifact == BuildArtifacts::CodeOnly {
214            let out = format!(
215                "{}{}Your contract's code is ready. You can find it here:\n{}",
216                opt_size_diff,
217                build_mode,
218                self.dest_wasm
219                    .as_ref()
220                    .expect("wasm path must exist")
221                    .display()
222                    .to_string()
223                    .bold()
224            );
225            return out
226        };
227
228        let mut out = format!(
229            "{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n",
230            opt_size_diff,
231            build_mode,
232            self.target_directory.display().to_string().bold(),
233        );
234        if let Some(metadata_result) = self.metadata_result.as_ref() {
235            let bundle = format!(
236                "  - {} (code + metadata)\n",
237                util::base_name(&metadata_result.dest_bundle).bold()
238            );
239            out.push_str(&bundle);
240        }
241        if let Some(dest_wasm) = self.dest_wasm.as_ref() {
242            let wasm = format!(
243                "  - {} (the contract's code)\n",
244                util::base_name(dest_wasm).bold()
245            );
246            out.push_str(&wasm);
247        }
248        if let Some(metadata_result) = self.metadata_result.as_ref() {
249            let metadata = format!(
250                "  - {} (the contract's metadata)",
251                util::base_name(&metadata_result.dest_metadata).bold()
252            );
253            out.push_str(&metadata);
254        }
255        out
256    }
257
258    /// Display the build results in a pretty formatted JSON string.
259    pub fn serialize_json(&self) -> Result<String> {
260        Ok(serde_json::to_string_pretty(self)?)
261    }
262}
263
264/// Executes the supplied cargo command on the project in the specified directory,
265/// defaults to the current directory.
266///
267/// Uses the unstable cargo feature [`build-std`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std)
268/// to build the standard library with [`panic_immediate_abort`](https://github.com/johnthagen/min-sized-rust#remove-panic-string-formatting-with-panic_immediate_abort)
269/// which reduces the size of the Wasm binary by not including panic strings and
270/// formatting code.
271///
272/// # `Cargo.toml` optimizations
273///
274/// The original `Cargo.toml` will be amended to remove the `rlib` crate type in order to
275/// minimize the final Wasm binary size.
276///
277/// Preferred default `[profile.release]` settings will be added if they are missing,
278/// existing user-defined settings will be preserved.
279///
280/// The `[workspace]` will be added if it is missing to ignore `workspace` from parent
281/// `Cargo.toml`.
282///
283/// To disable this and use the original `Cargo.toml` as is then pass the `-Z
284/// original_manifest` flag.
285#[allow(clippy::too_many_arguments)]
286fn exec_cargo_for_onchain_target(
287    crate_metadata: &CrateMetadata,
288    command: &str,
289    features: &Features,
290    build_mode: &BuildMode,
291    network: &Network,
292    verbosity: &Verbosity,
293    unstable_flags: &UnstableFlags,
294    target: &Target,
295) -> Result<()> {
296    let cargo_build = |manifest_path: &ManifestPath| {
297        let target_dir = format!(
298            "--target-dir={}",
299            crate_metadata.target_directory.to_string_lossy()
300        );
301        let mut args = vec![target_dir, "--release".to_owned()];
302        args.extend(onchain_cargo_options(target));
303        network.append_to_args(&mut args);
304
305        let mut features = features.clone();
306        if build_mode == &BuildMode::Debug {
307            features.push("ink/ink-debug");
308        } else {
309            args.push("-Zbuild-std-features=panic_immediate_abort".to_owned());
310        }
311        features.append_to_args(&mut args);
312        let mut env = Vec::new();
313        if rustc_version::version_meta()?.channel == rustc_version::Channel::Stable {
314            // Allow nightly features on a stable toolchain
315            env.push(("RUSTC_BOOTSTRAP", Some("1".to_string())))
316        }
317
318        // merge target specific flags with the common flags (defined here)
319        // We want to disable warnings here as they will be duplicates of the clippy pass.
320        // However, if we want to do so with either `--cap-lints allow` or  `-A
321        // warnings` the build will fail. It seems that the cross compilation
322        // depends on some warning to be enabled. Until we figure that out we need
323        // to live with duplicated warnings. For the metadata build we can disable
324        // warnings.
325        let rustflags = {
326            let common_flags = "-Clinker-plugin-lto";
327            if let Some(target_flags) = target.rustflags() {
328                format!("{}\x1f{}", common_flags, target_flags)
329            } else {
330                common_flags.to_string()
331            }
332        };
333
334        // the linker needs our linker script as file
335        if matches!(target, Target::RiscV) {
336            fs::create_dir_all(&crate_metadata.target_directory)?;
337            let path = crate_metadata
338                .target_directory
339                .join(".riscv_memory_layout.ld");
340            fs::write(&path, include_bytes!("../riscv_memory_layout.ld"))?;
341            let path = path.display();
342            env.push((
343                "CARGO_ENCODED_RUSTFLAGS",
344                Some(format!("{rustflags}\x1f-Clink-arg=-T{path}",)),
345            ));
346        } else {
347            env.push(("CARGO_ENCODED_RUSTFLAGS", Some(rustflags)));
348        };
349
350        execute_cargo(util::cargo_cmd(
351            command,
352            &args,
353            manifest_path.directory(),
354            *verbosity,
355            env,
356        ))
357    };
358
359    if unstable_flags.original_manifest {
360        verbose_eprintln!(
361            verbosity,
362            "{} {}",
363            "warning:".yellow().bold(),
364            "with 'original-manifest' enabled, the contract binary may not be of optimal size."
365                .bold()
366        );
367        cargo_build(&crate_metadata.manifest_path)?;
368    } else {
369        Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
370            .with_root_package_manifest(|manifest| {
371                manifest
372                    .with_replaced_lib_to_bin()?
373                    .with_profile_release_defaults(Profile::default_contract_release())?
374                    .with_merged_workspace_dependencies(crate_metadata)?
375                    .with_empty_workspace();
376                Ok(())
377            })?
378            .using_temp(cargo_build)?;
379    }
380
381    Ok(())
382}
383
384/// Check if the `INK_STATIC_BUFFER_SIZE` is set.
385/// If so, then checks if the current contract has already been compiled with a new value.
386/// If not, or metadata is not present, we need to clean binaries and rebuild.
387fn check_buffer_size_invoke_cargo_clean(
388    crate_metadata: &CrateMetadata,
389    verbosity: &Verbosity,
390) -> Result<()> {
391    if let Ok(buffer_size) = std::env::var("INK_STATIC_BUFFER_SIZE") {
392        let buffer_size_value: u64 = buffer_size
393            .parse()
394            .context("`INK_STATIC_BUFFER_SIZE` must have an integer value.")?;
395
396        let extract_buffer_size = |metadata_path: PathBuf| -> Result<u64> {
397            let size = ContractMetadata::load(metadata_path)
398                .context("Metadata is not present")?
399                .abi
400                // get `spec` field
401                .get("spec")
402                .context("spec field should be present in ABI.")?
403                // get `environment` field
404                .get("environment")
405                .context("environment field should be present in ABI.")?
406                // get `staticBufferSize` field
407                .get("staticBufferSize")
408                .context("`staticBufferSize` must be specified.")?
409                // convert to u64
410                .as_u64()
411                .context("`staticBufferSize` value must be an integer.")?;
412
413            Ok(size)
414        };
415
416        let cargo = util::cargo_cmd(
417            "clean",
418            Vec::<&str>::new(),
419            crate_metadata.manifest_path.directory(),
420            *verbosity,
421            vec![],
422        );
423
424        match extract_buffer_size(crate_metadata.metadata_path()) {
425            Ok(contract_buffer_size) if contract_buffer_size == buffer_size_value => {
426                verbose_eprintln!(
427                    verbosity,
428                    "{} {}",
429                    "info:".green().bold(),
430                    "Detected a configured buffer size, but the value is already specified."
431                        .bold()
432                );
433            }
434            Ok(_) => {
435                verbose_eprintln!(
436                    verbosity,
437                    "{} {}",
438                    "warning:".yellow().bold(),
439                    "Detected a change in the configured buffer size. Rebuilding the project."
440                        .bold()
441                );
442                execute_cargo(cargo)?;
443            }
444            Err(_) => {
445                verbose_eprintln!(
446                    verbosity,
447                    "{} {}",
448                    "warning:".yellow().bold(),
449                    "Cannot find the previous size of the static buffer. Rebuilding the project."
450                        .bold()
451                );
452                execute_cargo(cargo)?;
453            }
454        }
455    }
456    Ok(())
457}
458
459/// Executes the supplied cargo command, reading the output and scanning for known errors.
460/// Writes the captured stderr back to stderr and maintains the cargo tty progress bar.
461fn execute_cargo(cargo: duct::Expression) -> Result<()> {
462    match cargo.unchecked().run() {
463        Ok(out) if out.status.success() => Ok(()),
464        Ok(out) => anyhow::bail!(String::from_utf8_lossy(&out.stderr).to_string()),
465        Err(e) => anyhow::bail!("Cannot run `cargo` command: {:?}", e),
466    }
467}
468
469/// Run linting that involves two steps: `clippy` and `dylint`. Both are mandatory as
470/// they're part of the compilation process and implement security-critical features.
471fn lint(
472    extra_lints: bool,
473    crate_metadata: &CrateMetadata,
474    target: &Target,
475    verbosity: &Verbosity,
476) -> Result<()> {
477    verbose_eprintln!(
478        verbosity,
479        " {} {}",
480        "[==]".bold(),
481        "Checking clippy linting rules".bright_green().bold()
482    );
483    exec_cargo_clippy(crate_metadata, *verbosity)?;
484
485    // TODO (jubnzv): Dylint needs a custom toolchain installed by the user. Currently,
486    // it's required only for RiscV target. We're working on the toolchain integration
487    // and will make this step mandatory for all targets in future releases.
488    if extra_lints || matches!(target, Target::RiscV) {
489        verbose_eprintln!(
490            verbosity,
491            " {} {}",
492            "[==]".bold(),
493            "Checking ink! linting rules".bright_green().bold()
494        );
495        exec_cargo_dylint(extra_lints, crate_metadata, target, *verbosity)?;
496    }
497
498    Ok(())
499}
500
501/// Run cargo clippy on the unmodified manifest.
502fn exec_cargo_clippy(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Result<()> {
503    let args = [
504        "--all-features",
505        // customize clippy lints after the "--"
506        "--",
507        // these are hard errors because we want to guarantee that implicit overflows
508        // and lossy integer conversions never happen
509        // See https://github.com/use-ink/cargo-contract/pull/1190
510        "-Dclippy::arithmetic_side_effects",
511        // See https://github.com/use-ink/cargo-contract/pull/1895
512        "-Dclippy::cast_possible_truncation",
513        "-Dclippy::cast_possible_wrap",
514        "-Dclippy::cast_sign_loss",
515    ];
516    // we execute clippy with the plain manifest no temp dir required
517    execute_cargo(util::cargo_cmd(
518        "clippy",
519        args,
520        crate_metadata.manifest_path.directory(),
521        verbosity,
522        vec![],
523    ))
524}
525
526/// Returns a list of cargo options used for on-chain builds
527fn onchain_cargo_options(target: &Target) -> Vec<String> {
528    vec![
529        format!("--target={}", target.llvm_target()),
530        "-Zbuild-std=core,alloc".to_owned(),
531        "--no-default-features".to_owned(),
532    ]
533}
534
535/// Inject our custom lints into the manifest and execute `cargo dylint` .
536///
537/// We create a temporary folder, extract the linting driver there and run
538/// `cargo dylint` with it.
539fn exec_cargo_dylint(
540    extra_lints: bool,
541    crate_metadata: &CrateMetadata,
542    target: &Target,
543    verbosity: Verbosity,
544) -> Result<()> {
545    check_dylint_requirements(crate_metadata.manifest_path.directory())?;
546
547    // `dylint` is verbose by default, it doesn't have a `--verbose` argument,
548    let verbosity = match verbosity {
549        Verbosity::Verbose => Verbosity::Default,
550        Verbosity::Default | Verbosity::Quiet => Verbosity::Quiet,
551    };
552
553    let mut args = if extra_lints {
554        vec![
555            "--lib=ink_linting_mandatory".to_owned(),
556            "--lib=ink_linting".to_owned(),
557        ]
558    } else {
559        vec!["--lib=ink_linting_mandatory".to_owned()]
560    };
561    args.push("--".to_owned());
562    // Pass on-chain build options to ensure the linter expands all conditional `cfg_attr`
563    // macros, as it does for the release build.
564    args.extend(onchain_cargo_options(target));
565
566    let target_dir = &crate_metadata.target_directory.to_string_lossy();
567    let env = vec![
568        // We need to set the `CARGO_TARGET_DIR` environment variable in
569        // case `cargo dylint` is invoked.
570        //
571        // This is because we build from a temporary directory (to patch the manifest)
572        // but still want the output to live at a fixed path. `cargo dylint` does
573        // not accept this information on the command line.
574        ("CARGO_TARGET_DIR", Some(target_dir.to_string())),
575        // There are generally problems with having a custom `rustc` wrapper, while
576        // executing `dylint` (which has a custom linker). Especially for `sccache`
577        // there is this bug: https://github.com/mozilla/sccache/issues/1000.
578        // Until we have a justification for leaving the wrapper we should unset it.
579        ("RUSTC_WRAPPER", None),
580    ];
581
582    Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
583        .with_root_package_manifest(|manifest| {
584            manifest.with_dylint()?;
585            Ok(())
586        })?
587        .using_temp(|manifest_path| {
588            let cargo = util::cargo_cmd(
589                "dylint",
590                &args,
591                manifest_path.directory(),
592                verbosity,
593                env,
594            );
595            cargo.run()?;
596            Ok(())
597        })?;
598
599    Ok(())
600}
601
602/// Checks if all requirements for `dylint` are installed.
603///
604/// We require both `cargo-dylint` and `dylint-link` because the driver is being
605/// built at runtime on demand. These must be built using a custom version of the
606/// toolchain, as the linter utilizes the unstable rustc API.
607///
608/// This function takes a `_working_dir` which is only used for unit tests.
609fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> {
610    let execute_cmd = |cmd: &mut Command| {
611        let mut child = if let Ok(child) = cmd
612            .stdout(std::process::Stdio::null())
613            .stderr(std::process::Stdio::null())
614            .spawn()
615        {
616            child
617        } else {
618            tracing::debug!("Error spawning `{:?}`", cmd);
619            return false
620        };
621
622        child.wait().map(|ret| ret.success()).unwrap_or_else(|err| {
623            tracing::debug!("Error waiting for `{:?}`: {:?}", cmd, err);
624            false
625        })
626    };
627
628    // Check if the required toolchain is present and is installed with `rustup`.
629    if let Ok(output) = Command::new("rustup").arg("toolchain").arg("list").output() {
630        anyhow::ensure!(
631            String::from_utf8_lossy(&output.stdout).contains(linting::TOOLCHAIN_VERSION),
632            format!(
633                "Toolchain `{0}` was not found!\n\
634                This specific version is required to provide additional source code analysis.\n\n\
635                You can install it by executing:\n\
636                  rustup install {0}\n\
637                  rustup component add rust-src --toolchain {0}\n\
638                  rustup run {0} cargo install cargo-dylint dylint-link",
639                linting::TOOLCHAIN_VERSION,
640            )
641            .to_string()
642            .bright_yellow());
643    } else {
644        anyhow::bail!(format!(
645            "Toolchain `{0}` was not found!\n\
646            This specific version is required to provide additional source code analysis.\n\n\
647            Install `rustup` according to https://rustup.rs/ and then run:\
648              rustup install {0}\n\
649              rustup component add rust-src --toolchain {0}\n\
650              rustup run {0} cargo install cargo-dylint dylint-link",
651            linting::TOOLCHAIN_VERSION,
652        )
653        .to_string()
654        .bright_yellow());
655    }
656
657    // when testing this function we should never fall back to a `cargo` specified
658    // in the env variable, as this would mess with the mocked binaries.
659    #[cfg(not(test))]
660    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
661    #[cfg(test)]
662    let cargo = "cargo";
663
664    if !execute_cmd(Command::new(cargo).arg("dylint").arg("--version")) {
665        anyhow::bail!("cargo-dylint was not found!\n\
666            Make sure it is installed and the binary is in your PATH environment.\n\n\
667            You can install it by executing `cargo install cargo-dylint`."
668            .to_string()
669            .bright_yellow());
670    }
671
672    // On windows we cannot just run the linker with --version as there is no command
673    // which just outputs some information. It always needs to do some linking in
674    // order to return successful exit code.
675    #[cfg(windows)]
676    let dylint_link_found = which::which("dylint-link").is_ok();
677    #[cfg(not(windows))]
678    let dylint_link_found = execute_cmd(Command::new("dylint-link").arg("--version"));
679    if !dylint_link_found {
680        anyhow::bail!("dylint-link was not found!\n\
681            Make sure it is installed and the binary is in your PATH environment.\n\n\
682            You can install it by executing `cargo install dylint-link`."
683            .to_string()
684            .bright_yellow());
685    }
686
687    Ok(())
688}
689
690/// Checks whether the supplied `ink_version` already contains the debug feature.
691///
692/// This feature was introduced in `3.0.0-rc4` with `ink_env/ink-debug`.
693pub fn assert_debug_mode_supported(ink_version: &Version) -> Result<()> {
694    tracing::debug!("Contract version: {:?}", ink_version);
695    let minimum_version = Version::parse("3.0.0-rc4").expect("parsing version failed");
696    if ink_version < &minimum_version {
697        anyhow::bail!(
698            "Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
699        );
700    }
701    Ok(())
702}
703
704/// Executes build of the smart contract which produces a Wasm binary that is ready for
705/// deploying.
706///
707/// It does so by invoking `cargo build` and then post processing the final binary.
708pub fn execute(args: ExecuteArgs) -> Result<BuildResult> {
709    let ExecuteArgs {
710        manifest_path,
711        verbosity,
712        features,
713        build_mode,
714        network,
715        build_artifact,
716        unstable_flags,
717        optimization_passes,
718        extra_lints,
719        output_type,
720        target,
721        ..
722    } = &args;
723
724    // if image exists, then --verifiable was called and we need to build inside docker.
725    if build_mode == &BuildMode::Verifiable {
726        return docker_build(args)
727    }
728
729    // The CLI flag `optimization-passes` overwrites optimization passes which are
730    // potentially defined in the `Cargo.toml` profile.
731    let optimization_passes = match optimization_passes {
732        Some(opt_passes) => *opt_passes,
733        None => {
734            let mut manifest = Manifest::new(manifest_path.clone())?;
735            // if no setting is found, neither on the cli nor in the profile,
736            // then we use the default
737            manifest.profile_optimization_passes().unwrap_or_default()
738        }
739    };
740
741    let crate_metadata = CrateMetadata::collect(manifest_path, *target)?;
742
743    if build_mode == &BuildMode::Debug {
744        assert_debug_mode_supported(&crate_metadata.ink_version)?;
745    }
746
747    if let Err(e) = check_contract_ink_compatibility(&crate_metadata.ink_version, None) {
748        eprintln!("{} {}", "warning:".yellow().bold(), e.to_string().bold());
749    }
750
751    let clean_metadata = || {
752        fs::remove_file(crate_metadata.metadata_path()).ok();
753        fs::remove_file(crate_metadata.contract_bundle_path()).ok();
754    };
755
756    let (opt_result, metadata_result, dest_wasm) = match build_artifact {
757        BuildArtifacts::CheckOnly => {
758            // Check basically means only running our linter without building.
759            lint(*extra_lints, &crate_metadata, target, verbosity)?;
760            (None, None, None)
761        }
762        BuildArtifacts::CodeOnly => {
763            // when building only the code metadata will become stale
764            clean_metadata();
765            let (opt_result, _, dest_wasm) =
766                local_build(&crate_metadata, &optimization_passes, &args)?;
767            (opt_result, None, Some(dest_wasm))
768        }
769        BuildArtifacts::All => {
770            let (opt_result, build_info, dest_wasm) =
771                local_build(&crate_metadata, &optimization_passes, &args).inspect_err(
772                    |_| {
773                        // build error -> bundle is stale
774                        clean_metadata();
775                    },
776                )?;
777
778            let metadata_result = MetadataArtifacts {
779                dest_metadata: crate_metadata.metadata_path(),
780                dest_bundle: crate_metadata.contract_bundle_path(),
781            };
782
783            // skip metadata generation if contract unchanged and all metadata artifacts
784            // exist.
785            if opt_result.is_some()
786                || !metadata_result.dest_metadata.exists()
787                || !metadata_result.dest_bundle.exists()
788            {
789                // if metadata build fails after a code build it might become stale
790                clean_metadata();
791                metadata::execute(
792                    &crate_metadata,
793                    dest_wasm.as_path(),
794                    &metadata_result,
795                    features,
796                    *network,
797                    *verbosity,
798                    unstable_flags,
799                    build_info,
800                )?;
801            }
802            (opt_result, Some(metadata_result), Some(dest_wasm))
803        }
804    };
805
806    Ok(BuildResult {
807        dest_wasm,
808        metadata_result,
809        target_directory: crate_metadata.target_directory,
810        optimization_result: opt_result,
811        build_mode: *build_mode,
812        build_artifact: *build_artifact,
813        verbosity: *verbosity,
814        image: None,
815        output_type: output_type.clone(),
816    })
817}
818
819/// Build the contract on host locally
820fn local_build(
821    crate_metadata: &CrateMetadata,
822    optimization_passes: &OptimizationPasses,
823    args: &ExecuteArgs,
824) -> Result<(Option<OptimizationResult>, BuildInfo, PathBuf)> {
825    let ExecuteArgs {
826        verbosity,
827        features,
828        build_mode,
829        network,
830        unstable_flags,
831        keep_debug_symbols,
832        extra_lints,
833        skip_wasm_validation,
834        target,
835        max_memory_pages,
836        ..
837    } = args;
838
839    // We always want to lint first so we don't suppress any warnings when a build is
840    // skipped because of a matching fingerprint.
841    lint(*extra_lints, crate_metadata, target, verbosity)?;
842
843    let pre_fingerprint = Fingerprint::new(crate_metadata)?;
844
845    verbose_eprintln!(
846        verbosity,
847        " {} {}",
848        "[==]".bold(),
849        "Building cargo project".bright_green().bold()
850    );
851    check_buffer_size_invoke_cargo_clean(crate_metadata, verbosity)?;
852    exec_cargo_for_onchain_target(
853        crate_metadata,
854        "build",
855        features,
856        build_mode,
857        network,
858        verbosity,
859        unstable_flags,
860        target,
861    )?;
862
863    // We persist the latest target we used so we trigger a rebuild when we switch
864    fs::write(&crate_metadata.target_file_path, target.llvm_target())?;
865
866    let cargo_contract_version = if let Ok(version) = Version::parse(VERSION) {
867        version
868    } else {
869        anyhow::bail!(
870            "Unable to parse version number for the currently running \
871                    `cargo-contract` binary."
872        );
873    };
874
875    let build_info = BuildInfo {
876        rust_toolchain: util::rust_toolchain()?,
877        cargo_contract_version,
878        build_mode: *build_mode,
879        wasm_opt_settings: WasmOptSettings {
880            optimization_passes: *optimization_passes,
881            keep_debug_symbols: *keep_debug_symbols,
882        },
883    };
884
885    let post_fingerprint = Fingerprint::new(crate_metadata)?.ok_or_else(|| {
886        anyhow::anyhow!(
887            "Expected '{}' to be generated by build",
888            crate_metadata.original_code.display()
889        )
890    })?;
891
892    tracing::debug!(
893        "Fingerprint before build: {:?}, after build: {:?}",
894        pre_fingerprint,
895        post_fingerprint
896    );
897
898    let dest_code_path = crate_metadata.dest_code.clone();
899
900    if pre_fingerprint == Some(post_fingerprint) && crate_metadata.dest_code.exists() {
901        tracing::info!(
902            "No changes in the original wasm at {}, fingerprint {:?}. \
903                Skipping Wasm optimization and metadata generation.",
904            crate_metadata.original_code.display(),
905            pre_fingerprint
906        );
907        return Ok((None, build_info, dest_code_path))
908    }
909
910    verbose_eprintln!(
911        verbosity,
912        " {} {}",
913        "[==]".bold(),
914        "Post processing code".bright_green().bold()
915    );
916
917    // remove build artifacts so we don't have anything stale lingering around
918    for t in Target::iter() {
919        fs::remove_file(crate_metadata.dest_code.with_extension(t.dest_extension())).ok();
920    }
921
922    let original_size =
923        fs::metadata(&crate_metadata.original_code)?.len() as f64 / 1000.0;
924
925    match target {
926        Target::Wasm => {
927            let handler = WasmOptHandler::new(*optimization_passes, *keep_debug_symbols)?;
928            handler.optimize(&crate_metadata.original_code, &crate_metadata.dest_code)?;
929            post_process_wasm(
930                &crate_metadata.dest_code,
931                *skip_wasm_validation,
932                verbosity,
933                *max_memory_pages,
934            )?;
935        }
936        Target::RiscV => {
937            fs::copy(&crate_metadata.original_code, &crate_metadata.dest_code)?;
938        }
939    }
940
941    let optimized_size = fs::metadata(&dest_code_path)?.len() as f64 / 1000.0;
942
943    let optimization_result = OptimizationResult {
944        original_size,
945        optimized_size,
946    };
947
948    Ok((
949        Some(optimization_result),
950        build_info,
951        crate_metadata.dest_code.clone(),
952    ))
953}
954
955/// Unique fingerprint for a file to detect whether it has changed.
956#[derive(Debug, Eq, PartialEq)]
957struct Fingerprint {
958    path: PathBuf,
959    hash: [u8; 32],
960    modified: std::time::SystemTime,
961    target: String,
962}
963
964impl Fingerprint {
965    fn new(crate_metadata: &CrateMetadata) -> Result<Option<Fingerprint>> {
966        let code_path = &crate_metadata.original_code;
967        let target_path = &crate_metadata.target_file_path;
968        if code_path.exists() {
969            let modified = fs::metadata(code_path)?.modified()?;
970            let bytes = fs::read(code_path)?;
971            let hash = blake2_hash(&bytes);
972            Ok(Some(Self {
973                path: code_path.clone(),
974                hash,
975                modified,
976                target: fs::read_to_string(target_path).with_context(|| {
977                    format!(
978                        "Cannot read {}.\n A clean build will fix this.",
979                        target_path.display()
980                    )
981                })?,
982            }))
983        } else {
984            Ok(None)
985        }
986    }
987}
988
989/// Returns the blake2 hash of the code slice.
990pub fn code_hash(code: &[u8]) -> [u8; 32] {
991    blake2_hash(code)
992}
993
994/// Returns the blake2 hash of the given bytes.
995fn blake2_hash(code: &[u8]) -> [u8; 32] {
996    use blake2::digest::{
997        consts::U32,
998        Digest as _,
999    };
1000    let mut blake2 = blake2::Blake2b::<U32>::new();
1001    blake2.update(code);
1002    let result = blake2.finalize();
1003    result.into()
1004}
1005
1006/// Testing individual functions where the build itself is not actually invoked. See
1007/// [`tests`] for all tests which invoke the `build` command.
1008#[cfg(test)]
1009mod unit_tests {
1010    use super::*;
1011    use crate::Verbosity;
1012    use semver::Version;
1013
1014    #[test]
1015    pub fn debug_mode_must_be_compatible() {
1016        assert_debug_mode_supported(
1017            &Version::parse("3.0.0-rc4").expect("parsing must work"),
1018        )
1019        .expect("debug mode must be compatible");
1020        assert_debug_mode_supported(
1021            &Version::parse("4.0.0-rc1").expect("parsing must work"),
1022        )
1023        .expect("debug mode must be compatible");
1024        assert_debug_mode_supported(&Version::parse("5.0.0").expect("parsing must work"))
1025            .expect("debug mode must be compatible");
1026    }
1027
1028    #[test]
1029    pub fn debug_mode_must_be_incompatible() {
1030        let res = assert_debug_mode_supported(
1031            &Version::parse("3.0.0-rc3").expect("parsing must work"),
1032        )
1033        .expect_err("assertion must fail");
1034        assert_eq!(
1035            res.to_string(),
1036            "Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
1037        );
1038    }
1039
1040    #[test]
1041    fn build_result_seralization_sanity_check() {
1042        // given
1043        let raw_result = r#"{
1044  "dest_wasm": "/path/to/contract.wasm",
1045  "metadata_result": {
1046    "dest_metadata": "/path/to/contract.json",
1047    "dest_bundle": "/path/to/contract.contract"
1048  },
1049  "target_directory": "/path/to/target",
1050  "optimization_result": {
1051    "original_size": 64.0,
1052    "optimized_size": 32.0
1053  },
1054  "build_mode": "Debug",
1055  "build_artifact": "All",
1056  "verbosity": "Quiet",
1057  "image": null
1058}"#;
1059
1060        let build_result = BuildResult {
1061            dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")),
1062            metadata_result: Some(MetadataArtifacts {
1063                dest_metadata: PathBuf::from("/path/to/contract.json"),
1064                dest_bundle: PathBuf::from("/path/to/contract.contract"),
1065            }),
1066            target_directory: PathBuf::from("/path/to/target"),
1067            optimization_result: Some(OptimizationResult {
1068                original_size: 64.0,
1069                optimized_size: 32.0,
1070            }),
1071            build_mode: Default::default(),
1072            build_artifact: Default::default(),
1073            image: None,
1074            verbosity: Verbosity::Quiet,
1075            output_type: OutputType::Json,
1076        };
1077
1078        // when
1079        let serialized_result = build_result.serialize_json();
1080
1081        // then
1082        assert!(serialized_result.is_ok());
1083        assert_eq!(serialized_result.unwrap(), raw_result);
1084    }
1085}