diff --git a/Cargo.lock b/Cargo.lock index 6acf00d5204..bad745bd46e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6966,6 +6966,7 @@ dependencies = [ "gear-wasm-instrument", "gear-wasm-optimizer", "gmeta", + "itertools 0.13.0", "log", "parity-wasm", "pathdiff", diff --git a/Cargo.toml b/Cargo.toml index c864e1c4a3f..431a14e5bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -507,6 +507,7 @@ errno = "0.3" # gear-lazy-pages nix = "0.26.4" # gear-lazy-pages indexmap = "2.2.6" # utils/weight-diff indicatif = "*" # utils/wasm-gen +itertools = "0.13" # utils/wasm-builder keyring = "1.2.1" # gcli libp2p = "=0.51.4" # gcli (same version as sc-consensus) mimalloc = { version = "0.1.43", default-features = false } # node/cli diff --git a/utils/wasm-builder/Cargo.toml b/utils/wasm-builder/Cargo.toml index c337dcdc248..a5bceb82d0c 100644 --- a/utils/wasm-builder/Cargo.toml +++ b/utils/wasm-builder/Cargo.toml @@ -26,6 +26,7 @@ gear-core.workspace = true gear-wasm-instrument.workspace = true gear-wasm-optimizer.workspace = true rustc_version.workspace = true +itertools.workspace = true [dev-dependencies] wabt.workspace = true diff --git a/utils/wasm-builder/src/crate_info.rs b/utils/wasm-builder/src/crate_info.rs index bc4a0acffa0..87f854ffc74 100644 --- a/utils/wasm-builder/src/crate_info.rs +++ b/utils/wasm-builder/src/crate_info.rs @@ -20,7 +20,7 @@ use anyhow::{Context, Result}; use cargo_metadata::{Metadata, MetadataCommand, Package}; use std::{collections::BTreeMap, path::Path}; -use crate::builder_error::BuilderError; +use crate::{builder_error::BuilderError, multiple_crate_versions}; /// Helper to get a crate info extracted from the `Cargo.toml`. #[derive(Debug, Default)] @@ -52,6 +52,9 @@ impl CrateInfo { let root_package = Self::root_package(&metadata) .ok_or_else(|| BuilderError::RootPackageNotFound.into()) .and_then(Self::check)?; + + multiple_crate_versions::check(&metadata, &root_package.id)?; + let name = root_package.name.clone(); let snake_case_name = name.replace('-', "_"); let version = root_package.version.to_string(); diff --git a/utils/wasm-builder/src/lib.rs b/utils/wasm-builder/src/lib.rs index 190e254ac35..aca7abb27ad 100644 --- a/utils/wasm-builder/src/lib.rs +++ b/utils/wasm-builder/src/lib.rs @@ -32,6 +32,7 @@ use wasm_project::ProjectType; mod builder_error; pub mod code_validator; mod crate_info; +mod multiple_crate_versions; mod smart_fs; mod wasm_project; diff --git a/utils/wasm-builder/src/multiple_crate_versions.rs b/utils/wasm-builder/src/multiple_crate_versions.rs new file mode 100644 index 00000000000..38ed8840e32 --- /dev/null +++ b/utils/wasm-builder/src/multiple_crate_versions.rs @@ -0,0 +1,71 @@ +use anyhow::{Error, Result}; +use cargo_metadata::{DependencyKind, Metadata, Node, Package, PackageId}; +use itertools::Itertools; +use std::collections::HashSet; + +const DEFAULT_DENIED_DUPLICATE_CRATES: [&str; 1] = ["gstd"]; +/// Returns the list of crates name. +fn denied_duplicate_crates() -> HashSet<&'static str> { + option_env!("__GEAR_WASM_BUILDER_DENIED_DUPLICATE_CRATES").map_or_else( + || DEFAULT_DENIED_DUPLICATE_CRATES.into(), + |v| v.split(',').collect(), + ) +} + +pub(crate) fn check(metadata: &Metadata, root_id: &PackageId) -> Result<()> { + let denied_duplicate_crates = denied_duplicate_crates(); + let mut packages = metadata.packages.clone(); + packages.sort_by(|a, b| a.name.cmp(&b.name)); + + let mut duplicates: Vec = Vec::new(); + + if let Some(resolve) = &metadata.resolve { + for (name, group) in &packages + .iter() + .filter(|p| denied_duplicate_crates.contains(&p.name.as_str())) + .chunk_by(|p| &p.name) + { + let group: Vec<&Package> = group.collect(); + + if group.len() <= 1 { + continue; + } + + if group + .iter() + .all(|p| is_normal_dep(&resolve.nodes, root_id, &p.id)) + { + let mut versions: Vec<_> = group.into_iter().map(|p| &p.version).collect(); + versions.sort(); + let versions = versions.iter().join(", "); + + duplicates.push(format!( + "multiple versions for dependency `{name}`: {versions}" + )); + } + } + } + + if duplicates.is_empty() { + Ok(()) + } else { + Err(Error::msg(duplicates.join("\n"))) + } +} + +fn is_normal_dep(nodes: &[Node], local_id: &PackageId, dep_id: &PackageId) -> bool { + fn depends_on(node: &Node, dep_id: &PackageId) -> bool { + node.deps.iter().any(|dep| { + dep.pkg == *dep_id + && dep + .dep_kinds + .iter() + .any(|info| matches!(info.kind, DependencyKind::Normal)) + }) + } + + nodes + .iter() + .filter(|node| depends_on(node, dep_id)) + .any(|node| node.id == *local_id || is_normal_dep(nodes, local_id, &node.id)) +} diff --git a/utils/wasm-builder/tests/smoke.rs b/utils/wasm-builder/tests/smoke.rs index dd63c4961c5..8c3eeb44d56 100644 --- a/utils/wasm-builder/tests/smoke.rs +++ b/utils/wasm-builder/tests/smoke.rs @@ -153,3 +153,19 @@ fn features_tracking() { assert!(read_export_entry("handle_signal").is_some()); assert!(read_export_entry("handle_reply").is_none()); } + +#[ignore] +#[test] +/// Build fails on multiple crate versions check. +/// Suppose that the `syn` crate is referenced more than once +fn build_release_for_target_deny_duplicate_crate() { + let mut cmd = CargoRunner::new() + .args(["build", "--release", "--target", TARGET]) + .0; + cmd.arg("--color=always"); + cmd.arg("--manifest-path=test-program/Cargo.toml"); + cmd.arg("--config=env.GEAR_WASM_BUILDER_DENIED_DUPLICATE_CRATES=\'syn\'"); + + let status = cmd.status().expect("cargo run error"); + assert!(!status.success()) +}