Your message dated Sun, 28 Dec 2025 16:19:57 +0000
with message-id <[email protected]>
and subject line Bug#1121339: fixed in debputy 0.1.79
has caused the Debian Bug report #1121339,
regarding dh-debputy: manifest rules generating (Static-)Built-Using fields
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1121339: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1121339
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: dh-debputy
Version: 0.1.78
Severity: wishlist
Tags: patch

Hello.

The attachment lets debputy generate the (Static-)Built-Using fields
from manifest rules, replacing the dh-builtusing debhelper plugin.
It seems ready for a first review, if you are interested.

'bug1120283.py' should only exist until the patch with the same
contents is applied to the python3-debian package.

I have made the source_package and dpkg_arch_query_table
HighLevelManifest attributes public.  Their value is accessible via
various tricks like manifest.condition_context(pkg).source_package
anyway.

'test_built_using.py' demonstrates various possible use cases. Advices
about the way to merge it into the test suite would be welcome.
>From 3939d179b55a695df2f4d927d7b3344e02387b94 Mon Sep 17 00:00:00 2001
From: Nicolas Boulenguez <[email protected]>
Date: Mon, 24 Nov 2025 11:35:59 +0100
Subject: built-using: initial suggestion


diff --git a/MANIFEST-FORMAT.md b/MANIFEST-FORMAT.md
index d7f18d0e..a697b866 100644
--- a/MANIFEST-FORMAT.md
+++ b/MANIFEST-FORMAT.md
@@ -2523,6 +2523,64 @@ exists and is a directory, it will also be checked for 
"not-installed" paths.
 
 Integration mode availability: dh-sequence-zz-debputy, full
 
+## Built-Using dependency relations (`built-using`)
+
+Generate a `Built-Using` dependency relation on the
+build dependency selected by the `sources-for`, which
+may contain a `*` wildcard matching any number of
+arbitrary characters.
+
+packages:
+  PKG:
+    built-using:
+    - sources-for: foo-*-source   # foo-3.1.0-source
+    - sources-for: foo
+      when:                       # foo is always installed
+        arch-matches: amd64       # but only used on amd64
+    static-built-using:
+    - sources-for: librust-*-dev  # several relations
+
+Either of these conditions prevents the generation:
+* PKG is not part of the current build because of its
+  `Architecture` or `Build-Profiles` fields.
+* The match in `Build-Depends` carries an invalid
+  architecture or build profile restriction.
+* The match in `Build-Depends` is not installed.
+  This should only happen inside alternatives, see below.
+* The manifest item carries an invalid `when:` condition.
+  This may be useful when the match must be installed
+  for unrelated reasons.
+
+Matches are searched in the `Build-Depends` field of
+the source package, and either `Build-Depends-Indep`
+or `Build-Depends-Arch` depending on PKG.
+
+In alternatives like `a|b`, each option may match
+separately.  This is a compromise between
+reproducibility on automatic builders (where the set
+of installed package is constant), and least surprise
+during local builds (where `b` may be installed
+alone).  There seems to be no one-size fits all
+solution when both are installed.
+
+Architecture qualifiers and version restrictions in
+`Build-Depends` are ignored. The only allowed
+co-installations require a common source and version.
+
+List where each element has the following attributes:
+
+Integration mode availability: any integration mode
+
+## Static-Built-Using dependency relations (`static-built-using`)
+
+This rule behaves like `built-using`, except that the
+affected field in the eventual binary package is
+`Static-Built-Using`.
+
+List where each element has the following attributes:
+
+Integration mode availability: any integration mode
+
 
 # Remove paths during clean (`remove-during-clean`)
 
diff --git a/docs/MANIFEST-FORMAT.md.j2 b/docs/MANIFEST-FORMAT.md.j2
index bf2b0d3e..f8017004 100644
--- a/docs/MANIFEST-FORMAT.md.j2
+++ b/docs/MANIFEST-FORMAT.md.j2
@@ -512,6 +512,8 @@ To show concrete examples:
 <<render_pmr('packages.{{PACKAGE}}::binary-version', 2)>>
 <<render_pmr('packages.{{PACKAGE}}::clean-after-removal', 2)>>
 <<render_pmr('packages.{{PACKAGE}}::installation-search-dirs', 2)>>
+<<render_pmr('packages.{{PACKAGE}}::built-using', 2)>>
+<<render_pmr('packages.{{PACKAGE}}::static-built-using', 2)>>
 
 <<render_pmr('::remove-during-clean', 1)>>
 
