Przeglądaj źródła

reworked local build process

UwU 2 miesięcy temu
rodzic
commit
5a126e14c6
2 zmienionych plików z 137 dodań i 49 usunięć
  1. 133 49
      src/main/services/lwe.service.ts
  2. 4 0
      src/main/utils/paths.ts

+ 133 - 49
src/main/services/lwe.service.ts

@@ -14,7 +14,7 @@ import {
   getWaylandDisplay,
   getXdgRuntimeDir
 } from '../utils/platform'
-import { getWorkshopPath } from '../utils/paths'
+import { getWorkshopPath, getLweManifestPath } from '../utils/paths'
 import { getLweRepoUrl, getLweRepoBranch } from './config.service'
 import { DEFAULT_LWE_REPO } from '@shared/constants'
 
@@ -51,6 +51,25 @@ async function removeBuildDir(dir: string): Promise<void> {
   await execFileAsync(elevate, ['rm', '-rf', dir], { timeout: 30_000, encoding: 'utf8' })
 }
 
+interface LweInstallManifest {
+  files: string[]
+  symlink: string
+}
+
+function readLweManifest(): LweInstallManifest | undefined {
+  try {
+    const parsed = JSON.parse(fs.readFileSync(getLweManifestPath(), 'utf8')) as LweInstallManifest
+    if (Array.isArray(parsed.files) && typeof parsed.symlink === 'string') return parsed
+  } catch { /* falls back to heuristic cleanup in uninstallLwe */ }
+  return undefined
+}
+
+function writeLweManifest(manifest: LweInstallManifest): void {
+  try {
+    fs.writeFileSync(getLweManifestPath(), JSON.stringify(manifest, null, 2), 'utf8')
+  } catch { /* non-fatal */ }
+}
+
 function findLweBinary(): string | undefined {
   for (const p of LWE_SEARCH_PATHS) {
     try {
@@ -178,6 +197,9 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
     win.webContents.send(IpcChannels.EVENT_LWE_INSTALL_PROGRESS, progress)
   }
 
+  // read by the catch handler to decide whether to preserve or wipe the build cache
+  let isLocalDir = false
+
   try {
     send({ stage: 'cloning', message: 'Checking build dependencies...', percentage: 0 })
 
@@ -195,29 +217,57 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
       return
     }
 
-    // Clean previous build
-    await removeBuildDir(BUILD_DIR)
-
-    // Clone from the configured repo (custom fork or local path), or the official repo.
-    // Local paths are expanded so git clone sees an absolute path.
     let repo = getLweRepoUrl() ?? DEFAULT_LWE_REPO
     if (repo.startsWith('~/')) repo = path.join(os.homedir(), repo.slice(2))
     const branch = getLweRepoBranch()
-    const cloneArgs = ['clone', '--depth', '1', '--recursive']
-    if (branch) cloneArgs.push('--branch', branch)
-    cloneArgs.push(repo, BUILD_DIR)
 
-    send({
-      stage: 'cloning',
-      message: `Cloning ${repo}${branch ? ` (${branch})` : ''}...`,
-      percentage: 5
-    })
-    await execFileAsync('git', cloneArgs, {
-      timeout: 120_000,
-      maxBuffer: 10 * 1024 * 1024
-    })
+    isLocalDir = (() => {
+      try { return fs.statSync(repo).isDirectory() } catch { return false }
+    })()
+
+    // Reuse the previous build dir when it's the same local source, so CEF's ~2GB download
+    // and compiled objects survive between iterations instead of a full rebuild every time.
+    const sourceMarker = path.join(BUILD_DIR, '.lwe-source-path')
+    const canReuseBuildCache = isLocalDir && (() => {
+      try { return fs.readFileSync(sourceMarker, 'utf8').trim() === repo } catch { return false }
+    })()
+    if (!canReuseBuildCache) {
+      await removeBuildDir(BUILD_DIR)
+    }
+
+    if (isLocalDir) {
+      // Copy the working tree instead of `git clone`, so uncommitted edits don't need a commit first.
+      send({ stage: 'cloning', message: `Copying local source from ${repo}...`, percentage: 5 })
+      fs.mkdirSync(BUILD_DIR, { recursive: true })
+      if (isCommandAvailable('rsync')) {
+        // --delete so files removed from the source don't linger in a reused build dir
+        await execFileAsync('rsync', [
+          '-a', '--delete', '--exclude=/.git', '--exclude=/build',
+          `${repo}/`, `${BUILD_DIR}/`
+        ], { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 })
+      } else {
+        await execFileAsync('bash', [
+          '-c',
+          `tar -C ${JSON.stringify(repo)} --exclude=.git --exclude=build -cf - . | tar -C ${JSON.stringify(BUILD_DIR)} -xf -`
+        ], { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 })
+      }
+      fs.writeFileSync(sourceMarker, repo, 'utf8')
+    } else {
+      const cloneArgs = ['clone', '--depth', '1', '--recursive']
+      if (branch) cloneArgs.push('--branch', branch)
+      cloneArgs.push(repo, BUILD_DIR)
+
+      send({
+        stage: 'cloning',
+        message: `Cloning ${repo}${branch ? ` (${branch})` : ''}...`,
+        percentage: 5
+      })
+      await execFileAsync('git', cloneArgs, {
+        timeout: 120_000,
+        maxBuffer: 10 * 1024 * 1024
+      })
+    }
 
-    // Create build dir
     const cmakeBuild = path.join(BUILD_DIR, 'build')
     fs.mkdirSync(cmakeBuild, { recursive: true })
 
@@ -255,10 +305,18 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
     // 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 && patchelf --set-rpath /usr/local/lib64:/usr/local/lib /usr/local/linux-wallpaperengine 2>/dev/null; ldconfig; rm -rf ${JSON.stringify(BUILD_DIR)}`
-    await execFileAsync(installTool, ['bash', '-c', installScript], {
+    // install_manifest.txt lists every file `make install` placed; capture it so uninstall
+    // can remove exactly those files instead of guessing.
+    const manifestMarker = '###LWE_INSTALL_MANIFEST###'
+    // Local repo: hand the build dir back to the invoking user instead of deleting it, so the
+    // next unprivileged cmake/make can still write into what `make install` (root) just touched.
+    const reclaim = isLocalDir
+      ? `chown -R ${process.getuid()}:${process.getgid()} ${JSON.stringify(BUILD_DIR)}`
+      : `rm -rf ${JSON.stringify(BUILD_DIR)}`
+    const installScript = `cd ${JSON.stringify(cmakeBuild)} && export PATH=${JSON.stringify(pathEnv)} && make install && patchelf --set-rpath /usr/local/lib64:/usr/local/lib /usr/local/linux-wallpaperengine 2>/dev/null; ldconfig; echo ${manifestMarker}; cat install_manifest.txt 2>/dev/null; ${reclaim}`
+    const { stdout: installOutput } = await execFileAsync(installTool, ['bash', '-c', installScript], {
       timeout: 120_000,
-      maxBuffer: 2 * 1024 * 1024,
+      maxBuffer: 5 * 1024 * 1024,
       encoding: 'utf8'
     })
 
@@ -275,6 +333,15 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
       // Non-fatal: user may already have it in PATH or can add /usr/local to PATH
     }
 
+    const manifestIdx = installOutput.indexOf(manifestMarker)
+    if (manifestIdx !== -1) {
+      const installedFiles = installOutput.slice(manifestIdx + manifestMarker.length)
+        .split('\n').map(l => l.trim()).filter(Boolean)
+      if (installedFiles.length > 0) {
+        writeLweManifest({ files: installedFiles, symlink: linkPath })
+      }
+    }
+
     // Invalidate cache so next status check re-detects
     invalidateCommandCache(LWE_BINARY)
 
@@ -289,9 +356,13 @@ export async function installLwe(win: BrowserWindow): Promise<void> {
       })
     }
   } catch (err) {
-    try {
-      await removeBuildDir(BUILD_DIR)
-    } catch { /* ignore cleanup errors */ }
+    // Failures here happen before the elevated install step, so the build dir isn't root-tainted -
+    // leave it for a local repo so a fix-and-retry stays incremental.
+    if (!isLocalDir) {
+      try {
+        await removeBuildDir(BUILD_DIR)
+      } catch { /* ignore cleanup errors */ }
+    }
 
     // execFile puts stdout/stderr on the error object when encoding/maxBuffer are set
     const e = err as Error & { stdout?: string; stderr?: string }
@@ -339,39 +410,52 @@ export async function uninstallLwe(): Promise<{ ok: boolean; message: string }>
   stopLwe()
 
   const elevate = isCommandAvailable('pkexec') ? 'pkexec' : 'sudo'
-  const binaryPath = status.path
 
-  // Resolve symlink to find the real binary location
-  let realPath: string
-  try {
-    realPath = fs.realpathSync(binaryPath)
-  } catch {
-    realPath = binaryPath
-  }
-
-  // Collect all files to remove (binary, symlink if different, known lib dir)
-  const filesToRemove: string[] = [realPath]
-  if (realPath !== binaryPath) filesToRemove.push(binaryPath)
+  const manifest = readLweManifest()
+  let filesToRemove: string[]
+  let dirsToPrune: string[] = []
 
-  // Check for LWE lib directory next to the real binary (cmake install puts libs there)
-  const binDir = path.dirname(realPath)
-  const libDir = path.join(binDir, 'lib')
-  if (fs.existsSync(libDir)) {
-    // Only remove if it looks like it belongs to LWE (contains libcef or similar)
+  if (manifest) {
+    filesToRemove = [...manifest.files, manifest.symlink]
+    // deepest first, so a dir only prunes once its own contents are gone
+    dirsToPrune = [...new Set(manifest.files.map(f => path.dirname(f)))].sort((a, b) => b.length - a.length)
+  } else {
+    // no manifest (pre-dates this, or lost): binary + lib dir only, won't clean up CEF resources
+    const binaryPath = status.path
+    let realPath: string
     try {
-      const entries = fs.readdirSync(libDir)
-      if (entries.some(e => e.includes('cef') || e.includes('wallpaper'))) {
-        filesToRemove.push(libDir)
-      }
-    } catch { /* ignore */ }
+      realPath = fs.realpathSync(binaryPath)
+    } catch {
+      realPath = binaryPath
+    }
+
+    filesToRemove = [realPath]
+    if (realPath !== binaryPath) filesToRemove.push(binaryPath)
+
+    const binDir = path.dirname(realPath)
+    const libDir = path.join(binDir, 'lib')
+    if (fs.existsSync(libDir)) {
+      try {
+        const entries = fs.readdirSync(libDir)
+        if (entries.some(e => e.includes('cef') || e.includes('wallpaper'))) {
+          filesToRemove.push(libDir)
+        }
+      } catch { /* ignore */ }
+    }
   }
 
   try {
-    await execFileAsync(elevate, ['rm', '-rf', ...filesToRemove], {
-      timeout: 30_000,
+    const quoted = (paths: string[]) => paths.map(p => JSON.stringify(p)).join(' ')
+    const script = dirsToPrune.length > 0
+      ? `rm -f ${quoted(filesToRemove)}; rmdir --ignore-fail-on-non-empty -p ${quoted(dirsToPrune)} 2>/dev/null; ldconfig`
+      : `rm -f ${quoted(filesToRemove)}; ldconfig`
+    await execFileAsync(elevate, ['bash', '-c', script], {
+      timeout: 60_000,
       encoding: 'utf8'
     })
 
+    try { fs.rmSync(getLweManifestPath(), { force: true }) } catch { /* ignore */ }
+
     // Invalidate caches
     invalidateCommandCache(LWE_BINARY)
 

+ 4 - 0
src/main/utils/paths.ts

@@ -48,3 +48,7 @@ export function getDataPath(): string {
 export function getPreviewCachePath(): string {
   return path.join(getDataPath(), 'preview-cache')
 }
+
+export function getLweManifestPath(): string {
+  return path.join(getDataPath(), 'lwe-install-manifest.json')
+}