xref: /openbmc/openbmc/poky/meta/lib/oe/npm_registry.py (revision 92b42cb3)
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