diff --git a/src/debputy/bug1120283.py b/src/debputy/bug1120283.py
new file mode 100644
index 00000000..ba7c5bce
--- /dev/null
+++ b/src/debputy/bug1120283.py
@@ -0,0 +1,54 @@
+"""Version 2 of patch in bug #1120283 for python-debian.
+
+A new debian.deb822 will hopefully one day replace this file.
+"""
+import collections.abc
+
+from debian.debian_support import DpkgArchTable
+from debian.deb822 import PkgRelation
+
+
+def holds_on_arch(
+    relation: "PkgRelation.ParsedRelation",
+    arch: str,
+    table: DpkgArchTable,
+) -> bool:
+    """Is relation active on the given architecture?
+
+    >>> table = DpkgArchTable.load_arch_table()
+    >>> rel = PkgRelation.parse_relations("foo [armel linux-any]")[0][0]
+    >>> holds_on_arch(rel, "amd64", table)
+    True
+    >>> holds_on_arch(rel, "hurd-i386", table)
+    False
+    """
+    archs = relation["arch"]
+    return (archs is None
+            or table.architecture_is_concerned(
+                arch,
+                tuple(("" if a.enabled else "!") + a.arch for a in archs)))
+
+
+def holds_with_profiles(
+    relation: "PkgRelation.ParsedRelation",
+    profiles: collections.abc.Container[str],
+) -> bool:
+    """Is relation active under the given profiles?
+
+    >>> relation = PkgRelation.parse_relations("foo <a !b> <c>")[0][0]
+    >>> holds_with_profiles(relation, ("a", "b"))
+    False
+    >>> holds_with_profiles(relation, ("c", ))
+    True
+    """
+    restrictions = relation["restrictions"]
+    return (restrictions is None
+            or any(all(term.enabled == (term.profile in profiles)
+                       for term in restriction_list)
+                   for restriction_list in restrictions))
+
+
+# (cd src && PYTHONPATH=. python3 -m debputy.bug1120283 -v)
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
diff --git a/src/debputy/built_using.py b/src/debputy/built_using.py
new file mode 100644
index 00000000..b9c8261c
--- /dev/null
+++ b/src/debputy/built_using.py
@@ -0,0 +1,172 @@
+"""Generate `Built-Using: foo-src (= 1.2)` in the control file for `pkg`
+from a `packages.pkg.built-using.source-for: foo` stanza in the manifest.
+"""
+import collections.abc
+import re
+import subprocess
+import typing
+
+import debian.deb822
+
+import debputy.bug1120283
+import debputy.highlevel_manifest
+import debputy.packages
+import debputy.plugin.api.impl_types
+import debputy.plugins.debputy.binary_package_rules
+import debputy.util
+
+_VALID_GLOB = re.compile("[a-z*][a-z0-9.+*-]*")
+_GLOB_TO_RE = str.maketrans({".": "[.]", "+": "[+]", "*": ".*"})
+_Field = typing.Literal["Built-Using", "Static-Built-Using"]
+
+
+def _d(pkg: debputy.packages.BinaryPackage, field: _Field, msg: str) -> None:
+    # pylint: disable=protected-access
+    debputy.util._debug_log(f"packages.{pkg.name}.{field.lower()}: {msg}")
+
+
+def _e(pkg: debputy.packages.BinaryPackage, field: _Field, msg: str
+       ) -> typing.NoReturn:
+    # pylint: disable=protected-access
+    debputy.util._error(f"packages.{pkg.name}.{field.lower()}: {msg}")
+
+
+def _w(pkg: debputy.packages.BinaryPackage, field: _Field, msg: str) -> None:
+    # pylint: disable=protected-access
+    debputy.util._warn(f"packages.{pkg.name}.{field.lower()}: {msg}")
+
+
+def _sources_for(
+    deps: typing.Iterable[str],
+) -> collections.abc.Mapping[str, str]:
+    """Map installed packages among deps to a "source (= version)"
+    relation, excluding unknown or not installed packages.
+
+    >>> r = _sources_for(("dpkg", "dpkg", "dpkg-dev", "gcc", "dummy"))
+    >>> r["dpkg"]  # doctest: +ELLIPSIS
+    'dpkg (= ...)'
+    >>> r["dpkg-dev"]  # doctest: +ELLIPSIS
+    'dpkg (= ...)'
+    >>> r["gcc"]  # doctest: +ELLIPSIS
+    'gcc-defaults (= ...)'
+    >>> "dummy" in r
+    False
+    """
+    cp = subprocess.run(
+        args=(
+            "dpkg-query", "-Wf${db:Status-Abbrev}${Package}:"
+            "${source:Package} (= ${source:Version})\n", *deps,
+        ),
+        capture_output=True,
+        check=False,
+        text=True,
+    )
+    # 0: OK    1: unknown package    2: other
+    assert cp.returncode in (0, 1)
+    # For the example above, stdout is:
+    # "ii dpkg:dpkg (= 1.22.21)\n"
+    # "ii dpkg-dev:dpkg (= 1.22.21)\n"
+    # "ii gcc:gcc-defaults (= 1.220)\n"
+    #   ^: the package is (i)nstalled
+    # The regular expression is used once in the program lifetime,
+    # so precompiling it has no benefit.
+    return dict(m.groups() for m in re.finditer(".i.([^:].+):(.+)", cp.stdout))
+
+
+class _Todo(typing.NamedTuple):
+    """For efficiency, a first pass constructs a todo list, then at
+    most one dpkg-query subprocess is spawn."""
+    pkg: debputy.packages.BinaryPackage
+    field: _Field
+    dep: str
+    first_option: bool  # This relation was the first in its alternative.
+
+
+def _enabled_match(
+    manifest: debputy.highlevel_manifest.HighLevelManifest,
+    pkg: debputy.packages.BinaryPackage,
+    field: _Field,
+    bu: debputy.plugins.debputy.binary_package_rules.BuiltUsingParsedFormat,
+    relation: "debian.deb822.PkgRelation.ParsedRelation",
+) -> bool:
+    # A logical conjunction of restrictions, with logging.
+    name = relation["name"]
+    host = manifest.dpkg_architecture_variables.current_host_arch
+    table = manifest.dpkg_arch_query_table
+    if not debputy.bug1120283.holds_on_arch(relation, host, table):
+        _d(pkg, field, f"{name} is disabled by host architecture {host}.")
+        return False
+    profiles = manifest.deb_options_and_profiles.deb_build_profiles
+    if not debputy.bug1120283.holds_with_profiles(relation, profiles):
+        _d(pkg, field, f"{name} is disabled by profiles {' '.join(profiles)}.")
+        return False
+    if "when" in bu \
+       and not bu["when"].evaluate(manifest.condition_context(pkg)):
+        _d(pkg, field, f"{name} is disabled by its manifest condition.")
+        return False
+    return True
+
+
+def _pattern(
+    manifest: debputy.highlevel_manifest.HighLevelManifest,
+    pkg: debputy.packages.BinaryPackage,
+    bu: debputy.plugins.debputy.binary_package_rules.BuiltUsingParsedFormat,
+    field: _Field,
+) -> typing.Iterator[_Todo]:
+    # Process an item in a (static-)built-using list.
+    glob = bu["sources_for"]
+    if _VALID_GLOB.fullmatch(glob) is None:
+        _e(pkg, field, f"invalid characters in '{glob}'.")
+    regex = re.compile(glob.translate(_GLOB_TO_RE))
+    if pkg.is_arch_all:
+        other = "Build-Depends-Indep"
+    else:
+        other = "Build-Depends-Arch"
+    at_least_a_match = False
+    # pylint: disable=too-many-nested-blocks
+    for bd_field in ("Build-Depends", other):
+        raw = manifest.source_package.fields.get(bd_field)
+        if raw is not None:
+            for options in debian.deb822.PkgRelation.parse_relations(raw):
+                for idx, relation in enumerate(options):
+                    name = relation["name"]
+                    if regex.fullmatch(name):
+                        at_least_a_match = True
+                        if _enabled_match(manifest, pkg, field, bu, relation):
+                            yield _Todo(pkg, field, name, not idx)
+    if not at_least_a_match:
+        _w(pkg, field, f"{glob} matches in neither Build-Depends nor {other}.")
+
+
+def add_relations(
+    manifest: debputy.highlevel_manifest.HighLevelManifest,
+    package_data_table: debputy.plugin.api.impl_types.PackageDataTable,
+) -> None:
+    """The documentation is in
+    plugins.debputy.binary_package_rules.register_binary_package_rules."""
+    todo: list[_Todo] = []
+    for pkg in manifest.active_packages:
+        for bu in manifest.package_state_for(pkg.name).built_using:
+            todo.extend(_pattern(manifest, pkg, bu, "Built-Using"))
+        for bu in manifest.package_state_for(pkg.name).static_built_using:
+            todo.extend(_pattern(manifest, pkg, bu, "Static-Built-Using"))
+    if todo:
+        # Run the costly dpkg-query subprocess.
+        relations = _sources_for(t.dep for t in todo)
+        for t in todo:
+            if t.dep in relations:
+                package_data_table[t.pkg.name].substvars.add_dependency(
+                    f"debputy:{t.field}", relations[t.dep],
+                )
+            # With Build-Depends: a | b, in usual configurations,
+            # a is installed but b might not be.
+            elif t.first_option:
+                _w(t.pkg, t.field, f"{t.dep} is not installed")
+            else:
+                _d(t.pkg, t.field, f"{t.dep} is not installed")
+
+
+# (cd src && PYTHONPATH=. python3 -m debputy.built_using -v)
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
diff --git a/src/debputy/commands/debputy_cmd/__main__.py 
b/src/debputy/commands/debputy_cmd/__main__.py
index c67a38ff..a0899b52 100644
--- a/src/debputy/commands/debputy_cmd/__main__.py
+++ b/src/debputy/commands/debputy_cmd/__main__.py
@@ -22,6 +22,7 @@ from typing import (
 )
 from collections.abc import Sequence
 
