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