blob: 16ea02d8a8d6812f7f85ea206dd466add513f921 [file]
# ex:ts=4:sw=4:sts=4:et
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
"""
BitBake 'Fetch' implementation for cargo registries, e.g. crates.io
SRC_URI = "crate://some.host/crate_name/version;OptionA=xxx;OptionB=xxx;..."
Supported SRC_URI options are:
- protocol
The method to use to download the crate file. Common options are "http",
"https", and "ftp". The default is "https".
"""
# Copyright (C) 2016 Doug Goldstein
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Based on functions from the base bb module, Copyright 2003 Holger Schurig
import hashlib
import json
import os
import subprocess
import re
from functools import cmp_to_key
import bb
from bb.fetch import logger, subprocess_setup, UnpackError, runfetchcmd
from bb.fetch.wget import Wget
class Crate(Wget):
"""Class to fetch crates via wget"""
def _cargo_bitbake_path(self, rootdir):
return os.path.join(rootdir, "cargo_home", "bitbake")
def supports(self, ud, d):
"""
Check to see if a given url is for this fetcher
"""
return ud.type in ['crate']
def recommends_checksum(self, urldata):
return True
def urldata_init(self, ud, d):
"""
Sets up to download the respective crate from crates.io
"""
if ud.type == 'crate':
self._crate_urldata_init(ud, d)
super(Crate, self).urldata_init(ud, d)
def _generate_index_path(self, name):
# https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files
if len(name) == 1:
return f"1/{name}"
elif len(name) == 2:
return f"2/{name}"
elif len(name) == 3:
return f"3/{name[0]}/{name}"
else:
return f"{name[0:2]}/{name[2:4]}/{name}"
def _crate_urldata_init(self, ud, d):
"""
Sets up the download for a crate
"""
# URL syntax is: crate://HOST/NAME/VERSION
# break the URL apart by /
parts = ud.url.split('/')
if len(parts) < 5:
raise bb.fetch.ParameterError("Invalid URL: Must be crate://HOST/NAME/VERSION", ud.url)
# version is expected to be the last token
# but ignore possible url parameters which will be used
# by the top fetcher class
version = parts[-1].split(";")[0]
# second to last field is name
name = parts[-2]
# host (this is to allow custom crate registries to be specified
host = '/'.join(parts[2:-2])
if 'protocol' in ud.parm:
proto = ud.parm['protocol']
else:
proto = 'https'
# If using crates.io use the CDN directly as per https://crates.io/data-access
if host == 'crates.io':
if proto != 'https':
bb.warn("URL: %s does the %s protocol which is not supported by crates.io. Please change to ;protocol=https in the url." % (ud.url, proto))
ud.url = "https://static.crates.io/crates/%s/%s/download" % (name, version)
ud.versionsurl = 'https://index.crates.io/' + self._generate_index_path(name)
else:
ud.url = "%s://%s/%s/%s/download" % (proto, host, name, version)
ud.versionsurl = "%s://%s/%s/versions" % (proto, host, name)
ud.parm['downloadfilename'] = "%s-%s.crate" % (name, version)
if 'name' not in ud.parm:
ud.parm['name'] = '%s-%s' % (name, version)
logger.debug2("Fetching %s to %s" % (ud.url, ud.parm['downloadfilename']))
def unpack(self, ud, rootdir, d):
"""
Uses the crate to build the necessary paths for cargo to utilize it
"""
if ud.type == 'crate':
return self._crate_unpack(ud, rootdir, d)
else:
super(Crate, self).unpack(ud, rootdir, d)
def _crate_unpack(self, ud, rootdir, d):
"""
Unpacks a crate
"""
thefile = ud.localpath
# possible metadata we need to write out
metadata = {}
bp = d.getVar('BP')
if bp == ud.parm.get('name'):
cmd = ['tar', '-xz', '--no-same-owner', '-f', thefile]
ud.unpack_tracer.unpack("crate-extract", rootdir)
else:
cargo_bitbake = self._cargo_bitbake_path(rootdir)
ud.unpack_tracer.unpack("cargo-extract", cargo_bitbake)
cmd = ['tar', '-xz', '--no-same-owner', '-f', thefile, '-C', cargo_bitbake]
# ensure we've got these paths made
bb.utils.mkdirhier(cargo_bitbake)
# generate metadata necessary
with open(thefile, 'rb') as f:
# get the SHA256 of the original tarball
tarhash = hashlib.sha256(f.read()).hexdigest()
metadata['files'] = {}
metadata['package'] = tarhash
bb.note("Unpacking %s to %s/" % (thefile, os.getcwd()))
runfetchcmd(cmd, d, workdir=rootdir)
# if we have metadata to write out..
if len(metadata) > 0:
cratepath = os.path.splitext(os.path.basename(thefile))[0]
bbpath = self._cargo_bitbake_path(rootdir)
mdfile = '.cargo-checksum.json'
mdpath = os.path.join(bbpath, cratepath, mdfile)
with open(mdpath, "w") as f:
json.dump(metadata, f)
def latest_versionstring(self, ud, d, filter_regex=None):
"""
Return the latest upstream version, dispatching to the appropriate
parser based on the versionsurl format.
"""
if ud.versionsurl.startswith('https://index.crates.io/'):
return self._latest_versionstring_from_index(ud, d, filter_regex)
return self._latest_versionstring_from_api(ud, d, filter_regex)
def _latest_versionstring_from_api(self, ud, d, filter_regex=None):
"""
Parse the latest version from a [name]/versions JSON API response.
"""
json_data = json.loads(self._fetch_index(ud.versionsurl, ud, d))
versions = [(0, i["num"], "") for i in json_data["versions"]]
if filter_regex:
versions = [v for v in versions if re.match(filter_regex, v[1])]
versions = sorted(versions, key=cmp_to_key(bb.utils.vercmp))
return (versions[-1][1], "") if versions else ("", "")
def _latest_versionstring_from_index(self, ud, d, filter_regex=None):
"""
Parse the latest version from a Cargo sparse index file (NDJSON).
https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files
"""
versions = []
response = self._fetch_index(ud.versionsurl, ud, d)
for line in response.splitlines():
data = json.loads(line)
if not data.get("yanked", False):
versions.append((0, data["vers"], ""))
if filter_regex:
versions = [v for v in versions if re.match(filter_regex, v[1])]
versions = sorted(versions, key=cmp_to_key(bb.utils.vercmp))
return (versions[-1][1], "") if versions else ("", "")