Jetson ISO Customization Guide#
This topic describes how to customize an already-built Jetson ISO image without running a full build.
A Jetson ISO image file is a self-contained, UEFI-bootable installer composed of the following:
GRUB, kernel, and initrd (
boot/andcasper/): Files that UEFI loads to start the installer.Embedded apt repository (
pool/anddists/): Packages and package metadata that the installer uses during installation without requiring a network connection.Squashfs layers (
casper/*.squashfs): OS file system layers, such asubuntu-server-minimal*.squashfs, plus a custom NVIDIA® overlay, such as*.nvidia.squashfs.GPT and appended ESP: UEFI reads partition 2, an embedded FAT32 image, to find
BOOTAA64.efi.md5sum.txt: Checksum entries for ISO files. Modified files need updated entries.
Every workflow in this topic follows the same pattern: capture El Torito boot
parameters, extract the ISO tree, modify files in place, update
md5sum.txt, and repack with xorriso -as mkisofs.
Live Installer Rootfs and Target Rootfs#
When choosing which squashfs to edit, understand the difference between the live installer rootfs and the target rootfs:
Live installer rootfs: All squashfs layers merged, including
*.nvidia.squashfs. This rootfs is where subiquity runs. Files in the NVIDIA overlay, such as ptable files, autoinstall YAML files, and setup scripts, exist only in the installer environment and do not reach the installed system.Target rootfs: The OS layers only, such as
ubuntu-server-minimal.squashfsandubuntu-server-minimal.ubuntu-server.squashfs. The NVIDIA overlay is not included. Anything that must be present on the installed system must be added to an OS squashfs.
Customization Options#
What to Change |
Where It Lives |
Details |
|---|---|---|
Target disk partition layout |
|
|
autoinstall and subiquity behavior |
|
|
Bootloader or UEFI capsule, such as pinmux and BCT changes |
Customized |
|
Add or replace apt packages |
|
|
GRUB menu or kernel command line |
|
Prerequisites#
Install the required host tools:
$ sudo apt install squashfs-tools xorriso reprepro dpkg-dev
All workflows require root privileges because loop mounts and squashfs operations require them.
ISO Structure#
The ISO contains the following high-level structure:
<iso>/
|-- casper/
| |-- Image # kernel
| |-- initrd # initramfs
| |-- *.squashfs # OS file system layers
| `-- *.nvidia.squashfs # NVIDIA overlay
|-- boot/grub/
| |-- grub.cfg # GRUB menu
| `-- efi.img # FAT32 EFI partition image
|-- pool/ # Debian package files
|-- dists/ # apt repo metadata
`-- md5sum.txt # checksums for ISO files
Repacking Approach#
All modifications extract the full ISO tree to a work directory, make changes in
place, and then repack by using xorriso -as mkisofs with El Torito boot
parameters captured from the source ISO by using
-report_el_torito as_mkisofs:
# 1. Capture El Torito boot parameters from the source ISO.
BOOT_OPTS=$(xorriso -indev input.iso -report_el_torito as_mkisofs 2>/dev/null)
# Jetson ISOs are built with -partition_offset 16, but -report_el_torito
# reports it as 0. Force-correct it so the appended ESP byte range resolves.
BOOT_OPTS=$(echo "$BOOT_OPTS" | sed 's/-partition_offset 0/-partition_offset 16/')
# 2. Extract the full ISO tree to a work directory.
MNTDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)
sudo mount -o loop,ro input.iso "$MNTDIR"
sudo cp -a "$MNTDIR/." "$WORKDIR/"
sudo umount "$MNTDIR"
sudo chmod -R u+w "$WORKDIR"
sudo chown -R "$USER:$USER" "$WORKDIR"
# 3. Modify files directly in $WORKDIR.
# 4. Update $WORKDIR/md5sum.txt for every changed file.
# 5. Repack.
xorriso -as mkisofs \
$BOOT_OPTS \
-o output.iso \
"$WORKDIR/"
The -report_el_torito as_mkisofs option outputs boot catalog arguments in
mkisofs format so that the output ISO keeps the same boot geometry as the source
ISO.
Note
-report_el_torito as_mkisofs does not reliably round-trip
-partition_offset. Jetson ISOs are built with -partition_offset 16,
but the reporter emits -partition_offset 0. If you use the reported
value as is, the output ISO GPT layout shifts, and the
--interval:local_fs:START-END byte range used for the appended ESP no
longer points at the embedded efi.img. The preceding example restores
-partition_offset 16 after capture.
Partition Table#
Partition tables are stored inside the NVIDIA squashfs at
/opt/nvidia/ptable.<chip>.<storage>.dynamic. The correct table is selected
during installation based on the detected board and storage device.
File |
Chip |
Storage |
|---|---|---|
|
Jetson Orin |
eMMC ( |
|
Jetson Orin |
NVMe ( |
|
Jetson Orin |
USB |
|
Jetson Thor |
NVMe |
|
Jetson Thor |
USB |
Each file uses sfdisk partition table format. The
CHANGE_BOOT_DISK_NAME_1 placeholder is substituted at runtime by
jetsoniso_setup.bash before the ptable file is piped to sfdisk. Preserve
that placeholder.
Dumping the Current Partition Table from an ISO#
The safest starting point is to extract the ptable file directly from the ISO that you are customizing:
MNTDIR=$(mktemp -d)
SQ_DIR=$(mktemp -d)
sudo mount -o loop,ro jetson-iso.iso "$MNTDIR"
NVIDIA_SQ=$(find "$MNTDIR/casper" -name "*.nvidia.squashfs" | head -1)
sudo unsquashfs -p 32 -f -d "$SQ_DIR/nvidia_fs" "$NVIDIA_SQ"
sudo umount "$MNTDIR"
# List all ptable files.
ls "$SQ_DIR/nvidia_fs/opt/nvidia/"ptable.*.dynamic
# Inspect a specific one.
cat "$SQ_DIR/nvidia_fs/opt/nvidia/ptable.orin.nvme.dynamic"
The output is valid sfdisk dump format and can be used directly as a
template. For example, ptable.orin.nvme.dynamic looks like this:
label: gpt
device: /dev/CHANGE_BOOT_DISK_NAME_1
unit: sectors
first-lba: 40
sector-size: 512
/dev/CHANGE_BOOT_DISK_NAME_1p1 : start= 3131968, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="APP"
/dev/CHANGE_BOOT_DISK_NAME_1p2 : start= 40, size= 262144, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="A_kernel"
/dev/CHANGE_BOOT_DISK_NAME_1p3 : start= 262184, size= 1536, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="A_kernel-dtb"
/dev/CHANGE_BOOT_DISK_NAME_1p4 : start= 263720, size= 64768, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="reserved_for_chain_A_user"
/dev/CHANGE_BOOT_DISK_NAME_1p5 : start= 328488, size= 262144, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="B_kernel"
/dev/CHANGE_BOOT_DISK_NAME_1p6 : start= 590632, size= 1536, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="B_kernel-dtb"
/dev/CHANGE_BOOT_DISK_NAME_1p7 : start= 592168, size= 64768, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="reserved_for_chain_B_user"
/dev/CHANGE_BOOT_DISK_NAME_1p8 : start= 656936, size= 204800, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="recovery"
/dev/CHANGE_BOOT_DISK_NAME_1p9 : start= 861736, size= 1024, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="recovery-dtb"
/dev/CHANGE_BOOT_DISK_NAME_1p10 : start= 862760, size= 131072, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="esp"
/dev/CHANGE_BOOT_DISK_NAME_1p11 : start= 993832, size= 204800, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="recovery_alt"
/dev/CHANGE_BOOT_DISK_NAME_1p12 : start= 1198632, size= 1024, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="recovery-dtb_alt"
/dev/CHANGE_BOOT_DISK_NAME_1p13 : start= 1199656, size= 131072, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="esp_alt"
/dev/CHANGE_BOOT_DISK_NAME_1p14 : start= 1330752, size= 819200, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="UDA"
/dev/CHANGE_BOOT_DISK_NAME_1p15 : start= 2149952, size= 982016, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name="reserved"
The GUID EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 is the Microsoft Basic
Data GUID, used for the non-ESP partitions. The GUID
C12A7328-F81F-11D2-BA4B-00A0C93EC93B is the EFI System Partition GUID.
The APP partition, p1, intentionally has no size= field. sfdisk
extends it to fill the remainder of the disk. Its actual byte size is measured
after partitioning and substituted into the autoinstall YAML storage block
through the *_DYNAMIC_APP_SIZE placeholders. That substitution happens
against the YAML file, not against the ptable file.
Editing a Partition Table#
Use the following flow to edit a ptable file and rebuild the NVIDIA squashfs:
INPUT_ISO=jetson-iso.iso
OUTPUT_ISO=jetson-iso-custom.iso
MNTDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)
SQ_DIR=$(mktemp -d)
# 1. Capture boot parameters and extract the full ISO tree.
BOOT_OPTS=$(xorriso -indev "$INPUT_ISO" -report_el_torito as_mkisofs 2>/dev/null)
BOOT_OPTS=$(echo "$BOOT_OPTS" | sed 's/-partition_offset 0/-partition_offset 16/')
sudo mount -o loop,ro "$INPUT_ISO" "$MNTDIR"
sudo cp -a "$MNTDIR/." "$WORKDIR/"
sudo umount "$MNTDIR"
sudo chmod -R u+w "$WORKDIR"
sudo chown -R "$USER:$USER" "$WORKDIR"
# 2. Find and extract the NVIDIA squashfs.
NVIDIA_SQ=$(find "$WORKDIR/casper" -name "*.nvidia.squashfs" | head -1)
SQ_NAME=$(basename "$NVIDIA_SQ")
sudo unsquashfs -p 32 -f -d "$SQ_DIR/nvidia_fs" "$NVIDIA_SQ"
# 3. Edit the target partition table file.
sudo nano "$SQ_DIR/nvidia_fs/opt/nvidia/ptable.orin.nvme.dynamic"
# 4. Rebuild squashfs and replace it in the work directory.
COMP=$(unsquashfs -s "$NVIDIA_SQ" | awk '/^Compression/ {print $2}')
TMPFS=$(mktemp --suffix=.squashfs)
sudo mksquashfs "$SQ_DIR/nvidia_fs" "$TMPFS" \
-b 1048576 -noappend -comp "${COMP:-gzip}"
sudo mv "$TMPFS" "$WORKDIR/casper/$SQ_NAME"
# 5. Update md5sum.txt.
NEW_HASH=$(md5sum "$WORKDIR/casper/$SQ_NAME" | awk '{print $1}')
sed -i "s|^[0-9a-f]\+ \./casper/${SQ_NAME}$|${NEW_HASH} ./casper/${SQ_NAME}|" \
"$WORKDIR/md5sum.txt"
# 6. Repack.
xorriso -as mkisofs \
$BOOT_OPTS \
-o "$OUTPUT_ISO" \
"$WORKDIR/"
autoinstall.yaml#
The installer uses two levels of configuration inside the NVIDIA squashfs:
/autoinstall.yaml: A thin wrapper that invokespreseed.sh./ai/jetsoniso_orin-ai.yaml: Jetson Orin install configuration./ai/jetsoniso_thor-ai.yaml: Jetson Thor install configuration./ai/jetsoniso_setup.bash: Runtime board detection and final substitutions.
Each platform YAML file, such as jetsoniso_<platform>-ai.yaml, is an Ubuntu
autoinstall document with the following top-level sections:
Section |
Purpose |
|---|---|
|
Disables network suites and sets dpkg conflict-resolution options. |
|
Contains the partition layout, conditionally activated by
|
|
Routes installer progress output to the system log. |
|
Provides a minimal cloud-init stub. |
|
Contains the main installation logic, including writing
|
Most customizations target late-commands. The storage section is the
other common target when changing partition layouts. When modifying partition layouts, prefer editing the ptable
files described in Partition Table.
Runtime placeholders, such as CHANGE_BOOT_DISK_NAME_1, CHANGE_TNSPEC,
and #CHANGE_STORAGE_DO_*, are resolved by jetsoniso_setup.bash during
installation and must remain intact.
To edit an autoinstall YAML file, use the same extract, rebuild, checksum, and
repack flow used for ptable changes. Edit the appropriate platform file in
$SQ_DIR/nvidia_fs/ai/:
sudo nano "$SQ_DIR/nvidia_fs/ai/jetsoniso_orin-ai.yaml"
Bootloader/Capsule Customization#
Do not patch boot/grub/efi.img directly inside the ISO. For custom
bootloader configuration, such as pinmux, BPMP DTB, partition layout, or BCT
changes, build a customized Bootloader Update Payload (BUP), ship it as a custom
nvidia-l4t-bootloader_*.deb file, and then add that Debian package to the ISO
apt repository by following Debian Package Repository.
The installer pulls the Debian package during installation and applies the BUP or capsule like any other bootloader update. For more information about BUP and capsule generation, refer to Update and Redundancy.
Debian Package Repository#
The ISO carries a self-contained apt repository in pool/ for Debian package
files and in dists/ for index metadata. Use reprepro to regenerate the
index after adding or replacing packages.
Official repositories are GPG-signed. reprepro regenerates
dists/<codename>/Release whenever the package index changes, which
invalidates the original InRelease signature. The following steps re-sign
the repository with a local key and embed the matching public key in the OS
squashfs that subiquity copies into /target. This procedure lets the target apt trust
the re-signed repository during installation.
Add or Replace a Package#
The following example shows how to add or replace a package in the ISO apt repository:
INPUT_ISO=jetson-iso.iso
OUTPUT_ISO=jetson-iso-custom.iso
NEW_DEB=my-package_1.0_arm64.deb
DISTRO=noble
MNTDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)
SQ_DIR=$(mktemp -d)
# 1. Capture boot parameters and extract the full ISO tree.
BOOT_OPTS=$(xorriso -indev "$INPUT_ISO" -report_el_torito as_mkisofs 2>/dev/null)
BOOT_OPTS=$(echo "$BOOT_OPTS" | sed 's/-partition_offset 0/-partition_offset 16/')
sudo mount -o loop,ro "$INPUT_ISO" "$MNTDIR"
sudo cp -a "$MNTDIR/." "$WORKDIR/"
sudo umount "$MNTDIR"
sudo chmod -R u+w "$WORKDIR"
sudo chown -R "$USER:$USER" "$WORKDIR"
# 2. Create a local repo signing key if one does not already exist.
if ! gpg --list-keys "Jetson ISO Repo Signing Key" &>/dev/null; then
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: Jetson ISO Repo Signing Key
Expire-Date: 0
EOF
fi
KEY_FP=$(gpg --list-keys --with-colons "Jetson ISO Repo Signing Key" \
| awk -F: '/^fpr/{print $10; exit}')
# 3. Add the new package to pool/. Remove the old version first if replacing.
DEB_PREFIX=$(echo "$NEW_DEB" | cut -c1)
DEB_PKGNAME=$(echo "$NEW_DEB" | cut -d_ -f1)
mkdir -p "$WORKDIR/pool/main/${DEB_PREFIX}/${DEB_PKGNAME}"
cp "$NEW_DEB" "$WORKDIR/pool/main/${DEB_PREFIX}/${DEB_PKGNAME}/"
# 4. Rebuild the repo index with reprepro.
mkdir -p "$WORKDIR/conf"
cat > "$WORKDIR/conf/distributions" <<EOF
Origin: NVIDIA
Label: NVIDIA Server
Suite: ${DISTRO}
Codename: ${DISTRO}
Architectures: arm64
Components: main restricted
Description: Installable packages for NVIDIA Server
SignWith: ${KEY_FP}
EOF
# Re-index all .deb files in pool/, not just the new package.
find "$WORKDIR/pool" -name "*.deb" -print0 \
| xargs -0 reprepro --basedir "$WORKDIR" --conf "$WORKDIR/conf" \
includedeb "$DISTRO"
# 5. Update md5sum.txt for all changed dists/ and pool/ files.
while IFS= read -r -d '' f; do
rel="${f#"$WORKDIR/"}"
HASH=$(md5sum "$f" | awk '{print $1}')
if grep -q " \./${rel}$" "$WORKDIR/md5sum.txt"; then
sed -i "s|^[0-9a-f]\+ \./${rel}$|${HASH} ./${rel}|" \
"$WORKDIR/md5sum.txt"
else
echo "${HASH} ./${rel}" >> "$WORKDIR/md5sum.txt"
fi
done < <(find "$WORKDIR/dists" "$WORKDIR/pool" -type f -print0)
# 6. Embed the public key in the OS squashfs so the target apt trusts it.
TARGET_SQ=$(find "$WORKDIR/casper" -name "ubuntu-server-minimal.ubuntu-server.squashfs" | head -1)
SQ_NAME=$(basename "$TARGET_SQ")
OS_DIR=$(mktemp -d)
sudo unsquashfs -p 32 -f -d "$OS_DIR/os_fs" "$TARGET_SQ"
sudo mkdir -p "$OS_DIR/os_fs/etc/apt/trusted.gpg.d"
gpg --export "$KEY_FP" \
| sudo tee "$OS_DIR/os_fs/etc/apt/trusted.gpg.d/custom_repo.gpg" > /dev/null
COMP=$(unsquashfs -s "$TARGET_SQ" | awk '/^Compression/ {print $2}')
TMPFS=$(mktemp --suffix=.squashfs)
sudo mksquashfs "$OS_DIR/os_fs" "$TMPFS" \
-b 1048576 -noappend -comp "${COMP:-gzip}"
sudo mv "$TMPFS" "$WORKDIR/casper/$SQ_NAME"
NEW_SQ_HASH=$(md5sum "$WORKDIR/casper/$SQ_NAME" | awk '{print $1}')
sed -i "s|^[0-9a-f]\+ \./casper/${SQ_NAME}$|${NEW_SQ_HASH} ./casper/${SQ_NAME}|" \
"$WORKDIR/md5sum.txt"
# 7. Repack.
xorriso -as mkisofs \
$BOOT_OPTS \
-o "$OUTPUT_ISO" \
"$WORKDIR/"
To change the installed package list, add the package name to the platform
package list file inside the NVIDIA® squashfs, at /ai/jetsoniso-pkgs.
This squashfs is separate from the one modified to embed the signing key.
Follow the extract and rebuild flow from autoinstall.yaml to edit
$SQ_DIR/nvidia_fs/ai/jetsoniso-pkgs.
If you do not want to add a signing key, skip the key generation and OS
squashfs key-embedding steps, remove the SignWith line from
conf/distributions, and modify the autoinstall YAML inside the
NVIDIA squashfs to add trusted=yes to the cdrom apt source. For
example, change deb [check-date=no] to
deb [check-date=no trusted=yes] for the file:///tmp/cdrom/ line.
The trusted=yes option bypasses GPG verification only. Hash checks on
individual packages remain intact.
Combining Multiple Changes#
Because all workflows share a single extracted $WORKDIR, you can combine
changes by applying all modifications to the same work directory, updating
md5sum.txt for every changed file, and producing the output ISO with one
xorriso call:
BOOT_OPTS=$(xorriso -indev input.iso -report_el_torito as_mkisofs 2>/dev/null)
BOOT_OPTS=$(echo "$BOOT_OPTS" | sed 's/-partition_offset 0/-partition_offset 16/')
MNTDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)
sudo mount -o loop,ro input.iso "$MNTDIR"
sudo cp -a "$MNTDIR/." "$WORKDIR/"
sudo umount "$MNTDIR"
sudo chmod -R u+w "$WORKDIR"
sudo chown -R "$USER:$USER" "$WORKDIR"
# Apply squashfs modifications and update $WORKDIR/casper/<name>.squashfs.
# Edit $WORKDIR/boot/grub/grub.cfg.
# Update $WORKDIR/md5sum.txt for all changed files.
xorriso -as mkisofs \
$BOOT_OPTS \
-o output.iso \
"$WORKDIR/"
Before repacking, the md5sum.txt update must reflect all changed files.