#60 WIP: split features into subpackages
Merged 7 months ago by ignatenkobrain. Opened 8 months ago by ignatenkobrain.
fedora-rust/ ignatenkobrain/rust2rpm splitkokot  into  master

file added
+26

@@ -0,0 +1,26 @@ 

+ # rust2rpm

+ 

+ Convert Rust crates to RPM.

+ 

+ ## `.rust2rpm.conf`

+ 

+ You can place configuration file which is used as source for additional

+ information for spec generation.

+ 

+ Some simple example would be better than many words ;)

+ 

+ ```ini

+ [DEFAULT]

+ buildrequires =

+   pkgconfig(foo) >= 1.2.3

+ lib.requires =

+   pkgconfig(foo) >= 1.2.3

+ 

+ [fedora]

+ bin.requires =

+   findutils

+ buildrequires =

+ lib.requires =

+ lib+default.requires =

+   pkgconfig(bar) >= 2.0.0

+ ```

file removed
-5

@@ -1,5 +0,0 @@ 

- ========

- rust2rpm

- ========

- 

- Convert Rust crates to RPM.

file modified
+2 -2

@@ -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$

file modified
+10

@@ -84,3 +84,13 @@ 

    %{__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

+ }

file modified
+32 -14

@@ -18,15 +18,19 @@ 

  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

@@ -75,7 +79,7 @@ 

      if git is not None:

          name = subprocess.check_output([git, "config", "user.name"], universal_newlines=True).strip()

          email = subprocess.check_output([git, "config", "user.email"], universal_newlines=True).strip()

-         return "{} <{}>".format(name, email)

+         return f"{name} <{email}>"

  

      return None

  

@@ -104,23 +108,23 @@ 

  def download(crate, version):

      if version is None:

          # Now we need to get latest version

-         url = requests.compat.urljoin(API_URL, "crates/{}/versions".format(crate))

+         url = requests.compat.urljoin(API_URL, f"crates/{crate}/versions")

          req = requests.get(url)

          req.raise_for_status()

          versions = req.json()["versions"]

          version = next(version["num"] for version in versions if not version["yanked"])

  

      os.makedirs(CACHEDIR, exist_ok=True)

-     cratef_base = "{}-{}.crate".format(crate, version)

+     cratef_base = f"{crate}-{version}.crate"

      cratef = os.path.join(CACHEDIR, cratef_base)

      if not os.path.isfile(cratef):

-         url = requests.compat.urljoin(API_URL, "crates/{}/{}/download#".format(crate, version))

+         url = requests.compat.urljoin(API_URL, f"crates/{crate}/{version}/download#")

          req = requests.get(url, stream=True)

          req.raise_for_status()

          total = int(req.headers["Content-Length"])

          with remove_on_error(cratef), \

               open(cratef, "wb") as f:

-             for chunk in tqdm.tqdm(req.iter_content(), "Downloading {}".format(cratef_base),

+             for chunk in tqdm.tqdm(req.iter_content(), f"Downloading {cratef_base}".format(cratef_base),

                                     total=total, unit="B", unit_scale=True):

                  f.write(chunk)

      return cratef, crate, version

@@ -128,14 +132,14 @@ 

  @contextlib.contextmanager

  def toml_from_crate(cratef, crate, version):

      with tempfile.TemporaryDirectory() as tmpdir:

-         target_dir = "{}/".format(tmpdir)

+         target_dir = f"{tmpdir}/"

          with tarfile.open(cratef, "r") as archive:

              for n in archive.getnames():

                  if not os.path.abspath(os.path.join(target_dir, n)).startswith(target_dir):

                      raise Exception("Unsafe filenames!")

              archive.extractall(target_dir)

-         toml_relpath = "{}-{}/Cargo.toml".format(crate, version)

-         toml = "{}/{}".format(tmpdir, toml_relpath)

+         toml_relpath = f"{crate}-{version}/Cargo.toml"

+         toml = f"{tmpdir}/{toml_relpath}"

          if not os.path.isfile(toml):

              raise IOError("crate does not contain Cargo.toml file")

          yield toml

@@ -195,6 +199,11 @@ 

          shutil.copy2(cratef, os.path.join(os.getcwd(), f"{metadata.name}-{version}.crate"))

      return crate, diff, metadata

  

+ def to_list(s):

+     if not s:

+         return []

+     return list(filter(None, (l.strip() for l in s.splitlines())))

+ 

  def main():

      parser = argparse.ArgumentParser("rust2rpm",

                                       formatter_class=argparse.RawTextHelpFormatter)

@@ -227,6 +236,8 @@ 

                                                 patch=args.patch,

                                                 store=args.store_crate)

  

+     JINJA_ENV.globals["normalize_deps"] = normalize_deps