+import debputy.built_using
 from debputy import DEBPUTY_ROOT_DIR, DEBPUTY_PLUGIN_ROOT_DIR
 from debputy.analysis import REFERENCE_DATA_TABLE
 from debputy.build_support import perform_clean, perform_builds
@@ -930,6 +931,7 @@ def assemble(
                 )
             run_package_processors(manifest, package_metadata_context, fs_root)
 
+        debputy.built_using.add_relations(manifest, package_data_table)
         cross_package_control_files(package_data_table, manifest)
     for binary_data in package_data_table:
         if not binary_data.binary_package.should_be_acted_on:
diff --git a/src/debputy/highlevel_manifest.py 
b/src/debputy/highlevel_manifest.py
index c5255626..6da58ec7 100644
--- a/src/debputy/highlevel_manifest.py
+++ b/src/debputy/highlevel_manifest.py
@@ -72,7 +72,10 @@ from .plugin.api.spec import (
     INTEGRATION_MODE_DH_DEBPUTY_RRR,
     INTEGRATION_MODE_FULL,
 )
-from debputy.plugins.debputy.binary_package_rules import ServiceRule
+from debputy.plugins.debputy.binary_package_rules import (
+    BuiltUsingParsedFormat,
+    ServiceRule,
+)
 from debputy.plugins.debputy.build_system_rules import BuildRule
 from .plugin.plugin_state import run_in_context_of_plugin
 from .substitution import Substitution
