talideon.com

Blackout Ireland

January 21, 2008 at 1:34PM Port management tools from my ~/bin directory

Here’s two small tools I have in my ~/bin directory. The first one, check-deps, checks the upwards and downwards dependencies for each package are consistent:

#!/usr/bin/env python

from __future__ import with_statement
import os

PKG_DB = '/var/db/pkg'

def each_dep(lines):
    """Extracts the package dependencies from the iterable."""
    return (line[8:-1] for line in lines if line.startswith('@pkgdep '))

# Figure out from the dependencies which packages are required by which.
reqs = {}
for pkg in os.listdir(PKG_DB):
    path = os.path.join(PKG_DB, pkg, "+CONTENTS")
    if os.path.isfile(path):
        with open(path) as f:
            for dep in each_dep(f):
                if dep not in reqs:
                    reqs[dep] = set()
                reqs[dep].add(pkg)

# Check that the currently recorded ones are valid.
for pkg in reqs.keys():
    path = os.path.join(PKG_DB, pkg, "+REQUIRED_BY")
    if os.path.isfile(path):
        with open(path) as f:
            for line in f:
                line = line[:-1]
                if line in reqs[pkg]:
                    reqs[pkg].remove(line)
                else:
                    print "===>\tErroneous dep '%s' in '%s'" & (line, pkg)
    if len(reqs[pkg]) == 0:
        del reqs[pkg]
    else:
        print "===>\t%s does not record:\n%s" % (pkg, "\n".join(reqs[pkg]))

if len(reqs) == 0:
    print "All clean!"

The second one, check-packing, checks that the file manifest for each port is correct and that all the files are intact by comparing them against their recorded MD5 hashes:

#!/usr/bin/env python

from __future__ import with_statement
import os
import hashlib
import sys

PKG_DB = '/var/db/pkg'


def each_file(f):
    """
    Generates a list of tuples, the first element being an absolute file
    path, and the second being its recorded hash, from a package manifest.
    """
    cwd = ''
    file = ''
    try:
        for line in f:
            if line[0] != '@':
                file = os.path.join(cwd, line.strip())
            elif line.startswith('@cwd '):
                new_cwd = line.split(' ', 2)[1].strip()
                # This check is a hack for dealing with broken manifests.
                # mysql-client-5.0, nss, pth, portupgrade and zsh are
                # examples.
                if new_cwd != '':
                    cwd = new_cwd
            elif file != '' and line.startswith('@comment MD5:'):
                digest = line.split(':', 2)[1].strip()
                yield (file, digest)
                file = ''
    finally:
        pass


def hash_matches(path, original_hash):
    """Checks if the given file matches a hash."""
    with open(path) as f:
        new_hash = hashlib.md5(f.read()).hexdigest()
    return new_hash == original_hash


for pkg in sorted(os.listdir(PKG_DB)):
    path = os.path.join(PKG_DB, pkg, "+CONTENTS")
    if os.path.isfile(path):
        print "===>\tChecking %s" % (pkg,)
        with open(path) as f:
            for (file_path, hash) in each_file(f):
                try:
                    if not os.path.exists(file_path):
                        if os.path.islink(file_path):
                            print "Missing: %s -> %s" % (file_path, os.path.realpath(file_path))
                        else:
                            print "Missing: %s" % (file_path,)
                    elif not os.path.islink(file_path) and not hash_matches(file_path, hash):
                        print "Altered: %s" % (file_path,)
                except KeyboardInterrupt:
                    raise
                except:
                    print "Unchecked: %s" % (file_path,)

They might be useful to someone.

Update (Feb 12th): Removed some stupid inefficiencies in how the files are hashed.