diff options
Diffstat (limited to 'src/debputy/lsp/lsp_debian_control_reference_data.py')
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 2067 |
1 files changed, 2067 insertions, 0 deletions
diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py new file mode 100644 index 0000000..f4791cb --- /dev/null +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -0,0 +1,2067 @@ +import dataclasses +import functools +import itertools +import textwrap +from abc import ABC +from enum import Enum, auto +from typing import ( + FrozenSet, + Optional, + cast, + Mapping, + Iterable, + List, + Generic, + TypeVar, + Union, +) + +from debian.debian_support import DpkgArchTable +from lsprotocol.types import DiagnosticSeverity, Diagnostic, DiagnosticTag + +from debputy.lsp.quickfixes import ( + propose_correct_text_quick_fix, + propose_remove_line_quick_fix, +) +from debputy.lsp.text_util import ( + normalize_dctrl_field_name, + LintCapablePositionCodec, + detect_possible_typo, + te_range_to_lsp, +) +from debputy.lsp.vendoring._deb822_repro.parsing import ( + Deb822KeyValuePairElement, + LIST_SPACE_SEPARATED_INTERPRETATION, + Deb822ParagraphElement, + Deb822FileElement, +) +from debputy.lsp.vendoring._deb822_repro.tokens import Deb822FieldNameToken + +try: + from debputy.lsp.vendoring._deb822_repro.locatable import ( + Position as TEPosition, + Range as TERange, + START_POSITION, + ) +except ImportError: + pass + + +F = TypeVar("F", bound="Deb822KnownField") +S = TypeVar("S", bound="StanzaMetadata") + + +ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( + [ + "admin", + "cli-mono", + "comm", + "database", + "debian-installer", + "debug", + "devel", + "doc", + "editors", + "education", + "electronics", + "embedded", + "fonts", + "games", + "gnome", + "gnu-r", + "gnustep", + "graphics", + "hamradio", + "haskell", + "interpreters", + "introspection", + "java", + "javascript", + "kde", + "kernel", + "libdevel", + "libs", + "lisp", + "localization", + "mail", + "math", + "metapackages", + "misc", + "net", + "news", + "ocaml", + "oldlibs", + "otherosfs", + "perl", + "php", + "python", + "ruby", + "rust", + "science", + "shells", + "sound", + "tasks", + "tex", + "text", + "utils", + "vcs", + "video", + "virtual", + "web", + "x11", + "xfce", + "zope", + ] +) + +ALL_COMPONENTS = frozenset( + [ + "main", + "restricted", # Ubuntu + "non-free", + "non-free-firmware", + "contrib", + ] +) + + +def _fields(*fields: F) -> Mapping[str, F]: + return {normalize_dctrl_field_name(f.name.lower()): f for f in fields} + + +@dataclasses.dataclass(slots=True, frozen=True) +class Keyword: + value: str + hover_text: Optional[str] = None + is_obsolete: bool = False + replaced_by: Optional[str] = None + + +def _allowed_values(*values: Union[str, Keyword]) -> Mapping[str, Keyword]: + as_keywords = (k if isinstance(k, Keyword) else Keyword(k) for k in values) + return {k.value: k for k in as_keywords} + + +ALL_SECTIONS = _allowed_values( + *[ + s if c is None else f"{c}/{s}" + for c, s in itertools.product( + itertools.chain(cast("Iterable[Optional[str]]", [None]), ALL_COMPONENTS), + ALL_SECTIONS_WITHOUT_COMPONENT, + ) + ] +) + + +def all_architectures_and_wildcards(arch2table) -> Iterable[Union[str, Keyword]]: + wildcards = set() + yield Keyword( + "any", + hover_text=textwrap.dedent( + """\ + The package is an architecture dependent package and need to be compiled for each and every + architecture it. + + The name `any` refers to the fact that this is an architecture *wildcard* matching + *any machine architecture* supported by dpkg. + """ + ), + ) + yield Keyword( + "all", + hover_text=textwrap.dedent( + """\ + The package is an architecture independent package. This is typically fitting for packages containing + only scripts, data or documentation. + + This name `all` refers to the fact that the package can be used for *all* architectures at the same. + Though note that it is still subject to the rules of the `Multi-Arch` field. + """ + ), + ) + for arch_name, quad_tuple in arch2table.items(): + yield arch_name + cpu_wc = "any-" + quad_tuple.cpu_name + os_wc = quad_tuple.os_name + "-any" + if cpu_wc not in wildcards: + yield cpu_wc + wildcards.add(cpu_wc) + if os_wc not in wildcards: + yield os_wc + wildcards.add(os_wc) + # Add the remaining wildcards + + +@functools.lru_cache +def dpkg_arch_and_wildcards() -> FrozenSet[str]: + dpkg_arch_table = DpkgArchTable.load_arch_table() + return frozenset(all_architectures_and_wildcards(dpkg_arch_table._arch2table)) + + +class FieldValueClass(Enum): + SINGLE_VALUE = auto() + SPACE_SEPARATED_LIST = auto() + BUILD_PROFILES_LIST = auto() + COMMA_SEPARATED_LIST = auto() + COMMA_SEPARATED_EMAIL_LIST = auto() + FREE_TEXT_FIELD = auto() + DEP5_FILE_LIST = auto() + + +@dataclasses.dataclass(slots=True, frozen=True) +class Deb822KnownField: + name: str + field_value_class: FieldValueClass + warn_if_default: bool = True + replaced_by: Optional[str] = None + deprecated_with_no_replacement: bool = False + missing_field_severity: Optional[DiagnosticSeverity] = None + default_value: Optional[str] = None + known_values: Optional[Mapping[str, Keyword]] = None + unknown_value_diagnostic_severity: Optional[DiagnosticSeverity] = ( + DiagnosticSeverity.Error + ) + hover_text: Optional[str] = None + spellcheck_value: bool = False + is_stanza_name: bool = False + is_single_value_field: bool = True + + def field_diagnostics( + self, + kvpair: Deb822KeyValuePairElement, + stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], + *, + field_name_typo_reported: bool = False, + ) -> Iterable[Diagnostic]: + field_name_token = kvpair.field_token + field_range_te = kvpair.range_in_parent().relative_to(stanza_position) + field_position_te = field_range_te.start_pos + yield from self._diagnostics_for_field_name( + field_name_token, + field_position_te, + field_name_typo_reported, + position_codec, + lines, + ) + if not self.spellcheck_value: + yield from self._known_value_diagnostics( + kvpair, field_position_te, position_codec, lines + ) + + def _diagnostics_for_field_name( + self, + token: Deb822FieldNameToken, + token_position: "TEPosition", + typo_detected: bool, + position_codec: "LintCapablePositionCodec", + lines: List[str], + ) -> Iterable[Diagnostic]: + field_name = token.text + # Defeat the case-insensitivity from python-debian + field_name_cased = str(field_name) + token_range_server_units = te_range_to_lsp( + TERange.from_position_and_size(token_position, token.size()) + ) + token_range = position_codec.range_to_client_units( + lines, + token_range_server_units, + ) + if self.deprecated_with_no_replacement: + yield Diagnostic( + token_range, + f"{field_name_cased} is deprecated and no longer used", + severity=DiagnosticSeverity.Warning, + source="debputy", + tags=[DiagnosticTag.Deprecated], + data=propose_remove_line_quick_fix(), + ) + elif self.replaced_by is not None: + yield Diagnostic( + token_range, + f"{field_name_cased} is a deprecated name for {self.replaced_by}", + severity=DiagnosticSeverity.Warning, + source="debputy", + tags=[DiagnosticTag.Deprecated], + data=propose_correct_text_quick_fix(self.replaced_by), + ) + + if not typo_detected and field_name_cased != self.name: + yield Diagnostic( + token_range, + f"Non-canonical spelling of {self.name}", + severity=DiagnosticSeverity.Information, + source="debputy", + data=propose_correct_text_quick_fix(self.name), + ) + + def _known_value_diagnostics( + self, + kvpair: Deb822KeyValuePairElement, + field_position_te: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], + ) -> Iterable[Diagnostic]: + unknown_value_severity = self.unknown_value_diagnostic_severity + allowed_values = self.known_values + if not allowed_values: + return + hint_text = None + values = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) + value_off = kvpair.value_element.position_in_parent().relative_to( + field_position_te + ) + first_value = True + for value_ref in values.iter_value_references(): + value = value_ref.value + if ( + not first_value + and self.field_value_class == FieldValueClass.SINGLE_VALUE + ): + value_loc = value_ref.locatable + value_position_te = value_loc.position_in_parent().relative_to( + value_off + ) + value_range_in_server_units = te_range_to_lsp( + TERange.from_position_and_size(value_position_te, value_loc.size()) + ) + value_range = position_codec.range_to_client_units( + lines, + value_range_in_server_units, + ) + yield Diagnostic( + value_range, + f"The field {self.name} can only have exactly one value.", + severity=DiagnosticSeverity.Error, + source="debputy", + ) + # TODO: Add quickfix if the value is also invalid + continue + first_value = False + + known_value = self.known_values.get(value) + if known_value is None: + candidates = detect_possible_typo( + value, + self.known_values, + ) + if hint_text is None: + if len(self.known_values) < 5: + values = ", ".join(sorted(self.known_values)) + hint_text = f" Known values for this field: {values}" + else: + hint_text = "" + fix_data = None + severity = unknown_value_severity + fix_text = hint_text + if candidates: + match = candidates[0] + fix_text = f' It is possible that the value is a typo of "{match}".{fix_text}' + fix_data = [propose_correct_text_quick_fix(m) for m in candidates] + elif severity is None: + continue + if severity is None: + severity = DiagnosticSeverity.Warning + message = fix_text + else: + message = f'The value "{value}" is not supported in {self.name}.{fix_text}' + elif known_value.is_obsolete: + replacement = known_value.replaced_by + if replacement is not None: + message = f'The value "{value}" has been replaced by {replacement}' + severity = DiagnosticSeverity.Warning + fix_data = [propose_correct_text_quick_fix(replacement)] + else: + message = ( + f'The value "{value}" is obsolete without a single replacement' + ) + severity = DiagnosticSeverity.Warning + fix_data = None + else: + # All good + continue + + value_loc = value_ref.locatable + value_position_te = value_loc.position_in_parent().relative_to(value_off) + value_range_in_server_units = te_range_to_lsp( + TERange.from_position_and_size(value_position_te, value_loc.size()) + ) + value_range = position_codec.range_to_client_units( + lines, + value_range_in_server_units, + ) + yield Diagnostic( + value_range, + message, + severity=severity, + source="debputy", + data=fix_data, + ) + + +@dataclasses.dataclass(slots=True, frozen=True) +class DctrlKnownField(Deb822KnownField): + inherits_from_source: bool = False + + +SOURCE_FIELDS = _fields( + DctrlKnownField( + "Source", + FieldValueClass.SINGLE_VALUE, + missing_field_severity=DiagnosticSeverity.Error, + is_stanza_name=True, + hover_text=textwrap.dedent( + """\ + Declares the name of the source package. + + Note this must match the name in the first entry of debian/changelog file. + """ + ), + ), + DctrlKnownField( + "Standards-Version", + FieldValueClass.SINGLE_VALUE, + missing_field_severity=DiagnosticSeverity.Error, + hover_text=textwrap.dedent( + """\ + Declares the last semantic version of the Debian Policy this package as last checked against. + + **Example*: + ``` + Standards-Version: 4.5.2 + ``` + + Note that the last version part of the full Policy version (the **.X** in 4.5.2**.X**) is + typically omitted as it is used solely for editorial changes to the policy (e.g. typo fixes). + """ + ), + ), + DctrlKnownField( + "Section", + FieldValueClass.SINGLE_VALUE, + known_values=ALL_SECTIONS, + unknown_value_diagnostic_severity=DiagnosticSeverity.Warning, + hover_text=textwrap.dedent( + """\ + Define the default section for packages in this source package. + + Example: + ``` + Section: devel + ``` + + Please see https://packages.debian.org/unstable for more details about the sections. + """ + ), + ), + DctrlKnownField( + "Priority", + FieldValueClass.SINGLE_VALUE, + default_value="optional", + warn_if_default=False, + known_values=_allowed_values( + Keyword( + "required", + hover_text=textwrap.dedent( + """\ + The package is necessary for the proper functioning of the system (read: dpkg needs it). + + Applicable if dpkg *needs* this package to function and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "important", + hover_text=textwrap.dedent( + """\ + The *important* packages are a bare minimum of commonly-expected and necessary tools. + + Applicable if 99% of all users in the distribution needs this package and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "standard", + hover_text=textwrap.dedent( + """\ + These packages provide a reasonable small but not too limited character-mode system. This is + what will be installed by default (by the debian-installer) if the user does not select anything + else. This does not include many large applications. + + Applicable if your distribution installer will install this package by default on a new system + and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "optional", + hover_text="This is the default priority and used by the majority of all packages" + " in the Debian archive", + ), + Keyword( + "extra", + is_obsolete=True, + replaced_by="optional", + hover_text="Obsolete alias of `optional`.", + ), + ), + hover_text=textwrap.dedent( + """\ + Define the default priority for packages in this source package. + + The priority field describes how important the package is for the functionality of the system. + + Example: + ``` + Priority: optional + ``` + + Unless you know you need a different value, you should choose <b>optional</b> for your packages. + """ + ), + ), + DctrlKnownField( + "Maintainer", + FieldValueClass.SINGLE_VALUE, + missing_field_severity=DiagnosticSeverity.Error, + hover_text=textwrap.dedent( + """\ + The maintainer of the package. + + **Example**: + ``` + Maintainer: Jane Contributor <jane@janes.email-provider.org> + ``` + + Note: If a person is listed in the Maintainer field, they should *not* be listed in Uploaders field. + """ + ), + ), + DctrlKnownField( + "Uploaders", + FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, + hover_text=textwrap.dedent( + """\ + Comma separated list of uploaders associated with the package. + + **Example**: + ``` + Uploaders: + John Doe <john@doe.org>, + Lisbeth Worker <lis@worker.org>, + ``` + + Formally uploaders are considered co-maintainers for the package with the party listed in the + **Maintainer** field being the primary maintainer. In practice, each maintainer or maintenance + team can have their own ruleset about the difference between the **Maintainer** and the + **Uploaders**. As an example, the Python packaging team has a different rule set for how to + react to a package depending on whether the packaging team is the **Maintainer** or in the + **Uploaders** field. + + Note: If a person is listed in the Maintainer field, they should *not* be listed in Uploaders field. + """ + ), + ), + DctrlKnownField( + "Vcs-Browser", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + URL to the Version control system repo used for the packaging. The URL should be usable with a + browser *without* requiring any login. + + This should be used together with one of the other **Vcs-** fields. + """ + ), + ), + DctrlKnownField( + "Vcs-Git", + FieldValueClass.SPACE_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable with `git clone` + *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + + Note it is possible to specify a branch via the `-b` option. + + ``` + Vcs-Git: https://salsa.debian.org/some/packaging-repo -b debian/unstable + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Svn", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable with `svn checkout` + *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Arch", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable for getting a copy of the + sources *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Cvs", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable for getting a copy of the + sources *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Darcs", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable for getting a copy of the + sources *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Hg", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable for getting a copy of the + sources *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "Vcs-Mtn", + FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value + hover_text=textwrap.dedent( + """\ + URL to the git repo used for the packaging. The URL should be usable for getting a copy of the + sources *without* requiring any login. + + This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. + ``` + """ + ), + ), + DctrlKnownField( + "DM-Upload-Allowed", + FieldValueClass.SINGLE_VALUE, + deprecated_with_no_replacement=True, + default_value="no", + known_values=_allowed_values("yes", "no"), + hover_text=textwrap.dedent( + """\ + Obsolete field + + It was used to enabling Debian Maintainers to upload the package without requiring a Debian Developer + to sign the package. This mechanism has been replaced by a new authorization mechanism. + + Please see https://lists.debian.org/debian-devel-announce/2012/09/msg00008.html for details about the + replacement. + ``` + """ + ), + ), + DctrlKnownField( + "Build-Depends", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + All minimum build-dependencies for this source package. Needed for any target including **clean**. + """ + ), + ), + DctrlKnownField( + "Build-Depends-Arch", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Build-dependencies required for building the architecture dependent binary packages of this source + package. + + These build-dependencies must be satisfied when executing the **build-arch** and **binary-arch** + targets either directly or indirectly in addition to those listed in **Build-Depends**. + + Note that these dependencies are <em>not</em> available during **clean**. + """ + ), + ), + DctrlKnownField( + "Build-Depends-Indep", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Build-dependencies required for building the architecture independent binary packages of this source + package. + + These build-dependencies must be satisfied when executing the **build-indep** and **binary-indep** + targets either directly or indirectly in addition to those listed in **Build-Depends**. + + Note that these dependencies are <em>not</em> available during **clean**. + """ + ), + ), + DctrlKnownField( + "Build-Conflicts", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Packages that must **not** be installed during **any** part of the build, including the **clean** + target **clean**. + + Where possible, it is often better to configure the build so that it does not react to the package + being present in the first place. Usually this is a question of using a `--without-foo` or + `--disable-foo` or such to the build configuration. + """ + ), + ), + DctrlKnownField( + "Build-Conflicts-Arch", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Packages that must **not** be installed during the **build-arch** or **binary-arch** targets. + This also applies when these targets are run implicitly such as via the **binary** target. + + Where possible, it is often better to configure the build so that it does not react to the package + being present in the first place. Usually this is a question of using a `--without-foo` or + `--disable-foo` or such to the build configuration. + """ + ), + ), + DctrlKnownField( + "Build-Conflicts-Indep", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Packages that must **not** be installed during the **build-indep** or **binary-indep** targets. + This also applies when these targets are run implicitly such as via the **binary** target. + + Where possible, it is often better to configure the build so that it does not react to the package + being present in the first place. Usually this is a question of using a `--without-foo` or + `--disable-foo` or such to the build configuration. + """ + ), + ), + DctrlKnownField( + "Testsuite", + FieldValueClass.SPACE_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Declares that this package provides or should run install time tests via `autopkgtest`. + + This field can be used to request an automatically generated autopkgtests via the **autodep8** package. + Please refer to the documentation of the **autodep8** package for which values you can put into + this field and what kind of testsuite the keywords will provide. + + Declaring this field in *debian/control* is only necessary when you want additional tests beyond + those in *debian/tests/control* as **dpkg** automatically records the package provided ones from + *debian/tests/control*. + """ + ), + ), + DctrlKnownField( + "Homepage", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Link to the upstream homepage for this source package. + + **Example**: + ``` + Homepage: https://www.janes-tools.org/frob-cleaner + ``` + """ + ), + ), + DctrlKnownField( + "Rules-Requires-Root", + FieldValueClass.SPACE_SEPARATED_LIST, + unknown_value_diagnostic_severity=None, + known_values=_allowed_values( + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The build process will not require root or fakeroot during any step. This enables + dpkg-buildpackage and debhelper to perform several optimizations during the build. + + This is the default with dpkg-build-api at version 1 or later. + """ + ), + ), + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The build process assumes that dpkg-buildpackage will run the relevant binary + target with root or fakeroot. This was the historical default behaviour. + + This is the default with dpkg-build-api at version 0. + """ + ), + ), + ), + hover_text=textwrap.dedent( + """\ + Declare if and when the package build assumes it is run as root or fakeroot. + + Most packages do not need to run as root or fakeroot and the legacy behaviour comes with a + performance cost. This field can be used to explicitly declare that the legacy behaviour is + unnecessary. + + **Example:** + ``` + Rules-Requires-Root: no + ``` + + Setting this field to `no` *can* cause the package to stop building if it requires root. + Depending on the situation, it might require some trivial or some complicated changes to fix that. + If it breaks and you cannot figure out how to fix it, then reset the field to `binary-targets` + and move on until you have time to fix it. + + The default value for this field depends on the ``dpkg-build-api`` version. If the package + `` Build-Depends`` on ``dpkg-build-api (>= 1)`` or later, the default is ``no``. Otherwise, + the default is ``binary-target`` + + Note it is **not** possible to require running the package as "true root". + """ + ), + ), + DctrlKnownField( + "Bugs", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Provide a custom bug tracker URL + + This field is *not* used by packages uploaded to Debian or most derivatives as the distro tooling + has a default bugtracker built-in. It is primarily useful for third-party provided packages such + that bug reporting tooling can redirect the user to their bug tracker. + """ + ), + ), + DctrlKnownField( + "Origin", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Declare the origin of the package. + + This field is *not* used by packages uploaded to Debian or most derivatives as the origin would + be the distribution. It is primarily useful for third-party provided packages as some tools will + detect this field. + """ + ), + ), + DctrlKnownField( + "X-Python-Version", + FieldValueClass.COMMA_SEPARATED_LIST, + replaced_by="X-Python3-Version", + hover_text=textwrap.dedent( + """\ + Obsolete field for declaring the supported Python2 versions + + Since Python2 is no longer supported, this field is now redundant. For Python3, the field is + called **X-Python3-Version**. + """ + ), + ), + DctrlKnownField( + "X-Python3-Version", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + # Too lazy to provide a better description + """\ + For declaring the supported Python3 versions + + This is used by the tools from `dh-python` package. Please see the documentation of that package + for when and how to use it. + """ + ), + ), + DctrlKnownField( + "XS-Autobuild", + FieldValueClass.SINGLE_VALUE, + known_values=_allowed_values("yes"), + hover_text=textwrap.dedent( + """\ + Used for non-free packages to denote that they may be auto-build on the Debian build infrastructure + + Note that adding this field **must** be combined with following the instructions at + https://www.debian.org/doc/manuals/developers-reference/pkgs.html#non-free-buildd + """ + ), + ), + DctrlKnownField( + "Description", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + hover_text=textwrap.dedent( + """\ + This field contains a human-readable description of the package. However, it is not used directly. + + Binary packages can reference parts of it via the `${source:Synopsis}` and the + `${source:Extended-Description}` substvars. Without any of these substvars, the `Description` field + of the `Source` stanza remains unused. + + The first line immediately after the field is called the *Synopsis* and is a short "noun-phrase" + intended to provide a one-line summary of a package. The lines after the **Synopsis** is known + as the **Extended Description** and is intended as a longer summary of a package. + + **Example:** + ``` + Description: documentation generator for Python projects + Sphinx is a tool for producing documentation for Python projects, using + reStructuredText as markup language. + . + Sphinx features: + * HTML, CHM, LaTeX output, + * Cross-referencing source code, + * Automatic indices, + * Code highlighting, using Pygments, + * Extensibility. Existing extensions: + - automatic testing of code snippets, + - including docstrings from Python modules. + . + Build-depend on sphinx if your package uses /usr/bin/sphinx-* + executables. Build-depend on python3-sphinx if your package uses + the Python API (for instance by calling python3 -m sphinx). + ``` + + The **Synopsis** is usually displayed in cases where there is limited space such as when reviewing + the search results from `apt search foo`. It is often a good idea to imagine that the **Synopsis** + part is inserted into a sentence like "The package provides {{Synopsis-goes-here}}". The + **Extended Description** is a standalone description that should describe what the package does and + how it relates to the rest of the system (in terms of, for example, which subsystem it is which part of). + Please see https://www.debian.org/doc/debian-policy/ch-controlfields.html#description for more details + about the description field and suggestions for how to write it. + """ + ), + ), +) + +BINARY_FIELDS = _fields( + DctrlKnownField( + "Package", + FieldValueClass.SINGLE_VALUE, + is_stanza_name=True, + missing_field_severity=DiagnosticSeverity.Error, + hover_text="Declares the name of a binary package", + ), + DctrlKnownField( + "Package-Type", + FieldValueClass.SINGLE_VALUE, + default_value="deb", + known_values=_allowed_values( + Keyword("deb", hover_text="The package will be built as a regular deb."), + Keyword( + "udeb", + hover_text="The package will be built as a micro-deb (also known as a udeb). These are solely used by the debian-installer.", + ), + ), + hover_text=textwrap.dedent( + """\ + **Special-purpose only**. *This field is a special purpose field and is rarely needed.* + *You are recommended to omit unless you know you need it or someone told you to use it.* + + Determines the type of package. This field can be used to declare that a given package is a different + type of package than usual. The primary case where this is known to be useful is for building + micro-debs ("udeb") to be consumed by the debian-installer. + """ + ), + ), + DctrlKnownField( + "Architecture", + FieldValueClass.SPACE_SEPARATED_LIST, + missing_field_severity=DiagnosticSeverity.Error, + unknown_value_diagnostic_severity=None, + known_values=_allowed_values(*dpkg_arch_and_wildcards()), + hover_text=textwrap.dedent( + """\ + Determines which architectures this package can be compiled for or if it is an architecture-independent + package. The value is a space-separated list of dpkg architecture names or wildcards. + + **Example**: + ``` + Package: architecture-specific-package + Architecture: any + # ... + + + Package: data-only-package + Architecture: all + Multi-Arch: foreign + # ... + + + Package: linux-only-package + Architecture: linux-any + # ... + ``` + + When in doubt, stick to the values **all** (for scripts, data or documentation, etc.) or **any** + (for anything that can be compiled). For official Debian packages, it is often easier to attempt the + compilation for unsupported architectures than to maintain the list of machine architectures that work. + """ + ), + ), + DctrlKnownField( + "Essential", + FieldValueClass.SINGLE_VALUE, + default_value="no", + known_values=_allowed_values( + Keyword( + "yes", + hover_text="The package is essential and uninstalling it will completely and utterly break the" + " system beyond repair.", + ), + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The package is a regular package. This is the default and recommended.</p> + + Note that declaring a package to be "Essential: no" is the same as not having the field except omitting + the field wastes fewer bytes on everyone's hard disk. + """ + ), + ), + ), + hover_text=textwrap.dedent( + """\ + **Special-purpose only**. *This field is a special purpose field and is rarely needed.* + *You are recommended to omit unless you know you need it or someone told you to use it.* + + Whether the package should be considered Essential as defined by Debian Policy. + + Essential packages are subject to several distinct but very important rules: + + * Essential packages are considered essential for the system to work. The packaging system + (APT and dpkg) will refuse to uninstall it without some very insisting force options and warnings. + + * Other packages are not required to declare explicit dependencies on essential packages as a + side-effect of the above except as to ensure a that the given essential package is upgraded + to a given minimum version. + + * Once installed, essential packages function must at all time no matter where dpkg is in its + installation or upgrade process. During bootstrapping or installation, this requirement is + relaxed. + """ + ), + ), + DctrlKnownField( + "XB-Important", + FieldValueClass.SINGLE_VALUE, + replaced_by="Protected", + default_value="no", + known_values=_allowed_values( + Keyword( + "yes", + hover_text="The package is protected and attempts to uninstall it will cause strong warnings to the" + " user that they might be breaking the system.", + ), + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The package is a regular package. This is the default and recommended.</p> + + Note that declaring a package to be `XB-Important: no` is the same as not having the field + except omitting the field wastes fewer bytes on everyone's hard-disk. + """ + ), + ), + ), + ), + DctrlKnownField( + "Protected", + FieldValueClass.SINGLE_VALUE, + default_value="no", + known_values=_allowed_values( + Keyword( + "yes", + hover_text="The package is protected and attempts to uninstall it will cause strong warnings to the" + " user that they might be breaking the system.", + ), + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The package is a regular package. This is the default and recommended.</p> + + Note that declaring a package to be `Protected: no` is the same as not having the field + except omitting the field wastes fewer bytes on everyone's hard-disk. + """ + ), + ), + ), + ), + DctrlKnownField( + "Pre-Depends", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + **Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are* + *probably not looking for this field (except to set a **${misc:Pre-Depends}** relation. Incorrect use* + *of this field can cause issues - among other causing issues during upgrades that users cannot work* + *around without passing `--force-*` options to dpkg.* + + This field is like *Depends*, except that is also forces dpkg to complete installation of the packages + named before even starting the installation of the package which declares the pre-dependency. + + **Example**: + ``` + Pre-Depends: ${misc:Pre-Depends} + ``` + + Note this is a very strong dependency and not all packages support being a pre-dependency because it + puts additional requirements on the package being depended on. Use of **${misc:Pre-Depends}** is + pre-approved and recommended. Essential packages are known to support being in **Pre-Depends**. + However, careless use of **Pre-Depends** for essential packages can still cause dependency resolvers + problems. + """ + ), + ), + DctrlKnownField( + "Depends", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Lists the packages that must be installed, before this package is installed. + + **Example**: + ``` + Package: foo + Architecture: any + Depends: ${misc:Depends}, + ${shlibs:Depends}, + libfoo1 (= ${binary:Version}), + foo-data (= ${source:Version}), + ``` + + This field declares an absolute dependency. Before installing the package, **dpkg** will require + all dependencies to be in state `configured` first. Though, if there is a circular dependency between + two or more packages, **dpkg** will break that circle at an arbitrary point where necessary based on + built-in heuristics. + + This field should be used if the depended-on package is required for the depending package to provide a + *significant amount of functionality* or when it is used in the **postinst** or **prerm** maintainer + scripts. + """ + ), + ), + DctrlKnownField( + "Recommends", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Lists the packages that *should* be installed when this package is installed in all but + *unusual installations*.</p> + + **Example**: + ``` + Recommends: foo-optional + ``` + + By default, APT will attempt to install recommends unless they cannot be installed or the user + has configured APT skip recommends. Notably, during automated package builds for the Debian + archive, **Recommends** are **not** installed. + + As implied, the package must have some core functionality that works **without** the + **Recommends** being satisfied as they are not guaranteed to be there. If the package cannot + provide any functionality without a given package, that package should be in **Depends**. + """ + ), + ), + DctrlKnownField( + "Suggests", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Lists the packages that may make this package more useful but not installing them is perfectly + reasonable as well. Suggests can also be useful for add-ons that only make sense in particular + corner cases like supporting a non-standard file format. + + **Example**: + ``` + Suggests: bar + ``` + """ + ), + ), + DctrlKnownField( + "Enhances", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + This field is similar to Suggests but works in the opposite direction. It is used to declare that + this package can enhance the functionality of another package. + + **Example**: + ``` + Package: foo + Provide: debputy-plugin-foo + Enhances: debputy + ``` + """ + ), + ), + DctrlKnownField( + "Provides", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + Declare this package also provide one or more other packages. This means that this package can + substitute for the provided package in some relations. + + *Example*: + ``` + Package: foo + ... + + Package: foo-plus + Provides: foo (= ${source:Upstream-Version}) + ``` + + If the provides relation is versioned, it must use a "strictly equals" version. If it does not + declare a version, then it *cannot* be used to satisfy a dependency with a version restriction. + Consider the following example: + + **Archive scenario*: (This is *not* a debian/control file, despite the resemblance) + ``` + Package foo + Depends: bar (>= 1.0) + + Package: bar + Version: 0.9 + + Package: bar-plus + Provides: bar (= 1.0) + + Package: bar-clone + Provides: bar + ``` + + In this archive scenario, the `bar-plus` package will satisfy the dependency of `foo` as the + only one. The `bar` package fails because the version is only *0.9* and `bar-clone` because + the provides is unversioned, but the dependency clause is versioned. + """ + ), + ), + DctrlKnownField( + "Conflicts", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + **Warning**: *You may be looking for Breaks instead of Conflicts*. + + This package cannot be installed together with the packages listed in the Conflicts field. This + is a *bigger hammer* than **Breaks** and is used sparingly. Notably, if you want to do a versioned + **Conflicts** then you *almost certainly* want **Breaks** instead. + + **Example**: + ``` + Conflicts: bar + ``` + + Please check the description of the **Breaks** field for when you would use **Breaks** vs. + **Conflicts**. + + Note if a package conflicts with itself (indirectly or via **Provides**), then it is using a + special rule for **Conflicts**. See section + 7.6.2 "[Replacing whole packages, forcing their removal]" in the Debian Policy Manual. + + [Replacing whole packages, forcing their removal]: https://www.debian.org/doc/debian-policy/ch-relationships.html#replacing-whole-packages-forcing-their-removal + """ + ), + ), + DctrlKnownField( + "Breaks", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + This package cannot be installed together with the packages listed in the `Breaks` field. + + This is often use to declare versioned issues such as "This package does not work with foo if + it is version 1.0 or less". In comparison, `Conflicts` is generally used to declare that + "This package does not work at all as long as foo is installed". + + **Example**: + ``` + Breaks: bar (<= 1.0~) + ```` + + **Breaks vs. Conflicts**: + + * I moved files from **foo** to **bar** in version X, what should I do? + + Add `Breaks: foo (<< X~)` + `Replaces: foo (<< X~)` to **bar** + + * Upgrading **bar** while **foo** is version X or less causes problems **foo** or **bar** to break. + How do I solve this? + + Add `Breaks: foo (<< X~)` to **bar** + + * The **foo** and **bar** packages provide the same functionality (interface) but different + implementations and there can be at most one of them. What should I do? + + See section 7.6.2 [Replacing whole packages, forcing their removal] in the Debian Policy Manual. + + * How to handle when **foo** and **bar** packages are unrelated but happen to provide the same binary? + + Attempt to resolve the name conflict by renaming the clashing files in question on either (or both) sides. + + Note the use of *~* in version numbers in the answers are generally used to ensure this works correctly in + case of a backports (in the Debian archive), where the package is rebuilt with the "~bpo" suffix in its + version. + + [Replacing whole packages, forcing their removal]: https://www.debian.org/doc/debian-policy/ch-relationships.html#replacing-whole-packages-forcing-their-removal + """ + ), + ), + DctrlKnownField( + "Replaces", + FieldValueClass.COMMA_SEPARATED_LIST, + hover_text=textwrap.dedent( + """\ + This package either replaces another package or overwrites files that used to be provided by + another package. + + **Attention**: The `Replaces` field is **always** used with either `Breaks` or `Conflicts` field. + + **Example**: + ``` + Package: foo + ... + + # The foo package was split to move data files into foo-data in version 1.2-3 + Package: foo-data + Replaces: foo (<< 1.2-3~) + Breaks: foo (<< 1.2-3~) + ``` + + Please check the description of the `Breaks` field for when you would use `Breaks` vs. `Conflicts`. + It also covers common uses of `Replaces`. + """ + ), + ), + DctrlKnownField( + "Build-Profiles", + FieldValueClass.BUILD_PROFILES_LIST, + hover_text=textwrap.dedent( + """\ + **Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are* + *advised to leave it at its default until you have a working basic package or lots of time to understand* + *this topic.* + + Declare that the package will only built when the given build-profiles are satisfied. + + This field is primarily used in combination with build profiles inside the build dependency related fields + to reduce the number of build dependencies required during bootstrapping of a new architecture. + + **Example*: + ``` + Package: foo + ... + + Package: foo-udeb + Package-Type: udeb + # Skip building foo-udeb when the build profile "noudeb" is set (e.g., via dpkg-buildpackage -Pnoudeb) + Build-Profiles: <!noudeb> + ``` + + Note that there is an official list of "common" build profiles with predefined purposes along with rules + for how and when the can be used. This list can be found at + https://wiki.debian.org/BuildProfileSpec#Registered_profile_names. + """ + ), + ), + DctrlKnownField( + "Section", + FieldValueClass.SINGLE_VALUE, + missing_field_severity=DiagnosticSeverity.Error, + inherits_from_source=True, + known_values=ALL_SECTIONS, + unknown_value_diagnostic_severity=DiagnosticSeverity.Warning, + hover_text=textwrap.dedent( + """\ + Define the section for this package. + + Example: + ``` + Section: devel + ``` + + Please see https://packages.debian.org/unstable for more details about the sections. + """ + ), + ), + DctrlKnownField( + "Priority", + FieldValueClass.SINGLE_VALUE, + default_value="optional", + warn_if_default=False, + missing_field_severity=DiagnosticSeverity.Error, + inherits_from_source=True, + known_values=_allowed_values( + Keyword( + "required", + hover_text=textwrap.dedent( + """\ + The package is necessary for the proper functioning of the system (read: dpkg needs it). + + Applicable if dpkg *needs* this package to function and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "important", + hover_text=textwrap.dedent( + """\ + The *important* packages are a bare minimum of commonly-expected and necessary tools. + + Applicable if 99% of all users in the distribution needs this package and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "standard", + hover_text=textwrap.dedent( + """\ + These packages provide a reasonable small but not too limited character-mode system. This is + what will be installed by default (by the debian-installer) if the user does not select anything + else. This does not include many large applications. + + Applicable if your distribution installer will install this package by default on a new system + and it is not a library. + + No two packages that both have a priority of *standard* or higher may conflict with + each other. + """ + ), + ), + Keyword( + "optional", + hover_text="This is the default priority and used by the majority of all packages" + " in the Debian archive", + ), + Keyword( + "extra", + is_obsolete=True, + replaced_by="optional", + hover_text="Obsolete alias of `optional`.", + ), + ), + hover_text=textwrap.dedent( + """\ + Define the priority this package. + + The priority field describes how important the package is for the functionality of the system. + + Example: + ``` + Priority: optional + ``` + + Unless you know you need a different value, you should choose <b>optional</b> for your packages. + """ + ), + ), + DctrlKnownField( + "Multi-Arch", + FieldValueClass.SINGLE_VALUE, + # Explicit "no" tends to be used as "someone reviewed this and concluded no", so we do + # not warn about it being explicitly "no". + warn_if_default=False, + default_value="no", + known_values=_allowed_values( + Keyword( + "no", + hover_text=textwrap.dedent( + """\ + The default. The package can be installed for at most one architecture at the time. It can + *only* satisfy relations for the same architecture as itself. Note that `Architecture: all` + packages are considered as a part of the system's "primary" architecture (see output of + `dpkg --print-architecture`). + + Note: Despite the "no", the package *can* be installed for a foreign architecture (as an example, + you can install a 32-bit version of a package on a 64-bit system). However, packages depending + on it must also be installed for the foreign architecture. + """ + ), + ), + Keyword( + "foreign", + hover_text=textwrap.dedent( + """\ + The package can be installed for at most one architecture at the time. However, it can + satisfy relations for packages regardless of their architecture. This is often useful for packages + solely providing data or binaries that have "Multi-Arch neutral interfaces". + + Sadly, describing a "Multi-Arch neutral interface" is hard and often only done by Multi-Arch + experts on a case-by-case basis. Some programs and scripts have "Multi-Arch dependent interfaces" + and are not safe to declare as `Multi-Arch: foreign`. + + The name "foreign" refers to the fact that the package can satisfy relations for native + *and foreign* architectures at the same time. + """ + ), + ), + Keyword( + "same", + hover_text=textwrap.dedent( + """\ + The same version of the package can be co-installed for multiple architecture. However, + for this to work, the package *must* ship all files in architecture unique paths (usually + beneath `/usr/lib/<DEB_HOST_MULTIARCH>`) or have bit-for-bit identical content + in files that are in non-architecture unique paths (such as files beneath `/usr/share/doc`). + + The name `same` refers to the fact that the package can satisfy relations only for the `same` + architecture as itself. However, in this case, it is co-installable with itself as noted above. + Note: This value **cannot** be used with `Architecture: all`. + """ + ), + ), + Keyword( + "allowed", + hover_text=textwrap.dedent( + """\ + **Advanced value**. The package is *not* co-installable with itself but can satisfy Multi-Arch + foreign and Multi-Arch same relations at the same. This is useful for implementations of + scripting languages (such as Perl or Python). Here the interpreter contextually need to + satisfy some relations as `Multi-Arch: foreign` and others as `Multi-Arch: same`. + + Typically, native extensions or plugins will need a `Multi-Arch: same`-relation as they only + work with the interpreter compiled for the same machine architecture as themselves whereas + scripts are usually less picky and can rely on the `Multi-Arch: foreign` relation. Packages + wanting to rely on the "Multi-Arch: foreign" interface must explicitly declare this adding a + `:any` suffix to the package name in the dependency relation (e.g. `Depends: python3:any`). + However, the `:any"`suffix cannot be used unconditionally and should not be used unless you + know you need it. + """ + ), + ), + ), + hover_text=textwrap.dedent( + """\ + **Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are* + *advised to leave it at its default until you have a working basic package or lots of time to understand* + *this topic.* + + This field is used to declare the Multi-Arch interface of the package. + + The `Multi-Arch` field is used to inform the installation system (APT and dpkg) about how it should handle + dependency relations involving this package and foreign architectures. This is useful for multiple purposes + such as cross-building without emulation and installing 32-bit packages on a 64-bit system. The latter is + often done to use legacy apps or old games that was never ported to 64-bit machines. + + **Example**: + ``` + Multi-Arch: foreign + ``` + + The rules for `Multi-Arch` can be quite complicated, but in many cases the following simple rules of thumb + gets you a long way: + + * If the [Multi-Arch hinter] comes with a hint, then it almost certainly correct. You are recommended + to check the hint for further details (some changes can be complicated to do). Note that the + Multi-Arch hinter is only run for official Debian packages and may not be applicable to your case. + + * If you have an `Architecture: all` data-only package, then it often want to be `Multi-Arch: foreign` + + * If you have an architecture dependent package, where everything is installed in + `/usr/lib/${DEB_HOST_MULTIARCH}` (plus a bit of standard documentation in `/usr/share/doc`), then + you *probably* want `Multi-Arch: same` + + * If none of the above applies, then omit the field unless you know what you are doing or you are + receiving advice from a Multi-Arch expert. + + + There are 4 possible values for the Multi-Arch field, though not all values are applicable to all packages: + + + * `no` - The default. The package can be installed for at most one architecture at the time. It can + *only* satisfy relations for the same architecture as itself. Note that `Architecture: all` packages + are considered as a part of the system's "primary" architecture (see output of `dpkg --print-architecture`). + + Use of an explicit `no` over omitting the field is commonly done to signal that someone took the + effort to understand the situation and concluded `no` was the right answer. + + Note: Despite the `no`, the package *can* be installed for a foreign architecture (e.g. you can + install a 32-bit version of a package on a 64-bit system). However, packages depending on it must also + be installed for the foreign architecture. + + + * `foreign` - The package can be installed for at most one architecture at the time. However, it can + satisfy relations for packages regardless of their architecture. This is often useful for packages + solely providing data or binaries that have "Multi-Arch neutral interfaces". Sadly, describing + a "Multi-Arch neutral interface" is hard and often only done by Multi-Arch experts on a case-by-case + basis. Among other, scripts despite being the same on all architectures can still have a "non-neutral" + "Multi-Arch" interface if their output is architecture dependent or if they dependencies force them + out of the `foreign` role. The dependency issue usually happens when depending indirectly on an + `Multi-Arch: allowed` package. + + Some programs are have "Multi-Arch dependent interfaces" and are not safe to declare as + `Multi-Arch: foreign`. The name `foreign` refers to the fact that the package can satisfy relations + for native *and foreign* architectures at the same time. + + + * `same` - The same version of the package can be co-installed for multiple architecture. However, + for this to work, the package **must** ship all files in architecture unique paths (usually + beneath `/usr/lib/${DEB_HOST_MULTIARCH}`) **or** have bit-for-bit identical content in files + that are in non-architecture unique paths (e.g. `/usr/share/doc`). Note that these packages + typically do not contain configuration files or **dpkg** `conffile`s. + + The name `same` refers to the fact that the package can satisfy relations only for the "same" + architecture as itself. However, in this case, it is co-installable with itself as noted above. + + Note: This value **cannot** be used with `Architecture: all`. + + + * `allowed` - **Advanced value**. This value is for a complex use-case that most people does not + need. Consider it only if none of the other values seem to do the trick. + + The package is **NOT** co-installable with itself but can satisfy Multi-Arch foreign and Multi-Arch same + relations at the same. This is useful for implementations of scripting languages (e.g. Perl or Python). + Here the interpreter contextually need to satisfy some relations as `Multi-Arch: foreign` and others as + `Multi-Arch: same` (or `Multi-Arch: no`). + + Typically, native extensions or plugins will need a `Multi-Arch: same`-relation as they only work with + the interpreter compiled for the same machine architecture as themselves whereas scripts are usually + less picky and can rely on the `Multi-Arch: foreign` relation. Packages wanting to rely on the + `Multi-Arch: foreign` interface must explicitly declare this adding a `:any` suffix to the package name + in the dependency relation (such as `Depends: python3:any`). However, the `:any` suffix cannot be used + unconditionally and should not be used unless you know you need it. + + Note that depending indirectly on a `Multi-Arch: allowed` package can require a `Architecture: all` + + `Multi-Arch: foreign` package to be converted to a `Architecture: any` package. This case is named + the "Multi-Arch interpreter problem", since it is commonly seen with script interpreters. However, + despite the name, it can happen to any kind of package. The bug [Debian#984701] is an example of + this happen in practice. + + [Multi-Arch hinter]: https://wiki.debian.org/MultiArch/Hints + [Debian#984701]: https://bugs.debian.org/984701 + """ + ), + ), + DctrlKnownField( + "X-DH-Build-For-Type", + FieldValueClass.SINGLE_VALUE, + default_value="host", + known_values=_allowed_values( + Keyword( + "host", + hover_text="The package should be compiled for `DEB_HOST_TARGET` (the default).", + ), + Keyword( + "target", + hover_text="The package should be compiled for `DEB_TARGET_ARCH`.", + ), + ), + hover_text=textwrap.dedent( + """\ + **Special-purpose only**. *This field is a special purpose field and is rarely needed.* + *You are recommended to omit unless you know you need it or someone told you to use it.* + + This field is used when building a cross-compiling C-compiler (or similar cases), some packages need + to be build for target (DEB_**TARGET**_ARCH) rather than the host (DEB_**HOST**_ARCH) architecture. + + **Example**: + ``` + Package: gcc + Architecture: any + # ... + + Package: libgcc-s1 + Architecture: any + # When building a cross-compiling gcc, then this library needs to be built for the target architecture + # as binaries compiled by gcc will link with this library. + X-DH-Build-For-Type: target + # ... + ``` + + If you are in doubt, then you probably do **not** need this field. + """ + ), + ), + DctrlKnownField( + "X-Time64-Compat", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Special purpose field renamed to the 64-bit time transition. + + It is used to inform packaging helpers what the original (non-transitioned) package name + was when the auto-detection is inadequate. The non-transitioned package name is then + conditionally provided in the `${t64:Provides}` substitution variable. + """ + ), + ), + DctrlKnownField( + "Homepage", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Link to the upstream homepage for this binary package. + + This field is rarely used in Package stanzas as most binary packages should have the + same homepage as the source package. Though, in the exceptional case where a particular + binary package should have a more specific homepage than the source package, you can + use this field to override the source package field. + ``` + """ + ), + ), + DctrlKnownField( + "Description", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + # It will build just fine. But no one will know what it is for, so it probably won't be installed + missing_field_severity=DiagnosticSeverity.Warning, + hover_text=textwrap.dedent( + """\ + A human-readable description of the package. This field consists of two related but distinct parts. + + + The first line immediately after the field is called the *Synopsis* and is a short "noun-phrase" + intended to provide a one-line summary of the package. The lines after the **Synopsis** is known + as the **Extended Description** and is intended as a longer summary of the package. + + **Example:** + ``` + Description: documentation generator for Python projects + Sphinx is a tool for producing documentation for Python projects, using + reStructuredText as markup language. + . + Sphinx features: + * HTML, CHM, LaTeX output, + * Cross-referencing source code, + * Automatic indices, + * Code highlighting, using Pygments, + * Extensibility. Existing extensions: + - automatic testing of code snippets, + - including docstrings from Python modules. + . + Build-depend on sphinx if your package uses /usr/bin/sphinx-* + executables. Build-depend on python3-sphinx if your package uses + the Python API (for instance by calling python3 -m sphinx). + ``` + + The **Synopsis** is usually displayed in cases where there is limited space such as when reviewing + the search results from `apt search foo`. It is often a good idea to imagine that the **Synopsis** + part is inserted into a sentence like "The package provides {{Synopsis-goes-here}}". The + **Extended Description** is a standalone description that should describe what the package does and + how it relates to the rest of the system (in terms of, for example, which subsystem it is which part of). + Please see https://www.debian.org/doc/debian-policy/ch-controlfields.html#description for more details + about the description field and suggestions for how to write it. + """ + ), + ), + DctrlKnownField( + "XB-Cnf-Visible-Pkgname", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + **Special-case field**: *This field is only useful in very special circumstances.* + *Consider whether you truly need it before adding this field.* + + This field is used by `command-not-found` and can be used to override which package + `command-not-found` should propose the user to install. + + Normally, when `command-not-found` detects a missing command, it will suggest the + user to install the package name listed in the `Package` field. In most cases, this + is what you want. However, in certain special-cases, the binary is provided by a + minimal package for technical reasons (like `python3-minimal`) and the user should + really install a package that provides more features (such as `python3` to follow + the example). + + **Example**: + ``` + Package: python3-minimal + XB-Cnf-Visible-Pkgname: python3 + ``` + + Related bug: https://bugs.launchpad.net/ubuntu/+source/python-defaults/+bug/1867157 + """ + ), + ), + DctrlKnownField( + "X-DhRuby-Root", + FieldValueClass.SINGLE_VALUE, + hover_text=textwrap.dedent( + """\ + Used by `dh_ruby` to request "multi-binary" layout and where the root for the given + package is. + + Please refer to the documentation of `dh_ruby` for more details. + + https://manpages.debian.org/dh_ruby + """ + ), + ), +) +_DEP5_HEADER_FIELDS = _fields( + Deb822KnownField( + "Format", + FieldValueClass.SINGLE_VALUE, + is_stanza_name=True, + missing_field_severity=DiagnosticSeverity.Error, + ), + Deb822KnownField( + "Upstream-Name", + FieldValueClass.FREE_TEXT_FIELD, + ), + Deb822KnownField( + "Upstream-Contract", + FieldValueClass.FREE_TEXT_FIELD, + ), + Deb822KnownField( + "Source", + FieldValueClass.FREE_TEXT_FIELD, + ), + Deb822KnownField( + "Disclaimer", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + ), + Deb822KnownField( + "Comment", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + ), + Deb822KnownField( + "License", + FieldValueClass.FREE_TEXT_FIELD, + # Do not tempt people to change legal text because the spellchecker wants to do a typo fix. + spellcheck_value=False, + ), +) +_DEP5_FILES_FIELDS = _fields( + Deb822KnownField( + "Files", + FieldValueClass.DEP5_FILE_LIST, + is_stanza_name=True, + missing_field_severity=DiagnosticSeverity.Error, + ), + Deb822KnownField( + "Copyright", + FieldValueClass.FREE_TEXT_FIELD, + # Mostly going to be names with very little free-text; high risk of false positives with low value + spellcheck_value=False, + missing_field_severity=DiagnosticSeverity.Error, + ), + Deb822KnownField( + "License", + FieldValueClass.FREE_TEXT_FIELD, + missing_field_severity=DiagnosticSeverity.Error, + # Do not tempt people to change legal text because the spellchecker wants to do a typo fix. + spellcheck_value=False, + ), + Deb822KnownField( + "Comment", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + ), +) +_DEP5_LICENSE_FIELDS = _fields( + Deb822KnownField( + "License", + FieldValueClass.FREE_TEXT_FIELD, + is_stanza_name=True, + # Do not tempt people to change legal text because the spellchecker wants to do a typo fix. + spellcheck_value=False, + missing_field_severity=DiagnosticSeverity.Error, + ), + Deb822KnownField( + "Comment", + FieldValueClass.FREE_TEXT_FIELD, + spellcheck_value=True, + ), +) + + +@dataclasses.dataclass(slots=True, frozen=True) +class StanzaMetadata(Mapping[str, F], Generic[F], ABC): + stanza_type_name: str + stanza_fields: Mapping[str, F] + + def stanza_diagnostics( + self, + stanza: Deb822ParagraphElement, + stanza_position_in_file: "TEPosition", + ) -> Iterable[Diagnostic]: + raise NotImplementedError + + def __getitem__(self, key: str) -> F: + key_lc = key.lower() + key_norm = normalize_dctrl_field_name(key_lc) + return self.stanza_fields[key_norm] + + def __len__(self) -> int: + return len(self.stanza_fields) + + def __iter__(self): + return iter(self.stanza_fields.keys()) + + +@dataclasses.dataclass(slots=True, frozen=True) +class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): + def stanza_diagnostics( + self, + stanza: Deb822ParagraphElement, + stanza_position_in_file: "TEPosition", + ) -> Iterable[Diagnostic]: + pass + + +@dataclasses.dataclass(slots=True, frozen=True) +class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): + + def stanza_diagnostics( + self, + stanza: Deb822ParagraphElement, + stanza_position_in_file: "TEPosition", + ) -> Iterable[Diagnostic]: + pass + + +class Deb822FileMetadata(Generic[S]): + def classify_stanza(self, stanza: Deb822ParagraphElement, stanza_idx: int) -> S: + return self.guess_stanza_classification_by_idx(stanza_idx) + + def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S: + raise NotImplementedError + + def stanza_types(self) -> Iterable[S]: + raise NotImplementedError + + def __getitem__(self, item: str) -> S: + raise NotImplementedError + + def file_diagnostics( + self, + file: Deb822FileElement, + ) -> Iterable[Diagnostic]: + raise NotImplementedError + + def get(self, item: str) -> Optional[S]: + try: + return self[item] + except KeyError: + return None + + +_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( + "Source", + SOURCE_FIELDS, +) +_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata("Package", BINARY_FIELDS) + +_DEP5_HEADER_STANZA = Dep5StanzaMetadata( + "Header", + _DEP5_HEADER_FIELDS, +) +_DEP5_FILES_STANZA = Dep5StanzaMetadata( + "Files", + _DEP5_FILES_FIELDS, +) +_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( + "License", + _DEP5_LICENSE_FIELDS, +) + + +class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata]): + def classify_stanza(self, stanza: Deb822ParagraphElement, stanza_idx: int) -> S: + if stanza_idx == 0: + return _DEP5_HEADER_STANZA + if stanza_idx > 0: + if "Files" in stanza: + return _DEP5_FILES_STANZA + return _DEP5_LICENSE_STANZA + raise ValueError("The stanza_idx must be 0 or greater") + + def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S: + if stanza_idx == 0: + return _DEP5_HEADER_STANZA + if stanza_idx > 0: + return _DEP5_FILES_STANZA + raise ValueError("The stanza_idx must be 0 or greater") + + def stanza_types(self) -> Iterable[S]: + yield _DEP5_HEADER_STANZA + yield _DEP5_FILES_STANZA + yield _DEP5_LICENSE_STANZA + + def __getitem__(self, item: str) -> S: + if item == "Header": + return _DEP5_FILES_STANZA + if item == "Files": + return _DEP5_FILES_STANZA + if item == "License": + return _DEP5_LICENSE_STANZA + raise KeyError(item) + + +class DctrlFileMetadata(Deb822FileMetadata[DctrlStanzaMetadata]): + def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S: + if stanza_idx == 0: + return _DCTRL_SOURCE_STANZA + if stanza_idx > 0: + return _DCTRL_PACKAGE_STANZA + raise ValueError("The stanza_idx must be 0 or greater") + + def stanza_types(self) -> Iterable[S]: + yield _DCTRL_SOURCE_STANZA + yield _DCTRL_PACKAGE_STANZA + + def __getitem__(self, item: str) -> S: + if item == "Source": + return _DCTRL_SOURCE_STANZA + if item == "Package": + return _DCTRL_PACKAGE_STANZA + raise KeyError(item) |