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.

301 changes: 190 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 @@ -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
126 changes: 125 additions & 1 deletion crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5697,6 +5697,10 @@ enum NativeArgKind {
/// similar — the callee expects the full NaN-boxed value, not an
/// unboxed raw pointer. Common pattern in fastify context methods.
JsvalI64,
/// Truncate f64 to i32 via fptosi.
I32,
/// Truncate f64 to i64 via fptosi.
I64,
}

/// What the runtime function returns.
Expand Down Expand Up @@ -5737,6 +5741,8 @@ const NA_F64: NativeArgKind = NativeArgKind::F64;
const NA_STR: NativeArgKind = NativeArgKind::StrPtr;
const NA_PTR: NativeArgKind = NativeArgKind::PtrI64;
const NA_JSV: NativeArgKind = NativeArgKind::JsvalI64;
const NA_I32: NativeArgKind = NativeArgKind::I32;
const NA_I64: NativeArgKind = NativeArgKind::I64;
const NR_PTR: NativeRetKind = NativeRetKind::Ptr;
const NR_STR: NativeRetKind = NativeRetKind::Str;
const NR_F64: NativeRetKind = NativeRetKind::F64;
Expand Down Expand Up @@ -6685,6 +6691,112 @@ const NATIVE_MODULE_TABLE: &[NativeModSig] = &[
class_filter: None, runtime: "js_worker_threads_parent_port", args: &[], ret: NR_F64 },
NativeModSig { module: "worker_threads", has_receiver: true, method: "postMessage",
class_filter: None, runtime: "js_worker_threads_post_message", args: &[NA_F64], ret: NR_F64 },

