'use strict' // Symlink and permission healing, shared by wine builds, prefixes, game // imports and LAN sync. Rewires links to relative paths (absolute always; // relative only when broken) and restores exec bits on build binaries. const fsp = require('fs').promises const fs = require('fs') const path = require('path') const { pathExists: exists } = require('./fsutil') // dosdevices/ mappings are supposed to point outside the prefix; never touched. const SKIP_DIRS = new Set(['dosdevices']) async function collectSymlinks(dir, out) { let entries try { entries = await fsp.readdir(dir, { withFileTypes: true }) } catch { return } for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isSymbolicLink()) out.push(full) else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) await collectSymlinks(full, out) } } // A missing target can only be something PLWL creates later (fake-steam, // files/wines/...); the restriction keeps /usr/lib/... links alone. const ANCHOR_DIRS = new Set(['wines', 'wineprefixes', 'apps', 'data', 'translations']) // True when `candidate` sits under `root` and its deepest existing parent is // at least two components inside it. function anchoredInside(root, candidate) { let p = candidate while (p.length > root.length + 1) { p = path.dirname(p) if (p === root || !p.startsWith(root + path.sep)) return false if (exists(p)) return path.relative(root, p).split(path.sep).length >= 2 } return false } // The new target for an absolute link, relative to the link's own directory; // falls back to the longest suffix that exists locally. function relativeTarget(link, target, roots) { const dir = path.dirname(link) for (const root of roots) { if (!root) continue if (target.startsWith(root + path.sep)) { const rel = path.relative(dir, target) if (rel && !path.isAbsolute(rel)) return rel } } // second chance for links whose file is not here yet for (const requireLeaf of [true, false]) { for (const root of roots) { if (!root) continue let i = target.indexOf(path.sep, 1) while (i !== -1) { const rest = target.slice(i + 1) if (rest && !rest.split(path.sep).includes('..')) { const candidate = path.join(root, rest) if (candidate.startsWith(root + path.sep)) { const ok = requireLeaf ? exists(candidate) : ANCHOR_DIRS.has(rest.replace(/\\/g, '/').split('/')[0]) && anchoredInside(root, candidate) if (ok) { const rel = path.relative(dir, candidate) if (rel && !path.isAbsolute(rel)) return rel } } } i = target.indexOf(path.sep, i + 1) } } } return null } // Rewire rewritable symlinks under `root`; returns how many changed. Repeats // until nothing changes (links can chain). async function rewireSymlinks(root, roots = []) { if (!root) return 0 const candidates = [root, ...roots.filter(r => r && r !== root)] const links = [] await collectSymlinks(root, links) let fixed = 0 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 absolute = path.isAbsolute(target) // a relative link that still resolves is already relocatable if (!absolute && exists(path.resolve(path.dirname(link), target))) continue const from = absolute ? path.normalize(target) : path.resolve(path.dirname(link), target) const next = relativeTarget(link, from, candidates) if (!next || next === target) continue try { await fsp.unlink(link) await fsp.symlink(next, link) fixed++ changed++ } catch {} } if (!changed) break } return fixed } // Binaries a wine/proton build must be able to execute; content copies drop // the bit and spawn fails with EACCES. Windows .exe files need no bit. const EXEC_NAMES = new Set(['proton', 'wine', 'wine64', 'wine-preloader', 'wine64-preloader', 'wineserver', 'winetricks', 'wineboot']) // Give the exec bit back to a build's binaries and bin/ dirs. async function restoreExecutableBits(root) { if (!root) return 0 let fixed = 0 const walk = async dir => { let entries try { entries = await fsp.readdir(dir, { withFileTypes: true }) } catch { return } for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isSymbolicLink()) continue if (entry.isDirectory()) { await walk(full) continue } if (!entry.isFile()) continue const inBin = path.basename(dir) === 'bin' if (!inBin && !EXEC_NAMES.has(entry.name) && !entry.name.endsWith('.sh')) continue try { const st = await fsp.stat(full) if (st.mode & 0o111) continue await fsp.chmod(full, (st.mode & 0o777) | 0o755) fixed++ } catch {} } } await walk(root) return fixed } module.exports = { rewireSymlinks, restoreExecutableBits, collectSymlinks, relativeTarget, EXEC_NAMES }