Build staged BlackArch cloud images from an Arch-based Linux host.
The build flow is intentionally shell-only:
- Stage 1 builds a reusable Arch + BlackArch rootfs artifact.
- Stage 2 assembles a bootable raw disk image for a selected platform profile.
- Stage 3 exports the final profile-specific artifact.
Supported platform profiles:
generic-qemuExportsqcow2, uses a Btrfs root filesystem, installs and enablesqemu-guest-agent, defaults to2G, and keeps the current BIOS+UEFI boot path.digitaloceanExportsimg.gz, uses an ext4 root filesystem, keeps a BIOS-only boot path, skipsqemu-guest-agent, adds a DigitalOcean-specificcloud-initdatasource override, cleanscloud-initstate before packaging, and defaults to4G.
Profile customization is localized through:
profiles/<name>.envProfile defaults and the supportedPROFILE_*settings.profiles/<name>.shOptional shell hook for advanced profile-specific logic.profiles/<name>/rootfs-overlay/Optional files copied into the mounted image root during Stage 2 without preserving host uid/gid from the build checkout.
Future platforms should be added by introducing new profile files and localized profile logic, not by cloning the whole pipeline.
Runtime boot validation is not implemented yet. This repository only builds the staged artifacts.
The common rootfs stage includes:
- Arch Linux base system
cloud-initopenssh- BlackArch repository bootstrap using the in-repo setup script by default
- the selected BlackArch logical profile:
coreorcommon - optional extra packages from
BLACKARCH_PACKAGES
The assembled image includes:
- GRUB configured per profile boot mode
- kernel root arguments normalized to stable filesystem UUIDs instead of loop-device paths
- serial console on
tty0andttyS0 systemd-networkd,systemd-resolved,systemd-timesyncd, andsshd- profile-specific root filesystem behavior:
generic-qemuuses Btrfs with Zstandard compressiondigitaloceanuses ext4 - Stage 1 suppresses the early
mkinitcpiopackage hook for the reusable rootfs tree - Stage 1 recreates the kernel preset and
/boot/vmlinuz-*copy needed for Stage 2 finalization without generating initramfs yet - Stage 1 now renders the preset from the upstream
mkinitcpiotemplate, resolves all known placeholders, and fails early if any%...%token remains - Stage 2 rebuilds the final initramfs after the real image root filesystem is mounted, skipping the
autodetecthook so cloud drivers are not stripped based on the build host - Stage 2 generates the final
grub.cfgonly after the initramfs exists, then validates that the boot config contains bothlinuxandinitrdentries without host loop-device paths - Stage 2 validates that
/,/etc,/etc/cloud, and/etc/cloud/cloud.cfg.dremain root-owned after profile overlays and hooks run
.
├── build.sh # Thin user-facing orchestrator
├── VERSION # Canonical codebase release version (SemVer)
├── profiles/
│ ├── digitalocean.env # DigitalOcean profile defaults
│ ├── digitalocean.sh # Optional DigitalOcean profile hook
│ ├── digitalocean/
│ │ └── rootfs-overlay/ # DigitalOcean rootfs overlay files
│ └── generic-qemu.env # Generic QEMU/KVM profile defaults
├── images/
│ ├── base.sh # Common rootfs and bootable disk customization hooks
│ └── blackarch-cloud.sh # BlackArch repo/profile + cloud-init customization
├── scripts/
│ ├── build-rootfs.sh # Stage 1: build reusable rootfs artifact
│ ├── assemble-image.sh # Stage 2: assemble bootable raw staging image
│ ├── export-image.sh # Stage 3: export final profile artifact
│ ├── check-build-env.sh # Host preflight checks
│ ├── clean-build-state.sh # Remove tmp/ leftovers and output artifacts
│ ├── setup-blackarch-repo.sh # In-image BlackArch repository bootstrap
│ └── lib/ # Shared config, logging, manifests, mounts, validation
└── Makefile # Convenience targets
Build on an Arch-based Linux host with:
- root privileges for loop devices, mounts, package installation, and image creation
- network access to Arch package mirrors and BlackArch resources
- enough free disk space for the selected profile and package set
Required commands:
arch-chrootblockdevcurlfstrimgpgconfgziplosetupmkfs.ext4mountmountpointpacmanpacstrapqemu-imgsha256sumsgdisktartruncateudevadmumountzstdsudowhen the build is started as a non-root user
Additional commands are required by profile behavior:
btrfs,chattr, andmkfs.btrfsRequired for Btrfs-root profiles such asgeneric-qemu.mkfs.fatRequired for profiles that keep an EFI system partition, such asgeneric-qemu.
Run the preflight checks before building:
make check-envDefault build for generic-qemu:
sudo IMAGE_PROFILE=generic-qemu ./build.shDigitalOcean export:
sudo IMAGE_PROFILE=digitalocean ./build.shExplicit build ID:
sudo IMAGE_PROFILE=generic-qemu ./build.sh 20260320.0Explicit build ID via environment:
sudo IMAGE_PROFILE=generic-qemu BUILD_ID=20260320.0 ./build.shCurated common BlackArch profile:
sudo IMAGE_PROFILE=generic-qemu BLACKARCH_PROFILE=common DISK_SIZE=20G ./build.shAdditional BlackArch packages:
sudo IMAGE_PROFILE=digitalocean BLACKARCH_PACKAGES="blackarch-officials" DISK_SIZE=20G ./build.shYou can also run the convenience target:
make buildmake build preserves the supported image/build environment variables across sudo, so profile and package overrides work there too.
Examples:
IMAGE_PROFILE=digitalocean make buildIMAGE_PROFILE=generic-qemu BLACKARCH_PROFILE=common DISK_SIZE=20G make buildIMAGE_PROFILE=digitalocean BUILD_ID=20260321.2 make buildReuse an existing compatible rootfs artifact for another profile build:
sudo IMAGE_PROFILE=digitalocean BUILD_ID=20260321.2 REUSE_ROOTFS=true ./build.shBuild all supported profiles sequentially with one BUILD_ID and one shared Stage 1 rootfs artifact:
BUILD_ID=20260321.2 make build-allIf a compatible rootfs tarball already exists for that BUILD_ID, reuse it from the first profile onward:
BUILD_ID=20260321.2 REUSE_ROOTFS=true make build-allOverride the profile list used by make build-all:
IMAGE_PROFILES="generic-qemu digitalocean" BUILD_ID=20260321.2 make build-allThe builder resolves three separate version values:
release_versionThe codebase release version. This is read from the top-levelVERSIONfile and must be SemVer such as0.4.0,0.4.1,0.5.0, or1.0.0-rc.1.build_idThe concrete artifact build identity. This usesYYYYMMDD.N, for example20260321.2.artifact_versionThe combined artifact identifier:<release_version>+<build_id>, for example0.4.0+20260321.2.
build_id resolution order is:
- positional argument to
./build.sh BUILD_ID- legacy
BUILD_VERSION - auto-generated next
YYYYMMDD.Nbased on existing files underoutput/
If BUILD_ID and BUILD_VERSION are both set, they must match. Legacy BUILD_VERSION is only consumed when it already matches YYYYMMDD.N; unrelated ambient values are ignored.
Artifact filenames include both pieces of information:
BlackArch-Linux-x86_64-<profile>-v<release_version>+<build_id>.<ext>blackarch-rootfs-v<release_version>+<build_id>.tar.zst
The Stage 1 rootfs tarball is profile-neutral. When REUSE_ROOTFS=true, the builder will reuse an existing compatible rootfs artifact instead of rebuilding Stage 1. Compatibility is checked against the rootfs manifest, including the current git commit and the Stage 1 inputs that affect the reusable rootfs contents.
The manifests remain key=value files and record explicit version/build metadata, including:
release_versionbuild_idartifact_versiongit_commitgit_tagprofileartifact_formatfilesystemboot_modebuilt_at_utc
When HEAD is not at an exact tag, git_tag=none.
To create a release:
- Update
VERSIONto the new SemVer release. - Commit the change.
- Optionally tag the release commit as
v<release_version>. - Run one or more builds. Each build gets its own
build_id, even whenVERSIONstays the same.
Version bump policy:
- Patch bump: bug fixes, build logic fixes, boot fixes, cloud-init fixes, ownership fixes, and other backwards-compatible maintenance.
- Minor bump: new profiles, new artifact formats, additive profile features, additive manifest fields, or additive release-workflow improvements.
- Major bump: incompatible environment variable changes, incompatible profile schema changes, incompatible manifest changes, or output naming changes that downstream automation must adapt to.
- Rebuild only: keep
VERSIONunchanged and produce a newbuild_id. Do not mint a new SemVer just to rebuild the same release.
Core staged-build settings:
VERSIONTop-level repository file containing the canonical SemVerrelease_version.IMAGE_PROFILEgeneric-qemuordigitalocean. Default:generic-qemu.BUILD_IDOptional explicitYYYYMMDD.Nbuild identity. If unset, the builder auto-selects the next daily build number.BUILD_VERSIONLegacy compatibility alias forBUILD_ID. It is only honored when it already matchesYYYYMMDD.N. PreferBUILD_IDfor new automation.REUSE_ROOTFStrueorfalse. Default:false. Whentrue,build.shreuses an existing compatible rootfs tarball for the selectedrelease_versionandbuild_idinstead of rebuilding Stage 1.IMAGE_PROFILESSpace-separated profile list used bymake build-all. Default:generic-qemu digitalocean.DISK_SIZEFinal raw disk size used for Stage 2 assembly.DEFAULT_DISK_SIZECompatibility override used whenDISK_SIZEis unset. If neither is set, the profile default is used:generic-qemu=>2Gdigitalocean=>4G
BlackArch settings:
BLACKARCH_PROFILEcoreorcommon. Default:core.BLACKARCH_PACKAGESSpace-separated extra packages installed after the BlackArch repository is enabled.BLACKARCH_KEYRING_VERSIONKeyring bundle version used by the built-in BlackArch bootstrap. Default:20251011.BLACKARCH_KEYRING_SHA256Optional explicit SHA256 for the selected keyring archive. Required when using an unpinned custom keyring version.BLACKARCH_STRAP_URLOptional compatibility override for using an external BlackArch strap script.BLACKARCH_STRAP_SHA256Required checksum forBLACKARCH_STRAP_URL.
Image customization settings:
IMAGE_ENABLE_QEMU_GUEST_AGENTOptional override. When unset, the selected profile decides the default.generic-qemuresolves totrue;digitaloceanresolves tofalse.IMAGE_HOSTNAMEIMAGE_SWAP_SIZEIMAGE_LOCALEIMAGE_TIMEZONEIMAGE_KEYMAPIMAGE_DEFAULT_USERDefault:blackarch.IMAGE_DEFAULT_USER_GECOSIMAGE_PASSWORDLESS_SUDO
Each profile is resolved from profiles/<name>.env and can optionally add:
profiles/<name>.shA hook script that definesprofile_hook(). The current pipeline calls it during Stage 2 finalization.profiles/<name>/rootfs-overlay/A filesystem tree copied into the mounted image root before bootloader installation.
Supported profile variables:
PROFILE_IDPROFILE_NAME_SUFFIXPROFILE_FINAL_FORMATPROFILE_ROOT_FS_TYPEPROFILE_DEFAULT_DISK_SIZEPROFILE_BOOT_MODESupported values:bios,bios+uefiPROFILE_EFI_PARTITION_SIZEUsed whenPROFILE_BOOT_MODE=bios+uefiPROFILE_PACMAN_PACKAGESSpace-separated packages installed in Stage 2PROFILE_ENABLE_SYSTEMD_UNITSSpace-separated units enabled in Stage 2PROFILE_DISABLE_SYSTEMD_UNITSSpace-separated units disabled in Stage 2PROFILE_ROOTFS_OVERLAY_DIROptional overlay path, resolved relative toprofiles/when not absolutePROFILE_HOOK_SCRIPTOptional hook path, resolved relative toprofiles/when not absolute
Minimal example for a new profile:
# profiles/example.env
#!/usr/bin/env bash
# shellcheck disable=SC2034
PROFILE_ID="example"
PROFILE_NAME_SUFFIX="example"
PROFILE_FINAL_FORMAT="qcow2"
PROFILE_ROOT_FS_TYPE="ext4"
PROFILE_DEFAULT_DISK_SIZE="4G"
PROFILE_BOOT_MODE="bios"
PROFILE_EFI_PARTITION_SIZE=""
PROFILE_PACMAN_PACKAGES="qemu-guest-agent"
PROFILE_ENABLE_SYSTEMD_UNITS="qemu-guest-agent.service"
PROFILE_DISABLE_SYSTEMD_UNITS=""
PROFILE_ROOTFS_OVERLAY_DIR="example/rootfs-overlay"
PROFILE_HOOK_SCRIPT="example.sh"If profiles/example/rootfs-overlay/ exists, its files are copied into the target rootfs during Stage 2. If profiles/example.sh exists, it can define:
#!/usr/bin/env bash
function profile_hook() {
local hook_name="${1}"
case "${hook_name}" in
finalize)
:
;;
esac
}Successful builds write staged artifacts under output/:
output/rootfs/blackarch-rootfs-v<release_version>+<build_id>.tar.zstoutput/rootfs/blackarch-rootfs-v<release_version>+<build_id>.manifestoutput/images/BlackArch-Linux-x86_64-generic-qemu-v<release_version>+<build_id>.qcow2output/images/BlackArch-Linux-x86_64-generic-qemu-v<release_version>+<build_id>.qcow2.SHA256output/images/BlackArch-Linux-x86_64-generic-qemu-v<release_version>+<build_id>.manifestoutput/images/BlackArch-Linux-x86_64-digitalocean-v<release_version>+<build_id>.img.gzoutput/images/BlackArch-Linux-x86_64-digitalocean-v<release_version>+<build_id>.img.gz.SHA256output/images/BlackArch-Linux-x86_64-digitalocean-v<release_version>+<build_id>.manifestoutput/images/BlackArch-Linux-x86_64-<profile>-v<release_version>+<build_id>.build.log
The manifest files are simple key=value records.
The reusable rootfs manifest is intentionally profile-neutral. It includes the shared Stage 1 identity and configuration, including:
artifact_typerootfs_nameartifact_nameartifact_formatrelease_versionbuild_idartifact_versiongit_commitgit_tagblackarch_profileblackarch_packagesimage_hostnameimage_default_userimage_default_user_gecosimage_localeimage_timezoneimage_keymapimage_passwordless_sudoblackarch_keyring_versionblackarch_bootstrap_moderootfs_input_fingerprintbuilt_at_utc
Final image manifests include:
artifact_typeimage_nameartifact_nameartifact_formatrootfs_artifactrelease_versionbuild_idartifact_versiongit_commitgit_tagprofilefilesystemboot_modebuilt_at_utc
They also keep resolved build settings such as disk size, BlackArch profile/package selections, profile package/unit lists, and image customization defaults.
Verify the final checksum after a build:
cd output/images
sha256sum -c BlackArch-Linux-x86_64-generic-qemu-v<release_version>+<build_id>.qcow2.SHA256or:
cd output/images
sha256sum -c BlackArch-Linux-x86_64-digitalocean-v<release_version>+<build_id>.img.gz.SHA256DigitalOcean note:
- the profile now exports a gzip-compressed raw image with an
.img.gzname - the profile now assembles an ext4-root BIOS-only image instead of reusing the generic BIOS+UEFI layout
- the profile adds a
cloud-initdatasource override fromprofiles/digitalocean/rootfs-overlay/ - the profile preserves the base image hostname instead of accepting a potentially overlong droplet hostname from metadata
- the profile cleans
cloud-initstate from its Stage 2 hook before export - runtime platform validation is still not implemented, so DigitalOcean-specific boot/import verification is still manual
The images are prepared for cloud-init environments with these defaults:
- remote SSH
rootlogin is disabled - SSH password authentication is disabled
- the default cloud user is
blackarch - the default cloud user gets passwordless
sudounless overridden
Manual boot/runtime validation is still your responsibility. This repository does not yet provide a validate-image.sh, QEMU smoke-boot stage, or provider-specific runtime checks.
make helpAvailable targets:
buildRun preflight checks and then invoke./build.sh.check-envValidate the host build environment.lintRunbash -nandshellcheck.cleanRemove build leftovers undertmp/and delete staged output artifacts.