From d7caa1148d5fcac70030e3fb1eb698927e69960f Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 26 Oct 2018 11:20:13 +0200 Subject: [PATCH 7/8] 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 | 142 ++++++++-------- 6 files changed, 260 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..119dea5 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.replace(" ", "")) + 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..0d9a80b 100644 --- a/rust2rpm/templates/main.spec +++ b/rust2rpm/templates/main.spec @@ -48,82 +48,102 @@ 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(feature) }} + {% 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 }}" 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 +163,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