blob: b5f148ca03367452c5ee7aedd9d78044eefa7dc8 [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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600198 if not outputurl in fetchedlist:
199 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
200 fetchedlist.append(outputurl)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500201
202 dependencies = pdata.get('dependencies', {})
203 optionalDependencies = pdata.get('optionalDependencies', {})
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500204 dependencies.update(optionalDependencies)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500205 depsfound = {}
206 optdepsfound = {}
207 data[pkg]['deps'] = {}
208 for dep in dependencies:
209 if dep in optionalDependencies:
210 optdepsfound[dep] = dependencies[dep]
211 else:
212 depsfound[dep] = dependencies[dep]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600213 for dep, version in optdepsfound.items():
214 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, optional=True, fetchedlist=fetchedlist)
215 for dep, version in depsfound.items():
216 self._getdependencies(dep, data[pkg]['deps'], version, d, ud, fetchedlist=fetchedlist)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500217
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600218 def _getshrinkeddependencies(self, pkg, data, version, d, ud, lockdown, manifest, toplevel=True):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500219 logger.debug(2, "NPM shrinkwrap file is %s" % data)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600220 if toplevel:
221 name = data.get('name', None)
222 if name and name != pkg:
223 for obj in data.get('dependencies', []):
224 if obj == pkg:
225 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest, False)
226 return
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500227 outputurl = "invalid"
228 if ('resolved' not in data) or (not data['resolved'].startswith('http')):
229 # will be the case for ${PN}
230 fetchcmd = "npm view %s@%s dist.tarball --registry %s" % (pkg, version, ud.registry)
231 logger.debug(2, "Found this matching URL: %s" % str(fetchcmd))
232 outputurl = runfetchcmd(fetchcmd, d, True)
233 else:
234 outputurl = data['resolved']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600235 self._runwget(ud, d, "%s --directory-prefix=%s %s" % (self.basecmd, ud.prefixdir, outputurl), False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500236 manifest[pkg] = {}
237 manifest[pkg]['tgz'] = os.path.basename(outputurl).rstrip()
238 manifest[pkg]['deps'] = {}
239
240 if pkg in lockdown:
241 sha1_expected = lockdown[pkg][version]
242 sha1_data = bb.utils.sha1_file("npm/%s/%s" % (ud.pkgname, manifest[pkg]['tgz']))
243 if sha1_expected != sha1_data:
244 msg = "\nFile: '%s' has %s checksum %s when %s was expected" % (manifest[pkg]['tgz'], 'sha1', sha1_data, sha1_expected)
245 raise ChecksumError('Checksum mismatch!%s' % msg)
246 else:
247 logger.debug(2, "No lockdown data for %s@%s" % (pkg, version))
248
249 if 'dependencies' in data:
250 for obj in data['dependencies']:
251 logger.debug(2, "Found dep is %s" % str(obj))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600252 self._getshrinkeddependencies(obj, data['dependencies'][obj], data['dependencies'][obj]['version'], d, ud, lockdown, manifest[pkg]['deps'], False)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500253
254 def download(self, ud, d):
255 """Fetch url"""
256 jsondepobj = {}
257 shrinkobj = {}
258 lockdown = {}
259
260 if not os.listdir(ud.pkgdatadir) and os.path.exists(ud.fullmirror):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500261 dest = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500262 bb.utils.mkdirhier(dest)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600263 runfetchcmd("tar -xJf %s" % (ud.fullmirror), d, workdir=dest)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500264 return
265
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500266 if ud.parm.get("noverify", None) != '1':
267 shwrf = d.getVar('NPM_SHRINKWRAP')
268 logger.debug(2, "NPM shrinkwrap file is %s" % shwrf)
269 if shwrf:
270 try:
271 with open(shwrf) as datafile:
272 shrinkobj = json.load(datafile)
273 except Exception as e:
274 raise FetchError('Error loading NPM_SHRINKWRAP file "%s" for %s: %s' % (shwrf, ud.pkgname, str(e)))
275 elif not ud.ignore_checksums:
276 logger.warning('Missing shrinkwrap file in NPM_SHRINKWRAP for %s, this will lead to unreliable builds!' % ud.pkgname)
277 lckdf = d.getVar('NPM_LOCKDOWN')
278 logger.debug(2, "NPM lockdown file is %s" % lckdf)
279 if lckdf:
280 try:
281 with open(lckdf) as datafile:
282 lockdown = json.load(datafile)
283 except Exception as e:
284 raise FetchError('Error loading NPM_LOCKDOWN file "%s" for %s: %s' % (lckdf, ud.pkgname, str(e)))
285 elif not ud.ignore_checksums:
286 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 -0500287
288 if ('name' not in shrinkobj):
289 self._getdependencies(ud.pkgname, jsondepobj, ud.version, d, ud)
290 else:
291 self._getshrinkeddependencies(ud.pkgname, shrinkobj, ud.version, d, ud, lockdown, jsondepobj)
292
293 with open(ud.localpath, 'w') as outfile:
294 json.dump(jsondepobj, outfile)
295
296 def build_mirror_data(self, ud, d):
297 # Generate a mirror tarball if needed
298 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
299 # it's possible that this symlink points to read-only filesystem with PREMIRROR
300 if os.path.islink(ud.fullmirror):
301 os.unlink(ud.fullmirror)
302
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500303 dldir = d.getVar("DL_DIR")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500304 logger.info("Creating tarball of npm data")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600305 runfetchcmd("tar -cJf %s npm/%s npm/%s" % (ud.fullmirror, ud.bbnpmmanifest, ud.pkgname), d,
306 workdir=dldir)
307 runfetchcmd("touch %s.done" % (ud.fullmirror), d, workdir=dldir)