diff options
Diffstat (limited to 'debian/bin')
-rwxr-xr-x | debian/bin/check_upstream.py | 91 | ||||
-rwxr-xr-x | debian/bin/gencontrol.py | 341 | ||||
-rwxr-xr-x | debian/bin/release-update | 95 | ||||
-rwxr-xr-x | debian/bin/update-modinfo | 60 |
4 files changed, 587 insertions, 0 deletions
diff --git a/debian/bin/check_upstream.py b/debian/bin/check_upstream.py new file mode 100755 index 0000000..e2931c4 --- /dev/null +++ b/debian/bin/check_upstream.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import errno, filecmp, fnmatch, glob, os.path, re, sys +from debian import deb822 +from enum import Enum + +sys.path.insert(0, "debian/lib/python") +rules_defs = dict((match.group(1), match.group(2)) + for line in open('debian/rules.defs') + for match in [re.match(r'(\w+)\s*:=\s*(.*)\n', line)]) +sys.path.append('/usr/share/linux-support-%s/lib/python' % + rules_defs['KERNELVERSION']) +from debian_linux.firmware import FirmwareWhence +from config import Config + +class DistState(Enum): + undistributable = 1 + non_free = 2 + free = 3 + +def is_source_available(section): + for file_info in section.files.values(): + if not (file_info.source or file_info.binary.endswith('.cis')): + return False + return True + +def check_section(section): + if re.search(r'^BSD\b' + r'|^GPLv2 or OpenIB\.org BSD\b' + r'|\bPermission\s+is\s+hereby\s+granted\s+for\s+the\s+' + r'distribution\s+of\s+this\s+firmware\s+(?:data|image)\b' + r'(?!\s+as\s+part\s+of)' + r'|\bRedistribution\s+and\s+use\s+in(?:\s+source\s+and)?' + r'\s+binary\s+forms\b' + r'|\bPermission\s+is\s+hereby\s+granted\b[^.]+\sto' + r'\s+deal\s+in\s+the\s+Software\s+without' + r'\s+restriction\b' + r'|\bredistributable\s+in\s+binary\s+form\b', + section.licence): + return (DistState.free if is_source_available(section) + else DistState.non_free) + elif re.match(r'^(?:D|Red)istributable\b', section.licence): + return DistState.non_free + elif re.match(r'^GPL(?:v[23]|\+)?\b|^Dual GPL(?:v[23])?/', section.licence): + return (DistState.free if is_source_available(section) + else DistState.undistributable) + else: + # Unrecognised and probably undistributable + return DistState.undistributable + +def main(source_dir='.'): + config = Config() + over_dirs = ['debian/config/' + package for + package in config['base',]['packages']] + with open("debian/copyright") as f: + exclusions = deb822.Deb822(f).get("Files-Excluded", '').strip().split() + packaged_files = {} + for package in config['base',]['packages']: + for filename in config['base', package]['files']: + packaged_files[filename] = package + + for section in FirmwareWhence(open(os.path.join(source_dir, 'WHENCE'))): + dist_state = check_section(section) + for file_info in section.files.values(): + if dist_state == DistState.non_free: + if not any(fnmatch.fnmatch(file_info.binary, exclusion) + for exclusion in exclusions): + if file_info.binary in packaged_files: + update_file(source_dir, over_dirs, file_info.binary) + elif os.path.isfile(filename): + print('I: %s is not included in any binary package' % + file_info.binary) + else: + print('I: %s: could be added' % file_info.binary) + elif dist_state == DistState.undistributable: + if os.path.isfile(file_info.binary): + print('W: %s appears to be undistributable' % + file_info.binary) + +def update_file(source_dir, over_dirs, filename): + source_file = os.path.join(source_dir, filename) + for over_dir in over_dirs: + for over_file in ([os.path.join(over_dir, filename)] + + glob.glob(os.path.join(over_dir, filename + '-*'))): + if os.path.isfile(over_file): + if not filecmp.cmp(source_file, over_file, True): + print('I: %s: changed' % filename) + return + +if __name__ == '__main__': + main(*sys.argv[1:]) diff --git a/debian/bin/gencontrol.py b/debian/bin/gencontrol.py new file mode 100755 index 0000000..293432b --- /dev/null +++ b/debian/bin/gencontrol.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 + +import io +import json +import locale +import os +import re +import sys + +sys.path.insert(0, "debian/lib/python") +sys.path.append(sys.argv[1] + "/lib/python") +locale.setlocale(locale.LC_CTYPE, "C.UTF-8") + +from config import Config +from debian_linux.debian import BinaryPackage, PackageRelation, _ControlFileDict +from debian_linux.debian import PackageDescription as PackageDescriptionBase +import debian_linux.gencontrol +from debian_linux.gencontrol import Makefile, MakeFlags, PackagesList +from debian_linux.utils import TextWrapper +from debian_linux.utils import Templates as TemplatesBase +from collections import OrderedDict + +class PackageDescription(PackageDescriptionBase): + __slots__ = () + + def __init__(self, value = None): + self.short = [] + self.long = [] + if value is not None: + value = value.split("\n", 1) + self.append_short(value[0]) + if len(value) > 1: + self.append(value[1]) + + def __str__(self): + wrap = TextWrapper(width = 74, fix_sentence_endings = True).wrap + short = ', '.join(self.short) + long_pars = [] + for t in self.long: + if isinstance(t, str): + t = wrap(t) + long_pars.append('\n '.join(t)) + long = '\n .\n '.join(long_pars) + return short + '\n ' + long + + def append_pre(self, l): + self.long.append(l) + + def extend(self, desc): + if isinstance(desc, PackageDescription): + self.short.extend(desc.short) + self.long.extend(desc.long) + elif isinstance(desc, (list, tuple)): + for i in desc: + self.append(i) + +BinaryPackage._fields['Description'] = PackageDescription + +class Template(_ControlFileDict): + _fields = OrderedDict(( + ('Template', str), + ('Type', str), + ('Default', str), + ('Description', PackageDescriptionBase), + )) + + +class Templates(TemplatesBase): + def get_templates_control(self, key: str, context: dict[str, str] = {}) -> Template: + return Template.read_rfc822(io.StringIO(self.get(key, context))) + + +class GenControl(debian_linux.gencontrol.Gencontrol): + def __init__(self): + self.config = Config() + self.templates = Templates() + + with open('debian/modinfo.json', 'r') as f: + self.modinfo = json.load(f) + + # Make another dict keyed by firmware names + self.firmware_modules = {} + for name, info in self.modinfo.items(): + for firmware_filename in info['firmware']: + self.firmware_modules.setdefault(firmware_filename, []) \ + .append(name) + + def __call__(self): + packages = PackagesList() + makefile = Makefile() + + self.do_source(packages) + self.do_extra(packages, makefile) + self.do_main(packages, makefile) + + self.write(packages, makefile) + + def do_source(self, packages): + packages['source'] = self.templates.get_source_control("control.source", {})[0] + + def do_extra(self, packages, makefile): + config_entry = self.config['base',] + vars = {} + vars.update(config_entry) + + for package_binary in self.templates.get_control("control.extra", {}): + assert package_binary['Package'].startswith('firmware-') + package = package_binary['Package'].replace('firmware-', '') + + makeflags = MakeFlags() + makeflags['FILES'] = '' + makeflags['PACKAGE'] = package + makefile.add_cmds('binary-indep', ["$(MAKE) -f debian/rules.real binary-indep %s" % makeflags]) + + packages.append(package_binary) + + def do_main(self, packages, makefile): + config_entry = self.config['base',] + vars = {} + vars.update(config_entry) + + makeflags = MakeFlags() + + for i in ('build', 'binary-arch', 'setup'): + makefile.add_cmds("%s_%%" % i, ["@true"]) + + for package in config_entry['packages']: + self.do_package(packages, makefile, package, vars.copy(), makeflags.copy()) + + def do_package(self, packages, makefile, package, vars, makeflags): + config_entry = self.config['base', package] + vars.update(config_entry) + vars['package'] = package + vars['package-env-prefix'] = 'FIRMWARE_' + package.upper().replace('-', '_') + + makeflags['PACKAGE'] = package + + # Those might be absent, set them to empty string for replacement to work: + empty_list = ['replaces', 'conflicts', 'breaks', 'provides', 'recommends'] + for optional in ['replaces', 'conflicts', 'breaks', 'provides', 'recommends']: + if optional not in vars: + vars[optional] = '' + + package_dir = "debian/config/%s" % package + + try: + os.unlink('debian/firmware-%s.bug-presubj' % package) + except OSError: + pass + os.symlink('bug-presubj', 'debian/firmware-%s.bug-presubj' % package) + + files_orig = config_entry['files'] + files_real = {} + files_unused = [] + links = {} + links_rev = {} + + # Look for additional and replacement files in binary package config + for root, dirs, files in os.walk(package_dir): + try: + dirs.remove('.svn') + except ValueError: + pass + for f in files: + cur_path = root + '/' + f + if root != package_dir: + f = root[len(package_dir) + 1 : ] + '/' + f + if os.path.islink(cur_path): + if f in files_orig: + links[f] = os.readlink(cur_path) + continue + f1 = f.rsplit('-', 1) + if f in files_orig: + files_real[f] = f, cur_path, None + continue + if len(f1) > 1: + f_base, f_version = f1 + if f_base in files_orig: + if f_base in files_real: + raise RuntimeError("Multiple files for %s" % f_base) + files_real[f_base] = f_base, package_dir + '/' + f, \ + f_version + continue + # Whitelist files not expected to be installed as firmware + if f in ['defines', 'LICENSE.install', + 'update.py', 'update.sh']: + continue + files_unused.append(f) + + # Take all the other files from upstream + for f in files_orig: + if f not in files_real and f not in links: + f_upstream = os.path.join('debian/build/install', f) + if os.path.islink(f_upstream): + links[f] = os.readlink(f_upstream) + elif os.path.isfile(f_upstream): + files_real[f] = f, f_upstream, None + + for f in links: + link_target = os.path.normpath(os.path.join(f, '..', links[f])) + links_rev.setdefault(link_target, []).append(f) + + if files_unused: + print('W: %s: unused files:' % package, ' '.join(files_unused), + file=sys.stderr) + + makeflags['FILES'] = ' '.join(["%s:%s" % (i[1], i[0]) for i in sorted(files_real.values())]) + vars['files_real'] = ' '.join(["/lib/firmware/%s" % i for i in config_entry['files']]) + + makeflags['LINKS'] = ' '.join(["%s:%s" % (link, target) + for link, target in sorted(links.items())]) + + files_desc = ["Contents:"] + firmware_meta_temp = self.templates.get("metainfo.xml.firmware") + firmware_meta_list = [] + module_names = set() + + wrap = TextWrapper(width = 71, fix_sentence_endings = True, + initial_indent = ' * ', + subsequent_indent = ' ').wrap + for f in config_entry['files']: + firmware_meta_list.append(self.substitute(firmware_meta_temp, + {'filename': f})) + for module_name in self.firmware_modules.get(f, []): + module_names.add(module_name) + if f in links: + continue + f, f_real, version = files_real[f] + c = self.config.get(('base', package, f), {}) + desc = c.get('desc') + if version is None: + version = c.get('version') + try: + f = f + ', ' + ', '.join(sorted(links_rev[f])) + except KeyError: + pass + if desc and version: + desc = "%s, version %s (%s)" % (desc, version, f) + elif desc: + desc = "%s (%s)" % (desc, f) + else: + desc = "%s" % f + files_desc.extend(wrap(desc)) + + modaliases = set() + for module_name in module_names: + for modalias in self.modinfo[module_name]['alias']: + modaliases.add(modalias) + modalias_meta_list = [ + self.substitute(self.templates.get("metainfo.xml.modalias"), + {'alias': alias}) + for alias in sorted(list(modaliases)) + ] + + packages_binary = self.templates.get_control("control.binary", vars) + + packages_binary[0]['Description'].append_pre(files_desc) + + if 'initramfs-tools' in config_entry.get('support', []): + postinst = self.templates.get('postinst.initramfs-tools') + open("debian/firmware-%s.postinst" % package, 'w').write(self.substitute(postinst, vars)) + + if 'license-accept' in config_entry: + license = open("%s/LICENSE.install" % package_dir, 'r').read() + preinst = self.templates.get('preinst.license') + preinst_filename = "debian/firmware-%s.preinst" % package + open(preinst_filename, 'w').write(self.substitute(preinst, vars)) + + templates = self.templates.get_templates_control('templates.license', vars) + templates[0]['Description'].append(re.sub('\n\n', '\n.\n', license)) + templates_filename = "debian/firmware-%s.templates" % package + self.write_rfc822(open(templates_filename, 'w'), templates) + + desc = packages_binary[0]['Description'] + desc.append( +"""This firmware is covered by the %s. +You must agree to the terms of this license before it is installed.""" +% vars['license-title']) + packages_binary[0]['Pre-Depends'] = PackageRelation('debconf | debconf-2.0') + + packages.extend(packages_binary) + + makefile.add_cmds('binary-indep', ["$(MAKE) -f debian/rules.real binary-indep %s" % makeflags]) + + vars['firmware-list'] = ''.join(firmware_meta_list) + vars['modalias-list'] = ''.join(modalias_meta_list) + # Underscores are preferred to hyphens + vars['package-metainfo'] = package.replace('-', '_') + # Summary must not contain line breaks + vars['longdesc-metainfo'] = re.sub(r'\s+', ' ', vars['longdesc']) + package_meta_temp = self.templates.get("metainfo.xml", {}) + # XXX Might need to escape some characters + open("debian/firmware-%s.metainfo.xml" % package, 'w').write(self.substitute(package_meta_temp, vars)) + + def process_template(self, in_entry, vars): + e = Template() + for key, value in in_entry.items(): + if isinstance(value, PackageDescription): + e[key] = self.process_description(value, vars) + elif key[:2] == 'X-': + pass + else: + e[key] = self.substitute(value, vars) + return e + + def process_templates(self, in_entries, vars): + entries = [] + for i in in_entries: + entries.append(self.process_template(i, vars)) + return entries + + def substitute(self, s, vars): + if isinstance(s, (list, tuple)): + return [self.substitute(i, vars) for i in s] + def subst(match): + if match.group(1): + return vars.get(match.group(2), '') + else: + return vars[match.group(2)] + return re.sub(r'@(\??)([-_a-z]+)@', subst, str(s)) + + def write(self, packages, makefile): + self.write_control(packages.values()) + self.write_makefile(makefile) + + def write_control(self, list): + self.write_rfc822(open("debian/control", 'w'), list) + + def write_makefile(self, makefile): + f = open("debian/rules.gen", 'w') + makefile.write(f) + f.close() + + def write_rfc822(self, f, list): + for entry in list: + for key, value in entry.items(): + f.write("%s: %s\n" % (key, value)) + f.write('\n') + +if __name__ == '__main__': + GenControl()() diff --git a/debian/bin/release-update b/debian/bin/release-update new file mode 100755 index 0000000..5eb75cd --- /dev/null +++ b/debian/bin/release-update @@ -0,0 +1,95 @@ +#!/usr/bin/python3 + +import sys +sys.path.append(sys.path[0] + "/../lib/python") + +import os, re, subprocess +import locale + +from debian_linux.debian import Changelog, Version + +def print_stable_log(log, cur_ver, new_ver): + log.flush() # serialise our output with git's + subprocess.check_call(['git', 'log', '--reverse', '--no-merges', + '--pretty= - %s', + '{}..{}'.format(cur_ver, new_ver)], + stdout=log) + +def main(repo, new_ver): + locale.setlocale(locale.LC_CTYPE, "C.UTF-8") + os.environ['GIT_DIR'] = repo + '/.git' + + changelog = Changelog(version=Version) + cur_ver = changelog[0].version.upstream + + # Nothing to update + if cur_ver == new_ver: + sys.exit(0) + + new_pkg_ver = new_ver + '-1' + + # Three possible cases: + # 1. The current version has been released so we need to add a new + # version to the changelog. + # 2. The current version has not been released so we're changing its + # version string. + # (a) There are no stable updates included in the current version, + # so we need to insert an introductory line, the URL(s) and + # git log(s) and a blank line at the top. + # (b) One or more stable updates are already included in the current + # version, so we need to insert the URL(s) and git log(s) after + # them. + + changelog_intro = 'New upstream version:' + + # Case 1 + if changelog[0].distribution != 'UNRELEASED': + subprocess.check_call(['dch', '-v', new_pkg_ver, '-D', 'UNRELEASED', + changelog_intro]) + + with open('debian/changelog', 'r') as old_log: + with open('debian/changelog.new', 'w') as new_log: + line_no = 0 + inserted = False + intro_line = ' * {}\n'.format(changelog_intro) + + for line in old_log: + line_no += 1 + + # Case 2 + if changelog[0].distribution == 'UNRELEASED' and line_no == 1: + print('{} ({}) UNRELEASED; urgency={}' + .format(changelog[0].source, new_pkg_ver, + changelog[0].urgency), + file=new_log) + continue + + if not inserted: + # Case 2(a) + if line_no == 3 and line != intro_line: + new_log.write(intro_line) + print_stable_log(new_log, cur_ver, new_ver) + new_log.write('\n') + inserted = True + # Case 1 or 2(b) + elif line_no > 3 and line == '\n': + print_stable_log(new_log, cur_ver, new_ver) + inserted = True + + # Check that we inserted before hitting the end of the + # first version entry + assert not (line.startswith(' -- ') and not inserted) + + new_log.write(line) + + os.rename('debian/changelog.new', 'debian/changelog') + +if __name__ == '__main__': + if len(sys.argv) != 3: + print('''\ +Usage: {} REPO VERSION +REPO is the git repository to generate a changelog from +VERSION is the upstream version'''.format(sys.argv[0]), + file=sys.stderr) + sys.exit(2) + main(*sys.argv[1:]) diff --git a/debian/bin/update-modinfo b/debian/bin/update-modinfo new file mode 100755 index 0000000..64a27cc --- /dev/null +++ b/debian/bin/update-modinfo @@ -0,0 +1,60 @@ +#!/usr/bin/python3 + +# Update the module information used to generate related device IDs + +import json +import os.path +import subprocess +import sys + + +def iter_modules(base_dir): + def onerror(e): + raise e + for root, dirs, files in \ + os.walk(os.path.join(base_dir, 'kernel'), onerror=onerror): + for name in files: + if name.endswith('.ko'): + yield name[:-3], os.path.join(root, name) + + +def get_module_info(filename, attr_name): + output = subprocess.check_output(['modinfo', '-F', attr_name, filename], + text=True) + if output == '': + return [] + return output.rstrip('\n').split('\n') + + +def main(kernel_id=None): + if kernel_id is None: + kernel_dir = '/lib/modules/' + os.uname().release + elif '/' not in kernel_id: + kernel_dir = '/lib/modules/' + kernel_id + else: + kernel_dir = kernel_id + + modinfo = {} + for name, filename in iter_modules(kernel_dir): + # We only care about modules that might request firmware + firmware = get_module_info(filename, 'firmware') + if not firmware: + continue + + # We only care about aliases generated from device IDs, which + # start with <type> ":" + aliases = [alias + for alias in get_module_info(filename, 'alias') + if ':' in alias] + + modinfo[name] = { + 'alias': aliases, + 'firmware': firmware, + } + + with open('debian/modinfo.json', 'w') as f: + json.dump(modinfo, f, indent=2, sort_keys=True) + + +if __name__ == '__main__': + main(*sys.argv[1:]) |