@@ -179,6 +182,8 @@ class PackageTransformationDefinition:
     )
     install_rules: list[InstallRule] = field(default_factory=list)
     requested_service_rules: list[ServiceRule] = field(default_factory=list)
+    built_using: Sequence[BuiltUsingParsedFormat] = 
field(default_factory=tuple)
+    static_built_using: Sequence[BuiltUsingParsedFormat] = 
field(default_factory=tuple)
 
 
 def _path_to_tar_member(
@@ -1206,12 +1211,12 @@ class HighLevelManifest:
             remove_during_clean_rules
         )
         self._install_rules = install_rules
-        self._source_package = source_package
+        self.source_package = source_package
         self._binary_packages = binary_packages
         self.substitution = substitution
         self.package_transformations = package_transformations
         self._dpkg_architecture_variables = dpkg_architecture_variables
-        self._dpkg_arch_query_table = dpkg_arch_query_table
+        self.dpkg_arch_query_table = dpkg_arch_query_table
         self._build_env = build_env
         self._used_for: set[str] = set()
         self.build_environments = build_environments
@@ -1223,7 +1228,7 @@ class HighLevelManifest:
             substitution=self.substitution,
             deb_options_and_profiles=self._build_env,
             dpkg_architecture_variables=self._dpkg_architecture_variables,
-            dpkg_arch_query_table=self._dpkg_arch_query_table,
+            dpkg_arch_query_table=self.dpkg_arch_query_table,
         )
 
     def source_version(self, include_binnmu_version: bool = True) -> str:
@@ -1574,7 +1579,7 @@ class HighLevelManifest:
                 )
 
             package_data_dict[package] = BinaryPackageData(
-                self._source_package,
+                self.source_package,
                 dctrl_bin,
                 build_system_pkg_staging_dir,
                 fs_root,
@@ -1622,7 +1627,7 @@ class HighLevelManifest:
             substitution=package_transformation.substitution,
             deb_options_and_profiles=self._build_env,
             dpkg_architecture_variables=self._dpkg_architecture_variables,
-            dpkg_arch_query_table=self._dpkg_arch_query_table,
+            dpkg_arch_query_table=self.dpkg_arch_query_table,
         )
         norm_rules = list(
             builtin_mode_normalization_rules(
diff --git a/src/debputy/highlevel_manifest_parser.py 
b/src/debputy/highlevel_manifest_parser.py
index 73280956..2a9350c8 100644
--- a/src/debputy/highlevel_manifest_parser.py
+++ b/src/debputy/highlevel_manifest_parser.py
@@ -609,6 +609,9 @@ class YAMLManifestParser(HighLevelManifestParser):
                     )
                 if service_rules:
                     package_state.requested_service_rules.extend(service_rules)
+                package_state.built_using = parsed.get("built-using", ())
+                package_state.static_built_using = parsed.get(
+                    "static-built-using", ())
         self._build_rules = parsed_data.get("builds")
 
         return self.build_manifest()
diff --git a/src/debputy/plugins/debputy/binary_package_rules.py 
b/src/debputy/plugins/debputy/binary_package_rules.py
index e6cd1169..d9d1d2fe 100644
--- a/src/debputy/plugins/debputy/binary_package_rules.py
+++ b/src/debputy/plugins/debputy/binary_package_rules.py
@@ -1,5 +1,6 @@
 import dataclasses
 import os
+import textwrap
 from typing import (
     Any,
     List,
@@ -20,7 +21,10 @@ from debputy._manifest_constants import (
     MK_SERVICES,
 )
 from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand, 
MaintscriptSnippet
-from debputy.manifest_parser.base_types import FileSystemExactMatchRule
+from debputy.manifest_parser.base_types import (
+    DebputyParsedContentStandardConditional,
+    FileSystemExactMatchRule,
+)
 from debputy.manifest_parser.declarative_parser import ParserGenerator
 from debputy.manifest_parser.exceptions import ManifestParseException
 from debputy.manifest_parser.parse_hints import DebputyParseHint
@@ -132,6 +136,78 @@ def register_binary_package_rules(api: 
DebputyPluginInitializerProvider) -> None
         ),
     )
 
