blob: db581e280e2fee63075986872b2142c82d64086d [file] [log] [blame]
#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#
import bb
import json
import subprocess
_ALWAYS_SAFE = frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
'abcdefghijklmnopqrstuvwxyz'
'0123456789'
'_.-~')
MISSING_OK = object()
REGISTRY = "https://registry.npmjs.org"
# we can not use urllib.parse here because npm expects lowercase
# hex-chars but urllib generates uppercase ones
def uri_quote(s, safe = '/'):
res = ""
safe_set = set(safe)
for c in s:
if c in _ALWAYS_SAFE or c in safe_set:
res += c
else:
res += '%%%02x' % ord(c)
return res
class PackageJson:
def __init__(self, spec):
self.__spec = spec
@property
def name(self):
return self.__spec['name']
@property
def version(self):
return self.__spec['version']
@property
def empty_manifest(self):
return {
'name': self.name,
'description': self.__spec.get('description', ''),
'versions': {},
}
def base_filename(self):
return uri_quote(self.name, safe = '@')
def as_manifest_entry(self, tarball_uri):
res = {}
## NOTE: 'npm install' requires more than basic meta information;
## e.g. it takes 'bin' from this manifest entry but not the actual
## 'package.json'
for (idx,dflt) in [('name', None),
('description', ""),
('version', None),
('bin', MISSING_OK),
('man', MISSING_OK),
('scripts', MISSING_OK),
('directories', MISSING_OK),
('dependencies', MISSING_OK),
('devDependencies', MISSING_OK),
('optionalDependencies', MISSING_OK),
('license', "unknown")]:
if idx in self.__spec:
res[idx] = self.__spec[idx]
elif dflt == MISSING_OK:
pass
elif dflt != None:
res[idx] = dflt
else:
raise Exception("%s-%s: missing key %s" % (self.name,
self.version,
idx))
res['dist'] = {
'tarball': tarball_uri,
}
return res
class ManifestImpl:
def __init__(self, base_fname, spec):
self.__base = base_fname
self.__spec = spec
def load(self):
try:
with open(self.filename, "r") as f:
res = json.load(f)
except IOError:
res = self.__spec.empty_manifest
return res
def save(self, meta):
with open(self.filename, "w") as f:
json.dump(meta, f, indent = 2)
@property
def filename(self):
return self.__base + ".meta"
class Manifest:
def __init__(self, base_fname, spec):
self.__base = base_fname
self.__spec = spec
self.__lockf = None
self.__impl = None
def __enter__(self):
self.__lockf = bb.utils.lockfile(self.__base + ".lock")
self.__impl = ManifestImpl(self.__base, self.__spec)
return self.__impl
def __exit__(self, exc_type, exc_val, exc_tb):
bb.utils.unlockfile(self.__lockf)
class NpmCache:
def __init__(self, cache):
self.__cache = cache
@property
def path(self):
return self.__cache
def run(self, type, key, fname):
subprocess.run(['oe-npm-cache', self.__cache, type, key, fname],
check = True)
class NpmRegistry:
def __init__(self, path, cache):
self.__path = path
self.__cache = NpmCache(cache + '/_cacache')
bb.utils.mkdirhier(self.__path)
bb.utils.mkdirhier(self.__cache.path)
@staticmethod
## This function is critical and must match nodejs expectations
def _meta_uri(spec):
return REGISTRY + '/' + uri_quote(spec.name, safe = '@')
@staticmethod
## Exact return value does not matter; just make it look like a
## usual registry url
def _tarball_uri(spec):
return '%s/%s/-/%s-%s.tgz' % (REGISTRY,
uri_quote(spec.name, safe = '@'),
uri_quote(spec.name, safe = '@/'),
spec.version)
def add_pkg(self, tarball, pkg_json):
pkg_json = PackageJson(pkg_json)
base = os.path.join(self.__path, pkg_json.base_filename())
with Manifest(base, pkg_json) as manifest:
meta = manifest.load()
tarball_uri = self._tarball_uri(pkg_json)
meta['versions'][pkg_json.version] = pkg_json.as_manifest_entry(tarball_uri)
manifest.save(meta)
## Cache entries are a little bit dependent on the nodejs
## version; version specific cache implementation must
## mitigate differences
self.__cache.run('meta', self._meta_uri(pkg_json), manifest.filename);
self.__cache.run('tgz', tarball_uri, tarball);