diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba6c907..7827e96 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,9 +6,9 @@
|
Windows |
macOS |
Linux |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-arm64.AppImage) |
-|

| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-arm64.deb) |
-| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.1/ROSI-Linux-aarch64.rpm) |
+| **EXE:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-x64.exe) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Windows-arm64.exe) | **[Universal DMG](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-MacOS-universal.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-x86_64.AppImage) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-arm64.AppImage) |
+| 
| **[Universal ZIP](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-MacOS-universal.zip)** | **DEB:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-amd64.deb) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-arm64.deb) |
+| | | **RPM:** [x64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-x86_64.rpm) / [arm64](https://github.com/BurntToasters/ROSI/releases/download/v4.1.2/ROSI-Linux-aarch64.rpm) |
> [!IMPORTANT]
> The `.sig` files in this repo are NOT normal GPG signatures — they are for ROSI's built-in updater to verify the integrity of updates before downloading and installing.
@@ -30,6 +30,10 @@
---
+## Changes in `v4.1.2:`
+
+- **macOS:** Addressed a codesigning issue with yt-dlp/ffmpeg on macOS builds of ROSI.
+
## Changes in `v4.1.1:`
_What's a new feature update without a major bugfix am I right ;)_
diff --git a/build-scripts/sign-mac-helpers.js b/build-scripts/sign-mac-helpers.js
new file mode 100644
index 0000000..b06d33f
--- /dev/null
+++ b/build-scripts/sign-mac-helpers.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+/**
+ * Re-sign bundled helper binaries after electron-builder signs the app.
+ * PyInstaller sidecars (yt-dlp) extract a Python runtime at launch; without
+ * disable-library-validation they fail with Team ID mismatches on macOS.
+ */
+exports.default = async function signMacHelpers(context) {
+ if (context.electronPlatformName !== 'darwin') {
+ return;
+ }
+
+ const entitlements = path.join(__dirname, '..', 'build', 'entitlements.helper.plist');
+ if (!fs.existsSync(entitlements)) {
+ console.warn(`[sign-mac-helpers] Entitlements not found: ${entitlements}`);
+ return;
+ }
+
+ const identity = process.env.CSC_NAME || process.env.APPLE_SIGNING_IDENTITY;
+ if (!identity) {
+ console.warn('[sign-mac-helpers] No signing identity found; skipping helper re-sign');
+ return;
+ }
+
+ const appPath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`);
+ const resourcesPath = path.join(appPath, 'Contents', 'Resources');
+ const helpers = [
+ path.join(resourcesPath, 'assets', 'yt-dlp_macos'),
+ path.join(resourcesPath, 'ffmpeg', 'ffmpeg'),
+ path.join(resourcesPath, 'ffmpeg', 'ffprobe'),
+ ];
+
+ for (const target of helpers) {
+ if (!fs.existsSync(target)) {
+ console.log(`[sign-mac-helpers] Skipping missing helper: ${target}`);
+ continue;
+ }
+
+ console.log(`[sign-mac-helpers] Signing ${target}`);
+ execFileSync(
+ 'codesign',
+ [
+ '--force',
+ '--options',
+ 'runtime',
+ '--entitlements',
+ entitlements,
+ '--sign',
+ identity,
+ target,
+ ],
+ { stdio: 'inherit' }
+ );
+ }
+};
diff --git a/build/entitlements.helper.plist b/build/entitlements.helper.plist
new file mode 100644
index 0000000..da02cff
--- /dev/null
+++ b/build/entitlements.helper.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/electron-builder.github.yml b/electron-builder.github.yml
index 17d4cc3..a8d33c2 100644
--- a/electron-builder.github.yml
+++ b/electron-builder.github.yml
@@ -12,3 +12,4 @@ mac:
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
+afterSign: build-scripts/sign-mac-helpers.js
diff --git a/src/main/downloader.ts b/src/main/downloader.ts
index ead654f..fb33807 100644
--- a/src/main/downloader.ts
+++ b/src/main/downloader.ts
@@ -583,6 +583,16 @@ export function startDownload(
if (exitType === 'failed') {
cleanupPathFile();
sendProgress(session, `❌ Download failed: yt-dlp process exited with code ${code}`);
+ if (
+ downloadErrorData.includes('different Team IDs') ||
+ downloadErrorData.includes('[PYI-') ||
+ downloadErrorData.includes('Failed to load Python shared library')
+ ) {
+ sendProgress(
+ session,
+ ' macOS blocked the bundled yt-dlp runtime (code signing). Install yt-dlp via Homebrew as a workaround, or use a rebuilt ROSI release with signed helpers.'
+ );
+ }
sendProgress(session, ` Check console and stderr output above for details.`);
completeSession(session, '❌ Download failed.', 'failed');
return;
diff --git a/src/main/main.ts b/src/main/main.ts
index 6f12ded..dc622e0 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
import * as fs from 'fs';
import { randomUUID } from 'crypto';
import log from 'electron-log/main.js';
-import { isPackaged, resolveYtdlpPath, verifyBundledFfmpeg } from './platform';
+import { isPackaged, initializeYtdlpPath, verifyBundledFfmpeg } from './platform';
import {
loadSettings,
saveSettings,
@@ -82,7 +82,7 @@ process.on('unhandledRejection', (reason) => {
let ytdlpPath: string | null = null;
function getYtdlpPath(): string {
if (!ytdlpPath) {
- ytdlpPath = resolveYtdlpPath();
+ throw new Error('yt-dlp path has not been initialized.');
}
return ytdlpPath;
}
@@ -435,13 +435,14 @@ if (isPrimaryInstance && !isSmokeRun) {
});
}
-void app.whenReady().then(() => {
+void app.whenReady().then(async () => {
if (!isPrimaryInstance) {
app.quit();
return;
}
- const resolvedYtdlpPath = getYtdlpPath();
+ ytdlpPath = await initializeYtdlpPath();
+ const resolvedYtdlpPath = ytdlpPath;
if (!fs.existsSync(resolvedYtdlpPath)) {
dialog.showErrorBox(
'Missing Dependency',
diff --git a/src/main/platform.ts b/src/main/platform.ts
index 8950acc..4028a45 100644
--- a/src/main/platform.ts
+++ b/src/main/platform.ts
@@ -298,7 +298,65 @@ function getYtdlpBinaryName() {
export const ytdlpBinary = getYtdlpBinaryName();
-export function resolveYtdlpPath(): string {
+let effectiveYtdlpPath: string | null = null;
+
+const MAX_PROBE_BUFFER = 4096;
+
+function findMacSystemYtdlpPath(): string | null {
+ if (!isMac) return null;
+
+ const homeDir = safeHomeDir();
+ const candidates = [
+ path.join(homeDir, '.local', 'bin', 'yt-dlp'),
+ '/opt/homebrew/bin/yt-dlp',
+ '/usr/local/bin/yt-dlp',
+ ];
+
+ for (const candidate of candidates) {
+ try {
+ if (fs.existsSync(candidate)) {
+ const stats = fs.statSync(candidate);
+ if (stats.isFile()) return candidate;
+ }
+ } catch {
+ // try next candidate
+ }
+ }
+
+ return null;
+}
+
+function probeYtdlpBinary(ytdlpPath: string): Promise<{ ok: boolean; detail: string }> {
+ return new Promise((resolve) => {
+ let stderr = '';
+ let stdout = '';
+ const proc = spawn(ytdlpPath, ['--version'], {
+ env: { ...process.env, PATH: buildEnhancedPath() },
+ shell: false,
+ });
+
+ proc.stdout?.on('data', (data: Buffer) => {
+ if (stdout.length < 512) stdout += data.toString();
+ });
+ proc.stderr?.on('data', (data: Buffer) => {
+ if (stderr.length < MAX_PROBE_BUFFER) stderr += data.toString();
+ });
+
+ proc.on('close', (code) => {
+ if (code === 0) {
+ resolve({ ok: true, detail: stdout.trim() || stderr.trim() });
+ return;
+ }
+ resolve({ ok: false, detail: stderr.trim() || stdout.trim() || `exit code ${code}` });
+ });
+
+ proc.on('error', (err: Error) => {
+ resolve({ ok: false, detail: err.message });
+ });
+ });
+}
+
+function resolveBundledYtdlpPath(): string {
let resolved = '';
if (isPackaged) {
@@ -365,3 +423,58 @@ export function resolveYtdlpPath(): string {
return resolved;
}
+
+export function resolveYtdlpPath(): string {
+ return effectiveYtdlpPath ?? resolveBundledYtdlpPath();
+}
+
+export async function initializeYtdlpPath(): Promise {
+ if (effectiveYtdlpPath) return effectiveYtdlpPath;
+
+ const bundled = resolveBundledYtdlpPath();
+ if (!isMac || !isPackaged) {
+ effectiveYtdlpPath = bundled;
+ return bundled;
+ }
+
+ const bundledProbe = await probeYtdlpBinary(bundled);
+ if (bundledProbe.ok) {
+ log.info(
+ `Bundled yt-dlp verified: ${bundledProbe.detail.split('\n')[0] ?? bundledProbe.detail}`
+ );
+ effectiveYtdlpPath = bundled;
+ return bundled;
+ }
+
+ log.warn(`Bundled yt-dlp failed startup check at ${bundled}: ${bundledProbe.detail}`);
+
+ const systemPath = findMacSystemYtdlpPath();
+ if (systemPath) {
+ const systemProbe = await probeYtdlpBinary(systemPath);
+ if (systemProbe.ok) {
+ log.info(`Using system yt-dlp fallback at ${systemPath}`);
+ effectiveYtdlpPath = systemPath;
+ return systemPath;
+ }
+ log.warn(`System yt-dlp failed startup check at ${systemPath}: ${systemProbe.detail}`);
+ }
+
+ effectiveYtdlpPath = bundled;
+ return bundled;
+}
+
+export function verifyBundledYtdlp(): void {
+ const bundled = resolveBundledYtdlpPath();
+ if (!fs.existsSync(bundled)) {
+ log.info('No bundled yt-dlp found.');
+ return;
+ }
+
+ void probeYtdlpBinary(bundled).then((result) => {
+ if (result.ok) {
+ log.info(`Bundled yt-dlp verified: ${result.detail.split('\n')[0] ?? result.detail}`);
+ return;
+ }
+ log.warn(`Bundled yt-dlp at ${bundled} failed verification: ${result.detail}`);
+ });
+}
diff --git a/src/tests/main.ipc.test.ts b/src/tests/main.ipc.test.ts
index 22233d6..d4a3ecd 100644
--- a/src/tests/main.ipc.test.ts
+++ b/src/tests/main.ipc.test.ts
@@ -249,7 +249,7 @@ vi.mock('electron', () => ({
vi.mock('../main/platform', () => ({
isPackaged: true,
- resolveYtdlpPath: vi.fn(() => ytdlpFixturePath),
+ initializeYtdlpPath: vi.fn(async () => ytdlpFixturePath),
verifyBundledFfmpeg: vi.fn(),
}));
diff --git a/src/tests/queue.test.ts b/src/tests/queue.test.ts
index 7e9a332..4bbe116 100644
--- a/src/tests/queue.test.ts
+++ b/src/tests/queue.test.ts
@@ -177,7 +177,7 @@ vi.mock('electron', () => ({
vi.mock('../main/platform', () => ({
isPackaged: true,
- resolveYtdlpPath: vi.fn(() => getCurrentYtdlpFixturePath()),
+ initializeYtdlpPath: vi.fn(async () => getCurrentYtdlpFixturePath()),
verifyBundledFfmpeg: vi.fn(),
}));