+    api.pluggable_manifest_rule(
+        rule_type=OPARSER_PACKAGES,
+        rule_name="built-using",
+        parsed_format=list[BuiltUsingParsedFormat],
+        handler=_unpack_list,
+        inline_reference_documentation=reference_documentation(
+            title="Built-Using dependency relations (`built-using`)",
+            description=textwrap.dedent(
+                f"""\
+                Generate a `Built-Using` dependency relation on the
+                build dependency selected by the `sources-for`, which
+                may contain a `*` wildcard matching any number of
+                arbitrary characters.
+
+                packages:
+                  PKG:
+                    built-using:
+                    - sources-for: foo-*-source   # foo-3.1.0-source
+                    - sources-for: foo
+                      when:                       # foo is always installed
+                        arch-matches: amd64       # but only used on amd64
+                    static-built-using:
+                    - sources-for: librust-*-dev  # several relations
+
+                Either of these conditions prevents the generation:
+                * PKG is not part of the current build because of its
+                  `Architecture` or `Build-Profiles` fields.
+                * The match in `Build-Depends` carries an invalid
+                  architecture or build profile restriction.
+                * The match in `Build-Depends` is not installed.
+                  This should only happen inside alternatives, see below.
+                * The manifest item carries an invalid `when:` condition.
+                  This may be useful when the match must be installed
+                  for unrelated reasons.
+
+                Matches are searched in the `Build-Depends` field of
+                the source package, and either `Build-Depends-Indep`
+                or `Build-Depends-Arch` depending on PKG.
+
+                In alternatives like `a|b`, each option may match
+                separately.  This is a compromise between
+                reproducibility on automatic builders (where the set
+                of installed package is constant), and least surprise
+                during local builds (where `b` may be installed
+                alone).  There seems to be no one-size fits all
+                solution when both are installed.
+
+                Architecture qualifiers and version restrictions in
+                `Build-Depends` are ignored. The only allowed
+                co-installations require a common source and version.
+                """,
+            ),
+        ),
+    )
+
+    api.pluggable_manifest_rule(
+        rule_type=OPARSER_PACKAGES,
+        rule_name="static-built-using",
+        parsed_format=list[BuiltUsingParsedFormat],
+        handler=_unpack_list,
+        inline_reference_documentation=reference_documentation(
+            title="Static-Built-Using dependency relations 
(`static-built-using`)",
+            description=textwrap.dedent(
+                f"""\
+                This rule behaves like `built-using`, except that the
+                affected field in the eventual binary package is
+                `Static-Built-Using`.
+                """,
+            ),
+        ),
+    )
+
 
 class ServiceRuleSourceFormat(TypedDict):
     service: str
@@ -224,6 +300,11 @@ class BinaryVersionParsedFormat(DebputyParsedContent):
     binary_version: str
 
 
+class BuiltUsingParsedFormat(DebputyParsedContentStandardConditional):
+    """Also used for static-built-using."""
+    sources_for: str
+
+
 class ListParsedFormat(DebputyParsedContent):
     elements: list[Any]
 
diff --git a/src/debputy/util.py b/src/debputy/util.py
index 36cf2a47..d3aa8226 100644
--- a/src/debputy/util.py
+++ b/src/debputy/util.py
@@ -478,6 +478,7 @@ def glob_escape(replacement_value: str) -> str:
 
 
 # TODO: This logic should probably be moved to `python-debian`
