diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index 4f3b0c1a7..5e3750778 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -30,6 +30,9 @@ mod validate_wasm; mod wasm_opt; mod workspace; +#[deprecated(since = "2.0.2", note = "Use MetadataArtifacts instead")] +pub use self::metadata::MetadataArtifacts as MetadataResult; + pub use self::{ args::{ BuildArtifacts, @@ -46,7 +49,7 @@ pub use self::{ crate_metadata::CrateMetadata, metadata::{ BuildInfo, - MetadataResult, + MetadataArtifacts, WasmOptSettings, }, new::new_contract_project, @@ -118,7 +121,7 @@ pub struct BuildResult { /// Path to the resulting Wasm file. pub dest_wasm: Option, /// Result of the metadata generation. - pub metadata_result: Option, + pub metadata_result: Option, /// Path to the directory where output files are written to. pub target_directory: PathBuf, /// If existent the result of the optimization. @@ -136,26 +139,20 @@ pub struct BuildResult { impl BuildResult { pub fn display(&self) -> String { - if self.optimization_result.is_none() && self.metadata_result.is_none() { - return "\nNo changes in contract detected: Wasm and metadata artifacts unchanged." - .to_string() - } - - let (opt_size_diff, newlines) = - if let Some(ref opt_result) = self.optimization_result { - let size_diff = format!( - "\nOriginal wasm size: {}, Optimized: {}\n\n", - format!("{:.1}K", opt_result.original_size).bold(), - format!("{:.1}K", opt_result.optimized_size).bold(), - ); - debug_assert!( - opt_result.optimized_size > 0.0, - "optimized file size must be greater 0" - ); - (size_diff, "\n\n") - } else { - ("\n".to_string(), "") - }; + let opt_size_diff = if let Some(ref opt_result) = self.optimization_result { + let size_diff = format!( + "\nOriginal wasm size: {}, Optimized: {}\n\n", + format!("{:.1}K", opt_result.original_size).bold(), + format!("{:.1}K", opt_result.optimized_size).bold(), + ); + debug_assert!( + opt_result.optimized_size > 0.0, + "optimized file size must be greater 0" + ); + size_diff + } else { + "\n".to_string() + }; let build_mode = format!( "The contract was built in {} mode.\n\n", @@ -178,11 +175,10 @@ impl BuildResult { }; let mut out = format!( - "{}{}Your contract artifacts are ready. You can find them in:\n{}{}", + "{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n", opt_size_diff, build_mode, self.target_directory.display().to_string().bold(), - newlines, ); if let Some(metadata_result) = self.metadata_result.as_ref() { let bundle = format!( @@ -613,28 +609,51 @@ pub fn execute(args: ExecuteArgs) -> Result { } }; - let build = || -> Result<(Option<(OptimizationResult, BuildInfo)>, BuildSteps)> { - let mut build_steps = BuildSteps::new(); - let pre_fingerprint = Fingerprint::try_from_path(&crate_metadata.original_wasm)?; + let build = + || -> Result<(Option, BuildInfo, PathBuf, BuildSteps)> { + let mut build_steps = BuildSteps::new(); + let pre_fingerprint = + Fingerprint::try_from_path(&crate_metadata.original_wasm)?; - maybe_println!( - verbosity, - " {} {}", - format!("{build_steps}").bold(), - "Building cargo project".bright_green().bold() - ); - build_steps.increment_current(); - exec_cargo_for_wasm_target( - &crate_metadata, - "build", - &features, - build_mode, - network, - verbosity, - &unstable_flags, - )?; + maybe_println!( + verbosity, + " {} {}", + format!("{build_steps}").bold(), + "Building cargo project".bright_green().bold() + ); + build_steps.increment_current(); + exec_cargo_for_wasm_target( + &crate_metadata, + "build", + &features, + build_mode, + network, + verbosity, + &unstable_flags, + )?; + + let cargo_contract_version = if let Ok(version) = Version::parse(VERSION) { + version + } else { + anyhow::bail!( + "Unable to parse version number for the currently running \ + `cargo-contract` binary." + ); + }; + + let build_info = BuildInfo { + rust_toolchain: util::rust_toolchain()?, + cargo_contract_version, + build_mode, + wasm_opt_settings: WasmOptSettings { + optimization_passes, + keep_debug_symbols, + }, + }; - let post_fingerprint = Fingerprint::try_from_path(&crate_metadata.original_wasm)? + let post_fingerprint = Fingerprint::try_from_path( + &crate_metadata.original_wasm, + )? .ok_or_else(|| { anyhow::anyhow!( "Expected '{}' to be generated by build", @@ -642,68 +661,54 @@ pub fn execute(args: ExecuteArgs) -> Result { ) })?; - if pre_fingerprint == Some(post_fingerprint) - && crate_metadata.dest_wasm.exists() - && crate_metadata.metadata_path().exists() - && crate_metadata.contract_bundle_path().exists() - { - tracing::info!( - "No changes in the original wasm at {}, fingerprint {:?}. \ - Skipping Wasm optimization and metadata generation.", - crate_metadata.original_wasm.display(), - pre_fingerprint - ); - return Ok((None, build_steps)) - } - - maybe_lint(&mut build_steps)?; + let dest_wasm_path = crate_metadata.dest_wasm.clone(); - maybe_println!( - verbosity, - " {} {}", - format!("{build_steps}").bold(), - "Post processing wasm file".bright_green().bold() - ); - build_steps.increment_current(); - post_process_wasm(&crate_metadata, skip_wasm_validation, &verbosity)?; + if pre_fingerprint == Some(post_fingerprint) + && crate_metadata.dest_wasm.exists() + { + tracing::info!( + "No changes in the original wasm at {}, fingerprint {:?}. \ + Skipping Wasm optimization and metadata generation.", + crate_metadata.original_wasm.display(), + pre_fingerprint + ); + return Ok((None, build_info, dest_wasm_path, build_steps)) + } - maybe_println!( - verbosity, - " {} {}", - format!("{build_steps}").bold(), - "Optimizing wasm file".bright_green().bold() - ); - build_steps.increment_current(); + maybe_lint(&mut build_steps)?; - let handler = WasmOptHandler::new(optimization_passes, keep_debug_symbols)?; - let optimization_result = handler.optimize( - &crate_metadata.dest_wasm, - &crate_metadata.contract_artifact_name, - )?; + maybe_println!( + verbosity, + " {} {}", + format!("{build_steps}").bold(), + "Post processing wasm file".bright_green().bold() + ); + build_steps.increment_current(); + post_process_wasm(&crate_metadata, skip_wasm_validation, &verbosity)?; - let cargo_contract_version = if let Ok(version) = Version::parse(VERSION) { - version - } else { - anyhow::bail!( - "Unable to parse version number for the currently running \ - `cargo-contract` binary." + maybe_println!( + verbosity, + " {} {}", + format!("{build_steps}").bold(), + "Optimizing wasm file".bright_green().bold() ); - }; + build_steps.increment_current(); - let build_info = BuildInfo { - rust_toolchain: crate::util::rust_toolchain()?, - cargo_contract_version, - build_mode, - wasm_opt_settings: WasmOptSettings { - optimization_passes, - keep_debug_symbols, - }, - }; + let handler = WasmOptHandler::new(optimization_passes, keep_debug_symbols)?; + let optimization_result = handler.optimize( + &crate_metadata.dest_wasm, + &crate_metadata.contract_artifact_name, + )?; - Ok((Some((optimization_result, build_info)), build_steps)) - }; + Ok(( + Some(optimization_result), + build_info, + dest_wasm_path, + build_steps, + )) + }; - let (opt_result, metadata_result) = match build_artifact { + let (opt_result, metadata_result, dest_wasm) = match build_artifact { BuildArtifacts::CheckOnly => { let mut build_steps = BuildSteps::new(); maybe_lint(&mut build_steps)?; @@ -723,34 +728,38 @@ pub fn execute(args: ExecuteArgs) -> Result { verbosity, &unstable_flags, )?; - (None, None) + (None, None, None) } BuildArtifacts::CodeOnly => { - let (build_result, _) = build()?; - let opt_result = build_result.map(|(opt_result, _)| opt_result); - (opt_result, None) + let (opt_result, _, dest_wasm, _) = build()?; + (opt_result, None, Some(dest_wasm)) } BuildArtifacts::All => { - let (build_result, build_steps) = build()?; - - match build_result { - Some((opt_result, build_info)) => { - let metadata_result = metadata::execute( - &crate_metadata, - opt_result.dest_wasm.as_path(), - network, - verbosity, - build_steps, - &unstable_flags, - build_info, - )?; - (Some(opt_result), Some(metadata_result)) - } - None => (None, None), + let (opt_result, build_info, dest_wasm, build_steps) = build()?; + + let metadata_result = MetadataArtifacts { + dest_metadata: crate_metadata.metadata_path(), + dest_bundle: crate_metadata.contract_bundle_path(), + }; + // skip metadata generation if contract unchanged and all metadata artifacts exist. + if opt_result.is_some() + || !metadata_result.dest_metadata.exists() + || !metadata_result.dest_bundle.exists() + { + metadata::execute( + &crate_metadata, + dest_wasm.as_path(), + &metadata_result, + network, + verbosity, + build_steps, + &unstable_flags, + build_info, + )?; } + (opt_result, Some(metadata_result), Some(dest_wasm)) } }; - let dest_wasm = opt_result.as_ref().map(|r| r.dest_wasm.clone()); Ok(BuildResult { dest_wasm, @@ -909,7 +918,7 @@ mod unit_tests { let build_result = BuildResult { dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")), - metadata_result: Some(MetadataResult { + metadata_result: Some(MetadataArtifacts { dest_metadata: PathBuf::from("/path/to/contract.json"), dest_bundle: PathBuf::from("/path/to/contract.contract"), }), diff --git a/crates/build/src/metadata.rs b/crates/build/src/metadata.rs index 22a81b552..02f5c0360 100644 --- a/crates/build/src/metadata.rs +++ b/crates/build/src/metadata.rs @@ -54,9 +54,9 @@ use std::{ }; use url::Url; -/// Metadata generation result. +/// Artifacts resulting from metadata generation. #[derive(serde::Serialize)] -pub struct MetadataResult { +pub struct MetadataArtifacts { /// Path to the resulting metadata file. pub dest_metadata: PathBuf, /// Path to the bundled file. @@ -107,18 +107,17 @@ pub struct WasmOptSettings { /// Generates a file with metadata describing the ABI of the smart contract. /// /// It does so by generating and invoking a temporary workspace member. +#[allow(clippy::too_many_arguments)] pub(crate) fn execute( crate_metadata: &CrateMetadata, final_contract_wasm: &Path, + metadata_artifacts: &MetadataArtifacts, network: Network, verbosity: Verbosity, mut build_steps: BuildSteps, unstable_options: &UnstableFlags, build_info: BuildInfo, -) -> Result { - let out_path_metadata = crate_metadata.metadata_path(); - let out_path_bundle = crate_metadata.contract_bundle_path(); - +) -> Result<()> { // build the extended contract project metadata let ExtendedMetadataResult { source, @@ -159,7 +158,7 @@ pub(crate) fn execute( let mut metadata = metadata.clone(); metadata.remove_source_wasm_attribute(); let contents = serde_json::to_string_pretty(&metadata)?; - fs::write(&out_path_metadata, contents)?; + fs::write(&metadata_artifacts.dest_metadata, contents)?; build_steps.increment_current(); } @@ -170,7 +169,7 @@ pub(crate) fn execute( "Generating bundle".bright_green().bold() ); let contents = serde_json::to_string(&metadata)?; - fs::write(&out_path_bundle, contents)?; + fs::write(&metadata_artifacts.dest_bundle, contents)?; Ok(()) }; @@ -191,10 +190,7 @@ pub(crate) fn execute( .using_temp(generate_metadata)?; } - Ok(MetadataResult { - dest_metadata: out_path_metadata, - dest_bundle: out_path_bundle, - }) + Ok(()) } /// Generate the extended contract project metadata diff --git a/crates/build/src/tests.rs b/crates/build/src/tests.rs index ee82f81bb..bd31a7065 100644 --- a/crates/build/src/tests.rs +++ b/crates/build/src/tests.rs @@ -18,6 +18,7 @@ use crate::{ util::tests::TestContractManifest, BuildArtifacts, BuildMode, + BuildResult, CrateMetadata, ExecuteArgs, ManifestPath, @@ -38,6 +39,7 @@ use std::{ Path, PathBuf, }, + time::SystemTime, }; macro_rules! build_tests { @@ -74,7 +76,8 @@ build_tests!( building_contract_with_source_file_in_subfolder_must_work, missing_cargo_dylint_installation_must_be_detected, generates_metadata, - unchanged_contract_skips_optimization_and_metadata_steps + unchanged_contract_skips_optimization_and_metadata_steps, + unchanged_contract_no_metadata_artifacts_generates_metadata ); fn build_code_only(manifest_path: &ManifestPath) -> Result<()> { @@ -507,31 +510,109 @@ fn unchanged_contract_skips_optimization_and_metadata_steps( ..Default::default() }; + fn get_last_modified(res: &BuildResult) -> (SystemTime, SystemTime, SystemTime) { + assert!( + res.dest_wasm.is_some(), + "dest_wasm should always be returned for a full build" + ); + assert!( + res.metadata_result.is_some(), + "metadata_result should always be returned for a full build" + ); + let dest_wasm_modified = file_last_modified(res.dest_wasm.as_ref().unwrap()); + let metadata_result_modified = + file_last_modified(&res.metadata_result.as_ref().unwrap().dest_metadata); + let contract_bundle_modified = + file_last_modified(&res.metadata_result.as_ref().unwrap().dest_bundle); + ( + dest_wasm_modified, + metadata_result_modified, + contract_bundle_modified, + ) + } + // when let res1 = super::execute(args.clone()).expect("build failed"); + let (opt_result_modified1, metadata_modified1, contract_bundle_modified1) = + get_last_modified(&res1); let res2 = super::execute(args).expect("build failed"); + let (opt_result_modified2, metadata_modified2, contract_bundle_modified2) = + get_last_modified(&res2); // then - assert!( - res1.optimization_result.is_some(), - "Initial build should perform wasm optimization" + assert_eq!( + opt_result_modified1, opt_result_modified2, + "Subsequent build of unchanged contract should not perform optimization" ); - assert!( - res1.metadata_result.is_some(), - "Initial build should perform generate metadata" + assert_eq!( + metadata_modified1, metadata_modified2, + "Subsequent build of unchanged contract should not perform metadata generation" ); + assert_eq!(contract_bundle_modified1, contract_bundle_modified2, "Subsequent build of unchanged contract should not perform contract bundle generation"); + + Ok(()) +} + +fn unchanged_contract_no_metadata_artifacts_generates_metadata( + manifest_path: &ManifestPath, +) -> Result<()> { + let res1 = super::execute(ExecuteArgs { + manifest_path: manifest_path.clone(), + build_artifact: BuildArtifacts::CodeOnly, + ..Default::default() + }) + .expect("build failed"); + + // CodeOnly should only generate Wasm code artifact + assert!(res1.dest_wasm.as_ref().unwrap().exists()); + assert!(res1.metadata_result.is_none()); + + let dest_wasm_modified_pre = file_last_modified(&res1.dest_wasm.unwrap()); + + let res2 = super::execute(ExecuteArgs { + manifest_path: manifest_path.clone(), + build_artifact: BuildArtifacts::All, + ..Default::default() + }) + .expect("build failed"); + + let dest_wasm_modified_post = file_last_modified(res2.dest_wasm.as_ref().unwrap()); + + // Code remains unchanged, but metadata artifacts are now generated + assert_eq!(dest_wasm_modified_pre, dest_wasm_modified_post); assert!( - res2.metadata_result.is_none(), - "Subsequent build should not perform wasm optimization" + res2.metadata_result + .as_ref() + .unwrap() + .dest_metadata + .exists(), + "Metadata file should have been generated" ); assert!( - res2.metadata_result.is_none(), - "Subsequent build should not generate metadata" + res2.metadata_result.as_ref().unwrap().dest_bundle.exists(), + "Contract bundle should have been generated" ); Ok(()) } +/// Get the last modified date of the given file. +/// Panics if the file does not exist. +fn file_last_modified(path: &Path) -> SystemTime { + fs::metadata(path) + .unwrap_or_else(|err| { + panic!("Failed to read metadata for '{}': {}", path.display(), err) + }) + .modified() + .unwrap_or_else(|err| { + panic!( + "Failed to read modified time for '{}': {}", + path.display(), + err + ) + }) +} + fn build_byte_str(bytes: &[u8]) -> String { let mut str = String::new(); write!(str, "0x").expect("failed writing to string");