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
« 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
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
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})#')
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]
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)
36def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str:
37 return os.path.join('debian', '.debhelper', binary_package.name, 'dbgsym-root')
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
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
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)
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)
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
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)
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)
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
127def dhe_read_dbgsym_migration(binary_package: BinaryPackage) -> Optional[str]:
128 return _read_dbgsym_file(binary_package, 'dbgsym-migration')
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()))
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:
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)
168def _make_script_replacement_func(variables: Dict[str, SnippetReplacement],
169 ) -> Callable[[re.Match[str]], str]:
170 cache: Dict[str, str] = {}
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}#'
184 return _impl
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
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)
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)