summaryrefslogtreecommitdiffstats
path: root/debian/bin/gencontrol.py
blob: 41a9d4506914173648e7a2c4233a6032cfa237b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#!/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()()