// ========== perry/container ==========
NativeModSig { module: "perry/container", has_receiver: false, method: "run",
class_filter: None, runtime: "js_container_run", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "create",
class_filter: None, runtime: "js_container_create", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "start",
class_filter: None, runtime: "js_container_start", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "stop",
class_filter: None, runtime: "js_container_stop", args: &[NA_STR, NA_F64], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "remove",
class_filter: None, runtime: "js_container_remove", args: &[NA_STR, NA_F64], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "list",
class_filter: None, runtime: "js_container_list", args: &[NA_F64], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "inspect",
class_filter: None, runtime: "js_container_inspect", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "inspectImage",
class_filter: None, runtime: "js_container_inspectImage", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "logs",
class_filter: None, runtime: "js_container_logs", args: &[NA_STR, NA_I32], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "exec",
class_filter: None, runtime: "js_container_exec", args: &[NA_STR, NA_STR, NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "pullImage",
class_filter: None, runtime: "js_container_pullImage", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "listImages",
class_filter: None, runtime: "js_container_listImages", args: &[], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "removeImage",
class_filter: None, runtime: "js_container_removeImage", args: &[NA_STR, NA_F64], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "getBackend",
class_filter: None, runtime: "js_container_getBackend", args: &[], ret: NR_STR },
NativeModSig { module: "perry/container", has_receiver: false, method: "detectBackend",
class_filter: None, runtime: "js_container_detectBackend", args: &[], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: false, method: "composeUp",
class_filter: None, runtime: "js_container_composeUp", args: &[NA_STR], ret: NR_PTR },

// ========== perry/container-compose & perry/compose ==========
NativeModSig { module: "perry/container-compose", has_receiver: false, method: "up",
class_filter: None, runtime: "js_container_composeUp", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: false, method: "up",
class_filter: None, runtime: "js_container_composeUp", args: &[NA_STR], ret: NR_PTR },
// ComposeHandle instance methods (perry/container)
NativeModSig { module: "perry/container", has_receiver: true, method: "down",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_down", args: &[NA_I32], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "ps",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_ps", args: &[], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "logs",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_logs", args: &[NA_STR, NA_I32], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "exec",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_exec", args: &[NA_STR, NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "config",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_config", args: &[], ret: NR_STR },
NativeModSig { module: "perry/container", has_receiver: true, method: "start",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_start", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "stop",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_stop", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "restart",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_restart", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/container", has_receiver: true, method: "graph",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_graph", args: &[], ret: NR_STR },
NativeModSig { module: "perry/container", has_receiver: true, method: "status",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_status", args: &[], ret: NR_PTR },

// ComposeHandle instance methods (perry/compose)
NativeModSig { module: "perry/compose", has_receiver: true, method: "down",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_down", args: &[NA_I32], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "ps",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_ps", args: &[], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "logs",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_logs", args: &[NA_STR, NA_I32], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "exec",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_exec", args: &[NA_STR, NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "config",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_config", args: &[], ret: NR_STR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "start",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_start", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "stop",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_stop", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "restart",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_restart", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "graph",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_graph", args: &[], ret: NR_STR },
NativeModSig { module: "perry/compose", has_receiver: true, method: "status",
class_filter: Some("ComposeHandle"), runtime: "js_container_compose_status", args: &[], ret: NR_PTR },

// ========== perry/workloads ==========
NativeModSig { module: "perry/workloads", has_receiver: false, method: "graph",
class_filter: None, runtime: "js_workload_graph", args: &[NA_STR, NA_STR], ret: NR_STR },
NativeModSig { module: "perry/workloads", has_receiver: false, method: "node",
class_filter: None, runtime: "js_workload_node", args: &[NA_STR, NA_STR], ret: NR_STR },
NativeModSig { module: "perry/workloads", has_receiver: false, method: "runGraph",
class_filter: None, runtime: "js_workload_runGraph", args: &[NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/workloads", has_receiver: false, method: "inspectGraph",
class_filter: None, runtime: "js_workload_inspectGraph", args: &[NA_STR], ret: NR_PTR },
// GraphHandle instance methods
NativeModSig { module: "perry/workloads", has_receiver: true, method: "down",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_down", args: &[NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/workloads", has_receiver: true, method: "status",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_status", args: &[], ret: NR_PTR },
NativeModSig { module: "perry/workloads", has_receiver: true, method: "graph",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_graph", args: &[], ret: NR_STR },
NativeModSig { module: "perry/workloads", has_receiver: true, method: "logs",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_logs", args: &[NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/workloads", has_receiver: true, method: "exec",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_exec", args: &[NA_STR, NA_STR], ret: NR_PTR },
NativeModSig { module: "perry/workloads", has_receiver: true, method: "ps",
class_filter: Some("GraphHandle"), runtime: "js_workload_handle_ps", args: &[], ret: NR_PTR },
];

/// Walk a statement to collect LocalIds declared inside a closure body —
Expand Down Expand Up @@ -6917,6 +7029,18 @@ fn lower_native_module_dispatch(
llvm_args.push((I64, bits));
arg_types.push(I64);
}
NativeArgKind::I32 => {
let blk = ctx.block();
let i = blk.fptosi(DOUBLE, &lowered, crate::types::I32);
llvm_args.push((crate::types::I32, i));
arg_types.push(crate::types::I32);
}
NativeArgKind::I64 => {
let blk = ctx.block();
let i = blk.fptosi(DOUBLE, &lowered, I64);
llvm_args.push((I64, i));
arg_types.push(I64);
}
}
}
// If fewer args than sig expects, pad with undefined / 0.
Expand All @@ -6926,7 +7050,7 @@ fn lower_native_module_dispatch(
llvm_args.push((DOUBLE, double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))));
arg_types.push(DOUBLE);
}
NativeArgKind::StrPtr | NativeArgKind::PtrI64 | NativeArgKind::JsvalI64 => {
NativeArgKind::StrPtr | NativeArgKind::PtrI64 | NativeArgKind::JsvalI64 | NativeArgKind::I32 | NativeArgKind::I64 => {
llvm_args.push((I64, "0".to_string()));
arg_types.push(I64);
}
Expand Down
46 changes: 46 additions & 0 deletions crates/perry-container-compose/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[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"
futures = "0.3"
md-5 = "0.10"
hex = "0.4"
dotenvy = { workspace = true }
indexmap = { version = "2.2", features = ["serde"] }
dashmap = "5"
rand = "0.8"
regex = "1"
atty = "0.2"
dialoguer = "0.11"
console = "0.15"
once_cell = "1"
which = "6.0"

[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