|
@@ -36,29 +36,37 @@ export function detectDistro(): LinuxDistro {
|
|
|
return 'unknown'
|
|
return 'unknown'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Aligned with https://github.com/Almamu/linux-wallpaperengine (README + Wayland + FBOProvider gmpxx)
|
|
|
const DEPS_DEBIAN = [
|
|
const DEPS_DEBIAN = [
|
|
|
'build-essential', 'cmake', 'pkg-config',
|
|
'build-essential', 'cmake', 'pkg-config',
|
|
|
'libxrandr-dev', 'libxinerama-dev', 'libxcursor-dev', 'libxi-dev',
|
|
'libxrandr-dev', 'libxinerama-dev', 'libxcursor-dev', 'libxi-dev',
|
|
|
'libgl-dev', 'libglew-dev', 'freeglut3-dev', 'libsdl2-dev',
|
|
'libgl-dev', 'libglew-dev', 'freeglut3-dev', 'libsdl2-dev',
|
|
|
'liblz4-dev', 'libavcodec-dev', 'libavformat-dev', 'libavutil-dev', 'libswscale-dev',
|
|
'liblz4-dev', 'libavcodec-dev', 'libavformat-dev', 'libavutil-dev', 'libswscale-dev',
|
|
|
'libxxf86vm-dev', 'libglm-dev', 'libglfw3-dev',
|
|
'libxxf86vm-dev', 'libglm-dev', 'libglfw3-dev',
|
|
|
- 'libmpv-dev', 'libpulse-dev', 'libfftw3-dev'
|
|
|
|
|
|
|
+ 'libmpv-dev', 'mpv', 'libpulse-dev', 'libpulse0', 'libfftw3-dev',
|
|
|
|
|
+ 'libwayland-dev', 'wayland-protocols', 'libegl1-mesa-dev',
|
|
|
|
|
+ 'libgmp-dev'
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
+// Fedora/Nobara: do not add 'ffmpeg' — conflicts with ffmpeg-free; use ffmpeg-free-devel only
|
|
|
const DEPS_FEDORA = [
|
|
const DEPS_FEDORA = [
|
|
|
'gcc', 'g++', 'cmake', 'pkg-config',
|
|
'gcc', 'g++', 'cmake', 'pkg-config',
|
|
|
'libXrandr-devel', 'libXinerama-devel', 'libXcursor-devel', 'libXi-devel',
|
|
'libXrandr-devel', 'libXinerama-devel', 'libXcursor-devel', 'libXi-devel',
|
|
|
'mesa-libGL-devel', 'glew-devel', 'freeglut-devel', 'SDL2-devel',
|
|
'mesa-libGL-devel', 'glew-devel', 'freeglut-devel', 'SDL2-devel',
|
|
|
'lz4-devel', 'ffmpeg-free-devel',
|
|
'lz4-devel', 'ffmpeg-free-devel',
|
|
|
'libXxf86vm-devel', 'glm-devel', 'glfw-devel',
|
|
'libXxf86vm-devel', 'glm-devel', 'glfw-devel',
|
|
|
- 'mpv-devel', 'pulseaudio-libs-devel', 'fftw-devel'
|
|
|
|
|
|
|
+ 'mpv-devel', 'pulseaudio-libs-devel', 'fftw-devel',
|
|
|
|
|
+ 'wayland-devel', 'wayland-protocols-devel', 'mesa-libEGL-devel',
|
|
|
|
|
+ 'gmp-devel'
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
const DEPS_ARCH = [
|
|
const DEPS_ARCH = [
|
|
|
'base-devel', 'cmake', 'pkg-config',
|
|
'base-devel', 'cmake', 'pkg-config',
|
|
|
'glew', 'freeglut', 'sdl2', 'lz4', 'ffmpeg',
|
|
'glew', 'freeglut', 'sdl2', 'lz4', 'ffmpeg',
|
|
|
'glm', 'glfw', 'mpv', 'libpulse', 'fftw',
|
|
'glm', 'glfw', 'mpv', 'libpulse', 'fftw',
|
|
|
- 'libxrandr', 'libxinerama', 'libxcursor', 'libxi', 'libxxf86vm'
|
|
|
|
|
|
|
+ 'libxrandr', 'libxinerama', 'libxcursor', 'libxi', 'libxxf86vm',
|
|
|
|
|
+ 'wayland', 'wayland-protocols', 'mesa',
|
|
|
|
|
+ 'gmp'
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
export async function installLweDeps(win: BrowserWindow): Promise<void> {
|
|
export async function installLweDeps(win: BrowserWindow): Promise<void> {
|
|
@@ -88,7 +96,8 @@ export async function installLweDeps(win: BrowserWindow): Promise<void> {
|
|
|
break
|
|
break
|
|
|
case 'fedora':
|
|
case 'fedora':
|
|
|
pkgManager = 'dnf'
|
|
pkgManager = 'dnf'
|
|
|
- installArgs = ['install', '-y']
|
|
|
|
|
|
|
+ // --skip-unavailable: avoid failing on missing/broken repos (e.g. Cursor 403) or package renames
|
|
|
|
|
+ installArgs = ['install', '-y', '--skip-unavailable']
|
|
|
packages = DEPS_FEDORA
|
|
packages = DEPS_FEDORA
|
|
|
break
|
|
break
|
|
|
case 'arch':
|
|
case 'arch':
|
|
@@ -162,17 +171,19 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
|
|
|
const cmakeBuild = path.join(BUILD_DIR, 'build')
|
|
const cmakeBuild = path.join(BUILD_DIR, 'build')
|
|
|
fs.mkdirSync(cmakeBuild, { recursive: true })
|
|
fs.mkdirSync(cmakeBuild, { recursive: true })
|
|
|
|
|
|
|
|
- // CMake configure — disable -Werror so upstream warnings don't break the build on newer GCC
|
|
|
|
|
- send({ stage: 'building', message: 'Running cmake configure...', percentage: 25 })
|
|
|
|
|
|
|
+ // CMake configure — CEF download can take several minutes; install to /usr/local and symlink into PATH
|
|
|
|
|
+ send({ stage: 'building', message: 'Running cmake (downloading CEF if needed, may take a few minutes)...', percentage: 25 })
|
|
|
await execFileAsync('cmake', [
|
|
await execFileAsync('cmake', [
|
|
|
'..',
|
|
'..',
|
|
|
'-DCMAKE_BUILD_TYPE=Release',
|
|
'-DCMAKE_BUILD_TYPE=Release',
|
|
|
|
|
+ '-DCMAKE_INSTALL_PREFIX=/usr/local',
|
|
|
'-DCMAKE_C_FLAGS=-Wno-error',
|
|
'-DCMAKE_C_FLAGS=-Wno-error',
|
|
|
'-DCMAKE_CXX_FLAGS=-Wno-error'
|
|
'-DCMAKE_CXX_FLAGS=-Wno-error'
|
|
|
], {
|
|
], {
|
|
|
cwd: cmakeBuild,
|
|
cwd: cmakeBuild,
|
|
|
- timeout: 120_000,
|
|
|
|
|
- maxBuffer: 10 * 1024 * 1024
|
|
|
|
|
|
|
+ timeout: 600_000,
|
|
|
|
|
+ maxBuffer: 10 * 1024 * 1024,
|
|
|
|
|
+ encoding: 'utf8'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// Build
|
|
// Build
|
|
@@ -181,20 +192,39 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
|
|
|
await execFileAsync('make', ['-j', String(cores)], {
|
|
await execFileAsync('make', ['-j', String(cores)], {
|
|
|
cwd: cmakeBuild,
|
|
cwd: cmakeBuild,
|
|
|
timeout: 600_000,
|
|
timeout: 600_000,
|
|
|
- maxBuffer: 10 * 1024 * 1024
|
|
|
|
|
|
|
+ maxBuffer: 10 * 1024 * 1024,
|
|
|
|
|
+ encoding: 'utf8'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
send({ stage: 'building', message: 'Build complete.', percentage: 80 })
|
|
send({ stage: 'building', message: 'Build complete.', percentage: 80 })
|
|
|
|
|
|
|
|
- // Install — needs sudo via graphical prompt
|
|
|
|
|
|
|
+ // Install — needs sudo via graphical prompt (binary ends up at /usr/local/linux-wallpaperengine)
|
|
|
send({ stage: 'installing', message: 'Installing (sudo required)...', percentage: 85 })
|
|
send({ stage: 'installing', message: 'Installing (sudo required)...', percentage: 85 })
|
|
|
|
|
|
|
|
const installTool = isCommandAvailable('pkexec') ? 'pkexec' : 'sudo'
|
|
const installTool = isCommandAvailable('pkexec') ? 'pkexec' : 'sudo'
|
|
|
- await execFileAsync(installTool, ['make', 'install'], {
|
|
|
|
|
- cwd: cmakeBuild,
|
|
|
|
|
- timeout: 120_000
|
|
|
|
|
|
|
+ // pkexec often runs the command in a different cwd (e.g. / or root's home), so run install
|
|
|
|
|
+ // inside a shell that cd's to the build dir first; PATH preserved for make
|
|
|
|
|
+ const pathEnv = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin'
|
|
|
|
|
+ const installScript = `cd ${JSON.stringify(cmakeBuild)} && export PATH=${JSON.stringify(pathEnv)} && make install`
|
|
|
|
|
+ await execFileAsync(installTool, ['bash', '-c', installScript], {
|
|
|
|
|
+ timeout: 120_000,
|
|
|
|
|
+ maxBuffer: 2 * 1024 * 1024,
|
|
|
|
|
+ encoding: 'utf8'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ // Symlink binary into PATH so `linux-wallpaperengine` is found
|
|
|
|
|
+ const binaryPath = '/usr/local/linux-wallpaperengine'
|
|
|
|
|
+ const linkPath = '/usr/local/bin/linux-wallpaperengine'
|
|
|
|
|
+ try {
|
|
|
|
|
+ await execFileAsync(installTool, ['ln', '-sf', binaryPath, linkPath], {
|
|
|
|
|
+ timeout: 10_000,
|
|
|
|
|
+ encoding: 'utf8',
|
|
|
|
|
+ maxBuffer: 65536
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // Non-fatal: user may already have it in PATH or can add /usr/local to PATH
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Cleanup build dir
|
|
// Cleanup build dir
|
|
|
fs.rmSync(BUILD_DIR, { recursive: true, force: true })
|
|
fs.rmSync(BUILD_DIR, { recursive: true, force: true })
|
|
|
|
|
|
|
@@ -218,55 +248,160 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
|
|
|
}
|
|
}
|
|
|
} catch { /* ignore cleanup errors */ }
|
|
} catch { /* ignore cleanup errors */ }
|
|
|
|
|
|
|
|
- // Extract a short, useful error from potentially huge compiler output
|
|
|
|
|
- const fullMsg = (err as Error).message ?? String(err)
|
|
|
|
|
- const stderr = (err as { stderr?: string }).stderr
|
|
|
|
|
- const errorSource = stderr || fullMsg
|
|
|
|
|
|
|
+ // Capture both stdout and stderr (Node provides these on execFile error when encoding/maxBuffer set)
|
|
|
|
|
+ const e = err as Error & { stdout?: string; stderr?: string }
|
|
|
|
|
+ const fullMsg = e.message ?? String(err)
|
|
|
|
|
+ const combined = [e.stdout, e.stderr].filter(Boolean).join('\n')
|
|
|
|
|
+ const lines = (combined || fullMsg).split('\n')
|
|
|
|
|
+
|
|
|
|
|
+ // 1. Real compiler/linker errors (highest priority)
|
|
|
|
|
+ const compilerErrors = lines.filter(
|
|
|
|
|
+ (l: string) => /:\d+:\d+: (?:fatal )?error:/i.test(l) || /^\/.*error:/i.test(l) ||
|
|
|
|
|
+ /undefined reference/i.test(l) || /ld returned/i.test(l) ||
|
|
|
|
|
+ /collect2: error/i.test(l)
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- // Find actual error lines (not warnings)
|
|
|
|
|
- const errorLines = errorSource.split('\n').filter(
|
|
|
|
|
- (l: string) => /error:/i.test(l) || /fatal/i.test(l) || /make.*\*\*\*/i.test(l)
|
|
|
|
|
|
|
+ // 2. CMake / install / permission errors
|
|
|
|
|
+ const otherErrors = lines.filter(
|
|
|
|
|
+ (l: string) => /CMake Error/i.test(l) || /No such file or directory/i.test(l) ||
|
|
|
|
|
+ /Permission denied/i.test(l) || /cannot create/i.test(l) ||
|
|
|
|
|
+ /fatal:/i.test(l) || /failed to/i.test(l)
|
|
|
)
|
|
)
|
|
|
- const shortMsg = errorLines.length > 0
|
|
|
|
|
- ? errorLines.slice(-3).join('\n')
|
|
|
|
|
- : errorSource.split('\n').slice(-5).join('\n')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ let excerpt: string
|
|
|
|
|
+ if (compilerErrors.length > 0) {
|
|
|
|
|
+ // Show the first few real compiler errors (most useful)
|
|
|
|
|
+ excerpt = compilerErrors.slice(0, 10).join('\n')
|
|
|
|
|
+ } else if (otherErrors.length > 0) {
|
|
|
|
|
+ excerpt = otherErrors.slice(0, 8).join('\n')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fallback: show last 30 lines (skip empty/make-summary-only lines)
|
|
|
|
|
+ excerpt = lines.filter((l: string) => l.trim() && !/^make\[\d+\]: (Entering|Leaving)/i.test(l))
|
|
|
|
|
+ .slice(-30).join('\n')
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
send({
|
|
send({
|
|
|
stage: 'error',
|
|
stage: 'error',
|
|
|
- message: `Build failed:\n${shortMsg}`,
|
|
|
|
|
|
|
+ message: `Build failed:\n${excerpt.trim() || fullMsg}`,
|
|
|
percentage: 0
|
|
percentage: 0
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export function launchLwe(
|
|
|
|
|
|
|
+/** Directory containing the LWE binary; used for LD_LIBRARY_PATH. */
|
|
|
|
|
+function getLweLibDir(): string {
|
|
|
|
|
+ const p = whichCommand(LWE_BINARY)
|
|
|
|
|
+ if (!p) return '/usr/local'
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resolved = fs.realpathSync(p)
|
|
|
|
|
+ return path.dirname(resolved)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return path.dirname(p)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** LD_LIBRARY_PATH value so the loader finds LWE's .so files (binary dir + lib subdir). */
|
|
|
|
|
+function getLweLdLibraryPath(): string {
|
|
|
|
|
+ const libDir = getLweLibDir()
|
|
|
|
|
+ const paths = [libDir, path.join(libDir, 'lib')]
|
|
|
|
|
+ const existing = process.env.LD_LIBRARY_PATH
|
|
|
|
|
+ if (existing) paths.push(existing)
|
|
|
|
|
+ return paths.join(':')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Full path to LWE binary (resolved symlink). */
|
|
|
|
|
+function getLweBinaryPath(): string {
|
|
|
|
|
+ const p = whichCommand(LWE_BINARY)
|
|
|
|
|
+ if (!p) return LWE_BINARY
|
|
|
|
|
+ try {
|
|
|
|
|
+ return fs.realpathSync(p)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return p
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Launch LWE and resolve after 2s if still running; reject if it exits with error (so UI can show message). */
|
|
|
|
|
+export function launchLweAsync(
|
|
|
wallpaperPath: string,
|
|
wallpaperPath: string,
|
|
|
options: { screenRoot?: string; fps?: number } = {}
|
|
options: { screenRoot?: string; fps?: number } = {}
|
|
|
-): ChildProcess {
|
|
|
|
|
|
|
+): Promise<void> {
|
|
|
stopLwe()
|
|
stopLwe()
|
|
|
|
|
|
|
|
const args: string[] = []
|
|
const args: string[] = []
|
|
|
|
|
+ if (options.screenRoot) args.push('--screen-root', options.screenRoot)
|
|
|
|
|
+ if (options.fps) args.push('--fps', String(options.fps))
|
|
|
|
|
+ args.push(wallpaperPath)
|
|
|
|
|
|
|
|
- if (options.screenRoot) {
|
|
|
|
|
- args.push('--screen-root', options.screenRoot)
|
|
|
|
|
- }
|
|
|
|
|
- if (options.fps) {
|
|
|
|
|
- args.push('--fps', String(options.fps))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Run via 'env' so LD_LIBRARY_PATH is set for the dynamic linker (some environments don't pass our env to the child)
|
|
|
|
|
+ const ldPath = getLweLdLibraryPath()
|
|
|
|
|
+ const binaryPath = getLweBinaryPath()
|
|
|
|
|
+ const spawnArgs = [`LD_LIBRARY_PATH=${ldPath}`, binaryPath, ...args]
|
|
|
|
|
|
|
|
- args.push(wallpaperPath)
|
|
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const child = spawn('env', spawnArgs, {
|
|
|
|
|
+ stdio: ['ignore', 'ignore', 'pipe'],
|
|
|
|
|
+ detached: true
|
|
|
|
|
+ })
|
|
|
|
|
+ activeProcess = child
|
|
|
|
|
|
|
|
- activeProcess = spawn(LWE_BINARY, args, {
|
|
|
|
|
- stdio: 'ignore',
|
|
|
|
|
- detached: true
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ const stderrChunks: Buffer[] = []
|
|
|
|
|
+ if (child.stderr) {
|
|
|
|
|
+ child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk))
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- activeProcess.on('exit', () => {
|
|
|
|
|
- activeProcess = null
|
|
|
|
|
|
|
+ let settled = false
|
|
|
|
|
+ const cleanup = () => {
|
|
|
|
|
+ activeProcess = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ child.on('exit', (code) => {
|
|
|
|
|
+ cleanup()
|
|
|
|
|
+ if (settled) return
|
|
|
|
|
+ settled = true
|
|
|
|
|
+ if (code !== 0 && code !== null) {
|
|
|
|
|
+ const stderr = Buffer.concat(stderrChunks).toString('utf8').trim()
|
|
|
|
|
+ const msg = stderr ? stderr.split('\n').slice(0, 3).join('\n') : `Exit code ${code}`
|
|
|
|
|
+ reject(new Error(`linux-wallpaperengine failed: ${msg}`))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ child.on('error', (err) => {
|
|
|
|
|
+ cleanup()
|
|
|
|
|
+ if (!settled) {
|
|
|
|
|
+ settled = true
|
|
|
|
|
+ reject(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ child.unref()
|
|
|
|
|
+
|
|
|
|
|
+ // If still running after 2s, consider it started successfully
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!settled && activeProcess === child && child.exitCode === null) {
|
|
|
|
|
+ settled = true
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 2000)
|
|
|
})
|
|
})
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+/** Sync launch (no wait); use launchLweAsync in handlers so startup failures are visible. */
|
|
|
|
|
+export function launchLwe(
|
|
|
|
|
+ wallpaperPath: string,
|
|
|
|
|
+ options: { screenRoot?: string; fps?: number } = {}
|
|
|
|
|
+): ChildProcess {
|
|
|
|
|
+ stopLwe()
|
|
|
|
|
+ const args: string[] = []
|
|
|
|
|
+ if (options.screenRoot) args.push('--screen-root', options.screenRoot)
|
|
|
|
|
+ if (options.fps) args.push('--fps', String(options.fps))
|
|
|
|
|
+ args.push(wallpaperPath)
|
|
|
|
|
+ const ldPath = getLweLdLibraryPath()
|
|
|
|
|
+ const binaryPath = getLweBinaryPath()
|
|
|
|
|
+ activeProcess = spawn('env', [`LD_LIBRARY_PATH=${ldPath}`, binaryPath, ...args], { stdio: 'ignore', detached: true })
|
|
|
|
|
+ activeProcess.on('exit', () => { activeProcess = null })
|
|
|
activeProcess.unref()
|
|
activeProcess.unref()
|
|
|
-
|
|
|
|
|
return activeProcess
|
|
return activeProcess
|
|
|
}
|
|
}
|
|
|
|
|
|