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
12 changes: 12 additions & 0 deletions ci/capsule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
header:
version: 14

# meta-oe provides the `openembedded-layer` collection name, which gates
# our local python3-pyfdt and qdte recipes under
# dynamic-layers/openembedded-layer/ (see conf/layer.conf BBFILES_DYNAMIC).
# meta-python is not required: pyfdt is shipped via the local recipe,
# and qdte's other runtime deps (fs, pyfatfs, tkinter) are stubbed by
# recipe-local patches when running --nogui.
repos:
meta-openembedded:
url: https://github.com/openembedded/meta-openembedded
layers:
meta-oe:

local_conf_header:
capsule: |
PREFERRED_PROVIDER_virtual/qcom-capsule-firmware = "firmware-qcom-capsule"
Expand Down
113 changes: 55 additions & 58 deletions classes-recipe/qcom-capsule.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,12 @@ CAPSULE_SUB_PUB ?= ""
# ---------------------------------------------------------------------------
# XBLConfig DTB certificate injection
# ---------------------------------------------------------------------------
# The class automatically detects the post-DDR DTB by parsing the output of
# xblconfig_parser.py dump (looks for the first entry matching post-ddr*.dtb).
# Both the filename and the section index are extracted from the dump output.
#
# XBLCONFIG_DTB overrides auto-detection when set to an explicit filename.
# XBLCONFIG_DTB_SECTION overrides the auto-detected section index.
#
# When a post-DDR DTB is found (auto or explicit), the class will:
# 1. dump XBLConfig sections
# 2. patch QcCapsuleRootCert in the DTB with the converted root cert
# 3. re-pack the updated DTB back into xbl_config.elf
XBLCONFIG_DTB ?= ""
XBLCONFIG_DTB_SECTION ?= ""
# The class invokes QDTE (qdte --nogui) to disassemble xbl_config.elf,
# patch QcCapsuleRootCert in the named DTB, and reassemble in one shot.
# QDTE does not auto-detect which DTB inside xbl_config.elf carries the
# property, so XBLCONFIG_DTB must be set to the filename of the post-DDR
# DTB (e.g. "post-ddr-kodiak-1.0.dtb").
XBLCONFIG_DTB ?= ""

# ---------------------------------------------------------------------------
# Boot binaries location
Expand Down Expand Up @@ -89,7 +82,8 @@ inherit python3native deploy
CAPSULE_DIR = "${WORKDIR}/capsule_gen"

do_compile[depends] += "cbsp-boot-utilities-native:do_populate_sysroot \
edk2-basetools-native:do_populate_sysroot"
edk2-basetools-native:do_populate_sysroot \
qdte-native:do_populate_sysroot"
do_compile[dirs] = "${CAPSULE_DIR}"
do_compile[cleandirs] = "${CAPSULE_DIR}"

