Coverage for rust2rpm/patching.py: 22%
105 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
1import ast
2from datetime import datetime, timezone
3from difflib import unified_diff
4import re
5import os
6import shutil
7import subprocess
8from typing import Optional
10from rust2rpm import cfg, log
11from rust2rpm.utils import detect_editor
14# [target.'cfg(not(any(target_os="windows", target_os="macos")))'.dependencies]
15# [target."cfg(windows)".dependencies.winapi]
16# [target."cfg(target_arch = \"wasm32\")".dev-dependencies.wasm-bindgen-test]
18TARGET_DEPENDENCY_LINE = re.compile(
19 r"""
20 ^ \[ target\.(?P<cfg>(?P<quote>['"])cfg\(.*\)(?P=quote))
21 \.
22 (?P<type> dependencies|build-dependencies|dev-dependencies)
23 (?:\. (?P<feature>[a-zA-Z0-9_-]+) )?
24 ] \s* $
25 """,
26 re.VERBOSE,
27)
30def filter_out_features_re(dropped_features: set[str]) -> re.Pattern:
31 # This is a bit simplistic. But it doesn't seem worth the trouble to write
32 # a grammar for this. Can be always done later. If we parse this using a
33 # grammar, we beget the question of how to preserve formatting idiosyncrasies.
34 # Regexp replacement makes it trivial to minimize changes.
35 match_features = "|".join(dropped_features)
36 match_suffix = f"(?:/[{cfg.IDENT_CHARS[1]}]+)?"
38 return re.compile(
39 rf"""
40 (?P<comma> ,)? \s* (?P<quote>['"])
41 ({match_features}) {match_suffix}
42 (?P=quote) \s* (?(comma) |,?) \s*
43 """,
44 re.VERBOSE,
45 )
48def file_mtime_in_iso_format(path: str) -> str:
49 mtime = os.stat(path).st_mtime
50 return datetime.fromtimestamp(mtime, timezone.utc).isoformat()
53def make_diff_from_lines(
54 diff_path: str, lines_before: list[str], mtime_before: str, lines_after: list[str], mtime_after: str
55) -> list[str]:
56 return list(
57 unified_diff(
58 lines_before,
59 lines_after,
60 fromfile=diff_path,
61 tofile=diff_path,
62 fromfiledate=mtime_before,
63 tofiledate=mtime_after,
64 lineterm="",
65 )
66 )
69def preprocess_cargo_toml(contents: str, features: set[str]) -> Optional[str]:
70 if shutil.which("rust2rpm-helper"): 70 ↛ 73line 70 didn't jump to line 73 because the condition on line 70 was always true
71 return preprocess_cargo_toml_helper(contents)
72 else:
73 log.warn(
74 "Falling back to broken built-in implementation for stripping non-applicable "
75 + "target-specific depdendencies: rust2rpm-helper is not installed"
76 )
77 return preprocess_cargo_toml_fallback(contents, features)
80def preprocess_cargo_toml_helper(contents: str) -> Optional[str]:
81 ret1 = subprocess.run(
82 ["rust2rpm-helper", "normalize-version", "-"], input=contents, text=True, stdout=subprocess.PIPE
83 )
84 ret1.check_returncode()
85 patched1 = ret1.stdout or contents
87 ret2 = subprocess.run(["rust2rpm-helper", "strip-foreign", "-"], input=patched1, text=True, stdout=subprocess.PIPE)
88 ret2.check_returncode()
89 patched2 = ret2.stdout or patched1
91 if patched2 == contents:
92 return None
93 else:
94 return patched2
97def preprocess_cargo_toml_fallback(contents: str, features: set[str]) -> Optional[str]:
98 lines = contents.splitlines()
100 kept_lines = []
101 dropped_lines = 0
102 dropped_optional_deps = set()
104 keep = True
105 feature = None
107 for line in lines:
108 if m := TARGET_DEPENDENCY_LINE.match(line):
109 expr = m.group("cfg")
110 expr = ast.literal_eval(expr)
112 try:
113 keep = cfg.parse_and_evaluate(expr)
114 except (ValueError, cfg.ParseException):
115 log.warn(f"Could not evaluate {expr!r}, treating as true.")
116 keep = True
118 if not keep:
119 feature = m.group("feature")
120 log.info(f"Dropping target-specific dependency on {feature!r}.")
122 elif line == "optional = true\n" and feature:
123 if not keep:
124 # dropped feature was optional:
125 # remove occurrences from feature dependencies
126 if feature in features:
127 dropped_optional_deps.add(feature)
128 else:
129 dropped_optional_deps.add(f"dep:{feature}")
131 else:
132 # optional dependency occurs in multiple targets:
133 # do not drop from feature dependencies
134 if feature in dropped_optional_deps:
135 dropped_optional_deps.remove(feature)
136 if f"dep:{feature}" in dropped_optional_deps:
137 dropped_optional_deps.remove(f"dep:{feature}")
139 elif line.startswith("["):
140 # previous section ended, let's keep printing lines again
141 keep = True
143 if keep:
144 kept_lines.append(line)
145 else:
146 dropped_lines += 1
148 if not dropped_lines:
149 # nothing to do, let's bail out
150 return None
152 output_lines = []
153 in_features = False
154 feature_filter = filter_out_features_re(dropped_optional_deps)
156 for line in kept_lines:
157 if line.rstrip() == "[features]":
158 in_features = True
159 elif line.startswith("["):
160 in_features = False
161 elif in_features:
162 line = re.sub(feature_filter, "", line)
163 if not line:
164 continue
166 output_lines += [line]
168 return "\n".join(output_lines)
171def make_patches(
172 name: str, version: str, patch: bool, patch_foreign: bool, toml_path: str, features: set[str]
173) -> tuple[Optional[list[str]], Optional[list[str]]]:
174 """Returns up to two patches (automatic and manual).
176 For the manual patch, an editor is spawned to open the file at `toml_path`
177 and a diff is made after the editor returns.
178 """
180 mtime_before = file_mtime_in_iso_format(toml_path)
182 with open(toml_path) as file:
183 toml_before = file.read()
185 diff1 = diff2 = None
186 diff_path = f"{name}-{version}/Cargo.toml"
188 # patching Cargo.toml involves multiple steps:
189 #
190 # 1) attempt to automatically drop "foreign" dependencies
191 # If this succeeded, remove the original Cargo.toml file, write the
192 # changed contents back to disk, and generate a diff.
193 # 2) if requested, open Cargo.toml in an editor for further, manual changes
194 #
195 # The changes from *both* steps must be reflected in the Cargo.toml file
196 # that ends up on disk after this function returns. Otherwise, the
197 # calculated metadata and generated spec file will not reflect the patches
198 # to Cargo.toml that were generated here.
200 if toml_after := patch_foreign and preprocess_cargo_toml(toml_before, features):
201 # remove original Cargo.toml file
202 os.remove(toml_path)
204 # write auto-patched Cargo.toml to disk
205 with open(toml_path, "w") as file:
206 file.write(toml_after)
208 mtime_after1 = file_mtime_in_iso_format(toml_path)
209 diff1 = make_diff_from_lines(
210 diff_path, toml_before.splitlines(), mtime_before, toml_after.splitlines(), mtime_after1
211 )
212 else:
213 toml_after = toml_before
215 if patch:
216 # open editor for Cargo.toml
217 editor = detect_editor()
218 subprocess.check_call([editor, toml_path])
220 with open(toml_path) as file:
221 toml_after2 = file.read()
223 mtime_after2 = file_mtime_in_iso_format(toml_path)
224 diff2 = make_diff_from_lines(
225 diff_path, toml_after.splitlines(), mtime_before, toml_after2.splitlines(), mtime_after2
226 )
228 return diff1, diff2