Creating Debian packages from binaries

Last post update: 2024-03-30 11:21:43

If you have binary files that you want to bundle into a Debian package to make use of existing package management tools, then this blog post could be beneficial to you as it describes all the necessary steps required to create an automated binary package building system.

First, ensure that the required tools are installed:

apt install dpkg dpkg-dev python

Create package building script file build.py with the following code:

import os, tempfile, subprocess, shutil, stat

class Package:
    def __init__(self, name, key_values):
        self.name = name
        self.key_values = key_values
    def apply(self, common):
        for key, value in common.items():
            if not key in self.key_values:
                self.key_values[key] = value
    def prepare(self):
        if not "Description" in self.key_values:
            self.key_values["Description"] = "Package for " + self.name + "\n ."
    def getControl(self):
        lines = []
        for key, value in self.key_values.items():
            lines.append(key + ": " + value)
        return "\n".join(lines) + "\n\n"
    control = property(getControl)
    def __getattr__(self, name):
        return self.key_values[name]
    def set_installed_size(self, size):
        self.key_values["Installed-Size"] = size
    def set(self, key, value):
        self.key_values[key] = value

def parseOptions(options):
    result = {}
    for option in options:
        key, value = option.split(": ", 1)
        result[key] = value
    return result

def parseDescription(description):
    result = []
    for line in description.split("\n"):
        if len(line) == 0:
            continue
        if line.startswith(" "):
            result[-1] += "\n" + line
        else:
            result.append(line)
    return result

def loadCommon():
    result = {}
    with open("common", "r") as file:
        options = parseDescription(file.read())
        result = parseOptions(options)
    return result

def loadPackages():
    result = []
    with open("packages", "r") as file:
        descriptions = file.read().split("\n\n")
        for description in descriptions:
            options = parseDescription(description)
            key_values = parseOptions(options)
            result.append(Package(key_values["Package"], key_values))
    return result

def isExecutalbeOrLibrary(name, stats):
    return stats.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) or name.startswith("lib")

run_dir = os.getcwd()

common = loadCommon()
packages = loadPackages()
for package in packages:
    package.apply(common)
    package.prepare()

with tempfile.TemporaryDirectory(prefix = "build-deb-") as temp_dir:
    for package in packages:
        package_dir = os.path.join(temp_dir, package.name)
        os.mkdir(package_dir)
        debian_dir = os.path.join(package_dir, "DEBIAN")
        os.mkdir(debian_dir)
        shlibs_debian_dir = os.path.join(package_dir, "debian")
        os.mkdir(shlibs_debian_dir)
        installed_size = 0
        created_dirs = []
        copied_files = []
        executables = []
        if os.path.isdir(package.name):
            for root, subdirs, files in os.walk(package.name):
                for name in subdirs:
                    path = os.path.join(temp_dir, root, name)
                    created_dirs.append(path)
                    os.mkdir(path)
                for name in files:
                    path = os.path.join(temp_dir, root, name)
                    copied_files.append(path)
                    shutil.copy(os.path.join(root, name), path)
                    stats = os.stat(path)
                    installed_size += stats.st_size
                    if isExecutalbeOrLibrary(name, stats):
                        executables.append(path)
        package.set_installed_size(str(installed_size // 1024))
        if len(executables) > 0:
            with open(os.path.join(shlibs_debian_dir, "control"), "w") as file:
                file.write("Source: " + package.name + "\n")
                file.write(package.control)
            os.chdir(package_dir)
            subprocess.run(["dpkg-shlibdeps"] + executables, check = True)
            os.chdir(run_dir)
        shlibdeps_output = None
        if os.path.isfile(os.path.join(shlibs_debian_dir, "substvars")):
            with open(os.path.join(shlibs_debian_dir, "substvars"), "r") as file:
                shlibdeps_output = file.read()
        for root, subdirs, files in os.walk(shlibs_debian_dir):
            for name in files:
                os.remove(os.path.join(shlibs_debian_dir, name))
        os.rmdir(shlibs_debian_dir)
        if shlibdeps_output != None:
            for line in shlibdeps_output.split("\n"):
                try:
                    name, value = line.split("shlibs:", 1)[1].split("=", 1)
                    package.set(name, value)
                except IndexError:
                    pass
        with open(os.path.join(debian_dir, "control"), "w") as file:
            file.write(package.control)
        subprocess.run(["dpkg-deb", "--root-owner-group", "--build", package_dir, "."], check = True)
        for path in copied_files:
            os.remove(os.path.join(package_dir, path))
        created_dirs.reverse()
        for path in created_dirs:
            os.rmdir(os.path.join(package_dir, path))

Create package description file packages with Debian control statements (Debian documentation) for each package separated by empty line:

Package: test-package-1
Version: 1.5.1

Package: test-package-2
Version: 1.9.2

Package: test-package-3
Version: 1.0

Create common package description file common with Debian control statements that apply to all packages:

Section: misc
Priority: optional
Architecture: amd64
Maintainer: User <user@example.com>

Create directories for each described package and place your binary files in them. Directory names must match package names specified with Package parameter in package description file. Each directory is treated as a root directory for package, so place executable files in usr/bin/ subdirectory.

Finally, run build script to create described packages in current directory:

python build.py