diff --git a/.npmignore b/.npmignore index 59a4e49914bfd..592319b74c822 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,8 @@ # include sources from lib except for injected, but not map files !lib/**/*.js +# Include Windows dependency checker executable. +!bin/PrintDeps.exe # Injected files are included via lib/generated, see src/injected/README.md lib/injected/ #types diff --git a/bin/PrintDeps.exe b/bin/PrintDeps.exe new file mode 100644 index 0000000000000..30779974038f6 Binary files /dev/null and b/bin/PrintDeps.exe differ diff --git a/packages/build_package.js b/packages/build_package.js index 1349f0c4d5be2..715456a79a058 100755 --- a/packages/build_package.js +++ b/packages/build_package.js @@ -15,6 +15,7 @@ * limitations under the License. */ const fs = require('fs'); +const os = require('os'); const path = require('path'); const rmSync = require('rimraf').sync; const ncp = require('ncp'); @@ -27,7 +28,7 @@ const cpAsync = util.promisify(ncp); const SCRIPT_NAME = path.basename(__filename); const ROOT_PATH = path.join(__dirname, '..'); -const PLAYWRIGHT_CORE_FILES = ['lib', 'types', 'NOTICE', 'LICENSE', '.npmignore']; +const PLAYWRIGHT_CORE_FILES = ['bin', 'lib', 'types', 'NOTICE', 'LICENSE', '.npmignore']; const PACKAGES = { 'playwright': { @@ -157,7 +158,8 @@ if (!args.some(arg => arg === '--no-cleanup')) { await writeToPackage('browsers.json', JSON.stringify(browsersJSON, null, 2)); // 6. Run npm pack - const {stdout, stderr, status} = spawnSync('npm', ['pack'], {cwd: packagePath, encoding: 'utf8'}); + const shell = os.platform() === 'win32'; + const {stdout, stderr, status} = spawnSync('npm', ['pack'], {cwd: packagePath, encoding: 'utf8', shell}); if (status !== 0) { console.log(`ERROR: "npm pack" failed`); console.log(stderr); diff --git a/src/install/browserPaths.ts b/src/install/browserPaths.ts index 03e6cb4531c11..8c3df5040ae08 100644 --- a/src/install/browserPaths.ts +++ b/src/install/browserPaths.ts @@ -61,6 +61,16 @@ export function linuxLddDirectories(browserPath: string, browser: BrowserDescrip return []; } +export function windowsExeAndDllDirectories(browserPath: string, browser: BrowserDescriptor): string[] { + if (browser.name === 'chromium') + return [path.join(browserPath, 'chrome-win')]; + if (browser.name === 'firefox') + return [path.join(browserPath, 'firefox')]; + if (browser.name === 'webkit') + return [browserPath]; + return []; +} + export function executablePath(browserPath: string, browser: BrowserDescriptor): string | undefined { let tokens: string[] | undefined; if (browser.name === 'chromium') { diff --git a/src/server/validateDependencies.ts b/src/server/validateDependencies.ts index 0698116eb0076..90f3bec6f7aa7 100644 --- a/src/server/validateDependencies.ts +++ b/src/server/validateDependencies.ts @@ -19,7 +19,7 @@ import * as path from 'path'; import * as os from 'os'; import { spawn } from 'child_process'; import { getUbuntuVersion } from '../helper'; -import { linuxLddDirectories, BrowserDescriptor } from '../install/browserPaths.js'; +import { linuxLddDirectories, windowsExeAndDllDirectories, BrowserDescriptor } from '../install/browserPaths.js'; const accessAsync = util.promisify(fs.access.bind(fs)); const checkExecutable = (filePath: string) => accessAsync(filePath, fs.constants.X_OK).then(() => true).catch(e => false); @@ -41,8 +41,65 @@ const DL_OPEN_LIBRARIES = { async function validateDependencies(browserPath: string, browser: BrowserDescriptor) { // We currently only support Linux. - if (os.platform() !== 'linux') + if (os.platform() === 'linux') + return await validateDependenciesLinux(browserPath, browser); + if (os.platform() === 'win32' && os.arch() === 'x64') + return await validateDependenciesWindows(browserPath, browser); +} + +async function validateDependenciesWindows(browserPath: string, browser: BrowserDescriptor) { + const directoryPaths = windowsExeAndDllDirectories(browserPath, browser); + const lddPaths: string[] = []; + for (const directoryPath of directoryPaths) + lddPaths.push(...(await executablesOrSharedLibraries(directoryPath))); + const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependenciesWindows(lddPath))); + const missingDeps: Set = new Set(); + for (const deps of allMissingDeps) { + for (const dep of deps) + missingDeps.add(dep); + } + + if (!missingDeps.size) return; + + let isCrtMissing = false; + let isMediaFoundationMissing = false; + for (const dep of missingDeps) { + if (dep.startsWith('api-ms-win-crt')) + isCrtMissing = true; + else if (dep === 'mf.dll' || dep === 'mfplat.dll' || dep === 'msmpeg2vdec.dll') + isMediaFoundationMissing = true; + } + + const details = []; + + if (isCrtMissing) { + 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`, + ``); + } + + if (isMediaFoundationMissing) { + 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`, + ``); + } + + details.push( + `Full list of missing libraries:`, + ` ${[...missingDeps].join('\n ')}`, + ``); + + throw new Error(`Host system is missing dependencies!\n\n${details.join('\n')}`); +} + +async function validateDependenciesLinux(browserPath: string, browser: BrowserDescriptor) { const directoryPaths = linuxLddDirectories(browserPath, browser); const lddPaths: string[] = []; for (const directoryPath of directoryPaths) @@ -100,6 +157,17 @@ async function validateDependencies(browserPath: string, browser: BrowserDescrip throw new Error('Host system is missing dependencies!\n\n' + missingPackagesMessage + missingDependenciesMessage); } +function isSharedLib(basename: string) { + switch (os.platform()) { + case 'linux': + return basename.endsWith('.so') || basename.includes('.so.'); + case 'win32': + return basename.endsWith('.dll'); + default: + return false; + } +} + async function executablesOrSharedLibraries(directoryPath: string): Promise { const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file)); const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath))); @@ -107,7 +175,7 @@ async function executablesOrSharedLibraries(directoryPath: string): Promise { const basename = path.basename(filePath).toLowerCase(); - if (basename.endsWith('.so') || basename.includes('.so.')) + if (isSharedLib(basename)) return filePath; if (await checkExecutable(filePath)) return filePath; @@ -117,6 +185,21 @@ async function executablesOrSharedLibraries(directoryPath: string): Promise> { + const dirname = path.dirname(filePath); + const {stdout, code} = await spawnAsync(path.join(__dirname, '../../bin/PrintDeps.exe'), [filePath], { + cwd: dirname, + env: { + ...process.env, + LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH ? `${process.env.LD_LIBRARY_PATH}:${dirname}` : dirname, + }, + }); + if (code !== 0) + return []; + const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim()); + return missingDeps; +} + async function missingFileDependencies(filePath: string): Promise> { const dirname = path.dirname(filePath); const {stdout, code} = await spawnAsync('ldd', [filePath], {