Compare commits

...

9 Commits
master ... dev

12 changed files with 1342 additions and 0 deletions

1
.gitignore vendored
View File

@ -129,3 +129,4 @@ dmypy.json
# Pyre type checker
.pyre/
*.glade~

10
setup.py 100644
View File

@ -0,0 +1,10 @@
from distutils.core import setup
setup(name="winestarter",
version="0.0.1-dev",
description="Setup tools for wine",
author="Marko Semet",
author_email="marko@marko10-000.de",
url="https://marko10-000.de/project/winestarter",
packages=["winestarter"]
)

View File

@ -0,0 +1,53 @@
from . import gui
import asyncio
import argparse
import threading
LOOP = asyncio.get_event_loop()
# Args parse
parser = argparse.ArgumentParser(description="Wine Starter")
parser.add_argument("--config", "-c", dest="configs", type=str,
action="append", nargs=1, help="Configuration to make software runable.")
parser.add_argument("--not-run", nargs="?", const=True,
default=False, help="Doesn't start software directly.")
parser.add_argument("start", type=str, nargs="?",
const=None, help="Instance to run.")
args = parser.parse_args()
# Run gui and asyncio
_terminate = threading.Lock()
_terminate.acquire()
async def _run_async():
# Wait for gui thread init
while not _terminate.acquire(blocking=False):
await asyncio.sleep(0)
# Run sets
try:
await gui.app.update_instances()
except:
gui.app.quit()
raise
# Wait for async ends
while not _terminate.acquire(blocking=False):
await asyncio.sleep(0.1)
def _run_gui():
def inited_func():
_terminate.release()
try:
gui.run_gui(asyncloop=LOOP, inited=inited_func, configs=list(
map(lambda x: x[0], args.configs)))
finally:
_terminate.release()
threading.Thread(target=_run_gui).start()
LOOP.run_until_complete(_run_async())

View File

@ -0,0 +1,68 @@
from . import paths
import asyncio
import hashlib
import json
import os
class ConfigAndBackup():
__instance = None
def _hash_instance(self):
hash = hashlib.sha3_256()
hash.update(self.__instance.get_instance().encode())
return hash.hexdigest()
async def _list_installed_hashes(self):
# List archives
archives_proc = await asyncio.create_subprocess_exec(b"borg", b"list", b"--json", paths.BACKUP_DIR.encode(), stdout=asyncio.subprocess.PIPE)
stdout, stderr = await archives_proc.communicate()
if archives_proc.returncode != 0:
raise RuntimeError("Borg crashed.")
archives = []
for i in json.loads(stdout)["archives"]:
archives.append(i["name"])
# Filter own
instance_hash = self._hash_instance()
return list(map(lambda x: x[len(instance_hash) + 1:], filter(lambda x: x.startswith(instance_hash), archives)))
async def _run_backup(self, name: bytes, *args, path:bytes=None):
# Check if exists (when true delete it)
archive_name = b"%s::%s" % (paths.BACKUP_DIR.encode(), name)
if name in await self._list_installed_hashes():
process = await asyncio.create_subprocess_exec(b"borg", b"delete", archive_name)
await process.wait()
if process.returncode != 0:
raise RuntimeError("Backup can't be created.")
# Create backup
if path is None:
path = self.__instance.get_instance_path().encode()
process = await asyncio.create_subprocess_exec(b"borg", b"create", b"-C", b"zstd,5", archive_name, *args, cwd=path)
await process.wait()
if process.returncode != 0:
raise RuntimeError("Backup can't be created.")
def __init__(self, instance):
# Set values
self.__instance = instance
async def init(self):
# Check borg
process = await asyncio.create_subprocess_exec(b"borg", b"--version", stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
await process.wait()
if process.returncode != 0:
raise RuntimeError("Can't find borg backup.")
# Create archive
process = await asyncio.create_subprocess_exec(b"borg", b"info", paths.BACKUP_DIR.encode(), stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
await process.wait()
if process.returncode != 0:
process = await asyncio.create_subprocess_exec(b"borg", b"init", b"-e", b"none", paths.BACKUP_DIR.encode(), stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
await process.wait()
if process.returncode != 0:
raise RuntimeError("Borg repo is broken.")
async def gen_install_backup(self, name:str):
await self._run_backup(b"install_%s" + (name.encode(),), b".")

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkMenu" id="mainMenu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="mainMenuSettings">
<property name="label">gtk-preferences</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="mainMenuAbout">
<property name="label">gtk-dialog-info</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</object>
</child>
</object>
<object class="GtkApplicationWindow" id="main_window">
<property name="width_request">800</property>
<property name="height_request">600</property>
<property name="can_focus">False</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="main_header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Wine Starter</property>
<property name="subtitle" translatable="yes">&lt;Application&gt;</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popup">mainMenu</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkStackSwitcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="stack">main_content</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="main_content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;TO REPLACE&gt;</property>
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

150
winestarter/gui.py 100644
View File

@ -0,0 +1,150 @@
from . import instance
import os
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio
import asyncio
# Templates
_template_path = os.path.join(os.path.split(__file__)[0], "templates")
@Gtk.Template(filename=os.path.join(_template_path, "StartWidget.glade"))
class _StartWidget(Gtk.Box):
__gtype_name__ = "StartWidget"
__image = Gtk.Template.Child(name="start_image")
__name: Gtk.Label = Gtk.Template.Child("start_name")
__settings = Gtk.Template.Child("start_settings")
__run = Gtk.Template.Child("start_run")
__add_snapshot = Gtk.Template.Child("start_add_snapshot")
__snapshots = Gtk.Template.Child("start_snapshots")
__instance:instance.Instance
__aplication = None
def __init__(self, application, instance:instance.Instance, *args, **kargs):
# Init template
super().__init__(*args, **kargs)
self.__instance = instance
self.__aplication = application
# Set values
self.__name.set_label(instance.get_instance())
# Run callback
self.__run.connect("clicked", self.__run_clicked)
def __run_clicked(self, button):
self.__aplication.run_async(self.__instance.run(), wait=False)
@Gtk.Template(filename=os.path.join(_template_path, "AboutDialog.glade"))
class _AboutDialog(Gtk.AboutDialog):
__gtype_name__ = "AboutDialog"
template_type = "AboutDialog"
# Application
class _Application(Gtk.Application):
_builder: Gtk.Builder
_window: Gtk.ApplicationWindow
_stack: Gtk.Stack
_configsSource: list
_instance: str
_configs: dict
_configs_box:dict
_inited = None
_asyncloop = None
def __init__(self, asyncloop, inited, configs: list = [], instance: str = None, *args, **kargs):
super().__init__(*args, application_id="de.marko10_000.WineStarter", **kargs)
self._configsSource = list(configs)
self._instance = str(instance)
self._inited = inited
self._asyncloop = asyncloop
def do_startup(self):
# Load configs
configs = self._configs = {}
for i in self._configsSource:
tmp = instance.gen_instance(i)
configs[tmp.get_instance()] = tmp
# Start gtk application
Gtk.Application.do_startup(self)
def do_activate(self):
# Load gui
self._builder = builder = Gtk.Builder()
builder.add_from_file(os.path.join(
os.path.split(__file__)[0], "gui.glade"))
# Gen gui
self._window = builder.get_object("main_window")
self._stack = builder.get_object("main_content")
self._configs_box = instance_box = {}
if len(self._configs) == 1:
self._window.remove(self._window.get_child())
tmp = Gtk.Box()
tmp.show()
self._window.add(tmp)
instance_box[list(self._configs.keys())[0]] = tmp
else:
for i in self._stack.get_children():
self._stack.remove(i)
raise NotImplementedError()
# Add main window callbacks
def show_about(menu_entry):
_AboutDialog(parent=self._window).show_all()
builder.get_object("mainMenuAbout").connect("activate", show_about)
# Show main window
self._window.set_application(self)
self._window.show_all()
# Notify init end
self._inited()
def _set_instance_ui(self, instance:str, gui):
# Clean box
box = self._configs_box[instance]
for i in box.get_children():
box.remove(i)
# Set item
box.pack_start(gui, True, True, 0)
gui.show_all()
async def update_instances(self, start:str=None):
# Load gui for instances
for iID, i in self._configs.items():
tmp = _StartWidget(self, i)
self._set_instance_ui(iID, tmp)
tmp.show_all()
# Start
if start is not None:
raise NotImplementedError() # TODO
def run_async(self, command, wait:bool=True):
# Wait
if wait:
asyncio.run_coroutine_threadsafe(command, self._asyncloop)
else:
async def runner():
self._asyncloop.create_task(command)
asyncio.run_coroutine_threadsafe(runner(), self._asyncloop)
app: _Application
def run_gui(asyncloop, inited=lambda: None, configs: list = None):
global app
app = _Application(inited=inited, configs=configs, asyncloop=asyncloop)
app.run()

View File

@ -0,0 +1,323 @@
import abc
import asyncio
import hashlib
import os
import shutil
import yaml
from . import paths, wine, config
# Step
class Step(abc.ABC):
@abc.abstractclassmethod
async def run(self, wine, instance):
raise NotImplementedError()
@abc.abstractclassmethod
async def content_to_hash(self, wine, instance):
raise NotImplementedError()
class _StepPreInit(Step):
def _find_drive_c(self, wine):
return os.path.join(os.path.abspath(wine.path), "drive_c")
def _gen_paths(self, wine, instance):
# Find paths
drive_c = self._find_drive_c(wine)
volume_dir = os.path.abspath(os.path.join(paths.VOLUME_DIR, instance.get_instance()))
# Iterate
for volume, path in instance.get_volumes().items():
if "\x00" in volume or "/" in volume or "\\" in volume:
raise ValueError("Volume name contains slash or backslash")
voldir = os.path.join(volume_dir, volume)
pathdir = os.path.join(drive_c, path)
yield (voldir, pathdir)
async def run(self, wine:wine.WineConfig, instance):
# Reset path
shutil.rmtree(wine.path)
os.makedirs(self._find_drive_c(wine), exist_ok=True)
# Create volumes
for volume, path in self._gen_paths(wine, instance):
os.makedirs(volume, exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)
os.symlink(os.path.relpath(volume, os.path.dirname(path)), path)
async def content_to_hash(self, wine, instance):
return b"\x00".join(map(lambda x: b"%s\x00%s" % x, map(lambda x: (x[0].encode("UTF-8"), x[1].encode("UTF-8")), sorted(self._gen_paths(wine, instance)))))
class _StepWineInit(Step):
async def run(self, wine, isinstance):
result = await wine.run_command([b"wineboot", b"--init"], with_display=False)
if not result:
raise RuntimeError() # TODO: gen exception + handling
return result
async def content_to_hash(self, wine, instance):
version = await asyncio.create_subprocess_exec(b"wine", b"--version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await version.communicate()
if version.returncode != 0:
raise RuntimeError("Can't get wine version.")
return stdout.splitlines()[0]
class _StepExec(Step):
__command: list
__in_drive_c: bool
__cwd_path: str
def __init__(self, command, in_drive_c: bool = False, cwd_path: str = None):
self.__command = list(map(str.encode, command))
self.__in_drive_c = in_drive_c
self.__cwd_path = cwd_path
async def run(self, wine, instance):
tmp_cwd = self.__cwd_path
if self.__in_drive_c:
tmp_cwd = None
result = await wine.run_command(args=self.__command, cwd_in_instance=self.__in_drive_c, cwd_path=tmp_cwd)
if not result:
print(repr(result))
raise RuntimeError() # TODO: Exception
return result
async def content_to_hash(self, wine, isinstance):
return b"\x00".join(self.__command)
# Instance management
class Instance():
__instance: str
__run: _StepExec
__wine: wine.WineConfig
__steps: list
__volumes: dict
__backup: config.ConfigAndBackup
__has_to_restore_backup: bool
def __gen_hash(self, last_hash:bytes, name:str, data:bytes):
hasher = hashlib.sha3_256()
hasher.update(last_hash)
hasher.update(name.encode("UTF-8"))
hasher.update(data)
return hasher.hexdigest()
def __init__(self, instance: str, run: _StepExec, steps: list = None, volumes: dict = {}):
if not instance:
raise ValueError("instance name can't be empty")
self.__instance = str(instance)
self.__run = run
self.__wine = wine.WineConfig(os.path.join(
paths.INSTANCE_DIR, instance), os.path.join(paths.FAKEHOME_DIR, instance))
self.__steps = list(steps)
self.__volumes = dict(volumes)
self.__has_to_restore_backup = True
async def __run_install(self, name: str, step: Step, last_hash: bytes, stop_wine:bool=True):
if not isinstance(step, Step):
raise ValueError("step have to be a Step.")
# TODO: Check if allready done
# TODO: Update gui (show install window)
# TODO: Update gui (show that step is running)
result = await step.run(self.__wine, self)
if stop_wine:
await self.__wine.stop_wine()
# Gen backup and return
next_hash = self.__gen_hash(last_hash, name, await step.content_to_hash(self.__wine, self))
await self.__backup.gen_install_backup(next_hash)
return next_hash.encode("UTF-8")
async def install(self):
# TODO: Set gui information
last_hash = await self.__run_install("", _StepPreInit(), b"", stop_wine=False)
last_hash = await self.__run_install("init", _StepWineInit(), last_hash)
for name, step in self.__steps:
last_hash = await self.__run_install(name, step, last_hash)
def get_instance(self):
return self.__instance
def get_volumes(self):
return dict(self.__volumes)
def get_instance_path(self):
return self.__wine.path
async def is_installed(self):
return False # TODO
async def run(self):
# Create backup
self.__backup = config.ConfigAndBackup(self)
await self.__backup.init()
# Install and run
await self.install()
# TODO: Update gui (hide main window)
await self.__run.run(self.__wine, self)
# Create instance
def _get_relative_file(current_file, file):
return os.path.abspath(os.path.join(os.path.split(current_file)[0], file))
def _gen_run(file, config):
config = dict(config)
# Load command
if "command" not in config:
raise ValueError(
"Can't find command in exec in file " + repr(file) + ".")
if not isinstance(config["command"], list):
raise ValueError(
"Command in exec is not an array in file " + repr(file) + ".")
command = config["command"]
del config["command"]
# Check if use in_drive_c
in_drive_c = False
if "in_drive_c" in config:
if config["in_drive_c"]:
in_drive_c = bool(config["in_drive_c"])
del config["in_drive_c"]
# Check for unused arguments
if config:
raise ValueError("Unused attributes " +
", ".join(config.keys()) + " in file " + repr(file) + ".")
# Return new exec step
return _StepExec(command=command, in_drive_c=in_drive_c, cwd_path=os.path.dirname(file))
def _load_volumes(file, config):
# Precheck
if not isinstance(config, list):
raise ValueError(
"Volume list have to be a list in file " + repr(file) + ".")
# Volumes
volumes = []
for i in config:
# Check entry
if not isinstance(i, dict):
raise ValueError(
"Volume instance have to be an dict in file " + repr(file) + ".")
i = dict(i)
# Load name and path
if "name" not in i:
raise ValueError(
"Can't find name in volume info in file " + repr(file) + ".")
if "path" not in i:
raise ValueError(
"Can't find path in volume info in file " + repr(file) + ".")
name = str(i["name"])
path = str(i["path"])
del i["name"]
del i["path"]
volumes.append((name, path))
# Check for unused attributes
if i:
raise ValueError("Volume has unused attirbutes " +
", ".join(i.keys()) + " in file " + repr(file) + ".")
return volumes
def _load_module(file: str, content: dict):
# Check if string to load file
if isinstance(content, str):
file = _get_relative_file(file, content)
with open(file, "rb") as f:
content = yaml.load(f, Loader=yaml.SafeLoader)
content = dict(content)
# Load sub modules
result = []
volumes = []
if "modules" in content:
modules = content["modules"]
del content["modules"]
if not isinstance(modules, list):
raise ValueError(
"\"modules\" isn't a list in file " + repr(modules) + ".")
for i in modules:
tmp_result, tmp_volumes = _load_module(file, i)
result += tmp_result
volumes += tmp_volumes
# Load volumes
if "volumes" in content:
volumes += _load_volumes(file, content["volumes"])
del content["volumes"]
# Load name
if "name" not in content:
raise ValueError(
"name is missing in module in file " + repr(file) + ".")
name = content["name"]
del content["name"]
# Check if type exists
if "type" not in content:
raise ValueError("Can't find type in file " + repr(file) + ".")
type_name = content["type"]
del content["type"]
if type_name == "exec":
if tuple(content.keys()) != ("run",):
raise ValueError("Exec only supports \"run\" attribute but has " +
", ".join(content.keys()) + " in file " + repr(file) + ".")
result.append((name, _gen_run(file, content["run"])))
else:
raise ValueError("Type " + repr(type_name) +
" is unknown in file " + repr(file) + ".")
# Cleanup modules
return result, volumes
def gen_instance(file):
# Load base data
with open(file, "rb") as f:
content = dict(yaml.load(f, Loader=yaml.SafeLoader))
if "name" not in content:
raise ValueError("Can't find name inside of " + repr(file) + ".")
name = content["name"]
del content["name"]
# Load run
if "run" not in content:
raise ValueError("Can't find run inside of " + repr(file) + ".")
run = _gen_run(file, content["run"])
del content["run"]
# Load modules
modules = []
volumes = []
if "modules" not in content:
raise ValueError("Can't find \"modules\" in " + repr(file) + ".")
if not isinstance(content["modules"], list):
raise ValueError(
"\"modules\" have to be an array in file " + repr(file) + ".")
for i in content["modules"]:
tmp_modules, tmp_volumes = _load_module(file, i)
modules += tmp_modules
volumes += tmp_volumes
del content["modules"]
# Check unused attributes
if content:
raise ValueError("Unused attributes " +
", ".join(content.keys()) + " in file " + repr(file) + ".")
# Return new instance
return Instance(instance=name, run=run, steps=modules, volumes=dict(volumes))

View File

@ -0,0 +1,89 @@
import os
# Find dirs
INSTANCE_DIR = None
FAKEHOME_DIR = None
CONFIG_DIR = None
BACKUP_DIR = None
DOWNLOADS_DIR = None
VOLUME_DIR = None
if os.environ.get("XDG_CACHE_HOME", None) is not None:
# Check if in flatpak
if os.environ.get("FLATPAK_ID", None) is None:
tmp = os.path.join(os.environ.get("XDG_CACHE_HOME"), "winestarter")
else:
tmp = os.environ.get("XDG_CACHE_HOME")
# Set paths for backup and downloads
if BACKUP_DIR is None:
BACKUP_DIR = os.path.join(tmp, "backups")
if DOWNLOADS_DIR is None:
DOWNLOADS_DIR = os.path.join(tmp, "downloads")
del tmp
if os.environ.get("XDG_CONFIG_HOME", None) is not None:
# Check if in flatpak
if os.environ.get("FLATPAK_ID", None) is None:
tmp = os.path.join(os.environ.get("XDG_CONFIG_HOME"), "winestarter")
else:
tmp = os.environ.get("XDG_CONFIG_HOME")
# Set paths for config dir
if CONFIG_DIR is None:
CONFIG_DIR = os.path.join(tmp, "config")
del tmp
if os.environ.get("XDG_DATA_HOME", None) is not None:
# Check if in flatpak
if os.environ.get("FLATPAK_ID", None) is None:
tmp = os.path.join(os.environ.get("XDG_DATA_HOME"), "winestarter")
else:
tmp = os.environ.get("XDG_DATA_HOME")
# Set paths for instances, fakehomes, config, backups and downloads
if INSTANCE_DIR is None:
INSTANCE_DIR = os.path.join(tmp, "instances")
if FAKEHOME_DIR is None:
FAKEHOME_DIR = os.path.join(tmp, "fakehomes")
if CONFIG_DIR is None:
CONFIG_DIR = os.path.join(tmp, "config")
if BACKUP_DIR is None:
BACKUP_DIR = os.path.join(tmp, "backups")
if DOWNLOADS_DIR is None:
DOWNLOADS_DIR = os.path.join(tmp, "downloads")
if VOLUME_DIR is None:
VOLUME_DIR = os.path.join(tmp, "volumes")
del tmp
if os.environ.get("HOME", None):
# As backup the home dir
tmp = os.path.join(os.environ.get("HOME"), ".winestarter")
if INSTANCE_DIR is None:
INSTANCE_DIR = os.path.join(tmp, "instances")
if FAKEHOME_DIR is None:
FAKEHOME_DIR = os.path.join(tmp, "fakehomes")
if CONFIG_DIR is None:
CONFIG_DIR = os.path.join(tmp, "config")
if BACKUP_DIR is None:
BACKUP_DIR = os.path.join(tmp, "backups")
if DOWNLOADS_DIR is None:
DOWNLOADS_DIR = os.path.join(tmp, "downloads")
if VOLUME_DIR is None:
VOLUME_DIR = os.path.join(tmp, "volumes")
del tmp
if None in (INSTANCE_DIR, FAKEHOME_DIR, CONFIG_DIR, BACKUP_DIR, DOWNLOADS_DIR, VOLUME_DIR):
print(repr((INSTANCE_DIR, FAKEHOME_DIR, CONFIG_DIR, BACKUP_DIR, DOWNLOADS_DIR, VOLUME_DIR)))
raise ValueError("Can't find HOME or XDG_DATA_HOME")
# Create paths
INSTANCE_DIR = os.path.abspath(INSTANCE_DIR)
FAKEHOME_DIR = os.path.abspath(FAKEHOME_DIR)
CONFIG_DIR = os.path.abspath(CONFIG_DIR)
BACKUP_DIR = os.path.abspath(BACKUP_DIR)
DOWNLOADS_DIR = os.path.abspath(DOWNLOADS_DIR)
VOLUME_DIR = os.path.abspath(VOLUME_DIR)
os.makedirs(INSTANCE_DIR, exist_ok=True)
os.makedirs(FAKEHOME_DIR, exist_ok=True)
os.makedirs(CONFIG_DIR, exist_ok=True)
os.makedirs(BACKUP_DIR, exist_ok=True)
os.makedirs(DOWNLOADS_DIR, exist_ok=True)
os.makedirs(VOLUME_DIR, exist_ok=True)

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkOffscreenWindow" id="install">
<property name="can_focus">False</property>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="app_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-cdrom</property>
<property name="icon_size">6</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="app_name">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;Application&gt;</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="app_steps">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="AboutDialog" parent="GtkAboutDialog">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="type_hint">dialog</property>
<property name="urgency_hint">True</property>
<property name="program_name">Wine Starter</property>
<property name="license" translatable="yes">Dieses Programm kommt OHNE JEDWEDE GARANTIE.
Besuchen Sie &lt;a href="https://www.gnu.org/licenses/agpl-3.0.html"&gt;GNU Affero General Public License, Version 3 oder neuer&lt;/a&gt; für weitere Informationen.</property>
<property name="logo_icon_name">image-missing</property>
<property name="wrap_license">True</property>
<property name="license_type">agpl-3-0</property>
<child type="titlebar">
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="StartWidget" parent="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkImage" id="start_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">32</property>
<property name="stock">gtk-cdrom</property>
<property name="icon_size">6</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="start_name">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;Application&gt;</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="start_settings">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<property name="direction">none</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="start_run">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_top">8</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-play</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">RUN</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="width_request">250</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">32</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="yalign">1</property>
<property name="xscale">0</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Snapshots:</property>
<property name="ellipsize">start</property>
<property name="single_line_mode">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="start_add_snapshot">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-add</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkListBox" id="start_snapshots">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</template>
</interface>

119
winestarter/wine.py 100644
View File

@ -0,0 +1,119 @@
import asyncio
import os
class _Result():
stdout = str
stderr = str
result = int
def __init__(self, result, stdout, stderr, is_wine:bool=False):
# Check values
if not isinstance(result, int):
raise ValueError("result have to be an int.")
if not isinstance(stdout, str):
raise ValueError("stdout have to be an string.")
if not isinstance(stderr, str):
raise ValueError("stderr have to be an string.")
# Set values
self.stdout = stdout
self.stderr = stderr
if is_wine:
lines = stderr.splitlines(keepends=True)
if len(lines) == 1 and lines[0].startswith("wine: cannot find '") and lines[0].endswith("'\n"):
self.result = -1
else:
self.result = result
else:
self.result = result
def __bool__(self):
return self.result == 0
def __repr__(self):
return "<Wine-Result result=" + str(self.result) + " stdout=" + repr(self.stdout) + " stderr=" + repr(self.stderr) + ">"
class _RunCommand():
__args = list
__envs = dict
__cwd: str
def __init__(self, args, envs):
self.__args = args
self.__envs = envs
self.__cwd = None
@property
def cwd(self):
return self.__cwd
@cwd.setter
def cwd(self, cwd: str):
if self.__cwd is not None:
raise RuntimeError("change dir can be done onle ones.")
if not isinstance(cwd, str):
raise ValueError("change dir have to be a string.")
self.__cwd = cwd
async def run(self):
# Copy own env
env = dict(os.environ)
for iID, i in self.__envs.items():
env[iID] = i
# Run proc
proc = await asyncio.create_subprocess_exec(*(self.__args), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env, cwd=self.__cwd)
std_out, std_err = await proc.communicate()
return _Result(proc.returncode, std_out.decode(), std_err.decode(), is_wine=self.__args[0] == b"wine")
class WineConfig():
path = str
home = str
def __init__(self, path, home):
# Check values
if not isinstance(path, str):
raise ValueError("path have to be a string.")
if not isinstance(home, str):
raise ValueError("home have to be a string.")
# Set values
self.path = path
self.home = home
def preapre_command(self, args, envs={}, with_display=True, cwd_in_instance: bool = False, cwd_path: str = None):
envs = dict(envs)
# Set base env
envs["HOME"] = self.home
envs["WINEPREFIX"] = self.path
# Check if display have to remove
if not with_display:
envs["DISPLAY"] = ""
envs["WAYLAND_DISPLAY"] = ""
# Create prepared command
result = _RunCommand(args, envs)
# Change dir into instance
if cwd_in_instance:
result.cwd = os.path.join(self.path, "drive_c")
if cwd_path:
if cwd_in_instance:
raise ValueError(
"cwd_path and cwd_in_instance can't be set at the same time.")
result.cwd = cwd_path
# Return result
return result
async def run_command(self, *args, **kargs):
cmd = self.preapre_command(*args, **kargs)
return await cmd.run()
async def stop_wine(self):
return await self.run_command([b"wineserver", b"-w"])