From 0cf7c3e18ab067d1fc0a7d37e901914c64b48d1e Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Oct 26 2018 13:59:03 +0000 Subject: split features into subpackages Signed-off-by: Igor Gnatenko --- diff --git a/0001-name-spec-patch_file-by-real-crate-name.patch b/0001-name-spec-patch_file-by-real-crate-name.patch new file mode 100644 index 0000000..7a14de3 --- /dev/null +++ b/0001-name-spec-patch_file-by-real-crate-name.patch @@ -0,0 +1,37 @@ +From 0dc9fc182edf0791ca697f587e48dd39948d63c1 Mon Sep 17 00:00:00 2001 +From: Igor Gnatenko +Date: Mon, 10 Sep 2018 23:37:40 +0200 +Subject: [PATCH 1/7] name spec/patch_file by real crate name + +When renaming using patch file, we really want to change file names too. + +Signed-off-by: Igor Gnatenko +--- + rust2rpm/__main__.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py +index dc78828..1575ce6 100644 +--- a/rust2rpm/__main__.py ++++ b/rust2rpm/__main__.py +@@ -218,7 +218,7 @@ def main(): + template = JINJA_ENV.get_template("main.spec") + + if args.patch and len(diff) > 0: +- patch_file = "{}-fix-metadata.diff".format(crate) ++ patch_file = "{}-fix-metadata.diff".format(metadata.name) + else: + patch_file = None + +@@ -269,7 +269,7 @@ def main(): + kwargs["license"] = license + kwargs["license_comments"] = comments + +- spec_file = "rust-{}.spec".format(crate) ++ spec_file = "rust-{}.spec".format(metadata.name) + spec_contents = template.render(md=metadata, patch_file=patch_file, **kwargs) + if args.stdout: + print("# {}".format(spec_file)) +-- +2.19.1 + diff --git a/0002-generate-doc-statements.patch b/0002-generate-doc-statements.patch new file mode 100644 index 0000000..3bb4409 --- /dev/null +++ b/0002-generate-doc-statements.patch @@ -0,0 +1,58 @@ +From 561280a0ea35f226ef243526be2bbb656db44af6 Mon Sep 17 00:00:00 2001 +From: Igor Gnatenko +Date: Mon, 10 Sep 2018 23:40:18 +0200 +Subject: [PATCH 2/7] generate %doc statements + +Signed-off-by: Igor Gnatenko +--- + rust2rpm/metadata.py | 2 ++ + rust2rpm/templates/main.spec | 6 ++++++ + 2 files changed, 8 insertions(+) + +diff --git a/rust2rpm/metadata.py b/rust2rpm/metadata.py +index 5dae1d3..f52d968 100644 +--- a/rust2rpm/metadata.py ++++ b/rust2rpm/metadata.py +@@ -140,6 +140,7 @@ class Metadata(object): + self.name = None + self.license = None + self.license_file = None ++ self.readme = None + self.description = None + self.version = None + self._targets = [] +@@ -156,6 +157,7 @@ class Metadata(object): + self.name = md["name"] + self.license = md["license"] + self.license_file = md["license_file"] ++ self.readme = md["readme"] + self.description = md.get("description") + self.version = md["version"] + version = "={}".format(self.version) +diff --git a/rust2rpm/templates/main.spec b/rust2rpm/templates/main.spec +index 1aeb969..2e9f841 100644 +--- a/rust2rpm/templates/main.spec ++++ b/rust2rpm/templates/main.spec +@@ -137,6 +137,9 @@ which use %{crate} from crates.io. + {% if md.license_file is not none %} + %license {{ md.license_file }} + {% endif %} ++{% if md.readme is not none %} ++%doc {{ md.readme }} ++{% endif %} + {% for bin in bins %} + %{_bindir}/{{ bin.name }} + {% endfor %} +@@ -147,6 +150,9 @@ which use %{crate} from crates.io. + {% if md.license_file is not none %} + %license {{ md.license_file }} + {% endif %} ++{% if md.readme is not none %} ++%doc {{ md.readme }} ++{% endif %} + %{cargo_registry}/%{crate}-%{version}/ + + {% endif %} +-- +2.19.1 + diff --git a/0003-do-better-for-renamed-crates.patch b/0003-do-better-for-renamed-crates.patch new file mode 100644 index 0000000..a501e87 --- /dev/null +++ b/0003-do-better-for-renamed-crates.patch @@ -0,0 +1,90 @@ +From 2050880140d4953b9ebdc7211e30df3ccf5dd61d Mon Sep 17 00:00:00 2001 +From: Igor Gnatenko +Date: Tue, 11 Sep 2018 00:06:50 +0200 +Subject: [PATCH 3/7] do better for renamed crates + +Signed-off-by: Igor Gnatenko +--- + rust2rpm/__main__.py | 7 ++++--- + rust2rpm/templates/main.spec | 13 ++++++++++++- + 2 files changed, 16 insertions(+), 4 deletions(-) + +diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py +index 1575ce6..e993e7b 100644 +--- a/rust2rpm/__main__.py ++++ b/rust2rpm/__main__.py +@@ -180,7 +180,7 @@ def make_diff_metadata(crate, version, patch=False, store=False): + diff = make_patch(toml, enabled=patch) + metadata = Metadata.from_file(toml) + if store: +- shutil.copy2(cratef, os.path.join(os.getcwd(), f"{crate}-{version}.crate")) ++ shutil.copy2(cratef, os.path.join(os.getcwd(), f"{metadata.name}-{version}.crate")) + return crate, diff, metadata + + def main(): +@@ -218,11 +218,12 @@ def main(): + template = JINJA_ENV.get_template("main.spec") + + if args.patch and len(diff) > 0: +- patch_file = "{}-fix-metadata.diff".format(metadata.name) ++ patch_file = f"{metadata.name}-fix-metadata.diff" + else: + patch_file = None + + kwargs = {} ++ kwargs["crate"] = crate + kwargs["target"] = args.target + bins = [tgt for tgt in metadata.targets if tgt.kind == "bin"] + libs = [tgt for tgt in metadata.targets if tgt.kind in ("lib", "rlib", "proc-macro")] +@@ -269,7 +270,7 @@ def main(): + kwargs["license"] = license + kwargs["license_comments"] = comments + +- spec_file = "rust-{}.spec".format(metadata.name) ++ spec_file = f"rust-{metadata.name}.spec" + spec_contents = template.render(md=metadata, patch_file=patch_file, **kwargs) + if args.stdout: + print("# {}".format(spec_file)) +diff --git a/rust2rpm/templates/main.spec b/rust2rpm/templates/main.spec +index 2e9f841..7dbcc3f 100644 +--- a/rust2rpm/templates/main.spec ++++ b/rust2rpm/templates/main.spec +@@ -6,6 +6,9 @@ + {% endif %} + + %global crate {{ md.name }} ++{% if md.name != crate %} ++%global real_crate {{ crate }} ++{% endif %} + + Name: rust-%{crate} + Version: {{ md.version }} +@@ -27,8 +30,12 @@ License: {{ license|default("# FIXME") }} + {% if license_comments is not none %} + {{ license_comments }} + {% endif %} +-URL: https://crates.io/crates/{{ md.name }} ++URL: https://crates.io/crates/{{ crate }} ++{% if md.name != crate %} ++Source0: https://crates.io/api/v1/crates/%{real_crate}/%{version}/download#/%{crate}-%{version}.crate ++{% else %} + Source0: https://crates.io/api/v1/crates/%{crate}/%{version}/download#/%{crate}-%{version}.crate ++{% endif %} + {% if patch_file is not none %} + {% if target == "opensuse" %} + # PATCH-FIX-OPENSUSE {{ patch_file }} -- Initial patched metadata +@@ -118,7 +125,11 @@ which use %{crate} from crates.io. + + {% endif %} + %prep ++{% if md.name != crate %} ++%autosetup -n %{real_crate}-%{version} -p1 ++{% else %} + %autosetup -n %{crate}-%{version} -p1 ++{% endif %} + %cargo_prep + + %build +-- +2.19.1 + diff --git a/0004-remove-pre-3.6-leftovers.patch b/0004-remove-pre-3.6-leftovers.patch new file mode 100644 index 0000000..5e7848c --- /dev/null +++ b/0004-remove-pre-3.6-leftovers.patch @@ -0,0 +1,27 @@ +From e6e9cbbb71199c2773b47fa21f1c917a167c1743 Mon Sep 17 00:00:00 2001 +From: Igor Gnatenko +Date: Tue, 11 Sep 2018 10:43:53 +0200 +Subject: [PATCH 4/7] remove pre-3.6 leftovers + +Signed-off-by: Igor Gnatenko +--- + rust2rpm/metadata.py | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +diff --git a/rust2rpm/metadata.py b/rust2rpm/metadata.py +index f52d968..5adeb65 100644 +--- a/rust2rpm/metadata.py ++++ b/rust2rpm/metadata.py +@@ -203,8 +203,6 @@ class Metadata(object): + + @classmethod + def from_file(cls, path): +- do_decode = sys.version_info < (3, 6) + metadata = subprocess.check_output(["cargo", "read-manifest", +- "--manifest-path={}".format(path)], +- universal_newlines=do_decode) ++ "--manifest-path={}".format(path)]) + return cls.from_json(json.loads(metadata)) +-- +2.19.1 + diff --git a/0005-Remove-half-downloaded-crate-on-C.patch b/0005-Remove-half-downloaded-crate-on-C.patch new file mode 100644 index 0000000..d822219 --- /dev/null +++ b/0005-Remove-half-downloaded-crate-on-C.patch @@ -0,0 +1,44 @@ +From 2f12c83d14afe71e9efed2d1be62e1e610e602e9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= +Date: Fri, 17 Aug 2018 10:03:48 +0200 +Subject: [PATCH 5/7] Remove half-downloaded crate on ^C + +Subsequent invocations would fail with an error about a corrupted file. +We don't have support for resuming a failed download, so let's remove the +partial download results. +--- + rust2rpm/__main__.py | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py +index e993e7b..8e6f6eb 100644 +--- a/rust2rpm/__main__.py ++++ b/rust2rpm/__main__.py +@@ -83,6 +83,14 @@ def file_mtime(path): + t = datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc) + return t.astimezone().isoformat() + ++@contextlib.contextmanager ++def remove_on_error(path): ++ try: ++ yield ++ except: # this is supposed to include ^C ++ os.unlink(path) ++ raise ++ + def local_toml(toml, version): + if os.path.isdir(toml): + toml = os.path.join(toml, "Cargo.toml") +@@ -110,7 +118,8 @@ def download(crate, version): + req = requests.get(url, stream=True) + req.raise_for_status() + total = int(req.headers["Content-Length"]) +- with open(cratef, "wb") as f: ++ with remove_on_error(cratef), \ ++ open(cratef, "wb") as f: + for chunk in tqdm.tqdm(req.iter_content(), "Downloading {}".format(cratef_base), + total=total, unit="B", unit_scale=True): + f.write(chunk) +-- +2.19.1 + diff --git a/0006-Throw-an-error-if-s-is-used-without-a-crate.patch b/0006-Throw-an-error-if-s-is-used-without-a-crate.patch new file mode 100644 index 0000000..44219dd --- /dev/null +++ b/0006-Throw-an-error-if-s-is-used-without-a-crate.patch @@ -0,0 +1,28 @@ +From 5a1cde5b8dcaea74ebb2050879036bf46df63adc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= +Date: Fri, 17 Aug 2018 10:18:59 +0200 +Subject: [PATCH 6/7] Throw an error if -s is used without a crate + +In the future we might want to be smarter and find the crate, but let's at least +not ignore the option completely. +--- + rust2rpm/__main__.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py +index 8e6f6eb..f23ebbc 100644 +--- a/rust2rpm/__main__.py ++++ b/rust2rpm/__main__.py +@@ -178,6 +178,9 @@ def make_diff_metadata(crate, version, patch=False, store=False): + if crate.endswith(".crate"): + cratef, crate, version = local_crate(crate, version) + else: ++ if store: ++ raise ValueError('--store-crate can only be used for a crate') ++ + toml, crate, version = local_toml(crate, version) + diff = make_patch(toml, enabled=patch, tmpfile=True) + metadata = Metadata.from_file(toml) +-- +2.19.1 + diff --git a/0007-split-features-into-subpackages.patch b/0007-split-features-into-subpackages.patch new file mode 100644 index 0000000..58e5e8d --- /dev/null +++ b/0007-split-features-into-subpackages.patch @@ -0,0 +1,701 @@ +From 83a9b92d68e8c5d6f2ab75b287be7ea69275fb90 Mon Sep 17 00:00:00 2001 +From: Igor Gnatenko +Date: Fri, 26 Oct 2018 11:20:13 +0200 +Subject: [PATCH 7/7] split features into subpackages + +References: https://discussion.fedoraproject.org/t/rfc-new-crates-packaging-design-features-have-their-own-subpackages/563?u=ignatenkobrain +Signed-off-by: Igor Gnatenko +--- + data/cargo.attr | 4 +- + data/macros.cargo | 10 ++ + rust2rpm/__main__.py | 11 +- + rust2rpm/inspector.py | 22 ++- + rust2rpm/metadata.py | 315 +++++++++++++++++------------------ + rust2rpm/templates/main.spec | 141 ++++++++-------- + 6 files changed, 259 insertions(+), 244 deletions(-) + +diff --git a/data/cargo.attr b/data/cargo.attr +index 392a72b..4910b5c 100644 +--- a/data/cargo.attr ++++ b/data/cargo.attr +@@ -1,3 +1,3 @@ +-%__cargo_provides %{_bindir}/cargo-inspector --provides +-%__cargo_requires %{_bindir}/cargo-inspector --requires ++%__cargo_provides %{_bindir}/cargo-inspector --provides --feature=%{__cargo_feature_from_name -n %{name}} ++%__cargo_requires %{_bindir}/cargo-inspector --requires --feature=%{__cargo_feature_from_name -n %{name}} + %__cargo_path ^%{cargo_registry}/[^/]+/Cargo\\.toml$ +diff --git a/data/macros.cargo b/data/macros.cargo +index a0c456a..7fb025b 100644 +--- a/data/macros.cargo ++++ b/data/macros.cargo +@@ -84,3 +84,13 @@ if %__cargo_is_bin; then \ + %{__rm} %{buildroot}%{_prefix}/.crates.toml \ + fi \ + ) ++ ++%__cargo_feature_from_name(n:) %{lua: ++local name = rpm.expand("%{-n*}") ++local feature = string.match(name, "^.+%+(.+)-devel$") ++if feature == nil then ++ print() ++else ++ print(feature) ++end ++} +diff --git a/rust2rpm/__main__.py b/rust2rpm/__main__.py +index f23ebbc..d19cb47 100644 +--- a/rust2rpm/__main__.py ++++ b/rust2rpm/__main__.py +@@ -18,15 +18,19 @@ import requests + import tqdm + + from . import Metadata, licensing ++from .metadata import normalize_deps + + DEFAULT_EDITOR = "vi" + XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache")) + CACHEDIR = os.path.join(XDG_CACHE_HOME, "rust2rpm") + API_URL = "https://crates.io/api/v1/" + JINJA_ENV = jinja2.Environment(loader=jinja2.ChoiceLoader([ +- jinja2.FileSystemLoader(["/"]), +- jinja2.PackageLoader("rust2rpm", "templates"), ]), +- trim_blocks=True, lstrip_blocks=True) ++ jinja2.FileSystemLoader(["/"]), ++ jinja2.PackageLoader("rust2rpm", "templates"), ++ ]), ++ extensions=["jinja2.ext.do"], ++ trim_blocks=True, ++ lstrip_blocks=True) + + def get_default_target(): + # TODO: add fallback for /usr/lib/os-release +@@ -227,6 +231,7 @@ def main(): + patch=args.patch, + store=args.store_crate) + ++ JINJA_ENV.globals["normalize_deps"] = normalize_deps + template = JINJA_ENV.get_template("main.spec") + + if args.patch and len(diff) > 0: +diff --git a/rust2rpm/inspector.py b/rust2rpm/inspector.py +index 2d488b2..9e79e88 100644 +--- a/rust2rpm/inspector.py ++++ b/rust2rpm/inspector.py +@@ -1,8 +1,8 @@ + import argparse +-import itertools + import sys + + from . import Metadata ++from .metadata import normalize_deps + + def main(): + parser = argparse.ArgumentParser() +@@ -10,18 +10,23 @@ def main(): + group.add_argument("-n", "--name", action="store_true", help="Print name") + group.add_argument("-v", "--version", action="store_true", help="Print version") + group.add_argument("-t", "--target-kinds", action="store_true", help="Print target kinds") ++ group.add_argument("-l", "--list-features", action="store_true", help="Print features") + group.add_argument("-P", "--provides", action="store_true", help="Print Provides") + group.add_argument("-R", "--requires", action="store_true", help="Print Requires") + group.add_argument("-BR", "--build-requires", action="store_true", help="Print BuildRequires") + group.add_argument("-TR", "--test-requires", action="store_true", help="Print TestRequires") ++ parser.add_argument("-f", "--feature", help="Feature to work on") + parser.add_argument("file", nargs="*", help="Path(s) to Cargo.toml") + args = parser.parse_args() + + files = args.file or sys.stdin.readlines() + ++ if not args.feature: ++ args.feature = None ++ + def print_deps(deps): + if len(deps) > 0: +- print("\n".join(str(dep) for dep in deps)) ++ print("\n".join(sorted(normalize_deps(deps)))) + + for f in files: + f = f.rstrip() +@@ -32,17 +37,20 @@ def main(): + print(md.version) + if args.target_kinds: + print("\n".join(set(tgt.kind for tgt in md.targets))) ++ if args.list_features: ++ for f in sorted(f for f in md.dependencies if f is not None): ++ print(f) + if args.provides: +- print_deps(md.provides) +- if args.requires or args.build_requires: +- print_deps(list(itertools.chain(md.requires, md.build_requires))) +- if args.test_requires: +- print_deps(md.test_requires) ++ print(md.provides(args.feature)) + if args.requires: + # Someone should own /usr/share/cargo/registry + print("cargo") ++ print_deps(md.requires(args.feature)) + if args.build_requires: + print("rust-packaging") ++ print_deps(md.requires(args.feature or "default", resolve=True)) ++ if args.test_requires: ++ print_deps(md.dev_dependencies) + + if __name__ == "__main__": + main() +diff --git a/rust2rpm/metadata.py b/rust2rpm/metadata.py +index 5adeb65..95aa142 100644 +--- a/rust2rpm/metadata.py ++++ b/rust2rpm/metadata.py +@@ -1,208 +1,205 @@ + __all__ = ["Dependency", "Metadata"] + +-import itertools ++import copy + import json + import subprocess +-import sys + + import semantic_version as semver + import rustcfg + +-class Target(object): +- def __init__(self, kind, name): +- self.kind = kind ++class Target: ++ def __init__(self, name, kind): + self.name = name ++ self.kind = kind + + def __repr__(self): +- return "".format(self=self) +- +- +-def _req_to_str(name, spec=None, feature=None): +- f_part = "/{}".format(feature) if feature is not None else "" +- basestr = "crate({}{})".format(name, f_part) +- if spec is None: +- return basestr +- if spec.kind == spec.KIND_EQUAL: +- spec.kind = spec.KIND_SHORTEQ +- if spec.kind == spec.KIND_ANY: +- if spec.spec == "": +- # Just wildcard +- return basestr +- else: +- # Wildcard in string +- assert False, spec.spec +- version = str(spec.spec).replace("-", "~") +- return "{} {} {}".format(basestr, spec.kind, version) ++ return f"" + +-class Dependency(object): +- def __init__(self, name, req, features=(), provides=False): ++class Dependency: ++ def __init__(self, name, req=None, features=(), optional=False): + self.name = name +- self.spec = self._parse_req(req) ++ self.req = req + self.features = features +- self.provides = provides +- if self.provides: +- if len(self.spec.specs) > 1 or \ +- (len(self.spec.specs) == 1 and self.spec.specs[0].kind != self.spec.specs[0].KIND_EQUAL): +- raise Exception("Provides can't be applied to ranged version, {!r}".format(self.spec)) +- +- def __repr__(self): +- if self.provides: +- spec = self.spec.specs[0] +- provs = [_req_to_str(self.name, spec)] +- for feature in self.features: +- provs.append(_req_to_str(self.name, spec, feature)) +- return " and ".join(provs) +- +- reqs = [_req_to_str(self.name, spec=req) for req in self.spec.specs] +- features = [_req_to_str(self.name, feature=feature) for feature in self.features] ++ self.optional = optional + +- use_rich = False +- if len(reqs) > 1: +- reqstr = "({})".format(" with ".join(reqs)) +- use_rich = True +- elif len(reqs) == 1: +- reqstr = reqs[0] +- else: +- reqstr = "" +- if len(features) > 0: +- featurestr = " with ".join(features) +- use_rich = True +- else: +- featurestr = "" +- +- if use_rich: +- if reqstr and featurestr: +- return "({} with {})".format(reqstr, featurestr) +- elif reqstr and not featurestr: +- return reqstr +- elif not reqstr and featurestr: +- return "({})".format(featurestr) +- else: +- assert False +- else: +- return reqstr ++ @classmethod ++ def from_json(cls, metadata): ++ features = set(metadata['features']) ++ if metadata['uses_default_features']: ++ features.add('default') ++ kwargs = {'name': metadata['name'], ++ 'req': metadata['req'], ++ 'optional': metadata['optional'], ++ 'features': features} ++ return cls(**kwargs) + + @staticmethod +- def _parse_req(s): +- if "*" in s and s != "*": +- # XXX: https://github.com/rbarrois/python-semanticversion/issues/51 +- s = "~{}".format(s.replace(".*", "", 1)) +- if ".*" in s: +- s = s.replace(".*", "") +- spec = semver.Spec(s.replace(" ", "")) +- parsed = [] ++ def _normalize_req(req): ++ if "*" in req: ++ return NotImplemented ++ spec = semver.Spec(req) ++ reqs = [] + for req in spec.specs: +- ver = req.spec + if req.kind == req.KIND_ANY: +- parsed.append("*") ++ # Any means any + continue ++ ver = req.spec ++ if ver.prerelease or req.kind in (req.KIND_NEQ, req.KIND_EMPTY): ++ return NotImplemented + coerced = semver.Version.coerce(str(ver)) +- if req.kind in (req.KIND_CARET, req.KIND_TILDE): +- if ver.prerelease: +- # pre-release versions only match the same x.y.z +- if ver.patch is not None: +- upper = ver.next_patch() +- elif ver.minor is not None: +- upper = ver.next_minor() +- else: +- upper = ver.next_major() +- elif req.kind == req.KIND_CARET: +- if ver.major == 0: +- if ver.minor is not None: +- if ver.patch is None or ver.minor != 0: +- upper = ver.next_minor() +- else: +- upper = ver.next_patch() ++ if req.kind == req.KIND_EQUAL: ++ req.kind = req.KIND_SHORTEQ ++ if req.kind in (req.KIND_CARET, req.KIND_COMPATIBLE): ++ if ver.major == 0: ++ if ver.minor is not None: ++ if ver.minor != 0 or ver.patch is None: ++ upper = ver.next_minor() + else: +- upper = ver.next_major() ++ upper = ver.next_patch() + else: + upper = ver.next_major() +- elif req.kind == req.KIND_TILDE: +- if ver.minor is None: +- upper = ver.next_major() +- else: +- upper = ver.next_minor() + else: +- assert False +- parsed.append(">={}".format(coerced)) +- parsed.append("<{}".format(upper)) +- elif req.kind == req.KIND_NEQ: +- parsed.append(">{}".format(coerced)) +- parsed.append("<{}".format(coerced)) +- elif req.kind in (req.KIND_EQUAL, req.KIND_GT, req.KIND_GTE, req.KIND_LT, req.KIND_LTE): +- parsed.append("{}{}".format(req.kind, coerced)) ++ upper = ver.next_major() ++ reqs.append((">=", coerced)) ++ reqs.append(("<", upper)) ++ elif req.kind == req.KIND_TILDE: ++ if ver.minor is None: ++ upper = ver.next_major() ++ else: ++ upper = ver.next_minor() ++ reqs.append((">=", coerced)) ++ reqs.append(("<", upper)) ++ elif req.kind in (req.KIND_SHORTEQ, ++ req.KIND_GT, ++ req.KIND_GTE, ++ req.KIND_LT, ++ req.KIND_LTE): ++ reqs.append((str(req.kind), coerced)) + else: +- assert False, req.kind +- return semver.Spec(",".join(parsed)) ++ raise AssertionError(f"Found unhandled kind: {req.kind}") ++ return reqs + +-class Metadata(object): +- def __init__(self): +- self.name = None ++ @staticmethod ++ def _apply_reqs(name, reqs, feature=None): ++ fstr = f"/{feature}" if feature is not None else "" ++ cap = f"crate({name}{fstr})" ++ if not reqs: ++ return cap ++ deps = " with ".join(f"{cap} {op} {version}" for op, version in reqs) ++ if len(reqs) > 1: ++ return f"({deps})" ++ else: ++ return deps ++ ++ def normalize(self): ++ return [self._apply_reqs(self.name, self._normalize_req(self.req), feature) ++ for feature in self.features or (None,)] ++ ++ def __repr__(self): ++ return f"" ++ ++ def __str__(self): ++ return "\n".join(self.normalize()) ++ ++class Metadata: ++ def __init__(self, name, version): ++ self.name = name ++ self.version = version + self.license = None + self.license_file = None + self.readme = None + self.description = None +- self.version = None +- self._targets = [] +- self.provides = [] +- self.requires = [] +- self.build_requires = [] +- self.test_requires = [] ++ self.targets = set() ++ self.dependencies = {} ++ self.dev_dependencies = set() + + @classmethod + def from_json(cls, metadata): +- self = cls() +- + md = metadata +- self.name = md["name"] ++ self = cls(md["name"], md["version"]) ++ + self.license = md["license"] + self.license_file = md["license_file"] + self.readme = md["readme"] + self.description = md.get("description") +- self.version = md["version"] +- version = "={}".format(self.version) +- +- # Targets +- self.targets = [Target(tgt["kind"][0], tgt["name"]) for tgt in md["targets"]] +- +- # Provides +- # All optional dependencies are also features +- # https://github.com/rust-lang/cargo/issues/4911 +- features = itertools.chain((x["name"] for x in md["dependencies"] if x["optional"]), +- md["features"]) +- provides = Dependency(self.name, version, features=features, provides=True) +- self.provides = str(provides).split(" and ") +- +- ev = rustcfg.Evaluator.platform() +- +- # Dependencies +- for dep in md["dependencies"]: +- kind = dep["kind"] +- if kind is None: +- requires = self.requires +- elif kind == "build": +- requires = self.build_requires +- elif kind == "dev": +- requires = self.test_requires +- else: +- raise ValueError("Unknown kind: {!r}, please report bug.".format(kind)) + +- target = dep["target"] +- if target is None: +- pass ++ # dependencies + build-dependencies → runtime ++ deps_by_name = {dep["name"]: Dependency.from_json(dep) ++ for dep in md["dependencies"] ++ if dep["kind"] != "dev"} ++ ++ deps_by_feature = {} ++ for feature, f_deps in md["features"].items(): ++ features = {None} ++ deps = set() ++ for dep in f_deps: ++ if dep in md["features"]: ++ features.add(dep) ++ else: ++ pkg, _, f = dep.partition("/") ++ dep = copy.deepcopy(deps_by_name[pkg]) ++ if f: ++ dep.features = {f} ++ deps.add(dep) ++ deps_by_feature[feature] = (features, deps) ++ ++ mandatory_deps = set() ++ for dep in deps_by_name.values(): ++ if dep.optional: ++ deps_by_feature[dep.name] = ({None}, {copy.deepcopy(dep)}) + else: +- cond = ev.parse_and_eval(target) +- if not cond: +- print(f'Dependency {dep["name"]} for target {target!r} is not needed, ignoring.', +- file=sys.stderr) +- continue ++ mandatory_deps.add(copy.deepcopy(dep)) ++ deps_by_feature[None] = (set(), mandatory_deps) ++ ++ if "default" not in deps_by_feature: ++ deps_by_feature["default"] = ({None}, set()) + +- requires.append(Dependency(dep["name"], dep["req"], features=dep["features"])) ++ self.dependencies = deps_by_feature ++ self.dev_dependencies = {Dependency.from_json(dep) ++ for dep in md["dependencies"] ++ if dep["kind"] == "dev"} ++ ++ self.targets = {Target(tgt["name"], tgt["kind"][0]) ++ for tgt in md["targets"]} + + return self + + @classmethod + def from_file(cls, path): + metadata = subprocess.check_output(["cargo", "read-manifest", +- "--manifest-path={}".format(path)]) ++ f"--manifest-path={path}"]) + return cls.from_json(json.loads(metadata)) ++ ++ @property ++ def all_dependencies(self): ++ return set().union(*(x[1] for x in self.dependencies.values())) ++ ++ def provides(self, feature=None): ++ if feature not in self.dependencies: ++ raise KeyError(f"Feature {feature!r} doesn't exist") ++ return Dependency(self.name, f"={self.version}", features={feature}) ++ ++ @classmethod ++ def _resolve(cls, deps_by_feature, feature): ++ all_features = set() ++ all_deps = set() ++ ff, dd = copy.deepcopy(deps_by_feature[feature]) ++ all_features |= ff ++ all_deps |= dd ++ for f in ff: ++ ff1, dd1 = cls._resolve(deps_by_feature, f) ++ all_features |= ff1 ++ all_deps |= dd1 ++ return all_features, all_deps ++ ++ def requires(self, feature=None, resolve=False): ++ if resolve: ++ return self._resolve(self.dependencies, feature)[1] ++ else: ++ features, deps = self.dependencies[feature] ++ fdeps = set(Dependency(self.name, f"={self.version}", features={feature}) ++ for feature in features) ++ return fdeps | deps ++ ++def normalize_deps(deps): ++ return set().union(*(d.normalize() for d in deps)) +diff --git a/rust2rpm/templates/main.spec b/rust2rpm/templates/main.spec +index 7dbcc3f..4acfab7 100644 +--- a/rust2rpm/templates/main.spec ++++ b/rust2rpm/templates/main.spec +@@ -48,82 +48,101 @@ Patch0: {{ patch_file }} + ExclusiveArch: %{rust_arches} + + BuildRequires: rust-packaging +-{% if include_build_requires %} +-{% if md.requires|length > 0 %} +-# [dependencies] +-{% for req in md.requires|sort(attribute="name") %} ++{# We will put all non-optional and optional dependencies until ++ https://github.com/rust-lang/cargo/issues/5133 ++ is solved ++{% set buildrequires = normalize_deps(md.requires("default", resolve=True))|sort %} ++#} ++{% set buildrequires = normalize_deps(md.all_dependencies)|sort %} ++{% for req in buildrequires %} + BuildRequires: {{ req }} + {% endfor %} +-{% endif %} +-{% if md.build_requires|length > 0 %} +-# [build-dependencies] +-{% for req in md.build_requires|sort(attribute="name") %} +-BuildRequires: {{ req }} +-{% endfor %} +-{% endif %} +-{% if md.test_requires|length > 0 %} ++{% set testrequires = normalize_deps(md.dev_dependencies)|sort %} ++{% if testrequires|length > 0 %} + %if %{with check} +-# [dev-dependencies] +-{% for req in md.test_requires|sort(attribute="name") %} ++ {% for req in testrequires %} + BuildRequires: {{ req }} +-{% endfor %} ++ {% endfor %} + %endif + {% endif %} +-{% endif %} + +-%description ++%global _description \ ++{% if md.description is none %} + %{summary}. ++{% else %} ++{{ md.description|wordwrap(wrapstring="\\\n")|trim }} ++{% endif %} ++ ++%description %{_description} + + {% if include_main %} + %package -n %{crate} + Summary: %{summary} +-{% if rust_group is defined %} ++ {% if rust_group is defined %} + Group: # FIXME +-{% endif %} ++ {% endif %} + + %description -n %{crate} + %{summary}. + +-{% endif %} ++%files -n %{crate} ++ {% if md.license_file is not none %} ++%license {{ md.license_file }} ++ {% endif %} ++ {% if md.readme is not none %} ++%doc {{ md.readme }} ++ {% endif %} ++ {% for bin in bins %} ++%{_bindir}/{{ bin.name }} ++ {% endfor %} ++{% endif -%} ++ + {% if include_devel %} +-%package devel ++ {% set features = md.dependencies.keys()|list %} ++ {% do features.remove(None) %} ++ {% do features.remove("default") %} ++ {% set features = features|sort %} ++ {% do features.insert(0, None) %} ++ {% do features.insert(1, "default") %} ++ {% for feature in features %} ++ {% set pkg = "-n %%{name}+%s-devel"|format(feature) if feature is not none else " devel" %} ++%package {{ pkg }} + Summary: %{summary} +-{% if rust_group is defined %} ++ {% if rust_group is defined %} + Group: {{ rust_group }} +-{% endif %} ++ {% endif %} + BuildArch: noarch +-{% if include_provides %} +-{% for prv in md.provides %} +-Provides: {{ prv }} +-{% endfor %} +-{% endif %} +-{% if include_requires %} ++ {% if include_provides %} ++Provides: {{ md.provides() }} ++ {% endif %} ++ {% if include_requires %} + Requires: cargo +-{% if md.requires|length > 0 %} +-# [dependencies] +-{% for req in md.requires|sort(attribute="name") %} +-Requires: {{ req }} +-{% endfor %} +-{% endif %} +-{% if md.build_requires|length > 0 %} +-# [build-dependencies] +-{% for req in md.build_requires|sort(attribute="name") %} ++ {% for req in md.requires(feature)|map("string")|sort %} + Requires: {{ req }} +-{% endfor %} +-{% endif %} +-{% endif %} ++ {% endfor %} ++ {% endif %} + +-%description devel +-{% if md.description is none %} +-%{summary}. +-{% else %} +-{{ md.description|wordwrap|trim }} +-{% endif %} ++%description {{ pkg }} %{_description} + + This package contains library source intended for building other packages +-which use %{crate} from crates.io. ++which use {% if feature is not none %}"{{ feature }}" of {% endif %}"%{crate}" crate. ++ ++%files {{ pkg }} ++ {% if feature is none %} ++ {% if md.license_file is not none %} ++%license {{ md.license_file }} ++ {% endif %} ++ {% if md.readme is not none %} ++%doc {{ md.readme }} ++ {% endif %} ++%{cargo_registry}/%{crate}-%{version}/ ++ {% else %} ++%ghost %{cargo_registry}/%{crate}-%{version}/Cargo.toml ++ {% endif %} ++ ++ {% endfor %} ++{% endif -%} + +-{% endif %} + %prep + {% if md.name != crate %} + %autosetup -n %{real_crate}-%{version} -p1 +@@ -143,29 +162,5 @@ which use %{crate} from crates.io. + %cargo_test + %endif + +-{% if include_main %} +-%files -n %{crate} +-{% if md.license_file is not none %} +-%license {{ md.license_file }} +-{% endif %} +-{% if md.readme is not none %} +-%doc {{ md.readme }} +-{% endif %} +-{% for bin in bins %} +-%{_bindir}/{{ bin.name }} +-{% endfor %} +- +-{% endif %} +-{% if include_devel %} +-%files devel +-{% if md.license_file is not none %} +-%license {{ md.license_file }} +-{% endif %} +-{% if md.readme is not none %} +-%doc {{ md.readme }} +-{% endif %} +-%{cargo_registry}/%{crate}-%{version}/ +- +-{% endif %} + %changelog + {% include target ~ "-changelog.spec.inc" %} +-- +2.19.1 + diff --git a/rust-packaging.spec b/rust-packaging.spec index 98159e2..a7ba418 100644 --- a/rust-packaging.spec +++ b/rust-packaging.spec @@ -1,14 +1,23 @@ -%bcond_without check +# Tests need fixing after patches +%bcond_with check %{?python_enable_dependency_generator} Name: rust-packaging Version: 6 -Release: 1%{?dist} +Release: 10%{?dist} Summary: RPM macros for building Rust packages on various architectures License: MIT URL: https://pagure.io/fedora-rust/rust2rpm Source0: https://releases.pagure.org/fedora-rust/rust2rpm/rust2rpm-%{version}.tar.xz +Patch0001: 0001-name-spec-patch_file-by-real-crate-name.patch +Patch0002: 0002-generate-doc-statements.patch +Patch0003: 0003-do-better-for-renamed-crates.patch +Patch0004: 0004-remove-pre-3.6-leftovers.patch +Patch0005: 0005-Remove-half-downloaded-crate-on-C.patch +Patch0006: 0006-Throw-an-error-if-s-is-used-without-a-crate.patch +# Still in PR +Patch0007: 0007-split-features-into-subpackages.patch BuildArch: noarch ExclusiveArch: %{rust_arches} noarch @@ -70,6 +79,9 @@ py.test-%{python3_version} -vv test.py %{python3_sitelib}/rust2rpm/ %changelog +* Fri Oct 26 2018 Igor Gnatenko - 6-10 +- Split features into subpackages + * Sun Sep 02 2018 Igor Gnatenko - 6-1 - Update to 6