diff --git a/cmd/crates/soroban-test/tests/it/build.rs b/cmd/crates/soroban-test/tests/it/build.rs index 3524e90f9..bd1c463f7 100644 --- a/cmd/crates/soroban-test/tests/it/build.rs +++ b/cmd/crates/soroban-test/tests/it/build.rs @@ -1,6 +1,6 @@ use assert_fs::TempDir; use fs_extra::dir::CopyOptions; -use predicates::prelude::predicate; +use predicates::prelude::{predicate, PredicateBooleanExt}; use shell_escape::escape; use soroban_cli::xdr::{Limited, Limits, ReadXdr, ScMetaEntry, ScMetaV0}; use soroban_spec_tools::contract::Spec; @@ -401,6 +401,251 @@ fn remap_absolute_paths() { assert!(noremap_has_abs_paths); } +#[test] +fn build_warns_when_overflow_checks_missing() { + let sandbox = TestEnv::default(); + let temp = TempDir::new().unwrap(); + let dir_path = temp.path(); + + // Create a workspace without overflow-checks in profile + std::fs::write( + dir_path.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["contract"] + +[profile.release] +opt-level = "z" +"#, + ) + .unwrap(); + + std::fs::create_dir_all(dir_path.join("contract/src")).unwrap(); + std::fs::write( + dir_path.join("contract/Cargo.toml"), + r#" +[package] +name = "test-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +"#, + ) + .unwrap(); + std::fs::write(dir_path.join("contract/src/lib.rs"), "").unwrap(); + + // Build will fail (no panic handler), but warning should appear first + sandbox + .new_assert_cmd("contract") + .current_dir(dir_path) + .arg("build") + .assert() + .failure() + .stderr(predicate::str::contains( + "`overflow-checks` is not enabled for profile `release`", + )); +} + +#[test] +fn build_no_warning_when_overflow_checks_enabled() { + let sandbox = TestEnv::default(); + let temp = TempDir::new().unwrap(); + let dir_path = temp.path(); + + // Create a workspace with overflow-checks = true + std::fs::write( + dir_path.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["contract"] + +[profile.release] +opt-level = "z" +overflow-checks = true +"#, + ) + .unwrap(); + + std::fs::create_dir_all(dir_path.join("contract/src")).unwrap(); + std::fs::write( + dir_path.join("contract/Cargo.toml"), + r#" +[package] +name = "test-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +"#, + ) + .unwrap(); + std::fs::write(dir_path.join("contract/src/lib.rs"), "").unwrap(); + + // Build will fail (no panic handler), but no overflow warning should appear + sandbox + .new_assert_cmd("contract") + .current_dir(dir_path) + .arg("build") + .assert() + .failure() + .stderr(predicate::str::contains("overflow-checks").not()); +} + +#[test] +fn build_no_warning_when_profile_inherits_overflow_checks() { + let sandbox = TestEnv::default(); + let temp = TempDir::new().unwrap(); + let dir_path = temp.path(); + + // Create a workspace where custom profile inherits from release with overflow-checks + std::fs::write( + dir_path.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["contract"] + +[profile.release] +opt-level = "z" +overflow-checks = true + +[profile.custom] +inherits = "release" +"#, + ) + .unwrap(); + + std::fs::create_dir_all(dir_path.join("contract/src")).unwrap(); + std::fs::write( + dir_path.join("contract/Cargo.toml"), + r#" +[package] +name = "test-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +"#, + ) + .unwrap(); + std::fs::write(dir_path.join("contract/src/lib.rs"), "").unwrap(); + + // Build with custom profile - no warning should appear + sandbox + .new_assert_cmd("contract") + .current_dir(dir_path) + .arg("build") + .arg("--profile=custom") + .assert() + .failure() + .stderr(predicate::str::contains("overflow-checks").not()); +} + +#[test] +fn build_warns_when_inherited_profile_missing_overflow_checks() { + let sandbox = TestEnv::default(); + let temp = TempDir::new().unwrap(); + let dir_path = temp.path(); + + // Create a workspace where custom profile inherits from release without overflow-checks + std::fs::write( + dir_path.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["contract"] + +[profile.release] +opt-level = "z" + +[profile.custom] +inherits = "release" +"#, + ) + .unwrap(); + + std::fs::create_dir_all(dir_path.join("contract/src")).unwrap(); + std::fs::write( + dir_path.join("contract/Cargo.toml"), + r#" +[package] +name = "test-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +"#, + ) + .unwrap(); + std::fs::write(dir_path.join("contract/src/lib.rs"), "").unwrap(); + + // Build with custom profile - warning should appear + sandbox + .new_assert_cmd("contract") + .current_dir(dir_path) + .arg("build") + .arg("--profile=custom") + .assert() + .failure() + .stderr(predicate::str::contains( + "`overflow-checks` is not enabled for profile `custom`", + )); +} + +#[test] +fn build_no_warning_with_print_commands_only() { + let sandbox = TestEnv::default(); + let temp = TempDir::new().unwrap(); + let dir_path = temp.path(); + + // Create a workspace without overflow-checks + std::fs::write( + dir_path.join("Cargo.toml"), + r#" +[workspace] +resolver = "2" +members = ["contract"] + +[profile.release] +opt-level = "z" +"#, + ) + .unwrap(); + + std::fs::create_dir_all(dir_path.join("contract/src")).unwrap(); + std::fs::write( + dir_path.join("contract/Cargo.toml"), + r#" +[package] +name = "test-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +"#, + ) + .unwrap(); + std::fs::write(dir_path.join("contract/src/lib.rs"), "").unwrap(); + + // With --print-commands-only, no warning should appear + sandbox + .new_assert_cmd("contract") + .current_dir(dir_path) + .arg("build") + .arg("--print-commands-only") + .assert() + .success() + .stderr(predicate::str::contains("overflow-checks").not()); +} + #[test] fn build_always_injects_cli_version() { let sandbox = TestEnv::default(); diff --git a/cmd/soroban-cli/src/commands/contract/build.rs b/cmd/soroban-cli/src/commands/contract/build.rs index 7cd33ab33..6f08bac3a 100644 --- a/cmd/soroban-cli/src/commands/contract/build.rs +++ b/cmd/soroban-cli/src/commands/contract/build.rs @@ -177,6 +177,11 @@ impl Cmd { let packages = self.packages(&metadata)?; let target_dir = &metadata.target_directory; + // Run build configuration checks (only when actually building) + if !self.print_commands_only { + run_checks(&print, metadata.workspace_root.as_std_path(), &self.profile); + } + if let Some(package) = &self.package { if packages.is_empty() { return Err(Error::PackageNotFound { @@ -599,3 +604,70 @@ fn get_wasm_target() -> Result { Ok(WASM_TARGET.into()) } } + +/// Run build configuration checks and emit warnings for potential issues. +/// Each check is responsible for emitting its own warnings. +fn run_checks(print: &Print, workspace_root: &Path, profile: &str) { + let cargo_toml_path = workspace_root.join("Cargo.toml"); + + let cargo_toml_str = match fs::read_to_string(&cargo_toml_path) { + Ok(s) => s, + Err(e) => { + print.warnln(format!("Could not read Cargo.toml to run checks: {e}")); + return; + } + }; + + let doc: toml_edit::DocumentMut = match cargo_toml_str.parse() { + Ok(d) => d, + Err(e) => { + print.warnln(format!("Could not parse Cargo.toml to run checks: {e}")); + return; + } + }; + + check_overflow_checks(print, &doc, profile); + // Future checks can be added here +} + +/// Check if overflow-checks is enabled for the specified profile. +/// Emits a warning if not enabled. +fn check_overflow_checks(print: &Print, doc: &toml_edit::DocumentMut, profile: &str) { + // Helper to check a profile and follow inheritance chain + fn get_overflow_checks( + doc: &toml_edit::DocumentMut, + profile: &str, + visited: &mut Vec, + ) -> Option { + if visited.contains(&profile.to_string()) { + return None; // Prevent infinite loops + } + visited.push(profile.to_string()); + + let profile_section = doc.get("profile")?.get(profile)?; + + // Check if overflow-checks is explicitly set + if let Some(val) = profile_section + .get("overflow-checks") + .and_then(toml_edit::Item::as_bool) + { + return Some(val); + } + + // Check inherited profile + if let Some(inherits) = profile_section.get("inherits").and_then(|v| v.as_str()) { + return get_overflow_checks(doc, inherits, visited); + } + + None + } + + let mut visited = Vec::new(); + if get_overflow_checks(doc, profile, &mut visited) != Some(true) { + print.warnln(format!( + "`overflow-checks` is not enabled for profile `{profile}`. \ + To prevent silent integer overflow, add `overflow-checks = true` to \ + [profile.{profile}] in your Cargo.toml." + )); + } +}