From 449be1849e25159e9eea122efa9d831ae459a32e Mon Sep 17 00:00:00 2001 From: toby <80687022+toby-bro@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:57:13 +0200 Subject: [PATCH] Make python-fx an optional "tui" extra python-fx is used only by qltui.py, for the "interactive report" entry of the post-run report menu. It was a mandatory runtime dependency, so every `pip install qiling` also pulled pyfx and its tree -- 19 packages and 25 MB, none of which the framework itself imports. The cost is not only size. python-fx requires `pillow<11,>=10.4`, so that cap propagates to every qiling consumer: they are held on a pillow line that no longer receives fixes, and pillow 10.4.0 publishes no cp314 wheels, so `pip install qiling` on Python 3.14 falls back to building pillow from source. Move it to a `tui` extra and import it at the point of use, mirroring the existing optional-termcolor guard (qltui.py:13). `qltool qltui` keeps working unchanged without the extra; selecting "interactive report" prints how to install it instead of failing. test_qltui_import already owns "the installed qltui imports cleanly"; it now also asserts pyfx is not imported at module load, which is the contract this change adds. It fails before the change and passes after. Closes #1665 Co-Authored-By: Claude Opus 5 (1M context) --- ARCHITECTURE/modules/cli-build.md | 5 ++- ARCHITECTURE/modules/extensions.md | 2 +- README.md | 2 + poetry.lock | 67 +++++++++++++++++++----------- pyproject.toml | 4 +- qltui.py | 8 +++- tests/test_qltool.py | 3 +- 7 files changed, 59 insertions(+), 32 deletions(-) diff --git a/ARCHITECTURE/modules/cli-build.md b/ARCHITECTURE/modules/cli-build.md index 89b5bd752..30c971edc 100644 --- a/ARCHITECTURE/modules/cli-build.md +++ b/ARCHITECTURE/modules/cli-build.md @@ -31,8 +31,9 @@ examples/tests belongs to its runtime owner. Use the [root baseline](../../ARCHITECTURE.md#code-conventions). `argparse` actions convert enum/environment inputs and build `Qiling` keyword -arguments. Preserve option names and exit status. TUI uses questionary/fx/ -termcolor; testing its import is not testing its prompts. Keep build metadata +arguments. Preserve option names and exit status. TUI uses questionary/termcolor; its +interactive report viewer imports `fx` lazily and ships in the `tui` +extra; testing its import is not testing its prompts. Keep build metadata in the manifest, resolve its lock with Poetry, and retain package license and resource attribution. Documentation here contains coding context only. diff --git a/ARCHITECTURE/modules/extensions.md b/ARCHITECTURE/modules/extensions.md index 7f31a30e1..d0a6393b9 100644 --- a/ARCHITECTURE/modules/extensions.md +++ b/ARCHITECTURE/modules/extensions.md @@ -30,7 +30,7 @@ paths are explicitly owned by hardware/bare-metal modules. ## Local Conventions Use [root conventions](../../ARCHITECTURE.md#code-conventions); optional -packages use manifest extras `fuzz` and `RE`. Format classes expose +packages use manifest extras `fuzz` and `RE` (the TUI report viewer uses `tui`). Format classes expose `FORMAT_NAME` and activate/deactivate/dump methods; imports register subclasses with factories. No common C standard is declared for fuzz harnesses; inspect the local `fuzz.c`/build instructions. Generated SDK stubs require review; diff --git a/README.md b/README.md index 668d335e0..29772fc0f 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,8 @@ Qiling also provides a friendly tool named `qltool` to quickly emulate shellcode Installing Qiling with pip also installs the `qltool` command, including `qltool qltui` for the terminal UI. In a source checkout, use `./qltool` as shown below. Example binaries and rootfs files must be downloaded separately. +The interactive JSON report viewer offered by `qltool qltui` is an optional +extra: install it with `pip install qiling[tui]`. With qltool, easy execution can be performed: diff --git a/poetry.lock b/poetry.lock index 2d99b4731..0f429ad95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,12 +1,13 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "antlr4-python3-runtime" version = "4.13.2" description = "ANTLR 4.13.2 runtime for Python 3" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8"}, {file = "antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916"}, @@ -16,9 +17,10 @@ files = [ name = "asciimatics" version = "1.15.0" description = "A cross-platform package to replace curses (mouse/keyboard input & text colours/positioning) and create ASCII animations" -optional = false +optional = true python-versions = ">= 3.8" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "asciimatics-1.15.0-py3-none-any.whl", hash = "sha256:0fe068a6bed522929bd04bb5b8a2fb6ebf0aef1b7a9b3843cf71030a34bc38d5"}, {file = "asciimatics-1.15.0.tar.gz", hash = "sha256:cfdd398042727519d8b73e62b8ef82c0becfed4eb420899c3b96c98d0b96821a"}, @@ -136,9 +138,10 @@ pycparser = "*" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -optional = false +optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -151,10 +154,10 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -optional = false +optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +markers = "extra == \"tui\" and sys_platform == \"win32\" or extra == \"tui\" and platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -164,9 +167,10 @@ files = [ name = "dacite" version = "1.8.1" description = "Simple creation of data classes from dictionaries." -optional = false +optional = true python-versions = ">=3.6" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe"}, ] @@ -206,9 +210,10 @@ files = [ name = "first" version = "2.0.2" description = "Return the first true value of an iterable." -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "first-2.0.2-py2.py3-none-any.whl", hash = "sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86"}, {file = "first-2.0.2.tar.gz", hash = "sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"}, @@ -369,9 +374,10 @@ test = ["objgraph", "psutil", "setuptools"] name = "jsonpath-ng" version = "1.6.1" description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"}, {file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"}, @@ -400,9 +406,10 @@ files = [ name = "loguru" version = "0.7.2" description = "Python logging made (stupidly) simple" -optional = false +optional = true python-versions = ">=3.5" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, @@ -413,7 +420,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==7.2.5) ; python_version >= \"3.9\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "mypy (==v1.5.1) ; python_version >= \"3.8\"", "pre-commit (==3.4.0) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.4.0) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.1.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.0.0) ; python_version >= \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.9\"", "sphinx-rtd-theme (==1.3.0) ; python_version >= \"3.9\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.11.0) ; python_version >= \"3.8\""] +dev = ["Sphinx (==7.2.5) ; python_version >= \"3.9\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==0.910) ; python_version < \"3.6\"", "mypy (==0.971) ; python_version == \"3.6\"", "mypy (==1.4.1) ; python_version == \"3.7\"", "mypy (==1.5.1) ; python_version >= \"3.8\"", "pre-commit (==3.4.0) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.4.0) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.1.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.0.0) ; python_version >= \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.9\"", "sphinx-rtd-theme (==1.3.0) ; python_version >= \"3.9\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.11.0) ; python_version >= \"3.8\""] [[package]] name = "multiprocess" @@ -448,9 +455,10 @@ dill = ">=0.3.9" name = "overrides" version = "7.7.0" description = "A decorator to automatically detect mismatch when overriding a method." -optional = false +optional = true python-versions = ">=3.6" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, @@ -472,9 +480,10 @@ files = [ name = "pillow" version = "10.4.0" description = "Python Imaging Library (Fork)" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, @@ -570,9 +579,10 @@ xmp = ["defusedxml"] name = "ply" version = "3.11" description = "Python Lex & Yacc" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, @@ -622,9 +632,10 @@ files = [ name = "pyfiglet" version = "1.0.2" description = "Pure-python FIGlet implementation" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea"}, {file = "pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab"}, @@ -634,9 +645,10 @@ files = [ name = "pyperclip" version = "1.9.0" description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, ] @@ -645,9 +657,10 @@ files = [ name = "python-fx" version = "0.3.2" description = "A python-native fx-alike terminal JSON viewer." -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "python_fx-0.3.2-py3-none-any.whl", hash = "sha256:5498475b0f391b1649732328b58d188d9fc4b3f90f5bfb77d5c6e2ece2432c5f"}, {file = "python_fx-0.3.2.tar.gz", hash = "sha256:9646f58c716e2db6698bff3dfa55fa721b8b0cb741506287a87bc08055a96ceb"}, @@ -693,10 +706,10 @@ unicodecsv = "*" name = "pywin32" version = "308" description = "Python for Window Extensions" -optional = false +optional = true python-versions = "*" groups = ["main"] -markers = "sys_platform == \"win32\"" +markers = "extra == \"tui\" and sys_platform == \"win32\"" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -850,9 +863,10 @@ tests = ["pytest", "pytest-cov"] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -979,9 +993,10 @@ unicorn = ">=2.0.1" name = "urwid" version = "2.6.15" description = "A full-featured console (xterm et al.) user interface library" -optional = false +optional = true python-versions = ">3.7" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "urwid-2.6.15-py3-none-any.whl", hash = "sha256:71b3171cabaa0092902f556768756bd2f2ebb24c0da287ee08f081d235340cb7"}, {file = "urwid-2.6.15.tar.gz", hash = "sha256:9ecc57330d88c8d9663ffd7092a681674c03ff794b6330ccfef479af7aa9671b"}, @@ -1017,10 +1032,10 @@ files = [ name = "win32-setctime" version = "1.2.0" description = "A small Python utility to set file creation time on Windows" -optional = false +optional = true python-versions = ">=3.5" groups = ["main"] -markers = "sys_platform == \"win32\"" +markers = "extra == \"tui\" and sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, @@ -1060,9 +1075,10 @@ files = [ name = "yamale" version = "5.2.1" description = "A schema and validator for YAML." -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "yamale-5.2.1-py3-none-any.whl", hash = "sha256:e44cd30cf3055ee4b34c1c71d6fe35490a127dcbd36f82f27859d105a9989922"}, {file = "yamale-5.2.1.tar.gz", hash = "sha256:19bbe713d588f07177bc519a46070c0793ed126ea37f425a76055b99703f835a"}, @@ -1148,8 +1164,9 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [extras] fuzz = ["fuzzercorn", "unicornafl"] re = ["r2libr"] +tui = ["python-fx"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "b65631e55580e06f60d535d41d2c2d0c4ee786609c7232988ceda8ce6b586a6d" +content-hash = "137b17a52549761c60d1c27ef2f8c8aa5628ee4e8f84188329713cc7e79e369c" diff --git a/pyproject.toml b/pyproject.toml index cc6a6252b..cacdb608f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ gevent = ">=24.10" multiprocess = ">=0.70.12.2" windows-curses = { version = "^2.1.0", markers = "platform_system == 'Windows'" } pyyaml = "^6.0.1" -python-fx = "*" questionary = "*" termcolor = "*" @@ -54,9 +53,12 @@ fuzzercorn = { version = "^0.0.1", markers = "platform_system == 'Linux'", optio r2libr = { version = "^5.7.4", optional = true } +python-fx = { version = "*", optional = true } + [tool.poetry.extras] fuzz = ["unicornafl", "fuzzercorn"] RE = ["r2libr"] +tui = ["python-fx"] [build-system] requires = ["poetry-core>=2.0,<3.0"] diff --git a/qltui.py b/qltui.py index 917e15083..b6e1ef507 100644 --- a/qltui.py +++ b/qltui.py @@ -5,7 +5,6 @@ import argparse import json -from pyfx import PyfxApp from pprint import pprint from datetime import datetime @@ -473,7 +472,12 @@ def show_report(ql: Qiling, report, hook_dictionary): if command == 'report': pprint(report) elif command == 'interactive report': - PyfxApp(data=report).run() + try: + from pyfx import PyfxApp + except ImportError: + print('the interactive report viewer is not installed; get it with: pip install qiling[tui]') + else: + PyfxApp(data=report).run() elif command == 'save to json': time = datetime.now().strftime("%Y_%m_%d_%H-%M-%S") report_name = f"report_{ql.targetname.replace('.', '_')}_{os_name}_{arch_name}_{time}.json" diff --git a/tests/test_qltool.py b/tests/test_qltool.py index 9949b90f7..a7be24d4c 100644 --- a/tests/test_qltool.py +++ b/tests/test_qltool.py @@ -83,7 +83,8 @@ def test_qltool_shellcode_exit_status(self): def test_qltui_import(self): result = subprocess.run( - [sys.executable, '-I', '-c', 'import qltui; assert callable(qltui.get_data)'], + [sys.executable, '-I', '-c', + 'import sys, qltui; assert callable(qltui.get_data); assert "pyfx" not in sys.modules'], cwd=self.cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30 )