#!/usr/bin/env python3 import dataclasses import io import itertools import json import locale import os import pathlib import re import sys from typing import Iterable, Optional 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, pattern_to_re from debian_linux.dataclasses_deb822 import field_deb822, read_deb822, write_deb822 from debian_linux.debian import BinaryPackage as BinaryPackageBase, PackageDescription, PackageRelation import debian_linux.gencontrol from debian_linux.gencontrol import MakeFlags from debian_linux.utils import Templates as TemplatesBase # XXX Delete after this field is added in linux-support @dataclasses.dataclass class BinaryPackage(BinaryPackageBase): homepage: 'Optional[str]' = field_deb822( 'Homepage', default=None, ) @dataclasses.dataclass class Template: template: 'str' = field_deb822('Template') type: 'str' = field_deb822('Type') default: 'Optional[str]' = field_deb822( 'Default', default=None, ) description: PackageDescription = field_deb822( 'Description', default_factory=PackageDescription, ) class Templates(TemplatesBase): def get_control( self, key: str, context: dict[str, str] = {}, ) -> Iterable[BinaryPackage]: return read_deb822(BinaryPackage, io.StringIO(self.get(key, context))) def get_templates_control( self, key: str, context: dict[str, str] = {} ) -> Iterable[Template]: return read_deb822(Template, io.StringIO(self.get(key, context))) class GenControl(debian_linux.gencontrol.Gencontrol): def __init__(self): super().__init__(Config(), 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 do_main(self): config_entry = self.config['base',] vars = {} vars.update(config_entry) makeflags = MakeFlags() self.file_errors = False self.file_packages = {} for package in config_entry['packages']: self.do_package(package, vars.copy(), makeflags.copy()) for canon_path, package_suffixes in self.file_packages.items(): if len(package_suffixes) > 1: print(f'E: {canon_path!s} is included in multiple packages:', ', '.join(f'firmware-{suffix}' for suffix in package_suffixes), file=sys.stderr) self.file_errors = True if self.file_errors: raise Exception('error(s) found in file lists') def do_package(self, 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] = '' cur_dir = pathlib.Path.cwd() install_dir = pathlib.Path('debian/build/install') package_dir = pathlib.Path('debian/config') / package try: os.unlink('debian/firmware-%s.bug-presubj' % package) except OSError: pass os.symlink('bug-presubj', 'debian/firmware-%s.bug-presubj' % package) files_include = [(pattern, pattern_to_re(pattern)) for pattern in config_entry['files']] files_exclude = [pattern_to_re(pattern) for pattern in config_entry.get('files-excluded', [])] files_added = set() files_unused = set() files_real = {} links = {} links_rev = {} # List all additional and replacement files in binary package # config so we can: # - match dangling symlinks which pathlib.Path.glob() would ignore # - warn if any are unused for root, dir_names, file_names in os.walk(package_dir): root = pathlib.Path(root) for name in file_names: if not (root == package_dir \ and name in ['defines', 'LICENSE.install', 'update.py', 'update.sh']): canon_path = root.relative_to(package_dir) / name files_added.add(canon_path) files_unused.add(canon_path) for pattern, pattern_re in files_include: matched = False matched_more = False for paths, is_added in [ (((canon_path, package_dir / canon_path) for canon_path in files_added if pattern_re.fullmatch(str(canon_path))), True), (((cur_path.relative_to(install_dir), cur_path) for cur_path in install_dir.glob(pattern)), False) ]: for canon_path, cur_path in paths: canon_name = str(canon_path) if any(exc_pattern_re.fullmatch(canon_name) for exc_pattern_re in files_exclude): continue matched = True # Skip if already matched by earlier pattern or in # other directory if canon_path in files_real or canon_path in links: continue matched_more = True if is_added: files_unused.remove(canon_path) if cur_path.is_symlink(): links[canon_path] = cur_path.readlink() elif cur_path.is_file(): files_real[canon_path] = cur_path self.file_packages.setdefault(canon_path, []) \ .append(package) # Non-matching pattern is an error if not matched: print(f'E: {package}: {pattern} did not match anything', file=sys.stderr) self.file_errors = True # Redundant pattern deserves a warning elif not matched_more: print(f'W: {package}: pattern {pattern} is redundant with earlier patterns', file=sys.stderr) for canon_path in links: link_target = ((canon_path.parent / links[canon_path]) .resolve(strict=False) .relative_to(cur_dir)) links_rev.setdefault(link_target, []).append(canon_path) if files_unused: print(f'W: {package}: unused files:', ', '.join(str(path) for path in files_unused), file=sys.stderr) makeflags['FILES'] = \ ' '.join([f'"{source}":"{dest}"' for dest, source in sorted(files_real.items())]) \ .replace(',', '[comma]') makeflags['LINKS'] = \ ' '.join([f'"{link}":"{target}"' for link, target in sorted(links.items())]) \ .replace(',', '[comma]') firmware_meta_temp = self.templates.get("metainfo.xml.firmware") firmware_meta_list = [] module_names = set() for canon_path in sorted(itertools.chain(files_real, links)): canon_name = str(canon_path) firmware_meta_list.append(self.substitute(firmware_meta_temp, {'filename': canon_name})) for module_name in self.firmware_modules.get(canon_name, []): module_names.add(module_name) 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 = list(self.templates.get_control("binary.control", vars)) scripts = {} if 'initramfs-tools' in config_entry.get('support', []): postinst = self.templates.get('postinst.initramfs-tools') scripts.setdefault("postinst", []).append(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') scripts.setdefault("preinst", []).append(self.substitute(preinst, vars)) templates = list(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 write_deb822(templates, open(templates_filename, 'w')) 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') if config_entry.get('usrmovemitigation', []): vars['files'] = ' '.join(config_entry['usrmovemitigation']) for script in ("preinst", "postinst"): script_template = self.templates.get(script + '.usrmovemitigation') script_content = self.substitute(script_template, vars) scripts.setdefault(script, []).append(script_content) del vars['files'] for script, script_contents in scripts.items(): script_contents.insert(0, "#!/bin/sh\n\nset -e\n") script_contents.append("#DEBHELPER#\n\nexit 0\n") open("debian/firmware-%s.%s" % (package, script), "w").write("\n".join(script_contents)) self.bundle.add_packages(packages_binary, (package,), 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)) # XXX Delete after updating to linux-support-6.11 def do_extra(self) -> None: try: packages_extra = self.templates.get_control("extra.control", self.vars) except KeyError: return for package in packages_extra: package.meta_rules_target = 'meta' if not package.architecture: raise RuntimeError('Require Architecture in debian/templates/extra.control') for arch in package.architecture: self.bundle.add_packages([package], (arch, ), MakeFlags(), arch=arch, check_packages=False) 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)) if __name__ == '__main__': GenControl()()