blob: 4427b1bb879d7207f08d323ef4077860c6a0cfb2 [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05004"""
5BitBake 'Fetch' NPM implementation
6
7The NPM fetcher is used to retrieve files from the npmjs repository
8
9Usage in the recipe:
10
11 SRC_URI = "npm://registry.npmjs.org/;name=${PN};version=${PV}"
12 Suported SRC_URI options are:
13
14 - name
15 - version
16
Patrick Williamsc0f7c042017-02-23 20:41:17 -060017 npm://registry.npmjs.org/${PN}/-/${PN}-${PV}.tgz would become npm://registry.npmjs.org;name=${PN};version=${PV}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050018 The fetcher all triggers off the existence of ud.localpath. If that exists and has the ".done" stamp, its assumed the fetch is good/done
19
20"""
21
22import os
23import sys
Patrick Williamsc0f7c042017-02-23 20:41:17 -060024import urllib.request, urllib.parse, urllib.error
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050025import json
26import subprocess
27import signal
28import bb
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050029from bb.fetch2 import FetchMethod
30from bb.fetch2 import FetchError
31from bb.fetch2 import ChecksumError
32from bb.fetch2 import runfetchcmd
33from bb.fetch2 import logger
34from bb.fetch2 import UnpackError
35from bb.fetch2 import ParameterError
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050036
37def subprocess_setup():
38 # Python installs a SIGPIPE handler by default. This is usually not what
39 # non-Python subprocesses expect.
40 # SIGPIPE errors are known issues with gzip/bash
41 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
42
43class Npm(FetchMethod):
44
45 """Class to fetch urls via 'npm'"""
46 def init(self, d):
47 pass
48
49 def supports(self, ud, d):
50 """
51 Check to see if a given url can be fetched with npm
52 """
53 return ud.type in ['npm']
54
55 def debug(self, msg):
56 logger.debug(1, "NpmFetch: %s", msg)
57
58 def clean(self, ud, d):
59 logger.debug(2, "Calling cleanup %s" % ud.pkgname)
60 bb.utils.remove(ud.localpath, False)
61 bb.utils.remove(ud.pkgdatadir, True)
62 bb.utils.remove(ud.fullmirror, False)
63
64 def urldata_init(self, ud, d):
65 """
66 init NPM specific variable within url data
67 """
68 if 'downloadfilename' in ud.parm:
69 ud.basename = ud.parm['downloadfilename']
70 else:
71 ud.basename = os.path.basename(ud.path)
72
73 # can't call it ud.name otherwise fetcher base class will start doing sha1stuff
74 # TODO: find a way to get an sha1/sha256 manifest of pkg & all deps
75 ud.pkgname = ud.parm.get("name", None)
76 if not ud.pkgname:
77 raise ParameterError("NPM fetcher requires a name parameter", ud.url)
78 ud.version = ud.parm.get("version", None)
79 if not ud.version:
80 raise ParameterError("NPM fetcher requires a version parameter", ud.url)
81 ud.bbnpmmanifest = "%s-%s.deps.json" % (ud.pkgname, ud.version)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050082 ud.bbnpmmanifest = ud.bbnpmmanifest.replace('/', '-')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050083 ud.registry = "http://%s" % (ud.url.replace('npm://', '', 1).split(';'))[0]
84 prefixdir = "npm/%s" % ud.pkgname
85 ud.pkgdatadir = d.expand("${DL_DIR}/%s" % prefixdir)
86 if not os.path.exists(ud.pkgdatadir):
87 bb.utils.mkdirhier(ud.pkgdatadir)
88 ud.localpath = d.expand("${DL_DIR}/npm/%s" % ud.bbnpmmanifest)
89
Brad Bishop6e60e8b2018-02-01 10:27:11 -050090 self.basecmd = d.getVar("FETCHCMD_wget") or "/usr/bin/env wget -O -t 2 -T 30 -nv --passive-ftp --no-check-certificate "
Patrick Williamsc0f7c042017-02-23 20:41:17 -060091 ud.prefixdir = prefixdir
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050092
Brad Bishop6e60e8b2018-02-01 10:27:11 -050093 ud.write_tarballs = ((d.getVar("BB_GENERATE_MIRROR_TARBALLS") or "0") != "0")
Brad Bishopd7bf8c12018-02-25 22:55:05 -050094 mirrortarball = 'npm_%s-%s.tar.xz' % (ud.pkgname, ud.version)
95 mirrortarball = mirrortarball.replace('/', '-')
96 ud.fullmirror = os.path.join(d.getVar("DL_DIR"), mirrortarball)
97 ud.mirrortarballs = [mirrortarball]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050098
99 def need_update(self, ud, d):
100 if os.path.exists(ud.localpath):
101 return False
102 return True
103
104 def _runwget(self, ud, d, command, quiet):
105 logger.debug(2, "Fetching %s using command '%s'" % (ud.url, command))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500106 bb.fetch2.check_network_access(d, command, ud.url)
107 dldir = d.getVar("DL_DIR")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600108 runfetchcmd(command, d, quiet, workdir=dldir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500109
110 def _unpackdep(self, ud, pkg, data, destdir, dldir, d):
111 file = data[pkg]['tgz']
112 logger.debug(2, "file to extract is %s" % file)
113 if file.endswith('.tgz') or file.endswith('.tar.gz') or file.endswith('.tar.Z'):
114 cmd = 'tar xz --strip 1 --no-same-owner --warning=no-unknown-keyword -f %s/%s' % (dldir, file)
115 else:
116 bb.fatal("NPM package %s downloaded not a tarball!" % file)
117
118 # Change to subdir before executing command
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500119 if not os.path.exists(destdir):
120 os.makedirs(destdir)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500121 path = d.getVar('PATH')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500122 if path:
123 cmd = "PATH=\"%s\" %s" % (path, cmd)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600124 bb.note("Unpacking %s to %s/" % (file, destdir))
125 ret = subprocess.call(cmd, preexec_fn=subprocess_setup, shell=True, cwd=destdir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500126
127 if ret != 0:
128 raise UnpackError("Unpack command %s failed with return value %s" % (cmd, ret), ud.url)
129
130 if 'deps' not in data[pkg]:
131 return
132 for dep in data[pkg]['deps']:
133 self._unpackdep(ud, dep, data[pkg]['deps'], "%s/node_modules/%s" % (destdir, dep), dldir, d)
134
135
136 def unpack(self, ud, destdir, d):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500137 dldir = d.getVar("DL_DIR")
138 with open("%s/npm/%s" % (dldir, ud.bbnpmmanifest)) as datafile:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500139 workobj = json.load(datafile)
140 dldir = "%s/%s" % (os.path.dirname(ud.localpath), ud.pkgname)
141
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600142 if 'subdir' in ud.parm:
143 unpackdir = '%s/%s' % (destdir, ud.parm.get('subdir'))
144 else:
145 unpackdir = '%s/npmpkg' % destdir
146
147 self._unpackdep(ud, ud.pkgname, workobj, unpackdir, dldir, d)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500148
149 def _parse_view(self, output):
150 '''
151 Parse the output of npm view --json; the last JSON result
152 is assumed to be the one that we're interested in.
153 '''
Brad Bishop15ae2502019-06-18 21:44:24 -0400154 pdata = json.loads(output);
155 try:
156 return pdata[-1]
157 except:
158 return pdata
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500159
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600160 def _getdependencies(self, pkg, data, version, d, ud, optional=False, fetchedlist=None):
161 if fetchedlist is None:
162 fetchedlist = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500163 pkgfullname = pkg
164 if version != '*' and not '/' in version:
165 pkgfullname += "@'%s'" % version
166 logger.debug(2, "Calling getdeps on %s" % pkg)
167 fetchcmd = "npm view %s --json --registry %s" % (pkgfullname, ud.registry)
168 output = runfetchcmd(fetchcmd, d, True)
169 pdata = self._parse_view(output)
170 if not pdata:
171 raise FetchError("The command '%s' returned no output" % fetchcmd)
172 if optional:
173 pkg_os = pdata.get('os', None)
174 if pkg_os:
175 if not isinstance(pkg_os, list):
176 pkg_os = [pkg_os]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500177 blacklist = False
178 for item in pkg_os:
179 if item.startswith('!'):
180 blacklist = True
181 break
182 if (not blacklist and 'linux' not in pkg_os) or '!linux' in pkg_os:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500183 logger.debug(2, "Skipping %s since it's incompatible with Linux" % pkg)
184 return
185 #logger.debug(2, "Output URL is %s - %s - %s" % (ud.basepath, ud.basename, ud.localfile))
186 outputurl = pdata['dist']['tarball']
187 data[pkg] = {}
188 data[pkg]['tgz'] = os.path.basename(outputurl)
Brad Bishop316dfdd2018-06-25 12:45:53 -0400189 if outputurl in fetchedlist:
190 return
191
192 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
193 fetchedlist.append(outputurl)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500194
195 dependencies = pdata.get('dependencies', {})
196 optionalDependencies = pdata.get('optionalDependencies', {})
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500197 dependencies.update(optionalDependencies)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500198 depsfound = {}
199 optdepsfound = {}
200 data[pkg]['deps'] = {}
201 for dep in dependencies:
202 if dep in optionalDependencies:
203 optdepsfound[dep] = dependencies[dep]
204 else:
205 depsfound[dep] = dependencies[dep]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600206 for dep, version in optdepsfound.items():
207 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, optional=True, fetchedlist=fetchedlist)
208 for dep, version in depsfound.items():
209 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, fetchedlist=fetchedlist)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500210
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600211 def _getshrinkeddependencies(self, pkg, data, version, d, ud, lockdown, manifest, toplevel=True):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500212 logger.debug(2, "NPM shrinkwrap file is %s" % data)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600213 if toplevel:
214 name = data.get('name', None)
215 if name and name != pkg:
216 for obj in data.get('dependencies', []):
217 if obj == pkg:
218 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest, False)
219 return
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500220 outputurl = "invalid"
Brad Bishop19323692019-04-05 15:28:33 -0400221 if ('resolved' not in data) or (not data['resolved'].startswith('http://') and not data['resolved'].startswith('https://')):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500222 # will be the case for ${PN}
223 fetchcmd = "npm view %s@%s dist.tarball --registry %s" % (pkg, version, ud.registry)
224 logger.debug(2, "Found this matching URL: %s" % str(fetchcmd))
225 outputurl = runfetchcmd(fetchcmd, d, True)
226 else:
227 outputurl = data['resolved']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600228 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500229 manifest[pkg] = {}
230 manifest[pkg]['tgz'] = os.path.basename(outputurl).rstrip()
231 manifest[pkg]['deps'] = {}
232
233 if pkg in lockdown:
234 sha1_expected = lockdown[pkg][version]
235 sha1_data = bb.utils.sha1_file("npm/%s/%s" % (ud.pkgname, manifest[pkg]['tgz']))
236 if sha1_expected != sha1_data:
237 msg = "\nFile: '%s' has %s checksum %s when %s was expected" % (manifest[pkg]['tgz'], 'sha1', sha1_data, sha1_expected)
238 raise ChecksumError('Checksum mismatch!%s' % msg)
239 else:
240 logger.debug(2, "No lockdown data for %s@%s" % (pkg, version))
241
242 if 'dependencies' in data:
243 for obj in data['dependencies']:
244 logger.debug(2, "Found dep is %s" % str(obj))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600245 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest[pkg]['deps'], False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500246
247 def download(self, ud, d):
248 """Fetch url"""
249 jsondepobj = {}
250 shrinkobj = {}
251 lockdown = {}
252
253 if not os.listdir(ud.pkgdatadir) and os.path.exists(ud.fullmirror):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500254 dest = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500255 bb.utils.mkdirhier(dest)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600256 runfetchcmd("tar -xJf %s" % (ud.fullmirror), d, workdir=dest)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500257 return
258
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500259 if ud.parm.get("noverify", None) != '1':
260 shwrf = d.getVar('NPM_SHRINKWRAP')
261 logger.debug(2, "NPM shrinkwrap file is %s" % shwrf)
262 if shwrf:
263 try:
264 with open(shwrf) as datafile:
265 shrinkobj = json.load(datafile)
266 except Exception as e:
267 raise FetchError('Error loading NPM_SHRINKWRAP file "%s" for %s: %s' % (shwrf, ud.pkgname, str(e)))
268 elif not ud.ignore_checksums:
269 logger.warning('Missing shrinkwrap file in NPM_SHRINKWRAP for %s, this will lead to unreliable builds!' % ud.pkgname)
270 lckdf = d.getVar('NPM_LOCKDOWN')
271 logger.debug(2, "NPM lockdown file is %s" % lckdf)
272 if lckdf:
273 try:
274 with open(lckdf) as datafile:
275 lockdown = json.load(datafile)
276 except Exception as e:
277 raise FetchError('Error loading NPM_LOCKDOWN file "%s" for %s: %s' % (lckdf, ud.pkgname, str(e)))
278 elif not ud.ignore_checksums:
279 logger.warning('Missing lockdown file in NPM_LOCKDOWN for %s, this will lead to unreproducible builds!' % ud.pkgname)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500280
281 if ('name' not in shrinkobj):
282 self._getdependencies(ud.pkgname, jsondepobj, ud.version, d, ud)
283 else:
284 self._getshrinkeddependencies(ud.pkgname, shrinkobj, ud.version, d, ud, lockdown, jsondepobj)
285
286 with open(ud.localpath, 'w') as outfile:
287 json.dump(jsondepobj, outfile)
288
289 def build_mirror_data(self, ud, d):
290 # Generate a mirror tarball if needed
291 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
292 # it's possible that this symlink points to read-only filesystem with PREMIRROR
293 if os.path.islink(ud.fullmirror):
294 os.unlink(ud.fullmirror)
295
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500296 dldir = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297 logger.info("Creating tarball of npm data")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600298 runfetchcmd("tar -cJf %s npm/%s npm/%s" % (ud.fullmirror, ud.bbnpmmanifest, ud.pkgname), d,
299 workdir=dldir)
300 runfetchcmd("touch %s.done" % (ud.fullmirror), d, workdir=dldir)