feat(release): add cross-platform packaging

Add target-aware engine provisioning, platform package configs, and CI/release verification for macOS arm64, Windows x64, and Linux AppImage.
This commit is contained in:
NimBold
2026-06-23 21:26:51 +03:30
parent 0e3118ec2c
commit f603b74a99
41 changed files with 1889 additions and 382 deletions
+31 -10
View File
@@ -13,8 +13,10 @@ import { canonicalizeDownloadFileName, categoryForFileName } from '../utils/down
import { fetchMediaMetadataDeduped } from '../utils/mediaMetadata';
import {
resolveCategoryDestination,
resolveDownloadFilePath
resolveDownloadFilePath,
downloadLocationEquals
} from '../utils/downloadLocations';
import { getPlatformInfo } from '../utils/platform';
import { isTransferLocked } from '../utils/downloadActions';
import { useToast } from '../contexts/ToastContext';
import {
@@ -319,9 +321,10 @@ export const AddDownloadsModal = () => {
directory: true,
multiple: false,
defaultPath: saveLocation.startsWith('~') ? undefined : saveLocation
});
});
if (selected && typeof selected === 'string') {
setSaveLocation(selected);
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
setSaveLocation(approvedPath);
setIsSaveLocationManual(true);
}
} catch (e) {
@@ -347,6 +350,7 @@ export const AddDownloadsModal = () => {
let useSharedDestination = isSaveLocationManual;
const destinationOverrides: Record<number, string> = {};
const settings = useSettingsStore.getState();
const platform = await getPlatformInfo().catch(() => ({ os: 'unknown' }));
if (settings.askWhereToSaveEachFile && parsedItems.length > 0) {
for (const [index, item] of parsedItems.entries()) {
try {
@@ -360,7 +364,8 @@ export const AddDownloadsModal = () => {
defaultPath: suggestedLocation.startsWith('~') ? undefined : suggestedLocation
});
if (selected && typeof selected === 'string') {
destinationOverrides[index] = selected;
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
destinationOverrides[index] = approvedPath;
} else {
setIsSubmitting(false);
return;
@@ -398,8 +403,13 @@ export const AddDownloadsModal = () => {
const destination = download.destination ||
await resolveCategoryDestination(settings, download.category);
if (
destination === itemLocation &&
download.fileName === finalFile &&
downloadLocationEquals(
destination,
download.fileName,
itemLocation,
finalFile,
platform.os
) &&
download.status !== 'failed'
) {
fileExistsInStore = true;
@@ -456,6 +466,7 @@ export const AddDownloadsModal = () => {
destinationOverrides: Record<number, string> = {}
) => {
let itemsToAdd: Array<AddDownloadDraftRow | null> = [...parsedItems];
const platform = await getPlatformInfo().catch(() => ({ os: 'unknown' }));
if (resolutions) {
for (const res of resolutions) {
@@ -491,8 +502,13 @@ export const AddDownloadsModal = () => {
const destination = download.destination ||
await resolveCategoryDestination(currentSettings, download.category);
if (
destination === itemLocation &&
download.fileName === newName &&
downloadLocationEquals(
destination,
download.fileName,
itemLocation,
newName,
platform.os
) &&
download.status !== 'failed'
) {
storeHas = true;
@@ -534,8 +550,13 @@ export const AddDownloadsModal = () => {
const destination = download.destination ||
await resolveCategoryDestination(currentSettings, download.category);
if (
destination === itemLocation &&
download.fileName === finalFile &&
downloadLocationEquals(
destination,
download.fileName,
itemLocation,
finalFile,
platform.os
) &&
download.status !== 'failed'
) {
existingItem = download;
+2 -2
View File
@@ -211,8 +211,8 @@ export default function LogsView() {
<div className="bg-black/10 border-y border-border-modal px-4 py-2 shrink-0 flex items-center gap-2 text-text-muted text-[10px] select-none">
<Info size={12} className="text-text-muted opacity-80 shrink-0" />
<span className="opacity-90 leading-tight">
<strong className="font-medium text-text-primary mr-1">Privacy Note:</strong>
Telemetry securely captures basic hardware capabilities (OS, CPU, RAM) exclusively for troubleshooting. No unique identifiers or sensitive paths are collected. Logs remain entirely offline on your device until manually exported.
<strong className="font-medium text-text-primary mr-1">Local diagnostics:</strong>
Firelink keeps bounded rotating logs on this device for troubleshooting. Common secrets, URL queries, and home-directory paths are redacted during display and export. Nothing is uploaded automatically.
</span>
</div>
+14 -2
View File
@@ -8,6 +8,7 @@ import { PostQueueAction, SchedulerSettings, useSettingsStore } from '../store/u
import { MAIN_QUEUE_ID, useDownloadStore } from '../store/useDownloadStore';
import { WindowDragRegion } from './WindowDragRegion';
import { useToast } from '../contexts/ToastContext';
import { usePlatformInfo } from '../utils/platform';
const days = [
{ value: 0, label: 'Su' },
@@ -65,7 +66,8 @@ export default function SchedulerView() {
const { addToast } = useToast();
const [permissionMessage, setPermissionMessage] = useState('');
const [automationPermissionGranted, setAutomationPermissionGranted] = useState<boolean | null>(null);
const isMac = navigator.userAgent.includes('Mac');
const platform = usePlatformInfo();
const isMac = platform.os === 'macos';
useEffect(() => {
setDraft(savedSettings);
@@ -365,7 +367,7 @@ export default function SchedulerView() {
</section>
</div>
{isMac && (
{isMac ? (
<section className="app-card mt-4 max-w-[760px] p-5">
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
<LockKeyhole size={17} className="text-accent" /> System Permissions
@@ -393,6 +395,16 @@ export default function SchedulerView() {
</div>
{permissionMessage && <p className="mt-3 text-[11px] text-text-muted">{permissionMessage}</p>}
</section>
) : (
<section className="app-card mt-4 max-w-[760px] p-5">
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
<LockKeyhole size={17} className="text-accent" /> System Actions
</div>
<p className="text-[12px] text-text-muted">
Sleep, restart, and shut down use {platform.os === 'windows' ? 'Windows system privileges' : 'your Linux desktop and system policy'}.
Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.
</p>
</section>
)}
</div>
</div>
+27 -19
View File
@@ -24,6 +24,7 @@ import {
DOWNLOAD_CATEGORIES,
normalizeCategorySubfolder
} from '../utils/downloadLocations';
import { usePlatformInfo } from '../utils/platform';
const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[] = [
{ type: 'downloads', label: 'Downloads', icon: Download },
@@ -176,6 +177,7 @@ const CategoryFolderInput = ({
export default function SettingsView() {
const settings = useSettingsStore();
const activeTab = settings.activeSettingsTab;
const platform = usePlatformInfo();
// Local state for engine status
const [engineStatus, setEngineStatus] = useState<EngineStatusItem[] | null>(null);
@@ -362,7 +364,8 @@ runEngineChecks(false);
defaultPath: currentPath.startsWith('~') ? undefined : currentPath
});
if (selected && typeof selected === 'string') {
settings.setCategoryDirectoryOverride(category, selected);
const approvedPath = await settings.approveDownloadRoot(selected);
settings.setCategoryDirectoryOverride(category, approvedPath);
}
} catch (e) {
console.error(`Failed to select folder for ${category}:`, e);
@@ -379,7 +382,8 @@ runEngineChecks(false);
: settings.baseDownloadFolder
});
if (base && typeof base === 'string') {
settings.setBaseDownloadFolder(base);
const approvedBase = await settings.approveDownloadRoot(base);
settings.setBaseDownloadFolder(approvedBase);
try {
const safeSubfolders = Object.fromEntries(
DOWNLOAD_CATEGORIES.map(category => [
@@ -391,7 +395,7 @@ runEngineChecks(false);
])
);
await invoke('create_category_directories', {
baseFolder: base,
baseFolder: approvedBase,
subfolders: safeSubfolders
});
} catch (e) {
@@ -647,24 +651,28 @@ runEngineChecks(false);
</div>
</div>
<h2 className="settings-section-title">macOS Integration</h2>
<h2 className="settings-section-title">
{platform.os === 'macos' ? 'macOS Integration' : 'Desktop Integration'}
</h2>
<div className="mac-settings-group">
{platform.os === 'macos' && (
<label className="mac-settings-row cursor-default">
<div className="settings-row-label">
<span>Show badge on Dock icon</span>
<small>Displays active download count on Firelink Dock icon.</small>
</div>
<input
type="checkbox"
checked={settings.showDockBadge}
onChange={(e) => settings.setShowDockBadge(e.target.checked)}
className="mac-switch"
/>
</label>
)}
<label className="mac-settings-row cursor-default">
<div className="settings-row-label">
<span>Show badge on Dock icon</span>
<small>Displays the number of active downloads on the Firelink Dock icon.</small>
</div>
<input
type="checkbox"
checked={settings.showDockBadge}
onChange={(e) => settings.setShowDockBadge(e.target.checked)}
className="mac-switch"
/>
</label>
<label className="mac-settings-row cursor-default">
<div className="settings-row-label">
<span>Show menu bar icon</span>
<small>Provides quick access to downloads and queues from the macOS menu bar.</small>
<span>{platform.os === 'macos' ? 'Show menu bar icon' : 'Show system tray icon'}</span>
<small>Provides quick access to downloads and queues.</small>
</div>
<input
type="checkbox"
@@ -732,7 +740,7 @@ runEngineChecks(false);
)}
<p className="settings-group-footer">
{settings.proxyMode === 'none' && 'Downloads ignore configured proxies.'}
{settings.proxyMode === 'system' && 'Downloads use the matching macOS system proxy when one is configured.'}
{settings.proxyMode === 'system' && `Downloads use the detected ${platform.os === 'macos' ? 'macOS' : platform.os === 'windows' ? 'Windows' : 'desktop'} system proxy when available.`}
{settings.proxyMode === 'custom' && (settings.proxyHost
? `Downloads use http://${settings.proxyHost}:${settings.proxyPort}.`
: 'Enter a proxy host and port to enable the custom proxy.')}