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

1import textwrap 

2from typing import List, Any, Dict, Tuple, TYPE_CHECKING, cast 

3 

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 

28 

29if TYPE_CHECKING: 

30 from debputy.highlevel_manifest_parser import YAMLManifestParser 

31 

32 

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: 

49 

50 manifest-version: "0.1" 

51 

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: 

76 

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}} 

88 

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. 

94 

95 As usual, `debputy` will insist that all declared variables must be used. 

96 

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. 

121 

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: 

125 

126 installations: 

127 - install: 

128 sources: "usr/bin/foo" 

129 into: foo 

130 - install: 

131 sources: "usr/*" 

132 into: foo-extra 

133 

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") 

140 

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. 

143 

144 All installations are currently run in *source* package context. This implies that: 

145 

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. 

149 

150 This is a limitation that should be fixed in `debputy`. 

151 

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 ) 

165 

166 

167class ManifestVersionFormat(DebputyParsedContent): 

168 manifest_version: ManifestVersion 

169 

170 

171class ListOfInstallRulesFormat(DebputyParsedContent): 

172 elements: List[InstallRule] 

173 

174 

175class DictFormat(DebputyParsedContent): 

176 mapping: Dict[str, Any] 

177 

178 

179class ManifestVariablesParsedFormat(DebputyParsedContent): 

180 variables: Dict[str, str] 

181 

182 

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 

196 

197 

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) 

233 

234 

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 

242 

243 

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"]