+     JINJA_ENV.globals["to_list"] = to_list

      template = JINJA_ENV.get_template("main.spec")

  

      if args.patch and len(diff) > 0:

@@ -259,7 +270,7 @@ 

          kwargs["include_provides"] = True

          kwargs["include_requires"] = True

      else:

-         assert False, "Unknown target {!r}".format(args.target)

+         assert False, f"Unknown target {args.target!r}"

  

      if args.target == "mageia":

          kwargs["pkg_release"] = "%mkrel 1"

@@ -282,13 +293,20 @@ 

          kwargs["license"] = license

          kwargs["license_comments"] = comments

  

+     conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())

+     conf.read(".rust2rpm.conf")

+     if args.target not in conf:

+         conf.add_section(args.target)

+ 

+     kwargs["distconf"] = conf[args.target]

+ 

      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))

+         print(f"# {spec_file}")

          print(spec_contents)

          if patch_file is not None:

-             print("# {}".format(patch_file))

+             print(f"# {patch_file}")

              print("".join(diff), end="")

      else:

          with open(spec_file, "w") as fobj:

file modified
+15 -7

@@ -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 @@ 

      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 @@ 

              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()

file modified
+165 -158

@@ -1,208 +1,215 @@ 

  __all__ = ["Dependency", "Metadata"]

  

- import itertools

+ import collections

+ 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 "<Target {self.kind}|{self.name}>".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"<Target {self.name} ({self.kind})>"

  

- 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 and req != "*":

+             raise NotImplementedError(f"'*' is not supported: {req}")

+         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:

+                 raise NotImplementedError(f"Pre-release requirement is not supported: {ver}")

+             if req.kind in (req.KIND_NEQ, req.KIND_EMPTY):

+                 raise NotImplementedError(f"'!=' and empty kinds are not supported: {req}")

              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

+ 

+     @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"<Dependency: {self.name} {self.req} ({', '.join(sorted(self.features))})>"

+ 

+     def __str__(self):

+         return "\n".join(self.normalize())

  

- class Metadata(object):

-     def __init__(self):

-         self.name = None

+ 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

+         # dependencies + build-dependencies → runtime

+         deps_by_name = collections.defaultdict(list)

          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))

+             if dep["kind"] == "dev":

+                 continue

+             deps_by_name[dep["name"]].append(Dependency.from_json(dep))

+ 

+         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("/")

+                     for dep in deps_by_name[pkg]:

+                         dep = copy.deepcopy(dep)

+                         if f:

+                             dep.features = {f}

+                         deps.add(dep)

+             deps_by_feature[feature] = (features, deps)

+ 

+         mandatory_deps = set()

+         for name, deps in deps_by_name.items():

+             fdeps = set()

+             for dep in deps:

+                 if dep.optional:

+                     fdeps.add(copy.deepcopy(dep))

+                 else:

+                     mandatory_deps.add(copy.deepcopy(dep))

+             if fdeps:

+                 deps_by_feature[name] = ({None}, fdeps)

+         deps_by_feature[None] = (set(), mandatory_deps)

  

-             target = dep["target"]

-             if target is None:

-                 pass

-             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

+         if "default" not in deps_by_feature:

+             deps_by_feature["default"] = ({None}, set())

+ 

+         self.dependencies = deps_by_feature

+         self.dev_dependencies = {Dependency.from_json(dep)

+                                  for dep in md["dependencies"]

+                                  if dep["kind"] == "dev"}

  

-             requires.append(Dependency(dep["name"], dep["req"], features=dep["features"]))

+         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))

file modified
+83 -72

@@ -48,82 +48,117 @@ 

  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 %}

+ {% for req in to_list(distconf.get("buildrequires"))|sort %}

+ BuildRequires:  {{ req }}

+ {% endfor %}

  

- %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 %}

+   {% for req in to_list(distconf.get("bin.requires"))|sort %}

+ Requires:       {{ req }}

+   {% endfor %}

  

  %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 %}

+     {% if feature is none %}

+       {% set pkg = "   devel" %}

+       {% set conf_prefix = "lib" %}

+     {% else %}

+       {% set pkg = "-n %%{name}+%s-devel"|format(feature) %}

+       {% set conf_prefix = "lib+%s"|format(feature) %}

+     {% endif %}

+ %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") %}

+       {% for req in md.requires(feature)|map("string")|sort %}

  Requires:       {{ req }}

- {% endfor %}

- {% endif %}

- {% if md.build_requires|length > 0 %}

- # [build-dependencies]

- {% for req in md.build_requires|sort(attribute="name") %}

+       {% endfor %}

+     {% endif %}

+     {% for req in to_list(distconf.get("%s.requires"|format(conf_prefix)))|sort %}

  Requires:       {{ req }}

