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