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