Skip to main content

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