Coverage for debputy/debhelper_emulation.py: 0%

159 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-22 14:29 +0100

1import contextlib 

2import dataclasses 

3import errno 

4import os.path 

5import re 

6import shutil 

7from typing import Dict, Optional, Callable, Union, Iterable, TextIO, Tuple, Generator, Any, Literal, Sequence 

8 

9from debputy.maintscript_snippet import STD_CONTROL_SCRIPTS 

10from debputy.packages import BinaryPackage 

11from debputy.substitution import Substitution 

12from debputy.util import _error, _escape_shell_word, ensure_dir 

13 

14SnippetReplacement = Union[str, Callable[[str], str]] 

15MAINTSCRIPT_TOKEN_NAME_PATTERN = r'[A-Za-z0-9_.+]+' 

16MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN) 

17MAINTSCRIPT_TOKEN_REGEX = re.compile(f'#({MAINTSCRIPT_TOKEN_NAME_PATTERN})#') 

18 

19 

20@dataclasses.dataclass(slots=True, frozen=True) 

21class DHConfigFileLine: 

22 config_file: str 

23 line_no: int 

24 executable_config: bool 

25 original_line: str 

26 tokens: Sequence[str] 

27 

28 

29def dhe_generated_file(binary_package: BinaryPackage, filename: str, mkdirs: bool = True) -> str: 

30 parent_dir = os.path.join('debian/.debhelper/generated/', binary_package.name) 

31 if mkdirs: 

32 ensure_dir(parent_dir) 

33 return os.path.join(parent_dir, filename) 

34 

35 

36def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str: 

37 return os.path.join('debian', '.debhelper', binary_package.name, 'dbgsym-root') 

38 

39 

40def _concat_slurp_files(*files) -> str: 

41 result = '' 

42 for file in files: 

43 with open(file, 'rt') as fd: 

44 result += fd.read() 

45 if not result.endswith('\n'): 

46 result += "\n" 

47 return result 

48 

49 

50@contextlib.contextmanager 

51def _lines_from(config_file: str) -> Generator[Tuple[TextIO, bool], Any, Any]: 

52 if os.access(config_file, os.X_OK): 

53 raise NotImplementedError("We cannot emulate DH_CONFIG_ACT_ON_PACKAGES yet") 

54 else: 

55 with open(config_file, 'rt') as fd: 

56 yield fd, False 

57 

58 

59def dhe_filedoublearray(config_file: str, substitution: Optional[Substitution]) -> Iterable[DHConfigFileLine]: 

60 with _lines_from(config_file) as (fd, is_executable): 

61 for line_no, orig_line in enumerate(fd, start=1): 

62 orig_line = orig_line.rstrip("\n") 

63 line = orig_line.strip() 

64 if not line: 

65 if is_executable: 

66 _error(f'Executable config file "{config_file}" produced a non-empty whitespace only line (as line' 

67 f' {line_no})') 

68 continue 

69 if line.startswith("#") and not is_executable: 

70 continue 

71 # TODO: Support globs (?) 

72 parts = tuple(substitution.substitute(w, f'{config_file} line {line_no} token "{w}"') for w in line.split()) 

73 yield DHConfigFileLine(config_file, line_no, is_executable, orig_line, parts) 

74 

75 

76def dhe_pkgfile(binary_package: BinaryPackage, 

77 basename: str, 

78 always_fallback_to_packageless_variant: bool = False, 

79 mandatory: bool = False 

80 ) -> Optional[str]: 

81 # TODO: Architecture specific files 

82 possible_names = [ 

83 "%s.%s" % (binary_package.name, basename) 

84 ] 

85 if binary_package.is_main_package or always_fallback_to_packageless_variant: 

86 possible_names.append(basename) 

87 

88 for name in possible_names: 

89 path = "debian/%s" % name 

90 if os.path.exists(path): 

91 return path 

92 if mandatory: 

93 raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), os.path.join('debian', possible_names[0])) 

94 return None 

95 

96 

97def dhe_install_pkg_file_as_ctrl_file_if_present(binary_package: BinaryPackage, 

98 basename: str, 

99 root_dir: str, 

100 mode: int, 

101 **kwargs, 

102 ) -> None: 

103 source = dhe_pkgfile(binary_package, basename, **kwargs) 

104 if source is None: 

105 return 

106 ctrl_dir = os.path.join(root_dir, 'DEBIAN') 

107 ensure_dir(ctrl_dir) 

108 dhe_install_path(source, os.path.join(ctrl_dir, basename), mode) 

109 

110 

111def dhe_install_path(source: str, dest: str, mode: int) -> None: 

112 # TODO: "install -p -mXXXX foo bar" silently discards broken 

113 # symlinks to install the file in place. (#868204) 

114 print(f" install -p -m{oct(mode)[2:]} {_escape_shell_word(source)} {_escape_shell_word(dest)}") 

115 shutil.copyfile(source, dest) 

116 os.chmod(dest, mode) 

117 

118 

119def _read_dbgsym_file(binary_package: BinaryPackage, dbgsym_file: str) -> Optional[str]: 

120 p = os.path.join('debian', '.debhelper', binary_package.name, dbgsym_file) 

121 if os.path.isfile(p): 

122 with open(p, 'rt') as fd: 

