Skip to content

Commit 7d431a6

Browse files
committed
jetson-orin: use XML-aware splicing for flash partition layout
Add splice-flash-xml.py, a Python script that replaces the <device type="sdmmc_user"> partitions in NVIDIA's flash XML with a custom layout defined in JSON. This replaces fragile line-count based head/tail splicing that breaks when the upstream BSP XML changes. Convert partition-template.nix to use the new script. The partition layout is now defined as structured Nix data serialized to JSON. Partition sizes are injected at build time from sdImage metadata via --set instead of flash-time sed substitution. Features: --set PARTITION.FIELD=VALUE override partition child element values --remove-device remove sdmmc_user device (QSPI-only) Signed-off-by: Jörg Thalheim <joerg@thalheim.io>
1 parent cd63df2 commit 7d431a6

File tree

2 files changed

+244
-96
lines changed

2 files changed

+244
-96
lines changed

modules/reference/hardware/jetpack/nvidia-jetson-orin/partition-template.nix

Lines changed: 89 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -19,92 +19,85 @@ let
1919
# sdImage containing ESP and root partitions (compressed)
2020
images = config.system.build.sdImage;
2121

22-
# Partition XML with placeholders (substituted at flash time by preFlashCommands)
23-
partitionsEmmc = pkgs.writeText "sdmmc.xml" ''
24-
<partition name="master_boot_record" type="protective_master_boot_record">
25-
<allocation_policy> sequential </allocation_policy>
26-
<filesystem_type> basic </filesystem_type>
27-
<size> 512 </size>
28-
<file_system_attribute> 0 </file_system_attribute>
29-
<allocation_attribute> 8 </allocation_attribute>
30-
<percent_reserved> 0 </percent_reserved>
31-
</partition>
32-
<partition name="primary_gpt" type="primary_gpt">
33-
<allocation_policy> sequential </allocation_policy>
34-
<filesystem_type> basic </filesystem_type>
35-
<size> 19968 </size>
36-
<file_system_attribute> 0 </file_system_attribute>
37-
<allocation_attribute> 8 </allocation_attribute>
38-
<percent_reserved> 0 </percent_reserved>
39-
</partition>
40-
<partition name="esp" id="2" type="data">
41-
<allocation_policy> sequential </allocation_policy>
42-
<filesystem_type> basic </filesystem_type>
43-
<size> ESP_SIZE </size>
44-
<file_system_attribute> 0 </file_system_attribute>
45-
<allocation_attribute> 0x8 </allocation_attribute>
46-
<percent_reserved> 0 </percent_reserved>
47-
<filename> bootloader/esp.img </filename>
48-
<partition_type_guid> C12A7328-F81F-11D2-BA4B-00A0C93EC93B </partition_type_guid>
49-
<description> EFI system partition with systemd-boot. </description>
50-
</partition>
51-
<partition name="APP" id="1" type="data">
52-
<allocation_policy> sequential </allocation_policy>
53-
<filesystem_type> basic </filesystem_type>
54-
<size> ROOT_SIZE </size>
55-
<file_system_attribute> 0 </file_system_attribute>
56-
<allocation_attribute> 0x8 </allocation_attribute>
57-
<align_boundary> 16384 </align_boundary>
58-
<percent_reserved> 0x808 </percent_reserved>
59-
<unique_guid> APPUUID </unique_guid>
60-
<filename> root.img </filename>
61-
<description> **Required.** Contains the rootfs. This partition must be assigned
62-
the "1" for id as it is physically put to the end of the device, so that it
63-
can be accessed as the fixed known special device `/dev/mmcblk0p1`. </description>
64-
</partition>
65-
<partition name="secondary_gpt" type="secondary_gpt">
66-
<allocation_policy> sequential </allocation_policy>
67-
<filesystem_type> basic </filesystem_type>
68-
<size> 0xFFFFFFFFFFFFFFFF </size>
69-
<file_system_attribute> 0 </file_system_attribute>
70-
<allocation_attribute> 8 </allocation_attribute>
71-
<percent_reserved> 0 </percent_reserved>
72-
</partition>
73-
'';
74-
75-
# Line counts for replacing the sdmmc_user device section in NVIDIA's flash XML.
76-
# These numbers specify where to splice our custom partition layout.
22+
# eMMC partition layout as structured Nix data.
23+
# Serialized to JSON and spliced into NVIDIA's flash XML by
24+
# splice-flash-xml.py, which replaces the <device type="sdmmc_user">
25+
# children. This avoids fragile line-count splicing.
7726
#
78-
# WARNING: When updating jetpack-nixos/BSP version, verify these line counts
79-
# still match the <device type="sdmmc_user"> section boundaries in:
80-
# - flash_t234_qspi_sdmmc.xml (standard)
81-
# - flash_t234_qspi_sdmmc_industrial.xml (industrial variant)
82-
partitionTemplateReplaceRange =
83-
if (config.hardware.nvidia-jetpack.som == "orin-agx-industrial") then
84-
if (!cfg.flashScriptOverrides.onlyQSPI) then
85-
{
86-
firstLineCount = 631;
87-
lastLineCount = 2;
88-
}
89-
else
90-
{
91-
# QSPI-only: remove entire sdmmc_user device section
92-
firstLineCount = 630;
93-
lastLineCount = 1;
94-
}
95-
else if !cfg.flashScriptOverrides.onlyQSPI then
96-
{
97-
firstLineCount = 618;
98-
lastLineCount = 2;
99-
}
100-
else
101-
{
102-
# QSPI-only: remove entire sdmmc_user device section
103-
firstLineCount = 617;
104-
lastLineCount = 1;
27+
# Partition sizes are injected at build time from the sdImage metadata
28+
# via --set (sectors * 512 → bytes).
29+
partitionsEmmc = [
30+
{
31+
name = "master_boot_record";
32+
type = "protective_master_boot_record";
33+
children = {
34+
allocation_policy = "sequential";
35+
filesystem_type = "basic";
36+
size = "512";
37+
file_system_attribute = "0";
38+
allocation_attribute = "8";
39+
percent_reserved = "0";
40+
};
41+
}
42+
{
43+
name = "primary_gpt";
44+
type = "primary_gpt";
45+
children = {
46+
allocation_policy = "sequential";
47+
filesystem_type = "basic";
48+
size = "19968";
49+
file_system_attribute = "0";
50+
allocation_attribute = "8";
51+
percent_reserved = "0";
52+
};
53+
}
54+
{
55+
name = "esp";
56+
type = "data";
57+
children = {
58+
allocation_policy = "sequential";
59+
filesystem_type = "basic";
60+
size = "0"; # overridden by --set from sdImage metadata at build time
61+
file_system_attribute = "0";
62+
allocation_attribute = "0x8";
63+
percent_reserved = "0";
64+
filename = "bootloader/esp.img";
65+
partition_type_guid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
66+
description = "EFI system partition with systemd-boot.";
67+
};
68+
}
69+
{
70+
name = "APP";
71+
type = "data";
72+
children = {
73+
allocation_policy = "sequential";
74+
filesystem_type = "basic";
75+
size = "0"; # overridden by --set from sdImage metadata at build time
76+
file_system_attribute = "0";
77+
allocation_attribute = "0x8";
78+
align_boundary = "16384";
79+
percent_reserved = "0x808";
80+
unique_guid = "APPUUID";
81+
filename = "root.img";
82+
description = "Contains the rootfs, placed at the end of the device as /dev/mmcblk0p1.";
10583
};
84+
}
85+
{
86+
name = "secondary_gpt";
87+
type = "secondary_gpt";
88+
children = {
89+
allocation_policy = "sequential";
90+
filesystem_type = "basic";
91+
size = "0xFFFFFFFFFFFFFFFF";
92+
file_system_attribute = "0";
93+
allocation_attribute = "8";
94+
percent_reserved = "0";
95+
};
96+
}
97+
];
10698

107-
# Build the final flash.xml by splicing our partition layout into NVIDIA's template
99+
# Build the final flash.xml by replacing the sdmmc_user partitions
100+
# in NVIDIA's template with our layout using XML-aware splicing.
108101
partitionTemplate =
109102
let
110103
inherit (pkgs.nvidia-jetpack) bspSrc;
@@ -115,17 +108,19 @@ let
115108
else
116109
"${bspSrc}/bootloader/generic/cfg/flash_t234_qspi_sdmmc.xml";
117110
in
118-
pkgs.runCommand "flash.xml" { } (
119-
''
120-
head -n ${toString partitionTemplateReplaceRange.firstLineCount} ${xmlFile} >"$out"
121-
''
122-
+ lib.optionalString (!cfg.flashScriptOverrides.onlyQSPI) ''
123-
cat ${partitionsEmmc} >>"$out"
124-
''
125-
+ ''
126-
tail -n ${toString partitionTemplateReplaceRange.lastLineCount} ${xmlFile} >>"$out"
111+
pkgs.runCommand "flash.xml"
112+
{
113+
nativeBuildInputs = [ pkgs.buildPackages.python3 ];
114+
}
127115
''
128-
);
116+
python3 ${./splice-flash-xml.py} \
117+
${lib.optionalString cfg.flashScriptOverrides.onlyQSPI "--remove-device"} \
118+
--set "esp.size=$(($(cat ${images}/esp.size) * 512))" \
119+
--set "APP.size=$(($(cat ${images}/root.size) * 512))" \
120+
${xmlFile} \
121+
${pkgs.writeText "sdmmc.json" (builtins.toJSON partitionsEmmc)} \
122+
"$out"
123+
'';
129124

130125
# preFlashCommands: Extract images from sdImage and patch flash.xml
131126
preFlashScript = pkgs.writeShellApplication {
@@ -171,12 +166,10 @@ let
171166
bs=512 iseek="$ROOT_OFFSET" count="$ROOT_SIZE" status=progress
172167
173168
echo ""
174-
echo "Patching flash.xml with image paths and sizes..."
169+
echo "Patching flash.xml with image paths..."
175170
sed -i \
176171
-e "s#bootloader/esp.img#$WORKDIR/bootloader/esp.img#" \
177172
-e "s#root.img#$WORKDIR/bootloader/root.img#" \
178-
-e "s#ESP_SIZE#$((ESP_SIZE * 512))#" \
179-
-e "s#ROOT_SIZE#$((ROOT_SIZE * 512))#" \
180173
flash.xml
181174
''}
182175
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: 2022-2026 TII (SSRC) and the Ghaf contributors
3+
# SPDX-License-Identifier: Apache-2.0
4+
"""Replace the sdmmc_user device partitions in NVIDIA's flash XML.
5+
6+
Reads NVIDIA's flash_t234_qspi_sdmmc*.xml, finds the
7+
<device type="sdmmc_user"> element, replaces all its <partition>
8+
children with partitions defined in a JSON file, and writes the
9+
result. This avoids fragile line-count splicing that breaks when
10+
the upstream XML changes.
11+
12+
Usage:
13+
splice-flash-xml.py [--set PARTITION.FIELD=VALUE]...
14+
[--remove-device]
15+
<nvidia-flash.xml> <partitions.json> <output.xml>
16+
17+
The JSON file is a list of partition objects, each with:
18+
- name (str): partition name attribute
19+
- type (str): partition type attribute
20+
- children (dict): child element tag -> text content
21+
22+
The --set flag overrides a child element value for a named partition.
23+
For example: --set APP.size=12345678
24+
25+
The --remove-device flag removes the sdmmc_user device element entirely
26+
(for QSPI-only flashing where no eMMC partitions are needed).
27+
"""
28+
29+
import argparse
30+
import json
31+
import xml.etree.ElementTree as ET
32+
from pathlib import Path
33+
34+
35+
def parse_set_value(value: str) -> tuple[str, str, str]:
36+
"""Parse 'PARTITION.FIELD=VALUE' into (partition_name, field, value)."""
37+
lhs, _, rhs = value.partition("=")
38+
if not rhs:
39+
msg = f"--set requires PARTITION.FIELD=VALUE, got: {value!r}"
40+
raise argparse.ArgumentTypeError(msg)
41+
part_name, _, field = lhs.partition(".")
42+
if not field:
43+
msg = f"--set requires PARTITION.FIELD=VALUE, got: {value!r}"
44+
raise argparse.ArgumentTypeError(msg)
45+
return part_name, field, rhs
46+
47+
48+
def splice_partitions(
49+
flash_xml: Path,
50+
partitions_json: Path,
51+
output: Path,
52+
*,
53+
overrides: list[tuple[str, str, str]],
54+
remove_device: bool = False,
55+
) -> None:
56+
"""Replace sdmmc_user partitions in NVIDIA's flash XML with our layout."""
57+
tree = ET.parse(flash_xml)
58+
root = tree.getroot()
59+
60+
sdmmc_user = next(
61+
(d for d in root.iter("device") if d.get("type") == "sdmmc_user"),
62+
None,
63+
)
64+
if sdmmc_user is None:
65+
msg = f"No <device type='sdmmc_user'> found in {flash_xml}"
66+
raise ValueError(msg)
67+
68+
if remove_device:
69+
# QSPI-only: remove the entire sdmmc_user device element
70+
root.remove(sdmmc_user)
71+
else:
72+
# Remove all existing children
73+
for child in list(sdmmc_user):
74+
sdmmc_user.remove(child)
75+
76+
partitions: list[dict[str, str | dict[str, str]]] = json.loads(
77+
partitions_json.read_text()
78+
)
79+
80+
# Apply --set overrides
81+
for part_name, field, value in overrides:
82+
for part_def in partitions:
83+
if part_def["name"] == part_name:
84+
children = part_def["children"]
85+
assert isinstance(children, dict)
86+
children[field] = value
87+
break
88+
else:
89+
msg = f"--set: partition {part_name!r} not found"
90+
raise ValueError(msg)
91+
92+
for part_def in partitions:
93+
part = ET.SubElement(
94+
sdmmc_user,
95+
"partition",
96+
name=str(part_def["name"]),
97+
type=str(part_def["type"]),
98+
)
99+
part.text = "\n"
100+
part.tail = "\n"
101+
children = part_def["children"]
102+
assert isinstance(children, dict)
103+
for tag, text in children.items():
104+
child = ET.SubElement(part, tag)
105+
child.text = f" {text} "
106+
child.tail = "\n"
107+
108+
ET.indent(tree, space=" ")
109+
# Write without xml_declaration — we prepend it manually to match
110+
# NVIDIA's original format (no encoding attribute). Some NVIDIA
111+
# tools choke on encoding='utf-8' in the declaration.
112+
with open(output, "w") as f:
113+
f.write('<?xml version="1.0"?>\n')
114+
tree.write(f, xml_declaration=False, encoding="unicode")
115+
# Ensure trailing newline — NVIDIA's flash.sh pipes the XML
116+
# through `while read line` which silently drops the last line
117+
# if it lacks a newline terminator.
118+
f.write("\n")
119+
120+
121+
def main() -> None:
122+
parser = argparse.ArgumentParser(
123+
description="Splice partition layout into NVIDIA flash XML"
124+
)
125+
parser.add_argument("flash_xml", type=Path, help="NVIDIA flash XML template")
126+
parser.add_argument("partitions_json", type=Path, help="Partition layout JSON")
127+
parser.add_argument("output", type=Path, help="Output XML path")
128+
parser.add_argument(
129+
"--set",
130+
dest="overrides",
131+
action="append",
132+
default=[],
133+
metavar="PARTITION.FIELD=VALUE",
134+
help="Override a partition child element value",
135+
)
136+
parser.add_argument(
137+
"--remove-device",
138+
action="store_true",
139+
help="Remove the sdmmc_user device entirely (QSPI-only flash)",
140+
)
141+
args = parser.parse_args()
142+
143+
overrides = [parse_set_value(v) for v in args.overrides]
144+
145+
splice_partitions(
146+
flash_xml=args.flash_xml,
147+
partitions_json=args.partitions_json,
148+
output=args.output,
149+
overrides=overrides,
150+
remove_device=args.remove_device,
151+
)
152+
153+
154+
if __name__ == "__main__":
155+
main()

0 commit comments

Comments
 (0)