Coverage for debputy/manifest_path_rules.py: 0%
152 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-22 14:29 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-22 14:29 +0100
1import dataclasses
2import stat
3from enum import Enum
4from typing import Optional, Iterable, Set
6from debputy._deb_options_profiles import DebOptionsAndProfiles
7from debputy.filesystem_scan import FSPath
8from debputy.intermediate_manifest import TarMember, PathType
9from debputy.manifest_conditions import ManifestCondition
10from debputy.packages import BinaryPackage, SourcePackage
11from debputy.path_matcher import MatchRule, ExactFileSystemPath, MatchRuleType
12from debputy.substitution import Substitution
13from debputy.util import debian_policy_normalize_symlink_target, apply_symbolic_mode, _error
16class ManifestPathInfoRuleType(Enum):
17 ENSURE_METADATA = ('ensure-metadata', False, True)
18 EXCLUDE_PATH = ('remove-path', False, False)
19 CREATE_SYMLINK = ('create-symlink', True, False)
20 CREATE_DIRECTORY = ('create-directory', True, True)
22 @property
23 def creates_path(self) -> bool:
24 return self.value[1]
26 @property
27 def can_have_children(self):
28 return self.value[1]
31@dataclasses.dataclass(slots=True, frozen=False)
32class ManifestPathRule:
33 rule_type: ManifestPathInfoRuleType
34 definition_source: str
35 match_rule: MatchRule
36 ensure_path_type: Optional['PathType'] = None
37 symbolic_mode: Optional[str] = None
38 mode: Optional[int] = None
39 dirmode: Optional[int] = None
40 special_mode: Optional[int] = None
41 uid: int = 0
42 uname: str = 'root'
43 gid: int = 0
44 gname: str = 'root'
45 link_target: Optional[str] = None
46 is_implicit_definition: bool = False
47 is_builtin_definition: bool = False
48 triggered_by: Optional['ManifestPathRule'] = None
49 condition: Optional[ManifestCondition] = None
50 _used: bool = False
51 _children: Optional[Set[ExactFileSystemPath]] = None
53 @classmethod
54 def ensure_metadata(cls, definition_source: str, match_rule: MatchRule, **kwargs):
55 if 'match_rule' in kwargs:
56 raise ValueError(f'The match_rule is a positional parameter for ensure_metadata, saw it in the kwargs part')
57 return EnsurePathMetadataPathRule(definition_source, match_rule, **kwargs)
59 @classmethod
60 def exclusion(cls, definition_source: str, match_rule: MatchRule):
61 return ExclusionPathRule(definition_source, match_rule)
63 @classmethod
64 def symlink(cls, definition_source: str, link_path: ExactFileSystemPath, link_target: str,
65 condition: Optional['ManifestPathRule'] = None):
66 return EnsureSymlinkPathRule(
67 definition_source,
68 link_path,
69 link_target,
70 condition=condition,
71 )
73 @classmethod
74 def directory(cls,
75 definition_source: str,
76 directory_path: ExactFileSystemPath,
77 mode=0o755,
78 uid=0,
79 uname='root',
80 gid=0,
81 gname='root',
82 triggered_by: Optional['ManifestPathRule'] = None,
83 dirmode=None,
84 special_mode=None,
85 condition: Optional['ManifestPathRule'] = None,
86 ) -> 'EnsureDirectoryPathRule':
87 is_implicit_definition = True if triggered_by is not None else False
88 if dirmode is not None and dirmode != mode or special_mode is not None and special_mode != mode:
89 raise ValueError('Please pass only "mode" to the directory method')
90 if mode is None:
91 mode = 0o755
92 return EnsureDirectoryPathRule(
93 definition_source,
94 directory_path,
95 mode=mode,
96 uid=uid,
97 uname=uname,
98 gid=gid,
99 gname=gname,
100 is_implicit_definition=is_implicit_definition,
101 triggered_by=triggered_by,
102 condition=condition,
103 )
105 @property
106 def member_path(self) -> Optional[str]:
107 match_rule = self.match_rule
108 if match_rule.rule_type != MatchRuleType.EXACT_MATCH:
109 return None
110 assert match_rule.lookup_key() is not None
111 return match_rule.lookup_key()
113 def add_virtual_child(self, child_path_info: 'ManifestPathRule') -> None:
114 if self.ensure_path_type is None:
115 self.ensure_path_type = PathType.DIRECTORY
116 if self.ensure_path_type != PathType.DIRECTORY or not self.rule_type.can_have_children:
117 assert self.member_path is not None
118 _error(f'Conflicting definition for path "{self.member_path}":\n'
119 f' * The path /must/ be a directory due to "{child_path_info.definition_source}"\n'
120 f' * However, at the same time, the path /cannot/ be a directory due to "{self.definition_source}"\n'
121 f'Please resolve the conflict by removing or changing the path in one of the definitions')
122 child_match_rule = child_path_info.match_rule
123 assert isinstance(child_match_rule, ExactFileSystemPath)
124 if self._children is None:
125 self._children = set()
126 self._children.add(child_match_rule)
128 def children(self) -> Iterable[ExactFileSystemPath]:
129 if self._children:
130 yield from self._children
132 def mark_used(self):
133 self._used = True
135 @property
136 def has_been_used(self):
137 return self._used
139 # EnsureMetadata and EnsureSymlink can both cause a symlink to be created, so we have this in both places.
140 def _create_symlink(self,
141 tar_path: 'FSPath',
142 link_target: str,
143 mtime: int,
144 ) -> TarMember:
145 return TarMember.virtual_path(
146 tar_path.tar_path,
147 PathType.SYMLINK,
148 mtime,
149 link_target=link_target,
150 # Force mode to be 0777 as that is the mode we see in the data.tar. In theory, tar lets you set
151 # it to whatever. However, for reproducibility, we have to be well-behaved - and that is 0777.
152 mode=0o0777,
153 owner=self.uname,
154 uid=self.uid,
155 group=self.gname,
156 gid=self.gid,
157 )
159 # EnsureMetadata is likely to hit virtual directories.
160 def _create_virtual_directory(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
161 assert tar_path.is_dir
163 if tar_path.has_fs_path:
164 # Prefer mode from a real path if we have one for an implicit definition.
165 st = tar_path.stat()
166 mode = stat.S_IMODE(st.st_mode)
167 elif self.is_implicit_definition and self.rule_type == ManifestPathInfoRuleType.CREATE_DIRECTORY\
168 and self.mode is not None:
169 mode = self.mode
170 else:
171 mode = 0o755
172 return TarMember.virtual_path(
173 tar_path.tar_path,
174 PathType.DIRECTORY,
175 clamp_mtime_to,
176 mode=mode,
177 owner=self.uname,
178 uid=self.uid,
179 group=self.gname,
180 gid=self.gid,
181 )
183 def should_include_path(self) -> bool:
184 return True
186 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
187 raise NotImplementedError
190class EnsurePathMetadataPathRule(ManifestPathRule):
192 def __init__(self,
193 definition_source: str,
194 match_rule: MatchRule,
195 symbolic_mode: Optional[str] = None,
196 mode: Optional[int] = None,
197 dirmode: Optional[int] = None,
198 special_mode: Optional[int] = None,
199 uid=0,
200 uname='root',
201 gid=0,
202 gname='root',
203 is_builtin_definition=False,
204 condition: Optional['ManifestPathRule'] = None,
205 ) -> None:
206 if symbolic_mode is not None and (mode is not None or dirmode is not None or special_mode is not None):
207 raise ValueError("symbolic_mode is mutually exclusive with any of the other modes")
208 super().__init__(
209 rule_type=ManifestPathInfoRuleType.ENSURE_METADATA,
210 definition_source=definition_source,
211 match_rule=match_rule,
212 symbolic_mode=symbolic_mode,
213 mode=mode,
214 dirmode=dirmode,
215 special_mode=special_mode,
216 uid=uid,
217 uname=uname,
218 gid=gid,
219 gname=gname,
220 is_builtin_definition=is_builtin_definition,
221 condition=condition,
222 )
224 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
225 self._used = True
226 if tar_path.is_dir:
227 # Special-case (implicit directory)
228 if not tar_path.has_fs_path:
229 return self._create_virtual_directory(tar_path, clamp_mtime_to)
230 mode = self.dirmode
231 elif tar_path.is_file:
232 mode = self.mode
233 elif tar_path.is_symlink:
234 # Special-case,
235 st = tar_path.stat()
236 link_target = tar_path.readlink()
237 return self._create_symlink(
238 tar_path,
239 debian_policy_normalize_symlink_target(tar_path.path, link_target),
240 min(int(st.st_mtime), clamp_mtime_to),
241 )
242 else:
243 mode = self.special_mode
245 if self.symbolic_mode is not None:
246 assert mode is None
247 st = tar_path.stat()
248 base_mode = stat.S_IMODE(st.st_mode)
249 mode = apply_symbolic_mode(tar_path, base_mode, self.symbolic_mode)
251 tar_member = TarMember.from_file(
252 tar_path.tar_path,
253 tar_path.fs_path,
254 mode=mode,
255 uid=self.uid,
256 owner=self.uname,
257 gid=self.gid,
258 group=self.gname,
259 clamp_mtime_to=clamp_mtime_to,
260 )
262 if self.ensure_path_type is not None and self.ensure_path_type != tar_member.path_type:
263 fixit_hint = ''
264 if self.ensure_path_type == PathType.DIRECTORY:
265 fixit_hint = ('\n'
266 'You can often fix this by either removing the physical path in the file system'
267 ' OR by explicitly defining this path as a directory in the manifest. In that case,'
268 ' the result will be a directory. Alternatively, check if the manifest reference'
269 ' point to something that should create a directory here. Maybe the path in the'
270 ' manifest has a typo.'
271 )
272 _error(f'Had expected {tar_member.member_path} to be a {self.ensure_path_type.name} due'
273 f' to {self.definition_source}, but in the file system the path pointed to a'
274 f' {tar_member.path_type.name} (fs path is: {tar_member.fs_path}).{fixit_hint}')
276 return tar_member
279class EnsureSymlinkPathRule(ManifestPathRule):
281 def __init__(self,
282 definition_source: str,
283 link_path: ExactFileSystemPath,
284 link_target: str,
285 condition: Optional['ManifestPathRule'],
286 ) -> None:
287 super().__init__(
288 rule_type=ManifestPathInfoRuleType.CREATE_SYMLINK,
289 definition_source=definition_source,
290 match_rule=link_path,
291 link_target=debian_policy_normalize_symlink_target(link_path.path, link_target),
292 ensure_path_type=PathType.SYMLINK,
293 condition=condition,
294 )
296 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
297 self._used = True
298 return self._create_symlink(
299 tar_path,
300 self.link_target,
301 clamp_mtime_to,
302 )
305class EnsureDirectoryPathRule(ManifestPathRule):
307 def __init__(self,
308 definition_source: str,
309 directory_path: ExactFileSystemPath,
310 mode=0o755,
311 uid=0,
312 uname='root',
313 gid=0,
314 gname='root',
315 triggered_by: Optional['ManifestPathRule'] = None,
316 is_implicit_definition: bool = False,
317 condition: Optional['ManifestPathRule'] = None,
318 ) -> None:
319 super().__init__(
320 rule_type=ManifestPathInfoRuleType.CREATE_DIRECTORY,
321 definition_source=definition_source,
322 match_rule=directory_path,
323 ensure_path_type=PathType.DIRECTORY,
324 mode=mode,
325 uid=uid,
326 uname=uname,
327 gid=gid,
328 gname=gname,
329 is_implicit_definition=is_implicit_definition,
330 triggered_by=triggered_by,
331 condition=condition,
332 )
334 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
335 self._used = True
336 return self._create_virtual_directory(tar_path, clamp_mtime_to)
339class ExclusionPathRule(ManifestPathRule):
340 def __init__(self,
341 definition_source: str,
342 path_to_be_removed: MatchRule,
343 ) -> None:
344 super().__init__(
345 rule_type=ManifestPathInfoRuleType.EXCLUDE_PATH,
346 definition_source=definition_source,
347 match_rule=path_to_be_removed,
348 )
350 def should_include_path(self) -> bool:
351 return False
353 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember:
354 raise TypeError("Bug: An exclusion rule cannot cause a TarMember to be added to the data.tar")