Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,92 +19,85 @@ let
# sdImage containing ESP and root partitions (compressed)
images = config.system.build.sdImage;

# Partition XML with placeholders (substituted at flash time by preFlashCommands)
partitionsEmmc = pkgs.writeText "sdmmc.xml" ''
<partition name="master_boot_record" type="protective_master_boot_record">
<allocation_policy> sequential </allocation_policy>
<filesystem_type> basic </filesystem_type>
<size> 512 </size>
<file_system_attribute> 0 </file_system_attribute>
<allocation_attribute> 8 </allocation_attribute>
<percent_reserved> 0 </percent_reserved>
</partition>
<partition name="primary_gpt" type="primary_gpt">
<allocation_policy> sequential </allocation_policy>
<filesystem_type> basic </filesystem_type>
<size> 19968 </size>
<file_system_attribute> 0 </file_system_attribute>
<allocation_attribute> 8 </allocation_attribute>
<percent_reserved> 0 </percent_reserved>
</partition>
<partition name="esp" id="2" type="data">
<allocation_policy> sequential </allocation_policy>
<filesystem_type> basic </filesystem_type>
<size> ESP_SIZE </size>
<file_system_attribute> 0 </file_system_attribute>
<allocation_attribute> 0x8 </allocation_attribute>
<percent_reserved> 0 </percent_reserved>
<filename> bootloader/esp.img </filename>
<partition_type_guid> C12A7328-F81F-11D2-BA4B-00A0C93EC93B </partition_type_guid>
<description> EFI system partition with systemd-boot. </description>
</partition>
<partition name="APP" id="1" type="data">
<allocation_policy> sequential </allocation_policy>
<filesystem_type> basic </filesystem_type>
<size> ROOT_SIZE </size>
<file_system_attribute> 0 </file_system_attribute>
<allocation_attribute> 0x8 </allocation_attribute>
<align_boundary> 16384 </align_boundary>
<percent_reserved> 0x808 </percent_reserved>
<unique_guid> APPUUID </unique_guid>
<filename> root.img </filename>
<description> **Required.** Contains the rootfs. This partition must be assigned
the "1" for id as it is physically put to the end of the device, so that it
can be accessed as the fixed known special device `/dev/mmcblk0p1`. </description>
</partition>
<partition name="secondary_gpt" type="secondary_gpt">
<allocation_policy> sequential </allocation_policy>
<filesystem_type> basic </filesystem_type>
<size> 0xFFFFFFFFFFFFFFFF </size>
<file_system_attribute> 0 </file_system_attribute>
<allocation_attribute> 8 </allocation_attribute>
<percent_reserved> 0 </percent_reserved>
</partition>
'';

