2017-01-30 23:44:25 +00:00
|
|
|
import argparse
|
2017-02-17 16:30:45 +00:00
|
|
|
from datetime import datetime, timezone
|
|
|
|
import difflib
|
2017-01-30 23:44:25 +00:00
|
|
|
import os
|
2017-03-06 15:48:19 +00:00
|
|
|
import shutil
|
2017-01-30 23:44:25 +00:00
|
|
|
import tarfile
|
|
|
|
import tempfile
|
2017-03-06 15:48:19 +00:00
|
|
|
import time
|
2017-01-30 23:44:25 +00:00
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import jinja2
|
|
|
|
import requests
|
2017-02-05 13:20:55 +00:00
|
|
|
import tqdm
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-12 10:28:51 +00:00
|
|
|
from . import Metadata
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-17 20:09:14 +00:00
|
|
|
DEFAULT_EDITOR = "vi"
|
2017-02-05 12:56:50 +00:00
|
|
|
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
|
|
|
|
CACHEDIR = os.path.join(XDG_CACHE_HOME, "rust2rpm")
|
2017-01-30 23:44:25 +00:00
|
|
|
API_URL = "https://crates.io/api/v1/"
|
|
|
|
TEMPLATE = """# Generated by rust2rpm
|
2017-02-03 09:05:26 +00:00
|
|
|
%bcond_without check
|
2017-02-11 17:39:16 +00:00
|
|
|
{% if not include_debug %}
|
2017-02-10 22:06:17 +00:00
|
|
|
%global debug_package %{nil}
|
2017-02-11 17:39:16 +00:00
|
|
|
{% endif %}
|
2017-02-03 09:05:26 +00:00
|
|
|
|
2017-02-03 09:39:37 +00:00
|
|
|
%global crate {{ md.name }}
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
Name: {{ name }}
|
2017-02-03 09:39:37 +00:00
|
|
|
Version: {{ md.version }}
|
2017-01-30 23:44:25 +00:00
|
|
|
Release: 1%{?dist}
|
2017-02-17 23:11:04 +00:00
|
|
|
{% if md.description is none %}
|
|
|
|
Summary: # FIXME
|
|
|
|
{% else %}
|
|
|
|
{% set description_lines = md.description.split("\n") %}
|
|
|
|
Summary: {{ description_lines|join(" ")|trim }}
|
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-05 11:10:50 +00:00
|
|
|
License: {{ md.license|default("# FIXME") }}
|
2017-02-03 09:39:37 +00:00
|
|
|
URL: https://crates.io/crates/{{ md.name }}
|
2017-01-30 23:44:25 +00:00
|
|
|
Source0: https://crates.io/api/v1/crates/%{crate}/%{version}/download#/%{crate}-%{version}.crate
|
2017-02-17 16:30:45 +00:00
|
|
|
{% if patch_file is not none %}
|
|
|
|
# Initial patched metadata
|
|
|
|
Patch0: {{ patch_file }}
|
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
|
|
|
|
ExclusiveArch: %{rust_arches}
|
|
|
|
|
|
|
|
BuildRequires: rust
|
|
|
|
BuildRequires: cargo
|
2017-02-26 22:43:49 +00:00
|
|
|
{% if include_build_requires %}
|
2017-02-26 18:07:00 +00:00
|
|
|
{% if md.requires|length > 0 %}
|
|
|
|
# [dependencies]
|
|
|
|
{% for req in md.requires|sort(attribute="name") %}
|
|
|
|
BuildRequires: {{ req }}
|
|
|
|
{% endfor %}
|
|
|
|
{% endif %}
|
|
|
|
{% if md.build_requires|length > 0 %}
|
|
|
|
# [build-dependencies]
|
2017-02-11 18:04:30 +00:00
|
|
|
{% for req in md.build_requires|sort(attribute="name") %}
|
2017-02-03 09:39:37 +00:00
|
|
|
BuildRequires: {{ req }}
|
2017-01-30 23:54:56 +00:00
|
|
|
{% endfor %}
|
2017-02-26 18:07:00 +00:00
|
|
|
{% endif %}
|
2017-02-03 09:39:37 +00:00
|
|
|
{% if md.test_requires|length > 0 %}
|
2017-02-03 09:05:26 +00:00
|
|
|
%if %{with check}
|
2017-02-26 18:07:00 +00:00
|
|
|
# [dev-dependencies]
|
2017-02-11 18:04:30 +00:00
|
|
|
{% for req in md.test_requires|sort(attribute="name") %}
|
2017-02-03 09:39:37 +00:00
|
|
|
BuildRequires: {{ req }}
|
2017-02-03 09:05:26 +00:00
|
|
|
{% endfor %}
|
|
|
|
%endif
|
|
|
|
{% endif %}
|
2017-02-26 22:43:49 +00:00
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
|
|
|
|
%description
|
|
|
|
%{summary}.
|
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
{% if name_devel is not none %}
|
|
|
|
%package {{ name_devel }}
|
2017-01-30 23:44:25 +00:00
|
|
|
Summary: %{summary}
|
|
|
|
BuildArch: noarch
|
2017-02-26 22:43:49 +00:00
|
|
|
{% if include_provides %}
|
2017-02-03 09:39:37 +00:00
|
|
|
{% for prv in md.provides %}
|
|
|
|
Provides: {{ prv }}
|
2017-01-30 23:44:25 +00:00
|
|
|
{% endfor %}
|
2017-02-26 22:43:49 +00:00
|
|
|
{% endif %}
|
|
|
|
{% if include_requires %}
|
2017-02-26 18:07:00 +00:00
|
|
|
{% if md.requires|length > 0 %}
|
|
|
|
# [dependencies]
|
2017-02-11 18:04:30 +00:00
|
|
|
{% for req in md.requires|sort(attribute="name") %}
|
2017-01-30 23:44:25 +00:00
|
|
|
Requires: {{ req }}
|
2017-01-30 23:54:56 +00:00
|
|
|
{% endfor %}
|
2017-02-03 09:39:37 +00:00
|
|
|
{% endif %}
|
2017-02-26 18:07:00 +00:00
|
|
|
{% if md.build_requires|length > 0 %}
|
|
|
|
# [build-dependencies]
|
|
|
|
{% for req in md.build_requires|sort(attribute="name") %}
|
|
|
|
Requires: {{ req }}
|
|
|
|
{% endfor %}
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
%description {{ name_devel }}
|
2017-02-17 23:11:04 +00:00
|
|
|
{% if md.description is none %}
|
2017-01-30 23:44:25 +00:00
|
|
|
%{summary}.
|
2017-02-17 23:11:04 +00:00
|
|
|
{% else %}
|
|
|
|
{{ md.description|wordwrap|trim }}
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
This package contains library source intended for building other packages
|
|
|
|
which use %{crate} from crates.io.
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
%prep
|
2017-02-17 16:30:45 +00:00
|
|
|
%autosetup -n %{crate}-%{version} -p1
|
2017-01-30 23:44:25 +00:00
|
|
|
%cargo_prep
|
|
|
|
|
2017-02-10 21:33:34 +00:00
|
|
|
%build
|
|
|
|
%cargo_build
|
|
|
|
|
2017-01-30 23:44:25 +00:00
|
|
|
%install
|
2017-02-10 21:33:13 +00:00
|
|
|
%cargo_install
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-03 09:05:26 +00:00
|
|
|
%if %{with check}
|
2017-01-30 23:44:25 +00:00
|
|
|
%check
|
|
|
|
%cargo_test
|
2017-02-03 09:05:26 +00:00
|
|
|
%endif
|
2017-01-30 23:44:25 +00:00
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
{% if include_main %}
|
|
|
|
%files
|
2017-02-19 18:55:19 +00:00
|
|
|
{% if md.license_file is not none %}
|
|
|
|
%license {{ md.license_file }}
|
|
|
|
{% endif %}
|
2017-02-11 17:39:16 +00:00
|
|
|
{% for bin in bins %}
|
|
|
|
%{_bindir}/{{ bin.name }}
|
|
|
|
{% endfor %}
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
{% if name_devel is not none %}
|
|
|
|
%files {{ name_devel }}
|
2017-02-05 11:07:49 +00:00
|
|
|
{% if md.license_file is not none %}
|
|
|
|
%license {{ md.license_file }}
|
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
%{cargo_registry}/%{crate}-%{version}/
|
|
|
|
|
2017-02-11 17:39:16 +00:00
|
|
|
{% endif %}
|
2017-01-30 23:44:25 +00:00
|
|
|
%changelog
|
2017-03-06 15:48:19 +00:00
|
|
|
* {{ date }} {{ packager|default("rust2rpm <nobody@fedoraproject.org>") }} - {{ md.version }}-1
|
|
|
|
- Initial package
|
2017-01-30 23:44:25 +00:00
|
|
|
"""
|
|
|
|
JINJA_ENV = jinja2.Environment(undefined=jinja2.StrictUndefined,
|
|
|
|
trim_blocks=True, lstrip_blocks=True)
|
|
|
|
|
2017-02-17 20:09:14 +00:00
|
|
|
def detect_editor():
|
|
|
|
terminal = os.getenv("TERM")
|
|
|
|
terminal_is_dumb = terminal is None or terminal == "dumb"
|
|
|
|
editor = None
|
|
|
|
if not terminal_is_dumb:
|
|
|
|
editor = os.getenv("VISUAL")
|
|
|
|
if editor is None:
|
|
|
|
editor = os.getenv("EDITOR")
|
|
|
|
if editor is None:
|
|
|
|
if terminal_is_dumb:
|
|
|
|
raise Exception("Terminal is dumb, but EDITOR unset")
|
|
|
|
else:
|
|
|
|
editor = DEFAULT_EDITOR
|
|
|
|
return editor
|
|
|
|
|
2017-03-06 15:48:19 +00:00
|
|
|
def detect_packager():
|
|
|
|
rpmdev_packager = shutil.which("rpmdev-packager")
|
|
|
|
if rpmdev_packager is not None:
|
|
|
|
return subprocess.check_output(rpmdev_packager, universal_newlines=True).strip()
|
|
|
|
|
|
|
|
git = shutil.which("git")
|
|
|
|
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 None
|
|
|
|
|
2017-02-17 16:30:45 +00:00
|
|
|
def file_mtime(path):
|
|
|
|
t = datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc)
|
|
|
|
return t.astimezone().isoformat()
|
|
|
|
|
2017-02-05 17:14:52 +00:00
|
|
|
def main():
|
2017-01-30 23:44:25 +00:00
|
|
|
parser = argparse.ArgumentParser()
|
2017-02-18 00:54:02 +00:00
|
|
|
parser.add_argument("-", "--stdout", action="store_true",
|
|
|
|
help="Print spec and patches into stdout")
|
2017-02-26 22:43:49 +00:00
|
|
|
parser.add_argument("-t", "--target", action="store",
|
|
|
|
choices=("plain", "fedora"), default="fedora",
|
2017-01-30 23:44:25 +00:00
|
|
|
help="Distribution target")
|
2017-02-17 16:30:45 +00:00
|
|
|
parser.add_argument("-p", "--patch", action="store_true",
|
|
|
|
help="Do initial patching of Cargo.toml")
|
2017-01-30 23:44:25 +00:00
|
|
|
parser.add_argument("crate", help="crates.io name")
|
|
|
|
parser.add_argument("version", nargs="?", help="crates.io version")
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
2017-02-17 16:30:45 +00:00
|
|
|
if args.patch:
|
2017-02-17 20:09:14 +00:00
|
|
|
editor = detect_editor()
|
2017-02-17 16:30:45 +00:00
|
|
|
|
2017-01-30 23:44:25 +00:00
|
|
|
if args.version is None:
|
|
|
|
# Now we need to get latest version
|
|
|
|
url = requests.compat.urljoin(API_URL, "crates/{}/versions".format(args.crate))
|
|
|
|
req = requests.get(url)
|
|
|
|
req.raise_for_status()
|
|
|
|
args.version = req.json()["versions"][0]["num"]
|
|
|
|
|
2017-02-05 12:56:50 +00:00
|
|
|
if not os.path.isdir(CACHEDIR):
|
|
|
|
os.mkdir(CACHEDIR)
|
2017-02-05 13:20:55 +00:00
|
|
|
cratef_base = "{}-{}.crate".format(args.crate, args.version)
|
|
|
|
cratef = os.path.join(CACHEDIR, cratef_base)
|
2017-01-30 23:44:25 +00:00
|
|
|
if not os.path.isfile(cratef):
|
|
|
|
url = requests.compat.urljoin(API_URL, "crates/{}/{}/download#".format(args.crate, args.version))
|
|
|
|
req = requests.get(url, stream=True)
|
|
|
|
req.raise_for_status()
|
2017-02-05 13:20:55 +00:00
|
|
|
total = int(req.headers["Content-Length"])
|
2017-01-30 23:44:25 +00:00
|
|
|
with open(cratef, "wb") as f:
|
2017-02-05 13:20:55 +00:00
|
|
|
for chunk in tqdm.tqdm(req.iter_content(), "Downloading {}".format(cratef_base),
|
|
|
|
total=total, unit="B", unit_scale=True):
|
2017-01-30 23:44:25 +00:00
|
|
|
f.write(chunk)
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
target_dir = "{}/".format(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)
|
2017-02-17 16:30:45 +00:00
|
|
|
toml_relpath = "{}-{}/Cargo.toml".format(args.crate, args.version)
|
|
|
|
toml = "{}/{}".format(tmpdir, toml_relpath)
|
2017-01-30 23:44:25 +00:00
|
|
|
assert os.path.isfile(toml)
|
|
|
|
|
2017-02-17 16:30:45 +00:00
|
|
|
if args.patch:
|
|
|
|
mtime_before = file_mtime(toml)
|
|
|
|
with open(toml, "r") as fobj:
|
|
|
|
toml_before = fobj.readlines()
|
|
|
|
subprocess.check_call([editor, toml])
|
|
|
|
mtime_after = file_mtime(toml)
|
|
|
|
with open(toml, "r") as fobj:
|
|
|
|
toml_after = fobj.readlines()
|
|
|
|
diff = list(difflib.unified_diff(toml_before, toml_after,
|
|
|
|
fromfile=toml_relpath, tofile=toml_relpath,
|
|
|
|
fromfiledate=mtime_before, tofiledate=mtime_after))
|
|
|
|
|
2017-02-05 17:14:52 +00:00
|
|
|
metadata = Metadata.from_file(toml)
|
2017-01-30 23:44:25 +00:00
|
|
|
|
|
|
|
template = JINJA_ENV.from_string(TEMPLATE)
|
2017-02-17 16:30:45 +00:00
|
|
|
|
|
|
|
if args.patch and len(diff) > 0:
|
|
|
|
patch_file = "{}-{}-fix-metadata.diff".format(args.crate, args.version)
|
|
|
|
else:
|
|
|
|
patch_file = None
|
|
|
|
|
2017-02-18 00:45:35 +00:00
|
|
|
kwargs = {}
|
|
|
|
bins = [tgt for tgt in metadata.targets if tgt.kind == "bin"]
|
2017-02-18 18:53:08 +00:00
|
|
|
libs = [tgt for tgt in metadata.targets if tgt.kind in ("lib", "proc-macro")]
|
2017-02-18 00:45:35 +00:00
|
|
|
is_bin = len(bins) > 0
|
|
|
|
is_lib = len(libs) > 0
|
|
|
|
if is_bin:
|
|
|
|
spec_basename = args.crate
|
|
|
|
kwargs["include_debug"] = True
|
|
|
|
kwargs["name"] = "%{crate}"
|
|
|
|
kwargs["include_main"] = True
|
|
|
|
kwargs["bins"] = bins
|
|
|
|
if not is_lib:
|
|
|
|
kwargs["name_devel"] = None
|
|
|
|
else:
|
|
|
|
kwargs["name_devel"] = "-n rust-%{crate}-devel"
|
|
|
|
elif is_lib:
|
|
|
|
spec_basename = "rust-{}".format(args.crate)
|
|
|
|
kwargs["include_debug"] = False
|
|
|
|
kwargs["name"] = "rust-%{crate}"
|
|
|
|
kwargs["include_main"] = False
|
|
|
|
kwargs["name_devel"] = " devel"
|
|
|
|
else:
|
|
|
|
raise ValueError("No bins and no libs")
|
|
|
|
|
2017-02-26 22:43:49 +00:00
|
|
|
if args.target == "fedora":
|
|
|
|
kwargs["include_build_requires"] = True
|
|
|
|
kwargs["include_provides"] = False
|
|
|
|
kwargs["include_requires"] = False
|
|
|
|
elif args.target == "plain":
|
|
|
|
kwargs["include_build_requires"] = True
|
|
|
|
kwargs["include_provides"] = True
|
|
|
|
kwargs["include_requires"] = True
|
|
|
|
else:
|
|
|
|
assert False, "Unknown target {!r}".format(args.target)
|
|
|
|
|
2017-03-06 15:48:19 +00:00
|
|
|
kwargs["date"] = time.strftime("%a %b %d %Y")
|
|
|
|
kwargs["packager"] = detect_packager()
|
|
|
|
|
2017-02-18 00:54:02 +00:00
|
|
|
spec_file = "{}.spec".format(spec_basename)
|
2017-02-26 22:43:49 +00:00
|
|
|
spec_contents = template.render(md=metadata, patch_file=patch_file, **kwargs)
|
2017-02-18 00:54:02 +00:00
|
|
|
if args.stdout:
|
|
|
|
print("# {}".format(spec_file))
|
|
|
|
print(spec_contents)
|
|
|
|
if patch_file is not None:
|
|
|
|
print("# {}".format(patch_file))
|
|
|
|
print("".join(diff), end="")
|
|
|
|
else:
|
|
|
|
with open(spec_file, "w") as fobj:
|
|
|
|
fobj.write(spec_contents)
|
2017-03-06 15:51:31 +00:00
|
|
|
fobj.write("\n")
|
2017-02-18 00:54:02 +00:00
|
|
|
if patch_file is not None:
|
|
|
|
with open(patch_file, "w") as fobj:
|
|
|
|
fobj.writelines(diff)
|
2017-02-05 17:14:52 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|