diff --git a/crates/build/src/args.rs b/crates/build/src/args.rs index ad5485ecc..0aa23d5f0 100644 --- a/crates/build/src/args.rs +++ b/crates/build/src/args.rs @@ -115,11 +115,11 @@ pub enum BuildArtifacts { impl BuildArtifacts { /// Returns the number of steps required to complete a build artifact. /// Used as output on the cli. - pub fn steps(&self) -> BuildSteps { + pub fn steps(&self) -> usize { match self { - BuildArtifacts::All => BuildSteps::new(5), - BuildArtifacts::CodeOnly => BuildSteps::new(4), - BuildArtifacts::CheckOnly => BuildSteps::new(1), + BuildArtifacts::All => 5, + BuildArtifacts::CodeOnly => 4, + BuildArtifacts::CheckOnly => 1, } } } @@ -134,25 +134,38 @@ impl Default for BuildArtifacts { #[derive(Debug, Clone, Copy)] pub struct BuildSteps { pub current_step: usize, - pub total_steps: usize, + pub total_steps: Option, } impl BuildSteps { - pub fn new(total_steps: usize) -> Self { + pub fn new() -> Self { Self { current_step: 1, - total_steps, + total_steps: None, } } pub fn increment_current(&mut self) { self.current_step += 1; } + + pub fn set_total_steps(&mut self, steps: usize) { + self.total_steps = Some(steps) + } +} + +impl Default for BuildSteps { + fn default() -> Self { + Self::new() + } } impl fmt::Display for BuildSteps { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{}/{}]", self.current_step, self.total_steps) + let total_steps = self + .total_steps + .map_or("*".to_string(), |steps| steps.to_string()); + write!(f, "[{}/{}]", self.current_step, total_steps) } } @@ -181,6 +194,7 @@ impl fmt::Display for BuildMode { } /// The type of output to display at the end of a build. +#[derive(Clone, Debug)] pub enum OutputType { /// Output build results in a human readable format. HumanReadable, diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index 5e75b36f9..4f3b0c1a7 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -79,6 +79,7 @@ use parity_wasm::elements::{ }; use semver::Version; use std::{ + fs, path::{ Path, PathBuf, @@ -94,7 +95,7 @@ const MAX_MEMORY_PAGES: u32 = 16; const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Arguments to use when executing `build` or `check` commands. -#[derive(Default)] +#[derive(Default, Clone)] pub struct ExecuteArgs { /// The location of the Cargo manifest (`Cargo.toml`) file to use. pub manifest_path: ManifestPath, @@ -135,16 +136,26 @@ pub struct BuildResult { impl BuildResult { pub fn display(&self) -> String { - let optimization = self.display_optimization(); - let size_diff = format!( - "\nOriginal wasm size: {}, Optimized: {}\n\n", - format!("{:.1}K", optimization.0).bold(), - format!("{:.1}K", optimization.1).bold(), - ); - debug_assert!( - optimization.1 > 0.0, - "optimized file size must be greater 0" - ); + 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 build_mode = format!( "The contract was built in {} mode.\n\n", @@ -154,7 +165,7 @@ impl BuildResult { if self.build_artifact == BuildArtifacts::CodeOnly { let out = format!( "{}{}Your contract's code is ready. You can find it here:\n{}", - size_diff, + opt_size_diff, build_mode, self.dest_wasm .as_ref() @@ -167,10 +178,11 @@ impl BuildResult { }; let mut out = format!( - "{}{}Your contract artifacts are ready. You can find them in:\n{}\n\n", - size_diff, + "{}{}Your contract artifacts are ready. You can find them in:\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!( @@ -196,17 +208,6 @@ impl BuildResult { out } - /// Returns a tuple of `(original_size, optimized_size)`. - /// - /// Panics if no optimization result is available. - fn display_optimization(&self) -> (f64, f64) { - let optimization = self - .optimization_result - .as_ref() - .expect("optimization result must exist"); - (optimization.original_size, optimization.optimized_size) - } - /// Display the build results in a pretty formatted JSON string. pub fn serialize_json(&self) -> Result { Ok(serde_json::to_string_pretty(self)?) @@ -593,10 +594,10 @@ pub fn execute(args: ExecuteArgs) -> Result { assert_debug_mode_supported(&crate_metadata.ink_version)?; } - let maybe_lint = || -> Result { + let maybe_lint = |steps: &mut BuildSteps| -> Result<()> { + let total_steps = build_artifact.steps(); if lint { - let mut steps = build_artifact.steps(); - steps.total_steps += 1; + steps.set_total_steps(total_steps + 1); maybe_println!( verbosity, " {} {}", @@ -605,14 +606,16 @@ pub fn execute(args: ExecuteArgs) -> Result { ); steps.increment_current(); exec_cargo_dylint(&crate_metadata, verbosity)?; - Ok(steps) + Ok(()) } else { - Ok(build_artifact.steps()) + steps.set_total_steps(total_steps); + Ok(()) } }; - let build = || -> Result<(OptimizationResult, BuildInfo, BuildSteps)> { - let mut build_steps = maybe_lint()?; + let build = || -> Result<(Option<(OptimizationResult, BuildInfo)>, BuildSteps)> { + let mut build_steps = BuildSteps::new(); + let pre_fingerprint = Fingerprint::try_from_path(&crate_metadata.original_wasm)?; maybe_println!( verbosity, @@ -631,6 +634,30 @@ pub fn execute(args: ExecuteArgs) -> Result { &unstable_flags, )?; + let post_fingerprint = Fingerprint::try_from_path(&crate_metadata.original_wasm)? + .ok_or_else(|| { + anyhow::anyhow!( + "Expected '{}' to be generated by build", + crate_metadata.original_wasm.display() + ) + })?; + + 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)?; + maybe_println!( verbosity, " {} {}", @@ -673,12 +700,13 @@ pub fn execute(args: ExecuteArgs) -> Result { }, }; - Ok((optimization_result, build_info, build_steps)) + Ok((Some((optimization_result, build_info)), build_steps)) }; let (opt_result, metadata_result) = match build_artifact { BuildArtifacts::CheckOnly => { - let build_steps = maybe_lint()?; + let mut build_steps = BuildSteps::new(); + maybe_lint(&mut build_steps)?; maybe_println!( verbosity, @@ -698,23 +726,28 @@ pub fn execute(args: ExecuteArgs) -> Result { (None, None) } BuildArtifacts::CodeOnly => { - let (optimization_result, _build_info, _) = build()?; - (Some(optimization_result), None) + let (build_result, _) = build()?; + let opt_result = build_result.map(|(opt_result, _)| opt_result); + (opt_result, None) } BuildArtifacts::All => { - let (optimization_result, build_info, build_steps) = build()?; - - let metadata_result = crate::metadata::execute( - &crate_metadata, - optimization_result.dest_wasm.as_path(), - network, - verbosity, - build_steps, - &unstable_flags, - build_info, - )?; - - (Some(optimization_result), Some(metadata_result)) + 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 dest_wasm = opt_result.as_ref().map(|r| r.dest_wasm.clone()); @@ -731,8 +764,41 @@ pub fn execute(args: ExecuteArgs) -> Result { }) } +/// Unique fingerprint for a file to detect whether it has changed. +#[derive(Debug, Eq, PartialEq)] +struct Fingerprint { + path: PathBuf, + hash: [u8; 32], + modified: std::time::SystemTime, +} + +impl Fingerprint { + pub fn try_from_path

(path: P) -> Result> + where + P: AsRef, + { + if path.as_ref().exists() { + let modified = fs::metadata(&path)?.modified()?; + let bytes = fs::read(&path)?; + let hash = blake2_hash(&bytes); + Ok(Some(Self { + path: path.as_ref().to_path_buf(), + hash, + modified, + })) + } else { + Ok(None) + } + } +} + /// Returns the blake2 hash of the code slice. pub fn code_hash(code: &[u8]) -> [u8; 32] { + blake2_hash(code) +} + +/// Returns the blake2 hash of the given bytes. +fn blake2_hash(code: &[u8]) -> [u8; 32] { use blake2::digest::{ consts::U32, Digest as _, diff --git a/crates/build/src/tests.rs b/crates/build/src/tests.rs index 50ffe0b78..ee82f81bb 100644 --- a/crates/build/src/tests.rs +++ b/crates/build/src/tests.rs @@ -18,6 +18,7 @@ use crate::{ util::tests::TestContractManifest, BuildArtifacts, BuildMode, + CrateMetadata, ExecuteArgs, ManifestPath, OptimizationPasses, @@ -72,7 +73,8 @@ build_tests!( build_with_json_output_works, building_contract_with_source_file_in_subfolder_must_work, missing_cargo_dylint_installation_must_be_detected, - generates_metadata + generates_metadata, + unchanged_contract_skips_optimization_and_metadata_steps ); fn build_code_only(manifest_path: &ManifestPath) -> Result<()> { @@ -393,7 +395,7 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { )?; test_manifest.write()?; - let crate_metadata = crate::crate_metadata::CrateMetadata::collect(manifest_path)?; + let crate_metadata = CrateMetadata::collect(manifest_path)?; // usually this file will be produced by a previous build step let final_contract_wasm_path = &crate_metadata.dest_wasm; @@ -496,6 +498,40 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { Ok(()) } +fn unchanged_contract_skips_optimization_and_metadata_steps( + manifest_path: &ManifestPath, +) -> Result<()> { + // given + let args = ExecuteArgs { + manifest_path: manifest_path.clone(), + ..Default::default() + }; + + // when + let res1 = super::execute(args.clone()).expect("build failed"); + let res2 = super::execute(args).expect("build failed"); + + // then + assert!( + res1.optimization_result.is_some(), + "Initial build should perform wasm optimization" + ); + assert!( + res1.metadata_result.is_some(), + "Initial build should perform generate metadata" + ); + assert!( + res2.metadata_result.is_none(), + "Subsequent build should not perform wasm optimization" + ); + assert!( + res2.metadata_result.is_none(), + "Subsequent build should not generate metadata" + ); + + Ok(()) +} + fn build_byte_str(bytes: &[u8]) -> String { let mut str = String::new(); write!(str, "0x").expect("failed writing to string"); @@ -554,6 +590,7 @@ impl BuildTestContext { ) -> Result<()> { println!("Running {name}"); let manifest_path = ManifestPath::new(self.working_dir.join("Cargo.toml"))?; + let crate_metadata = CrateMetadata::collect(&manifest_path)?; match test(&manifest_path) { Ok(()) => (), Err(err) => { @@ -563,6 +600,10 @@ impl BuildTestContext { // revert to the original template files, but keep the `target` dir from the previous run. self.remove_all_except_target_dir()?; copy_dir_all(&self.template_dir, &self.working_dir)?; + // remove the original wasm artifact to force it to be rebuilt + if crate_metadata.original_wasm.exists() { + fs::remove_file(&crate_metadata.original_wasm)?; + } Ok(()) }