wine import auto-fix symlinks

This commit is contained in:
olcxja 2026-09-26 09:39:28 +02:00
commit 2b7623227d

View file

@ -106,6 +106,17 @@ async function refreshWines() {
found = [] found = []
} }
const prev = readJson(REGISTRY, []) 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)) 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 for (const w of found) if (disabled.has(w.id)) w.disabled = true
const merged = [...found] const merged = [...found]
@ -168,6 +179,84 @@ function dedupeDir(base, name) {
return candidate 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) { async function importWineFolder(srcDir) {
const type = detectType(srcDir) const type = detectType(srcDir)
let copySrc = srcDir let copySrc = srcDir
@ -184,9 +273,13 @@ async function importWineFolder(srcDir) {
} }
} }
const dst = dedupeDir(WINES_DIR, path.basename(copySrc)) 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() 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 }