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
147 changes: 147 additions & 0 deletions .github/workflows/nogui-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: BSD-3-Clause-Clear
#
# Headless (--nogui) integration smoke tests.
#
# These exercise the command-line contract that downstream automation
# (Yocto, signing pipelines) relies on, by invoking run.py externally on
# real Qualcomm boot binaries and checking the produced ELF actually
# contains the edited property:
#
# run.py --nogui --allow_unsigned --input_file <elf>
# --output_path <dir> --output_file <name>
# --modify "<dtb>/<path>=<value>"
#
# Two containers are covered:
# * xbl_config.elf - reassembled via the unsigned GenXBLConfig path
# (no sectools required)
# * uefi_dtbs.elf - a pure-DTB (.dtbs) ELF whose wrapper is rebuilt via
# `sectools elf-tool generate` (sectools required)
#
# All binaries are public "test-device" boot images / public security tools
# fetched from Qualcomm Software Center; nothing proprietary is committed.

name: QDTE nogui smoke

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
workflow_dispatch:

permissions:
contents: read

env:
# Public boot-binary / tools archives. Bump the build numbers here to
# refresh the fixtures; the embedded DTB names below are tied to them.
XBL_ZIP_URL: "https://softwarecenter.qualcomm.com/nexus/generic/product/chip/tech-package/QCM6490_bootbinaries.1.0/qcm6490_bootbinaries.1.0-test-device-public/00126/QCM6490_bootbinaries_00126.zip"
XBL_ELF_IN_ZIP: "QCM6490_bootbinaries/xbl_config.elf"
# The first DTB embedded in the reassembled ELF (by file offset); this is
# the one `fdtdump --scan` decodes, so the verified edit targets it.
XBL_DTB_NAME: "pre-ddr-kodiak-1.0.dtb"

UEFI_ZIP_URL: "https://softwarecenter.qualcomm.com/nexus/generic/product/chip/tech-package/QRB2210_bootbinaries.1.0/qrb2210_bootbinaries.1.0-test-device-public/00010/Agatti_bootbinaries.zip"
UEFI_ELF_IN_ZIP: "Agatti_bootbinaries/uefi_dtbs.elf"
UEFI_DTB_NAME: "qcom,uefi-agatti-1.0.dtb"

# Public Qualcomm Security Tools (sectools). The download URL 301-redirects,
# so curl is invoked with -L. ubuntu-latest runners are x86_64 -> Linux/.
SECTOOLS_ZIP_URL: "https://softwarecenter.qualcomm.com/api/download/software/tools/Qualcomm_Security_Tools/All/1.48.0/1.48.zip"
SECTOOLS_IN_ZIP: "1.48/Linux/sectools"

FIXTURES_DIR: "${{ github.workspace }}/.smoke-fixtures"

jobs:
nogui-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
# QDTE headless runtime deps only. With the headless import guards
# in place, --nogui needs neither tkinter nor fs/pyfatfs.
python -m pip install --upgrade pip
python -m pip install pyfdt six
# fdtdump (verification) lives in device-tree-compiler.
sudo apt-get update
sudo apt-get install -y device-tree-compiler

- name: Cache test binaries
id: cache-fixtures
uses: actions/cache@v4
with:
path: ${{ env.FIXTURES_DIR }}
key: smoke-fixtures-${{ hashFiles('.github/workflows/qcom-nogui-smoke.yml') }}

- name: Fetch test binaries
if: steps.cache-fixtures.outputs.cache-hit != 'true'
run: |
set -euo pipefail
mkdir -p "$FIXTURES_DIR"
tmp="$(mktemp -d)"

echo "::group::xbl_config.elf"
curl -fsSL -o "$tmp/xbl.zip" "$XBL_ZIP_URL"
unzip -o -j "$tmp/xbl.zip" "$XBL_ELF_IN_ZIP" -d "$FIXTURES_DIR"
echo "::endgroup::"

echo "::group::uefi_dtbs.elf"
curl -fsSL -o "$tmp/uefi.zip" "$UEFI_ZIP_URL"
unzip -o -j "$tmp/uefi.zip" "$UEFI_ELF_IN_ZIP" -d "$FIXTURES_DIR"
echo "::endgroup::"

echo "::group::sectools"
curl -fsSL -o "$tmp/sectools.zip" "$SECTOOLS_ZIP_URL"
unzip -o -j "$tmp/sectools.zip" "$SECTOOLS_IN_ZIP" -d "$FIXTURES_DIR/sectools"
chmod +x "$FIXTURES_DIR/sectools/sectools"
echo "::endgroup::"

