1# 2# Copyright OpenEmbedded Contributors 3# 4# SPDX-License-Identifier: MIT 5# 6 7import bb 8import json 9import subprocess 10 11_ALWAYS_SAFE = frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ' 12 'abcdefghijklmnopqrstuvwxyz' 13 '0123456789' 14 '_.-~()') 15 16MISSING_OK = object() 17 18REGISTRY = "https://registry.npmjs.org" 19 20# we can not use urllib.parse here because npm expects lowercase 21# hex-chars but urllib generates uppercase ones 22def uri_quote(s, safe = '/'): 23 res = "" 24 safe_set = set(safe) 25 for c in s: 26 if c in _ALWAYS_SAFE or c in safe_set: 27 res += c 28 else: 29 res += '%%%02x' % ord(c) 30 return res 31 32class PackageJson: 33 def __init__(self, spec): 34 self.__spec = spec 35 36 @property 37 def name(self): 38 return self.__spec['name'] 39 40 @property 41 def version(self): 42 return self.__spec['version'] 43 44 @property 45 def empty_manifest(self): 46 return { 47 'name': self.name, 48 'description': self.__spec.get('description', ''), 49 'versions': {}, 50 } 51 52 def base_filename(self): 53 return uri_quote(self.name, safe = '@') 54 55 def as_manifest_entry(self, tarball_uri): 56 res = {} 57 58 ## NOTE: 'npm install' requires more than basic meta information; 59 ## e.g. it takes 'bin' from this manifest entry but not the actual 60 ## 'package.json' 61 for (idx,dflt) in [('name', None), 62 ('description', ""), 63 ('version', None), 64 ('bin', MISSING_OK), 65 ('man', MISSING_OK), 66 ('scripts', MISSING_OK), 67 ('directories', MISSING_OK), 68 ('dependencies', MISSING_OK), 69 ('devDependencies', MISSING_OK), 70 ('optionalDependencies', MISSING_OK), 71 ('license', "unknown")]: 72 if idx in self.__spec: 73 res[idx] = self.__spec[idx] 74 elif dflt == MISSING_OK: 75 pass 76 elif dflt != None: 77 res[idx] = dflt 78 else: 79 raise Exception("%s-%s: missing key %s" % (self.name, 80 self.version, 81 idx)) 82 83 res['dist'] = { 84 'tarball': tarball_uri, 85 } 86 87 return res 88 89class ManifestImpl: 90 def __init__(self, base_fname, spec): 91 self.__base = base_fname 92 self.__spec = spec 93 94 def load(self): 95 try: 96 with open(self.filename, "r") as f: 97 res = json.load(f) 98 except IOError: 99 res = self.__spec.empty_manifest 100 101 return res 102 103 def save(self, meta): 104 with open(self.filename, "w") as f: 105 json.dump(meta, f, indent = 2) 106 107 @property 108 def filename(self): 109 return self.__base + ".meta" 110 111class Manifest: 112 def __init__(self, base_fname, spec): 113 self.__base = base_fname 114 self.__spec = spec 115 self.__lockf = None 116 self.__impl = None 117 118 def __enter__(self): 119 self.__lockf = bb.utils.lockfile(self.__base + ".lock") 120 self.__impl = ManifestImpl(self.__base, self.__spec) 121 return self.__impl 122 123 def __exit__(self, exc_type, exc_val, exc_tb): 124 bb.utils.unlockfile(self.__lockf) 125 126class NpmCache: 127 def __init__(self, cache): 128 self.__cache = cache 129 130 @property 131 def path(self): 132 return self.__cache 133 134 def run(self, type, key, fname): 135 subprocess.run(['oe-npm-cache', self.__cache, type, key, fname], 136 check = True) 137 138class NpmRegistry: 139 def __init__(self, path, cache): 140 self.__path = path 141 self.__cache = NpmCache(cache + '/_cacache') 142 bb.utils.mkdirhier(self.__path) 143 bb.utils.mkdirhier(self.__cache.path) 144 145 @staticmethod 146 ## This function is critical and must match nodejs expectations 147 def _meta_uri(spec): 148 return REGISTRY + '/' + uri_quote(spec.name, safe = '@') 149 150 @staticmethod 151 ## Exact return value does not matter; just make it look like a 152 ## usual registry url 153 def _tarball_uri(spec): 154 return '%s/%s/-/%s-%s.tgz' % (REGISTRY, 155 uri_quote(spec.name, safe = '@'), 156 uri_quote(spec.name, safe = '@/'), 157 spec.version) 158 159 def add_pkg(self, tarball, pkg_json): 160 pkg_json = PackageJson(pkg_json) 161 base = os.path.join(self.__path, pkg_json.base_filename()) 162 163 with Manifest(base, pkg_json) as manifest: 164 meta = manifest.load() 165 tarball_uri = self._tarball_uri(pkg_json) 166 167 meta['versions'][pkg_json.version] = pkg_json.as_manifest_entry(tarball_uri) 168 169 manifest.save(meta) 170 171 ## Cache entries are a little bit dependent on the nodejs 172 ## version; version specific cache implementation must 173 ## mitigate differences 174 self.__cache.run('meta', self._meta_uri(pkg_json), manifest.filename); 175 self.__cache.run('tgz', tarball_uri, tarball); 176