use std::{
env, fs,
path::{Path, PathBuf},
};
use crate::{
artifact_io::{
embed_candid_metadata, maybe_shrink_wasm_artifact, write_gzip_artifact, write_wasm_artifact,
},
bootstrap_store::build_bootstrap_wasm_store_artifact,
cargo_command, remove_optional_file,
role_contract::{
PackageValidationMode, RolePackageEvidence, RolePackageValidation, finding_detail,
resolve_declared_role_package_contract_from_config,
validate_declared_role_package_from_config,
},
should_export_candid_artifacts,
};
use super::{
CanisterBuildProfile, WorkspaceBuildContext,
cache::{canister_build_target_root, configure_canister_cargo_command},
candid::{extract_candid, remove_stale_icp_candid_sidecars},
model::{
CanisterArtifactBuildOutput, CanisterArtifactBuildSpec, LOCAL_ARTIFACT_ROOT_RELATIVE,
ROOT_ROLE, WASM_STORE_ROLE, WASM_TARGET,
},
wasm_store::build_hidden_wasm_store_artifact,
};
pub fn build_workspace_canister_artifact(
context: &WorkspaceBuildContext,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
if context.role == WASM_STORE_ROLE {
return build_hidden_wasm_store_artifact(context);
}
let config_source = fs::read_to_string(&context.config_path)?;
let config = canic_core::bootstrap::parse_config_model(&config_source)?;
let spec = resolve_canister_artifact_build_spec(context, &config)?;
build_workspace_canister_artifact_from_spec(context, &spec)
}
pub fn copy_icp_wasm_output(
canister_name: &str,
output: &CanisterArtifactBuildOutput,
) -> Result<(), Box<dyn std::error::Error>> {
let Some(path) = env::var_os("ICP_WASM_OUTPUT_PATH").map(PathBuf::from) else {
return Ok(());
};
if !output.wasm_path.is_file() {
return Err(format!(
"missing ICP wasm output source for {canister_name}: {}",
output.wasm_path.display()
)
.into());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&output.wasm_path, Path::new(&path))?;
Ok(())
}
pub fn build_workspace_canister_artifact_from_spec(
context: &WorkspaceBuildContext,
spec: &CanisterArtifactBuildSpec,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
if context.role != spec.role {
return Err(format!(
"build context role {} does not match admitted role {}",
context.role, spec.role
)
.into());
}
let canister_name = spec.role.as_str();
let require_embedded_release_artifacts = canister_name == ROOT_ROLE;
if require_embedded_release_artifacts {
build_bootstrap_wasm_store_artifact(context)?;
}
fs::create_dir_all(&spec.artifact_root)?;
remove_stale_icp_candid_sidecars(&spec.artifact_root)?;
let release_wasm_path = run_canister_build(
context,
&spec.package_manifest_path,
&spec.package_name,
require_embedded_release_artifacts,
)?;
write_wasm_artifact(&release_wasm_path, &spec.wasm_path)?;
maybe_shrink_wasm_artifact(&spec.wasm_path)?;
let network = &context.build_network;
if should_export_candid_artifacts(network) {
let debug_context = context.with_profile(CanisterBuildProfile::Debug);
let debug_wasm_path = run_canister_build(
&debug_context,
&spec.package_manifest_path,
&spec.package_name,
require_embedded_release_artifacts,
)?;
extract_candid(&debug_wasm_path, &spec.did_path)?;
embed_candid_metadata(&spec.wasm_path, &spec.did_path)?;
} else {
remove_optional_file(&spec.did_path)?;
}
write_gzip_artifact(&spec.wasm_path, &spec.wasm_gz_path)?;
Ok(CanisterArtifactBuildOutput {
artifact_root: spec.artifact_root.clone(),
wasm_path: spec.wasm_path.clone(),
wasm_gz_path: spec.wasm_gz_path.clone(),
did_path: spec.did_path.clone(),
})
}
pub fn resolve_canister_artifact_build_spec(
context: &WorkspaceBuildContext,
config: &canic_core::bootstrap::compiled::ConfigModel,
) -> Result<CanisterArtifactBuildSpec, Box<dyn std::error::Error>> {
let canister_name = context.role.as_str();
let role = canic_core::ids::CanisterRole::owned(canister_name.to_string());
validate_artifact_role_attached(config, canister_name)?;
let evidence = match validate_declared_role_package_from_config(
&context.config_path,
config,
&role,
PackageValidationMode::Build,
) {
RolePackageValidation::Supported(evidence) => evidence,
RolePackageValidation::Unsupported(finding) => {
return Err(format!("{}: {}", finding.code(), finding_detail(&finding)).into());
}
};
require_declared_role_contract(config, &evidence)?;
let artifact_root = context
.icp_root
.join(LOCAL_ARTIFACT_ROOT_RELATIVE)
.join(canister_name);
Ok(CanisterArtifactBuildSpec {
role: canister_name.to_string(),
package_name: evidence.role_package_name,
package_manifest_path: evidence.role_manifest_path,
wasm_path: artifact_root.join(format!("{canister_name}.wasm")),
wasm_gz_path: artifact_root.join(format!("{canister_name}.wasm.gz")),
did_path: artifact_root.join(format!("{canister_name}.did")),
artifact_root,
})
}
fn require_declared_role_contract(
config: &canic_core::bootstrap::compiled::ConfigModel,
evidence: &RolePackageEvidence,
) -> Result<(), Box<dyn std::error::Error>> {
match resolve_declared_role_package_contract_from_config(config, evidence) {
canic_core::role_contract::RoleContractResolution::Resolved { .. } => Ok(()),
canic_core::role_contract::RoleContractResolution::Rejected { errors } => Err(errors
.iter()
.map(|finding| format!("{}: {}", finding.code(), finding_detail(finding)))
.collect::<Vec<_>>()
.join("; ")
.into()),
}
}
fn validate_artifact_role_attached(
config: &canic_core::bootstrap::compiled::ConfigModel,
canister_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let role = canic_core::ids::CanisterRole::owned(canister_name.to_string());
if !config.roles.contains_key(&role) {
return Err(format!(
"role {canister_name} is not declared; declare the role before building an artifact"
)
.into());
}
if !config.attached_roles().contains(&role) {
let fleet = config.fleet_name().unwrap_or("<unknown>");
return Err(format!(
"role {fleet}.{canister_name} is declared but not attached to topology; run `canic fleet role attach {fleet} {canister_name} --subnet <subnet>` before building an artifact"
)
.into());
}
Ok(())
}
fn run_canister_build(
context: &WorkspaceBuildContext,
manifest_path: &Path,
package_name: &str,
require_embedded_release_artifacts: bool,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let target_root = canister_build_target_root(&context.workspace_root);
let mut command = cargo_command();
context.apply_to_command(&mut command);
command
.current_dir(&context.workspace_root)
.env(
canic_core::role_contract::CANONICAL_BUILD_MARKER_ENV,
canic_core::role_contract::CANONICAL_BUILD_MARKER_VALUE,
)
.args([
"build",
"--manifest-path",
&manifest_path.display().to_string(),
"--target",
WASM_TARGET,
])
.args(context.profile.cargo_args());
configure_canister_cargo_command(&mut command, &context.workspace_root);
if require_embedded_release_artifacts {
command.env(
canic_core::role_contract::CANONICAL_BUILD_REQUIRE_EMBEDDED_ARTIFACTS_ENV,
canic_core::role_contract::CANONICAL_BUILD_MARKER_VALUE,
);
}
let output = command.output()?;
if !output.status.success() {
return Err(format!(
"cargo build failed for {}: {}",
manifest_path.display(),
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(target_root
.join(WASM_TARGET)
.join(context.profile.target_dir_name())
.join(format!("{}.wasm", package_name.replace('-', "_"))))
}