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/ and casper/): Files that UEFI loads to start the installer.

  • Embedded apt repository (pool/ and dists/): Packages and package metadata that the installer uses during installation without requiring a network connection.

  • Squashfs layers (casper/*.squashfs): OS file system layers, such as ubuntu-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.squashfs and ubuntu-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

*.nvidia.squashfs/opt/nvidia/ptable.<chip>.<storage>.dynamic

Partition Table

autoinstall and subiquity behavior

*.nvidia.squashfs/ai/jetsoniso_<platform>-ai.yaml

autoinstall.yaml

Bootloader or UEFI capsule, such as pinmux and BCT changes

Customized nvidia-l4t-bootloader_*.deb added through the ISO apt repository.

Bootloader/Capsule Customization

Add or replace apt packages

pool/ and dists/; OS squashfs for the target apt signing key.

Debian Package Repository

GRUB menu or kernel command line

boot/grub/grub.cfg

GRUB Menu

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

ptable.orin.mmc.dynamic

Jetson Orin

eMMC (mmcblk0) or microSD (mmcblk1)

ptable.orin.nvme.dynamic

Jetson Orin

NVMe (nvme0n1)

ptable.orin.sdx.dynamic

Jetson Orin

USB

ptable.thor.nvme.dynamic

Jetson Thor

NVMe

ptable.thor.sdx.dynamic

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 invokes preseed.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

apt

Disables network suites and sets dpkg conflict-resolution options.

storage

Contains the partition layout, conditionally activated by #CHANGE_STORAGE_DO_* comments at runtime.

reporting

Routes installer progress output to the system log.

user-data

Provides a minimal cloud-init stub.

late-commands

Contains the main installation logic, including writing nv_boot_control.conf, configuring apt sources, installing packages from the ISO pool, installing the bootloader again, and optionally adding network apt sources.

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.

GRUB Menu#

The GRUB menu, including timeout, menu entries, and kernel command lines, lives at boot/grub/grub.cfg in the ISO tree. Edit it directly in the extracted work directory:

INPUT_ISO=jetson-iso.iso
OUTPUT_ISO=jetson-iso-custom.iso

MNTDIR=$(mktemp -d)
WORKDIR=$(mktemp -d)

# 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"

# Edit the GRUB menu.
nano "$WORKDIR/boot/grub/grub.cfg"

# Update md5sum.txt for grub.cfg.
NEW_HASH=$(md5sum "$WORKDIR/boot/grub/grub.cfg" | awk '{print $1}')
sed -i "s|^[0-9a-f]\+  \./boot/grub/grub\.cfg$|${NEW_HASH}  ./boot/grub/grub.cfg|" \
    "$WORKDIR/md5sum.txt"

xorriso -as mkisofs \
    $BOOT_OPTS \
    -o "$OUTPUT_ISO" \
    "$WORKDIR/"

The following kernel command-line parameters are useful:

  • force-bootdisk=<device>: Sets the target storage device, such as nvme0n1 or mmcblk0. Omit this parameter for automatic detection.

  • autoinstall: Triggers unattended installation.

  • loglevel=<n>: Sets kernel log verbosity.

  • subiquity.debug: Enables verbose installer logs.

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.