From 7f7df1e65f944d011cc6d17a8e4869f830a1b9b2 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 15:13:00 -0800 Subject: [PATCH 01/15] Add AMD detection and handling --- src/constants.ts | 13 ++++++ src/preload.ts | 2 +- src/utils.ts | 86 +++++++++++++++++++++++++++++---------- src/virtualEnvironment.ts | 58 +++++++++++++++++++++++++- tests/unit/utils.test.ts | 79 ++++++++++++++++++++++++++++------- 5 files changed, 198 insertions(+), 40 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2e1057775..8bef67b56 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -185,6 +185,19 @@ export const NIGHTLY_CPU_TORCH_URL = TorchMirrorUrl.NightlyCpu; /** @deprecated Use {@link TorchMirrorUrl} instead. */ export const DEFAULT_PYPI_INDEX_URL = TorchMirrorUrl.Default; +export const AMD_ROCM_SDK_PACKAGES: string[] = [ + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_core-0.1.dev0-py3-none-win_amd64.whl', + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_devel-0.1.dev0-py3-none-win_amd64.whl', + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_libraries_custom-0.1.dev0-py3-none-win_amd64.whl', + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm-0.1.dev0.tar.gz', +]; + +export const AMD_TORCH_PACKAGES: string[] = [ + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torch-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl', + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchaudio-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl', + 'https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchvision-0.24.0+rocmsdk20251116-cp312-cp312-win_amd64.whl', +]; + /** The log files used by the desktop process. */ export enum LogFile { /** The ComfyUI server log file. */ diff --git a/src/preload.ts b/src/preload.ts index 6bbacc93f..f795f38e8 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -18,7 +18,7 @@ const openFolder = async (folderPath: string) => { ipcRenderer.send(IPC_CHANNELS.OPEN_PATH, path.join(basePath, folderPath)); }; -export type GpuType = 'nvidia' | 'mps' | 'unsupported'; +export type GpuType = 'nvidia' | 'amd' | 'mps' | 'unsupported'; export type TorchDeviceType = GpuType | 'cpu'; export interface InstallOptions { diff --git a/src/utils.ts b/src/utils.ts index fc521d872..18bf24b74 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -126,6 +126,54 @@ export async function rotateLogFiles(logDir: string, baseName: string, maxFiles } const execAsync = promisify(exec); +const AMD_WMI_QUERY = + 'powershell.exe -c "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; +const AMD_VENDOR_ID = '1002'; +const PCI_VENDOR_ID_REGEX = /ven_([\da-f]{4})/i; + +/** + * Checks whether a PNPDeviceID contains the specified PCI vendor ID. + * @param pnpDeviceId The PNPDeviceID string from WMI. + * @param vendorId The PCI vendor ID to match (hex). + * @return `true` if the vendor ID matches. + */ +function hasPciVendorId(pnpDeviceId: string, vendorId: string): boolean { + const match = pnpDeviceId.match(PCI_VENDOR_ID_REGEX); + return match?.[1]?.toUpperCase() === vendorId.toUpperCase(); +} + +/** + * Detects NVIDIA GPUs on Windows using nvidia-smi. + * @return `true` if nvidia-smi executes successfully. + */ +async function hasNvidiaGpuViaSmi(): Promise { + try { + await execAsync('nvidia-smi'); + return true; + } catch { + return false; + } +} + +/** + * Detects AMD GPUs on Windows by parsing PNPDeviceID values from CIM. + * @return `true` if an AMD GPU vendor ID is detected. + */ +async function hasAmdGpuViaWmi(): Promise { + try { + const res = await execAsync(AMD_WMI_QUERY); + const stdout = res?.stdout?.trim(); + if (!stdout) return false; + + return stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .some((pnpDeviceId) => hasPciVendorId(pnpDeviceId, AMD_VENDOR_ID)); + } catch { + return false; + } +} interface HardwareValidation { isValid: boolean; @@ -156,41 +204,35 @@ export async function validateHardware(): Promise { return { isValid: true, gpu: 'mps' }; } - // Windows NVIDIA GPU validation + // Windows GPU validation if (process.platform === 'win32') { const graphics = await si.graphics(); - const hasNvidia = graphics.controllers.some((controller) => controller.vendor.toLowerCase().includes('nvidia')); + const hasNvidia = graphics.controllers.some((controller) => { + const details = `${controller.vendor ?? ''} ${controller.model ?? ''}`.toLowerCase(); + return details.includes('nvidia'); + }); if (process.env.SKIP_HARDWARE_VALIDATION) { console.log('Skipping hardware validation'); + if (hasNvidia) return { isValid: true, gpu: 'nvidia' }; + if (await hasAmdGpuViaWmi()) return { isValid: true, gpu: 'amd' }; return { isValid: true }; } - if (!hasNvidia) { - try { - // wmic is unreliable. Check in PS. - const res = await execAsync( - 'powershell.exe -c "$n = \'*NVIDIA*\'; Get-CimInstance win32_videocontroller | ? { $_.Name -like $n -or $_.VideoProcessor -like $n -or $_.AdapterCompatibility -like $n }"' - ); - if (!res?.stdout) throw new Error('No NVIDIA GPU detected'); - } catch { - try { - await execAsync('nvidia-smi'); - } catch { - return { - isValid: false, - error: 'ComfyUI requires an NVIDIA GPU on Windows. No NVIDIA GPU was detected.', - }; - } - } - } + if (hasNvidia) return { isValid: true, gpu: 'nvidia' }; + + if (await hasNvidiaGpuViaSmi()) return { isValid: true, gpu: 'nvidia' }; + if (await hasAmdGpuViaWmi()) return { isValid: true, gpu: 'amd' }; - return { isValid: true, gpu: 'nvidia' }; + return { + isValid: false, + error: 'ComfyUI requires an NVIDIA or AMD GPU on Windows. No supported GPU was detected.', + }; } return { isValid: false, - error: 'ComfyUI currently supports only Windows (NVIDIA GPU) and Apple Silicon Macs.', + error: 'ComfyUI currently supports only Windows (NVIDIA or AMD GPU) and Apple Silicon Macs.', }; } catch (error) { log.error('Error validating hardware:', error); diff --git a/src/virtualEnvironment.ts b/src/virtualEnvironment.ts index 3f51a763d..eb5c33537 100644 --- a/src/virtualEnvironment.ts +++ b/src/virtualEnvironment.ts @@ -7,7 +7,7 @@ import { readdir, rm } from 'node:fs/promises'; import os, { EOL } from 'node:os'; import path from 'node:path'; -import { InstallStage, TorchMirrorUrl } from './constants'; +import { AMD_ROCM_SDK_PACKAGES, AMD_TORCH_PACKAGES, InstallStage, TorchMirrorUrl } from './constants'; import { PythonImportVerificationError } from './infrastructure/pythonImportVerificationError'; import { useAppState } from './main-process/appState'; import { createInstallStageInfo } from './main-process/installStages'; @@ -233,7 +233,9 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor { function compiledRequirements() { if (process.platform === 'darwin') return 'macos'; if (process.platform === 'win32') { - return selectedDevice === 'cpu' ? 'windows_cpu' : 'windows_nvidia'; + if (selectedDevice === 'cpu') return 'windows_cpu'; + if (selectedDevice === 'amd') return 'windows_amd'; + return 'windows_nvidia'; } } } @@ -377,6 +379,12 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor { public async installRequirements(callbacks?: ProcessCallbacks): Promise { useAppState().setInstallStage(createInstallStageInfo(InstallStage.INSTALLING_REQUIREMENTS, { progress: 25 })); + if (this.selectedDevice === 'amd') { + log.info('Skipping compiled requirements for AMD GPU; using manual install.'); + await this.manualInstall(callbacks); + return; + } + const installCmd = getPipInstallArgs({ requirementsFile: this.requirementsCompiledPath, indexStrategy: 'unsafe-best-match', @@ -604,6 +612,12 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor { }) ); + if (this.selectedDevice === 'amd') { + await this.installAmdRocmSdk(callbacks); + await this.installAmdTorch(callbacks); + return; + } + const torchMirror = this.torchMirror || getDefaultTorchMirror(this.selectedDevice); const config: PipInstallConfig = { packages: ['torch', 'torchvision', 'torchaudio'], @@ -621,6 +635,46 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor { } } + /** + * Installs AMD ROCm SDK packages on Windows. + * @param callbacks The callbacks to use for the command. + */ + private async installAmdRocmSdk(callbacks?: ProcessCallbacks): Promise { + if (process.platform !== 'win32') { + throw new Error('AMD ROCm packages are currently supported only on Windows.'); + } + + const installArgs = getPipInstallArgs({ + packages: AMD_ROCM_SDK_PACKAGES, + }); + + log.info('Installing AMD ROCm SDK packages.'); + const { exitCode } = await this.runUvCommandAsync(installArgs, callbacks); + if (exitCode !== 0) { + throw new Error(`Failed to install AMD ROCm SDK packages: exit code ${exitCode}`); + } + } + + /** + * Installs AMD ROCm PyTorch wheels on Windows. + * @param callbacks The callbacks to use for the command. + */ + private async installAmdTorch(callbacks?: ProcessCallbacks): Promise { + if (process.platform !== 'win32') { + throw new Error('AMD ROCm packages are currently supported only on Windows.'); + } + + const installArgs = getPipInstallArgs({ + packages: AMD_TORCH_PACKAGES, + }); + + log.info('Installing AMD ROCm PyTorch packages.'); + const { exitCode } = await this.runUvCommandAsync(installArgs, callbacks); + if (exitCode !== 0) { + throw new Error(`Failed to install AMD ROCm PyTorch packages: exit code ${exitCode}`); + } + } + /** * Installs the requirements for ComfyUI core using `requirements.txt`. * @param callbacks The callbacks to use for the command. diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 9e8cb761e..1b59f5dea 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -1,11 +1,28 @@ +import type { ChildProcess } from 'node:child_process'; +import { exec } from 'node:child_process'; import type { Systeminformation } from 'systeminformation'; import si from 'systeminformation'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { validateHardware } from '@/utils'; +vi.mock('node:child_process', () => ({ + exec: vi.fn(), +})); vi.mock('systeminformation'); +const execMock = vi.mocked(exec); + +const createChildProcess = (): ChildProcess => + ({ + kill: vi.fn(), + on: vi.fn(), + }) as unknown as ChildProcess; + +beforeEach(() => { + execMock.mockReset(); +}); + describe('validateHardware', () => { it('accepts Apple Silicon Mac', async () => { vi.stubGlobal('process', { ...process, platform: 'darwin' }); @@ -36,28 +53,60 @@ describe('validateHardware', () => { expect(result).toStrictEqual({ isValid: true, gpu: 'nvidia' }); }); - it('rejects Windows with AMD GPU', async () => { + it('accepts Windows with AMD GPU', async () => { vi.stubGlobal('process', { ...process, platform: 'win32' }); - // Simulate a system with an AMD GPU vi.mocked(si.graphics).mockResolvedValue({ - controllers: [{ vendor: 'AMD', model: 'Radeon RX 6800' }], + controllers: [{ vendor: 'Intel', model: 'Iris Xe' }], } as Systeminformation.GraphicsData); - vi.mock('node:child_process', async () => { - const actual = await vi.importActual('node:child_process'); - return { - ...actual, - exec: (_cmd: string, callback: (error: Error | null, stdout: string, stderr: string) => void) => { - setImmediate(() => callback(new Error('mocked exec failure'), '', '')); - return { kill: () => {}, on: () => {} } as any; - }, - }; - }); + execMock.mockImplementation((( + command: string, + callback: (error: Error | null, stdout: string, stderr: string) => void + ) => { + if (command.includes('nvidia-smi')) { + setImmediate(() => callback(new Error('mocked exec failure'), '', '')); + return createChildProcess(); + } + if (command.includes('PNPDeviceID')) { + setImmediate(() => callback(null, 'PCI\\VEN_1002&DEV_73FF\r\n', '')); + return createChildProcess(); + } + + setImmediate(() => callback(null, '', '')); + return createChildProcess(); + }) as typeof exec); + + const result = await validateHardware(); + expect(result).toStrictEqual({ isValid: true, gpu: 'amd' }); + }); + + it('rejects Windows with unsupported GPU', async () => { + vi.stubGlobal('process', { ...process, platform: 'win32' }); + vi.mocked(si.graphics).mockResolvedValue({ + controllers: [{ vendor: 'Intel', model: 'Iris Xe' }], + } as Systeminformation.GraphicsData); + + execMock.mockImplementation((( + command: string, + callback: (error: Error | null, stdout: string, stderr: string) => void + ) => { + if (command.includes('nvidia-smi')) { + setImmediate(() => callback(new Error('mocked exec failure'), '', '')); + return createChildProcess(); + } + if (command.includes('PNPDeviceID')) { + setImmediate(() => callback(null, '', '')); + return createChildProcess(); + } + + setImmediate(() => callback(null, '', '')); + return createChildProcess(); + }) as typeof exec); const result = await validateHardware(); expect(result).toStrictEqual({ isValid: false, - error: expect.stringContaining('No NVIDIA GPU was detected'), + error: expect.stringContaining('NVIDIA or AMD'), }); }); }); From 84e5eca3cbe9b536fcf4342b0c1ae6bf6be8a90a Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 16:42:17 -0800 Subject: [PATCH 02/15] Readd Wmi detection for nvidia, refactor --- src/utils.ts | 73 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 18bf24b74..310e1b5ea 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,7 @@ import net from 'node:net'; import path from 'node:path'; import { promisify } from 'node:util'; import si from 'systeminformation'; +import type { Systeminformation } from 'systeminformation'; import type { GpuType } from './preload'; @@ -126,10 +127,13 @@ export async function rotateLogFiles(logDir: string, baseName: string, maxFiles } const execAsync = promisify(exec); -const AMD_WMI_QUERY = +const WMI_PNP_DEVICE_ID_QUERY = 'powershell.exe -c "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; const AMD_VENDOR_ID = '1002'; +const NVIDIA_VENDOR_ID = '10DE'; const PCI_VENDOR_ID_REGEX = /ven_([\da-f]{4})/i; +const VENDOR_ID_REGEX = /([\da-f]{4})/i; +type WindowsGpuType = Extract; /** * Checks whether a PNPDeviceID contains the specified PCI vendor ID. @@ -142,6 +146,35 @@ function hasPciVendorId(pnpDeviceId: string, vendorId: string): boolean { return match?.[1]?.toUpperCase() === vendorId.toUpperCase(); } +function normalizeVendorId(value?: string): string | undefined { + if (!value) return undefined; + const match = value.match(VENDOR_ID_REGEX); + return match?.[1]?.toUpperCase(); +} + +function getWindowsGpuFromController(controller: Systeminformation.GraphicsControllerData): WindowsGpuType | undefined { + const vendorId = normalizeVendorId(controller.vendorId); + if (vendorId === NVIDIA_VENDOR_ID) return 'nvidia'; + if (vendorId === AMD_VENDOR_ID) return 'amd'; + + const details = [controller.vendor, controller.model, controller.name, controller.subVendor] + .filter(Boolean) + .join(' ') + .toLowerCase(); + + if (details.includes('nvidia')) return 'nvidia'; + if (details.includes('amd') || details.includes('radeon') || details.includes('advanced micro devices')) return 'amd'; + return undefined; +} + +function getWindowsGpuFromGraphics(graphics: Systeminformation.GraphicsData): WindowsGpuType | undefined { + for (const controller of graphics.controllers) { + const detected = getWindowsGpuFromController(controller); + if (detected) return detected; + } + return undefined; +} + /** * Detects NVIDIA GPUs on Windows using nvidia-smi. * @return `true` if nvidia-smi executes successfully. @@ -156,12 +189,13 @@ async function hasNvidiaGpuViaSmi(): Promise { } /** - * Detects AMD GPUs on Windows by parsing PNPDeviceID values from CIM. - * @return `true` if an AMD GPU vendor ID is detected. + * Detects GPUs on Windows by parsing PNPDeviceID values from CIM. + * @param vendorId The PCI vendor ID to match (hex). + * @return `true` if the vendor ID is detected. */ -async function hasAmdGpuViaWmi(): Promise { +async function hasGpuViaWmi(vendorId: string): Promise { try { - const res = await execAsync(AMD_WMI_QUERY); + const res = await execAsync(WMI_PNP_DEVICE_ID_QUERY); const stdout = res?.stdout?.trim(); if (!stdout) return false; @@ -169,12 +203,28 @@ async function hasAmdGpuViaWmi(): Promise { .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) - .some((pnpDeviceId) => hasPciVendorId(pnpDeviceId, AMD_VENDOR_ID)); + .some((pnpDeviceId) => hasPciVendorId(pnpDeviceId, vendorId)); } catch { return false; } } +/** + * Detects AMD GPUs on Windows by parsing PNPDeviceID values from CIM. + * @return `true` if an AMD GPU vendor ID is detected. + */ +async function hasAmdGpuViaWmi(): Promise { + return hasGpuViaWmi(AMD_VENDOR_ID); +} + +/** + * Detects NVIDIA GPUs on Windows by parsing PNPDeviceID values from CIM. + * @return `true` if an NVIDIA GPU vendor ID is detected. + */ +async function hasNvidiaGpuViaWmi(): Promise { + return hasGpuViaWmi(NVIDIA_VENDOR_ID); +} + interface HardwareValidation { isValid: boolean; /** The detected GPU (not guaranteed to be valid - check isValid) */ @@ -207,20 +257,19 @@ export async function validateHardware(): Promise { // Windows GPU validation if (process.platform === 'win32') { const graphics = await si.graphics(); - const hasNvidia = graphics.controllers.some((controller) => { - const details = `${controller.vendor ?? ''} ${controller.model ?? ''}`.toLowerCase(); - return details.includes('nvidia'); - }); + const detectedGpu = getWindowsGpuFromGraphics(graphics); if (process.env.SKIP_HARDWARE_VALIDATION) { console.log('Skipping hardware validation'); - if (hasNvidia) return { isValid: true, gpu: 'nvidia' }; + if (detectedGpu) return { isValid: true, gpu: detectedGpu }; + if (await hasNvidiaGpuViaWmi()) return { isValid: true, gpu: 'nvidia' }; if (await hasAmdGpuViaWmi()) return { isValid: true, gpu: 'amd' }; return { isValid: true }; } - if (hasNvidia) return { isValid: true, gpu: 'nvidia' }; + if (detectedGpu) return { isValid: true, gpu: detectedGpu }; + if (await hasNvidiaGpuViaWmi()) return { isValid: true, gpu: 'nvidia' }; if (await hasNvidiaGpuViaSmi()) return { isValid: true, gpu: 'nvidia' }; if (await hasAmdGpuViaWmi()) return { isValid: true, gpu: 'amd' }; From d32ebb9c46aee647dff268d0ee262129994be348 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 16:48:39 -0800 Subject: [PATCH 03/15] Extract VENDOR_IDs to constants --- src/constants.ts | 3 +++ src/utils.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 8bef67b56..0d3b3c816 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -119,6 +119,9 @@ export const ELECTRON_BRIDGE_API = 'electronAPI'; export const SENTRY_URL_ENDPOINT = 'https://942cadba58d247c9cab96f45221aa813@o4507954455314432.ingest.us.sentry.io/4508007940685824'; +export const AMD_VENDOR_ID = '1002'; +export const NVIDIA_VENDOR_ID = '10DE'; + export interface MigrationItem { id: string; label: string; diff --git a/src/utils.ts b/src/utils.ts index 310e1b5ea..e802a44ca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,6 +9,7 @@ import { promisify } from 'node:util'; import si from 'systeminformation'; import type { Systeminformation } from 'systeminformation'; +import { AMD_VENDOR_ID, NVIDIA_VENDOR_ID } from './constants'; import type { GpuType } from './preload'; export async function pathAccessible(path: string): Promise { @@ -129,8 +130,6 @@ export async function rotateLogFiles(logDir: string, baseName: string, maxFiles const execAsync = promisify(exec); const WMI_PNP_DEVICE_ID_QUERY = 'powershell.exe -c "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; -const AMD_VENDOR_ID = '1002'; -const NVIDIA_VENDOR_ID = '10DE'; const PCI_VENDOR_ID_REGEX = /ven_([\da-f]{4})/i; const VENDOR_ID_REGEX = /([\da-f]{4})/i; type WindowsGpuType = Extract; From f259ceb04ae2deac5ddb4ab190d3fa64219bf601 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 16:52:50 -0800 Subject: [PATCH 04/15] Add -NoProfile -NonInteractive to WMI_PNP_DEVICE_ID_QUERY --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index e802a44ca..0f0ddd544 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -129,7 +129,7 @@ export async function rotateLogFiles(logDir: string, baseName: string, maxFiles const execAsync = promisify(exec); const WMI_PNP_DEVICE_ID_QUERY = - 'powershell.exe -c "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; + 'powershell.exe -NoProfile -NonInteractive -Command "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; const PCI_VENDOR_ID_REGEX = /ven_([\da-f]{4})/i; const VENDOR_ID_REGEX = /([\da-f]{4})/i; type WindowsGpuType = Extract; From b45b01d9043db936fa7581fcef698dd00e7ac186 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 17:26:17 -0800 Subject: [PATCH 05/15] Switch Wmi querying to JSON output and force UTF-8 --- src/utils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 0f0ddd544..a3b7716a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -129,7 +129,7 @@ export async function rotateLogFiles(logDir: string, baseName: string, maxFiles const execAsync = promisify(exec); const WMI_PNP_DEVICE_ID_QUERY = - 'powershell.exe -NoProfile -NonInteractive -Command "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID"'; + 'powershell.exe -NoProfile -NonInteractive -Command "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty PNPDeviceID | ConvertTo-Json -Compress"'; const PCI_VENDOR_ID_REGEX = /ven_([\da-f]{4})/i; const VENDOR_ID_REGEX = /([\da-f]{4})/i; type WindowsGpuType = Extract; @@ -198,11 +198,14 @@ async function hasGpuViaWmi(vendorId: string): Promise { const stdout = res?.stdout?.trim(); if (!stdout) return false; - return stdout - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .some((pnpDeviceId) => hasPciVendorId(pnpDeviceId, vendorId)); + const parsed = JSON.parse(stdout) as unknown; + const pnpDeviceIds: string[] = Array.isArray(parsed) + ? parsed.filter((entry): entry is string => typeof entry === 'string') + : typeof parsed === 'string' + ? [parsed] + : []; + + return pnpDeviceIds.some((pnpDeviceId) => hasPciVendorId(pnpDeviceId, vendorId)); } catch { return false; } From e938ef4cb20bc5b8d4c89b231ff211b9052b2bda Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 17:52:27 -0800 Subject: [PATCH 06/15] Add compiled requirements and overrides --- README.md | 6 +- assets/override_amd.txt | 4 + assets/requirements/amd_requirements.txt | 5 + assets/requirements/windows_amd.compiled | 356 +++++++++++++++++++++++ src/virtualEnvironment.ts | 6 - 5 files changed, 370 insertions(+), 7 deletions(-) create mode 100644 assets/override_amd.txt create mode 100644 assets/requirements/amd_requirements.txt create mode 100644 assets/requirements/windows_amd.compiled diff --git a/README.md b/README.md index 7ec0fc0ea..7be201fea 100644 --- a/README.md +++ b/README.md @@ -196,11 +196,15 @@ You can generate the compiled requirements files by running the following comman #### Windows ```powershell -## Nvidia Cuda requirements +## NVIDIA CUDA requirements uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_nvidia.compiled --override assets\override.txt ` --index-url https://pypi.org/simple ` --extra-index-url https://download.pytorch.org/whl/cu129 +## AMD ROCm requirements +uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt assets\requirements\amd_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_amd.compiled --override assets\override_amd.txt ` +--index-url https://pypi.org/simple + ## CPU requirements uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_cpu.compiled ` --index-url https://pypi.org/simple diff --git a/assets/override_amd.txt b/assets/override_amd.txt new file mode 100644 index 000000000..ae3ed9cc0 --- /dev/null +++ b/assets/override_amd.txt @@ -0,0 +1,4 @@ +# ensure usage of AMD ROCm PyTorch wheels +torch @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torch-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl +torchaudio @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchaudio-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl +torchvision @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchvision-0.24.0+rocmsdk20251116-cp312-cp312-win_amd64.whl diff --git a/assets/requirements/amd_requirements.txt b/assets/requirements/amd_requirements.txt new file mode 100644 index 000000000..2ddb8afd1 --- /dev/null +++ b/assets/requirements/amd_requirements.txt @@ -0,0 +1,5 @@ +# AMD ROCm SDK packages for Windows +rocm-sdk-core @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_core-0.1.dev0-py3-none-win_amd64.whl +rocm-sdk-devel @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_devel-0.1.dev0-py3-none-win_amd64.whl +rocm-sdk-libraries-custom @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_libraries_custom-0.1.dev0-py3-none-win_amd64.whl +rocm @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm-0.1.dev0.tar.gz diff --git a/assets/requirements/windows_amd.compiled b/assets/requirements/windows_amd.compiled new file mode 100644 index 000000000..7fdb03c19 --- /dev/null +++ b/assets/requirements/windows_amd.compiled @@ -0,0 +1,356 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile assets/ComfyUI/requirements.txt assets/ComfyUI/manager_requirements.txt assets/requirements/amd_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match --python-platform x86_64-pc-windows-msvc --python-version 3.12 --override assets/override_amd.txt --index-url https://pypi.org/simple -o assets/requirements/windows_amd.compiled +--index-url https://pypi.org/simple + +aiohappyeyeballs==2.6.1 + # via aiohttp + # from https://pypi.org/simple +aiohttp==3.13.2 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +aiosignal==1.4.0 + # via aiohttp + # from https://pypi.org/simple +alembic==1.17.2 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +annotated-types==0.7.0 + # via pydantic + # from https://pypi.org/simple +attrs==25.4.0 + # via aiohttp + # from https://pypi.org/simple +av==16.0.1 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +certifi==2025.11.12 + # via requests + # from https://pypi.org/simple +cffi==2.0.0 + # via + # cryptography + # pynacl + # from https://pypi.org/simple +chardet==5.2.0 + # via comfyui-manager + # from https://pypi.org/simple +charset-normalizer==3.4.4 + # via requests + # from https://pypi.org/simple +click==8.3.1 + # via typer + # from https://pypi.org/simple +colorama==0.4.6 + # via + # click + # tqdm + # from https://pypi.org/simple +comfyui-embedded-docs==0.3.1 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +comfyui-manager==4.0.3b5 + # via -r assets/ComfyUI/manager_requirements.txt + # from https://pypi.org/simple +comfyui-workflow-templates==0.7.59 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +comfyui-workflow-templates-core==0.3.43 + # via comfyui-workflow-templates + # from https://pypi.org/simple +comfyui-workflow-templates-media-api==0.3.22 + # via comfyui-workflow-templates + # from https://pypi.org/simple +comfyui-workflow-templates-media-image==0.3.36 + # via comfyui-workflow-templates + # from https://pypi.org/simple +comfyui-workflow-templates-media-other==0.3.47 + # via comfyui-workflow-templates + # from https://pypi.org/simple +comfyui-workflow-templates-media-video==0.3.19 + # via comfyui-workflow-templates + # from https://pypi.org/simple +cryptography==46.0.3 + # via pyjwt + # from https://pypi.org/simple +einops==0.8.1 + # via + # -r assets/ComfyUI/requirements.txt + # spandrel + # from https://pypi.org/simple +filelock==3.20.1 + # via + # huggingface-hub + # torch + # transformers + # from https://pypi.org/simple +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal + # from https://pypi.org/simple +fsspec==2025.12.0 + # via + # huggingface-hub + # torch + # from https://pypi.org/simple +gitdb==4.0.12 + # via gitpython + # from https://pypi.org/simple +gitpython==3.1.45 + # via comfyui-manager + # from https://pypi.org/simple +greenlet==3.3.0 + # via sqlalchemy + # from https://pypi.org/simple +hf-xet==1.2.0 + # via huggingface-hub + # from https://pypi.org/simple +huggingface-hub==0.36.0 + # via + # comfyui-manager + # tokenizers + # transformers + # from https://pypi.org/simple +idna==3.11 + # via + # requests + # yarl + # from https://pypi.org/simple +jinja2==3.1.6 + # via torch + # from https://pypi.org/simple +kornia==0.8.2 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +kornia-rs==0.1.10 + # via kornia + # from https://pypi.org/simple +mako==1.3.10 + # via alembic + # from https://pypi.org/simple +markdown-it-py==4.0.0 + # via rich + # from https://pypi.org/simple +markupsafe==3.0.3 + # via + # jinja2 + # mako + # from https://pypi.org/simple +mdurl==0.1.2 + # via markdown-it-py + # from https://pypi.org/simple +mpmath==1.3.0 + # via sympy + # from https://pypi.org/simple +multidict==6.7.0 + # via + # aiohttp + # yarl + # from https://pypi.org/simple +networkx==3.6.1 + # via torch + # from https://pypi.org/simple +numpy==2.4.0 + # via + # -r assets/ComfyUI/requirements.txt + # scipy + # spandrel + # torchsde + # torchvision + # transformers + # from https://pypi.org/simple +packaging==25.0 + # via + # huggingface-hub + # kornia + # transformers + # from https://pypi.org/simple +pillow==12.0.0 + # via + # -r assets/ComfyUI/requirements.txt + # torchvision + # from https://pypi.org/simple +propcache==0.4.1 + # via + # aiohttp + # yarl + # from https://pypi.org/simple +psutil==7.2.1 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +pycparser==2.23 + # via cffi + # from https://pypi.org/simple +pydantic==2.12.5 + # via + # -r assets/ComfyUI/requirements.txt + # pydantic-settings + # from https://pypi.org/simple +pydantic-core==2.41.5 + # via pydantic + # from https://pypi.org/simple +pydantic-settings==2.12.0 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +pygithub==2.8.1 + # via comfyui-manager + # from https://pypi.org/simple +pygments==2.19.2 + # via rich + # from https://pypi.org/simple +pyjwt==2.10.1 + # via pygithub + # from https://pypi.org/simple +pynacl==1.6.1 + # via pygithub + # from https://pypi.org/simple +python-dotenv==1.2.1 + # via pydantic-settings + # from https://pypi.org/simple +pyyaml==6.0.3 + # via + # -r assets/ComfyUI/requirements.txt + # huggingface-hub + # transformers + # from https://pypi.org/simple +regex==2025.11.3 + # via transformers + # from https://pypi.org/simple +requests==2.32.5 + # via + # huggingface-hub + # pygithub + # transformers + # from https://pypi.org/simple +rich==14.2.0 + # via + # comfyui-manager + # typer + # from https://pypi.org/simple +rocm @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm-0.1.dev0.tar.gz + # via + # -r assets/requirements/amd_requirements.txt + # torch +rocm-sdk-core @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_core-0.1.dev0-py3-none-win_amd64.whl + # via + # -r assets/requirements/amd_requirements.txt + # rocm +rocm-sdk-devel @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_devel-0.1.dev0-py3-none-win_amd64.whl + # via -r assets/requirements/amd_requirements.txt +rocm-sdk-libraries-custom @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/rocm_sdk_libraries_custom-0.1.dev0-py3-none-win_amd64.whl + # via + # -r assets/requirements/amd_requirements.txt + # rocm +safetensors==0.7.0 + # via + # -r assets/ComfyUI/requirements.txt + # spandrel + # transformers + # from https://pypi.org/simple +scipy==1.16.3 + # via + # -r assets/ComfyUI/requirements.txt + # torchsde + # from https://pypi.org/simple +sentencepiece==0.2.1 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +setuptools==80.9.0 + # via torch + # from https://pypi.org/simple +shellingham==1.5.4 + # via typer + # from https://pypi.org/simple +smmap==5.0.2 + # via gitdb + # from https://pypi.org/simple +spandrel==0.4.1 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +sqlalchemy==2.0.45 + # via + # -r assets/ComfyUI/requirements.txt + # alembic + # from https://pypi.org/simple +sympy==1.14.0 + # via torch + # from https://pypi.org/simple +tokenizers==0.22.1 + # via + # -r assets/ComfyUI/requirements.txt + # transformers + # from https://pypi.org/simple +toml==0.10.2 + # via comfyui-manager + # from https://pypi.org/simple +torch @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torch-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl + # via + # --override assets/override_amd.txt + # -r assets/ComfyUI/requirements.txt + # kornia + # spandrel + # torchaudio + # torchsde + # torchvision +torchaudio @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchaudio-2.9.0+rocmsdk20251116-cp312-cp312-win_amd64.whl + # via + # --override assets/override_amd.txt + # -r assets/ComfyUI/requirements.txt +torchsde==0.2.6 + # via -r assets/ComfyUI/requirements.txt + # from https://pypi.org/simple +torchvision @ https://repo.radeon.com/rocm/windows/rocm-rel-7.1.1/torchvision-0.24.0+rocmsdk20251116-cp312-cp312-win_amd64.whl + # via + # --override assets/override_amd.txt + # -r assets/ComfyUI/requirements.txt + # spandrel +tqdm==4.67.1 + # via + # -r assets/ComfyUI/requirements.txt + # huggingface-hub + # transformers + # from https://pypi.org/simple +trampoline==0.1.2 + # via torchsde + # from https://pypi.org/simple +transformers==4.57.3 + # via + # -r assets/ComfyUI/requirements.txt + # comfyui-manager + # from https://pypi.org/simple +typer==0.21.0 + # via comfyui-manager + # from https://pypi.org/simple +typing-extensions==4.15.0 + # via + # aiosignal + # alembic + # comfyui-manager + # huggingface-hub + # pydantic + # pydantic-core + # pygithub + # spandrel + # sqlalchemy + # torch + # typer + # typing-inspection + # from https://pypi.org/simple +typing-inspection==0.4.2 + # via + # pydantic + # pydantic-settings + # from https://pypi.org/simple +urllib3==2.6.2 + # via + # pygithub + # requests + # from https://pypi.org/simple +uv==0.9.20 + # via comfyui-manager + # from https://pypi.org/simple +yarl==1.22.0 + # via + # -r assets/ComfyUI/requirements.txt + # aiohttp + # from https://pypi.org/simple diff --git a/src/virtualEnvironment.ts b/src/virtualEnvironment.ts index eb5c33537..a4a8d0fbc 100644 --- a/src/virtualEnvironment.ts +++ b/src/virtualEnvironment.ts @@ -379,12 +379,6 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor { public async installRequirements(callbacks?: ProcessCallbacks): Promise { useAppState().setInstallStage(createInstallStageInfo(InstallStage.INSTALLING_REQUIREMENTS, { progress: 25 })); - if (this.selectedDevice === 'amd') { - log.info('Skipping compiled requirements for AMD GPU; using manual install.'); - await this.manualInstall(callbacks); - return; - } - const installCmd = getPipInstallArgs({ requirementsFile: this.requirementsCompiledPath, indexStrategy: 'unsafe-best-match', From 6b6b95e41e58e855aba3f27850f04f0e0227f122 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 18:10:41 -0800 Subject: [PATCH 07/15] Fix test failues --- tests/unit/utils.test.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 1b59f5dea..4a5c25b71 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -56,26 +56,9 @@ describe('validateHardware', () => { it('accepts Windows with AMD GPU', async () => { vi.stubGlobal('process', { ...process, platform: 'win32' }); vi.mocked(si.graphics).mockResolvedValue({ - controllers: [{ vendor: 'Intel', model: 'Iris Xe' }], + controllers: [{ vendorId: '1002', vendor: 'AMD' }], } as Systeminformation.GraphicsData); - execMock.mockImplementation((( - command: string, - callback: (error: Error | null, stdout: string, stderr: string) => void - ) => { - if (command.includes('nvidia-smi')) { - setImmediate(() => callback(new Error('mocked exec failure'), '', '')); - return createChildProcess(); - } - if (command.includes('PNPDeviceID')) { - setImmediate(() => callback(null, 'PCI\\VEN_1002&DEV_73FF\r\n', '')); - return createChildProcess(); - } - - setImmediate(() => callback(null, '', '')); - return createChildProcess(); - }) as typeof exec); - const result = await validateHardware(); expect(result).toStrictEqual({ isValid: true, gpu: 'amd' }); }); @@ -95,7 +78,7 @@ describe('validateHardware', () => { return createChildProcess(); } if (command.includes('PNPDeviceID')) { - setImmediate(() => callback(null, '', '')); + setImmediate(() => callback(null, '["PCI\\\\VEN_8086&DEV_46A6"]\r\n', '')); return createChildProcess(); } From 9e5e5cfaa6d6463b0de528e2a4de83ae481bb214 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 18:20:04 -0800 Subject: [PATCH 08/15] README Compiled Requirements tweak --- README.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/README.md b/README.md index 7be201fea..6eb7f8a57 100644 --- a/README.md +++ b/README.md @@ -189,36 +189,7 @@ yarn make --windows ### Compiled Requirements -The `requirements.compiled` file is a pre-compiled requirements file that is used to install the dependencies for the application. It is generated by the `comfy-cli`. - -You can generate the compiled requirements files by running the following commands from the project root directory: - -#### Windows - -```powershell -## NVIDIA CUDA requirements -uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_nvidia.compiled --override assets\override.txt ` ---index-url https://pypi.org/simple ` ---extra-index-url https://download.pytorch.org/whl/cu129 - -## AMD ROCm requirements -uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt assets\requirements\amd_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_amd.compiled --override assets\override_amd.txt ` ---index-url https://pypi.org/simple - -## CPU requirements -uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_cpu.compiled ` ---index-url https://pypi.org/simple -``` - -#### macOS - -```bash -## macOS requirements -uv pip compile assets/ComfyUI/requirements.txt assets/ComfyUI/manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets/requirements/macos.compiled --override assets/override.txt \ ---index-url https://pypi.org/simple -``` - -If you are working with a legacy ComfyUI bundle that does not include `manager_requirements.txt`, replace that path in the commands above with `assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt`. +The source of truth for how compiled requirements are produced is in the header comments of the `.compiled` files. See `assets/requirements/*.compiled` for the exact commands and overrides in use. ### Troubleshooting From 1cbf60e9882f906ae5284771a28d6a91bfaef1dc Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 18:38:08 -0800 Subject: [PATCH 09/15] README fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6eb7f8a57..9aa436262 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please read the [user guide](https://docs.comfy.org/installation/desktop) # Download -Windows (NVIDIA) NSIS x64: [Download](https://download.comfy.org/windows/nsis/x64) +Windows (NVIDIA/AMD) NSIS x64: [Download](https://download.comfy.org/windows/nsis/x64) macOS ARM: [Download](https://download.comfy.org/mac/dmg/arm64) From 36d3e7552102d70fcb0d03a1b9e7be9adcc9792d Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 21:53:47 -0800 Subject: [PATCH 10/15] Switch to temporary frontend branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7f14245d..c2499c2c2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "config": { "frontend": { "version": "1.34.9", - "optionalBranch": "" + "optionalBranch": "bl/amd-gpu-picker" }, "comfyUI": { "version": "0.6.0", From 49bba0956879b816a882491e7b428aa7b693d789 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 22:52:59 -0800 Subject: [PATCH 11/15] Improve logging --- scripts/downloadFrontend.js | 44 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/scripts/downloadFrontend.js b/scripts/downloadFrontend.js index 6d6cf4093..aae81f1fd 100644 --- a/scripts/downloadFrontend.js +++ b/scripts/downloadFrontend.js @@ -34,7 +34,7 @@ if (frontend.optionalBranch) { await fs.cp(path.join(frontendDir, 'dist'), 'assets/ComfyUI/web_custom_versions/desktop_app', { recursive: true }); await fs.rm(frontendDir, { recursive: true }); } catch (error) { - console.error('Error building frontend:', error.message); + console.error('Error building frontend:', error instanceof Error ? error.message : String(error)); process.exit(1); } @@ -45,12 +45,42 @@ if (frontend.optionalBranch) { * @param {Record} env Additional environment variables. */ function execAndLog(command, cwd, env = {}) { - const output = execSync(command, { - cwd, - encoding: 'utf8', - env: { ...process.env, ...env }, - }); - console.log(output); + try { + const output = execSync(command, { + cwd, + encoding: 'utf8', + env: { ...process.env, ...env }, + }); + console.log(output); + } catch (error) { + console.error(`Command failed: ${command}`); + logExecErrorOutput(error); + throw error; + } + } + + /** + * Log stdout/stderr for exec failures when available. + * @param {unknown} error The error thrown by execSync. + */ + function logExecErrorOutput(error) { + if (!error || typeof error !== 'object') { + return; + } + + const execError = /** @type {{ stdout?: { toString?: () => string }, stderr?: { toString?: () => string } }} */ ( + error + ); + const stdoutText = execError.stdout?.toString?.(); + const stderrText = execError.stderr?.toString?.(); + + if (stdoutText) { + console.error(stdoutText); + } + + if (stderrText) { + console.error(stderrText); + } } } else { // Download normal frontend release zip From 5b957d9268176c59d1d841470eeb671eda51611f Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 29 Dec 2025 23:23:37 -0800 Subject: [PATCH 12/15] Don't typecheck during build --- scripts/downloadFrontend.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/downloadFrontend.js b/scripts/downloadFrontend.js index aae81f1fd..140fa1edd 100644 --- a/scripts/downloadFrontend.js +++ b/scripts/downloadFrontend.js @@ -29,7 +29,12 @@ if (frontend.optionalBranch) { try { execAndLog(`git clone ${frontendRepo} --depth 1 --branch ${frontend.optionalBranch} ${frontendDir}`); execAndLog(`pnpm install --frozen-lockfile`, frontendDir, { COREPACK_ENABLE_STRICT: '0' }); - execAndLog(`pnpm run build`, frontendDir, { COREPACK_ENABLE_STRICT: '0', DISTRIBUTION: 'desktop' }); + // Run the build directly to avoid test-only typecheck failures. + execAndLog(`pnpm exec nx build`, frontendDir, { + COREPACK_ENABLE_STRICT: '0', + DISTRIBUTION: 'desktop', + NODE_OPTIONS: '--max-old-space-size=8192', + }); await fs.mkdir('assets/ComfyUI/web_custom_versions/desktop_app', { recursive: true }); await fs.cp(path.join(frontendDir, 'dist'), 'assets/ComfyUI/web_custom_versions/desktop_app', { recursive: true }); await fs.rm(frontendDir, { recursive: true }); From d9f0094ea665fd09f683c21234a10fc56c9af787 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 30 Dec 2025 12:35:31 -0800 Subject: [PATCH 13/15] Remove AMD from README for now --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aa436262..6eb7f8a57 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Please read the [user guide](https://docs.comfy.org/installation/desktop) # Download -Windows (NVIDIA/AMD) NSIS x64: [Download](https://download.comfy.org/windows/nsis/x64) +Windows (NVIDIA) NSIS x64: [Download](https://download.comfy.org/windows/nsis/x64) macOS ARM: [Download](https://download.comfy.org/mac/dmg/arm64) From e26caac2784c7d3d901084975f742a7bc49afdd5 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 30 Dec 2025 12:37:35 -0800 Subject: [PATCH 14/15] Remove testing FE branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2499c2c2..d7f14245d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "config": { "frontend": { "version": "1.34.9", - "optionalBranch": "bl/amd-gpu-picker" + "optionalBranch": "" }, "comfyUI": { "version": "0.6.0", From ad5940ca1d9e985d5186a0c9f1e477d248b58e74 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 30 Dec 2025 12:46:08 -0800 Subject: [PATCH 15/15] Preoptimize ugly test --- tests/unit/utils.test.ts | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 4a5c25b71..6a4fdee1a 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -2,7 +2,7 @@ import type { ChildProcess } from 'node:child_process'; import { exec } from 'node:child_process'; import type { Systeminformation } from 'systeminformation'; import si from 'systeminformation'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { validateHardware } from '@/utils'; @@ -19,10 +19,28 @@ const createChildProcess = (): ChildProcess => on: vi.fn(), }) as unknown as ChildProcess; +type ExecResponse = { error?: Error | null; stdout?: string; stderr?: string }; + +const withExecResponses = (responses: Array<[RegExp, ExecResponse]>, fallback: ExecResponse = {}) => { + execMock.mockImplementation((( + command: string, + callback: (error: Error | null, stdout: string, stderr: string) => void + ) => { + const match = responses.find(([pattern]) => pattern.test(command)); + const { error = null, stdout = '', stderr = '' } = match?.[1] ?? fallback; + setImmediate(() => callback(error ?? null, stdout, stderr)); + return createChildProcess(); + }) as typeof exec); +}; + beforeEach(() => { execMock.mockReset(); }); +afterEach(() => { + vi.unstubAllGlobals(); +}); + describe('validateHardware', () => { it('accepts Apple Silicon Mac', async () => { vi.stubGlobal('process', { ...process, platform: 'darwin' }); @@ -69,22 +87,10 @@ describe('validateHardware', () => { controllers: [{ vendor: 'Intel', model: 'Iris Xe' }], } as Systeminformation.GraphicsData); - execMock.mockImplementation((( - command: string, - callback: (error: Error | null, stdout: string, stderr: string) => void - ) => { - if (command.includes('nvidia-smi')) { - setImmediate(() => callback(new Error('mocked exec failure'), '', '')); - return createChildProcess(); - } - if (command.includes('PNPDeviceID')) { - setImmediate(() => callback(null, '["PCI\\\\VEN_8086&DEV_46A6"]\r\n', '')); - return createChildProcess(); - } - - setImmediate(() => callback(null, '', '')); - return createChildProcess(); - }) as typeof exec); + withExecResponses([ + [/nvidia-smi/, { error: new Error('mocked exec failure') }], + [/PNPDeviceID/, { stdout: '["PCI\\\\VEN_8086&DEV_46A6"]\r\n' }], + ]); const result = await validateHardware(); expect(result).toStrictEqual({