Skip to content
Open
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
34 changes: 34 additions & 0 deletions src/main/lib/gpu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
parseRocmSmiDriverVersion,
parseWmiDriverVersions,
checkLinuxAmdKfdAccess,
validateHardware,
type SystemGpuEntry
} from './gpu'

Expand Down Expand Up @@ -281,3 +282,36 @@ describe('checkLinuxAmdKfdAccess', () => {
expect(await checkLinuxAmdKfdAccess(node)).toBeNull()
})
})

describe('validateHardware architecture gate', () => {
const realPlatform = process.platform
const realArch = process.arch
const setHost = (platform: string, arch: string): void => {
Object.defineProperty(process, 'platform', { value: platform })
Object.defineProperty(process, 'arch', { value: arch })
}

afterEach(() => {
setHost(realPlatform, realArch)
})

// Without this the wizard filters every bundle out and shows a bare "No
// options available", which the user cannot tell from an R2 outage.
it('blocks the local install on an architecture with no published bundle', async () => {
setHost('linux', 'arm64')
const result = await validateHardware()

expect(result.supported).toBe(false)
expect(result.error).toMatch(/ARM64 Linux/)
// The other half of the message: this machine is still useful.
expect(result.error).toMatch(/cloud or remote/)
})

it('does not gate an x64 Linux host on architecture', async () => {
setHost('linux', 'x64')
const result = await validateHardware()

// May still warn about the AMD compute node; it must not be blocked here.
expect(result.error).toBeUndefined()
})
})
13 changes: 11 additions & 2 deletions src/main/lib/gpu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFile } from 'child_process'
import fs from 'fs'
import type { HardwareValidation, NvidiaDriverCheck } from '../../types/ipc'
import { unsupportedHostReason } from '../sources/standalone/envPaths'

type GpuId = 'nvidia' | 'amd' | 'intel' | 'mps'

Expand Down Expand Up @@ -559,9 +560,17 @@ export async function checkLinuxAmdKfdAccess(kfdPath = KFD_PATH): Promise<string
}

/** Validate hardware for standalone install. Rejects Intel Macs (MPS needs
* Apple Silicon); surfaces a non-blocking warning when a Linux AMD GPU is
* present but its compute device node is missing or inaccessible. */
* Apple Silicon) and architectures with no published bundle; surfaces a
* non-blocking warning when a Linux AMD GPU is present but its compute
* device node is missing or inaccessible. */
async function validateHardware(): Promise<HardwareValidation> {
// Ask before probing the GPU: no bundle exists for this architecture, so
// whatever the GPU turns out to be, the local install has nothing to offer.
// Reporting it here gets the wizard's existing explain-and-block treatment
// instead of an empty release list the user cannot tell from an outage.
const unsupportedHost = unsupportedHostReason()
if (unsupportedHost) return { supported: false, error: unsupportedHost }

if (process.platform === 'darwin') {
const gpu = await detectMacGPU()
if (!gpu) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/sources/standalone/envPaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
recommendVariant,
stripPlatform,
variantAccel,
unsupportedHostReason,
variantMatchesHost,
variantMatchesHostArch
} from './envPaths'
Expand Down Expand Up @@ -210,6 +211,31 @@ describe('architecture-specific vendor ids', () => {
})
})

describe('unsupportedHostReason', () => {
// The filter and the reason have to agree: a host the filter empties out
// must have something to say, or the wizard shows a bare "no options".
const CATALOG = ['win-nvidia', 'win-nvidia-arm64', 'mac-mps', 'linux-nvidia']

it('explains the ARM64 Linux gap the architecture filter creates', () => {
setHost('linux', 'arm64')
expect(CATALOG.some(variantMatchesHost)).toBe(false)
expect(unsupportedHostReason()).toMatch(/ARM64 Linux/)
})

it('stays silent on hosts the catalog serves', () => {
for (const [platform, arch] of [
['win32', 'x64'],
['win32', 'arm64'],
['darwin', 'arm64'],
['linux', 'x64']
] as const) {
setHost(platform, arch)
expect(CATALOG.some(variantMatchesHost), `${platform}/${arch}`).toBe(true)
expect(unsupportedHostReason(), `${platform}/${arch}`).toBeNull()
}
})
})

describe('variantMatchesHost', () => {
it('accepts the beta- prefix only directly in front of the host platform', () => {
setHost('win32', 'arm64')
Expand Down
19 changes: 19 additions & 0 deletions src/main/sources/standalone/envPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ export function variantMatchesHostArch(variantId: string): boolean {
return false
}

/**
* Why the running host has no standalone bundle at all, or null when it has
* some. `variantMatchesHostArch` filters an ARM64 Linux app down to nothing,
* and an empty option list reads exactly like an R2 outage — so the reason
* lives here, next to the filter that creates it, and `validateHardware`
* turns it into the same block-and-explain the wizard already gives an Intel
* Mac. Delete this carve-out when `linux-*-arm64` bundles ship; the filter
* above accepts them with no further change.
*/
export function unsupportedHostReason(): string | null {
if (process.platform === 'linux' && process.arch === 'arm64') {
return (
'ComfyUI does not publish a local runtime for ARM64 Linux yet. ' +
'You can still connect this machine to cloud or remote workspaces.'
)
}
return null
}

/** True when a vendor id targets the running platform, with or without the
* `beta-` prefix (`win-nvidia` and `beta-win-nvidia-arm64` on Windows). */
export function variantMatchesHostPlatform(variantId: string): boolean {
Expand Down
Loading