2020-07-15 15:24:38 -07:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2021-07-01 16:17:59 -07:00
2021-02-11 06:36:15 -08:00
import fs from 'fs' ;
import path from 'path' ;
2020-07-15 15:24:38 -07:00
import * as os from 'os' ;
2021-07-01 17:14:04 -07:00
import childProcess from 'child_process' ;
2022-04-07 12:55:44 -08:00
import * as utils from '../../utils' ;
2022-04-07 19:18:22 -08:00
import { spawnAsync } from '../../utils/spawnAsync' ;
import { hostPlatform } from '../../utils/hostPlatform' ;
2022-04-06 21:21:27 -08:00
import { buildPlaywrightCLICommand } from '.' ;
2021-11-09 10:55:13 -08:00
import { deps } from './nativeDeps' ;
2022-04-07 13:36:13 -08:00
import { getPlaywrightVersion } from '../../common/userAgent' ;
2020-07-15 15:24:38 -07:00
2022-04-06 21:21:27 -08:00
const BIN_DIRECTORY = path . join ( __dirname , '..' , '..' , '..' , 'bin' ) ;
const packageJSON = require ( '../../../package.json' ) ;
2022-03-25 15:45:53 -06:00
const dockerVersionFilePath = '/ms-playwright/.docker-info' ;
export async function writeDockerVersion ( dockerImageNameTemplate : string ) {
await fs . promises . mkdir ( path . dirname ( dockerVersionFilePath ) , { recursive : true } ) ;
await fs . promises . writeFile ( dockerVersionFilePath , JSON . stringify ( {
driverVersion : packageJSON.version ,
dockerImageName : dockerImageNameTemplate.replace ( '%version%' , packageJSON . version ) ,
} , null , 2 ) , 'utf8' ) ;
// Make sure version file is globally accessible.
await fs . promises . chmod ( dockerVersionFilePath , 0 o777 ) ;
}
async function readDockerVersion ( ) : Promise < null | { driverVersion : string , dockerImageName : string } > {
return await fs . promises . readFile ( dockerVersionFilePath , 'utf8' )
. then ( text = > JSON . parse ( text ) )
. catch ( e = > null ) ;
}
2021-10-29 20:12:46 +02:00
2021-06-03 09:55:33 -07:00
const checkExecutable = ( filePath : string ) = > fs . promises . access ( filePath , fs . constants . X_OK ) . then ( ( ) = > true ) . catch ( e = > false ) ;
2020-07-15 15:24:38 -07:00
2020-12-14 16:40:51 -08:00
function isSupportedWindowsVersion ( ) : boolean {
if ( os . platform ( ) !== 'win32' || os . arch ( ) !== 'x64' )
return false ;
const [ major , minor ] = os . release ( ) . split ( '.' ) . map ( token = > parseInt ( token , 10 ) ) ;
// This is based on: https://stackoverflow.com/questions/42524606/how-to-get-windows-version-using-node-js/44916050#44916050
// The table with versions is taken from: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexw#remarks
// Windows 7 is not supported and is encoded as `6.1`.
return major > 6 || ( major === 6 && minor > 1 ) ;
2020-07-30 17:15:46 -07:00
}
2021-07-13 19:03:49 -07:00
export type DependencyGroup = 'chromium' | 'firefox' | 'webkit' | 'tools' ;
2021-11-25 01:04:42 +01:00
export async function installDependenciesWindows ( targets : Set < DependencyGroup > , dryRun : boolean ) : Promise < void > {
2021-10-29 20:12:46 +02:00
if ( targets . has ( 'chromium' ) ) {
2021-11-25 01:04:42 +01:00
const command = 'powershell.exe' ;
const args = [ '-ExecutionPolicy' , 'Bypass' , '-File' , path . join ( BIN_DIRECTORY , 'install_media_pack.ps1' ) ] ;
if ( dryRun ) {
console . log ( ` ${ command } ${ quoteProcessArgs ( args ) . join ( ' ' ) } ` ) ; // eslint-disable-line no-console
return ;
}
2022-04-07 19:18:22 -08:00
const { code } = await spawnAsync ( command , args , { cwd : BIN_DIRECTORY , stdio : 'inherit' } ) ;
2021-10-29 20:12:46 +02:00
if ( code !== 0 )
throw new Error ( 'Failed to install windows dependencies!' ) ;
2021-07-01 17:14:04 -07:00
}
}
2021-11-25 01:04:42 +01:00
export async function installDependenciesLinux ( targets : Set < DependencyGroup > , dryRun : boolean ) {
2021-07-01 17:14:04 -07:00
const libraries : string [ ] = [ ] ;
2022-05-10 05:05:39 -06:00
let platform = hostPlatform ;
if ( platform === 'generic-linux' || platform === 'generic-linux-arm64' ) {
console . warn ( 'BEWARE: your OS is not officially supported by Playwright; installing dependencies for Ubuntu as a fallback.' ) ; // eslint-disable-line no-console
platform = hostPlatform === 'generic-linux' ? 'ubuntu20.04' : 'ubuntu20.04-arm64' ;
}
2021-07-01 17:14:04 -07:00
for ( const target of targets ) {
2022-05-10 05:05:39 -06:00
const info = deps [ platform ] ;
2021-11-09 10:55:13 -08:00
if ( ! info ) {
console . warn ( 'Cannot install dependencies for this linux distribution!' ) ; // eslint-disable-line no-console
return ;
}
libraries . push ( . . . info [ target ] ) ;
2021-07-01 17:14:04 -07:00
}
const uniqueLibraries = Array . from ( new Set ( libraries ) ) ;
2021-11-25 01:04:42 +01:00
if ( ! dryRun )
2022-05-10 05:05:39 -06:00
console . log ( ` Installing dependencies... ` ) ; // eslint-disable-line no-console
2021-07-01 17:14:04 -07:00
const commands : string [ ] = [ ] ;
commands . push ( 'apt-get update' ) ;
commands . push ( [ 'apt-get' , 'install' , '-y' , '--no-install-recommends' ,
. . . uniqueLibraries ,
] . join ( ' ' ) ) ;
2022-04-07 19:18:22 -08:00
const { command , args , elevatedPermissions } = await transformCommandsForRoot ( commands ) ;
2021-11-25 01:04:42 +01:00
if ( dryRun ) {
console . log ( ` ${ command } ${ quoteProcessArgs ( args ) . join ( ' ' ) } ` ) ; // eslint-disable-line no-console
return ;
}
2021-12-06 14:49:22 -08:00
if ( elevatedPermissions )
console . log ( 'Switching to root user to install dependencies...' ) ; // eslint-disable-line no-console
2021-09-14 14:09:37 +02:00
const child = childProcess . spawn ( command , args , { stdio : 'inherit' } ) ;
2022-02-26 00:45:27 +01:00
await new Promise < void > ( ( resolve , reject ) = > {
child . on ( 'exit' , ( code : number ) = > code === 0 ? resolve ( ) : reject ( new Error ( ` Installation process exited with code: ${ code } ` ) ) ) ;
2021-09-14 14:09:37 +02:00
child . on ( 'error' , reject ) ;
} ) ;
}
2021-07-01 16:17:59 -07:00
export async function validateDependenciesWindows ( windowsExeAndDllDirectories : string [ ] ) {
const directoryPaths = windowsExeAndDllDirectories ;
2020-07-30 17:15:46 -07:00
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 < string > = new Set ( ) ;
for ( const deps of allMissingDeps ) {
for ( const dep of deps )
missingDeps . add ( dep ) ;
}
if ( ! missingDeps . size )
2020-07-15 15:24:38 -07:00
return ;
2020-07-30 17:15:46 -07:00
let isCrtMissing = false ;
let isMediaFoundationMissing = false ;
for ( const dep of missingDeps ) {
2021-01-11 15:01:29 -08:00
if ( dep . startsWith ( 'api-ms-win-crt' ) || dep === 'vcruntime140.dll' || dep === 'vcruntime140_1.dll' || dep === 'msvcp140.dll' )
2020-07-30 17:15:46 -07:00
isCrtMissing = true ;
2020-07-31 14:11:11 -07:00
else if ( dep === 'mf.dll' || dep === 'mfplat.dll' || dep === 'msmpeg2vdec.dll' || dep === 'evr.dll' || dep === 'avrt.dll' )
2020-07-30 17:15:46 -07:00
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 ` ,
2020-09-11 01:36:08 +02:00
` ` ,
` For Windows N editions visit: ` ,
` https://support.microsoft.com/en-us/help/3145500/media-feature-pack-list-for-windows-n-editions ` ,
2020-07-30 17:15:46 -07:00
` ` ) ;
}
details . push (
` Full list of missing libraries: ` ,
` ${ [ . . . missingDeps ] . join ( '\n ' ) } ` ,
` ` ) ;
2020-12-14 16:40:51 -08:00
const message = ` Host system is missing dependencies! \ n \ n ${ details . join ( '\n' ) } ` ;
if ( isSupportedWindowsVersion ( ) ) {
throw new Error ( message ) ;
} else {
2022-01-12 19:52:40 -08:00
// eslint-disable-next-line no-console
2020-12-14 16:40:51 -08:00
console . warn ( ` WARNING: running on unsupported windows version! ` ) ;
2022-01-12 19:52:40 -08:00
// eslint-disable-next-line no-console
2020-12-14 16:40:51 -08:00
console . warn ( message ) ;
}
2020-07-30 17:15:46 -07:00
}
2021-10-27 18:58:13 +02:00
export async function validateDependenciesLinux ( sdkLanguage : string , linuxLddDirectories : string [ ] , dlOpenLibraries : string [ ] ) {
2021-07-01 16:17:59 -07:00
const directoryPaths = linuxLddDirectories ;
2020-07-15 15:24:38 -07:00
const lddPaths : string [ ] = [ ] ;
for ( const directoryPath of directoryPaths )
lddPaths . push ( . . . ( await executablesOrSharedLibraries ( directoryPath ) ) ) ;
2022-03-23 15:06:14 -06:00
const missingDepsPerFile = await Promise . all ( lddPaths . map ( lddPath = > missingFileDependencies ( lddPath , directoryPaths ) ) ) ;
2020-07-17 16:50:20 -07:00
const missingDeps : Set < string > = new Set ( ) ;
2022-03-23 15:06:14 -06:00
for ( const deps of missingDepsPerFile ) {
2020-07-15 15:24:38 -07:00
for ( const dep of deps )
missingDeps . add ( dep ) ;
}
2021-07-01 16:17:59 -07:00
for ( const dep of ( await missingDLOPENLibraries ( dlOpenLibraries ) ) )
2020-07-29 09:58:45 -07:00
missingDeps . add ( dep ) ;
2020-07-15 15:24:38 -07:00
if ( ! missingDeps . size )
return ;
2022-03-23 15:06:14 -06:00
const allMissingDeps = new Set ( missingDeps ) ;
2020-07-17 16:50:20 -07:00
// Check Ubuntu version.
const missingPackages = new Set ( ) ;
2022-05-10 05:05:39 -06:00
const libraryToPackageNameMapping = deps [ hostPlatform ] ? {
2022-04-07 19:18:22 -08:00
. . . ( deps [ hostPlatform ] ? . lib2package || { } ) ,
2021-11-09 10:55:13 -08:00
. . . MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU ,
2022-05-10 05:05:39 -06:00
} : { } ;
2021-11-09 10:55:13 -08:00
// Translate missing dependencies to package names to install with apt.
for ( const missingDep of missingDeps ) {
const packageName = libraryToPackageNameMapping [ missingDep ] ;
if ( packageName ) {
missingPackages . add ( packageName ) ;
missingDeps . delete ( missingDep ) ;
2020-07-17 16:50:20 -07:00
}
}
2021-07-19 13:54:42 +03:00
const maybeSudo = ( process . getuid ( ) !== 0 ) && os . platform ( ) !== 'win32' ? 'sudo ' : '' ;
2022-03-25 15:45:53 -06:00
const dockerInfo = await readDockerVersion ( ) ;
2022-03-23 15:06:14 -06:00
const errorLines = [
` Host system is missing dependencies to run browsers. ` ,
] ;
2022-03-25 15:45:53 -06:00
// Ignore patch versions when comparing docker container version and Playwright version:
// we **NEVER** roll browsers in patch releases, so native dependencies do not change.
2022-04-07 13:36:13 -08:00
if ( dockerInfo && ! dockerInfo . driverVersion . startsWith ( getPlaywrightVersion ( true /* majorMinorOnly */ ) + '.' ) ) {
2022-03-25 15:45:53 -06:00
// We are running in a docker container with unmatching version.
// In this case, we know how to install dependencies in it.
2022-04-07 13:36:13 -08:00
const pwVersion = getPlaywrightVersion ( ) ;
2022-03-25 15:45:53 -06:00
const requiredDockerImage = dockerInfo . dockerImageName . replace ( dockerInfo . driverVersion , pwVersion ) ;
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 } ${ buildPlaywrightCLICommand ( sdkLanguage , 'install-deps' ) } ` ,
` ` ,
2022-06-09 13:20:18 +02:00
` - (alternative 2) use apt inside docker: ` ,
2022-03-25 15:45:53 -06:00
` ` ,
` ${ maybeSudo } apt-get install ${ [ . . . missingPackages ] . join ( '\\\n ' ) } ` ,
` ` ,
` <3 Playwright Team ` ,
] ) ;
} else if ( missingPackages . size && ! missingDeps . size ) {
// Only known dependencies are missing for browsers.
2022-03-23 15:06:14 -06:00
// Suggest installation with a Playwright CLI.
errorLines . push ( . . . [
2021-07-19 13:54:42 +03:00
` Please install them with the following command: ` ,
` ` ,
2021-10-27 18:58:13 +02:00
` ${ maybeSudo } ${ buildPlaywrightCLICommand ( sdkLanguage , 'install-deps' ) } ` ,
2021-07-19 13:54:42 +03:00
` ` ,
2022-06-09 13:20:18 +02:00
` Alternatively, use apt: ` ,
2022-03-23 15:06:14 -06:00
` ${ maybeSudo } apt-get install ${ [ . . . missingPackages ] . join ( '\\\n ' ) } ` ,
2020-07-17 16:50:20 -07:00
` ` ,
2022-03-23 15:06:14 -06:00
` <3 Playwright Team ` ,
] ) ;
} else {
// Unhappy path: we either run on unknown distribution, or we failed to resolve all missing
// libraries to package names.
// Print missing libraries only:
errorLines . push ( . . . [
` Missing libraries: ` ,
. . . [ . . . allMissingDeps ] . map ( dep = > ' ' + dep ) ,
] ) ;
2020-07-17 16:50:20 -07:00
}
2022-03-23 15:06:14 -06:00
throw new Error ( '\n' + utils . wrapInASCIIBox ( errorLines . join ( '\n' ) , 1 ) ) ;
2020-07-15 15:24:38 -07:00
}
2020-07-30 17:15:46 -07:00
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 ;
}
}
2020-07-15 15:24:38 -07:00
async function executablesOrSharedLibraries ( directoryPath : string ) : Promise < string [ ] > {
2022-04-08 11:50:53 -07:00
if ( ! fs . existsSync ( directoryPath ) )
return [ ] ;
2021-06-03 09:55:33 -07:00
const allPaths = ( await fs . promises . readdir ( directoryPath ) ) . map ( file = > path . resolve ( directoryPath , file ) ) ;
const allStats = await Promise . all ( allPaths . map ( aPath = > fs . promises . stat ( aPath ) ) ) ;
2021-02-09 09:00:00 -08:00
const filePaths = allPaths . filter ( ( aPath , index ) = > ( allStats [ index ] as any ) . isFile ( ) ) ;
2020-07-15 15:24:38 -07:00
const executablersOrLibraries = ( await Promise . all ( filePaths . map ( async filePath = > {
const basename = path . basename ( filePath ) . toLowerCase ( ) ;
2020-07-30 17:15:46 -07:00
if ( isSharedLib ( basename ) )
2020-07-15 15:24:38 -07:00
return filePath ;
if ( await checkExecutable ( filePath ) )
return filePath ;
return false ;
} ) ) ) . filter ( Boolean ) ;
return executablersOrLibraries as string [ ] ;
}
2020-07-30 17:15:46 -07:00
async function missingFileDependenciesWindows ( filePath : string ) : Promise < Array < string > > {
2022-04-06 21:21:27 -08:00
const executable = path . join ( __dirname , '..' , '..' , '..' , 'bin' , 'PrintDeps.exe' ) ;
2020-07-30 17:15:46 -07:00
const dirname = path . dirname ( filePath ) ;
2022-04-07 19:18:22 -08:00
const { stdout , code } = await spawnAsync ( executable , [ filePath ] , {
2020-07-30 17:15:46 -07:00
cwd : dirname ,
env : {
. . . process . env ,
LD_LIBRARY_PATH : process.env.LD_LIBRARY_PATH ? ` ${ process . env . LD_LIBRARY_PATH } : ${ dirname } ` : dirname ,
} ,
} ) ;
if ( code !== 0 )
return [ ] ;
2020-09-11 01:36:08 +02:00
const missingDeps = stdout . split ( '\n' ) . map ( line = > line . trim ( ) ) . filter ( line = > line . endsWith ( 'not found' ) && line . includes ( '=>' ) ) . map ( line = > line . split ( '=>' ) [ 0 ] . trim ( ) . toLowerCase ( ) ) ;
2020-07-30 17:15:46 -07:00
return missingDeps ;
}
2020-11-09 16:47:15 -08:00
async function missingFileDependencies ( filePath : string , extraLDPaths : string [ ] ) : Promise < Array < string > > {
2020-07-15 15:24:38 -07:00
const dirname = path . dirname ( filePath ) ;
2020-11-09 16:47:15 -08:00
let LD_LIBRARY_PATH = extraLDPaths . join ( ':' ) ;
if ( process . env . LD_LIBRARY_PATH )
LD_LIBRARY_PATH = ` ${ process . env . LD_LIBRARY_PATH } : ${ LD_LIBRARY_PATH } ` ;
2022-04-07 19:18:22 -08:00
const { stdout , code } = await spawnAsync ( 'ldd' , [ filePath ] , {
2020-07-15 15:24:38 -07:00
cwd : dirname ,
env : {
. . . process . env ,
2020-11-09 16:47:15 -08:00
LD_LIBRARY_PATH ,
2020-07-15 15:24:38 -07:00
} ,
} ) ;
2020-07-29 09:58:45 -07:00
if ( code !== 0 )
return [ ] ;
2020-11-09 16:47:15 -08:00
const missingDeps = stdout . split ( '\n' ) . map ( line = > line . trim ( ) ) . filter ( line = > line . endsWith ( 'not found' ) && line . includes ( '=>' ) ) . map ( line = > line . split ( '=>' ) [ 0 ] . trim ( ) ) ;
2020-07-29 09:58:45 -07:00
return missingDeps ;
}
2021-07-01 16:17:59 -07:00
async function missingDLOPENLibraries ( libraries : string [ ] ) : Promise < string [ ] > {
2020-07-29 09:58:45 -07:00
if ( ! libraries . length )
return [ ] ;
2020-08-13 11:39:50 -07:00
// NOTE: Using full-qualified path to `ldconfig` since `/sbin` is not part of the
// default PATH in CRON.
// @see https://github.com/microsoft/playwright/issues/3397
2022-04-07 19:18:22 -08:00
const { stdout , code , error } = await spawnAsync ( '/sbin/ldconfig' , [ '-p' ] , { } ) ;
2020-08-12 08:47:41 -07:00
if ( code !== 0 || error )
2020-07-29 09:58:45 -07:00
return [ ] ;
const isLibraryAvailable = ( library : string ) = > stdout . toLowerCase ( ) . includes ( library . toLowerCase ( ) ) ;
return libraries . filter ( library = > ! isLibraryAvailable ( library ) ) ;
}
2020-09-01 16:29:38 -07:00
const MANUAL_LIBRARY_TO_PACKAGE_NAME_UBUNTU : { [ s : string ] : string } = {
// libgstlibav.so (the only actual library provided by gstreamer1.0-libav) is not
// in the ldconfig cache, so we detect the actual library required for playing h.264
// and if it's missing recommend installing missing gstreamer lib.
// gstreamer1.0-libav -> libavcodec57 -> libx264-152
2020-09-02 08:47:43 -07:00
'libx264.so' : 'gstreamer1.0-libav' ,
2020-09-01 16:29:38 -07:00
} ;
2021-11-25 01:04:42 +01:00
function quoteProcessArgs ( args : string [ ] ) : string [ ] {
return args . map ( arg = > {
if ( arg . includes ( ' ' ) )
return ` " ${ arg } " ` ;
return arg ;
} ) ;
}
2022-04-07 19:18:22 -08:00
export async function transformCommandsForRoot ( commands : string [ ] ) : Promise < { command : string , args : string [ ] , elevatedPermissions : boolean } > {
const isRoot = process . getuid ( ) === 0 ;
if ( isRoot )
return { command : 'sh' , args : [ '-c' , ` ${ commands . join ( '&& ' ) } ` ] , elevatedPermissions : false } ;
const sudoExists = await spawnAsync ( 'which' , [ 'sudo' ] ) ;
if ( sudoExists . code === 0 )
return { command : 'sudo' , args : [ '--' , 'sh' , '-c' , ` ${ commands . join ( '&& ' ) } ` ] , elevatedPermissions : true } ;
return { command : 'su' , args : [ 'root' , '-c' , ` ${ commands . join ( '&& ' ) } ` ] , elevatedPermissions : true } ;
}