+# See bug1120283.py.
 def active_profiles_match(
     profiles_raw: str,
     active_build_profiles: set[str] | frozenset[str],
diff --git a/test_built_using.py b/test_built_using.py
new file mode 100644
index 00000000..74ad2b2d
--- /dev/null
+++ b/test_built_using.py
@@ -0,0 +1,371 @@
+"debputy: test generation of the (static-)built-using field."
+
+import os
+import re
+import subprocess
+import typing
+
+ARCH = subprocess.check_output(
+    ("dpkg-architecture", "-qDEB_BUILD_ARCH"),
+    encoding="ascii",
+).rstrip()
+RELATION_RE = re.compile(r"([a-z0-9.+*-]+) \(= [^)]+\)")
+
+
+def create(path: str, contents: str) -> None:
+    "Write a file to disk."
+    with open(path, "w", encoding="ascii") as f:
+        f.write(contents)
+
+
+def slurp(path: str) -> typing.Iterator[str]:
+    "Read a file from disk."
+    with open(path, encoding="ascii") as f:
+        yield from f
+
+
+def cat(path: str) -> None:
+    "Read, indent and output a file."
+    if os.path.exists(path):
+        print(f"-- {path}:")
+        with open(path, encoding="ascii") as f:
+            for line in f:
+                print(f"  {line}", end='')
+    else:
+        print(f"-- {path} does not exist")
+
+
+class BinPkg(typing.NamedTuple):
+    "Fake binary package built by a Test."
+    name: str
+    binary_paragraph: str
+    manifest: str
+    expected_bu: set[str] | None = set()  # None means not built
+    expected_sbu: set[str] | None = set()
+
+
+class Test(typing.NamedTuple):
+    "Fake source package used by a test."
+    name: str
+    source_paragraph: str
+    packages: typing.Sequence[BinPkg]
+
+
+# More or less in sync with dh-builtusing unit-tests.
+tests = (
+    Test(
+        name="01basic",
+        source_paragraph=f"""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~),
+  autotools-dev <dummy>, gcc [!{ARCH}], libc6, make
+Build-Depends-Arch: libdpkg-perl
+Build-Depends-Indep: libbinutils""",
+        packages=(
+            BinPkg(
+                name="foo",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: autotools-dev  # disabled by profile restriction
+    - sources-for: gcc            # disabled by architecture restriction
+    - sources-for: 'lib*'         # match in BD and BD-Indep, not BD-Arch
+    static-built-using:           # also check static-built-using
+    - sources-for: make""",
+                expected_bu={"binutils", "glibc"},
+                expected_sbu={"make-dfsg"},
+            ),
+            BinPkg(
+                name="bar",
+                binary_paragraph="Architecture: any",
+                manifest="""\
+    built-using:
+    - sources-for: 'lib*'         # match in BD and BD-Arch, not BD-Indep""",
+                expected_bu={"dpkg", "glibc"},
+            ),
+            BinPkg(
+                name="package-disabled-by-arch",
+                binary_paragraph=f"Architecture: !{ARCH}",
+                manifest="""\
+    built-using:
+    - sources-for: libc6          # package disabled by architecture""",
+                expected_bu=None,
+                expected_sbu=None,
+            ),
+            BinPkg(
+                name="package-disabled-by-profile",
+                binary_paragraph="""\
+Architecture: all
+Build-Profiles: <dummy>""",
+                manifest="""\
+    built-using:
+    - sources-for: make           # package disabled by build profile""",
+                expected_bu=None,
+                expected_sbu=None,
+            ),
+        ),
+    ),
+    Test(
+        name="30or-dependency",
+        source_paragraph="""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~),
+  cpp | dummy1, dummy2 | binutils""",
+        packages=(
+            BinPkg(
+                name="foo",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: cpp
+    - sources-for: binutils""",
+                expected_bu={"gcc-defaults", "binutils"},
+            ),
+        ),
+    ),
+    Test(
+        name="40pattern",
+        source_paragraph="""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~), gcc, g++
+Build-Depends-Arch: libbinutils
+Build-Depends-Indep: libc6""",
+        packages=(
+            BinPkg(
+                name="initial",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: '*c'""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="final",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: 'gc*'""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="empty",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: 'g*cc'""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="one-char",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: 'g*c'""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="encoding-star-plus",  # + is common re character
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: 'g*+'""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="encoding-plus",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: g++            # match despite regex characters""",
+                expected_bu={"gcc-defaults"},
+            ),
+            BinPkg(
+                name="ambiguous-all",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: 'lib*'""",
+                expected_bu={"glibc"},
+            ),
+            BinPkg(
+                name="ambiguous-any",
+                binary_paragraph="Architecture: any",
+                manifest="""\
+    built-using:
+    - sources-for: 'lib*'""",
+                expected_bu={"binutils"},
+            ),
+        ),
+    ),
+    Test(
+        name="50same-source",
+        source_paragraph="""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~),
+  autotools-dev, cpp, gcc, libc6, make""",
+        packages=(
+            BinPkg(
+                name="foo",
+                binary_paragraph="Architecture: any",
+                manifest=f"""\
+    built-using:
+    - sources-for: '*-dev'        # multiple matches
+    - sources-for: cpp
+    - sources-for: gcc            # same source than cpp
+    - sources-for: libc6          # architecture manifest condition
+      when:
+        arch-matches: '!{ARCH}'
+    - sources-for: make           # profile manifest condition
+      when:
+        build-profiles-matches: <dummyprofile>""",
+                expected_bu={"autotools-dev", "dpkg", "gcc-defaults"},
+            ),
+        ),
+    ),
+    Test(
+        name="70arch-suffix",
+        source_paragraph=f"""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~),
+  debhelper:all, gcc:{ARCH}, libc6:{ARCH}""",
+        packages=(
+            BinPkg(
+                name="foo",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: debhelper      # all
+    - sources-for: gcc            # native
+    - sources-for: libc6          # same""",
+                expected_bu={"debhelper", "gcc-defaults", "glibc"},
+            ),
+        ),
+    ),
+    Test(
+        name="70multiarch",
+        source_paragraph="""\
+Build-Depends: debputy (>= 0.1.45~), dpkg-dev (>= 1.22.7~),
+  gcc, libc6, make""",
+        packages=(
+            BinPkg(
+                name="foo",
+                binary_paragraph="Architecture: all",
+                manifest="""\
+    built-using:
+    - sources-for: gcc            # no
+    - sources-for: libc6          # same
+    - sources-for: dpkg-dev       # foreign
+    - sources-for: make           # allowed""",
+                expected_bu={"gcc-defaults", "glibc", "dpkg", "make-dfsg"},
+            ),
+        ),
+    ),
+)
+
+os.mkdir("test-built-using-tmpdir")
+os.chdir("test-built-using-tmpdir")
+create("format", """\
+3.0 (native)
+""")
+create("changelog", """\
+foo (0) unstable; urgency=medium
+
+  * For testing purposes.  Closes: #0.
+
+ -- Test <testing@nowhere>  Sun, 12 Oct 2025 11:44:43 +0000
+""")
+create("Makefile", """\
+all:
+\ttouch $(files)
+install: all
+\tinstall -Dt$(DESTDIR)/usr/share/foo $(files)
+""")
+create("copyright", """\
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files: *
+Copyright: 2025 Someone <[email protected]>
+License: GPL-3+
+""")
+for test in tests:
+    print(f"Testing {test.name}")
+    os.makedirs(f"{test.name}/foo/debian/source")
+    os.chdir(f"{test.name}/foo")
+    os.symlink("../../../../format", "debian/source/format")
+    os.symlink("../../../changelog", "debian/changelog")
+    os.symlink("../../../copyright", "debian/copyright")
+    os.symlink("../../Makefile", "Makefile")
+
+    with open("debian/control", "w", encoding="ascii") as control_file:
+        control_file.write(f"""\
+Source: foo
+Section: misc
+Priority: optional
+Maintainer: Test <testing@nowhere>
+Standards-Version: 4.7.2
+Build-Driver: debputy
+{test.source_paragraph}
+""")
+        for p in test.packages:
+            control_file.write(f"""
+Package: {p.name}
+Description: short
+ Long.
+{p.binary_paragraph}
+""")
+
+    with open("debian/debputy.manifest", "w", encoding="ascii") as manifest:
+        manifest.write(f"""\
+manifest-version: '0.1'
+default-build-environment:
+  set:
+    files: {' '.join(p.name for p in test.packages)}
+""")
+        if len(test.packages) != 1:
+            manifest.write("installations:\n")
+            for p in test.packages:
+                manifest.write(f"""\
+- install:
+    source: usr/share/foo/{p.name}
+    into: {p.name}
+""")
+        manifest.write("packages:\n")
+        for p in test.packages:
+            manifest.write(f"  {p.name}:\n{p.manifest}\n")
+
+    subprocess.check_output(
+        args=(
+            "../../../debputy.sh",
+            "internal-command",
+            "dpkg-build-driver-run-task",
+            "binary",
+            "--debug",
+         ),
+        encoding="utf-8",
+        env={"DEBPUTY_DEBUG": "1"},
+    )
+
+    for p in test.packages:
+
+        # This can obviously be improved.
+        control = "debian/.debputy/scratch-dir/materialization-dirs/" \
+            + p.name + "/deb-root/DEBIAN/control"
+
+        built = os.path.exists(control)
+        for expected, field in (
+            (p.expected_bu, "Built-Using"),
+            (p.expected_sbu, "Static-Built-Using"),
+        ):
+            got: set[str] | None = None
+            if built:
+                got = set()
+                r = re.compile(f"^{field}: ")
+                ls = tuple(line for line in slurp(control) if r.match(line))
+                assert len(ls) in (0, 1)
+                if ls:
+                    for relation in ls[0][len(field) + 2:-1].split(", "):
+                        m = RELATION_RE.fullmatch(relation)
+                        assert m is not None
+                        got.add(m.group(1))
+            if expected != got:
+                print(f"FAIL: {p.name}.{field} expected {expected}, got {got}")
+                cat(control)
+                cat("debian/control")
+                cat("debian/debputy.manifest")
+
+    os.chdir("../..")