Expand Down Expand Up @@ -203,55 +197,61 @@ python generate_fvupdate() {

do_compile[prefuncs] += "generate_fvupdate"

# Inject the OEM root certificate into xbl_config.elf.
# Dumps the config sections, auto-detects the post-DDR DTB (or uses
# XBLCONFIG_DTB / XBLCONFIG_DTB_SECTION overrides), patches QcCapsuleRootCert
# in that DTB, and repacks the updated DTB back into xbl_config.elf in place.
# Inject the OEM root certificate into xbl_config.elf using QDTE.
#
# QDTE's --nogui mode disassembles xbl_config.elf into its constituent
# DTBs, applies an EDIT_PROPERTY_VALUE op via --modify, then reassembles.
# That collapses the three previous cbsp-boot-utilities steps (dump,
# set-dtb-property, replace) into a single invocation -- but QDTE does
# not auto-detect which DTB inside xbl_config.elf to patch, so the
# integrator must set XBLCONFIG_DTB (e.g. "post-ddr-kodiak-1.0.dtb").
#
# The DER cert is converted directly into QDTE's --modify value syntax
# via python3 (staged through python3-native, in PATH); cbsp-boot-utilities'
# bin-to-hex is not on this path.
#
# $1 - path to xbl_config.elf (modified in place on success)
patch_xblconfig_cert() {
local xbl_config="$1"
local staged_dir
staged_dir=$(dirname "${xbl_config}")

XBL_DUMP_LOG="${CAPSULE_DIR}/xbl_dump.log"
qcom-capsule-tool parse-config \
"${xbl_config}" dump \
--out-dir "${staged_dir}" | tee "${XBL_DUMP_LOG}"

DTB_PATCH="${XBLCONFIG_DTB}"
DTB_SECTION="${XBLCONFIG_DTB_SECTION}"
if [ -z "${DTB_PATCH}" ]; then
# Parse a line like:
# [+] config_item[6] -> PH# 8 -> './post-ddr-kodiak-1.0.dtb' (90280 bytes)
POST_DDR_LINE=$(grep -m1 "post-ddr.*\.dtb" "${XBL_DUMP_LOG}" || true)
if [ -n "${POST_DDR_LINE}" ]; then
DTB_PATCH=$(echo "${POST_DDR_LINE}" | sed "s|.* -> '||;s|'.*||" | xargs basename)
DTB_SECTION=$(echo "${POST_DDR_LINE}" | sed "s/.*PH# \([0-9]*\).*/\1/")
fi
if [ -z "${XBLCONFIG_DTB}" ]; then
bbfatal "XBLCONFIG_DTB must be set when using QDTE for cert injection."
fi

if [ -n "${DTB_PATCH}" ]; then
ORIG_DTB="${staged_dir}/${DTB_PATCH}"
UPDATED_DTB="${staged_dir}/${DTB_PATCH%.dtb}-updated.dtb"

qcom-capsule-tool set-dtb-property \
"${ORIG_DTB}" \
/sw/uefi/uefiplat \
QcCapsuleRootCert \
"@list:${ROOT_INC}" \
"${UPDATED_DTB}"

qcom-capsule-tool parse-config \
"${xbl_config}" replace \
"${DTB_SECTION}" \
"${UPDATED_DTB}" \
"${staged_dir}/xbl_config_patched.elf"

mv "${staged_dir}/xbl_config_patched.elf" \
"${xbl_config}"

touch "${CAPSULE_DIR}/.xbl_with_oem_cert"
fi
# QcCapsuleRootCert is stored in the DTB as FdtPropertyWords: a
# length-prefixed sequence of 32-bit big-endian unsigned integers.
# Verified via fdtdump on a reference post-DDR DTB:
#
# QcCapsuleRootCert = <0x000003a6 0x308203a2 0x3082028a ... >;
#
# Word 0 is the cert length in bytes; subsequent words pack the DER
# cert four bytes at a time, big-endian, zero-padded to a multiple
# of 4 bytes. QDTE's --modify splits on ';' and feeds each token to
# the property setter; word-typed properties expect uint32 hex
# literals (matching the existing property type).
QDTE_CERT_VALUE=$(python3 -c '
import struct, sys
data = open(sys.argv[1], "rb").read()
pad = (-len(data)) % 4
padded = data + b"\x00" * pad
words = [len(data)] + list(struct.unpack(">%dI" % (len(padded) // 4), padded))
print(";".join("0x%08x" % w for w in words))
' "${CAPSULE_ROOT_CER}")

local qdte_outdir="${CAPSULE_DIR}/qdte_out"
mkdir -p "${qdte_outdir}"

qdte --nogui \
--allow_unsigned \
--input_file "${xbl_config}" \
--output_path "${qdte_outdir}" \
--output_file "xbl_config.elf" \
--modify "${XBLCONFIG_DTB}/sw/uefi/uefiplat/QcCapsuleRootCert=${QDTE_CERT_VALUE}"

install -m 0644 "${qdte_outdir}/xbl_config.elf" "${xbl_config}"
touch "${CAPSULE_DIR}/.xbl_with_oem_cert"
}

do_compile() {
Expand Down Expand Up @@ -279,9 +279,6 @@ do_compile() {

cd "${CAPSULE_DIR}"

ROOT_INC="${CAPSULE_DIR}/QcFMPRoot.inc"
qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${ROOT_INC}"

# Stage boot binaries so they are writable (XBLConfig patching modifies
# xbl_config.elf in place)
BOOTBINS_STAGED="${CAPSULE_DIR}/bootbins"
Expand Down
5 changes: 5 additions & 0 deletions conf/machine/include/qcom-qcs6490.inc
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ MACHINE_ESSENTIAL_EXTRA_RRECOMMENDS += " \
MACHINE_EXTRA_RRECOMMENDS += " \
packagegroup-qcom-boot-additional \
"

# Post-DDR DTB embedded inside xbl_config.elf for QCM6490 (Kodiak).
# Used by qcom-capsule.bbclass to locate the DTB into which the OEM
# capsule root certificate is injected at build time.
XBLCONFIG_DTB ?= "post-ddr-kodiak-1.0.dtb"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
SUMMARY = "Python Flattened Device Tree library"
DESCRIPTION = "Pure-Python library for parsing and writing Flattened \
Device Tree (FDT) blobs. Required by QDTE (qdte-native) for headless \
xbl_config.elf DTB manipulation in the UEFI capsule build pipeline."
HOMEPAGE = "https://github.com/superna9999/pyfdt"
LICENSE = "Apache-2.0"

# The 0.3 sdist on PyPI ships no LICENSE file, so anchor the checksum
# to the Apache-2.0 header embedded at the top of pyfdt/pyfdt.py.
LIC_FILES_CHKSUM = "file://pyfdt/pyfdt.py;beginline=4;endline=18;md5=727e7a76c771b92141ef85ee99d820ff"

SRC_URI[sha256sum] = "61601c2005ff394a25a6c84c6da2088bbf888328038400d27e4eeb1b04b9f4f0"

inherit pypi setuptools3

BBCLASSEXTEND = "native nativesdk"
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
From 17e451d07c84718be4c48fe5b2b3a7fce4797a17 Mon Sep 17 00:00:00 2001
From: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Date: Sun, 14 Jun 2026 20:54:26 +0200
Subject: [PATCH] controller: make non_hlos_parser import optional

The NON-HLOS.bin parser uses PyFilesystem2 (fs) and pyfatfs to walk a
FAT-formatted multi-firmware image. This is functionality only
reachable via the Tk GUI -- specifically the DTGUIController class
which inherits non_hlos_parser.nhlos_Operator and is only instantiated
in the GUI branch of run().

When QDTE is used headless (--nogui) for tasks like patching a single
DTB property inside xbl_config.elf, requiring fs / pyfatfs to be
installed is unnecessary friction. In environments such as
OpenEmbedded native sysroots these packages are not always packaged.

Make the top-level import conditional on its availability and provide
a stub class so the DTGUIController class definition still parses.
The stub is never instantiated (controller.run only calls
DTGUIController(...) in the non-nogui branch) so the GUI behaviour is
unchanged. When fs / pyfatfs are present the real module loads as
before.

Upstream-Status: Submitted [https://github.com/qualcomm/DTE/pull/N]
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
---
controller.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/controller.py b/controller.py
index 6c836b9..add04c3 100644
--- a/controller.py
+++ b/controller.py
@@ -66,7 +66,20 @@ from pyfdt import pyfdt
import Autocmd as cmd

#nhlos parser lib
-import non_hlos_parser
+#
+# The NON-HLOS.bin parser is only used by the GUI's FAT image browser
+# (DTGUIController instantiated via the `else` branch in run()). Its
+# transitive deps (`fs`, `pyfatfs`) are unnecessary for headless
+# --nogui usage such as xbl_config.elf modification, so import lazily
+# and fall back to a stub when those packages are not installed. The
+# stub satisfies the DTGUIController class-definition contract without
+# ever being instantiated under --nogui.
+try:
+ import non_hlos_parser
+except ImportError:
+ class non_hlos_parser: # type: ignore
+ class nhlos_Operator:
+ pass

import get_qsahara_files

--
2.53.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
From f7938c07dca4712f16f88435577caf02cd800649 Mon Sep 17 00:00:00 2001
From: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Date: Sun, 14 Jun 2026 21:05:31 +0200
Subject: [PATCH] run: stub tkinter for --nogui mode

QDTE's modules import tkinter at top level pervasively (controller.py,
treeview.py, settings.py, hexview.py, xblcfgint.py, and several
others), and define GUI classes like `class DTGUIController(tk.Frame, ...)`
whose bases are looked up at class-definition time. As a result the
mere act of importing controller -- which run.py does
unconditionally -- requires Tcl/Tk to be available in the host
environment, even when the user only wants the headless --nogui mode
for tasks like patching a single property inside xbl_config.elf.

In minimal Yocto native sysroots, Qualcomm-internal CI containers, and
similar restricted environments, tkinter is not always packaged. The
result is a hard ImportError before any headless logic has a chance
to run.

Install a permissive stub `tkinter` module (plus submodules: font,
ttk, colorchooser, messagebox, filedialog, simpledialog,
scrolledtext) into sys.modules at the very top of run.py when
--nogui is present in argv. The stub is:

* subclassable, so `class Foo(tk.Frame)` succeeds during
class-body execution;
* callable, so `tk.Tk()` evaluates without raising (the run_str
expression at the bottom of __main__ short-circuits via
`None if nogui else tk.Tk()`, but the lookup itself still
has to succeed);
* attribute-polymorphic via __getattr__ on both the stub class
and the stub module (PEP 562), so `from tkinter import E /
W / N / ttk / font / ...` all resolve to the stub class.

The stubs are never instantiated under --nogui because the GUI code
paths (DTGUIController, TreeView, etc.) are only constructed in the
non-nogui branch of controller.run(). When tkinter is present and
the GUI is used, the stub installation is skipped entirely and
behaviour is unchanged.

Upstream-Status: Submitted [https://github.com/qualcomm/DTE/pull/N]
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
---
run.py | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)

diff --git a/run.py b/run.py
index fb3767d..b35d4ce 100644
--- a/run.py
+++ b/run.py
@@ -14,6 +14,45 @@ An example of when this file could come in use is when profiling application per
decide upon an appropriate tool to use and set up the output profile file, etc.
"""
import sys
+
+# When invoked in --nogui mode, QDTE never instantiates any tkinter
+# widgets -- the controller.run() nogui branch only uses DTWrapper and
+# Autocmd. Install a permissive stub for `tkinter` (and its submodules)
+# before anything else loads so that the pervasive top-level
+# `import tkinter` / `from tkinter import E` / `class X(tk.Frame)`
+# statements throughout QDTE succeed in environments where Tcl/Tk is
+# not available, such as minimal Yocto native sysroots.
+if '--nogui' in sys.argv:
+ import types
+
+ class _TkStub:
+ """Pretends to be any tkinter symbol -- class, constant, callable.
+ Subclassable (so `class Foo(tk.Frame)` works), callable (so
+ `tk.Tk()` works if it ever runs), attribute-polymorphic."""
+ def __init__(self, *args, **kwargs):
+ pass
+ def __getattr__(self, name):
+ return _TkStub
+ def __call__(self, *args, **kwargs):
+ return _TkStub()
+ def __class_getitem__(cls, item):
+ return _TkStub
+
+ def _make_stub_module(name):
+ mod = types.ModuleType(name)
+ mod.__path__ = [] # mark as package so submodule imports resolve
+ # PEP 562: __getattr__ on a module handles `from <mod> import X`
+ # for unknown names by returning the stub class.
+ mod.__getattr__ = lambda n: _TkStub
+ return mod
+
+ sys.modules['tkinter'] = _make_stub_module('tkinter')
+ for _sub in ('font', 'ttk', 'colorchooser', 'messagebox',
+ 'filedialog', 'simpledialog', 'scrolledtext'):
+ _stub = _make_stub_module('tkinter.' + _sub)
+ sys.modules['tkinter.' + _sub] = _stub
+ setattr(sys.modules['tkinter'], _sub, _stub)
+
import argparse
import tkinter as tk
import flags as gflags
--
2.53.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
From 3da82bee62fe3999968b0c3935a33d23b6149947 Mon Sep 17 00:00:00 2001
From: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Date: Sun, 14 Jun 2026 21:13:43 +0200
Subject: [PATCH] controller: guard quts2 lookup on Linux

flags.py:global_info defines a "quts2" key only in its Windows branch
(it points at the secondary 64-bit "Program Files\Qualcomm\QUTS"
install path). On Linux, only "quts" is present, so the QUTS startup
probe in controller.py raises KeyError on the elif lookup before any
QDTE logic gets a chance to run.

Guard the second lookup with a containment check so the Linux path
falls through cleanly. The QUTS toolchain is optional anyway -- the
surrounding block silently ignores missing files via the
os.path.exists chain that follows.

Upstream-Status: Submitted [https://github.com/qualcomm/DTE/pull/N]
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
---
controller.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/controller.py b/controller.py
index add04c3..e4c9850 100644
--- a/controller.py
+++ b/controller.py
@@ -86,9 +86,12 @@ import get_qsahara_files

QUTS_STATE = None
quts_path = None
+# The "quts2" key is only present in the Windows branch of
+# flags.py:global_info (see the platform check there). Guard the
+# lookup so the Linux startup path does not raise KeyError.
if os.path.exists(gl_info["quts"]):
quts_path = gl_info["quts"]
-elif os.path.exists(gl_info["quts2"]):
+elif "quts2" in gl_info and os.path.exists(gl_info["quts2"]):
quts_path = gl_info["quts2"]
if quts_path and os.path.exists(os.path.join(quts_path,'Common','ttypes.py'))\
and os.path.exists(os.path.join(quts_path,'ImageManagementService','ImageManagementService.py'))\
--
2.53.0

Loading
Loading