Compare commits

...

2 Commits

Author SHA1 Message Date
Nikhil Sonti
191397adc4 fix: address podman bundle review comments 2026-04-15 18:02:21 -07:00
Nikhil Sonti
a9740d87ce feat: bundle podman runtime for browseros server 2026-04-15 17:49:32 -07:00
9 changed files with 305 additions and 8 deletions

View File

@@ -8,10 +8,33 @@
* On Linux, machine operations are no-ops since Podman runs natively.
*/
import { existsSync } from 'node:fs'
import { join } from 'node:path'
const isLinux = process.platform === 'linux'
const PODMAN_BUNDLE_PATH = ['bin', 'third_party', 'podman'] as const
export type LogFn = (msg: string) => void
function getPodmanBinaryName(platform: NodeJS.Platform): string {
return platform === 'win32' ? 'podman.exe' : 'podman'
}
export function resolveBundledPodmanPath(
resourcesDir?: string,
platform: NodeJS.Platform = process.platform,
): string | null {
if (!resourcesDir) return null
const bundledPath = join(
resourcesDir,
...PODMAN_BUNDLE_PATH,
getPodmanBinaryName(platform),
)
return existsSync(bundledPath) ? bundledPath : null
}
export class PodmanRuntime {
private podmanPath: string
private machineReady = false
@@ -243,6 +266,19 @@ export class PodmanRuntime {
let runtime: PodmanRuntime | null = null
export function configurePodmanRuntime(config: {
resourcesDir?: string
podmanPath?: string
}): PodmanRuntime {
const podmanPath =
config.podmanPath ??
resolveBundledPodmanPath(config.resourcesDir) ??
'podman'
runtime = new PodmanRuntime({ podmanPath })
return runtime
}
export function getPodmanRuntime(): PodmanRuntime {
if (!runtime) runtime = new PodmanRuntime()
return runtime

View File

@@ -14,6 +14,7 @@ import path from 'node:path'
import { EXIT_CODES } from '@browseros/shared/constants/exit-codes'
import { createHttpServer } from './api/server'
import { getOpenClawService } from './api/services/openclaw/openclaw-service'
import { configurePodmanRuntime } from './api/services/openclaw/podman-runtime'
import { CdpBackend } from './browser/backends/cdp'
import { Browser } from './browser/browser'
import type { ServerConfig } from './config'
@@ -55,6 +56,9 @@ export class Application {
resourcesDir: path.resolve(this.config.resourcesDir),
})
configurePodmanRuntime({
resourcesDir: path.resolve(this.config.resourcesDir),
})
await this.initCoreServices()
if (!this.config.cdpPort) {

View File

@@ -0,0 +1,83 @@
/**
* @license
* Copyright 2025 BrowserOS
*/
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import {
configurePodmanRuntime,
getPodmanRuntime,
resolveBundledPodmanPath,
} from '../../../../src/api/services/openclaw/podman-runtime'
describe('podman runtime', () => {
let tempDir: string
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browseros-podman-test-'))
})
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true })
configurePodmanRuntime({ podmanPath: 'podman' })
})
it('returns the bundled podman path when the executable exists', () => {
const bundledPath = path.join(
tempDir,
'bin',
'third_party',
'podman',
'podman',
)
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
fs.writeFileSync(bundledPath, 'podman')
expect(resolveBundledPodmanPath(tempDir, 'darwin')).toBe(bundledPath)
})
it('uses the windows executable name for bundled podman', () => {
const bundledPath = path.join(
tempDir,
'bin',
'third_party',
'podman',
'podman.exe',
)
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
fs.writeFileSync(bundledPath, 'podman')
expect(resolveBundledPodmanPath(tempDir, 'win32')).toBe(bundledPath)
})
it('returns null when no bundled podman executable exists', () => {
expect(resolveBundledPodmanPath(tempDir, 'darwin')).toBeNull()
})
it('configures the runtime to prefer the bundled podman path', () => {
const bundledPath = path.join(
tempDir,
'bin',
'third_party',
'podman',
'podman',
)
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
fs.writeFileSync(bundledPath, 'podman')
const runtime = configurePodmanRuntime({ resourcesDir: tempDir })
expect(runtime.getPodmanPath()).toBe(bundledPath)
expect(getPodmanRuntime().getPodmanPath()).toBe(bundledPath)
})
it('falls back to PATH podman when no bundled executable is present', () => {
const runtime = configurePodmanRuntime({ resourcesDir: tempDir })
expect(runtime.getPodmanPath()).toBe('podman')
})
})

View File

