summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-06 20:41:53 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commitc32db56912b06a89490fda5d554468dbc12a39a2 (patch)
tree8084bd9c1b56b13354cddf99b39356967f0142ea
parent4baa3503321fb2ad9edfebc5e89bcd37115e626e (diff)
downloadmeta-virtualization-c32db56912b06a89490fda5d554468dbc12a39a2.tar.gz
vcontainer: add shared infrastructure and runner
Add core vcontainer infrastructure shared by vdkr and vpdmn: Scripts: - vrunner.sh: QEMU runner supporting both Docker and Podman runtimes - vcontainer-common.sh: Shared CLI functions and command handling - vcontainer-init-common.sh: Shared init script functions for QEMU guest - vdkr-preinit.sh: Initramfs preinit for switch_root to squashfs overlay Recipes: - vcontainer-native: Installs vrunner.sh and shared scripts - vcontainer-initramfs-create.inc: Shared initramfs build logic Features: - Docker/Podman-compatible commands: images, pull, load, save, run, exec - Memory resident mode for fast command execution - KVM acceleration when host matches target - Interactive mode with volume mounts - Squashfs rootfs with tmpfs overlay Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-common.sh2004
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-init-common.sh537
-rw-r--r--recipes-containers/vcontainer/files/vdkr-preinit.sh133
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh1353
-rw-r--r--recipes-containers/vcontainer/vcontainer-initramfs-create.inc237
-rw-r--r--recipes-containers/vcontainer/vcontainer-native.bb43
6 files changed, 4307 insertions, 0 deletions
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh
new file mode 100755
index 00000000..cd76ec6c
--- /dev/null
+++ b/recipes-containers/vcontainer/files/vcontainer-common.sh
@@ -0,0 +1,2004 @@
1#!/bin/bash
2# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# vcontainer-common.sh: Shared code for vdkr and vpdmn CLI wrappers
7#
8# This file is sourced by vdkr.sh and vpdmn.sh after they set:
9# VCONTAINER_RUNTIME_NAME - Tool name (vdkr or vpdmn)
10# VCONTAINER_RUNTIME_CMD - Container command (docker or podman)
11# VCONTAINER_RUNTIME_PREFIX - Env var prefix (VDKR or VPDMN)
12# VCONTAINER_IMPORT_TARGET - skopeo target (docker-daemon: or containers-storage:)
13# VCONTAINER_STATE_FILE - State image name (docker-state.img or podman-state.img)
14# VCONTAINER_OTHER_PREFIX - Other tool's prefix for orphan checking (VPDMN or VDKR)
15# VCONTAINER_VERSION - Tool version
16
17set -e
18
19SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
21# Validate required variables are set
22: "${VCONTAINER_RUNTIME_NAME:?VCONTAINER_RUNTIME_NAME must be set}"
23: "${VCONTAINER_RUNTIME_CMD:?VCONTAINER_RUNTIME_CMD must be set}"
24: "${VCONTAINER_RUNTIME_PREFIX:?VCONTAINER_RUNTIME_PREFIX must be set}"
25: "${VCONTAINER_IMPORT_TARGET:?VCONTAINER_IMPORT_TARGET must be set}"
26: "${VCONTAINER_STATE_FILE:?VCONTAINER_STATE_FILE must be set}"
27: "${VCONTAINER_OTHER_PREFIX:?VCONTAINER_OTHER_PREFIX must be set}"
28: "${VCONTAINER_VERSION:?VCONTAINER_VERSION must be set}"
29
30# ============================================================================
31# Configuration Management
32# ============================================================================
33# Config directory can be set via:
34# 1. --config-dir command line option
35# 2. ${VCONTAINER_RUNTIME_PREFIX}_CONFIG_DIR environment variable
36# 3. Default: ~/.config/${VCONTAINER_RUNTIME_NAME}
37#
38# Config file format: key=value (one per line)
39# Supported keys: arch, timeout, state-dir, verbose
40# ============================================================================
41
42# Pre-parse --config-dir from command line (needs to happen before detect_default_arch)
43_preparse_config_dir() {
44 local i=1
45 while [ $i -le $# ]; do
46 local arg="${!i}"
47 case "$arg" in
48 --config-dir)
49 i=$((i + 1))
50 echo "${!i}"
51 return
52 ;;
53 --config-dir=*)
54 echo "${arg#--config-dir=}"
55 return
56 ;;
57 esac
58 i=$((i + 1))
59 done
60 echo ""
61}
62
63_PREPARSE_CONFIG_DIR=$(_preparse_config_dir "$@")
64
65# Get environment variable value dynamically
66_get_env_var() {
67 local var_name="${VCONTAINER_RUNTIME_PREFIX}_$1"
68 echo "${!var_name}"
69}
70
71CONFIG_DIR="${_PREPARSE_CONFIG_DIR:-$(_get_env_var CONFIG_DIR)}"
72[ -z "$CONFIG_DIR" ] && CONFIG_DIR="$HOME/.config/$VCONTAINER_RUNTIME_NAME"
73CONFIG_FILE="$CONFIG_DIR/config"
74
75# Read a config value
76# Usage: config_get <key> [default]
77config_get() {
78 local key="$1"
79 local default="$2"
80
81 if [ -f "$CONFIG_FILE" ]; then
82 local value=$(grep "^${key}=" "$CONFIG_FILE" 2>/dev/null | cut -d= -f2- | tr -d '[:space:]')
83 if [ -n "$value" ]; then
84 echo "$value"
85 return
86 fi
87 fi
88 echo "$default"
89}
90
91# Write a config value
92# Usage: config_set <key> <value>
93config_set() {
94 local key="$1"
95 local value="$2"
96
97 mkdir -p "$CONFIG_DIR"
98
99 if [ -f "$CONFIG_FILE" ]; then
100 # Remove existing key
101 grep -v "^${key}=" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" 2>/dev/null || true
102 mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
103 fi
104
105 # Add new value
106 echo "${key}=${value}" >> "$CONFIG_FILE"
107}
108
109# Remove a config value
110# Usage: config_unset <key>
111config_unset() {
112 local key="$1"
113
114 if [ -f "$CONFIG_FILE" ]; then
115 grep -v "^${key}=" "$CONFIG_FILE" > "$CONFIG_FILE.tmp" 2>/dev/null || true
116 mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
117 fi
118}
119
120# List all config values
121config_list() {
122 if [ -f "$CONFIG_FILE" ]; then
123 cat "$CONFIG_FILE"
124 fi
125}
126
127# Get config default value
128config_default() {
129 local key="$1"
130 case "$key" in
131 arch) uname -m ;;
132 timeout) echo "300" ;;
133 state-dir) echo "$HOME/.$VCONTAINER_RUNTIME_NAME" ;;
134 verbose) echo "false" ;;
135 *) echo "" ;;
136 esac
137}
138
139# ============================================================================
140# Architecture Detection
141# ============================================================================
142# Priority order:
143# 1. --arch / -a command line flag (parsed below)
144# 2. Executable name: ${name}-aarch64 -> aarch64, ${name}-x86_64 -> x86_64
145# 3. ${PREFIX}_ARCH environment variable
146# 4. Config file: $CONFIG_DIR/config (arch key)
147# 5. Legacy config file: $CONFIG_DIR/arch (for backwards compatibility)
148# 6. Host architecture (uname -m)
149# ============================================================================
150
151detect_arch_from_name() {
152 local prog_name=$(basename "$0")
153 case "$prog_name" in
154 ${VCONTAINER_RUNTIME_NAME}-aarch64) echo "aarch64" ;;
155 ${VCONTAINER_RUNTIME_NAME}-x86_64) echo "x86_64" ;;
156 *) echo "" ;;
157 esac
158}
159
160detect_default_arch() {
161 # Check executable name first
162 local name_arch=$(detect_arch_from_name)
163 if [ -n "$name_arch" ]; then
164 echo "$name_arch"
165 return
166 fi
167
168 # Check environment variable
169 local env_arch=$(_get_env_var ARCH)
170 if [ -n "$env_arch" ]; then
171 echo "$env_arch"
172 return
173 fi
174
175 # Check new config file (arch key)
176 local config_arch=$(config_get "arch" "")
177 if [ -n "$config_arch" ]; then
178 echo "$config_arch"
179 return
180 fi
181
182 # Check legacy config file for backwards compatibility
183 local legacy_file="$CONFIG_DIR/arch"
184 if [ -f "$legacy_file" ]; then
185 local legacy_arch=$(cat "$legacy_file" | tr -d '[:space:]')
186 if [ -n "$legacy_arch" ]; then
187 echo "$legacy_arch"
188 return
189 fi
190 fi
191
192 # Fall back to host architecture
193 uname -m
194}
195
196DEFAULT_ARCH=$(detect_default_arch)
197BLOB_DIR="$(_get_env_var BLOB_DIR)"
198VERBOSE="$(_get_env_var VERBOSE)"
199[ -z "$VERBOSE" ] && VERBOSE="false"
200STATELESS="$(_get_env_var STATELESS)"
201[ -z "$STATELESS" ] && STATELESS="false"
202
203# Default state directory (per-architecture)
204DEFAULT_STATE_DIR="$(_get_env_var STATE_DIR)"
205[ -z "$DEFAULT_STATE_DIR" ] && DEFAULT_STATE_DIR="$HOME/.$VCONTAINER_RUNTIME_NAME"
206
207# Other tool's state directory (for orphan checking)
208OTHER_STATE_DIR="$HOME/.$(echo $VCONTAINER_OTHER_PREFIX | tr 'A-Z' 'a-z')"
209
210# Runner script
211RUNNER="$(_get_env_var RUNNER)"
212[ -z "$RUNNER" ] && RUNNER="$SCRIPT_DIR/vrunner.sh"
213
214# Colors (use $'...' for proper escape interpretation)
215RED=$'\033[0;31m'
216GREEN=$'\033[0;32m'
217YELLOW=$'\033[0;33m'
218BLUE=$'\033[0;34m'
219CYAN=$'\033[0;36m'
220BOLD=$'\033[1m'
221NC=$'\033[0m'
222
223# Check OCI image architecture and warn/error if mismatched
224# Usage: check_oci_arch <oci_dir> <target_arch>
225# Returns: 0 if match or non-OCI, 1 if mismatch
226check_oci_arch() {
227 local oci_dir="$1"
228 local target_arch="$2"
229
230 # Only check OCI directories
231 if [ ! -f "$oci_dir/index.json" ]; then
232 return 0
233 fi
234
235 # Try to extract architecture from the OCI image
236 # OCI structure: index.json -> manifest -> config blob -> architecture
237 local image_arch=""
238
239 # First, get the manifest digest from index.json
240 local manifest_digest=$(cat "$oci_dir/index.json" 2>/dev/null | \
241 grep -o '"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' | head -1 | \
242 sed 's/.*sha256:\([a-f0-9]*\)".*/\1/')
243
244 if [ -n "$manifest_digest" ]; then
245 local manifest_file="$oci_dir/blobs/sha256/$manifest_digest"
246 if [ -f "$manifest_file" ]; then
247 # Get the config digest from manifest
248 local config_digest=$(cat "$manifest_file" 2>/dev/null | \
249 grep -o '"config"[[:space:]]*:[[:space:]]*{[^}]*"digest"[[:space:]]*:[[:space:]]*"sha256:[a-f0-9]*"' | \
250 sed 's/.*sha256:\([a-f0-9]*\)".*/\1/')
251
252 if [ -n "$config_digest" ]; then
253 local config_file="$oci_dir/blobs/sha256/$config_digest"
254 if [ -f "$config_file" ]; then
255 # Extract architecture from config
256 image_arch=$(cat "$config_file" 2>/dev/null | \
257 grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | \
258 sed 's/.*"\([^"]*\)"$/\1/')
259 fi
260 fi
261 fi
262 fi
263
264 if [ -z "$image_arch" ]; then
265 # Couldn't determine architecture, allow import with warning
266 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Warning: Could not determine image architecture" >&2
267 return 0
268 fi
269
270 # Normalize architecture names
271 local normalized_image_arch="$image_arch"
272 local normalized_target_arch="$target_arch"
273
274 case "$image_arch" in
275 arm64) normalized_image_arch="aarch64" ;;
276 amd64) normalized_image_arch="x86_64" ;;
277 esac
278
279 case "$target_arch" in
280 arm64) normalized_target_arch="aarch64" ;;
281 amd64) normalized_target_arch="x86_64" ;;
282 esac
283
284 if [ "$normalized_image_arch" != "$normalized_target_arch" ]; then
285 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture mismatch!" >&2
286 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Image architecture: ${BOLD}$image_arch${NC}" >&2
287 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Target architecture: ${BOLD}$target_arch${NC}" >&2
288 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Use --arch $image_arch to import to a matching environment" >&2
289 return 1
290 fi
291
292 [ "$VERBOSE" = "true" ] && echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Image architecture: $image_arch (matches target)" >&2
293 return 0
294}
295
296show_usage() {
297 local PROG_NAME=$(basename "$0")
298 local RUNTIME_UPPER=$(echo "$VCONTAINER_RUNTIME_CMD" | sed 's/./\U&/')
299 cat << EOF
300${BOLD}${PROG_NAME}${NC} v$VCONTAINER_VERSION - ${RUNTIME_UPPER} CLI for cross-architecture emulation
301
302${BOLD}USAGE:${NC}
303 ${PROG_NAME} [OPTIONS] <command> [args...]
304
305${BOLD}${RUNTIME_UPPER}-COMPATIBLE COMMANDS:${NC}
306 ${BOLD}Images:${NC}
307 ${CYAN}images${NC} List images in emulated ${RUNTIME_UPPER}
308 ${CYAN}pull${NC} <image> Pull image from registry
309 ${CYAN}load${NC} -i <file> Load ${RUNTIME_UPPER} image archive (${VCONTAINER_RUNTIME_CMD} save output)
310 ${CYAN}import${NC} <tarball> [name:tag] Import rootfs tarball as image
311 ${CYAN}save${NC} -o <file> <image> Save image to tar archive
312 ${CYAN}tag${NC} <source> <target> Tag an image
313 ${CYAN}rmi${NC} <image> Remove an image
314 ${CYAN}history${NC} <image> Show image layer history
315 ${CYAN}inspect${NC} <image|container> Display detailed info
316
317 ${BOLD}Containers:${NC}
318 ${CYAN}run${NC} [opts] <image> [cmd] Run a command in a new container
319 ${CYAN}ps${NC} [options] List containers
320 ${CYAN}rm${NC} <container> Remove a container
321 ${CYAN}logs${NC} <container> View container logs
322 ${CYAN}start${NC} <container> Start a stopped container
323 ${CYAN}stop${NC} <container> Stop a running container
324 ${CYAN}restart${NC} <container> Restart a container
325 ${CYAN}kill${NC} <container> Kill a running container
326 ${CYAN}pause${NC} <container> Pause a running container
327 ${CYAN}unpause${NC} <container> Unpause a container
328 ${CYAN}commit${NC} <container> <image> Create image from container
329 ${CYAN}exec${NC} [opts] <container> <cmd> Execute command in container
330 ${CYAN}cp${NC} <src> <dest> Copy files to/from container
331
332 ${BOLD}Registry:${NC}
333 ${CYAN}login${NC} [options] Log in to a registry
334 ${CYAN}logout${NC} [registry] Log out from a registry
335 ${CYAN}push${NC} <image> Push image to registry
336 ${CYAN}search${NC} <term> Search registries for images
337
338 ${BOLD}System:${NC}
339 ${CYAN}info${NC} Display system info
340 ${CYAN}version${NC} Show ${RUNTIME_UPPER} version
341 ${CYAN}system df${NC} Show disk usage of images/containers/volumes
342 ${CYAN}system prune${NC} Remove unused data
343 ${CYAN}system prune -a${NC} Remove all unused images
344
345${BOLD}EXTENDED COMMANDS (${VCONTAINER_RUNTIME_NAME}-specific):${NC}
346 ${CYAN}vimport${NC} <path> [name:tag] Import from OCI dir, tarball, or directory (auto-detect)
347 ${CYAN}vrun${NC} [opts] <image> [cmd] Run command, clearing entrypoint (see RUN vs VRUN below)
348 ${CYAN}vstorage${NC} List all storage directories (alias: vstorage list)
349 ${CYAN}vstorage list${NC} List all storage directories with details
350 ${CYAN}vstorage path [arch]${NC} Show path to storage directory
351 ${CYAN}vstorage df${NC} Show detailed disk usage breakdown
352 ${CYAN}vstorage clean [arch|--all]${NC} Clean storage directories (stops memres first)
353 ${CYAN}clean${NC} ${YELLOW}[DEPRECATED]${NC} Use 'vstorage clean' instead
354
355${BOLD}MEMORY RESIDENT MODE (vmemres):${NC}
356 ${CYAN}vmemres start${NC} [-p port:port] Start memory resident VM in background
357 ${CYAN}vmemres stop${NC} Stop memory resident VM
358 ${CYAN}vmemres restart${NC} [--clean] Restart VM (optionally clean state first)
359 ${CYAN}vmemres status${NC} Show memory resident VM status
360 ${CYAN}vmemres list${NC} List all running memres instances
361 (Note: 'memres' also works as an alias for 'vmemres')
362
363 Port forwarding with vmemres:
364 -p <host_port>:<container_port>[/protocol]
365 Forward host port to container port (protocol: tcp or udp, default: tcp)
366 Multiple -p options can be specified
367
368${BOLD}RUN vs VRUN:${NC}
369 ${CYAN}run${NC} - Full ${RUNTIME_UPPER} passthrough. Entrypoint is honored.
370 Command args are passed TO the entrypoint.
371 Example: run alpine /bin/sh -> entrypoint receives '/bin/sh' as arg
372 ${CYAN}vrun${NC} - Convenience wrapper. Clears entrypoint when command given.
373 Command args become the container's command directly.
374 Example: vrun alpine /bin/sh -> runs /bin/sh as PID 1
375
376 Use 'run' when you need --entrypoint, -e, --rm, or other ${VCONTAINER_RUNTIME_CMD} options.
377 Use 'vrun' for simple "run this command in image" cases.
378
379${BOLD}CONFIGURATION (vconfig):${NC}
380 ${CYAN}vconfig${NC} Show all configuration values
381 ${CYAN}vconfig${NC} <key> Get configuration value
382 ${CYAN}vconfig${NC} <key> <value> Set configuration value
383 ${CYAN}vconfig${NC} <key> --reset Reset to default value
384
385 Supported keys: arch, timeout, state-dir, verbose
386 Config file: \$CONFIG_DIR/config (default: ~/.config/${VCONTAINER_RUNTIME_NAME}/config)
387
388${BOLD}GLOBAL OPTIONS:${NC}
389 --arch, -a <arch> Target architecture: x86_64 or aarch64 [default: ${DEFAULT_ARCH}]
390 --config-dir <path> Configuration directory [default: ~/.config/${VCONTAINER_RUNTIME_NAME}]
391 --instance, -I <name> Use named instance (shortcut for --state-dir ~/.$VCONTAINER_RUNTIME_NAME/<name>)
392 --blob-dir <path> Path to kernel/initramfs blobs (override default)
393 --stateless Start with fresh ${RUNTIME_UPPER} state (no persistence)
394 --state-dir <path> Override state directory [default: ~/.$VCONTAINER_RUNTIME_NAME/<arch>]
395 --storage <file> Export ${VCONTAINER_RUNTIME_CMD} storage after command (tar file)
396 --input-storage <tar> Load ${RUNTIME_UPPER} state from tar before command
397 --no-kvm Disable KVM acceleration (use TCG emulation)
398 --verbose, -v Enable verbose output
399 --help, -h Show this help
400
401${BOLD}${RUNTIME_UPPER} RUN/VRUN OPTIONS:${NC}
402 All ${VCONTAINER_RUNTIME_CMD} run options are passed through (e.g., -it, -e, -p, --rm, etc.)
403 Interactive mode (-it) automatically handles daemon stop/restart
404 -v <host>:<container>[:mode] Mount host path in container (requires vmemres)
405 mode: ro (read-only) or rw (read-write, default)
406
407${BOLD}EXAMPLES:${NC}
408 # List images (uses persistent state by default)
409 ${PROG_NAME} images
410
411 # Import rootfs tarball (matches '${VCONTAINER_RUNTIME_CMD} import' exactly)
412 ${PROG_NAME} import rootfs.tar myapp:latest
413
414 # Import OCI directory (extended command, auto-detects format)
415 ${PROG_NAME} vimport ./container-oci/ myapp:latest
416 ${PROG_NAME} images # Image persists!
417
418 # Save image to tar archive
419 ${PROG_NAME} save -o myapp.tar myapp:latest
420
421 # Load a ${RUNTIME_UPPER} image archive (from '${VCONTAINER_RUNTIME_CMD} save')
422 ${PROG_NAME} load -i myapp.tar
423
424 # Start fresh (ignore existing state)
425 ${PROG_NAME} --stateless images
426
427 # Export storage for deployment to target
428 ${PROG_NAME} --storage /tmp/${VCONTAINER_RUNTIME_CMD}-storage.tar vimport ./container-oci/ myapp:latest
429
430 # Run a command in a container (${VCONTAINER_RUNTIME_CMD}-compatible syntax)
431 ${PROG_NAME} run alpine /bin/echo hello
432 ${PROG_NAME} run --rm alpine uname -m # Check container architecture
433
434 # Interactive shell (${VCONTAINER_RUNTIME_CMD}-compatible syntax)
435 ${PROG_NAME} run -it alpine /bin/sh
436
437 # With environment variables and other ${VCONTAINER_RUNTIME_CMD} options
438 ${PROG_NAME} run --rm -e FOO=bar myapp:latest
439 ${PROG_NAME} run -it -p 8080:80 nginx:latest
440
441 # Pull an image from a registry
442 ${PROG_NAME} pull alpine:latest
443
444 # vrun: convenience wrapper (clears entrypoint when command given)
445 ${PROG_NAME} vrun myapp:latest /bin/ls -la # Runs /bin/ls directly, not via entrypoint
446
447 # Volume mounts (requires memres to be running)
448 ${PROG_NAME} memres start
449 ${PROG_NAME} vrun -v /tmp/data:/data alpine cat /data/file.txt
450 ${PROG_NAME} vrun -v /home/user/src:/src:ro alpine ls /src
451 ${PROG_NAME} run -v ./local:/app --rm myapp:latest /app/run.sh
452
453 # Port forwarding (web server)
454 ${PROG_NAME} memres start -p 8080:80 # Forward host:8080 to guest:80
455 ${PROG_NAME} run -d --rm --network=host nginx:alpine # Container uses host network
456 curl http://localhost:8080 # Access nginx from host
457
458 # Port forwarding (SSH into a container)
459 ${PROG_NAME} memres start -p 2222:22 # Forward host:2222 to guest:22
460 ${PROG_NAME} run -d --network=host my-ssh-image # Container with SSH server
461 ssh -p 2222 localhost # SSH from host into container
462
463 # Multiple instances with different ports
464 ${PROG_NAME} memres list # Show running instances
465 ${PROG_NAME} -I web memres start -p 8080:80 # Start named instance
466 ${PROG_NAME} -I web images # Use named instance
467 ${PROG_NAME} -I backend run -d --network=host my-api:latest
468
469${BOLD}NOTES:${NC}
470 - Architecture detection (in priority order):
471 1. --arch / -a flag
472 2. Executable name (${VCONTAINER_RUNTIME_NAME}-aarch64 or ${VCONTAINER_RUNTIME_NAME}-x86_64)
473 3. ${VCONTAINER_RUNTIME_PREFIX}_ARCH environment variable
474 4. Config file: ~/.config/${VCONTAINER_RUNTIME_NAME}/arch
475 5. Host architecture (uname -m)
476 - Current architecture: ${DEFAULT_ARCH}
477 - State persists in ~/.$VCONTAINER_RUNTIME_NAME/<arch>/
478 - Use --stateless for fresh ${RUNTIME_UPPER} state each run
479 - Use --storage to export ${RUNTIME_UPPER} storage to tar file
480 - run vs vrun:
481 run = exact ${VCONTAINER_RUNTIME_CMD} run syntax (entrypoint honored)
482 vrun = clears entrypoint when command given (runs command directly)
483
484${BOLD}ENVIRONMENT:${NC}
485 ${VCONTAINER_RUNTIME_PREFIX}_BLOB_DIR Path to kernel/initramfs blobs
486 ${VCONTAINER_RUNTIME_PREFIX}_STATE_DIR Base directory for state [default: ~/.$VCONTAINER_RUNTIME_NAME]
487 ${VCONTAINER_RUNTIME_PREFIX}_STATELESS Run stateless by default (true/false)
488 ${VCONTAINER_RUNTIME_PREFIX}_VERBOSE Enable verbose output (true/false)
489
490EOF
491}
492
493# Build runner args
494build_runner_args() {
495 local args=()
496
497 # Specify runtime (docker for vdkr, podman for vpdmn)
498 args+=("--runtime" "$VCONTAINER_RUNTIME_CMD")
499 args+=("--arch" "$TARGET_ARCH")
500
501 [ -n "$BLOB_DIR" ] && args+=("--blob-dir" "$BLOB_DIR")
502 [ "$VERBOSE" = "true" ] && args+=("--verbose")
503 [ "$NETWORK" = "true" ] && args+=("--network")
504 [ "$INTERACTIVE" = "true" ] && args+=("--interactive")
505 [ -n "$STORAGE_OUTPUT" ] && args+=("--output-type" "storage" "--output" "$STORAGE_OUTPUT")
506 [ -n "$STATE_DIR" ] && args+=("--state-dir" "$STATE_DIR")
507 [ -n "$INPUT_STORAGE" ] && args+=("--input-storage" "$INPUT_STORAGE")
508 [ "$DISABLE_KVM" = "true" ] && args+=("--no-kvm")
509
510 # Add port forwards (each -p adds a --port-forward)
511 for pf in "${PORT_FORWARDS[@]}"; do
512 args+=("--port-forward" "$pf")
513 done
514
515 echo "${args[@]}"
516}
517
518# Parse global options first
519TARGET_ARCH="$DEFAULT_ARCH"
520STORAGE_OUTPUT=""
521STATE_DIR=""
522INPUT_STORAGE=""
523NETWORK="true"
524INTERACTIVE="false"
525PORT_FORWARDS=()
526DISABLE_KVM="false"
527COMMAND=""
528COMMAND_ARGS=()
529
530while [ $# -gt 0 ]; do
531 case $1 in
532 --arch|-a)
533 # Only parse as global option before command is set
534 if [ -z "$COMMAND" ]; then
535 TARGET_ARCH="$2"
536 shift 2
537 else
538 COMMAND_ARGS+=("$1")
539 shift
540 fi
541 ;;
542 --blob-dir)
543 BLOB_DIR="$2"
544 shift 2
545 ;;
546 --storage)
547 STORAGE_OUTPUT="$2"
548 shift 2
549 ;;
550 --state-dir)
551 STATE_DIR="$2"
552 shift 2
553 ;;
554 --instance|-I)
555 # Shortcut: -I web expands to --state-dir ~/.$VCONTAINER_RUNTIME_NAME/web
556 STATE_DIR="$DEFAULT_STATE_DIR/$2"
557 shift 2
558 ;;
559 --config-dir)
560 # Already pre-parsed, just consume it
561 shift 2
562 ;;
563 --config-dir=*)
564 # Already pre-parsed, just consume it
565 shift
566 ;;
567 --input-storage)
568 INPUT_STORAGE="$2"
569 shift 2
570 ;;
571 --stateless)
572 STATELESS="true"
573 shift
574 ;;
575 --no-network)
576 NETWORK="false"
577 shift
578 ;;
579 --no-kvm)
580 DISABLE_KVM="true"
581 shift
582 ;;
583 -it|--interactive)
584 INTERACTIVE="true"
585 shift
586 ;;
587 -i)
588 # -i alone means interactive, but only before we have a command
589 # After a command, -i might be an argument (e.g., load -i file)
590 if [ -z "$COMMAND" ]; then
591 INTERACTIVE="true"
592 else
593 COMMAND_ARGS+=("$1")
594 fi
595 shift
596 ;;
597 -t)
598 # -t alone means interactive (allocate TTY) before command
599 # After command, -t might be an argument
600 if [ -z "$COMMAND" ]; then
601 INTERACTIVE="true"
602 else
603 COMMAND_ARGS+=("$1")
604 fi
605 shift
606 ;;
607 --verbose)
608 VERBOSE="true"
609 shift
610 ;;
611 -v)
612 # -v can mean verbose (before command) or volume (after command like run/vrun)
613 if [ -z "$COMMAND" ]; then
614 VERBOSE="true"
615 else
616 # After command, -v is likely a volume flag - pass to subcommand
617 COMMAND_ARGS+=("$1")
618 fi
619 shift
620 ;;
621 --help|-h)
622 show_usage
623 exit 0
624 ;;
625 --version)
626 echo "$VCONTAINER_RUNTIME_NAME version $VCONTAINER_VERSION"
627 exit 0
628 ;;
629 -*)
630 # Unknown option - might be for subcommand
631 COMMAND_ARGS+=("$1")
632 shift
633 ;;
634 *)
635 if [ -z "$COMMAND" ]; then
636 COMMAND="$1"
637 else
638 COMMAND_ARGS+=("$1")
639 fi
640 shift
641 ;;
642 esac
643done
644
645if [ -z "$COMMAND" ]; then
646 show_usage
647 exit 0
648fi
649
650# Set up state directory (default to persistent unless --stateless)
651if [ "$STATELESS" != "true" ] && [ -z "$STATE_DIR" ] && [ -z "$INPUT_STORAGE" ]; then
652 STATE_DIR="$DEFAULT_STATE_DIR/$TARGET_ARCH"
653fi
654
655# Check runner exists
656if [ ! -x "$RUNNER" ]; then
657 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Runner script not found: $RUNNER" >&2
658 exit 1
659fi
660
661# Helper function to check if daemon is running
662daemon_is_running() {
663 # Use STATE_DIR if set, otherwise use default
664 local state_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
665 local pid_file="$state_dir/daemon.pid"
666 if [ -f "$pid_file" ]; then
667 local pid=$(cat "$pid_file")
668 if [ -d "/proc/$pid" ]; then
669 return 0
670 fi
671 fi
672 return 1
673}
674
675# Helper function to run command via daemon or regular mode
676run_runtime_command() {
677 local runtime_cmd="$1"
678 local runner_args=$(build_runner_args)
679
680 if daemon_is_running; then
681 # Use daemon mode - faster
682 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode" >&2
683 "$RUNNER" $runner_args --daemon-send "$runtime_cmd"
684 else
685 # Regular mode - start QEMU for this command
686 "$RUNNER" $runner_args -- "$runtime_cmd"
687 fi
688}
689
690# Helper function to run command with input
691# Uses daemon mode with virtio-9p if daemon is running, otherwise regular mode
692run_runtime_command_with_input() {
693 local input_path="$1"
694 local input_type="$2"
695 local runtime_cmd="$3"
696 local runner_args=$(build_runner_args)
697
698 if daemon_is_running; then
699 # Use daemon mode with virtio-9p shared directory
700 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode for file I/O" >&2
701 "$RUNNER" $runner_args --input "$input_path" --input-type "$input_type" --daemon-send-input -- "$runtime_cmd"
702 else
703 # Regular mode - start QEMU for this command
704 "$RUNNER" $runner_args --input "$input_path" --input-type "$input_type" -- "$runtime_cmd"
705 fi
706}
707
708# ============================================================================
709# Volume Mount Support
710# ============================================================================
711# Volumes are copied to the share directory before running the container.
712# Format: -v /host/path:/container/path[:ro|:rw]
713#
714# Implementation:
715# - Copy host path to $SHARE_DIR/volumes/<hash>/
716# - Transform -v to use /mnt/share/volumes/<hash>:/container/path
717# - After container exits, sync back for :rw mounts (default)
718#
719# Limitations:
720# - Requires daemon mode (memres) for volume mounts
721# - Changes in container are synced back after container exits (not real-time)
722# - Large volumes may be slow to copy
723# ============================================================================
724
725# Array to track volume mounts for cleanup/sync
726declare -a VOLUME_MOUNTS=()
727declare -a VOLUME_MODES=()
728
729# Generate a short hash for volume directory naming
730volume_hash() {
731 echo "$1" | md5sum | cut -c1-8
732}
733
734# Global to receive result from prepare_volume (avoids subshell issue)
735PREPARE_VOLUME_RESULT=""
736
737# Prepare a volume mount: copy host path to share directory
738# Sets PREPARE_VOLUME_RESULT to the guest path (avoids subshell issue with arrays)
739prepare_volume() {
740 local host_path="$1"
741 local container_path="$2"
742 local mode="$3" # ro or rw (default: rw)
743
744 [ -z "$mode" ] && mode="rw"
745 PREPARE_VOLUME_RESULT=""
746
747 # Validate host path exists
748 if [ ! -e "$host_path" ]; then
749 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume source not found: $host_path" >&2
750 return 1
751 fi
752
753 # Get share directory
754 local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
755 local volumes_dir="$share_dir/volumes"
756
757 # Create volumes directory
758 mkdir -p "$volumes_dir"
759
760 # Generate unique directory name based on host path
761 local hash=$(volume_hash "$host_path")
762 local vol_dir="$volumes_dir/$hash"
763
764 # Clean and copy
765 rm -rf "$vol_dir"
766 mkdir -p "$vol_dir"
767
768 if [ -d "$host_path" ]; then
769 # Directory: copy contents
770 cp -rL "$host_path"/* "$vol_dir/" 2>/dev/null || true
771 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume: copied directory $host_path -> /mnt/share/volumes/$hash" >&2
772 else
773 # File: copy file
774 cp -L "$host_path" "$vol_dir/"
775 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume: copied file $host_path -> /mnt/share/volumes/$hash" >&2
776 fi
777
778 # Sync to ensure data is visible to guest
779 sync
780
781 # Track for later sync-back (in parent shell, not subshell)
782 VOLUME_MOUNTS+=("$host_path:$vol_dir:$container_path")
783 VOLUME_MODES+=("$mode")
784
785 # Set result in global variable (caller reads this, not $(prepare_volume))
786 PREPARE_VOLUME_RESULT="/mnt/share/volumes/$hash"
787}
788
789# Sync volumes back from guest to host (for :rw mounts)
790sync_volumes_back() {
791 local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
792 local volumes_dir="$share_dir/volumes"
793
794 # Wait for 9p filesystem to sync writes from guest to host
795 sleep 1
796 sync
797
798 for i in "${!VOLUME_MOUNTS[@]}"; do
799 local mount="${VOLUME_MOUNTS[$i]}"
800 local mode="${VOLUME_MODES[$i]}"
801
802 if [ "$mode" = "rw" ]; then
803 # Parse mount string: host_path:vol_dir:container_path
804 local host_path=$(echo "$mount" | cut -d: -f1)
805 local vol_dir=$(echo "$mount" | cut -d: -f2)
806
807 if [ -d "$vol_dir" ] && [ -d "$host_path" ]; then
808 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Syncing volume back: $vol_dir -> $host_path" >&2
809 # Use rsync if available, otherwise cp
810 if command -v rsync >/dev/null 2>&1; then
811 rsync -a --delete "$vol_dir/" "$host_path/"
812 else
813 rm -rf "$host_path"/*
814 cp -rL "$vol_dir"/* "$host_path/" 2>/dev/null || true
815 fi
816 elif [ -f "$host_path" ]; then
817 # Single file mount
818 local filename=$(basename "$host_path")
819 if [ -f "$vol_dir/$filename" ]; then
820 cp -L "$vol_dir/$filename" "$host_path"
821 fi
822 fi
823 fi
824 done
825}
826
827# Clean up volume directories
828cleanup_volumes() {
829 local share_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
830 local volumes_dir="$share_dir/volumes"
831
832 if [ -d "$volumes_dir" ]; then
833 rm -rf "$volumes_dir"
834 fi
835
836 # Clear tracking arrays
837 VOLUME_MOUNTS=()
838 VOLUME_MODES=()
839}
840
841# Global variable to hold transformed volume arguments
842TRANSFORMED_VOLUME_ARGS=()
843
844# Parse volume mounts from arguments and transform them
845# Input: array elements passed as arguments
846# Output: sets TRANSFORMED_VOLUME_ARGS with transformed arguments
847# Side effect: populates VOLUME_MOUNTS array
848parse_and_prepare_volumes() {
849 TRANSFORMED_VOLUME_ARGS=()
850 local args=("$@")
851 local i=0
852
853 while [ $i -lt ${#args[@]} ]; do
854 local arg="${args[$i]}"
855
856 case "$arg" in
857 -v|--volume)
858 # Next arg is the volume spec
859 i=$((i + 1))
860 if [ $i -ge ${#args[@]} ]; then
861 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -v requires an argument" >&2
862 return 1
863 fi
864 local vol_spec="${args[$i]}"
865
866 # Parse volume spec: host:container[:mode]
867 local host_path=$(echo "$vol_spec" | cut -d: -f1)
868 local container_path=$(echo "$vol_spec" | cut -d: -f2)
869 local mode=$(echo "$vol_spec" | cut -d: -f3)
870
871 # Make host path absolute
872 if [[ "$host_path" != /* ]]; then
873 host_path="$(pwd)/$host_path"
874 fi
875
876 # Prepare volume (sets PREPARE_VOLUME_RESULT global)
877 prepare_volume "$host_path" "$container_path" "$mode" || return 1
878 local guest_path="$PREPARE_VOLUME_RESULT"
879
880 # Add transformed volume option
881 if [ -d "$host_path" ]; then
882 TRANSFORMED_VOLUME_ARGS+=("-v" "${guest_path}:${container_path}${mode:+:$mode}")
883 else
884 # For single file, include filename
885 local filename=$(basename "$host_path")
886 TRANSFORMED_VOLUME_ARGS+=("-v" "${guest_path}/${filename}:${container_path}${mode:+:$mode}")
887 fi
888 ;;
889 *)
890 TRANSFORMED_VOLUME_ARGS+=("$arg")
891 ;;
892 esac
893 i=$((i + 1))
894 done
895}
896
897# Handle commands
898case "$COMMAND" in
899 images)
900 # runtime images
901 run_runtime_command "$VCONTAINER_RUNTIME_CMD images ${COMMAND_ARGS[*]}"
902 ;;
903
904 pull)
905 # runtime pull <image>
906 # Daemon mode already has networking enabled, so this works via daemon
907 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
908 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} pull requires <image>" >&2
909 exit 1
910 fi
911
912 IMAGE_NAME="${COMMAND_ARGS[0]}"
913
914 if daemon_is_running; then
915 # Use daemon mode (already has networking)
916 run_runtime_command "$VCONTAINER_RUNTIME_CMD pull $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
917 else
918 # Regular mode - need to enable networking
919 NETWORK="true"
920 RUNNER_ARGS=$(build_runner_args)
921 "$RUNNER" $RUNNER_ARGS -- "$VCONTAINER_RUNTIME_CMD pull $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
922 fi
923 ;;
924
925 load)
926 # runtime load -i <file>
927 # Parse -i argument
928 INPUT_FILE=""
929 LOAD_ARGS=()
930 i=0
931 while [ $i -lt ${#COMMAND_ARGS[@]} ]; do
932 arg="${COMMAND_ARGS[$i]}"
933 case "$arg" in
934 -i|--input)
935 i=$((i + 1))
936 INPUT_FILE="${COMMAND_ARGS[$i]}"
937 ;;
938 *)
939 LOAD_ARGS+=("$arg")
940 ;;
941 esac
942 i=$((i + 1))
943 done
944
945 if [ -z "$INPUT_FILE" ]; then
946 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} load requires -i <file>" >&2
947 exit 1
948 fi
949
950 if [ ! -f "$INPUT_FILE" ]; then
951 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} File not found: $INPUT_FILE" >&2
952 exit 1
953 fi
954
955 run_runtime_command_with_input "$INPUT_FILE" "tar" \
956 "$VCONTAINER_RUNTIME_CMD load -i {INPUT}/$(basename "$INPUT_FILE") ${LOAD_ARGS[*]}"
957 ;;
958
959 import)
960 # runtime import <tarball> [name:tag] - matches Docker/Podman's import exactly
961 # Only accepts tarballs (rootfs archives), not OCI directories
962 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
963 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} import requires <tarball> [name:tag]" >&2
964 echo "For OCI directories, use 'vimport' instead." >&2
965 exit 1
966 fi
967
968 INPUT_PATH="${COMMAND_ARGS[0]}"
969 IMAGE_NAME="${COMMAND_ARGS[1]:-imported:latest}"
970
971 if [ ! -e "$INPUT_PATH" ]; then
972 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $INPUT_PATH" >&2
973 exit 1
974 fi
975
976 # Only accept files (tarballs), not directories
977 if [ -d "$INPUT_PATH" ]; then
978 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} import only accepts tarballs, not directories" >&2
979 echo "For OCI directories, use: $VCONTAINER_RUNTIME_NAME vimport $INPUT_PATH $IMAGE_NAME" >&2
980 exit 1
981 fi
982
983 run_runtime_command_with_input "$INPUT_PATH" "tar" \
984 "$VCONTAINER_RUNTIME_CMD import {INPUT}/$(basename "$INPUT_PATH") $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
985 ;;
986
987 vimport)
988 # Extended import: handles OCI directories, tarballs, and plain directories
989 # Auto-detects format
990 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
991 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} vimport requires <path> [name:tag]" >&2
992 exit 1
993 fi
994
995 INPUT_PATH="${COMMAND_ARGS[0]}"
996 IMAGE_NAME="${COMMAND_ARGS[1]:-imported:latest}"
997
998 if [ ! -e "$INPUT_PATH" ]; then
999 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $INPUT_PATH" >&2
1000 exit 1
1001 fi
1002
1003 # Detect input type
1004 if [ -d "$INPUT_PATH" ]; then
1005 if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then
1006 INPUT_TYPE="oci"
1007
1008 # Check architecture before importing
1009 if ! check_oci_arch "$INPUT_PATH" "$TARGET_ARCH"; then
1010 exit 1
1011 fi
1012
1013 # Use skopeo to properly import OCI image with full metadata (entrypoint, cmd, etc.)
1014 # This preserves the container config unlike raw import
1015 RUNTIME_CMD="skopeo copy oci:{INPUT} ${VCONTAINER_IMPORT_TARGET}$IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
1016 else
1017 # Directory but not OCI - check if it looks like a deploy/images dir
1018 # and provide a helpful hint
1019 if ls "$INPUT_PATH"/*-oci >/dev/null 2>&1; then
1020 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Directory is not an OCI container: $INPUT_PATH" >&2
1021 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Found OCI directories inside. Did you mean one of these?" >&2
1022 for oci_dir in "$INPUT_PATH"/*-oci; do
1023 if [ -d "$oci_dir" ]; then
1024 echo " $(basename "$oci_dir")" >&2
1025 fi
1026 done
1027 echo "" >&2
1028 echo "Example: $VCONTAINER_RUNTIME_NAME vimport $INPUT_PATH/$(ls "$INPUT_PATH" | grep -m1 '\-oci$') myimage:latest" >&2
1029 exit 1
1030 fi
1031 INPUT_TYPE="dir"
1032 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD import {INPUT} $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
1033 fi
1034 else
1035 INPUT_TYPE="tar"
1036 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD import {INPUT}/$(basename "$INPUT_PATH") $IMAGE_NAME && $VCONTAINER_RUNTIME_CMD images"
1037 fi
1038
1039 run_runtime_command_with_input "$INPUT_PATH" "$INPUT_TYPE" "$RUNTIME_CMD"
1040 ;;
1041
1042 save)
1043 # runtime save -o <file> <image>
1044 OUTPUT_FILE=""
1045 IMAGE_NAME=""
1046 i=0
1047 while [ $i -lt ${#COMMAND_ARGS[@]} ]; do
1048 arg="${COMMAND_ARGS[$i]}"
1049 case "$arg" in
1050 -o|--output)
1051 i=$((i + 1))
1052 OUTPUT_FILE="${COMMAND_ARGS[$i]}"
1053 ;;
1054 *)
1055 IMAGE_NAME="$arg"
1056 ;;
1057 esac
1058 i=$((i + 1))
1059 done
1060
1061 if [ -z "$OUTPUT_FILE" ]; then
1062 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} save requires -o <file>" >&2
1063 exit 1
1064 fi
1065
1066 if [ -z "$IMAGE_NAME" ]; then
1067 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} save requires <image> name" >&2
1068 exit 1
1069 fi
1070
1071 if daemon_is_running; then
1072 # Use daemon mode with virtio-9p - save to shared dir, then copy to host
1073 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon mode for save" >&2
1074 SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
1075
1076 # Clear share dir and run save command
1077 rm -rf "$SHARE_DIR"/* 2>/dev/null || true
1078 run_runtime_command "$VCONTAINER_RUNTIME_CMD save -o /mnt/share/output.tar $IMAGE_NAME"
1079
1080 # Copy from share dir to output file
1081 if [ -f "$SHARE_DIR/output.tar" ]; then
1082 cp "$SHARE_DIR/output.tar" "$OUTPUT_FILE"
1083 rm -f "$SHARE_DIR/output.tar"
1084 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Saved to $OUTPUT_FILE ($(du -h "$OUTPUT_FILE" | cut -f1))"
1085 else
1086 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Save failed - output not found in shared directory" >&2
1087 exit 1
1088 fi
1089 else
1090 # Regular mode - use serial output
1091 RUNNER_ARGS=$(build_runner_args)
1092 RUNNER_ARGS=$(echo "$RUNNER_ARGS" | sed 's/--output-type storage//')
1093 "$RUNNER" $RUNNER_ARGS --output-type tar --output "$OUTPUT_FILE" \
1094 -- "$VCONTAINER_RUNTIME_CMD save -o /tmp/output.tar $IMAGE_NAME"
1095 fi
1096 ;;
1097
1098 tag|rmi)
1099 # Commands that work with existing images
1100 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}"
1101 ;;
1102
1103 # Container lifecycle commands
1104 ps)
1105 # List containers
1106 run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}"
1107 ;;
1108
1109 rm)
1110 # Remove containers
1111 run_runtime_command "$VCONTAINER_RUNTIME_CMD rm ${COMMAND_ARGS[*]}"
1112 ;;
1113
1114 logs)
1115 # View container logs
1116 run_runtime_command "$VCONTAINER_RUNTIME_CMD logs ${COMMAND_ARGS[*]}"
1117 ;;
1118
1119 inspect)
1120 # Inspect container or image
1121 run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}"
1122 ;;
1123
1124 start|stop|restart|kill|pause|unpause)
1125 # Container state commands
1126 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}"
1127 ;;
1128
1129 # Image commands
1130 commit)
1131 # Commit container to image
1132 run_runtime_command "$VCONTAINER_RUNTIME_CMD commit ${COMMAND_ARGS[*]}"
1133 ;;
1134
1135 history)
1136 # Show image history
1137 run_runtime_command "$VCONTAINER_RUNTIME_CMD history ${COMMAND_ARGS[*]}"
1138 ;;
1139
1140 # Registry commands
1141 push)
1142 # Push image to registry
1143 run_runtime_command "$VCONTAINER_RUNTIME_CMD push ${COMMAND_ARGS[*]}"
1144 ;;
1145
1146 search)
1147 # Search registries
1148 run_runtime_command "$VCONTAINER_RUNTIME_CMD search ${COMMAND_ARGS[*]}"
1149 ;;
1150
1151 login)
1152 # Login to registry - may need credentials via stdin
1153 # For non-interactive: runtime login -u user -p pass registry
1154 run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}"
1155 ;;
1156
1157 logout)
1158 # Logout from registry
1159 run_runtime_command "$VCONTAINER_RUNTIME_CMD logout ${COMMAND_ARGS[*]}"
1160 ;;
1161
1162 # Runtime exec - execute command in running container
1163 exec)
1164 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then
1165 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} exec requires <container> <command>" >&2
1166 exit 1
1167 fi
1168
1169 # Check for interactive flags
1170 EXEC_INTERACTIVE=false
1171 EXEC_ARGS=()
1172 for arg in "${COMMAND_ARGS[@]}"; do
1173 case "$arg" in
1174 -it|-ti|--interactive|--tty)
1175 EXEC_INTERACTIVE=true
1176 EXEC_ARGS+=("$arg")
1177 ;;
1178 -i|-t)
1179 EXEC_INTERACTIVE=true
1180 EXEC_ARGS+=("$arg")
1181 ;;
1182 *)
1183 EXEC_ARGS+=("$arg")
1184 ;;
1185 esac
1186 done
1187
1188 if [ "$EXEC_INTERACTIVE" = "true" ]; then
1189 # Interactive exec can use daemon_interactive if daemon is running
1190 if daemon_is_running; then
1191 # Use daemon interactive mode - keeps daemon running
1192 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2
1193 RUNNER_ARGS=$(build_runner_args)
1194 "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}"
1195 else
1196 # No daemon running, use regular QEMU
1197 RUNNER_ARGS=$(build_runner_args)
1198 "$RUNNER" $RUNNER_ARGS -- "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}"
1199 fi
1200 else
1201 # Non-interactive exec via daemon
1202 run_runtime_command "$VCONTAINER_RUNTIME_CMD exec ${EXEC_ARGS[*]}"
1203 fi
1204 ;;
1205
1206 # Runtime cp - copy files to/from container
1207 cp)
1208 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then
1209 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires <src> <dest>" >&2
1210 echo "Usage: $VCONTAINER_RUNTIME_NAME cp <container>:<path> <local_path>" >&2
1211 echo " $VCONTAINER_RUNTIME_NAME cp <local_path> <container>:<path>" >&2
1212 exit 1
1213 fi
1214
1215 SRC="${COMMAND_ARGS[0]}"
1216 DEST="${COMMAND_ARGS[1]}"
1217
1218 # Determine direction: host->container or container->host
1219 if [[ "$SRC" == *":"* ]] && [[ "$DEST" != *":"* ]]; then
1220 # Container to host: runtime cp container:/path /local/path
1221 # Run runtime cp to /mnt/share, then copy from share to host
1222 CONTAINER_PATH="$SRC"
1223 HOST_PATH="$DEST"
1224 SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
1225
1226 if daemon_is_running; then
1227 rm -rf "$SHARE_DIR"/* 2>/dev/null || true
1228 run_runtime_command "$VCONTAINER_RUNTIME_CMD cp $CONTAINER_PATH /mnt/share/"
1229 # Find what was copied and move to destination
1230 if [ -n "$(ls -A "$SHARE_DIR" 2>/dev/null)" ]; then
1231 cp -r "$SHARE_DIR"/* "$HOST_PATH" 2>/dev/null || cp -r "$SHARE_DIR"/* "$(dirname "$HOST_PATH")/"
1232 rm -rf "$SHARE_DIR"/*
1233 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Copied to $HOST_PATH"
1234 else
1235 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Copy failed - no files in share directory" >&2
1236 exit 1
1237 fi
1238 else
1239 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2
1240 exit 1
1241 fi
1242
1243 elif [[ "$SRC" != *":"* ]] && [[ "$DEST" == *":"* ]]; then
1244 # Host to container: runtime cp /local/path container:/path
1245 HOST_PATH="$SRC"
1246 CONTAINER_PATH="$DEST"
1247
1248 if [ ! -e "$HOST_PATH" ]; then
1249 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Source not found: $HOST_PATH" >&2
1250 exit 1
1251 fi
1252
1253 if daemon_is_running; then
1254 SHARE_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}/share"
1255 rm -rf "$SHARE_DIR"/* 2>/dev/null || true
1256 cp -r "$HOST_PATH" "$SHARE_DIR/"
1257 sync
1258 BASENAME=$(basename "$HOST_PATH")
1259 run_runtime_command "$VCONTAINER_RUNTIME_CMD cp /mnt/share/$BASENAME $CONTAINER_PATH"
1260 rm -rf "$SHARE_DIR"/*
1261 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Copied to $CONTAINER_PATH"
1262 else
1263 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} cp requires daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2
1264 exit 1
1265 fi
1266 else
1267 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid cp syntax. One path must be container:path" >&2
1268 exit 1
1269 fi
1270 ;;
1271
1272 vconfig)
1273 # Configuration management (runs on host, not in VM)
1274 VALID_KEYS="arch timeout state-dir verbose"
1275
1276 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1277 # Show all config
1278 echo "$VCONTAINER_RUNTIME_NAME configuration ($CONFIG_FILE):"
1279 echo ""
1280 for key in $VALID_KEYS; do
1281 value=$(config_get "$key" "")
1282 default=$(config_default "$key")
1283 if [ -n "$value" ]; then
1284 echo " ${CYAN}$key${NC} = $value"
1285 else
1286 echo " ${CYAN}$key${NC} = $default ${YELLOW}(default)${NC}"
1287 fi
1288 done
1289 echo ""
1290 echo "Config directory: $CONFIG_DIR"
1291 else
1292 KEY="${COMMAND_ARGS[0]}"
1293 VALUE="${COMMAND_ARGS[1]:-}"
1294
1295 # Validate key
1296 if ! echo "$VALID_KEYS" | grep -qw "$KEY"; then
1297 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown config key: $KEY" >&2
1298 echo "Valid keys: $VALID_KEYS" >&2
1299 exit 1
1300 fi
1301
1302 if [ -z "$VALUE" ]; then
1303 # Get value
1304 current=$(config_get "$KEY" "")
1305 default=$(config_default "$KEY")
1306 if [ -n "$current" ]; then
1307 echo "$current"
1308 else
1309 echo "$default"
1310 fi
1311 elif [ "$VALUE" = "--reset" ]; then
1312 # Reset to default
1313 config_unset "$KEY"
1314 echo "Reset $KEY to default: $(config_default "$KEY")"
1315 else
1316 # Validate value for arch
1317 if [ "$KEY" = "arch" ]; then
1318 case "$VALUE" in
1319 aarch64|x86_64) ;;
1320 *)
1321 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid architecture: $VALUE" >&2
1322 echo "Valid values: aarch64, x86_64" >&2
1323 exit 1
1324 ;;
1325 esac
1326 fi
1327
1328 # Validate value for verbose
1329 if [ "$KEY" = "verbose" ]; then
1330 case "$VALUE" in
1331 true|false) ;;
1332 *)
1333 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Invalid verbose value: $VALUE" >&2
1334 echo "Valid values: true, false" >&2
1335 exit 1
1336 ;;
1337 esac
1338 fi
1339
1340 # Set value
1341 config_set "$KEY" "$VALUE"
1342 echo "Set $KEY = $VALUE"
1343 fi
1344 fi
1345 ;;
1346
1347 clean)
1348 # DEPRECATED: Use 'vstorage clean' instead
1349 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} DEPRECATED: 'clean' is deprecated, use 'vstorage clean' instead" >&2
1350 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean - clean current architecture" >&2
1351 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean <arch> - clean specific architecture" >&2
1352 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} vstorage clean --all - clean all architectures" >&2
1353 echo "" >&2
1354
1355 # Still perform the clean for now (will be removed in future version)
1356 CLEAN_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
1357 if [ -d "$CLEAN_DIR" ]; then
1358 # Stop memres if running
1359 if [ -f "$CLEAN_DIR/daemon.pid" ]; then
1360 pid=$(cat "$CLEAN_DIR/daemon.pid" 2>/dev/null)
1361 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
1362 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..."
1363 kill "$pid" 2>/dev/null || true
1364 fi
1365 fi
1366 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing state directory: $CLEAN_DIR"
1367 rm -rf "$CLEAN_DIR"
1368 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} State cleaned. Next run will start fresh."
1369 else
1370 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} No state directory found for $TARGET_ARCH"
1371 fi
1372 ;;
1373
1374 info)
1375 run_runtime_command "$VCONTAINER_RUNTIME_CMD info"
1376 ;;
1377
1378 version)
1379 run_runtime_command "$VCONTAINER_RUNTIME_CMD version"
1380 ;;
1381
1382 system)
1383 # Passthrough to runtime system commands (df, prune, events, etc.)
1384 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1385 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} system requires a subcommand: df, prune, events, info" >&2
1386 exit 1
1387 fi
1388 run_runtime_command "$VCONTAINER_RUNTIME_CMD system ${COMMAND_ARGS[*]}"
1389 ;;
1390
1391 vstorage)
1392 # Host-side storage management (runs on host, not in VM)
1393 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1394 STORAGE_CMD="list"
1395 else
1396 STORAGE_CMD="${COMMAND_ARGS[0]}"
1397 fi
1398
1399 case "$STORAGE_CMD" in
1400 list)
1401 echo "$VCONTAINER_RUNTIME_NAME storage directories:"
1402 echo ""
1403 found=0
1404 for state_dir in "$DEFAULT_STATE_DIR"/*/; do
1405 [ -d "$state_dir" ] || continue
1406 found=1
1407 instance=$(basename "$state_dir")
1408 size=$(du -sh "$state_dir" 2>/dev/null | cut -f1)
1409
1410 echo " ${CYAN}$instance${NC}"
1411 echo " Path: $state_dir"
1412 echo " Size: $size"
1413
1414 # Check if memres is running
1415 if [ -f "$state_dir/daemon.pid" ]; then
1416 pid=$(cat "$state_dir/daemon.pid")
1417 if [ -d "/proc/$pid" ]; then
1418 echo " Status: ${GREEN}memres running${NC} (PID $pid)"
1419 else
1420 echo " Status: stopped"
1421 fi
1422 else
1423 echo " Status: no memres"
1424 fi
1425 echo ""
1426 done
1427 if [ $found -eq 0 ]; then
1428 echo " (no storage directories found)"
1429 echo ""
1430 fi
1431
1432 # Total size
1433 if [ -d "$DEFAULT_STATE_DIR" ] && [ $found -gt 0 ]; then
1434 total=$(du -sh "$DEFAULT_STATE_DIR" 2>/dev/null | cut -f1)
1435 echo "Total: $total"
1436 fi
1437 ;;
1438
1439 path)
1440 # Show path for specific or current architecture
1441 arch="${COMMAND_ARGS[1]:-$TARGET_ARCH}"
1442 echo "${STATE_DIR:-$DEFAULT_STATE_DIR/$arch}"
1443 ;;
1444
1445 df)
1446 # Detailed breakdown
1447 for state_dir in "$DEFAULT_STATE_DIR"/*/; do
1448 [ -d "$state_dir" ] || continue
1449 instance=$(basename "$state_dir")
1450 echo "${BOLD}$instance${NC}:"
1451
1452 # Show individual components
1453 for item in "$VCONTAINER_STATE_FILE" share; do
1454 if [ -e "$state_dir/$item" ]; then
1455 item_size=$(du -sh "$state_dir/$item" 2>/dev/null | cut -f1)
1456 printf " %-20s %s\n" "$item" "$item_size"
1457 fi
1458 done
1459 echo ""
1460 done
1461 ;;
1462
1463 clean)
1464 # Clean storage for specific arch or all
1465 arch="${COMMAND_ARGS[1]:-}"
1466 if [ "$arch" = "--all" ]; then
1467 # Stop any running memres first
1468 for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do
1469 [ -f "$pid_file" ] || continue
1470 pid=$(cat "$pid_file" 2>/dev/null)
1471 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
1472 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..."
1473 kill "$pid" 2>/dev/null || true
1474 fi
1475 done
1476 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..."
1477 rm -rf "$DEFAULT_STATE_DIR"
1478 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned."
1479 elif [ -n "$arch" ]; then
1480 # Clean specific arch
1481 clean_dir="$DEFAULT_STATE_DIR/$arch"
1482 if [ -d "$clean_dir" ]; then
1483 # Stop memres if running
1484 if [ -f "$clean_dir/daemon.pid" ]; then
1485 pid=$(cat "$clean_dir/daemon.pid" 2>/dev/null)
1486 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
1487 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..."
1488 kill "$pid" 2>/dev/null || true
1489 fi
1490 fi
1491 rm -rf "$clean_dir"
1492 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaned: $clean_dir"
1493 else
1494 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Not found: $clean_dir"
1495 fi
1496 else
1497 # Clean current arch (same as existing clean command)
1498 clean_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
1499 if [ -d "$clean_dir" ]; then
1500 # Stop memres if running
1501 if [ -f "$clean_dir/daemon.pid" ]; then
1502 pid=$(cat "$clean_dir/daemon.pid" 2>/dev/null)
1503 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
1504 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping memres (PID $pid)..."
1505 kill "$pid" 2>/dev/null || true
1506 fi
1507 fi
1508 rm -rf "$clean_dir"
1509 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaned: $clean_dir"
1510 else
1511 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} No storage directory found for $TARGET_ARCH"
1512 fi
1513 fi
1514 ;;
1515
1516 *)
1517 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown vstorage subcommand: $STORAGE_CMD" >&2
1518 echo "Usage: $VCONTAINER_RUNTIME_NAME vstorage [list|path|df|clean]" >&2
1519 echo "" >&2
1520 echo "Subcommands:" >&2
1521 echo " list List all storage directories with details" >&2
1522 echo " path [arch] Show path to storage directory" >&2
1523 echo " df Show detailed disk usage breakdown" >&2
1524 echo " clean [arch|--all] Clean storage directories" >&2
1525 exit 1
1526 ;;
1527 esac
1528 ;;
1529
1530 vrun)
1531 # Extended run: run a command in a container (runtime-like syntax)
1532 # Usage: <tool> vrun [options] <image> [command] [args...]
1533 # Options:
1534 # -it, -i, -t Interactive mode with TTY
1535 # --network, -n Enable networking
1536 # -p <host>:<guest> Forward port from host to container
1537 # -v <host>:<container> Mount host directory in container
1538 #
1539 # Parse vrun-specific options (allows runtime-like: vdkr vrun -it alpine /bin/sh)
1540 VRUN_VOLUMES=()
1541 HAS_VOLUMES=false
1542
1543 while [ ${#COMMAND_ARGS[@]} -gt 0 ]; do
1544 case "${COMMAND_ARGS[0]}" in
1545 -it|--interactive)
1546 INTERACTIVE="true"
1547 COMMAND_ARGS=("${COMMAND_ARGS[@]:1}")
1548 ;;
1549 -i|-t)
1550 INTERACTIVE="true"
1551 COMMAND_ARGS=("${COMMAND_ARGS[@]:1}")
1552 ;;
1553 --no-network)
1554 NETWORK="false"
1555 COMMAND_ARGS=("${COMMAND_ARGS[@]:1}")
1556 ;;
1557 -p|--publish)
1558 # Port forward: -p 8080:80 or -p 8080:80/tcp
1559 NETWORK="true" # Port forwarding requires networking
1560 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then
1561 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -p requires <host_port>:<container_port>" >&2
1562 exit 1
1563 fi
1564 PORT_FORWARDS+=("${COMMAND_ARGS[1]}")
1565 COMMAND_ARGS=("${COMMAND_ARGS[@]:2}")
1566 ;;
1567 -v|--volume)
1568 # Volume mount: -v /host/path:/container/path[:ro|:rw]
1569 if [ ${#COMMAND_ARGS[@]} -lt 2 ]; then
1570 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -v requires <host_path>:<container_path>" >&2
1571 exit 1
1572 fi
1573 VRUN_VOLUMES+=("-v" "${COMMAND_ARGS[1]}")
1574 HAS_VOLUMES=true
1575 COMMAND_ARGS=("${COMMAND_ARGS[@]:2}")
1576 ;;
1577 -*)
1578 # Unknown option - stop parsing, rest goes to container
1579 break
1580 ;;
1581 *)
1582 # Not an option - this is the image name
1583 break
1584 ;;
1585 esac
1586 done
1587
1588 # Volume mounts require daemon mode
1589 if [ "$HAS_VOLUMES" = "true" ] && ! daemon_is_running; then
1590 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2
1591 exit 1
1592 fi
1593
1594 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1595 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} vrun requires <image> [command] [args...]" >&2
1596 echo "Usage: $VCONTAINER_RUNTIME_NAME vrun [options] <image> [command] [args...]" >&2
1597 echo "" >&2
1598 echo "Options:" >&2
1599 echo " -it, -i, -t Interactive mode with TTY" >&2
1600 echo " --no-network Disable networking" >&2
1601 echo " -p <host>:<container> Forward port" >&2
1602 echo " -v <host>:<container> Mount host directory in container" >&2
1603 echo "" >&2
1604 echo "Examples:" >&2
1605 echo " $VCONTAINER_RUNTIME_NAME vrun alpine /bin/ls -la" >&2
1606 echo " $VCONTAINER_RUNTIME_NAME vrun -it alpine /bin/sh" >&2
1607 echo " $VCONTAINER_RUNTIME_NAME vrun -p 8080:80 nginx:latest" >&2
1608 echo " $VCONTAINER_RUNTIME_NAME vrun -v /tmp/data:/data alpine cat /data/file.txt" >&2
1609 exit 1
1610 fi
1611
1612 IMAGE_NAME="${COMMAND_ARGS[0]}"
1613 CONTAINER_CMD=""
1614
1615 # Build command from remaining args
1616 for ((i=1; i<${#COMMAND_ARGS[@]}; i++)); do
1617 if [ -n "$CONTAINER_CMD" ]; then
1618 CONTAINER_CMD="$CONTAINER_CMD ${COMMAND_ARGS[$i]}"
1619 else
1620 CONTAINER_CMD="${COMMAND_ARGS[$i]}"
1621 fi
1622 done
1623
1624 # Prepare volume mounts if any
1625 VOLUME_OPTS=""
1626 if [ "$HAS_VOLUMES" = "true" ]; then
1627 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Preparing volume mounts..." >&2
1628
1629 # Parse and prepare volumes (transforms host paths to guest paths)
1630 parse_and_prepare_volumes "${VRUN_VOLUMES[@]}" || {
1631 cleanup_volumes
1632 exit 1
1633 }
1634
1635 # Build volume options string from transformed args
1636 VOLUME_OPTS="${TRANSFORMED_VOLUME_ARGS[*]}"
1637 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Volume options: $VOLUME_OPTS" >&2
1638 fi
1639
1640 # Build runtime run command
1641 RUNTIME_RUN_OPTS="--rm"
1642 if [ "$INTERACTIVE" = "true" ]; then
1643 RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS -it"
1644 fi
1645 # Use host networking when enabled (container shares VM's network stack)
1646 # This is needed because Docker runs with --bridge=none
1647 if [ "$NETWORK" = "true" ]; then
1648 RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS --network=host --dns=10.0.2.3 --dns=8.8.8.8"
1649 fi
1650
1651 # Add volume mounts
1652 if [ -n "$VOLUME_OPTS" ]; then
1653 RUNTIME_RUN_OPTS="$RUNTIME_RUN_OPTS $VOLUME_OPTS"
1654 fi
1655
1656 if [ -n "$CONTAINER_CMD" ]; then
1657 # Clear entrypoint when command provided - ensures command runs directly
1658 # without being passed to image's entrypoint (e.g., prevents 'sh /bin/echo')
1659 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run $RUNTIME_RUN_OPTS --entrypoint '' $IMAGE_NAME $CONTAINER_CMD"
1660 else
1661 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run $RUNTIME_RUN_OPTS $IMAGE_NAME"
1662 fi
1663
1664 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Runtime command: $RUNTIME_CMD" >&2
1665
1666 # Use daemon mode for non-interactive runs
1667 if [ "$INTERACTIVE" = "true" ]; then
1668 # Interactive mode with volumes still needs to stop daemon (volumes use share dir)
1669 # Interactive mode without volumes can use daemon_interactive (faster)
1670 if [ "$HAS_VOLUMES" = "false" ] && daemon_is_running; then
1671 # Use daemon interactive mode - keeps daemon running
1672 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2
1673 RUNNER_ARGS=$(build_runner_args)
1674 "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$RUNTIME_CMD"
1675 exit $?
1676 else
1677 # Fall back to regular QEMU for interactive (stop daemon if running)
1678 DAEMON_WAS_RUNNING=false
1679 if daemon_is_running; then
1680 DAEMON_WAS_RUNNING=true
1681 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping daemon for interactive mode..." >&2
1682 "$RUNNER" --state-dir "${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" --daemon-stop >/dev/null 2>&1 || true
1683 sleep 1
1684 fi
1685 RUNNER_ARGS=$(build_runner_args)
1686 "$RUNNER" $RUNNER_ARGS -- "$RUNTIME_CMD"
1687 VRUN_EXIT=$?
1688
1689 # Sync volumes back after container exits
1690 if [ "$HAS_VOLUMES" = "true" ]; then
1691 sync_volumes_back
1692 cleanup_volumes
1693 fi
1694
1695 # Restart daemon if it was running before
1696 if [ "$DAEMON_WAS_RUNNING" = "true" ]; then
1697 echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting daemon..." >&2
1698 "$RUNNER" $RUNNER_ARGS --daemon-start >/dev/null 2>&1 || true
1699 fi
1700
1701 exit $VRUN_EXIT
1702 fi
1703 else
1704 # Non-interactive can use daemon mode
1705 run_runtime_command "$RUNTIME_CMD"
1706 VRUN_EXIT=$?
1707
1708 # Sync volumes back after container exits
1709 if [ "$HAS_VOLUMES" = "true" ]; then
1710 sync_volumes_back
1711 cleanup_volumes
1712 fi
1713
1714 exit $VRUN_EXIT
1715 fi
1716 ;;
1717
1718 run)
1719 # Runtime run command - mirrors 'docker/podman run' syntax
1720 # Usage: <tool> run [options] <image> [command]
1721 # Automatically prepends 'runtime run' to the arguments
1722 # Supports volume mounts with -v (requires daemon mode)
1723 if [ ${#COMMAND_ARGS[@]} -eq 0 ]; then
1724 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} run requires an image" >&2
1725 echo "Usage: $VCONTAINER_RUNTIME_NAME run [options] <image> [command]" >&2
1726 echo "" >&2
1727 echo "Examples:" >&2
1728 echo " $VCONTAINER_RUNTIME_NAME run alpine /bin/echo hello" >&2
1729 echo " $VCONTAINER_RUNTIME_NAME run -it alpine /bin/sh" >&2
1730 echo " $VCONTAINER_RUNTIME_NAME run --rm -e FOO=bar myapp:latest" >&2
1731 echo " $VCONTAINER_RUNTIME_NAME run -v /tmp/data:/data alpine cat /data/file.txt" >&2
1732 exit 1
1733 fi
1734
1735 # Check if any volume mounts are present
1736 RUN_HAS_VOLUMES=false
1737 for arg in "${COMMAND_ARGS[@]}"; do
1738 if [ "$arg" = "-v" ] || [ "$arg" = "--volume" ]; then
1739 RUN_HAS_VOLUMES=true
1740 break
1741 fi
1742 done
1743
1744 # Volume mounts require daemon mode
1745 if [ "$RUN_HAS_VOLUMES" = "true" ] && ! daemon_is_running; then
1746 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Volume mounts require daemon mode. Start with: $VCONTAINER_RUNTIME_NAME memres start" >&2
1747 exit 1
1748 fi
1749
1750 # Transform volume mounts if present
1751 if [ "$RUN_HAS_VOLUMES" = "true" ]; then
1752 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Preparing volume mounts for run command..." >&2
1753
1754 # Parse and prepare volumes (transforms host paths to guest paths)
1755 parse_and_prepare_volumes "${COMMAND_ARGS[@]}" || {
1756 cleanup_volumes
1757 exit 1
1758 }
1759 # Update COMMAND_ARGS with transformed values
1760 COMMAND_ARGS=("${TRANSFORMED_VOLUME_ARGS[@]}")
1761 fi
1762
1763 # Build runtime run command from args
1764 # Note: -it may have been consumed by global parser, so add it back if INTERACTIVE is set
1765 if [ "$INTERACTIVE" = "true" ]; then
1766 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run -it ${COMMAND_ARGS[*]}"
1767 else
1768 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run ${COMMAND_ARGS[*]}"
1769 fi
1770
1771 if [ "$INTERACTIVE" = "true" ]; then
1772 # Interactive mode with volumes still needs to stop daemon (volumes use share dir)
1773 # Interactive mode without volumes can use daemon_interactive (faster)
1774 if [ "$RUN_HAS_VOLUMES" = "false" ] && daemon_is_running; then
1775 # Use daemon interactive mode - keeps daemon running
1776 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Using daemon interactive mode" >&2
1777 RUNNER_ARGS=$(build_runner_args)
1778 "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$RUNTIME_CMD"
1779 exit $?
1780 else
1781 # Fall back to regular QEMU for interactive (stop daemon if running)
1782 DAEMON_WAS_RUNNING=false
1783 if daemon_is_running; then
1784 DAEMON_WAS_RUNNING=true
1785 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Stopping daemon for interactive mode..." >&2
1786 "$RUNNER" --state-dir "${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}" --daemon-stop >/dev/null 2>&1 || true
1787 sleep 1
1788 fi
1789 RUNNER_ARGS=$(build_runner_args)
1790 "$RUNNER" $RUNNER_ARGS -- "$RUNTIME_CMD"
1791 RUN_EXIT=$?
1792
1793 # Sync volumes back after container exits
1794 if [ "$RUN_HAS_VOLUMES" = "true" ]; then
1795 sync_volumes_back
1796 cleanup_volumes
1797 fi
1798
1799 # Restart daemon if it was running before
1800 if [ "$DAEMON_WAS_RUNNING" = "true" ]; then
1801 echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting daemon..." >&2
1802 "$RUNNER" $RUNNER_ARGS --daemon-start >/dev/null 2>&1 || true
1803 fi
1804
1805 exit $RUN_EXIT
1806 fi
1807 else
1808 # Non-interactive - use daemon mode when available
1809 run_runtime_command "$RUNTIME_CMD"
1810 RUN_EXIT=$?
1811
1812 # Sync volumes back after container exits
1813 if [ "$RUN_HAS_VOLUMES" = "true" ]; then
1814 sync_volumes_back
1815 cleanup_volumes
1816 fi
1817
1818 exit $RUN_EXIT
1819 fi
1820 ;;
1821
1822 # Memory resident subcommand: <tool> memres start|stop|restart|status
1823 # vmemres is the preferred name (v prefix for tool-specific commands)
1824 memres|vmemres)
1825 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1826 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} memres requires a subcommand: start, stop, restart, status, list" >&2
1827 exit 1
1828 fi
1829
1830 MEMRES_CMD="${COMMAND_ARGS[0]}"
1831
1832 # Parse memres-specific options (after the subcommand)
1833 MEMRES_ARGS=("${COMMAND_ARGS[@]:1}")
1834 i=0
1835 while [ $i -lt ${#MEMRES_ARGS[@]} ]; do
1836 arg="${MEMRES_ARGS[$i]}"
1837 case "$arg" in
1838 -p|--publish)
1839 # Port forward: -p 8080:80 or -p 8080:80/tcp
1840 i=$((i + 1))
1841 if [ $i -ge ${#MEMRES_ARGS[@]} ]; then
1842 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} -p requires <host_port>:<container_port>" >&2
1843 exit 1
1844 fi
1845 PORT_FORWARDS+=("${MEMRES_ARGS[$i]}")
1846 ;;
1847 esac
1848 i=$((i + 1))
1849 done
1850
1851 RUNNER_ARGS=$(build_runner_args)
1852
1853 case "$MEMRES_CMD" in
1854 start)
1855 if daemon_is_running; then
1856 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} A memres instance is already running for $TARGET_ARCH"
1857 echo ""
1858 "$RUNNER" $RUNNER_ARGS --daemon-status
1859 echo ""
1860 echo "Options:"
1861 echo " 1) Restart with new settings (stops current instance)"
1862 echo " 2) Start additional instance with different --state-dir"
1863 echo " 3) Cancel"
1864 echo ""
1865 read -p "Choice [1-3]: " choice
1866 case "$choice" in
1867 1)
1868 echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Restarting memres..."
1869 "$RUNNER" $RUNNER_ARGS --daemon-stop
1870 sleep 1
1871 "$RUNNER" $RUNNER_ARGS --daemon-start
1872 ;;
1873 2)
1874 echo ""
1875 echo "To start an additional instance, use -I <name>:"
1876 echo " $VCONTAINER_RUNTIME_NAME -I web memres start -p 8080:80"
1877 echo " $VCONTAINER_RUNTIME_NAME -I api memres start -p 3000:3000"
1878 echo ""
1879 echo "Then interact with it:"
1880 echo " $VCONTAINER_RUNTIME_NAME -I web images"
1881 echo ""
1882 exit 0
1883 ;;
1884 *)
1885 echo "Cancelled."
1886 exit 0
1887 ;;
1888 esac
1889 else
1890 "$RUNNER" $RUNNER_ARGS --daemon-start
1891 fi
1892 ;;
1893 stop)
1894 "$RUNNER" $RUNNER_ARGS --daemon-stop
1895 ;;
1896 restart)
1897 # Stop if running
1898 "$RUNNER" $RUNNER_ARGS --daemon-stop 2>/dev/null || true
1899
1900 # Clean if --clean was passed
1901 for arg in "${COMMAND_ARGS[@]:1}"; do
1902 if [ "$arg" = "--clean" ]; then
1903 CLEAN_DIR="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
1904 if [ -d "$CLEAN_DIR" ]; then
1905 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Cleaning state directory: $CLEAN_DIR"
1906 rm -rf "$CLEAN_DIR"
1907 fi
1908 break
1909 fi
1910 done
1911
1912 # Start
1913 "$RUNNER" $RUNNER_ARGS --daemon-start
1914 ;;
1915 status)
1916 "$RUNNER" $RUNNER_ARGS --daemon-status
1917 ;;
1918 list)
1919 # Show all running memres instances
1920 echo "Running memres instances:"
1921 echo ""
1922 found=0
1923 tracked_pids=""
1924 for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do
1925 [ -f "$pid_file" ] || continue
1926 pid=$(cat "$pid_file" 2>/dev/null)
1927 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
1928 instance_dir=$(dirname "$pid_file")
1929 instance_name=$(basename "$instance_dir")
1930 echo " ${CYAN}$instance_name${NC}"
1931 echo " PID: $pid"
1932 echo " State: $instance_dir"
1933 if [ -f "$instance_dir/qemu.log" ]; then
1934 # Try to extract port forwards from qemu command line
1935 ports=$(grep -o 'hostfwd=[^,]*' "$instance_dir/qemu.log" 2>/dev/null | sed 's/hostfwd=tcp:://g; s/-/:/' | tr '\n' ' ')
1936 [ -n "$ports" ] && echo " Ports: $ports"
1937 fi
1938 echo ""
1939 found=$((found + 1))
1940 tracked_pids="$tracked_pids $pid"
1941 fi
1942 done
1943 if [ $found -eq 0 ]; then
1944 echo " (none)"
1945 fi
1946
1947 # Check for zombie/orphan QEMU processes (vdkr or vpdmn)
1948 echo ""
1949 echo "Checking for orphan QEMU processes..."
1950 zombies=""
1951 for qemu_pid in $(pgrep -f "qemu-system.*runtime=(docker|podman)" 2>/dev/null || true); do
1952 # Skip if this PID is already tracked
1953 if echo "$tracked_pids" | grep -qw "$qemu_pid"; then
1954 continue
1955 fi
1956 # Also check other tool's state dirs
1957 other_tracked=false
1958 for vpid_file in "$OTHER_STATE_DIR"/*/daemon.pid; do
1959 [ -f "$vpid_file" ] || continue
1960 vpid=$(cat "$vpid_file" 2>/dev/null)
1961 if [ "$vpid" = "$qemu_pid" ]; then
1962 other_tracked=true
1963 break
1964 fi
1965 done
1966 if [ "$other_tracked" = "true" ]; then
1967 continue
1968 fi
1969 zombies="$zombies $qemu_pid"
1970 done
1971
1972 if [ -n "$zombies" ]; then
1973 echo ""
1974 echo -e "${YELLOW}Orphan QEMU processes found:${NC}"
1975 for zpid in $zombies; do
1976 # Extract runtime from cmdline
1977 cmdline=$(cat /proc/$zpid/cmdline 2>/dev/null | tr '\0' ' ')
1978 runtime=$(echo "$cmdline" | grep -o 'runtime=[a-z]*' | cut -d= -f2)
1979 state_dir=$(echo "$cmdline" | grep -o 'path=[^,]*daemon.sock' | sed 's|path=||; s|/daemon.sock||')
1980 echo ""
1981 echo " ${RED}PID $zpid${NC} (${runtime:-unknown})"
1982 [ -n "$state_dir" ] && echo " State: $state_dir"
1983 echo " Kill with: kill $zpid"
1984 done
1985 echo ""
1986 echo -e "To kill all orphans: ${CYAN}kill$zombies${NC}"
1987 else
1988 echo " (no orphans found)"
1989 fi
1990 ;;
1991 *)
1992 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown memres subcommand: $MEMRES_CMD" >&2
1993 echo "Usage: $VCONTAINER_RUNTIME_NAME memres start|stop|restart|status|list" >&2
1994 exit 1
1995 ;;
1996 esac
1997 ;;
1998
1999 *)
2000 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Unknown command: $COMMAND" >&2
2001 echo "Run '$VCONTAINER_RUNTIME_NAME --help' for usage" >&2
2002 exit 1
2003 ;;
2004esac
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
new file mode 100755
index 00000000..872508db
--- /dev/null
+++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
@@ -0,0 +1,537 @@
1#!/bin/sh
2# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# vcontainer-init-common.sh
7# Shared init functions for vdkr and vpdmn
8#
9# This file is sourced by vdkr-init.sh and vpdmn-init.sh after they set:
10# VCONTAINER_RUNTIME_NAME - Tool name (vdkr or vpdmn)
11# VCONTAINER_RUNTIME_CMD - Container command (docker or podman)
12# VCONTAINER_RUNTIME_PREFIX - Kernel param prefix (docker or podman)
13# VCONTAINER_STATE_DIR - Storage directory (/var/lib/docker or /var/lib/containers/storage)
14# VCONTAINER_SHARE_NAME - virtio-9p share name (vdkr_share or vpdmn_share)
15# VCONTAINER_VERSION - Version string
16
17# ============================================================================
18# Environment Setup
19# ============================================================================
20
21setup_base_environment() {
22 export LD_LIBRARY_PATH="/lib:/lib64:/usr/lib:/usr/lib64"
23 export PATH="/bin:/sbin:/usr/bin:/usr/sbin"
24 export HOME="/root"
25 export USER="root"
26 export LOGNAME="root"
27}
28
29# ============================================================================
30# Filesystem Mounts
31# ============================================================================
32
33mount_base_filesystems() {
34 # Mount essential filesystems if not already mounted
35 mountpoint -q /dev || mount -t devtmpfs devtmpfs /dev
36 mountpoint -q /proc || mount -t proc proc /proc
37 mountpoint -q /sys || mount -t sysfs sysfs /sys
38
39 # Mount devpts for pseudo-terminals (needed for interactive mode)
40 mkdir -p /dev/pts
41 mountpoint -q /dev/pts || mount -t devpts devpts /dev/pts
42
43 # Enable IP forwarding (container runtimes check this)
44 echo 1 > /proc/sys/net/ipv4/ip_forward
45
46 # Configure loopback interface
47 ip link set lo up
48 ip addr add 127.0.0.1/8 dev lo 2>/dev/null || true
49}
50
51mount_tmpfs_dirs() {
52 # These are tmpfs (rootfs is read-only)
53 mount -t tmpfs tmpfs /tmp
54 mount -t tmpfs tmpfs /run
55 mount -t tmpfs tmpfs /mnt
56 mount -t tmpfs tmpfs /var/run 2>/dev/null || true
57 mount -t tmpfs tmpfs /var/tmp 2>/dev/null || true
58
59 # Create a writable /etc using tmpfs overlay
60 mkdir -p /tmp/etc-overlay
61 cp -a /etc/* /tmp/etc-overlay/ 2>/dev/null || true
62 mount --bind /tmp/etc-overlay /etc
63}
64
65setup_cgroups() {
66 mkdir -p /sys/fs/cgroup
67 mount -t cgroup2 none /sys/fs/cgroup 2>/dev/null || {
68 mount -t tmpfs cgroup /sys/fs/cgroup 2>/dev/null || true
69 for subsys in devices memory cpu,cpuacct blkio net_cls freezer pids; do
70 subsys_dir=$(echo $subsys | cut -d, -f1)
71 mkdir -p /sys/fs/cgroup/$subsys_dir
72 mount -t cgroup -o $subsys cgroup /sys/fs/cgroup/$subsys_dir 2>/dev/null || true
73 done
74 }
75}
76
77# ============================================================================
78# Quiet Boot / Logging
79# ============================================================================
80
81# Check for interactive mode (suppresses boot messages)
82check_quiet_boot() {
83 QUIET_BOOT=0
84 for param in $(cat /proc/cmdline); do
85 case "$param" in
86 ${VCONTAINER_RUNTIME_PREFIX}_interactive=1) QUIET_BOOT=1 ;;
87 esac
88 done
89}
90
91# Logging function - suppresses output in interactive mode
92log() {
93 [ "$QUIET_BOOT" = "0" ] && echo "$@"
94}
95
96# ============================================================================
97# Kernel Command Line Parsing
98# ============================================================================
99
100parse_cmdline() {
101 # Initialize variables with defaults
102 RUNTIME_CMD_B64=""
103 RUNTIME_INPUT="none"
104 RUNTIME_OUTPUT="text"
105 RUNTIME_STATE="none"
106 RUNTIME_NETWORK="0"
107 RUNTIME_INTERACTIVE="0"
108 RUNTIME_DAEMON="0"
109
110 for param in $(cat /proc/cmdline); do
111 case "$param" in
112 ${VCONTAINER_RUNTIME_PREFIX}_cmd=*)
113 RUNTIME_CMD_B64="${param#${VCONTAINER_RUNTIME_PREFIX}_cmd=}"
114 ;;
115 ${VCONTAINER_RUNTIME_PREFIX}_input=*)
116 RUNTIME_INPUT="${param#${VCONTAINER_RUNTIME_PREFIX}_input=}"
117 ;;
118 ${VCONTAINER_RUNTIME_PREFIX}_output=*)
119 RUNTIME_OUTPUT="${param#${VCONTAINER_RUNTIME_PREFIX}_output=}"
120 ;;
121 ${VCONTAINER_RUNTIME_PREFIX}_state=*)
122 RUNTIME_STATE="${param#${VCONTAINER_RUNTIME_PREFIX}_state=}"
123 ;;
124 ${VCONTAINER_RUNTIME_PREFIX}_network=*)
125 RUNTIME_NETWORK="${param#${VCONTAINER_RUNTIME_PREFIX}_network=}"
126 ;;
127 ${VCONTAINER_RUNTIME_PREFIX}_interactive=*)
128 RUNTIME_INTERACTIVE="${param#${VCONTAINER_RUNTIME_PREFIX}_interactive=}"
129 ;;
130 ${VCONTAINER_RUNTIME_PREFIX}_daemon=*)
131 RUNTIME_DAEMON="${param#${VCONTAINER_RUNTIME_PREFIX}_daemon=}"
132 ;;
133 esac
134 done
135
136 # Decode the command (not required for daemon mode)
137 RUNTIME_CMD=""
138 if [ -n "$RUNTIME_CMD_B64" ]; then
139 RUNTIME_CMD=$(echo "$RUNTIME_CMD_B64" | base64 -d 2>/dev/null)
140 fi
141
142 # Require command for non-daemon mode
143 if [ -z "$RUNTIME_CMD" ] && [ "$RUNTIME_DAEMON" != "1" ]; then
144 echo "===ERROR==="
145 echo "No command provided (${VCONTAINER_RUNTIME_PREFIX}_cmd= missing)"
146 sleep 2
147 reboot -f
148 fi
149
150 log "Command: $RUNTIME_CMD"
151 log "Input type: $RUNTIME_INPUT"
152 log "Output type: $RUNTIME_OUTPUT"
153 log "State type: $RUNTIME_STATE"
154}
155
156# ============================================================================
157# Disk Detection
158# ============================================================================
159
160detect_disks() {
161 log "Waiting for block devices..."
162 sleep 2
163
164 log "Block devices:"
165 [ "$QUIET_BOOT" = "0" ] && ls -la /dev/vd* 2>/dev/null || log "No /dev/vd* devices"
166
167 # Determine which disk is input and which is state
168 # Drive layout (rootfs.img is always /dev/vda, mounted by preinit as /):
169 # /dev/vda = rootfs.img (already mounted as /)
170 # /dev/vdb = input (if present)
171 # /dev/vdc = state (if both input and state present)
172 # /dev/vdb = state (if only state, no input)
173
174 INPUT_DISK=""
175 STATE_DISK=""
176
177 if [ "$RUNTIME_INPUT" != "none" ] && [ "$RUNTIME_STATE" = "disk" ]; then
178 # Both present: rootfs=vda, input=vdb, state=vdc
179 INPUT_DISK="/dev/vdb"
180 STATE_DISK="/dev/vdc"
181 elif [ "$RUNTIME_STATE" = "disk" ]; then
182 # Only state: rootfs=vda, state=vdb
183 STATE_DISK="/dev/vdb"
184 elif [ "$RUNTIME_INPUT" != "none" ]; then
185 # Only input: rootfs=vda, input=vdb
186 INPUT_DISK="/dev/vdb"
187 fi
188}
189
190# ============================================================================
191# Input Disk Handling
192# ============================================================================
193
194mount_input_disk() {
195 mkdir -p /mnt/input
196
197 if [ -n "$INPUT_DISK" ] && [ -b "$INPUT_DISK" ]; then
198 log "Mounting input from $INPUT_DISK..."
199 if mount -t ext4 "$INPUT_DISK" /mnt/input 2>&1; then
200 log "SUCCESS: Mounted $INPUT_DISK"
201 log "Input contents:"
202 [ "$QUIET_BOOT" = "0" ] && ls -la /mnt/input/
203 else
204 log "WARNING: Failed to mount $INPUT_DISK, continuing without input"
205 RUNTIME_INPUT="none"
206 fi
207 elif [ "$RUNTIME_INPUT" != "none" ]; then
208 log "WARNING: No input device found, continuing without input"
209 RUNTIME_INPUT="none"
210 fi
211}
212
213# ============================================================================
214# Network Configuration
215# ============================================================================
216
217configure_networking() {
218 if [ "$RUNTIME_NETWORK" = "1" ]; then
219 log "Configuring network..."
220
221 # Find the network interface (usually eth0 or enp0s* with virtio)
222 NET_IFACE=""
223 for iface in eth0 enp0s2 enp0s3 ens3; do
224 if [ -d "/sys/class/net/$iface" ]; then
225 NET_IFACE="$iface"
226 break
227 fi
228 done
229
230 if [ -n "$NET_IFACE" ]; then
231 log "Found network interface: $NET_IFACE"
232
233 # Bring up the interface
234 ip link set "$NET_IFACE" up
235
236 # QEMU slirp provides:
237 # Guest IP: 10.0.2.15/24
238 # Gateway: 10.0.2.2
239 # DNS: 10.0.2.3
240 ip addr add 10.0.2.15/24 dev "$NET_IFACE"
241 ip route add default via 10.0.2.2
242
243 # Configure DNS
244 mkdir -p /etc
245 rm -f /etc/resolv.conf
246 cat > /etc/resolv.conf << 'DNSEOF'
247nameserver 10.0.2.3
248nameserver 8.8.8.8
249nameserver 1.1.1.1
250DNSEOF
251
252 sleep 1
253
254 # Verify connectivity
255 log "Testing network connectivity..."
256 if ping -c 1 -W 3 10.0.2.2 >/dev/null 2>&1; then
257 log " Gateway (10.0.2.2): OK"
258 else
259 log " Gateway (10.0.2.2): FAILED"
260 fi
261
262 if ping -c 1 -W 3 8.8.8.8 >/dev/null 2>&1; then
263 log " External (8.8.8.8): OK"
264 else
265 log " External (8.8.8.8): FAILED (may be filtered)"
266 fi
267
268 log "Network configured: $NET_IFACE (10.0.2.15)"
269 [ "$QUIET_BOOT" = "0" ] && ip addr show "$NET_IFACE"
270 [ "$QUIET_BOOT" = "0" ] && ip route
271 [ "$QUIET_BOOT" = "0" ] && cat /etc/resolv.conf
272 else
273 log "WARNING: No network interface found"
274 [ "$QUIET_BOOT" = "0" ] && ls /sys/class/net/
275 fi
276 else
277 log "Networking: disabled"
278 fi
279}
280
281# ============================================================================
282# Daemon Mode
283# ============================================================================
284
285run_daemon_mode() {
286 log "=== Daemon Mode ==="
287
288 # Find the virtio-serial port for command channel
289 DAEMON_PORT=""
290 for port in /dev/vport0p1 /dev/vport1p1 /dev/vport2p1 /dev/virtio-ports/${VCONTAINER_RUNTIME_NAME} /dev/hvc1; do
291 if [ -c "$port" ]; then
292 DAEMON_PORT="$port"
293 log "Found virtio-serial port: $port"
294 break
295 fi
296 done
297
298 if [ -z "$DAEMON_PORT" ]; then
299 log "ERROR: Could not find virtio-serial port for daemon mode"
300 log "Available devices:"
301 ls -la /dev/hvc* /dev/vport* /dev/virtio-ports/ 2>/dev/null || true
302 sleep 5
303 reboot -f
304 fi
305
306 log "Using virtio-serial port: $DAEMON_PORT"
307
308 # Mount virtio-9p shared directory for file I/O
309 mkdir -p /mnt/share
310 MOUNT_ERR=$(mount -t 9p -o trans=virtio,version=9p2000.L,cache=none ${VCONTAINER_SHARE_NAME} /mnt/share 2>&1)
311 if [ $? -eq 0 ]; then
312 log "Mounted virtio-9p share at /mnt/share"
313 else
314 log "WARNING: Could not mount virtio-9p share: $MOUNT_ERR"
315 log "Available filesystems:"
316 cat /proc/filesystems 2>/dev/null | head -20
317 fi
318
319 # Open bidirectional FD to the virtio-serial port
320 exec 3<>"$DAEMON_PORT"
321
322 log "Daemon ready, waiting for commands..."
323
324 # Command loop
325 while true; do
326 CMD_B64=""
327 if read -r CMD_B64 <&3; then
328 log "Received: '$CMD_B64'"
329 # Handle special commands
330 case "$CMD_B64" in
331 "===PING===")
332 echo "===PONG===" | cat >&3
333 continue
334 ;;
335 "===SHUTDOWN===")
336 log "Received shutdown command"
337 echo "===SHUTTING_DOWN===" | cat >&3
338 break
339 ;;
340 esac
341
342 # Decode command
343 CMD=$(echo "$CMD_B64" | base64 -d 2>/dev/null)
344 if [ -z "$CMD" ]; then
345 printf "===ERROR===\nFailed to decode command\n===END===\n" | cat >&3
346 continue
347 fi
348
349 # Check for interactive command
350 if echo "$CMD" | grep -q "^===INTERACTIVE==="; then
351 CMD="${CMD#===INTERACTIVE===}"
352 log "Interactive command: $CMD"
353
354 printf "===INTERACTIVE_READY===\n" >&3
355
356 export TERM=linux
357 script -qf -c "$CMD" /dev/null <&3 >&3 2>&1
358 INTERACTIVE_EXIT=$?
359
360 sleep 0.5
361 printf "\n===INTERACTIVE_END=%d===\n" "$INTERACTIVE_EXIT" >&3
362
363 log "Interactive command completed (exit: $INTERACTIVE_EXIT)"
364 continue
365 fi
366
367 # Check if command needs input from shared directory
368 NEEDS_INPUT=false
369 if echo "$CMD" | grep -q "^===USE_INPUT==="; then
370 NEEDS_INPUT=true
371 CMD="${CMD#===USE_INPUT===}"
372 log "Command needs input from shared directory"
373 fi
374
375 log "Executing: $CMD"
376
377 # Verify shared directory has content if needed
378 if [ "$NEEDS_INPUT" = "true" ]; then
379 if ! mountpoint -q /mnt/share; then
380 printf "===ERROR===\nvirtio-9p share not mounted\n===END===\n" | cat >&3
381 continue
382 fi
383 if [ -z "$(ls -A /mnt/share 2>/dev/null)" ]; then
384 printf "===ERROR===\nShared directory is empty\n===END===\n" | cat >&3
385 continue
386 fi
387 log "Shared directory contents:"
388 ls -la /mnt/share/ 2>/dev/null || true
389 fi
390
391 # Replace {INPUT} placeholder
392 INPUT_PATH="/mnt/share"
393 CMD=$(echo "$CMD" | sed "s|{INPUT}|$INPUT_PATH|g")
394
395 # Execute command
396 EXEC_OUTPUT="/tmp/daemon_output.txt"
397 EXEC_EXIT_CODE=0
398 eval "$CMD" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$?
399
400 # Clean up shared directory
401 if [ "$NEEDS_INPUT" = "true" ]; then
402 log "Cleaning shared directory..."
403 rm -rf /mnt/share/* 2>/dev/null || true
404 fi
405
406 # Send response
407 {
408 echo "===OUTPUT_START==="
409 cat "$EXEC_OUTPUT"
410 echo "===OUTPUT_END==="
411 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
412 echo "===END==="
413 } | cat >&3
414
415 log "Command completed (exit code: $EXEC_EXIT_CODE)"
416 else
417 sleep 1
418 fi
419 done
420
421 exec 3>&-
422 log "Daemon shutting down..."
423}
424
425# ============================================================================
426# Command Execution (non-daemon mode)
427# ============================================================================
428
429prepare_input_path() {
430 INPUT_PATH=""
431 if [ "$RUNTIME_INPUT" = "oci" ] && [ -d "/mnt/input" ]; then
432 INPUT_PATH="/mnt/input"
433 elif [ "$RUNTIME_INPUT" = "tar" ] && [ -d "/mnt/input" ]; then
434 INPUT_PATH=$(find /mnt/input -name "*.tar" -o -name "*.tar.gz" | head -n 1)
435 [ -z "$INPUT_PATH" ] && INPUT_PATH="/mnt/input"
436 elif [ "$RUNTIME_INPUT" = "dir" ]; then
437 INPUT_PATH="/mnt/input"
438 fi
439 export INPUT_PATH
440}
441
442execute_command() {
443 # Substitute {INPUT} placeholder
444 RUNTIME_CMD_FINAL=$(echo "$RUNTIME_CMD" | sed "s|{INPUT}|$INPUT_PATH|g")
445
446 log "=== Executing ${VCONTAINER_RUNTIME_CMD} Command ==="
447 log "Command: $RUNTIME_CMD_FINAL"
448 log ""
449
450 if [ "$RUNTIME_INTERACTIVE" = "1" ]; then
451 # Interactive mode
452 export TERM=linux
453 printf '\r\033[K'
454 eval "$RUNTIME_CMD_FINAL"
455 EXEC_EXIT_CODE=$?
456 else
457 # Non-interactive mode
458 EXEC_OUTPUT="/tmp/runtime_output.txt"
459 EXEC_EXIT_CODE=0
460 eval "$RUNTIME_CMD_FINAL" > "$EXEC_OUTPUT" 2>&1 || EXEC_EXIT_CODE=$?
461
462 log "Exit code: $EXEC_EXIT_CODE"
463
464 case "$RUNTIME_OUTPUT" in
465 text)
466 echo "===OUTPUT_START==="
467 cat "$EXEC_OUTPUT"
468 echo "===OUTPUT_END==="
469 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
470 ;;
471
472 tar)
473 if [ -f /tmp/output.tar ]; then
474 dmesg -n 1
475 echo "===TAR_START==="
476 base64 /tmp/output.tar
477 echo "===TAR_END==="
478 echo "===EXIT_CODE=$EXEC_EXIT_CODE==="
479 else
480 echo "===ERROR==="
481 echo "Expected /tmp/output.tar but file not found"
482 echo "Command output:"
483 cat "$EXEC_OUTPUT"
484 fi
485 ;;
486
487 storage)
488 # This is handled by runtime-specific code
489 handle_storage_output
490 ;;
491
492 *)
493 echo "===ERROR==="
494 echo "Unknown output type: $RUNTIME_OUTPUT"
495 ;;
496 esac
497 fi
498}
499
500# ============================================================================
501# Graceful Shutdown
502# ============================================================================
503
504graceful_shutdown() {
505 log "=== Shutting down gracefully ==="
506
507 # Runtime-specific cleanup (implemented by sourcing script)
508 if type stop_runtime_daemons >/dev/null 2>&1; then
509 stop_runtime_daemons
510 fi
511
512 sync
513
514 # Unmount state disk if mounted
515 if mount | grep -q "$VCONTAINER_STATE_DIR"; then
516 log "Unmounting state disk..."
517 sync
518 umount "$VCONTAINER_STATE_DIR" || {
519 log "Warning: umount failed, trying lazy unmount"
520 umount -l "$VCONTAINER_STATE_DIR" 2>/dev/null || true
521 }
522 fi
523
524 # Unmount input
525 umount /mnt/input 2>/dev/null || true
526
527 # Final sync and flush
528 sync
529 for dev in /dev/vd*; do
530 [ -b "$dev" ] && blockdev --flushbufs "$dev" 2>/dev/null || true
531 done
532 sync
533 sleep 2
534
535 log "=== ${VCONTAINER_RUNTIME_NAME} Complete ==="
536 poweroff -f
537}
diff --git a/recipes-containers/vcontainer/files/vdkr-preinit.sh b/recipes-containers/vcontainer/files/vdkr-preinit.sh
new file mode 100644
index 00000000..08738022
--- /dev/null
+++ b/recipes-containers/vcontainer/files/vdkr-preinit.sh
@@ -0,0 +1,133 @@
1#!/bin/sh
2# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# vdkr-preinit.sh
7# Minimal init for initramfs - mounts rootfs and does switch_root
8#
9# This script runs from the initramfs and:
10# 1. Mounts essential filesystems
11# 2. Finds and mounts the rootfs.img (squashfs, read-only)
12# 3. Creates overlayfs with tmpfs for writes
13# 4. Executes switch_root to the overlay root filesystem
14#
15# The real init (/init or /sbin/init on rootfs) then runs vdkr-init.sh logic
16
17# Mount essential filesystems first (needed to check cmdline)
18mount -t proc proc /proc
19mount -t sysfs sysfs /sys
20mount -t devtmpfs devtmpfs /dev
21
22# Check for quiet mode (interactive)
23QUIET=0
24for param in $(cat /proc/cmdline 2>/dev/null); do
25 case "$param" in
26 docker_interactive=1) QUIET=1 ;;
27 esac
28done
29
30log() {
31 [ "$QUIET" = "0" ] && echo "$@"
32}
33
34log "=== vdkr preinit (squashfs) ==="
35
36# Wait for block devices to appear
37log "Waiting for block devices..."
38sleep 2
39
40# Show available block devices
41log "Block devices:"
42[ "$QUIET" = "0" ] && ls -la /dev/vd* 2>/dev/null || log "No virtio block devices found"
43
44# The rootfs.img is always the first virtio-blk device (/dev/vda)
45# Additional devices (input, state) come after
46ROOTFS_DEV="/dev/vda"
47
48if [ ! -b "$ROOTFS_DEV" ]; then
49 echo "ERROR: Rootfs device $ROOTFS_DEV not found!"
50 echo "Available devices:"
51 ls -la /dev/
52 sleep 10
53 reboot -f
54fi
55
56# Create mount points for overlay setup
57mkdir -p /mnt/lower # squashfs (read-only)
58mkdir -p /mnt/upper # tmpfs for overlay upper
59mkdir -p /mnt/work # tmpfs for overlay work
60mkdir -p /mnt/root # final overlayfs mount
61
62# Mount squashfs read-only
63log "Mounting squashfs rootfs from $ROOTFS_DEV..."
64
65if ! mount -t squashfs -o ro "$ROOTFS_DEV" /mnt/lower; then
66 # Fallback to ext4 for backwards compatibility
67 log "squashfs mount failed, trying ext4..."
68 if ! mount -t ext4 -o ro "$ROOTFS_DEV" /mnt/lower; then
69 echo "ERROR: Failed to mount rootfs (tried squashfs and ext4)!"
70 sleep 10
71 reboot -f
72 fi
73 # ext4 fallback - just use it directly without overlay
74 log "Using ext4 rootfs directly (no overlay)"
75 mount --move /mnt/lower /mnt/root
76else
77 log "Squashfs mounted successfully"
78
79 # Create tmpfs for overlay upper/work directories
80 # Size is generous since container operations need temp space
81 log "Creating tmpfs overlay..."
82 mount -t tmpfs -o size=1G tmpfs /mnt/upper
83 mkdir -p /mnt/upper/upper
84 mkdir -p /mnt/upper/work
85
86 # Create overlayfs combining squashfs (lower) + tmpfs (upper)
87 log "Mounting overlayfs..."
88 if ! mount -t overlay overlay -o lowerdir=/mnt/lower,upperdir=/mnt/upper/upper,workdir=/mnt/upper/work /mnt/root; then
89 echo "ERROR: Failed to mount overlayfs!"
90 sleep 10
91 reboot -f
92 fi
93
94 log "Overlayfs mounted successfully"
95fi
96
97if [ "$QUIET" = "0" ]; then
98 echo "Contents:"
99 ls -la /mnt/root/
100fi
101
102# Verify init exists on rootfs
103if [ ! -x /mnt/root/init ] && [ ! -x /mnt/root/sbin/init ]; then
104 echo "ERROR: No init found on rootfs!"
105 sleep 10
106 reboot -f
107fi
108
109# Move filesystems to new root before switch_root
110# This way they persist across switch_root and the new init doesn't need to remount
111mkdir -p /mnt/root/proc /mnt/root/sys /mnt/root/dev
112mount --move /proc /mnt/root/proc
113mount --move /sys /mnt/root/sys
114mount --move /dev /mnt/root/dev
115
116# Switch to real root
117# switch_root will:
118# 1. Mount the new root
119# 2. chroot into it
120# 3. Execute the new init
121# 4. Delete everything in the old initramfs
122log "Switching to real root..."
123
124if [ -x /mnt/root/init ]; then
125 exec switch_root /mnt/root /init
126elif [ -x /mnt/root/sbin/init ]; then
127 exec switch_root /mnt/root /sbin/init
128fi
129
130# If we get here, switch_root failed
131echo "ERROR: switch_root failed!"
132sleep 10
133reboot -f
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
new file mode 100755
index 00000000..588261ff
--- /dev/null
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -0,0 +1,1353 @@
1#!/bin/bash
2# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6# vrunner.sh
7# Core runner for vdkr/vpdmn: execute container commands in QEMU-emulated environment
8#
9# This script is runtime-agnostic and supports both Docker and Podman via --runtime.
10#
11# Boot flow:
12# 1. QEMU loads kernel + tiny initramfs (busybox + preinit)
13# 2. preinit mounts rootfs.img (/dev/vda) and does switch_root
14# 3. Real /init runs on actual ext4 filesystem
15# 4. Container runtime starts, executes command, outputs results
16#
17# This two-stage boot is required because runc needs pivot_root,
18# which doesn't work from initramfs (rootfs isn't a mount point).
19#
20# Drive layout:
21# /dev/vda = rootfs.img (ro, ext4 with container tools)
22# /dev/vdb = input disk (optional, user data)
23# /dev/vdc = state disk (optional, persistent container storage)
24#
25# Version: 3.4.0
26
27set -e
28
29VERSION="3.4.0"
30SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
32# Runtime selection: docker or podman
33# This affects blob directory, cmdline prefix, state directory, and log prefix
34RUNTIME="${VRUNNER_RUNTIME:-docker}"
35
36# Configuration
37TARGET_ARCH="${VDKR_ARCH:-${VPDMN_ARCH:-aarch64}}"
38TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}"
39VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}"
40
41# Runtime-specific settings (set after parsing --runtime)
42set_runtime_config() {
43 case "$RUNTIME" in
44 docker)
45 TOOL_NAME="vdkr"
46 BLOB_SUBDIR="vdkr-blobs"
47 BLOB_SUBDIR_ALT="blobs"
48 CMDLINE_PREFIX="docker"
49 STATE_DIR_BASE="${VDKR_STATE_DIR:-$HOME/.vdkr}"
50 STATE_FILE="docker-state.img"
51 ;;
52 podman)
53 TOOL_NAME="vpdmn"
54 BLOB_SUBDIR="vpdmn-blobs"
55 BLOB_SUBDIR_ALT="blobs/vpdmn"
56 CMDLINE_PREFIX="podman"
57 STATE_DIR_BASE="${VPDMN_STATE_DIR:-$HOME/.vpdmn}"
58 STATE_FILE="podman-state.img"
59 ;;
60 *)
61 echo "ERROR: Unknown runtime: $RUNTIME (use docker or podman)" >&2
62 exit 1
63 ;;
64 esac
65}
66
67# Blob locations - relative to script for relocatable installation
68# Determined after runtime is set
69# Note: If BLOB_DIR was set via --blob-dir argument, don't override it
70set_blob_dir() {
71 # Skip if already set by command line argument
72 if [ -n "$BLOB_DIR" ]; then
73 return
74 fi
75 if [ -n "${VDKR_BLOB_DIR:-${VPDMN_BLOB_DIR:-}}" ]; then
76 BLOB_DIR="${VDKR_BLOB_DIR:-${VPDMN_BLOB_DIR}}"
77 elif [ -d "$SCRIPT_DIR/$BLOB_SUBDIR" ]; then
78 BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR"
79 elif [ -d "$SCRIPT_DIR/$BLOB_SUBDIR_ALT" ]; then
80 BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR_ALT"
81 else
82 BLOB_DIR="$SCRIPT_DIR/$BLOB_SUBDIR"
83 fi
84}
85
86# Colors
87RED=$'\033[0;31m'
88GREEN=$'\033[0;32m'
89YELLOW=$'\033[0;33m'
90BLUE=$'\033[0;34m'
91CYAN=$'\033[0;36m'
92NC=$'\033[0m'
93
94log() {
95 local level="$1"
96 local message="$2"
97 local prefix="[${TOOL_NAME:-vdkr}]"
98 case "$level" in
99 "INFO") [ "$VERBOSE" = "true" ] && echo -e "${GREEN}${prefix}${NC} $message" >&2 || true ;;
100 "WARN") echo -e "${YELLOW}${prefix}${NC} $message" >&2 ;;
101 "ERROR") echo -e "${RED}${prefix}${NC} $message" >&2 ;;
102 "DEBUG") [ "$VERBOSE" = "true" ] && echo -e "${BLUE}${prefix}${NC} $message" >&2 || true ;;
103 esac
104}
105
106show_usage() {
107 cat << 'EOF'
108vrunner.sh - Execute docker commands in QEMU-emulated environment
109
110USAGE:
111 vrunner.sh [OPTIONS] -- <docker-command> [args...]
112
113OPTIONS:
114 --arch <arch> Target architecture (aarch64, x86_64) [default: aarch64]
115 --input <path> Input file/directory for docker command (mounted as {INPUT})
116 --input-type <type> Input type: none, oci, tar, dir [default: auto-detect]
117 --input-storage <tar> Restore Docker state from tar before running command
118 --state-dir <path> Use persistent directory for Docker storage between runs
119 --output-type <type> Output type: text, tar, storage [default: text]
120 --output <path> Output file for tar/storage output types
121 --blob-dir <path> Directory containing kernel/initramfs blobs
122 --network, -n Enable networking (slirp user-mode, outbound only)
123 --interactive, -it Run in interactive mode (connects terminal to container)
124 --timeout <secs> QEMU timeout [default: 300]
125 --no-kvm Disable KVM acceleration (use TCG emulation)
126 --batch-import Batch import mode: import multiple OCI containers in one session
127 --keep-temp Keep temporary files for debugging
128 --verbose, -v Enable verbose output
129 --help, -h Show this help
130
131INPUT TYPES:
132 none No input data (docker commands that don't need files)
133 oci OCI container directory (has index.json, blobs/)
134 tar Tar archive (docker save output, etc.)
135 dir Generic directory
136
137OUTPUT TYPES:
138 text Capture command stdout/stderr as text (default)
139 tar Expect command to create /tmp/output.tar, return as file
140 storage Export entire /var/lib/docker as tar
141
142PLACEHOLDERS:
143 {INPUT} Replaced with path to mounted input inside QEMU
144
145EXAMPLES:
146 # List images (no input needed)
147 vrunner.sh -- docker images
148
149 # Load an image from tar
150 vrunner.sh --input myimage.tar -- docker load -i {INPUT}
151
152 # Import an OCI container
153 vrunner.sh --input ./container-oci/ --input-type oci \
154 -- docker import {INPUT}/blobs/sha256/LARGEST myimage:latest
155
156 # Save an image to tar (after loading)
157 vrunner.sh --input myimage.tar --output-type tar \
158 -- 'docker load -i {INPUT} && docker save -o /tmp/output.tar myimage:latest'
159
160 # Get full docker storage after operations
161 vrunner.sh --input myimage.tar --output-type storage --output storage.tar \
162 -- docker load -i {INPUT}
163
164 # Pull an image from a registry (requires --network)
165 vrunner.sh --network -- docker pull alpine:latest
166
167 # Batch import multiple OCI containers in one session
168 vrunner.sh --batch-import --output storage.tar \
169 -- /path/to/app-oci:myapp:latest /path/to/db-oci:mydb:v1.0
170
171 # Batch import with existing storage (additive)
172 vrunner.sh --batch-import --input-storage existing.tar --output merged.tar \
173 -- /path/to/new-oci:newapp:latest
174
175EOF
176}
177
178# Parse arguments
179INPUT_PATH=""
180INPUT_TYPE="none"
181NETWORK="false"
182INTERACTIVE="false"
183INPUT_STORAGE=""
184STATE_DIR=""
185OUTPUT_TYPE="text"
186OUTPUT_FILE=""
187KEEP_TEMP="false"
188DISABLE_KVM="false"
189DOCKER_CMD=""
190PORT_FORWARDS=()
191
192# Batch import mode
193BATCH_IMPORT="false"
194
195# Daemon mode options
196DAEMON_MODE="" # start, send, stop, status
197DAEMON_SOCKET_DIR="" # Directory for daemon socket/PID files
198
199while [ $# -gt 0 ]; do
200 case $1 in
201 --runtime)
202 RUNTIME="$2"
203 shift 2
204 ;;
205 --arch)
206 TARGET_ARCH="$2"
207 shift 2
208 ;;
209 --input)
210 INPUT_PATH="$2"
211 shift 2
212 ;;
213 --input-type)
214 INPUT_TYPE="$2"
215 shift 2
216 ;;
217 --input-storage)
218 INPUT_STORAGE="$2"
219 shift 2
220 ;;
221 --state-dir)
222 STATE_DIR="$2"
223 shift 2
224 ;;
225 --output-type)
226 OUTPUT_TYPE="$2"
227 shift 2
228 ;;
229 --output)
230 OUTPUT_FILE="$2"
231 shift 2
232 ;;
233 --blob-dir)
234 BLOB_DIR="$2"
235 shift 2
236 ;;
237 --timeout)
238 TIMEOUT="$2"
239 shift 2
240 ;;
241 --network|-n)
242 NETWORK="true"
243 shift
244 ;;
245 --port-forward)
246 # Format: host_port:container_port or host_port:container_port/protocol
247 PORT_FORWARDS+=("$2")
248 shift 2
249 ;;
250 --interactive|-it)
251 INTERACTIVE="true"
252 shift
253 ;;
254 --keep-temp)
255 KEEP_TEMP="true"
256 shift
257 ;;
258 --no-kvm)
259 DISABLE_KVM="true"
260 shift
261 ;;
262 --batch-import)
263 BATCH_IMPORT="true"
264 # Force storage output type for batch import
265 OUTPUT_TYPE="storage"
266 shift
267 ;;
268 --daemon-start)
269 DAEMON_MODE="start"
270 shift
271 ;;
272 --daemon-send)
273 DAEMON_MODE="send"
274 shift
275 ;;
276 --daemon-send-input)
277 DAEMON_MODE="send-input"
278 shift
279 ;;
280 --daemon-interactive)
281 DAEMON_MODE="interactive"
282 shift
283 ;;
284 --daemon-stop)
285 DAEMON_MODE="stop"
286 shift
287 ;;
288 --daemon-status)
289 DAEMON_MODE="status"
290 shift
291 ;;
292 --daemon-socket-dir)
293 DAEMON_SOCKET_DIR="$2"
294 shift 2
295 ;;
296 --verbose|-v)
297 VERBOSE="true"
298 shift
299 ;;
300 --help|-h)
301 show_usage
302 exit 0
303 ;;
304 --)
305 shift
306 DOCKER_CMD="$*"
307 break
308 ;;
309 *)
310 # If we hit a non-option, assume rest is docker command
311 DOCKER_CMD="$*"
312 break
313 ;;
314 esac
315done
316
317# Initialize runtime-specific configuration
318set_runtime_config
319set_blob_dir
320
321# Daemon mode handling
322# Set default socket directory based on architecture
323# If --state-dir was provided, use it for daemon files too
324if [ -z "$DAEMON_SOCKET_DIR" ]; then
325 if [ -n "$STATE_DIR" ]; then
326 DAEMON_SOCKET_DIR="$STATE_DIR"
327 else
328 DAEMON_SOCKET_DIR="${STATE_DIR_BASE}/${TARGET_ARCH}"
329 fi
330fi
331DAEMON_PID_FILE="$DAEMON_SOCKET_DIR/daemon.pid"
332DAEMON_SOCKET="$DAEMON_SOCKET_DIR/daemon.sock"
333DAEMON_QEMU_LOG="$DAEMON_SOCKET_DIR/qemu.log"
334DAEMON_INPUT_IMG="$DAEMON_SOCKET_DIR/daemon-input.img"
335DAEMON_INPUT_SIZE_MB=2048 # 2GB input disk for daemon mode
336
337# Daemon helper functions
338daemon_is_running() {
339 if [ -f "$DAEMON_PID_FILE" ]; then
340 local pid=$(cat "$DAEMON_PID_FILE" 2>/dev/null)
341 if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
342 return 0
343 fi
344 fi
345 return 1
346}
347
348daemon_status() {
349 if daemon_is_running; then
350 local pid=$(cat "$DAEMON_PID_FILE")
351 echo "Daemon running (PID: $pid)"
352 echo "Socket: $DAEMON_SOCKET"
353 echo "Architecture: $TARGET_ARCH"
354 return 0
355 else
356 echo "Daemon not running"
357 return 1
358 fi
359}
360
361daemon_stop() {
362 if ! daemon_is_running; then
363 log "WARN" "Daemon is not running"
364 return 0
365 fi
366
367 local pid=$(cat "$DAEMON_PID_FILE")
368 log "INFO" "Stopping daemon (PID: $pid)..."
369
370 # Send shutdown command via socket
371 if [ -S "$DAEMON_SOCKET" ]; then
372 echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true
373 sleep 2
374 fi
375
376 # If still running, kill it
377 if kill -0 "$pid" 2>/dev/null; then
378 log "INFO" "Sending SIGTERM..."
379 kill "$pid" 2>/dev/null || true
380 sleep 2
381 fi
382
383 # Force kill if necessary
384 if kill -0 "$pid" 2>/dev/null; then
385 log "WARN" "Sending SIGKILL..."
386 kill -9 "$pid" 2>/dev/null || true
387 fi
388
389 rm -f "$DAEMON_PID_FILE" "$DAEMON_SOCKET"
390 log "INFO" "Daemon stopped"
391}
392
393daemon_send() {
394 local cmd="$1"
395
396 if ! daemon_is_running; then
397 log "ERROR" "Daemon is not running. Start it with --daemon-start"
398 exit 1
399 fi
400
401 if [ ! -S "$DAEMON_SOCKET" ]; then
402 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET"
403 exit 1
404 fi
405
406 # Encode command in base64 and send
407 local cmd_b64=$(echo -n "$cmd" | base64 -w0)
408
409 # Send command and read response using coproc
410 # This allows us to kill socat when we're done reading
411 coproc SOCAT { socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null; }
412
413 local EXIT_CODE=0
414 local in_output=false
415 local TIMEOUT=60
416
417 # Send command to socat's stdin
418 echo "$cmd_b64" >&${SOCAT[1]}
419
420 # Read response from socat's stdout with timeout
421 while IFS= read -t $TIMEOUT -r line <&${SOCAT[0]}; do
422 case "$line" in
423 "===OUTPUT_START===")
424 in_output=true
425 ;;
426 "===OUTPUT_END===")
427 in_output=false
428 ;;
429 "===EXIT_CODE="*"===")
430 EXIT_CODE="${line#===EXIT_CODE=}"
431 EXIT_CODE="${EXIT_CODE%===}"
432 ;;
433 "===END===")
434 break
435 ;;
436 *)
437 if [ "$in_output" = "true" ]; then
438 echo "$line"
439 fi
440 ;;
441 esac
442 done
443
444 # Clean up - close FDs and kill socat
445 exec {SOCAT[0]}<&- {SOCAT[1]}>&-
446 kill $SOCAT_PID 2>/dev/null || true
447 wait $SOCAT_PID 2>/dev/null || true
448
449 return ${EXIT_CODE:-0}
450}
451
452# Copy input data to shared directory and send command
453daemon_send_with_input() {
454 local input_path="$1"
455 local input_type="$2"
456 local cmd="$3"
457
458 if ! daemon_is_running; then
459 log "ERROR" "Daemon is not running. Start it with --daemon-start"
460 exit 1
461 fi
462
463 # Shared directory for virtio-9p
464 local share_dir="$DAEMON_SOCKET_DIR/share"
465 if [ ! -d "$share_dir" ]; then
466 log "ERROR" "Daemon share directory not found: $share_dir"
467 exit 1
468 fi
469
470 # Clear and populate shared directory
471 log "INFO" "Copying input to shared directory..."
472 rm -rf "$share_dir"/*
473
474 if [ -d "$input_path" ]; then
475 # Directory - copy contents (use -L to dereference symlinks)
476 cp -rL "$input_path"/* "$share_dir/" 2>/dev/null || cp -r "$input_path"/* "$share_dir/"
477 else
478 # Single file - copy it
479 cp "$input_path" "$share_dir/"
480 fi
481
482 # Sync to ensure data is visible to guest
483 sync
484
485 # Mark command as needing input (prefix with special marker)
486 # Guest reads from /mnt/share (virtio-9p mount)
487 local full_cmd="===USE_INPUT===$cmd"
488
489 # Send command via daemon_send
490 daemon_send "$full_cmd"
491}
492
493# Run interactive command through daemon (for run -it, exec -it)
494daemon_interactive() {
495 local cmd="$1"
496
497 if ! daemon_is_running; then
498 log "ERROR" "Daemon is not running"
499 return 1
500 fi
501
502 if [ ! -S "$DAEMON_SOCKET" ]; then
503 log "ERROR" "Daemon socket not found: $DAEMON_SOCKET"
504 return 1
505 fi
506
507 # Encode command with interactive prefix
508 local cmd_b64=$(echo -n "===INTERACTIVE===$cmd" | base64 -w0)
509
510 # Use expect to handle sending command then going interactive
511 # expect properly handles PTY creation and signal passthrough
512 if command -v expect >/dev/null 2>&1; then
513 # Disable terminal signal generation so Ctrl+C becomes byte 0x03
514 local saved_stty=""
515 if [ -t 0 ]; then
516 saved_stty=$(stty -g)
517 stty -isig
518 fi
519
520 expect -c "
521 log_user 0
522 set timeout -1
523 spawn socat -,rawer UNIX-CONNECT:$DAEMON_SOCKET
524 send \"$cmd_b64\r\"
525 # Wait for READY signal before showing output
526 expect \"===INTERACTIVE_READY===\" {}
527 log_user 1
528 # Interactive mode - exit on END marker or EOF
529 interact {
530 -o \"===INTERACTIVE_END\" {
531 return
532 }
533 eof {
534 return
535 }
536 }
537 " 2>/dev/null
538 local rc=$?
539
540 # Restore terminal
541 if [ -n "$saved_stty" ]; then
542 stty "$saved_stty"
543 fi
544 return $rc
545 fi
546
547 # Fallback: no expect available, use basic approach (Ctrl+C won't work well)
548 log "WARN" "expect not found, interactive mode may have issues with Ctrl+C"
549 {
550 echo "$cmd_b64"
551 cat
552 } | socat - "UNIX-CONNECT:$DAEMON_SOCKET"
553 return $?
554}
555
556# Handle daemon modes that don't need a docker command
557if [ "$DAEMON_MODE" = "status" ]; then
558 daemon_status
559 exit $?
560fi
561
562if [ "$DAEMON_MODE" = "stop" ]; then
563 daemon_stop
564 exit $?
565fi
566
567if [ "$DAEMON_MODE" = "send" ]; then
568 if [ -z "$DOCKER_CMD" ]; then
569 log "ERROR" "No command specified for --daemon-send"
570 exit 1
571 fi
572 daemon_send "$DOCKER_CMD"
573 exit $?
574fi
575
576if [ "$DAEMON_MODE" = "send-input" ]; then
577 if [ -z "$DOCKER_CMD" ]; then
578 log "ERROR" "No command specified for --daemon-send-input"
579 exit 1
580 fi
581 if [ -z "$INPUT_PATH" ]; then
582 log "ERROR" "No input specified for --daemon-send-input (use --input)"
583 exit 1
584 fi
585 daemon_send_with_input "$INPUT_PATH" "$INPUT_TYPE" "$DOCKER_CMD"
586 exit $?
587fi
588
589if [ "$DAEMON_MODE" = "interactive" ]; then
590 if [ -z "$DOCKER_CMD" ]; then
591 log "ERROR" "No command specified for --daemon-interactive"
592 exit 1
593 fi
594 daemon_interactive "$DOCKER_CMD"
595 exit $?
596fi
597
598# For non-daemon mode, require docker command (unless batch import)
599if [ -z "$DOCKER_CMD" ] && [ "$DAEMON_MODE" != "start" ] && [ "$BATCH_IMPORT" != "true" ]; then
600 log "ERROR" "No docker command specified"
601 echo ""
602 show_usage
603 exit 1
604fi
605
606# Create temp directory early (needed for batch import and other operations)
607TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$"
608mkdir -p "$TEMP_DIR"
609
610cleanup() {
611 if [ "$KEEP_TEMP" = "true" ]; then
612 log "DEBUG" "Keeping temp directory: $TEMP_DIR"
613 else
614 rm -rf "$TEMP_DIR" 2>/dev/null || true
615 fi
616}
617trap cleanup EXIT INT TERM
618
619# Batch import mode: parse container list and build compound command
620# Format: path:image:tag path:image:tag ...
621if [ "$BATCH_IMPORT" = "true" ]; then
622 if [ -z "$DOCKER_CMD" ]; then
623 log "ERROR" "Batch import requires container list: path:image:tag ..."
624 exit 1
625 fi
626
627 log "INFO" "Batch import mode enabled"
628
629 # Parse container entries
630 BATCH_ENTRIES=()
631 BATCH_PATHS=()
632 BATCH_IMAGES=()
633
634 for entry in $DOCKER_CMD; do
635 # Parse path:image:tag
636 # Handle colons carefully - path might have colons in edge cases
637 # Format is: /path/to/oci:imagename:tag
638 path="${entry%%:*}"
639 rest="${entry#*:}"
640 image="${rest%%:*}"
641 tag="${rest#*:}"
642
643 if [ -z "$path" ] || [ -z "$image" ] || [ -z "$tag" ]; then
644 log "ERROR" "Invalid batch entry: $entry (expected path:image:tag)"
645 exit 1
646 fi
647
648 if [ ! -d "$path" ]; then
649 log "ERROR" "OCI directory not found: $path"
650 exit 1
651 fi
652
653 BATCH_ENTRIES+=("$entry")
654 BATCH_PATHS+=("$path")
655 BATCH_IMAGES+=("$image:$tag")
656 log "DEBUG" "Batch entry: $path -> $image:$tag"
657 done
658
659 log "INFO" "Processing ${#BATCH_ENTRIES[@]} containers"
660
661 # Create combined input disk with numbered subdirectories
662 # /0/ = first OCI dir, /1/ = second, etc.
663 BATCH_INPUT_DIR="$TEMP_DIR/batch-input"
664 mkdir -p "$BATCH_INPUT_DIR"
665
666 for i in "${!BATCH_PATHS[@]}"; do
667 src="${BATCH_PATHS[$i]}"
668 dest="$BATCH_INPUT_DIR/$i"
669 log "DEBUG" "Copying $src -> $dest"
670 # Use cp -rL to dereference symlinks (OCI containers often use hardlinks)
671 cp -rL "$src" "$dest"
672 done
673
674 # Override INPUT_PATH to point to combined directory
675 INPUT_PATH="$BATCH_INPUT_DIR"
676 INPUT_TYPE="dir"
677
678 # Build compound skopeo command
679 # Each container: skopeo copy oci:/mnt/input/N docker-daemon:image:tag
680 # Note: VM init script mounts input disk at /mnt/input (see mount_input_disk)
681 COMPOUND_CMD=""
682 for i in "${!BATCH_IMAGES[@]}"; do
683 img="${BATCH_IMAGES[$i]}"
684 if [ "$RUNTIME" = "docker" ]; then
685 CMD="skopeo copy oci:/mnt/input/$i docker-daemon:$img"
686 else
687 CMD="skopeo copy oci:/mnt/input/$i containers-storage:$img"
688 fi
689
690 if [ -z "$COMPOUND_CMD" ]; then
691 COMPOUND_CMD="$CMD"
692 else
693 COMPOUND_CMD="$COMPOUND_CMD && $CMD"
694 fi
695 done
696
697 # Add final images command to show what was imported
698 if [ "$RUNTIME" = "docker" ]; then
699 COMPOUND_CMD="$COMPOUND_CMD && docker images"
700 else
701 COMPOUND_CMD="$COMPOUND_CMD && podman images"
702 fi
703
704 log "DEBUG" "Batch command: $COMPOUND_CMD"
705 DOCKER_CMD="$COMPOUND_CMD"
706fi
707
708# Auto-detect input type if input provided but type not specified
709if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" = "none" ]; then
710 if [ -d "$INPUT_PATH" ]; then
711 if [ -f "$INPUT_PATH/index.json" ] || [ -f "$INPUT_PATH/oci-layout" ]; then
712 INPUT_TYPE="oci"
713 else
714 INPUT_TYPE="dir"
715 fi
716 elif [ -f "$INPUT_PATH" ]; then
717 INPUT_TYPE="tar"
718 fi
719 log "DEBUG" "Auto-detected input type: $INPUT_TYPE"
720fi
721
722# Validate output file for types that need it
723if [ "$OUTPUT_TYPE" = "tar" ] || [ "$OUTPUT_TYPE" = "storage" ]; then
724 if [ -z "$OUTPUT_FILE" ]; then
725 OUTPUT_FILE="/tmp/vdkr-output-$$.tar"
726 log "WARN" "No --output specified, using: $OUTPUT_FILE"
727 fi
728fi
729
730log "INFO" "vdkr-run v$VERSION"
731log "INFO" "Architecture: $TARGET_ARCH"
732log "INFO" "Docker command: $DOCKER_CMD"
733[ -n "$INPUT_PATH" ] && log "INFO" "Input: $INPUT_PATH ($INPUT_TYPE)"
734[ -n "$INPUT_STORAGE" ] && log "INFO" "Input storage: $INPUT_STORAGE"
735[ -n "$STATE_DIR" ] && log "INFO" "State directory: $STATE_DIR"
736log "INFO" "Output type: $OUTPUT_TYPE"
737[ -n "$OUTPUT_FILE" ] && log "INFO" "Output file: $OUTPUT_FILE"
738[ "$NETWORK" = "true" ] && log "INFO" "Networking: enabled (slirp)"
739[ "$INTERACTIVE" = "true" ] && log "INFO" "Interactive mode: enabled"
740
741# Find kernel, initramfs, and rootfs
742case "$TARGET_ARCH" in
743 aarch64)
744 KERNEL_IMAGE="$BLOB_DIR/aarch64/Image"
745 INITRAMFS="$BLOB_DIR/aarch64/initramfs.cpio.gz"
746 ROOTFS_IMG="$BLOB_DIR/aarch64/rootfs.img"
747 QEMU_CMD="qemu-system-aarch64"
748 QEMU_MACHINE="-M virt -cpu cortex-a57"
749 CONSOLE="ttyAMA0"
750 ;;
751 x86_64)
752 KERNEL_IMAGE="$BLOB_DIR/x86_64/bzImage"
753 INITRAMFS="$BLOB_DIR/x86_64/initramfs.cpio.gz"
754 ROOTFS_IMG="$BLOB_DIR/x86_64/rootfs.img"
755 QEMU_CMD="qemu-system-x86_64"
756 # Use q35 + Skylake-Client to match oe-core qemux86-64 machine
757 QEMU_MACHINE="-M q35 -cpu Skylake-Client"
758 CONSOLE="ttyS0"
759 ;;
760 *)
761 log "ERROR" "Unsupported architecture: $TARGET_ARCH"
762 exit 1
763 ;;
764esac
765
766# Check for kernel
767if [ ! -f "$KERNEL_IMAGE" ]; then
768 log "ERROR" "Kernel not found: $KERNEL_IMAGE"
769 log "ERROR" "Set VDKR_BLOB_DIR or --blob-dir to location of vdkr blobs"
770 log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-build"
771 exit 1
772fi
773
774# Check for initramfs
775if [ ! -f "$INITRAMFS" ]; then
776 log "ERROR" "Initramfs not found: $INITRAMFS"
777 log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-build"
778 exit 1
779fi
780
781# Check for rootfs image (ext4 with Docker tools)
782if [ ! -f "$ROOTFS_IMG" ]; then
783 log "ERROR" "Rootfs image not found: $ROOTFS_IMG"
784 log "ERROR" "Build with: MACHINE=qemuarm64 bitbake vdkr-initramfs-create"
785 exit 1
786fi
787
788# Find QEMU - check PATH and common locations
789if ! command -v "$QEMU_CMD" >/dev/null 2>&1; then
790 # Try common locations
791 for path in \
792 "${STAGING_BINDIR_NATIVE:-}" \
793 "/usr/bin"; do
794 if [ -n "$path" ] && [ -x "$path/$QEMU_CMD" ]; then
795 QEMU_CMD="$path/$QEMU_CMD"
796 break
797 fi
798 done
799fi
800
801if ! command -v "$QEMU_CMD" >/dev/null 2>&1 && [ ! -x "$QEMU_CMD" ]; then
802 log "ERROR" "QEMU not found: $QEMU_CMD"
803 exit 1
804fi
805
806log "DEBUG" "Using QEMU: $QEMU_CMD"
807
808# Check for KVM acceleration (when host matches target)
809USE_KVM="false"
810if [ "$DISABLE_KVM" = "true" ]; then
811 log "DEBUG" "KVM disabled by --no-kvm flag"
812else
813 HOST_ARCH=$(uname -m)
814 if [ "$HOST_ARCH" = "$TARGET_ARCH" ] || \
815 { [ "$HOST_ARCH" = "x86_64" ] && [ "$TARGET_ARCH" = "x86_64" ]; }; then
816 if [ -w /dev/kvm ]; then
817 USE_KVM="true"
818 # Use host CPU for best performance with KVM
819 case "$TARGET_ARCH" in
820 x86_64)
821 QEMU_MACHINE="-M q35 -cpu host"
822 ;;
823 aarch64)
824 QEMU_MACHINE="-M virt -cpu host"
825 ;;
826 esac
827 log "INFO" "KVM acceleration enabled"
828 else
829 log "DEBUG" "KVM not available (no write access to /dev/kvm)"
830 fi
831 fi
832fi
833
834log "DEBUG" "Using initramfs: $INITRAMFS"
835
836# Create input disk image if needed
837DISK_OPTS=""
838if [ -n "$INPUT_PATH" ] && [ "$INPUT_TYPE" != "none" ]; then
839 log "INFO" "Creating input disk image..."
840 INPUT_IMG="$TEMP_DIR/input.img"
841
842 # Calculate size (use -L to dereference hardlinks in OCI containers)
843 if [ -d "$INPUT_PATH" ]; then
844 SIZE_KB=$(du -skL "$INPUT_PATH" | cut -f1)
845 else
846 SIZE_KB=$(($(stat -c%s "$INPUT_PATH") / 1024))
847 fi
848 SIZE_MB=$(( (SIZE_KB / 1024) + 20 ))
849 [ $SIZE_MB -lt 20 ] && SIZE_MB=20
850
851 log "DEBUG" "Input size: ${SIZE_KB}KB, Image size: ${SIZE_MB}MB"
852
853 dd if=/dev/zero of="$INPUT_IMG" bs=1M count=$SIZE_MB 2>/dev/null
854
855 if [ -d "$INPUT_PATH" ]; then
856 mke2fs -t ext4 -d "$INPUT_PATH" "$INPUT_IMG" 2>/dev/null
857 else
858 # Single file - create temp dir with the file
859 EXTRACT_DIR="$TEMP_DIR/input-extract"
860 mkdir -p "$EXTRACT_DIR"
861 cp "$INPUT_PATH" "$EXTRACT_DIR/"
862 mke2fs -t ext4 -d "$EXTRACT_DIR" "$INPUT_IMG" 2>/dev/null
863 fi
864
865 DISK_OPTS="-drive file=$INPUT_IMG,if=virtio,format=raw"
866 log "DEBUG" "Input disk: $(ls -lh "$INPUT_IMG" | awk '{print $5}')"
867fi
868
869# Create state disk for persistent storage (--state-dir)
870STATE_DISK_OPTS=""
871if [ -n "$STATE_DIR" ]; then
872 mkdir -p "$STATE_DIR"
873 STATE_IMG="$STATE_DIR/$STATE_FILE"
874
875 if [ ! -f "$STATE_IMG" ]; then
876 log "INFO" "Creating new state disk at $STATE_IMG..."
877 # Create 2GB state disk for Docker storage
878 dd if=/dev/zero of="$STATE_IMG" bs=1M count=2048 2>/dev/null
879 mke2fs -t ext4 "$STATE_IMG" 2>/dev/null
880 else
881 log "INFO" "Using existing state disk: $STATE_IMG"
882 fi
883
884 # Use cache=directsync to ensure writes are flushed to disk
885 # Combined with graceful shutdown wait, this ensures data integrity
886 STATE_DISK_OPTS="-drive file=$STATE_IMG,if=virtio,format=raw,cache=directsync"
887 log "DEBUG" "State disk: $(ls -lh "$STATE_IMG" | awk '{print $5}')"
888fi
889
890# Create state disk from input-storage tar (--input-storage)
891if [ -n "$INPUT_STORAGE" ] && [ -z "$STATE_DIR" ]; then
892 if [ ! -f "$INPUT_STORAGE" ]; then
893 log "ERROR" "Input storage file not found: $INPUT_STORAGE"
894 exit 1
895 fi
896
897 log "INFO" "Creating state disk from $INPUT_STORAGE..."
898 STATE_IMG="$TEMP_DIR/state.img"
899
900 # Calculate size from tar + headroom
901 TAR_SIZE_KB=$(($(stat -c%s "$INPUT_STORAGE") / 1024))
902 STATE_SIZE_MB=$(( (TAR_SIZE_KB / 1024) * 2 + 500 )) # 2x tar size + 500MB headroom
903 [ $STATE_SIZE_MB -lt 500 ] && STATE_SIZE_MB=500
904
905 log "DEBUG" "Tar size: ${TAR_SIZE_KB}KB, State disk: ${STATE_SIZE_MB}MB"
906
907 dd if=/dev/zero of="$STATE_IMG" bs=1M count=$STATE_SIZE_MB 2>/dev/null
908 mke2fs -t ext4 "$STATE_IMG" 2>/dev/null
909
910 # Mount and extract tar
911 MOUNT_DIR="$TEMP_DIR/state-mount"
912 mkdir -p "$MOUNT_DIR"
913
914 # Use fuse2fs if available, otherwise need root
915 # Note: We exclude special device files that can't be created without root
916 # Docker's backingFsBlockDev is a block device that gets recreated at runtime anyway
917 # IMPORTANT: The tar has paths like docker/image/... but the state disk is mounted
918 # at /var/lib/docker, so we need to strip the docker/ prefix with --strip-components=1
919 if command -v fuse2fs >/dev/null 2>&1; then
920 fuse2fs "$STATE_IMG" "$MOUNT_DIR" -o rw
921 tar --no-same-owner --strip-components=1 --exclude=volumes/backingFsBlockDev -xf "$INPUT_STORAGE" -C "$MOUNT_DIR"
922 fusermount -u "$MOUNT_DIR"
923 else
924 log "WARN" "fuse2fs not found, using debugfs to inject tar (slower)"
925 # Extract tar to temp, then use mke2fs -d
926 # Use --no-same-owner since we're not root (ownership set to current user)
927 EXTRACT_DIR="$TEMP_DIR/state-extract"
928 mkdir -p "$EXTRACT_DIR"
929 tar --no-same-owner --strip-components=1 --exclude=volumes/backingFsBlockDev -xf "$INPUT_STORAGE" -C "$EXTRACT_DIR"
930 mke2fs -t ext4 -d "$EXTRACT_DIR" "$STATE_IMG" 2>/dev/null
931 fi
932
933 # Use cache=directsync to ensure writes are flushed to disk
934 STATE_DISK_OPTS="-drive file=$STATE_IMG,if=virtio,format=raw,cache=directsync"
935 log "DEBUG" "State disk: $(ls -lh "$STATE_IMG" | awk '{print $5}')"
936fi
937
938# Encode command as base64
939DOCKER_CMD_B64=$(echo -n "$DOCKER_CMD" | base64 -w0)
940
941# Build kernel command line
942# In interactive mode, use 'quiet' to suppress kernel boot messages
943# Use CMDLINE_PREFIX for runtime-specific parameters (docker_ or podman_)
944if [ "$INTERACTIVE" = "true" ]; then
945 KERNEL_APPEND="console=$CONSOLE,115200 quiet loglevel=0 init=/init"
946else
947 KERNEL_APPEND="console=$CONSOLE,115200 init=/init"
948fi
949# Tell init script which runtime we're using
950KERNEL_APPEND="$KERNEL_APPEND runtime=$RUNTIME"
951KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_cmd=$DOCKER_CMD_B64"
952KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_input=$INPUT_TYPE"
953KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_output=$OUTPUT_TYPE"
954
955# Tell init script if we have a state disk
956if [ -n "$STATE_DISK_OPTS" ]; then
957 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_state=disk"
958fi
959
960# Tell init script if networking is enabled
961if [ "$NETWORK" = "true" ]; then
962 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_network=1"
963fi
964
965# Tell init script if interactive mode
966if [ "$INTERACTIVE" = "true" ]; then
967 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_interactive=1"
968fi
969
970# Build QEMU command
971# Drive ordering is important:
972# /dev/vda = rootfs.img (read-only, ext4 with Docker tools)
973# /dev/vdb = input disk (if any)
974# /dev/vdc = state disk (if any)
975# The preinit script in initramfs mounts /dev/vda and does switch_root
976# Build QEMU options
977QEMU_OPTS="$QEMU_MACHINE -nographic -smp 2 -m 2048"
978if [ "$USE_KVM" = "true" ]; then
979 QEMU_OPTS="$QEMU_OPTS -enable-kvm"
980fi
981QEMU_OPTS="$QEMU_OPTS -kernel $KERNEL_IMAGE"
982QEMU_OPTS="$QEMU_OPTS -initrd $INITRAMFS"
983QEMU_OPTS="$QEMU_OPTS -drive file=$ROOTFS_IMG,if=virtio,format=raw,readonly=on"
984QEMU_OPTS="$QEMU_OPTS $DISK_OPTS"
985QEMU_OPTS="$QEMU_OPTS $STATE_DISK_OPTS"
986
987# Add networking if enabled (slirp user-mode networking)
988if [ "$NETWORK" = "true" ]; then
989 # Slirp provides NAT'd outbound connectivity without root privileges
990 # Guest gets 10.0.2.15, gateway is 10.0.2.2, DNS is 10.0.2.3
991 NETDEV_OPTS="user,id=net0"
992
993 # Add port forwards (hostfwd=tcp::host_port-:container_port)
994 for pf in "${PORT_FORWARDS[@]}"; do
995 # Parse host_port:container_port or host_port:container_port/protocol
996 HOST_PORT="${pf%%:*}"
997 CONTAINER_PART="${pf#*:}"
998 CONTAINER_PORT="${CONTAINER_PART%%/*}"
999
1000 # Check for protocol suffix (default to tcp)
1001 if [[ "$CONTAINER_PART" == */* ]]; then
1002 PROTOCOL="${CONTAINER_PART##*/}"
1003 else
1004 PROTOCOL="tcp"
1005 fi
1006
1007 NETDEV_OPTS="$NETDEV_OPTS,hostfwd=$PROTOCOL::$HOST_PORT-:$CONTAINER_PORT"
1008 log "INFO" "Port forward: $HOST_PORT -> $CONTAINER_PORT ($PROTOCOL)"
1009 done
1010
1011 QEMU_OPTS="$QEMU_OPTS -netdev $NETDEV_OPTS -device virtio-net-pci,netdev=net0"
1012else
1013 # Explicitly disable networking
1014 QEMU_OPTS="$QEMU_OPTS -nic none"
1015fi
1016
1017# Daemon mode: add virtio-serial for command channel
1018if [ "$DAEMON_MODE" = "start" ]; then
1019 # Check for required tools
1020 if ! command -v socat >/dev/null 2>&1; then
1021 log "ERROR" "Daemon mode requires 'socat' but it is not installed."
1022 log "ERROR" "Install with: sudo apt install socat"
1023 exit 1
1024 fi
1025
1026 # Check if daemon is already running
1027 if daemon_is_running; then
1028 log "ERROR" "Daemon is already running. Use --daemon-stop first."
1029 exit 1
1030 fi
1031
1032 # Create socket directory
1033 mkdir -p "$DAEMON_SOCKET_DIR"
1034
1035 # Create shared directory for file I/O (virtio-9p)
1036 DAEMON_SHARE_DIR="$DAEMON_SOCKET_DIR/share"
1037 mkdir -p "$DAEMON_SHARE_DIR"
1038
1039 # Add virtio-9p for shared directory access
1040 # Host writes to $DAEMON_SHARE_DIR, guest mounts as /mnt/share
1041 # Use runtime-specific mount tag (vdkr_share or vpdmn_share)
1042 SHARE_TAG="${TOOL_NAME}_share"
1043 # Use security_model=none for simplest file sharing (no permission mapping)
1044 # This allows writes from container (running as root) to propagate to host
1045 QEMU_OPTS="$QEMU_OPTS -virtfs local,path=$DAEMON_SHARE_DIR,mount_tag=$SHARE_TAG,security_model=none,id=$SHARE_TAG"
1046
1047 # Add virtio-serial device for command channel
1048 # Using virtserialport creates /dev/vport0p1 in guest, host sees unix socket
1049 # virtconsole would use hvc* but requires virtio_console kernel module
1050 QEMU_OPTS="$QEMU_OPTS -chardev socket,id=vdkr,path=$DAEMON_SOCKET,server=on,wait=off"
1051 QEMU_OPTS="$QEMU_OPTS -device virtio-serial-pci"
1052 QEMU_OPTS="$QEMU_OPTS -device virtserialport,chardev=vdkr,name=vdkr"
1053
1054 # Tell init script to run in daemon mode
1055 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_daemon=1"
1056
1057 # Always enable networking for daemon mode
1058 if [ "$NETWORK" != "true" ]; then
1059 log "INFO" "Enabling networking for daemon mode"
1060 NETWORK="true"
1061 # Build netdev options with any port forwards
1062 DAEMON_NETDEV="user,id=net0"
1063 for pf in "${PORT_FORWARDS[@]}"; do
1064 # Parse host_port:container_port or host_port:container_port/protocol
1065 HOST_PORT="${pf%%:*}"
1066 CONTAINER_PART="${pf#*:}"
1067 CONTAINER_PORT="${CONTAINER_PART%%/*}"
1068 if [[ "$CONTAINER_PART" == */* ]]; then
1069 PROTOCOL="${CONTAINER_PART##*/}"
1070 else
1071 PROTOCOL="tcp"
1072 fi
1073 DAEMON_NETDEV="$DAEMON_NETDEV,hostfwd=$PROTOCOL::$HOST_PORT-:$CONTAINER_PORT"
1074 log "INFO" "Port forward: $HOST_PORT -> $CONTAINER_PORT ($PROTOCOL)"
1075 done
1076 QEMU_OPTS="$QEMU_OPTS -netdev $DAEMON_NETDEV -device virtio-net-pci,netdev=net0"
1077 else
1078 # NETWORK was already true, but check if we need to add port forwards
1079 # that weren't included in the earlier networking setup
1080 # (This happens when NETWORK was set to true before daemon mode was detected)
1081 if [ ${#PORT_FORWARDS[@]} -gt 0 ]; then
1082 # Port forwards should already be included from earlier networking setup
1083 for pf in "${PORT_FORWARDS[@]}"; do
1084 HOST_PORT="${pf%%:*}"
1085 CONTAINER_PART="${pf#*:}"
1086 CONTAINER_PORT="${CONTAINER_PART%%/*}"
1087 log "INFO" "Port forward configured: $HOST_PORT -> $CONTAINER_PORT"
1088 done
1089 fi
1090 fi
1091
1092 log "INFO" "Starting daemon..."
1093 log "DEBUG" "PID file: $DAEMON_PID_FILE"
1094 log "DEBUG" "Socket: $DAEMON_SOCKET"
1095 log "DEBUG" "Command: $QEMU_CMD $QEMU_OPTS -append \"$KERNEL_APPEND\""
1096
1097 # Start QEMU in background
1098 $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND" > "$DAEMON_QEMU_LOG" 2>&1 &
1099 QEMU_PID=$!
1100 echo "$QEMU_PID" > "$DAEMON_PID_FILE"
1101
1102 log "INFO" "QEMU started (PID: $QEMU_PID)"
1103
1104 # Wait for socket to appear (Docker starting)
1105 # Docker can take 60+ seconds to start, so wait up to 120 seconds
1106 log "INFO" "Waiting for daemon to be ready..."
1107 READY=false
1108 for i in $(seq 1 120); do
1109 if [ -S "$DAEMON_SOCKET" ]; then
1110 # Socket exists, try to connect
1111 # Keep stdin open for 3 seconds to allow response to arrive
1112 RESPONSE=$( { echo "===PING==="; sleep 3; } | timeout 10 socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true)
1113 if echo "$RESPONSE" | grep -q "===PONG==="; then
1114 log "DEBUG" "Got PONG response"
1115 READY=true
1116 break
1117 else
1118 log "DEBUG" "No PONG, got: $RESPONSE"
1119 fi
1120 fi
1121
1122 # Check if QEMU died
1123 if ! kill -0 "$QEMU_PID" 2>/dev/null; then
1124 log "ERROR" "QEMU process died during startup"
1125 cat "$DAEMON_QEMU_LOG" >&2
1126 rm -f "$DAEMON_PID_FILE"
1127 exit 1
1128 fi
1129
1130 log "DEBUG" "Waiting... ($i/60)"
1131 sleep 1
1132 done
1133
1134 if [ "$READY" = "true" ]; then
1135 log "INFO" "Daemon is ready!"
1136 echo "Daemon running (PID: $QEMU_PID)"
1137 echo "Socket: $DAEMON_SOCKET"
1138 exit 0
1139 else
1140 log "ERROR" "Daemon failed to become ready within 120 seconds"
1141 cat "$DAEMON_QEMU_LOG" >&2
1142 kill "$QEMU_PID" 2>/dev/null || true
1143 rm -f "$DAEMON_PID_FILE" "$DAEMON_SOCKET"
1144 exit 1
1145 fi
1146fi
1147
1148log "INFO" "Starting QEMU..."
1149log "DEBUG" "Command: $QEMU_CMD $QEMU_OPTS -append \"$KERNEL_APPEND\""
1150
1151# Interactive mode runs QEMU in foreground with stdio connected
1152if [ "$INTERACTIVE" = "true" ]; then
1153 # Check if stdin is a terminal
1154 if [ ! -t 0 ]; then
1155 log "WARN" "Interactive mode requested but stdin is not a terminal"
1156 fi
1157
1158 # Show a starting message
1159 # The init script will clear this line when the container is ready
1160 if [ -t 1 ]; then
1161 printf "\r\033[0;36m[vdkr]\033[0m Starting container... \r"
1162 fi
1163
1164 # Save terminal settings to restore later
1165 if [ -t 0 ]; then
1166 SAVED_STTY=$(stty -g)
1167 # Put terminal in raw mode so Ctrl+C etc go to guest
1168 stty raw -echo
1169 fi
1170
1171 # Run QEMU with stdio (not in background)
1172 # The -serial mon:stdio connects the serial console to our terminal
1173 $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND"
1174 QEMU_EXIT=$?
1175
1176 # Restore terminal settings
1177 if [ -t 0 ]; then
1178 stty "$SAVED_STTY"
1179 fi
1180
1181 echo ""
1182 log "INFO" "Interactive session ended (exit code: $QEMU_EXIT)"
1183 exit $QEMU_EXIT
1184fi
1185
1186# Non-interactive mode: run QEMU in background and capture output
1187QEMU_OUTPUT="$TEMP_DIR/qemu_output.txt"
1188timeout $TIMEOUT $QEMU_CMD $QEMU_OPTS -append "$KERNEL_APPEND" > "$QEMU_OUTPUT" 2>&1 &
1189QEMU_PID=$!
1190
1191# Monitor for completion
1192COMPLETE=false
1193for i in $(seq 1 $TIMEOUT); do
1194 if [ ! -d "/proc/$QEMU_PID" ]; then
1195 log "DEBUG" "QEMU ended after $i seconds"
1196 break
1197 fi
1198
1199 # Check for completion markers based on output type
1200 case "$OUTPUT_TYPE" in
1201 text)
1202 if grep -q "===OUTPUT_END===" "$QEMU_OUTPUT" 2>/dev/null; then
1203 COMPLETE=true
1204 break
1205 fi
1206 ;;
1207 tar)
1208 if grep -q "===TAR_END===" "$QEMU_OUTPUT" 2>/dev/null; then
1209 COMPLETE=true
1210 break
1211 fi
1212 ;;
1213 storage)
1214 if grep -q "===STORAGE_END===" "$QEMU_OUTPUT" 2>/dev/null; then
1215 COMPLETE=true
1216 break
1217 fi
1218 ;;
1219 esac
1220
1221 # Check for error
1222 if grep -q "===ERROR===" "$QEMU_OUTPUT" 2>/dev/null; then
1223 log "ERROR" "Error in QEMU:"
1224 grep -A10 "===ERROR===" "$QEMU_OUTPUT"
1225 break
1226 fi
1227
1228 # Progress indicator
1229 if [ $((i % 30)) -eq 0 ]; then
1230 if grep -q "Docker daemon is ready" "$QEMU_OUTPUT" 2>/dev/null; then
1231 log "INFO" "Docker is running, executing command..."
1232 elif grep -q "Starting Docker" "$QEMU_OUTPUT" 2>/dev/null; then
1233 log "INFO" "Docker is starting..."
1234 fi
1235 fi
1236
1237 sleep 1
1238done
1239
1240# Wait for QEMU to exit gracefully (poweroff from inside flushes disks properly)
1241# Only kill if it hangs after seeing completion marker
1242if [ "$COMPLETE" = "true" ] && [ -d "/proc/$QEMU_PID" ]; then
1243 log "DEBUG" "Waiting for QEMU to complete graceful shutdown..."
1244 # Give QEMU up to 30 seconds to poweroff after command completes
1245 for wait_i in $(seq 1 30); do
1246 if [ ! -d "/proc/$QEMU_PID" ]; then
1247 log "DEBUG" "QEMU shutdown complete"
1248 break
1249 fi
1250 sleep 1
1251 done
1252fi
1253
1254# Force kill QEMU only if still running after grace period
1255if [ -d "/proc/$QEMU_PID" ]; then
1256 log "WARN" "QEMU still running, forcing termination..."
1257 kill $QEMU_PID 2>/dev/null || true
1258 wait $QEMU_PID 2>/dev/null || true
1259fi
1260
1261# Extract results
1262if [ "$COMPLETE" = "true" ]; then
1263 # Get exit code
1264 EXIT_CODE=$(grep -oP '===EXIT_CODE=\K[0-9]+' "$QEMU_OUTPUT" | head -1)
1265 EXIT_CODE="${EXIT_CODE:-0}"
1266
1267 case "$OUTPUT_TYPE" in
1268 text)
1269 log "INFO" "=== Command Output ==="
1270 # Use awk for precise extraction between markers
1271 awk '/===OUTPUT_START===/{capture=1; next} /===OUTPUT_END===/{capture=0} capture' "$QEMU_OUTPUT"
1272 log "INFO" "=== Exit Code: $EXIT_CODE ==="
1273 ;;
1274
1275 tar)
1276 log "INFO" "Extracting tar output..."
1277 # Use awk for precise extraction between markers
1278 # Strip ANSI escape codes and non-base64 characters from serial console output
1279 awk '/===TAR_START===/{capture=1; next} /===TAR_END===/{capture=0} capture' "$QEMU_OUTPUT" | \
1280 tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g' | tr -cd 'A-Za-z0-9+/=\n' | base64 -d > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt"
1281
1282 if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then
1283 log "WARN" "Base64 decode warnings: $(cat "${TEMP_DIR}/b64_errors.txt")"
1284 fi
1285
1286 if tar -tf "$OUTPUT_FILE" >/dev/null 2>&1; then
1287 log "INFO" "SUCCESS: Output saved to $OUTPUT_FILE"
1288 log "INFO" "Size: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')"
1289 else
1290 log "ERROR" "Output file is not a valid tar"
1291 exit 1
1292 fi
1293 ;;
1294
1295 storage)
1296 log "INFO" "Extracting storage..."
1297 # Use awk for precise extraction: capture lines between markers (not including markers)
1298 # This avoids grep -v "===" which could accidentally remove valid base64 lines
1299 # Pipeline:
1300 # 1. awk: extract lines between STORAGE_START and STORAGE_END markers
1301 # 2. tr -d '\r': remove carriage returns
1302 # 3. sed: remove ANSI escape codes
1303 # 4. grep -v: remove kernel log messages (lines starting with [ followed by timestamp)
1304 # 5. tr -cd: keep only valid base64 characters
1305 awk '/===STORAGE_START===/{capture=1; next} /===STORAGE_END===/{capture=0} capture' "$QEMU_OUTPUT" | \
1306 tr -d '\r' | \
1307 sed 's/\x1b\[[0-9;]*m//g' | \
1308 grep -v '^\[[[:space:]]*[0-9]' | \
1309 tr -cd 'A-Za-z0-9+/=\n' > "${TEMP_DIR}/storage_b64.txt"
1310
1311 B64_SIZE=$(wc -c < "${TEMP_DIR}/storage_b64.txt")
1312 log "DEBUG" "Base64 data extracted: $B64_SIZE bytes"
1313
1314 # Decode with error reporting (not suppressed)
1315 if ! base64 -d < "${TEMP_DIR}/storage_b64.txt" > "$OUTPUT_FILE" 2>"${TEMP_DIR}/b64_errors.txt"; then
1316 log "ERROR" "Base64 decode failed"
1317 if [ -s "${TEMP_DIR}/b64_errors.txt" ]; then
1318 log "ERROR" "Decode errors: $(cat "${TEMP_DIR}/b64_errors.txt")"
1319 fi
1320 # Show a sample of the base64 data for debugging
1321 log "DEBUG" "First 200 chars of base64: $(head -c 200 "${TEMP_DIR}/storage_b64.txt")"
1322 log "DEBUG" "Last 200 chars of base64: $(tail -c 200 "${TEMP_DIR}/storage_b64.txt")"
1323 exit 1
1324 fi
1325
1326 DECODED_SIZE=$(wc -c < "$OUTPUT_FILE")
1327 log "DEBUG" "Decoded storage size: $DECODED_SIZE bytes"
1328
1329 if tar -tf "$OUTPUT_FILE" >/dev/null 2>&1; then
1330 log "INFO" "SUCCESS: Docker storage saved to $OUTPUT_FILE"
1331 log "INFO" "Size: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')"
1332 log "INFO" ""
1333 log "INFO" "To deploy: tar -xf $OUTPUT_FILE -C /var/lib/"
1334 else
1335 log "ERROR" "Storage file is not a valid tar (size: $DECODED_SIZE bytes)"
1336 log "DEBUG" "Tar validation output: $(tar -tf "$OUTPUT_FILE" 2>&1 | head -10)"
1337 exit 1
1338 fi
1339 ;;
1340 esac
1341
1342 exit "${EXIT_CODE:-0}"
1343else
1344 log "ERROR" "Command execution failed or timed out"
1345 log "ERROR" "QEMU output saved to: $QEMU_OUTPUT"
1346
1347 if [ "$VERBOSE" = "true" ]; then
1348 log "DEBUG" "=== Last 50 lines of QEMU output ==="
1349 tail -50 "$QEMU_OUTPUT"
1350 fi
1351
1352 exit 1
1353fi
diff --git a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc
new file mode 100644
index 00000000..a0930a68
--- /dev/null
+++ b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc
@@ -0,0 +1,237 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# vcontainer-initramfs-create.inc
6# ===========================================================================
7# Shared code for building QEMU boot blobs (vdkr/vpdmn)
8# ===========================================================================
9#
10# This .inc file contains common code for building boot blobs.
11# Individual recipes (vdkr-initramfs-create, vpdmn-initramfs-create)
12# set VCONTAINER_RUNTIME and include this file.
13#
14# Required variables from including recipe:
15# VCONTAINER_RUNTIME - "vdkr" or "vpdmn"
16#
17# Boot flow:
18# QEMU boots kernel + tiny initramfs
19# -> preinit mounts rootfs.img from /dev/vda
20# -> switch_root into rootfs.img
21# -> ${VCONTAINER_RUNTIME}-init.sh runs
22#
23# ===========================================================================
24
25HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/"
26LICENSE = "MIT"
27LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
28
29inherit deploy
30
31# Not built by default - user must explicitly request via bitbake or vcontainer-native
32EXCLUDE_FROM_WORLD = "1"
33
34# Need squashfs-tools-native for unsquashfs to extract files from rootfs.img
35DEPENDS = "squashfs-tools-native"
36
37# Always rebuild - no sstate caching for this recipe
38# This ensures source file changes (like init scripts in the rootfs) are picked up
39SSTATE_SKIP_CREATION = "1"
40do_compile[nostamp] = "1"
41do_deploy[nostamp] = "1"
42
43# Only populate native sysroot, skip target sysroot to avoid libgcc conflicts
44INHIBIT_DEFAULT_DEPS = "1"
45
46# Dependencies:
47# 1. The multiconfig rootfs image from same vruntime-* multiconfig
48# 2. The kernel from main build (not multiconfig)
49#
50# Use regular depends for rootfs-image since both recipes are in the same multiconfig
51# Use mcdepends for kernel since it's from the main (default) config
52do_compile[depends] = "${VCONTAINER_RUNTIME}-rootfs-image:do_image_complete"
53do_compile[mcdepends] = "mc:${VCONTAINER_MULTICONFIG}::virtual/kernel:do_deploy"
54
55# Preinit is shared between vdkr and vpdmn
56SRC_URI = "file://vdkr-preinit.sh"
57
58S = "${UNPACKDIR}"
59B = "${WORKDIR}/build"
60
61def get_kernel_image_name(d):
62 arch = d.getVar('TARGET_ARCH')
63 if arch == 'aarch64':
64 return 'Image'
65 elif arch in ['x86_64', 'i686', 'i586']:
66 return 'bzImage'
67 elif arch == 'arm':
68 return 'zImage'
69 return 'Image'
70
71def get_multiconfig_name(d):
72 arch = d.getVar('TARGET_ARCH')
73 if arch == 'aarch64':
74 return 'vruntime-aarch64'
75 elif arch in ['x86_64', 'i686', 'i586']:
76 return 'vruntime-x86-64'
77 return 'vruntime-aarch64'
78
79def get_blob_arch(d):
80 """Map TARGET_ARCH to vrunner blob architecture (aarch64 or x86_64)"""
81 arch = d.getVar('TARGET_ARCH')
82 if arch == 'aarch64':
83 return 'aarch64'
84 elif arch in ['x86_64', 'i686', 'i586']:
85 return 'x86_64'
86 return 'aarch64'
87
88KERNEL_IMAGETYPE_INITRAMFS = "${@get_kernel_image_name(d)}"
89VCONTAINER_MULTICONFIG = "${@get_multiconfig_name(d)}"
90BLOB_ARCH = "${@get_blob_arch(d)}"
91
92# Path to the multiconfig build output
93VCONTAINER_MC_DEPLOY = "${TOPDIR}/tmp-${VCONTAINER_MULTICONFIG}/deploy/images/${MACHINE}"
94
95do_compile() {
96 mkdir -p ${B}
97
98 # =========================================================================
99 # PART 1: BUILD TINY INITRAMFS (just for switch_root)
100 # =========================================================================
101 INITRAMFS_DIR="${B}/initramfs"
102 rm -rf ${INITRAMFS_DIR}
103 mkdir -p ${INITRAMFS_DIR}/bin
104 mkdir -p ${INITRAMFS_DIR}/proc
105 mkdir -p ${INITRAMFS_DIR}/sys
106 mkdir -p ${INITRAMFS_DIR}/dev
107 mkdir -p ${INITRAMFS_DIR}/mnt/root
108
109 bbnote "Building tiny initramfs for switch_root..."
110
111 # Extract busybox from the multiconfig rootfs image (squashfs)
112 MC_TMPDIR="${TOPDIR}/tmp-${VCONTAINER_MULTICONFIG}"
113 ROOTFS_SRC="${MC_TMPDIR}/deploy/images/${MACHINE}/${VCONTAINER_RUNTIME}-rootfs-image-${MACHINE}.rootfs.squashfs"
114
115 if [ ! -f "${ROOTFS_SRC}" ]; then
116 bbfatal "Rootfs image not found at ${ROOTFS_SRC}. Build it first with: bitbake mc:${VCONTAINER_MULTICONFIG}:${VCONTAINER_RUNTIME}-rootfs-image"
117 fi
118
119 # Extract busybox from rootfs using unsquashfs
120 # In usrmerge layouts, busybox is at usr/bin/busybox
121 BUSYBOX_PATH="usr/bin/busybox"
122 EXTRACT_DIR="${B}/squashfs-extract"
123
124 bbnote "Extracting busybox from $BUSYBOX_PATH"
125 # Try native sysroot first, fall back to system unsquashfs
126 UNSQUASHFS="${WORKDIR}/recipe-sysroot-native/usr/bin/unsquashfs"
127 if [ ! -x "$UNSQUASHFS" ]; then
128 UNSQUASHFS="/usr/bin/unsquashfs"
129 fi
130 if [ ! -x "$UNSQUASHFS" ]; then
131 bbfatal "unsquashfs not found in native sysroot or at /usr/bin/unsquashfs"
132 fi
133 bbnote "Using unsquashfs: $UNSQUASHFS"
134
135 rm -rf "${EXTRACT_DIR}"
136 $UNSQUASHFS -d "${EXTRACT_DIR}" "${ROOTFS_SRC}" "$BUSYBOX_PATH"
137
138 if [ ! -f "${EXTRACT_DIR}/${BUSYBOX_PATH}" ]; then
139 bbfatal "Failed to extract busybox from rootfs image"
140 fi
141 cp "${EXTRACT_DIR}/${BUSYBOX_PATH}" "${INITRAMFS_DIR}/bin/busybox"
142 chmod +x ${INITRAMFS_DIR}/bin/busybox
143
144 # Create minimal symlinks
145 cd ${INITRAMFS_DIR}/bin
146 for cmd in sh mount umount mkdir ls cat echo sleep switch_root reboot; do
147 ln -sf busybox $cmd 2>/dev/null || true
148 done
149 cd -
150
151 # Install preinit script as /init
152 cp ${S}/vdkr-preinit.sh ${INITRAMFS_DIR}/init
153 chmod +x ${INITRAMFS_DIR}/init
154
155 # Create tiny initramfs cpio
156 bbnote "Creating tiny initramfs cpio archive..."
157 cd ${INITRAMFS_DIR}
158 find . | cpio -o -H newc 2>/dev/null | gzip -9 > ${B}/initramfs.cpio.gz
159 cd -
160
161 INITRAMFS_SIZE=$(stat -c%s ${B}/initramfs.cpio.gz)
162 bbnote "Tiny initramfs created: ${INITRAMFS_SIZE} bytes ($(expr ${INITRAMFS_SIZE} / 1024)KB)"
163
164 # =========================================================================
165 # PART 2: COPY ROOTFS FROM MULTICONFIG BUILD
166 # =========================================================================
167 bbnote "Looking for multiconfig rootfs at: ${MC_TMPDIR}/deploy/images/${MACHINE}"
168
169 # ROOTFS_SRC already set above when extracting busybox
170 cp "${ROOTFS_SRC}" ${B}/rootfs.img
171 ROOTFS_SIZE=$(stat -c%s ${B}/rootfs.img)
172 bbnote "Rootfs image copied: ${ROOTFS_SIZE} bytes ($(expr ${ROOTFS_SIZE} / 1024 / 1024)MB)"
173
174 # =========================================================================
175 # PART 3: COPY KERNEL
176 # =========================================================================
177 bbnote "Copying kernel image..."
178 KERNEL_FILE="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE_INITRAMFS}"
179 if [ -f "${KERNEL_FILE}" ]; then
180 cp "${KERNEL_FILE}" ${B}/kernel
181 KERNEL_SIZE=$(stat -c%s ${B}/kernel)
182 bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)"
183 else
184 bbwarn "Kernel not found at ${KERNEL_FILE}"
185 fi
186}
187
188do_install[noexec] = "1"
189do_package[noexec] = "1"
190do_packagedata[noexec] = "1"
191do_package_write_rpm[noexec] = "1"
192do_package_write_ipk[noexec] = "1"
193do_package_write_deb[noexec] = "1"
194do_populate_sysroot[noexec] = "1"
195
196do_deploy() {
197 # Deploy to ${VCONTAINER_RUNTIME}/<arch> subdirectory
198 install -d ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}
199
200 if [ -f ${B}/initramfs.cpio.gz ]; then
201 install -m 0644 ${B}/initramfs.cpio.gz ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/
202 bbnote "Deployed initramfs.cpio.gz to ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/"
203 fi
204
205 if [ -f ${B}/rootfs.img ]; then
206 install -m 0644 ${B}/rootfs.img ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/
207 bbnote "Deployed rootfs.img to ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/"
208 fi
209
210 if [ -f ${B}/kernel ]; then
211 install -m 0644 ${B}/kernel ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS}
212 bbnote "Deployed kernel as ${VCONTAINER_RUNTIME}/${BLOB_ARCH}/${KERNEL_IMAGETYPE_INITRAMFS}"
213 fi
214
215 cat > ${DEPLOYDIR}/${VCONTAINER_RUNTIME}/${BLOB_ARCH}/README << EOF
216${VCONTAINER_RUNTIME} Boot Blobs
217==================
218
219Built for: ${TARGET_ARCH}
220Machine: ${MACHINE}
221Multiconfig: ${VCONTAINER_MULTICONFIG}
222Date: $(date)
223
224Files:
225 ${KERNEL_IMAGETYPE_INITRAMFS} - Kernel image for QEMU
226 initramfs.cpio.gz - Tiny initramfs (switch_root only)
227 rootfs.img - Root filesystem with container tools
228
229Boot flow:
230 QEMU boots kernel + initramfs
231 -> preinit mounts rootfs.img from /dev/vda
232 -> switch_root into rootfs.img
233 -> ${VCONTAINER_RUNTIME}-init.sh runs container commands
234EOF
235}
236
237addtask deploy after do_compile before do_build
diff --git a/recipes-containers/vcontainer/vcontainer-native.bb b/recipes-containers/vcontainer/vcontainer-native.bb
new file mode 100644
index 00000000..1d19ac1b
--- /dev/null
+++ b/recipes-containers/vcontainer/vcontainer-native.bb
@@ -0,0 +1,43 @@
1# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield
2#
3# SPDX-License-Identifier: MIT
4#
5# vcontainer-native.bb
6# ===========================================================================
7# Native recipe providing vrunner.sh for container cross-installation
8# ===========================================================================
9#
10# This recipe installs vrunner.sh into the native sysroot so that
11# container-bundle.bbclass and container-cross-install.bbclass can use it
12# to cross-install containers into target images.
13#
14# Note: This does NOT build the blobs. Blobs must be built separately via
15# multiconfig (see vdkr-initramfs-create, vpdmn-initramfs-create).
16#
17# ===========================================================================
18
19SUMMARY = "Container cross-install runner script"
20DESCRIPTION = "Provides vrunner.sh for cross-installing containers into images"
21HOMEPAGE = "https://git.yoctoproject.org/meta-virtualization/"
22LICENSE = "MIT"
23LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
24
25inherit native
26
27# Runtime dependencies for vrunner.sh
28DEPENDS = "coreutils-native socat-native"
29
30SRC_URI = "\
31 file://vrunner.sh \
32 file://vcontainer-common.sh \
33"
34
35S = "${UNPACKDIR}"
36
37do_install() {
38 install -d ${D}${bindir}
39 install -m 0755 ${S}/vrunner.sh ${D}${bindir}/vrunner.sh
40 install -m 0644 ${S}/vcontainer-common.sh ${D}${bindir}/vcontainer-common.sh
41}
42
43BBCLASSEXTEND = "native"