fix dll overrides & add disable UMU
This commit is contained in:
parent
5d5ca345f1
commit
8c6db38fab
12 changed files with 295 additions and 45 deletions
|
|
@ -69,7 +69,8 @@ A sync also carries **how the apps are set up**: the Wine/Proton build and prefi
|
||||||
|
|
||||||
- **System Wine** — uses the `wine` binary from PATH.
|
- **System Wine** — uses the `wine` binary from PATH.
|
||||||
- **Imported builds** — drop any wine or Proton build into `files/wines/`. The folder may contain the build directly, or one level of nesting (zip-extracted layouts are detected automatically). After the copy, PLWL rewires the build's absolute symlinks to relative ones and restores the executable bit on the binaries it needs to run (`proton`, `wine`, `wineserver`, `winetricks`), so a build that arrived from a zip or another machine works without further fiddling. A build can also be given a **displayed name** of its own (Wine versions): it is used everywhere the build is offered — library config, settings and the *Open with PLWL* popup — while the folder in `files/wines/` keeps its name and the symlinks inside prefixes stay valid.
|
- **Imported builds** — drop any wine or Proton build into `files/wines/`. The folder may contain the build directly, or one level of nesting (zip-extracted layouts are detected automatically). After the copy, PLWL rewires the build's absolute symlinks to relative ones and restores the executable bit on the binaries it needs to run (`proton`, `wine`, `wineserver`, `winetricks`), so a build that arrived from a zip or another machine works without further fiddling. A build can also be given a **displayed name** of its own (Wine versions): it is used everywhere the build is offered — library config, settings and the *Open with PLWL* popup — while the folder in `files/wines/` keeps its name and the symlinks inside prefixes stay valid.
|
||||||
- **Proton without Steam** — fully supported. Proton is invoked like Steam does it: `STEAM_COMPAT_CLIENT_INSTALL_PATH` points to a fake steam client folder, `STEAM_COMPAT_DATA_PATH` is a per-build compat folder, and the prefix lives at `<compat>/pfx` which PLWL links to its own prefix folder. No Steam installation required (GE builds work out of the box; for a few Valve builds that refuse to run without a client, PLWL fakes a minimal steam folder in `files/data/fake-steam`).
|
- **Proton without Steam** — fully supported. Proton is invoked like Steam does it: `STEAM_COMPAT_DATA_PATH` is a per-build compat folder linked to the launcher's own prefix, and `STEAM_COMPAT_CLIENT_INSTALL_PATH` points at an installed Steam client when there is one (Proton reads its legacy DLLs and `steamclient.so` from there, read-only) and at the empty `files/data/fake-steam` otherwise. GE builds work out of the box: PLWL launches through Proton's built-in UMU path (`UMU_ID`), which needs no Steam client at all. An app that breaks under umu.exe has a per-app **Disable UMU** switch in its config — PLWL then runs the build's own wine directly (Proton's `runinprefix` verb): no umu.exe/steam.exe launcher and no Proton prefix setup take part, so the run stays as close to plain wine as possible. (Proton's session init still reads `STEAM_COMPAT_CLIENT_INSTALL_PATH` on every verb; a disableUmu run gets the empty `fake-steam` folder and nothing is copied out of it.)
|
||||||
|
- **One C: drive per prefix** — Proton normally keeps its prefix one level deeper (`<compat>/pfx/drive_c`) than wine (`<prefix>/drive_c`), which left a prefix used with both kinds with two separate C: drives (two registries, two sets of saves). Whenever a prefix has no `pfx` yet (a fresh prefix, or a plain-wine prefix first used through Proton), PLWL makes `pfx` a relative symlink back to the prefix itself, so Proton and wine share one `drive_c`, one registry and one `dosdevices`. Existing layouts are left exactly as they are — prefixes from older versions keep their real `pfx/` folder, and a prefix with both `drive_c` trees keeps both, because two live Windows installs cannot be merged safely (both stay visible to the prefix import picker).
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,34 @@ handlers in src/ipc/. Keep new APIs in that pattern.
|
||||||
relative target) and after a unit lands (healUnitRoots). dosdevices drive
|
relative target) and after a unit lands (healUnitRoots). dosdevices drive
|
||||||
mappings and targets that leave files/ (a mount, a system path, the user's
|
mappings and targets that leave files/ (a mount, a system path, the user's
|
||||||
home) stay absolute - they are machine-specific by design.
|
home) stay absolute - they are machine-specific by design.
|
||||||
|
- A prefix that has NO `pfx` yet gets ONE C: drive: `pfx` is a relative
|
||||||
|
symlink to the prefix itself, so Proton's `<compatdata>/pfx`
|
||||||
|
(runner.linkPrefix) resolves to `<prefix>` and shares `drive_c`, the
|
||||||
|
registry and `dosdevices` with plain wine. `prefixes.ensureUnifiedPrefix`
|
||||||
|
only ever CREATES that link; an existing layout (a real `pfx/` folder from
|
||||||
|
an older PLWL, a both-drives prefix) is left exactly as it is. Hoisting a
|
||||||
|
prefix's files one level up was tried and removed: it breaks every relative
|
||||||
|
link written against the old depth (the `umu.exe` link into the build is the
|
||||||
|
one that kills a Proton launch), and moving a live Windows install is not
|
||||||
|
the launcher's call. `healPrefix`/`rewireSymlinks` repairs such a prefix by
|
||||||
|
re-anchoring broken relative links through files/. Never assume a single
|
||||||
|
drive_c path: use `prefixDriveRoots()` (deduplicated) and compare realpaths
|
||||||
|
when deciding whether a folder is inside a prefix's C: drive.
|
||||||
|
- App launch env is built in runner.buildEnv only. WINEDLLOVERRIDES is an
|
||||||
|
ordered semicolon list: inherited value first, the app's overrides last,
|
||||||
|
because Wine lets a later entry replace an earlier one for the same module.
|
||||||
|
An empty setting (`dll=`) is Wine's "disabled" and must survive the filter.
|
||||||
|
Proton appends its own dxvk/nvapi/steam.exe defaults AFTER everything, so an
|
||||||
|
app override only wins where Proton has no default. `disableUmu` (per app)
|
||||||
|
drops UMU_ID and switches Proton to its `runinprefix` verb, so no
|
||||||
|
umu.exe/steam.exe launcher and no Proton prefix setup take part - the
|
||||||
|
build's own wine runs as close to plain wine as it gets.
|
||||||
|
STEAM_COMPAT_CLIENT_INSTALL_PATH must be set in BOTH modes: Proton's session
|
||||||
|
init reads it on every verb (sync_appcache runs even for runinprefix, and
|
||||||
|
with SteamGameId set a missing variable aborts the launch with a KeyError
|
||||||
|
before wine starts). On the default path it prefers a real Steam client
|
||||||
|
found on the machine; a disableUmu run always gets the empty
|
||||||
|
files/data/fake-steam, whose contents are never read.
|
||||||
- Wine and Proton create prefix links with ABSOLUTE local targets whenever they
|
- Wine and Proton create prefix links with ABSOLUTE local targets whenever they
|
||||||
update a prefix (font/dll stubs, Steam appcache), so on-disk links are not
|
update a prefix (font/dll stubs, Steam appcache), so on-disk links are not
|
||||||
always relative after a run: the sync payload relativizes them anyway, and
|
always relative after a run: the sync payload relativizes them anyway, and
|
||||||
|
|
|
||||||
|
|
@ -81,11 +81,14 @@
|
||||||
"config.prefixDefault": "Default prefix ({0})",
|
"config.prefixDefault": "Default prefix ({0})",
|
||||||
"config.prefixLocked": "This app was imported from a prefix, its prefix cannot be changed",
|
"config.prefixLocked": "This app was imported from a prefix, its prefix cannot be changed",
|
||||||
"config.prefixLockedHint": "Pinned to the prefix it was imported from (its paths are stored relative to it)",
|
"config.prefixLockedHint": "Pinned to the prefix it was imported from (its paths are stored relative to it)",
|
||||||
|
"config.disableUmu": "Disable UMU (launch with wine directly)",
|
||||||
|
"config.disableUmuHint": "Runs the game with the build's own wine binary instead of its umu.exe/steam.exe launcher - no UMU and no Steam client is started, and Proton's prefix setup is skipped too. This is the closest to a plain wine run.",
|
||||||
"config.dllOverrides": "DLL overrides",
|
"config.dllOverrides": "DLL overrides",
|
||||||
"config.overrides.add": "Add override",
|
"config.overrides.add": "Add override",
|
||||||
"config.overrides.dll": "DLL name",
|
"config.overrides.dll": "DLL name",
|
||||||
"config.overrides.setting": "Setting",
|
"config.overrides.setting": "Setting",
|
||||||
"config.overrides.remove": "Remove",
|
"config.overrides.remove": "Remove",
|
||||||
|
"config.overrides.hint": "Wine module names, e.g. dinput8. native,builtin (the default) loads a DLL shipped with the game and falls back to Wine's builtin; disabled blocks the module. 'name=setting' pasted into the name box is split automatically.",
|
||||||
"config.save": "Save changes",
|
"config.save": "Save changes",
|
||||||
"config.shortcut": "Create shortcut",
|
"config.shortcut": "Create shortcut",
|
||||||
"config.shortcutCreated": "Shortcut for {0} created (menu + desktop)",
|
"config.shortcutCreated": "Shortcut for {0} created (menu + desktop)",
|
||||||
|
|
|
||||||
|
|
@ -81,11 +81,14 @@
|
||||||
"config.prefixDefault": "Prefiks domyślny ({0})",
|
"config.prefixDefault": "Prefiks domyślny ({0})",
|
||||||
"config.prefixLocked": "Ta aplikacja została zaimportowana z prefiksu, nie można zmienić jej prefiksu",
|
"config.prefixLocked": "Ta aplikacja została zaimportowana z prefiksu, nie można zmienić jej prefiksu",
|
||||||
"config.prefixLockedHint": "Przypięta do prefiksu, z którego została zaimportowana (ścieżki trzymane są względem niego)",
|
"config.prefixLockedHint": "Przypięta do prefiksu, z którego została zaimportowana (ścieżki trzymane są względem niego)",
|
||||||
|
"config.disableUmu": "Wyłącz UMU (uruchamiaj bezpośrednio wine)",
|
||||||
|
"config.disableUmuHint": "Uruchamia grę bezpośrednio binarką wine z builda, z pominięciem launchera umu.exe/steam.exe — bez UMU i bez uruchamiania klienta Steam, z pominięciem przygotowania prefiksu przez Protona. To najbliżej zwykłego uruchomienia wine.",
|
||||||
"config.dllOverrides": "Nadpisania DLL",
|
"config.dllOverrides": "Nadpisania DLL",
|
||||||
"config.overrides.add": "Dodaj nadpisanie",
|
"config.overrides.add": "Dodaj nadpisanie",
|
||||||
"config.overrides.dll": "Nazwa DLL",
|
"config.overrides.dll": "Nazwa DLL",
|
||||||
"config.overrides.setting": "Ustawienie",
|
"config.overrides.setting": "Ustawienie",
|
||||||
"config.overrides.remove": "Usuń",
|
"config.overrides.remove": "Usuń",
|
||||||
|
"config.overrides.hint": "Nazwy modułów Wine, np. dinput8. native,builtin (domyślne) wczytuje DLL dołączony do gry, a gdy go nie ma — wbudowany Wine; disabled blokuje moduł. Wklejone 'nazwa=ustawienie' zostanie rozdzielone automatycznie.",
|
||||||
"config.save": "Zapisz zmiany",
|
"config.save": "Zapisz zmiany",
|
||||||
"config.shortcut": "Utwórz skrót",
|
"config.shortcut": "Utwórz skrót",
|
||||||
"config.shortcutCreated": "Skrót dla {0} utworzony (menu + pulpit)",
|
"config.shortcutCreated": "Skrót dla {0} utworzony (menu + pulpit)",
|
||||||
|
|
|
||||||
|
|
@ -1235,6 +1235,7 @@ async function openGameConfig(id) {
|
||||||
const lockHint = qs('#gc-prefix-lock-hint')
|
const lockHint = qs('#gc-prefix-lock-hint')
|
||||||
if (lockHint) lockHint.style.display = prefixLocked ? '' : 'none'
|
if (lockHint) lockHint.style.display = prefixLocked ? '' : 'none'
|
||||||
renderOverrides(game.dllOverrides || [])
|
renderOverrides(game.dllOverrides || [])
|
||||||
|
qs('#gc-disable-umu').checked = !!game.disableUmu
|
||||||
renderTools()
|
renderTools()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1257,10 +1258,12 @@ function renderOverrides(overrides) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// native,builtin first: that is the default a new override row starts on
|
||||||
|
// (load a native DLL when the game has one, fall back to Wine's builtin)
|
||||||
const OVERRIDE_OPTIONS = [
|
const OVERRIDE_OPTIONS = [
|
||||||
|
{ value: 'native,builtin', label: 'native,builtin' },
|
||||||
{ value: 'native', label: 'native' },
|
{ value: 'native', label: 'native' },
|
||||||
{ value: 'builtin', label: 'builtin' },
|
{ value: 'builtin', label: 'builtin' },
|
||||||
{ value: 'native,builtin', label: 'native,builtin' },
|
|
||||||
{ value: 'builtin,native', label: 'builtin,native' },
|
{ value: 'builtin,native', label: 'builtin,native' },
|
||||||
{ value: '', label: 'disabled' }
|
{ value: '', label: 'disabled' }
|
||||||
]
|
]
|
||||||
|
|
@ -1274,23 +1277,48 @@ function buildOverrideRow(o) {
|
||||||
`
|
`
|
||||||
row.querySelector('.dll').value = o.dll || ''
|
row.querySelector('.dll').value = o.dll || ''
|
||||||
const selectHost = document.createElement('span')
|
const selectHost = document.createElement('span')
|
||||||
new CustomSelect(selectHost, OVERRIDE_OPTIONS, o.setting || 'native')
|
// a stored empty setting IS the "disabled" choice - a `||` fallback would
|
||||||
|
// turn every disabled override back into the default on reopen
|
||||||
|
new CustomSelect(selectHost, OVERRIDE_OPTIONS, o.setting == null ? 'native,builtin' : o.setting)
|
||||||
row.insertBefore(selectHost, row.querySelector('[data-act=del]'))
|
row.insertBefore(selectHost, row.querySelector('[data-act=del]'))
|
||||||
row.querySelector('[data-act=del]').onclick = () => row.remove()
|
row.querySelector('[data-act=del]').onclick = () => row.remove()
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOverrideRow() {
|
function addOverrideRow() {
|
||||||
qs('#gc-overrides').appendChild(buildOverrideRow({ dll: '', setting: 'native' }))
|
qs('#gc-overrides').appendChild(buildOverrideRow({ dll: '', setting: 'native,builtin' }))
|
||||||
const rows = qsa('#gc-overrides .override-row')
|
const rows = qsa('#gc-overrides .override-row')
|
||||||
rows[rows.length - 1].querySelector('.dll').focus()
|
rows[rows.length - 1].querySelector('.dll').focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The setting a guide or a launch option uses ("n", "native,builtin", "d"...)
|
||||||
|
// mapped onto the values this select offers. Unknown text keeps the select's
|
||||||
|
// choice instead of inventing one.
|
||||||
|
function normalizeOverrideSetting(raw) {
|
||||||
|
const v = String(raw || '').trim().toLowerCase().replace(/\s+/g, '')
|
||||||
|
if (!v || v === 'd' || v === 'disabled') return ''
|
||||||
|
if (v === 'n' || v === 'native') return 'native'
|
||||||
|
if (v === 'b' || v === 'builtin') return 'builtin'
|
||||||
|
if (v === 'n,b' || v === 'native,builtin') return 'native,builtin'
|
||||||
|
if (v === 'b,n' || v === 'builtin,native') return 'builtin,native'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function readOverrides() {
|
function readOverrides() {
|
||||||
return qsa('#gc-overrides .override-row').map(row => ({
|
return qsa('#gc-overrides .override-row').map(row => {
|
||||||
dll: row.querySelector('.dll').value.trim(),
|
let dll = row.querySelector('.dll').value.trim()
|
||||||
setting: row.querySelector('.custom-select').dataset.value ?? 'native'
|
let setting = row.querySelector('.custom-select').dataset.value ?? 'native,builtin'
|
||||||
})).filter(o => o.dll)
|
// "dinput8=native" pasted into the name box is the form every guide uses -
|
||||||
|
// split it here instead of storing a name with '=' that Wine cannot parse
|
||||||
|
const eq = dll.indexOf('=')
|
||||||
|
if (eq > 0) {
|
||||||
|
const pasted = normalizeOverrideSetting(dll.slice(eq + 1))
|
||||||
|
if (pasted !== null) setting = pasted
|
||||||
|
dll = dll.slice(0, eq).trim()
|
||||||
|
}
|
||||||
|
dll = dll.replace(/\.dll$/i, '')
|
||||||
|
return { dll, setting }
|
||||||
|
}).filter(o => o.dll)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveGameConfig() {
|
async function saveGameConfig() {
|
||||||
|
|
@ -1322,10 +1350,14 @@ async function saveGameConfig() {
|
||||||
wine: selects['gc-wine'].value,
|
wine: selects['gc-wine'].value,
|
||||||
prefix,
|
prefix,
|
||||||
env,
|
env,
|
||||||
dllOverrides: readOverrides()
|
dllOverrides: readOverrides(),
|
||||||
|
disableUmu: qs('#gc-disable-umu').checked
|
||||||
}
|
}
|
||||||
await window.plwl.gamesUpdate(game.id, patch)
|
await window.plwl.gamesUpdate(game.id, patch)
|
||||||
state.games = await window.plwl.gamesList()
|
state.games = await window.plwl.gamesList()
|
||||||
|
// show what was actually stored: a pasted "name=setting" was split and a
|
||||||
|
// .dll suffix stripped, so the rows would otherwise disagree with the file
|
||||||
|
renderOverrides(patch.dllOverrides)
|
||||||
toast(t('config.saved'), 'success')
|
toast(t('config.saved'), 'success')
|
||||||
if (prefixLockedHint) toast(t('config.prefixLocked'), 'error')
|
if (prefixLockedHint) toast(t('config.prefixLocked'), 'error')
|
||||||
if (state.currentView === 'gameconfig') {
|
if (state.currentView === 'gameconfig') {
|
||||||
|
|
|
||||||
|
|
@ -180,12 +180,19 @@
|
||||||
<span id="gc-prefix"></span>
|
<span id="gc-prefix"></span>
|
||||||
<p class="subtitle" id="gc-prefix-lock-hint" data-t="config.prefixLockedHint" style="display:none"></p>
|
<p class="subtitle" id="gc-prefix-lock-hint" data-t="config.prefixLockedHint" style="display:none"></p>
|
||||||
|
|
||||||
|
<label class="check-label">
|
||||||
|
<input type="checkbox" id="gc-disable-umu">
|
||||||
|
<span data-t="config.disableUmu"></span>
|
||||||
|
</label>
|
||||||
|
<p class="subtitle" data-t="config.disableUmuHint"></p>
|
||||||
|
|
||||||
<label class="field-label" data-t="config.env"></label>
|
<label class="field-label" data-t="config.env"></label>
|
||||||
<textarea id="gc-env" rows="3" data-ph="config.envHint"></textarea>
|
<textarea id="gc-env" rows="3" data-ph="config.envHint"></textarea>
|
||||||
|
|
||||||
<label class="field-label" data-t="config.dllOverrides"></label>
|
<label class="field-label" data-t="config.dllOverrides"></label>
|
||||||
<div id="gc-overrides" class="overrides"></div>
|
<div id="gc-overrides" class="overrides"></div>
|
||||||
<button class="button" id="gc-override-add" data-t="config.overrides.add"></button>
|
<button class="button" id="gc-override-add" data-t="config.overrides.add"></button>
|
||||||
|
<p class="subtitle" data-t="config.overrides.hint"></p>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="button danger" id="btn-gc-clear-playtime" data-t="config.clearPlaytime"></button>
|
<button class="button danger" id="btn-gc-clear-playtime" data-t="config.clearPlaytime"></button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const fsp = require('fs').promises
|
const fs = require('fs')
|
||||||
|
const fsp = fs.promises
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { DIRS, resolvePortablePath } = require('./paths')
|
const { DIRS, resolvePortablePath } = require('./paths')
|
||||||
const { newGame, newGameId, addGame } = require('./library')
|
const { newGame, newGameId, addGame } = require('./library')
|
||||||
|
|
@ -82,8 +83,22 @@ async function importGameFromPrefix(prefix, srcDir, onProgress) {
|
||||||
const driveRoots = prefixDriveRoots(prefix)
|
const driveRoots = prefixDriveRoots(prefix)
|
||||||
if (!driveRoots.length) return { ok: false, error: 'no-drive-c' }
|
if (!driveRoots.length) return { ok: false, error: 'no-drive-c' }
|
||||||
const src = path.resolve(String(srcDir))
|
const src = path.resolve(String(srcDir))
|
||||||
|
// realpath on both sides: a unified prefix reaches drive_c through the pfx
|
||||||
|
// symlink too, and that path is the same folder (see ensureUnifiedPrefix)
|
||||||
|
let realSrc
|
||||||
|
try {
|
||||||
|
realSrc = fs.realpathSync(src)
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: 'outside-drive-c' }
|
||||||
|
}
|
||||||
const insideDrive = driveRoots.some(root => {
|
const insideDrive = driveRoots.some(root => {
|
||||||
const rel = path.relative(root, src)
|
let realRoot
|
||||||
|
try {
|
||||||
|
realRoot = fs.realpathSync(root)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const rel = path.relative(realRoot, realSrc)
|
||||||
return rel && !rel.startsWith('..') && !path.isAbsolute(rel)
|
return rel && !rel.startsWith('..') && !path.isAbsolute(rel)
|
||||||
})
|
})
|
||||||
if (!insideDrive) return { ok: false, error: 'outside-drive-c' }
|
if (!insideDrive) return { ok: false, error: 'outside-drive-c' }
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,9 @@ function newGame({ id, name, exePath, workdir, mode }) {
|
||||||
prefix: '',
|
prefix: '',
|
||||||
args: '',
|
args: '',
|
||||||
dllOverrides: [],
|
dllOverrides: [],
|
||||||
|
// Proton-only switch: keep UMU (the default path) or fall back to the
|
||||||
|
// build's steam.exe launcher for this app (see runner.buildEnv)
|
||||||
|
disableUmu: false,
|
||||||
env: {},
|
env: {},
|
||||||
iconSource: '',
|
iconSource: '',
|
||||||
playSeconds: 0,
|
playSeconds: 0,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ function makeTarget(openPath, wineId, prefixName) {
|
||||||
prefix: prefixName || '',
|
prefix: prefixName || '',
|
||||||
args: '',
|
args: '',
|
||||||
dllOverrides: [],
|
dllOverrides: [],
|
||||||
|
disableUmu: false,
|
||||||
env: {},
|
env: {},
|
||||||
iconSource: '',
|
iconSource: '',
|
||||||
playSeconds: 0,
|
playSeconds: 0,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ function seed() {
|
||||||
if (!defs || !Array.isArray(defs.prefixes) || !defs.prefixes.length) {
|
if (!defs || !Array.isArray(defs.prefixes) || !defs.prefixes.length) {
|
||||||
const seeded = { default: MAIN, prefixes: [{ name: MAIN, created: Date.now(), locked: true }] }
|
const seeded = { default: MAIN, prefixes: [{ name: MAIN, created: Date.now(), locked: true }] }
|
||||||
if (!fs.existsSync(prefixPath(MAIN))) fs.mkdirSync(prefixPath(MAIN), { recursive: true })
|
if (!fs.existsSync(prefixPath(MAIN))) fs.mkdirSync(prefixPath(MAIN), { recursive: true })
|
||||||
|
ensureUnifiedPrefix(MAIN)
|
||||||
writeJson(REGISTRY, seeded)
|
writeJson(REGISTRY, seeded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,22 +43,65 @@ function prefixExists(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every Windows C: drive a prefix has: plain wine keeps one at the top
|
// Every Windows C: drive a prefix has: plain wine keeps one at the top
|
||||||
// (WINEPREFIX=<prefix>), Proton-managed prefixes nest one under pfx/
|
// (WINEPREFIX=<prefix>), legacy Proton-managed prefixes nest one under pfx/
|
||||||
// (WINEPREFIX=<prefix>/pfx). A prefix used with BOTH wine kinds has both,
|
// (WINEPREFIX=<prefix>/pfx). A prefix used with BOTH wine kinds has both, and
|
||||||
// and an import may come from either one - so callers that can accept
|
// an import may come from either one - so callers that can accept several get
|
||||||
// several get them all, top level first.
|
// them all, top level first. A unified prefix (pfx -> .) reaches the same
|
||||||
|
// drive twice; it is reported once.
|
||||||
function prefixDriveRoots(name) {
|
function prefixDriveRoots(name) {
|
||||||
const root = prefixPath(name)
|
const root = prefixPath(name)
|
||||||
const found = []
|
const found = []
|
||||||
|
const seen = new Set()
|
||||||
for (const base of [root, path.join(root, 'pfx')]) {
|
for (const base of [root, path.join(root, 'pfx')]) {
|
||||||
try {
|
try {
|
||||||
const driveC = path.join(base, 'drive_c')
|
const driveC = path.join(base, 'drive_c')
|
||||||
if (fs.statSync(driveC).isDirectory()) found.push(driveC)
|
if (!fs.statSync(driveC).isDirectory()) continue
|
||||||
|
const real = fs.realpathSync(driveC)
|
||||||
|
if (seen.has(real)) continue
|
||||||
|
seen.add(real)
|
||||||
|
found.push(driveC)
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lstatOrNull(p) {
|
||||||
|
try {
|
||||||
|
return fs.lstatSync(p)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wine keeps its C: drive at <prefix>/drive_c, Proton always runs its prefix
|
||||||
|
// from <compatdata>/pfx - and PLWL points compatdata at the launcher's prefix
|
||||||
|
// folder, so without a link a prefix used with both wine kinds grows TWO C:
|
||||||
|
// drives with separate registries, saves and installed DLLs. Making `pfx` a
|
||||||
|
// relative symlink back to the prefix itself collapses them: Proton's
|
||||||
|
// <compatdata>/pfx resolves to the prefix root, the exact folder wine uses.
|
||||||
|
// Every walker PLWL owns (fsutil, symlinks, LAN sync) treats symlinks as
|
||||||
|
// links and never follows them, so the self-reference is safe.
|
||||||
|
//
|
||||||
|
// Only a prefix that has no pfx AT ALL gets the link - that covers a fresh
|
||||||
|
// prefix and a plain-wine prefix first used through Proton. An existing
|
||||||
|
// layout is left exactly as it is: a prefix from an older PLWL keeps its real
|
||||||
|
// pfx/ folder (and here the two wine kinds keep two C: drives), and a prefix
|
||||||
|
// with both real drive_c trees keeps them - moving Windows installs around
|
||||||
|
// behind the user's back loses data and breaks the absolute/relative links
|
||||||
|
// inside them.
|
||||||
|
function ensureUnifiedPrefix(name) {
|
||||||
|
if (!name) return false
|
||||||
|
const pfx = path.join(prefixPath(name), 'pfx')
|
||||||
|
if (lstatOrNull(pfx)) return false
|
||||||
|
try {
|
||||||
|
fs.symlinkSync('.', pfx, 'dir')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// the prefix folder may not exist yet - the link is added on creation
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// True when a path IS one of a prefix's own roots: the prefix folder, its
|
// True when a path IS one of a prefix's own roots: the prefix folder, its
|
||||||
// pfx/ or either drive_c. A copy/move import of such a folder would move the
|
// pfx/ or either drive_c. A copy/move import of such a folder would move the
|
||||||
// prefix's C: drive (or the whole prefix) into files/apps, gutting it - the
|
// prefix's C: drive (or the whole prefix) into files/apps, gutting it - the
|
||||||
|
|
@ -82,6 +126,7 @@ function createPrefix(name) {
|
||||||
if (clean.toLowerCase() === MAIN.toLowerCase()) return { ok: false, error: 'alreadyExists' }
|
if (clean.toLowerCase() === MAIN.toLowerCase()) return { ok: false, error: 'alreadyExists' }
|
||||||
if (fs.existsSync(prefixPath(clean))) return { ok: false, error: 'alreadyExists' }
|
if (fs.existsSync(prefixPath(clean))) return { ok: false, error: 'alreadyExists' }
|
||||||
fs.mkdirSync(prefixPath(clean), { recursive: true })
|
fs.mkdirSync(prefixPath(clean), { recursive: true })
|
||||||
|
ensureUnifiedPrefix(clean)
|
||||||
const defs = listPrefixes()
|
const defs = listPrefixes()
|
||||||
defs.prefixes.push({ name: clean, created: Date.now() })
|
defs.prefixes.push({ name: clean, created: Date.now() })
|
||||||
if (!defs.prefixes.some(p => p.name === defs.default)) defs.default = clean
|
if (!defs.prefixes.some(p => p.name === defs.default)) defs.default = clean
|
||||||
|
|
@ -153,4 +198,4 @@ function setDefaultPrefix(name) {
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { listPrefixes, savePrefixes, createPrefix, deletePrefix, renamePrefix, setDefaultPrefix, prefixPath, prefixExists, prefixDriveRoots, isPrefixRoot, healPrefix, MAIN }
|
module.exports = { listPrefixes, savePrefixes, createPrefix, deletePrefix, renamePrefix, setDefaultPrefix, prefixPath, prefixExists, prefixDriveRoots, isPrefixRoot, healPrefix, ensureUnifiedPrefix, MAIN }
|
||||||
|
|
|
||||||
144
src/runner.js
144
src/runner.js
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
const { spawn } = require('child_process')
|
const { spawn } = require('child_process')
|
||||||
const { DIRS } = require('./paths')
|
const { DIRS } = require('./paths')
|
||||||
const { prefixPath } = require('./prefixes')
|
const { prefixPath, ensureUnifiedPrefix } = require('./prefixes')
|
||||||
const { prefixNameForGame } = require('./library')
|
const { prefixNameForGame } = require('./library')
|
||||||
|
|
||||||
// The fake Steam install Proton is pointed at through
|
// The fake Steam install PLWL falls back to when no real Steam client is
|
||||||
// STEAM_COMPAT_CLIENT_INSTALL_PATH. Both folders Proton links into its prefix
|
// found (see findSteamClientDir). Both folders Proton links into its prefix
|
||||||
// have to exist: it symlinks <prefix>/drive_c/Program Files (x86)/Steam/
|
// have to exist: it symlinks <prefix>/drive_c/Program Files (x86)/Steam/
|
||||||
// appcache -> <fake>/appcache while setting the prefix up, and a link into a
|
// appcache -> <fake>/appcache while setting the prefix up, and a link into a
|
||||||
// missing folder is a dangling link in a tree that gets synced.
|
// missing folder is a dangling link in a tree that gets synced.
|
||||||
|
|
@ -19,6 +20,73 @@ function ensureFakeSteam() {
|
||||||
return fake
|
return fake
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proton expects the Steam client it is normally started by at
|
||||||
|
// STEAM_COMPAT_CLIENT_INSTALL_PATH: it copies the client's legacy DLLs from
|
||||||
|
// <client>/legacycompat into the prefix, its built-in steam.exe loads the
|
||||||
|
// client's own steamclient.so, and media libs come from <client>/ubuntu12_*.
|
||||||
|
// A client installed on this machine is worth using (read-only) - without one
|
||||||
|
// a Valve build's launch dies in a "steamclient_main.c" assert dialog - and
|
||||||
|
// only when there is none does PLWL fall back to the empty fake folder. GE
|
||||||
|
// builds don't need any of it: their UMU path never starts steam.exe.
|
||||||
|
const STEAM_CLIENT_MARKERS = [
|
||||||
|
['legacycompat', 'steamclient.dll'],
|
||||||
|
['ubuntu12_32', 'steamclient.so']
|
||||||
|
]
|
||||||
|
|
||||||
|
function findSteamClientDir() {
|
||||||
|
const home = os.homedir()
|
||||||
|
const dataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share')
|
||||||
|
const candidates = [
|
||||||
|
process.env.STEAM_COMPAT_CLIENT_INSTALL_PATH,
|
||||||
|
path.join(home, '.steam', 'steam'),
|
||||||
|
path.join(home, '.steam', 'root'),
|
||||||
|
path.join(dataHome, 'Steam'),
|
||||||
|
// flatpak Steam keeps its install inside the sandbox's data dir
|
||||||
|
path.join(home, '.var', 'app', 'com.valvesoftware.Steam', '.local', 'share', 'Steam')
|
||||||
|
]
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (!dir) continue
|
||||||
|
for (const marker of STEAM_CLIENT_MARKERS) {
|
||||||
|
if (fs.existsSync(path.join(dir, ...marker))) return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function steamClientDir() {
|
||||||
|
return findSteamClientDir() || ensureFakeSteam()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module names travel in a semicolon-separated WINEDLLOVERRIDES list where
|
||||||
|
// '=' splits name from setting and ',' separates names - anything else in a
|
||||||
|
// name would corrupt the entries around it. A trailing .dll is what guides
|
||||||
|
// write; Wine strips it itself, so PLWL stores the bare name.
|
||||||
|
function sanitizeDllName(name) {
|
||||||
|
const clean = String(name || '').trim().replace(/\.dll$/i, '')
|
||||||
|
return /^[^;=\s]+$/.test(clean) ? clean : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wine replaces an earlier entry for the same module with a later one, so an
|
||||||
|
// inherited WINEDLLOVERRIDES goes FIRST and the app's own overrides are
|
||||||
|
// appended after it. An empty setting is Wine's "disabled" and has to survive
|
||||||
|
// as `name=`, not be dropped. Proton appends its own dxvk/nvapi/steam.exe
|
||||||
|
// defaults after everything - an app override for one of those modules only
|
||||||
|
// takes effect where Proton itself has no default.
|
||||||
|
function dllOverridesFor(game, env) {
|
||||||
|
const parts = []
|
||||||
|
const inherited = String(env.WINEDLLOVERRIDES || '').trim().replace(/;+$/, '')
|
||||||
|
if (inherited) parts.push(inherited)
|
||||||
|
for (const entry of game.dllOverrides || []) {
|
||||||
|
if (!entry) continue
|
||||||
|
const dll = sanitizeDllName(entry.dll)
|
||||||
|
if (!dll) continue
|
||||||
|
const setting = String(entry.setting == null ? '' : entry.setting).trim()
|
||||||
|
if (!/^[a-z,]*$/i.test(setting)) continue
|
||||||
|
parts.push(`${dll}=${setting}`)
|
||||||
|
}
|
||||||
|
return parts.join(';')
|
||||||
|
}
|
||||||
|
|
||||||
// Wine only ever repairs links that are BROKEN: a prefix that came from
|
// Wine only ever repairs links that are BROKEN: a prefix that came from
|
||||||
// another machine (or from an older PLWL) can still hold absolute links into
|
// another machine (or from an older PLWL) can still hold absolute links into
|
||||||
// its build that happen to resolve here, and those would be synced out again.
|
// its build that happen to resolve here, and those would be synced out again.
|
||||||
|
|
@ -74,23 +142,41 @@ function buildEnv({ game, wine }) {
|
||||||
const isProton = wine && wine.type === 'proton'
|
const isProton = wine && wine.type === 'proton'
|
||||||
// same fallback the games service snapshots into its running entries
|
// same fallback the games service snapshots into its running entries
|
||||||
// (library.prefixNameForGame), so in-use checks always name this folder
|
// (library.prefixNameForGame), so in-use checks always name this folder
|
||||||
const prefixDir = prefixPath(prefixNameForGame(game))
|
const prefixName = prefixNameForGame(game)
|
||||||
|
// a prefix with no pfx yet gets the shared-link layout, so every launch -
|
||||||
|
// wine, Proton, tools - lands on that prefix's ONE C: drive; an existing
|
||||||
|
// nested layout is left as it is (see ensureUnifiedPrefix)
|
||||||
|
try {
|
||||||
|
ensureUnifiedPrefix(prefixName)
|
||||||
|
} catch {}
|
||||||
|
const prefixDir = prefixPath(prefixName)
|
||||||
|
|
||||||
if (isProton) {
|
if (isProton) {
|
||||||
const fakeSteam = ensureFakeSteam()
|
|
||||||
const buildRoot = path.resolve(wine.path, '..')
|
|
||||||
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = fakeSteam
|
|
||||||
env.STEAM_COMPAT_DATA_PATH = path.join(DIRS.DATA, 'compat', game.id)
|
env.STEAM_COMPAT_DATA_PATH = path.join(DIRS.DATA, 'compat', game.id)
|
||||||
env.STEAM_COMPAT_APP_ID = '0'
|
env.STEAM_COMPAT_APP_ID = '0'
|
||||||
env.SteamAppId = '0'
|
env.SteamAppId = '0'
|
||||||
env.SteamGameId = '0'
|
env.SteamGameId = '0'
|
||||||
// Launch like the UMU protocol does (UMU/PortProton/Heroic): with UMU_ID
|
// Proton's session init reads STEAM_COMPAT_CLIENT_INSTALL_PATH on EVERY
|
||||||
// set, newer Protons (incl. DW Proton 11, GE-Proton 10+) run the game via
|
// verb: its appcache sync (g_compatdata.sync_appcache) runs even for
|
||||||
// their built-in umu.exe instead of the steam.exe helper, which would
|
// runinprefix and does os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] as
|
||||||
// otherwise try to load Steam's steamclient.so and crash with an
|
// soon as SteamGameId is set, so the variable must never be dropped - a
|
||||||
// "Assertion failed \"!status\"" dialog (steamclient_main.c) when no
|
// missing one aborts the launch with a KeyError before wine even starts.
|
||||||
// Steam client is installed.
|
// Only what it POINTS AT depends on the mode: a UMU run may use a real
|
||||||
if (!env.UMU_ID) env.UMU_ID = 'umu-0'
|
// client found on this machine, a disableUmu run always gets the empty
|
||||||
|
// fake folder (runinprefix copies nothing out of it; no steam.exe and no
|
||||||
|
// umu.exe is started there).
|
||||||
|
if (game.disableUmu) {
|
||||||
|
delete env.UMU_ID
|
||||||
|
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = ensureFakeSteam()
|
||||||
|
} else {
|
||||||
|
env.STEAM_COMPAT_CLIENT_INSTALL_PATH = steamClientDir()
|
||||||
|
// Launch like the UMU protocol does (UMU/PortProton/Heroic): with UMU_ID
|
||||||
|
// set, newer Protons (incl. GE-Proton 9+) run the game through their
|
||||||
|
// built-in umu.exe instead of the steam.exe helper, which needs the
|
||||||
|
// Steam client's DLLs and dies in a "steamclient_main.c" assert dialog
|
||||||
|
// without them (see steamClientDir).
|
||||||
|
if (!env.UMU_ID) env.UMU_ID = 'umu-0'
|
||||||
|
}
|
||||||
env.WINEPREFIX = path.join(env.STEAM_COMPAT_DATA_PATH, 'pfx')
|
env.WINEPREFIX = path.join(env.STEAM_COMPAT_DATA_PATH, 'pfx')
|
||||||
env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs')
|
env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs')
|
||||||
fs.mkdirSync(env.PROTON_LOG_DIR, { recursive: true })
|
fs.mkdirSync(env.PROTON_LOG_DIR, { recursive: true })
|
||||||
|
|
@ -108,16 +194,25 @@ function buildEnv({ game, wine }) {
|
||||||
env.WINEPREFIX = prefixDir
|
env.WINEPREFIX = prefixDir
|
||||||
}
|
}
|
||||||
|
|
||||||
env.WINEDLLOVERRIDES = (game.dllOverrides || [])
|
const overrides = dllOverridesFor(game, env)
|
||||||
.filter(o => o && o.dll && o.setting)
|
if (overrides) env.WINEDLLOVERRIDES = overrides
|
||||||
.map(o => `${o.dll}=${o.setting}`)
|
else delete env.WINEDLLOVERRIDES
|
||||||
.join(';')
|
|
||||||
if (!env.WINEDLLOVERRIDES) delete env.WINEDLLOVERRIDES
|
|
||||||
|
|
||||||
|
// the app's own environment comes last: it overrides everything above,
|
||||||
|
// including a WINEDLLOVERRIDES configured here
|
||||||
for (const [k, v] of Object.entries(game.env || {})) env[k] = v
|
for (const [k, v] of Object.entries(game.env || {})) env[k] = v
|
||||||
return { env, prefixDir }
|
return { env, prefixDir }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proton verbs: 'run' goes through the build's umu.exe/steam.exe launcher,
|
||||||
|
// while 'runinprefix' runs the build's wine directly (same session env, no
|
||||||
|
// umu/steam launcher ever starts - see game.disableUmu). runinprefix also skips
|
||||||
|
// Proton's prefix setup, so a prefix that never saw a normal Proton run stays
|
||||||
|
// a plain wine prefix instead of gaining Proton's default DLLs.
|
||||||
|
function protonVerb(game) {
|
||||||
|
return game.disableUmu ? 'runinprefix' : 'run'
|
||||||
|
}
|
||||||
|
|
||||||
function parseArgs(args) {
|
function parseArgs(args) {
|
||||||
const out = []
|
const out = []
|
||||||
const re = /"([^"]*)"|'([^']*)'|(\S+)/g
|
const re = /"([^"]*)"|'([^']*)'|(\S+)/g
|
||||||
|
|
@ -167,7 +262,7 @@ async function runGame({ game, wine, systemWine, onExit }) {
|
||||||
let base
|
let base
|
||||||
if (isProton) {
|
if (isProton) {
|
||||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||||
base = ['run', exe, ...argList]
|
base = [protonVerb(game), exe, ...argList]
|
||||||
} else {
|
} else {
|
||||||
cmd = systemWine ? 'wine' : wine.wineBin
|
cmd = systemWine ? 'wine' : wine.wineBin
|
||||||
base = [exe, ...argList]
|
base = [exe, ...argList]
|
||||||
|
|
@ -200,6 +295,13 @@ async function runGame({ game, wine, systemWine, onExit }) {
|
||||||
started = true
|
started = true
|
||||||
const logFile = latestLogPath(game.id)
|
const logFile = latestLogPath(game.id)
|
||||||
const ws = openLatestLog(game.id)
|
const ws = openLatestLog(game.id)
|
||||||
|
// a launch line of our own before the raw wine output: when a game
|
||||||
|
// refuses to start, "which prefix, which overrides, umu or steam.exe"
|
||||||
|
// is exactly what has to be answerable from the log
|
||||||
|
ws.write(`[plwl] ${cmd} ${base.join(' ')}\n`)
|
||||||
|
ws.write(`[plwl] WINEPREFIX=${env.WINEPREFIX}\n`)
|
||||||
|
if (env.WINEDLLOVERRIDES) ws.write(`[plwl] WINEDLLOVERRIDES=${env.WINEDLLOVERRIDES}\n`)
|
||||||
|
if (isProton) ws.write(`[plwl] proton launcher: ${game.disableUmu ? 'direct wine (no UMU, no Steam)' : `umu.exe (UMU_ID=${env.UMU_ID})`}\n`)
|
||||||
child.stdout.pipe(ws)
|
child.stdout.pipe(ws)
|
||||||
child.stderr.pipe(ws)
|
child.stderr.pipe(ws)
|
||||||
child.unref()
|
child.unref()
|
||||||
|
|
@ -237,7 +339,7 @@ async function runTool({ game, wine, tool, args, systemWine }) {
|
||||||
let base
|
let base
|
||||||
if (isProton) {
|
if (isProton) {
|
||||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||||
base = ['run', tool, ...argList]
|
base = [protonVerb(game), tool, ...argList]
|
||||||
} else if (systemWine) {
|
} else if (systemWine) {
|
||||||
cmd = tool
|
cmd = tool
|
||||||
base = argList
|
base = argList
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,11 @@
|
||||||
//
|
//
|
||||||
// Everything here rewires a link to a RELATIVE path when the file it points at
|
// 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
|
// can be found locally, so the tree stays relocatable forever, and restores
|
||||||
// the executable bit on the binaries a wine/proton build needs.
|
// 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.
|
||||||
|
|
||||||
const fsp = require('fs').promises
|
const fsp = require('fs').promises
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
|
@ -116,10 +120,11 @@ function relativeTarget(link, target, roots) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewire every absolute symlink under `root` that can be expressed relative to
|
// Rewire the symlinks under `root` that can be expressed relative to `roots`
|
||||||
// `roots` (usually [root, DIRS.FILES]). Returns how many links changed. A link
|
// (usually [root, DIRS.FILES]): absolute ones always, relative ones only when
|
||||||
// may only become rewirable once another link it chains through has been
|
// they no longer resolve. Returns how many links changed. A link may only
|
||||||
// fixed, so the pass repeats until nothing changes.
|
// become rewirable once another link it chains through has been fixed, so the
|
||||||
|
// pass repeats until nothing changes.
|
||||||
async function rewireSymlinks(root, roots = []) {
|
async function rewireSymlinks(root, roots = []) {
|
||||||
if (!root) return 0
|
if (!root) return 0
|
||||||
const candidates = [root, ...roots.filter(r => r && r !== root)]
|
const candidates = [root, ...roots.filter(r => r && r !== root)]
|
||||||
|
|
@ -135,8 +140,13 @@ async function rewireSymlinks(root, roots = []) {
|
||||||
} catch {
|
} catch {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!path.isAbsolute(target)) continue // already relocatable
|
const absolute = path.isAbsolute(target)
|
||||||
const next = relativeTarget(link, path.normalize(target), candidates)
|
// 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
|
||||||
|
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
|
if (!next || next === target) continue
|
||||||
try {
|
try {
|
||||||
await fsp.unlink(link)
|
await fsp.unlink(link)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue