blob: 730c346a936820d39fd0fe6d062f113152799b8b [file] [log] [blame]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' NPM implementation
5
6The NPM fetcher is used to retrieve files from the npmjs repository
7
8Usage in the recipe:
9
10 SRC_URI = "npm://registry.npmjs.org/;name=${PN};version=${PV}"
11 Suported SRC_URI options are:
12
13 - name
14 - version
15
Patrick Williamsc0f7c042017-02-23 20:41:17 -060016 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 -050017 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
18
19"""
20
21import os
22import sys
Patrick Williamsc0f7c042017-02-23 20:41:17 -060023import urllib.request, urllib.parse, urllib.error
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024import json
25import subprocess
26import signal
27import bb
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050028from bb.fetch2 import FetchMethod
29from bb.fetch2 import FetchError
30from bb.fetch2 import ChecksumError
31from bb.fetch2 import runfetchcmd
32from bb.fetch2 import logger
33from bb.fetch2 import UnpackError
34from bb.fetch2 import ParameterError
35from distutils import spawn
36
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 '''
154 pdata = None
155 outdeps = {}
156 datalines = []
157 bracelevel = 0
158 for line in output.splitlines():
159 if bracelevel:
160 datalines.append(line)
161 elif '{' in line:
162 datalines = []
163 datalines.append(line)
164 bracelevel = bracelevel + line.count('{') - line.count('}')
165 if datalines:
166 pdata = json.loads('\n'.join(datalines))
167 return pdata
168
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600169 def _getdependencies(self, pkg, data, version, d, ud, optional=False, fetchedlist=None):
170 if fetchedlist is None:
171 fetchedlist = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500172 pkgfullname = pkg
173 if version != '*' and not '/' in version:
174 pkgfullname += "@'%s'" % version
175 logger.debug(2, "Calling getdeps on %s" % pkg)
176 fetchcmd = "npm view %s --json --registry %s" % (pkgfullname, ud.registry)
177 output = runfetchcmd(fetchcmd, d, True)
178 pdata = self._parse_view(output)
179 if not pdata:
180 raise FetchError("The command '%s' returned no output" % fetchcmd)
181 if optional:
182 pkg_os = pdata.get('os', None)
183 if pkg_os:
184 if not isinstance(pkg_os, list):
185 pkg_os = [pkg_os]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500186 blacklist = False
187 for item in pkg_os:
188 if item.startswith('!'):
189 blacklist = True
190 break
191 if (not blacklist and 'linux' not in pkg_os) or '!linux' in pkg_os:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500192 logger.debug(2, "Skipping %s since it's incompatible with Linux" % pkg)
193 return
194 #logger.debug(2, "Output URL is %s - %s - %s" % (ud.basepath, ud.basename, ud.localfile))
195 outputurl = pdata['dist']['tarball']
196 data[pkg] = {}
197 data[pkg]['tgz'] = os.path.basename(outputurl)
Brad Bishop316dfdd2018-06-25 12:45:53 -0400198 if outputurl in fetchedlist:
199 return
200
201 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
202 fetchedlist.append(outputurl)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500203
204 dependencies = pdata.get('dependencies', {})
205 optionalDependencies = pdata.get('optionalDependencies', {})
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500206 dependencies.update(optionalDependencies)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500207 depsfound = {}
208 optdepsfound = {}
209 data[pkg]['deps'] = {}
210 for dep in dependencies:
211 if dep in optionalDependencies:
212 optdepsfound[dep] = dependencies[dep]
213 else:
214 depsfound[dep] = dependencies[dep]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600215 for dep, version in optdepsfound.items():
216 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, optional=True, fetchedlist=fetchedlist)
217 for dep, version in depsfound.items():
218 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, fetchedlist=fetchedlist)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500219
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600220 def _getshrinkeddependencies(self, pkg, data, version, d, ud, lockdown, manifest, toplevel=True):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 logger.debug(2, "NPM shrinkwrap file is %s" % data)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600222 if toplevel:
223 name = data.get('name', None)
224 if name and name != pkg:
225 for obj in data.get('dependencies', []):
226 if obj == pkg:
227 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest, False)
228 return
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500229 outputurl = "invalid"
230 if ('resolved' not in data) or (not data['resolved'].startswith('http')):
231 # will be the case for ${PN}
232 fetchcmd = "npm view %s@%s dist.tarball --registry %s" % (pkg, version, ud.registry)
233 logger.debug(2, "Found this matching URL: %s" % str(fetchcmd))
234 outputurl = runfetchcmd(fetchcmd, d, True)
235 else:
236 outputurl = data['resolved']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600237 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500238 manifest[pkg] = {}
239 manifest[pkg]['tgz'] = os.path.basename(outputurl).rstrip()
240 manifest[pkg]['deps'] = {}
241
242 if pkg in lockdown:
243 sha1_expected = lockdown[pkg][version]
244 sha1_data = bb.utils.sha1_file("npm/%s/%s" % (ud.pkgname, manifest[pkg]['tgz']))
245 if sha1_expected != sha1_data:
246 msg = "\nFile: '%s' has %s checksum %s when %s was expected" % (manifest[pkg]['tgz'], 'sha1', sha1_data, sha1_expected)
247 raise ChecksumError('Checksum mismatch!%s' % msg)
248 else:
249 logger.debug(2, "No lockdown data for %s@%s" % (pkg, version))
250
251 if 'dependencies' in data:
252 for obj in data['dependencies']:
253 logger.debug(2, "Found dep is %s" % str(obj))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600254 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest[pkg]['deps'], False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500255
256 def download(self, ud, d):
257 """Fetch url"""
258 jsondepobj = {}
259 shrinkobj = {}
260 lockdown = {}
261
262 if not os.listdir(ud.pkgdatadir) and os.path.exists(ud.fullmirror):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500263 dest = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500264 bb.utils.mkdirhier(dest)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600265 runfetchcmd("tar -xJf %s" % (ud.fullmirror), d, workdir=dest)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500266 return
267
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500268 if ud.parm.get("noverify", None) != '1':
269 shwrf = d.getVar('NPM_SHRINKWRAP')
270 logger.debug(2, "NPM shrinkwrap file is %s" % shwrf)
271 if shwrf:
272 try:
273 with open(shwrf) as datafile:
274 shrinkobj = json.load(datafile)
275 except Exception as e:
276 raise FetchError('Error loading NPM_SHRINKWRAP file "%s" for %s: %s' % (shwrf, ud.pkgname, str(e)))
277 elif not ud.ignore_checksums:
278 logger.warning('Missing shrinkwrap file in NPM_SHRINKWRAP for %s, this will lead to unreliable builds!' % ud.pkgname)
279 lckdf = d.getVar('NPM_LOCKDOWN')
280 logger.debug(2, "NPM lockdown file is %s" % lckdf)
281 if lckdf:
282 try:
283 with open(lckdf) as datafile:
284 lockdown = json.load(datafile)
285 except Exception as e:
286 raise FetchError('Error loading NPM_LOCKDOWN file "%s" for %s: %s' % (lckdf, ud.pkgname, str(e)))
287 elif not ud.ignore_checksums:
288 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 -0500289
290 if ('name' not in shrinkobj):
291 self._getdependencies(ud.pkgname, jsondepobj, ud.version, d, ud)
292 else:
293 self._getshrinkeddependencies(ud.pkgname, shrinkobj, ud.version, d, ud, lockdown, jsondepobj)
294
295 with open(ud.localpath, 'w') as outfile:
296 json.dump(jsondepobj, outfile)
297
298 def build_mirror_data(self, ud, d):
299 # Generate a mirror tarball if needed
300 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
301 # it's possible that this symlink points to read-only filesystem with PREMIRROR
302 if os.path.islink(ud.fullmirror):
303 os.unlink(ud.fullmirror)
304
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500305 dldir = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500306 logger.info("Creating tarball of npm data")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600307 runfetchcmd("tar -cJf %s npm/%s npm/%s" % (ud.fullmirror, ud.bbnpmmanifest, ud.pkgname), d,
308 workdir=dldir)
309 runfetchcmd("touch %s.done" % (ud.fullmirror), d, workdir=dldir)