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
299 changes: 188 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,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
98 changes: 94 additions & 4 deletions crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R
if let Some(sig) = perry_system_table_lookup(name) {
return lower_perry_ui_table_call(ctx, sig, args);
}
if let Some(sig) = perry_container_table_lookup(name) {
return lower_perry_ui_table_call(ctx, sig, args);
}
if let Some(sig) = perry_compose_table_lookup(name) {
return lower_perry_ui_table_call(ctx, sig, args);
}
if let Some(sig) = perry_workloads_table_lookup(name) {
return lower_perry_ui_table_call(ctx, sig, args);
}
// Built-in runtime extern functions (`js_weakmap_set`,
// `js_regexp_exec`, etc.) that start with `js_` are resolved
// directly against the runtime library — bypass the import-
Expand Down Expand Up @@ -2645,7 +2654,7 @@ pub(crate) fn lower_native_method_call(
}
}
let return_type = match sig.ret {
UiReturnKind::Widget => I64,
UiReturnKind::Widget | UiReturnKind::Promise | UiReturnKind::Str => I64,
UiReturnKind::F64 => DOUBLE,
UiReturnKind::Void => crate::types::VOID,
};
Expand All @@ -2658,10 +2667,14 @@ pub(crate) fn lower_native_method_call(
blk.call_void(sig.runtime, &ref_args);
Ok(double_literal(0.0))
}
UiReturnKind::Widget => {
UiReturnKind::Widget | UiReturnKind::Promise => {
let raw = blk.call(I64, sig.runtime, &ref_args);
Ok(crate::expr::nanbox_pointer_inline(blk, &raw))
}
UiReturnKind::Str => {
let raw = blk.call(I64, sig.runtime, &ref_args);
Ok(crate::expr::nanbox_string_inline(blk, &raw))
}
UiReturnKind::F64 => {
Ok(blk.call(DOUBLE, sig.runtime, &ref_args))
}
Expand Down Expand Up @@ -3489,6 +3502,10 @@ enum UiArgKind {
enum UiReturnKind {
/// Widget handle: NaN-box the i64 result with POINTER_TAG.
Widget,
/// Promise handle: NaN-box the i64 result with POINTER_TAG.
Promise,
/// String handle: NaN-box the i64 result with STRING_TAG.
Str,
/// Raw f64: pass through unchanged. Used by `scrollviewGetOffset` etc.
F64,
/// Void return: emit `call void` and return the `0.0` sentinel f64.
Expand Down Expand Up @@ -3523,6 +3540,7 @@ 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_UI_TABLE: &[UiSig] = &[
// ---- Constructors (return widget handle) ----
UiSig { method: "Divider", runtime: "perry_ui_divider_create",
Expand Down Expand Up @@ -4012,6 +4030,52 @@ fn perry_system_table_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_SYSTEM_TABLE.iter().find(|s| s.method == method)
}

// =============================================================================
// perry/container dispatch table
// =============================================================================

static 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::Str], ret: UiReturnKind::Promise },
UiSig { method: "remove", runtime: "js_container_remove", args: &[UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "list", runtime: "js_container_list", args: &[UiArgKind::Str], 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::Str], 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::I64Raw], 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 },
];

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

// =============================================================================
// perry/compose dispatch table
// =============================================================================

