Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 246 additions & 1 deletion cmd/crates/soroban-test/tests/it/build.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down
72 changes: 72 additions & 0 deletions cmd/soroban-cli/src/commands/contract/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -599,3 +604,70 @@ fn get_wasm_target() -> Result<String, Error> {
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<String>,
) -> Option<bool> {
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."
));
}
}