xref: /openbmc/openbmc/poky/meta/lib/oe/npm_registry.py (revision 520786cc)
192b42cb3SPatrick Williams#
292b42cb3SPatrick Williams# Copyright OpenEmbedded Contributors
392b42cb3SPatrick Williams#
492b42cb3SPatrick Williams# SPDX-License-Identifier: MIT
592b42cb3SPatrick Williams#
692b42cb3SPatrick Williams
7615f2f11SAndrew Geisslerimport bb
8615f2f11SAndrew Geisslerimport json
9615f2f11SAndrew Geisslerimport subprocess
10615f2f11SAndrew Geissler
11615f2f11SAndrew Geissler_ALWAYS_SAFE = frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
12615f2f11SAndrew Geissler                         'abcdefghijklmnopqrstuvwxyz'
13615f2f11SAndrew Geissler                         '0123456789'
14*520786ccSPatrick Williams                         '_.-~()')
15615f2f11SAndrew Geissler
16615f2f11SAndrew GeisslerMISSING_OK = object()
17615f2f11SAndrew Geissler
18615f2f11SAndrew GeisslerREGISTRY = "https://registry.npmjs.org"
19615f2f11SAndrew Geissler
20615f2f11SAndrew Geissler# we can not use urllib.parse here because npm expects lowercase
21615f2f11SAndrew Geissler# hex-chars but urllib generates uppercase ones
22615f2f11SAndrew Geisslerdef uri_quote(s, safe = '/'):
23615f2f11SAndrew Geissler    res = ""
24615f2f11SAndrew Geissler    safe_set = set(safe)
25615f2f11SAndrew Geissler    for c in s:
26615f2f11SAndrew Geissler        if c in _ALWAYS_SAFE or c in safe_set:
27615f2f11SAndrew Geissler            res += c
28615f2f11SAndrew Geissler        else:
29615f2f11SAndrew Geissler            res += '%%%02x' % ord(c)
30615f2f11SAndrew Geissler    return res
31615f2f11SAndrew Geissler
32615f2f11SAndrew Geisslerclass PackageJson:
33615f2f11SAndrew Geissler    def __init__(self, spec):
34615f2f11SAndrew Geissler        self.__spec = spec
35615f2f11SAndrew Geissler
36615f2f11SAndrew Geissler    @property
37615f2f11SAndrew Geissler    def name(self):
38615f2f11SAndrew Geissler        return self.__spec['name']
39615f2f11SAndrew Geissler
40615f2f11SAndrew Geissler    @property
41615f2f11SAndrew Geissler    def version(self):
42615f2f11SAndrew Geissler        return self.__spec['version']
43615f2f11SAndrew Geissler
44615f2f11SAndrew Geissler    @property
45615f2f11SAndrew Geissler    def empty_manifest(self):
46615f2f11SAndrew Geissler        return {
47615f2f11SAndrew Geissler            'name': self.name,
48615f2f11SAndrew Geissler            'description': self.__spec.get('description', ''),
49615f2f11SAndrew Geissler            'versions': {},
50615f2f11SAndrew Geissler        }
51615f2f11SAndrew Geissler
52615f2f11SAndrew Geissler    def base_filename(self):
53615f2f11SAndrew Geissler        return uri_quote(self.name, safe = '@')
54615f2f11SAndrew Geissler
55615f2f11SAndrew Geissler    def as_manifest_entry(self, tarball_uri):
56615f2f11SAndrew Geissler        res = {}
57615f2f11SAndrew Geissler
58615f2f11SAndrew Geissler        ## NOTE: 'npm install' requires more than basic meta information;
59615f2f11SAndrew Geissler        ## e.g. it takes 'bin' from this manifest entry but not the actual
60615f2f11SAndrew Geissler        ## 'package.json'
61615f2f11SAndrew Geissler        for (idx,dflt) in [('name', None),
62615f2f11SAndrew Geissler                           ('description', ""),
63615f2f11SAndrew Geissler                           ('version', None),
64615f2f11SAndrew Geissler                           ('bin', MISSING_OK),
65615f2f11SAndrew Geissler                           ('man', MISSING_OK),
66615f2f11SAndrew Geissler                           ('scripts', MISSING_OK),
67615f2f11SAndrew Geissler                           ('directories', MISSING_OK),
68615f2f11SAndrew Geissler                           ('dependencies', MISSING_OK),
69615f2f11SAndrew Geissler                           ('devDependencies', MISSING_OK),
70615f2f11SAndrew Geissler                           ('optionalDependencies', MISSING_OK),
71615f2f11SAndrew Geissler                           ('license', "unknown")]:
72615f2f11SAndrew Geissler            if idx in self.__spec:
73615f2f11SAndrew Geissler                res[idx] = self.__spec[idx]
74615f2f11SAndrew Geissler            elif dflt == MISSING_OK:
75615f2f11SAndrew Geissler                pass
76615f2f11SAndrew Geissler            elif dflt != None:
77615f2f11SAndrew Geissler                res[idx] = dflt
78615f2f11SAndrew Geissler            else:
79615f2f11SAndrew Geissler                raise Exception("%s-%s: missing key %s" % (self.name,
80615f2f11SAndrew Geissler                                                           self.version,
81615f2f11SAndrew Geissler                                                           idx))
82615f2f11SAndrew Geissler
83615f2f11SAndrew Geissler        res['dist'] = {
84615f2f11SAndrew Geissler            'tarball': tarball_uri,
85615f2f11SAndrew Geissler        }
86615f2f11SAndrew Geissler
87615f2f11SAndrew Geissler        return res
88615f2f11SAndrew Geissler
89615f2f11SAndrew Geisslerclass ManifestImpl:
90615f2f11SAndrew Geissler    def __init__(self, base_fname, spec):
91615f2f11SAndrew Geissler        self.__base = base_fname
92615f2f11SAndrew Geissler        self.__spec = spec
93615f2f11SAndrew Geissler
94615f2f11SAndrew Geissler    def load(self):
95615f2f11SAndrew Geissler        try:
96615f2f11SAndrew Geissler            with open(self.filename, "r") as f:
97615f2f11SAndrew Geissler                res = json.load(f)
98615f2f11SAndrew Geissler        except IOError:
99615f2f11SAndrew Geissler            res = self.__spec.empty_manifest
100615f2f11SAndrew Geissler
101615f2f11SAndrew Geissler        return res
102615f2f11SAndrew Geissler
103615f2f11SAndrew Geissler    def save(self, meta):
104615f2f11SAndrew Geissler        with open(self.filename, "w") as f:
105615f2f11SAndrew Geissler            json.dump(meta, f, indent = 2)
106615f2f11SAndrew Geissler
107615f2f11SAndrew Geissler    @property
108615f2f11SAndrew Geissler    def filename(self):
109615f2f11SAndrew Geissler        return self.__base + ".meta"
110615f2f11SAndrew Geissler
111615f2f11SAndrew Geisslerclass Manifest:
112615f2f11SAndrew Geissler    def __init__(self, base_fname, spec):
113615f2f11SAndrew Geissler        self.__base = base_fname
114615f2f11SAndrew Geissler        self.__spec = spec
115615f2f11SAndrew Geissler        self.__lockf = None
116615f2f11SAndrew Geissler        self.__impl = None
117615f2f11SAndrew Geissler
118615f2f11SAndrew Geissler    def __enter__(self):
119615f2f11SAndrew Geissler        self.__lockf = bb.utils.lockfile(self.__base + ".lock")
120615f2f11SAndrew Geissler        self.__impl  = ManifestImpl(self.__base, self.__spec)
121615f2f11SAndrew Geissler        return self.__impl
122615f2f11SAndrew Geissler
123615f2f11SAndrew Geissler    def __exit__(self, exc_type, exc_val, exc_tb):
124615f2f11SAndrew Geissler        bb.utils.unlockfile(self.__lockf)
125615f2f11SAndrew Geissler
126615f2f11SAndrew Geisslerclass NpmCache:
127615f2f11SAndrew Geissler    def __init__(self, cache):
128615f2f11SAndrew Geissler        self.__cache = cache
129615f2f11SAndrew Geissler
130615f2f11SAndrew Geissler    @property
131615f2f11SAndrew Geissler    def path(self):
132615f2f11SAndrew Geissler        return self.__cache
133615f2f11SAndrew Geissler
134615f2f11SAndrew Geissler    def run(self, type, key, fname):
135615f2f11SAndrew Geissler        subprocess.run(['oe-npm-cache', self.__cache, type, key, fname],
136615f2f11SAndrew Geissler                       check = True)
137615f2f11SAndrew Geissler
138615f2f11SAndrew Geisslerclass NpmRegistry:
139615f2f11SAndrew Geissler    def __init__(self, path, cache):
140615f2f11SAndrew Geissler        self.__path = path
141615f2f11SAndrew Geissler        self.__cache = NpmCache(cache + '/_cacache')
142615f2f11SAndrew Geissler        bb.utils.mkdirhier(self.__path)
143615f2f11SAndrew Geissler        bb.utils.mkdirhier(self.__cache.path)
144615f2f11SAndrew Geissler
145615f2f11SAndrew Geissler    @staticmethod
146615f2f11SAndrew Geissler    ## This function is critical and must match nodejs expectations
147615f2f11SAndrew Geissler    def _meta_uri(spec):
148615f2f11SAndrew Geissler        return REGISTRY + '/' + uri_quote(spec.name, safe = '@')
149615f2f11SAndrew Geissler
150615f2f11SAndrew Geissler    @staticmethod
151615f2f11SAndrew Geissler    ## Exact return value does not matter; just make it look like a
152615f2f11SAndrew Geissler    ## usual registry url
153615f2f11SAndrew Geissler    def _tarball_uri(spec):
154615f2f11SAndrew Geissler        return '%s/%s/-/%s-%s.tgz' % (REGISTRY,
155615f2f11SAndrew Geissler                                      uri_quote(spec.name, safe = '@'),
156615f2f11SAndrew Geissler                                      uri_quote(spec.name, safe = '@/'),
157615f2f11SAndrew Geissler                                      spec.version)
158615f2f11SAndrew Geissler
159615f2f11SAndrew Geissler    def add_pkg(self, tarball, pkg_json):
160615f2f11SAndrew Geissler        pkg_json = PackageJson(pkg_json)
161615f2f11SAndrew Geissler        base = os.path.join(self.__path, pkg_json.base_filename())
162615f2f11SAndrew Geissler
163615f2f11SAndrew Geissler        with Manifest(base, pkg_json) as manifest:
164615f2f11SAndrew Geissler            meta = manifest.load()
165615f2f11SAndrew Geissler            tarball_uri = self._tarball_uri(pkg_json)
166615f2f11SAndrew Geissler
167615f2f11SAndrew Geissler            meta['versions'][pkg_json.version] = pkg_json.as_manifest_entry(tarball_uri)
168615f2f11SAndrew Geissler
169615f2f11SAndrew Geissler            manifest.save(meta)
170615f2f11SAndrew Geissler
171615f2f11SAndrew Geissler            ## Cache entries are a little bit dependent on the nodejs
172615f2f11SAndrew Geissler            ## version; version specific cache implementation must
173615f2f11SAndrew Geissler            ## mitigate differences
174615f2f11SAndrew Geissler            self.__cache.run('meta', self._meta_uri(pkg_json), manifest.filename);
175615f2f11SAndrew Geissler            self.__cache.run('tgz',  tarball_uri, tarball);
176