summaryrefslogtreecommitdiffstats
path: root/debian/lib/python/debian_linux/gencontrol.py
diff options
context:
space:
mode:
Diffstat (limited to 'debian/lib/python/debian_linux/gencontrol.py')
-rw-r--r--debian/lib/python/debian_linux/gencontrol.py607
1 files changed, 607 insertions, 0 deletions
diff --git a/debian/lib/python/debian_linux/gencontrol.py b/debian/lib/python/debian_linux/gencontrol.py
new file mode 100644
index 000000000..0b3ff5bde
--- /dev/null
+++ b/debian/lib/python/debian_linux/gencontrol.py
@@ -0,0 +1,607 @@
+from __future__ import annotations
+
+import os
+import pathlib
+import re
+import typing
+from collections import OrderedDict
+
+from .debian import Changelog, PackageArchitecture, \
+ PackageBuildRestrictFormula, PackageBuildRestrictList, \
+ PackageBuildRestrictTerm, PackageRelation, Version
+from .utils import Templates
+
+
+class PackagesList(OrderedDict):
+ def append(self, package):
+ self[package['Package']] = package
+
+ def extend(self, packages):
+ for package in packages:
+ self[package['Package']] = package
+
+ def setdefault(self, package):
+ return super().setdefault(package['Package'], package)
+
+
+class Makefile:
+ def __init__(self):
+ self.rules = {}
+
+ def add_cmds(self, name, cmds):
+ rule = self.rules.setdefault(name, MakefileRule(name))
+ rule.add_cmds(MakefileRuleCmdsSimple(cmds))
+
+ def add_deps(self, name, deps):
+ rule = self.rules.setdefault(name, MakefileRule(name))
+ rule.add_deps(deps)
+
+ for i in deps:
+ self.rules.setdefault(i, MakefileRule(i))
+
+ def add_rules(self, name, target, makeflags, packages=set(), packages_extra=set()):
+ rule = self.rules.setdefault(name, MakefileRule(name))
+ rule.add_cmds(MakefileRuleCmdsRules(target, makeflags, packages, packages_extra))
+
+ def write(self, out):
+ out.write('''\
+.NOTPARALLEL:
+.PHONY:
+packages_enabled := $(shell dh_listpackages)
+define if_package
+$(if $(filter $(1),$(packages_enabled)),$(2))
+endef
+''')
+ for k, rule in sorted(self.rules.items()):
+ rule.write(out)
+
+
+class MakefileRule:
+ def __init__(self, name):
+ self.name = name
+ self.cmds = []
+ self.deps = set()
+
+ def add_cmds(self, cmds):
+ self.cmds.append(cmds)
+
+ def add_deps(self, deps):
+ assert type(deps) is list
+ self.deps.update(deps)
+
+ def write(self, out):
+ if self.cmds:
+ out.write(f'{self.name}:{" ".join(sorted(self.deps))}\n')
+ for c in self.cmds:
+ c.write(out)
+ else:
+ out.write(f'{self.name}:{" ".join(sorted(self.deps))}\n')
+
+
+class MakefileRuleCmdsRules:
+ def __init__(self, target, makeflags, packages, packages_extra):
+ self.target = target
+ self.makeflags = makeflags.copy()
+ self.packages = packages
+ self.packages_extra = packages_extra
+
+ packages_all = packages | packages_extra
+
+ if packages_all:
+ if len(packages_all) == 1:
+ package_name = list(packages_all)[0]
+ self.makeflags['PACKAGE_NAME'] = package_name
+ self.makeflags['DESTDIR'] = f'$(CURDIR)/debian/{package_name}'
+ else:
+ self.makeflags['DESTDIR'] = '$(CURDIR)/debian/tmp'
+
+ self.makeflags['DH_OPTIONS'] = ' '.join(f'-p{i}' for i in sorted(packages_all))
+
+ def write(self, out):
+ cmd = f'$(MAKE) -f debian/rules.real {self.target} {self.makeflags}'
+ if self.packages:
+ out.write(f'\t$(call if_package, {" ".join(sorted(self.packages))}, {cmd})\n')
+ else:
+ out.write(f'\t{cmd}\n')
+
+
+class MakefileRuleCmdsSimple:
+ def __init__(self, cmds):
+ self.cmds = cmds
+
+ def write(self, out):
+ for i in self.cmds:
+ out.write(f'\t{i}\n')
+
+
+class MakeFlags(dict):
+ def __str__(self):
+ return ' '.join("%s='%s'" % i for i in sorted(self.items()))
+
+ def copy(self):
+ return self.__class__(super(MakeFlags, self).copy())
+
+
+class PackagesBundle:
+ name: typing.Optional[str]
+ templates: Templates
+ base: pathlib.Path
+ makefile: Makefile
+ packages: PackagesList
+
+ def __init__(
+ self,
+ name: typing.Optional[str],
+ templates: Templates,
+ base: pathlib.Path = pathlib.Path('debian'),
+ ) -> None:
+ self.name = name
+ self.templates = templates
+ self.base = base
+ self.makefile = Makefile()
+ self.packages = PackagesList()
+
+ def add(
+ self,
+ pkgid: str,
+ ruleid: tuple[str],
+ makeflags: MakeFlags,
+ replace: dict[str, str],
+ *,
+ arch: str = None,
+ check_packages: bool = True,
+ ) -> list[typing.Any]:
+ ret = []
+ for raw_package in self.templates.get_control(f'{pkgid}.control', replace):
+ package = self.packages.setdefault(raw_package)
+ package_name = package['Package']
+ ret.append(package)
+
+ package.meta.setdefault('rules-ruleids', {})[ruleid] = makeflags
+ if arch:
+ package.meta.setdefault('architectures', PackageArchitecture()).add(arch)
+ package.meta['rules-check-packages'] = check_packages
+
+ for name in (
+ 'bug-presubj',
+ 'lintian-overrides',
+ 'maintscript',
+ 'postinst',
+ 'postrm',
+ 'preinst',
+ 'prerm',
+ ):
+ try:
+ template = self.templates.get(f'{pkgid}.{name}',
+ replace | {'package': package_name})
+ except KeyError:
+ pass
+ else:
+ with self.path(f'{package_name}.{name}').open('w') as f:
+ f.write(template)
+ os.chmod(f.fileno(),
+ self.templates.get_mode(f'{pkgid}.{name}') & 0o777)
+
+ return ret
+
+ def add_packages(
+ self,
+ packages: PackagesList,
+ ruleid: tuple[str],
+ makeflags: MakeFlags,
+ *,
+ arch: str = None,
+ check_packages: bool = True,
+ ) -> None:
+ for package in packages:
+ package = self.packages.setdefault(package)
+ package.meta.setdefault('rules-ruleids', {})[ruleid] = makeflags
+ if arch:
+ package.meta.setdefault('architectures', PackageArchitecture()).add(arch)
+ package.meta['rules-check-packages'] = check_packages
+
+ def path(self, name) -> pathlib.Path:
+ if self.name:
+ raise RuntimeError
+ return self.base / f'debian/generated.{self.name}/{name}'
+ return self.base / name
+
+ @property
+ def path_control(self) -> pathlib.Path:
+ return self.path('control')
+
+ @property
+ def path_makefile(self) -> pathlib.Path:
+ return self.path('rules.gen')
+
+ @staticmethod
+ def __ruleid_deps(ruleid: tuple[str], name: str) -> typing.Iterator[tuple[str, str]]:
+ """
+ Generate all the rules dependencies.
+ ```
+ build: build_a
+ build_a: build_a_b
+ build_a_b: build_a_b_image
+ ```
+ """
+ r = ruleid + (name, )
+ yield (
+ '',
+ '_' + '_'.join(r[:1]),
+ )
+ for i in range(1, len(r)):
+ yield (
+ '_' + '_'.join(r[:i]),
+ '_' + '_'.join(r[:i + 1]),
+ )
+
+ def extract_makefile(self) -> None:
+ targets = {}
+
+ for package_name, package in self.packages.items():
+ target_name = package.meta.get('rules-target')
+ ruleids = package.meta.get('rules-ruleids')
+ makeflags = MakeFlags({
+ # Requires Python 3.9+
+ k.removeprefix('rules-makeflags-').upper(): v
+ for (k, v) in package.meta.items() if k.startswith('rules-makeflags-')
+ })
+
+ if ruleids:
+ arches = package.meta.get('architectures')
+ if arches:
+ package['Architecture'] = arches
+ else:
+ arches = package.get('Architecture')
+
+ if target_name:
+ for ruleid, makeflags_package in ruleids.items():
+ target_key = frozenset(
+ [target_name, ruleid]
+ + [f'{k}_{v}' for (k, v) in makeflags.items()]
+ )
+ target = targets.setdefault(
+ target_key,
+ {
+ 'name': target_name,
+ 'ruleid': ruleid,
+ },
+ )
+
+ if package.meta['rules-check-packages']:
+ target.setdefault('packages', set()).add(package_name)
+ else:
+ target.setdefault('packages_extra', set()).add(package_name)
+ makeflags_package = makeflags_package.copy()
+ makeflags_package.update(makeflags)
+ target['makeflags'] = makeflags_package
+
+ if arches == set(['all']):
+ target['type'] = 'indep'
+ else:
+ target['type'] = 'arch'
+
+ for target in targets.values():
+ name = target['name']
+ ruleid = target['ruleid']
+ packages = target.get('packages', set())
+ packages_extra = target.get('packages_extra', set())
+ makeflags = target['makeflags']
+ ttype = target['type']
+
+ rule = '_'.join(ruleid)
+ self.makefile.add_rules(f'setup_{rule}_{name}',
+ f'setup_{name}', makeflags, packages, packages_extra)
+ self.makefile.add_rules(f'build-{ttype}_{rule}_{name}',
+ f'build_{name}', makeflags, packages, packages_extra)
+ self.makefile.add_rules(f'binary-{ttype}_{rule}_{name}',
+ f'binary_{name}', makeflags, packages, packages_extra)
+
+ for i, j in self.__ruleid_deps(ruleid, name):
+ self.makefile.add_deps(f'setup{i}',
+ [f'setup{j}'])
+ self.makefile.add_deps(f'build-{ttype}{i}',
+ [f'build-{ttype}{j}'])
+ self.makefile.add_deps(f'binary-{ttype}{i}',
+ [f'binary-{ttype}{j}'])
+
+ def merge_build_depends(self):
+ # Merge Build-Depends pseudo-fields from binary packages into the
+ # source package
+ source = self.packages["source"]
+ arch_all = PackageArchitecture("all")
+ for name, package in self.packages.items():
+ if name == "source":
+ continue
+ dep = package.get("Build-Depends")
+ if not dep:
+ continue
+ del package["Build-Depends"]
+ for group in dep:
+ for item in group:
+ if package["Architecture"] != arch_all and not item.arches:
+ item.arches = sorted(package["Architecture"])
+ if package.get("Build-Profiles") and not item.restrictions:
+ item.restrictions = package["Build-Profiles"]
+ if package["Architecture"] == arch_all:
+ dep_type = "Build-Depends-Indep"
+ else:
+ dep_type = "Build-Depends-Arch"
+ if dep_type not in source:
+ source[dep_type] = PackageRelation()
+ source[dep_type].extend(dep)
+
+ def write(self) -> None:
+ self.write_control()
+ self.write_makefile()
+
+ def write_control(self) -> None:
+ with self.path_control.open('w', encoding='utf-8') as f:
+ self.write_rfc822(f, self.packages.values())
+
+ def write_makefile(self) -> None:
+ with self.path_makefile.open('w', encoding='utf-8') as f:
+ self.makefile.write(f)
+
+ def write_rfc822(self, f: typing.TextIO, entries: typing.Iterable) -> None:
+ for entry in entries:
+ for key, value in entry.items():
+ if value:
+ f.write(u"%s: %s\n" % (key, value))
+ f.write('\n')
+
+
+def iter_featuresets(config):
+ for featureset in config['base', ]['featuresets']:
+ if config.merge('base', None, featureset).get('enabled', True):
+ yield featureset
+
+
+def iter_arches(config):
+ return iter(config['base', ]['arches'])
+
+
+def iter_arch_featuresets(config, arch):
+ for featureset in config['base', arch].get('featuresets', []):
+ if config.merge('base', arch, featureset).get('enabled', True):
+ yield featureset
+
+
+def iter_flavours(config, arch, featureset):
+ return iter(config['base', arch, featureset]['flavours'])
+
+
+class Gencontrol(object):
+ def __init__(self, config, templates, version=Version):
+ self.config, self.templates = config, templates
+ self.changelog = Changelog(version=version)
+ self.vars = {}
+ self.bundles = {None: PackagesBundle(None, templates)}
+ # TODO: Remove after all references are gone
+ self.packages = self.bundle.packages
+ self.makefile = self.bundle.makefile
+
+ @property
+ def bundle(self) -> PackagesBundle:
+ return self.bundles[None]
+
+ def __call__(self):
+ self.do_source()
+ self.do_main()
+ self.do_extra()
+
+ self.write()
+
+ def do_source(self):
+ source = self.templates.get_source_control("source.control", self.vars)[0]
+ if not source.get('Source'):
+ source['Source'] = self.changelog[0].source
+ self.packages['source'] = source
+
+ def do_main(self):
+ vars = self.vars.copy()
+
+ makeflags = MakeFlags()
+ extra = {}
+
+ self.do_main_setup(vars, makeflags, extra)
+ self.do_main_makefile(makeflags, extra)
+ self.do_main_packages(vars, makeflags, extra)
+ self.do_main_recurse(vars, makeflags, extra)
+
+ def do_main_setup(self, vars, makeflags, extra):
+ pass
+
+ def do_main_makefile(self, makeflags, extra):
+ pass
+
+ def do_main_packages(self, vars, makeflags, extra):
+ pass
+
+ def do_main_recurse(self, vars, makeflags, extra):
+ for featureset in iter_featuresets(self.config):
+ self.do_indep_featureset(featureset,
+ vars.copy(), makeflags.copy(), extra)
+ for arch in iter_arches(self.config):
+ self.do_arch(arch, vars.copy(),
+ makeflags.copy(), extra)
+
+ def do_extra(self):
+ try:
+ packages_extra = self.templates.get_control("extra.control", self.vars)
+ except KeyError:
+ return
+
+ extra_arches = {}
+ for package in packages_extra:
+ arches = package['Architecture']
+ for arch in arches:
+ i = extra_arches.get(arch, [])
+ i.append(package)
+ extra_arches[arch] = i
+ for arch in sorted(extra_arches.keys()):
+ self.bundle.add_packages(packages_extra, (arch, 'real'),
+ MakeFlags(), check_packages=False)
+
+ def do_indep_featureset(self, featureset, vars,
+ makeflags, extra):
+ vars['localversion'] = ''
+ if featureset != 'none':
+ vars['localversion'] = '-' + featureset
+
+ self.do_indep_featureset_setup(vars, makeflags, featureset, extra)
+ self.do_indep_featureset_makefile(featureset, makeflags,
+ extra)
+ self.do_indep_featureset_packages(featureset,
+ vars, makeflags, extra)
+
+ def do_indep_featureset_setup(self, vars, makeflags, featureset, extra):
+ pass
+
+ def do_indep_featureset_makefile(self, featureset, makeflags,
+ extra):
+ makeflags['FEATURESET'] = featureset
+
+ def do_indep_featureset_packages(self, featureset, vars, makeflags, extra):
+ pass
+
+ def do_arch(self, arch, vars, makeflags, extra):
+ vars['arch'] = arch
+
+ self.do_arch_setup(vars, makeflags, arch, extra)
+ self.do_arch_makefile(arch, makeflags, extra)
+ self.do_arch_packages(arch, vars, makeflags, extra)
+ self.do_arch_recurse(arch, vars, makeflags, extra)
+
+ def do_arch_setup(self, vars, makeflags, arch, extra):
+ pass
+
+ def do_arch_makefile(self, arch, makeflags, extra):
+ makeflags['ARCH'] = arch
+
+ def do_arch_packages(self, arch, vars, makeflags,
+ extra):
+ pass
+
+ def do_arch_recurse(self, arch, vars, makeflags,
+ extra):
+ for featureset in iter_arch_featuresets(self.config, arch):
+ self.do_featureset(arch, featureset,
+ vars.copy(), makeflags.copy(), extra)
+
+ def do_featureset(self, arch, featureset, vars,
+ makeflags, extra):
+ vars['localversion'] = ''
+ if featureset != 'none':
+ vars['localversion'] = '-' + featureset
+
+ self.do_featureset_setup(vars, makeflags, arch, featureset, extra)
+ self.do_featureset_makefile(arch, featureset, makeflags, extra)
+ self.do_featureset_packages(arch, featureset, vars, makeflags, extra)
+ self.do_featureset_recurse(arch, featureset, vars, makeflags, extra)
+
+ def do_featureset_setup(self, vars, makeflags, arch, featureset, extra):
+ pass
+
+ def do_featureset_makefile(self, arch, featureset, makeflags,
+ extra):
+ makeflags['FEATURESET'] = featureset
+
+ def do_featureset_packages(self, arch, featureset, vars, makeflags, extra):
+ pass
+
+ def do_featureset_recurse(self, arch, featureset, vars, makeflags, extra):
+ for flavour in iter_flavours(self.config, arch, featureset):
+ self.do_flavour(arch, featureset, flavour,
+ vars.copy(), makeflags.copy(), extra)
+
+ def do_flavour(self, arch, featureset, flavour, vars,
+ makeflags, extra):
+ vars['localversion'] += '-' + flavour
+
+ self.do_flavour_setup(vars, makeflags, arch, featureset, flavour,
+ extra)
+ self.do_flavour_makefile(arch, featureset, flavour, makeflags, extra)
+ self.do_flavour_packages(arch, featureset, flavour,
+ vars, makeflags, extra)
+
+ def do_flavour_setup(self, vars, makeflags, arch, featureset, flavour,
+ extra):
+ for i in (
+ ('kernel-arch', 'KERNEL_ARCH'),
+ ('localversion', 'LOCALVERSION'),
+ ):
+ if i[0] in vars:
+ makeflags[i[1]] = vars[i[0]]
+
+ def do_flavour_makefile(self, arch, featureset, flavour,
+ makeflags, extra):
+ makeflags['FLAVOUR'] = flavour
+
+ def do_flavour_packages(self, arch, featureset,
+ flavour, vars, makeflags, extra):
+ pass
+
+ def substitute(self, s, vars):
+ if isinstance(s, (list, tuple)):
+ return [self.substitute(i, vars) for i in s]
+
+ def subst(match):
+ return vars[match.group(1)]
+
+ return re.sub(r'@([-_a-z0-9]+)@', subst, str(s))
+
+ def write(self):
+ for bundle in self.bundles.values():
+ bundle.merge_build_depends()
+ bundle.extract_makefile()
+ bundle.write()
+
+ # TODO: Remove
+ def write_control(self, name='debian/control'):
+ self.write_rfc822(open(name, 'w', encoding='utf-8'), self.packages.values())
+
+ # TODO: Remove
+ def write_makefile(self, name='debian/rules.gen'):
+ f = open(name, 'w')
+ self.makefile.write(f)
+ f.close()
+
+ # TODO: Remove
+ def write_rfc822(self, f, list):
+ for entry in list:
+ for key, value in entry.items():
+ f.write(u"%s: %s\n" % (key, value))
+ f.write('\n')
+
+
+def merge_packages(packages, new, arch):
+ for new_package in new:
+ name = new_package['Package']
+ if name in packages:
+ package = packages.get(name)
+ package['Architecture'].add(arch)
+
+ for field in ('Depends', 'Provides', 'Suggests', 'Recommends',
+ 'Conflicts'):
+ if field in new_package:
+ if field in package:
+ v = package[field]
+ v.extend(new_package[field])
+ else:
+ package[field] = new_package[field]
+
+ else:
+ new_package['Architecture'] = arch
+ packages.append(new_package)
+
+
+def add_package_build_restriction(package, term):
+ if not isinstance(term, PackageBuildRestrictTerm):
+ term = PackageBuildRestrictTerm(term)
+ old_form = package['Build-Profiles']
+ new_form = PackageBuildRestrictFormula()
+ for old_list in old_form:
+ new_list = PackageBuildRestrictList(list(old_list) + [term])
+ new_form.add(new_list)
+ package['Build-Profiles'] = new_form