pi-gen carries three dependency lists that disagree, and picks branches by architecture
Tool used to create the official Raspberry Pi OS images
At a glance
- What is it?
- A shell tool that assembles Raspberry Pi OS images out of numbered stage directories. The mechanics are documented closely enough to show where the edges are: the apt line, the Dockerfile and the depends file each name a different tool set, 32 bit and 64 bit builds come from different branches whose combination is never stated, and BASE_DIR is documented as unsafe to change.
- Who is it for?
- pi-gen suits someone who already understands debootstrap and wants a reproducible image with their own stages, and the per-variable documentation is unusually specific about how to read current values off a running system. Before a first build, settle four things.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- RPi-Distro/pi-gen3,287 ★ · +26
| Repository | 2026-09-24 | 2026-11-14 |
|---|---|---|
| RPi-Distro/pi-gen | 3.3k | 3.3k |
26 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Three dependency lists, and no two of them match
There are three places a tool list lives here, and they are not the same list.
The first is the apt line in the file itself:
apt install coreutils quilt parted qemu-user-binfmt debootstrap zerofree zip \
dosfstools e2fsprogs libarchive-tools libcap2-bin grep rsync xz-utils file git curl bc \
gpg pigz xxd arch-test bmap-tools kmodThe second is inside the `Dockerfile`, which installs `qemu-user` rather than `qemu-user-binfmt`, leaves out `bmap-tools`, and adds `vim`, `udev`, `ca-certificates` and `fdisk`, none of which appear in the line above. The third is the `depends` file, whose format is `<tool>[:<debian-package>]`, so a tool name and the Debian package providing it can diverge there in a way neither of the other two can express. A container build therefore installs a slightly different set from a native build, and neither set is generated from `depends`.
Architecture and release pick different branches, with no stated combination
Two branch rules appear near the top, and they are about different axes. The first says 32 bit images should be built from the `master` branch and 64 bit images from the `arm64` branch. The second is inside the `RELEASE` variable, whose default is `trixie`: since different releases carry different sets of packages, a `bullseye` image should be built from the `bullseye` branch.
So there is at least an architecture axis and a release axis, and the file never says how to combine them. Nothing states whether an arm64 build of trixie comes from `arm64`, from a combined branch, or from a checkout configured some other way. There are also no GitHub releases for this repository, so there is no tag to pin either. What a builder actually does is read the branch name, clone it, and set `RELEASE` to match, which means the two values have to be kept consistent by hand.
The container builds on bullseye while the config defaults to trixie
`build-docker.sh` and the `Dockerfile` are the answer offered for anyone not on a Debian host. The image takes its base from a build argument, `ARG BASE_IMAGE=debian:bullseye`, copies the whole tree to `/pi-gen/`, and declares `VOLUME [ "/pi-gen/work", "/pi-gen/deploy"]` so build output and deployment output survive the container. `ENV DEBIAN_FRONTEND=noninteractive` keeps apt from stopping for prompts.
Those two volume paths are the defaults for `WORK_DIR` and `DEPLOY_DIR`, which are derived from `BASE_DIR`, so the declaration lines up with the configuration surface rather than inventing its own layout.
The base image is the part worth pausing on. The container defaults to bullseye while `RELEASE` defaults to trixie, so an unmodified container build is assembling a trixie target from a bullseye userspace. `BASE_IMAGE` is overridable, which makes this a default rather than a wall, but the two defaults point at different Debian releases and nothing in the file mentions the gap.
QEMU is a supported output mode and an unsupported build environment
Two passages describe QEMU and they do not agree on status. `USE_QEMU` defaults to `0`, and setting it to `1` enables a mode that creates an image mountable via QEMU for an emulated environment, with `-qemu` added to the image file name. That reads like a supported feature with a delivery format.
The dependencies section says the opposite about the environment. QEMU is only used outside a native build environment, which is not supported. The advice for anyone who does it anyway is specific: on a distribution whose `qemu-user` binaries are still dynamically linked, install `qemu-user-static` instead of `qemu-user-binfmt`, or the chroot will fail.
That failure mode is the useful part. It is a hard stop with a named cause, and it depends on which qemu package the distribution ships. Note also that the apt line installs `qemu-user-binfmt` while the Dockerfile installs `qemu-user`, so neither documented path installs the static build that the warning points at.
build.sh reads config from wherever you invoked it
`build.sh` sources a file named `config` in the current working directory, and that fragment is a bash shell fragment meant to set environment variables. Reading configuration from the invocation directory rather than from `BASE_DIR` means the same checkout produces different images depending on where the command was typed, which matters most for anyone scripting builds or running them from a parent directory.
`BASE_DIR` is the directory holding stage directories, build scripts and, by default, both the work and deployment directories. Its own entry carries a caution: changing this value will probably break build.sh. That is an unusual thing to find in a variable you are invited to set, and it is why the Docker volume declaration lines up with the defaults rather than inviting relocation.
Two variables change what ends up inside the image rather than where files land. `APT_PROXY` is deliberately not included in the image, which is what makes an `apt-cacher` or similar safe to use during development. `TEMP_REPO` adds an extra apt repository in one-line-style format for pulling in pre-release software, with `RELEASE` substituted into it.
The work directory keeps a full copy of the target for every stage
`WORK_DIR` defaults to `$BASE_DIR/work` and is the directory the target system is built in. The entry states plainly that it stores a complete copy of the target system for each build stage, amounting to tens of gigabytes in the case of Raspbian. Stage count is visible in the tree, which runs from `stage0` through `stage5`, and each one of those copies is a full filesystem rather than an increment.
The variable can be moved if you have a suitably large and fast storage location for stages to be built and cached. The filesystem constraint is stated more firmly than the size one: if the working directory is on an NTFS partition you probably will not be able to build, and the instruction is to make sure it is a proper Linux filesystem.
`DEPLOY_DIR` defaults to `$BASE_DIR/deploy` and is the output directory for target system images and NOOBS bundles. Two top level directories match that split, `export-image/` and `export-noobs/`, so a build produces more than one kind of artifact and the two are produced by separate scripts.
A deprecated switch still changes your output, and the four extensions disagree
`DEPLOY_ZIP` is marked deprecated in favour of `DEPLOY_COMPRESSION`, and that is the whole of the deprecation notice. It still has behaviour: if `DEPLOY_ZIP=0` is present in a config file, the result is the same as `DEPLOY_COMPRESSION=none`. So a knob that is officially retired remains capable of changing the artifact for anyone who copied an older config and never removed the line.
`DEPLOY_COMPRESSION` takes four values, and the resulting file names are not symmetrical. `none` deploys the actual image as `.img`. `zip` deploys a `.zip`. `gz` deploys a `.img.gz`. `xz` deploys a `.img.xz`. The zip case is the odd one out, since compressing a disk image normally yields an extension appended to `.img` rather than replacing it.
`COMPRESSION_LEVEL` defaults to `6` and runs from 0 to 9, with the trade-off spelled out: 0 is no compression but very fast, 9 gives the best compression but is very slow.
The clone path cannot contain spaces, and the default release string is reserved
Getting started is one clone:
git clone https://github.com/RPI-Distro/pi-gen.git`--depth 1` can be added for a shallow clone containing only the latest revision, with the instruction not to do this on a development machine. The path matters more than the depth: clone to a base path not containing spaces, because debootstrap does not support that configuration and pi-gen will not run.
`PI_GEN_RELEASE` defaults to `Raspberry Pi reference` and is used in `/etc/issue.txt`, with the note that the default should only be used for official Raspberry Pi builds. A custom image that never sets it ships a release string claiming the official reference build.
`IMG_NAME` defaults to `raspios-$RELEASE-$ARCH`, for example `raspios-trixie-armhf`, and export files in stages may add suffixes to it, so the final name is not fully determined by the config. The first-boot user works the same way: `FIRST_USER_NAME` defaults to `pi`, that user exists only during image creation, and unless `DISABLE_FIRST_BOOT_USER_RENAME` is set to `1` it is renamed on first boot to a name the final user chooses. The entry for `FIRST_USER_PASS` opens with its default, unset, and its description stops after two letters.
Editorial conclusion
pi-gen suits someone who already understands debootstrap and wants a reproducible image with their own stages, and the per-variable documentation is unusually specific about how to read current values off a running system. Before a first build, settle four things. Which branch you are on, because 32 bit comes from master, 64 bit from arm64, and a bullseye image needs the bullseye branch, with no stated rule for combining the two. Which dependency list you trust, since the apt line, the Dockerfile and the depends file differ. Where your work directory lives, since it holds a full copy of the target per stage and BASE_DIR is marked as probably breaking build.sh. And what your image claims to be, since the default release string is reserved for official Raspberry Pi builds and ends up in /etc/issue.txt.
Frequently asked questions
how to use pi gen
Install the apt dependencies, clone the repository to a path without spaces, create a `config` shell fragment in the directory you will run from, and run `build.sh`, which sources that file for its variables. 32 bit images are built from the `master` branch and 64 bit images from `arm64`.
pi gen alternative
This repository names no alternative tool. What it offers instead of a native Debian host is a Docker path: `build-docker.sh` with a `Dockerfile` whose base comes from the `BASE_IMAGE` build argument, defaulting to `debian:bullseye`.
Which pi-gen branch should a 64 bit trixie build come from?
The stated rules are that 64 bit images come from the `arm64` branch and that a `bullseye` image comes from the `bullseye` branch, while `RELEASE` defaults to `trixie`. No rule is given for combining the architecture axis with the release axis, and the repository has no GitHub releases to pin instead.
What does pi-gen need before it will build an image?
A Debian-based operating system released after 2017, with the packages from the apt line installed, or the Docker build on other distributions. The `depends` file names the same category of tools in the format `<tool>[:<debian-package>]`, though its contents differ from both that line and the Dockerfile's.
How does pi-gen handle the default pi user?
`FIRST_USER_NAME` defaults to `pi` and that account exists only while the image is being created. Unless `DISABLE_FIRST_BOOT_USER_RENAME` is set to `1`, it is renamed on first boot to a name the final user picks, which the file describes as a security feature against shipping a default username. If `FIRST_USER_NAME` is `pi` and `FIRST_USER_PASS` is unset, the setup wizard launches on first boot.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rpi-distro-pi-gen)Star history chart for your README
[](https://hysenlabs.com/projects/rpi-distro-pi-gen)Was this useful?