mod output;
#[cfg(test)]
mod tests;
use super::{
AppCanisterArtifactBuildOutput, CanisterBuildProfile, TimedCanisterArtifactBuildOutput,
WorkspaceBuildContext,
cache::{
CargoBuildProgress, configure_canister_cargo_command, configure_declaration_command,
lock_canister_build_target, output_canister_cargo_command,
},
candid::remove_stale_icp_candid_sidecars,
candid_cache::{
CandidExtractionCache, CandidExtractionInput, extract_configured_candid,
extract_configured_candids,
},
model::{
CanisterArtifactBuildOptions, CanisterArtifactBuildOutput, CanisterArtifactBuildSpec,
CanisterArtifactSource, ConfiguredCanisterArtifactBuildOutput, FLEET_COORDINATOR_ROLE,
WASM_STORE_ROLE, WASM_TARGET,
},
};
use crate::{
artifact_io::{CapturedWasmArtifact, WasmArtifactFinalization, finalize_wasm_artifact},
bootstrap_coordinator::{
build_bootstrap_fleet_coordinator_artifact, compile_bootstrap_fleet_coordinator_artifact,
},
bootstrap_store::{build_bootstrap_wasm_store_artifact, compile_bootstrap_wasm_store_artifact},
build_toolchain::BuildToolchain,
cargo_command,
cargo_metadata::CargoFeatureSelection,
release_set::AppConfigSnapshot,
role_contract::{
PackageValidationMode, RoleCargoGraphEvidence, RolePackageValidation, finding_detail,
resolve_declared_role_package_contract, validate_declared_role_package,
validate_declared_role_packages,
},
should_embed_candid_metadata,
};
use std::{
collections::BTreeMap,
env, fmt,
fmt::{Display, Formatter},
fs,
path::{Path, PathBuf},
process::Command,
time::Instant,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CanisterArtifactBuilder {
toolchain: BuildToolchain,
}
pub fn prepare_workspace_infrastructure_packages(
context: &WorkspaceBuildContext,
) -> Result<(), Box<dyn std::error::Error>> {
crate::bootstrap_coordinator::prepare_bootstrap_fleet_coordinator_package(context)?;
crate::bootstrap_store::prepare_bootstrap_wasm_store_package(context)
}
struct CanisterBuildGroup<'a> {
workspace_root: PathBuf,
specs: Vec<&'a CanisterArtifactBuildSpec>,
}
impl CanisterBuildGroup<'_> {
fn progress(&self, batch: usize, batches: usize, started: Instant) -> CargoBuildProgress {
CargoBuildProgress::batch(
self.specs.iter().map(|spec| spec.role.as_str()),
batch,
batches,
started,
)
}
}
impl CanisterArtifactBuilder {
pub fn for_profile(profile: CanisterBuildProfile) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
toolchain: BuildToolchain::resolve(profile)?,
})
}
#[must_use]
pub fn diagnostic_lines(&self) -> Vec<String> {
self.toolchain.diagnostic_lines()
}
pub fn prepare_complete_build_reuse(
&self,
context: &WorkspaceBuildContext,
progress: impl FnMut(super::BuildReuseProgress) -> std::io::Result<()>,
) -> Result<super::CompleteBuildReuse, super::BuildReuseError> {
super::CompleteBuildReuse::prepare(context, &self.toolchain, progress)
}
pub fn build_workspace_canister_artifact(
&self,
context: &WorkspaceBuildContext,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
self.build_workspace_canister_artifact_with_options(
context,
&CanisterArtifactBuildOptions::default(),
)
}
pub fn build_workspace_canister_artifact_with_options(
&self,
context: &WorkspaceBuildContext,
options: &CanisterArtifactBuildOptions,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
self.toolchain.require_profile(context.profile)?;
let _build_target_lock = lock_canister_build_target(&context.workspace_root)?;
match CanisterArtifactSource::for_role(&context.role) {
CanisterArtifactSource::FleetCoordinator => {
require_default_build_options(options, FLEET_COORDINATOR_ROLE)?;
return build_bootstrap_fleet_coordinator_artifact(context, &self.toolchain);
}
CanisterArtifactSource::WasmStore => {
require_default_build_options(options, WASM_STORE_ROLE)?;
return build_bootstrap_wasm_store_artifact(context, &self.toolchain);
}
CanisterArtifactSource::Root => require_default_build_options(options, "root")?,
CanisterArtifactSource::DeclaredRole => {}
}
let config = AppConfigSnapshot::load(&context.config_path)?;
let spec = resolve_canister_artifact_build_spec(context, config.model(), options)?;
build_workspace_canister_artifact_from_spec(context, &spec, options, &self.toolchain)
}
pub fn build_workspace_app_artifacts(
&self,
context: &WorkspaceBuildContext,
roles: &[String],
) -> Result<AppCanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
self.toolchain.require_profile(context.profile)?;
let _build_target_lock = lock_canister_build_target(&context.workspace_root)?;
let config = AppConfigSnapshot::load(&context.config_path)?;
let specs = resolve_canister_artifact_build_specs(context, config.model(), roles)?;
std::thread::scope(|scope| {
let coordinator_started = Instant::now();
let coordinator = compile_bootstrap_fleet_coordinator_artifact(context)?;
let coordinator = scope.spawn(move || {
coordinator
.finish(&self.toolchain)
.map(|output| TimedCanisterArtifactBuildOutput {
output,
elapsed: coordinator_started.elapsed(),
})
.map_err(|error| error.to_string())
});
let store_started = Instant::now();
let store = compile_bootstrap_wasm_store_artifact(context)?;
let store = scope.spawn(move || {
store
.finish(&self.toolchain)
.map(|output| TimedCanisterArtifactBuildOutput {
output,
elapsed: store_started.elapsed(),
})
.map_err(|error| error.to_string())
});
let configured_started = Instant::now();
let configured = build_configured_artifacts(context, &specs, &self.toolchain);
let configured_elapsed = configured_started.elapsed();
let coordinator = coordinator
.join()
.map_err(|_| "Coordinator finalization thread panicked")?;
let store = store
.join()
.map_err(|_| "Store finalization thread panicked")?;
Ok(AppCanisterArtifactBuildOutput {
coordinator: coordinator?,
store: store?,
configured: configured?,
configured_elapsed,
})
})
}
pub fn build_workspace_configured_canister_artifacts(
&self,
context: &WorkspaceBuildContext,
roles: &[String],
) -> Result<Vec<ConfiguredCanisterArtifactBuildOutput>, Box<dyn std::error::Error>> {
self.toolchain.require_profile(context.profile)?;
let _build_target_lock = lock_canister_build_target(&context.workspace_root)?;
let config = AppConfigSnapshot::load(&context.config_path)?;
let specs = resolve_canister_artifact_build_specs(context, config.model(), roles)?;
build_configured_artifacts(context, &specs, &self.toolchain)
}
}
pub fn build_workspace_canister_artifact(
context: &WorkspaceBuildContext,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
CanisterArtifactBuilder::for_profile(context.profile)?
.build_workspace_canister_artifact(context)
}
fn build_configured_artifacts(
context: &WorkspaceBuildContext,
specs: &[CanisterArtifactBuildSpec],
toolchain: &BuildToolchain,
) -> Result<Vec<ConfiguredCanisterArtifactBuildOutput>, Box<dyn std::error::Error>> {
let outputs =
build_workspace_canister_artifacts_from_specs_with_toolchain(context, specs, toolchain)?;
Ok(specs
.iter()
.map(|spec| spec.role.clone())
.zip(outputs)
.map(|(role, output)| ConfiguredCanisterArtifactBuildOutput { role, output })
.collect())
}
pub fn build_workspace_canister_artifact_with_options(
context: &WorkspaceBuildContext,
options: &CanisterArtifactBuildOptions,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
CanisterArtifactBuilder::for_profile(context.profile)?
.build_workspace_canister_artifact_with_options(context, options)
}
fn require_default_build_options(
options: &CanisterArtifactBuildOptions,
role: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if options == &CanisterArtifactBuildOptions::default() {
return Ok(());
}
Err(format!(
"caller-selected Cargo features and sidecar-only Candid are unsupported for built-in role {role}"
)
.into())
}
pub fn build_workspace_configured_canister_artifacts(
context: &WorkspaceBuildContext,
roles: &[String],
) -> Result<Vec<ConfiguredCanisterArtifactBuildOutput>, Box<dyn std::error::Error>> {
CanisterArtifactBuilder::for_profile(context.profile)?
.build_workspace_configured_canister_artifacts(context, roles)
}
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(())
}
fn build_workspace_canister_artifact_from_spec(
context: &WorkspaceBuildContext,
spec: &CanisterArtifactBuildSpec,
options: &CanisterArtifactBuildOptions,
toolchain: &BuildToolchain,
) -> 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());
}
prepare_canister_artifact_output(spec)?;
let release_wasm_path =
run_canister_profile_candid_build(context, &spec.package_manifest_path, options)?;
let cache = CandidExtractionCache::prepare(context);
let candid = extract_configured_candid(cache.as_ref(), &spec.role, &release_wasm_path)?;
let profile = canic_core::role_contract::derive_protocol_profile_hashes(
&spec.canic_version,
&canic_contracts::ids::CanisterRole::owned(spec.role.clone()),
&spec.capabilities,
&candid,
);
let release_wasm_path = run_canister_build(
context,
&spec.package_manifest_path,
Some(profile.protocol_profile_digest),
options,
)?;
finish_canister_artifact_output(
context,
spec,
&release_wasm_path,
&candid,
profile,
options.sidecar_only_candid,
toolchain,
)
}
#[expect(
clippy::too_many_lines,
reason = "one locked build session orders declaration extraction, runtime capture and finalization across workspace batches"
)]
fn build_workspace_canister_artifacts_from_specs_with_toolchain(
context: &WorkspaceBuildContext,
specs: &[CanisterArtifactBuildSpec],
toolchain: &BuildToolchain,
) -> Result<Vec<CanisterArtifactBuildOutput>, Box<dyn std::error::Error>> {
toolchain.require_profile(context.profile)?;
if specs.is_empty() {
return Ok(Vec::new());
}
for spec in specs {
prepare_canister_artifact_output(spec)?;
}
let started = Instant::now();
let workspace_groups = group_build_specs_without_feature_changes(context, specs)?;
eprintln!(
"Build phase dependency feature admission: {:.2}s ({} compatible batches)",
started.elapsed().as_secs_f64(),
workspace_groups.len()
);
let started = Instant::now();
let mut profiles = BTreeMap::new();
let cache = CandidExtractionCache::prepare(context);
for (index, group) in workspace_groups.iter().enumerate() {
let messages = run_canister_build_batch(
context,
group,
group.progress(index + 1, workspace_groups.len(), started),
)?;
let declarations = declaration_inputs(&group.specs, &messages)?;
let candids = extract_configured_candids(cache.as_ref(), &declarations)?;
for (spec, candid) in group.specs.iter().zip(candids) {
let profile = canic_core::role_contract::derive_protocol_profile_hashes(
&spec.canic_version,
&canic_contracts::ids::CanisterRole::owned(spec.role.clone()),
&spec.capabilities,
&candid,
);
profiles.insert(spec.role.clone(), (candid, profile));
}
}
eprintln!(
"Build phase declaration and Candid extraction: {:.2}s",
started.elapsed().as_secs_f64()
);
let started = Instant::now();
let mut captured = BTreeMap::new();
for (index, group) in workspace_groups.iter().enumerate() {
let entries = group
.specs
.iter()
.map(
|spec| canic_core::role_contract::build_context::ProtocolBuildEntry {
package: spec.package_name.clone(),
role: spec.role.clone(),
digest: profiles[&spec.role].1.protocol_profile_digest.to_string(),
},
)
.collect();
let batch =
canic_core::role_contract::build_context::encode_protocol_build_context(entries)?;
let messages = run_canister_runtime_batch(
context,
group,
batch,
group.progress(index + 1, workspace_groups.len(), started),
)?;
for spec in &group.specs {
let wasm = output::select(&messages, &spec.package_manifest_path)?;
captured.insert(
spec.role.clone(),
CapturedWasmArtifact::capture(&wasm, &spec.wasm_path)?,
);
}
}
eprintln!(
"Build phase runtime Cargo/link: {:.2}s",
started.elapsed().as_secs_f64()
);
std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(specs.len());
for spec in specs {
let (candid, profile) = profiles.remove(&spec.role).expect("derived role profile");
let source = captured.remove(&spec.role).expect("captured role output");
handles.push(scope.spawn(move || {
finish_canister_artifact_output(
context,
spec,
source.path(),
&candid,
profile,
false,
toolchain,
)
.map_err(|error| error.to_string())
}));
}
handles
.into_iter()
.map(|handle| {
handle
.join()
.map_err(|_| "configured artifact finalization thread panicked".to_string())?
.map_err(Into::into)
})
.collect()
})
}
fn declaration_inputs<'a>(
specs: &[&'a CanisterArtifactBuildSpec],
messages: &[u8],
) -> Result<Vec<CandidExtractionInput<'a>>, output::CargoArtifactError> {
specs
.iter()
.map(|spec| {
Ok(CandidExtractionInput {
role: &spec.role,
wasm: output::select(messages, &spec.package_manifest_path)?,
})
})
.collect()
}
fn group_build_specs_by_workspace(
specs: &[CanisterArtifactBuildSpec],
) -> BTreeMap<PathBuf, Vec<&CanisterArtifactBuildSpec>> {
let mut groups = BTreeMap::<PathBuf, Vec<&CanisterArtifactBuildSpec>>::new();
for spec in specs {
groups
.entry(spec.cargo_workspace_root.clone())
.or_default()
.push(spec);
}
groups
}
fn group_build_specs_without_feature_changes<'a>(
context: &WorkspaceBuildContext,
specs: &'a [CanisterArtifactBuildSpec],
) -> Result<Vec<CanisterBuildGroup<'a>>, Box<dyn std::error::Error>> {
let mut groups = Vec::new();
for (workspace, specs) in group_build_specs_by_workspace(specs) {
let packages = specs
.iter()
.map(|spec| spec.package_name.clone())
.collect::<Vec<_>>();
let compatible = super::batch::compatible_package_groups(context, &workspace, &packages)?;
for packages in compatible {
let selected = specs
.iter()
.copied()
.filter(|spec| packages.contains(&spec.package_name))
.collect();
groups.push(CanisterBuildGroup {
workspace_root: workspace.clone(),
specs: selected,
});
}
}
Ok(groups)
}
fn run_canister_runtime_batch(
context: &WorkspaceBuildContext,
group: &CanisterBuildGroup<'_>,
batch: String,
progress: CargoBuildProgress,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut command = canister_cargo_build_command(
context,
&group.workspace_root.join("Cargo.toml"),
context.profile,
);
command.env(
canic_core::role_contract::build_context::PROTOCOL_BUILD_CONTEXT_ENV,
batch,
);
for spec in &group.specs {
command.arg("--package").arg(&spec.package_name);
}
let output = output_canister_cargo_command(&mut command, progress)?;
if !output.status.success() {
return Err(format!(
"configured runtime batch failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(output.stdout)
}
fn prepare_canister_artifact_output(
spec: &CanisterArtifactBuildSpec,
) -> Result<(), Box<dyn std::error::Error>> {
fs::create_dir_all(&spec.artifact_root)?;
remove_stale_icp_candid_sidecars(&spec.artifact_root)?;
Ok(())
}
fn finish_canister_artifact_output(
context: &WorkspaceBuildContext,
spec: &CanisterArtifactBuildSpec,
release_wasm_path: &Path,
candid: &[u8],
profile: canic_core::role_contract::ProtocolProfileHashes,
sidecar_only_candid: bool,
toolchain: &BuildToolchain,
) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
let transforms = finalize_wasm_artifact(
&WasmArtifactFinalization {
release_build_id: context.release_build_id,
profile: context.profile,
build_network: context.build_network,
embed_candid: !sidecar_only_candid
&& should_embed_candid_metadata(context.build_network),
validate_sidecar_only: sidecar_only_candid,
source_wasm_path: release_wasm_path,
candid,
wasm_path: &spec.wasm_path,
did_path: &spec.did_path,
wasm_gz_path: &spec.wasm_gz_path,
},
toolchain,
)?;
Ok(CanisterArtifactBuildOutput {
package_name: spec.package_name.clone(),
package_version: spec.package_version.clone(),
protocol_release_identity: spec.canic_version.clone(),
protocol_role: canic_contracts::ids::CanisterRole::owned(spec.role.clone()),
protocol_capabilities: spec.capabilities.clone(),
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(),
candid_sha256: profile.candid_sha256,
protocol_profile_digest: profile.protocol_profile_digest,
transforms,
})
}
pub fn resolve_canister_artifact_build_spec(
context: &WorkspaceBuildContext,
config: &canic_core::bootstrap::compiled::ConfigModel,
options: &CanisterArtifactBuildOptions,
) -> Result<CanisterArtifactBuildSpec, Box<dyn std::error::Error>> {
let canister_name = context.role.as_str();
let role = canic_contracts::ids::CanisterRole::owned(canister_name.to_string());
validate_artifact_role_deployable(config, canister_name)?;
let validation = validate_declared_role_package(
&context.config_path,
config,
&role,
PackageValidationMode::Build,
&CargoFeatureSelection {
features: options.cargo_features.clone(),
default_features: options.default_features,
},
);
resolve_canister_artifact_build_spec_from_validation(context, config, canister_name, validation)
}
pub fn resolve_canister_artifact_build_specs(
context: &WorkspaceBuildContext,
config: &canic_core::bootstrap::compiled::ConfigModel,
roles: &[String],
) -> Result<Vec<CanisterArtifactBuildSpec>, Box<dyn std::error::Error>> {
let mut failures = Vec::new();
let mut admitted = Vec::with_capacity(roles.len());
for role in roles {
match validate_artifact_role_deployable(config, role) {
Ok(()) => admitted.push((
role,
canic_contracts::ids::CanisterRole::owned(role.clone()),
)),
Err(source) => failures.push(ConfiguredBuildSpecFailure {
role: role.clone(),
source,
}),
}
}
if !failures.is_empty() {
return Err(ConfiguredBuildSpecFailures(failures).into());
}
let role_ids = admitted
.iter()
.map(|(_, role_id)| role_id.clone())
.collect::<Vec<_>>();
let validations = validate_declared_role_packages(
&context.config_path,
config,
&role_ids,
PackageValidationMode::Build,
);
let mut specs = Vec::with_capacity(roles.len());
for ((role, _), validation) in admitted.into_iter().zip(validations) {
match resolve_canister_artifact_build_spec_from_validation(
context, config, role, validation,
) {
Ok(spec) => specs.push(spec),
Err(source) => failures.push(ConfiguredBuildSpecFailure {
role: role.clone(),
source,
}),
}
}
if failures.is_empty() {
Ok(specs)
} else {
Err(ConfiguredBuildSpecFailures(failures).into())
}
}
#[derive(Debug)]
struct ConfiguredBuildSpecFailures(Vec<ConfiguredBuildSpecFailure>);
#[derive(Debug)]
struct ConfiguredBuildSpecFailure {
role: String,
source: Box<dyn std::error::Error>,
}
impl Display for ConfiguredBuildSpecFailure {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.role, self.source)
}
}
impl Display for ConfiguredBuildSpecFailures {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(
formatter,
"configured build specification failed: {}",
self.0
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
)
}
}
impl std::error::Error for ConfiguredBuildSpecFailures {}
fn resolve_canister_artifact_build_spec_from_validation(
context: &WorkspaceBuildContext,
config: &canic_core::bootstrap::compiled::ConfigModel,
canister_name: &str,
validation: RolePackageValidation,
) -> Result<CanisterArtifactBuildSpec, Box<dyn std::error::Error>> {
let evidence = match validation {
RolePackageValidation::Supported(evidence) => evidence,
RolePackageValidation::Unsupported(finding) => {
return Err(format!("{}: {}", finding.code(), finding_detail(&finding)).into());
}
};
let contract = require_declared_role_contract(config, &evidence)?;
let artifact_root = context.artifact_root().join(canister_name);
Ok(CanisterArtifactBuildSpec {
role: canister_name.to_string(),
package_name: evidence.role_package_name,
package_version: evidence.role_package_version,
canic_version: evidence.canic_version,
capabilities: contract.capabilities,
package_manifest_path: evidence.role_manifest_path,
cargo_workspace_root: evidence.cargo_workspace_root,
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: &RoleCargoGraphEvidence,
) -> Result<canic_core::role_contract::ResolvedRoleContract, Box<dyn std::error::Error>> {
match resolve_declared_role_package_contract(config, evidence) {
canic_core::role_contract::RoleContractResolution::Resolved { contract } => Ok(contract),
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_deployable(
config: &canic_core::bootstrap::compiled::ConfigModel,
canister_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let role = canic_contracts::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.deployable_roles().contains(&role) {
let app = config.app_id().as_str();
return Err(format!(
"role {app}.{canister_name} is declared but not attached to topology; run `canic app role attach {app} {canister_name} --component-spec <component-spec>` before building an artifact"
)
.into());
}
Ok(())
}
fn run_canister_build(
context: &WorkspaceBuildContext,
manifest_path: &Path,
protocol_profile_digest: Option<canic_core::role_contract::ProtocolProfileDigest>,
options: &CanisterArtifactBuildOptions,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut command = canister_runtime_command(context, manifest_path, context.profile, options);
command.env_remove(canic_core::role_contract::PROTOCOL_PROFILE_DIGEST_ENV);
if let Some(digest) = protocol_profile_digest {
command.env(
canic_core::role_contract::PROTOCOL_PROFILE_DIGEST_ENV,
digest.to_string(),
);
}
let output =
output_canister_cargo_command(&mut command, CargoBuildProgress::single(&context.role))?;
if !output.status.success() {
return Err(format!(
"cargo build failed for {}: {}",
manifest_path.display(),
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(output::select(&output.stdout, manifest_path)?)
}
fn run_canister_profile_candid_build(
context: &WorkspaceBuildContext,
manifest_path: &Path,
options: &CanisterArtifactBuildOptions,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut command =
canister_profile_candid_command(context, manifest_path, context.profile, options);
let output =
output_canister_cargo_command(&mut command, CargoBuildProgress::single(&context.role))?;
if !output.status.success() {
return Err(format!(
"profile Candid build failed for {}: {}",
manifest_path.display(),
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(output::select(&output.stdout, manifest_path)?)
}
fn canister_profile_candid_command(
context: &WorkspaceBuildContext,
manifest_path: &Path,
profile: CanisterBuildProfile,
options: &CanisterArtifactBuildOptions,
) -> Command {
let mut command = canister_cargo_build_command(context, manifest_path, profile);
configure_declaration_command(&mut command, context);
apply_cargo_feature_selection(&mut command, options);
command
}
fn canister_runtime_command(
context: &WorkspaceBuildContext,
manifest_path: &Path,
profile: CanisterBuildProfile,
options: &CanisterArtifactBuildOptions,
) -> Command {
let mut command = canister_cargo_build_command(context, manifest_path, profile);
apply_cargo_feature_selection(&mut command, options);
command
}
fn apply_cargo_feature_selection(command: &mut Command, options: &CanisterArtifactBuildOptions) {
if !options.default_features {
command.arg("--no-default-features");
}
if !options.cargo_features.is_empty() {
command.arg("--features").arg(
options
.cargo_features
.iter()
.cloned()
.collect::<Vec<_>>()
.join(","),
);
}
}
fn run_canister_build_batch(
context: &WorkspaceBuildContext,
group: &CanisterBuildGroup<'_>,
progress: CargoBuildProgress,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut command = canister_cargo_batch_command(
context,
&group.workspace_root,
&group.specs,
context.profile,
);
let output = output_canister_cargo_command(&mut command, progress)?;
if output.status.success() {
return Ok(output.stdout);
}
let roles = group
.specs
.iter()
.map(|spec| spec.role.as_str())
.collect::<Vec<_>>()
.join(", ");
Err(format!(
"Cargo batch build failed for configured roles {roles}: {}",
String::from_utf8_lossy(&output.stderr)
)
.into())
}
fn canister_cargo_batch_command(
context: &WorkspaceBuildContext,
cargo_workspace_root: &Path,
specs: &[&CanisterArtifactBuildSpec],
profile: CanisterBuildProfile,
) -> Command {
let manifest_path = cargo_workspace_root.join("Cargo.toml");
let mut command = canister_cargo_build_command(context, &manifest_path, profile);
configure_declaration_command(&mut command, context);
for spec in specs {
command.arg("--package").arg(&spec.package_name);
}
command
}
pub(super) fn canister_cargo_build_command(
context: &WorkspaceBuildContext,
manifest_path: &Path,
profile: CanisterBuildProfile,
) -> Command {
canister_cargo_command(context, manifest_path, profile, "build")
}
fn canister_cargo_command(
context: &WorkspaceBuildContext,
manifest_path: &Path,
profile: CanisterBuildProfile,
cargo_subcommand: &str,
) -> Command {
let build_context = context.with_profile(profile);
let mut command = cargo_command();
build_context.apply_to_command(&mut command);
command
.env_remove(canic_core::role_contract::PROTOCOL_PROFILE_DIGEST_ENV)
.env_remove(canic_core::role_contract::build_context::PROTOCOL_BUILD_CONTEXT_ENV)
.current_dir(&build_context.workspace_root)
.env(
canic_core::role_contract::CANONICAL_BUILD_MARKER_ENV,
canic_core::role_contract::CANONICAL_BUILD_MARKER_VALUE,
)
.args([
cargo_subcommand,
"--locked",
"--keep-going",
"--message-format=json-render-diagnostics",
"--manifest-path",
&manifest_path.display().to_string(),
"--target",
WASM_TARGET,
])
.args(profile.cargo_args());
configure_canister_cargo_command(&mut command, &build_context.workspace_root);
command
}