cleanup comments, update translations, bump to 0.4.1

This commit is contained in:
olcxja 2026-10-05 15:42:44 +02:00
commit 7d8ab87eee
28 changed files with 412 additions and 1114 deletions

View file

@ -1,36 +1,15 @@
'use strict'
// Symlink and permission healing, shared by wine builds, prefixes, game
// imports and LAN sync.
//
// Two things break when a tree is copied between machines (or the launcher is
// just moved):
// - symlinks stored ABSOLUTE keep pointing at the path they had on the
// machine that wrote them. A Proton prefix is full of them (fonts, built-in
// program stubs, dlls, all pointing into its build), and a wine build's
// bundled default prefix has the same problem.
// - the executable bit is dropped by every content-piping copy
// (createWriteStream, fs.copyFile), which makes an imported `proton` or
// `wine` binary unrunnable - the launcher then cannot start that build at
// all.
//
// Everything here rewires a link to a RELATIVE path when the file it points at
// can be found locally, so the tree stays relocatable forever, and restores
// the executable bit on the binaries a wine/proton build needs. An absolute
// link is always rewritten; a relative one is only touched when it no longer
// resolves, because a tree that changed layout (a prefix whose contents moved
// one level up, see prefixes.ensureUnifiedPrefix) keeps relative links written
// against the old depth and every one of them breaks at once.
// 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/ holds the prefix's drive mappings (c:, z:, d:, com1 ...). They
// are *supposed* to point outside the prefix - at the filesystem root, a
// mounted CD or a serial port - and wine rewrites them itself, so they are
// never touched.
// dosdevices/ mappings are supposed to point outside the prefix; never touched.
const SKIP_DIRS = new Set(['dosdevices'])
async function collectSymlinks(dir, out) {
@ -47,18 +26,12 @@ async function collectSymlinks(dir, out) {
}
}
// A target that does not exist here YET can only be something PLWL itself
// creates later: Proton links files/data/fake-steam/appcache into its prefix,
// a prefix links into files/wines/<build>/... (see the fallback pass below).
// Restricting the fallback to these trees keeps an unrelated /usr/lib/... link
// from being mistaken for a wine build's own files/lib/ directory.
// 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. `files/data/fake-steam/appcache` passes even
// before appcache is created, because files/data/fake-steam is already there;
// `files/bin/<x>` does not, because a single-component parent is no evidence
// that the link really belongs to the launcher.
// 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) {
@ -69,12 +42,8 @@ function anchoredInside(root, candidate) {
return false
}
// The new target for an absolute link: the same file, reached from the link's
// own directory. `roots` are the trees the target may live in - the tree being
// healed first, then (for a prefix linking into its build) the launcher's
// files/ root. When the target is not directly inside one of them, the longest
// suffix that exists here is used instead, which is what makes a link written
// on another machine work on this one.
// 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) {
@ -84,9 +53,7 @@ function relativeTarget(link, target, roots) {
if (rel && !path.isAbsolute(rel)) return rel
}
}
// Second chance for links whose file is not here yet (or whose parent chain
// only partially exists): the relative path is still the right one, and
// writing it now beats leaving a link that can never resolve.
// second chance for links whose file is not here yet
for (const requireLeaf of [true, false]) {
for (const root of roots) {
if (!root) continue
@ -112,11 +79,8 @@ function relativeTarget(link, target, roots) {
return null
}
// Rewire the symlinks under `root` that can be expressed relative to `roots`
// (usually [root, DIRS.FILES]): absolute ones always, relative ones only when
// they no longer resolve. Returns how many links changed. A link may only
// become rewirable once another link it chains through has been fixed, so the
// pass repeats until nothing changes.
// 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)]
@ -133,9 +97,7 @@ async function rewireSymlinks(root, roots = []) {
continue
}
const absolute = path.isAbsolute(target)
// a relative link that still resolves is already relocatable - only a
// broken one is worth touching, and the suffix search in relativeTarget
// finds the file it was pointing at before the layout changed
// 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)
@ -152,15 +114,11 @@ async function rewireSymlinks(root, roots = []) {
return fixed
}
// Binaries a wine/proton build has to be able to execute. A copy that pipes
// file contents (the LAN sync, fs.copyFile) drops the bit, and without it
// `spawn` fails with EACCES and the build can never run. Windows .exe files
// are NOT in here on purpose: wine starts those itself, they need no bit.
// 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 anything in a bin/ dir).
// Used after a sync or an import - and to repair a build that was synced
// before the sync knew about permissions, without re-copying it.
// Give the exec bit back to a build's binaries and bin/ dirs.
async function restoreExecutableBits(root) {
if (!root) return 0
let fixed = 0