use crate::{
crate_metadata::CrateMetadata,
maybe_println, util, validate_wasm,
workspace::{Manifest, ManifestPath, Profile, Workspace},
BuildArtifacts, BuildResult, OptimizationPasses, OptimizationResult, UnstableFlags,
UnstableOptions, Verbosity, VerbosityFlags,
};
use anyhow::{Context, Result};
use colored::Colorize;
use parity_wasm::elements::{External, MemoryType, Module, Section};
use regex::Regex;
use std::{
convert::TryFrom,
ffi::OsStr,
fs::metadata,
path::{Path, PathBuf},
process::Command,
str,
};
use structopt::StructOpt;
const MAX_MEMORY_PAGES: u32 = 16;
#[derive(Debug, StructOpt)]
#[structopt(name = "build")]
pub struct BuildCommand {
#[structopt(long, parse(from_os_str))]
manifest_path: Option<PathBuf>,
#[structopt(
long = "generate",
default_value = "all",
value_name = "all | code-only",
verbatim_doc_comment
)]
build_artifact: BuildArtifacts,
#[structopt(flatten)]
verbosity: VerbosityFlags,
#[structopt(flatten)]
unstable_options: UnstableOptions,
#[structopt(long = "optimization-passes")]
optimization_passes: Option<OptimizationPasses>,
}
impl BuildCommand {
pub fn exec(&self) -> Result<BuildResult> {
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 optimization_passes = match self.optimization_passes {
Some(opt_passes) => opt_passes,
None => {
let mut manifest = Manifest::new(manifest_path.clone())?;
match manifest.get_profile_optimization_passes() {
None => OptimizationPasses::default(),
Some(opt_passes) => opt_passes,
}
}
};
execute(
&manifest_path,
verbosity,
self.build_artifact,
unstable_flags,
optimization_passes,
)
}
}
#[derive(Debug, StructOpt)]
#[structopt(name = "check")]
pub struct CheckCommand {
#[structopt(long, parse(from_os_str))]
manifest_path: Option<PathBuf>,
#[structopt(flatten)]
verbosity: VerbosityFlags,
#[structopt(flatten)]
unstable_options: UnstableOptions,
}
impl CheckCommand {
pub fn exec(&self) -> Result<BuildResult> {
let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?;
let unstable_flags: UnstableFlags =
TryFrom::<&UnstableOptions>::try_from(&self.unstable_options)?;
let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?;
execute(
&manifest_path,
verbosity,
BuildArtifacts::CheckOnly,
unstable_flags,
OptimizationPasses::Zero,
)
}
}
fn exec_cargo_for_wasm_target(
crate_metadata: &CrateMetadata,
command: &str,
verbosity: Verbosity,
unstable_flags: &UnstableFlags,
) -> Result<()> {
util::assert_channel()?;
std::env::set_var(
"RUSTFLAGS",
"-C link-arg=-z -C link-arg=stack-size=65536 -C link-arg=--import-memory",
);
let cargo_build = |manifest_path: &ManifestPath| {
let target_dir = &crate_metadata.target_directory;
let args = [
"--target=wasm32-unknown-unknown",
"-Zbuild-std",
"-Zbuild-std-features=panic_immediate_abort",
"--no-default-features",
"--release",
&format!("--target-dir={}", target_dir.to_string_lossy()),
];
util::invoke_cargo(command, &args, manifest_path.directory(), verbosity)?;
Ok(())
};
if unstable_flags.original_manifest {
maybe_println!(
verbosity,
"{} {}",
"warning:".yellow().bold(),
"with 'original-manifest' enabled, the contract binary may not be of optimal size."
.bold()
);
cargo_build(&crate_metadata.manifest_path)?;
} else {
Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
manifest
.with_removed_crate_type("rlib")?
.with_profile_release_defaults(Profile::default_contract_release())?;
Ok(())
})?
.using_temp(cargo_build)?;
}
std::env::remove_var("RUSTFLAGS");
Ok(())
}
fn ensure_maximum_memory_pages(module: &mut Module, maximum_allowed_pages: u32) -> Result<()> {
let mem_ty = module
.import_section_mut()
.and_then(|section| {
section
.entries_mut()
.iter_mut()
.find_map(|entry| match entry.external_mut() {
External::Memory(ref mut mem_ty) => Some(mem_ty),
_ => None,
})
})
.context("Memory import is not found. Is --import-memory specified in the linker args")?;
if let Some(requested_maximum) = mem_ty.limits().maximum() {
if requested_maximum > maximum_allowed_pages {
anyhow::bail!(
"The wasm module requires {} pages. The maximum allowed number of pages is {}",
requested_maximum,
maximum_allowed_pages,
);
}
} else {
let initial = mem_ty.limits().initial();
*mem_ty = MemoryType::new(initial, Some(MAX_MEMORY_PAGES));
}
Ok(())
}
fn strip_custom_sections(module: &mut Module) {
module.sections_mut().retain(|section| {
!matches!(
section,
Section::Custom(_) | Section::Name(_) | Section::Reloc(_)
)
});
}
fn post_process_wasm(crate_metadata: &CrateMetadata) -> Result<()> {
let mut module =
parity_wasm::deserialize_file(&crate_metadata.original_wasm).context(format!(
"Loading original wasm file '{}'",
crate_metadata.original_wasm.display()
))?;
if pwasm_utils::optimize(&mut module, ["call", "deploy"].to_vec()).is_err() {
anyhow::bail!("Optimizer failed");
}
ensure_maximum_memory_pages(&mut module, MAX_MEMORY_PAGES)?;
strip_custom_sections(&mut module);
validate_wasm::validate_import_section(&module)?;
debug_assert!(
!module.clone().to_bytes().unwrap().is_empty(),
"resulting wasm size of post processing must be > 0"
);
parity_wasm::serialize_to_file(&crate_metadata.dest_wasm, module)?;
Ok(())
}
fn optimize_wasm(
crate_metadata: &CrateMetadata,
optimization_passes: OptimizationPasses,
) -> Result<OptimizationResult> {
let mut dest_optimized = crate_metadata.dest_wasm.clone();
dest_optimized.set_file_name(format!("{}-opt.wasm", crate_metadata.package_name));
let _ = do_optimization(
crate_metadata.dest_wasm.as_os_str(),
&dest_optimized.as_os_str(),
optimization_passes,
)?;
if !dest_optimized.exists() {
return Err(anyhow::anyhow!(
"Optimization failed, optimized wasm output file `{}` not found.",
dest_optimized.display()
));
}
let original_size = metadata(&crate_metadata.dest_wasm)?.len() as f64 / 1000.0;
let optimized_size = metadata(&dest_optimized)?.len() as f64 / 1000.0;
std::fs::rename(&dest_optimized, &crate_metadata.dest_wasm)?;
Ok(OptimizationResult {
dest_wasm: crate_metadata.dest_wasm.clone(),
original_size,
optimized_size,
})
}
fn do_optimization(
dest_wasm: &OsStr,
dest_optimized: &OsStr,
optimization_level: OptimizationPasses,
) -> Result<()> {
let which = which::which("wasm-opt");
if which.is_err() {
anyhow::bail!(
"wasm-opt not found! Make sure the binary is in your PATH environment.\n\
We use this tool to optimize the size of your contract's Wasm binary.\n\n\
wasm-opt is part of the binaryen package. You can find detailed\n\
installation instructions on https://github.com/WebAssembly/binaryen#tools.\n\n\
There are ready-to-install packages for many platforms:\n\
* Debian/Ubuntu: apt-get install binaryen\n\
* Homebrew: brew install binaryen\n\
* Arch Linux: pacman -S binaryen\n\
* Windows: binary releases at https://github.com/WebAssembly/binaryen/releases"
.to_string()
.bright_yellow()
);
}
let wasm_opt_path = which
.as_ref()
.expect("we just checked if which returned an err; qed")
.as_path();
log::info!("Path to wasm-opt executable: {}", wasm_opt_path.display());
let _ = check_wasm_opt_version_compatibility(wasm_opt_path)?;
log::info!(
"Optimization level passed to wasm-opt: {}",
optimization_level
);
let output = Command::new(wasm_opt_path)
.arg(dest_wasm)
.arg(format!("-O{}", optimization_level))
.arg("-o")
.arg(dest_optimized)
.arg("--zero-filled-memory")
.output()
.map_err(|err| {
anyhow::anyhow!(
"Executing {} failed with {:?}",
wasm_opt_path.display(),
err
)
})?;
if !output.status.success() {
let err = str::from_utf8(&output.stderr)
.expect("Cannot convert stderr output of wasm-opt to string")
.trim();
anyhow::bail!(
"The wasm-opt optimization failed.\n\n\
The error which wasm-opt returned was: \n{}",
err
);
}
Ok(())
}
fn check_wasm_opt_version_compatibility(wasm_opt_path: &Path) -> Result<()> {
let cmd = Command::new(wasm_opt_path)
.arg("--version")
.output()
.map_err(|err| {
anyhow::anyhow!(
"Executing `{:?} --version` failed with {:?}",
wasm_opt_path.display(),
err
)
})?;
if !cmd.status.success() {
let err = str::from_utf8(&cmd.stderr)
.expect("Cannot convert stderr output of wasm-opt to string")
.trim();
anyhow::bail!(
"Getting version information from wasm-opt failed.\n\
The error which wasm-opt returned was: \n{}",
err
);
}
let version_stdout = str::from_utf8(&cmd.stdout)
.expect("Cannot convert stdout output of wasm-opt to string")
.trim();
let re = Regex::new(r"wasm-opt version (\d+)").expect("invalid regex");
let captures = re.captures(version_stdout).ok_or_else(|| {
anyhow::anyhow!(
"Unable to extract version information from '{}'.\n\
Your wasm-opt version is most probably too old. Make sure you use a version >= 99.",
version_stdout
)
})?;
let version_number: u32 = captures
.get(1) .ok_or_else(|| {
anyhow::anyhow!(
"Unable to extract version number from '{:?}'",
version_stdout
)
})?
.as_str()
.parse()
.map_err(|err| {
anyhow::anyhow!(
"Parsing version number failed with '{:?}' for '{:?}'",
err,
version_stdout
)
})?;
log::info!(
"The wasm-opt version output is '{}', which was parsed to '{}'",
version_stdout,
version_number
);
if version_number < 99 {
anyhow::bail!(
"Your wasm-opt version is {}, but we require a version >= 99.",
version_number
);
}
Ok(())
}
fn assert_compatible_ink_dependencies(
manifest_path: &ManifestPath,
verbosity: Verbosity,
) -> Result<()> {
for dependency in ["parity-scale-codec", "scale-info"].iter() {
let args = ["-i", dependency, "--duplicates"];
let _ = util::invoke_cargo("tree", &args, manifest_path.directory(), verbosity).map_err(
|_| {
anyhow::anyhow!(
"Mismatching versions of `{}` were found!\n\
Please ensure that your contract and your ink! dependencies use a compatible \
version of this package.",
dependency
)
},
)?;
}
Ok(())
}
pub(crate) fn execute(
manifest_path: &ManifestPath,
verbosity: Verbosity,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
optimization_passes: OptimizationPasses,
) -> Result<BuildResult> {
let crate_metadata = CrateMetadata::collect(&manifest_path)?;
assert_compatible_ink_dependencies(&manifest_path, verbosity)?;
let build = || -> Result<OptimizationResult> {
maybe_println!(
verbosity,
" {} {}",
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,
" {} {}",
format!("[2/{}]", build_artifact.steps()).bold(),
"Post processing wasm file".bright_green().bold()
);
post_process_wasm(&crate_metadata)?;
maybe_println!(
verbosity,
" {} {}",
format!("[3/{}]", build_artifact.steps()).bold(),
"Optimizing wasm file".bright_green().bold()
);
let optimization_result = optimize_wasm(&crate_metadata, optimization_passes)?;
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,
})
}
#[cfg(feature = "test-ci-only")]
#[cfg(test)]
mod tests_ci_only {
use super::{assert_compatible_ink_dependencies, check_wasm_opt_version_compatibility};
use crate::{
cmd::BuildCommand,
util::tests::{with_new_contract_project, with_tmp_dir},
workspace::Manifest,
BuildArtifacts, ManifestPath, OptimizationPasses, UnstableFlags, UnstableOptions,
Verbosity, VerbosityFlags,
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
io::Write,
path::{Path, PathBuf},
};
fn write_optimization_passes_into_manifest(
cargo_toml_path: &PathBuf,
passes: OptimizationPasses,
) {
let manifest_path =
ManifestPath::new(cargo_toml_path).expect("manifest path creation failed");
let mut manifest = Manifest::new(manifest_path.clone()).expect("manifest creation failed");
manifest
.set_profile_optimization_passes(passes)
.expect("setting `optimization-passes` in profile failed");
manifest
.write(&manifest_path)
.expect("writing manifest failed");
}
#[cfg(unix)]
fn mock_wasm_opt_version(tmp_dir: &Path, version: &str) -> PathBuf {
let path = tmp_dir.join("wasm-opt-mocked");
{
let mut file = std::fs::File::create(&path).unwrap();
let version = format!("#!/bin/sh\necho \"wasm-opt version {}\"", version);
file.write_all(version.as_bytes())
.expect("writing wasm-opt-mocked failed");
}
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o777))
.expect("setting permissions failed");
path
}
#[test]
fn build_code_only() {
with_new_contract_project(|manifest_path| {
let res = super::execute(
&manifest_path,
Verbosity::Default,
BuildArtifacts::CodeOnly,
UnstableFlags::default(),
OptimizationPasses::default(),
)
.expect("build failed");
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);
assert!(optimized_size < 3.0);
Ok(())
})
}
#[test]
fn check_must_not_output_contract_artifacts_in_project_dir() {
with_new_contract_project(|manifest_path| {
let project_dir = manifest_path.directory().expect("directory must exist");
super::execute(
&manifest_path,
Verbosity::Default,
BuildArtifacts::CheckOnly,
UnstableFlags::default(),
OptimizationPasses::default(),
)
.expect("build failed");
assert!(
!project_dir.join("target/ink/new_project.contract").exists(),
"found contract artifact in project directory!"
);
assert!(
!project_dir.join("target/ink/new_project.wasm").exists(),
"found wasm artifact in project directory!"
);
Ok(())
})
}
#[test]
fn optimization_passes_from_cli_must_take_precedence_over_profile() {
with_new_contract_project(|manifest_path| {
write_optimization_passes_into_manifest(
&manifest_path.clone().into(),
OptimizationPasses::Three,
);
let cmd = BuildCommand {
manifest_path: Some(manifest_path.into()),
build_artifact: BuildArtifacts::All,
verbosity: VerbosityFlags::default(),
unstable_options: UnstableOptions::default(),
optimization_passes: Some(OptimizationPasses::Zero),
};
let res = cmd.exec().expect("build failed");
let optimization = res
.optimization_result
.expect("no optimization result available");
let optimized_size = optimization.optimized_size.trunc();
let original_size = optimization.original_size.trunc();
assert!(
optimized_size == original_size,
"The optimized size {:?} differs from the original size {:?}",
optimized_size,
original_size
);
Ok(())
})
}
#[test]
fn optimization_passes_from_profile_must_be_used() {
with_new_contract_project(|manifest_path| {
write_optimization_passes_into_manifest(
&manifest_path.clone().into(),
OptimizationPasses::Three,
);
let cmd = BuildCommand {
manifest_path: Some(manifest_path.into()),
build_artifact: BuildArtifacts::All,
verbosity: VerbosityFlags::default(),
unstable_options: UnstableOptions::default(),
optimization_passes: None,
};
let res = cmd.exec().expect("build failed");
let optimization = res
.optimization_result
.expect("no optimization result available");
let optimized_size = optimization.optimized_size.trunc();
let original_size = optimization.original_size.trunc();
assert!(
optimized_size < original_size,
"The optimized size DOES NOT {:?} differ from the original size {:?}",
optimized_size,
original_size
);
Ok(())
})
}
#[test]
fn project_template_dependencies_must_be_ink_compatible() {
with_new_contract_project(|manifest_path| {
let res = assert_compatible_ink_dependencies(&manifest_path, Verbosity::Default);
assert!(res.is_ok());
Ok(())
})
}
#[test]
fn detect_mismatching_parity_scale_codec_dependencies() {
with_new_contract_project(|manifest_path| {
let mut manifest = Manifest::new(manifest_path.clone())?;
manifest
.set_dependency_version("scale", "1.0.0")
.expect("setting `scale` version failed");
manifest
.write(&manifest_path)
.expect("writing manifest failed");
let res = assert_compatible_ink_dependencies(&manifest_path, Verbosity::Default);
assert!(res.is_err());
Ok(())
})
}
#[cfg(unix)]
#[test]
fn incompatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
with_tmp_dir(|path| {
let path = mock_wasm_opt_version(path, "98 (version_13-79-gc12cc3f50)");
let res = check_wasm_opt_version_compatibility(&path);
assert!(res.is_err());
assert_eq!(
format!("{:?}", res),
"Err(Your wasm-opt version is 98, but we require a version >= 99.)"
);
Ok(())
})
}
#[cfg(unix)]
#[test]
fn compatible_wasm_opt_version_must_be_detected_if_built_from_repo() {
with_tmp_dir(|path| {
let path = mock_wasm_opt_version(path, "99 (version_99-79-gc12cc3f50");
let res = check_wasm_opt_version_compatibility(&path);
assert!(res.is_ok());
Ok(())
})
}
#[cfg(unix)]
#[test]
fn incompatible_wasm_opt_version_must_be_detected_if_installed_as_package() {
with_tmp_dir(|path| {
let path = mock_wasm_opt_version(path, "98");
let res = check_wasm_opt_version_compatibility(&path);
assert!(res.is_err());
assert_eq!(
format!("{:?}", res),
"Err(Your wasm-opt version is 98, but we require a version >= 99.)"
);
Ok(())
})
}
#[cfg(unix)]
#[test]
fn compatible_wasm_opt_version_must_be_detected_if_installed_as_package() {
with_tmp_dir(|path| {
let path = mock_wasm_opt_version(path, "99");
let res = check_wasm_opt_version_compatibility(&path);
assert!(res.is_ok());
Ok(())
})
}
}