Public release release-98d969b26488
Source commit: 98d969b26488b4cda8b2381fa870276a00ca98ea Public tree identity: sha256:d02dd1e8d3547a489bb300741bed544bdc60bb8fe0bd541fd43ce9bfa7129a3e
This commit is contained in:
commit
8ded2b6a42
210 changed files with 76954 additions and 0 deletions
18
examples/launch-build-demo/Cargo.toml
Normal file
18
examples/launch-build-demo/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "launch-build-demo"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/build.rs"
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
clusterflux = { package = "clusterflux-sdk", path = "../../crates/clusterflux-sdk" }
|
||||
futures-executor.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
14
examples/launch-build-demo/README.md
Normal file
14
examples/launch-build-demo/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Launch Build Demo
|
||||
|
||||
This build workflow is expressed as Rust source code. It uses `env!("linux")`,
|
||||
recognizes `env!("windows")` when a Windows development node is attached,
|
||||
spawns debugger-visible virtual tasks, and returns portable artifact and source
|
||||
handles instead of moving large bytes through task arguments.
|
||||
|
||||
The Linux environment is defined by `envs/linux/Containerfile`. The Windows environment is a user-attached development contract and does not claim secure managed Windows sandboxing.
|
||||
|
||||
Inspect the bundle metadata:
|
||||
|
||||
```bash
|
||||
clusterflux bundle inspect --project examples/launch-build-demo
|
||||
```
|
||||
3
examples/launch-build-demo/envs/linux/Containerfile
Normal file
3
examples/launch-build-demo/envs/linux/Containerfile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
FROM docker.io/library/alpine:3.20
|
||||
RUN apk add --no-cache build-base tar zstd
|
||||
WORKDIR /workspace
|
||||
2
examples/launch-build-demo/envs/windows/Dockerfile
Normal file
2
examples/launch-build-demo/envs/windows/Dockerfile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# User-attached Windows development execution contract.
|
||||
# This is not a managed untrusted Windows sandbox.
|
||||
6
examples/launch-build-demo/fixture/hello-clusterflux.c
Normal file
6
examples/launch-build-demo/fixture/hello-clusterflux.c
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
puts("hello from a real Clusterflux build");
|
||||
return 0;
|
||||
}
|
||||
33
examples/launch-build-demo/src/bin/sdk-product-runtime.rs
Normal file
33
examples/launch-build-demo/src/bin/sdk-product-runtime.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use std::error::Error;
|
||||
|
||||
use futures_executor::block_on;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let handle = block_on(
|
||||
clusterflux::spawn::task_with_arg(41_i32, |_| -> i32 {
|
||||
panic!("product-mode spawn invoked its local Rust closure")
|
||||
})
|
||||
.task_id("task_add_one")
|
||||
.name("SDK product Wasmtime task")
|
||||
.env(clusterflux::env!("linux"))
|
||||
.start(),
|
||||
)?;
|
||||
let task_spec = handle
|
||||
.task_spec()
|
||||
.cloned()
|
||||
.ok_or("CLUSTERFLUX_SDK_COORDINATOR did not enable product runtime")?;
|
||||
let virtual_thread_id = handle.virtual_thread_id();
|
||||
let result = block_on(handle.join())?;
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"kind": "clusterflux-sdk-product-runtime",
|
||||
"virtual_thread_id": virtual_thread_id,
|
||||
"task_spec": task_spec,
|
||||
"remote_result": result,
|
||||
"local_function_invoked_by_spawn": false,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
403
examples/launch-build-demo/src/build.rs
Normal file
403
examples/launch-build-demo/src/build.rs
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
use clusterflux::{Artifact, EnvRef, SourceSnapshot};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, clusterflux::TaskArg)]
|
||||
pub struct BuildReport {
|
||||
pub linux_thread: u64,
|
||||
pub linux_parallel_thread: u64,
|
||||
pub package_thread: u64,
|
||||
pub linux_artifact: Artifact,
|
||||
pub package_artifact: Artifact,
|
||||
pub source: SourceSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, clusterflux::TaskArg)]
|
||||
pub struct PackageInput {
|
||||
pub release_name: String,
|
||||
pub source: SourceSnapshot,
|
||||
pub executable: Option<Artifact>,
|
||||
pub inputs: Vec<Artifact>,
|
||||
}
|
||||
|
||||
pub fn linux_env() -> EnvRef {
|
||||
clusterflux::env!("linux")
|
||||
}
|
||||
|
||||
pub fn windows_env() -> EnvRef {
|
||||
clusterflux::env!("windows")
|
||||
}
|
||||
|
||||
#[clusterflux::task(capabilities = "source_filesystem")]
|
||||
pub async fn prepare_source() -> SourceSnapshot {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return clusterflux::source::snapshot()
|
||||
.await
|
||||
.expect("the source task should snapshot the node checkout it was placed on");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SourceSnapshot {
|
||||
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[clusterflux::task(capabilities = "command")]
|
||||
pub async fn compile_linux(source: SourceSnapshot) -> Artifact {
|
||||
let _ = &source;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let executable = clusterflux::fs::output_path("hello-clusterflux")
|
||||
.expect("the executable output path should be task-local");
|
||||
let output = clusterflux::command::Command::new("cc")
|
||||
.args([
|
||||
"-Os",
|
||||
"-static",
|
||||
"-s",
|
||||
"fixture/hello-clusterflux.c",
|
||||
"-o",
|
||||
executable.as_str(),
|
||||
])
|
||||
.current_dir("/workspace")
|
||||
.env("SOURCE_DATE_EPOCH", "0")
|
||||
.timeout(std::time::Duration::from_secs(180))
|
||||
.network_disabled()
|
||||
.output()
|
||||
.await
|
||||
.expect("the declared Linux environment should provide a real C compiler");
|
||||
assert_eq!(output.status_code, Some(0));
|
||||
return clusterflux::fs::flush(&executable)
|
||||
.await
|
||||
.expect("the command-created executable should flush to node artifact storage");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Artifact {
|
||||
id: "hello-clusterflux-native-test".to_owned(),
|
||||
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
.to_owned(),
|
||||
size_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[clusterflux::task]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn task_add_one(input: i32) -> i32 {
|
||||
input + 1
|
||||
}
|
||||
|
||||
#[clusterflux::task]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn task_trap(_input: i32) -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
core::arch::wasm32::unreachable();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
panic!("intentional task trap")
|
||||
}
|
||||
|
||||
#[clusterflux::task(capabilities = "command")]
|
||||
pub async fn package_release(input: PackageInput) -> Artifact {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
assert_eq!(input.release_name, "release.tar");
|
||||
let executable = input
|
||||
.executable
|
||||
.as_ref()
|
||||
.expect("the package input should contain the compiler artifact");
|
||||
assert!(input.inputs.iter().any(|artifact| artifact == executable));
|
||||
let executable = clusterflux::fs::materialize(&executable, "package/hello-clusterflux")
|
||||
.await
|
||||
.expect("the retaining node should materialize the compiler artifact locally");
|
||||
let release = clusterflux::fs::output_path("release.tar")
|
||||
.expect("the release output path should be task-local");
|
||||
let output = clusterflux::command::Command::new("tar")
|
||||
.args([
|
||||
"--sort=name",
|
||||
"--mtime=@0",
|
||||
"--owner=0",
|
||||
"--group=0",
|
||||
"--numeric-owner",
|
||||
"--mode=0755",
|
||||
"-cf",
|
||||
release.as_str(),
|
||||
"-C",
|
||||
"/clusterflux/output/package",
|
||||
"hello-clusterflux",
|
||||
])
|
||||
.current_dir("/workspace")
|
||||
.env("SOURCE_DATE_EPOCH", "0")
|
||||
.timeout(std::time::Duration::from_secs(180))
|
||||
.network_disabled()
|
||||
.output()
|
||||
.await
|
||||
.expect("the declared Linux environment should package the materialized executable");
|
||||
assert_eq!(output.status_code, Some(0));
|
||||
assert_eq!(
|
||||
executable.as_str(),
|
||||
"/clusterflux/output/package/hello-clusterflux"
|
||||
);
|
||||
return clusterflux::fs::flush(&release)
|
||||
.await
|
||||
.expect("the command-created release archive should flush as the final artifact");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let _ = input;
|
||||
Artifact {
|
||||
id: "release-native-test".to_owned(),
|
||||
digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
.to_owned(),
|
||||
size_bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[clusterflux::task(capabilities = "command")]
|
||||
pub async fn abort_probe() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let output = clusterflux::command::Command::new("sh")
|
||||
.args(["-c", "sleep 30"])
|
||||
.output()
|
||||
.await
|
||||
.expect("abort probe command should either finish or be stopped by process abort");
|
||||
return output.status_code.unwrap_or(-1);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
0
|
||||
}
|
||||
|
||||
#[clusterflux::task(capabilities = "command")]
|
||||
pub async fn long_join_probe() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let output = clusterflux::command::Command::new("sh")
|
||||
.args(["-c", "sleep 125"])
|
||||
.timeout(std::time::Duration::from_secs(180))
|
||||
.network_disabled()
|
||||
.output()
|
||||
.await
|
||||
.expect("the controlled long-running command should complete normally");
|
||||
return output.status_code.unwrap_or(-1);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
0
|
||||
}
|
||||
|
||||
#[clusterflux::task]
|
||||
pub fn cooperative_cancellation_probe() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
loop {
|
||||
if clusterflux::process::cancellation_requested()
|
||||
.expect("task control should remain available while the Wasm task is running")
|
||||
{
|
||||
// Cancellation is a request observed and handled by task code. Returning
|
||||
// normally is intentionally distinct from the runtime's forced abort path.
|
||||
return 17;
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
17
|
||||
}
|
||||
|
||||
#[clusterflux::task]
|
||||
pub fn debug_child_probe() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
loop {
|
||||
if clusterflux::process::cancellation_requested()
|
||||
.expect("child debug participant should retain task control")
|
||||
{
|
||||
return 23;
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
23
|
||||
}
|
||||
|
||||
#[clusterflux::task]
|
||||
pub async fn debug_parent_probe() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let child = clusterflux::spawn::task(debug_child_probe)
|
||||
.name("debug child participant")
|
||||
.start()
|
||||
.await
|
||||
.expect("parent debug participant should spawn its child");
|
||||
return child
|
||||
.join()
|
||||
.await
|
||||
.expect("parent debug participant should join its child");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
23
|
||||
}
|
||||
|
||||
#[clusterflux::main]
|
||||
pub async fn build_main() -> BuildReport {
|
||||
run_build_workflow().await
|
||||
}
|
||||
|
||||
#[clusterflux::main(name = "fail")]
|
||||
pub async fn fail_main() -> Result<i32, String> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let child = clusterflux::spawn::task_with_arg(0_i32, |value| task_trap(value))
|
||||
.task_id("task_trap")
|
||||
.name("intentional failing child")
|
||||
.start()
|
||||
.await
|
||||
.map_err(|error| format!("failing entrypoint could not launch its child: {error}"))?;
|
||||
return child
|
||||
.join()
|
||||
.await
|
||||
.map_err(|error| format!("intentional child failure: {error}"));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Err("intentional child failure".to_owned())
|
||||
}
|
||||
|
||||
#[clusterflux::main(name = "restart")]
|
||||
pub async fn restart_main() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return clusterflux::spawn::task_with_arg(0_i32, |value| task_trap(value))
|
||||
.task_id("task_trap")
|
||||
.name("edited restart probe")
|
||||
.failure_policy(clusterflux::spawn::TaskFailurePolicy::AwaitOperator)
|
||||
.start()
|
||||
.await
|
||||
.expect("restart probe should launch its child")
|
||||
.join()
|
||||
.await
|
||||
.expect("restart probe child should complete");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
task_add_one(41)
|
||||
}
|
||||
|
||||
#[clusterflux::main(name = "park-wake")]
|
||||
pub async fn park_wake_main() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let mut value = 0_i32;
|
||||
for _ in 0..16 {
|
||||
let next = clusterflux::spawn::task_with_arg(value, |input| task_add_one(input))
|
||||
.task_id("task_add_one")
|
||||
.name("park/wake fuel refill probe")
|
||||
.start()
|
||||
.await
|
||||
.expect("park/wake probe should launch")
|
||||
.join()
|
||||
.await
|
||||
.expect("park/wake probe should complete");
|
||||
value = i32::try_from(next).expect("park/wake result should remain in i32 range");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
16
|
||||
}
|
||||
|
||||
#[clusterflux::main(name = "long-join")]
|
||||
pub async fn long_join_main() -> i32 {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
return clusterflux::spawn::async_task(long_join_probe)
|
||||
.name("controlled task longer than two minutes")
|
||||
.env(linux_env())
|
||||
.start()
|
||||
.await
|
||||
.expect("long join probe should launch")
|
||||
.join()
|
||||
.await
|
||||
.expect("long join probe should remain joinable beyond two minutes");
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
0
|
||||
}
|
||||
|
||||
pub async fn run_build_workflow() -> BuildReport {
|
||||
let source = clusterflux::spawn::async_task(prepare_source)
|
||||
.name("prepare source snapshot")
|
||||
.start()
|
||||
.await
|
||||
.unwrap()
|
||||
.join()
|
||||
.await
|
||||
.unwrap();
|
||||
let linux = clusterflux::spawn::async_task_with_arg(source.clone(), compile_linux)
|
||||
.name("compile linux")
|
||||
.env(linux_env())
|
||||
.start()
|
||||
.await
|
||||
.unwrap();
|
||||
let linux_parallel = clusterflux::spawn::async_task_with_arg(source.clone(), compile_linux)
|
||||
.name("compile linux in parallel")
|
||||
.env(linux_env())
|
||||
.start()
|
||||
.await
|
||||
.unwrap();
|
||||
let linux_thread = linux.virtual_thread_id();
|
||||
let linux_parallel_thread = linux_parallel.virtual_thread_id();
|
||||
let linux_artifact = linux.join().await.unwrap();
|
||||
let linux_parallel_artifact = linux_parallel.join().await.unwrap();
|
||||
assert_eq!(linux_artifact.digest, linux_parallel_artifact.digest);
|
||||
let package = clusterflux::spawn::async_task_with_arg(
|
||||
PackageInput {
|
||||
release_name: "release.tar".to_owned(),
|
||||
source: source.clone(),
|
||||
executable: Some(linux_artifact.clone()),
|
||||
inputs: vec![linux_artifact.clone()],
|
||||
},
|
||||
package_release,
|
||||
)
|
||||
.name("package artifacts")
|
||||
.env(linux_env())
|
||||
.start()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let package_thread = package.virtual_thread_id();
|
||||
let package_artifact = package.join().await.unwrap();
|
||||
|
||||
BuildReport {
|
||||
linux_thread,
|
||||
linux_parallel_thread,
|
||||
package_thread,
|
||||
linux_artifact,
|
||||
package_artifact,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures_executor::block_on;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flagship_workflow_is_rust_source_with_spawned_virtual_tasks() {
|
||||
let main_report = block_on(build_main());
|
||||
assert_eq!(
|
||||
main_report.linux_artifact.id,
|
||||
"hello-clusterflux-native-test"
|
||||
);
|
||||
assert_eq!(task_add_one(41), 42);
|
||||
assert_eq!(block_on(restart_main()), 42);
|
||||
assert_eq!(block_on(park_wake_main()), 16);
|
||||
assert_eq!(block_on(long_join_main()), 0);
|
||||
assert_eq!(linux_env().name, "linux");
|
||||
assert_eq!(windows_env().name, "windows");
|
||||
|
||||
let report = block_on(run_build_workflow());
|
||||
|
||||
assert_ne!(report.linux_thread, report.package_thread);
|
||||
assert_ne!(report.linux_thread, report.linux_parallel_thread);
|
||||
assert_eq!(report.linux_artifact.id, "hello-clusterflux-native-test");
|
||||
assert_eq!(report.package_artifact.id, "release-native-test");
|
||||
assert_eq!(
|
||||
report.source.digest,
|
||||
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue