Merged UWINE into the repo

This commit is contained in:
tcsenpai 2024-02-25 22:00:47 +01:00
parent 4f6f8eeaed
commit 69a50bf7b7
41 changed files with 864 additions and 3034 deletions

8
.gitignore vendored
View File

@ -1 +1,7 @@
var
launcher
.git
.env
__pycache__
test_launcher
var
_not_needed_

346
README.md Executable file
View File

@ -0,0 +1,346 @@
# ULWGL-LAUNCHER-TCS ( ⾣ UWINE )
The final wrapper for ULWGL (and its launcher) has come to town.
Play and run Windows software with Proton without Steam in a (maybe a couple of) click.
![Hello world](https://raw.githubusercontent.com/tcsenpai/UWINE/main/screenshot.png)
## What is UWINE
UWINE is a wrapper around ULWGL designed to help as many users as possible to run WIndows software (included but not limited to games) using Proton (and its flavors like ProtonGE) without Steam client needed.
## Credits and License
This software is distributed under the MIT license.
The original ULWGL-Launcher repository and its creators (https://github.com/Open-Wine-Components/ULWGL-launcher) are to be credited for all the amazing work they are doing.
This is just an humble wrapper designed for the following reason:
- I am lazy and I don't want to tinker with the terminal each time I want to play a game
- Many people are lazy
- Many others are non tech-savy
## ⭐ Features
- Few dependencies and great flexibility thanks to environmental variables
- Slightly modified ULWGL_Launcher to have a cleaner start
- Can run programs as simply as "uwine program.exe" or "uwine winecfg" for internal tools
- Can be used to "Open with..."
- The Launcher provided can be used to "Open with..." too
- Is fully customizable through both a human readable CLI interface and a practical format for `env` files
- Can execute _launchers_ by using the above `env` files mechanism with "uwine -l launcher_of_game"
- Can be configured through both the `env` file and the CLI interface together (with the CLI interface overriding the `env` file)
- Is modular and so is readable
- Can be expanded easily
- I use it daily
- Is quite nice
## Install
### ⚙ Prerequisites
- Download the last ULWGL Launcher binary release
- _NOTE: this software has been tested with 0.1 RC3 release in mind. You may want to download that version if you encounter problems with different versions._
- Extract it somewhere and ensure that it contains the executables (ULWGL specifically)
- NOTE: ulwgl-run is not needed, as the script uses its own runner
- Not needed of my machine but: ensure to read the README.md to understand ULWGL Launcher prerequisites
### ⏬ Get UWINE
#### ⚙ Download and Configuration
- `git clone https://github.com/thecookingsenpai/UWINE`
- `cd UWINE`
- Now take a moment to have a look at the .env file. It contains some useful tips. You can add custom paths to it, for example, Steam usually install its instances in subfolders of `$HOME/.steam/steam/compatibilitytools.d`
- The .env file works as a launcher that helps you run your programs in the easiest way possible. While is not really needed (see below), the provided example shows how UWINE can run with almost 0 configuration
#### 🕮 A brief explanation: skip if you can't be bothered
It is important to understand how UWINE works. The 'uwine' executable has to fill the following variables:
- PROTONPATH
- string pointing to your custom Proton installation or to the Steam one (see above)
- defaults to [script_dir]/protons
- WINEPREFIX
- string pointing to a custom prefix for wine (aka where savegames and programs are installed)
- defaults to [script_dir]/PREFIX and creates a new one if needed
- GAMEID
- integer representing the GameID in the ULWLG Database
- defaults to 0 which should work for the majority of the apps
- IDS
- string pointing to a .json file containing mappings of game names to game ids
- defaults to [script_dir]/ids.json
- if it doesn't exist, the mapping will be empty
- ULWLGDIR
- string pointing to your ULWLGDIR installation
- defaults to [script_dir]/launcher which is perfectly fine if extract ULWLG_Launcher there (not in a subfoldr)
- FILEPATH
- string pointing to the file to execute
- if not specified, it must be specified from the CLI (see below)
- CUSTOMVARS
- dictionary-like string (JSON compatible) defining any environmental variable you want to set
- can be omitted if you don't need it
- PREDIRECTIVES
- string containing parameters and arguments to be *prepended* to the command (e.g. gamescope)
- defaults to ""
- POSTDIRECTIVES
- string containing parameters and arguments to be *appended* to the command (e.g. -dx11)
- defaults to ""
- can be set using the `-a` flag
**_TIP: You should consider using ProtonUp (or ProtonUp-QT for KDE users) to help you easily install Proton instances in the 'protons' folder or wherever you like the most_**
#### 🧧 Bonus: CUSTOMVARS
While adding custom environmental variables both in your shell or in the env file is supported, UWINE has a mechanism that is designed to help you in setting custom system variables.
By editing the `env` file you want to use, you can add:
```json
CUSTOMVARS="{
"YOUR_VAR":"YOUR_VALUE"
}"
```
The above syntax allows UWINE to present you in a nicer and more organized way your final command.
### 🚀 Launch
Once launched, UWINE will quickly sets the above variables based on either the `.env` file (or the one provided) or/and the command line arguments
After some basic checks (like if the file exists, if PROTONPATH is set...), UWINE will look for the `ulwgl-run` binary and use it to launch the program with the above mentioned variables.
Thats why the next section is really important.
## Command Line Arguments & Launchers
The preferred way to use UWINE is by creating `env` files following the above format and then just run `uwine` to load them.
### Load the .env file
`uwine`
This command will try to load the `.env` file in the script directory.
### Load a custom env file
`uwine -l [your_env_file]`
This command will try to load the env file specified.
### Specify your variables (or mix the two)
`uwine -h`
This command will show you the following:
```bash
usage: uwine [-h] [-l ENVFILE] [-g GAMEID] [-p PROTONPATH] [-i IDS] [-w WINEPREFIX] [-u ULWLGDIR] [-a ADDITIONALARGS] [-v] [filepath]
ULWGL Launcher Wrapper for human beings
positional arguments:
filepath Path to the file to be launched
options:
-h, --help show this help message and exit
-l ENVFILE, --load ENVFILE
Load a specific env file
-g GAMEID, --game-id GAMEID
Game ID to be used
-p PROTONPATH, --proton-path PROTONPATH
Path to the Proton installation
-i IDS, --ids-json IDS
Path to the ids.json file
-w WINEPREFIX, --wine-prefix WINEPREFIX
Path to the Wine prefix
-u ULWLGDIR, --ulwgl ULWLGDIR
Path to the ULWGL installation
-a ADDITIONALARGS, --additionalargs ADDITIONALARGS
Additional arguments to be passed to the software (as a string)
-v, --version show program's version number and exit
```
The help file is self explanatory.
For example, a possible usage is:
`uwine winecfg -u "/your/hdd/data/ulwgl" -w "/your/hdd/data/ulwgl/PREFIX/" -p "/your/hdd/data/ulwgl/protons/GE-Proton8-32/"`
This will launch the built-in `winecfg` program using the specified variables.
Another example could be:
`uwine -l mygame`
Where `mygame` must be a valid env file as described above.
#### About ADDITIONALARGS
It is often necessary to add additional arguments to your programs (e.g. `-dx11`) to modify their behavior. To help in this, you can simply use the `-a` option specifying a string that will be appended to your launched program.
For example, to launch Palworld with `-dx11`:
`uwine Palworld.exe -a "-dx11"`
Multiple arguments can be chained together in the string such as:
`uwine game.exe -a "-dx11 --skip-launcher"`
In following updates this feature will be also available in the `env` file.
### Optional: the ids.json file
As the excellent tutorial on the ULWGL Launcher repository page explains, ULWGL aims to build a complete, open database of games with the relative protonfixes that should be applied automatically by the launcher itself.
As UWINE's intent is to simplify things, you can skip this part and us the launcher with the default game id `0`. Anyway, experimental game id support is included.
At the moment the `ids.json` contains just an example of how it works.
`{ "gamename": gameid }`
The syntax is simple as the above example. Baldur's Gate 3 has been included as an example. Launching UWINE with:
`uwine game.exe bg3`
Will instruct UWINE to look for "bg3" in the ids.json file. If not found, it will default to `0`. You can also do:
`uwine game.exe 12345`
Where 12345 is a custom game id that you can either invent (but why) or look up on the ULWGL database (https://ulwgl.openwinecomponents.org/).
Howeaver, this is considered quite experimental for both the projects.
### Optional: add this directory to $PATH
To quickly use UWINE, is recommended to add the directory you are using (the ULWGL Launcher directory containing uwine) to the $PATH variable.
You can add this:
`PATH="$PATH:/your/path/to/the/ULWGL/launcher/directory"`
To either your `.profile`, `.bashrc` or equivalent configuration file.
### ??? Profit
Done! You should be able to run
`uwine any_program.exe` from anywhere.
### ✋ Handy stuff: Open With
This repository offers also a preconfigured launcher (`uwine.desktop`) that should work on every modern linux DM/WM. You can put it anywhere (e.g. in your applications menu) and you can configure .exe and .msi (and any other) files to "Open with..." uwine.
You can also set `.uwine` files to open with `uwine -l ` or use the given launcher (`uwine_launcher.desktop`) to launch env files.
If this does not work, you can simply "Open with..." and add a custom command called 'uwine' / 'uwine -l' that launches in the terminal.
### ✋ Handy stuff 2: AIO Package
An All-in-One package is on the way and contains the official ULWGL release tested with uwine and uwine installed in it.
Is not as funny but it should be even more straightforward.
## Issues, bugs, dragons
This is even more experimental than ULWGL itself. Bugs are to be expected.
Please open issues or pr if you encounter some.
Before doing that, though, please ensure that the problem is with UWINE and not with ULWGL or Proton.
For example: some Steam games may fail to launch complaining about the Steam Client not being found. That's an ULWGL / Proton problem. Surely is not an UWINE problem. Probably. Anyway....
### Testing environment and compatibility
This software has been tested on the following system (a laptop):
- KUbuntu 23.10 with Plasma on Wayland
- Python 3.11
- Kernel 6.5, Kernel 6.7.4 Xanmod x64v3 and 6.7.5 Xanmod x64v3
- ProtonGE-8.32 both in protons local folder and in .steam default folder
- ProtonUp 2.8.2
- CPU AMD 7 7730U (Zen 3) with integrated RADEON Graphics (RADV Renoir)
- 16GB RAM (14GB + 2GB VRAM)
- Both _bash_ and _xonsh_ shells
- Both external and internal SSDs
## Roadmap
- Testing, testing, testing
- Finding bugs, fixing bugs
- Full support to all ULWGL features
- Testing again for safety measure
I will use (as I am doing) UWINE for my daily gaming experience, so I will update it accordingly. Feel free (please) to do the same.
## Call to test
We need you! Everybody needs you! Linux itself needs you!
Yes, you.
And by that I mean: please stress test and help me improve this software so that we can help the ULWGL team focusing on the important stuff.
An era of gaming, an era of universal Proton compatibility is coming.
Be part of it!
## Disclaimer
Sorry for any mistake. It wasn't inentional.
## License
MIT License.

13
env.example Normal file
View File

@ -0,0 +1,13 @@
# You can either specify everything in here (obtainin a sort of launcher)
# or you can specify the variables in the command line.
# You can also specify the variables in a file and source it.
# Anyway there are default values for all the variables, so you can just run
# the script without any arguments and it will show you how to do stuff.
PROTONPATH="/home/<youruser>/uwine/protons/GE-Proton8-32"
WINEPREFIX="/home/<youruser>/uwine/PREFIX"
GAMEID="0"
ULWLGDIR="/home/<youruser>/uwine/launcher"
FILEPATH="regedit"
CUSTOMVARS='{
"DVXK": "1"
}'

3
ids.json Normal file
View File

@ -0,0 +1,3 @@
{
"bg3": "1086940"
}

View File

@ -1,139 +0,0 @@
# ULWGL
Unified Linux Wine Game Launcher
# WHAT IS THIS?
This is a work in progress POC (proof of concept) for a unified launcher for windows games on linux. It is essentially a copy of the Steam Linux Runtime/Steam Runtime Tools (https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) that Valve uses for proton, with some modifications made so that it can be used outside of Steam.
# WHAT DOES IT DO?
When steam launches a proton game, it launches it like this:
```
/home/tcrider/.local/share/Steam/ubuntu12_32/reaper SteamLaunch AppId=348550 -- /home/tcrider/.local/share/Steam/ubuntu12_32/steam-launch-wrapper -- /home/tcrider/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point --verb=waitforexitandrun -- /home/tcrider/.local/share/Steam/compatibilitytools.d/GE-Proton8-27/proton waitforexitandrun /home/tcrider/.local/share/Steam/steamapps/common/Guilty Gear XX Accent Core Plus R/GGXXACPR_Win.exe
```
We can ignore this `/home/tcrider/.local/share/Steam/ubuntu12_32/steam-launch-wrapper`, it's just a process runner with no real value other than forwarding environment variables (more on that later).
I managed to pull the envvars it uses by making steam run printenv for the games command line. We needed these envvars because proton expects them in order to function. With them we can essentially make proton run without needing steam at all.
Next this part `/home/tcrider/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point`
The first part `/home/tcrider/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/` is steam-runtime-tools compiled https://gitlab.steamos.cloud/steamrt/steam-runtime-tools and is used alongside the sniper runtime container used during proton builds.
The second part `_v2-entry-point` is just a bash script which loads proton into the container and runs the game.
So, ULWGL is basically a copy paste of SteamLinuxRuntime_sniper, which is a compiled version of steam-runtime-tools. We've renamed _v2-entry-point to ULWGL and added `ulwgl-run` to replace steam-launch-wrapper.
When you use `ulwgl-run` to run a game, it uses the specified WINEPREFIX, proton version, executable, and arguements passed to it to run the game in proton, inside steam's runtime container JUST like if you were running the game through Steam, except now you're no longer limited to Steam's game library or forced to add the game to Steam's library, in fact, you don't even have to have steam installed.
# HOW DO I USE IT?
Usage:
`WINEPREFIX=<wine-prefix-path> GAMEID=<ulwgl-id> PROTONPATH=<proton-version-path> ./ulwgl-run <executable-path> <arguements>`
Ex:
`WINEPREFIX=$HOME/Games/epic-games-store GAMEID=ulwgl-dauntless PROTONPATH="$HOME/.steam/steam/compatibilitytools.d/GE-Proton8-28" ./ulwgl-run "$HOME/Games/epic-games-store/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe" "-opengl -SkipBuildPatchPrereq"`
Optional (used mainly for protonfixes): `STORE`
`WINEPREFIX=$HOME/Games/epic-games-store GAMEID=ulwgl-dauntless STORE=egs PROTONPATH="$HOME/.steam/steam/compatibilitytools.d/GE-Proton8-28" ./ulwgl-run "$HOME/Games/epic-games-store/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe" "-opengl -SkipBuildPatchPrereq"`
# WHAT DOES THIS MEAN FOR OTHER LAUNCHERS (lutris/bottles/heroic/legendary,etc):
- everyone can use + contribute to the same protonfixes, no more managing individual install scripts per launcher
- everyone can run their games through proton just like a native steam game
- no steam or steam binaries required
- a unified online database of game fixes (protonfixes)
right now protonfixes packages a folder of 'gamefixes' however it could likely be recoded to pull from online quite easily
The idea is to get all of these tools using this same `ulwgl-run` and just feeding their envvars into it. That way any changes that need to happen can happen in proton-ge and/or protonfixes, or a 'unified proton' build based off GE, or whatever they want.
# WHAT IS THE BASIC PLAN OF PUTTING THIS INTO ACTION?
1. We build a database containing various game titles, their IDs from different stores, and their correlating ULWGL ID.
2. Various launchers then search the database to pull the ULWGL ID, and feed it as the game ID to ulwgl-run alongside the store type, proton version, wine prefix, game executable, and launch arguements.
3. When the game gets launched from ulwgl-run, protonfixes picks up the store type and ULWGL ID and finds the appropriate fix script for it, then applies it before running the game.
4. protonfixes has folders separated for each store type. The ULWGL ID for a game remains the exact same across multiple stores, the only difference being it can have store specific scripts OR it can just symlink to another existing script that already has the fixes it needs.
Example:
Borderlands 3 from EGS store.
1. Generally a launcher is going to know which store it is using already, so that is easy enough to determine and feed the STORE variable to the launcher.
2. To determine the game title, EGS has various codenames such as 'Catnip'. The launcher would see "ok store is egs and codename is Catnip, let's search the ULWGL database for those"
3. In our ULWGL unified database, we create a 'title' column, 'store' column, 'codename' column, 'ULWGL-ID' column. We add a line for Borderlands 3 and fill in the details for each column.
4. Now the launcher can search 'Catnip' and 'egs' as the codename and store in the database and correlate it with Borderlands 3 and ULWGL-12345. It can then feed ULWGL-12345 to the ulwgl-run script.
README notes from Valve's steam-runtime-tools:
Steam Linux Runtime 3.0 (sniper)
================================
This container-based release of the Steam Runtime is used for native
Linux games, and for Proton 8.0+.
For general information please see
<https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/container-runtime.md>
and
<https://gitlab.steamos.cloud/steamrt/steamrt/-/blob/steamrt/sniper/README.md>
Release notes
-------------
Please see
<https://gitlab.steamos.cloud/steamrt/steamrt/-/wikis/Sniper-release-notes>
Known issues
------------
Please see
<https://github.com/ValveSoftware/steam-runtime/blob/master/doc/steamlinuxruntime-known-issues.md>
Reporting bugs
--------------
Please see
<https://github.com/ValveSoftware/steam-runtime/blob/master/doc/reporting-steamlinuxruntime-bugs.md>
Development and debugging
-------------------------
The runtime's behaviour can be changed by running the Steam client with
environment variables set.
`STEAM_LINUX_RUNTIME_LOG=1` will enable logging. Log files appear in
`SteamLinuxRuntime_sniper/var/slr-*.log`, with filenames containing the app ID.
`slr-latest.log` is a symbolic link to whichever one was created most
recently.
`STEAM_LINUX_RUNTIME_VERBOSE=1` produces more detailed log output,
either to a log file (if `STEAM_LINUX_RUNTIME_LOG=1` is also used) or to
the same place as `steam` output (otherwise).
`PRESSURE_VESSEL_SHELL=instead` runs an interactive shell in the
container instead of running the game.
Please see
<https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/distro-assumptions.md>
for details of assumptions made about the host operating system, and some
advice on debugging the container runtime on new Linux distributions.
Game developers who are interested in targeting this environment should
check the SDK documentation <https://gitlab.steamos.cloud/steamrt/sniper/sdk>
and general information for game developers
<https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/slr-for-game-developers.md>.
Licensing and copyright
-----------------------
The Steam Runtime contains many third-party software packages under
various open-source licenses.
For full source code, please see the version-numbered subdirectory of
<https://repo.steampowered.com/steamrt-images-sniper/snapshots/>
corresponding to the version numbers listed in VERSIONS.txt.

View File

@ -1,13 +0,0 @@
"compatibilitytools"
{
"compat_tools"
{
"ULWGL-Runner" // Internal name of this tool
{
"install_path" "."
"display_name" "ULWGL-Runner"
"from_oslist" "windows"
"to_oslist" "linux"
}
}
}

View File

@ -1,8 +0,0 @@
// Generated file, do not edit
"manifest"
{
"commandline" "/ulwgl-run %verb%"
"version" "2"
"use_tool_subprocess_reaper" "1"
"compatmanager_layer_name" "ulwgl-runner"
}

View File

@ -1 +0,0 @@
../../../ULWGL/ulwgl-run

Binary file not shown.

View File

@ -1,183 +0,0 @@
id: org.openwinecomponents.ulwgl.launcher
runtime: org.freedesktop.Platform
runtime-version: &runtime-version '23.08'
x-gl-version: &gl-version '1.4'
x-gl-versions: &gl-versions 23.08;23.08-extra;1.4
x-gl-merge-dirs: &gl-merge-dirs vulkan/icd.d;glvnd/egl_vendor.d;OpenCL/vendors;lib/dri;lib/d3d;vulkan/explicit_layer.d;vulkan/implicit_layer.d
sdk: org.freedesktop.Sdk
command: ulwgl-run
separate-locales: false
sdk-extensions:
- org.freedesktop.Sdk.Compat.i386
- org.freedesktop.Sdk.Extension.toolchain-i386
finish-args:
- --allow=devel
- --allow=multiarch
- --device=all
- --allow=bluetooth
- --allow=per-app-dev-shm
- --env=PATH=/app/bin:/app/utils/bin:/usr/bin:/usr/lib/extensions/vulkan/MangoHud/bin:/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/OBSVkCapture/bin
- --filesystem=xdg-data/lutris:rw
- --filesystem=xdg-data/Steam:rw
- --filesystem=xdg-data/applications:rw
- --filesystem=~/.steam:rw
- --filesystem=~/Games:rw
- --filesystem=~/.local/share:rw
- --filesystem=~/.var/app/com.valvesoftware.Steam:rw
- --filesystem=~/.var/app/org.openwinecomponents.ulwgl.launcher:rw
- --filesystem=xdg-documents
- --filesystem=xdg-desktop
- --env=TZ=
- --unset-env=TZ
- --env=LC_ADDRESS=C
- --env=LC_COLLATE=C
- --env=LC_MONETARY=C
- --env=LC_MEASUREMENT=C
- --env=LC_NAME=C
- --env=LC_NUMERIC=C
- --env=LC_TELEPHONE=C
- --env=SDL_VIDEODRIVER=
- --unset-env=SDL_VIDEODRIVER
- --env=DBUS_FATAL_WARNINGS=0
- --env=XDG_CONFIG_DIRS=/etc/xdg:/usr/lib/x86_64-linux-gnu/GL:/usr/lib/i386-linux-gnu/GL
# Wine uses UDisks2 to enumerate disk drives
- --system-talk-name=org.freedesktop.UDisks2
# should fix access to SD card on the deck
- --filesystem=/run/media
# There are still quite a few users using /mnt/ for external drives
- --filesystem=/mnt
# should fix steamdeck controler navigation
- --filesystem=/run/udev:ro
# should fix discord rich presence
- --filesystem=xdg-run/app/com.discordapp.Discord:create
- --persist=.
- --share=ipc
- --socket=wayland
- --socket=x11
- --socket=pulseaudio
- --share=network
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.kde.StatusNotifierWatcher
# Required for bwrap to work
- --talk-name=org.freedesktop.portal.Background
add-extensions:
org.freedesktop.Platform.Compat.i386:
directory: lib/i386-linux-gnu
version: *runtime-version
org.freedesktop.Platform.Compat.i386.Debug:
directory: lib/debug/lib/i386-linux-gnu
version: *runtime-version
no-autodownload: true
org.freedesktop.Platform.GL32:
directory: lib/i386-linux-gnu/GL
version: *gl-version
versions: *gl-versions
subdirectories: true
no-autodownload: true
autodelete: false
add-ld-path: lib
merge-dirs: *gl-merge-dirs
download-if: active-gl-driver
enable-if: active-gl-driver
autoprune-unless: active-gl-driver
org.freedesktop.Platform.GL32.Debug:
directory: lib/debug/lib/i386-linux-gnu/GL
version: *gl-version
versions: *gl-versions
subdirectories: true
no-autodownload: true
merge-dirs: *gl-merge-dirs
enable-if: active-gl-driver
autoprune-unless: active-gl-driver
org.freedesktop.Platform.VAAPI.Intel.i386:
directory: lib/i386-linux-gnu/dri/intel-vaapi-driver
version: *runtime-version
versions: *runtime-version
autodelete: false
no-autodownload: true
add-ld-path: lib
download-if: have-intel-gpu
autoprune-unless: have-intel-gpu
org.freedesktop.Platform.ffmpeg-full:
directory: lib/ffmpeg
add-ld-path: .
version: *runtime-version
no-autodownload: true
autodelete: false
org.freedesktop.Platform.ffmpeg_full.i386:
directory: lib32/ffmpeg
add-ld-path: .
version: *runtime-version
no-autodownload: true
autodelete: false
com.valvesoftware.Steam.CompatibilityTool:
subdirectories: true
directory: share/steam/compatibilitytools.d
version: stable
versions: stable;beta;test
no-autodownload: true
autodelete: true
com.valvesoftware.Steam.Utility:
subdirectories: true
directory: utils
version: stable
versions: stable;beta;test
add-ld-path: lib
merge-dirs: bin;lib/python3.10/site-packages;share/vulkan/explicit_layer.d;share/vulkan/implicit_layer.d;share/steam/compatibilitytools.d;
no-autodownload: true
autodelete: true
modules:
# --- ULWGL ---
- name: ulwgl-run
buildsystem: simple
build-commands:
- install -D ulwgl-run-cli /app/bin/ulwgl-run
- install -D ULWGL-launcher.tar.gz /app/share/ULWGL/ULWGL-launcher.tar.gz
sources:
- type: file
path: ulwgl-run-cli
- type: file
url: https://github.com/Open-Wine-Components/ULWGL-launcher/releases/download/0.1-RC3/ULWGL-launcher.tar.gz
sha256: e25c4dd0636d04e7c8c534cf3c5bbdca5ae0d49f146ee8395306174700899952
- name: platform-bootstrap
buildsystem: simple
build-commands:
- |
set -e
mkdir -p /app/bin
mkdir -p /app/lib/i386-linux-gnu
mkdir -p /app/lib/i386-linux-gnu/GL
mkdir -p /app/lib/i386-linux-gnu/dri/intel-vaapi-driver
mkdir -p /app/lib/debug/lib/i386-linux-gnu
mkdir -p /app/lib/debug/lib/i386-linux-gnu/GL
install -Dm644 -t /app/etc ld.so.conf
mkdir -p /app/lib{,32}/ffmpeg
mkdir -p /app/share/steam/compatibilitytools.d
mkdir -p /app/utils /app/share/vulkan
ln -srv /app/{utils/,}share/vulkan/explicit_layer.d
ln -srv /app/{utils/,}share/vulkan/implicit_layer.d
mkdir -p /app/links/lib
ln -srv /app/lib /app/links/lib/x86_64-linux-gnu
ln -srv /app/lib32 /app/links/lib/i386-linux-gnu
sources:
- type: inline
dest-filename: ld.so.conf
contents: |
# We just make any GL32 extension have higher priority
include /run/flatpak/ld.so.conf.d/app-*-org.freedesktop.Platform.GL32.*.conf
/app/lib32
/app/lib/i386-linux-gnu
/lib64

View File

@ -1,77 +0,0 @@
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.8
target-version = "py38"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F", "RET", "PTH", "D", "W", "BLE001", "EM"]
ignore = ["D100", "D203", "D213"]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"

View File

@ -1,23 +0,0 @@
#!/bin/sh
# Generated file, do not edit
set -eu
me="$(readlink -f "$0")"
here="${me%/*}"
me="${me##*/}"
dir=sniper_platform_0.20240125.75305
pressure_vessel="${PRESSURE_VESSEL_PREFIX:-"${here}/pressure-vessel"}"
export PRESSURE_VESSEL_COPY_RUNTIME=1
export PRESSURE_VESSEL_GC_LEGACY_RUNTIMES=1
export PRESSURE_VESSEL_RUNTIME="${dir}"
unset PRESSURE_VESSEL_RUNTIME_ARCHIVE
export PRESSURE_VESSEL_RUNTIME_BASE="${here}"
if [ -z "${PRESSURE_VESSEL_VARIABLE_DIR-}" ]; then
export PRESSURE_VESSEL_VARIABLE_DIR="${here}/var"
fi
exec "${pressure_vessel}/bin/pressure-vessel-unruntime" "$@"

View File

@ -1 +0,0 @@
ulwgl_run.py

View File

@ -1,72 +0,0 @@
#!/bin/sh
# use for debug only.
# set -x
ULWGL_PROTON_VER="ULWGL-Proton-8.0-5-3"
ULWGL_LAUNCHER_VER="0.1-RC3"
me="$(readlink -f "$0")"
ulwgl_link="https://github.com/Open-Wine-Components/ULWGL-launcher/releases/download/$ULWGL_LAUNCHER_VER/ULWGL-launcher.tar.gz"
ulwgl_dir="$HOME"/.local/share/ULWGL
proton_link="https://github.com/Open-Wine-Components/ULWGL-Proton/releases/download/$ULWGL_PROTON_VER/$ULWGL_PROTON_VER"
proton_dir="$HOME"/.local/share/Steam/compatibilitytools.d
ulwgl_cache="$HOME"/.cache/ULWGL
if [ ! -d "$ulwgl_cache" ]; then
mkdir -p "$ulwgl_cache"
fi
# Self-update
# In flatpak it will check for /app/share/ULWGL/ULWGL-launcher.tar.gz and check version
# In distro package it will check for /usr/share/ULWGL/ULWGL-launcher.tar.gz and check version
# If tarball does not exist it will just download it.
if [ ! -d "$ulwgl_dir" ]; then
mkdir -p "$ulwgl_dir"
if [ -f "${me%/*/*}"/share/ULWGL/ULWGL-launcher.tar.gz ]; then
tar -zxvf "${me%/*/*}"/share/ULWGL/ULWGL-launcher.tar.gz --one-top-level="$ulwgl_dir"
else
wget "$ulwgl_link" -O "$ulwgl_cache/ULWGL-launcher.tar.gz"
tar -zxvf "$ulwgl_cache/ULWGL-launcher.tar.gz" --one-top-level="$ulwgl_dir"
rm "$ulwgl_cache/ULWGL-launcher.tar.gz"
fi
else
if [ "$ULWGL_LAUNCHER_VER" != "$(cat "$ulwgl_dir"/ULWGL-VERSION)" ]; then
rm -Rf "$ulwgl_dir" --preserve-root=all
if [ -f "${me%/*/*}"/share/ULWGL/ULWGL-launcher.tar.gz ]; then
tar -zxvf "${me%/*/*}"/share/ULWGL/ULWGL-launcher.tar.gz --one-top-level="$ulwgl_dir"
else
wget "$ulwgl_link" -O "$ulwgl_cache/ULWGL-launcher.tar.gz"
tar -zxvf "$ulwgl_cache/ULWGL-launcher.tar.gz" --one-top-level="$ulwgl_dir"
rm "$ulwgl_cache/ULWGL-launcher.tar.gz"
fi
fi
fi
if [ -z "$PROTONPATH" ]; then
if [ ! -d "$proton_dir"/$ULWGL_PROTON_VER ]; then
wget "$proton_link".tar.gz -O "$ulwgl_cache/$ULWGL_PROTON_VER".tar.gz
wget "$proton_link".sha512sum -O "$ulwgl_cache/$ULWGL_PROTON_VER".sha512sum
cd "$ulwgl_cache" || exit
checksum=$(sha512sum "$ULWGL_PROTON_VER".tar.gz)
cd - || exit
if [ "$checksum" = "$(cat "$ulwgl_cache/$ULWGL_PROTON_VER".sha512sum)" ]; then
tar -zxvf "$ulwgl_cache/$ULWGL_PROTON_VER".tar.gz --one-top-level="$proton_dir"
rm "$ulwgl_cache/$ULWGL_PROTON_VER".tar.gz
rm "$ulwgl_cache/$ULWGL_PROTON_VER".sha512sum
else
echo "ERROR: $ulwgl_cache/$ULWGL_PROTON_VER.tar.gz checksum does not match $ulwgl_cache/$ULWGL_PROTON_VER.sha512sum, aborting!"
rm "$ulwgl_cache/$ULWGL_PROTON_VER".tar.gz
rm "$ulwgl_cache/$ULWGL_PROTON_VER".sha512sum
exit 1
fi
fi
export PROTONPATH="$proton_dir/$ULWGL_PROTON_VER"
else
export PROTONPATH="$PROTONPATH"
fi
"$ulwgl_dir/ulwgl-run" "$@"

View File

@ -1,108 +0,0 @@
#!/bin/sh
# use for debug only.
# set -x
if [ -z "$1" ] || [ -z "$WINEPREFIX" ] || [ -z "$GAMEID" ] || [ -z "$PROTONPATH" ]; then
echo "Usage: WINEPREFIX=<wine-prefix-path> GAMEID=<ulwgl-id> PROTONPATH=<proton-version-path> ./gamelauncher.sh <executable-path> <arguments>"
echo "Ex:"
echo "WINEPREFIX=$HOME/Games/epic-games-store GAMEID=egs PROTONPATH=\"$HOME/.steam/steam/compatibilitytools.d/GE-Proton8-28\" ./gamelauncher.sh \"$HOME/Games/epic-games-store/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe\" \"-opengl -SkipBuildPatchPrereq\""
exit 1
fi
me="$(readlink -f "$0")"
here="${me%/*}"
if [ "$WINEPREFIX" ]; then
if [ ! -d "$WINEPREFIX" ]; then
mkdir -p "$WINEPREFIX"
export PROTON_DLL_COPY="*"
fi
if [ ! -d "$WINEPREFIX"/pfx ]; then
ln -s "$WINEPREFIX" "$WINEPREFIX"/pfx > /dev/null 2>&1
fi
if [ ! -f "$WINEPREFIX"/tracked_files ]; then
touch "$WINEPREFIX"/tracked_files
fi
if [ ! -f "$WINEPREFIX/dosdevices/" ]; then
mkdir -p "$WINEPREFIX"/dosdevices
ln -s "../drive_c" "$WINEPREFIX/dosdevices/c:" > /dev/null 2>&1
fi
fi
if [ -n "$PROTONPATH" ]; then
if [ ! -d "$PROTONPATH" ]; then
echo "ERROR: $PROTONPATH is invalid, aborting!"
exit 1
fi
fi
export ULWGL_ID="$GAMEID"
export STEAM_COMPAT_APP_ID="0"
numcheck='^[0-9]+$'
if echo "$ULWGL_ID" | cut -d "-" -f 2 | grep -Eq "$numcheck"; then
STEAM_COMPAT_APP_ID=$(echo "$ULWGL_ID" | cut -d "-" -f 2)
export STEAM_COMPAT_APP_ID
fi
export SteamAppId="$STEAM_COMPAT_APP_ID"
export SteamGameId="$STEAM_COMPAT_APP_ID"
# TODO: Ideally this should be the main game install path, which is often, but not always the path of the game's executable.
if [ -z "$STEAM_COMPAT_INSTALL_PATH" ]; then
exepath="$(readlink -f "$1")"
gameinstallpath="${exepath%/*}"
export STEAM_COMPAT_INSTALL_PATH="$gameinstallpath"
fi
compat_lib_path=$(findmnt -T "$STEAM_COMPAT_INSTALL_PATH" | tail -n 1 | awk '{ print $1 }')
if [ "$compat_lib_path" != "/" ]; then
export STEAM_COMPAT_LIBRARY_PATHS="${STEAM_COMPAT_LIBRARY_PATHS:+"${STEAM_COMPAT_LIBRARY_PATHS}:"}$compat_lib_path"
fi
if [ -z "$STEAM_RUNTIME_LIBRARY_PATH" ]; then
# The following info taken from steam ~/.local/share/ubuntu12_32/steam-runtime/run.sh
host_library_paths=
exit_status=0
ldconfig_output=$(/sbin/ldconfig -XNv 2> /dev/null; exit $?) || exit_status=$?
if [ $exit_status != 0 ]; then
echo "Warning: An unexpected error occurred while executing \"/sbin/ldconfig -XNv\", the exit status was $exit_status"
fi
while read -r line; do
# If line starts with a leading / and contains :, it's a new path prefix
case "$line" in /*:*)
library_path_prefix=$(echo "$line" | cut -d: -f1)
host_library_paths=$host_library_paths$library_path_prefix:
esac
done <<EOLDCONFIG
$ldconfig_output
EOLDCONFIG
host_library_paths="${LD_LIBRARY_PATH:+"${LD_LIBRARY_PATH}:"}$host_library_paths"
steam_runtime_library_paths="${STEAM_COMPAT_INSTALL_PATH}:${host_library_paths}"
export STEAM_RUNTIME_LIBRARY_PATH="$steam_runtime_library_paths"
fi
if [ -z "$PROTON_VERB" ]; then
export PROTON_VERB="waitforexitandrun"
fi
export STEAM_COMPAT_CLIENT_INSTALL_PATH=''
export STEAM_COMPAT_DATA_PATH="$WINEPREFIX"
export STEAM_COMPAT_SHADER_PATH="$STEAM_COMPAT_DATA_PATH"/shadercache
export PROTON_CRASH_REPORT_DIR='/tmp/ULWGL_crashreports'
export FONTCONFIG_PATH=''
export EXE="$1"
if [ "$EXE" = "createprefix" ]; then
# Hack, leave empty.
# forces proton to create a prefix without actually running anything.
EXE=""
fi
shift 1
export STEAM_COMPAT_TOOL_PATHS="$PROTONPATH:$here"
export STEAM_COMPAT_MOUNTS="$PROTONPATH:$here"
"$here"/ULWGL "--verb=$PROTON_VERB" -- "$PROTONPATH"/proton "$PROTON_VERB" "$EXE" "$@"

View File

@ -1,26 +0,0 @@
from enum import Enum
from logging import INFO, WARNING, DEBUG, ERROR
SIMPLE_FORMAT = "%(levelname)s: %(message)s"
DEBUG_FORMAT = "%(levelname)s [%(module)s.%(funcName)s:%(lineno)s]:%(message)s"
class Level(Enum):
"""Represent the Log level values for the logger module."""
INFO = INFO
WARNING = WARNING
DEBUG = DEBUG
ERROR = ERROR
class Color(Enum):
"""Represent the color to be applied to a string."""
RESET = "\u001b[0m"
INFO = "\u001b[34m"
WARNING = "\033[33m"
ERROR = "\033[31m"
BOLD = "\033[1m"
DEBUG = "\u001b[35m"

View File

@ -1,261 +0,0 @@
from pathlib import Path
from os import environ
from tarfile import open as tar_open
from typing import Dict, List, Tuple, Any, Union
from hashlib import sha512
from shutil import rmtree
from http.client import HTTPSConnection, HTTPResponse, HTTPException, HTTPConnection
from ssl import create_default_context
from json import loads as loads_json
from urllib.request import urlretrieve
from sys import stderr
def get_ulwgl_proton(env: Dict[str, str]) -> Union[Dict[str, str]]:
"""Attempt to find existing Proton from the system or downloads the latest if PROTONPATH is not set.
Only fetches the latest if not first found in .local/share/Steam/compatibilitytools.d
.cache/ULWGL is referenced for the latest then as fallback
"""
files: List[Tuple[str, str]] = []
try:
files = _fetch_releases()
except HTTPException:
print("Offline.\nContinuing ...", file=stderr)
cache: Path = Path.home().joinpath(".cache/ULWGL")
steam_compat: Path = Path.home().joinpath(".local/share/Steam/compatibilitytools.d")
cache.mkdir(exist_ok=True, parents=True)
steam_compat.mkdir(exist_ok=True, parents=True)
# Prioritize the Steam compat
if _get_from_steamcompat(env, steam_compat, cache, files):
return env
# Use the latest Proton in the cache if it exists
if _get_from_cache(env, steam_compat, cache, files, True):
return env
# Download the latest if Proton is not in Steam compat
# If the digests mismatched, refer to the cache in the next block
if _get_latest(env, steam_compat, cache, files):
return env
# Refer to an old version previously downloaded
# Reached on digest mismatch, user interrupt or download failure/no internet
if _get_from_cache(env, steam_compat, cache, files, False):
return env
# No internet and cache/compat tool is empty, just return and raise an exception from the caller
return env
def _fetch_releases() -> List[Tuple[str, str]]:
"""Fetch the latest releases from the Github API."""
files: List[Tuple[str, str]] = []
resp: HTTPResponse = None
conn: HTTPConnection = HTTPSConnection(
"api.github.com", timeout=30, context=create_default_context()
)
conn.request(
"GET",
"/repos/Open-Wine-Components/ULWGL-Proton/releases",
headers={
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "",
},
)
resp = conn.getresponse()
if resp and resp.status != 200:
return files
# Attempt to acquire the tarball and checksum from the JSON data
releases: List[Dict[str, Any]] = loads_json(resp.read().decode("utf-8"))
for release in releases:
if "assets" in release:
assets: List[Dict[str, Any]] = release["assets"]
for asset in assets:
if (
"name" in asset
and (
asset["name"].endswith("sum")
or (
asset["name"].endswith("tar.gz")
and asset["name"].startswith("ULWGL-Proton")
)
)
and "browser_download_url" in asset
):
if asset["name"].endswith("sum"):
files.append((asset["name"], asset["browser_download_url"]))
else:
files.append((asset["name"], asset["browser_download_url"]))
if len(files) == 2:
break
break
conn.close()
return files
def _fetch_proton(
env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]]
) -> Dict[str, str]:
"""Download the latest ULWGL-Proton and set it as PROTONPATH."""
hash, hash_url = files[0]
proton, proton_url = files[1]
proton_dir: str = proton[: proton.find(".tar.gz")] # Proton dir
# TODO: Parallelize this
print(f"Downloading {hash} ...", file=stderr)
urlretrieve(hash_url, cache.joinpath(hash).as_posix())
print(f"Downloading {proton} ...", file=stderr)
urlretrieve(proton_url, cache.joinpath(proton).as_posix())
print("Completed.", file=stderr)
with cache.joinpath(proton).open(mode="rb") as file:
if (
sha512(file.read()).hexdigest()
!= cache.joinpath(hash).read_text().split(" ")[0]
):
err: str = "Digests mismatched.\nFalling back to cache ..."
raise ValueError(err)
print(f"{proton}: SHA512 is OK", file=stderr)
_extract_dir(cache.joinpath(proton), steam_compat)
environ["PROTONPATH"] = steam_compat.joinpath(proton_dir).as_posix()
env["PROTONPATH"] = environ["PROTONPATH"]
return env
def _extract_dir(proton: Path, steam_compat: Path) -> None:
"""Extract from the cache to another location."""
with tar_open(proton.as_posix(), "r:gz") as tar:
print(f"Extracting {proton} -> {steam_compat.as_posix()} ...", file=stderr)
tar.extractall(path=steam_compat.as_posix())
print("Completed.", file=stderr)
def _cleanup(tarball: str, proton: str, cache: Path, steam_compat: Path) -> None:
"""Remove files that may have been left in an incomplete state to avoid corruption.
We want to do this when a download for a new release is interrupted
"""
print("Keyboard Interrupt.\nCleaning ...", file=stderr)
if cache.joinpath(tarball).is_file():
print(f"Purging {tarball} in {cache} ...", file=stderr)
cache.joinpath(tarball).unlink()
if steam_compat.joinpath(proton).is_dir():
print(f"Purging {proton} in {steam_compat} ...", file=stderr)
rmtree(steam_compat.joinpath(proton).as_posix())
def _get_from_steamcompat(
env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]]
) -> Union[Dict[str, str], None]:
"""Refer to Steam compat folder for any existing Proton directories."""
proton_dir: str = "" # Latest Proton
if len(files) == 2:
proton_dir: str = files[1][0][: files[1][0].find(".tar.gz")]
for proton in steam_compat.glob("ULWGL-Proton*"):
print(f"{proton.name} found in: {steam_compat.as_posix()}", file=stderr)
environ["PROTONPATH"] = proton.as_posix()
env["PROTONPATH"] = environ["PROTONPATH"]
# Notify the user that they're not using the latest
if proton_dir and proton.name != proton_dir:
print(
"ULWGL-Proton is outdated.\nFor latest release, please download "
+ files[1][1],
file=stderr,
)
return env
return None
def _get_from_cache(
env: Dict[str, str],
steam_compat: Path,
cache: Path,
files: List[Tuple[str, str]],
use_latest=True,
) -> Union[Dict[str, str], None]:
"""Refer to ULWGL cache directory.
Use the latest in the cache when present. When download fails, use an old version
Older Proton versions are only referred to when: digests mismatch, user interrupt, or download failure/no internet
"""
path: Path = None
name: str = ""
for tarball in cache.glob("ULWGL-Proton*.tar.gz"):
if files and tarball == cache.joinpath(files[1][0]) and use_latest:
path = tarball
name = tarball.name
break
if tarball != cache.joinpath(files[1][0]) and not use_latest:
path = tarball
name = tarball.name
break
if path:
proton_dir: str = name[: name.find(".tar.gz")] # Proton dir
print(f"{name} found in: {path}", file=stderr)
try:
_extract_dir(path, steam_compat)
environ["PROTONPATH"] = steam_compat.joinpath(proton_dir).as_posix()
env["PROTONPATH"] = environ["PROTONPATH"]
return env
except KeyboardInterrupt:
if steam_compat.joinpath(proton_dir).is_dir():
print(f"Purging {proton_dir} in {steam_compat} ...", file=stderr)
rmtree(steam_compat.joinpath(proton_dir).as_posix())
raise
return None
def _get_latest(
env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]]
) -> Union[Dict[str, str], None]:
"""Download the latest Proton for new installs -- empty cache and Steam compat.
When the digests mismatched or when interrupted, refer to cache for an old version
"""
if files:
print("Fetching latest release ...", file=stderr)
try:
_fetch_proton(env, steam_compat, cache, files)
env["PROTONPATH"] = environ["PROTONPATH"]
except ValueError:
# Digest mismatched or download failed
# Refer to the cache for old version next
return None
except KeyboardInterrupt:
tarball: str = files[1][0]
proton_dir: str = tarball[: tarball.find(".tar.gz")] # Proton dir
# Exit cleanly
# Clean up extracted data and cache to prevent corruption/errors
# Refer to the cache for old version next
_cleanup(tarball, proton_dir, cache, steam_compat)
return None
return env

View File

@ -1,13 +0,0 @@
import logging
from sys import stderr
from ulwgl_consts import SIMPLE_FORMAT, DEBUG_FORMAT
simple_formatter = logging.Formatter(SIMPLE_FORMAT)
debug_formatter = logging.Formatter(DEBUG_FORMAT)
log = logging.getLogger(__name__)
console_handler = logging.StreamHandler(stream=stderr)
console_handler.setFormatter(simple_formatter)
log.addHandler(console_handler)
log.setLevel(logging.CRITICAL + 1)

View File

@ -1,129 +0,0 @@
import os
from pathlib import Path
from typing import Dict, Set, Any, List
from argparse import Namespace
def set_env_toml(env: Dict[str, str], args: Namespace) -> Dict[str, str]:
"""Read a TOML file then sets the environment variables for the Steam RT.
In the TOML file, certain keys map to Steam RT environment variables. For example:
proton -> $PROTONPATH
prefix -> $WINEPREFIX
game_id -> $GAMEID
exe -> $EXE
At the moment we expect the tables: 'ulwgl'
"""
try:
import tomllib
except ModuleNotFoundError:
msg: str = "tomllib requires Python 3.11"
raise ModuleNotFoundError(msg)
toml: Dict[str, Any] = None
path_config: str = Path(getattr(args, "config", None)).expanduser().as_posix()
if not Path(path_config).is_file():
msg: str = "Path to configuration is not a file: " + getattr(
args, "config", None
)
raise FileNotFoundError(msg)
with Path(path_config).open(mode="rb") as file:
toml = tomllib.load(file)
_check_env_toml(env, toml)
for key, val in toml["ulwgl"].items():
if key == "prefix":
env["WINEPREFIX"] = val
elif key == "game_id":
env["GAMEID"] = val
elif key == "proton":
env["PROTONPATH"] = val
elif key == "store":
env["STORE"] = val
elif key == "exe":
if toml.get("ulwgl").get("launch_args"):
env["EXE"] = val + " " + " ".join(toml.get("ulwgl").get("launch_args"))
else:
env["EXE"] = val
return env
def _check_env_toml(env: Dict[str, str], toml: Dict[str, Any]):
"""Check for required or empty key/value pairs when reading a TOML config.
NOTE: Casing matters in the config and we do not check if the game id is set
"""
table: str = "ulwgl"
required_keys: List[str] = ["proton", "prefix", "exe"]
if table not in toml:
err: str = f"Table '{table}' in TOML is not defined."
raise ValueError(err)
for key in required_keys:
if key not in toml[table]:
err: str = f"The following key in table '{table}' is required: {key}"
raise ValueError(err)
# Raise an error for executables that do not exist
# One case this can happen is when game options are appended at the end of the exe
# Users should use launch_args for that
if key == "exe" and not Path(toml[table][key]).expanduser().is_file():
val: str = toml[table][key]
err: str = f"Value for key '{key}' in TOML is not a file: {val}"
raise FileNotFoundError(err)
# The proton and wine prefix need to be folders
if (key == "proton" and not Path(toml[table][key]).expanduser().is_dir()) or (
key == "prefix" and not Path(toml[table][key]).expanduser().is_dir()
):
dir: str = Path(toml[table][key]).expanduser().as_posix()
err: str = f"Value for key '{key}' in TOML is not a directory: {dir}"
raise NotADirectoryError(err)
# Check for empty keys
for key, val in toml[table].items():
if not val and isinstance(val, str):
err: str = f"Value is empty for '{key}' in TOML.\nPlease specify a value or remove the following entry:\n{key} = {val}"
raise ValueError(err)
return toml
def enable_steam_game_drive(env: Dict[str, str]) -> Dict[str, str]:
"""Enable Steam Game Drive functionality.
Expects STEAM_COMPAT_INSTALL_PATH to be set
STEAM_RUNTIME_LIBRARY_PATH will not be set if the exe directory does not exist
"""
paths: Set[str] = set()
root: Path = Path("/")
# Check for mount points going up toward the root
# NOTE: Subvolumes can be mount points
for path in Path(env["STEAM_COMPAT_INSTALL_PATH"]).parents:
if path.is_mount() and path != root:
if env["STEAM_COMPAT_LIBRARY_PATHS"]:
env["STEAM_COMPAT_LIBRARY_PATHS"] = (
env["STEAM_COMPAT_LIBRARY_PATHS"] + ":" + path.as_posix()
)
else:
env["STEAM_COMPAT_LIBRARY_PATHS"] = path.as_posix()
break
if "LD_LIBRARY_PATH" in os.environ:
paths.add(Path(os.environ["LD_LIBRARY_PATH"]).as_posix())
if env["STEAM_COMPAT_INSTALL_PATH"]:
paths.add(env["STEAM_COMPAT_INSTALL_PATH"])
# Hard code for now because these paths seem to be pretty standard
# This way we avoid shelling to ldconfig
paths.add("/usr/lib")
paths.add("/usr/lib32")
env["STEAM_RUNTIME_LIBRARY_PATH"] = ":".join(list(paths))
return env

View File

@ -1,315 +0,0 @@
#!/usr/bin/env python3
import os
import sys
from traceback import print_exception
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from pathlib import Path
from typing import Dict, Any, List, Set, Union, Tuple
from ulwgl_plugins import enable_steam_game_drive, set_env_toml
from re import match
from subprocess import run
from ulwgl_dl_util import get_ulwgl_proton
from ulwgl_consts import Level
from ulwgl_util import msg
from ulwgl_log import log, console_handler, debug_formatter
verbs: Set[str] = {
"waitforexitandrun",
"run",
"runinprefix",
"destroyprefix",
"getcompatpath",
"getnativepath",
}
def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103
opt_args: Set[str] = {"--help", "-h", "--config"}
exe: str = Path(__file__).name
usage: str = f"""
example usage:
GAMEID= {exe} /home/foo/example.exe
WINEPREFIX= GAMEID= {exe} /home/foo/example.exe
WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe
WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe -opengl
WINEPREFIX= GAMEID= PROTONPATH= {exe} ""
WINEPREFIX= GAMEID= PROTONPATH= PROTON_VERB= {exe} /home/foo/example.exe
WINEPREFIX= GAMEID= PROTONPATH= STORE= {exe} /home/foo/example.exe
ULWGL_LOG= GAMEID= {exe} /home/foo/example.exe
{exe} --config /home/foo/example.toml
"""
parser: ArgumentParser = ArgumentParser(
description="Unified Linux Wine Game Launcher",
epilog=usage,
formatter_class=RawTextHelpFormatter,
)
parser.add_argument("--config", help="path to TOML file (requires Python 3.11)")
if not sys.argv[1:]:
err: str = "Please see project README.md for more info and examples.\nhttps://github.com/Open-Wine-Components/ULWGL-launcher"
parser.print_help(sys.stderr)
raise SystemExit(err)
if sys.argv[1:][0] in opt_args:
return parser.parse_args(sys.argv[1:])
if sys.argv[1] in verbs:
if "PROTON_VERB" not in os.environ:
os.environ["PROTON_VERB"] = sys.argv[1]
sys.argv.pop(1)
return sys.argv[1], sys.argv[2:]
def set_log() -> None:
"""Adjust the log level for the logger."""
levels: Set[str] = {"1", "warn", "debug"}
if os.environ["ULWGL_LOG"] not in levels:
return
if os.environ["ULWGL_LOG"] == "1":
# Show the envvars and command at this level
log.setLevel(level=Level.INFO.value)
elif os.environ["ULWGL_LOG"] == "warn":
log.setLevel(level=Level.WARNING.value)
elif os.environ["ULWGL_LOG"] == "debug":
# Show all logs
console_handler.setFormatter(debug_formatter)
log.addHandler(console_handler)
log.setLevel(level=Level.DEBUG.value)
os.environ.pop("ULWGL_LOG")
def setup_pfx(path: str) -> None:
"""Create a symlink to the WINE prefix and tracked_files file."""
pfx: Path = Path(path).joinpath("pfx").expanduser()
if pfx.is_symlink():
pfx.unlink()
if not pfx.is_dir():
pfx.symlink_to(Path(path).expanduser())
Path(path).joinpath("tracked_files").expanduser().touch()
def check_env(
env: Dict[str, str], toml: Dict[str, Any] = None
) -> Union[Dict[str, str], Dict[str, Any]]:
"""Before executing a game, check for environment variables and set them.
WINEPREFIX, GAMEID and PROTONPATH are strictly required.
"""
if "GAMEID" not in os.environ:
err: str = "Environment variable not set: GAMEID"
raise ValueError(err)
env["GAMEID"] = os.environ["GAMEID"]
if "WINEPREFIX" not in os.environ:
pfx: Path = Path.home().joinpath("Games/ULWGL/" + env["GAMEID"])
pfx.mkdir(parents=True, exist_ok=True)
os.environ["WINEPREFIX"] = pfx.as_posix()
if not Path(os.environ["WINEPREFIX"]).expanduser().is_dir():
pfx: Path = Path(os.environ["WINEPREFIX"])
pfx.mkdir(parents=True, exist_ok=True)
os.environ["WINEPREFIX"] = pfx.as_posix()
env["WINEPREFIX"] = os.environ["WINEPREFIX"]
# Proton Version
if (
"PROTONPATH" in os.environ
and os.environ["PROTONPATH"]
and Path(
"~/.local/share/Steam/compatibilitytools.d/" + os.environ["PROTONPATH"]
)
.expanduser()
.is_dir()
):
log.debug(msg("Proton version selected", Level.DEBUG))
os.environ["PROTONPATH"] = (
Path("~/.local/share/Steam/compatibilitytools.d")
.joinpath(os.environ["PROTONPATH"])
.expanduser()
.as_posix()
)
if "PROTONPATH" not in os.environ:
os.environ["PROTONPATH"] = ""
get_ulwgl_proton(env)
env["PROTONPATH"] = os.environ["PROTONPATH"]
# If download fails/doesn't exist in the system, raise an error
if not os.environ["PROTONPATH"]:
err: str = "Download failed.\nProton could not be found in cache or compatibilitytools.d\nPlease set $PROTONPATH or visit https://github.com/Open-Wine-Components/ULWGL-Proton/releases"
raise FileNotFoundError(err)
return env
def set_env(
env: Dict[str, str], args: Union[Namespace, Tuple[str, List[str]]]
) -> Dict[str, str]:
"""Set various environment variables for the Steam RT.
Filesystem paths will be formatted and expanded as POSIX
"""
# PROTON_VERB
# For invalid Proton verbs, just assign the waitforexitandrun
if "PROTON_VERB" in os.environ and os.environ["PROTON_VERB"] in verbs:
env["PROTON_VERB"] = os.environ["PROTON_VERB"]
else:
env["PROTON_VERB"] = "waitforexitandrun"
# EXE
# Empty string for EXE will be used to create a prefix
if isinstance(args, tuple) and isinstance(args[0], str) and not args[0]:
env["EXE"] = ""
env["STEAM_COMPAT_INSTALL_PATH"] = ""
env["PROTON_VERB"] = "waitforexitandrun"
elif isinstance(args, tuple):
env["EXE"] = Path(args[0]).expanduser().as_posix()
env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix()
else:
# Config branch
env["EXE"] = Path(env["EXE"]).expanduser().as_posix()
env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix()
if "STORE" in os.environ:
env["STORE"] = os.environ["STORE"]
# ULWGL_ID
env["ULWGL_ID"] = env["GAMEID"]
env["STEAM_COMPAT_APP_ID"] = "0"
if match(r"^ulwgl-[\d\w]+$", env["ULWGL_ID"]):
env["STEAM_COMPAT_APP_ID"] = env["ULWGL_ID"][env["ULWGL_ID"].find("-") + 1 :]
env["SteamAppId"] = env["STEAM_COMPAT_APP_ID"]
env["SteamGameId"] = env["SteamAppId"]
# PATHS
env["WINEPREFIX"] = Path(env["WINEPREFIX"]).expanduser().as_posix()
env["PROTONPATH"] = Path(env["PROTONPATH"]).expanduser().as_posix()
env["STEAM_COMPAT_DATA_PATH"] = env["WINEPREFIX"]
env["STEAM_COMPAT_SHADER_PATH"] = env["STEAM_COMPAT_DATA_PATH"] + "/shadercache"
env["STEAM_COMPAT_TOOL_PATHS"] = (
env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix()
)
env["STEAM_COMPAT_MOUNTS"] = env["STEAM_COMPAT_TOOL_PATHS"]
return env
def build_command(
env: Dict[str, str], command: List[str], opts: List[str] = None
) -> List[str]:
"""Build the command to be executed."""
paths: List[Path] = [
Path.home().joinpath(".local/share/ULWGL/ULWGL"),
Path(__file__).parent.joinpath("ULWGL"),
]
entry_point: str = ""
verb: str = env["PROTON_VERB"]
# Find the ULWGL script in $HOME/.local/share then cwd
for path in paths:
if path.is_file():
entry_point = path.as_posix()
break
# Raise an error if the _v2-entry-point cannot be found
if not entry_point:
home: str = Path.home().as_posix()
dir: str = Path(__file__).parent.as_posix()
msg: str = (
f"Path to _v2-entry-point cannot be found in: {home}/.local/share or {dir}"
)
raise FileNotFoundError(msg)
if not Path(env.get("PROTONPATH")).joinpath("proton").is_file():
err: str = "The following file was not found in PROTONPATH: proton"
raise FileNotFoundError(err)
command.extend([entry_point, "--verb", verb, "--"])
command.extend(
[
Path(env.get("PROTONPATH")).joinpath("proton").as_posix(),
verb,
env.get("EXE"),
]
)
if opts:
command.extend([*opts])
return command
def main() -> int: # noqa: D103
env: Dict[str, str] = {
"WINEPREFIX": "",
"GAMEID": "",
"PROTON_CRASH_REPORT_DIR": "/tmp/ULWGL_crashreports",
"PROTONPATH": "",
"STEAM_COMPAT_APP_ID": "",
"STEAM_COMPAT_TOOL_PATHS": "",
"STEAM_COMPAT_LIBRARY_PATHS": "",
"STEAM_COMPAT_MOUNTS": "",
"STEAM_COMPAT_INSTALL_PATH": "",
"STEAM_COMPAT_CLIENT_INSTALL_PATH": "",
"STEAM_COMPAT_DATA_PATH": "",
"STEAM_COMPAT_SHADER_PATH": "",
"FONTCONFIG_PATH": "",
"EXE": "",
"SteamAppId": "",
"SteamGameId": "",
"STEAM_RUNTIME_LIBRARY_PATH": "",
"STORE": "",
"PROTON_VERB": "",
"ULWGL_ID": "",
}
command: List[str] = []
args: Union[Namespace, Tuple[str, List[str]]] = parse_args()
opts: List[str] = None
if "ULWGL_LOG" in os.environ:
set_log()
if isinstance(args, Namespace) and getattr(args, "config", None):
set_env_toml(env, args)
else:
# Reference the game options
opts = args[1]
check_env(env)
setup_pfx(env["WINEPREFIX"])
set_env(env, args)
# Game Drive
enable_steam_game_drive(env)
# Set all environment variables
# NOTE: `env` after this block should be read only
for key, val in env.items():
log.info(msg(f"{key}={val}", Level.INFO))
os.environ[key] = val
build_command(env, command, opts)
log.debug(msg(command, Level.DEBUG))
return run(command).returncode
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
# Until Reaper is part of the command sequence, spawned process may still be alive afterwards
log.warning(msg("Keyboard Interrupt", Level.WARNING))
sys.exit(1)
except Exception as e: # noqa: BLE001
print_exception(e)
sys.exit(1)

File diff suppressed because it is too large Load Diff

View File

@ -1,533 +0,0 @@
import unittest
import ulwgl_run
import os
import argparse
from argparse import Namespace
from unittest.mock import patch
from pathlib import Path
from tomllib import TOMLDecodeError
from shutil import rmtree
import re
import ulwgl_plugins
import tarfile
class TestGameLauncherPlugins(unittest.TestCase):
"""Test suite ulwgl_run.py plugins."""
def setUp(self):
"""Create the test directory, exe and environment variables."""
self.env = {
"WINEPREFIX": "",
"GAMEID": "",
"PROTON_CRASH_REPORT_DIR": "/tmp/ULWGL_crashreports",
"PROTONPATH": "",
"STEAM_COMPAT_APP_ID": "",
"STEAM_COMPAT_TOOL_PATHS": "",
"STEAM_COMPAT_LIBRARY_PATHS": "",
"STEAM_COMPAT_MOUNTS": "",
"STEAM_COMPAT_INSTALL_PATH": "",
"STEAM_COMPAT_CLIENT_INSTALL_PATH": "",
"STEAM_COMPAT_DATA_PATH": "",
"STEAM_COMPAT_SHADER_PATH": "",
"FONTCONFIG_PATH": "",
"EXE": "",
"SteamAppId": "",
"SteamGameId": "",
"STEAM_RUNTIME_LIBRARY_PATH": "",
"ULWGL_ID": "",
"STORE": "",
"PROTON_VERB": "",
}
self.test_opts = "-foo -bar"
# Proton verb
# Used when testing build_command
self.test_verb = "waitforexitandrun"
# Test directory
self.test_file = "./tmp.AKN6tnueyO"
# Executable
self.test_exe = self.test_file + "/" + "foo"
# Cache
self.test_cache = Path("./tmp.ND7tcK5m3K")
# Steam compat dir
self.test_compat = Path("./tmp.1A5cflhwQa")
# ULWGL-Proton dir
self.test_proton_dir = Path("ULWGL-Proton-jPTxUsKDdn")
# ULWGL-Proton release
self.test_archive = Path(self.test_cache).joinpath(
f"{self.test_proton_dir}.tar.gz"
)
self.test_cache.mkdir(exist_ok=True)
self.test_compat.mkdir(exist_ok=True)
self.test_proton_dir.mkdir(exist_ok=True)
# Mock the proton file in the dir
self.test_proton_dir.joinpath("proton").touch(exist_ok=True)
# Mock the release downloaded in the cache: tmp.5HYdpddgvs/ULWGL-Proton-jPTxUsKDdn.tar.gz
# Expected directory structure within the archive:
#
# +-- ULWGL-Proton-5HYdpddgvs (root directory)
# | +-- proton (normal file)
with tarfile.open(self.test_archive.as_posix(), "w:gz") as tar:
tar.add(
self.test_proton_dir.as_posix(), arcname=self.test_proton_dir.as_posix()
)
Path(self.test_file).mkdir(exist_ok=True)
Path(self.test_exe).touch()
def tearDown(self):
"""Unset environment variables and delete test files after each test."""
for key, val in self.env.items():
if key in os.environ:
os.environ.pop(key)
if Path(self.test_file).exists():
rmtree(self.test_file)
if self.test_cache.exists():
rmtree(self.test_cache.as_posix())
if self.test_compat.exists():
rmtree(self.test_compat.as_posix())
if self.test_proton_dir.exists():
rmtree(self.test_proton_dir.as_posix())
def test_build_command_nofile(self):
"""Test build_command.
A FileNotFoundError should be raised if $PROTONPATH/proton does not exist
NOTE: Also, FileNotFoundError will be raised if the _v2-entry-point (ULWGL) is not in $HOME/.local/share/ULWGL or in cwd
"""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}"]
exe = "{self.test_exe}"
"""
toml_path = self.test_file + "/" + test_toml
result = None
test_command = []
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
# Config
ulwgl_plugins.set_env_toml(self.env, result)
# Prefix
ulwgl_run.setup_pfx(self.env["WINEPREFIX"])
# Env
ulwgl_run.set_env(self.env, result)
# Game drive
ulwgl_plugins.enable_steam_game_drive(self.env)
for key, val in self.env.items():
os.environ[key] = val
# Build
with self.assertRaisesRegex(FileNotFoundError, "proton"):
ulwgl_run.build_command(self.env, test_command)
def test_build_command_toml(self):
"""Test build_command.
After parsing a valid TOML file, be sure we do not raise a FileNotFoundError
"""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
exe = "{self.test_exe}"
"""
toml_path = self.test_file + "/" + test_toml
result = None
test_command = []
test_command_result = None
Path(self.test_file + "/proton").touch()
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
# Config
ulwgl_plugins.set_env_toml(self.env, result)
# Prefix
ulwgl_run.setup_pfx(self.env["WINEPREFIX"])
# Env
ulwgl_run.set_env(self.env, result)
# Game drive
ulwgl_plugins.enable_steam_game_drive(self.env)
for key, val in self.env.items():
os.environ[key] = val
# Build
test_command_result = ulwgl_run.build_command(self.env, test_command)
self.assertTrue(
test_command_result is test_command, "Expected the same reference"
)
# Verify contents of the command
entry_point, opt1, verb, opt2, proton, verb2, exe = [*test_command]
# The entry point dest could change. Just check if there's a value
self.assertTrue(entry_point, "Expected an entry point")
self.assertEqual(opt1, "--verb", "Expected --verb")
self.assertEqual(verb, self.test_verb, "Expected a verb")
self.assertEqual(opt2, "--", "Expected --")
self.assertEqual(
proton,
Path(self.env.get("PROTONPATH") + "/proton").as_posix(),
"Expected the proton file",
)
self.assertEqual(verb2, self.test_verb, "Expected a verb")
self.assertEqual(exe, self.env["EXE"], "Expected the EXE")
def test_set_env_toml_opts_nofile(self):
"""Test set_env_toml for options that are a file.
An error should not be raised if a launch argument is a file
We allow this behavior to give users flexibility at the cost of security
"""
test_toml = "foo.toml"
toml_path = self.test_file + "/" + test_toml
toml_str = f"""
[ulwgl]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{toml_path}"]
exe = "{self.test_exe}"
"""
result = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
ulwgl_plugins.set_env_toml(self.env, result)
# Check if its the TOML file we just created
self.assertTrue(
Path(self.env["EXE"].split(" ")[1]).is_file(),
"Expected a file to be appended to the executable",
)
def test_set_env_toml_nofile(self):
"""Test set_env_toml for values that are not a file.
A FileNotFoundError should be raised if the 'exe' is not a file
"""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
exe = "./bar"
"""
toml_path = self.test_file + "/" + test_toml
result = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
with self.assertRaisesRegex(FileNotFoundError, "exe"):
ulwgl_plugins.set_env_toml(self.env, result)
def test_set_env_toml_err(self):
"""Test set_env_toml for valid TOML.
A TOMLDecodeError should be raised for invalid values
"""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = [[
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
"""
toml_path = self.test_file + "/" + test_toml
result = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
# Env
with self.assertRaisesRegex(TOMLDecodeError, "Invalid"):
ulwgl_plugins.set_env_toml(self.env, result)
def test_set_env_toml_nodir(self):
"""Test set_env_toml if certain key/value are not a dir.
An IsDirectoryError should be raised if the following keys are not dir: proton, prefix
"""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = "foo"
proton = "foo"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
"""
toml_path = self.test_file + "/" + test_toml
result = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
with self.assertRaisesRegex(NotADirectoryError, "proton"):
ulwgl_plugins.set_env_toml(self.env, result)
def test_set_env_toml_tables(self):
"""Test set_env_toml for expected tables.
A ValueError should be raised if the following tables are absent: ulwgl
"""
test_toml = "foo.toml"
toml_str = f"""
[foo]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
"""
toml_path = self.test_file + "/" + test_toml
result = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
with self.assertRaisesRegex(ValueError, "ulwgl"):
ulwgl_plugins.set_env_toml(self.env, result)
def test_set_env_toml_paths(self):
"""Test set_env_toml when specifying unexpanded file path values in the config file.
Example: ~/Games/foo.exe
An error should not be raised when passing unexpanded paths to the config file as well as the prefix, proton and exe keys
"""
test_toml = "foo.toml"
pattern = r"^/home/[\w\d]+" # Expects only unicode decimals and alphanumerics
# Replaces the expanded path to unexpanded
# Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file
path_to_tmp = Path(
Path(__file__).cwd().as_posix() + "/" + self.test_file
).as_posix()
path_to_exe = Path(
Path(__file__).cwd().as_posix() + "/" + self.test_exe
).as_posix()
# Replace /home/[a-zA-Z]+ substring in path with tilda
unexpanded_path = re.sub(
pattern,
"~",
path_to_tmp,
)
unexpanded_exe = re.sub(
pattern,
"~",
path_to_exe,
)
toml_str = f"""
[ulwgl]
prefix = "{unexpanded_path}"
proton = "{unexpanded_path}"
game_id = "{unexpanded_path}"
exe = "{unexpanded_exe}"
"""
# Path to TOML in unexpanded form
toml_path = unexpanded_path + "/" + test_toml
result = None
result_set_env = None
Path(toml_path).expanduser().touch()
with Path(toml_path).expanduser().open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
result_set_env = ulwgl_plugins.set_env_toml(self.env, result)
self.assertTrue(result_set_env is self.env, "Expected the same reference")
# Check that the paths are still in the unexpanded form after setting the env
# In main, we only expand them after this function exits to prepare for building the command
self.assertEqual(
self.env["EXE"], unexpanded_exe, "Expected path not to be expanded"
)
self.assertEqual(
self.env["PROTONPATH"],
unexpanded_path,
"Expected path not to be expanded",
)
self.assertEqual(
self.env["WINEPREFIX"],
unexpanded_path,
"Expected path not to be expanded",
)
self.assertEqual(
self.env["GAMEID"], unexpanded_path, "Expectd path not to be expanded"
)
def test_set_env_toml(self):
"""Test set_env_toml."""
test_toml = "foo.toml"
toml_str = f"""
[ulwgl]
prefix = "{self.test_file}"
proton = "{self.test_file}"
game_id = "{self.test_file}"
launch_args = ["{self.test_file}", "{self.test_file}"]
exe = "{self.test_exe}"
"""
toml_path = self.test_file + "/" + test_toml
result = None
result_set_env = None
Path(toml_path).touch()
with Path(toml_path).open(mode="w") as file:
file.write(toml_str)
with patch.object(
ulwgl_run,
"parse_args",
return_value=argparse.Namespace(config=toml_path),
):
# Args
result = ulwgl_run.parse_args()
self.assertIsInstance(
result, Namespace, "Expected a Namespace from parse_arg"
)
self.assertTrue(vars(result).get("config"), "Expected a value for --config")
# Env
result_set_env = ulwgl_plugins.set_env_toml(self.env, result)
self.assertTrue(result_set_env is self.env, "Expected the same reference")
self.assertTrue(self.env["EXE"], "Expected EXE to be set")
self.assertEqual(
self.env["EXE"],
self.test_exe + " " + " ".join([self.test_file, self.test_file]),
"Expectd GAMEID to be set",
)
self.assertEqual(
self.env["PROTONPATH"],
self.test_file,
"Expected PROTONPATH to be set",
)
self.assertEqual(
self.env["WINEPREFIX"],
self.test_file,
"Expected WINEPREFIX to be set",
)
self.assertEqual(
self.env["GAMEID"], self.test_file, "Expectd GAMEID to be set"
)
if __name__ == "__main__":
unittest.main()

View File

@ -1,20 +0,0 @@
from ulwgl_consts import Color, Level
from typing import Any
def msg(msg: Any, level: Level):
"""Return a log message depending on the log level.
The message will bolden the typeface and apply a color.
Expects the first parameter to be a string or implement __str__
"""
log: str = ""
if level == Level.INFO:
log = f"{Color.BOLD.value}{Color.INFO.value}{msg}{Color.RESET.value}"
elif level == Level.WARNING:
log = f"{Color.BOLD.value}{Color.WARNING.value}{msg}{Color.RESET.value}"
elif level == Level.DEBUG:
log = f"{Color.BOLD.value}{Color.DEBUG.value}{msg}{Color.RESET.value}"
return log

22
libs/customvars_loader.py Normal file
View File

@ -0,0 +1,22 @@
import os
import json
def set_customvars(provided_customvars, default_customvars):
customvars = default_customvars
# Support for the argument (overrides the env var)
if provided_customvars:
try:
provided_customvars = json.loads(provided_customvars)
except json.JSONDecodeError:
print(f"[ERROR] [CUSTOMVARS] Provided CUSTOMVARS={provided_customvars} is not a valid JSON")
print("[ERROR] [CUSTOMVARS] Defaulting to " + str(customvars))
provided_customvars = customvars
print(f"[INFO] [CUSTOMVARS] Provided CUSTOMVARS={provided_customvars}")
customvars = provided_customvars
else:
print("[WARNING] [CUSTOMVARS] CUSTOMVARS is not set. Using default value: '" + str(customvars) + "'")
# Setting the env vars
for key, value in customvars.items():
os.environ[key] = value
# Force check for the launcher at least
return customvars

41
libs/filepath_loader.py Normal file
View File

@ -0,0 +1,41 @@
#import dotenv
import os
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
# NOTE Sanity check for the game path
def set_filepath(provided_filepath, LAUNCHDIR):
# Support for the argument (overrides the env var)
if not provided_filepath:
if "FILEPATH" not in os.environ:
print("[ERROR] [FILEPATH] FILEPATH is not set. Exiting...")
exit(1)
else:
provided_filepath = os.environ["FILEPATH"]
# Get the path to the file
# Distinguish between absolute and relative paths
if os.path.isabs(provided_filepath):
filepath = provided_filepath
else:
filepath = os.path.join(LAUNCHDIR, provided_filepath)
# Check if the file exists
if not mustExist(filepath, fatal=False):
print(f"[WARNING] [FILEPATH] File not found: {filepath}")
print("[WARNING] [FILEPATH] ulwgl will be launched with the argument provided but it may not work.")
print("[INFO] [FILEPATH] Disregard this message if you are using an internal or custom binary (e.g. winecfg...)")
filepath=provided_filepath
print("[OK] [FILEPATH] " + filepath + "\n")
# Quick space sanity check
if " " in filepath:
print(f"[WARNING] [FILEPATH] {filepath} contains spaces")
print("[WARNING] [FILEPATH] This may cause issues with the launcher")
print("[WARNING] [FILEPATH] Consider renaming the file or moving it to a different location")
print("[QUICK FIX] [FILEPATH] Trying to escape the spaces")
filepath = filepath.replace(" ", "\ ")
print(f"[QUICK FIX] [FILEPATH] {filepath}\n")
return filepath

33
libs/gameid_loader.py Normal file
View File

@ -0,0 +1,33 @@
#import dotenv
import os
# SECTION Loading the .env file
#dotenv.load_dotenv()
# NOTE Support for game_id
def load_gameid(provided_game_id, default_game_id, ids):
game_id = default_game_id
if provided_game_id:
game_id = provided_game_id
print(f"[INFO] [GAMEID] Provided GAMEID={game_id}")
else:
# We need a valid GAMEID in the .env file in this case
if "GAMEID" not in os.environ:
print("[WARNING] [GAMEID] GAMEID is not set. Using default value: '" + str(game_id) + "'")
else:
game_id = os.environ["GAMEID"]
print(f"[INFO] [GAMEID] GAMEID={game_id}")
# If is not a digit, we will try to load from the ids object
if not str(game_id).isdigit():
print(f"[INFO] [GAMEID] {game_id} is not a digit: trying to load from ids.json file")
if str(game_id) in ids:
game_id = ids[str(game_id)]
print(f"[OK] [GAMEID] GAMEID={game_id}")
else:
print(f"[WARNING] [GAMEID] {game_id} is not in the ids.json file")
print("[WARNING] [GAMEID] Defaulting to 0")
game_id = 0
print(f"[OK] [GAMEID] GAMEID={game_id}")
return game_id

33
libs/ids_loader.py Normal file
View File

@ -0,0 +1,33 @@
#import dotenv
import os
import json
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
# dotenv.load_dotenv()
# NOTE Loading ids.json file
def load_ids(provided_ids, default_ids, default_ids_json_path):
ids_json_path = default_ids_json_path
ids = default_ids
# Support for the argument (overrides the env var)
if not provided_ids:
if "IDS_JSON" in os.environ:
ids_json_path = os.environ["IDS_JSON"]
print(f"[INFO] [IDS] IDS_JSON={ids_json_path}")
else:
print(f"[WARNING] [IDS] IDS_JSON is not set. Using default value: {default_ids_json_path}")
if provided_ids:
ids_json_path = provided_ids
if not mustExist(ids_json_path, fatal=False):
print(f"[WARNING] [IDS] Using default value: {default_ids_json_path}")
ids_json_path = default_ids_json_path
print(f"[OK] [IDS] IDS_JSON={ids_json_path}")
ids = json.loads(open(ids_json_path, "r").read())
# Support for non existing ids.json file
if not mustExist(ids_json_path, fatal=False):
print(f"[WARNING] [IDS] {ids_json_path} does not exist")
print("[WARNING] [IDS] Defaulting to 0 will be used in case of non digit game_id")
ids = {}
return ids, ids_json_path

36
libs/mustExist.py Normal file
View File

@ -0,0 +1,36 @@
import os
import sys
# Helper
def mustExist(path, fatal=True, is_dir=False):
# Sanitize the path
path = os.path.expanduser(path)
path = path.strip()
# Determine if the path is absolute or relative
if not os.path.isabs(path):
print(f"[INFO] [FILECHECK] {path} is a relative path")
path = os.path.abspath(path)
print(f"[INFO] [FILECHECK] Now it is an absolute path: {path}")
else:
print(f"[INFO] [FILECHECK] {path} is an absolute path")
if is_dir:
print(f"[INFO] [FILECHECK] Checking if '{path}' is a directory")
if not os.path.isdir(path):
print(f"[ERROR] [FILECHECK] '{path}' directory does not exist")
if fatal:
sys.exit(1)
else:
return False
else:
print(f"[OK] [FILECHECK] '{path}' is a directory")
return True
else:
if not os.path.exists(path):
print(f"[ERROR] [FILECHECK] '{path}' does not exist")
if fatal:
sys.exit(1)
else:
return False
else:
print(f"[OK] [FILECHECK] '{path}' exists")
return True

View File

@ -0,0 +1,22 @@
import os
#import dotenv
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
def set_postdirectives(provided_postdirectives):
postdirectives = ""
# Support for the argument (overrides the env var)
if provided_postdirectives is not None and provided_postdirectives != "":
print(f"[INFO] [POSTDIRECTIVES] Provided ULWGLDIR={provided_postdirectives}")
postdirectives = provided_postdirectives
else:
print("[INFO] [POSTDIRECTIVES] POSTDIRECTIVES is not set. Looking for the env var...")
if "POSTDIRECTIVES" not in os.environ:
print("[WARNING] [POSTDIRECTIVES] POSTDIRECTIVES is not set. Using default value: '" + postdirectives + "'")
else:
postdirectives = os.environ["POSTDIRECTIVES"]
print(f"[INFO] [POSTDIRECTIVES] POSTDIRECTIVES={postdirectives}")
return postdirectives

View File

@ -0,0 +1,22 @@
import os
#import dotenv
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
def set_predirectives(provided_predirectives):
predirectives = ""
# Support for the argument (overrides the env var)
if provided_predirectives is not None and provided_predirectives != "":
print(f"[INFO] [PREDIRECTIVES] Provided ULWGLDIR={provided_predirectives}")
predirectives = provided_predirectives
else:
print("[INFO] [PREDIRECTIVES] PREDIRECTIVES is not set. Looking for the env var...")
if "PREDIRECTIVES" not in os.environ:
print("[WARNING] [PREDIRECTIVES] PREDIRECTIVES is not set. Using default value: '" + predirectives + "'")
else:
predirectives = os.environ["PREDIRECTIVES"]
print(f"[INFO] [PREDIRECTIVES] PREDIRECTIVES={predirectives}")
return predirectives

36
libs/protonpath_loader.py Normal file
View File

@ -0,0 +1,36 @@
import os
#import dotenv
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
# NOTE Setting the Proton path with a fallback
def set_protonpath(provided_protonpath, default_proton_path, UWINEDIR):
proton_path = default_proton_path
# Support for the argument (overrides the env var)
if provided_protonpath:
proton_path = provided_protonpath
print(f"[INFO] Provided PROTONPATH={proton_path}")
# Distinguish between absolute and relative paths to support versioning
if not os.path.isabs(proton_path):
print(f"[INFO] {proton_path} is a relative path")
print(f"[INFO] Appending {UWINEDIR}/protons/ to {proton_path}")
proton_path = os.path.join(UWINEDIR, "protons", proton_path)
print("[INFO] Now it is an absolute path: " + proton_path)
if not mustExist(proton_path, fatal=False):
print(f"[WARNING] {proton_path} does not exist")
print("[WARNING] Defaulting to " + default_proton_path)
proton_path = default_proton_path
else:
# We need a valid PROTONPATH in the .env file in this case
if "PROTONPATH" not in os.environ:
print("[WARNING] PROTONPATH is not set. Using default value: '" + proton_path + "'")
else:
proton_path = os.environ["PROTONPATH"]
print(f"[INFO] PROTONPATH={proton_path}")
# We need this to exist
mustExist(proton_path)
print(f"[OK] PROTONPATH={proton_path}")
return proton_path

30
libs/ulwgl_loader.py Normal file
View File

@ -0,0 +1,30 @@
import os
#import dotenv
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
def set_ulwgldir(provided_ulwgldir, default_ulwgl_dir):
ulwgl_dir = default_ulwgl_dir
# Support for the argument (overrides the env var)
if provided_ulwgldir:
print(f"[INFO] [ULWGLDIR] Provided ULWGLDIR={provided_ulwgldir}")
if not mustExist(provided_ulwgldir, fatal=False, is_dir=True):
print(f"[WARNING] [ULWGLDIR] {provided_ulwgldir} does not exist")
print("[WARNING] [ULWGLDIR] Defaulting to " + default_ulwgl_dir)
ulwgl_dir = default_ulwgl_dir
else:
ulwgl_dir = provided_ulwgldir
else:
# We need a valid UWINEDIR in the .env file in this case
if "ULWLGDIR" not in os.environ:
print("[WARNING] [ULWGLDIR] ULWGLDIR is not set. Using default value: '" + ulwgl_dir + "'")
else:
ulwgl_dir = os.environ["ULWLGDIR"]
print(f"[INFO] [ULWGLDIR] ULWGLDIR={ulwgl_dir}")
# Force check for the launcher at least
mustExist(ulwgl_dir + "/ULWGL")
return ulwgl_dir

42
libs/ulwlg_runner.py Normal file
View File

@ -0,0 +1,42 @@
import os
def ulwlg_run(executable_path, ulwlg_dir, proton_dir, wineprefix, game_id):
executable_dir = os.path.dirname(executable_path)
os.environ["ULWGL_ID"] = str(game_id)
os.environ["STEAM_COMPAT_APP_ID"] = "0" # REVIEW Is this ok?
os.environ["SteamAppId"] = os.environ["STEAM_COMPAT_APP_ID"]
os.environ["SteamGameId"] = os.environ["STEAM_COMPAT_APP_ID"]
os.environ["PROTON_VERB"] = "waitforexitandrun"
os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
os.environ["STEAM_COMPAT_DATA_PATH"] = wineprefix
os.environ["STEAM_COMPAT_SHADER_PATH"] = wineprefix + "/shadercache"
os.environ["PROTON_CRASH_REPORT_DIR"] = "/tmp/ULWGL_crashreports"
os.environ["FONTCONFIG_PATH"] = ""
os.environ["STEAM_COMPAT_TOOL_PATHS"] = proton_dir + ":" + ulwlg_dir
os.environ["STEAM_COMPAT_MOUNTS"] = proton_dir + ":" + ulwlg_dir
if os.environ.get("STEAM_COMPAT_INSTALL_PATH") is None:
os.environ["STEAM_COMPAT_INSTALL_PATH"] = executable_dir
# Composing the command
composed_command = (
ulwlg_dir
+ "/ULWGL --verb=waitforexitandrun"
+ " -- "
+ proton_dir
+ "/proton waitforexitandrun "
+ executable_path
+ "$@"
)
print("[ULWGL_RUNNER] Composed command: " + composed_command)
os.system(composed_command)

30
libs/wineprefix_loader.py Normal file
View File

@ -0,0 +1,30 @@
#import dotenv
import os
import libs.mustExist as sanity
mustExist = sanity.mustExist
# SECTION Loading the .env file
#dotenv.load_dotenv()
# NOTE Setting the Wine prefix with a fallback
def set_wineprefix(provided_wineprefix, default_wine_prefix):
wine_prefix = default_wine_prefix
# Support for the argument (overrides the env var)
if provided_wineprefix:
wine_prefix = provided_wineprefix
print(f"[INFO] [WINEPREFIX] Provided WINEPREFIX={wine_prefix}")
if not mustExist(wine_prefix, fatal=False):
print(f"[WARNING] [WINEPREFIX] {wine_prefix} does not exist")
print("[WARNING] [WINEPREFIX] Defaulting to " + default_wine_prefix)
wine_prefix = default_wine_prefix
else:
# We need a valid WINEPREFIX in the .env file in this case
if "WINEPREFIX" not in os.environ:
print("[WARNING] [WINEPREFIX] WINEPREFIX is not set. Using default value: '" + wine_prefix + "'")
else:
wine_prefix = os.environ["WINEPREFIX"]
print(f"[INFO] [WINEPREFIX] WINEPREFIX={wine_prefix}")
# We need this to exist
mustExist(wine_prefix)
print(f"[OK] [WINEPREFIX] WINEPREFIX={wine_prefix}")
return wine_prefix

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
dotenv
tabulate

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

126
uwine Executable file
View File

@ -0,0 +1,126 @@
#!/bin/python
import os
import argparse
from tabulate import tabulate
import dotenv
import json
import libs.ids_loader as ids_loader
import libs.gameid_loader as gameid_loader
import libs.protonpath_loader as protonpath_loader
import libs.wineprefix_loader as wineprefix_loader
import libs.filepath_loader as filepath_loader
import libs.ulwgl_loader as ulwgl_loader
import libs.customvars_loader as customvars_loader
import libs.predirectives_loader as predirectives_loader
import libs.postdirectives_loader as postdirectives_loader
import libs.ulwlg_runner as ulwlg_runner
# SECTION Constants
LAUNCHDIR = os.getcwd()
print("[*] Launching in " + LAUNCHDIR)
UWINEDIR = os.path.dirname(os.path.realpath(__file__))
print("[*] UWINE is installed in " + UWINEDIR)
# SECTION Default values
ulwgl_dir = UWINEDIR + "/launcher"
proton_path = UWINEDIR + "/protons/current"
wine_prefix = UWINEDIR + "/PREFIX"
ids_json_path = UWINEDIR + "/ids.json"
ids = {}
game_id = 0
filepath = ""
envfile = ".env"
# NOTE Parsing the arguments
parser = argparse.ArgumentParser(
prog="uwine",
description="ULWGL Launcher Wrapper for human beings",
epilog="https://github.com/thecookingsenpai/UWINE",
)
parser.add_argument(
"filepath", help="Path to the file to be launched", type=str, nargs="?", default=None
)
parser.add_argument(
"-l", "--load", help="Load a specific env file", type=str, dest="envfile"
)
parser.add_argument(
"-g", "--game-id", dest="gameid", help="Game ID to be used", type=int
)
parser.add_argument(
"-p", "--proton-path", dest="protonpath", help="Path to the Proton installation"
)
parser.add_argument("-i", "--ids-json", dest="ids", help="Path to the ids.json file")
parser.add_argument(
"-w", "--wine-prefix", dest="wineprefix", help="Path to the Wine prefix"
)
parser.add_argument(
"-u", "--ulwgl", dest="ulwlgdir", help="Path to the ULWGL installation"
)
parser.add_argument(
"-a", "--additionalargs", dest="additionalargs", help="Additional arguments to be passed to the software (at the end, as a string)", type=str, default=""
)
parser.add_argument("-v", "--version", action="version", version="%(prog)s 0.1")
args = parser.parse_args()
print(args.ulwlgdir)
# Loading the .env file
if args.envfile:
envfile = args.envfile
print("[*] Loading the .env file: " + envfile)
dotenv.load_dotenv(dotenv_path=envfile)
# Ensuring we support either none or some customvars
if os.environ["CUSTOMVARS"]:
print("[INFO] [CUSTOMVARS] " + os.environ["CUSTOMVARS"])
env_defined_customvars = os.environ["CUSTOMVARS"]
else:
env_defined_customvars = {}
if __name__ == "__main__":
# SECTION Loading methods
ulwgl_dir = ulwgl_loader.set_ulwgldir(args.ulwlgdir, UWINEDIR)
ids, ids_json_path = ids_loader.load_ids(args.ids, ids, ids_json_path)
loaded_customvars = customvars_loader.set_customvars(env_defined_customvars, {})
os.environ["GAMEID"] = str(gameid_loader.load_gameid(args.gameid, game_id, ids))
os.environ["PROTONPATH"] = protonpath_loader.set_protonpath(
args.protonpath, proton_path, UWINEDIR
)
os.environ["WINEPREFIX"] = wineprefix_loader.set_wineprefix(
args.wineprefix, wine_prefix
)
filepath = filepath_loader.set_filepath(args.filepath, LAUNCHDIR)
# Directives support
predirectives = predirectives_loader.set_predirectives("") # Future support for postdirectives in cli
postdirectives = postdirectives_loader.set_postdirectives(args.additionalargs)
# SECTION Launching the game
print("\n[*] Launching the game...")
# ANCHOR Recap
# Lets make a nice table to show the user what we are going to do
print(
tabulate(
[
["ULWGLDIR", ulwgl_dir],
["WINEPREFIX", os.environ["WINEPREFIX"]],
["PROTONPATH", os.environ["PROTONPATH"]],
["IDS_JSON", ids_json_path],
["GAMEID", os.environ["GAMEID"]],
["PREDIRECTIVES", predirectives],
["FILEPATH", filepath],
["POSTDIRECTIVES", postdirectives],
["CUSTOMVARS", loaded_customvars]
],
headers=["Variable", "Value"],
tablefmt="fancy_grid",
)
)
# Launching with ulwlg_runner
ulwlg_runner.ulwlg_run(filepath, ulwgl_dir, os.environ["PROTONPATH"], os.environ["WINEPREFIX"], os.environ["GAMEID"])

10
uwine.desktop Normal file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.1
Type=Application
Name=UWine
Comment=Launch with UWine!
Icon=q4wine
Exec=uwine
Terminal=true
Actions=
Categories=Game;

10
uwine_launcher.desktop Normal file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.1
Type=Application
Name=UWine Launcher
Comment=Launch .uwine files (and compatible ones)
Icon=winetricks
Exec=uwine -l
Terminal=true
Actions=
Categories=Game;