Skip to content

Commit ab70f49

Browse files
committed
devops: add script to generate shared object => package mapping
We use this mapping to provide recommendations on which packages to install on Linux distributions. References microsoft#2745
1 parent 7d2078e commit ab70f49

File tree

5 files changed

+163
-0
lines changed

5 files changed

+163
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
RUN_RESULT
2+
playwright.tar.gz
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Mapping distribution libraries to package names
2+
3+
Playwright requires a set of packages on Linux distribution for browsers to work.
4+
Before launching browser on Linux, Playwright uses `ldd` to make sure browsers have all
5+
dependencies met.
6+
7+
If this is not the case, Playwright suggests users packages to install to
8+
meet the dependencies. This tool helps to maintain a map between package names
9+
and shared libraries it provides, per distribution.
10+
11+
## Usage
12+
13+
To generate a map of browser library to package name on Ubuntu:bionic:
14+
15+
```sh
16+
$ ./run.sh ubuntu:bionic
17+
```
18+
19+
Results will be saved to the `RUN_RESULT`.
20+
21+
22+
## How it works
23+
24+
The script does the following:
25+
26+
1. Launches docker with given linux distribution
27+
2. Installs playwright browsers inside the distribution
28+
3. For every dependency that Playwright browsers miss inside the distribution, uses `apt-file` to reverse-search package with the library.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const util = require('util');
5+
const path = require('path');
6+
const {spawn} = require('child_process');
7+
const browserPaths = require('playwright/lib/install/browserPaths.js');
8+
9+
(async () => {
10+
const allBrowsersPath = browserPaths.browsersPath();
11+
const {stdout} = await runCommand('find', [allBrowsersPath, '-executable', '-type', 'f']);
12+
const lddPaths = stdout.split('\n').map(f => f.trim()).filter(filePath => !filePath.toLowerCase().endsWith('.sh'));
13+
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependencies(lddPath)));
14+
const missingDeps = new Set();
15+
for (const deps of allMissingDeps) {
16+
for (const dep of deps)
17+
missingDeps.add(dep);
18+
}
19+
console.log(`==== MISSING DEPENDENCIES: ${missingDeps.size} ====`);
20+
console.log([...missingDeps].sort().join('\n'));
21+
22+
console.log('{');
23+
for (const dep of missingDeps) {
24+
const packages = await findPackages(dep);
25+
if (packages.length === 0)
26+
console.log(` // UNRESOLVED: ${dep} `);
27+
else if (packages.length === 1)
28+
console.log(` "${dep}": "${packages[0]}",`);
29+
else
30+
console.log(` "${dep}": ${JSON.stringify(packages)},`);
31+
}
32+
console.log('}');
33+
})();
34+
35+
async function findPackages(libraryName) {
36+
const {stdout} = await runCommand('apt-file', ['search', libraryName]);
37+
if (!stdout.trim())
38+
return [];
39+
const libs = stdout.trim().split('\n').map(line => line.split(':')[0]);
40+
return [...new Set(libs)];
41+
}
42+
43+
async function fileDependencies(filePath) {
44+
const {stdout} = await lddAsync(filePath);
45+
const deps = stdout.split('\n').map(line => {
46+
line = line.trim();
47+
const missing = line.includes('not found');
48+
const name = line.split('=>')[0].trim();
49+
return {name, missing};
50+
});
51+
return deps;
52+
}
53+
54+
async function missingFileDependencies(filePath) {
55+
const deps = await fileDependencies(filePath);
56+
return deps.filter(dep => dep.missing).map(dep => dep.name);
57+
}
58+
59+
async function lddAsync(filePath) {
60+
return await runCommand('ldd', [filePath], {
61+
cwd: path.dirname(filePath),
62+
env: {
63+
...process.env,
64+
LD_LIBRARY_PATH: path.dirname(filePath),
65+
},
66+
});
67+
}
68+
69+
function runCommand(command, args, options = {}) {
70+
const childProcess = spawn(command, args, options);
71+
72+
return new Promise((resolve) => {
73+
let stdout = '';
74+
let stderr = '';
75+
childProcess.stdout.on('data', data => stdout += data);
76+
childProcess.stderr.on('data', data => stderr += data);
77+
childProcess.on('close', (code) => {
78+
resolve({stdout, stderr, code});
79+
});
80+
});
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
# Install Node.js
4+
5+
apt-get update && apt-get install -y curl && \
6+
curl -sL https://deb.nodesource.com/setup_12.x | bash - && \
7+
apt-get install -y nodejs
8+
9+
# Install apt-file
10+
apt-get update && apt-get install -y apt-file && apt-file update
11+
12+
# Install tip-of-tree playwright
13+
mkdir /root/tmp && cd /root/tmp && npm init -y && npm i /root/hostfolder/playwright.tar.gz
14+
15+
cp /root/hostfolder/inside_docker/list_dependencies.js /root/tmp/list_dependencies.js
16+
17+
node list_dependencies.js | tee /root/hostfolder/RUN_RESULT
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
set -e
3+
set +x
4+
5+
if [[ ($1 == '--help') || ($1 == '-h') ]]; then
6+
echo "usage: $(basename $0) <image-name>"
7+
echo
8+
echo "List mapping between browser dependencies to package names and save results in RUN_RESULT file."
9+
echo "Example:"
10+
echo ""
11+
echo " $(basename $0) ubuntu:bionic"
12+
echo ""
13+
echo "NOTE: this requires Playwright dependencies to be installed with 'npm install'"
14+
echo " and Playwright itself being built with 'npm run build'"
15+
echo ""
16+
exit 0
17+
fi
18+
19+
if [[ $# == 0 ]]; then
20+
echo "ERROR: please provide base image name, e.g. 'ubuntu:bionic'"
21+
exit 1
22+
fi
23+
24+
function cleanup() {
25+
rm -f "playwright.tar.gz"
26+
}
27+
28+
trap "cleanup; cd $(pwd -P)" EXIT
29+
cd "$(dirname "$0")"
30+
31+
# We rely on `./playwright.tar.gz` to download browsers into the docker image.
32+
node ../../packages/build_package.js playwright ./playwright.tar.gz
33+
34+
docker run -v $PWD:/root/hostfolder --rm -it "$1" /root/hostfolder/inside_docker/process.sh
35+

0 commit comments

Comments
 (0)