From fa7008ffb7ac8d960eadc49b91a0201710930e41 Mon Sep 17 00:00:00 2001 From: mattchristenson Date: Tue, 29 Sep 2026 23:52:23 -0500 Subject: [PATCH] fix: back up the stylesheet next to it, and don't copy read-only permissions patchCss() named the backup after this.configMgr.stylesheetFileName, which doesn't exist, so it went to "undefined.bak" in GNOME Shell's working directory (the home folder). Both copies took their source's permissions: with a read-only install (e.g. NixOS) the user's stylesheet and the backup became read-only, and at the next update the copy failed with "Permission denied" and Forge didn't start. The backup is stylesheet.css.bak next to the stylesheet, the copies get default permissions, a read-only copy left by an earlier version is replaced, and a failed update is logged instead of stopping Forge. Fixes #266 Co-Authored-By: Claude Opus 5.5 --- lib/shared/theme.js | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/shared/theme.js b/lib/shared/theme.js index ae14422e..1de50550 100644 --- a/lib/shared/theme.js +++ b/lib/shared/theme.js @@ -196,17 +196,50 @@ export class ThemeManagerBase extends GObject.Object { if (this._needUpdate()) { let originalCss = this.configMgr.defaultStylesheetFile; let configCss = this.configMgr.stylesheetFile; - let copyConfigCss = Gio.File.new_for_path(this.configMgr.stylesheetFileName + ".bak"); - let backupFine = configCss.copy(copyConfigCss, Gio.FileCopyFlags.OVERWRITE, null, null); - let copyFine = originalCss.copy(configCss, Gio.FileCopyFlags.OVERWRITE, null, null); - if (backupFine && copyFine) { - this.settings.set_uint("css-last-update", this.cssTag); - return true; + if (!originalCss || !configCss) return false; + // The backup goes next to the user's stylesheet (it went to "undefined.bak" in the + // working directory, #266) + let copyConfigCss = Gio.File.new_for_path(`${configCss.get_path()}.bak`); + try { + let backupFine = this._copyFile(configCss, copyConfigCss); + let copyFine = this._copyFile(originalCss, configCss); + if (backupFine && copyFine) { + this.settings.set_uint("css-last-update", this.cssTag); + return true; + } + } catch (e) { + console.warn(`Forge: can't update the stylesheet ${configCss.get_path()}: ${e.message}`); } } return false; } + /** + * Copies `source` over `target`. The copy gets the usual permissions of a new file, not those + * of `source`: the extension's own files can be read-only (e.g. on NixOS), and a read-only copy + * couldn't be overwritten at the next update (#266). A read-only copy left by an earlier + * version is replaced. + * @param {Gio.File} source + * @param {Gio.File} target + */ + _copyFile(source, target) { + const flags = Gio.FileCopyFlags.OVERWRITE | Gio.FileCopyFlags.TARGET_DEFAULT_PERMS; + try { + return source.copy(target, flags, null, null); + } catch (e) { + // (only a target that exists and is read-only: not when the source can't be read) + const readOnlyTarget = + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PERMISSION_DENIED) && + target.query_exists(null) && + !target + .query_info("access::can-write", Gio.FileQueryInfoFlags.NONE, null) + .get_attribute_boolean("access::can-write"); + if (!readOnlyTarget) throw e; + target.delete(null); + return source.copy(target, flags, null, null); + } + } + /** * Credits: ExtensionSystem.js:_callExtensionEnable() */