Public release release-ff7f847f143d
Source commit: ff7f847f143dc67864bed68a86cf259114384bd5 Public tree identity: sha256:bb7dcd2ac78bdbad2f4eba0f49be649d446d67f96f4eb2796941c026412fc032
This commit is contained in:
commit
70319cde15
210 changed files with 78958 additions and 0 deletions
322
crates/clusterflux-cli/src/build.rs
Normal file
322
crates/clusterflux-cli/src/build.rs
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clusterflux_core::Digest;
|
||||
use serde_json::{json, Value};
|
||||
use wasmparser::{Parser, Payload};
|
||||
|
||||
use crate::errors::cli_error_summary_for_category;
|
||||
use crate::{bundle_inspection, BuildArgs, BundleInspectArgs};
|
||||
|
||||
pub(crate) fn build_report(args: BuildArgs, cwd: PathBuf) -> Result<Value> {
|
||||
let inspection = bundle_inspection(
|
||||
BundleInspectArgs {
|
||||
project: args.project.clone(),
|
||||
source_provider: args.source_provider.clone(),
|
||||
disabled_source_providers: args.disabled_source_providers.clone(),
|
||||
json: true,
|
||||
},
|
||||
cwd,
|
||||
)?;
|
||||
let diagnostics = inspection.pre_schedule_diagnostics.clone();
|
||||
let blocking_diagnostic = diagnostics
|
||||
.iter()
|
||||
.find(|diagnostic| diagnostic.severity == "error")
|
||||
.cloned();
|
||||
if let Some(diagnostic) = blocking_diagnostic {
|
||||
let machine_category = match diagnostic.category.as_str() {
|
||||
"environment" => "environment",
|
||||
"source_provider" | "capability" => "capability",
|
||||
_ => "unknown",
|
||||
};
|
||||
return Ok(json!({
|
||||
"command": "build",
|
||||
"status": "blocked_before_schedule",
|
||||
"bundle": inspection,
|
||||
"diagnostics": diagnostics,
|
||||
"contains_full_repository_upload": false,
|
||||
"content_addressed": false,
|
||||
"debug_metadata_available": false,
|
||||
"scheduled_work": false,
|
||||
"machine_error": cli_error_summary_for_category(machine_category, &diagnostic.message),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut wasm = compile_project_wasm(&inspection.project)?;
|
||||
let environment_manifest = serde_json::to_vec(&inspection.metadata.environments)?;
|
||||
append_custom_section(
|
||||
&mut wasm.bytes,
|
||||
"clusterflux.environments",
|
||||
&environment_manifest,
|
||||
);
|
||||
let bundle_digest = Digest::sha256(&wasm.bytes);
|
||||
let task_descriptors = descriptor_records(&wasm.bytes, "clusterflux.tasks")?;
|
||||
let entrypoint_descriptors = descriptor_records(&wasm.bytes, "clusterflux.entrypoints")?;
|
||||
if task_descriptors.is_empty() {
|
||||
bail!(
|
||||
"compiled Wasm module contains no #[clusterflux::task] descriptors; annotate at least one exported task"
|
||||
);
|
||||
}
|
||||
if entrypoint_descriptors.is_empty() {
|
||||
bail!(
|
||||
"compiled Wasm module contains no #[clusterflux::main] descriptors; annotate at least one entrypoint"
|
||||
);
|
||||
}
|
||||
let output = args.output.unwrap_or_else(|| {
|
||||
inspection.project.join(".clusterflux/build").join(
|
||||
bundle_digest
|
||||
.as_str()
|
||||
.trim_start_matches("sha256:")
|
||||
.get(..16)
|
||||
.unwrap_or("bundle"),
|
||||
)
|
||||
});
|
||||
let bundle_artifact = write_bundle(
|
||||
&output,
|
||||
&wasm,
|
||||
&bundle_digest,
|
||||
&inspection,
|
||||
&task_descriptors,
|
||||
&entrypoint_descriptors,
|
||||
)?;
|
||||
|
||||
Ok(json!({
|
||||
"command": "build",
|
||||
"status": "built",
|
||||
"bundle": inspection,
|
||||
"bundle_artifact": bundle_artifact,
|
||||
"diagnostics": diagnostics,
|
||||
"contains_full_repository_upload": false,
|
||||
"content_addressed": true,
|
||||
"debug_metadata_available": true,
|
||||
"scheduled_work": false,
|
||||
}))
|
||||
}
|
||||
|
||||
fn append_custom_section(module: &mut Vec<u8>, name: &str, data: &[u8]) {
|
||||
let mut section = Vec::new();
|
||||
encode_unsigned_leb(name.len() as u64, &mut section);
|
||||
section.extend_from_slice(name.as_bytes());
|
||||
section.extend_from_slice(data);
|
||||
module.push(0);
|
||||
encode_unsigned_leb(section.len() as u64, module);
|
||||
module.extend_from_slice(§ion);
|
||||
}
|
||||
|
||||
fn encode_unsigned_leb(mut value: u64, output: &mut Vec<u8>) {
|
||||
loop {
|
||||
let mut byte = (value & 0x7f) as u8;
|
||||
value >>= 7;
|
||||
if value != 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
output.push(byte);
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CompiledWasm {
|
||||
bytes: Vec<u8>,
|
||||
package: String,
|
||||
target: String,
|
||||
source_path: PathBuf,
|
||||
}
|
||||
|
||||
fn compile_project_wasm(project: &Path) -> Result<CompiledWasm> {
|
||||
let manifest = project.join("Cargo.toml");
|
||||
let metadata_output = Command::new("cargo")
|
||||
.args([
|
||||
"metadata",
|
||||
"--format-version",
|
||||
"1",
|
||||
"--no-deps",
|
||||
"--manifest-path",
|
||||
])
|
||||
.arg(&manifest)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run cargo metadata for {}", manifest.display()))?;
|
||||
if !metadata_output.status.success() {
|
||||
bail!(
|
||||
"cargo metadata failed for {}: {}",
|
||||
manifest.display(),
|
||||
String::from_utf8_lossy(&metadata_output.stderr).trim()
|
||||
);
|
||||
}
|
||||
let metadata: Value = serde_json::from_slice(&metadata_output.stdout)?;
|
||||
let canonical_manifest = std::fs::canonicalize(&manifest)?;
|
||||
let package = metadata["packages"]
|
||||
.as_array()
|
||||
.and_then(|packages| {
|
||||
packages.iter().find(|package| {
|
||||
package["manifest_path"]
|
||||
.as_str()
|
||||
.and_then(|path| std::fs::canonicalize(path).ok())
|
||||
.as_ref()
|
||||
== Some(&canonical_manifest)
|
||||
})
|
||||
})
|
||||
.context("cargo metadata did not return the requested project package")?;
|
||||
let package_name = package["name"]
|
||||
.as_str()
|
||||
.context("cargo package name missing")?;
|
||||
let target = package["targets"]
|
||||
.as_array()
|
||||
.and_then(|targets| {
|
||||
targets.iter().find(|target| {
|
||||
target["crate_types"]
|
||||
.as_array()
|
||||
.is_some_and(|types| types.iter().any(|kind| kind == "cdylib"))
|
||||
})
|
||||
})
|
||||
.context("Clusterflux project library must include crate-type = [\"cdylib\"]")?;
|
||||
let target_name = target["name"]
|
||||
.as_str()
|
||||
.context("cargo target name missing")?;
|
||||
let build = Command::new("cargo")
|
||||
// The MVP transports one Wasm bundle in a bounded control frame. These are
|
||||
// ordinary Cargo release-profile settings, applied only to the guest build,
|
||||
// that keep the product SDK/runtime inside that accepted boundary.
|
||||
.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z")
|
||||
.env("CARGO_PROFILE_RELEASE_LTO", "thin")
|
||||
.env("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1")
|
||||
.args([
|
||||
"build",
|
||||
"--quiet",
|
||||
"--release",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
"--lib",
|
||||
"--manifest-path",
|
||||
])
|
||||
.arg(&manifest)
|
||||
.output()
|
||||
.with_context(|| format!("failed to compile {} to Wasm", manifest.display()))?;
|
||||
if !build.status.success() {
|
||||
bail!(
|
||||
"Wasm bundle compilation failed for {}: {}",
|
||||
manifest.display(),
|
||||
String::from_utf8_lossy(&build.stderr).trim()
|
||||
);
|
||||
}
|
||||
let target_directory = metadata["target_directory"]
|
||||
.as_str()
|
||||
.context("cargo target_directory missing")?;
|
||||
let source_path = Path::new(target_directory)
|
||||
.join("wasm32-unknown-unknown/release")
|
||||
.join(format!("{}.wasm", target_name.replace('-', "_")));
|
||||
let bytes = std::fs::read(&source_path)
|
||||
.with_context(|| format!("compiled Wasm module missing at {}", source_path.display()))?;
|
||||
Ok(CompiledWasm {
|
||||
bytes,
|
||||
package: package_name.to_owned(),
|
||||
target: target_name.to_owned(),
|
||||
source_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn descriptor_records(module: &[u8], section_name: &str) -> Result<Vec<Value>> {
|
||||
let mut records: Vec<Value> = Vec::new();
|
||||
for payload in Parser::new(0).parse_all(module) {
|
||||
let Payload::CustomSection(section) = payload? else {
|
||||
continue;
|
||||
};
|
||||
if section.name() != section_name {
|
||||
continue;
|
||||
}
|
||||
for record in section
|
||||
.data()
|
||||
.split(|byte| *byte == b'\n' || *byte == 0)
|
||||
.filter(|record| !record.is_empty())
|
||||
{
|
||||
records.push(serde_json::from_slice(record).with_context(|| {
|
||||
format!("invalid descriptor record in Wasm custom section {section_name}")
|
||||
})?);
|
||||
}
|
||||
}
|
||||
records.sort_by(|left, right| left["name"].as_str().cmp(&right["name"].as_str()));
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
fn write_bundle(
|
||||
output: &Path,
|
||||
wasm: &CompiledWasm,
|
||||
bundle_digest: &Digest,
|
||||
inspection: &crate::bundle::BundleInspection,
|
||||
tasks: &[Value],
|
||||
entrypoints: &[Value],
|
||||
) -> Result<Value> {
|
||||
std::fs::create_dir_all(output)?;
|
||||
let module_path = output.join("module.wasm");
|
||||
let task_path = output.join("task-descriptors.json");
|
||||
let entrypoint_path = output.join("entrypoints.json");
|
||||
let environment_path = output.join("environments.json");
|
||||
let source_path = output.join("source-provider.json");
|
||||
let vfs_path = output.join("vfs-seed.json");
|
||||
let debug_path = output.join("debug-metadata.json");
|
||||
let manifest_path = output.join("manifest.json");
|
||||
std::fs::write(&module_path, &wasm.bytes)?;
|
||||
write_json(&task_path, &json!(tasks))?;
|
||||
write_json(&entrypoint_path, &json!(entrypoints))?;
|
||||
write_json(&environment_path, &json!(inspection.metadata.environments))?;
|
||||
write_json(&source_path, &json!(inspection.source_provider_manifest))?;
|
||||
write_json(
|
||||
&vfs_path,
|
||||
&json!({
|
||||
"epoch": 0,
|
||||
"mounts": ["/vfs/artifacts", "/vfs/sources", "/vfs/blobs"],
|
||||
"large_bytes_embedded": false,
|
||||
}),
|
||||
)?;
|
||||
write_json(&debug_path, &json!(inspection.metadata.debug_metadata))?;
|
||||
let manifest = json!({
|
||||
"kind": "clusterflux-bundle",
|
||||
"format_version": 1,
|
||||
"package": wasm.package,
|
||||
"target": wasm.target,
|
||||
"bundle_digest": bundle_digest,
|
||||
"module": "module.wasm",
|
||||
"module_size_bytes": wasm.bytes.len(),
|
||||
"task_descriptors": "task-descriptors.json",
|
||||
"entrypoints": "entrypoints.json",
|
||||
"environments": "environments.json",
|
||||
"source_provider": "source-provider.json",
|
||||
"vfs_seed": "vfs-seed.json",
|
||||
"debug_metadata": "debug-metadata.json",
|
||||
"required_capabilities": tasks.iter().flat_map(|task| {
|
||||
task["required_capabilities"].as_array().into_iter().flatten().cloned()
|
||||
}).collect::<Vec<_>>(),
|
||||
"metadata_identity": inspection.metadata.identity,
|
||||
"coordinator_receives_source_bytes_by_default": false,
|
||||
"embeds_full_repository": false,
|
||||
});
|
||||
write_json(&manifest_path, &manifest)?;
|
||||
Ok(json!({
|
||||
"directory": output,
|
||||
"manifest": manifest_path,
|
||||
"module": module_path,
|
||||
"compiled_module_source": wasm.source_path,
|
||||
"bundle_digest": bundle_digest,
|
||||
"module_size_bytes": wasm.bytes.len(),
|
||||
"task_descriptor_count": tasks.len(),
|
||||
"entrypoint_count": entrypoints.len(),
|
||||
"files": [
|
||||
"manifest.json",
|
||||
"module.wasm",
|
||||
"task-descriptors.json",
|
||||
"entrypoints.json",
|
||||
"environments.json",
|
||||
"source-provider.json",
|
||||
"vfs-seed.json",
|
||||
"debug-metadata.json",
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) -> Result<()> {
|
||||
let mut bytes = serde_json::to_vec_pretty(value)?;
|
||||
bytes.push(b'\n');
|
||||
std::fs::write(path, bytes).with_context(|| format!("failed to write {}", path.display()))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue