154 lines
5.1 KiB
JavaScript
154 lines
5.1 KiB
JavaScript
'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 }
|