debian-linux/debian/lib/python/debian_linux/gencontrol.py
Bastian Blank 3d9299c3ea Merge branch 'waldi/news-template' into 'master'
Generate NEWS file from templates

See merge request kernel-team/linux!889
2023-11-05 12:57:12 +00:00

604 lines
20 KiB
Python

from __future__ import annotations
import contextlib
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 (
'NEWS',
'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.open(f'{package_name}.{name}') as f:
f.write(template)
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:
return self.base / f'generated.{self.name}/{name}'
return self.base / name
@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]),
)
@contextlib.contextmanager
def open(self, name: str, mode: str = 'w') -> typing.TextIO:
path = self.path(name)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open(mode, encoding='utf-8') as f:
yield f
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 + (name, ))
self.makefile.add_rules(f'setup_{rule}',
f'setup_{name}', makeflags, packages, packages_extra)
self.makefile.add_rules(f'build-{ttype}_{rule}',
f'build_{name}', makeflags, packages, packages_extra)
self.makefile.add_rules(f'binary-{ttype}_{rule}',
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.open('control') as f:
self.write_rfc822(f, self.packages.values())
def write_makefile(self) -> None:
with self.open('rules.gen') 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, ),
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.extract_makefile()
bundle.merge_build_depends()
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