Coverage for src/debputy/plugin/debputy/manifest_root_rules.py: 79%
57 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
1import textwrap
2from typing import List, Any, Dict, Tuple, TYPE_CHECKING, cast
4from debputy._manifest_constants import (
5 ManifestVersion,
6 MK_MANIFEST_VERSION,
7 MK_INSTALLATIONS,
8 SUPPORTED_MANIFEST_VERSIONS,
9 MK_MANIFEST_DEFINITIONS,
10 MK_PACKAGES,
11 MK_MANIFEST_VARIABLES,
12)
13from debputy.exceptions import DebputySubstitutionError
14from debputy.installations import InstallRule
15from debputy.manifest_parser.base_types import DebputyParsedContent
16from debputy.manifest_parser.exceptions import ManifestParseException
17from debputy.manifest_parser.parser_data import ParserContextData
18from debputy.manifest_parser.util import AttributePath
19from debputy.plugin.api import reference_documentation
20from debputy.plugin.api.impl import DebputyPluginInitializerProvider
21from debputy.plugin.api.impl_types import (
22 OPARSER_MANIFEST_ROOT,
23 OPARSER_MANIFEST_DEFINITIONS,
24 SUPPORTED_DISPATCHABLE_OBJECT_PARSERS,
25 OPARSER_PACKAGES,
26)
27from debputy.substitution import VariableNameState, SUBST_VAR_RE
29if TYPE_CHECKING:
30 from debputy.highlevel_manifest_parser import YAMLManifestParser
33def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None:
34 # Registration order matters. Notably, definitions must come before anything that can
35 # use definitions (variables), which is why it is second only to the manifest version.
36 api.pluggable_manifest_rule(
37 OPARSER_MANIFEST_ROOT,
38 MK_MANIFEST_VERSION,
39 ManifestVersionFormat,
40 _handle_version,
41 source_format=ManifestVersion,
42 inline_reference_documentation=reference_documentation(
43 title="Manifest version",
44 description=textwrap.dedent(
45 """\
46 All `debputy` manifests must include a `debputy` manifest version, which will enable the
47 format to change over time. For now, there is only one version (`"0.1"`) and you have
48 to include the line:
50 manifest-version: "0.1"
52 On its own, the manifest containing only `manifest-version: "..."` will not do anything. So if you
53 end up only having the `manifest-version` key in the manifest, you can just remove the manifest and
54 rely entirely on the built-in rules.
55 """
56 ),
57 ),
58 )
59 api.pluggable_object_parser(
60 OPARSER_MANIFEST_ROOT,
61 MK_MANIFEST_DEFINITIONS,
62 object_parser_key=OPARSER_MANIFEST_DEFINITIONS,
63 on_end_parse_step=lambda _a, _b, _c, mp: mp._ensure_package_states_is_initialized(),
64 )
65 api.pluggable_manifest_rule(
66 OPARSER_MANIFEST_DEFINITIONS,
67 MK_MANIFEST_VARIABLES,
68 ManifestVariablesParsedFormat,
69 _handle_manifest_variables,
70 source_format=Dict[str, str],
71 inline_reference_documentation=reference_documentation(
72 title="Manifest Variables (`variables`)",
73 description=textwrap.dedent(
74 """\
75 It is possible to provide custom manifest variables via the `variables` attribute. An example:
77 manifest-version: '0.1'
78 definitions:
79 variables:
80 LIBPATH: "/usr/lib/{{DEB_HOST_MULTIARCH}}"
81 SONAME: "1"
82 installations:
83 - install:
84 source: build/libfoo.so.{{SONAME}}*
85 # The quotes here is for the YAML parser's sake.
86 dest-dir: "{{LIBPATH}}"
87 into: libfoo{{SONAME}}
89 The value of the `variables` key must be a mapping, where each key is a new variable name and
90 the related value is the value of said key. The keys must be valid variable name and not shadow
91 existing variables (that is, variables such as `PACKAGE` and `DEB_HOST_MULTIARCH` *cannot* be
92 redefined). The value for each variable *can* refer to *existing* variables as seen in the
93 example above.
95 As usual, `debputy` will insist that all declared variables must be used.
97 Limitations:
98 * When declaring variables that depends on another variable declared in the manifest, the
99 order is important. The variables are resolved from top to bottom.
100 * When a manifest variable depends on another manifest variable, the existing variable is
101 currently always resolved in source context. As a consequence, some variables such as
102 `{{PACKAGE}}` cannot be used when defining a variable. This restriction may be
103 lifted in the future.
104 """
105 ),
106 ),
107 )
108 api.pluggable_manifest_rule(
109 OPARSER_MANIFEST_ROOT,
110 MK_INSTALLATIONS,
111 List[InstallRule],
112 _handle_installation_rules,
113 inline_reference_documentation=reference_documentation(
114 title="Installations",
115 description=textwrap.dedent(
116 """\
117 For source packages building a single binary, the `dh_auto_install` from debhelper will default to
118 providing everything from upstream's install in the binary package. The `debputy` tool matches this
119 behaviour and accordingly, the `installations` feature is only relevant in this case when you need to
120 manually specify something upstream's install did not cover.
122 For sources, that build multiple binaries, where `dh_auto_install` does not detect anything to install,
123 or when `dh_auto_install --destdir debian/tmp` is used, the `installations` section of the manifest is
124 used to declare what goes into which binary package. An example:
126 installations:
127 - install:
128 sources: "usr/bin/foo"
129 into: foo
130 - install:
131 sources: "usr/*"
132 into: foo-extra
134 All installation rules are processed in order (top to bottom). Once a path has been matched, it can
135 no longer be matched by future rules. In the above example, then `usr/bin/foo` would be in the `foo`
136 package while everything in `usr` *except* `usr/bin/foo` would be in `foo-extra`. If these had been
137 ordered in reverse, the `usr/bin/foo` rule would not have matched anything and caused `debputy`
138 to reject the input as an error on that basis. This behaviour is similar to "DEP-5" copyright files,
139 except the order is reversed ("DEP-5" uses "last match wins", where here we are doing "first match wins")
141 In the rare case that some path need to be installed into two packages at the same time, then this is
142 generally done by changing `into` into a list of packages.
144 All installations are currently run in *source* package context. This implies that:
146 1) No package specific substitutions are available. Notably `{{PACKAGE}}` cannot be resolved.
147 2) All conditions are evaluated in source context. For 99.9% of users, this makes no difference,
148 but there is a cross-build feature that changes the "per package" architecture which is affected.
150 This is a limitation that should be fixed in `debputy`.
152 **Attention debhelper users**: Note the difference between `dh_install` (etc.) vs. `debputy` on
153 overlapping matches for installation.
154 """
155 ),
156 ),
157 )
158 api.pluggable_object_parser(
159 OPARSER_MANIFEST_ROOT,
160 MK_PACKAGES,
161 object_parser_key=OPARSER_PACKAGES,
162 on_end_parse_step=lambda _a, _b, _c, mp: mp._ensure_package_states_is_initialized(),
163 nested_in_package_context=True,
164 )
167class ManifestVersionFormat(DebputyParsedContent):
168 manifest_version: ManifestVersion
171class ListOfInstallRulesFormat(DebputyParsedContent):
172 elements: List[InstallRule]
175class DictFormat(DebputyParsedContent):
176 mapping: Dict[str, Any]
179class ManifestVariablesParsedFormat(DebputyParsedContent):
180 variables: Dict[str, str]
183def _handle_version(
184 _name: str,
185 parsed_data: ManifestVersionFormat,
186 _attribute_path: AttributePath,
187 _parser_context: ParserContextData,
188) -> str:
189 manifest_version = parsed_data["manifest_version"]
190 if manifest_version not in SUPPORTED_MANIFEST_VERSIONS: 190 ↛ 191line 190 didn't jump to line 191, because the condition on line 190 was never true
191 raise ManifestParseException(
192 "Unsupported manifest-version. This implementation supports the following versions:"
193 f' {", ".join(repr(v) for v in SUPPORTED_MANIFEST_VERSIONS)}"'
194 )
195 return manifest_version
198def _handle_manifest_variables(
199 _name: str,
200 parsed_data: ManifestVariablesParsedFormat,
201 variables_path: AttributePath,
202 parser_context: ParserContextData,
203) -> None:
204 variables = parsed_data.get("variables", {})
205 resolved_vars: Dict[str, Tuple[str, AttributePath]] = {}
206 manifest_parser: "YAMLManifestParser" = cast("YAMLManifestParser", parser_context)
207 substitution = manifest_parser.substitution
208 for key, value_raw in variables.items():
209 key_path = variables_path[key]
210 if not SUBST_VAR_RE.match("{{" + key + "}}"): 210 ↛ 211line 210 didn't jump to line 211, because the condition on line 210 was never true
211 raise ManifestParseException(
212 f"The variable at {key_path.path} has an invalid name and therefore cannot"
213 " be used."
214 )
215 if substitution.variable_state(key) != VariableNameState.UNDEFINED:
216 raise ManifestParseException(
217 f'The variable "{key}" is already reserved/defined. Error triggered by'
218 f" {key_path.path}."
219 )
220 try:
221 value = substitution.substitute(value_raw, key_path.path)
222 except DebputySubstitutionError:
223 if not resolved_vars:
224 raise
225 # See if flushing the variables work
226 substitution = manifest_parser.add_extra_substitution_variables(
227 **resolved_vars
228 )
229 resolved_vars = {}
230 value = substitution.substitute(value_raw, key_path.path)
231 resolved_vars[key] = (value, key_path)
232 substitution = manifest_parser.add_extra_substitution_variables(**resolved_vars)
235def _handle_installation_rules(
236 _name: str,
237 parsed_data: List[InstallRule],
238 _attribute_path: AttributePath,
239 _parser_context: ParserContextData,
240) -> List[Any]:
241 return parsed_data
244def _handle_opaque_dict(
245 _name: str,
246 parsed_data: DictFormat,
247 _attribute_path: AttributePath,
248 _parser_context: ParserContextData,
249) -> Dict[str, Any]:
250 return parsed_data["mapping"]