diff --git a/src/wines.js b/src/wines.js index 05656b0..82d9cc2 100644 --- a/src/wines.js +++ b/src/wines.js @@ -106,6 +106,17 @@ async function refreshWines() { found = [] } const prev = readJson(REGISTRY, []) + const prevById = new Map(prev.map(w => [w.id, w])) + // A build whose path changed since last scan was moved (or the launcher + // itself was) - rewire its absolute symlinks to the current location. + for (const w of found) { + const old = prevById.get(w.id) + if (!old || old.path !== w.path) { + try { + await fixBuildSymlinks(w.path) + } catch {} + } + } const disabled = new Set(prev.filter(w => w.disabled).map(w => w.id)) for (const w of found) if (disabled.has(w.id)) w.disabled = true const merged = [...found] @@ -168,6 +179,84 @@ function dedupeDir(base, name) { return candidate } +// --- symlink healing ------------------------------------------------------- +// Wine/Proton builds contain symlinks (e.g. inside the bundled default prefix) +// that point to absolute paths from wherever the build lived before (zip +// layout, previous install dir). After the build or the whole launcher is +// moved those links dangle and Proton crashes during prefix setup +// (try_copy -> FileNotFoundError). Rewire every absolute symlink whose content +// can be located inside the build, always storing the new target as a +// *relative* path so the build stays relocatable forever. +async function collectSymlinks(dir, out) { + let entries + try { + entries = await fsp.readdir(dir, { withFileTypes: true }) + } catch { + return + } + for (const e of entries) { + const full = path.join(dir, e.name) + if (e.isSymbolicLink()) { + out.push(full) + } else if (e.isDirectory() && e.name !== 'dosdevices') { + // dosdevices/ holds prefix drive mappings (c:, z:, ...) which are + // *supposed* to point outside the build - never touch them. + await collectSymlinks(full, out) + } + } +} + +function rewireTarget(root, link, target) { + if (!path.isAbsolute(target)) return null // already relocatable + // Absolute link that lives inside the build: make it relative. + if (target.startsWith(root + path.sep)) { + return path.relative(path.dirname(link), target) + } + // Absolute link to an old location: find the longest suffix of the target + // that exists inside the new build and point there instead. + let i = target.indexOf(path.sep, 1) + while (i !== -1) { + const rel = target.slice(i + 1) + if (rel) { + const candidate = path.join(root, rel) + if (!rel.split(path.sep).includes('..') && candidate.startsWith(root + path.sep) && exists(candidate)) { + return path.relative(path.dirname(link), candidate) + } + } + i = target.indexOf(path.sep, i + 1) + } + return null +} + +async function fixBuildSymlinks(root) { + const links = [] + await collectSymlinks(root, links) + let fixed = 0 + // A link may only become rewirable once another link it chains through has + // been fixed, so repeat until a full pass changes nothing. + for (let pass = 0; pass < 5; pass++) { + let changed = 0 + for (const link of links) { + let target + try { + target = await fsp.readlink(link) + } catch { + continue + } + const newTarget = rewireTarget(root, link, path.normalize(target)) + if (!newTarget || newTarget === target) continue + try { + await fsp.unlink(link) + await fsp.symlink(newTarget, link) + fixed++ + changed++ + } catch {} + } + if (!changed) break + } + return fixed +} + async function importWineFolder(srcDir) { const type = detectType(srcDir) let copySrc = srcDir @@ -184,9 +273,13 @@ async function importWineFolder(srcDir) { } } const dst = dedupeDir(WINES_DIR, path.basename(copySrc)) - await fsp.cp(copySrc, dst, { recursive: true }) + await fsp.cp(copySrc, dst, { recursive: true, verbatimSymlinks: true }) + let symlinksFixed = 0 + try { + symlinksFixed = await fixBuildSymlinks(dst) + } catch {} await refreshWines() - return { ok: true, path: dst } + return { ok: true, path: dst, symlinksFixed } } -module.exports = { refreshWines, getWines, getWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType } +module.exports = { refreshWines, getWines, getWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }