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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: TypeScript type check
run: pnpm run typecheck:ts

- name: Pack helper tests
run: pnpm run scripts:test

test-python:
name: Python Tests (${{ matrix.os }})
runs-on: ${{ matrix.runner }}
Expand Down
8 changes: 7 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ files:

asarUnpack:
- dist/splash/**
- node_modules/koffi/**
# koffi 3.x loads ../../@koromix/koffi-${platform}-${arch} from its JS.
# Unpack that package so require() can load the .node from disk, not asar.
- node_modules/@koromix/**/*

# Also copy the .node to resources/koffi/${platform}_${arch}/ (Koffi's Electron
# lookup). Pack fails if the optional native package is missing on the builder.
afterPack: scripts/copy-koffi-native.cjs

extraResources:
- from: backend
Expand Down
24 changes: 15 additions & 9 deletions electron/win-dll-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ export function removeCwdFromDllSearchPath(): void {
return
}

const require = createRequire(import.meta.url)
const koffi = require('koffi') as {
load: (name: string) => { func: (sig: string) => (...args: unknown[]) => unknown }
}
const kernel32 = koffi.load('kernel32.dll')
const setDllDirectoryW = kernel32.func('int SetDllDirectoryW(str16)')
const ok = setDllDirectoryW('')
if (!ok) {
console.error('[LTX Desktop] SetDllDirectoryW("") failed')
try {
const require = createRequire(import.meta.url)
const koffi = require('koffi') as {
load: (name: string) => { func: (sig: string) => (...args: unknown[]) => unknown }
}
const kernel32 = koffi.load('kernel32.dll')
const setDllDirectoryW = kernel32.func('int SetDllDirectoryW(str16)')
const ok = setDllDirectoryW('')
if (!ok) {
console.error('[LTX Desktop] SetDllDirectoryW("") failed')
}
} catch (err) {
// Missing/blocked koffi.node must not take down the app. Python still
// clears CWD from its own DLL search via PY_REMOVE_CWD_FROM_DLL_SEARCH.
console.error('[LTX Desktop] Failed to clear the Windows DLL search path', err)
}
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ltx-desktop",
"version": "1.2.1",
"version": "1.2.2",
"description": "LTX-2 Video Generation - Desktop App",
"type": "module",
"main": "dist-electron/main.js",
Expand All @@ -26,6 +26,7 @@
"openapi:generate": "pnpm openapi:export && pnpm openapi:types",
"openapi:check": "pnpm openapi:generate && git diff --exit-code -- frontend/generated/backend-openapi.json frontend/generated/backend-openapi.ts",
"backend:test": "cd backend && uv sync --frozen --extra test --extra dev && uv run pytest -v --tb=short",
"scripts:test": "node --test scripts/copy-koffi-native.test.cjs",
"build": "node scripts/run-script.js scripts/local-build",
"build:skip-python": "node scripts/run-script.js scripts/local-build --skip-python",
"build:fast": "node scripts/run-script.js scripts/local-build --unpack --skip-python",
Expand Down
75 changes: 75 additions & 0 deletions scripts/copy-koffi-native.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env node
// electron-builder afterPack: copy Koffi's platform .node next to the app so
// packaged Windows (and other OS) builds can load it.
//
// koffi 3.x keeps the native addon in an optional sibling package
// (@koromix/koffi-${platform}-${arch}), not inside node_modules/koffi. Packing
// koffi/** therefore ships JS/headers only. At runtime Koffi searches
// process.resourcesPath/koffi/${platform}_${arch}/koffi.node — copy there.
// electron-builder also asarUnpacks node_modules/@koromix/** as a second path.
'use strict'

const fs = require('node:fs')
const path = require('node:path')
const { createRequire } = require('node:module')

const ARCH_NAMES = ['ia32', 'x64', 'armv7l', 'arm64', 'universal']

function archName(arch) {
if (typeof arch === 'string') {
return arch
}
return ARCH_NAMES[arch] ?? String(arch)
}

function koffiNativeDest(resourcesDir, platform, arch) {
const triplet = `${platform}_${archName(arch)}`
return path.join(resourcesDir, 'koffi', triplet, 'koffi.node')
}

function resourcesDir(context) {
const packager = context.packager
if (typeof packager.getResourcesDir === 'function') {
return packager.getResourcesDir(context.appOutDir)
}
if (context.electronPlatformName === 'darwin') {
const name = packager.appInfo.productFilename
return path.join(context.appOutDir, `${name}.app`, 'Contents', 'Resources')
}
return path.join(context.appOutDir, 'resources')
}

function resolveKoffiNativeSrc(projectDir, platform, arch) {
const pkgName = `@koromix/koffi-${platform}-${archName(arch)}`
// koffi's exports map does not include ./package.json — resolve the entry.
const fromProject = createRequire(path.join(projectDir, 'package.json'))
const koffiEntry = fromProject.resolve('koffi')
const fromKoffi = createRequire(koffiEntry)
const nativeEntry = fromKoffi.resolve(pkgName)
const src = path.join(path.dirname(nativeEntry), `${platform}_${archName(arch)}`, 'koffi.node')
if (!fs.existsSync(src)) {
throw new Error(`Koffi native binary missing at ${src} (package ${pkgName})`)
}
return src
}

async function copyKoffiNative(context) {
const platform = context.electronPlatformName
const arch = archName(context.arch)
const projectDir = context.packager.projectDir
const dest = koffiNativeDest(resourcesDir(context), platform, arch)
const src = resolveKoffiNativeSrc(projectDir, platform, arch)

fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.copyFileSync(src, dest)
console.log(`[copy-koffi-native] ${src} → ${dest}`)
}

module.exports = {
default: copyKoffiNative,
archName,
koffiNativeDest,
resourcesDir,
resolveKoffiNativeSrc,
copyKoffiNative,
}
54 changes: 54 additions & 0 deletions scripts/copy-koffi-native.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict'

const assert = require('node:assert/strict')
const path = require('node:path')
const { describe, it } = require('node:test')
const { archName, koffiNativeDest, resourcesDir } = require('./copy-koffi-native.cjs')

describe('archName', () => {
it('passes string arch through', () => {
assert.equal(archName('x64'), 'x64')
})

it('maps electron-builder Arch enum integers', () => {
assert.equal(archName(1), 'x64')
assert.equal(archName(3), 'arm64')
})
})

describe('koffiNativeDest', () => {
it('matches Koffi resourcesPath lookup win32_x64/koffi.node', () => {
assert.equal(
koffiNativeDest(path.join('app', 'resources'), 'win32', 'x64'),
path.join('app', 'resources', 'koffi', 'win32_x64', 'koffi.node'),
)
})
})

describe('resourcesDir', () => {
it('uses packager.getResourcesDir when present', () => {
const dir = resourcesDir({
appOutDir: '/out',
packager: { getResourcesDir: (appOutDir) => `${appOutDir}/R` },
})
assert.equal(dir, '/out/R')
})

it('falls back to Contents/Resources on darwin', () => {
const dir = resourcesDir({
appOutDir: '/out',
electronPlatformName: 'darwin',
packager: { appInfo: { productFilename: 'LTX Desktop' } },
})
assert.equal(dir, path.join('/out', 'LTX Desktop.app', 'Contents', 'Resources'))
})

it('falls back to resources/ on win32', () => {
const dir = resourcesDir({
appOutDir: '/out',
electronPlatformName: 'win32',
packager: { appInfo: { productFilename: 'LTX Desktop' } },
})
assert.equal(dir, path.join('/out', 'resources'))
})
})
Loading