| |
@@ -20,7 +20,7 @@
|
| |
import requests
|
| |
import tqdm
|
| |
|
| |
- from . import Metadata, licensing, generator
|
| |
+ from . import Metadata, cfg, licensing, generator
|
| |
|
| |
DEFAULT_EDITOR = "vi"
|
| |
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
|
| |
@@ -36,6 +36,18 @@
|
| |
(?:AGPL|APACHE|BSD|GFDL|GNU|L?GPL|MIT|MPL|OFL)-.*[0-9].*
|
| |
""", re.VERBOSE)
|
| |
|
| |
+ # [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 sortify(func):
|
| |
"""Return a sorted list from a generator"""
|
| |
def wrapper(*args, **kwargs):
|
| |
@@ -198,35 +210,119 @@
|
| |
license_files = get_license_files(root_path)
|
| |
yield toml, doc_files, license_files
|
| |
|
| |
- def make_patch(args, toml, tmpfile=False):
|
| |
- """Spawns editor on toml and returns a unified diff after editor closes"""
|
| |
- if not args.patch:
|
| |
- return []
|
| |
+ def filter_out_features_re(dropped_features):
|
| |
+ # 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 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(fr'''(?P<comma> ,)? \s* (?P<quote>['"])
|
| |
+ ({match_features}) {match_suffix}
|
| |
+ (?P=quote) \s* (?(comma) |,?) \s*
|
| |
+ ''', re.VERBOSE)
|
| |
+
|
| |
+ def drop_foreign_dependencies(lines):
|
| |
+ dropped_lines = 0
|
| |
+ dropped_features = set()
|
| |
+ good_lines = []
|
| |
+
|
| |
+ value = True
|
| |
+ for line in lines:
|
| |
+ # print(f'{line=}')
|
| |
+ # [target.'cfg(not(any(target_os="windows", target_os="macos")))'.dependencies]
|
| |
+ if m := TARGET_DEPENDENCY_LINE.match(line):
|
| |
+ expr = m.group('cfg')
|
| |
+ expr = ast.literal_eval(expr)
|
| |
+ # print(f'matched: {expr=}')
|
| |
+ try:
|
| |
+ value = cfg.parse_and_evaluate(expr)
|
| |
+ except (ValueError, cfg.ParseException):
|
| |
+ print(f'Could not evaluate {expr!r}, treating as true')
|
| |
+ value = True
|
| |
+
|
| |
+ if not value:
|
| |
+ feature = m.group('feature')
|
| |
+ print(f'Skipping section {line.rstrip()} ({feature=})')
|
| |
+ dropped_features.add(feature)
|
| |
+
|
| |
+ elif line.startswith('['):
|
| |
+ # previous section ended, let's keep printing lines again
|
| |
+ value = True
|
| |
+
|
| |
+ # print(f'→ {value}')
|
| |
+
|
| |
+ if value:
|
| |
+ good_lines += [line]
|
| |
+ else:
|
| |
+ dropped_lines += 1
|
| |
+
|
| |
+ if not dropped_lines:
|
| |
+ # nothing to do, let's bail out
|
| |
+ return None
|
| |
+
|
| |
+ good_lines2 = []
|
| |
+ in_features = False
|
| |
+ filt = filter_out_features_re(dropped_features)
|
| |
+ for line in good_lines:
|
| |
+ if line.rstrip() == '[features]':
|
| |
+ in_features = True
|
| |
+ elif line.startswith('['):
|
| |
+ in_features = False
|
| |
+ elif in_features:
|
| |
+ line = re.sub(filt, '', line)
|
| |
+ if not line:
|
| |
+ continue
|
| |
+
|
| |
+ good_lines2 += [line]
|
| |
+
|
| |
+ return good_lines2
|
| |
+
|
| |
+
|
| |
+ def make_diff(path, lines1, mtime1, lines2, mtime2):
|
| |
+ relpath = "/".join(path.split("/")[-2:])
|
| |
+ return list(difflib.unified_diff(lines1, lines2,
|
| |
+ fromfile=path, tofile=path,
|
| |
+ fromfiledate=mtime1, tofiledate=mtime2))
|
| |
+
|
| |
|
| |
- editor = detect_editor()
|
| |
+ def make_patches(args, toml):
|
| |
+ """Returns up to two patches (automatic and manual).
|
| |
|
| |
+ For the manual patch, an editor is spawned on toml and a diff is
|
| |
+ made after the editor returns.
|
| |
+ """
|
| |
mtime_before = file_mtime(toml)
|
| |
toml_before = open(toml).readlines()
|
| |
|
| |
- # When we are editing a git checkout, we should not modify the real file.
|
| |
- # When we are editing an unpacked crate, we are free to edit anything.
|
| |
- # Let's keep the file name as close as possible to make editing easier.
|
| |
- if tmpfile:
|
| |
- tmpfile = tempfile.NamedTemporaryFile("w+t", dir=os.path.dirname(toml),
|
| |
- prefix="Cargo.", suffix=".toml")
|
| |
- tmpfile.writelines(toml_before)
|
| |
- tmpfile.flush()
|
| |
- fname = tmpfile.name
|
| |
+ diff1 = diff2 = None
|
| |
+
|
| |
+ # We always do the automatic part before asking the user for more edits.
|
| |
+ if toml_after := args.patch_foreign and drop_foreign_dependencies(toml_before):
|
| |
+ diff1 = make_diff(toml,
|
| |
+ toml_before, mtime_before,
|
| |
+ toml_after, mtime_before)
|
| |
else:
|
| |
- fname = toml
|
| |
- subprocess.check_call([editor, fname])
|
| |
- mtime_after = file_mtime(toml)
|
| |
- toml_after = open(fname).readlines()
|
| |
- toml_relpath = "/".join(toml.split("/")[-2:])
|
| |
- diff = list(difflib.unified_diff(toml_before, toml_after,
|
| |
- fromfile=toml_relpath, tofile=toml_relpath,
|
| |
- fromfiledate=mtime_before, tofiledate=mtime_after))
|
| |
- return diff
|
| |
+ toml_after = toml_before
|
| |
+
|
| |
+ if args.patch:
|
| |
+ with tempfile.NamedTemporaryFile("w+t", dir=os.path.dirname(toml),
|
| |
+ prefix="Cargo.", suffix=".toml") as tmpfile:
|
| |
+ tmpfile.writelines(toml_after)
|
| |
+ tmpfile.flush()
|
| |
+
|
| |
+ editor = detect_editor()
|
| |
+ subprocess.check_call([editor, tmpfile.name])
|
| |
+
|
| |
+ mtime_after2 = file_mtime(tmpfile.name)
|
| |
+ toml_after2 = open(tmpfile.name).readlines()
|
| |
+
|
| |
+ diff2 = make_diff(toml,
|
| |
+ toml_after, mtime_before,
|
| |
+ toml_after2, mtime_after2)
|
| |
+
|
| |
+ return diff1, diff2
|
| |
|
| |
def _is_path(path):
|
| |
return "/" in path or path in {".", ".."}
|
| |
@@ -300,12 +396,12 @@
|
| |
raise ValueError("--store-crate can only be used for a crate")
|
| |
|
| |
toml, crate, version, doc_files, license_files = local_toml(crate, version)
|
| |
- diff = make_patch(args, toml, tmpfile=True)
|
| |
+ diffs = make_patches(args, toml)
|
| |
metadata = Metadata.from_file(toml)
|
| |
if len(metadata) > 1:
|
| |
print(f"Warning: multiple metadata for {toml}")
|
| |
metadata = metadata[0]
|
| |
- return metadata.name, diff, metadata, doc_files, license_files
|
| |
+ return metadata.name, diffs, metadata, doc_files, license_files
|
| |
else:
|
| |
cratef, crate, version = download(crate, version)
|
| |
|
| |
@@ -313,14 +409,14 @@
|
| |
if not license_files:
|
| |
print(f"Warning: no license files detected in {crate}")
|
| |
|
| |
- diff = make_patch(args, toml)
|
| |
+ diffs = make_patches(args, toml)
|
| |
metadata = Metadata.from_file(toml)
|
| |
if len(metadata) > 1:
|
| |
print(f"Warning: multiple metadata for {toml}, ignoring everything except the first")
|
| |
metadata = metadata[0]
|
| |
if args.store_crate:
|
| |
shutil.copy2(cratef, os.path.join(os.getcwd(), f"{metadata.name}-{version}.crate"))
|
| |
- return crate, diff, metadata, doc_files, license_files
|
| |
+ return crate, diffs, metadata, doc_files, license_files
|
| |
|
| |
|
| |
def detect_rpmautospec(default_target, spec_file):
|
| |
@@ -417,8 +513,11 @@
|
| |
parser.add_argument("-t", "--target", action="store",
|
| |
choices=("plain", "fedora", "mageia", "opensuse"), default=default_target,
|
| |
help="Distribution target")
|
| |
+ parser.add_argument("--no-patch-foreign", action="store_false",
|
| |
+ default=True, dest="patch_foreign",
|
| |
+ help="Do not automatically drop foreign dependencies in Cargo.toml")
|
| |
parser.add_argument("-p", "--patch", action="store_true",
|
| |
- help="Do initial patching of Cargo.toml")
|
| |
+ help="Do manual patching of Cargo.toml")
|
| |
parser.add_argument("-s", "--store-crate", action="store_true",
|
| |
help="Store crate in current directory")
|
| |
parser.add_argument("-a", "--rpmautospec", action="store_true",
|
| |
@@ -469,11 +568,13 @@
|
| |
if args.crate is None:
|
| |
parser.error("crate/path argument missing and autodetection failed")
|
| |
|
| |
- crate, diff, metadata, doc_files, license_files = make_diff_metadata(args, args.crate, args.version)
|
| |
-
|
| |
- patch_file = f"{metadata.name}-fix-metadata.diff" if diff else None
|
| |
+ crate, diffs, metadata, doc_files, license_files = make_diff_metadata(args, args.crate, args.version)
|
| |
|
| |
pkg_name = package_name_suffixed(metadata.name, args.suffix)
|
| |
+
|
| |
+ patch_files = (f"{metadata.name}-automatic.diff" if diffs[0] else None,
|
| |
+ f"{metadata.name}-manual.diff" if diffs[1] else None)
|
| |
+
|
| |
spec_file = pathlib.Path(f"{pkg_name}.spec")
|
| |
|
| |
if args.target in {"fedora"} and args.existence_check and not os.path.exists(spec_file):
|
| |
@@ -522,7 +623,8 @@
|
| |
pkg_name = pkg_name,
|
| |
crate = crate,
|
| |
metadata = metadata,
|
| |
- patch_file = patch_file,
|
| |
+ patch_file_automatic=patch_files[0],
|
| |
+ patch_file_manual=patch_files[1],
|
| |
packager_identity = packager_identity,
|
| |
doc_files = doc_files,
|
| |
license_files = license_files,
|
| |
@@ -533,18 +635,20 @@
|
| |
if args.stdout:
|
| |
print(f"# {spec_file}")
|
| |
print(spec_contents)
|
| |
- if patch_file is not None:
|
| |
- print(f"# {patch_file}")
|
| |
- print("".join(diff), end="")
|
| |
+ for fname, diff in zip(patch_files, diffs):
|
| |
+ if fname:
|
| |
+ print(f"# {fname}")
|
| |
+ print("".join(diff), end="")
|
| |
else:
|
| |
with open(spec_file, "w") as fobj:
|
| |
fobj.write(spec_contents)
|
| |
fobj.write("\n")
|
| |
print(f'Wrote {fobj.name}')
|
| |
- if patch_file is not None:
|
| |
- with open(patch_file, "w") as fobj:
|
| |
- fobj.writelines(diff)
|
| |
- print(f'Wrote {fobj.name}')
|
| |
+ for fname, diff in zip(patch_files, diffs):
|
| |
+ if fname:
|
| |
+ with open(fname, "w") as fobj:
|
| |
+ fobj.writelines(diff)
|
| |
+ print(f'Wrote {fobj.name}')
|
| |
|
| |
if __name__ == "__main__":
|
| |
main()
|
| |
You could take a look at the test cases I wrote for the cfg expression evaluator I wrote a while ago:
https://github.com/ironthree/cargoman/blob/master/src/eval.rs#L78
The test cases were "inspired" by cfg expressions that I actually came across when dealing with various packages.