Skip to content
Merged
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
40 changes: 40 additions & 0 deletions crates/alien-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ pub async fn build_stack(mut stack: Stack, settings: &BuildSettings) -> Result<S
settings.platform.runtime_platform()
);

// Release auto-builds and library callers can construct BuildSettings directly instead of
// going through the CLI planner. Enforce the platform's architecture contract here so every
// path fails before doing build work or writing artifacts.
plan::resolve_targets_for_stack_platform(
&stack,
settings.platform.runtime_platform(),
settings.targets.as_deref(),
)?;

// Run preflights (compile-time checks only)
let preflight_runner = PreflightRunner::new();
let preflight_started = Instant::now();
Expand Down Expand Up @@ -3668,6 +3677,37 @@ mod tests {
assert!(!output.path().join("build").join("machines").exists());
}

#[tokio::test]
async fn aws_build_rejects_worker_target_mismatch_before_writing_artifacts() {
let output = tempdir().unwrap();
let worker = Worker::new("job".to_string())
.permissions("execution".to_string())
.code(WorkerCode::Image {
image: "registry.example.com/job:latest".to_string(),
})
.build();
let stack = Stack::new("worker-target-mismatch".to_string())
.add(worker, alien_core::ResourceLifecycle::Live)
.build();
let settings = BuildSettings {
output_directory: output.path().display().to_string(),
platform: PlatformBuildSettings::Aws {
managing_account_id: None,
},
targets: Some(vec![BinaryTarget::LinuxX64]),
cache_url: None,
override_base_image: None,
debug_mode: false,
};

let error = build_stack(stack, &settings)
.await
.expect_err("AWS Worker should reject an x86-only build");

assert!(error.to_string().contains("expected LinuxArm64"));
assert!(!output.path().join("build").join("aws").exists());
}

#[test]
fn source_cache_hash_ignores_build_artifacts() {
let src = tempdir().unwrap();
Expand Down
95 changes: 83 additions & 12 deletions crates/alien-build/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ fn resolved_compute_architecture(
stack: &Stack,
platform: Platform,
) -> crate::error::Result<Architecture> {
let architectures = stack
let mut architectures = stack
.resources()
.filter_map(|(_, entry)| entry.config.downcast_ref::<ComputeCluster>())
.flat_map(|cluster| cluster.capacity_groups.iter())
Expand All @@ -294,24 +294,40 @@ fn resolved_compute_architecture(
})
})
.collect::<crate::error::Result<Vec<_>>>()?;
if architectures.is_empty() {
return instance_catalog::default_architecture(platform).ok_or_else(|| {
AlienError::new(crate::error::ErrorData::BuildConfigInvalid {
message: format!("managed cloud {platform} has no default compute architecture"),
})
});
}
let mut architectures = architectures.into_iter().collect::<Vec<_>>();
architectures.sort_by_key(|architecture| match architecture {
Architecture::Arm64 => 0,
Architecture::X86_64 => 1,
});
architectures.dedup();
match architectures.as_slice() {
[architecture] => Ok(*architecture),
_ => Err(AlienError::new(crate::error::ErrorData::BuildConfigInvalid {

let compute_architecture = match architectures.as_slice() {
[] => None,
[architecture] => Some(*architecture),
_ => return Err(AlienError::new(crate::error::ErrorData::BuildConfigInvalid {
message: "compute pools require mixed CPU architectures; one platform image cannot satisfy both".to_string(),
})),
};
let worker_architecture = (platform == Platform::Aws
&& stack
.resources()
.any(|(_, entry)| entry.config.downcast_ref::<Worker>().is_some()))
.then_some(Architecture::Arm64);

match (compute_architecture, worker_architecture) {
(Some(compute), Some(worker)) if compute != worker => Err(AlienError::new(
crate::error::ErrorData::BuildConfigInvalid {
message: format!(
"AWS Worker requires {worker:?}, but compute pools require {compute:?}; one platform image cannot satisfy both"
),
},
)),
Comment thread
alongubkin marked this conversation as resolved.
(Some(compute), _) => Ok(compute),
(None, Some(worker)) => Ok(worker),
(None, None) => instance_catalog::default_architecture(platform).ok_or_else(|| {
AlienError::new(crate::error::ErrorData::BuildConfigInvalid {
message: format!("managed cloud {platform} has no default compute architecture"),
})
}),
}
}

Expand Down Expand Up @@ -446,6 +462,30 @@ mod tests {
stack_with(cluster.build())
}

fn stack_with_worker_and_architecture(architecture: Architecture) -> Stack {
let cluster = ComputeCluster::new("runtime".to_string())
.capacity_group(CapacityGroup {
group_id: "pool".to_string(),
instance_type: None,
profile: Some(MachineProfile {
cpu: "1".to_string(),
memory_bytes: 2 * 1024 * 1024 * 1024,
ephemeral_storage_bytes: 20 * 1024 * 1024 * 1024,
architecture: Some(architecture),
gpu: None,
}),
min_size: 1,
max_size: 1,
scale_policy: None,
nested_virtualization: None,
})
.build();
Stack::new("plan-test".to_string())
.add(worker_with(rust_source()), ResourceLifecycle::Live)
.add(cluster, ResourceLifecycle::Live)
.build()
}

#[test]
fn aws_explicit_x86_uses_x64_runner() {
let stack = stack_with_architectures(&[Architecture::X86_64]);
Expand All @@ -464,6 +504,37 @@ mod tests {
);
}

#[test]
fn aws_worker_without_compute_constraint_requires_arm64() {
let stack = stack_with(worker_with(rust_source()));

assert_eq!(
resolve_targets_for_stack_platform(&stack, Aws, None).expect("target should resolve"),
vec![BinaryTarget::LinuxArm64]
);
}

#[test]
fn aws_worker_and_arm64_compute_share_one_target() {
let stack = stack_with_worker_and_architecture(Architecture::Arm64);

assert_eq!(
resolve_targets_for_stack_platform(&stack, Aws, None).expect("target should resolve"),
vec![BinaryTarget::LinuxArm64]
);
}

#[test]
fn aws_worker_and_x86_compute_are_rejected_before_build() {
let stack = stack_with_worker_and_architecture(Architecture::X86_64);

let error = resolve_targets_for_stack_platform(&stack, Aws, None)
.expect_err("incompatible fixed architectures should fail");
let message = error.to_string();
assert!(message.contains("AWS Worker requires Arm64"));
assert!(message.contains("compute pools require X86_64"));
}

#[test]
fn gcp_and_azure_without_compute_constraint_keep_x64_default() {
let stack = Stack::new("unconstrained".to_string()).build();
Expand Down
Loading