Make the renderers plugin directory a package - #2015
Conversation
The pyinstaller specs gather plugin modules two different ways. The .py files are shipped verbatim by collect_data_files(include_py_files=True), while collect_submodules() supplies the hiddenimports that pyinstaller actually analyses for dependencies. The first walks the filesystem, the second walks pkgutil, and pkgutil skips a directory with no __init__.py. volatility3/framework/plugins/renderers has been such a directory since the arrow and parquet renderers moved into it, so parquet_renderer.py was copied into the binary but never analysed, and the pyarrow it imports was left out. The frozen build still advertised the arrow and parquet renderers, because the framework finds them at runtime with os.walk, and then died with an unhandled RuntimeError when either was selected. Adding the __init__.py makes pkgutil descend into the directory, which is all collect_submodules needs, and matches every other plugin subpackage. Both specs use the same collect_submodules line, so neither needs editing.
|
Thanks very much for the analysis and locating the problem. The issue you raise of vol.exe becoming too large are a concern (and I wonder if there's a way to require the user to have the DLLs installed, and we only carry the python files for it). Either way, this is much appreciated, and once we decide how to deal with the oversized EXE, I'll get this merged. Thanks! 5:) |
|
Thanks! I went and measured the size question. The short version: shipping only the python All numbers below are Windows, pyarrow 25.0.1, Python 3.14, one-file build. Why "python files only" does not work
Where the size actually comes fromNot from our spec. hiddenimports = collect_submodules('pyarrow', filter=lambda x: "tests" not in x)
datas = collect_data_files('pyarrow')
binaries = collect_dynamic_libs('pyarrow')So as soon as anything imports pyarrow the entire package is collected — about 84 MB into I first tried the obvious thing, adding the unused pyarrow submodules to What does helpFiltering
The filtered build gives byte-for-byte identical About 42 MiB looks like the floor if pyarrow ships at all: Options
The Happy to push (2) or (3) onto this branch, or do it as a follow-up. Just tell me which you |
|
I agree that we should have the init.py for completeness, but I think arrow may have to be a feature we don't support in the exe simply because it adds too much weight for not enough value. I was hoping there was a way that the DLLs could be found on the system automatically rather than us shipping them, but if not I'd almost sooner keep it as is (which I believe should simply not offer arrow support because the import should fail). I've changed it slightly so that if the arrow libraries can't be found the renderers don't display (there's still a debug message to say they couldn't found, but otherwise a normal user will have no indication). If there's demand we could offer a complete version (the 53Mb version), but otherwise I think the most common use case will be the slim version. I'll merge this once the tests have completed successfully. Thanks for your contribution! |
Fixes #1936.
Why pyarrow goes missing
Both specs gather the plugin modules twice, by two different mechanisms:
collect_data_fileswalks the filesystem, so it picks up any.pyfile itfinds.
collect_submoduleswalkspkgutil, andpkgutilwill not descendinto a directory with no
__init__.py.volatility3/framework/plugins/renderershas no__init__.py, so the twodisagree:
Only
hiddenimportsfeeds pyinstaller's dependency analysis. The renderer istherefore copied into the binary as an inert data file, its
import pyarrowisnever seen, and pyarrow is left out even though the workflow installs it via
pip install -e .[full,cloud,arrow].This is exactly the "the renderer code does make it into the final binary" part
of the issue.
Why it is only this directory
renderersis the only subdirectory undervolatility3/framework/pluginswithout an
__init__.py;linux,mac,windows,linux/graphics,linux/malware,linux/tracing,windows/malwareandwindows/registryallhave one.
The specs also call
collect_submodulesonvolatility3.framework.automagicand
volatility3.framework.symbols. I checked both:automagicis clean, andthe six directories under
symbols/windowswithout an__init__.py(
bigpools,consoles,gui,netscan,services,shimcache) hold nothingbut JSON, which
collect_data_files('volatility3.framework')already handles.So this is the only place the gap bites.
It dates from
e7c1126b, which moved the renderers into the new subdirectory.Before that they sat at
volatility3/framework/plugins/parquet_renderer.py,directly inside a package
collect_submodulescovers.What the user sees
The frozen build still advertises both renderers, because the framework
discovers them at runtime with
import_files, which usesos.walkand does notcare about
__init__.pyeither:Selecting one is an unhandled traceback rather than a clean error:
The fix
Add the missing
__init__.py, matching the other plugin subpackages. That isall
pkgutilneeds. Both specs use the samecollect_submodulesline, soneither spec needs editing and
volshell.exeis fixed at the same time.I preferred this over adding
hiddenimports = ['pyarrow', 'pyarrow.parquet']tothe specs, because that names one dependency of one renderer and leaves the
discovery gap in place for the next module added to the directory, and it would
have to be duplicated in both spec files.
Verification
Built
vol.specbefore and after on Windows with pyarrow installed, andcompared the analysis TOCs:
pyarrowentries inAnalysis-00.tocvol.exeRunning the built exe against
win-xp-laptop-2005-06-25.img:Both outputs read back correctly (23 rows, 2 columns,
Variable/Value) withpq.read_tableandpa.ipc.open_stream.Non-frozen behaviour is unchanged: 197 plugins discovered with and without the
change, zero import failures, identical plugin sets.
import_filesskipsfilenames starting with
__, so the new file is not itself imported as aplugin.
test/renderers/test_parquet_renderers.pypasses (4 passed, 4 skipped for theabsent Linux image).
ruff format,ruff checkandtest/volatility3_code_analysis.pyare clean.test/plugins/windows/windows.pygives an identical 58 failed / 23 passed withand without the change on my machine, so this is neutral for it. Those failures
are #2013, not this: the
_specific_tests pass a bare path to--single-location, which the CLI turns intofile://///E:\...on Windows.One thing worth deciding
The exe goes from 22.8 MB to 53.7 MB, because pyarrow brings a lot of
.pydand Arrow DLLs with it (
_dataset_parquet,_acero,_azurefs,_flightand friends). That is the cost of actually shipping the dependency the build
already installs, but it is a 2.4x jump and you may want it to be a deliberate
choice rather than a side effect of this fix. Measured with pyarrow 25.0.1 on
Python 3.14; CI is on 3.11 so the exact figures will differ.
If the size is unwelcome, the alternative is to drop
arrowfrom thepip installline inbuild-pyinstaller.ymland have the frozen build simplynot offer those renderers, but that needs the renderer to fail gracefully
instead of raising an unhandled
RuntimeError, which is a separate change.Happy to do it whichever way you prefer.
Possible follow-up
Nothing in CI would have caught this, and nothing would catch it coming back.
A smoke test in
build-pyinstaller.ymlbetween the build and move steps would:frameworkinfoneeds no memory image, and the renderer is constructed after theplugin runs, so this reaches the line that raises.
--helpwould not: argparseexits during parsing, well before
renderers[args.renderer]().I have left it out to keep this to the one file. Happy to add it here or
separately if you want it.