rm -rf "$tmp"
ls -l "$FIXTURES_DIR" "$FIXTURES_DIR/sectools"

# Each smoke test runs run.py --nogui to edit one property, then reads
# it back from the produced ELF with `fdtdump --scan`, which locates and
# decodes the embedded DTB. The expected value is matched as the 8-digit
# hex word fdtdump prints (e.g. <0x00000007>).

- name: nogui smoke - xbl_config.elf
run: |
set -euo pipefail
# xbl_config.elf reassembles via the unsigned GenXBLConfig path
# (no sectools). The hex literal also exercises the int(val, 0) fix.
out="$(mktemp -d)"
python run.py --nogui --allow_unsigned \
--input_file "$FIXTURES_DIR/xbl_config.elf" \
--output_path "$out" --output_file xbl_config.elf \
--modify "$XBL_DTB_NAME/soc/ddr_common_dt/ddr_log_level_dt=0x7"
test -f "$out/xbl_config.elf"
fdtdump --scan "$out/xbl_config.elf" 2>/dev/null \
| grep -q "ddr_log_level_dt = <0x00000007>"
echo "PASS: xbl_config.elf ddr_log_level_dt == 0x7"

- name: nogui smoke - uefi_dtbs.elf
run: |
set -euo pipefail
# uefi_dtbs.elf is a pure-DTB (.dtbs) ELF whose wrapper is rebuilt
# via `sectools elf-tool generate`, so --sectools_dir is required.
out="$(mktemp -d)"
python run.py --nogui --allow_unsigned \
--sectools_dir "$FIXTURES_DIR/sectools" \
--input_file "$FIXTURES_DIR/uefi_dtbs.elf" \
--output_path "$out" --output_file uefi_dtbs.elf \
--modify "$UEFI_DTB_NAME/soc/quantum_dt/enter_quantum=0x1"
test -f "$out/uefi_dtbs.elf"
fdtdump --scan "$out/uefi_dtbs.elf" 2>/dev/null \
| grep -q "enter_quantum = <0x00000001>"
echo "PASS: uefi_dtbs.elf enter_quantum == 0x1"

echo "All nogui smoke tests passed."
26 changes: 23 additions & 3 deletions controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,32 @@
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


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'))\
Expand Down Expand Up @@ -1696,7 +1712,11 @@ def run(root, **kwargs):
return """
else:
import ctypes
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
# ctypes.windll exists only on Windows; this call hides the console
# window behind the GUI there. Guard it so the GUI also launches on
# Linux/macOS, where windll is absent (AttributeError otherwise).
if hasattr(ctypes, "windll"):
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
DTGUIController(root, **kwargs)
# start processing events

Expand Down
20 changes: 18 additions & 2 deletions dtwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,12 @@ def apply(self, fdt):
for val in self.val_new:
#dtlogger.debug(type(val))
if isinstance(val,type('')):
vals.append(int(val))
# int(val, 0) auto-detects the base from a literal
# prefix ('0x' hex, '0o' octal, '0b' binary, else
# decimal). Plain int(val) only accepts decimal and
# rejects '0x...' tokens that callers naturally
# supply when mirroring fdtdump output.
vals.append(int(val, 0))
else:
vals.append(val)
#dtlogger.debug("new list is %s"%vals)
Expand Down Expand Up @@ -1006,7 +1011,18 @@ def __init__(self):
def _update_dtb(self):
self.dtb_mappings = {}
if self._fdt is not None:
self.dtb = self._fdt.to_dtb(self.dtb_mappings)
# Upstream PyPI pyfdt (0.3, the only released version) exposes
# `to_dtb(self)` with no parameters. QDTE was developed against
# an internal fork whose `to_dtb()` accepts an out-dict that gets
# populated with byte-offset mappings for the GUI hex viewer.
# Probe both signatures so we work with either: pass the
# mappings dict if accepted, otherwise call the zero-arg form
# and leave `dtb_mappings` empty (the hex viewer simply will
# not highlight node offsets in that case).
try:
self.dtb = self._fdt.to_dtb(self.dtb_mappings)
except TypeError:
self.dtb = self._fdt.to_dtb()
else:
self.dtb = None

Expand Down
39 changes: 39 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,45 @@
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
Expand Down
Loading