diff --git a/CHANGELOG.md b/CHANGELOG.md index b42d94d94fe36ad9f4107a656f0478fc091e6333..c6a92beac88cbc369ec9d9d92696e2e43c1fc59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Add option for JSON formatted output - [#324](https://github.com/paritytech/cargo-contract/pull/324) + ### Changed - Use new dependency resolver for template contract - [#325](https://github.com/paritytech/cargo-contract/pull/325) diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 61a32fd2ee74036a54121ac757b89f483d766b9f..ee465a792cf7ef3ae99982ea7db1e8668340812c 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -18,8 +18,8 @@ use crate::{ crate_metadata::CrateMetadata, maybe_println, util, validate_wasm, workspace::{Manifest, ManifestPath, Profile, Workspace}, - BuildArtifacts, BuildMode, BuildResult, OptimizationPasses, OptimizationResult, UnstableFlags, - UnstableOptions, Verbosity, VerbosityFlags, + BuildArtifacts, BuildMode, BuildResult, OptimizationPasses, OptimizationResult, OutputType, + UnstableFlags, UnstableOptions, Verbosity, VerbosityFlags, }; use anyhow::{Context, Result}; use colored::Colorize; @@ -39,6 +39,20 @@ use structopt::StructOpt; /// This is the maximum number of pages available for a contract to allocate. const MAX_MEMORY_PAGES: u32 = 16; +/// Arguments to use when executing `build` or `check` commands. +#[derive(Default)] +pub(crate) struct ExecuteArgs { + /// The location of the Cargo manifest (`Cargo.toml`) file to use. + pub(crate) manifest_path: ManifestPath, + verbosity: Verbosity, + build_mode: BuildMode, + build_artifact: BuildArtifacts, + unstable_flags: UnstableFlags, + optimization_passes: OptimizationPasses, + keep_debug_symbols: bool, + output_type: OutputType, +} + /// Executes build of the smart-contract which produces a wasm binary that is ready for deploying. /// /// It does so by invoking `cargo build` and then post processing the final binary. @@ -102,6 +116,10 @@ pub struct BuildCommand { /// This is useful if one wants to analyze or debug the optimized binary. #[structopt(long)] keep_debug_symbols: bool, + + /// Export the build output in JSON format. + #[structopt(long, conflicts_with = "verbose")] + output_json: bool, } impl BuildCommand { @@ -109,7 +127,7 @@ impl BuildCommand { let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; let unstable_flags: UnstableFlags = TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; - let verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; + let mut verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; // The CLI flag `optimization-passes` overwrites optimization passes which are // potentially defined in the `Cargo.toml` profile. @@ -130,15 +148,29 @@ impl BuildCommand { true => BuildMode::Release, false => BuildMode::Debug, }; - execute( - &manifest_path, + + let output_type = match self.output_json { + true => OutputType::Json, + false => OutputType::HumanReadable, + }; + + // We want to ensure that the only thing in `STDOUT` is our JSON formatted string. + if matches!(output_type, OutputType::Json) { + verbosity = Verbosity::Quiet; + } + + let args = ExecuteArgs { + manifest_path, verbosity, build_mode, - self.build_artifact, + build_artifact: self.build_artifact, unstable_flags, optimization_passes, - self.keep_debug_symbols, - ) + keep_debug_symbols: self.keep_debug_symbols, + output_type, + }; + + execute(args) } } @@ -160,15 +192,19 @@ impl CheckCommand { let unstable_flags: UnstableFlags = TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?; let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; - execute( - &manifest_path, + + let args = ExecuteArgs { + manifest_path, verbosity, - BuildMode::Debug, - BuildArtifacts::CheckOnly, + build_mode: BuildMode::Debug, + build_artifact: BuildArtifacts::CheckOnly, unstable_flags, - OptimizationPasses::Zero, - false, - ) + optimization_passes: OptimizationPasses::Zero, + keep_debug_symbols: false, + output_type: OutputType::default(), + }; + + execute(args) } } @@ -583,18 +619,21 @@ pub fn assert_debug_mode_supported(ink_version: &Version) -> anyhow::Result<()> /// Executes build of the smart-contract which produces a wasm binary that is ready for deploying. /// /// It does so by invoking `cargo build` and then post processing the final binary. -pub(crate) fn execute( - manifest_path: &ManifestPath, - verbosity: Verbosity, - build_mode: BuildMode, - build_artifact: BuildArtifacts, - unstable_flags: UnstableFlags, - optimization_passes: OptimizationPasses, - keep_debug_symbols: bool, -) -> Result { - let crate_metadata = CrateMetadata::collect(manifest_path)?; +pub(crate) fn execute(args: ExecuteArgs) -> Result { + let ExecuteArgs { + manifest_path, + verbosity, + build_mode, + build_artifact, + unstable_flags, + optimization_passes, + keep_debug_symbols, + output_type, + } = args; + + let crate_metadata = CrateMetadata::collect(&manifest_path)?; - assert_compatible_ink_dependencies(manifest_path, verbosity)?; + assert_compatible_ink_dependencies(&manifest_path, verbosity)?; if build_mode == BuildMode::Debug { assert_debug_mode_supported(&crate_metadata.ink_version)?; } @@ -663,6 +702,7 @@ pub(crate) fn execute( } }; let dest_wasm = opt_result.as_ref().map(|r| r.dest_wasm.clone()); + Ok(BuildResult { dest_wasm, metadata_result, @@ -671,6 +711,7 @@ pub(crate) fn execute( build_mode, build_artifact, verbosity, + output_type, }) } @@ -685,8 +726,8 @@ mod tests_ci_only { cmd::{build::load_module, BuildCommand}, util::tests::{with_new_contract_project, with_tmp_dir}, workspace::Manifest, - BuildArtifacts, BuildMode, ManifestPath, OptimizationPasses, UnstableFlags, - UnstableOptions, Verbosity, VerbosityFlags, + BuildArtifacts, BuildMode, ManifestPath, OptimizationPasses, OutputType, UnstableOptions, + Verbosity, VerbosityFlags, }; use semver::Version; #[cfg(unix)] @@ -741,16 +782,14 @@ mod tests_ci_only { #[test] fn build_code_only() { with_new_contract_project(|manifest_path| { - let res = super::execute( - &manifest_path, - Verbosity::Default, - BuildMode::Release, - BuildArtifacts::CodeOnly, - UnstableFlags::default(), - OptimizationPasses::default(), - false, - ) - .expect("build failed"); + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_mode: BuildMode::Release, + build_artifact: BuildArtifacts::CodeOnly, + ..Default::default() + }; + + let res = super::execute(args).expect("build failed"); // our ci has set `CARGO_TARGET_DIR` to cache artifacts. // this dir does not include `/target/` as a path, hence @@ -784,18 +823,14 @@ mod tests_ci_only { with_new_contract_project(|manifest_path| { // given let project_dir = manifest_path.directory().expect("directory must exist"); + let args = crate::cmd::build::ExecuteArgs { + manifest_path: manifest_path.clone(), + build_artifact: BuildArtifacts::CheckOnly, + ..Default::default() + }; // when - super::execute( - &manifest_path, - Verbosity::Default, - BuildMode::default(), - BuildArtifacts::CheckOnly, - UnstableFlags::default(), - OptimizationPasses::default(), - false, - ) - .expect("build failed"); + super::execute(args).expect("build failed"); // then assert!( @@ -828,6 +863,7 @@ mod tests_ci_only { // we choose zero optimization passes as the "cli" parameter optimization_passes: Some(OptimizationPasses::Zero), keep_debug_symbols: false, + output_json: false, }; // when @@ -867,6 +903,7 @@ mod tests_ci_only { // we choose no optimization passes as the "cli" parameter optimization_passes: None, keep_debug_symbols: false, + output_json: false, }; // when @@ -1031,6 +1068,7 @@ mod tests_ci_only { unstable_options: UnstableOptions::default(), optimization_passes: None, keep_debug_symbols: false, + output_json: false, }; let res = cmd.exec().expect("build failed"); @@ -1073,18 +1111,14 @@ mod tests_ci_only { fn building_template_in_debug_mode_must_work() { with_new_contract_project(|manifest_path| { // given - let build_mode = BuildMode::Debug; + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_mode: BuildMode::Debug, + ..Default::default() + }; // when - let res = super::execute( - &manifest_path, - Verbosity::Default, - build_mode, - BuildArtifacts::All, - UnstableFlags::default(), - OptimizationPasses::default(), - Default::default(), - ); + let res = super::execute(args); // then assert!(res.is_ok(), "building template in debug mode failed!"); @@ -1096,18 +1130,14 @@ mod tests_ci_only { fn building_template_in_release_mode_must_work() { with_new_contract_project(|manifest_path| { // given - let build_mode = BuildMode::Release; + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_mode: BuildMode::Release, + ..Default::default() + }; // when - let res = super::execute( - &manifest_path, - Verbosity::Default, - build_mode, - BuildArtifacts::All, - UnstableFlags::default(), - OptimizationPasses::default(), - Default::default(), - ); + let res = super::execute(args); // then assert!(res.is_ok(), "building template in release mode failed!"); @@ -1133,16 +1163,14 @@ mod tests_ci_only { .expect("setting lib path must work"); manifest.write(&manifest_path).expect("writing must work"); + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_artifact: BuildArtifacts::CheckOnly, + ..Default::default() + }; + // when - let res = super::execute( - &manifest_path, - Verbosity::Default, - BuildMode::default(), - BuildArtifacts::CheckOnly, - UnstableFlags::default(), - OptimizationPasses::default(), - Default::default(), - ); + let res = super::execute(args); // then assert!(res.is_ok(), "building contract failed!"); @@ -1153,16 +1181,15 @@ mod tests_ci_only { #[test] fn keep_debug_symbols_in_debug_mode() { with_new_contract_project(|manifest_path| { - let res = super::execute( - &manifest_path, - Verbosity::Default, - BuildMode::Debug, - BuildArtifacts::CodeOnly, - UnstableFlags::default(), - OptimizationPasses::default(), - true, - ) - .expect("build failed"); + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_mode: BuildMode::Debug, + build_artifact: BuildArtifacts::CodeOnly, + keep_debug_symbols: true, + ..Default::default() + }; + + let res = super::execute(args).expect("build failed"); // we specified that debug symbols should be kept assert!(has_debug_symbols(&res.dest_wasm.unwrap())); @@ -1174,16 +1201,15 @@ mod tests_ci_only { #[test] fn keep_debug_symbols_in_release_mode() { with_new_contract_project(|manifest_path| { - let res = super::execute( - &manifest_path, - Verbosity::Default, - BuildMode::Release, - BuildArtifacts::CodeOnly, - UnstableFlags::default(), - OptimizationPasses::default(), - true, - ) - .expect("build failed"); + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + build_mode: BuildMode::Release, + build_artifact: BuildArtifacts::CodeOnly, + keep_debug_symbols: true, + ..Default::default() + }; + + let res = super::execute(args).expect("build failed"); // we specified that debug symbols should be kept assert!(has_debug_symbols(&res.dest_wasm.unwrap())); @@ -1191,4 +1217,23 @@ mod tests_ci_only { Ok(()) }) } + + #[test] + fn build_with_json_output_works() { + with_new_contract_project(|manifest_path| { + // given + let args = crate::cmd::build::ExecuteArgs { + manifest_path, + output_type: OutputType::Json, + ..Default::default() + }; + + // when + let res = super::execute(args).expect("build failed"); + + // then + assert!(res.serialize_json().is_ok()); + Ok(()) + }) + } } diff --git a/src/cmd/metadata.rs b/src/cmd/metadata.rs index 720a20fcba481995f6c328509947fab204569ce3..92bb3c3b0647b8d53e0eca9d46d0927d3492e8a1 100644 --- a/src/cmd/metadata.rs +++ b/src/cmd/metadata.rs @@ -38,6 +38,7 @@ use url::Url; const METADATA_FILE: &str = "metadata.json"; /// Metadata generation result. +#[derive(serde::Serialize)] pub struct MetadataResult { /// Path to the resulting metadata file. pub dest_metadata: PathBuf, @@ -225,8 +226,7 @@ fn blake2_hash(code: &[u8]) -> CodeHash { mod tests { use crate::cmd::metadata::blake2_hash; use crate::{ - cmd, crate_metadata::CrateMetadata, util::tests::with_new_contract_project, BuildArtifacts, - BuildMode, ManifestPath, OptimizationPasses, UnstableFlags, Verbosity, + cmd, crate_metadata::CrateMetadata, util::tests::with_new_contract_project, ManifestPath, }; use anyhow::Context; use contract_metadata::*; @@ -319,15 +319,10 @@ mod tests { fs::create_dir_all(final_contract_wasm_path.parent().unwrap()).unwrap(); fs::write(final_contract_wasm_path, "TEST FINAL WASM BLOB").unwrap(); - let build_result = cmd::build::execute( - &test_manifest.manifest_path, - Verbosity::Default, - BuildMode::default(), - BuildArtifacts::All, - UnstableFlags::default(), - OptimizationPasses::default(), - false, - )?; + let mut args = crate::cmd::build::ExecuteArgs::default(); + args.manifest_path = test_manifest.manifest_path; + + let build_result = cmd::build::execute(args)?; let dest_bundle = build_result .metadata_result .expect("Metadata should be generated") diff --git a/src/main.rs b/src/main.rs index b2aaf38c5392342f4269ce734f5249a8d929decf..b7fb083b483f1c2891a9c6d7352881f8af84424f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -170,7 +170,7 @@ pub struct VerbosityFlags { } /// Denotes if output should be printed to stdout. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, serde::Serialize)] pub enum Verbosity { /// Use default output Default, @@ -180,6 +180,12 @@ pub enum Verbosity { Verbose, } +impl Default for Verbosity { + fn default() -> Self { + Verbosity::Default + } +} + impl Verbosity { /// Returns `true` if output should be printed (i.e. verbose output is set). pub(crate) fn is_verbose(&self) -> bool { @@ -235,7 +241,7 @@ impl TryFrom<&UnstableOptions> for UnstableFlags { } /// Describes which artifacts to generate -#[derive(Copy, Clone, Eq, PartialEq, Debug, StructOpt)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, StructOpt, serde::Serialize)] #[structopt(name = "build-artifacts")] pub enum BuildArtifacts { /// Generate the Wasm, the metadata and a bundled `.contract` file @@ -270,8 +276,14 @@ impl std::str::FromStr for BuildArtifacts { } } +impl Default for BuildArtifacts { + fn default() -> Self { + BuildArtifacts::All + } +} + /// The mode to build the contract in. -#[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, serde::Serialize)] pub enum BuildMode { /// Functionality to output debug messages is build into the contract. Debug, @@ -294,7 +306,22 @@ impl Display for BuildMode { } } +/// The type of output to display at the end of a build. +pub enum OutputType { + /// Output build results in a human readable format. + HumanReadable, + /// Output the build results JSON formatted. + Json, +} + +impl Default for OutputType { + fn default() -> Self { + OutputType::HumanReadable + } +} + /// Result of the metadata generation process. +#[derive(serde::Serialize)] pub struct BuildResult { /// Path to the resulting Wasm file. pub dest_wasm: Option, @@ -310,9 +337,13 @@ pub struct BuildResult { pub build_artifact: BuildArtifacts, /// The verbosity flags. pub verbosity: Verbosity, + /// The type of formatting to use for the build output. + #[serde(skip_serializing)] + pub output_type: OutputType, } /// Result of the optimization process. +#[derive(serde::Serialize)] pub struct OptimizationResult { /// The path of the optimized wasm file. pub dest_wasm: PathBuf, @@ -395,6 +426,11 @@ impl BuildResult { .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)?) + } } #[derive(Debug, StructOpt)] @@ -485,7 +521,10 @@ fn exec(cmd: Command) -> Result> { Command::New { name, target_dir } => cmd::new::execute(name, target_dir.as_ref()), Command::Build(build) => { let result = build.exec()?; - if result.verbosity.is_verbose() { + + if matches!(result.output_type, OutputType::Json) { + Ok(Some(result.serialize_json()?)) + } else if result.verbosity.is_verbose() { Ok(Some(result.display())) } else { Ok(None) @@ -540,3 +579,54 @@ fn exec(cmd: Command) -> Result> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_result_seralization_sanity_check() { + // given + let raw_result = r#"{ + "dest_wasm": "/path/to/contract.wasm", + "metadata_result": { + "dest_metadata": "/path/to/metadata.json", + "dest_bundle": "/path/to/contract.contract" + }, + "target_directory": "/path/to/target", + "optimization_result": { + "dest_wasm": "/path/to/contract.wasm", + "original_size": 64.0, + "optimized_size": 32.0 + }, + "build_mode": "Debug", + "build_artifact": "All", + "verbosity": "Quiet" +}"#; + + let build_result = crate::BuildResult { + dest_wasm: Some(PathBuf::from("/path/to/contract.wasm")), + metadata_result: Some(crate::cmd::metadata::MetadataResult { + dest_metadata: PathBuf::from("/path/to/metadata.json"), + dest_bundle: PathBuf::from("/path/to/contract.contract"), + }), + target_directory: PathBuf::from("/path/to/target"), + optimization_result: Some(crate::OptimizationResult { + dest_wasm: PathBuf::from("/path/to/contract.wasm"), + original_size: 64.0, + optimized_size: 32.0, + }), + build_mode: Default::default(), + build_artifact: Default::default(), + verbosity: Verbosity::Quiet, + output_type: OutputType::Json, + }; + + // when + let serialized_result = build_result.serialize_json(); + + // then + assert!(serialized_result.is_ok()); + assert_eq!(serialized_result.unwrap(), raw_result); + } +}