- {% endfor %}

- {% endif %}

- {% endif %}

+     {% endfor %}

  

- %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 +178,5 @@ 

  %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" %}

file modified
+36 -311

@@ -1,318 +1,43 @@ 

- import os

- import shutil

- import subprocess

- import sys

- import tempfile

- import textwrap

- 

  import pytest

  

  import rust2rpm

  

- DUMMY_LIB = """

- pub fn say_hello() {

-     println!("Hello, World!");

- }

- """

- DEPGEN = os.path.join(os.path.dirname(__file__), "cargodeps.py")

- 

- 

- @pytest.mark.parametrize("req, features, rpmdep", [

-     ("=1.0.0", [],

-      "crate(test) = 1.0.0"),

-     ("=1.0.0", ["feature"],

-      "(crate(test) = 1.0.0 with crate(test/feature))"),

-     (">=1.0.0,<2.0.0", [],

+ @pytest.mark.parametrize("req, rpmdep", [

+     ("^1.2.3",

+      "(crate(test) >= 1.2.3 with crate(test) < 2.0.0)"),

+     ("^1.2",

+      "(crate(test) >= 1.2.0 with crate(test) < 2.0.0)"),

+     ("^1",

       "(crate(test) >= 1.0.0 with crate(test) < 2.0.0)"),

-     (">=1.0.0,<2.0.0", ["feature"],

-      "((crate(test) >= 1.0.0 with crate(test) < 2.0.0) with crate(test/feature))"),

+     ("^0.2.3",

+      "(crate(test) >= 0.2.3 with crate(test) < 0.3.0)"),

+     ("^0.2",

+      "(crate(test) >= 0.2.0 with crate(test) < 0.3.0)"),

+     ("^0.0.3",

+      "(crate(test) >= 0.0.3 with crate(test) < 0.0.4)"),

+     ("^0.0",

+      "(crate(test) >= 0.0.0 with crate(test) < 0.1.0)"),

+     ("^0",

+      "(crate(test) >= 0.0.0 with crate(test) < 1.0.0)"),

+     ("~1.2.3",

+      "(crate(test) >= 1.2.3 with crate(test) < 1.3.0)"),

+     ("~1.2",

+      "(crate(test) >= 1.2.0 with crate(test) < 1.3.0)"),

+     ("~1",

+      "(crate(test) >= 1.0.0 with crate(test) < 2.0.0)"),

+     ("*",

+      "crate(test)"),

+     (">= 1.2.0",

+      "crate(test) >= 1.2.0"),

+     ("> 1",

+      "crate(test) > 1.0.0"),

+     ("< 2",

+      "crate(test) < 2.0.0"),

+     ("= 1.2.3",

+      "crate(test) = 1.2.3"),

+     (">= 1.2, < 1.5",

+      "(crate(test) >= 1.2.0 with crate(test) < 1.5.0)"),

  ])

- def test_dependency(req, features, rpmdep):

-     dep = rust2rpm.Dependency("test", req, features)

+ def test_dependency(req, rpmdep):

+     dep = rust2rpm.Dependency("test", req)

      assert str(dep) == rpmdep

- 

- @pytest.fixture

- def cargo_toml(request):

-     def make_cargo_toml(contents):

-         toml = os.path.join(tmpdir, "Cargo.toml")

-         with open(toml, "w") as fobj:

-             fobj.write(textwrap.dedent(contents))

-         return toml

- 

-     tmpdir = tempfile.mkdtemp(prefix="cargo-deps-")

-     srcdir = os.path.join(tmpdir, "src")

-     os.mkdir(srcdir)

-     with open(os.path.join(srcdir, "lib.rs"), "w") as fobj:

-         fobj.write(DUMMY_LIB)

- 

-     def finalize():

-         shutil.rmtree(tmpdir)

-     request.addfinalizer(finalize)

- 

-     return make_cargo_toml

- 

- @pytest.mark.parametrize("toml, provides, requires", [

- 

-     # Basic provides

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

-      """,

-      ["crate(hello) = 0.0.0"],

-      []),

- 

-     # Basic provides for feature

-     ("""

-      [package]

-      name = "hello"

-      version = "1.2.3"

- 

-      [features]

-      color = []

-      """,

-      ["crate(hello) = 1.2.3",

-       "crate(hello/color) = 1.2.3"],

-      []),

- 

-     # Provides for optional dependencies

-     ("""

-      [package]

-      name = "hello"

-      version = "1.2.3"

- 

-      [dependencies]

-      non_optional = "1"

-      serde = { version = "1", optional = true }

-      rand = { version = "0.4", optional = true }

- 

-      [features]

-      std = []

-      v1 = ["rand"]

-      """,

-      ["crate(hello) = 1.2.3",

-       "crate(hello/rand) = 1.2.3",

-       "crate(hello/serde) = 1.2.3",

-       "crate(hello/std) = 1.2.3",

-       "crate(hello/v1) = 1.2.3"],

-      ["(crate(non_optional) >= 1.0.0 with crate(non_optional) < 2.0.0)",

-       "(crate(rand) >= 0.4.0 with crate(rand) < 0.5.0)",

-       "(crate(serde) >= 1.0.0 with crate(serde) < 2.0.0)"]),

- 

-     # Caret requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^0"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 0.0.0 with crate(libc) < 1.0.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^0.0"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 0.0.0 with crate(libc) < 0.1.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^0.0.3"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 0.0.3 with crate(libc) < 0.0.4)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^0.2.3"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 0.2.3 with crate(libc) < 0.3.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^1"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.0.0 with crate(libc) < 2.0.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^1.2"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.0 with crate(libc) < 2.0.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "^1.2.3"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.3 with crate(libc) < 2.0.0)"]),

- 

-     # Tilde requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "~1"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.0.0 with crate(libc) < 2.0.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "~1.2"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.0 with crate(libc) < 1.3.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "~1.2.3"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.3 with crate(libc) < 1.3.0)"]),

- 

-     # Wildcard requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "*"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["crate(libc)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "1.*"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.0.0 with crate(libc) < 2.0.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "1.2.*"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.0 with crate(libc) < 1.3.0)"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "1.*.*"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.0.0 with crate(libc) < 2.0.0)"]),

- 

-     # Inequality requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = ">= 1.2.0"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["crate(libc) >= 1.2.0"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "> 1"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["crate(libc) > 1.0.0"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "< 2"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["crate(libc) < 2.0.0"]),

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = "= 1.2.3"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["crate(libc) = 1.2.3"]),

- 

-     # Multiple requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0"

- 

-      [dependencies]

-      libc = ">= 1.2, < 1.5"

-      """,

-      ["crate(hello) = 0.0.0"],

-      ["(crate(libc) >= 1.2.0 with crate(libc) < 1.5.0)"]),

- 

-     # Pre-release requirements

-     ("""

-      [package]

-      name = "hello"

-      version = "0.0.0-alpha"

- 

-      [dependencies]

-      foo-bar = "1.2.3-beta"

-      """,

-      ["crate(hello) = 0.0.0~alpha"],

-      ["(crate(foo-bar) >= 1.2.3~beta with crate(foo-bar) < 1.2.3)"]),

- 

- ])

- def test_depgen(toml, provides, requires, cargo_toml):

-     md = rust2rpm.Metadata.from_file(cargo_toml(toml))

-     assert [str(x) for x in md.provides] == provides

-     assert [str(x) for x in md.requires] == requires

rebased onto c87e1af9b7eda5c63fb6a9bfdf8729ce069e240c

8 months ago

rebased onto 83a9b92d68e8c5d6f2ab75b287be7ea69275fb90

8 months ago

rebased onto 571ba6b1f2a886b40a7de76980cd8c479015cc9e

8 months ago

rebased onto 4dfa564f5cd7c91118f397508eb8d0a80811d332

8 months ago

I found that fd-find 7.2.0 has its clap dependency specified two ways:

[dependencies.clap]
version = "2.31.2"
features = ["suggestions", "color", "wrap_help"]
[build-dependencies.clap]
version = "2.31.2"

But rust2rpm doesn't emit anything for the extra features, just:

$ rust2rpm --stdout fd-find |& grep clap
BuildRequires:  (crate(clap/default) >= 2.31.2 with crate(clap/default) < 3.0.0)

Then the build fails, because we should have clap/wrap_help -> term_size.

+ /usr/bin/cargo build -j8 --release
error: no matching package named `term_size` found
location searched: registry `https://github.com/rust-lang/crates.io-index`
required by package `clap v2.32.0`
    ... which is depended on by `fd-find v7.2.0 (/builddir/build/BUILD/fd-find-7.2.0)`

Oh, that one is interesting case indeed.

The question is when you reference a clap feature, which one should it take?

I expect cargo will just use the union of all of the clap features that are mentioned. There's that cargo bug about maybe someday separating how it compiles features for build deps versus regular deps, but for the purpose of rpmbuild we'll still need it all.

rebased onto d7caa1148d5fcac70030e3fb1eb698927e69960f

8 months ago

2 new commits added

  • add support for dependencies with same name
  • split features into subpackages
8 months ago

rebased onto 6f95899a21799054165858cacbe588a2a8bc6020

8 months ago

rebased onto 2cac5e5

7 months ago

Pull-Request has been merged by ignatenkobrain

7 months ago