mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(browser): support Termux Android scenarios (#1655)
* fix(browser): support Termux Android scenarios * fix(build): lock Termux artifact generation * ci(android): add manual Termux browser smoke verification --------- Co-authored-by: tyloryang <[邮箱]> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
11 files changed
+384
-3
No files matched your search
@@ -0,0 +1,57 @@
|
||||
name: Real Termux browser verification
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: real-termux-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
jobs:
|
||||
real-termux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download the pinned Termux app and verify its release checksum
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/rea-termux"
|
||||
cd "$RUNNER_TEMP/rea-termux"
|
||||
release=https://github.com/termux/termux-app/releases/download/v0.118.3
|
||||
apk=termux-app_v0.118.3+github-debug_x86_64.apk
|
||||
sums=termux-app_v0.118.3+github-debug_sha256sums
|
||||
curl --fail --location --retry 3 --output "$apk" "$release/$apk"
|
||||
curl --fail --location --retry 3 --output "$sums" "$release/$sums"
|
||||
awk -v apk="$apk" '$2 == apk || $2 == "*" apk' "$sums" > selected.sha256
|
||||
test "$(wc -l < selected.sha256)" -eq 1
|
||||
sha256sum --check selected.sha256
|
||||
- name: Enable emulator acceleration
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Build and capture a page inside Termux
|
||||
uses: reactivecircus/android-emulator-runner@4c44018e59b437e86cdfc41da381398f93ed8808 # v2
|
||||
with:
|
||||
api-level: 30
|
||||
arch: x86_64
|
||||
target: google_apis
|
||||
ram-size: 4096M
|
||||
heap-size: 512M
|
||||
disk-size: 8192M
|
||||
disable-animations: true
|
||||
script: bash scripts/verify/termux/emulator.sh
|
||||
- name: Upload Termux and Android diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: termux-browser-diagnostics
|
||||
path: ${{ runner.temp }}/rea-termux/diagnostics
|
||||
if-no-files-found: warn
|
||||
@@ -801,6 +801,35 @@ separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
|
||||
browser and managed-tool claims require their corresponding verification lanes.
|
||||
Runner labels follow the [GitHub hosted-runner reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
|
||||
|
||||
### Termux Android browser smoke test
|
||||
|
||||
The **Real Termux browser verification** workflow is manual-only while its
|
||||
emulator setup is being validated. After the workflow reaches the default
|
||||
branch, select a revision under **Actions → Real Termux browser verification →
|
||||
Run workflow**. It boots an Android 11 (API 30) x86_64 emulator, installs the
|
||||
checksum-verified Termux 0.118.3 APK, and lets the app initialize its bootstrap.
|
||||
The host stages the selected revision using `git archive HEAD`; no workstation
|
||||
build outputs or dependencies are copied into Android.
|
||||
|
||||
`scripts/verify/termux/emulator.sh` sends a `RUN_COMMAND` intent to Termux's app
|
||||
service. Root access is limited to staging files, sending the intent, and reading
|
||||
diagnostics; installation, compilation, Node, and Chromium run as Termux's app
|
||||
UID. The in-app script installs Termux's Android-linked Node distribution,
|
||||
checks its version against `.nvmrc`, pins npm from `packageManager`, runs
|
||||
`npm ci`, and builds with `build:termux`. Termux repository packages, including
|
||||
Chromium, are resolved at run time; their installed versions are logged.
|
||||
|
||||
The verifier leaves Playwright's cache environment overrides unset, requires
|
||||
a healthy public MCP doctor result, and captures URL plus DOM from a loopback
|
||||
HTTP fixture through `capture-browser-scenario`. It checks completed steps,
|
||||
the exact URL, a DOM marker, and reported browser cleanup. This lane covers one
|
||||
Android emulator and Termux combination, not physical ARM devices, Electron,
|
||||
or the complete desktop browser suite. A workflow definition alone does not
|
||||
establish passing Android coverage; inspect its execution receipt.
|
||||
|
||||
The `termux-browser-diagnostics` artifact retains the Termux command log, exit
|
||||
status, and Android logcat on success or failure. The emulator is disposable.
|
||||
|
||||
## Developer commands
|
||||
|
||||
Use source feedback while editing, explicit boundary checks for the changed
|
||||
|
||||
Generated
+16
@@ -64,6 +64,7 @@
|
||||
"oxlint": "1.87.0",
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"typescript-termux": "npm:typescript@5.9.3",
|
||||
"vitepress": "1.6.4",
|
||||
"vitest": "5.0.3"
|
||||
},
|
||||
@@ -7367,6 +7368,21 @@
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-termux": {
|
||||
"name": "typescript",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unbash": {
|
||||
"version": "4.0.12",
|
||||
"resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.12.tgz",
|
||||
|
||||
@@ -104,6 +104,8 @@
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check jscpd scan:todos && npm run verify:architecture-guards",
|
||||
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
|
||||
"build:termux": "node scripts/run-exclusive.mjs artifacts npm run build:termux:run",
|
||||
"build:termux:run": "npm run compile:termux && npm run artifacts:skills",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"lint": "oxlint && npm run verify:module-boundaries",
|
||||
@@ -239,6 +241,7 @@
|
||||
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
|
||||
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
|
||||
"compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json",
|
||||
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
|
||||
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
|
||||
@@ -312,6 +315,7 @@
|
||||
"oxlint": "1.87.0",
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"typescript-termux": "npm:typescript@5.9.3",
|
||||
"vitepress": "1.6.4",
|
||||
"vitest": "5.0.3"
|
||||
}
|
||||
|
||||
+16
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { access } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Route production MCP before importing Incur. Incur owns registration helpers
|
||||
@@ -15,6 +15,21 @@ const isMcpMode =
|
||||
args.length === 1 && (args[0] === "--mcp" || args[0] === "mcp");
|
||||
const isMcpDoctorMode = args[0] === "mcp" && args[1] === "doctor";
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
// Playwright's registry assumes one of its three desktop host platforms when
|
||||
// it chooses the browser cache directory. Termux reports `android`, although
|
||||
// its Node runtime and filesystem are POSIX-compatible. Keep the cache path
|
||||
// explicit so REA can start on Termux. Browser launch still requires a
|
||||
// caller-supplied Android-compatible Chromium executable.
|
||||
if (
|
||||
process.platform === "android" &&
|
||||
process.env.PLAYWRIGHT_BROWSERS_PATH === undefined
|
||||
) {
|
||||
const cacheRoot =
|
||||
process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache");
|
||||
process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
||||
}
|
||||
|
||||
const runtimeFiles = isMcpMode
|
||||
? ["dist/main.js"]
|
||||
: isMcpDoctorMode
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createServer } from "node:http";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
assert.equal(process.platform, "android", "Run this verifier inside Termux");
|
||||
assert.equal(process.env.PLAYWRIGHT_BROWSERS_PATH, undefined);
|
||||
// Import no browser provider here: the public dispatcher must establish its
|
||||
// Android cache fallback before Playwright initializes in each subprocess.
|
||||
const { stdout: doctorOutput } = await execute(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", "mcp", "doctor", "--format", "json"],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.equal(JSON.parse(doctorOutput).healthy, true, doctorOutput);
|
||||
const executable = `${process.env.PREFIX}/bin/chromium`;
|
||||
const { stdout: chromiumVersion } = await execute(executable, ["--version"], {
|
||||
timeout: 30_000,
|
||||
});
|
||||
const marker = "rea-termux-browser-smoke";
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||
response.end(`<!doctype html><html><body><h1>${marker}</h1></body></html>`);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
try {
|
||||
const url = `http://127.0.0.1:${server.address().port}/smoke`;
|
||||
const scenario = {
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url },
|
||||
actions: [
|
||||
{
|
||||
step_id: "ready",
|
||||
action: "wait_for",
|
||||
locator: { kind: "css", selector: "h1" },
|
||||
state: "visible",
|
||||
},
|
||||
],
|
||||
capture: { at_end: ["url", "dom"] },
|
||||
};
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/rea.mjs",
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify(scenario),
|
||||
"--json",
|
||||
],
|
||||
{ timeout: 180_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.error, undefined, stdout);
|
||||
const capture = evidence.normalized_result;
|
||||
assert.ok(
|
||||
capture.steps.every((step) => step.status === "completed"),
|
||||
stdout,
|
||||
);
|
||||
const artifacts = capture.steps.at(-1).artifacts;
|
||||
assert.equal(artifacts.url.state, "captured", stdout);
|
||||
assert.equal(artifacts.url.value.url, url);
|
||||
assert.equal(artifacts.dom.state, "captured", stdout);
|
||||
assert.ok(artifacts.dom.value.text.includes(`<h1>${marker}</h1>`), stdout);
|
||||
assert.equal(capture.browser.cleanup, "terminated-owned-process");
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.version,
|
||||
chromium: chromiumVersion.trim(),
|
||||
mcp_doctor: "healthy",
|
||||
url_capture: "passed",
|
||||
dom_capture: "passed",
|
||||
cleanup: capture.browser.cleanup,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) =>
|
||||
server.close((cause) => (cause ? reject(cause) : resolve())),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host-side orchestration. Root is used only to stage/read files and send the
|
||||
# intent; TermuxService launches all package/build/browser work as the app UID.
|
||||
set -euo pipefail
|
||||
|
||||
staging="${RUNNER_TEMP:?}/rea-termux"
|
||||
app_home=/data/data/com.termux/files/home
|
||||
prefix=/data/data/com.termux/files/usr
|
||||
mkdir -p "$staging/diagnostics"
|
||||
collect_diagnostics() {
|
||||
adb logcat -d > "$staging/diagnostics/logcat.txt" 2>&1 || true
|
||||
adb exec-out cat "$app_home/rea-ci/run.log" > "$staging/diagnostics/termux.log" 2>&1 || true
|
||||
adb exec-out cat "$app_home/rea-ci/status" > "$staging/diagnostics/status.txt" 2>&1 || true
|
||||
}
|
||||
trap collect_diagnostics EXIT
|
||||
|
||||
adb root
|
||||
adb wait-for-device
|
||||
adb install "$staging/termux-app_v0.118.3+github-debug_x86_64.apk"
|
||||
adb shell am start -n com.termux/.app.TermuxActivity
|
||||
# First launch unpacks the APK's bundled bootstrap. Do not replace it with a
|
||||
# root-installed userland: that would miss Termux's own initialization.
|
||||
deadline=$((SECONDS + 180))
|
||||
until adb shell test -x "$prefix/bin/bash"; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
echo 'Termux bootstrap did not finish within 180 seconds' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
git archive --format=tar HEAD > "$staging/source.tar"
|
||||
adb push "$staging/source.tar" /data/local/tmp/rea-source.tar
|
||||
adb push scripts/verify/termux/in-app.sh /data/local/tmp/rea-in-app.sh
|
||||
adb shell chmod 644 /data/local/tmp/rea-source.tar /data/local/tmp/rea-in-app.sh
|
||||
app_uid=$(adb shell stat -c %u /data/data/com.termux | tr -d '\r')
|
||||
[[ "$app_uid" =~ ^[0-9]+$ && "$app_uid" != 0 ]]
|
||||
adb shell mkdir -p "$app_home/rea-ci" "$app_home/.termux"
|
||||
adb shell "echo allow-external-apps=true > $app_home/.termux/termux.properties"
|
||||
adb shell cp /data/local/tmp/rea-in-app.sh "$app_home/rea-ci/in-app.sh"
|
||||
adb shell cp /data/local/tmp/rea-source.tar "$app_home/rea-ci/source.tar"
|
||||
adb shell chown -R "$app_uid:$app_uid" "$app_home/rea-ci" "$app_home/.termux"
|
||||
adb shell restorecon -RF "$app_home/rea-ci" "$app_home/.termux"
|
||||
# Reload the external-command policy in a fresh app process before sending it.
|
||||
adb shell am force-stop com.termux
|
||||
adb shell am start -n com.termux/.app.TermuxActivity
|
||||
# Root may send this permission-protected intent; the service executes as the
|
||||
# application, rather than running Node/Chromium through adb shell or su.
|
||||
adb shell am startservice \
|
||||
-n com.termux/.app.RunCommandService \
|
||||
-a com.termux.RUN_COMMAND \
|
||||
--es com.termux.RUN_COMMAND_PATH "$prefix/bin/bash" \
|
||||
--esa com.termux.RUN_COMMAND_ARGUMENTS "-l,$app_home/rea-ci/in-app.sh" \
|
||||
--es com.termux.RUN_COMMAND_WORKDIR "$app_home" \
|
||||
--ez com.termux.RUN_COMMAND_BACKGROUND true
|
||||
|
||||
deadline=$((SECONDS + 2100))
|
||||
until adb shell test -f "$app_home/rea-ci/status"; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
echo 'Termux verification exceeded 35 minutes' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
adb exec-out cat "$app_home/rea-ci/run.log"
|
||||
status=$(adb shell cat "$app_home/rea-ci/status" | tr -d '\r')
|
||||
[[ "$status" == 0 ]]
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
report="$HOME/rea-ci"
|
||||
exec > "$report/run.log" 2>&1
|
||||
# Publish the result only after all commands have settled. The host reads it as
|
||||
# root; no shared-storage permission or helper Android application is needed.
|
||||
trap 'result=$?; printf "%s\n" "$result" > "$report/status"' EXIT
|
||||
set -x
|
||||
[[ "$(id -u)" != 0 ]]
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export HUSKY=0
|
||||
pkg update -y
|
||||
pkg upgrade -y
|
||||
pkg install -y nodejs-lts npm git x11-repo termux-exec
|
||||
pkg install -y chromium
|
||||
mkdir -p "$report/source"
|
||||
tar -xf "$report/source.tar" -C "$report/source"
|
||||
cd "$report/source"
|
||||
node -e 'if (process.platform !== "android") throw new Error("Expected Android Node.js")'
|
||||
node -e 'const expected = require("node:fs").readFileSync(".nvmrc", "utf8").trim().replace(/^v/, ""); if (process.versions.node !== expected) throw new Error(`Termux Node ${process.versions.node} differs from .nvmrc ${expected}`)'
|
||||
# Android uses Termux's Bionic-linked Node distribution; the desktop .nvmrc
|
||||
# binary cannot run here. Keep npm at the repository's exact pinned version.
|
||||
npm install --prefix "$report/toolchain" --no-package-lock --ignore-scripts \
|
||||
"$(node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).packageManager')"
|
||||
export PATH="$report/toolchain/node_modules/.bin:$PATH"
|
||||
node --version
|
||||
npm --version
|
||||
dpkg-query -W nodejs-lts chromium termux-exec
|
||||
npm ci
|
||||
npm run build:termux
|
||||
unset PLAYWRIGHT_BROWSERS_PATH npm_config_playwright_browsers_path npm_package_config_playwright_browsers_path
|
||||
node scripts/verify/termux/browser.mjs
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
PlaywrightScenarioBrowserCleanupOwner,
|
||||
} from "./PlaywrightScenarioBrowser.js";
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
const hostPlatform = process.platform;
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
Object.defineProperty(process, "platform", { value: hostPlatform });
|
||||
});
|
||||
|
||||
it("locks one attached browser for spellings that discover the same endpoint", () => {
|
||||
expect(attachedScenarioLeaseKey("http://127.0.0.1:9222", "page-1")).toBe(
|
||||
@@ -73,6 +78,43 @@ it("passes the selected environment to the actual browser launch boundary", asyn
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Termux execution support when launching Chromium on Android", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "android" });
|
||||
vi.stubEnv("PREFIX", "/data/data/com.termux/files/usr");
|
||||
const selected = { LANG: "selected-language", PREFIX: process.env.PREFIX };
|
||||
let launchOptions: Parameters<
|
||||
typeof import("playwright-core").chromium.launchPersistentContext
|
||||
>[1];
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: process.execPath },
|
||||
start_url: { url: "https://example.test" },
|
||||
actions: [
|
||||
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
openPlaywrightScenarioBrowser(scenario, selected, {
|
||||
launcher: {
|
||||
connectOverCDP: async () => {
|
||||
throw new Error("Unexpected connection");
|
||||
},
|
||||
launchPersistentContext: async (_profile, options) => {
|
||||
launchOptions = options;
|
||||
throw new Error("launch boundary reached");
|
||||
},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("launch boundary reached");
|
||||
|
||||
expect(launchOptions?.args).toEqual(["--no-sandbox", "--single-process"]);
|
||||
expect(launchOptions?.env).toEqual({
|
||||
LANG: "selected-language",
|
||||
LD_PRELOAD: "/data/data/com.termux/files/usr/lib/libtermux-exec.so",
|
||||
TERMUX_EXEC__PROC_SELF_EXE: process.execPath,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps attached-target ownership until cancelled cleanup actually settles", async () => {
|
||||
const controller = new AbortController();
|
||||
const closeStarted = deferred<void>();
|
||||
|
||||
@@ -273,13 +273,26 @@ const allowedBrowserEnvironment = (
|
||||
"XAUTHORITY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
];
|
||||
return Object.fromEntries(
|
||||
const selected = Object.fromEntries(
|
||||
allowed
|
||||
.map((name) => [name, environment[name]] as const)
|
||||
.filter(
|
||||
(entry): entry is readonly [string, string] => entry[1] !== undefined,
|
||||
),
|
||||
);
|
||||
if (process.platform !== "android") return selected;
|
||||
|
||||
// Android rejects execve() for binaries below Termux's application data
|
||||
// directory. Termux supplies an exec interceptor that rewrites those calls
|
||||
// through the system linker and removes itself for the launched program.
|
||||
// Playwright must retain it for its Chromium child process.
|
||||
const prefix = environment.PREFIX;
|
||||
if (prefix === undefined) return selected;
|
||||
return {
|
||||
...selected,
|
||||
LD_PRELOAD: `${prefix}/lib/libtermux-exec.so`,
|
||||
TERMUX_EXEC__PROC_SELF_EXE: process.execPath,
|
||||
};
|
||||
};
|
||||
const findConnectedPage = async (
|
||||
browser: Pick<Browser, "contexts">,
|
||||
@@ -495,6 +508,13 @@ export const openPlaywrightScenarioBrowser = async (
|
||||
handleSIGINT: false,
|
||||
handleSIGTERM: false,
|
||||
timeout: 0,
|
||||
// Termux's Chromium cannot create its renderer process through Android's
|
||||
// executable boundary. Single-process mode keeps the browser and page in
|
||||
// the Termux-launched process so scenario navigation remains available.
|
||||
args:
|
||||
process.platform === "android"
|
||||
? ["--no-sandbox", "--single-process"]
|
||||
: [],
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
const cleanup = () =>
|
||||
|
||||
@@ -4,6 +4,18 @@ import { join } from "node:path";
|
||||
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// Playwright's registry does not derive a cache directory for Termux's
|
||||
// `android` platform. Match the production CLI fallback before workers import
|
||||
// browser modules.
|
||||
if (
|
||||
process.platform === "android" &&
|
||||
process.env.PLAYWRIGHT_BROWSERS_PATH === undefined
|
||||
) {
|
||||
const cacheRoot =
|
||||
process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache");
|
||||
process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
||||
}
|
||||
|
||||
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
|
||||
const COVERAGE_ENABLED = process.argv.some((argument) =>
|
||||
argument.startsWith("--coverage"),
|
||||
|
||||
Reference in new issue
Block a user