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