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>
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 itsindex.jsonmanifest annotations (io.containerd.image.namepreferred, elseorg.opencontainers.image.ref.name), falling back to the archive's filename and"latest"respectively.containers_storage.{h,cpp}— wraps thecontainers-storageCLI (import-layer,mount,unmount,layer --json,delete-layer), forcingfuse-overlayfsas the overlaymount_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 forkedunshare(2)per namespace type) for which--unshare-xxxflagsbwrapcan 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 forcontainers-storagecalls),run_process_foreground()inherits all of stdio (used for the interactivebwraprun). Alsofind_in_path(), a shared$PATHlookup.
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(orninja -C buildDir) - Run the executable:
./buildDir/slocker_lite -m <image.tar>(see--helpfor 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)overif (ptr == nullptr)/if (ptr != nullptr).
Licensing
- Every
.c/.cpp/.hfile undersrc/must start with the GPLv2-or-later copyright header (see any existing file undersrc/for the exact text). - After adding a new source file under
src/, run./add-license.shfrom the repo root to prepend the header (it readscopyright-headerand inserts it viased, skipping files that already have it, so it's safe to re-run at any time).
Build configuration notes
meson.buildsetswarning_level=3andcpp_std=c++20— keep new code warning-clean under-Wall -Wextra -Wpedantic-equivalent settings.- The single Meson
test()target runsslocker_liteagainst a fixture OCI image tar generated at build time bytests/gen_fixture.py(acustom_target) and checks its exit code (no test framework is wired in yet).