dependencies.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.dockerVersion = dockerVersion;
  6. exports.installDependenciesLinux = installDependenciesLinux;
  7. exports.installDependenciesWindows = installDependenciesWindows;
  8. exports.readDockerVersionSync = readDockerVersionSync;
  9. exports.transformCommandsForRoot = transformCommandsForRoot;
  10. exports.validateDependenciesLinux = validateDependenciesLinux;
  11. exports.validateDependenciesWindows = validateDependenciesWindows;
  12. exports.writeDockerVersion = writeDockerVersion;
  13. var _fs = _interopRequireDefault(require("fs"));
  14. var _path = _interopRequireDefault(require("path"));
  15. var os = _interopRequireWildcard(require("os"));
  16. var _child_process = _interopRequireDefault(require("child_process"));
  17. var utils = _interopRequireWildcard(require("../../utils"));
  18. var _spawnAsync = require("../../utils/spawnAsync");
  19. var _hostPlatform = require("../../utils/hostPlatform");
  20. var _ = require(".");
  21. var _nativeDeps = require("./nativeDeps");
  22. var _userAgent = require("../../utils/userAgent");
  23. function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  24. function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  25. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  26. /**
  27. * Copyright (c) Microsoft Corporation.
  28. *
  29. * Licensed under the Apache License, Version 2.0 (the "License");
  30. * you may not use this file except in compliance with the License.
  31. * You may obtain a copy of the License at
  32. *
  33. * http://www.apache.org/licenses/LICENSE-2.0
  34. *
  35. * Unless required by applicable law or agreed to in writing, software
  36. * distributed under the License is distributed on an "AS IS" BASIS,
  37. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  38. * See the License for the specific language governing permissions and
  39. * limitations under the License.
  40. */
  41. const BIN_DIRECTORY = _path.default.join(__dirname, '..', '..', '..', 'bin');
  42. const languageBindingVersion = process.env.PW_CLI_DISPLAY_VERSION || require('../../../package.json').version;
  43. const dockerVersionFilePath = '/ms-playwright/.docker-info';
  44. async function writeDockerVersion(dockerImageNameTemplate) {
  45. await _fs.default.promises.mkdir(_path.default.dirname(dockerVersionFilePath), {
  46. recursive: true
  47. });
  48. await _fs.default.promises.writeFile(dockerVersionFilePath, JSON.stringify(dockerVersion(dockerImageNameTemplate), null, 2), 'utf8');
  49. // Make sure version file is globally accessible.
  50. await _fs.default.promises.chmod(dockerVersionFilePath, 0o777);
  51. }
  52. function dockerVersion(dockerImageNameTemplate) {
  53. return {
  54. driverVersion: languageBindingVersion,
  55. dockerImageName: dockerImageNameTemplate.replace('%version%', languageBindingVersion)
  56. };
  57. }
  58. function readDockerVersionSync() {
  59. try {
  60. const data = JSON.parse(_fs.default.readFileSync(dockerVersionFilePath, 'utf8'));
  61. return {
  62. ...data,
  63. dockerImageNameTemplate: data.dockerImageName.replace(data.driverVersion, '%version%')
  64. };
  65. } catch (e) {
  66. return null;
  67. }
  68. }
  69. const checkExecutable = filePath => {
  70. if (process.platform === 'win32') return filePath.endsWith('.exe');
  71. return _fs.default.promises.access(filePath, _fs.default.constants.X_OK).then(() => true).catch(() => false);
  72. };
  73. function isSupportedWindowsVersion() {
  74. if (os.platform() !== 'win32' || os.arch() !== 'x64') return false;
  75. const [major, minor] = os.release().split('.').map(token => parseInt(token, 10));
  76. // This is based on: https://stackoverflow.com/questions/42524606/how-to-get-windows-version-using-node-js/44916050#44916050
  77. // The table with versions is taken from: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexw#remarks
  78. // Windows 7 is not supported and is encoded as `6.1`.
  79. return major > 6 || major === 6 && minor > 1;
  80. }
  81. async function installDependenciesWindows(targets, dryRun) {
  82. if (targets.has('chromium')) {
  83. const command = 'powershell.exe';
  84. const args = ['-ExecutionPolicy', 'Bypass', '-File', _path.default.join(BIN_DIRECTORY, 'install_media_pack.ps1')];
  85. if (dryRun) {
  86. console.log(`${command} ${quoteProcessArgs(args).join(' ')}`); // eslint-disable-line no-console
  87. return;
  88. }
  89. const {
  90. code
  91. } = await (0, _spawnAsync.spawnAsync)(command, args, {
  92. cwd: BIN_DIRECTORY,
  93. stdio: 'inherit'
  94. });
  95. if (code !== 0) throw new Error('Failed to install windows dependencies!');
  96. }
  97. }
  98. async function installDependenciesLinux(targets, dryRun) {
  99. const libraries = [];
  100. const platform = _hostPlatform.hostPlatform;
  101. if (!_hostPlatform.isOfficiallySupportedPlatform) console.warn(`BEWARE: your OS is not officially supported by Playwright; installing dependencies for ${platform} as a fallback.`); // eslint-disable-line no-console
  102. for (const target of targets) {
  103. const info = _nativeDeps.deps[platform];
  104. if (!info) {
  105. console.warn(`Cannot install dependencies for ${platform}!`); // eslint-disable-line no-console
  106. return;
  107. }
  108. libraries.push(...info[target]);
  109. }
  110. const uniqueLibraries = Array.from(new Set(libraries));
  111. if (!dryRun) console.log(`Installing dependencies...`); // eslint-disable-line no-console
  112. const commands = [];
  113. commands.push('apt-get update');
  114. commands.push(['apt-get', 'install', '-y', '--no-install-recommends', ...uniqueLibraries].join(' '));
  115. const {
  116. command,
  117. args,
  118. elevatedPermissions
  119. } = await transformCommandsForRoot(commands);
  120. if (dryRun) {
  121. console.log(`${command} ${quoteProcessArgs(args).join(' ')}`); // eslint-disable-line no-console
  122. return;
  123. }
  124. if (elevatedPermissions) console.log('Switching to root user to install dependencies...'); // eslint-disable-line no-console
  125. const child = _child_process.default.spawn(command, args, {
  126. stdio: 'inherit'
  127. });
  128. await new Promise((resolve, reject) => {
  129. child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Installation process exited with code: ${code}`)));
  130. child.on('error', reject);
  131. });
  132. }
  133. async function validateDependenciesWindows(windowsExeAndDllDirectories) {
  134. const directoryPaths = windowsExeAndDllDirectories;
  135. const lddPaths = [];
  136. for (const directoryPath of directoryPaths) lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
  137. const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependenciesWindows(lddPath)));
  138. const missingDeps = new Set();
  139. for (const deps of allMissingDeps) {
  140. for (const dep of deps) missingDeps.add(dep);
  141. }
  142. if (!missingDeps.size) return;
  143. let isCrtMissing = false;
  144. let isMediaFoundationMissing = false;
  145. for (const dep of missingDeps) {
  146. if (dep.startsWith('api-ms-win-crt') || dep === 'vcruntime140.dll' || dep === 'vcruntime140_1.dll' || dep === 'msvcp140.dll') isCrtMissing = true;else if (dep === 'mf.dll' || dep === 'mfplat.dll' || dep === 'msmpeg2vdec.dll' || dep === 'evr.dll' || dep === 'avrt.dll') isMediaFoundationMissing = true;
  147. }
  148. const details = [];
  149. if (isCrtMissing) {
  150. details.push(`Some of the Universal C Runtime files cannot be found on the system. You can fix`, `that by installing Microsoft Visual C++ Redistributable for Visual Studio from:`, `https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads`, ``);
  151. }
  152. if (isMediaFoundationMissing) {
  153. details.push(`Some of the Media Foundation files cannot be found on the system. If you are`, `on Windows Server try fixing this by running the following command in PowerShell`, `as Administrator:`, ``, ` Install-WindowsFeature Server-Media-Foundation`, ``, `For Windows N editions visit:`, `https://support.microsoft.com/en-us/help/3145500/media-feature-pack-list-for-windows-n-editions`, ``);
  154. }
  155. details.push(`Full list of missing libraries:`, ` ${[...missingDeps].join('\n ')}`, ``);
  156. const message = `Host system is missing dependencies!\n\n${details.join('\n')}`;
  157. if (isSupportedWindowsVersion()) {
  158. throw new Error(message);
  159. } else {
  160. // eslint-disable-next-line no-console
  161. console.warn(`WARNING: running on unsupported windows version!`);
  162. // eslint-disable-next-line no-console
  163. console.warn(message);
  164. }
  165. }
  166. async function validateDependenciesLinux(sdkLanguage, linuxLddDirectories, dlOpenLibraries) {
  167. var _deps$hostPlatform;
  168. const directoryPaths = linuxLddDirectories;
  169. const lddPaths = [];
  170. for (const directoryPath of directoryPaths) lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
  171. const missingDepsPerFile = await Promise.all(lddPaths.map(lddPath => missingFileDependencies(lddPath, directoryPaths)));
  172. const missingDeps = new Set();
  173. for (const deps of missingDepsPerFile) {
  174. for (const dep of deps) missingDeps.add(dep);
  175. }
  176. for (const dep of await missingDLOPENLibraries(dlOpenLibraries)) missingDeps.add(dep);
  177. if (!missingDeps.size) return;
  178. const allMissingDeps = new Set(missingDeps);
  179. // Check Ubuntu version.
  180. const missingPackages = new Set();
  181. const libraryToPackageNameMapping = _nativeDeps.deps[_hostPlatform.hostPlatform] ? {
  182. ...(((_deps$hostPlatform = _nativeDeps.deps[_hostPlatform.hostPlatform]) === null || _deps$hostPlatform === void 0 ? void 0 : _deps$hostPlatform.lib2package) || {}),
  183. ...MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU
  184. } : {};
  185. // Translate missing dependencies to package names to install with apt.
  186. for (const missingDep of missingDeps) {
  187. const packageName = libraryToPackageNameMapping[missingDep];
  188. if (packageName) {
  189. missingPackages.add(packageName);
  190. missingDeps.delete(missingDep);
  191. }
  192. }
  193. const maybeSudo = process.getuid() !== 0 && os.platform() !== 'win32' ? 'sudo ' : '';
  194. const dockerInfo = readDockerVersionSync();
  195. const errorLines = [`Host system is missing dependencies to run browsers.`];
  196. // Ignore patch versions when comparing docker container version and Playwright version:
  197. // we **NEVER** roll browsers in patch releases, so native dependencies do not change.
  198. if (dockerInfo && !dockerInfo.driverVersion.startsWith((0, _userAgent.getPlaywrightVersion)(true /* majorMinorOnly */) + '.')) {
  199. // We are running in a docker container with unmatching version.
  200. // In this case, we know how to install dependencies in it.
  201. const pwVersion = (0, _userAgent.getPlaywrightVersion)();
  202. const requiredDockerImage = dockerInfo.dockerImageName.replace(dockerInfo.driverVersion, pwVersion);
  203. errorLines.push(...[`This is most likely due to docker image version not matching Playwright version:`, `- Playwright: ${pwVersion}`, `- Docker: ${dockerInfo.driverVersion}`, ``, `Either:`, `- (recommended) use docker image "${requiredDockerImage}"`, `- (alternative 1) run the following command inside docker to install missing dependencies:`, ``, ` ${maybeSudo}${(0, _.buildPlaywrightCLICommand)(sdkLanguage, 'install-deps')}`, ``, `- (alternative 2) use apt inside docker:`, ``, ` ${maybeSudo}apt-get install ${[...missingPackages].join('\\\n ')}`, ``, `<3 Playwright Team`]);
  204. } else if (missingPackages.size && !missingDeps.size) {
  205. // Only known dependencies are missing for browsers.
  206. // Suggest installation with a Playwright CLI.
  207. errorLines.push(...[`Please install them with the following command:`, ``, ` ${maybeSudo}${(0, _.buildPlaywrightCLICommand)(sdkLanguage, 'install-deps')}`, ``, `Alternatively, use apt:`, ` ${maybeSudo}apt-get install ${[...missingPackages].join('\\\n ')}`, ``, `<3 Playwright Team`]);
  208. } else {
  209. // Unhappy path: we either run on unknown distribution, or we failed to resolve all missing
  210. // libraries to package names.
  211. // Print missing libraries only:
  212. errorLines.push(...[`Missing libraries:`, ...[...allMissingDeps].map(dep => ' ' + dep)]);
  213. }
  214. throw new Error('\n' + utils.wrapInASCIIBox(errorLines.join('\n'), 1));
  215. }
  216. function isSharedLib(basename) {
  217. switch (os.platform()) {
  218. case 'linux':
  219. return basename.endsWith('.so') || basename.includes('.so.');
  220. case 'win32':
  221. return basename.endsWith('.dll');
  222. default:
  223. return false;
  224. }
  225. }
  226. async function executablesOrSharedLibraries(directoryPath) {
  227. if (!_fs.default.existsSync(directoryPath)) return [];
  228. const allPaths = (await _fs.default.promises.readdir(directoryPath)).map(file => _path.default.resolve(directoryPath, file));
  229. const allStats = await Promise.all(allPaths.map(aPath => _fs.default.promises.stat(aPath)));
  230. const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile());
  231. const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => {
  232. const basename = _path.default.basename(filePath).toLowerCase();
  233. if (isSharedLib(basename)) return filePath;
  234. if (await checkExecutable(filePath)) return filePath;
  235. return false;
  236. }))).filter(Boolean);
  237. return executablersOrLibraries;
  238. }
  239. async function missingFileDependenciesWindows(filePath) {
  240. const executable = _path.default.join(__dirname, '..', '..', '..', 'bin', 'PrintDeps.exe');
  241. const dirname = _path.default.dirname(filePath);
  242. const {
  243. stdout,
  244. code
  245. } = await (0, _spawnAsync.spawnAsync)(executable, [filePath], {
  246. cwd: dirname,
  247. env: {
  248. ...process.env,
  249. LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH ? `${process.env.LD_LIBRARY_PATH}:${dirname}` : dirname
  250. }
  251. });
  252. if (code !== 0) return [];
  253. const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim().toLowerCase());
  254. return missingDeps;
  255. }
  256. async function missingFileDependencies(filePath, extraLDPaths) {
  257. const dirname = _path.default.dirname(filePath);
  258. let LD_LIBRARY_PATH = extraLDPaths.join(':');
  259. if (process.env.LD_LIBRARY_PATH) LD_LIBRARY_PATH = `${process.env.LD_LIBRARY_PATH}:${LD_LIBRARY_PATH}`;
  260. const {
  261. stdout,
  262. code
  263. } = await (0, _spawnAsync.spawnAsync)('ldd', [filePath], {
  264. cwd: dirname,
  265. env: {
  266. ...process.env,
  267. LD_LIBRARY_PATH
  268. }
  269. });
  270. if (code !== 0) return [];
  271. const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim());
  272. return missingDeps;
  273. }
  274. async function missingDLOPENLibraries(libraries) {
  275. if (!libraries.length) return [];
  276. // NOTE: Using full-qualified path to `ldconfig` since `/sbin` is not part of the
  277. // default PATH in CRON.
  278. // @see https://github.com/microsoft/playwright/issues/3397
  279. const {
  280. stdout,
  281. code,
  282. error
  283. } = await (0, _spawnAsync.spawnAsync)('/sbin/ldconfig', ['-p'], {});
  284. if (code !== 0 || error) return [];
  285. const isLibraryAvailable = library => stdout.toLowerCase().includes(library.toLowerCase());
  286. return libraries.filter(library => !isLibraryAvailable(library));
  287. }
  288. const MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU = {
  289. // libgstlibav.so (the only actual library provided by gstreamer1.0-libav) is not
  290. // in the ldconfig cache, so we detect the actual library required for playing h.264
  291. // and if it's missing recommend installing missing gstreamer lib.
  292. // gstreamer1.0-libav -> libavcodec57 -> libx264-152
  293. 'libx264.so': 'gstreamer1.0-libav'
  294. };
  295. function quoteProcessArgs(args) {
  296. return args.map(arg => {
  297. if (arg.includes(' ')) return `"${arg}"`;
  298. return arg;
  299. });
  300. }
  301. async function transformCommandsForRoot(commands) {
  302. const isRoot = process.getuid() === 0;
  303. if (isRoot) return {
  304. command: 'sh',
  305. args: ['-c', `${commands.join('&& ')}`],
  306. elevatedPermissions: false
  307. };
  308. const sudoExists = await (0, _spawnAsync.spawnAsync)('which', ['sudo']);
  309. if (sudoExists.code === 0) return {
  310. command: 'sudo',
  311. args: ['--', 'sh', '-c', `${commands.join('&& ')}`],
  312. elevatedPermissions: true
  313. };
  314. return {
  315. command: 'su',
  316. args: ['root', '-c', `${commands.join('&& ')}`],
  317. elevatedPermissions: true
  318. };
  319. }