mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-08-29 03:08:30 +08:00
helper fix
This commit is contained in:
@@ -29,6 +29,14 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
|
||||
# (and to avoid 429s when the platform matrix builds hit it at once).
|
||||
- name: Cache caption assets
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: caption-assets
|
||||
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
|
||||
|
||||
- name: Build Windows app
|
||||
run: npm run build:win
|
||||
env:
|
||||
@@ -78,6 +86,14 @@ jobs:
|
||||
env:
|
||||
npm_config_build_from_source: "false"
|
||||
|
||||
# ─── Cache caption assets ─────────────────────────────────
|
||||
# Avoid re-fetching the Whisper model from HuggingFace every run (and 429s under the matrix).
|
||||
- name: Cache caption assets
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: caption-assets
|
||||
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
|
||||
|
||||
# ─── Import Code Signing Certificate ──────────────────────
|
||||
# This is the KEY step that makes CI signing work.
|
||||
# We create a temporary keychain, import the .p12 cert into it,
|
||||
@@ -120,6 +136,14 @@ jobs:
|
||||
- name: Build Vite + Electron
|
||||
run: npx tsc && npx vite build
|
||||
|
||||
# ─── Build native macOS helpers ───────────────────────────
|
||||
# The package step below calls electron-builder directly (not `npm run build:mac`),
|
||||
# so the Swift screencapturekit + cursor helpers must be compiled here first or the
|
||||
# packaged app ships without them (no recording, "cursor helper couldn't be found").
|
||||
# Builds universal binaries into electron/native/bin/darwin-{arm64,x64}.
|
||||
- name: Build native macOS helpers
|
||||
run: npm run build:native:mac
|
||||
|
||||
# ─── Package with electron-builder ────────────────────────
|
||||
# electron-builder handles deep codesigning the .app bundle
|
||||
# "notarize: false" in electron-builder.json5 prevents it from
|
||||
@@ -245,6 +269,14 @@ jobs:
|
||||
- name: Install pacman build dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libarchive-tools
|
||||
|
||||
# Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
|
||||
# (and to avoid 429s when the platform matrix builds hit it at once).
|
||||
- name: Cache caption assets
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: caption-assets
|
||||
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
|
||||
|
||||
- name: Build Linux app
|
||||
run: npm run build:linux
|
||||
env:
|
||||
|
||||
@@ -18,14 +18,20 @@ const cursorHelperName = "openscreen-macos-cursor-helper";
|
||||
const packageDir = path.join(root, "electron", "native", "screencapturekit");
|
||||
const buildDir = path.join(packageDir, "build");
|
||||
const swiftBuildDir = path.join(buildDir, "swiftpm");
|
||||
const builtHelperPath = path.join(swiftBuildDir, "release", helperName);
|
||||
const localHelperPath = path.join(buildDir, helperName);
|
||||
const builtCursorHelperPath = path.join(swiftBuildDir, "release", cursorHelperName);
|
||||
const localCursorHelperPath = path.join(buildDir, cursorHelperName);
|
||||
const archTag = process.arch === "arm64" ? "darwin-arm64" : "darwin-x64";
|
||||
const distributableDir = path.join(root, "electron", "native", "bin", archTag);
|
||||
const distributablePath = path.join(distributableDir, helperName);
|
||||
const distributableCursorHelperPath = path.join(distributableDir, cursorHelperName);
|
||||
|
||||
// Build a universal (arm64 + x86_64) binary by default so both the arm64 and x64 DMGs ship a helper
|
||||
// that runs natively. Override with OPENSCREEN_MAC_HELPER_ARCHS (comma-separated) for a faster
|
||||
// single-arch local build.
|
||||
const archs = (process.env.OPENSCREEN_MAC_HELPER_ARCHS ?? "arm64,x86_64")
|
||||
.split(",")
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean);
|
||||
const archToTag = (arch) => (arch === "x86_64" || arch === "x64" ? "darwin-x64" : "darwin-arm64");
|
||||
// A universal binary runs on both arches, so when building both, place it in each dir the runtime
|
||||
// might read (it resolves electron/native/bin/<darwin-arm64|darwin-x64> by the running app's arch).
|
||||
const targetTags = archs.length > 1 ? ["darwin-arm64", "darwin-x64"] : [archToTag(archs[0])];
|
||||
|
||||
const xcodebuildVersion = spawnSync("xcodebuild", ["-version"], {
|
||||
cwd: root,
|
||||
@@ -50,42 +56,101 @@ if (xcodebuildVersion.status !== 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
"swift",
|
||||
["build", "-c", "release", "--package-path", packageDir, "--build-path", swiftBuildDir],
|
||||
{
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Failed to start Swift build: ${result.error.message}`);
|
||||
process.exit(1);
|
||||
// Locate a built binary by name under a build dir (SwiftPM's exact output path varies by version).
|
||||
function findArtifact(dir, name) {
|
||||
const stack = [dir];
|
||||
const matches = [];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) stack.push(full);
|
||||
else if (entry.isFile() && entry.name === name) matches.push(full);
|
||||
}
|
||||
}
|
||||
matches.sort((a, b) => {
|
||||
const ra = /release/i.test(a) ? 0 : 1;
|
||||
const rb = /release/i.test(b) ? 0 : 1;
|
||||
if (ra !== rb) return ra - rb;
|
||||
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
||||
});
|
||||
return matches[0] ?? null;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
// Build each arch in its own SwiftPM invocation. A single --arch uses SwiftPM's lenient build path
|
||||
// that tolerates the helper's `@main` inside a main.swift file; combining arches in one invocation
|
||||
// (--arch a --arch b) switches to the stricter "apple" build system that rejects it. We lipo the
|
||||
// slices together afterwards.
|
||||
const slicesByName = { [helperName]: [], [cursorHelperName]: [] };
|
||||
for (const arch of archs) {
|
||||
const archBuildDir = path.join(swiftBuildDir, arch);
|
||||
const result = spawnSync(
|
||||
"swift",
|
||||
[
|
||||
"build",
|
||||
"-c",
|
||||
"release",
|
||||
"--arch",
|
||||
arch,
|
||||
"--package-path",
|
||||
packageDir,
|
||||
"--build-path",
|
||||
archBuildDir,
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
if (result.error) {
|
||||
console.error(`Failed to start Swift build (${arch}): ${result.error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
for (const name of [helperName, cursorHelperName]) {
|
||||
const artifact = findArtifact(archBuildDir, name);
|
||||
if (!artifact) {
|
||||
console.error(`Swift build (${arch}) completed but artifact was not found: ${name}`);
|
||||
process.exit(1);
|
||||
}
|
||||
slicesByName[name].push(artifact);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(buildDir, { recursive: true });
|
||||
fs.mkdirSync(distributableDir, { recursive: true });
|
||||
for (const artifactPath of [builtHelperPath, builtCursorHelperPath]) {
|
||||
if (!fs.existsSync(artifactPath)) {
|
||||
console.error(`Swift build completed but expected artifact was not found: ${artifactPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
fs.copyFileSync(builtHelperPath, localHelperPath);
|
||||
fs.copyFileSync(builtHelperPath, distributablePath);
|
||||
fs.copyFileSync(builtCursorHelperPath, localCursorHelperPath);
|
||||
fs.copyFileSync(builtCursorHelperPath, distributableCursorHelperPath);
|
||||
fs.chmodSync(localHelperPath, 0o755);
|
||||
fs.chmodSync(distributablePath, 0o755);
|
||||
fs.chmodSync(localCursorHelperPath, 0o755);
|
||||
fs.chmodSync(distributableCursorHelperPath, 0o755);
|
||||
const targetDirs = targetTags.map((tag) => path.join(root, "electron", "native", "bin", tag));
|
||||
for (const dir of targetDirs) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
console.log(`Built macOS ScreenCaptureKit helper: ${localHelperPath}`);
|
||||
console.log(`Copied redistributable helper: ${distributablePath}`);
|
||||
console.log(`Built macOS cursor helper: ${localCursorHelperPath}`);
|
||||
console.log(`Copied redistributable cursor helper: ${distributableCursorHelperPath}`);
|
||||
for (const [name, localPath] of [
|
||||
[helperName, localHelperPath],
|
||||
[cursorHelperName, localCursorHelperPath],
|
||||
]) {
|
||||
const slices = slicesByName[name];
|
||||
let source = slices[0];
|
||||
// Stitch the per-arch slices into one universal (fat) binary so the same file runs on both arches.
|
||||
if (slices.length > 1) {
|
||||
const universal = path.join(buildDir, `${name}.universal`);
|
||||
const lipo = spawnSync("lipo", ["-create", ...slices, "-output", universal], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (lipo.status !== 0) {
|
||||
console.error(`lipo failed to combine ${name} (${archs.join(", ")})`);
|
||||
process.exit(lipo.status ?? 1);
|
||||
}
|
||||
source = universal;
|
||||
}
|
||||
for (const dest of [localPath, ...targetDirs.map((dir) => path.join(dir, name))]) {
|
||||
fs.copyFileSync(source, dest);
|
||||
fs.chmodSync(dest, 0o755);
|
||||
}
|
||||
console.log(`Built ${name} (${archs.join(", ")}) → ${targetTags.join(", ")}`);
|
||||
}
|
||||
|
||||
@@ -48,16 +48,64 @@ async function exists(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_ATTEMPTS = 6;
|
||||
// HuggingFace rate-limits (429) when the parallel CI matrix builds all hit it at once; also retry the
|
||||
// usual transient server errors.
|
||||
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function backoffMs(attempt, retryAfter) {
|
||||
// Honor Retry-After when the server sends it (seconds or an HTTP date).
|
||||
if (retryAfter) {
|
||||
const secs = Number(retryAfter);
|
||||
if (Number.isFinite(secs)) return Math.min(60_000, secs * 1000);
|
||||
const at = Date.parse(retryAfter);
|
||||
if (!Number.isNaN(at)) return Math.min(60_000, Math.max(0, at - Date.now()));
|
||||
}
|
||||
// Exponential backoff with jitter: ~2s, 4s, 8s, 16s, 32s, capped at 60s.
|
||||
return Math.min(60_000, 2000 * 2 ** (attempt - 1)) + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
async function fetchWithRetry(url) {
|
||||
let lastErr;
|
||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
const res = await fetch(url, { headers: { "user-agent": "openscreen-build" } });
|
||||
if (res.ok && res.body) return res;
|
||||
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_ATTEMPTS) {
|
||||
const wait = backoffMs(attempt, res.headers.get("retry-after"));
|
||||
console.log(
|
||||
` … HTTP ${res.status}, retry ${attempt}/${MAX_ATTEMPTS - 1} in ${Math.round(wait / 1000)}s`,
|
||||
);
|
||||
await sleep(wait);
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Failed to download ${url}: HTTP ${res.status} ${res.statusText}`);
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
const isHttp = err instanceof Error && err.message.startsWith("Failed to download");
|
||||
if (isHttp || attempt >= MAX_ATTEMPTS) throw err;
|
||||
// Network/DNS error: back off and retry.
|
||||
const wait = backoffMs(attempt, null);
|
||||
console.log(
|
||||
` … ${err.message}, retry ${attempt}/${MAX_ATTEMPTS - 1} in ${Math.round(wait / 1000)}s`,
|
||||
);
|
||||
await sleep(wait);
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
async function download(url, dest) {
|
||||
if (await exists(dest)) {
|
||||
console.log(` ✓ cached ${path.relative(OUT, dest)}`);
|
||||
return;
|
||||
}
|
||||
await mkdir(path.dirname(dest), { recursive: true });
|
||||
const res = await fetch(url);
|
||||
if (!res.ok || !res.body) {
|
||||
throw new Error(`Failed to download ${url}: HTTP ${res.status} ${res.statusText}`);
|
||||
}
|
||||
const res = await fetchWithRetry(url);
|
||||
const tmp = `${dest}.partial`;
|
||||
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
|
||||
const { rename } = await import("node:fs/promises");
|
||||
|
||||
Reference in New Issue
Block a user