From 2f3050df474f14c2ffbc097b88080a5c3465d7a4 Mon Sep 17 00:00:00 2001 From: Yannik Schmidt Date: Sun, 25 Feb 2024 01:05:53 +0100 Subject: [PATCH] wip: implement dependencies & install --- .gitignore | 2 + adminrun.py | 13 ++++ client.py | 12 ++-- client_details.py | 32 ++++++--- data_backend.py | 53 ++++++++++---- example_software_root/FreeDink/meta.yaml | 5 +- example_software_root/directx9/meta.yaml | 8 +++ localaction.py | 17 ++++- software.py | 90 +++++++++++++++++------- 9 files changed, 171 insertions(+), 61 deletions(-) create mode 100644 adminrun.py create mode 100644 example_software_root/directx9/meta.yaml diff --git a/.gitignore b/.gitignore index ff0bb0d..6e4cec6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.zip *.swp __pycache__/ +cache/ +install/ \ No newline at end of file diff --git a/adminrun.py b/adminrun.py new file mode 100644 index 0000000..23e2641 --- /dev/null +++ b/adminrun.py @@ -0,0 +1,13 @@ +from pyuac.main_decorator import main_requires_admin +import sys +import subprocess + +@main_requires_admin(return_output=True) +def main(path): + p = subprocess.Popen(path, subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + stdout, stderr = p.communicate() + +if __name__ == '__main__': + path = sys.argv[-1] + rv = main(path) + print(rv) \ No newline at end of file diff --git a/client.py b/client.py index 16aef78..7b2fe5a 100644 --- a/client.py +++ b/client.py @@ -45,7 +45,6 @@ def load_main(): # create tiles from meta files # for software in db.find_all_metadata(): - print(software.title) create_main_window_tile(software) # set update listener & update positions # @@ -68,10 +67,13 @@ def load_details(app, software): def create_main_window_tile(software): '''Create the main window tile''' - img = PIL.Image.open(software.get_thumbnail()) - img = img.resize((200, 300)) - img = PIL.ImageTk.PhotoImage(img) + if software.get_thumbnail(): + img = PIL.Image.open(software.get_thumbnail()) + img = img.resize((200, 300)) + else: + img = PIL.Image.new('RGB', (200, 300)) + img = PIL.ImageTk.PhotoImage(img) button = customtkinter.CTkButton(app, image=img, width=200, height=300, command=lambda: switch_to_game_details(software), @@ -117,7 +119,7 @@ def update_button_positions(event=None): if __name__ == "__main__": # define data backend # - db = data_backend.LocalFS(None, None, "./cache", remote_root_dir="example_software_root") + db = data_backend.LocalFS(None, None, "./install/", remote_root_dir="example_software_root") load_main() diff --git a/client_details.py b/client_details.py index 48f5fe4..2fd2ec1 100644 --- a/client_details.py +++ b/client_details.py @@ -11,8 +11,12 @@ def create_details_page(app, software): elements = [] - img = PIL.Image.open(software.get_thumbnail()) - img = img.resize((200, 300)) + if software.get_thumbnail(): + img = PIL.Image.open(software.get_thumbnail()) + img = img.resize((200, 300)) + else: + img = PIL.Image.new('RGB', (200, 300)) + img = PIL.ImageTk.PhotoImage(img) # thumbnail image # @@ -48,26 +52,32 @@ def create_details_page(app, software): elements.append(description) # dependencies # - dependencies_text = ",".join(software.dependencies) - dependencies = customtkinter.CTkLabel(info_frame, text=dependencies_text) - dependencies.pack(anchor="w", side="top", padx=20) - elements.append(dependencies) + if software.dependencies: + dependencies_text = ",".join(software.dependencies) + dependencies = customtkinter.CTkLabel(info_frame, text=dependencies_text) + dependencies.pack(anchor="w", side="top", padx=20) + elements.append(dependencies) # buttons # install_button = customtkinter.CTkButton(info_frame, text="Install", - command=lambda: software.install(software)) + command=lambda: software.install()) remove_button = customtkinter.CTkButton(info_frame, text="Remove", - command=lambda: software.remove(software)) + command=lambda: software.remove()) + if software.run_exe: + run_button = customtkinter.CTkButton(info_frame, text="Run", + command=lambda: software.run()) + run_button.pack(padx=10, pady=30, anchor="sw", side="left") + elements.append(run_button) - install_button.pack(padx=20, pady=30, anchor="sw", side="left") - remove_button.pack(padx=20, pady=30, anchor="sw", side="left") + install_button.pack(padx=10, pady=30, anchor="sw", side="left") + remove_button.pack(padx=10, pady=30, anchor="sw", side="left") elements.append(install_button) elements.append(remove_button) # add other pictures # i = 0 for path in software.pictures[1:]: - img = PIL.Image.open(software.get_thumbnail()) + img = PIL.Image.open(path) img = img.resize((200, 300)) img = PIL.ImageTk.PhotoImage(img) extra_pic_button = customtkinter.CTkButton(app, text="", image=img, width=200, height=300, diff --git a/data_backend.py b/data_backend.py index fbc5abf..e9c4f3d 100644 --- a/data_backend.py +++ b/data_backend.py @@ -5,20 +5,20 @@ import software class DataBackend: - def __init__(self, user, password, cache_dir, remote_root_dir=None): + def _create_cache_dir(self, cache_dir): + os.makedirs(cache_dir, exist_ok=True) + + def __init__(self, user, password, install_dir, remote_root_dir=None): self.user = user self.password = password - self.cache_dir = cache_dir self.remote_root_dir = remote_root_dir + self.install_dir = install_dir - if not os.path.isdir(self.cache_dir): - os.mkdir(self.cache_dir) - - def get(self, path): + def get(self, path, return_content=False): '''Return the contents of this path''' raise NotImplementedError() - + def list(self, path): '''List the contents of this path''' raise NotImplementedError() @@ -29,19 +29,44 @@ class DataBackend: class LocalFS(DataBackend): - def get(self, path): + def get(self, path, cache_dir=None, return_content=False): - fullpath = os.path.join(self.remote_root_dir, path) + # check the load cache dir # + if cache_dir: + self._create_cache_dir(cache_dir) + elif not cache_dir and not return_content: + AssertionError("Need to set either cache_dir or return_content!") + + # prepend root dir if not given # + fullpath = path + if self.remote_root_dir and not path.startswith(self.remote_root_dir): + fullpath = os.path.join(self.remote_root_dir, path) + + # load the file on remote # with open(fullpath, "rb") as f: - target = os.path.join(self.cache_dir, os.path.basename(path)) + print(cache_dir, path) + target = os.path.join(cache_dir, os.path.basename(path)) with open(target, "wb") as ft: + if return_content: + return f.read() ft.write(f.read()) return target - def list(self, path): - fullpath = os.path.join(self.remote_root_dir, path) - return os.listdir(fullpath) + def list(self, path, fullpaths=False): + + # prepend root dir if not given # + fullpath = path + if self.remote_root_dir and not path.startswith(self.remote_root_dir): + fullpath = os.path.join(self.remote_root_dir, path) + + if not os.path.isdir(fullpath): + return [] + + if fullpaths: + return [ os.path.join(path, filename) for filename in os.listdir(fullpath)] + else: + return os.listdir(fullpath) def find_all_metadata(self): @@ -51,6 +76,6 @@ class LocalFS(DataBackend): if not os.path.isfile(meta_file): continue else: - meta_info_list.append(software.Software(meta_file)) + meta_info_list.append(software.Software(meta_file, self)) return meta_info_list diff --git a/example_software_root/FreeDink/meta.yaml b/example_software_root/FreeDink/meta.yaml index 8f99bd3..1821ffc 100644 --- a/example_software_root/FreeDink/meta.yaml +++ b/example_software_root/FreeDink/meta.yaml @@ -8,8 +8,9 @@ description: > to a town that worships ducks. Dink is never freed from the grievances of being a pig farmer, a fact he is far too often reminded of by his nemesis, Milder Flatstomp. dependencies: - - dummy_dep_1 + - DirectX 9.0c link_only: false extra_files: dummy_file_1.txt: "%APP_DATA%/game_vault/" - dummy_dir_1: "%APP_DATA%/game_vault/" \ No newline at end of file + dummy_dir_1: "%APP_DATA%/game_vault/" +run_exe: "GNUFreeDink/dink.exe" \ No newline at end of file diff --git a/example_software_root/directx9/meta.yaml b/example_software_root/directx9/meta.yaml new file mode 100644 index 0000000..1594041 --- /dev/null +++ b/example_software_root/directx9/meta.yaml @@ -0,0 +1,8 @@ +genre: System Tools +title: DirectX 9.0c +description: > + The last official DirectX9 driver, needed for various games. +dependencies: +link_only: false +extra_files: +installer: DX/DXSETUP.exe \ No newline at end of file diff --git a/localaction.py b/localaction.py index 68ee6ce..1c7da20 100644 --- a/localaction.py +++ b/localaction.py @@ -27,9 +27,22 @@ def install_extra_files(extra_files_list, path): '''Copy/Install extra gamedata to a give location''' pass -def launch_software(path, synchronous=False): +def run_exe(path, synchronous=False): '''Launches a given software''' - pass + + if synchronous: + raise NotImplementedError("SYNC not yet implemented") + + print("Executing:", path) + try: + subprocess.Popen(path) + except OSError as e: + if "WinError 740" in str(e): + p = subprocess.Popen(["python", "adminrun.py", path], + subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + print(p.communicate()) + else: + raise e def remove_software(path): '''Remove a software at the target location''' diff --git a/software.py b/software.py index 166f32e..5eaef81 100644 --- a/software.py +++ b/software.py @@ -1,56 +1,92 @@ import yaml import os import localaction +import zipfile class Software: - def __init__(self, directory, backend): + def __init__(self, meta_file, backend): - if os.path.isfile(directory) and directory.endswith("meta.yaml"): - directory = os.path.dirname(directory) - - self.directory = directory - self._load_from_yaml() + self.meta_file = meta_file + self.directory = os.path.dirname(meta_file) self.backend = backend + self.cache_dir = os.path.join("cache", self.directory) + self._load_from_yaml() def _load_from_yaml(self): - fullpath = os.path.join(self.directory, "meta.yaml") - self.info_file = fullpath - with open(fullpath) as f: - meta = yaml.safe_load(f) + content = self.backend.get(self.meta_file, self.cache_dir, return_content=True) - self.title = meta.get("title") - self.genre = meta.get("genre") - self.description = meta.get("description") - self.dependencies = meta.get("dependencies") - self.link_only = meta.get("link_only") - self.link = meta.get("link") - self.extra_files = meta.get("extra_files") + meta = yaml.safe_load(content) + self.title = meta.get("title") + self.genre = meta.get("genre") + self.description = meta.get("description") + self.dependencies = meta.get("dependencies") + self.link_only = meta.get("link_only") + self.link = meta.get("link") + self.extra_files = meta.get("extra_files") + self.run_exe = meta.get("run_exe") + self.installer = meta.get("installer") + + self.pictures = [ self.backend.get(pp, self.cache_dir) for pp in + self.backend.list(os.path.join(self.directory, "pictures"), fullpaths=True) ] + + self.reg_files = self.backend.list(os.path.join(self.directory, "registry_files"), fullpaths=True) - self.pictures = [os.path.join(self.directory, "pictures", p) for p in - os.listdir(os.path.join(self.directory, "pictures"))] def get_thumbnail(self): + '''Return the thumbnail for this software''' + + if not self.pictures: + return None return self.pictures[0] def _extract_to_target(self, cache_src, target): '''Extract a cached, downloaded zip to the target location''' + software_path = os.path.join(target, self.title) + os.makedirs(software_path, exist_ok=True) + + with zipfile.ZipFile(cache_src, 'r') as zip_ref: + zip_ref.extractall(software_path) + def install(self): '''Install this software from the backend''' - local_file = self.backend.get_exe_or_data(): + print("Installing:", self.title, self.directory) + path = os.path.join(self.directory, "main_dir") + remote_file = self.backend.list(path, fullpaths=True)[0] + local_file = self.backend.get(remote_file, self.cache_dir) + + # execute or unpack # if local_file.endswith(".exe"): localaction.run_exe(local_file) elif local_file.endswith(".zip"): - _extract_to_target(INSTALL_DIR) + self._extract_to_target(local_file, self.backend.install_dir) - # download registry - # install registry - # TODO dependencies # - # download gamefiles - # install gamefiles - \ No newline at end of file + # download & install registry files # + for rf in self.reg_files: + path = self.backend.get(rf, cache_dir=self.cache_dir) + localaction.install_registry_file(path) + + # install dependencies # + if self.dependencies: + avail_software = self.backend.find_all_metadata() + for s in avail_software: + if s.title in self.dependencies: + s.install() + + # run installer if set # + if self.installer: + installer_path = os.path.join(self.backend.install_dir, self.title, self.installer) + print("Running installer:", installer_path) + localaction.run_exe(installer_path) + + # TODO download & install gamefiles # + + def run(self): + '''Run the configured exe for this software''' + if self.run_exe: + localaction.run_exe(os.path.join(self.backend.install_dir, self.title, self.run_exe)) \ No newline at end of file