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

1import dataclasses 

2import stat 

3from enum import Enum 

4from typing import Optional, Iterable, Set 

5 

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 

14 

15 

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) 

21 

22 @property 

23 def creates_path(self) -> bool: 

24 return self.value[1] 

25 

26 @property 

27 def can_have_children(self): 

28 return self.value[1] 

29 

30 

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 

52 

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) 

58 

59 @classmethod 

60 def exclusion(cls, definition_source: str, match_rule: MatchRule): 

61 return ExclusionPathRule(definition_source, match_rule) 

62 

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 ) 

72 

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 ) 

104 

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

112 

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) 

127 

128 def children(self) -> Iterable[ExactFileSystemPath]: 

129 if self._children: 

130 yield from self._children 

131 

132 def mark_used(self): 

133 self._used = True 

134 

135 @property 

136 def has_been_used(self): 

137 return self._used 

138 

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 ) 

158 

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 

162 

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 ) 

182 

183 def should_include_path(self) -> bool: 

184 return True 

185 

186 def generate_tar_member(self, tar_path: 'FSPath', clamp_mtime_to: int) -> TarMember: 

187 raise NotImplementedError 

188 

189 

190class EnsurePathMetadataPathRule(ManifestPathRule): 

191 

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 ) 

223 

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 

244 

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) 

250 

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 ) 

261 

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

275 

276 return tar_member 

277 

278 

279class EnsureSymlinkPathRule(ManifestPathRule): 

280 

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 ) 

295 

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 ) 

303 

304 

305class EnsureDirectoryPathRule(ManifestPathRule): 

306 

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 ) 

333 

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) 

337 

338 

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 ) 

349 

350 def should_include_path(self) -> bool: 

351 return False 

352 

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