use std::{convert::TryFrom, ffi::OsStr, fs::metadata, path::PathBuf};
#[cfg(feature = "binaryen-as-dependency")]
use std::{
fs::File,
io::{Read, Write},
};
#[cfg(not(feature = "binaryen-as-dependency"))]
use std::{process::Command, str};
use crate::{
crate_metadata::CrateMetadata,
maybe_println, util, validate_wasm,
workspace::{ManifestPath, Profile, Workspace},
BuildArtifacts, BuildResult, OptimizationResult, UnstableFlags, UnstableOptions, Verbosity,
VerbosityFlags,
};
use anyhow::{Context, Result};
use colored::Colorize;
use parity_wasm::elements::{External, MemoryType, Module, Section};
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,
}
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)?;
execute(
&manifest_path,
verbosity,
true,
self.build_artifact,
unstable_flags,
)
}
}
#[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,
false,
BuildArtifacts::CheckOnly,
unstable_flags,
)
}
}
fn build_cargo_project(
crate_metadata: &CrateMetadata,
build_artifact: BuildArtifacts,
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()),
];
if build_artifact == BuildArtifacts::CheckOnly {
util::invoke_cargo("check", &args, manifest_path.directory(), verbosity)?;
} else {
util::invoke_cargo("build", &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) -> 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(),
3,
)?;
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 {
original_size,
optimized_size,
})
}
#[cfg(feature = "binaryen-as-dependency")]
fn do_optimization(
dest_wasm: &OsStr,
dest_optimized: &OsStr,
optimization_level: u32,
) -> Result<()> {
let mut dest_wasm_file = File::open(dest_wasm)?;
let mut dest_wasm_file_content = Vec::new();
dest_wasm_file.read_to_end(&mut dest_wasm_file_content)?;
let codegen_config = binaryen::CodegenConfig {
optimization_level,
shrink_level: 1,
debug_info: false,
};
let mut module = binaryen::Module::read(&dest_wasm_file_content)
.map_err(|_| anyhow::anyhow!("binaryen failed to read file content"))?;
module.optimize(&codegen_config);
let mut optimized_wasm_file = File::create(dest_optimized)?;
optimized_wasm_file.write_all(&module.write())?;
Ok(())
}
#[cfg(not(feature = "binaryen-as-dependency"))]
fn do_optimization(
dest_wasm: &OsStr,
dest_optimized: &OsStr,
optimization_level: u32,
) -> Result<()> {
if which::which("wasm-opt").is_err() {
anyhow::bail!(
"{}",
"wasm-opt is not installed. Install this tool on your system in order to \n\
reduce the size of your contract's Wasm binary. \n\
See https://github.com/WebAssembly/binaryen#tools"
.bright_yellow()
);
}
let output = Command::new("wasm-opt")
.arg(dest_wasm)
.arg(format!("-O{}", optimization_level))
.arg("-o")
.arg(dest_optimized)
.arg("--zero-filled-memory")
.output()?;
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\
A possible source of error could be that you have an outdated binaryen package installed.\n\n\
The error which wasm-opt returned was: \n{}",
err
);
}
Ok(())
}
fn execute(
manifest_path: &ManifestPath,
verbosity: Verbosity,
optimize_contract: bool,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
) -> 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,
verbosity,
optimize_contract,
build_artifact,
unstable_flags,
)?;
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,
verbosity,
};
return Ok(res);
}
let res = super::metadata::execute(&manifest_path, verbosity, build_artifact, unstable_flags)?;
Ok(res)
}
pub(crate) fn execute_with_crate_metadata(
crate_metadata: &CrateMetadata,
verbosity: Verbosity,
optimize_contract: bool,
build_artifact: BuildArtifacts,
unstable_flags: UnstableFlags,
) -> 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)?;
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!(
verbosity,
" {} {}",
format!("[3/{}]", build_artifact.steps()).bold(),
"Optimizing wasm file".bright_green().bold()
);
let optimization_result = optimize_wasm(&crate_metadata)?;
Ok((
Some(crate_metadata.dest_wasm.clone()),
Some(optimization_result),
))
}
#[cfg(feature = "test-ci-only")]
#[cfg(test)]
mod tests_ci_only {
use crate::{cmd, util::tests::with_tmp_dir, BuildArtifacts, ManifestPath, UnstableFlags};
#[test]
fn build_template() {
with_tmp_dir(|path| {
cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
let manifest_path =
ManifestPath::new(&path.join("new_project").join("Cargo.toml")).unwrap();
let res = super::execute(
&manifest_path,
None,
true,
BuildArtifacts::All,
UnstableFlags::default(),
)
.expect("build failed");
assert!(res.target_directory.ends_with("target/ink"));
assert!(res.optimization_result.unwrap().optimized_size > 0.0);
assert!(res.optimization_result.unwrap().optimized_size < 3.0);
Ok(())
})
}
#[test]
fn check_must_not_create_target_in_project_dir() {
with_tmp_dir(|path| {
cmd::new::execute("new_project", Some(path)).expect("new project creation failed");
let project_dir = path.join("new_project");
let manifest_path = ManifestPath::new(&project_dir.join("Cargo.toml")).unwrap();
super::execute(
&manifest_path,
None,
true,
BuildArtifacts::CheckOnly,
UnstableFlags::default(),
)
.expect("build failed");
assert!(
!project_dir.join("target").exists(),
"found target folder in project directory!"
);
Ok(())
})
}
}