--- End Message ---
--- Begin Message ---
Source: debputy
Source-Version: 0.1.79
Done: Niels Thykier <[email protected]>

We believe that the bug you reported is fixed in the latest version of
debputy, which is due to be installed in the Debian FTP archive.

A summary of the changes between this version and the previous one is
attached.

Thank you for reporting the bug, which will now be closed.  If you
have further comments please address them to [email protected],
and the maintainer will reopen the bug report if appropriate.

Debian distribution maintenance software
pp.
Niels Thykier <[email protected]> (supplier of updated debputy package)

(This message was generated automatically at their request; if you
believe that there is a problem with it please contact the archive
administrators by mailing [email protected])


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Format: 1.8
Date: Sun, 28 Dec 2025 12:07:00 +0000
Source: debputy
Architecture: source
Version: 0.1.79
Distribution: unstable
Urgency: medium
Maintainer: Debputy Maintainers <[email protected]>
Changed-By: Niels Thykier <[email protected]>
Closes: 1121339
Changes:
 debputy (0.1.79) unstable; urgency=medium
 .
   * Backwards Incompatibility:
     - Install `debian/pkg.modprobe` files under `/usr/lib/modprobe.d`.
       No packages using `debputy` in Debian used this feature yet,
       so the change was done without migration. If you migrate a
       package using `debian/pkg.modprobe`, please consider adding
       `dh-debputy (>= 0.1.79~)` as Build-Dependency to ensure there
       are no surprises with backports (etc.).
 .
   * Manifest changes:
     - Add `built-using` and `static-built-using` keywords to enable
       `debputy` to generate `Built-Using` and `Static-Built-Using`
       for you. This replaces the `dh-sequence-builtusing` when
       using either `full` or `dh-sequence-zz-debputy` integration
       mode.  (Closes: #1121339)
 .
   * migrate-from-dh:
     - Detect and provide manual instructions for migrating from
       `dh-builtusing` in `dh-sequence-zz-debputy` or `full`
       integration mode.
 .
   * Plugin API:
     - Manifest detectors can now access some of the pluggable
       manifest rules via the context.
 .
   [ Michael Hudson-Doyle ]
   * debputy: Support Ubuntu's `Architecture-Variant` when deciding
     the output file name for binary packages.
 .
   [ Nicolas Boulenguez ]
   * Design and implement the `built-using` and `static-built-using`
     feature based on `dh-builtusing`.
 .
   [ Niels Thykier ]
   * debputy: Recognise `.org` as common upstream changelog extension.
     This is to match `dh_installchangelogs`.
   * Generated prerm: Validate that `python3` works before calling
     `py3clean`. This is to match what `dh-python` does.
Checksums-Sha1:
 1dd9e9fb348b32b04b504cb4e57dffdf4b86c2fe 2590 debputy_0.1.79.dsc
 7b3a483da7e783738c15973727cf54e1378f26d7 734664 debputy_0.1.79.tar.xz
 be61428ba5f6b80e588f410bce78f90e2b7cdb25 1285508 debputy_0.1.79.git.tar.xz
 7a300ef12524618c88b9fdc1429d81a5f04862f8 17131 debputy_0.1.79_source.buildinfo
Checksums-Sha256:
 9d0a062ebd9e6bb485d4f6dddd80c1c7bfde40a283771182479d985ccedf7eb0 2590 
debputy_0.1.79.dsc
 45a90babce8d3b1aa72fda8027fa6751a20d3900612fb729cd732b4af256d46b 734664 
debputy_0.1.79.tar.xz
 65467572472b844ff84408350fcb013028f38ce869437b870a5acd0abf9dd0f6 1285508 
debputy_0.1.79.git.tar.xz
 df279e694d39887700fcde7df077e8a7fe9f2515d8109d84e2cfec59d55bbe72 17131 
debputy_0.1.79_source.buildinfo
Files:
 724aebd9d752c2344ebe50f24d35eaa2 2590 devel optional debputy_0.1.79.dsc
 98b0e30e2928515bf7c8b7fae9f6893e 734664 devel optional debputy_0.1.79.tar.xz
 db2622c5761db7aabbeb1906b06b311f 1285508 devel None debputy_0.1.79.git.tar.xz
 c1c8c074016e19fda1ec9c7be22c3a23 17131 devel optional 
debputy_0.1.79_source.buildinfo
Git-Tag-Info: tag=6451d4d175e97fd0808b2380aa03d695e998e0d0 
fp=f5e7199aef5e5c67e555873f740d68888365d289
Git-Tag-Tagger: Niels Thykier <[email protected]>

-----BEGIN PGP SIGNATURE-----

iQIzBAEBCgAdFiEEN02M5NuW6cvUwJcqYG0ITkaDwHkFAmlRVIYACgkQYG0ITkaD
wHmV2A//UWFrVzZ+9G2YT9Nsd3g3zBrDpX4/l2ubnYfZXD90wl1/FoGgUKED/GKe
io9SNxU3d8580T66xlW7pE/2+/kvmsWpD6wvPDc8iPVFsxS2wGx0r/NG8u66N+Nm
q5ua7OTE2jH8y6SesTMO9Fcw4eGlWUV1ODjmB7K6AuD934Dq8zYAhxsMFHN7aMiC
XNL3X1Bjqj4w/dOczHLP5mgEi6ZnnolSkZnyT4X2cLwp5JHw4r/nU8iyrn7s9Ram
x2uQeekYZEzJBbcBl/AeMRUIDzsbqTe9fVLCojWbNPA2o5jQf94bTCGV21D+ER1f
Zv9lxe6w4x6Vku7FwrQcu7web/GWZSMF20UCVl9HKSgPd+WjexVmAJAs5jgrkRBr
f9OrMAdAhkvv0+9OuWX7u6cV1PJenn/m1TZ1l07vOQ2JRPPo9N1tta3Cd7earPtz
nKTCrT9hn4kMJw2JVbMm17pTQ9Eq5YAr4nPzc2TukvAtH8/u/JJZTncEFy/bpQ4F
o0QVviPTehfgPcMPPmW1OLQM0LFz/4tpgQcMNcJND9A4QDnYMvBeVOmRC5kkeaLb
LnhQQdO9YUspee/XbWtEAQ7EkSdZoHstax3JJq3K9OzL3D3LTnvvYTlyYzSroCBU
xy9MGon0vD4bmWFGdXTigQ91g72HbIlcngKzL+E7OFX3EEetSxc=
=B9ay
-----END PGP SIGNATURE-----

Attachment: pgp2ybEksQH9z.pgp
Description: PGP signature


--- End Message ---

Reply via email to