Files
slocker-lite/CLAUDE.md
T
ceamac 94ae5b36a2 Add -l/--list-images to discover OCI archives in a directory
list_oci_images() (src/oci_image.cpp) scans a directory non-recursively
for *.tar/*.tar.* files and, for each one that's a valid OCI Image
Layout archive, derives a name:tag from its index.json manifest
annotations -- io.containerd.image.name if present (a full reference),
else org.opencontainers.image.ref.name (conventionally just a bare
tag for skopeo/podman-produced archives). Falls back to the archive's
filename (.tar and any compression suffix stripped) for the name and
"latest" for the tag. Files that aren't OCI archives are skipped
quietly, since a directory scan is expected to hit unrelated tars.

-l/--list-images <directory> wires this into the CLI, printing
"name:tag<TAB>filename" per image. This is also why --log-level lost
its short form last session: -l needed to be free for this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 15:06:32 +00:00

5.2 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project state

slocker-lite (C++20, built with Meson) mounts an OCI Image Layout tar (oci-layout + index.json + blobs/sha256/*, as produced by skopeo/podman save --format oci-archive/modern docker save) using containers-storage and fuse-overlayfs, then (via -r/--run) runs a sandboxed command against it with bwrap. The real deployment target is Android with a stock kernel, where podman/docker don't run (missing namespace support) and there's no kernel overlayfs (hence fuse-overlayfs); bwrap is invoked in "degraded mode" using only whichever --unshare-xxx namespaces the running kernel actually supports. There is no README or broader architecture doc yet — treat this repo as still early-stage.

Source layout (all under src/):

  • main.cpp — CLI entry point, dependency checks, orchestration (mount_image(), run_container(), cleanup_image(), unmount_image(), list_images_command()).
  • oci_image.{h,cpp} — validates/parses the OCI Image Layout tar (libarchive + nlohmann_json) and extracts layer blobs. list_oci_images() scans a directory (non-recursively) for *.tar/*.tar.* files and, for each valid OCI archive, derives an image name/tag from its index.json manifest annotations (io.containerd.image.name preferred, else org.opencontainers.image.ref.name), falling back to the archive's filename and "latest" respectively.
  • containers_storage.{h,cpp} — wraps the containers-storage CLI (import-layer, mount, unmount, layer --json, delete-layer), forcing fuse-overlayfs as the overlay mount_program. cleanup_layer_chain() walks a layer's parent chain (children before parents) deleting each one.
  • bwrap.{h,cpp}detect_bwrap_unshare_args() probes the kernel (via a forked unshare(2) per namespace type) for which --unshare-xxx flags bwrap can actually use; build_bwrap_args()/run_bwrap() assemble and run the sandboxed command.
  • process.{h,cpp} — argv-based subprocess helpers (fork/execvp, no shell): run_process() captures stdout (used for containers-storage calls), run_process_foreground() inherits all of stdio (used for the interactive bwrap run). Also find_in_path(), a shared $PATH lookup.

Errors are logged via spdlog::error; every external command is also traced at debug level in run_process()/run_process_foreground() (src/process.cpp) — visible via SPDLOG_LEVEL=debug, since spdlog's default level is info — and a failed external command additionally logs a spdlog::warn, which is visible by default (no env var needed). The final "mounted image at: ..." success line is direct stdout program output, not a log.

Because containers-storage mount runs rootless, it reexecs itself into a private user+mount namespace to gain the privilege it needs for the overlay mount — which leaves the result invisible to a plain shell or child process outside that namespace. Confirmed containers-storage unshare does not rejoin an already-running mount's namespace; only nsenter targeting the live fuse-overlayfs daemon's PID does. -r/--run handles this automatically by locating that PID and running bwrap via nsenter into its namespaces. Running as root sidesteps all of this: no privilege reexec is needed, so the mount is already directly visible in the current namespace, and nsenter --user=... into it then fails ("reassociate to namespace 'ns/user' failed: Invalid argument") since the caller is already in that same user namespace. -r/--run detects geteuid() == 0 and skips nsenter automatically in that case; -n/--no-nsenter forces it off manually for any other situation where the mount turns out to already be directly visible.

Build & test commands

Build directory is buildDir/ (already configured).

  • Configure (only needed if buildDir/ is missing or deleted): meson setup buildDir
  • Build: meson compile -C buildDir (or ninja -C buildDir)
  • Run the executable: ./buildDir/slocker_lite -m <image.tar> (see --help for the full flag list: -m/--mount, -r/--run, -u/--umount, -c/--cleanup, -l/--list-images, -n/--no-nsenter, -t/--test, --log-level, -h/--help, -V/--version)
  • Run tests: meson test -C buildDir

Code style

  • Null-pointer checks: prefer if (!ptr) / if (ptr) over if (ptr == nullptr) / if (ptr != nullptr).

Licensing

  • Every .c/.cpp/.h file under src/ must start with the GPLv2-or-later copyright header (see any existing file under src/ for the exact text).
  • After adding a new source file under src/, run ./add-license.sh from the repo root to prepend the header (it reads copyright-header and inserts it via sed, skipping files that already have it, so it's safe to re-run at any time).

Build configuration notes

  • meson.build sets warning_level=3 and cpp_std=c++20 — keep new code warning-clean under -Wall -Wextra -Wpedantic-equivalent settings.
  • The single Meson test() target runs slocker_lite against a fixture OCI image tar generated at build time by tests/gen_fixture.py (a custom_target) and checks its exit code (no test framework is wired in yet).