libvips.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // Copyright 2013 Lovell Fuller and others.
  2. // SPDX-License-Identifier: Apache-2.0
  3. 'use strict';
  4. const fs = require('fs');
  5. const os = require('os');
  6. const path = require('path');
  7. const stream = require('stream');
  8. const zlib = require('zlib');
  9. const { createHash } = require('crypto');
  10. const detectLibc = require('detect-libc');
  11. const semverCoerce = require('semver/functions/coerce');
  12. const semverLessThan = require('semver/functions/lt');
  13. const semverSatisfies = require('semver/functions/satisfies');
  14. const simpleGet = require('simple-get');
  15. const tarFs = require('tar-fs');
  16. const agent = require('../lib/agent');
  17. const libvips = require('../lib/libvips');
  18. const platform = require('../lib/platform');
  19. const minimumGlibcVersionByArch = {
  20. arm: '2.28',
  21. arm64: '2.17',
  22. x64: '2.17'
  23. };
  24. const hasSharpPrebuild = [
  25. 'darwin-x64',
  26. 'darwin-arm64',
  27. 'linux-arm64',
  28. 'linux-x64',
  29. 'linuxmusl-x64',
  30. 'linuxmusl-arm64',
  31. 'win32-ia32',
  32. 'win32-x64'
  33. ];
  34. const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips;
  35. const localLibvipsDir = process.env.npm_config_sharp_libvips_local_prebuilds || '';
  36. const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download';
  37. const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`;
  38. const installationForced = !!(process.env.npm_config_sharp_install_force || process.env.SHARP_INSTALL_FORCE);
  39. const fail = function (err) {
  40. libvips.log(err);
  41. if (err.code === 'EACCES') {
  42. libvips.log('Are you trying to install as a root or sudo user?');
  43. libvips.log('- For npm <= v6, try again with the "--unsafe-perm" flag');
  44. libvips.log('- For npm >= v8, the user must own the directory "npm install" is run in');
  45. }
  46. libvips.log('Please see https://sharp.pixelplumbing.com/install for required dependencies');
  47. process.exit(1);
  48. };
  49. const handleError = function (err) {
  50. if (installationForced) {
  51. libvips.log(`Installation warning: ${err.message}`);
  52. } else {
  53. throw err;
  54. }
  55. };
  56. const verifyIntegrity = function (platformAndArch) {
  57. const expected = libvips.integrity(platformAndArch);
  58. if (installationForced || !expected) {
  59. libvips.log(`Integrity check skipped for ${platformAndArch}`);
  60. return new stream.PassThrough();
  61. }
  62. const hash = createHash('sha512');
  63. return new stream.Transform({
  64. transform: function (chunk, _encoding, done) {
  65. hash.update(chunk);
  66. done(null, chunk);
  67. },
  68. flush: function (done) {
  69. const digest = `sha512-${hash.digest('base64')}`;
  70. if (expected !== digest) {
  71. try {
  72. libvips.removeVendoredLibvips();
  73. } catch (err) {
  74. libvips.log(err.message);
  75. }
  76. libvips.log(`Integrity expected: ${expected}`);
  77. libvips.log(`Integrity received: ${digest}`);
  78. done(new Error(`Integrity check failed for ${platformAndArch}`));
  79. } else {
  80. libvips.log(`Integrity check passed for ${platformAndArch}`);
  81. done();
  82. }
  83. }
  84. });
  85. };
  86. const extractTarball = function (tarPath, platformAndArch) {
  87. const versionedVendorPath = path.join(__dirname, '..', 'vendor', minimumLibvipsVersion, platformAndArch);
  88. libvips.mkdirSync(versionedVendorPath);
  89. const ignoreVendorInclude = hasSharpPrebuild.includes(platformAndArch) && !process.env.npm_config_build_from_source;
  90. const ignore = function (name) {
  91. return ignoreVendorInclude && name.includes('include/');
  92. };
  93. stream.pipeline(
  94. fs.createReadStream(tarPath),
  95. verifyIntegrity(platformAndArch),
  96. new zlib.BrotliDecompress(),
  97. tarFs.extract(versionedVendorPath, { ignore }),
  98. function (err) {
  99. if (err) {
  100. if (/unexpected end of file/.test(err.message)) {
  101. fail(new Error(`Please delete ${tarPath} as it is not a valid tarball`));
  102. }
  103. fail(err);
  104. }
  105. }
  106. );
  107. };
  108. try {
  109. const useGlobalLibvips = libvips.useGlobalLibvips();
  110. if (useGlobalLibvips) {
  111. const globalLibvipsVersion = libvips.globalLibvipsVersion();
  112. libvips.log(`Detected globally-installed libvips v${globalLibvipsVersion}`);
  113. libvips.log('Building from source via node-gyp');
  114. process.exit(1);
  115. } else if (libvips.hasVendoredLibvips()) {
  116. libvips.log(`Using existing vendored libvips v${minimumLibvipsVersion}`);
  117. } else {
  118. // Is this arch/platform supported?
  119. const arch = process.env.npm_config_arch || process.arch;
  120. const platformAndArch = platform();
  121. if (arch === 'ia32' && !platformAndArch.startsWith('win32')) {
  122. throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
  123. }
  124. if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') {
  125. throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
  126. }
  127. // Linux libc version check
  128. const libcVersionRaw = detectLibc.versionSync();
  129. if (libcVersionRaw) {
  130. const libcFamily = detectLibc.familySync();
  131. const libcVersion = semverCoerce(libcVersionRaw).version;
  132. if (libcFamily === detectLibc.GLIBC && minimumGlibcVersionByArch[arch]) {
  133. if (semverLessThan(libcVersion, semverCoerce(minimumGlibcVersionByArch[arch]).version)) {
  134. handleError(new Error(`Use with glibc ${libcVersionRaw} requires manual installation of libvips >= ${minimumLibvipsVersion}`));
  135. }
  136. }
  137. if (libcFamily === detectLibc.MUSL) {
  138. if (semverLessThan(libcVersion, '1.1.24')) {
  139. handleError(new Error(`Use with musl ${libcVersionRaw} requires manual installation of libvips >= ${minimumLibvipsVersion}`));
  140. }
  141. }
  142. }
  143. // Node.js minimum version check
  144. const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node;
  145. if (!semverSatisfies(process.versions.node, supportedNodeVersion)) {
  146. handleError(new Error(`Expected Node.js version ${supportedNodeVersion} but found ${process.versions.node}`));
  147. }
  148. // Download to per-process temporary file
  149. const tarFilename = ['libvips', minimumLibvipsVersionLabelled, platformAndArch].join('-') + '.tar.br';
  150. const tarPathCache = path.join(libvips.cachePath(), tarFilename);
  151. if (fs.existsSync(tarPathCache)) {
  152. libvips.log(`Using cached ${tarPathCache}`);
  153. extractTarball(tarPathCache, platformAndArch);
  154. } else if (localLibvipsDir) {
  155. // If localLibvipsDir is given try to use binaries from local directory
  156. const tarPathLocal = path.join(path.resolve(localLibvipsDir), `v${minimumLibvipsVersionLabelled}`, tarFilename);
  157. libvips.log(`Using local libvips from ${tarPathLocal}`);
  158. extractTarball(tarPathLocal, platformAndArch);
  159. } else {
  160. const url = distBaseUrl + tarFilename;
  161. libvips.log(`Downloading ${url}`);
  162. simpleGet({ url: url, agent: agent(libvips.log) }, function (err, response) {
  163. if (err) {
  164. fail(err);
  165. } else if (response.statusCode === 404) {
  166. fail(new Error(`Prebuilt libvips ${minimumLibvipsVersion} binaries are not yet available for ${platformAndArch}`));
  167. } else if (response.statusCode !== 200) {
  168. fail(new Error(`Status ${response.statusCode} ${response.statusMessage}`));
  169. } else {
  170. const tarPathTemp = path.join(os.tmpdir(), `${process.pid}-${tarFilename}`);
  171. const tmpFileStream = fs.createWriteStream(tarPathTemp);
  172. response
  173. .on('error', function (err) {
  174. tmpFileStream.destroy(err);
  175. })
  176. .on('close', function () {
  177. if (!response.complete) {
  178. tmpFileStream.destroy(new Error('Download incomplete (connection was terminated)'));
  179. }
  180. })
  181. .pipe(tmpFileStream);
  182. tmpFileStream
  183. .on('error', function (err) {
  184. // Clean up temporary file
  185. try {
  186. fs.unlinkSync(tarPathTemp);
  187. } catch (e) {}
  188. fail(err);
  189. })
  190. .on('close', function () {
  191. try {
  192. // Attempt to rename
  193. fs.renameSync(tarPathTemp, tarPathCache);
  194. } catch (err) {
  195. // Fall back to copy and unlink
  196. fs.copyFileSync(tarPathTemp, tarPathCache);
  197. fs.unlinkSync(tarPathTemp);
  198. }
  199. extractTarball(tarPathCache, platformAndArch);
  200. });
  201. }
  202. });
  203. }
  204. }
  205. } catch (err) {
  206. fail(err);
  207. }