#[cfg(test)]
mod tests;
use crate::{
crate_metadata::CrateMetadata,
maybe_println,
util,
validate_wasm,
wasm_opt::WasmOptHandler,
workspace::{
Manifest,
ManifestPath,
Profile,
Workspace,
},
BuildArtifacts,
BuildMode,
BuildResult,
BuildSteps,
Network,
OptimizationPasses,
OptimizationResult,
OutputType,
UnstableFlags,
UnstableOptions,
Verbosity,
VerbosityFlags,
};
use anyhow::{
Context,
Result,
};
use colored::Colorize;
use parity_wasm::elements::{
External,
Internal,
MemoryType,
Module,
Section,
};
use semver::Version;
use std::{
convert::TryFrom,
path::{
Path,
PathBuf,
},
process::Command,
str,
};
const MAX_MEMORY_PAGES: u32 = 16;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Default)]
pub(crate) struct ExecuteArgs {
pub manifest_path: ManifestPath,
pub verbosity: Verbosity,
pub build_mode: BuildMode,
pub network: Network,
pub build_artifact: BuildArtifacts,
pub unstable_flags: UnstableFlags,
pub optimization_passes: OptimizationPasses,
pub keep_debug_symbols: bool,
pub lint: bool,
pub output_type: OutputType,
}
#[derive(Debug, clap::Args)]
#[clap(name = "build")]
pub struct BuildCommand {
#[clap(long, value_parser)]
manifest_path: Option<PathBuf>,
#[clap(long = "release")]
build_release: bool,
#[clap(long = "offline")]
build_offline: bool,
#[clap(long)]
lint: bool,
#[clap(long = "generate", value_enum, default_value = "all")]
build_artifact: BuildArtifacts,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(flatten)]
unstable_options: UnstableOptions,
#[clap(long)]
optimization_passes: Option<OptimizationPasses>,
#[clap(long)]
keep_debug_symbols: bool,
#[clap(long, conflicts_with = "verbose")]
output_json: bool,
}
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 mut 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,
}
}
};
let build_mode = match self.build_release {
true => BuildMode::Release,
false => BuildMode::Debug,
};
let network = match self.build_offline {
true => Network::Offline,
false => Network::Online,
};
if self.lint && matches!(network, Network::Offline) {
anyhow::bail!(
"Linting requires network access in order to download available lints"
)
}
let output_type = match self.output_json {
true => OutputType::Json,
false => OutputType::HumanReadable,
};
if matches!(output_type, OutputType::Json) {
verbosity = Verbosity::Quiet;
}
let args = ExecuteArgs {
manifest_path,
verbosity,
build_mode,
network,
build_artifact: self.build_artifact,
unstable_flags,
optimization_passes,
keep_debug_symbols: self.keep_debug_symbols,
lint: self.lint,
output_type,
};
execute(args)
}
}
#[derive(Debug, clap::Args)]
#[clap(name = "check")]
pub struct CheckCommand {
#[clap(long, value_parser)]
manifest_path: Option<PathBuf>,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(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)?;
let args = ExecuteArgs {
manifest_path,
verbosity,
build_mode: BuildMode::Debug,
network: Network::default(),
build_artifact: BuildArtifacts::CheckOnly,
unstable_flags,
optimization_passes: OptimizationPasses::Zero,
keep_debug_symbols: false,
lint: false,
output_type: OutputType::default(),
};
execute(args)
}
}
fn exec_cargo_for_wasm_target(
crate_metadata: &CrateMetadata,
command: &str,
build_mode: BuildMode,
network: Network,
verbosity: Verbosity,
unstable_flags: &UnstableFlags,
) -> Result<()> {
let cargo_build = |manifest_path: &ManifestPath| {
let target_dir = &crate_metadata.target_directory;
let target_dir = format!("--target-dir={}", target_dir.to_string_lossy());
let mut args = vec![
"--target=wasm32-unknown-unknown",
"-Zbuild-std",
"--no-default-features",
"--release",
&target_dir,
];
if network == Network::Offline {
args.push("--offline");
}
if build_mode == BuildMode::Debug {
args.push("--features=ink/ink-debug");
} else {
args.push("-Zbuild-std-features=panic_immediate_abort");
}
let env = vec![(
"RUSTFLAGS",
Some("-C link-arg=-zstack-size=65536 -C link-arg=--import-memory -Clinker-plugin-lto"),
)];
util::invoke_cargo(command, &args, manifest_path.directory(), verbosity, env)?;
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())?
.with_workspace()?;
Ok(())
})?
.using_temp(cargo_build)?;
}
Ok(())
}
fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Result<()> {
check_dylint_requirements(crate_metadata.manifest_path.directory())?;
let verbosity = match verbosity {
Verbosity::Verbose => Verbosity::Default,
Verbosity::Default | Verbosity::Quiet => Verbosity::Quiet,
};
let target_dir = &crate_metadata.target_directory.to_string_lossy();
let args = vec!["--lib=ink_linting"];
let env = vec![
("CARGO_TARGET_DIR", Some(target_dir.as_ref())),
("RUSTC_WRAPPER", None),
];
Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
manifest.with_dylint()?;
Ok(())
})?
.using_temp(|manifest_path| {
util::invoke_cargo("dylint", &args, manifest_path.directory(), verbosity, env)
.map(|_| ())
})?;
Ok(())
}
fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> {
let execute_cmd = |cmd: &mut Command| {
let mut child = if let Ok(child) = cmd
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
child
} else {
tracing::debug!("Error spawning `{:?}`", cmd);
return false
};
child.wait().map(|ret| ret.success()).unwrap_or_else(|err| {
tracing::debug!("Error waiting for `{:?}`: {:?}", cmd, err);
false
})
};
#[cfg(not(test))]
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
#[cfg(test)]
let cargo = "cargo";
if !execute_cmd(Command::new(cargo).arg("dylint").arg("--version")) {
anyhow::bail!("cargo-dylint was not found!\n\
Make sure it is installed and the binary is in your PATH environment.\n\n\
You can install it by executing `cargo install cargo-dylint`."
.to_string()
.bright_yellow());
}
#[cfg(windows)]
let dylint_link_found = which::which("dylint-link").is_ok();
#[cfg(not(windows))]
let dylint_link_found = execute_cmd(Command::new("dylint-link").arg("--version"));
if !dylint_link_found {
anyhow::bail!("dylint-link was not found!\n\
Make sure it is installed and the binary is in your PATH environment.\n\n\
You can install it by executing `cargo install dylint-link`."
.to_string()
.bright_yellow());
}
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| {
match section {
Section::Reloc(_) => false,
Section::Custom(custom) if custom.name() != "name" => false,
_ => true,
}
})
}
fn strip_exports(module: &mut Module) {
if let Some(section) = module.export_section_mut() {
section.entries_mut().retain(|entry| {
matches!(entry.internal(), Internal::Function(_))
&& (entry.field() == "call" || entry.field() == "deploy")
})
}
}
fn load_module<P: AsRef<Path>>(path: P) -> Result<Module> {
let path = path.as_ref();
parity_wasm::deserialize_file(path).context(format!(
"Loading of wasm module at '{}' failed",
path.display(),
))
}
fn post_process_wasm(crate_metadata: &CrateMetadata) -> Result<()> {
let mut module = load_module(&crate_metadata.original_wasm)
.context("Loading of original wasm failed")?;
strip_exports(&mut module);
ensure_maximum_memory_pages(&mut module, MAX_MEMORY_PAGES)?;
strip_custom_sections(&mut module);
validate_wasm::validate_import_section(&module)?;
debug_assert!(
!module.clone().into_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 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, vec![])
.with_context(|| {
format!(
"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 fn assert_debug_mode_supported(ink_version: &Version) -> anyhow::Result<()> {
tracing::debug!("Contract version: {:?}", ink_version);
let minimum_version = Version::parse("3.0.0-rc4").expect("parsing version failed");
if ink_version < &minimum_version {
anyhow::bail!(
"Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
);
}
Ok(())
}
pub(crate) fn execute(args: ExecuteArgs) -> Result<BuildResult> {
use crate::cmd::metadata::BuildInfo;
let ExecuteArgs {
manifest_path,
verbosity,
build_mode,
network,
build_artifact,
unstable_flags,
optimization_passes,
keep_debug_symbols,
lint,
output_type,
} = args;
let crate_metadata = CrateMetadata::collect(&manifest_path)?;
assert_compatible_ink_dependencies(&manifest_path, verbosity)?;
if build_mode == BuildMode::Debug {
assert_debug_mode_supported(&crate_metadata.ink_version)?;
}
let maybe_lint = || -> Result<BuildSteps> {
if lint {
let mut steps = build_artifact.steps();
steps.total_steps += 1;
maybe_println!(
verbosity,
" {} {}",
format!("{}", steps).bold(),
"Checking ink! linting rules".bright_green().bold()
);
steps.increment_current();
exec_cargo_dylint(&crate_metadata, verbosity)?;
Ok(steps)
} else {
Ok(build_artifact.steps())
}
};
let build = || -> Result<(OptimizationResult, BuildInfo, BuildSteps)> {
use crate::cmd::metadata::WasmOptSettings;
let mut build_steps = maybe_lint()?;
maybe_println!(
verbosity,
" {} {}",
format!("{}", build_steps).bold(),
"Building cargo project".bright_green().bold()
);
build_steps.increment_current();
exec_cargo_for_wasm_target(
&crate_metadata,
"build",
build_mode,
network,
verbosity,
&unstable_flags,
)?;
maybe_println!(
verbosity,
" {} {}",
format!("{}", build_steps).bold(),
"Post processing wasm file".bright_green().bold()
);
build_steps.increment_current();
post_process_wasm(&crate_metadata)?;
maybe_println!(
verbosity,
" {} {}",
format!("{}", build_steps).bold(),
"Optimizing wasm file".bright_green().bold()
);
build_steps.increment_current();
let handler = WasmOptHandler::new(optimization_passes, keep_debug_symbols)?;
let optimization_result = handler.optimize(
&crate_metadata.dest_wasm,
&crate_metadata.contract_artifact_name,
)?;
let cargo_contract_version = if let Ok(version) = Version::parse(VERSION) {
version
} else {
anyhow::bail!(
"Unable to parse version number for the currently running \
`cargo-contract` binary."
);
};
let build_info = BuildInfo {
rust_toolchain: crate::util::rust_toolchain()?,
cargo_contract_version,
build_mode,
wasm_opt_settings: WasmOptSettings {
optimization_passes,
keep_debug_symbols,
},
};
Ok((optimization_result, build_info, build_steps))
};
let (opt_result, metadata_result) = match build_artifact {
BuildArtifacts::CheckOnly => {
let build_steps = maybe_lint()?;
maybe_println!(
verbosity,
" {} {}",
format!("{}", build_steps).bold(),
"Executing `cargo check`".bright_green().bold()
);
exec_cargo_for_wasm_target(
&crate_metadata,
"check",
BuildMode::Release,
network,
verbosity,
&unstable_flags,
)?;
(None, None)
}
BuildArtifacts::CodeOnly => {
let (optimization_result, _build_info, _) = build()?;
(Some(optimization_result), None)
}
BuildArtifacts::All => {
let (optimization_result, build_info, build_steps) = build()?;
let metadata_result = super::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 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_mode,
build_artifact,
verbosity,
output_type,
})
}
#[cfg(test)]
mod unit_tests {
use super::{
assert_compatible_ink_dependencies,
assert_debug_mode_supported,
};
use crate::{
util::tests::{
with_new_contract_project,
TestContractManifest,
},
Verbosity,
};
use semver::Version;
#[test]
pub fn debug_mode_must_be_compatible() {
assert_debug_mode_supported(
&Version::parse("3.0.0-rc4").expect("parsing must work"),
)
.expect("debug mode must be compatible");
assert_debug_mode_supported(
&Version::parse("4.0.0-rc1").expect("parsing must work"),
)
.expect("debug mode must be compatible");
assert_debug_mode_supported(&Version::parse("5.0.0").expect("parsing must work"))
.expect("debug mode must be compatible");
}
#[test]
pub fn debug_mode_must_be_incompatible() {
let res = assert_debug_mode_supported(
&Version::parse("3.0.0-rc3").expect("parsing must work"),
)
.expect_err("assertion must fail");
assert_eq!(
res.to_string(),
"Building the contract in debug mode requires an ink! version newer than `3.0.0-rc3`!"
);
}
#[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 = TestContractManifest::new(manifest_path.clone())?;
manifest.set_dependency_version("scale", "1.0.0")?;
manifest.write()?;
let res =
assert_compatible_ink_dependencies(&manifest_path, Verbosity::Default);
assert!(res.is_err());
Ok(())
})
}
}