瀏覽代碼

we now installs

UwU 7 月之前
父節點
當前提交
c5129bba47
共有 3 個文件被更改,包括 179 次插入 和 43 次删除
  1. 1 0
      .gitignore
  2. 3 3
      src/main/ipc/wallpaper.handlers.ts
  3. 175 40
      src/main/services/lwe.service.ts

+ 1 - 0
.gitignore

@@ -4,3 +4,4 @@ dist/
 .env
 *.db
 preview-cache/
+.nanocoder/

+ 3 - 3
src/main/ipc/wallpaper.handlers.ts

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron'
 import { IpcChannels } from '@shared/ipc-channels'
 import { applyWallpaper, detectEnvironment } from '../services/wallpaper.service'
 import { getWallpaper, updateWallpaper } from '../services/library.service'
-import { getLweStatus, launchLwe } from '../services/lwe.service'
+import { getLweStatus, launchLweAsync } from '../services/lwe.service'
 import type { ApplyWallpaperOptions } from '@shared/types'
 import Store from 'electron-store'
 import * as os from 'os'
@@ -29,12 +29,12 @@ export function registerWallpaperHandlers(): void {
     if (!wallpaper) throw new Error(`Wallpaper ${options.wallpaperId} not found in library`)
     if (!wallpaper.localPath) throw new Error(`Wallpaper ${options.wallpaperId} has no local path`)
 
-    const isAnimated = wallpaper.type === 'scene' || wallpaper.type === 'web'
+    const isAnimated = wallpaper.type === 'scene' || wallpaper.type === 'web' || wallpaper.type === 'video'
     const lwe = getLweStatus()
 
     // Use linux-wallpaperengine for animated wallpapers on Linux if available
     if (isAnimated && os.platform() === 'linux' && lwe.installed) {
-      launchLwe(wallpaper.localPath)
+      await launchLweAsync(wallpaper.localPath)
 
       store.set('activeWallpaperId', options.wallpaperId)
       updateWallpaper(options.wallpaperId, {

+ 175 - 40
src/main/services/lwe.service.ts

@@ -36,29 +36,37 @@ export function detectDistro(): LinuxDistro {
   return 'unknown'
 }
 
+// Aligned with https://github.com/Almamu/linux-wallpaperengine (README + Wayland + FBOProvider gmpxx)
 const DEPS_DEBIAN = [
   'build-essential', 'cmake', 'pkg-config',
   'libxrandr-dev', 'libxinerama-dev', 'libxcursor-dev', 'libxi-dev',
   'libgl-dev', 'libglew-dev', 'freeglut3-dev', 'libsdl2-dev',
   'liblz4-dev', 'libavcodec-dev', 'libavformat-dev', 'libavutil-dev', 'libswscale-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 = [
   'gcc', 'g++', 'cmake', 'pkg-config',
   'libXrandr-devel', 'libXinerama-devel', 'libXcursor-devel', 'libXi-devel',
   'mesa-libGL-devel', 'glew-devel', 'freeglut-devel', 'SDL2-devel',
   'lz4-devel', 'ffmpeg-free-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 = [
   'base-devel', 'cmake', 'pkg-config',
   'glew', 'freeglut', 'sdl2', 'lz4', 'ffmpeg',
   '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> {
@@ -88,7 +96,8 @@ export async function installLweDeps(win: BrowserWindow): Promise<void> {
       break
     case 'fedora':
       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
       break
     case 'arch':
@@ -162,17 +171,19 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
     const cmakeBuild = path.join(BUILD_DIR, 'build')
     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', [
       '..',
       '-DCMAKE_BUILD_TYPE=Release',
+      '-DCMAKE_INSTALL_PREFIX=/usr/local',
       '-DCMAKE_C_FLAGS=-Wno-error',
       '-DCMAKE_CXX_FLAGS=-Wno-error'
     ], {
       cwd: cmakeBuild,
-      timeout: 120_000,
-      maxBuffer: 10 * 1024 * 1024
+      timeout: 600_000,
+      maxBuffer: 10 * 1024 * 1024,
+      encoding: 'utf8'
     })
 
     // Build
@@ -181,20 +192,39 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
     await execFileAsync('make', ['-j', String(cores)], {
       cwd: cmakeBuild,
       timeout: 600_000,
-      maxBuffer: 10 * 1024 * 1024
+      maxBuffer: 10 * 1024 * 1024,
+      encoding: 'utf8'
     })
 
     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 })
 
     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
     fs.rmSync(BUILD_DIR, { recursive: true, force: true })
 
@@ -218,55 +248,160 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
       }
     } 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({
       stage: 'error',
-      message: `Build failed:\n${shortMsg}`,
+      message: `Build failed:\n${excerpt.trim() || fullMsg}`,
       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,
   options: { screenRoot?: string; fps?: number } = {}
-): ChildProcess {
+): Promise<void> {
   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)
 
-  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()
-
   return activeProcess
 }