Coverage for rust2rpm/utils.py: 66%

72 statements  

« 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.""" 

2 

3import os 

4import re 

5import shutil 

6import subprocess 

7from pathlib import Path 

8 

9from rust2rpm import log 

10from rust2rpm.sysinfo import Target 

11 

12DEFAULT_EDITOR = "nano" 

13 

14 

15class NoEditorError(Exception): 

16 """Exception raised when no suitable terminal editor can be found.""" 

17 

18 

19def detect_editor() -> str: 

20 """Return the executable name of the default editor. 

21 

22 Precedence: 

23 

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. 

27 

28 Returns: 

29 Name of the editor executable, which is expected to be available in `$PATH`. 

30 

31 Raises: 

32 `NoEditorError`: The current terminal is too dumb to handle interactive use. 

33 

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 

48 

49 

50def detect_packager() -> str | None: 

51 """Return an identifier for the current user in the `name <email>` format. 

52 

53 Precedence: 

54 

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. 

60 

61 Returns: 

62 Identifier for the current user for use in RPM changelog entries. 

63 

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 

68 

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 

72 

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 

76 

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

82 

83 return None 

84 

85 

86def guess_crate_name(root: Path | None = None) -> str | None: 

87 """Return guessed crate name based on directory, spec file, and spec file contents. 

88 

89 Precedence: 

90 

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. 

102 

103 Arguments: 

104 root: Override root directory for heuristics. Defaults to the current directory. 

105 

106 Returns: 

107 Name of the current crate based on heuristics. 

108 

109 """ 

110 path = root or Path() 

111 specs = [*path.rglob("*.spec")] 

112 

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 

119 

120 if len(specs) == 1: 

121 crate = None 

122 spec = specs[0] 

123 

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 

137 

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 

143 

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 

148 

149 return None 

150 

151 

152def detect_rpmautospec(target: Target, spec_file: Path) -> bool: 

153 """Return guess whether %autorelease+%autochangelog should be used. 

154 

155 Precedence: 

156 

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. 

160 

161 Arguments: 

162 target: `Target` for the spec file generator. 

163 spec_file: Path to a spec file that might or might not exist. 

164 

165 Returns: 

166 Boolean flag that determines whether rpmautospec will be used. 

167 

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 

172 

173 try: 

174 text = spec_file.read_text() 

175 except FileNotFoundError: 

176 # A new file, let's try the new thing. 

177 return True 

178 

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