PLWL/src/paths.js

174 lines
5.7 KiB
JavaScript

'use strict'
const path = require('path')
const fs = require('fs')
// Packaged builds run from <portable root>/olcxja.plwl/resources/app.asar:
// the portable layout (<root>/files) is two levels above the resources dir
// and files bundled with the release are unpacked next to the asar.
function computeRoots() {
const m = __dirname.match(/^(.*)[/\\]app\.asar([/\\]|$)/)
if (m) {
const RESOURCES = path.resolve(m[1])
const APP_ROOT = path.resolve(RESOURCES, '..', '..')
return {
RESOURCES,
APP_ROOT,
// FILES = BUILD/files, so BUILD is the portable root itself.
BUILD: process.env.PLWL_BUILD_DIR || APP_ROOT,
// Bundled release files live inside the asar (Electron's patched fs
// can read them directly from the archive).
BUNDLED: fs.existsSync(path.join(RESOURCES, 'app.asar', 'files', 'translations'))
? path.join(RESOURCES, 'app.asar', 'files')
: path.join(APP_ROOT, 'files')
}
}
const APP_ROOT = path.resolve(__dirname, '..')
return {
RESOURCES: APP_ROOT,
APP_ROOT,
BUILD: process.env.PLWL_BUILD_DIR || path.join(APP_ROOT, 'build'),
BUNDLED: path.join(APP_ROOT, 'files')
}
}
const { RESOURCES, APP_ROOT, BUILD, BUNDLED } = computeRoots()
const FILES = path.join(BUILD, 'files')
const DATA = path.join(FILES, 'data')
const DIRS = {
BUILD,
APP_ROOT,
FILES,
DATA,
WINES: path.join(FILES, 'wines'),
PREFIXES: path.join(FILES, 'wineprefixes'),
APPS: path.join(FILES, 'apps'),
TRANSLATIONS: path.join(FILES, 'translations'),
BUNDLED_TRANSLATIONS: path.join(BUNDLED, 'translations'),
FAKE_STEAM: path.join(DATA, 'fake-steam')
}
// Bundled translation files are launcher-owned: on every start they are
// refreshed in the portable files/translations folder WHENEVER THEIR CONTENT
// CHANGED, so an update shipping new or changed keys reaches existing
// installs too (the old copy-only-if-missing logic froze translations at the
// version that happened to run first). Extra files in the target folder that
// the bundle does not ship (user-added languages) are left untouched.
function filesDiffer(a, b) {
try {
return !fs.readFileSync(a).equals(fs.readFileSync(b))
} catch {
return true // target missing/unreadable -> needs a (re)copy
}
}
function syncBundledTranslations(bundledDir, targetDir) {
let names = []
try {
names = fs.readdirSync(bundledDir)
} catch {
return 0
}
let updated = 0
for (const name of names) {
const src = path.join(bundledDir, name)
const dst = path.join(targetDir, name)
try {
if (!fs.statSync(src).isFile()) continue
} catch {
continue
}
if (!filesDiffer(src, dst)) continue
try {
fs.copyFileSync(src, dst)
updated++
} catch {}
}
return updated
}
function ensureDirs() {
for (const dir of [FILES, DATA, DIRS.WINES, DIRS.PREFIXES, DIRS.APPS, DIRS.TRANSLATIONS, DIRS.FAKE_STEAM]) {
fs.mkdirSync(dir, { recursive: true })
}
if (DIRS.TRANSLATIONS !== DIRS.BUNDLED_TRANSLATIONS) {
syncBundledTranslations(DIRS.BUNDLED_TRANSLATIONS, DIRS.TRANSLATIONS)
}
}
// Portable paths: anything stored under FILES/ goes into the registry as a
// path RELATIVE to FILES (e.g. apps/<id>/game.exe), so moving the whole
// launcher folder keeps the library working. Absolute paths outside FILES
// (games left in place) are stored as-is. Resolve on read, normalize on write.
function toPortablePath(p) {
if (typeof p !== 'string' || !p || !path.isAbsolute(p)) return p
const rel = path.relative(FILES, p)
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return p
return rel
}
function resolvePortablePath(p) {
if (typeof p !== 'string' || !p || path.isAbsolute(p)) return p
return path.join(FILES, p)
}
// The launcher's own icon (renderer/plwl.png), used by the tray, the app
// icon and as the .desktop shortcut fallback.
//
// The module-relative copy comes FIRST: it is the only candidate that exists
// in every layout, because a packaged build keeps renderer/ inside
// app.asar and Electron's patched fs reads straight out of the archive (a
// release only ships olcxja.plwl.png next to the portable root, so looking at
// APP_ROOT alone made the tray icon vanish in installed builds). Cached: it
// never moves at runtime.
function launcherIconCandidates() {
const bundled = path.join(__dirname, '..', 'renderer')
return [
path.join(bundled, 'plwl.png'),
path.join(bundled, 'plwl.svg'),
path.join(APP_ROOT, 'renderer', 'plwl.png'),
path.join(APP_ROOT, 'renderer', 'plwl.svg'),
path.join(APP_ROOT, 'olcxja.plwl.png')
]
}
function firstExisting(candidates) {
for (const candidate of candidates) {
try {
if (fs.existsSync(candidate)) return candidate
} catch {}
}
return null
}
let launcherIcon = null
function launcherIconPath() {
if (launcherIcon === null) launcherIcon = firstExisting(launcherIconCandidates()) || ''
return launcherIcon || null
}
// The same icon, but only as a real file on disk: a .desktop entry's Icon= is
// resolved by the desktop environment, which cannot look inside app.asar.
function launcherIconDiskPath() {
const onDisk = launcherIconCandidates().filter(candidate => !candidate.includes(`.asar${path.sep}`))
return firstExisting(onDisk)
}
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'))
} catch {
return fallback
}
}
function writeJson(file, data) {
fs.mkdirSync(path.dirname(file), { recursive: true })
const tmp = file + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(data, null, 2))
fs.renameSync(tmp, file)
}
module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, launcherIconPath, launcherIconDiskPath, readJson, writeJson, toPortablePath, resolvePortablePath }