123 return fd.read().strip() 

124 return None 

125 

126 

127def dhe_read_dbgsym_migration(binary_package: BinaryPackage) -> Optional[str]: 

128 return _read_dbgsym_file(binary_package, 'dbgsym-migration') 

129 

130 

131def dhe_read_dbgsym_build_ids(binary_package: BinaryPackage) -> str: 

132 raw = _read_dbgsym_file(binary_package, 'dbgsym-build-ids') 

133 if raw is None: 

134 return '' 

135 return " ".join(set(raw.split())) 

136 

137 

138def dhe_autoscript_expanded_snippet(binary_package: BinaryPackage, 

139 script: str, 

140 snippet: str, 

141 tool_with_version: str, 

142 snippet_order: Optional[Literal['service']] = None 

143 ) -> None: 

144 

145 assert snippet_order is None or snippet_order == 'service' 

146 assert script in STD_CONTROL_SCRIPTS 

147 if snippet_order is None: 

148 outfile = f'debian/{binary_package.name}.{script}.debhelper' 

149 else: 

150 outfile = dhe_generated_file(binary_package, f"{script}.{snippet_order}") 

151 reverse_order = script in ('postrm', 'prerm') 

152 if not snippet.endswith("\n"): 

153 snippet += "\n" 

154 combined_snippet = ( 

155 f"# Automatically added by {tool_with_version}\n" 

156 + snippet 

157 + "# End automatically added section\n" 

158 ) 

159 if reverse_order: 

160 with open(outfile, 'wt') as wfd, open(outfile, 'rt') as rfd: 

161 wfd.write(combined_snippet) 

162 shutil.copyfileobj(rfd, wfd) 

163 else: 

164 with open(outfile, 'at') as fd: 

165 fd.write(combined_snippet) 

166 

167 

168def _make_script_replacement_func(variables: Dict[str, SnippetReplacement], 

169 ) -> Callable[[re.Match[str]], str]: 

170 cache: Dict[str, str] = {} 

171 

172 def _impl(m: re.Match[str]) -> str: 

173 variable = m.group(1) 

174 if variable in cache: 

175 return cache[variable] 

176 if variable in variables: 

177 result = variables[variable] 

178 if isinstance(result, Callable): 

179 result = result(variable) 

180 cache[variable] = result 

181 return result 

182 return f'#{variable}#' 

183 

184 return _impl 

185 

186 

187def _generate_script_subst_table(package: str, 

188 debhelper_snippet_files: Iterable[str], 

189 extra_variables: Optional[Dict[str, SnippetReplacement]], 

190 ) -> Dict[str, SnippetReplacement]: 

191 variables: Dict[str, SnippetReplacement] = {} 

192 if extra_variables: 

193 variables.update(extra_variables) 

194 # "pkg.<pkg>." is 5 + the length of <pkg> 

195 subst_len = 5 + len(package) 

196 # Match debhelper's "pkg.<pkg>.FOO" => "FOO" rule 

197 for k, v in extra_variables.items(): 

198 if not MAINTSCRIPT_TOKEN_NAME_REGEX.match(k): 

199 raise ValueError(f'User defined token "{k}" does not match {MAINTSCRIPT_TOKEN_NAME_PATTERN}') 

200 if k.startswith(f'pkg.{package}.') and len(k) > subst_len: 

201 variables[k[subst_len:]] = v 

202 if 'PACKAGE' not in variables: 

203 variables['PACKAGE'] = package 

204 if 'DEBHELPER' not in variables: 

205 variables['DEBHELPER'] = lambda _: _concat_slurp_files(*debhelper_snippet_files) 

206 return variables 

207 

208 

209def debhelper_script_subst(binary_package: BinaryPackage, 

210 root_dir: str, 

211 script: str, 

212 extra_variables: Optional[Dict[str, SnippetReplacement]], 

213 ) -> None: 

214 assert script in STD_CONTROL_SCRIPTS 

215 package = binary_package.name 

216 user_provided_file = dhe_pkgfile(binary_package, script) 

217 ensure_dir(os.path.join(root_dir, 'DEBIAN')) 

218 dest = os.path.join(root_dir, 'DEBIAN', script) 

219 snippets = [ 

220 x for x in ( 

221 f'debian/{package}.{script}.debhelper', 

222 dhe_generated_file(binary_package, f"{script}.service", mkdirs=False), 

223 ) if os.path.isfile(x) 

224 ] 

225 has_snippets = bool(snippets) 

226 if script in ('prerm', 'postrm'): 

227 snippets = reversed(snippets) 

228 

229 variables = _generate_script_subst_table(package, snippets, extra_variables) 

230 if user_provided_file: 

231 with open(dest, 'wt') as dest_fd, open(user_provided_file, 'rt') as source_fd: 

232 replacement = _make_script_replacement_func(variables) 

233 for line in source_fd: 

234 dest_fd.write(MAINTSCRIPT_TOKEN_REGEX.sub(replacement, line)) 

235 elif has_snippets: 

236 with open(dest, 'wt') as fd: 

237 fd.write("#!/bin/sh\n") 

238 fd.write("set -e\n") 

239 fd.write(_concat_slurp_files(snippets)) 

240 else: 

241 return 

242 os.chmod(dest, 0o755)