# Line counts for replacing the sdmmc_user device section in NVIDIA's flash XML.
# These numbers specify where to splice our custom partition layout.
# eMMC partition layout as structured Nix data.
# Serialized to JSON and spliced into NVIDIA's flash XML by
# splice-flash-xml.py, which replaces the <device type="sdmmc_user">
# children. This avoids fragile line-count splicing.
#
# WARNING: When updating jetpack-nixos/BSP version, verify these line counts
# still match the <device type="sdmmc_user"> section boundaries in:
# - flash_t234_qspi_sdmmc.xml (standard)
# - flash_t234_qspi_sdmmc_industrial.xml (industrial variant)
partitionTemplateReplaceRange =
if (config.hardware.nvidia-jetpack.som == "orin-agx-industrial") then
if (!cfg.flashScriptOverrides.onlyQSPI) then
{
firstLineCount = 631;
lastLineCount = 2;
}
else
{
# QSPI-only: remove entire sdmmc_user device section
firstLineCount = 630;
lastLineCount = 1;
}
else if !cfg.flashScriptOverrides.onlyQSPI then
{
firstLineCount = 618;
lastLineCount = 2;
}
else
{
# QSPI-only: remove entire sdmmc_user device section
firstLineCount = 617;
lastLineCount = 1;
# Partition sizes are injected at build time from the sdImage metadata
# via --set (sectors * 512 → bytes).
partitionsEmmc = [
{
name = "master_boot_record";
type = "protective_master_boot_record";
children = {
allocation_policy = "sequential";
filesystem_type = "basic";
size = "512";
file_system_attribute = "0";
allocation_attribute = "8";
percent_reserved = "0";
};
}
{
name = "primary_gpt";
type = "primary_gpt";
children = {
allocation_policy = "sequential";
filesystem_type = "basic";
size = "19968";
file_system_attribute = "0";
allocation_attribute = "8";
percent_reserved = "0";
};
}
{
name = "esp";
type = "data";
children = {
allocation_policy = "sequential";
filesystem_type = "basic";
size = "0"; # overridden by --set from sdImage metadata at build time
file_system_attribute = "0";
allocation_attribute = "0x8";
percent_reserved = "0";
filename = "bootloader/esp.img";
partition_type_guid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
description = "EFI system partition with systemd-boot.";
};
}
{
name = "APP";
type = "data";
children = {
allocation_policy = "sequential";
filesystem_type = "basic";
size = "0"; # overridden by --set from sdImage metadata at build time
file_system_attribute = "0";
allocation_attribute = "0x8";
align_boundary = "16384";
percent_reserved = "0x808";
unique_guid = "APPUUID";
filename = "root.img";
description = "Contains the rootfs, placed at the end of the device as /dev/mmcblk0p1.";
};
}
{
name = "secondary_gpt";
type = "secondary_gpt";
children = {
allocation_policy = "sequential";
filesystem_type = "basic";
size = "0xFFFFFFFFFFFFFFFF";
file_system_attribute = "0";
allocation_attribute = "8";
percent_reserved = "0";
};
}
];

# Build the final flash.xml by splicing our partition layout into NVIDIA's template
# Build the final flash.xml by replacing the sdmmc_user partitions
# in NVIDIA's template with our layout using XML-aware splicing.
partitionTemplate =
let
inherit (pkgs.nvidia-jetpack) bspSrc;
Expand All @@ -115,17 +108,19 @@ let
else
"${bspSrc}/bootloader/generic/cfg/flash_t234_qspi_sdmmc.xml";
in
pkgs.runCommand "flash.xml" { } (
''
head -n ${toString partitionTemplateReplaceRange.firstLineCount} ${xmlFile} >"$out"
Copy link
Contributor Author

@Mic92 Mic92 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seemed a bit fragile, hence the PR

''
+ lib.optionalString (!cfg.flashScriptOverrides.onlyQSPI) ''
cat ${partitionsEmmc} >>"$out"
''
+ ''
tail -n ${toString partitionTemplateReplaceRange.lastLineCount} ${xmlFile} >>"$out"
pkgs.runCommand "flash.xml"
{
nativeBuildInputs = [ pkgs.buildPackages.python3 ];
}
''
);
python3 ${./splice-flash-xml.py} \
${lib.optionalString cfg.flashScriptOverrides.onlyQSPI "--remove-device"} \
--set "esp.size=$(($(cat ${images}/esp.size) * 512))" \
--set "APP.size=$(($(cat ${images}/root.size) * 512))" \
${xmlFile} \
${pkgs.writeText "sdmmc.json" (builtins.toJSON partitionsEmmc)} \
"$out"
'';

# preFlashCommands: Extract images from sdImage and patch flash.xml
preFlashScript = pkgs.writeShellApplication {
Expand Down Expand Up @@ -171,12 +166,10 @@ let
bs=512 iseek="$ROOT_OFFSET" count="$ROOT_SIZE" status=progress

echo ""
echo "Patching flash.xml with image paths and sizes..."
echo "Patching flash.xml with image paths..."
sed -i \
-e "s#bootloader/esp.img#$WORKDIR/bootloader/esp.img#" \
-e "s#root.img#$WORKDIR/bootloader/root.img#" \
-e "s#ESP_SIZE#$((ESP_SIZE * 512))#" \
-e "s#ROOT_SIZE#$((ROOT_SIZE * 512))#" \
flash.xml
''}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
# SPDX-License-Identifier: Apache-2.0
"""Replace the sdmmc_user device partitions in NVIDIA's flash XML.

Reads NVIDIA's flash_t234_qspi_sdmmc*.xml, finds the
<device type="sdmmc_user"> element, replaces all its <partition>
children with partitions defined in a JSON file, and writes the
result. This avoids fragile line-count splicing that breaks when
the upstream XML changes.

Usage:
splice-flash-xml.py [--set PARTITION.FIELD=VALUE]...
[--remove-device]
<nvidia-flash.xml> <partitions.json> <output.xml>

The JSON file is a list of partition objects, each with:
- name (str): partition name attribute
- type (str): partition type attribute
- children (dict): child element tag -> text content

The --set flag overrides a child element value for a named partition.
For example: --set APP.size=12345678

The --remove-device flag removes the sdmmc_user device element entirely
(for QSPI-only flashing where no eMMC partitions are needed).
"""

import argparse
import json
import xml.etree.ElementTree as ET
from pathlib import Path


def parse_set_value(value: str) -> tuple[str, str, str]:
"""Parse 'PARTITION.FIELD=VALUE' into (partition_name, field, value)."""
lhs, _, rhs = value.partition("=")
if not rhs:
msg = f"--set requires PARTITION.FIELD=VALUE, got: {value!r}"
raise argparse.ArgumentTypeError(msg)
part_name, _, field = lhs.partition(".")
if not field:
msg = f"--set requires PARTITION.FIELD=VALUE, got: {value!r}"
raise argparse.ArgumentTypeError(msg)
return part_name, field, rhs


def splice_partitions(
flash_xml: Path,
partitions_json: Path,
output: Path,
*,
overrides: list[tuple[str, str, str]],
remove_device: bool = False,
) -> None:
"""Replace sdmmc_user partitions in NVIDIA's flash XML with our layout."""
tree = ET.parse(flash_xml)
root = tree.getroot()

sdmmc_user = next(
(d for d in root.iter("device") if d.get("type") == "sdmmc_user"),
None,
)
if sdmmc_user is None:
msg = f"No <device type='sdmmc_user'> found in {flash_xml}"
raise ValueError(msg)

if remove_device:
# QSPI-only: remove the entire sdmmc_user device element
root.remove(sdmmc_user)
else:
# Remove all existing children
for child in list(sdmmc_user):
sdmmc_user.remove(child)

partitions: list[dict[str, str | dict[str, str]]] = json.loads(
partitions_json.read_text()
)

# Apply --set overrides
for part_name, field, value in overrides:
for part_def in partitions:
if part_def["name"] == part_name:
children = part_def["children"]
assert isinstance(children, dict)
children[field] = value
break
else:
msg = f"--set: partition {part_name!r} not found"
raise ValueError(msg)

for part_def in partitions:
part = ET.SubElement(
sdmmc_user,
"partition",
name=str(part_def["name"]),
type=str(part_def["type"]),
)
part.text = "\n"
part.tail = "\n"
children = part_def["children"]
assert isinstance(children, dict)
for tag, text in children.items():
child = ET.SubElement(part, tag)
child.text = f" {text} "
child.tail = "\n"

ET.indent(tree, space=" ")
# Write without xml_declaration — we prepend it manually to match
# NVIDIA's original format (no encoding attribute). Some NVIDIA
# tools choke on encoding='utf-8' in the declaration.
with open(output, "w") as f:
f.write('<?xml version="1.0"?>\n')
tree.write(f, xml_declaration=False, encoding="unicode")
# Ensure trailing newline — NVIDIA's flash.sh pipes the XML
# through `while read line` which silently drops the last line
# if it lacks a newline terminator.
f.write("\n")


def main() -> None:
parser = argparse.ArgumentParser(
description="Splice partition layout into NVIDIA flash XML"
)
parser.add_argument("flash_xml", type=Path, help="NVIDIA flash XML template")
parser.add_argument("partitions_json", type=Path, help="Partition layout JSON")
parser.add_argument("output", type=Path, help="Output XML path")
parser.add_argument(
"--set",
dest="overrides",
action="append",
default=[],
metavar="PARTITION.FIELD=VALUE",
help="Override a partition child element value",
)
parser.add_argument(
"--remove-device",
action="store_true",
help="Remove the sdmmc_user device entirely (QSPI-only flash)",
)
args = parser.parse_args()

overrides = [parse_set_value(v) for v in args.overrides]

splice_partitions(
flash_xml=args.flash_xml,
partitions_json=args.partitions_json,
output=args.output,
overrides=overrides,
remove_device=args.remove_device,
)


if __name__ == "__main__":
main()
Loading