Skip to content

Commit 0f6ff94

Browse files
committed
feat(studio-bridge): add Linux/Wine support for headless Studio
Adds the ability to run Roblox Studio under Wine on Linux, enabling headless CI testing and devcontainer workflows. New modules: - linux-config: environment-aware configuration resolution - linux-wine-env: Wine process environment assembly - linux-credential-writer: compile write-cred.c with MinGW, inject credentials into Wine's Credential Manager via Roblox API - linux-display-manager: Xvfb/openbox lifecycle management - linux-fflags: write ClientAppSettings.json with D3D11 renderer flags - linux-shader-patcher: patch #version 150 → 420 in shader pack - linux-studio-installer: download and extract Studio from CDN - linux-version-resolver: resolve latest version via clientsettingscdn - linux-prerequisites: dependency checker and installer New CLI commands (studio-bridge linux <cmd>): - setup: full environment provisioning (--install-deps for CI) - auth: credential injection from ROBLOSECURITY cookie - status: prerequisite health check Also moves shared auth code from nevermore-cli to nevermore-cli-helpers so studio-bridge can reuse cookie parsing and credential retrieval. Includes unit tests, GitHub Actions E2E workflow, and README.
1 parent 78aa686 commit 0f6ff94

35 files changed

Lines changed: 2398 additions & 195 deletions
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: studio-linux-e2e
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- 'tools/studio-bridge/src/linux/**'
8+
- 'tools/studio-bridge/src/process/**'
9+
- 'tools/studio-bridge/src/commands/linux/**'
10+
- 'tools/nevermore-cli-helpers/src/auth/**'
11+
- '.github/workflows/studio-linux-e2e.yml'
12+
13+
jobs:
14+
studio-linux-e2e:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 30
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v6
20+
21+
- name: Setup node
22+
uses: actions/setup-node@v6
23+
with:
24+
node-version: '21'
25+
26+
- name: Setup pnpm
27+
uses: pnpm/action-setup@v4
28+
with:
29+
cache: true
30+
31+
- name: Setup registries
32+
run: |
33+
echo -e "\n//npm.pkg.github.com/:_authToken=$GITHUB_TOKEN" >> .npmrc
34+
echo -e "\n//npm.pkg.github.com/:_authToken=$GITHUB_TOKEN" >> ~/.npmrc
35+
echo -e "\n//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
36+
echo -e "\n//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
40+
41+
- name: Install dependencies
42+
run: pnpm install --frozen-lockfile
43+
44+
- name: Build all tools
45+
run: pnpm -r --filter './tools/**' run build
46+
47+
- name: Setup Linux environment
48+
run: node tools/studio-bridge/dist/src/cli/cli.js linux setup --install-deps
49+
50+
- name: Verify environment health (pre-auth)
51+
run: node tools/studio-bridge/dist/src/cli/cli.js linux status
52+
53+
- name: Inject authentication
54+
if: ${{ env.ROBLOSECURITY != '' }}
55+
run: node tools/studio-bridge/dist/src/cli/cli.js linux auth
56+
env:
57+
ROBLOSECURITY: ${{ secrets.ROBLOSECURITY }}
58+
59+
- name: Verify Studio launches under Wine
60+
if: ${{ env.ROBLOSECURITY != '' }}
61+
timeout-minutes: 5
62+
env:
63+
ROBLOSECURITY: ${{ secrets.ROBLOSECURITY }}
64+
run: |
65+
STUDIO_DIR="${STUDIO_DIR:-$HOME/roblox-studio}"
66+
STUDIO_EXE="$STUDIO_DIR/RobloxStudioBeta.exe"
67+
WINEPREFIX="${WINEPREFIX:-$HOME/.wine}"
68+
69+
export DISPLAY=:99
70+
export WINEPREFIX
71+
export WINEARCH=win64
72+
export WINEDEBUG=-all
73+
export WINEDLLOVERRIDES='mscoree=d;mshtml=d'
74+
export MESA_GL_VERSION_OVERRIDE=4.5
75+
export MESA_GLSL_VERSION_OVERRIDE=450
76+
77+
echo "Launching Studio: wine $STUDIO_EXE"
78+
wine "$STUDIO_EXE" &
79+
STUDIO_PID=$!
80+
81+
# Wait up to 30s for the process to stabilize (not crash immediately)
82+
for i in $(seq 1 10); do
83+
sleep 3
84+
if ! kill -0 "$STUDIO_PID" 2>/dev/null; then
85+
echo "Studio process exited prematurely after ~$((i * 3))s"
86+
exit 1
87+
fi
88+
echo "Studio still running after $((i * 3))s..."
89+
done
90+
91+
echo "Studio survived 30s — launch test passed"
92+
kill "$STUDIO_PID" 2>/dev/null || true
93+
wait "$STUDIO_PID" 2>/dev/null || true
94+
95+
- name: Take screenshot
96+
if: always()
97+
run: |
98+
if command -v import &> /dev/null; then
99+
DISPLAY=:99 import -window root e2e-screenshot.png 2>/dev/null || true
100+
elif command -v xwd &> /dev/null; then
101+
DISPLAY=:99 xwd -root | convert xwd:- e2e-screenshot.png 2>/dev/null || true
102+
fi
103+
104+
- name: Upload screenshot
105+
if: always()
106+
uses: actions/upload-artifact@v4
107+
with:
108+
name: studio-screenshot
109+
path: e2e-screenshot.png
110+
if-no-files-found: ignore