static 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::I64Raw, UiArgKind::I64Raw], ret: UiReturnKind::Promise },
UiSig { method: "ps", runtime: "js_compose_ps", args: &[UiArgKind::I64Raw], ret: UiReturnKind::Promise },
UiSig { method: "logs", runtime: "js_compose_logs", args: &[UiArgKind::I64Raw, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "exec", runtime: "js_compose_exec", args: &[UiArgKind::I64Raw, UiArgKind::Str, UiArgKind::Str, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "config", runtime: "js_compose_config", args: &[UiArgKind::I64Raw], ret: UiReturnKind::Promise },
UiSig { method: "start", runtime: "js_compose_start", args: &[UiArgKind::I64Raw, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "stop", runtime: "js_compose_stop", args: &[UiArgKind::I64Raw, UiArgKind::Str], ret: UiReturnKind::Promise },
UiSig { method: "restart", runtime: "js_compose_restart", args: &[UiArgKind::I64Raw, UiArgKind::Str], ret: UiReturnKind::Promise },
];

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

/// Lower a perry/ui call described by `sig`. Walks each arg, applies
/// the per-kind coercion to produce an LLVM SSA value of the right type,
/// lazy-declares the runtime function, emits the call, and boxes the
Expand Down Expand Up @@ -4089,7 +4153,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 => I64,
UiReturnKind::Widget | UiReturnKind::Promise | UiReturnKind::Str => I64,
UiReturnKind::F64 => DOUBLE,
UiReturnKind::Void => crate::types::VOID,
};
Expand All @@ -4104,11 +4168,16 @@ fn lower_perry_ui_table_call(
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
llvm_args.iter().map(|(t, s)| (*t, s.as_str())).collect();
match sig.ret {
UiReturnKind::Widget => {
UiReturnKind::Widget | UiReturnKind::Promise => {
let blk = ctx.block();
let handle = blk.call(I64, sig.runtime, &arg_slices);
Ok(nanbox_pointer_inline(blk, &handle))
}
UiReturnKind::Str => {
let blk = ctx.block();
let handle = blk.call(I64, sig.runtime, &arg_slices);
Ok(nanbox_string_inline(blk, &handle))
}
UiReturnKind::F64 => {
Ok(ctx.block().call(DOUBLE, sig.runtime, &arg_slices))
}
Expand Down Expand Up @@ -4828,3 +4897,24 @@ fn lower_native_module_dispatch(
}
}
}

// =============================================================================
// perry/workloads dispatch table
// =============================================================================

static 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: "graph", 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 },
];

fn perry_workloads_table_lookup(method: &str) -> Option<&'static UiSig> {
PERRY_WORKLOADS_TABLE.iter().find(|s| s.method == method)
}
45 changes: 45 additions & 0 deletions crates/perry-container-compose/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "perry-container-compose"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors = ["Perry Contributors"]
description = "Port of container-compose/cli to Rust - Docker Compose-like experience for Apple Container / Podman"

[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
tokio = { workspace = true }
clap = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
async-trait = "0.1"
md-5 = "0.10"
hex = "0.4"
dotenvy = { workspace = true }
indexmap = { version = "2.2", features = ["serde"] }
rand = "0.8"
regex = "1"
atty = "0.2"
dialoguer = "0.11"
console = "0.15"
once_cell = "1"
dashmap = "5"
which = "6"

[dev-dependencies]
tokio = { workspace = true }
proptest = "1"

[features]
default = []
ffi = [] # Enable FFI exports for Perry TypeScript integration
integration-tests = [] # Tests that require a running container backend

[[bin]]
name = "perry-compose"
path = "src/main.rs"
23 changes: 23 additions & 0 deletions crates/perry-container-compose/examples/build/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { composeUp, composeDown } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
app: {
build: {
context: '.',
dockerfile: 'Dockerfile',
args: {
BUILD_ENV: 'production',
},
},
ports: ['8080:8080'],
environment: {
NODE_ENV: 'production',
},
},
},
});

// Tear down when done
await composeDown(stack);
36 changes: 36 additions & 0 deletions crates/perry-container-compose/examples/multi-service/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { composeUp, composeDown, composeLogs } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
db: {
image: 'postgres:16-alpine',
environment: {
// ${VAR:-default} interpolation is supported in string values
POSTGRES_USER: '${DB_USER:-myuser}',
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}',
POSTGRES_DB: 'mydb',
},
volumes: ['db-data:/var/lib/postgresql/data'],
ports: ['5432:5432'],
},
web: {
image: 'myapp:latest',
dependsOn: ['db'],
ports: ['3000:3000'],
environment: {
DATABASE_URL: 'postgres://${DB_USER:-myuser}:${DB_PASSWORD:-secret}@db:5432/mydb',
},
},
},
volumes: {
'db-data': {},
},
});

// Stream logs from both services
const logs = await composeLogs(stack, { services: ['web', 'db'], follow: false });
console.log(logs);

// Tear down, removing named volumes
await composeDown(stack, { volumes: true });
21 changes: 21 additions & 0 deletions crates/perry-container-compose/examples/simple/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { composeUp, composeDown, composePs } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
web: {
image: 'nginx:alpine',
containerName: 'simple-nginx',
ports: ['8080:80'],
labels: {
app: 'simple-nginx',
},
},
},
});

const statuses = await composePs(stack);
console.table(statuses);

// Tear down when done
await composeDown(stack);
Loading