| @@ -156,20 +156,40 @@ impl Builder for CargoBuilder { |
| ) -> Result<Vec<ArtifactPath>> { |
| let crate_version = read_crate_version(config.manifest_path.as_deref()).await?; |
| |
| + let feat_suf = feature_suffix(&config.features, config.no_default_features, &config.suffix); |
| + let rel_override = feat_suf.as_deref().map(|s| format!("release-{s}")); |
| + |
| if config.targets.is_empty() { |
| // Single host target: forward stderr lines as plain LogEvent::Line events. |
| let host = get_host_target().await?; |
| let line_tx = line_bridge(log, LogEvent::Line); |
| - run_cargo_build( |
| - &config, |
| - None, |
| - &host, |
| - &crate_version, |
| - abbaye_version, |
| - line_tx, |
| - None, |
| - ) |
| - .await |
| + if let Some(ref rel) = rel_override { |
| + // Feature flags affect the binary, so use an isolated target dir |
| + // to prevent cargo cache contamination from the default build. |
| + let tmpdir = TempDir::new().into_diagnostic()?; |
| + let artifacts = run_cargo_build( |
| + &config, |
| + None, |
| + &host, |
| + &crate_version, |
| + abbaye_version, |
| + line_tx, |
| + Some(tmpdir.path()), |
| + ) |
| + .await?; |
| + relocate_artifacts(artifacts, tmpdir.path(), Some(rel)).await |
| + } else { |
| + run_cargo_build( |
| + &config, |
| + None, |
| + &host, |
| + &crate_version, |
| + abbaye_version, |
| + line_tx, |
| + None, |
| + ) |
| + .await |
| + } |
| } else { |
| // Multiple targets: each runs in its own task with its own |
| // temporary target directory so cargo's file lock does not |
| @@ -182,6 +202,7 @@ impl Builder for CargoBuilder { |
| let abbaye_version = abbaye_version.to_owned(); |
| let target = target.clone(); |
| let log = log.clone(); |
| + let rel = rel_override.clone(); |
| |
| join_set.spawn(async move { |
| // Announce this target as a child task. |
| @@ -198,9 +219,12 @@ impl Builder for CargoBuilder { |
| line: l, |
| }); |
| |
| - let result = if config.parallel && !config.use_cross { |
| + let use_isolation = (config.parallel || rel.is_some()) && !config.use_cross; |
| + let result = if use_isolation { |
| // Give this invocation its own target directory so it |
| // does not contend with sibling builds on cargo's lock. |
| + // Also required when feature flags differ to prevent |
| + // binary overwrites from sibling builder entries. |
| let tmpdir = TempDir::new().into_diagnostic()?; |
| let r = run_cargo_build( |
| &config, |
| @@ -215,7 +239,9 @@ impl Builder for CargoBuilder { |
| // Copy artifacts to stable paths inside target/ before |
| // tmpdir is dropped, then let tmpdir clean up. |
| match r { |
| - Ok(artifacts) => relocate_artifacts(artifacts, tmpdir.path()).await, |
| + Ok(artifacts) => { |
| + relocate_artifacts(artifacts, tmpdir.path(), rel.as_deref()).await |
| + } |
| Err(e) => Err(e), |
| } |
| } else { |
| @@ -457,14 +483,25 @@ async fn run_cargo_build( |
| /// `<tmpdir>/<triple>/release/<name>`. Stripping the `tmpdir` prefix and |
| /// prepending `target/` gives the canonical path `target/<triple>/release/<name>`, |
| /// which is where a normal `cargo build --target <triple>` would place them. |
| +/// |
| +/// When `release_override` is set (e.g. `"release-no-default"`), the `release` |
| +/// component in the destination path is replaced, so artifacts from builds |
| +/// with different feature flags do not overwrite each other. |
| async fn relocate_artifacts( |
| artifacts: Vec<ArtifactPath>, |
| tmp_root: &std::path::Path, |
| + release_override: Option<&str>, |
| ) -> Result<Vec<ArtifactPath>> { |
| let mut relocated = Vec::with_capacity(artifacts.len()); |
| for artifact in artifacts { |
| let relative = artifact.path.strip_prefix(tmp_root).into_diagnostic()?; |
| - let stable = std::path::PathBuf::from("target").join(relative); |
| + let stable = if let Some(rel) = release_override { |
| + let relative_str = relative.to_string_lossy(); |
| + let replaced = relative_str.replace("/release/", &format!("/{rel}/")); |
| + std::path::PathBuf::from("target").join(&replaced) |
| + } else { |
| + std::path::PathBuf::from("target").join(relative) |
| + }; |
| if let Some(parent) = stable.parent() { |
| tokio::fs::create_dir_all(parent).await.into_diagnostic()?; |
| } |
| @@ -798,7 +835,9 @@ mod tests { |
| group_comment: None, |
| }]; |
| |
| - let relocated = relocate_artifacts(artifacts, &tmp_root).await.unwrap(); |
| + let relocated = relocate_artifacts(artifacts, &tmp_root, None) |
| + .await |
| + .unwrap(); |
| assert_eq!(relocated.len(), 1); |
| let expected = Path::new("target") |
| .join("x86_64-unknown-linux-musl") |
| @@ -812,6 +851,41 @@ mod tests { |
| |
| // ─── get_host_target ───────────────────────────────────────────────────── |
| |
| + #[tokio::test] |
| + async fn relocate_artifacts_with_release_override() { |
| + let tmp = tempfile::tempdir().unwrap(); |
| + let tmp_root = tmp.path().join("cross-tmp"); |
| + let triple = "arm-unknown-linux-gnueabihf"; |
| + let triple_dir = tmp_root.join(triple).join("release"); |
| + tokio::fs::create_dir_all(&triple_dir).await.unwrap(); |
| + let binary_path = triple_dir.join("myapp"); |
| + tokio::fs::write(&binary_path, b"feature-specific content") |
| + .await |
| + .unwrap(); |
| + |
| + let artifacts = vec![ArtifactPath { |
| + path: binary_path, |
| + name: "myapp-0.10.0-arm-unknown-linux-gnueabihf-no-default".to_owned(), |
| + hash: None, |
| + category: None, |
| + group_name: None, |
| + group_comment: None, |
| + }]; |
| + |
| + let relocated = relocate_artifacts(artifacts, &tmp_root, Some("release-no-default")) |
| + .await |
| + .unwrap(); |
| + assert_eq!(relocated.len(), 1); |
| + let expected = Path::new("target") |
| + .join(triple) |
| + .join("release-no-default") |
| + .join("myapp"); |
| + assert_eq!(relocated[0].path, expected); |
| + assert!(expected.exists(), "binary should exist at override path"); |
| + let content = tokio::fs::read_to_string(&expected).await.unwrap(); |
| + assert_eq!(content, "feature-specific content"); |
| + } |
| + |
| #[tokio::test] |
| async fn test_get_host_target_returns_triple() { |
| let triple = get_host_target().await.unwrap(); |