Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
547 changes: 547 additions & 0 deletions .github/workflows/container-tests.yml

Large diffs are not rendered by default.

300 changes: 189 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ members = [
"crates/perry-codegen-glance",
"crates/perry-codegen-wear-tiles",
"crates/perry-codegen-wasm",
"crates/perry-container-compose",
"crates/perry-ui-test",
"crates/perry-ui-testkit",
"crates/perry-doc-tests",
Expand All @@ -50,6 +51,7 @@ default-members = [
"crates/perry-codegen-glance",
"crates/perry-codegen-wear-tiles",
"crates/perry-codegen-wasm",
"crates/perry-container-compose",
]

# Aggressive release optimizations for small, fast binaries
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,43 @@ These packages are natively implemented in Rust — no Node.js required:
| **Database** | mysql2, pg, ioredis |
| **Security** | bcrypt, argon2, jsonwebtoken |
| **Utilities** | dotenv, uuid, nodemailer, zlib, node-cron |
| **Container** | perry/container (OCI container management) |

---

## Container Module

Perry includes a native container management module `perry/container` for creating, running, and managing OCI containers:

```typescript
import { run, list, composeUp } from 'perry/container';

// Run a container
const container = await run({
image: 'nginx:alpine',
name: 'my-nginx',
ports: ['8080:80'],
});

// List containers
const containers = await list();
console.log(containers);

// Multi-container orchestration
const compose = await composeUp({
services: {
web: { image: 'nginx:alpine' },
db: { image: 'postgres:15-alpine' },
},
});
```

**Platform support:**
- macOS/iOS: Podman (apple/container support coming soon)
- Linux: Podman (native)
- Windows: Podman Desktop (experimental)

See `example-code/container-demo/` for a complete example.

---

Expand Down
64 changes: 64 additions & 0 deletions block1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const PERRY_CONTAINER_TABLE: &[UiSig] = &[
UiSig { method: "run", runtime: "js_container_run",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "create", runtime: "js_container_create",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_container_start",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_container_stop",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "remove", runtime: "js_container_remove",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "list", runtime: "js_container_list",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "inspect", runtime: "js_container_inspect",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_container_logs",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_container_exec",
args: &[UiArgKind::Str, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "pullImage", runtime: "js_container_pullImage",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "listImages", runtime: "js_container_listImages",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "removeImage", runtime: "js_container_removeImage",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "getBackend", runtime: "js_container_getBackend",
args: &[], ret: UiReturnKind::Str },
UiSig { method: "detectBackend", runtime: "js_container_detectBackend",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "build", runtime: "js_container_build",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "composeUp", runtime: "js_container_composeUp",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_COMPOSE_TABLE: &[UiSig] = &[
UiSig { method: "up", runtime: "js_compose_up",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "down", runtime: "js_compose_down",
args: &[UiArgKind::F64, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "ps", runtime: "js_compose_ps",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_compose_logs",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_compose_exec",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "config", runtime: "js_compose_config",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_compose_start",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_compose_stop",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "restart", runtime: "js_compose_restart",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_WORKLOADS_TABLE: &[UiSig] = &[
UiSig { method: "graph", runtime: "js_workload_graph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Str },
UiSig { method: "runGraph", runtime: "js_workload_runGraph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
];
64 changes: 64 additions & 0 deletions block2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const PERRY_CONTAINER_TABLE: &[UiSig] = &[
UiSig { method: "run", runtime: "js_container_run",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "create", runtime: "js_container_create",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_container_start",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_container_stop",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "remove", runtime: "js_container_remove",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "list", runtime: "js_container_list",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "inspect", runtime: "js_container_inspect",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_container_logs",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_container_exec",
args: &[UiArgKind::Str, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "pullImage", runtime: "js_container_pullImage",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "listImages", runtime: "js_container_listImages",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "removeImage", runtime: "js_container_removeImage",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "getBackend", runtime: "js_container_getBackend",
args: &[], ret: UiReturnKind::Str },
UiSig { method: "detectBackend", runtime: "js_container_detectBackend",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "build", runtime: "js_container_build",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "composeUp", runtime: "js_container_composeUp",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_COMPOSE_TABLE: &[UiSig] = &[
UiSig { method: "up", runtime: "js_compose_up",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "down", runtime: "js_compose_down",
args: &[UiArgKind::F64, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "ps", runtime: "js_compose_ps",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_compose_logs",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_compose_exec",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "config", runtime: "js_compose_config",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_compose_start",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_compose_stop",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "restart", runtime: "js_compose_restart",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_WORKLOADS_TABLE: &[UiSig] = &[
UiSig { method: "graph", runtime: "js_workload_graph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Str },
UiSig { method: "runGraph", runtime: "js_workload_runGraph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
];
131 changes: 130 additions & 1 deletion crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2642,6 +2642,34 @@ pub(crate) fn lower_native_method_call(
}
}

// perry/workloads dispatch
if (module == "perry/workloads" || module == "perry/workload") && object.is_none() {
if let Some(sig) = perry_workloads_table_lookup(method) {
return lower_perry_ui_table_call(ctx, sig, args);
}
}

// perry/container dispatch
if (module == "perry/container" || module == "perry/container-compose") && object.is_none() {
if let Some(sig) = perry_container_table_lookup(method) {
return lower_perry_ui_table_call(ctx, sig, args);
}
}

// perry/compose dispatch
if module == "perry/compose" && object.is_none() {
if let Some(sig) = perry_compose_table_lookup(method) {
return lower_perry_ui_table_call(ctx, sig, args);
}
}

// perry/workloads dispatch
if (module == "perry/workloads" || module == "perry/workload") && object.is_none() {
if let Some(sig) = perry_workloads_table_lookup(method) {
return lower_perry_ui_table_call(ctx, sig, args);
}
}

// perry/plugin dispatch: loadPlugin, listPlugins, emitHook, etc.
if module == "perry/plugin" && object.is_none() {
if let Some(sig) = perry_plugin_table_lookup(method) {
Expand Down Expand Up @@ -4806,6 +4834,8 @@ enum UiReturnKind {
/// i64 result converted to plain JS number via `sitofp`. Used for integer
/// counts/IDs that the TS caller should see as a JS number (not a handle).
I64AsF64,
/// Returns a Promise handle (i64 pointer) -> NaN-box as POINTER.
Promise,
}

#[derive(Copy, Clone, Debug)]
Expand Down Expand Up @@ -4836,6 +4866,87 @@ struct UiSig {
/// returns the zero-sentinel). That's the behavior the entire perry/ui
/// surface had pre-v0.5.10 — adding a row here flips one method from
/// "silent no-op" to "real call into libperry_ui_macos.a".
const PERRY_CONTAINER_TABLE: &[UiSig] = &[
UiSig { method: "run", runtime: "js_container_run",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "create", runtime: "js_container_create",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_container_start",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_container_stop",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "remove", runtime: "js_container_remove",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "list", runtime: "js_container_list",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "inspect", runtime: "js_container_inspect",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_container_logs",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_container_exec",
args: &[UiArgKind::Str, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "pullImage", runtime: "js_container_pullImage",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "listImages", runtime: "js_container_listImages",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "removeImage", runtime: "js_container_removeImage",
args: &[UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "getBackend", runtime: "js_container_getBackend",
args: &[], ret: UiReturnKind::Str },
UiSig { method: "detectBackend", runtime: "js_container_detectBackend",
args: &[], ret: UiReturnKind::Promise },
UiSig { method: "build", runtime: "js_container_build",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "composeUp", runtime: "js_container_composeUp",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_COMPOSE_TABLE: &[UiSig] = &[
UiSig { method: "up", runtime: "js_compose_up",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "down", runtime: "js_compose_down",
args: &[UiArgKind::F64, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "ps", runtime: "js_compose_ps",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_compose_logs",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_compose_exec",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str],
ret: UiReturnKind::Promise },
UiSig { method: "config", runtime: "js_compose_config",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_compose_start",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_compose_stop",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "restart", runtime: "js_compose_restart",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
];

const PERRY_WORKLOADS_TABLE: &[UiSig] = &[
UiSig { method: "graph", runtime: "js_workload_graph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Str },
UiSig { method: "node", runtime: "js_workload_node",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Str },
UiSig { method: "runGraph", runtime: "js_workload_runGraph",
args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "inspectGraph", runtime: "js_workload_inspectGraph",
args: &[UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "down", runtime: "js_workload_handle_down",
args: &[UiArgKind::F64, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "status", runtime: "js_workload_handle_status",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
UiSig { method: "getGraph", runtime: "js_workload_handle_graph",
args: &[UiArgKind::F64], ret: UiReturnKind::Str },
UiSig { method: "logs", runtime: "js_workload_handle_logs",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_workload_handle_exec",
args: &[UiArgKind::F64, UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "ps", runtime: "js_workload_handle_ps",
args: &[UiArgKind::F64], ret: UiReturnKind::Promise },
];

const PERRY_UI_TABLE: &[UiSig] = &[
// ---- Constructors (return widget handle) ----
UiSig { method: "Divider", runtime: "perry_ui_divider_create",
Expand Down Expand Up @@ -5394,6 +5505,18 @@ fn perry_ui_instance_method_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_UI_INSTANCE_TABLE.iter().find(|s| s.method == method)
}

fn perry_container_table_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_CONTAINER_TABLE.iter().find(|s| s.method == method)
}

fn perry_compose_table_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_COMPOSE_TABLE.iter().find(|s| s.method == method)
}

fn perry_workloads_table_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_WORKLOADS_TABLE.iter().find(|s| s.method == method)
}

// =============================================================================
// perry/system dispatch table
// =============================================================================
Expand Down Expand Up @@ -5672,7 +5795,7 @@ fn lower_perry_ui_table_call(
// libperry_ui_*.a symbol. Same pending_declares mechanism the
// cross-module call site uses for `perry_fn_*`.
let return_type = match sig.ret {
UiReturnKind::Widget | UiReturnKind::I64AsF64 => I64,
UiReturnKind::Widget | UiReturnKind::I64AsF64 | UiReturnKind::Promise => I64,
UiReturnKind::F64 => DOUBLE,
UiReturnKind::Void => crate::types::VOID,
UiReturnKind::Str => I64,
Expand Down Expand Up @@ -5720,6 +5843,12 @@ fn lower_perry_ui_table_call(
let raw = blk.call(I64, sig.runtime, &arg_slices);
Ok(blk.sitofp(I64, &raw, DOUBLE))
}
UiReturnKind::Promise => {
let blk = ctx.block();
let raw = blk.call(I64, sig.runtime, &arg_slices);
// Promise handles are I64 (pointers), NaN-box them as POINTER
Ok(nanbox_pointer_inline(blk, &raw))
}
}
}

Expand Down
Loading