8789d2bbb stopped publishing PYTHONPATH when launching lutris-wrapper,
on the assumption the wrapper's own sys.path bootstrap covered every
case it needed to. It didn't: anyone running `./bin/lutris` from a
git checkout without installing Lutris would now hit
ModuleNotFoundError: No module named 'lutris'
at the wrapper's `from lutris.util.log import logger` line, before
the wrapper had a chance to do anything.
Two compounding causes:
1. The wrapper's bootstrap code lived inside `if __name__ == "__main__"`,
meaning it ran AFTER the top-level `from lutris...` imports. The
imports had to succeed first, and they were silently relying on the
parent process having injected PYTHONPATH=":".join(sys.path) into
the env before exec.
2. Even when it did run, the bootstrap's source-mode detection was
checking `LAUNCH_PATH/../lutris` — i.e. `share/lutris/lutris/` —
which has never existed. The check fell through to a second
never-existent path. So the bootstrap was, in practice, dead code
regardless of when it ran.
This commit fixes both:
* Move the source-tree detection above the lutris imports and look
three directories up (`share/lutris/bin/` → repo root), where the
`lutris/` package actually lives. Confirm via the package's
`__init__.py` rather than just a directory match.
* Restore `env["PYTHONPATH"] = ":".join(sys.path)` in monitored_command
as defense-in-depth. The wrapper now runs on the same Python as
Lutris (sys.executable, via the existing 8789d2bbb change), so the
inherited PYTHONPATH is ABI-compatible; and lutris-wrapper deletes
PYTHONPATH from os.environ before spawning the game, so the leak
doesn't reach Wine, umu-launcher, or any other runner subprocess.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stale build/ output from setup.py build created a duplicate lutris
package that caused mypy to bail before checking anything.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MonitoredCommand previously launched lutris-wrapper via its
`#!/usr/bin/env python3` shebang and worked around the resulting
ambiguity by exporting PYTHONPATH=":".join(sys.path) on the subprocess
env. The original author flagged the PYTHONPATH line as suspect
("not clear why this needs to be added"); the actual reason was that
in scenarios where the shebang picks up a different Python than the
one running Lutris (an AppImage with a bundled interpreter, a venv
launched from outside, etc.), the wrapper couldn't import lutris's
modules without that hint.
Two problems with that workaround:
1. When the shebang resolves to a host Python with a different minor
version, the inherited PYTHONPATH points at our stdlib, and host
Python crashes during site initialization with a `_sre` MAGIC
mismatch (a .pth file does `import importlib`, finds ours via
PYTHONPATH, ours doesn't match the host interpreter's compiled
constants).
2. PYTHONPATH propagates to every subprocess the wrapper then spawns,
poisoning Wine, umu-launcher, RetroArch, every emulator runner —
none of which need Lutris's sys.path and many of which break in
subtle ways when handed an inappropriate one.
Invoke the wrapper as [sys.executable, WRAPPER_SCRIPT, ...] so the
interpreter is unambiguous, and drop the PYTHONPATH export. sys.path
is naturally inherited by the wrapper since it's running the same
Python, and host subprocesses get a clean environment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
xrandr is the legacy fallback for `LegacyDisplayManager` when Mutter's
display config service isn't available (notably KDE/Wayland). Without
it the resolution dropdown collapses to a single dummy entry (users
have to hand-enter the value they want), and per-monitor info is
unavailable. The old "xrandr didn't return anything" log didn't tell
users what was wrong or how to fix it.
Wrap the lookup in a @cache_single helper so a clear "install
x11-xserver-utils / xrandr" warning fires exactly once per session,
then return the cached path (or None) on subsequent calls.
Refs #6707.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The lutris-wrapper subprocess imports lutris.util.log just for the logger,
but log.py was importing Gtk at module scope solely to annotate the
LOG_BUFFERS dict value type. That forced every wrapper invocation to load
the Gtk typelib (and transitively libgtk-4.so.1).
Under games configured with older bundled Wine runtimes, the wrapper's
LD_LIBRARY_PATH puts an older libgstvideo ahead of the system one, and
libgtk-4.so.1 then fails to resolve gst_video_info_dma_drm_to_video_info,
which surfaces in Python as an AssertionError in gi.overrides.Gdk
(g_type != TYPE_NONE) and crashes the wrapper before the game can start.
Move Gtk under TYPE_CHECKING and quote the annotation so log.py's runtime
import graph stays GTK-free. Process monitors have no UI need for Gtk.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Launching while runtime or wine component downloads are still in
progress can crash games or corrupt their Wine prefix. Game.launch()
now defers to LaunchUIDelegate.wait_for_component_updates(); the
GUI delegate shows a modal that auto-resolves when the download queue
empties, with Cancel and Launch Anyway as overrides.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two runner classes evaluated host-probing helpers in their class
bodies: atari800 called get_resolutions() (which hits xrandr) and
wine called is_fsync_supported() (which runs ctypes futex probes).
Both ran at module-import time during runner discovery, so any
exception from those probes crashed Lutris startup before the GUI
even loaded — the trigger behind #6707.
"choices" and "default" already support callables via
_evaluate_option; pass the functions themselves and let the widget
generator call them on demand. get_fsync_support is already
@cache_single, so the bool default doesn't repeat work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LINUX_SYSTEM.get() can return None when the command isn't installed,
but its annotation falsely claimed str. That lie hid a crash at
startup on systems without xrandr (e.g. Wayland-only Fedora), where
turn_off_except() and change_resolution() would hand [None] to
subprocess and die in os.path.dirname(None).
Fix the return type to str | None and have both callers early-return
when xrandr isn't available. _get_vidmodes() was already guarded by
an earlier fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
get_update_installers now enumerates goggame-*.info files in the install
dir to find which DLCs are currently installed, and passes them to
gogdl as a comma-separated --dlcs list. This keeps installed DLCs in
sync with base-game patches without surprising the user by pulling in
every owned-but-not-installed DLC (which is what --with-dlcs alone
would do).
DLC enumeration uses goggame-<productId>.info files, which GOG writes
for every installed product regardless of installation method — Lutris
doesn't track DLCs as separate DB rows (extends-based installers skip
creating a game entry), so the install directory is the authoritative
source.
Factored the goggame info discovery into find_gog_config_dir on
lutris.util.gog, extending it to handle Linux depot installs (which
nest the info file under '<gameName>/game/') as well as the Windows
and Linux offline layouts already covered. The duplicate version in
commands.py is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DLCs for depot-installed games previously fell through to the legacy
offline-installer path, which assumes the base game's goginstaller files
and registry state exist — neither holds for depot installs, so the DLC
install either silently no-op'd or required selecting a .sh/.exe by hand.
get_dlc_installers now branches on is_depot_installed and emits installers
that delegate to gogdl with `--with-dlcs --dlcs <id>`. Both flags are
required: gogdl's V2 manager gates ownership lookup on --with-dlcs before
consulting --dlcs, so passing only --dlcs silently returns "Owned dlcs []"
and nothing downloads. A preserve_manifest flag on gogdl_setup keeps the
existing base-game manifest so gogdl diffs old→new and only fetches the
DLC files.
Along the way:
- is_depot_installed now takes install_dir + runner and detects both the
V2 (Windows, shared cache) and Linux (in-tree .gogdl-linux-manifest)
manifest layouts. The Linux marker is nested under gogdl's
installDirectory subfolder, so we check one level down too.
- Factored manifest deletion into clear_stale_manifest so both platforms
get the stale-install safety, not just Windows.
- _find_gog_config_path no longer uses recursive glob, which followed
symlink cycles inside Wine prefixes and hung post-install.
- Log gogdl stdout (previously discarded) and tag stderr lines so
progress parsing can see output from either stream.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Only commit when the with-block exits normally (exc_type is None)
- Roll back on exception to preserve transactional semantics
- Wrap in try/finally so close() is always called even if
commit() or rollback() raises
On Fedora Silverblue, Kinoite, Bazzite, etc., /home is a symlink to
/var/home, and accounts created after first-boot often have $HOME set
to /var/home/<user> directly. Game directories stored under that path
were misclassified as protected by is_removeable(), so the uninstall
dialog refused to delete them.
Extend the existing /var special case to fold /var/home/<user>/* into
the /home/<user>/* logic, preserving the same protections for the user
root and PROTECTED_HOME_FOLDERS entries. Fixes#6685.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MissingGames.update_all_missing() is too expensive to run on every
event for large libraries (it stat()s every game), so it was wired only
to sidebar navigation. That left covers showing stale "missing" badges
between sidebar visits.
Add a synchronous update_one_missing(game_id, path=None) — one stat()
call, fires .updated only on transition — and register it against
GAME_UPDATED and GAME_START in MissingGames.__init__. The handler
reads the live path off the Game rather than the path cache, since
add_to_path_cache() is itself a GAME_UPDATED handler and the cache
isn't guaranteed to have been refreshed yet when ours fires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The openSUSE branch hard-required typelib-1_0-WebKit2-4_0, but the
codebase already supports either 4.0 or 4.1 (matching the alternation
in debian/control). 4.0 is reaching end of life, so a hard pin to it
prevents installs on systems that have moved on to 4.1.
Use a rich `(A or B)` Requires so either typelib satisfies the dep.
Fixes#6669.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Without glib-networking, GIO has no TLS backend, so HTTPS in the
embedded WebKit login views fails silently. This breaks every web
auth flow (EA, GOG, Steam web login, etc.) on systems where it
isn't already pulled in transitively.
Reported in #6664 against the EA App login.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runtime icons are downloaded on startup, but GTK's IconTheme doesn't
pick up the new files until something triggers a rescan — previously
that only happened when something like a sidebar hover bumped the
theme. Icons picked via StockIconImage would sit on their fallback
until then.
Hook into install_runtime_component_updates' completion to call
Gtk.IconTheme.get_default().rescan_if_needed(), which emits the
"changed" signal StockIconImage listens to, so widgets swap over to
the real icons as soon as the download lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The runner install dialog and saved-search edit box picked icons via
has_stock_icon, then handed the chosen name to Gtk.Image/Gtk.Button.
Switch both to StockIconImage so the selection re-runs on theme
changes.
In SearchFiltersBox._add_entry_box, drop the inner "no button if no
candidate resolves" branch — it was a bug; the button should still
appear (with StockIconImage's generic fallback) whenever the caller
asked for one.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A Gtk.Image that picks the first available icon from a candidate list
and re-evaluates the selection when the icon theme changes, so an icon
gained or lost by a theme switch is picked up without a restart.
Used in LutrisSidebar.get_sidebar_icon, SidebarRow.create_button_box,
and the search filters button in LutrisWindow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
xdg-desktop-portal 1.20.4's TrashFile rejects otherwise-trashable files
on some Btrfs layouts (nested subvolumes for /home vs /home/$USER),
returning "No suitable trash directory found". Users on affected
systems can't delete artwork, installers, or other files from Lutris.
Keep the portal as the primary path (still potentially required under Flatpak), but
fall back to Gio.File.trash_async() when the bus proxy can't be
acquired, the D-Bus call errors out, or the portal returns failure.
Downgrade the now-recoverable conditions from error to warning.
Resolves#6647
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`is_safe_to_delete` listed `CacheState.DOWNLOADING` twice in the
protection tuple, omitting `CacheState.DOWNLOADED`. Files in the
DOWNLOADED state (download complete, installation pending) were
therefore not protected and could be deleted before installation
finished.
Regression introduced in #6525.
Adds a nuclear option to kill all Wine processes system-wide,
useful when games or launchers leave orphan processes behind.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The primary causes of the issue is the manipulation of how the gog_cloud.py and gog_cloud_hooks.py was getting imported in UnitTest via the importlib mechanism. That seemed to have broken the resolution of paths to `lutris.services.*`.
As the GTK issues that it was trying to side step has been fixed at the `tests` root level, the workaround is no longer needed.
Additional changes have been added to help harden any potentially UnitTest environment and circular import issues.
* Updated the github unit test run to use a virtual env to install the package dependencies and run the test
* Moved the imports of `lutris.services` subpackage to the method calls inside of the `__init__.py` script which will defer any potential circular imports until after the `lutris/services/__init__.py` script is fully imported.
Why:
* The stubs would need to call super().run(), but this causes the same
error that the stubs were trying to resolve, i.e. `Call to untyped
function "run" in typed context`
On line 32, `"-" in locale` performs a membership test on the `locale` *module* (imported at line 3), not on the `user_locale` string variable. In Python 3, this raises `TypeError: argument of type 'module' is not iterable` at runtime when `get_lang_and_country()` is called, crashing the function. This is both a bug and a reliability issue.
Affected files: i18n.py
Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
The bare `except:` clause on line 18 catches all exceptions, including `SystemExit`, `KeyboardInterrupt`, and other critical exceptions. This can mask serious errors and prevent the process from being properly terminated. An attacker who can influence the settings file could cause unexpected behavior that goes undetected.
Affected files: migrate_hidden_ids.py
Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
Before, clicking Play on a Proton game while umu was fetching GE-Proton
or the steamrt3 runtime looked like nothing was happening — the game
stayed on "Launching" for minutes with no indication that a background
download was underway (issue #6632).
Add a Game.launch_status property that fires a new GAME_LAUNCH_STATUS
notification when assigned. Runners opt in via a new
Runner.attach_log_handlers hook; the wine runner installs a umu log
parser (lives in lutris.util.wine.proton) when the wine exe is umu,
which watches for "Downloading", "Extracting" and "Checking updates"
INFO lines and clears on "Using <Proton>". LutrisWindow mirrors the
status into the existing download queue as a pulsing ProgressBox, which
auto-removes itself once the status clears.
Also widens ProgressInfo.progress to float | None to match the
implementation, which already pulses when it's None.