Components Using External Tools
It often makes sense to write a component which makes use of third-party tools in one way or
another. If these tools are distributed in Python modules, the module can simply be added to the
requirements in the OFRAK package's setup.py
file. But for dependencies on tools which cannot be
simply installed with pip
, OFRAK has another mechanism for the making the dependency clear to users
and easy to install.
ComponentExternalTool
ComponentExternalTool is a class that encapsulates the information
that OFRAK tracks about an external tool which a component depends on. Each component that uses an
external tool should include a ComponentExternalTool object for that tool in its
external_dependencies
field (empty by default, this field does not need to be provided for
components which do not use an external tool). The ZipUnpacker is an example:
- At the top of the file, the
ComponentExternalTool
is declared:
UNZIP_TOOL = ComponentExternalTool(
"unzip",
"https://linux.die.net/man/1/unzip",
install_check_arg="--help",
apt_package="unzip",
brew_package="unzip",
)
- In the declaration of the component itself, the tool is listed in the component's
external_dependencies
:
class ZipUnpacker(Unpacker[None]):
"""
Unpack (decompress) a zip archive.
"""
targets = (ZipArchive,)
children = (File, Folder, SpecialFileType)
external_dependencies = (UNZIP_TOOL,) # HERE
async def unpack(self, resource: Resource, config=None):
...
And that's it! This allows OFRAK to do a couple things:
1. When a user requests a list of the non-Python OFRAK dependencies they need to install, OFRAK can
see that ZipUnpacker
depends on unzip
and reports this:
python3 -m ofrak deps
[✓] unzip
https://linux.die.net/man/1/unzip
[ApkIdentifier, ZipUnpacker]
...
- OFRAK can catch certain errors when the component runs which obviously arise from the dependency not being found, and re-raise them in a way that makes it more clear to the user they are missing a dependency with some hints on how to install it:
ofrak.component.abstract.ComponentMissingDependencyError: Missing unzip tool needed for ZipUnpacker!
apt installation: apt install unzip
brew installation: brew install unzip
See https://linux.die.net/man/1/unzip for more info and installation help.
See the ComponentExternalTool docs for a breakdown of the fields of that class.
Edge Cases
One of the functions of ComponentExternalTool is to provide a way for
OFRAK to check if each dependency is installed. By default, this is done by running a command
formed from fields of the ComponentExternalTool
:
retcode = subprocess.call([self.tool, self.install_check_arg], ...)
This works for most cases (the install_check_arg
provides a lot of flexibility), but does not
cover certain edge cases. For example, SquashfsUnpacker requires
specifically versions of unsquashfs
with the -no-exit-code
flag. A user may already have
unsquashfs
installed, but an unsuitable version, so simply checking for unsquashfs
can give a
false negative result when a user is checking for missing dependencies.
In such cases, ComponentExternalTool should be subclassed for that
edge case dependency and its is_tool_installed
method be overwritten. For the unsquashfs
example:
class _UnsquashfsV45Tool(ComponentExternalTool):
def __init__(self):
super().__init__("unsquashfs", "https://github.com/plougher/squashfs-tools.git", "")
def is_tool_installed(self) -> bool:
try:
result = subprocess.run(
["unsquashfs", "-help"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError:
return False
if 0 != result.returncode:
return False
if b"-no-exit" not in result.stdout:
# Version 4.5+ has the required -no-exit option
return False
return True
Then the dependency on unsquashfs
can be included in the unpacker as usual:
UNSQUASHFS = _UnsquashfsV45Tool()
...
class SquashfsUnpacker(Unpacker[None]):
"""Unpack a SquashFS filesystem."""
targets = (SquashfsFilesystem,)
children = (File, Folder, SpecialFileType)
external_dependencies = (UNSQUASHFS,)
async def unpack(self, resource: Resource, config=None):
...