This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
nbc is a Go CLI tool for installing bootc-compatible container images to physical disks with A/B partitioning for atomic updates. It handles partitioning, filesystem creation, container extraction, and bootloader installation without requiring the bootc command.
# Build
make build # Build binary with version info
# Format and lint (ALWAYS run before committing)
make fmt # Format all Go code
make lint # Run golangci-lint
# Testing
make test-unit # Unit tests only (no root required)
make test-integration # Integration tests (requires root, auto-escalates with sudo)
make test # Run all tests (unit then integration)
make test-incus # Full VM integration tests (requires root and Incus)
# Run specific test
go test -v ./pkg/... -run TestInstallConfig_ValidateNever run go test, go fmt, or linter commands directly - always use Makefile targets.
cmd/- Cobra CLI commands (parse flags, call pkg functions)pkg/- Core business logic (Installer, SystemUpdater, ContainerExtractor, etc.)pkg/types/- JSON output types for machine-readable outputpkg/testutil/- Test helpers (disk simulation, mock containers)
- Installer (
pkg/install.go) - Fresh installations to disk - SystemUpdater (
pkg/update.go) - A/B partition updates with rollback - ContainerExtractor (
pkg/container.go) - OCI image extraction via go-containerregistry - BootloaderInstaller (
pkg/bootloader.go) - GRUB2 or systemd-boot configuration - Reporter (
github.com/frostyard/std/reporter) - Interface for all user-facing output (TextReporter, JSONReporter, NoopReporter) - Shared Steps (
pkg/steps.go) - Common install/update operations (SetupTargetSystem, ExtractAndVerifyContainer) - Workflow (
pkg/workflow.go) - Composable step-based orchestration type
- CLI parses flags (grouped in typed flag structs per command), creates config struct
NewInstaller(cfg)/NewSystemUpdater()validates and sets defaultsInstall(ctx)/Update()orchestrates: partitioning → formatting → extraction → bootloader- All exported I/O functions accept
context.Contextfor cancellation - System config stored in
/var/lib/nbc/state/config.json(shared across A/B roots)
Any change to installation flow MUST also be applied to update flow:
pkg/install.go-Install()functionpkg/update.go-Update()functionpkg/steps.go- Shared operations used by both (SetupTargetSystem, ExtractAndVerifyContainer)
Keep in sync: kernel cmdline parameters, tmpfiles.d configs, bootloader entries, directory creation.
Changes to shared steps in pkg/steps.go automatically apply to both flows.
if err != nil {
return fmt.Errorf("failed to <action>: %w", err)
}- Error strings should not end with punctuation or newlines
- Wrap errors with context using
%w
- Always use
filepath.Join()for path construction - Never use string concatenation for paths
- Use the
Reporterinterface for all user-facing output — never rawfmt.Printinpkg/ - Create reporters via
clix.NewReporter()incmd/— handles JSON/text/silent mode - Access common flags via
clix.Verbose,clix.DryRun,clix.JSONOutput,clix.Silent - Use
clix.OutputJSON()andclix.OutputJSONError()for structured JSON output - Pass
Reporteras a parameter to functions that produce output
if dryRun {
progress.Message("[DRY RUN] Would wipe disk: %s", device)
return nil
}Operations use POSIX file locking to prevent concurrent access:
- Cache operations:
/var/run/nbc/cache.lock - System operations:
/var/run/nbc/system.lock
lock, err := AcquireSystemLock()
if err != nil {
return err
}
defer func() { _ = lock.Release() }()- Unit tests:
TestXxx - Integration tests:
TestIntegration_Xxx(require root) - Incus VM tests:
TestIncus_Xxx
testutil.RequireRoot(t)
testutil.RequireTools(t, "losetup", "sgdisk", "mkfs.vfat")
disk, err := testutil.CreateTestDisk(t, 50) // 50GB sparse imageUse standard library only - no testify/gomega:
if got != want {
t.Errorf("result = %v, want %v", got, want)
}Shim is hardcoded to load grubx64.efi in the same directory. For systemd-boot with Secure Boot:
EFI/BOOT/
├── BOOTX64.EFI ← shimx64.efi.signed
├── grubx64.efi ← systemd-bootx64.efi.signed (yes, named grubx64.efi)
└── mmx64.efi ← MOK manager
Do NOT include fbx64.efi - it causes "Restore Boot Option" blue screen.
- EFI System Partition (FAT32) - UEFI boot files
- Root1 (ext4/btrfs) - OS slot A
- Root2 (ext4/btrfs) - OS slot B
- Var (ext4/btrfs) - Shared
/varpartition
systemd.mount-extra=DEVICE:MOUNTPOINT:FSTYPE:OPTIONS
Example: systemd.mount-extra=UUID=abc123:/var:ext4:defaults
Kernel/initramfs must be in /usr/lib/modules/$KERNEL_VERSION/:
vmlinuzorvmlinuz-$KERNEL_VERSIONinitramfs.imgorinitrd.img