docs/testing/testing.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,23 @@ When tests fail in CI, the `post-test-results` command parses Jest-lua output an
204204

205205
The resolver code lives in `tools/nevermore-cli/src/utils/sourcemap/` and is shared with the `strip-sourcemap-jest` command.
206206

207+
## Linux headless testing
208+
209+
Studio can run headlessly on Linux via Wine, enabling E2E tests in devcontainers and GitHub Actions without a display or GPU. The `studio-bridge` CLI handles all environment setup:
210+
211+
```bash
212+
# One-time setup
213+
studio-bridge linux setup --install-deps
214+
studio-bridge linux auth # reads $ROBLOSECURITY env var
215+
216+
# Run tests the same as on Windows/macOS
217+
nevermore test
218+
```
219+
220+
Prerequisites (Wine 11, Xvfb, openbox, Mesa llvmpipe) are documented in `tools/studio-bridge/src/linux/README.md`. The `linux setup --install-deps` flag installs everything on Debian/Ubuntu but is opt-in — it never runs sudo automatically.
221+
222+
For CI, set `ROBLOSECURITY` as a repository or Codespace secret. The `.github/workflows/studio-linux-e2e.yml` workflow demonstrates the full flow.
223+
207224
## CI design principles
208225

209226
- **Workflows should be thin.** All logic lives in `nevermore-cli` commands — GitHub Actions workflows just call them. This keeps CI debuggable locally.

pnpm-lock.yaml

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/nevermore-cli-helpers/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
],
2424
"dependencies": {
2525
"@quenty/cli-output-helpers": "workspace:*",
26+
"inquirer": "^13.2.0",
2627
"latest-version": "^9.0.0",
2728
"semver": "^7.6.0"
2829
},
@@ -31,12 +32,15 @@
3132
"@types/semver": "^7.5.0",
3233
"prettier": "2.7.1",
3334
"typescript": "^5.9.3",
34-
"typescript-memoize": "^1.1.1"
35+
"typescript-memoize": "^1.1.1",
36+
"vitest": "^3.0.0"
3537
},
3638
"scripts": {
3739
"build": "tsc --build",
3840
"build:watch": "tsc --build --watch",
3941
"build:clean": "tsc --build --clean",
42+
"test": "vitest run",
43+
"test:watch": "vitest",
4044
"preinstall": "npx only-allow pnpm"
4145
},
4246
"publishConfig": {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { COOKIE_NAME, parseStudioCookieValue } from './cookie-parser.js';
3+
4+
describe('COOKIE_NAME', () => {
5+
it('equals .ROBLOSECURITY', () => {
6+
expect(COOKIE_NAME).toBe('.ROBLOSECURITY');
7+
});
8+
});
9+
10+
describe('parseStudioCookieValue', () => {
11+
it('parses COOK::<value> format with angle brackets', () => {
12+
const result = parseStudioCookieValue('COOK::<abc123>');
13+
expect(result).toBe('abc123');
14+
});
15+
16+
it('parses value from comma-separated list', () => {
17+
const result = parseStudioCookieValue('OTHER::stuff,COOK::<secret>');
18+
expect(result).toBe('secret');
19+
});
20+
21+
it('returns undefined for plain text', () => {
22+
expect(parseStudioCookieValue('just a string')).toBeUndefined();
23+
});
24+
25+
it('returns undefined for COOK:: without angle brackets', () => {
26+
expect(parseStudioCookieValue('COOK::noBrackets')).toBeUndefined();
27+
});
28+
29+
it('returns undefined for empty string', () => {
30+
expect(parseStudioCookieValue('')).toBeUndefined();
31+
});
32+
33+
it('handles a realistic cookie value', () => {
34+
const cookie = '_|WARNING:-DO-NOT-SHARE|_abc123def456';
35+
const result = parseStudioCookieValue(`COOK::<${cookie}>`);
36+
expect(result).toBe(cookie);
37+
});
38+
});

tools/nevermore-cli/src/utils/auth/roblox-auth/cookie-parser.ts renamed to tools/nevermore-cli-helpers/src/auth/roblox-auth/cookie-parser.ts

File renamed without changes.

0 commit comments

Comments
 (0)