Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

| <img height="20" src="https://github.com/user-attachments/assets/340d360e-79b1-4c70-bfab-d944085f75df" /> Windows | <img height="20" src="https://github.com/user-attachments/assets/42d7e887-4616-4e8c-b1d3-e44e01340f8c" /> macOS | <img height="20" src="https://github.com/user-attachments/assets/e0cc4f33-4516-408b-9c5c-be71a3ac316b" /> 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) |
| <div align="center"><a href="https://apps.microsoft.com/detail/9p4q134b2jw3?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="150"/></a></div> | **[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) |
| <div align="center"><a href="https://apps.microsoft.com/detail/9p4q134b2jw3?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="150"/></a></div> | **[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.
Expand All @@ -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 ;)_
Expand Down
59 changes: 59 additions & 0 deletions build-scripts/sign-mac-helpers.js
Original file line number Diff line number Diff line change
@@ -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' }
);
}
};
10 changes: 10 additions & 0 deletions build/entitlements.helper.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
1 change: 1 addition & 0 deletions electron-builder.github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ mac:
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
afterSign: build-scripts/sign-mac-helpers.js
10 changes: 10 additions & 0 deletions src/main/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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',
Expand Down
115 changes: 114 additions & 1 deletion src/main/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as os from 'os';
import * as fs from 'fs';
import { spawn } from 'child_process';
import { app, dialog } from 'electron';

Check warning on line 5 in src/main/platform.ts

View workflow job for this annotation

GitHub Actions / Lint & Format

'dialog' is defined but never used
import log from 'electron-log/main.js';

export const isWindows = process.platform === 'win32';
Expand Down Expand Up @@ -298,7 +298,65 @@

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) {
Expand Down Expand Up @@ -365,3 +423,58 @@

return resolved;
}

export function resolveYtdlpPath(): string {
return effectiveYtdlpPath ?? resolveBundledYtdlpPath();
}

export async function initializeYtdlpPath(): Promise<string> {
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}`);
});
}
2 changes: 1 addition & 1 deletion src/tests/main.ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));

Expand Down
2 changes: 1 addition & 1 deletion src/tests/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));

Expand Down
Loading