Unverified Commit cf0671b8 authored by Andrew Jones's avatar Andrew Jones Committed by GitHub
Browse files

Refactor build command (#223)

* Refactoring build, phase 1

* Fmt

* Extract local method for building, always optimize on build

* Add MetadataResult type

* Fmt

* Create dest wasm dir

* Execute build directly from metadata tests

* Modify existing build test to code only, since metadata test now builds all

* Fix errors after merge
parent bddfdf67
Pipeline #129690 passed with stages
in 5 minutes and 59 seconds
......@@ -81,7 +81,6 @@ impl BuildCommand {
execute(
&manifest_path,
verbosity,
true,
self.build_artifact,
unstable_flags,
optimization_passes,
......@@ -111,7 +110,6 @@ impl CheckCommand {
execute(
&manifest_path,
verbosity,
false,
BuildArtifacts::CheckOnly,
unstable_flags,
optimization_passes,
......@@ -119,7 +117,8 @@ impl CheckCommand {
}
}
/// Builds the project in the specified directory, defaults to the current directory.
/// Executes the supplied cargo command on the project in the specified directory, defaults to the
/// current directory.
///
/// Uses the unstable cargo feature [`build-std`](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std)
/// to build the standard library with [`panic_immediate_abort`](https://github.com/johnthagen/min-sized-rust#remove-panic-string-formatting-with-panic_immediate_abort)
......@@ -134,11 +133,11 @@ impl CheckCommand {
/// user-defined settings will be preserved.
///
/// To disable this and use the original `Cargo.toml` as is then pass the `-Z original_manifest` flag.
fn build_cargo_project(
fn exec_cargo_for_wasm_target(
crate_metadata: &CrateMetadata,
build_artifact: BuildArtifacts,
command: &str,
verbosity: Verbosity,
unstable_flags: UnstableFlags,
unstable_flags: &UnstableFlags,
) -> Result<()> {
util::assert_channel()?;
......@@ -159,11 +158,7 @@ fn build_cargo_project(
"--release",
&format!("--target-dir={}", target_dir.to_string_lossy()),
];
if build_artifact == BuildArtifacts::CheckOnly {
util::invoke_cargo("check", &args, manifest_path.directory(), verbosity)?;
} else {
util::invoke_cargo("build", &args, manifest_path.directory(), verbosity)?;
}
util::invoke_cargo(command, &args, manifest_path.directory(), verbosity)?;
Ok(())
};
......@@ -293,6 +288,7 @@ fn optimize_wasm(
// overwrite existing destination wasm file with the optimised version
std::fs::rename(&dest_optimized, &crate_metadata.dest_wasm)?;
Ok(OptimizationResult {
dest_wasm: crate_metadata.dest_wasm.clone(),
original_size,
optimized_size,
})
......@@ -384,94 +380,74 @@ fn do_optimization(
/// 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.
fn execute(
pub(crate) fn execute(
manifest_path: &ManifestPath,
verbosity: Verbosity,
optimize_contract: bool,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
optimization_passes: OptimizationPasses,
) -> Result<BuildResult> {
if build_artifact == BuildArtifacts::CodeOnly || build_artifact == BuildArtifacts::CheckOnly {
let crate_metadata = CrateMetadata::collect(manifest_path)?;
let (maybe_dest_wasm, maybe_optimization_result) = execute_with_crate_metadata(
&crate_metadata,
let crate_metadata = CrateMetadata::collect(manifest_path)?;
let build = || -> Result<OptimizationResult> {
maybe_println!(
verbosity,
optimize_contract,
build_artifact,
unstable_flags,
optimization_passes,
)?;
let res = BuildResult {
dest_wasm: maybe_dest_wasm,
dest_metadata: None,
dest_bundle: None,
target_directory: crate_metadata.target_directory,
optimization_result: maybe_optimization_result,
build_artifact,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Building cargo project".bright_green().bold()
);
exec_cargo_for_wasm_target(&crate_metadata, "build", verbosity, &unstable_flags)?;
maybe_println!(
verbosity,
};
return Ok(res);
}
" {} {}",
format!("[2/{}]", build_artifact.steps()).bold(),
"Post processing wasm file".bright_green().bold()
);
post_process_wasm(&crate_metadata)?;
let res = super::metadata::execute(
&manifest_path,
verbosity,
build_artifact,
unstable_flags,
optimization_passes,
)?;
Ok(res)
}
maybe_println!(
verbosity,
" {} {}",
format!("[3/{}]", build_artifact.steps()).bold(),
"Optimizing wasm file".bright_green().bold()
);
let optimization_result = optimize_wasm(&crate_metadata, optimization_passes)?;
/// 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.
///
/// # Note
///
/// Uses the supplied `CrateMetadata`. If an instance is not available use [`execute_build`]
///
/// Returns a tuple of `(maybe_optimized_wasm_path, maybe_optimization_result)`.
pub(crate) fn execute_with_crate_metadata(
crate_metadata: &CrateMetadata,
verbosity: Verbosity,
optimize_contract: bool,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
optimization_passes: OptimizationPasses,
) -> Result<(Option<PathBuf>, Option<OptimizationResult>)> {
maybe_println!(
verbosity,
" {} {}",
format!("[1/{}]", build_artifact.steps()).bold(),
"Building cargo project".bright_green().bold()
);
build_cargo_project(&crate_metadata, build_artifact, verbosity, unstable_flags)?;
if build_artifact == BuildArtifacts::CheckOnly {
return Ok((None, None));
}
maybe_println!(
verbosity,
" {} {}",
format!("[2/{}]", build_artifact.steps()).bold(),
"Post processing wasm file".bright_green().bold()
);
post_process_wasm(&crate_metadata)?;
if !optimize_contract {
return Ok((None, None));
}
maybe_println!(
Ok(optimization_result)
};
let (opt_result, metadata_result) = match build_artifact {
BuildArtifacts::CheckOnly => {
exec_cargo_for_wasm_target(&crate_metadata, "check", verbosity, &unstable_flags)?;
(None, None)
}
BuildArtifacts::CodeOnly => {
let optimization_result = build()?;
(Some(optimization_result), None)
}
BuildArtifacts::All => {
let optimization_result = build()?;
let metadata_result = super::metadata::execute(
&crate_metadata,
optimization_result.dest_wasm.as_path(),
verbosity,
build_artifact.steps(),
&unstable_flags,
)?;
(Some(optimization_result), Some(metadata_result))
}
};
let dest_wasm = opt_result.as_ref().map(|r| r.dest_wasm.clone());
Ok(BuildResult {
dest_wasm,
metadata_result,
target_directory: crate_metadata.target_directory,
optimization_result: opt_result,
build_artifact,
verbosity,
" {} {}",
format!("[3/{}]", build_artifact.steps()).bold(),
"Optimizing wasm file".bright_green().bold()
);
let optimization_result = optimize_wasm(&crate_metadata, optimization_passes)?;
Ok((
Some(crate_metadata.dest_wasm.clone()),
Some(optimization_result),
))
})
}
#[cfg(feature = "test-ci-only")]
......@@ -483,7 +459,7 @@ mod tests_ci_only {
};
#[test]
fn build_template() {
fn build_code_only() {
with_tmp_dir(|path| {
cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
let manifest_path =
......@@ -491,8 +467,7 @@ mod tests_ci_only {
let res = super::execute(
&manifest_path,
Verbosity::Default,
true,
BuildArtifacts::All,
BuildArtifacts::CodeOnly,
UnstableFlags::default(),
OptimizationPasses::default(),
)
......@@ -506,6 +481,11 @@ mod tests_ci_only {
// for `/ink` being the root path.
assert!(res.target_directory.ends_with("ink"));
assert!(
res.metadata_result.is_none(),
"CodeOnly should not generate the metadata"
);
let optimized_size = res.optimization_result.unwrap().optimized_size;
assert!(optimized_size > 0.0);
......@@ -528,7 +508,6 @@ mod tests_ci_only {
super::execute(
&manifest_path,
Verbosity::Default,
true,
BuildArtifacts::CheckOnly,
UnstableFlags::default(),
OptimizationPasses::default(),
......
......@@ -18,7 +18,7 @@ use crate::{
crate_metadata::CrateMetadata,
maybe_println, util,
workspace::{ManifestPath, Workspace},
BuildArtifacts, BuildResult, OptimizationPasses, OptimizationResult, UnstableFlags, Verbosity,
UnstableFlags, Verbosity,
};
use anyhow::Result;
......@@ -29,225 +29,186 @@ use contract_metadata::{
SourceLanguage, SourceWasm, User,
};
use semver::Version;
use std::{fs, path::PathBuf};
use std::{
fs,
path::{Path, PathBuf},
};
use url::Url;
const METADATA_FILE: &str = "metadata.json";
/// Executes the metadata generation process
struct GenerateMetadataCommand {
crate_metadata: CrateMetadata,
verbosity: Verbosity,
build_artifact: BuildArtifacts,
unstable_options: UnstableFlags,
optimization_passes: OptimizationPasses,
/// Metadata generation result.
pub struct MetadataResult {
/// Path to the resulting metadata file.
pub dest_metadata: PathBuf,
/// Path to the bundled file.
pub dest_bundle: PathBuf,
}
/// Result of generating the extended contract project metadata
struct ExtendedMetadataResult {
dest_wasm: Option<PathBuf>,
source: Source,
contract: Contract,
user: Option<User>,
optimization_result: Option<OptimizationResult>,
}
impl GenerateMetadataCommand {
pub fn exec(&self) -> Result<BuildResult> {
util::assert_channel()?;
let target_directory = self.crate_metadata.target_directory.clone();
let out_path_metadata = target_directory.join(METADATA_FILE);
let fname_bundle = format!("{}.contract", self.crate_metadata.package_name);
let out_path_bundle = target_directory.join(fname_bundle);
// build the extended contract project metadata
let ExtendedMetadataResult {
dest_wasm,
source,
contract,
user,
optimization_result,
} = self.extended_metadata()?;
let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> {
let mut current_progress = 4;
maybe_println!(
self.verbosity,
" {} {}",
format!("[{}/{}]", current_progress, self.build_artifact.steps()).bold(),
"Generating metadata".bright_green().bold()
);
let target_dir_arg = format!("--target-dir={}", target_directory.to_string_lossy());
let stdout = util::invoke_cargo(
"run",
&[
"--package",
"metadata-gen",
&manifest_path.cargo_arg(),
&target_dir_arg,
"--release",
],
self.crate_metadata.manifest_path.directory(),
self.verbosity,
)?;
/// Generates a file with metadata describing the ABI of the smart-contract.
///
/// It does so by generating and invoking a temporary workspace member.
pub(crate) fn execute(
crate_metadata: &CrateMetadata,
final_contract_wasm: &Path,
verbosity: Verbosity,
total_steps: usize,
unstable_options: &UnstableFlags,
) -> Result<MetadataResult> {
util::assert_channel()?;
let target_directory = crate_metadata.target_directory.clone();
let out_path_metadata = target_directory.join(METADATA_FILE);
let fname_bundle = format!("{}.contract", crate_metadata.package_name);
let out_path_bundle = target_directory.join(fname_bundle);
// build the extended contract project metadata
let ExtendedMetadataResult {
source,
contract,
user,
} = extended_metadata(crate_metadata, final_contract_wasm)?;
let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> {
let mut current_progress = 4;
maybe_println!(
verbosity,
" {} {}",
format!("[{}/{}]", current_progress, total_steps).bold(),
"Generating metadata".bright_green().bold()
);
let target_dir_arg = format!("--target-dir={}", target_directory.to_string_lossy());
let stdout = util::invoke_cargo(
"run",
&[
"--package",
"metadata-gen",
&manifest_path.cargo_arg(),
&target_dir_arg,
"--release",
],
crate_metadata.manifest_path.directory(),
verbosity,
)?;
let ink_meta: serde_json::Map<String, serde_json::Value> =
serde_json::from_slice(&stdout)?;
let metadata = ContractMetadata::new(source, contract, user, ink_meta);
{
let mut metadata = metadata.clone();
metadata.remove_source_wasm_attribute();
let contents = serde_json::to_string_pretty(&metadata)?;
fs::write(&out_path_metadata, contents)?;
current_progress += 1;
}
if self.build_artifact == BuildArtifacts::All {
maybe_println!(
self.verbosity,
" {} {}",
format!("[{}/{}]", current_progress, self.build_artifact.steps()).bold(),
"Generating bundle".bright_green().bold()
);
let contents = serde_json::to_string(&metadata)?;
fs::write(&out_path_bundle, contents)?;
}
let ink_meta: serde_json::Map<String, serde_json::Value> = serde_json::from_slice(&stdout)?;
let metadata = ContractMetadata::new(source, contract, user, ink_meta);
{
let mut metadata = metadata.clone();
metadata.remove_source_wasm_attribute();
let contents = serde_json::to_string_pretty(&metadata)?;
fs::write(&out_path_metadata, contents)?;
current_progress += 1;
}
Ok(())
};
if self.unstable_options.original_manifest {
generate_metadata(&self.crate_metadata.manifest_path)?;
} else {
Workspace::new(
&self.crate_metadata.cargo_meta,
&self.crate_metadata.root_package.id,
)?
maybe_println!(
verbosity,
" {} {}",
format!("[{}/{}]", current_progress, total_steps).bold(),
"Generating bundle".bright_green().bold()
);
let contents = serde_json::to_string(&metadata)?;
fs::write(&out_path_bundle, contents)?;
Ok(())
};
if unstable_options.original_manifest {
generate_metadata(&crate_metadata.manifest_path)?;
} else {
Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
manifest
.with_added_crate_type("rlib")?
.with_profile_release_lto(false)?;
Ok(())
})?
.with_metadata_gen_package(self.crate_metadata.manifest_path.absolute_directory()?)?
.with_metadata_gen_package(crate_metadata.manifest_path.absolute_directory()?)?
.using_temp(generate_metadata)?;
}
let dest_bundle = if self.build_artifact == BuildArtifacts::All {
Some(out_path_bundle)
} else {
None
};
Ok(BuildResult {
dest_metadata: Some(out_path_metadata),
dest_wasm,
dest_bundle,
optimization_result,
target_directory,
build_artifact: self.build_artifact,
verbosity: self.verbosity,
})
}
/// Generate the extended contract project metadata
fn extended_metadata(&self) -> Result<ExtendedMetadataResult> {
let contract_package = &self.crate_metadata.root_package;
let ink_version = &self.crate_metadata.ink_version;
let rust_version = Version::parse(&rustc_version::version()?.to_string())?;
let contract_name = contract_package.name.clone();
let contract_version = Version::parse(&contract_package.version.to_string())?;
let contract_authors = contract_package.authors.clone();
// optional
let description = contract_package.description.clone();
let documentation = self.crate_metadata.documentation.clone();
let repository = contract_package
.repository
.as_ref()
.map(|repo| Url::parse(&repo))
.transpose()?;
let homepage = self.crate_metadata.homepage.clone();
let license = contract_package.license.clone();
let (dest_wasm, hash, optimization_result) = self.wasm_hash()?;
let source = {
let lang = SourceLanguage::new(Language::Ink, ink_version.clone());
let compiler = SourceCompiler::new(Compiler::RustC, rust_version);
let maybe_wasm = if self.build_artifact == BuildArtifacts::All {
let wasm = fs::read(&self.crate_metadata.dest_wasm)?;
// The Wasm which we read must have the same hash as `source.hash`
debug_assert!({
let expected = blake2_hash(wasm.as_slice());
expected == hash
});
Some(SourceWasm::new(wasm))
} else {
None
};
Source::new(maybe_wasm, hash, lang, compiler)
};
// Required contract fields
let mut builder = Contract::builder();
builder
.name(contract_name)
.version(contract_version)
.authors(contract_authors);
if let Some(description) = description {
builder.description(description);
}
Ok(MetadataResult {
dest_metadata: out_path_metadata,
dest_bundle: out_path_bundle,
})
}
if let Some(documentation) = documentation {
builder.documentation(documentation);
}
/// Generate the extended contract project metadata
fn extended_metadata(
crate_metadata: &CrateMetadata,
final_contract_wasm: &Path,
) -> Result<ExtendedMetadataResult> {
let contract_package = &crate_metadata.root_package;
let ink_version = &crate_metadata.ink_version;
let rust_version = Version::parse(&rustc_version::version()?.to_string())?;
let contract_name = contract_package.name.clone();
let contract_version = Version::parse(&contract_package.version.to_string())?;
let contract_authors = contract_package.authors.clone();
// optional
let description = contract_package.description.clone();
let documentation = crate_metadata.documentation.clone();
let repository = contract_package
.repository
.as_ref()
.map(|repo| Url::parse(&repo))
.transpose()?;
let homepage = crate_metadata.homepage.clone();
let license = contract_package.license.clone();
let source = {
let lang = SourceLanguage::new(Language::Ink, ink_version.clone());
let compiler = SourceCompiler::new(Compiler::RustC, rust_version);
let wasm = fs::read(final_contract_wasm)?;
let hash = blake2_hash(wasm.as_slice());
Source::new(Some(SourceWasm::new(wasm)), hash, lang, compiler)
};
if let Some(repository) = repository {
builder.repository(repository);
}
// Required contract fields
let mut builder = Contract::builder();
builder
.name(contract_name)
.version(contract_version)
.authors(contract_authors);
if let Some(homepage) = homepage {
builder.homepage(homepage);
}
if let Some(description) = description {
builder.description(description);
}
if let Some(license) = license {
builder.license(license);
}
if let Some(documentation) = documentation {
builder.documentation(documentation);
}
let contract = builder
.build()
.map_err(|err| anyhow::anyhow!("Invalid contract metadata builder state: {}", err))?;
if let Some(repository) = repository {
builder.repository(repository);
}
// user defined metadata
let user = self.crate_metadata.user.clone().map(User::new);
if let Some(homepage) = homepage {
builder.homepage(homepage);
}
Ok(ExtendedMetadataResult {
dest_wasm: Some(dest_wasm),
source,
contract,
user,
optimization_result: Some(optimization_result),
})
if let Some(license) = license {
builder.license(license);
}
/// Compile the contract and then hash the resulting Wasm.
///
/// Return a tuple of `(dest_wasm, hash, optimization_result)`.
fn wasm_hash(&self) -> Result<(PathBuf, CodeHash, OptimizationResult)> {
let (maybe_dest_wasm, maybe_optimization_res) = super::build::execute_with_crate_metadata(
&self.crate_metadata,
self.verbosity,
true, // for the hash we always use the optimized version of the contract
self.build_artifact,
self.unstable_options.clone(),
self.optimization_passes,
)?;
let contract = builder
.build()
.map_err(|err| anyhow::anyhow!("Invalid contract metadata builder state: {}", err))?;
let wasm = fs::read(&self.crate_metadata.dest_wasm)?;
let dest_wasm = maybe_dest_wasm.expect("dest wasm must exist");