blob: f83485ad85a95fc2659e67304b741c2c3beb72cd [file] [log] [blame]
Andrew Geissler82c905d2020-04-13 13:39:40 -05001# Copyright (C) 2020 Savoir-Faire Linux
Brad Bishopc342db32019-05-15 21:57:59 -04002#
3# SPDX-License-Identifier: GPL-2.0-only
4#
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05005"""
Andrew Geissler82c905d2020-04-13 13:39:40 -05006BitBake 'Fetch' npm implementation
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05007
Andrew Geissler82c905d2020-04-13 13:39:40 -05008npm fetcher support the SRC_URI with format of:
9SRC_URI = "npm://some.registry.url;OptionA=xxx;OptionB=xxx;..."
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050010
Andrew Geissler82c905d2020-04-13 13:39:40 -050011Supported SRC_URI options are:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050012
Andrew Geissler82c905d2020-04-13 13:39:40 -050013- package
14 The npm package name. This is a mandatory parameter.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050015
Andrew Geissler82c905d2020-04-13 13:39:40 -050016- version
17 The npm package version. This is a mandatory parameter.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050018
Andrew Geissler82c905d2020-04-13 13:39:40 -050019- downloadfilename
20 Specifies the filename used when storing the downloaded file.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050021
Andrew Geissler82c905d2020-04-13 13:39:40 -050022- destsuffix
23 Specifies the directory to use to unpack the package (default: npm).
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024"""
25
Andrew Geissler82c905d2020-04-13 13:39:40 -050026import base64
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027import json
Andrew Geissler82c905d2020-04-13 13:39:40 -050028import os
29import re
30import shlex
31import tempfile
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050032import bb
Andrew Geissler82c905d2020-04-13 13:39:40 -050033from bb.fetch2 import Fetch
34from bb.fetch2 import FetchError
35from bb.fetch2 import FetchMethod
36from bb.fetch2 import MissingParameterError
37from bb.fetch2 import ParameterError
38from bb.fetch2 import URI
39from bb.fetch2 import check_network_access
40from bb.fetch2 import runfetchcmd
41from bb.utils import is_semver
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050042
Andrew Geissler82c905d2020-04-13 13:39:40 -050043def npm_package(package):
44 """Convert the npm package name to remove unsupported character"""
45 # Scoped package names (with the @) use the same naming convention
46 # as the 'npm pack' command.
Andrew Geissler8f840682023-07-21 09:09:43 -050047 name = re.sub("/", "-", package)
48 name = name.lower()
49 name = re.sub(r"[^\-a-z0-9]", "", name)
50 name = name.strip("-")
51 return name
52
Andrew Geissler82c905d2020-04-13 13:39:40 -050053
54def npm_filename(package, version):
55 """Get the filename of a npm package"""
56 return npm_package(package) + "-" + version + ".tgz"
57
Andrew Geissler7e0e3c02022-02-25 20:34:39 +000058def npm_localfile(package, version=None):
Andrew Geissler82c905d2020-04-13 13:39:40 -050059 """Get the local filename of a npm package"""
Andrew Geissler7e0e3c02022-02-25 20:34:39 +000060 if version is not None:
61 filename = npm_filename(package, version)
62 else:
63 filename = package
64 return os.path.join("npm2", filename)
Andrew Geissler82c905d2020-04-13 13:39:40 -050065
66def npm_integrity(integrity):
67 """
68 Get the checksum name and expected value from the subresource integrity
69 https://www.w3.org/TR/SRI/
70 """
71 algo, value = integrity.split("-", maxsplit=1)
72 return "%ssum" % algo, base64.b64decode(value).hex()
73
74def npm_unpack(tarball, destdir, d):
75 """Unpack a npm tarball"""
76 bb.utils.mkdirhier(destdir)
77 cmd = "tar --extract --gzip --file=%s" % shlex.quote(tarball)
78 cmd += " --no-same-owner"
Andrew Geisslereff27472021-10-29 15:35:00 -050079 cmd += " --delay-directory-restore"
Andrew Geissler82c905d2020-04-13 13:39:40 -050080 cmd += " --strip-components=1"
81 runfetchcmd(cmd, d, workdir=destdir)
Andrew Geissler595f6302022-01-24 19:11:47 +000082 runfetchcmd("chmod -R +X '%s'" % (destdir), d, quiet=True, workdir=destdir)
Andrew Geissler82c905d2020-04-13 13:39:40 -050083
84class NpmEnvironment(object):
85 """
86 Using a npm config file seems more reliable than using cli arguments.
87 This class allows to create a controlled environment for npm commands.
88 """
Stefan Herbrechtsmeier3d983ec2021-12-13 12:13:53 +010089 def __init__(self, d, configs=[], npmrc=None):
Andrew Geissler82c905d2020-04-13 13:39:40 -050090 self.d = d
Andrew Geisslereff27472021-10-29 15:35:00 -050091
Stefan Herbrechtsmeier3d983ec2021-12-13 12:13:53 +010092 self.user_config = tempfile.NamedTemporaryFile(mode="w", buffering=1)
93 for key, value in configs:
94 self.user_config.write("%s=%s\n" % (key, value))
Andrew Geisslereff27472021-10-29 15:35:00 -050095
96 if npmrc:
97 self.global_config_name = npmrc
98 else:
99 self.global_config_name = "/dev/null"
100
101 def __del__(self):
102 if self.user_config:
103 self.user_config.close()
Andrew Geissler82c905d2020-04-13 13:39:40 -0500104
105 def run(self, cmd, args=None, configs=None, workdir=None):
106 """Run npm command in a controlled environment"""
107 with tempfile.TemporaryDirectory() as tmpdir:
108 d = bb.data.createCopy(self.d)
Patrick Williamse760df82023-05-26 11:10:49 -0500109 d.setVar("PATH", d.getVar("PATH")) # PATH might contain $HOME - evaluate it before patching
Andrew Geissler82c905d2020-04-13 13:39:40 -0500110 d.setVar("HOME", tmpdir)
111
Andrew Geissler82c905d2020-04-13 13:39:40 -0500112 if not workdir:
113 workdir = tmpdir
114
115 def _run(cmd):
Stefan Herbrechtsmeier3d983ec2021-12-13 12:13:53 +0100116 cmd = "NPM_CONFIG_USERCONFIG=%s " % (self.user_config.name) + cmd
Andrew Geisslereff27472021-10-29 15:35:00 -0500117 cmd = "NPM_CONFIG_GLOBALCONFIG=%s " % (self.global_config_name) + cmd
Andrew Geissler82c905d2020-04-13 13:39:40 -0500118 return runfetchcmd(cmd, d, workdir=workdir)
119
Andrew Geissler82c905d2020-04-13 13:39:40 -0500120 if configs:
Andrew Geisslereff27472021-10-29 15:35:00 -0500121 bb.warn("Use of configs argument of NpmEnvironment.run() function"
122 " is deprecated. Please use args argument instead.")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500123 for key, value in configs:
Andrew Geisslereff27472021-10-29 15:35:00 -0500124 cmd += " --%s=%s" % (key, shlex.quote(value))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500125
126 if args:
127 for key, value in args:
128 cmd += " --%s=%s" % (key, shlex.quote(value))
129
130 return _run(cmd)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500131
132class Npm(FetchMethod):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500133 """Class to fetch a package from a npm registry"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500134
135 def supports(self, ud, d):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500136 """Check if a given url can be fetched with npm"""
137 return ud.type in ["npm"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500138
139 def urldata_init(self, ud, d):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500140 """Init npm specific variables within url data"""
141 ud.package = None
142 ud.version = None
143 ud.registry = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500144
Andrew Geissler82c905d2020-04-13 13:39:40 -0500145 # Get the 'package' parameter
146 if "package" in ud.parm:
147 ud.package = ud.parm.get("package")
148
149 if not ud.package:
150 raise MissingParameterError("Parameter 'package' required", ud.url)
151
152 # Get the 'version' parameter
153 if "version" in ud.parm:
154 ud.version = ud.parm.get("version")
155
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500156 if not ud.version:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500157 raise MissingParameterError("Parameter 'version' required", ud.url)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500158
Andrew Geissler82c905d2020-04-13 13:39:40 -0500159 if not is_semver(ud.version) and not ud.version == "latest":
160 raise ParameterError("Invalid 'version' parameter", ud.url)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500161
Andrew Geissler82c905d2020-04-13 13:39:40 -0500162 # Extract the 'registry' part of the url
Patrick Williams92b42cb2022-09-03 06:53:57 -0500163 ud.registry = re.sub(r"^npm://", "https://", ud.url.split(";")[0])
Andrew Geissler82c905d2020-04-13 13:39:40 -0500164
165 # Using the 'downloadfilename' parameter as local filename
166 # or the npm package name.
167 if "downloadfilename" in ud.parm:
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000168 ud.localfile = npm_localfile(d.expand(ud.parm["downloadfilename"]))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500169 else:
170 ud.localfile = npm_localfile(ud.package, ud.version)
171
172 # Get the base 'npm' command
173 ud.basecmd = d.getVar("FETCHCMD_npm") or "npm"
174
175 # This fetcher resolves a URI from a npm package name and version and
176 # then forwards it to a proxy fetcher. A resolve file containing the
177 # resolved URI is created to avoid unwanted network access (if the file
178 # already exists). The management of the donestamp file, the lockfile
179 # and the checksums are forwarded to the proxy fetcher.
180 ud.proxy = None
181 ud.needdonestamp = False
182 ud.resolvefile = self.localpath(ud, d) + ".resolved"
183
184 def _resolve_proxy_url(self, ud, d):
185 def _npm_view():
Andrew Geisslereff27472021-10-29 15:35:00 -0500186 args = []
187 args.append(("json", "true"))
188 args.append(("registry", ud.registry))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500189 pkgver = shlex.quote(ud.package + "@" + ud.version)
190 cmd = ud.basecmd + " view %s" % pkgver
191 env = NpmEnvironment(d)
192 check_network_access(d, cmd, ud.registry)
Andrew Geisslereff27472021-10-29 15:35:00 -0500193 view_string = env.run(cmd, args=args)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500194
195 if not view_string:
196 raise FetchError("Unavailable package %s" % pkgver, ud.url)
197
198 try:
199 view = json.loads(view_string)
200
201 error = view.get("error")
202 if error is not None:
203 raise FetchError(error.get("summary"), ud.url)
204
205 if ud.version == "latest":
206 bb.warn("The npm package %s is using the latest " \
207 "version available. This could lead to " \
208 "non-reproducible builds." % pkgver)
209 elif ud.version != view.get("version"):
210 raise ParameterError("Invalid 'version' parameter", ud.url)
211
212 return view
213
214 except Exception as e:
215 raise FetchError("Invalid view from npm: %s" % str(e), ud.url)
216
217 def _get_url(view):
218 tarball_url = view.get("dist", {}).get("tarball")
219
220 if tarball_url is None:
221 raise FetchError("Invalid 'dist.tarball' in view", ud.url)
222
223 uri = URI(tarball_url)
224 uri.params["downloadfilename"] = ud.localfile
225
226 integrity = view.get("dist", {}).get("integrity")
227 shasum = view.get("dist", {}).get("shasum")
228
229 if integrity is not None:
230 checksum_name, checksum_expected = npm_integrity(integrity)
231 uri.params[checksum_name] = checksum_expected
232 elif shasum is not None:
233 uri.params["sha1sum"] = shasum
234 else:
235 raise FetchError("Invalid 'dist.integrity' in view", ud.url)
236
237 return str(uri)
238
239 url = _get_url(_npm_view())
240
241 bb.utils.mkdirhier(os.path.dirname(ud.resolvefile))
242 with open(ud.resolvefile, "w") as f:
243 f.write(url)
244
245 def _setup_proxy(self, ud, d):
246 if ud.proxy is None:
247 if not os.path.exists(ud.resolvefile):
248 self._resolve_proxy_url(ud, d)
249
250 with open(ud.resolvefile, "r") as f:
251 url = f.read()
252
253 # Avoid conflicts between the environment data and:
254 # - the proxy url checksum
255 data = bb.data.createCopy(d)
256 data.delVarFlags("SRC_URI")
257 ud.proxy = Fetch([url], data)
258
259 def _get_proxy_method(self, ud, d):
260 self._setup_proxy(ud, d)
261 proxy_url = ud.proxy.urls[0]
262 proxy_ud = ud.proxy.ud[proxy_url]
263 proxy_d = ud.proxy.d
264 proxy_ud.setup_localpath(proxy_d)
265 return proxy_ud.method, proxy_ud, proxy_d
266
267 def verify_donestamp(self, ud, d):
268 """Verify the donestamp file"""
269 proxy_m, proxy_ud, proxy_d = self._get_proxy_method(ud, d)
270 return proxy_m.verify_donestamp(proxy_ud, proxy_d)
271
272 def update_donestamp(self, ud, d):
273 """Update the donestamp file"""
274 proxy_m, proxy_ud, proxy_d = self._get_proxy_method(ud, d)
275 proxy_m.update_donestamp(proxy_ud, proxy_d)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500276
277 def need_update(self, ud, d):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500278 """Force a fetch, even if localpath exists ?"""
279 if not os.path.exists(ud.resolvefile):
280 return True
281 if ud.version == "latest":
282 return True
283 proxy_m, proxy_ud, proxy_d = self._get_proxy_method(ud, d)
284 return proxy_m.need_update(proxy_ud, proxy_d)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500285
Andrew Geissler82c905d2020-04-13 13:39:40 -0500286 def try_mirrors(self, fetch, ud, d, mirrors):
287 """Try to use a mirror"""
288 proxy_m, proxy_ud, proxy_d = self._get_proxy_method(ud, d)
289 return proxy_m.try_mirrors(fetch, proxy_ud, proxy_d, mirrors)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500290
291 def download(self, ud, d):
292 """Fetch url"""
Andrew Geissler82c905d2020-04-13 13:39:40 -0500293 self._setup_proxy(ud, d)
294 ud.proxy.download()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500295
Andrew Geissler82c905d2020-04-13 13:39:40 -0500296 def unpack(self, ud, rootdir, d):
297 """Unpack the downloaded archive"""
298 destsuffix = ud.parm.get("destsuffix", "npm")
299 destdir = os.path.join(rootdir, destsuffix)
300 npm_unpack(ud.localpath, destdir, d)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500301
Andrew Geissler82c905d2020-04-13 13:39:40 -0500302 def clean(self, ud, d):
303 """Clean any existing full or partial download"""
304 if os.path.exists(ud.resolvefile):
305 self._setup_proxy(ud, d)
306 ud.proxy.clean()
307 bb.utils.remove(ud.resolvefile)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500308
Andrew Geissler82c905d2020-04-13 13:39:40 -0500309 def done(self, ud, d):
310 """Is the download done ?"""
311 if not os.path.exists(ud.resolvefile):
312 return False
313 proxy_m, proxy_ud, proxy_d = self._get_proxy_method(ud, d)
314 return proxy_m.done(proxy_ud, proxy_d)