Coverage for rust2rpm/utils.py: 26%

104 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-27 15:21 +0100

1import contextlib 

2import glob 

3import os 

4from pathlib import Path 

5import re 

6import sys 

7import shlex 

8import shutil 

9import subprocess 

10import requests 

11from typing import Optional 

12 

13from cargo2rpm.semver import Version 

14 

15from rust2rpm import log 

16 

17 

18DEFAULT_EDITOR = "nano" 

19 

20 

21@contextlib.contextmanager 

22def remove_on_error(path): 

23 try: 

24 yield 

25 except: # this is supposed to include ^C 

26 os.unlink(path) 

27 raise 

28 

29 

30@contextlib.contextmanager 

31def exit_on_common_errors(): 

32 """Suppress tracebacks on common "expected" exceptions""" 

33 try: 

34 yield 

35 except requests.exceptions.HTTPError as e: 

36 sys.exit(f"Failed to download metadata: {e}") 

37 except subprocess.CalledProcessError as e: 

38 cmd = shlex.join(e.cmd) 

39 sys.exit(f"Subcommand failed with code {e.returncode}: {cmd}") 

40 except FileNotFoundError as e: 

41 sys.exit(str(e)) 

42 

43 

44def detect_editor() -> str: 

45 terminal = os.getenv("TERM") 

46 terminal_is_dumb = not terminal or terminal == "dumb" 

47 editor = None 

48 if not terminal_is_dumb: 

49 editor = os.getenv("VISUAL") 

50 if not editor: 

51 editor = os.getenv("EDITOR") 

52 if not editor: 

53 if terminal_is_dumb: 

54 raise Exception("Terminal is dumb, but $EDITOR unset") 

55 editor = DEFAULT_EDITOR 

56 return editor 

57 

58 

59def detect_packager() -> Optional[str]: 

60 # If we're forcing the fallback... 

61 if os.getenv("RUST2RPM_NO_DETECT_PACKAGER"): 

62 return None 

63 

64 # If we're supplying packager identity through an environment variable... 

65 if packager := os.getenv("RUST2RPM_PACKAGER"): 

66 return packager 

67 

68 # If we're detecting packager identity through rpmdev-packager... 

69 if rpmdev_packager := shutil.which("rpmdev-packager"): 

70 return subprocess.check_output(rpmdev_packager, text=True).strip() 

71 

72 # If we're detecting packager identity through git configuration... 

73 if git := shutil.which("git"): 

74 name = subprocess.check_output([git, "config", "user.name"], text=True).strip() 

75 email = subprocess.check_output([git, "config", "user.email"], text=True).strip() 

76 return f"{name} <{email}>" 

77 

78 return None 

79 

80 

81def guess_crate_name() -> Optional[str]: 

82 """Guess crate name from directory name and/or spec file name 

83 

84 If a spec file is present, we use the %crate variable. This is the best 

85 option. But if we're in a new directory, and don't have a spec file yet, 

86 let's assume that this is a dist-git directory and extract the crate name. 

87 For compat packages, the directory name will contain a version suffix, 

88 but a compat package would almost always be created from an existing 

89 dist-git directory, hence there'd be a spec file, so we can ignore this. 

90 """ 

91 specs = glob.glob("*.spec") 

92 

93 if len(specs) > 1: 

94 log.error( 

95 "Found multiple spec files in the current working directory; " 

96 + "unable to determine crate name automatically." 

97 ) 

98 return None 

99 

100 if len(specs) == 1: 

101 crate = None 

102 spec = specs[0] 

103 

104 for line in open(spec): 

105 if m := re.match(r"^%(?:global|define)\s+crate\s+(\S+)\s+", line): 

106 if crate: 

107 log.error( 

108 f"Found multiple definitions of the '%crate' macro in {spec!r}; " 

109 + "unable to determine crate name automatically." 

110 ) 

111 return None 

112 crate = m.group(1) 

113 if "%" in crate: 

114 log.error("The value of the %crate macro appears to contain other macros and cannot be parsed.") 

115 return None 

116 if crate: 

117 log.success(f"Found valid spec file {spec!r} for the {crate!r} crate.") 

118 else: 

119 log.error(f"Invalid spec file {spec!r}; unable to determine crate name automatically.") 

120 return crate 

121 

122 dirname = os.path.basename(os.getcwd()) 

123 if m := re.match("^rust-([a-z+0-9_-]+)$", dirname): 

124 crate = m.group(1) 

125 log.info(f"Continuing with crate name {crate!r} based on the current working directory.") 

126 return crate 

127 

128 return None 

129 

130 

131def package_name_suffixed(name: str, suffix: Optional[str]): 

132 joiner = "_" if suffix and name[-1].isdigit() else "" 

133 return "rust-" + name + joiner + (suffix or "") 

134 

135 

136def package_name_compat(name: str, version: str): 

137 sv = Version.parse(version) 

138 if sv.major > 0: 

139 suffix = str(sv.major) 

140 elif sv.minor > 0: 

141 suffix = f"0.{str(sv.minor)}" 

142 else: 

143 suffix = f"0.0.{str(sv.patch)}" 

144 return package_name_suffixed(name, suffix) 

145 

146 

147def detect_rpmautospec(default_target: str, spec_file: Path): 

148 """Guess whether %autorelease+%autochangelog should be used 

149 

150 Returns False if we're not on Fedora or if the spec file exists and 

151 wasn't using rpmautospec already. 

152 """ 

153 

154 # We default to on only for selected distros for now… 

155 if default_target not in {"fedora", "epel8"}: 

156 return False 

157 

158 try: 

159 text = spec_file.read_text() 

160 except FileNotFoundError: 

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

162 return True 

163 

164 # We are redoing an existing spec file. Figure out if it had 

165 # %autochangelog enabled before. 

166 autochangelog_re = r"^\s*%(?:autochangelog|\{\??autochangelog\})\b" 

167 return any(re.match(autochangelog_re, line) for line in text.splitlines())