@@ -54,6 +54,136 @@
"os": ["windows"],
"arch": ["x64"]
},
{
"name": "Podman CLI - macOS ARM64",
"source": {
"type": "r2",
"key": "third_party/podman/podman-darwin-arm64"
},
"destination": "resources/bin/third_party/podman/podman",
"os": ["macos"],
"arch": ["arm64"],
"executable": true
},
{
"name": "Podman gvproxy - macOS ARM64",
"source": {
"type": "r2",
"key": "third_party/podman/gvproxy-darwin-arm64"
},
"destination": "resources/bin/third_party/podman/gvproxy",
"os": ["macos"],
"arch": ["arm64"],
"executable": true
},
{
"name": "Podman vfkit - macOS ARM64",
"source": {
"type": "r2",
"key": "third_party/podman/vfkit-darwin-arm64"
},
"destination": "resources/bin/third_party/podman/vfkit",
"os": ["macos"],
"arch": ["arm64"],
"executable": true
},
{
"name": "Podman krunkit - macOS ARM64",
"source": {
"type": "r2",
"key": "third_party/podman/krunkit-darwin-arm64"
},
"destination": "resources/bin/third_party/podman/krunkit",
"os": ["macos"],
"arch": ["arm64"],
"executable": true
},
{
"name": "Podman mac helper - macOS ARM64",
"source": {
"type": "r2",
"key": "third_party/podman/podman-mac-helper-darwin-arm64"
},
"destination": "resources/bin/third_party/podman/podman-mac-helper",
"os": ["macos"],
"arch": ["arm64"],
"executable": true
},
{
"name": "Podman CLI - macOS x64",
"notes": "krunkit is intentionally omitted on macOS x64 because the official amd64 Podman installer ships an arm64-only krunkit helper",
"source": {
"type": "r2",
"key": "third_party/podman/podman-darwin-x64"
},
"destination": "resources/bin/third_party/podman/podman",
"os": ["macos"],
"arch": ["x64"],
"executable": true
},
{
"name": "Podman gvproxy - macOS x64",
"source": {
"type": "r2",
"key": "third_party/podman/gvproxy-darwin-x64"
},
"destination": "resources/bin/third_party/podman/gvproxy",
"os": ["macos"],
"arch": ["x64"],
"executable": true
},
{
"name": "Podman vfkit - macOS x64",
"source": {
"type": "r2",
"key": "third_party/podman/vfkit-darwin-x64"
},
"destination": "resources/bin/third_party/podman/vfkit",
"os": ["macos"],
"arch": ["x64"],
"executable": true
},
{
"name": "Podman mac helper - macOS x64",
"source": {
"type": "r2",
"key": "third_party/podman/podman-mac-helper-darwin-x64"
},
"destination": "resources/bin/third_party/podman/podman-mac-helper",
"os": ["macos"],
"arch": ["x64"],
"executable": true
},
{
"name": "Podman CLI - Windows x64",
"source": {
"type": "r2",
"key": "third_party/podman/podman-windows-x64.exe"
},
"destination": "resources/bin/third_party/podman/podman.exe",
"os": ["windows"],
"arch": ["x64"]
},
{
"name": "Podman gvproxy - Windows x64",
"source": {
"type": "r2",
"key": "third_party/podman/gvproxy-windows-x64.exe"
},
"destination": "resources/bin/third_party/podman/gvproxy.exe",
"os": ["windows"],
"arch": ["x64"]
},
{
"name": "Podman win-sshproxy - Windows x64",
"source": {
"type": "r2",
"key": "third_party/podman/win-sshproxy-windows-x64.exe"
},
"destination": "resources/bin/third_party/podman/win-sshproxy.exe",
"os": ["windows"],
"arch": ["x64"]
},
{
"name": "ripgrep - macOS ARM64",
"source": {

View File

@@ -29,16 +29,33 @@ BROWSEROS_SERVER_BINARIES: Dict[str, Dict[str, str]] = {
"options": "runtime",
"entitlements": "browseros-executable-entitlements.plist",
},
"codex": {
"identifier_suffix": "codex",
"options": "runtime",
"entitlements": "browseros-executable-entitlements.plist",
},
"bun": {
"identifier_suffix": "bun",
"options": "runtime",
"entitlements": "browseros-executable-entitlements.plist",
},
"podman": {
"identifier_suffix": "podman",
"options": "runtime",
},
"gvproxy": {
"identifier_suffix": "gvproxy",
"options": "runtime",
},
"vfkit": {
"identifier_suffix": "vfkit",
"options": "runtime",
"entitlements": "podman-vfkit-entitlements.plist",
},
"krunkit": {
"identifier_suffix": "krunkit",
"options": "runtime",
"entitlements": "podman-krunkit-entitlements.plist",
},
"podman-mac-helper": {
"identifier_suffix": "podman_mac_helper",
"options": "runtime",
},
}

View File

@@ -18,8 +18,11 @@ from ...common.utils import (
BROWSEROS_SERVER_BINARIES: List[str] = [
"browseros_server.exe",
"codex.exe",
"bun.exe",
"third_party/bun.exe",
"third_party/rg.exe",
"third_party/podman/podman.exe",
"third_party/podman/gvproxy.exe",
"third_party/podman/win-sshproxy.exe",
]
@@ -102,7 +105,7 @@ class WindowsSignModule(CommandModule):
def get_browseros_server_binary_paths(build_output_dir: Path) -> List[Path]:
"""Return absolute paths to BrowserOS Server binaries for signing."""
server_dir = build_output_dir / "BrowserOSServer" / "default" / "resources" / "bin"
return [server_dir / binary for binary in BROWSEROS_SERVER_BINARIES]
return [server_dir / Path(binary) for binary in BROWSEROS_SERVER_BINARIES]
def build_mini_installer(ctx: Context) -> bool:

View File

@@ -22,6 +22,8 @@ class ExtractArtifactZipTest(unittest.TestCase):
"resources/bin/browseros_server": b"server-binary",
"resources/bin/third_party/bun": b"bun-binary",
"resources/bin/third_party/rg": b"rg-binary",
"resources/bin/third_party/podman/podman": b"podman-binary",
"resources/bin/third_party/podman/gvproxy": b"gvproxy-binary",
}
with tempfile.TemporaryDirectory() as temp_dir:

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.hypervisor</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>