|
fn build_workspace(args: BuildArgs) -> Result<()> { |
|
let ws_root = crate::find_workspace_root(Utf8Path::new("."))?.ok_or_else(|| { |
|
crate::error::CompileError::without_span( |
|
"no workspace root found; run from a directory containing a [workspace] Cell.toml", |
|
) |
|
})?; |
|
let all_members = crate::resolve_workspace_members(&ws_root)?; |
|
let members: Vec<_> = if let Some(ref pkg_name) = args.package { |
|
// Find the specific member by reading its manifest for the package name. |
|
let mut found = Vec::new(); |
|
for member_dir in &all_members { |
|
let pm = crate::package::PackageManager::new(member_dir.as_std_path()); |
|
let manifest = pm.read_manifest()?; |
|
if manifest.package.name == *pkg_name { |
|
found.push(member_dir.clone()); |
|
} |
|
} |
|
if found.is_empty() { |
|
return Err(crate::error::CompileError::without_span(format!( |
|
"workspace member '{}' not found; available members: {}", |
|
pkg_name, |
|
all_members.iter().map(|m| m.as_str().to_string()).collect::<Vec<_>>().join(", ") |
|
))); |
|
} |
|
found |
|
} else { |
|
all_members |
|
}; |
|
|
|
let opt_level = if args.release { 3 } else { 1 }; |
|
let mut member_results = Vec::new(); |
|
let mut human_lines = Vec::new(); |
|
let mut failure_diagnostics = Vec::new(); |
|
let mut failed = 0; |
|
let policy_args = effective_build_check_args(&args)?; |
|
let executable_surface_policy = executable_surface_policy(&policy_args); |
|
|
|
for member_dir in &members { |
|
let options = CompileOptions { |
|
edition: crate::CURRENT_EDITION, |
|
opt_level, |
|
output: None, |
|
debug: false, |
|
target: args.target.clone(), |
|
target_profile: args.target_profile.clone(), |
|
primitive_compat: args.primitive_compat.clone(), |
|
}; |
|
|
|
let resolution_options = build_resolution_options(&args, crate::package::DependencyScope::Runtime); |
|
let entry_scope = match (args.entry_action.as_deref(), args.entry_lock.as_deref()) { |
|
(Some(action), None) => Some(CompileEntryScope::Action(action.to_string())), |
|
(None, Some(lock)) => Some(CompileEntryScope::Lock(lock.to_string())), |
|
(None, None) => None, |
|
(Some(_), Some(_)) => { |
|
return Err(crate::error::CompileError::without_span("--entry-action and --entry-lock are mutually exclusive")); |
|
} |
|
}; |
|
let compile_result = crate::package::with_resolution_options(resolution_options, || { |
|
compile_path_with_executable_surface_policy(member_dir, options, entry_scope, executable_surface_policy) |
|
}); |
|
|
|
match compile_result { |
|
Ok(result) => { |
|
if let Err(e) = validate_check_policy(&result.metadata, &policy_args) { |
|
failure_diagnostics.push(e.clone().with_file(member_dir.clone())); |
|
member_results.push(serde_json::json!({ |
|
"member": member_dir.as_str(), |
|
"status": "failed", |
|
"error": e.message, |
|
})); |
|
failed += 1; |
|
continue; |
|
} |
|
|
|
let resolved = resolve_input_path(member_dir)?; |
|
let output_path = default_output_path_for_input(member_dir, &resolved, result.artifact_format)?; |
|
result.write_to_path(&output_path)?; |
|
let metadata_path = default_metadata_path_for_artifact(&output_path); |
|
result.write_metadata_to_path(&metadata_path)?; |
|
let verified_sidecars = result.write_verified_artifact_sidecars(&output_path)?; |
|
|
|
member_results.push(serde_json::json!({ |
|
"member": member_dir.as_str(), |
|
"status": "ok", |
|
"artifact": output_path.to_string(), |
|
"metadata": metadata_path.to_string(), |
|
"lowering_record": verified_sidecars.as_ref().map(|paths| paths.0.to_string()), |
|
"source_map": verified_sidecars.as_ref().map(|paths| paths.1.to_string()), |
|
"artifact_format": result.artifact_format.display_name(), |
|
"target_profile": result.metadata.target_profile.name, |
|
"artifact_hash": result.metadata.artifact_hash, |
|
"artifact_size_bytes": result.artifact_bytes.len(), |
|
"cache_hit": result.cache_hit, |
|
})); |
|
|
|
human_lines.push(format!("{} {}", "Built".green(), member_dir)); |
|
} |
|
Err(e) => { |
|
failure_diagnostics.push(e.clone().with_file(member_dir.clone())); |
|
member_results.push(serde_json::json!({ |
|
"member": member_dir.as_str(), |
|
"status": "failed", |
|
"error": e.message, |
|
})); |
|
failed += 1; |
|
} |
|
} |
|
} |
Summary
CellScript has a workspace surface, but its build and lock semantics are not yet a coherent workspace dependency graph.
The current implementation:
The last point is especially misleading: the resulting file passes the structural lock schema, but it is neither a source-resolution lock nor an artifact bundle.
Code evidence
CellScript/src/package/mod.rs
Lines 79 to 85 in 8ae6dc4
CellScript/src/cli/commands.rs
Lines 1096 to 1203 in 8ae6dc4
CellScript/src/cli/commands.rs
Lines 1215 to 1239 in 8ae6dc4
The existing tests prove that two independent members compile and that a member with its own pre-created lock can import a path dependency. They do not prove a shared workspace resolve, topological scheduling, one authoritative lock, duplicate-name rejection, exclude handling, or downstream invalidation.
Why this matters
A workspace lock is a trust boundary. Tooling must not use the same Cell.lock schema for two different meanings:
A consumer cannot currently answer:
Reference architecture
Cargo threads one Workspace through resolution and compilation, shares target/build directories, resolves all selected roots, then constructs an immutable UnitGraph before scheduling work:
Move package-alt similarly models a rooted package DAG and separates graph loading from BuildPlan compilation:
CellScript should adopt the separation, not Cargo's exact target model.
Proposed contract
Introduce a versioned workspace resolve/build plan with:
Do not store artifact records as ordinary LockedDependency nodes. If the workspace needs artifact closure, define a separate versioned workspace evidence manifest or consume the ProtocolBundle design in #9.
Acceptance criteria
Non-goals