228 lines
7.2 KiB
Python
228 lines
7.2 KiB
Python
import ast
|
|
from datetime import datetime, timezone
|
|
from difflib import unified_diff
|
|
import re
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
from rust2rpm import cfg, log
|
|
from rust2rpm.utils import detect_editor
|
|
|
|
|
|
# [target.'cfg(not(any(target_os="windows", target_os="macos")))'.dependencies]
|
|
# [target."cfg(windows)".dependencies.winapi]
|
|
# [target."cfg(target_arch = \"wasm32\")".dev-dependencies.wasm-bindgen-test]
|
|
|
|
TARGET_DEPENDENCY_LINE = re.compile(
|
|
r"""
|
|
^ \[ target\.(?P<cfg>(?P<quote>['"])cfg\(.*\)(?P=quote))
|
|
\.
|
|
(?P<type> dependencies|build-dependencies|dev-dependencies)
|
|
(?:\. (?P<feature>[a-zA-Z0-9_-]+) )?
|
|
] \s* $
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def filter_out_features_re(dropped_features: set[str]) -> re.Pattern:
|
|
# This is a bit simplistic. But it doesn't seem worth the trouble to write
|
|
# a grammar for this. Can be always done later. If we parse this using a
|
|
# grammar, we beget the question of how to preserve formatting idiosyncrasies.
|
|
# Regexp replacement makes it trivial to minimize changes.
|
|
match_features = "|".join(dropped_features)
|
|
match_suffix = f"(?:/[{cfg.IDENT_CHARS[1]}]+)?"
|
|
|
|
return re.compile(
|
|
rf"""
|
|
(?P<comma> ,)? \s* (?P<quote>['"])
|
|
({match_features}) {match_suffix}
|
|
(?P=quote) \s* (?(comma) |,?) \s*
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def file_mtime_in_iso_format(path: str) -> str:
|
|
mtime = os.stat(path).st_mtime
|
|
return datetime.fromtimestamp(mtime, timezone.utc).isoformat()
|
|
|
|
|
|
def make_diff_from_lines(
|
|
diff_path: str, lines_before: list[str], mtime_before: str, lines_after: list[str], mtime_after: str
|
|
) -> list[str]:
|
|
return list(
|
|
unified_diff(
|
|
lines_before,
|
|
lines_after,
|
|
fromfile=diff_path,
|
|
tofile=diff_path,
|
|
fromfiledate=mtime_before,
|
|
tofiledate=mtime_after,
|
|
lineterm="",
|
|
)
|
|
)
|
|
|
|
|
|
def preprocess_cargo_toml(contents: str, features: set[str]) -> Optional[str]:
|
|
if shutil.which("rust2rpm-helper"):
|
|
return preprocess_cargo_toml_helper(contents)
|
|
else:
|
|
log.warn(
|
|
"Falling back to broken built-in implementation for stripping non-applicable "
|
|
+ "target-specific depdendencies: rust2rpm-helper is not installed"
|
|
)
|
|
return preprocess_cargo_toml_fallback(contents, features)
|
|
|
|
|
|
def preprocess_cargo_toml_helper(contents: str) -> Optional[str]:
|
|
ret1 = subprocess.run(
|
|
["rust2rpm-helper", "normalize-version", "-"], input=contents, text=True, stdout=subprocess.PIPE
|
|
)
|
|
ret1.check_returncode()
|
|
patched1 = ret1.stdout or contents
|
|
|
|
ret2 = subprocess.run(["rust2rpm-helper", "strip-foreign", "-"], input=patched1, text=True, stdout=subprocess.PIPE)
|
|
ret2.check_returncode()
|
|
patched2 = ret2.stdout or patched1
|
|
|
|
if patched2 == contents:
|
|
return None
|
|
else:
|
|
return patched2
|
|
|
|
|
|
def preprocess_cargo_toml_fallback(contents: str, features: set[str]) -> Optional[str]:
|
|
lines = contents.splitlines()
|
|
|
|
kept_lines = []
|
|
dropped_lines = 0
|
|
dropped_optional_deps = set()
|
|
|
|
keep = True
|
|
feature = None
|
|
|
|
for line in lines:
|
|
if m := TARGET_DEPENDENCY_LINE.match(line):
|
|
expr = m.group("cfg")
|
|
expr = ast.literal_eval(expr)
|
|
|
|
try:
|
|
keep = cfg.parse_and_evaluate(expr)
|
|
except (ValueError, cfg.ParseException):
|
|
log.warn(f"Could not evaluate {expr!r}, treating as true.")
|
|
keep = True
|
|
|
|
if not keep:
|
|
feature = m.group("feature")
|
|
log.info(f"Dropping target-specific dependency on {feature!r}.")
|
|
|
|
elif line == "optional = true\n" and feature:
|
|
if not keep:
|
|
# dropped feature was optional:
|
|
# remove occurrences from feature dependencies
|
|
if feature in features:
|
|
dropped_optional_deps.add(feature)
|
|
else:
|
|
dropped_optional_deps.add(f"dep:{feature}")
|
|
|
|
else:
|
|
# optional dependency occurs in multiple targets:
|
|
# do not drop from feature dependencies
|
|
if feature in dropped_optional_deps:
|
|
dropped_optional_deps.remove(feature)
|
|
if f"dep:{feature}" in dropped_optional_deps:
|
|
dropped_optional_deps.remove(f"dep:{feature}")
|
|
|
|
elif line.startswith("["):
|
|
# previous section ended, let's keep printing lines again
|
|
keep = True
|
|
|
|
if keep:
|
|
kept_lines.append(line)
|
|
else:
|
|
dropped_lines += 1
|
|
|
|
if not dropped_lines:
|
|
# nothing to do, let's bail out
|
|
return None
|
|
|
|
output_lines = []
|
|
in_features = False
|
|
feature_filter = filter_out_features_re(dropped_optional_deps)
|
|
|
|
for line in kept_lines:
|
|
if line.rstrip() == "[features]":
|
|
in_features = True
|
|
elif line.startswith("["):
|
|
in_features = False
|
|
elif in_features:
|
|
line = re.sub(feature_filter, "", line)
|
|
if not line:
|
|
continue
|
|
|
|
output_lines += [line]
|
|
|
|
return "\n".join(output_lines)
|
|
|
|
|
|
def make_patches(
|
|
name: str, version: str, patch: bool, patch_foreign: bool, toml_path: str, features: set[str]
|
|
) -> tuple[Optional[list[str]], Optional[list[str]]]:
|
|
"""Returns up to two patches (automatic and manual).
|
|
|
|
For the manual patch, an editor is spawned to open the file at `toml_path`
|
|
and a diff is made after the editor returns.
|
|
"""
|
|
|
|
mtime_before = file_mtime_in_iso_format(toml_path)
|
|
|
|
with open(toml_path) as file:
|
|
toml_before = file.read()
|
|
|
|
diff1 = diff2 = None
|
|
diff_path = f"{name}-{version}/Cargo.toml"
|
|
|
|
# patching Cargo.toml involves multiple steps:
|
|
#
|
|
# 1) attempt to automatically drop "foreign" dependencies
|
|
# If this succeeded, remove the original Cargo.toml file, write the
|
|
# changed contents back to disk, and generate a diff.
|
|
# 2) if requested, open Cargo.toml in an editor for further, manual changes
|
|
#
|
|
# The changes from *both* steps must be reflected in the Cargo.toml file
|
|
# that ends up on disk after this function returns. Otherwise, the
|
|
# calculated metadata and generated spec file will not reflect the patches
|
|
# to Cargo.toml that were generated here.
|
|
|
|
if toml_after := patch_foreign and preprocess_cargo_toml(toml_before, features):
|
|
# remove original Cargo.toml file
|
|
os.remove(toml_path)
|
|
|
|
# write auto-patched Cargo.toml to disk
|
|
with open(toml_path, "w") as file:
|
|
file.write(toml_after)
|
|
|
|
mtime_after1 = file_mtime_in_iso_format(toml_path)
|
|
diff1 = make_diff_from_lines(
|
|
diff_path, toml_before.splitlines(), mtime_before, toml_after.splitlines(), mtime_after1
|
|
)
|
|
else:
|
|
toml_after = toml_before
|
|
|
|
if patch:
|
|
# open editor for Cargo.toml
|
|
editor = detect_editor()
|
|
subprocess.check_call([editor, toml_path])
|
|
|
|
with open(toml_path) as file:
|
|
toml_after2 = file.read()
|
|
|
|
mtime_after2 = file_mtime_in_iso_format(toml_path)
|
|
diff2 = make_diff_from_lines(
|
|
diff_path, toml_after.splitlines(), mtime_before, toml_after2.splitlines(), mtime_after2
|
|
)
|
|
|
|
return diff1, diff2
|