Coverage for rust2rpm/utils.py: 66%
72 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
1"""Module containing various helper functionality for interacting with the filesystem."""
3import os
4import re
5import shutil
6import subprocess
7from pathlib import Path
9from rust2rpm import log
10from rust2rpm.sysinfo import Target
12DEFAULT_EDITOR = "nano"
15class NoEditorError(Exception):
16 """Exception raised when no suitable terminal editor can be found."""
19def detect_editor() -> str:
20 """Return the executable name of the default editor.
22 Precedence:
24 - Return the value of the `$VISUAL` environment variable if it is set.
25 - Return the value of the `$EDITOR` environment variable if it is set.
26 - Return the default editor (`DEFAULT_EDITOR`) if neither are set.
28 Returns:
29 Name of the editor executable, which is expected to be available in `$PATH`.
31 Raises:
32 `NoEditorError`: The current terminal is too dumb to handle interactive use.
34 """
35 terminal = os.getenv("TERM")
36 terminal_is_dumb = not terminal or terminal == "dumb"
37 editor = None
38 if not terminal_is_dumb: 38 ↛ 40line 38 didn't jump to line 40 because the condition on line 38 was always true
39 editor = os.getenv("VISUAL")
40 if not editor: 40 ↛ 42line 40 didn't jump to line 42 because the condition on line 40 was always true
41 editor = os.getenv("EDITOR")
42 if not editor: 42 ↛ 47line 42 didn't jump to line 47 because the condition on line 42 was always true
43 if terminal_is_dumb: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true
44 msg = "Terminal is dumb, but $EDITOR unset"
45 raise NoEditorError(msg)
46 editor = DEFAULT_EDITOR
47 return editor
50def detect_packager() -> str | None:
51 """Return an identifier for the current user in the `name <email>` format.
53 Precedence:
55 - `None` if the `RUST2RPM_NO_DETECT_PACKAGER` environment variable is set.
56 - The value of the `RUST2RPM_PACKAGER` environment variable if it is set.
57 - The string printed by the `rpmdev-packager` tool if it is available.
58 - A user identifier based on the current user's `git` configuration.
59 - `None` if none of the previous were available.
61 Returns:
62 Identifier for the current user for use in RPM changelog entries.
64 """
65 # If we're forcing the fallback...
66 if os.getenv("RUST2RPM_NO_DETECT_PACKAGER"): 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true
67 return None
69 # If we're supplying packager identity through an environment variable...
70 if packager := os.getenv("RUST2RPM_PACKAGER"): 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true
71 return packager
73 # If we're detecting packager identity through rpmdev-packager...
74 if rpmdev_packager := shutil.which("rpmdev-packager"): 74 ↛ 78line 74 didn't jump to line 78 because the condition on line 74 was always true
75 return subprocess.check_output(rpmdev_packager, text=True).strip() # noqa: S603
77 # If we're detecting packager identity through git configuration...
78 if git := shutil.which("git"):
79 name = subprocess.check_output([git, "config", "user.name"], text=True).strip() # noqa: S603
80 email = subprocess.check_output([git, "config", "user.email"], text=True).strip() # noqa: S603
81 return f"{name} <{email}>"
83 return None
86def guess_crate_name(root: Path | None = None) -> str | None:
87 """Return guessed crate name based on directory, spec file, and spec file contents.
89 Precedence:
91 - If exactly one file matching `*.spec` is present in the root directory,
92 check contents for the presence of a `%crate` macro and return its value.
93 This supports spec files for *"compat"* packages, where spec file names
94 cannot be uniquely parsed into (name, version).
95 - If more than one file matching `*.spec` are present in the root directory,
96 return `None` and require specifying the crate name explicitly in CLI
97 arguments.
98 - If no files matching `*.spec` are present in the root directory, return
99 a guess based on the name of the root directory.
100 - If none of the previous heuristics matched, return `None` and require
101 specifying the crate name explicitly in CLI arguments.
103 Arguments:
104 root: Override root directory for heuristics. Defaults to the current directory.
106 Returns:
107 Name of the current crate based on heuristics.
109 """
110 path = root or Path()
111 specs = [*path.rglob("*.spec")]
113 if len(specs) > 1: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true
114 log.error(
115 "Found multiple spec files in the current working directory; "
116 "unable to determine crate name automatically.",
117 )
118 return None
120 if len(specs) == 1:
121 crate = None
122 spec = specs[0]
124 with spec.open() as file:
125 for line in file:
126 if m := re.match(r"^%(?:global|define)\s+crate\s+(\S+)\s+", line): 126 ↛ 125line 126 didn't jump to line 125 because the condition on line 126 was always true
127 if crate: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 log.error(
129 f"Found multiple definitions of the '%crate' macro in {spec!s}; "
130 "unable to determine crate name automatically.",
131 )
132 return None
133 crate = m.group(1)
134 if "%" in crate: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 log.error("The value of the %crate macro appears to contain other macros and cannot be parsed.")
136 return None
138 if crate: 138 ↛ 141line 138 didn't jump to line 141 because the condition on line 138 was always true
139 log.success(f"Found valid spec file {spec!s} for the {crate!r} crate.")
140 else:
141 log.error(f"Invalid spec file {spec!s}; unable to determine crate name automatically.")
142 return crate
144 if m := re.match("^rust-([a-z+0-9_-]+)$", path.absolute().name): 144 ↛ 149line 144 didn't jump to line 149 because the condition on line 144 was always true
145 crate = m.group(1)
146 log.info(f"Continuing with crate name {crate!r} based on the current working directory.")
147 return crate
149 return None
152def detect_rpmautospec(target: Target, spec_file: Path) -> bool:
153 """Return guess whether %autorelease+%autochangelog should be used.
155 Precedence:
157 - False if the target is not Fedora or EPEL 8
158 - If a spec file already exists, return whether it is using rpmautospec.
159 - If no spec file exists yet, return True.
161 Arguments:
162 target: `Target` for the spec file generator.
163 spec_file: Path to a spec file that might or might not exist.
165 Returns:
166 Boolean flag that determines whether rpmautospec will be used.
168 """
169 # We default to on only for selected distros for now…
170 if target not in {Target.FEDORA, Target.EPEL8}: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 return False
173 try:
174 text = spec_file.read_text()
175 except FileNotFoundError:
176 # A new file, let's try the new thing.
177 return True
179 # We are redoing an existing spec file. Figure out if it had
180 # %autochangelog enabled before.
181 autochangelog_re = r"^\s*%(?:autochangelog|\{\??autochangelog\})\b"
182 return any(re.match(autochangelog_re, line) for line in text.splitlines())