Commit ce420f6f authored by Jérémy Bobbio's avatar Jérémy Bobbio

Improve optional usage of external commands

The `tool_required` decorator now raises an exception when
the command cannot be found. This enables more flexible handling.
Associated Debian package is now suggested in the comment.

The list of external tools is now available through the `--list-tools`
command-line option. We also use this output to generate the Recommends field.
parent 866452ca
......@@ -23,34 +23,10 @@ This will compare `build1.changes` and `build2.changes` and create
External dependencies
---------------------
The various comparators rely on these external commands being available
in the path:
The various comparators rely on external commands being available. To
get a list of them, please run:
| command | Debian package |
+------------|--------------------|
| ar | binutils-multiarch |
| bzip2 | bzip2 |
| cpio | cpio |
| file | file |
| getfacl | acl |
| ghc | ghc |
| gpg | gnupg |
| gzip | gzip |
| ls | coreutils |
| lsattr | e2fsprogs |
| msgunfmt | gettext |
| objdump | binutils-multiarch |
| pdftk | pdftk |
| pdftotext | poppler-utils |
| readelf | binutils-multiarch |
| rpm2cpio | rpm2cpio |
| showttf | fontforge-extras |
| sng | sng |
| unsquashfs | squashfs-tools |
| vim | vim |
| xxd | vim-common |
| xz | xz-utils |
| zipinfo | unzip |
$ ./debbindiff.py --list-tools
Authors
-------
......
......@@ -37,6 +37,8 @@ def create_parser():
'of Debian packages')
parser.add_argument('--version', action='version',
version='debbindiff %s' % VERSION)
parser.add_argument('--list-tools', nargs=0, action=ListToolsAction,
help='show external tools required and exit')
parser.add_argument('--debug', dest='debug', action='store_true',
default=False, help='display debug messages')
parser.add_argument('--html', metavar='output', dest='html_output',
......@@ -52,6 +54,7 @@ def create_parser():
parser.add_argument('file2', help='second file to compare')
return parser
@contextmanager
def make_printer(path):
if path == '-':
......@@ -66,6 +69,17 @@ def make_printer(path):
output.close()
class ListToolsAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
from debbindiff.comparators.utils import tool_required, RequiredToolNotFound
print("External tools required:")
print(', '.join(tool_required.all))
print()
print("Available in packages:")
print(', '.join(sorted(set([RequiredToolNotFound.PROVIDERS[k]["debian"] for k in tool_required.all]))))
sys.exit(0)
def main():
parser = create_parser()
parsed_args = parser.parse_args(sys.argv[1:])
......
......@@ -26,6 +26,7 @@ from debbindiff.difference import get_source
@contextmanager
@tool_required('bzip2')
def decompress_bzip2(path):
with make_temp_directory() as temp_dir:
if path.endswith('.bz2'):
......@@ -40,7 +41,6 @@ def decompress_bzip2(path):
@binary_fallback
@tool_required('bzip2')
def compare_bzip2_files(path1, path2, source=None):
with decompress_bzip2(path1) as new_path1:
with decompress_bzip2(path2) as new_path2:
......
......@@ -25,6 +25,7 @@ from debbindiff.comparators.utils import binary_fallback, make_temp_directory, t
from debbindiff.difference import Difference
@tool_required('cpio')
def get_cpio_content(path, verbose=False):
cmd = ['cpio', '--quiet', '-tF', path]
if verbose:
......@@ -32,6 +33,7 @@ def get_cpio_content(path, verbose=False):
return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
@tool_required('cpio')
def extract_cpio_archive(path, destdir):
cmd = ['cpio', '--no-absolute-filenames', '--quiet', '-idF',
os.path.abspath(path)]
......@@ -44,7 +46,6 @@ def extract_cpio_archive(path, destdir):
@binary_fallback
@tool_required('cpio')
def compare_cpio_files(path1, path2, source=None):
differences = []
......
......@@ -30,6 +30,7 @@ def ls(path):
return subprocess.check_output(['ls', path], shell=False).decode('utf-8')
@tool_required('stat')
def stat(path):
output = subprocess.check_output(['stat', path], shell=False).decode('utf-8')
output = re.sub(r'^\s*File:.*$', '', output, flags=re.MULTILINE)
......@@ -37,6 +38,7 @@ def stat(path):
return output
@tool_required('lsattr')
def lsattr(path):
try:
output = subprocess.check_output(['lsattr', '-d', path], shell=False, stderr=subprocess.STDOUT).decode('utf-8')
......@@ -47,34 +49,44 @@ def lsattr(path):
return ''
@tool_required('getfacl')
def getfacl(path):
return subprocess.check_output(['getfacl', '-p', '-c', path], shell=False).decode('utf-8')
@tool_required('stat')
@tool_required('lsattr')
@tool_required('getfacl')
def compare_meta(path1, path2):
logger.debug('compare_meta(%s, %s)' % (path1, path2))
differences = []
stat1 = stat(path1)
stat2 = stat(path2)
if stat1 != stat2:
differences.append(Difference(
stat1.splitlines(1), stat2.splitlines(1),
path1, path2, source="stat"))
lsattr1 = lsattr(path1)
lsattr2 = lsattr(path2)
if lsattr1 != lsattr2:
differences.append(Difference(
lsattr1.splitlines(1), lsattr2.splitlines(1),
path1, path2, source="lattr"))
acl1 = getfacl(path1)
acl2 = getfacl(path2)
if acl1 != acl2:
differences.append(Difference(
acl1.splitlines(1), acl2.splitlines(1),
path1, path2, source="getfacl"))
try:
stat1 = stat(path1)
stat2 = stat(path2)
if stat1 != stat2:
differences.append(Difference(
stat1.splitlines(1), stat2.splitlines(1),
path1, path2, source="stat"))
except RequiredToolNotFound:
logger.warn("'stat' not found! Is PATH wrong?")
try:
lsattr1 = lsattr(path1)
lsattr2 = lsattr(path2)
if lsattr1 != lsattr2:
differences.append(Difference(
lsattr1.splitlines(1), lsattr2.splitlines(1),
path1, path2, source="lattr"))
except RequiredToolNotFound:
logger.info("Unable to find 'lsattr'.")
try:
acl1 = getfacl(path1)
acl2 = getfacl(path2)
if acl1 != acl2:
differences.append(Difference(
acl1.splitlines(1), acl2.splitlines(1),
path1, path2, source="getfacl"))
except RequiredToolNotFound:
logger.info("Unable to find 'getfacl'.")
return differences
......
......@@ -24,6 +24,7 @@ from debbindiff.comparators.utils import binary_fallback, get_ar_content, tool_r
from debbindiff.difference import Difference
@tool_required('readelf')
def readelf_all(path):
output = subprocess.check_output(
['readelf', '--all', path],
......@@ -32,6 +33,7 @@ def readelf_all(path):
return re.sub(re.escape(path), os.path.basename(path), output)
@tool_required('readelf')
def readelf_debug_dump(path):
output = subprocess.check_output(
['readelf', '--debug-dump', path],
......@@ -40,6 +42,7 @@ def readelf_debug_dump(path):
return re.sub(re.escape(path), os.path.basename(path), output)
@tool_required('objdump')
def objdump_disassemble(path):
output = subprocess.check_output(
['objdump', '--disassemble', path],
......@@ -74,15 +77,11 @@ def _compare_elf_data(path1, path2, source=None):
@binary_fallback
@tool_required('readelf')
@tool_required('objdump')
def compare_elf_files(path1, path2, source=None):
return _compare_elf_data(path1, path2, source=None)
@binary_fallback
@tool_required('readelf')
@tool_required('objdump')
def compare_static_lib_files(path1, path2, source=None):
differences = []
# look up differences in metadata
......
......@@ -22,12 +22,12 @@ from debbindiff.comparators.utils import binary_fallback, tool_required
from debbindiff.difference import Difference
@tool_required('showttf')
def show_ttf(path):
return subprocess.check_output(['showttf', path], shell=False)
@binary_fallback
@tool_required('showttf')
def compare_ttf_files(path1, path2, source=None):
ttf1 = show_ttf(path1)
ttf2 = show_ttf(path2)
......
......@@ -22,12 +22,12 @@ from debbindiff.comparators.utils import binary_fallback, tool_required
from debbindiff.difference import Difference
@tool_required('msgunfmt')
def msgunfmt(path):
return subprocess.check_output(['msgunfmt', path], shell=False)
@binary_fallback
@tool_required('msgunfmt')
def compare_mo_files(path1, path2, source=None):
mo1 = msgunfmt(path1)
mo2 = msgunfmt(path2)
......
......@@ -26,6 +26,7 @@ from debbindiff.difference import Difference, get_source
@contextmanager
@tool_required('gzip')
def decompress_gzip(path):
with make_temp_directory() as temp_dir:
if path.endswith('.gz'):
......@@ -39,13 +40,12 @@ def decompress_gzip(path):
yield temp_path
@tool_required('file')
def get_gzip_metadata(path):
return subprocess.check_output(['file', '--brief', path])
@binary_fallback
@tool_required('gzip')
@tool_required('file')
def compare_gzip_files(path1, path2, source=None):
differences = []
# check metadata
......
......@@ -22,12 +22,12 @@ from debbindiff.comparators.utils import binary_fallback, tool_required
from debbindiff.difference import Difference
@tool_required('ghc')
def show_iface(path):
return subprocess.check_output(['ghc', '--show-iface', path], shell=False)
@binary_fallback
@tool_required('ghc')
def compare_hi_files(path1, path2, source=None):
iface1 = show_iface(path1)
iface2 = show_iface(path2)
......
......@@ -22,6 +22,7 @@ from debbindiff.comparators.utils import binary_fallback, tool_required
from debbindiff.difference import Difference, get_source
@tool_required('pdftk')
def uncompress(path):
output = subprocess.check_output(
['pdftk', path, 'output', '-', 'uncompress'],
......@@ -29,6 +30,7 @@ def uncompress(path):
return output.decode('latin-1').encode('ascii', 'backslashreplace')
@tool_required('pdftotext')
def pdftotext(path):
return subprocess.check_output(
['pdftotext', path, '-'],
......@@ -36,8 +38,6 @@ def pdftotext(path):
@binary_fallback
@tool_required('pdftk')
@tool_required('pdftotext')
def compare_pdf_files(path1, path2, source=None):
differences = []
src = get_source(path1, path2) or 'FILE'
......
......@@ -22,6 +22,7 @@ from debbindiff.comparators.utils import binary_fallback, tool_required
from debbindiff.difference import Difference
@tool_required('sng')
def sng(path):
with open(path) as f:
p = subprocess.Popen(['sng'], shell=False, close_fds=True,
......@@ -33,7 +34,6 @@ def sng(path):
return out
@binary_fallback
@tool_required('sng')
def compare_png_files(path1, path2, source=None):
sng1 = sng(path1)
sng2 = sng(path2)
......
......@@ -48,6 +48,7 @@ def get_rpm_header(path, ts):
@contextmanager
@tool_required('rpm2cpio')
def extract_rpm_payload(path):
cmd = ['rpm2cpio', path]
with make_temp_directory() as temp_dir:
......@@ -63,7 +64,6 @@ def extract_rpm_payload(path):
@binary_fallback
@tool_required('rpm2cpio')
def compare_rpm_files(path1, path2, source=None):
try:
import rpm
......
......@@ -25,6 +25,7 @@ from debbindiff.comparators.utils import binary_fallback, make_temp_directory, t
from debbindiff.difference import Difference
@tool_required('unsquashfs')
def get_squashfs_content(path, verbose=True):
cmd = ['unsquashfs', '-d', '', '-ls', path]
content = ''
......@@ -37,6 +38,7 @@ def get_squashfs_content(path, verbose=True):
return content + subprocess.check_output(cmd, shell=False)
@tool_required('unsquashfs')
def extract_squashfs(path, destdir):
cmd = ['unsquashfs', '-n', '-f', '-d', destdir, path]
logger.debug("extracting %s into %s", path, destdir)
......@@ -48,7 +50,6 @@ def extract_squashfs(path, destdir):
@binary_fallback
@tool_required('unsquashfs')
def compare_squashfs_files(path1, path2, source=None):
differences = []
......
......@@ -72,18 +72,67 @@ def binary_fallback(original_function):
difference.comment = \
"Command `%s` exited with %d. Output:\n%s" \
% (cmd, e.returncode, output)
except RequiredToolNotFound as e:
difference = compare_binary_files(path1, path2, source=source)[0]
difference.comment = \
"'%s' not available in path. Falling back to binary comparison." % e.command
package = e.get_package()
if package:
difference.comment += "\nInstall '%s' to get a better output." % package
return [difference]
return with_fallback
class RequiredToolNotFound(Exception):
PROVIDERS = { 'ar': { 'debian': 'binutils-multiarch' }
, 'bzip2': { 'debian': 'bzip2' }
, 'cpio': { 'debian': 'cpio' }
, 'file': { 'debian': 'file' }
, 'getfacl': { 'debian': 'acl' }
, 'ghc': { 'debian': 'ghc' }
, 'gpg': { 'debian': 'gnupg' }
, 'gzip': { 'debian': 'gzip' }
, 'ls': { 'debian': 'coreutils' }
, 'lsattr': { 'debian': 'e2fsprogs' }
, 'msgunfmt': { 'debian': 'gettext' }
, 'objdump': { 'debian': 'binutils-multiarch' }
, 'pdftk': { 'debian': 'pdftk' }
, 'pdftotext': { 'debian': 'poppler-utils' }
, 'readelf': { 'debian': 'binutils-multiarch' }
, 'rpm2cpio': { 'debian': 'rpm2cpio' }
, 'showttf': { 'debian': 'fontforge-extras' }
, 'sng': { 'debian': 'sng' }
, 'stat': { 'debian': 'coreutils' }
, 'unsquashfs': { 'debian': 'squashfs-tools' }
, 'vim': { 'debian': 'vim' }
, 'xxd': { 'debian': 'vim-common' }
, 'xz': { 'debian': 'xz-utils' }
, 'zipinfo': { 'debian': 'unzip' }
}
def __init__(self, command):
self.command = command
def get_package(self):
providers = RequiredToolNotFound.PROVIDERS.get(self.command, None)
if not providers:
return None
# XXX: hardcode Debian for now
return providers['debian']
# decorator that checks if the specified tool is installed
def tool_required(filename):
def tool_required(command):
if not hasattr(tool_required, 'all'):
tool_required.all = set()
tool_required.all.add(command)
def wrapper(original_function):
def tool_check(*args):
if not find_executable(filename):
logger.info("Tool '%s' not found." % filename)
return []
return original_function(*args)
if find_executable(command):
def tool_check(*args):
return original_function(*args)
else:
def tool_check(*args):
raise RequiredToolNotFound(command)
return tool_check
return wrapper
......
......@@ -26,6 +26,7 @@ from debbindiff.difference import get_source
@contextmanager
@tool_required('xz')
def decompress_xz(path):
with make_temp_directory() as temp_dir:
if path.endswith('.xz'):
......@@ -40,7 +41,6 @@ def decompress_xz(path):
@binary_fallback
@tool_required('xz')
def compare_xz_files(path1, path2, source=None):
with decompress_xz(path1) as new_path1:
with decompress_xz(path2) as new_path2:
......
......@@ -27,6 +27,7 @@ import debbindiff.comparators
from debbindiff.comparators.utils import binary_fallback, make_temp_directory, tool_required
@tool_required('zipinfo')
def get_zipinfo(path, verbose=False):
if verbose:
cmd = ['zipinfo', '-v', path]
......@@ -38,7 +39,6 @@ def get_zipinfo(path, verbose=False):
@binary_fallback
@tool_required('zipinfo')
def compare_zip_files(path1, path2, source=None):
differences = []
with ZipFile(path1, 'r') as zip1:
......
......@@ -25,23 +25,7 @@ Depends: diffutils,
vim-common,
${misc:Depends},
${python:Depends},
Recommends: acl,
binutils-multiarch,
bzip2,
cpio,
e2fsprogs,
file,
fontforge-extras,
gettext,
ghc,
gzip,
pdftk,
poppler-utils,
rpm2cpio,
sng,
squashfs-tools,
unzip,
xz-utils
Recommends: ${debbindiff:Recommends}
Description: highlight differences between two builds of Debian packages
debbindiff was designed to easily compare two builds of the same Debian
package, and understand their differences.
......
......@@ -5,6 +5,10 @@ VERSION = $(shell dpkg-parsechangelog --show-field Version)
%:
dh $@ --with python2 --buildsystem=python_distutils
override_dh_auto_build:
dh_auto_build -O--buildsystem=python_distutils
echo "debbindiff:Recommends=$$(./debbindiff.py --list-tools | tail -n 1)" >> debian/substvars
override_dh_install:
mv debian/debbindiff/usr/bin/debbindiff.py debian/debbindiff/usr/bin/debbindiff
dh_install -O--buildsystem=python_distutils
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment