From a077492c9f1fb5e955e7fe4776c854d0e95f22ef Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Sun, 11 May 2025 17:18:45 -0500 Subject: [PATCH 01/27] Create buildapp-linux.command --- Scripts/buildapp-linux.command | 1 + 1 file changed, 1 insertion(+) create mode 100644 Scripts/buildapp-linux.command diff --git a/Scripts/buildapp-linux.command b/Scripts/buildapp-linux.command new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/Scripts/buildapp-linux.command @@ -0,0 +1 @@ +#!/bin/bash From bd52d59ce06ef2a755ee3d67b643846e09ac018c Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Sun, 11 May 2025 17:32:39 -0500 Subject: [PATCH 02/27] Delete Scripts/buildapp-linux.command --- Scripts/buildapp-linux.command | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Scripts/buildapp-linux.command diff --git a/Scripts/buildapp-linux.command b/Scripts/buildapp-linux.command deleted file mode 100644 index a9bf588..0000000 --- a/Scripts/buildapp-linux.command +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash From 8a271c9a53fcb2980d50edaaac8834a46b251871 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Sun, 11 May 2025 17:33:46 -0500 Subject: [PATCH 03/27] Create buildapp-linux.py --- Scripts/buildapp-linux.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 Scripts/buildapp-linux.py diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Scripts/buildapp-linux.py @@ -0,0 +1 @@ + From 80b62ebf72055e9523fddb74c43adfdf8a04f464 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Mon, 12 May 2025 18:01:25 -0500 Subject: [PATCH 04/27] buildapp-linux --- Scripts/buildapp-linux.py | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 8b13789..3cf29d8 100644 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1 +1,94 @@ +# Usage: python3 buildapp-linux.py [--debug] [--verbose] [--write-output] [--skip-app-creation] + # --debug: adds extra debug options + # --verbose: show stdout/stderr from pyinstaller + # --keep-output: skip deleting BuildAppOutput.py + # --skip-app-creation: skips running pyinstaller + # --skip-script-generate: uses an already-made BuildAppOutput.py (and forces keep-output) +from pathlib import Path +import re +import sys +import platform +import subprocess +import os + +dir = Path(__file__).resolve().parent.parent +file = dir / "ProperTree.py" +args = sys.argv[1:] +command = f'pyinstaller --add-data Scripts:Scripts {dir}/BuildAppOutput.py --name ProperTree --onefile -y' + +if platform.system() != "Linux": + print("Can only be run on Linux") + exit() + +if "--skip-app-creation" not in args: + try: + result = subprocess.run(['which', 'pyinstaller'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if result.stdout: + print(f"Found pyinstaller: {result.stdout.decode().strip()}") + else: + print("pyinstaller not found: \"which pyinstaller\" could not find a valid command.\nPlease install pyinstaller as a globally accessible command.") + exit() + except Exception as e: + print(f"pyinstaller not found: {e}\nPlease install pyinstaller as a globally accessible command.") + exit() + +with open(file, 'r') as f: + code = f.read() + +file_exists_check_pattern = r'os\.path\.exists\("Scripts/(.*?)"\)' +file_exists_check_match = re.search(file_exists_check_pattern, code) + +open_file_pattern = r'open\("Scripts/(.*)"\)' +open_file_match = re.search(open_file_pattern, code) + +if file_exists_check_match: + print("Match: file_exists_check") + value = file_exists_check_match.group(1) + code = re.sub(file_exists_check_pattern, f'os.path.exists(os.path.join(sys._MEIPASS,"Scripts","{value}"))', code) +else: + print("No match: file_exists_check") + +if open_file_match: + print("Match: open_file") + value = open_file_match.group(1) + code = re.sub(open_file_pattern, f'open(os.path.join(sys._MEIPASS,"Scripts","{value}"))', code) +else: + print("No match: open_file") + +code = code.replace('os.path.join(os.path.abspath(os.path.dirname(__file__)),"Scripts","update_check.py")', 'os.path.join(sys._MEIPASS,"Scripts","update_check.py")') +code = code.replace('os.path.dirname(__file__)', 'sys._MEIPASS') + +#code = '\n'.join(code.split('\n')[:21] + ["# added debug line\nraise Exception(f\"tempdir: {sys._MEIPASS} ;; tempdir[ls]: {subprocess.run(['ls', f'{sys._MEIPASS}/Scripts'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)}\")"] + code.split('\n')[21:]) + +if "--debug" in args: + code = '\n'.join([line for line in code.replace('mb.showerror(', 'debug_exception(').split('\n') if "tk.bell()" not in line]) + code = '\n'.join(code.split('\n')[:21] + [f"""\n# Generated Debug Exception Handler\ndef debug_exception(*args): + raise Exception( + f"--- DEBUG EXCEPTION CAUGHT ({{len(args)}} arguments) --\\n\\n" + + "\\n".join(str(arg) for arg in args) + )\n"""] + code.split('\n')[21:]) + +if "--skip-script-generate" not in args: + with open(dir / 'BuildAppOutput.py', 'w') as file: + print("Writing output...") + file.write(code) + +if "--skip-app-creation" not in args: + print("Creating application...") + result = subprocess.run(command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if "--verbose" in args: + if (result.stdout): + print(f"-- Verbose output (stdout) --\n{result.stdout.decode().strip()}") + else: + print(f"-- Verbose output (stderr) --\n{result.stderr.decode().strip()}") + + print(f"Created executable in ${Path(__file__).resolve().parent.parent}/dist") + +if "--keep-output" not in args and "--skip-script-generate" not in args: + print("Deleting temporary file...") + os.remove(dir / 'BuildAppOutput.py') + +print("Done!") \ No newline at end of file From c92c9c7741f115c5f50b0adae2f052d25e8eee2b Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Mon, 12 May 2025 20:23:31 -0500 Subject: [PATCH 05/27] buildapp-linux.py complete rewrite --- Scripts/buildapp-linux.py | 209 +++++++++++++++++++++++--------------- 1 file changed, 129 insertions(+), 80 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 3cf29d8..0ad6bc7 100644 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,94 +1,143 @@ -# Usage: python3 buildapp-linux.py [--debug] [--verbose] [--write-output] [--skip-app-creation] - # --debug: adds extra debug options - # --verbose: show stdout/stderr from pyinstaller - # --keep-output: skip deleting BuildAppOutput.py - # --skip-app-creation: skips running pyinstaller - # --skip-script-generate: uses an already-made BuildAppOutput.py (and forces keep-output) +# Usage: python3 buildapp-linux.py [--verbose] [--python [@]] + # "--verbose": Verbose mode. + # "--python [@]": Select a Python executable to use (default is output of "which python3"). from pathlib import Path -import re import sys import platform import subprocess import os +import shutil +import tarfile dir = Path(__file__).resolve().parent.parent -file = dir / "ProperTree.py" +scripts = dir / "Scripts" +dist = dir / "dist" / "linux" +payload_dir = dist / "payload" +payload_scripts = payload_dir / "Scripts" +settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/$USER/.ProperTree args = sys.argv[1:] -command = f'pyinstaller --add-data Scripts:Scripts {dir}/BuildAppOutput.py --name ProperTree --onefile -y' - -if platform.system() != "Linux": - print("Can only be run on Linux") - exit() - -if "--skip-app-creation" not in args: +verbose = "--verbose" in args + +# For verbose-specific logs. +def log(*args): + if verbose: + print('\n'.join(map(str, args))) + +def is_python(path): + if not os.path.isfile(path) or not os.access(path, os.X_OK): + log(f"is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") + return False + try: - result = subprocess.run(['which', 'pyinstaller'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run([path, '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + log(f"is_python log: result: {result}") - if result.stdout: - print(f"Found pyinstaller: {result.stdout.decode().strip()}") - else: - print("pyinstaller not found: \"which pyinstaller\" could not find a valid command.\nPlease install pyinstaller as a globally accessible command.") - exit() + if result.returncode == 0: + log(f"is_python success: {result.returncode}") + return True except Exception as e: - print(f"pyinstaller not found: {e}\nPlease install pyinstaller as a globally accessible command.") - exit() - -with open(file, 'r') as f: - code = f.read() + log(f"is_python fail: exception:\n{e}") + return False + + log("is_python fail: overlow[1]") + return False -file_exists_check_pattern = r'os\.path\.exists\("Scripts/(.*?)"\)' -file_exists_check_match = re.search(file_exists_check_pattern, code) - -open_file_pattern = r'open\("Scripts/(.*)"\)' -open_file_match = re.search(open_file_pattern, code) - -if file_exists_check_match: - print("Match: file_exists_check") - value = file_exists_check_match.group(1) - code = re.sub(file_exists_check_pattern, f'os.path.exists(os.path.join(sys._MEIPASS,"Scripts","{value}"))', code) -else: - print("No match: file_exists_check") - -if open_file_match: - print("Match: open_file") - value = open_file_match.group(1) - code = re.sub(open_file_pattern, f'open(os.path.join(sys._MEIPASS,"Scripts","{value}"))', code) +if platform.system() != "Linux": + print("Can only be run on Linux") + exit(1) + +if "--python" in args: + if args.index("--python") + 1 < len(args): + python = args[args.index("--python") + 1] + if not is_python(python): + print(f"Invalid python executable: {python}") + exit(1) + else: + print("Invalid Python executable: no executable provided") + exit(1) else: - print("No match: open_file") - -code = code.replace('os.path.join(os.path.abspath(os.path.dirname(__file__)),"Scripts","update_check.py")', 'os.path.join(sys._MEIPASS,"Scripts","update_check.py")') -code = code.replace('os.path.dirname(__file__)', 'sys._MEIPASS') - -#code = '\n'.join(code.split('\n')[:21] + ["# added debug line\nraise Exception(f\"tempdir: {sys._MEIPASS} ;; tempdir[ls]: {subprocess.run(['ls', f'{sys._MEIPASS}/Scripts'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)}\")"] + code.split('\n')[21:]) - -if "--debug" in args: - code = '\n'.join([line for line in code.replace('mb.showerror(', 'debug_exception(').split('\n') if "tk.bell()" not in line]) - code = '\n'.join(code.split('\n')[:21] + [f"""\n# Generated Debug Exception Handler\ndef debug_exception(*args): - raise Exception( - f"--- DEBUG EXCEPTION CAUGHT ({{len(args)}} arguments) --\\n\\n" + - "\\n".join(str(arg) for arg in args) - )\n"""] + code.split('\n')[21:]) - -if "--skip-script-generate" not in args: - with open(dir / 'BuildAppOutput.py', 'w') as file: - print("Writing output...") - file.write(code) - -if "--skip-app-creation" not in args: - print("Creating application...") - result = subprocess.run(command.split(' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - if "--verbose" in args: - if (result.stdout): - print(f"-- Verbose output (stdout) --\n{result.stdout.decode().strip()}") - else: - print(f"-- Verbose output (stderr) --\n{result.stderr.decode().strip()}") - - print(f"Created executable in ${Path(__file__).resolve().parent.parent}/dist") - -if "--keep-output" not in args and "--skip-script-generate" not in args: - print("Deleting temporary file...") - os.remove(dir / 'BuildAppOutput.py') - -print("Done!") \ No newline at end of file + result = subprocess.run(["which", "python3"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if result.stdout: + python = result.stdout.decode().strip() + else: + print("Invalid Python executable: no executable found") + exit(1) + +# Success +print(f"Found Python: {python}") + +# Generate the extraction script. It's meant to be as compact as possible. +script = f"""#!/bin/bash +S=$(awk '/^A/ {{print NR + 1; exit 0; }}' "$0") +mkdir "/tmp/.ProperTree" > /dev/null +tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree" +{python} /tmp/.ProperTree/ProperTree.py "$@" +rm -rf "/tmp/.ProperTree" +exit 0 +A +""" + +# We're gonna put our settings into a persistent user-specific directory, since otherwise it'd go into a temporary directory, which we don't want. +if not os.path.exists(settings): + os.makedirs(settings) + +print("Processing code...") +# Load ProperTree.py's code so we can edit it. +with open(dir / "ProperTree.py", 'r') as file: + code = file.read() + +# These next lines make sure ProperTree uses our new settings directory. +code = code.replace('Scripts/settings.json', f"{settings}/settings.json") +code = code.replace('Scripts/version.json', f"{settings}/version.json") +code = code.replace('join(pt_path,"Configuration.tex")', f'join("{settings}", "Configuration.tex")') + +# Put a small notice on the code. +code = f"# This code has been processed and edited from the original ProperTree.py.\n\n{code}" + +# Here, we're gonna transfer settings.json and version.json to our new settings directory. +if os.path.exists(scripts / "settings.json"): + print("Copying settings.json...") + shutil.copy(scripts / "settings.json", settings / "settings.json") +print("Copying version.json...") +shutil.copy(scripts / "version.json", settings / "version.json") + +# This creates /dist, /dist/linux, /dist/linux/payload, and /dist/linux/payload/Scripts all in one check. +log(f"Creating output directories... (sources: [{payload_scripts}])") +if not os.path.exists(payload_scripts): + os.makedirs(payload_scripts) + +print("Writing main.sh...") +with open(dist / 'main.sh', 'w') as file: + file.write(script) + +with open(payload_dir / 'ProperTree.py', 'w') as file: + file.write(code) + +# Copies from the Scripts folder into the payload's Scripts folders. +def copy_script(target): + log(f"Copying Python script: {target}") + shutil.copy(scripts / target, payload_scripts / target) + +print("Copying scripts...") +copy_script("__init__.py") +copy_script("config_tex_info.py") +copy_script("downloader.py") +copy_script("plist.py") +copy_script("plistwindow.py") +copy_script("update_check.py") +copy_script("utils.py") + +print("Creating payload...") +with tarfile.open(dist / "payload.tar.gz", "w:gz") as tar: + for dirpath, dirnames, filenames in os.walk(payload_dir): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + tar.add(filepath, arcname=os.path.relpath(filepath, payload_dir)) + +# Here's where we add the payload. +result = subprocess.run(f"cat {dist}/payload.tar.gz >> {dist}/main.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +# Done! +print("Done!") +exit(0) \ No newline at end of file From 1ef5dfb8f67404675ad853526ae4de33e6efc4f8 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Mon, 12 May 2025 23:18:59 -0500 Subject: [PATCH 06/27] More progress on buildapp-linux.py --- Scripts/buildapp-linux.py | 87 +++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 0ad6bc7..44d458d 100644 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,6 +1,13 @@ -# Usage: python3 buildapp-linux.py [--verbose] [--python [@]] +# Usage: python3 buildapp-linux.py [--verbose] [--python [@]] [--always-overwrite] [--use-existing-payload] # "--verbose": Verbose mode. # "--python [@]": Select a Python executable to use (default is output of "which python3"). + # "--always-overwrite": Always overwrite applicable files instead of prompting. + # "--use-existing-payload": Don't overwrite /dist/linux/payload/ProperTree.py. + +# Generated Files - The script will build results in /dist/linux. + # payload: This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app. + # shell: This is the directory used for makefile compilation. + # result: This is the directory with the results. It will have a ProperTree.sh (the raw shell file) and (maybe) an ELF executable named ProperTree. from pathlib import Path import sys @@ -15,10 +22,26 @@ dist = dir / "dist" / "linux" payload_dir = dist / "payload" payload_scripts = payload_dir / "Scripts" +result_dir = dist / "result" settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/$USER/.ProperTree args = sys.argv[1:] verbose = "--verbose" in args +# Delete /dist if it exists +if os.path.exists(dist): + if "--use-existing-payload" in args: + if os.path.exists(dist / "archive"): + shutil.rmtree(dist / "archive") + if os.path.exists(dist / "result"): + shutil.rmtree(dist / "result") + for item in os.listdir(dist): + item_path = os.path.join(dist, item) + if os.path.isfile(item_path): + os.remove(item_path) + else: + # Destroy it all + shutil.rmtree(dist) + # For verbose-specific logs. def log(*args): if verbose: @@ -71,8 +94,9 @@ def is_python(path): script = f"""#!/bin/bash S=$(awk '/^A/ {{print NR + 1; exit 0; }}' "$0") mkdir "/tmp/.ProperTree" > /dev/null -tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree" -{python} /tmp/.ProperTree/ProperTree.py "$@" +mkdir "/tmp/.ProperTree/app" > /dev/null +tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree/app" +{python} /tmp/.ProperTree/app/ProperTree.py "$@" rm -rf "/tmp/.ProperTree" exit 0 A @@ -92,28 +116,52 @@ def is_python(path): code = code.replace('Scripts/version.json', f"{settings}/version.json") code = code.replace('join(pt_path,"Configuration.tex")', f'join("{settings}", "Configuration.tex")') -# Put a small notice on the code. -code = f"# This code has been processed and edited from the original ProperTree.py.\n\n{code}" +def copy_settings_json(): + print("Copying settings.json...") + shutil.copy(scripts / "settings.json", settings / "settings.json") # Here, we're gonna transfer settings.json and version.json to our new settings directory. if os.path.exists(scripts / "settings.json"): - print("Copying settings.json...") - shutil.copy(scripts / "settings.json", settings / "settings.json") + # If the file already exists, then ask the user if they want to overwrite it. + if os.path.exists(settings / "settings.json") and "--always-overwrite" not in args: + while True: + response = input(f"Do you want to overwrite {settings / "settings.json"}? (y/n/x): >> ").strip().lower() + if response == 'y': + copy_settings_json() + break + elif response == 'n': + break + elif response == 'x': # Quit + print("Done!") + exit(0) + else: + print("Invalid input. Please enter 'y' or 'n'.") + else: + copy_settings_json() print("Copying version.json...") shutil.copy(scripts / "version.json", settings / "version.json") -# This creates /dist, /dist/linux, /dist/linux/payload, and /dist/linux/payload/Scripts all in one check. -log(f"Creating output directories... (sources: [{payload_scripts}])") +# This creates /dist, /dist/linux, /dist/linux/payload, /dist/linux/payload/Scripts, /dist/linux/result all in one check. +log(f"Creating output directories... (sources: [{payload_scripts}, {result_dir}])") if not os.path.exists(payload_scripts): os.makedirs(payload_scripts) +if not os.path.exists(result_dir): + os.makedirs(result_dir) +if not os.path.exists(dist / "archive"): + os.makedirs(dist / "archive") + +if not os.path.exists(payload_dir / 'ProperTree.py') and "--use-existing-payload" in args: + print("No ProperTree.py given for payload.") + exit(1) + +if "--use-existing-payload" not in args: + with open(payload_dir / 'ProperTree.py', 'w') as file: + file.write(code) print("Writing main.sh...") with open(dist / 'main.sh', 'w') as file: file.write(script) -with open(payload_dir / 'ProperTree.py', 'w') as file: - file.write(code) - # Copies from the Scripts folder into the payload's Scripts folders. def copy_script(target): log(f"Copying Python script: {target}") @@ -138,6 +186,21 @@ def copy_script(target): # Here's where we add the payload. result = subprocess.run(f"cat {dist}/payload.tar.gz >> {dist}/main.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +print("Copying script...") +subprocess.run(["chmod", "+x", f"{dist}/main.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +shutil.copy(dist / "main.sh", result_dir / "ProperTree.sh") + +print("Generating ELF...") +result = subprocess.run(["makeself", "--quiet", "--tar-quietly", f'{dist / "archive"}', f"{dist / "main.run"}", "ProperTree", f"{dist / "main.sh"}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +# If it exists, copy it. If it doesn't, just tell the user that it couldn't make an executable. +if os.path.exists(dist / "main.run"): + log("Copying ELF executable...") + shutil.copy(dist / "main.run", result_dir / "ProperTree") +else: + print("WARNING: An ELF executable was not created. Please note that in order to create an ELF executable, makeself must be installed. (For more info, run with --verbose)") + log(f"makeself stdio:\n{result.stdout}\n{result.stderr}") + # Done! print("Done!") exit(0) \ No newline at end of file From 5d0bebf9af4028aaea53b46409f72065fc5ae99e Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Mon, 12 May 2025 23:40:57 -0500 Subject: [PATCH 07/27] Final improvements on buildapp-linux.py --- Scripts/buildapp-linux.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 44d458d..8539be3 100644 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -112,9 +112,9 @@ def is_python(path): code = file.read() # These next lines make sure ProperTree uses our new settings directory. -code = code.replace('Scripts/settings.json', f"{settings}/settings.json") -code = code.replace('Scripts/version.json', f"{settings}/version.json") -code = code.replace('join(pt_path,"Configuration.tex")', f'join("{settings}", "Configuration.tex")') +code = code.replace('"Scripts/settings.json"', "f'/home/{os.environ.get('USER')}/.ProperTree/settings.json'") +code = code.replace('"Scripts/version.json"', "f'/home/{os.environ.get('USER')}/.ProperTree/version.json'") +code = code.replace('join(pt_path,"Configuration.tex")', 'join(f"/home/{os.environ.get(\'USER\')}/.ProperTree", "Configuration.tex")') def copy_settings_json(): print("Copying settings.json...") @@ -167,14 +167,15 @@ def copy_script(target): log(f"Copying Python script: {target}") shutil.copy(scripts / target, payload_scripts / target) -print("Copying scripts...") -copy_script("__init__.py") -copy_script("config_tex_info.py") -copy_script("downloader.py") -copy_script("plist.py") -copy_script("plistwindow.py") -copy_script("update_check.py") -copy_script("utils.py") +if "--use-existing-payload" not in args: + print("Copying scripts...") + copy_script("__init__.py") + copy_script("config_tex_info.py") + copy_script("downloader.py") + copy_script("plist.py") + copy_script("plistwindow.py") + copy_script("update_check.py") + copy_script("utils.py") print("Creating payload...") with tarfile.open(dist / "payload.tar.gz", "w:gz") as tar: From c394437e465bb45c79b68725628935c7b6b37137 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Tue, 13 May 2025 14:37:13 -0500 Subject: [PATCH 08/27] Now using C code for buildapp-linux --- Scripts/buildapp-linux.py | 125 +++++++++++++++++++++++++------------- Scripts/linux-app.c | 47 ++++++++++++++ 2 files changed, 129 insertions(+), 43 deletions(-) mode change 100644 => 100755 Scripts/buildapp-linux.py create mode 100644 Scripts/linux-app.c diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py old mode 100644 new mode 100755 index 8539be3..2d94d66 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -3,13 +3,18 @@ # "--python [@]": Select a Python executable to use (default is output of "which python3"). # "--always-overwrite": Always overwrite applicable files instead of prompting. # "--use-existing-payload": Don't overwrite /dist/linux/payload/ProperTree.py. + # "--skip-compile": Skip compiling the script to an ELF executable. -# Generated Files - The script will build results in /dist/linux. - # payload: This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app. - # shell: This is the directory used for makefile compilation. +# Generated Directories - The script will build in /dist/linux. + # payload: This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app-$ID. # result: This is the directory with the results. It will have a ProperTree.sh (the raw shell file) and (maybe) an ELF executable named ProperTree. +# Results - The script will build results in /dist/linux/result. + # ProperTree.sh: The shell script containing ProperTree. + # ProperTree: The optional ELF executable that can be run as an application instead of as a script. + from pathlib import Path +import re import sys import platform import subprocess @@ -24,24 +29,34 @@ payload_scripts = payload_dir / "Scripts" result_dir = dist / "result" settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/$USER/.ProperTree + args = sys.argv[1:] verbose = "--verbose" in args -# Delete /dist if it exists +# Delete /dist if it exists. +print(f"Clearing {dist}...") if os.path.exists(dist): if "--use-existing-payload" in args: - if os.path.exists(dist / "archive"): - shutil.rmtree(dist / "archive") - if os.path.exists(dist / "result"): - shutil.rmtree(dist / "result") for item in os.listdir(dist): - item_path = os.path.join(dist, item) - if os.path.isfile(item_path): - os.remove(item_path) + file = os.path.join(dist, item) + if os.path.isdir(file): + if item != "payload": + shutil.rmtree(file) + else: + os.remove(file) else: # Destroy it all shutil.rmtree(dist) +# Create /dist/linux. +if not os.path.exists(dist): + os.makedirs(dist) + +# Only clear /dist/linux. +if "--clear" in args: + print("Done!") + exit(0) + # For verbose-specific logs. def log(*args): if verbose: @@ -90,14 +105,22 @@ def is_python(path): # Success print(f"Found Python: {python}") -# Generate the extraction script. It's meant to be as compact as possible. +# Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. +# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in /home/$USER/.ProperTree. script = f"""#!/bin/bash +# This is an auto-generated script. +ID=$RANDOM S=$(awk '/^A/ {{print NR + 1; exit 0; }}' "$0") -mkdir "/tmp/.ProperTree" > /dev/null -mkdir "/tmp/.ProperTree/app" > /dev/null -tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree/app" -{python} /tmp/.ProperTree/app/ProperTree.py "$@" -rm -rf "/tmp/.ProperTree" +mkdir "/home/$USER/.ProperTree" > /dev/null 2>&1 +mkdir "/tmp/.ProperTree" > /dev/null 2>&1 +mkdir "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 +tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" +cp "/home/$USER/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 +cp "/home/$USER/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 +"{python}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" +cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "/home/$USER/.ProperTree/settings.json" > /dev/null 2>&1 +cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "/home/$USER/.ProperTree/Configuration.tex" > /dev/null 2>&1 +rm -rf "/tmp/.ProperTree" > /dev/null 2>&1 exit 0 A """ @@ -110,17 +133,19 @@ def is_python(path): # Load ProperTree.py's code so we can edit it. with open(dir / "ProperTree.py", 'r') as file: code = file.read() +# Load linux-app.c's code so we can edit it. +with open(scripts / "linux-app.c", 'r') as file: + ccode = file.read() # These next lines make sure ProperTree uses our new settings directory. -code = code.replace('"Scripts/settings.json"', "f'/home/{os.environ.get('USER')}/.ProperTree/settings.json'") -code = code.replace('"Scripts/version.json"', "f'/home/{os.environ.get('USER')}/.ProperTree/version.json'") -code = code.replace('join(pt_path,"Configuration.tex")', 'join(f"/home/{os.environ.get(\'USER\')}/.ProperTree", "Configuration.tex")') +#code = re.sub(r'join\(pt_path,["\']Configuration\.tex["\']\)', 'join(f"/home/{os.environ.get(\'USER\')}/.ProperTree", "Configuration.tex")', code) +#code = re.sub(r'["\']Scripts/settings.json["\']', "f'/home/{os.environ.get('USER')}/.ProperTree/settings.json'", code) def copy_settings_json(): print("Copying settings.json...") shutil.copy(scripts / "settings.json", settings / "settings.json") -# Here, we're gonna transfer settings.json and version.json to our new settings directory. +# Here, we're gonna transfer settings.json to our new settings directory. if os.path.exists(scripts / "settings.json"): # If the file already exists, then ask the user if they want to overwrite it. if os.path.exists(settings / "settings.json") and "--always-overwrite" not in args: @@ -138,8 +163,6 @@ def copy_settings_json(): print("Invalid input. Please enter 'y' or 'n'.") else: copy_settings_json() -print("Copying version.json...") -shutil.copy(scripts / "version.json", settings / "version.json") # This creates /dist, /dist/linux, /dist/linux/payload, /dist/linux/payload/Scripts, /dist/linux/result all in one check. log(f"Creating output directories... (sources: [{payload_scripts}, {result_dir}])") @@ -163,19 +186,22 @@ def copy_settings_json(): file.write(script) # Copies from the Scripts folder into the payload's Scripts folders. -def copy_script(target): - log(f"Copying Python script: {target}") +def copy_asset(target): + log(f"Copying asset: {target}") shutil.copy(scripts / target, payload_scripts / target) if "--use-existing-payload" not in args: - print("Copying scripts...") - copy_script("__init__.py") - copy_script("config_tex_info.py") - copy_script("downloader.py") - copy_script("plist.py") - copy_script("plistwindow.py") - copy_script("update_check.py") - copy_script("utils.py") + print("Copying assets...") + copy_asset("__init__.py") + copy_asset("config_tex_info.py") + copy_asset("downloader.py") + copy_asset("plist.py") + copy_asset("plistwindow.py") + copy_asset("update_check.py") + copy_asset("utils.py") + copy_asset("menu.plist") + copy_asset("snapshot.plist") + copy_asset("version.json") print("Creating payload...") with tarfile.open(dist / "payload.tar.gz", "w:gz") as tar: @@ -191,17 +217,30 @@ def copy_script(target): subprocess.run(["chmod", "+x", f"{dist}/main.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) shutil.copy(dist / "main.sh", result_dir / "ProperTree.sh") -print("Generating ELF...") -result = subprocess.run(["makeself", "--quiet", "--tar-quietly", f'{dist / "archive"}', f"{dist / "main.run"}", "ProperTree", f"{dist / "main.sh"}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +if "--skip-compile" not in args: + print("Embedding script...") + with open(dist / 'main.sh', 'rb') as file: + binary = file.read() + bytes = [f"0x{byte:02X}" for byte in binary] + + with open(dist / 'main.c', 'w') as file: + file.write(ccode.replace('const unsigned char shell_script[] = {};', f'const unsigned char shell_script[] = {{\n {', '.join(bytes)}\n}};')) -# If it exists, copy it. If it doesn't, just tell the user that it couldn't make an executable. -if os.path.exists(dist / "main.run"): - log("Copying ELF executable...") - shutil.copy(dist / "main.run", result_dir / "ProperTree") -else: - print("WARNING: An ELF executable was not created. Please note that in order to create an ELF executable, makeself must be installed. (For more info, run with --verbose)") - log(f"makeself stdio:\n{result.stdout}\n{result.stderr}") + print("Generating executable...") + try: + result = subprocess.run(["gcc", "-o", "dist/linux/main", f"{dist / "main.c"}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stderr = result.stderr + except Exception as e: + stderr = e + + # If it exists, copy it. If it doesn't, just tell the user that it couldn't make an executable. + if os.path.exists(dist / "main"): + log("Copying executable...") + shutil.copy(dist / "main", result_dir / "ProperTree") + else: + print("WARNING: An ELF executable was not created. Please note that in order to create an ELF executable, gcc must be installed. (For more info, run with --verbose)") + log(f"gcc error:\n{stderr}") # Done! -print("Done!") +print(f"Done! Results are in: {result_dir}") exit(0) \ No newline at end of file diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c new file mode 100644 index 0000000..8fd3b8d --- /dev/null +++ b/Scripts/linux-app.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include +#include +#include + +const unsigned char shell_script[] = {}; +const size_t shell_script_len = sizeof(shell_script); + +int main() { + DIR *entry = opendir("/tmp/.ProperTree"); + if (entry == NULL) { + if (mkdir("/tmp/.ProperTree", 0777) != 0) { + perror("Failed to create /tmp/.ProperTree."); + } + } + + char filename[100] = "/tmp/.ProperTree/script-XXXXXX"; + int fd = mkstemp(filename); + + if (fd == -1) { + perror("mkstemp"); + return 1; + } + + if (write(fd, shell_script, shell_script_len) != shell_script_len) { + perror("write"); + close(fd); + unlink(filename); + return 1; + } + + close(fd); + + if (chmod(filename, 0700) == -1) { + perror("chmod"); + unlink(filename); + return 1; + } + + int status = system(filename); + + unlink(filename); + return status; +} From 68d4e1a9ebf7332ae12cebd5b38f5361a65496eb Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Tue, 13 May 2025 16:05:32 -0500 Subject: [PATCH 09/27] Small improvements to buildapp-linux --- Scripts/buildapp-linux.py | 7 +++++-- Scripts/linux-app.c | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 2d94d66..6f3188f 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -14,7 +14,6 @@ # ProperTree: The optional ELF executable that can be run as an application instead of as a script. from pathlib import Path -import re import sys import platform import subprocess @@ -120,7 +119,7 @@ def is_python(path): "{python}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "/home/$USER/.ProperTree/settings.json" > /dev/null 2>&1 cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "/home/$USER/.ProperTree/Configuration.tex" > /dev/null 2>&1 -rm -rf "/tmp/.ProperTree" > /dev/null 2>&1 +rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 exit 0 A """ @@ -212,9 +211,13 @@ def copy_asset(target): # Here's where we add the payload. result = subprocess.run(f"cat {dist}/payload.tar.gz >> {dist}/main.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +if result.returncode != 0: + print(f"Error: {result.stderr}") print("Copying script...") subprocess.run(["chmod", "+x", f"{dist}/main.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +if result.returncode != 0: + print(f"Error: {result.stderr}") shutil.copy(dist / "main.sh", result_dir / "ProperTree.sh") if "--skip-compile" not in args: diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c index 8fd3b8d..4a214c8 100644 --- a/Scripts/linux-app.c +++ b/Scripts/linux-app.c @@ -13,7 +13,7 @@ int main() { DIR *entry = opendir("/tmp/.ProperTree"); if (entry == NULL) { if (mkdir("/tmp/.ProperTree", 0777) != 0) { - perror("Failed to create /tmp/.ProperTree."); + perror("mkdir"); } } @@ -42,6 +42,9 @@ int main() { int status = system(filename); - unlink(filename); + if (remove(filename) != 0) { + perror("remove"); + } + return status; } From cef7e2e33eeb859582fc5ed9cd0124adddb6bf21 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Tue, 13 May 2025 23:01:59 -0500 Subject: [PATCH 10/27] buildapp-linux.py now creates an installer script --- Scripts/buildapp-linux.py | 77 ++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 6f3188f..ba67817 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,3 +1,5 @@ +# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64 Debian-based distros, but any architecture is theoretically supported. + # Usage: python3 buildapp-linux.py [--verbose] [--python [@]] [--always-overwrite] [--use-existing-payload] # "--verbose": Verbose mode. # "--python [@]": Select a Python executable to use (default is output of "which python3"). @@ -11,7 +13,8 @@ # Results - The script will build results in /dist/linux/result. # ProperTree.sh: The shell script containing ProperTree. - # ProperTree: The optional ELF executable that can be run as an application instead of as a script. + # ProperTree: The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. + # install-ProperTree-x.x.sh: Installs ProperTree as an application. from pathlib import Path import sys @@ -20,6 +23,7 @@ import os import shutil import tarfile +import json dir = Path(__file__).resolve().parent.parent scripts = dir / "Scripts" @@ -32,6 +36,16 @@ args = sys.argv[1:] verbose = "--verbose" in args +# For verbose-specific logs. +def log(*args): + if verbose: + print('\n'.join(map(str, args))) + +# Get version. +with open(scripts / 'version.json', 'r') as file: + version = json.load(file)['version'] + log(f'ProperTree version: {version}') + # Delete /dist if it exists. print(f"Clearing {dist}...") if os.path.exists(dist): @@ -56,11 +70,6 @@ print("Done!") exit(0) -# For verbose-specific logs. -def log(*args): - if verbose: - print('\n'.join(map(str, args))) - def is_python(path): if not os.path.isfile(path) or not os.access(path, os.X_OK): log(f"is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") @@ -108,12 +117,13 @@ def is_python(path): # The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in /home/$USER/.ProperTree. script = f"""#!/bin/bash # This is an auto-generated script. +# ProperTree V. {version} ID=$RANDOM -S=$(awk '/^A/ {{print NR + 1; exit 0; }}' "$0") +DATA=$(awk '/^BREAKER/ {{print NR + 1; exit 0; }}' "$0") mkdir "/home/$USER/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 -tail -n+$S "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" +tail -n+$DATA "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" cp "/home/$USER/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 cp "/home/$USER/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 "{python}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" @@ -121,7 +131,43 @@ def is_python(path): cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "/home/$USER/.ProperTree/Configuration.tex" > /dev/null 2>&1 rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 exit 0 -A +BREAKER +""" + +# Generate the .desktop file for the installer. +desktop = f"""[Desktop Entry] +Name=ProperTree +Comment=By CorpNewt +Exec=~/.local/bin/ProperTree +Icon=~/.ProperTree/icon.png +Terminal=false +Type=Application +Categories=Utility;""" + +# Generate the install script. +# BREAKER is now DESTROYER to avoid awk confusion. +install_script = f"""#!/bin/bash +# This is an auto-generated script. +echo "Preparing..." +desktop="{desktop}" +rm "/home/$USER/.local/bin/ProperTree" > /dev/null 2>&1 +rm "/home/$USER/.local/bin/propertree" > /dev/null 2>&1 +rm "/home/$USER/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 +echo "Extracting payload..." +DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") +tail -n+$DATA "$0" > "/home/$USER/.local/bin/ProperTree" +echo "Writing files..." +echo "#!/bin/bash\n# This is an auto-generated script.\n\\"/home/$USER/.local/bin/ProperTree\\"" > "/home/$USER/.local/bin/propertree" +echo "$desktop" > "/home/$USER/.local/share/applications/ProperTree.desktop" +echo "Managing permissions..." +chmod +x "/home/$USER/.local/bin/ProperTree" +chmod +x "/home/$USER/.local/bin/propertree" +echo "Refreshing sources..." +update-desktop-database ~/.local/share/applications +source ~/.bashrc +echo "Done!" +exit 0 +DESTROYER """ # We're gonna put our settings into a persistent user-specific directory, since otherwise it'd go into a temporary directory, which we don't want. @@ -214,11 +260,22 @@ def copy_asset(target): if result.returncode != 0: print(f"Error: {result.stderr}") -print("Copying script...") +# Here's where we create install.sh by adding a payload here too. +with open(dist / "install.sh", 'wb') as file, open(dist / "main.sh", 'rb') as main_file: + file.write(install_script.encode('utf-8') + main_file.read()) + +# These next couple sections is processing and copying main.sh and install.sh. +print("Copying scripts...") + subprocess.run(["chmod", "+x", f"{dist}/main.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: print(f"Error: {result.stderr}") +subprocess.run(["chmod", "+x", f"{dist}/install.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +if result.returncode != 0: + print(f"Error: {result.stderr}") + shutil.copy(dist / "main.sh", result_dir / "ProperTree.sh") +shutil.copy(dist / "install.sh", result_dir / f"ProperTree-Installer-{version}.sh") if "--skip-compile" not in args: print("Embedding script...") From 0f865a064113bf7d82287364bcdc340809eadecf Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Tue, 13 May 2025 23:24:17 -0500 Subject: [PATCH 11/27] Small updates to buildapp-linux --- Scripts/buildapp-linux.py | 78 +++++++++++++++++++++++++-------------- Scripts/linux-app.c | 35 +++++++++++++++--- 2 files changed, 79 insertions(+), 34 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index ba67817..9a0c366 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -14,7 +14,11 @@ # Results - The script will build results in /dist/linux/result. # ProperTree.sh: The shell script containing ProperTree. # ProperTree: The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. - # install-ProperTree-x.x.sh: Installs ProperTree as an application. + # ProperTree-Installer-x.x.sh: Installs ProperTree as an application. + +# The Scripts + # ProperTree.sh: Runs ProperTree. Please note that it can be run with "--clear-data" to clear ProperTree data. + # ProperTree-Installer-x.x.sh: Installs ProperTree by adding "ProperTree" and "propertree" to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with "--uninstall" to delete these three files. from pathlib import Path import sys @@ -31,7 +35,7 @@ payload_dir = dist / "payload" payload_scripts = payload_dir / "Scripts" result_dir = dist / "result" -settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/$USER/.ProperTree +settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # $HOME/.ProperTree args = sys.argv[1:] verbose = "--verbose" in args @@ -114,58 +118,76 @@ def is_python(path): print(f"Found Python: {python}") # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. -# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in /home/$USER/.ProperTree. +# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in $HOME/.ProperTree. script = f"""#!/bin/bash # This is an auto-generated script. # ProperTree V. {version} +# Run with --clear-data to remove data. + +for arg in "$@"; do + if [ "$arg" == "--clear-data" ]; then + echo "Removing data..." + rm -rf "$HOME/.ProperTree" > /dev/null 2>&1 + echo "Done! ProperTree data has been cleared." + exit 0 + fi +done + ID=$RANDOM DATA=$(awk '/^BREAKER/ {{print NR + 1; exit 0; }}' "$0") -mkdir "/home/$USER/.ProperTree" > /dev/null 2>&1 +mkdir "$HOME/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 tail -n+$DATA "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" -cp "/home/$USER/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 -cp "/home/$USER/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 +cp "$HOME/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 +cp "$HOME/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 "{python}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" -cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "/home/$USER/.ProperTree/settings.json" > /dev/null 2>&1 -cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "/home/$USER/.ProperTree/Configuration.tex" > /dev/null 2>&1 +cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 +cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 exit 0 BREAKER """ -# Generate the .desktop file for the installer. -desktop = f"""[Desktop Entry] +# Generate the install script. Also includes an uninstall option. +# BREAKER is now DESTROYER to avoid awk confusion. +install_script = f"""#!/bin/bash +# This is an auto-generated script. +echo "Preparing..." + +rm "$HOME/.local/bin/ProperTree" > /dev/null 2>&1 +rm "$HOME/.local/bin/propertree" > /dev/null 2>&1 +rm "$HOME/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 + +for arg in "$@"; do + if [ "$arg" == "--uninstall" ]; then + echo "Done! ProperTree uninstalled. Your data was not affected." + exit 0 + fi +done + +desktop="[Desktop Entry] Name=ProperTree Comment=By CorpNewt -Exec=~/.local/bin/ProperTree -Icon=~/.ProperTree/icon.png +Exec=$HOME/.local/bin/ProperTree +Icon=$HOME/.ProperTree/icon.png Terminal=false Type=Application -Categories=Utility;""" +Categories=Utility;" -# Generate the install script. -# BREAKER is now DESTROYER to avoid awk confusion. -install_script = f"""#!/bin/bash -# This is an auto-generated script. -echo "Preparing..." -desktop="{desktop}" -rm "/home/$USER/.local/bin/ProperTree" > /dev/null 2>&1 -rm "/home/$USER/.local/bin/propertree" > /dev/null 2>&1 -rm "/home/$USER/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 echo "Extracting payload..." DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") -tail -n+$DATA "$0" > "/home/$USER/.local/bin/ProperTree" +tail -n+$DATA "$0" > "$HOME/.local/bin/ProperTree" echo "Writing files..." -echo "#!/bin/bash\n# This is an auto-generated script.\n\\"/home/$USER/.local/bin/ProperTree\\"" > "/home/$USER/.local/bin/propertree" -echo "$desktop" > "/home/$USER/.local/share/applications/ProperTree.desktop" +echo "#!/bin/bash\n# This is an auto-generated script.\n\\"$HOME/.local/bin/ProperTree\\"" > "$HOME/.local/bin/propertree" +echo "$desktop" > "$HOME/.local/share/applications/ProperTree.desktop" echo "Managing permissions..." -chmod +x "/home/$USER/.local/bin/ProperTree" -chmod +x "/home/$USER/.local/bin/propertree" +chmod +x "$HOME/.local/bin/ProperTree" +chmod +x "$HOME/.local/bin/propertree" echo "Refreshing sources..." update-desktop-database ~/.local/share/applications source ~/.bashrc -echo "Done!" +echo "Done! Run this script with --uninstall to uninstall the ProperTree application." exit 0 DESTROYER """ diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c index 4a214c8..216464d 100644 --- a/Scripts/linux-app.c +++ b/Scripts/linux-app.c @@ -9,7 +9,7 @@ const unsigned char shell_script[] = {}; const size_t shell_script_len = sizeof(shell_script); -int main() { +int main(int argc, char *argv[]) { DIR *entry = opendir("/tmp/.ProperTree"); if (entry == NULL) { if (mkdir("/tmp/.ProperTree", 0777) != 0) { @@ -39,12 +39,35 @@ int main() { unlink(filename); return 1; } + char **exec_args = malloc(sizeof(char *) * (argc + 1)); + if (!exec_args) { + perror("malloc"); + unlink(filename); + return 1; + } - int status = system(filename); - - if (remove(filename) != 0) { - perror("remove"); + exec_args[0] = filename; + for (int i = 1; i < argc; i++) { + exec_args[i] = argv[i]; } - return status; + exec_args[argc] = NULL; + pid_t pid = fork(); + + if (pid == -1) { + perror("fork"); + free(exec_args); + unlink(filename); + return 1; + } else if (pid == 0) { + execv(filename, exec_args); + perror("execv"); + _exit(127); + } else { + int status; + waitpid(pid, &status, 0); + remove(filename); + free(exec_args); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; + } } From 1513aff63de54c1d4f8a5566aab7ee7c9a599d0d Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Wed, 14 May 2025 09:23:06 -0500 Subject: [PATCH 12/27] Icon in buildapp-linux --- Scripts/buildapp-linux.py | 18 ++++++++++++------ Scripts/icon.png | Bin 0 -> 21561 bytes 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 Scripts/icon.png diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 9a0c366..f5bbb14 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -35,7 +35,7 @@ payload_dir = dist / "payload" payload_scripts = payload_dir / "Scripts" result_dir = dist / "result" -settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # $HOME/.ProperTree +settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/user/.ProperTree args = sys.argv[1:] verbose = "--verbose" in args @@ -117,8 +117,13 @@ def is_python(path): # Success print(f"Found Python: {python}") +# Get the icon binary. +with open(scripts / "icon.png", 'rb') as f: + content = f.read() + icon = ''.join(f'\\x{byte:02X}' for byte in content) + # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. -# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in $HOME/.ProperTree. +# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in /home/user/.ProperTree. script = f"""#!/bin/bash # This is an auto-generated script. # ProperTree V. {version} @@ -150,6 +155,7 @@ def is_python(path): """ # Generate the install script. Also includes an uninstall option. +# We embed the icon binary so we can copy it over without relying on external files. # BREAKER is now DESTROYER to avoid awk confusion. install_script = f"""#!/bin/bash # This is an auto-generated script. @@ -175,6 +181,9 @@ def is_python(path): Type=Application Categories=Utility;" +mkdir "$HOME/.ProperTree" > /dev/null 2>&1 +printf '{icon}' > "$HOME/.ProperTree/icon.png" + echo "Extracting payload..." DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") tail -n+$DATA "$0" > "$HOME/.local/bin/ProperTree" @@ -187,6 +196,7 @@ def is_python(path): echo "Refreshing sources..." update-desktop-database ~/.local/share/applications source ~/.bashrc + echo "Done! Run this script with --uninstall to uninstall the ProperTree application." exit 0 DESTROYER @@ -204,10 +214,6 @@ def is_python(path): with open(scripts / "linux-app.c", 'r') as file: ccode = file.read() -# These next lines make sure ProperTree uses our new settings directory. -#code = re.sub(r'join\(pt_path,["\']Configuration\.tex["\']\)', 'join(f"/home/{os.environ.get(\'USER\')}/.ProperTree", "Configuration.tex")', code) -#code = re.sub(r'["\']Scripts/settings.json["\']', "f'/home/{os.environ.get('USER')}/.ProperTree/settings.json'", code) - def copy_settings_json(): print("Copying settings.json...") shutil.copy(scripts / "settings.json", settings / "settings.json") diff --git a/Scripts/icon.png b/Scripts/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..987bb34e349fb90f419bd6951b5f246723e53381 GIT binary patch literal 21561 zcmc$G`9D=X918UOG9Ma-$&aX?p zkLd@l3|>YsNcZRR-N)q8zdc*a=q-m5!%Kg`;kL9$Wacm8veM66enf8y#U_{7*NxqX ztjvfmDOilX`sL#fpLZqe2Yn^WcY+4aG)({hdhCz>n+hO&wl1@+-g_!(e7uqC`uKaq z_ZoMzK^3jeT;Y&!*UW6IupMZVLhb00`b7u-Gip+uA5Rw^9*&mUC~E&oRsDiqDf>`y zad$d+{oH!%R(og%ocECYGDV^JZJINR*E#BTwrJYUue~iM#a2pzODe%8b;st5_QBt& zu?nZioj-32+v{_gwyvK0nXssf^dpg`1^hjCu3tU>JFy2-Xve%?-XlT%Q|f%QoUYOO zlSMorX6u>ZkNvTR$ByB@v&3zjLaeeGBvQ}2l1|+{T5(2ime{l{cf_BHaU4wFEexIX zDmCYzKNU!PHCaGt8uiTGDzz?;_?oZgqi}|9r=>FdvyZ`#?Kc`1VSjC$qeHUgU0tsH zd=wDnnuDxjWRlzH%#yQNvoSz&i@oxd7@b%*)r1>d0 zzgFOv^NUg2+?98Go+dK!`hWeWMf&WO=zL-s)zsN~!tmyze1T5kYnhu0ZNJY1@Q2T= z+|Y=>WG9`M!8jf?P``MT5MU@AKL2ZDoRK^m-NS9;M)vqqs??C{DxJ4QLZ5u0DZH}( zc6_$|&m2=llcUPLvoTYUH}YLFp)ohv4@oD7q{}}AqBb+HUk$%KbvIYZ@4sWT`*7>C z!^8QL^^@a($7ByL)7;3x4x9GPtl0Ckv%B%)7c|=cbYawH+~SL{A5QvSuXjGUc{${! z5=u818uUhEvFQ_neR;g}A~(yP8zqY6;5Zczfu4?a0+g?j32rUmo}TjdK)^7F9`F zTjH>N*s=VM)LRM3VZS>2?B$oh&7#H%ry949u2a2V>Av6CAI{PCTb-;cydF`xsv|@a z!xC2g0t7ja4G2Z)OM0K2s$U)-5KdZ3ix4l3r1R$j4n;V5J0-t}g$=aTC%s(V6()fU zLr)UH1I!Iq^Xh98!T-K{#Oidk*V6}UF%UmsV8u8=RAJ;=lla?$h~@r`J`r?{OHXpT z?m;S-!ngjP4@9(8e8$Ss_1G@naogYiBmUr2P?8Omz^{7i4tgMLfmAr#*89~^3XRx#n%P|9&1K?|Ujlgmktj-s6H+8};^quN(V^aVe!}&M(D~n)*Od%E+C6c+ z`!i8I$1#ZEceyJMaIz5K*ns+xK4OM$I}mDjFqu|hA1t`o66v?r(bl-wnaE_wXk2c5 zf!FEIwFZl;#n|$M`h)7v8yS-3+kaY^R6bwQt2UtL=G#-;OhT5Cy=})5nx{4Pb&Yw2a zKKf(!Bnr-!xv5^@Tgn@dp? zwrh(B8UK{48Mel!Jox;4u-nF5kCP9?2Uaup!k2qSpDRf)NWS;arb~XnH}x;QesWTy z7o8QStle{Px$Kac6YU)@O|;QFEMyOW+v13M2n@)}m+E z$ryEbyzwM9YQ9V4I53AVNd(J)RmQ-yRM9yKPLp-kRcCFVt6lpDaM(8Z3uTXfs=uwS zFm>hhi(38BXg{_wybha^Qiqw=Mt22C;kxjNVDPE(|3qmh{R-DDeSaqK^`Gd;8X9MgR&n%)R%bzd+?a<~v(P^v}R zUXi|ZqPBMzp9gLi3>j+r$pTUm4$t=o{@(;p0!}nE~j~ zY+x6QtG)1smmTk4ZEWn`fU|H$Wz=uE8+*$(gngxr=r1=047n6!s0f|dt_!#cgu&bo zY5%}M<-R+!B5>3$esyE{Z5qFH|N4GB^Olr&;+4jH46oG9Dd>n9Iip1H7yGq7wAAO` zb2cNxJ3_v(DckCU9rCVOu>_`PyT3&5>T38%&Yg_kcv z6umTtLy9#vR?5xRN4CK>Hj2W8Q(El7)ND*{)S~8Wcv94E%dF5I&4e_h4%cGnHk<|| z>5Ul`L~WTw4d|Ak!;frfGS@uqM-2sZ8Ng)8_Dw;*OQ+~ueT8O|8_3px|{`qLf!niA((Y& z3tX052QEp{0FPN%2djRfgRa*GElPZYqahkGb8l!-5FOsXi`;G24nnZUOF|3Xku{7_ zJdm|db{;QA%X2XAvNIiq{zWZa8tVfUd<1i$UjHTjVp<*`la!pNgq#hFcVv9i4dP+` ztsA`$s5K!t5oj9|6*&~U{YvDbe$3%7k+9!uE$eHyBv8QSTAxsuW5Xtr7oh0Txk;fI zqv>x~@`~N(6HVwRdT=MmgU^ItX9FBvb7H|nT?sg1sz4+JCjUJ8-*>`)LHGJpug9!| z@>hkICR|&wdYn@HzASeEZ5Xk|{7K~%@90B5ij$u+U4#hPDb09j&Kf|Y-)q&vfZLF$ z%YX_#ffpoi?LX$;)mD(PquF5(P+RufJ`}yTvZrN(Mg`utSHCuN@Kf%Ii zzw2+WMo-fK*qR%Da*c9u%k=_0b3%bt;Y8Rgk+$1xqc<7e*DJZdJIx5wGmgJ@fK z(Yb^%_uS66>*%YvR=?-}%7tin_PVW6^2d?T2X7gr1keoG*P2s$J2G#*FME+RHf<9~ zdb{C7cJK@M9LW^HOVO@^+RLF2zLSK-qSOrl7+e%+KcGeU!<(~VmFrLAjS<=^RoQX> zlUYy%x$)Gis4@8>BhuRBQ>%aEaq*}9Omg(r!sln-)UO_lC4UT+k-9VW;)QH(MlQ8@ zdq_#2R9+cS*FG41Y3Dj_-8-`IO_W++`O6MeE3o>>U1K|__$sCcI6-O`$*A7 zH?lokS&RZ_a`o@18^58MtulXP={ELH%ZQt6pt?_3^Z7tx*!1nbzUMUH>)iJ(Znd{X z>GqIp?xFmIXrCE(;mXRvU^IN!QDhdJ9sTEy4}+{j!!O6sf_f-nB?V#&5}z+bL9sBgesf+9`J|x*Ol%M9;6um zn{(47@5Xw~W)mf$?}_xmPq&4`qXiTHc^#z{I&)KH@N98f5`_KH%H~-`aUdS&m_iaS zTFFY+<>#lC#dC=z*+E0Uvaj*ao0bHAd47H-gia7-z@H*rSSfQ}ZCP(O^AW6k)XSQ#HAVNmb+_2tR9`VGN=9=AyG zLC4iPemg+tsiUw7Z z*me5VDC}`QER70r<*08Hn_mRB>K9*_*$SEu5(F6s51pp)eq|RI&y&JG@q0YKt%1@T zczpV%7&gz``3vkOn|88E)N#8>jB^4Sei(vthEo6yi6}xUFcFrva<~>oNV!qUDB4{H zj$aIXh#D0LgeE93c>1U$y->c@5)6MeRS;W&OQ#hkvEeCa>L*(Lrq##ZnB;9v)6(J( z!?z7H9oD%3uJC9pj}LbIZaMfUTJU$6>2VBzhV63^p*IRC(PNW2?7%pch9yN>ukzDE z7Csf>!lRRf&{c?2RKT+sKU`NDvxU`Mo+@}?O}tzrAA3Xk^T~+1zn~ATw8=M;2-RZ8 z!O>aK-~PoeUoYH@pMA8M;}tFVeHXqP0g2(CfK-}M{rMrtEwVr?wM!&^Tt+-GErk4m zQCHu5Tp+0jt`BmtR5#!*V_WS&EwiGewP#x4n_H8;ey)DUhkJ(RFTaN_+EaYG9$Hc! z93IL3clK5~>riR(4#TV@(b52@_Ms+;W-;eW#j9{s-G*rB0Sx429q@08Qo&`M$4#q0 zq7P1~XgNGkw69&_vcR=EPU)U@-Dm&p0akVBGg6&m(YH1aZ~+~K;GyME3(&UvBeJbC zYyg3)W2yE*Ss^+M>6CZzygn!{-lTM30G|hKZGmH_bY%no{rw(1Mce3VlKA6u4^G{g z5{m4J0Egz-gSswUkb@s8XP_^wjKka6D7cOGgSD08`46;MTG&iDq2b`a$BzoFss4-* zic`h!7y_&4_A|t%IZ&Cy+yocqp-|PxB!^$!dx$+ldZ8H>KGo<~;#Yg;&#*BCJ+)>_ zmm7DB_}v6NW6pahnn5UOS_fy}t~#YLr0LgxzhS?@4LZltuy9`z02xG#7rj)rrl-WQ zMPX9`2?h4<%SF6mNlkF~F`S}8ni2R{UEdn8Nt*!dis~0-T4L@kZ`}*6;e{PJZ zLzJq@I9{O=Ami^f-N|A1qTfr!ju0C#(A?*zG0adAYgBKsyNo<(H-~We+8CA%*iO(6 zCHt(dMGkY+nvN`c;~2)s5tRj-IGz3?$!_&y(HXxC;NaBYGmZ86FKOREUkj7w@u&BC zcm4H-4q?{P3~)eXf9Qb#B$hjaz)Ep4htwXd69@-oI1$uA{XhGbP(O{=ebFcTNZhMWwou(xnBTMt zgt@1LIEKJMGRItRvt{FiC{n~imrV^w6vM5HQnG!X)n*WEFC#MT;cfH3AUh?)mVS-5 z>2`nGIvLz}Q35m1Lgu65bB%}O7^t7_Ggz;naDudtKlgEWI;PS)a?B&}=2yHP$6J3ZS zsBtb=7c8o|Y0+82j$bzy$O9sL%8>xHk7pQml)I!pw?6hy10GZOv9M%k0H^qQP)$1Y z=F?=@&@y$$`U5n=4u&9Xh2a#gNCJgtO<~Ia1zPGH=q^Dn<*SvzszS?HL~kX`@!iqF zN;3c>j3RfCK|TAHj0d~7^O@h23vLJd99Yedz^J93M=InN@?49SU`A#g6HJX=s;mrB z+X_ENHl#LO#ubW!sG+1{q)saHGKWryG4caUH%>={p)n6q264GGC=!~`nF!|I%O`w; zxPeR8lAiwB{lK6vy~GO=K5y-U$Cl-VV3gOb<0l^2xf<{EJ1BtyJnFrC8KefPz@E@8 z39N~Nlq(r-X)Y{hO+o+ff%4J_%0qh39jp{C!VeF_H5qW75@F>f?i~0HnU(HOZ{Bb& zQKXA0Bm{IgUs`zpxasuP-BtK;FYK44Q_x`}1Q|y!Q;eRvPYfe*;O(>to7J_ zO+8xcEU-;Clch%67d|9;$b6SnqNiN`nl5=01SJ4IXnX1F5Yqyo6$3*b&J*%}5R4;C6dxe(*Bgita&?nephaZ7aH6DR}qQ^l>n)Hew3`C*>B-$ruYagD0wq)M)}d; zPPn?%xiQvtlJ?# z4x^E>KPB4?^$S*pb#ZKQ8IiKuM13i3;v`Vi@~h9VPs4^^kzCz(XUaZC3S1X`BnWNSC&L4^*e0Lsz7bhE?MeyctMpw0>l1i60CNOxF5C;J2#Y8TG%Mf^ za%FZIh`Wx!9c(?;%ApyL)5R8fzH)tLcAY_i%aU=np+w2Jk_w!n9C5nycgOJj3sGFm zpjXlbPE5QK@ORNL@czR^ro*0>$L=w){8Yb^O6EgEtVhkyUC!JO63CL@4-nLcg{C0_ zB)yclZ*S|GG}f;98saTMJev9#fk{c9r>j~A-99b4zVD7n!^o?W#p5h(FxCK%)Oui;boW*h48KGFw+z}e*{Cne-t8DKD?#psM)}aWRNw@Wwbp>ltxVSS zA8c|cPRz@HrzQ2Wc+Lk{9X7=gY`+l(w-uT8_cjkjDN`ypk{$+A&)uFF+iYoyi2KA5 zm%V54COO+ePy#Y!BL)bXf6Ip=aN40WT=}y6Sq6U-Vbd4lYPRu;!sK9ht+$dJJdI~Q z+D^W^fXUPWqIU(5|Iz{TGg^}NiaqOH@~4i#ee%wajKAn1lF8&Hog+iSV3y3>wb$4L6&T@BRuXhtXzG^mwO$q zG%yY3Abcm&JQ&#_yK5Q{&pl-x{v(rL5isp5Y@1i3tP8vHZ%*e|!J;I{DfHbnU^@;w z$VhhLV#0{JgOrALB#^l{9=2uw!;7^cw^8+)_cz` z_OT?=VNNiaEloQf26?fL}) zLQi#!%PhlK$>F@OI-0w~R3KD`S1@2?Sce8wmdu~rsEr-m4y%i;BsYvk1u<9-kmhwi zIEKxHIGOi(?_D3cF9xm+NIddmlm(NTINW&vk{%DvgFIV<{i~oargU7m4pi9gU}x@P z2Dzn~5N~WD@_oJT*PSretuazKT-7yk`_wD2tvU4g=y5gCx#Umfvi7WZy9#A0UikNN zxPp|Rn?V$A*dE7eKPPA`2q@S$JK~=IXCv_zZ>Xtm6{~Z5soBUdTQ-e7+6w0y&DfLiu=BJj^Ag!0rUO@Za!rGdFEu=`YU9ZtSXX%*v6Y^LbW~2(a z3%fsh|4}AS%0sK2@yvgUsz-UbTnTBtuJO=X+ITCl%(sD-PA+L`Wt9Iu?RObf?md}S zc=&<}koX)}Yz=_@76i@%37J^2TU7+B@Qa#SnzIiw8-?*#%Q?w!9h?E0N5<9QuzB3% zQl_-cYZvmd^8PekKd3i21DM##1FKseKF85aF%hH36@N$xZj`T`CN5IVPM_%+zJIgk zrnKm2(#%Rz_d;s=4OM&mwxPTaCs&UE6sx}VM>BS_dcHLQG1PG zbRQjXB*Dn+p@TP7=@RhSEXR8mvHqW9?S7!GH#}Y)o3Kh1N&-GXoV;Y=>be6fVnE^w zMP3WN-rw>n8nBZ0Tbg1TG6XzBjREg%_6qv(r{CA11fMlM12;uLcHzP;O;@^Ws>0if zxH^50#NOPh9)8ltw$*4qx`g%z%zLfRxZiohCzdfS21FJ*@Ap;xeB+tBmr^`Hs>4bi zfVCXdmOTM-XDD3?do1u1-_L~d{nD35mjDw+DSUt^bA0x%7}I;RkJu=_Ce}3P>{rt8 zdq<^Z^?5N$fH813%KP|!r0Y?%2!(x|v^Wd%X`(~je7=O<&-Xdl*CQ?pZLIzw4Yid& z>oP0Xy)%R3k|U168+~9=GCEBVMmmmnzt2mXzE!E>rGX%_C?NG-aW_EYkODvXn~a_f znzfvDmN{Lq!hv%_=w<8n1@A%+)z2A5;XD>%wjoEl_Kd0*v(>q9zGJx9Ehjapa{{9U zXg<~5J?33s(IOaZq9h6$+hBs!oe#Epr%qC)x#J#j%e(_ zv{+7*GbbPduiw5_-5YTmdjBy%o7xdrt-Dhzo8Ka4YmvvbHn!>IURTUpDuH;rssIZp55LGbpI+$zW=9+PToq3)*Ag8pzK? zgkYD#$^qs!UKXS-#qs)Sk(?Qa?ykj$y(4U*V3HlA8jD|{ljigE0Z|YhO?E_aWi_z; zB_w_@4;V|~OJ72F2n!yu<(;fOoi(Q@(!9*!Z-y>l@?2%La#>4?Pax4FrF@C$(t@O; zv6o&JH>WQ&)${JGf_^rDl&3F1yDdK$jsOY?oW=L_0T@uU{^$DZbzS?y7O7d?nGV2X=kF?(v>8 z&moi55e+}IUm?LoUcU9tr zJoo|(pSia#j#H5%(m`if^ejF=`Za%SJYGA_+!ncwGXYi@fHNJ(+g1r*i1NQZr6z7@ z9yj-(tCwO256#XTR>O#Ot}sAa9zq2$mZvftnY@5@qp*+SThqg7ZAqV!!vmz0cpf;h z{&jfHSu*z_rhPcVO_+~~h(S-)78qaN-1_5j`CeqBR~whjy_2qdQ67C9JD?(!`rcd3 zg(=a3hoNU0aQ3P?!zgy?b2HGJv*!@=#ya~ZnrbPMlhc04Ru-2KDp)C&qGPwMw9VuzZW0K zBEsnq(D;PGfjMSv8>rkZ_+tqz!0cMCUFKe2FR~2PURWSF51r%o=E`R4dUi@ zdlfr%8zTYDf8MOYJUU0lw_ktJL_iAZY%fFK7VrFR`|WLh5I2f3&p1!Kt6GuDzZ+)5 z^rnKVEQ5to&Jy?wDEr~|MTIxFi`F1O0a8$3BYU~glQ5fOHTI)jc8sUSsk8dNJ_=EK zVIIN`X?hZES14~eF#Y503;901pThq5JR3`ah@h%%-x}GIngzD$-DhoCQ#A#69}VVy zZ{u?rNTkX6jD^At?-`?OybbK9yj1JuDwX5J=m0SMJH}Gk%wrX=hi*B0Ct=~kvCI-s zd1o};vC0-&9~dTY{<`guTWeyZK_JaW!LY_b8{#W;04=Z$`nT9W5lUDJ+jk_ts*H+h zhZBY)kS;T|!f#Y7=!&0Y!*Qw4a3c0+a+m44Lz$N)S&3Ug>c6L(f?L)*BQ0NSt<3y* zXbLF(++R&^F_Aryt^YT>b8<82n2nGg5zgJL1fl6(t-4Wx^y;2^Ii1Z}XNRz^V-pUC zr7j7{eW9uR6u?FhMw|_o+7|*!qjDZr*4|*I*4`M7hap;)7@tEwWzab#1tfTgR({cc<18w_ zFEPL9KvAU&N}1lFbC-kfp-ZTNYGsA%rKr?Zex-Nkh)x9&Kv)J+18FnB>c3a*uhnIuZuBH`4LS5MOpEpa(Mq!czGc{2cabLi+4fmo@0GT*t>6I4GIJnkZe~gg7IbnSy0lrMVg_=ro*NS^K;WWS(d=#Hl{oy1^ zl32t{ONxTO9zeQtL+ck;?R;>S5?t@p|=C|?yMhqvvDf@*u3_k>;0bgdHmQv3rQ4E;E^(^4dGF!A60pLMgk7;jTui%)E z01PS~UOfjAquSq}!+h$*+e)D(?79UzHqC>bi^MNYh@-rQP`dh{anC8I7 zcwm&?GKjgb02R%PtLY_kH->dX`<$f&GLj;FR{UR3OMT;f;u=$K`?AEzBjbZizTHT| zl*>Yje<&INhx*ST`R4E<0SP0mOAx(hbwBs|VNKSU>o9XkPO==sztet*0cqhZ+l*gd$~J>c*b`ENS5v&dV1$Fn*6M`EA+Z@SgV;k0PiL+D))?joilIk$I&)vy)rtl1y z2lsSq#Q?QKkKRUKnyAp@!i|R8I2||elQ+br2lMv;!wmR@c5(t8S{Cjy@lD;Zmxv@F zO%iBf3iRbg%}SvT=TG>~(J_jl)GkG#cxqF6=&4sGhv-&)KN+1qZgvP$6^*FIB>4rd zA~DY-aC{8i2tX81-|u+=o3LwaCtNmsvw(N=6&tr9pCSyK5;yJMVOuG!-23?aiwh^? zm2r6!Frq|0I`+8hA0&_jFrR@wUIit@aBWQr`WSqgV~Dh;4UY)w$Z*m;W5%Fy^&ug2 z#lM8%;nVRtjI?rHBwOlfkR0BmAn>TYZ>|$QcU1D1PQQ?%Vev5att8G?w(1H<;f5w6 zFg+MV$}@A9R4XZ*01U&P*uw6)tPRM}<2E(4Nk9-=67FE542dg!aeeqcwH5d5idQ+L zhd#<7>_Z%Eyu>w*>>PB4$-;Rh3(%Dy4}cRMiR3_N@#@Oo{efJv!Jok%E|3BT=}m#g zTuZ2mq6*|23IO7)SLq?1{7UG(*iqJZ$te?hc0*t2-pfnxY51_;RaXnUj(kB~@t<+J z6=yrg{s-HX6u5s0uo}TdT(#1Wl}l?DOuLAMr99k#v{t*I(xIf++rDT9nk?-*0AfR+ zY=XfINaU}5%zc%5PbU6*e{qNWE!8J69Jp-AR)F?fzuNcnouq}#Q&L`vUV3Y*PjPcl zn)-fZjNUH(;ue=M4ZiM2uDQxBOx6PykEIkfv9KH+_pWXmK5=(@N6plE*DF!ZYf;`aZyVFE>~!Yyx-{+{CWZgagV-n z;#{XB?i-B?=Y_HQ2hN{RwO{vaDsNm1`BBiKnq~}6A`;dSCe^>&FKpPj_jq2aD2j$n zNdN-TogAKPrLyg|SJ9!wq)_O~0RCq+Td+U8jw-vTujX*klQ$=*Or3EEUYfS(SwJ)7 z*wy{2Cy4)mHAUc1_LW(k_;fbJXK?^;EvsTajO;!(o-z?FXV}bQ!x?g&^ z&g(yQ;+*>vP8%wK?`rto{na9tz_7ssQKW^<4JU?4$erC*fJsS30E}L~FB0{5Q7U_g z2Qsu|GZ?)V5O(xvb%*xsVX2`{op6dBVCCpZut`1}(sxtHApZQLkdEm2vC;25Wvq~@ z=5%G~P#z%50c3@koQlUz-jc}zTuy_d7uksZh?lFKjjX#}VV5-1S6GCo9rF^eiU8|m zIO?Z%O;QOG2M=}v@v(tjwOv7@U0NYTgv@A-Q>+uZkc24Px9!zF9E2WAm0eNqckHW&1)8NvOpl>YDqlzyVqye51l>x=J5tgY^lFG(hQ++2$;O zs7?+djY{0k$|FH~CUyr*=)lx?8VCt!a&@mEGL3+0ODsQRf<=d_G21i0<=0Z~6m|)y zs9!k^K@7>XfDRyj7DczZBQm+BnLW0(y1%lUyQe=BsV7i8n`wu=q4Xt$ zL^t$wQ&toF!zEYZ9IhVo=1VkeB}|tS(0$vnEHC~>)in}qeipxf6+T9MGxlGTxel~^ z*gG5YH;fy+EE;!%0R!n#0Z}!IN>5`(J)$2(+kRdVUhoP#k`#6gI|}Jg?7aEh*2?ZT z>RM#LkGSK$QQX~rc`Uoin7~NPD#Mbd)N@<<4s-#pj|3;?5{Mt+3%qdeOId^hz8vm3 zXG^((AcjDCRUwEp-&;3-@oQ7Ixb83E2^8o_0p*%uF4&@~jKcUtb#S zDZ7z2*tIht7Hy=WvF(=?UdTX9l5Msyqq`NDuRyoy4myIEC>VKv;#VF(b>aa)ydDhS zcKo&d!r*nUi$V+UH)oKPz3&4$FO6g@UObG)`VZ0D#;EKa)FJyhFiQ#8X_cQ^cp4hOJ)>2IRZ;E>KsEw(Iw zqm|pSwCWNr!7nbK+D!jY_H|NeN2~cQorRc@1Bi7wIG-0-beBH1i6zXNC370f&!<&BoxKUS;vPWOr<{ z#i^G3$ioW8r>8INE!>HFg0JH~VFC%SpPkDs@@x60A zWy|j|t*R9w-^)t2v^j|pqVWFtH=KFE`#hY~KRB(uNzs&18jGFstap0xW+r|pA$Mr4 z+D4RvF91d)Y!F-*wAT${Ew8M6&x+sXPe}fal|%5WG96wpj923ZnB9T9w@hMt^UptL zz@vb{@C&a>ls076Pz2*eYZlnQQdGYoAaCYQ&(~t!`v#oRL!&Oe?3pHUbljB!;Af+0 zn6~^ap=m*zAotqShEe5~D7>fVeWO`}1m01+sk^@Vp{*ZgP~iVO3W1QAYCCbj~)vHRi<@ z{r6K{{;QHHxW#TiJ~kLWl}=MaEoBmZ3(6unUVm~7nf24+*z3vt;2tY&bwrnHL-_7{9%=-h3yrKq@?NBZFbgT565 zs@H^WMK7x}0HVJ?=1z!{N{e@ro{kt+vtXjhl_f_j@__8XN#9}Y)qIyS)aH$aNGU)( zD(7moCM>51;=*(aU*4~!4v(GAM z71aupiQZL$IPcf^7Uj!|lLY^5bUh&5bK=qZaGczH3*_^=$O*(rMqxEru`7`LK}4B0 zdKktFD4!OMIJ59xbDUNN!#Mejafelppb67BTXp5CnGD-*!< zLJ`{`gJ1Kl*cw@uUnK)3t?sVB*+KdL$f_-%_0M)E=&D3t(zOWSrA%R3)<<{1u)bmaK2W35-L@Ye#!RUu8TZ}1lvLkzXg zD?@*@%>K`(vf_5NeQ_Sz!0D1QNVqOz#p5r)7(Y!F9|fADI{ zWWngc%;}Cx+GA(t3N5z^1?U_tYhx?7v^X){j3~2Yw2TI-{sN_$p)9aWFU&DhB!|SeFi78 z?jO3+UsH%bVwzbAo3>GX;q~3$n4><>#;`B(SvLtX)Z-HlKn!` z?~c$y%x-^`ttrMxsmex<#%Dtw&k8e^)QZZe676_IswD6=E}KJX5~MR6WNqI!*9?FQ z^;%poE=Zt=RJ1R2C@Gu6Nnu@C+@ zCqX0=U9(Kb4HdBMDGnemhDb}V$KAU5`Jzm1O>I%jJRe9@!|B)FaE6gJ8;cnso`$H< zRtPl|IwZRglR>T&+*2Gm_NS0+4oIimg1IhHR3TceYW@%;JvOMt2Rc1WZnnl4gJQk%sYi6N z-t*{MN?*fHPQWVKU$6&l-|&=y{ImQTh^YdUnra;a<@s!H!83b6y5EepSjx$C%Hqi0 zy+*Cv9h{)*4~6rrLdLAN47ut$z}jPsV?#+{A$VMJ?WBh2Tsb-Gw6{oi^3GTk`1IV@ zJBCdN^52alp|~^PT~mbbK(d^A?j1Ik+t=em%5XQJ@$V&&H86>&oXD+KBf*+kEdhG- zPiH88+8_f(vq#2?0T=d=9XRP-FMqWY36HIeBc!bt=I;laopf*y>!_Q44sdKV0odat zujH|hN+EuiGRuDeEmT3%mj5Vh@kRo6|IK)QRr*w3bOgils#wK4(7!L3vg2mh-|@y0 zoXKV^hfU!pY3m}!M>pd*Ug_|wsWqrVBpAyEB;4Z%7^`GDWUX-yLbjvaG-{NUBiIC= zez0gcnHF{*29%Tmyx*wHpv^PbPyoe7`RKa%iRfx2S%l}^;E4(FbBq^6kvk=NPFWi% z^)OOK;k3E9#T?{F4B{2^nkBq)=wbJ-L|{5B8Gg8oL-%ac4n6ei6UEK-k>f^;EqA>*<65REPo290f3J<&~%wK*86guSfRe^xN=7N`P|KnyZLsx(di87)RK2REEyTEw%5@>bro&q~1uO30nfnSEu%vo{k;5uy&`LSUGih_G@ zx9vL63s@=K-4#JdKO^i8I3Daqn%Zp ztORm^*-|o&D+OP|MCQO9*yRAaZZ{e5B~8wBhb3gQje`M`lc8j^^QcJV@dvVzhs{4d z=|yb{5vmF)W-PmM_RusSVx+4C>gx4>X)xV$5E@y_#)rs(ixd{^TvvcdW0wwmQvrE9 zN&teWow9y@3Kq*fg65&pwrYufc~pJwKrKfV@(o#kvs0w2FR`fZW|AU}qQRZi1EsqJ zii(lf|7q!RL%n{o)jCFVdpMJT6%(u;hQ8_ z@o}q0nV#6oJ#Rd6>4rBqFy0T zzD#~eS=rPEV5o@|5J~Lz&?99s9;#%ZGZ!lvk7*1ygChufJyrAJrH_*tI-a!P#F7ql z2$TUUj1y%zCpjdIXgY{tWJr*ubCobpL^!IDgQqhX9!t53ZC%xBssFB9=$>$R;sYz4V&Opp(^iQ*1 zyEO*@oJ;?G7l74h=`tAq-#zj1)DIOnNr;BBR9;%A9{_aLCy;nQn>wcZV<_UZ0dO;{d44JF~hk3w02K?sl3ZK3+V(R*qsYOS} zLBcW%77@bmNj_dh<(&~i44!vrMG%BWIgt-u9xBJ8>0*5T<;2bIAHmMsnRPmfY7Wy; zRUz9tN%U!b?lw-#Gk)enPKq?vzkhPoe>`fMb)@Oixff&h6i8i>fzHX{5s((PZFeG@ z*Adr$Ca?}al%^Xla(jGPya9e(B!T-g^0x9-^TVrtdJ~URwbXTl1~o_N6J+786680C zndA(~G3<;>rqUgJJKGiI1EvTs9TYzLKHo$MytK`4^tSd+%sWy>yRqN0wSF=Kzep67pfe*FG)U*GG#ug~Xwf3ExW zs@Bg>>9Dezy%qp^XT`JC45ISvZ?GI_(yrgHKfuFErDb8fT!CW2z!-> zkrw)ly6Ib+PTXS(MEhX%?}81tO!CYIp(xOuS-TQ&Xa>CHiW}4swx%&y1fgG>Z~zbv zm3<7@$g`XtC!J_~TewO#E~f8E9fCpN;B}rv$wDGRr=bKgb1*{Ppw}0l2h?|lo3Z1# zgk>!}+LvDznsbtUQr;40^n=O|nvYX3?swaX#T$k-i!TCH6CXR278H-ySnFM4jk2}2 zl_0ZHtc*>?DNdu-JYE?1*$7GyGO)ld zV|UqND^9~J#(RaooV92@MgU@RKqz4I;M>~aYvfYk*K-m1H}fd~@~ofTJr>s4C{MNe{px_m9o=u^6+Wn+*&u^4=(B#orB*FXAyI&`YS<|(BSlj*u48{9Qdn@qJIvm zEn~UalO_^#o!lqw7~{_Xkewn0kp0yD41Sv8G1|!Ttnz8c&!+*GL$ZJjxE+dV$50sL zba-y1vZ=kOTrmdNGFo&IzZHdALsbCzfEysrAbzl(ipfch%4=YJ-$(^AI$)y-8HHhV zE_F`DuXW0?b)+9u?Y57(1vOh^o#9>p5Fa}W7(QZpQjI-=KO%&{;h@hn*$jTfV;Gl1 z8Byi`w#oSlXkVXN#~jv}%LYX|f&5KG5bd-Th#_=Wg;v2GlefRpdaAxFX}Mn}BKg$& z*pK~Y&jSETimnvA8AL*(K^{JPA3_#l1WtSj#1^y{|^2~eAr?NoN z2>y#WP?l5W+~_H;qu6 zMMr=K;8ay+v~BWg9=I>BzJ~-F^M?ord+#REJl^~QpjH_BhO4I{Rc*LJTl%`isN4Zz zVBpbT^Qe7z_C(Cya(y|i&q#D5{OtAm%ns|oD&XK3w9B`Bz15V}Ye~53Wh35mF%N)q z)?r*_*7g7%mqJSuc$`U0=U6`m*Lf&4 zB-lsfZbqFQn4;DiWPPt}Ec{Z8>;c3m@e;&odQ;hKaWzj18`DVZwM`5hJ9iWKQ{n2F z$I1@na)Tpb7T3_=1a0&i`D{>&$p`2;YsP!dcl;)KR~fFHXbh-$DQVGq?{x-2?X2b2 zNuPD%46xX}lI7m$ea#S~H%ydJhq<8B6h|w!1io-u@Qt2&{zvFfkps%~t)}KW zS{GHg+P~L%*}Aa!o3fdk6T2wZuX1XI-3W0Lav8hTW`l9zFs!FpKq=zH0hQ!TVw&Wd zdtX>;%smBAiUK%B5V;Op_aXE7M@fFQ1_Ri$Ki)qkt_B)#{`=0S_iBGPtc~XbZ?;Pz zaz`Vm(7dE-14@RN4M~?Nl$(PKk5+>O1g!I1_0#p|3ZON@-kJh@ExMOx0S8^-ip79j zw>S`srgU9d2ocUt=+JVlOaiUO&5mxXu9dxA-QCe-{qkgLw7ZsmqubX~M7lx?@Y|8$ z#I%LG_eb`x$PmI0y}pYgw?)& zt|qPPrx0>72&h_~6N6=-l;InW`Ij$NyhLXLW6E0(0hCV_l>jHwKZst4vOTCR+VK(D zl64k_n-V^J*7{Ak#|a?mgGmH6oHT^8%lmo2TPkqn-ID3_{OqTXCVO_z^>^A}irY%x z5hV1(!=~lSiTVLe%XNDAosA!{nTYc|FOx4QU$jaQ;p0EPqPD95?0PysyE_zqziRl> zjn|j17XQlC`3D_gPMqP2xb~&9rWqN%!xm&pNrqK>7NllPomzO4z;XfXJk3C3VOk?? zmIhJPa|QNtwDo~Ro9CK0dMvzYbzCo{IZ9M?|KWm0=(XU+WD<|h-ddyzqozec4}{)t zDqobCG8GZCNJ?-$Aw9gIOuptw7nDD$XuE)R*mZD?%}r?N@rVLKGcmE5d-9zYxP~eb z+J8hDb#jxsmRm}d>}W97C;t_S=eKDIYXe`e}7w5o{LGB z`J9!S2+qi(8NG|HJJ-g% zK4SIf1k!Uu22Hr|SEEMw02dL|Fw_x+bOI50i+M6llKC;O7^w;=DsxTQuqM7W!%d(D zmA7D(i|@5b&JKQlHF#b{U&60tiyltL8@cu!{Yng^qlQ}xsyXKcnEMsSVzD1_Fl0>0 z@E30}ZGz}@MsJ6TF$Xsd)Pq%QN?`mOmoENyoTUJ(Np;Y@(v1daV@MsPiTa-elNYc5 z{35-T`1Q57*er9cjgPvgksiqta5%{VvLB-OV6-dJuq;D4@xoT)kKP<3bpdUHhzrbg zI)yQRl*)7WY1+)dtRN;?7!Wl;GgVet_!S~D3>i`W!t64{z|=xNfv4ScXnVyuYm*)} zjT=z=xo=1s@?zTu?u}Z@on`l7{F)gswT6@kn&}XyldniJ4~W}PBNGoQCKnidGk1y$ zP&}!$M`rhjunv4D0+Ga5Mz10ANW8T`j`G$PiSH*T+)x< zg(!xr?pCz01FBa^20^jkRBRV4gRp`24ZagzsKlXYa_>Q4kd2!hBbz>TvibC_W@7WB zWKKb{N;*SI@!&^}=PdJw%l#6wi=taX?u%Swj!W+c3=5QgvIIy}vxB6t7P}d9`7Qs% zrT=v>vE%s++4*_U)+{U0IGVY?a_uf5bNO?(COK`AKkF;y+@GTxH%flh4&FbS*NV>G zH=?(n_~v>ubU62BSX|CmPp=$*dKmTLc>y+y9(y!!t7gc2WOsE2#oSB!r@lZ@(on!Z z^X;+1$?n-X+|;-h!ZANMTO4XX&cj8cG~OLQ2(HK^df?6^V?|V#i;yMTxjwe5o?Ue$ z#M6zEVK}#8bB6zMLtJ-wW3!_#_15un_N?NK)S825l8x%uTrTar6l-4G#%yM-=#ev) zbuTRDjKCk-tMtt!`GFK6<3bQ4*7rf~ z6yvES!LfS;^XNE9={yiqJ1pFO7Z62$?_1US>{V+J5L2m>-bqrbs}DK%)%RTH3v^=B zI1vn9+Vi{L=l(F->CWlP)tKLdI#ugr-1rC&$vok}tGh@J3jFXR8!Hy_goH=1abNOe ztfTiP7imRd<~cSL-!GTW8$=Emu=dx6Ut6yZj2UO2GjpX@+N{omj=Kk)`8t{-7IN1X zeRQ=eOZNg}+_}&rD_K2 zDRv*pxmgyXcP!BZD-X+WcfY0FmrN8o^GxO zl1{)_y03t*Y}fE(^&EBP5IgDAy+zV-UUEZ*9KGtlNltG(Bq1jovo0MHb*z%!zE(80 z3}mcfvPI(W^gPmAVf$I7roT2y6Usm zqHfHtlPx6qANshdnjk|!sMYihVHeSQfppm+)*ma(j=F1p+DUr6n-cTiiF$$!*mSLe zG}JDdB--i`FydL@oY4ZyQoM9;4@mSZ|2Y_}@F;~t+dEt;8ayp$6L+TVv;?-}S(Sx` z*VJ(YnFRvsVK9)T4%u1-JAEamj@X?AZhfSAq{}QS?Y#HzBP@30w=eVe8`8(8L4WIT z#sitJU<}3V-9GCZhJDN<-jySAtFc=eW6;*?gpk9CWH&F!|gKukrj0QK_29c&mK&&n@2Si*V&O?=v8d!o}Y zd;g%jFSGw9jQ+C!W=9HRHukQ$!%zb$|E}g*Ctl9nSU{jx_3fvpKztH-6KcL$!{1un zUz|vLzLXnYX+R`7aJyXPH_Ju~cHBLfcwQ4ziV6|^MHUa11naBHKPid|GdD?(O~0|z zWjp%;8y~2yV<^FqJZZiSqNlaAKa0AJt#OjvtmYy0xf|6bl}QJ5L|b7U zH7Ze`so~~dB81nx%>Knewp^z9F3Vj(qSYHtwl Date: Wed, 14 May 2025 10:07:45 -0500 Subject: [PATCH 13/27] comments --- Scripts/buildapp-linux.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index f5bbb14..4ed2a23 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -29,23 +29,23 @@ import tarfile import json -dir = Path(__file__).resolve().parent.parent -scripts = dir / "Scripts" -dist = dir / "dist" / "linux" -payload_dir = dist / "payload" -payload_scripts = payload_dir / "Scripts" -result_dir = dist / "result" +dir = Path(__file__).resolve().parent.parent # Directory of ProperTree +scripts = dir / "Scripts" # /Scripts +dist = dir / "dist" / "linux" # /dist/linux +payload_dir = dist / "payload" # /dist/linux/payload +payload_scripts = payload_dir / "Scripts" # /dist/linux/payload/Scripts +result_dir = dist / "result" # /dist/linux/result settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/user/.ProperTree args = sys.argv[1:] verbose = "--verbose" in args -# For verbose-specific logs. +# For verbose-specific logs. These will only be seen with --verbose. def log(*args): if verbose: print('\n'.join(map(str, args))) -# Get version. +# Get the version of ProperTree. with open(scripts / 'version.json', 'r') as file: version = json.load(file)['version'] log(f'ProperTree version: {version}') @@ -69,11 +69,12 @@ def log(*args): if not os.path.exists(dist): os.makedirs(dist) -# Only clear /dist/linux. +# Only clear /dist/linux if --clear is used. if "--clear" in args: print("Done!") exit(0) +# Tries to see if the inputted Python executable is a valid one. def is_python(path): if not os.path.isfile(path) or not os.access(path, os.X_OK): log(f"is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") @@ -179,7 +180,8 @@ def is_python(path): Icon=$HOME/.ProperTree/icon.png Terminal=false Type=Application -Categories=Utility;" +Categories=Utility +MimeType=text/xml;" mkdir "$HOME/.ProperTree" > /dev/null 2>&1 printf '{icon}' > "$HOME/.ProperTree/icon.png" From db3195157d543fa1f614087b9a46360c95c50912 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Wed, 14 May 2025 13:30:21 -0500 Subject: [PATCH 14/27] More documentation for buildapp-linux.py --- Scripts/buildapp-linux.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 4ed2a23..cfc3ae3 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,4 +1,6 @@ -# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64 Debian-based distros, but any architecture is theoretically supported. +# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64 Debian-based distros, but any architecture/distro is theoretically supported. + # ProperTree by: CorpNewt + # This script by: Calebh101 # Usage: python3 buildapp-linux.py [--verbose] [--python [@]] [--always-overwrite] [--use-existing-payload] # "--verbose": Verbose mode. @@ -14,11 +16,22 @@ # Results - The script will build results in /dist/linux/result. # ProperTree.sh: The shell script containing ProperTree. # ProperTree: The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. - # ProperTree-Installer-x.x.sh: Installs ProperTree as an application. + # ProperTree-Installer-V.sh: Installs ProperTree as an application. # The Scripts # ProperTree.sh: Runs ProperTree. Please note that it can be run with "--clear-data" to clear ProperTree data. - # ProperTree-Installer-x.x.sh: Installs ProperTree by adding "ProperTree" and "propertree" to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with "--uninstall" to delete these three files. + # ProperTree-Installer-V.sh: Installs ProperTree by adding "ProperTree" and "propertree" to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with "--uninstall" to delete these three files. + +# Extra Files (in /dist/linux) + # main: The exact same file as ProperTree (in /result). + # main.sh: The exact same file as ProperTree.sh (in /result). + # main.c: The generated source code for the executable. This can be used to compile the executable for multiple architectures. It contains all the necessary data, so you don't have to include multiple files. + # payload.tar.gz: The compressed version of /payload. + +# Known Issues + # ARM is not officially supported by this script, but you can compile main.c (in /dist/linux) from source. + # ProperTree does not have an icon in the taskbar. This is due to the fact that when you run the .desktop file from the launcher, it doesn't directly load a GUI; it goes through ProperTree.py, which loads a window by itself - separate from the launcher or the .desktop. + # ProperTree's taskbar window is named "Toplevel". This is on ProperTree.py's side (most likely tkinter's side), and I do not know a fix for this at the moment. from pathlib import Path import sys @@ -118,7 +131,8 @@ def is_python(path): # Success print(f"Found Python: {python}") -# Get the icon binary. +# Get the icon binary and also copy the icon to the settings directory. +print("Processing icon...") with open(scripts / "icon.png", 'rb') as f: content = f.read() icon = ''.join(f'\\x{byte:02X}' for byte in content) @@ -133,7 +147,8 @@ def is_python(path): for arg in "$@"; do if [ "$arg" == "--clear-data" ]; then echo "Removing data..." - rm -rf "$HOME/.ProperTree" > /dev/null 2>&1 + rm -rf "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 + rm -rf "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 echo "Done! ProperTree data has been cleared." exit 0 fi @@ -168,6 +183,8 @@ def is_python(path): for arg in "$@"; do if [ "$arg" == "--uninstall" ]; then + echo "Uninstalling..." + rm "$HOME/.ProperTree/icon.png" > /dev/null 2>&1 echo "Done! ProperTree uninstalled. Your data was not affected." exit 0 fi @@ -190,7 +207,7 @@ def is_python(path): DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") tail -n+$DATA "$0" > "$HOME/.local/bin/ProperTree" echo "Writing files..." -echo "#!/bin/bash\n# This is an auto-generated script.\n\\"$HOME/.local/bin/ProperTree\\"" > "$HOME/.local/bin/propertree" +echo "#!/bin/bash\n# This is an auto-generated script.\n\\"$HOME/.local/bin/ProperTree\\" \\"@\\"" > "$HOME/.local/bin/propertree" echo "$desktop" > "$HOME/.local/share/applications/ProperTree.desktop" echo "Managing permissions..." chmod +x "$HOME/.local/bin/ProperTree" @@ -212,6 +229,7 @@ def is_python(path): # Load ProperTree.py's code so we can edit it. with open(dir / "ProperTree.py", 'r') as file: code = file.read() + # Load linux-app.c's code so we can edit it. with open(scripts / "linux-app.c", 'r') as file: ccode = file.read() From 7c162e9c62d626accda1711f98166507e5236e60 Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 14 May 2025 23:11:59 -0500 Subject: [PATCH 15/27] Bug fixes with the installer script for buildapp-linux --- Scripts/buildapp-linux.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index cfc3ae3..e9a8bc8 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -201,21 +201,36 @@ def is_python(path): MimeType=text/xml;" mkdir "$HOME/.ProperTree" > /dev/null 2>&1 +mkdir "$HOME/.local" > /dev/null 2>&1 +mkdir "$HOME/.local/bin" > /dev/null 2>&1 printf '{icon}' > "$HOME/.ProperTree/icon.png" echo "Extracting payload..." DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") tail -n+$DATA "$0" > "$HOME/.local/bin/ProperTree" + echo "Writing files..." -echo "#!/bin/bash\n# This is an auto-generated script.\n\\"$HOME/.local/bin/ProperTree\\" \\"@\\"" > "$HOME/.local/bin/propertree" echo "$desktop" > "$HOME/.local/share/applications/ProperTree.desktop" + +cat << 'EOF' > "$HOME/.local/bin/propertree" +#!/bin/bash +# This is an auto-generated script. +"/home/caleb/.local/bin/ProperTree" "$@" +EOF + echo "Managing permissions..." chmod +x "$HOME/.local/bin/ProperTree" chmod +x "$HOME/.local/bin/propertree" + echo "Refreshing sources..." update-desktop-database ~/.local/share/applications source ~/.bashrc +if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + echo "WARNING: $HOME/.local/bin is not in PATH. You will not be able to run ProperTree from the command line if it's not in PATH." + echo 'Please add `PATH="$PATH:$HOME/.local/bin"` to PATH in .bashrc or whatever you use.' +fi + echo "Done! Run this script with --uninstall to uninstall the ProperTree application." exit 0 DESTROYER From 37ab4c6bda38f61c37677401b754c23dd562a8b0 Mon Sep 17 00:00:00 2001 From: Caleb Date: Thu, 15 May 2025 21:22:34 -0500 Subject: [PATCH 16/27] Comments on buildapp-linux.py --- Scripts/buildapp-linux.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index e9a8bc8..fac3278 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -2,6 +2,10 @@ # ProperTree by: CorpNewt # This script by: Calebh101 +# Notes: + # This script places data in /home/$USER/.ProperTree. Please do not delete this directory or any files in it. + # This script needs Python 3 to run. + # Usage: python3 buildapp-linux.py [--verbose] [--python [@]] [--always-overwrite] [--use-existing-payload] # "--verbose": Verbose mode. # "--python [@]": Select a Python executable to use (default is output of "which python3"). From 9ed111cd19edb51a33feb08d9dbb4281d9906a1e Mon Sep 17 00:00:00 2001 From: Caleb Date: Fri, 16 May 2025 16:10:24 -0500 Subject: [PATCH 17/27] More documentation for buildapp-linux.py and linux-app.c --- Scripts/buildapp-linux.py | 64 ++++++++++++++++++++++++--------------- Scripts/linux-app.c | 7 +++-- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index fac3278..d9f58c6 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -10,7 +10,7 @@ # "--verbose": Verbose mode. # "--python [@]": Select a Python executable to use (default is output of "which python3"). # "--always-overwrite": Always overwrite applicable files instead of prompting. - # "--use-existing-payload": Don't overwrite /dist/linux/payload/ProperTree.py. + # "--use-existing-payload": Don't overwrite /dist/linux/payload/ProperTree.py. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. # "--skip-compile": Skip compiling the script to an ELF executable. # Generated Directories - The script will build in /dist/linux. @@ -18,7 +18,7 @@ # result: This is the directory with the results. It will have a ProperTree.sh (the raw shell file) and (maybe) an ELF executable named ProperTree. # Results - The script will build results in /dist/linux/result. - # ProperTree.sh: The shell script containing ProperTree. + # ProperTree.sh: The shell script containing ProperTree. This manages all of ProperTree's files and data. # ProperTree: The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. # ProperTree-Installer-V.sh: Installs ProperTree as an application. @@ -26,6 +26,14 @@ # ProperTree.sh: Runs ProperTree. Please note that it can be run with "--clear-data" to clear ProperTree data. # ProperTree-Installer-V.sh: Installs ProperTree by adding "ProperTree" and "propertree" to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with "--uninstall" to delete these three files. +# The Process + # 1. Generate a main script and an install script. + # 2. Generate a payload by copying over required assets in /Scripts and compress it. + # 3. Attach the payload to the main script. + # 4. Attach the main script as another payload to the installer. + # 5. Optionally compile a generated C file to the executable. This uses the least external resources possible, as it only used the base install of gcc. It does not need any extra tools or packages. + # 6. Copy it all into result. + # Extra Files (in /dist/linux) # main: The exact same file as ProperTree (in /result). # main.sh: The exact same file as ProperTree.sh (in /result). @@ -46,7 +54,7 @@ import tarfile import json -dir = Path(__file__).resolve().parent.parent # Directory of ProperTree +dir = Path(__file__).resolve().parent.parent # Directory of ProperTree. All of the refrences of the other directories are based on this, aside from data and temporary directories. scripts = dir / "Scripts" # /Scripts dist = dir / "dist" / "linux" # /dist/linux payload_dir = dist / "payload" # /dist/linux/payload @@ -54,12 +62,16 @@ result_dir = dist / "result" # /dist/linux/result settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/user/.ProperTree +# Capture the arguments passed. args = sys.argv[1:] -verbose = "--verbose" in args + +if platform.system() != "Linux": + print("Can only be run on Linux") + exit(1) # For verbose-specific logs. These will only be seen with --verbose. def log(*args): - if verbose: + if "--verbose" in args: print('\n'.join(map(str, args))) # Get the version of ProperTree. @@ -111,23 +123,23 @@ def is_python(path): log("is_python fail: overlow[1]") return False -if platform.system() != "Linux": - print("Can only be run on Linux") - exit(1) - if "--python" in args: + # Find the argument that they're claiming is a valid Python executable. if args.index("--python") + 1 < len(args): python = args[args.index("--python") + 1] if not is_python(python): print(f"Invalid python executable: {python}") exit(1) - else: + else: # Otherwise, they trolled us. print("Invalid Python executable: no executable provided") exit(1) -else: +else: # If they didn't supply a Python, we take the result from "which python3". The reason we use "which python3" instead of "which python" is because there's typically not a default "python" executable, but Debian expects the user to use "python3". result = subprocess.run(["which", "python3"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.stdout: python = result.stdout.decode().strip() + if not is_python(python): + print(f"Invalid python executable: {python} (auto-detected)") + exit(1) else: print("Invalid Python executable: no executable found") exit(1) @@ -135,14 +147,14 @@ def is_python(path): # Success print(f"Found Python: {python}") -# Get the icon binary and also copy the icon to the settings directory. +# Get the icon binary so we can embed it. print("Processing icon...") with open(scripts / "icon.png", 'rb') as f: content = f.read() icon = ''.join(f'\\x{byte:02X}' for byte in content) # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. -# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are placed back in /home/user/.ProperTree. +# The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are copied back in /home/user/.ProperTree and the temporary directory is deleted. script = f"""#!/bin/bash # This is an auto-generated script. # ProperTree V. {version} @@ -177,6 +189,7 @@ def is_python(path): # Generate the install script. Also includes an uninstall option. # We embed the icon binary so we can copy it over without relying on external files. # BREAKER is now DESTROYER to avoid awk confusion. +# It also handles instances where /home/$USER/.local/bin isn't in path (or when it doesn't even exist), which happens on clean installations of Debian-based distros. install_script = f"""#!/bin/bash # This is an auto-generated script. echo "Preparing..." @@ -235,23 +248,23 @@ def is_python(path): echo 'Please add `PATH="$PATH:$HOME/.local/bin"` to PATH in .bashrc or whatever you use.' fi -echo "Done! Run this script with --uninstall to uninstall the ProperTree application." +echo "Done! Run this script with --uninstall to uninstall the ProperTree application. You can also run ProperTree with --clear-data to clear ProperTree data." exit 0 DESTROYER """ -# We're gonna put our settings into a persistent user-specific directory, since otherwise it'd go into a temporary directory, which we don't want. +# We're gonna put our settings into a persistent user-specific directory, since otherwise ProperTree would generate it in a temporary directory, which we don't want as it just gets deleted. if not os.path.exists(settings): os.makedirs(settings) +# Load ProperTree.py's code. This will be written later. print("Processing code...") -# Load ProperTree.py's code so we can edit it. with open(dir / "ProperTree.py", 'r') as file: code = file.read() -# Load linux-app.c's code so we can edit it. +# Load linux-app.c's code so we can embed main.sh into it. with open(scripts / "linux-app.c", 'r') as file: - ccode = file.read() + ccode = file.read() # This isn't a typo; "code" was already taken def copy_settings_json(): print("Copying settings.json...") @@ -262,13 +275,13 @@ def copy_settings_json(): # If the file already exists, then ask the user if they want to overwrite it. if os.path.exists(settings / "settings.json") and "--always-overwrite" not in args: while True: - response = input(f"Do you want to overwrite {settings / "settings.json"}? (y/n/x): >> ").strip().lower() + response = input(f"Do you want to overwrite {settings / "settings.json"}? (y/n/c): >> ").strip().lower() if response == 'y': copy_settings_json() break elif response == 'n': break - elif response == 'x': # Quit + elif response == 'c': # Quit print("Done!") exit(0) else: @@ -282,9 +295,9 @@ def copy_settings_json(): os.makedirs(payload_scripts) if not os.path.exists(result_dir): os.makedirs(result_dir) -if not os.path.exists(dist / "archive"): - os.makedirs(dist / "archive") +# If the user decided to use the existing payload (for debugging), but ProperTree.py isn't found, then we tell them and exit. +# The other scripts normally packaged into /payload/Scripts will have errors at runtime if not present, so it's not our problem. if not os.path.exists(payload_dir / 'ProperTree.py') and "--use-existing-payload" in args: print("No ProperTree.py given for payload.") exit(1) @@ -302,6 +315,7 @@ def copy_asset(target): log(f"Copying asset: {target}") shutil.copy(scripts / target, payload_scripts / target) +# Copy all the assets. if "--use-existing-payload" not in args: print("Copying assets...") copy_asset("__init__.py") @@ -315,6 +329,7 @@ def copy_asset(target): copy_asset("snapshot.plist") copy_asset("version.json") +# Compress the payload into a .tar.gz. print("Creating payload...") with tarfile.open(dist / "payload.tar.gz", "w:gz") as tar: for dirpath, dirnames, filenames in os.walk(payload_dir): @@ -322,12 +337,12 @@ def copy_asset(target): filepath = os.path.join(dirpath, filename) tar.add(filepath, arcname=os.path.relpath(filepath, payload_dir)) -# Here's where we add the payload. +# Here's where we add the payload. We cat payload.tar.gz into main.sh, which uses the delimiter "BREAKER" to decide where the binary is. result = subprocess.run(f"cat {dist}/payload.tar.gz >> {dist}/main.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: print(f"Error: {result.stderr}") -# Here's where we create install.sh by adding a payload here too. +# Here's where we create install.sh by adding a payload here too. The payload is just main.sh, uncompressed. with open(dist / "install.sh", 'wb') as file, open(dist / "main.sh", 'rb') as main_file: file.write(install_script.encode('utf-8') + main_file.read()) @@ -355,6 +370,7 @@ def copy_asset(target): print("Generating executable...") try: + # Run gcc, a C compiler. main.c is the generated source code, and it can be compiled manually to ARM or any other architecture. result = subprocess.run(["gcc", "-o", "dist/linux/main", f"{dist / "main.c"}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stderr = result.stderr except Exception as e: diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c index 216464d..38657cd 100644 --- a/Scripts/linux-app.c +++ b/Scripts/linux-app.c @@ -6,18 +6,18 @@ #include #include -const unsigned char shell_script[] = {}; +const unsigned char shell_script[] = {}; // Placeholder, as raw bytes will be written here by buildapp-linux.py const size_t shell_script_len = sizeof(shell_script); int main(int argc, char *argv[]) { DIR *entry = opendir("/tmp/.ProperTree"); if (entry == NULL) { - if (mkdir("/tmp/.ProperTree", 0777) != 0) { + if (mkdir("/tmp/.ProperTree", 0777) != 0) { // Make /tmp/.ProperTree if it doesn't exist perror("mkdir"); } } - char filename[100] = "/tmp/.ProperTree/script-XXXXXX"; + char filename[100] = "/tmp/.ProperTree/script-XXXXXX"; // Generate the script's name to be extracted and ran int fd = mkstemp(filename); if (fd == -1) { @@ -39,6 +39,7 @@ int main(int argc, char *argv[]) { unlink(filename); return 1; } + char **exec_args = malloc(sizeof(char *) * (argc + 1)); if (!exec_args) { perror("malloc"); From e3ce1ab3632e70cbc142bf373ffa328fa1a63af1 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 17 May 2025 10:38:36 -0500 Subject: [PATCH 18/27] Some bug fixes for buildapp-linux.py --- Scripts/buildapp-linux.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index d9f58c6..5fe3cfe 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -232,7 +232,7 @@ def is_python(path): cat << 'EOF' > "$HOME/.local/bin/propertree" #!/bin/bash # This is an auto-generated script. -"/home/caleb/.local/bin/ProperTree" "$@" +"$HOME/.local/bin/ProperTree" "$@" EOF echo "Managing permissions..." @@ -318,16 +318,10 @@ def copy_asset(target): # Copy all the assets. if "--use-existing-payload" not in args: print("Copying assets...") - copy_asset("__init__.py") - copy_asset("config_tex_info.py") - copy_asset("downloader.py") - copy_asset("plist.py") - copy_asset("plistwindow.py") - copy_asset("update_check.py") - copy_asset("utils.py") - copy_asset("menu.plist") - copy_asset("snapshot.plist") - copy_asset("version.json") + for x in os.listdir(scripts): + if x.startswith(".") or not x.lower().endswith((".py",".plist","version.json")): + continue + copy_asset(x) # Compress the payload into a .tar.gz. print("Creating payload...") From 786ee7746ae6d8e757ae85bcc10cc39cca1c0827 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 17 May 2025 16:56:19 -0500 Subject: [PATCH 19/27] buildapp-linux now supports Python 2, along with some more features --- Scripts/buildapp-linux.py | 240 ++++++++++++++++++++++---------------- Scripts/linux-app.c | 1 + 2 files changed, 143 insertions(+), 98 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 5fe3cfe..2a5f58e 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,16 +1,14 @@ +#!/usr/bin/python3 # A Python script to compile ProperTree to run as a native Linux app. Officially supports x64 Debian-based distros, but any architecture/distro is theoretically supported. - # ProperTree by: CorpNewt - # This script by: Calebh101 -# Notes: - # This script places data in /home/$USER/.ProperTree. Please do not delete this directory or any files in it. - # This script needs Python 3 to run. - -# Usage: python3 buildapp-linux.py [--verbose] [--python [@]] [--always-overwrite] [--use-existing-payload] - # "--verbose": Verbose mode. - # "--python [@]": Select a Python executable to use (default is output of "which python3"). +# Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--always-overwrite] [--use-existing-payload] + # "--verbose" or "-v": Verbose mode. + # "--python PYTHON" or "-p PYTHON": Select a Python executable to use (default is output of "which python3"). + # "--clear" or "-c": Clear /dist/linux. Does not respect --use-existing-payload. + # "--dir DIR" or "-d DIR": Select the root directory of ProperTree to use. + # "--out DIR" or "-o DIR": Select an output directory to use. All files will be placed in here; it will not make an extra subdirectory. # "--always-overwrite": Always overwrite applicable files instead of prompting. - # "--use-existing-payload": Don't overwrite /dist/linux/payload/ProperTree.py. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. + # "--use-existing-payload": Don't overwrite /dist/linux/payload. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. # "--skip-compile": Skip compiling the script to an ELF executable. # Generated Directories - The script will build in /dist/linux. @@ -45,44 +43,72 @@ # ProperTree does not have an icon in the taskbar. This is due to the fact that when you run the .desktop file from the launcher, it doesn't directly load a GUI; it goes through ProperTree.py, which loads a window by itself - separate from the launcher or the .desktop. # ProperTree's taskbar window is named "Toplevel". This is on ProperTree.py's side (most likely tkinter's side), and I do not know a fix for this at the moment. -from pathlib import Path -import sys import platform import subprocess import os import shutil import tarfile import json - -dir = Path(__file__).resolve().parent.parent # Directory of ProperTree. All of the refrences of the other directories are based on this, aside from data and temporary directories. -scripts = dir / "Scripts" # /Scripts -dist = dir / "dist" / "linux" # /dist/linux -payload_dir = dist / "payload" # /dist/linux/payload -payload_scripts = payload_dir / "Scripts" # /dist/linux/payload/Scripts -result_dir = dist / "result" # /dist/linux/result -settings = Path(f'/home/{os.environ.get('USER')}/.ProperTree').resolve() # /home/user/.ProperTree - -# Capture the arguments passed. -args = sys.argv[1:] +import argparse + +# Set up the argument parser. +parser = argparse.ArgumentParser( + prog='buildapp-linux.py', + description='A Python script to compile ProperTree to run as a native Linux app.', + usage='python3 buildapp-linux.py [--verbose] [--dir DIR] [--out DIR] [--clear] [--python PYTHON] [--always-overwrite] [--use-existing-payload]', +) + +# Define script arguments. +parser.add_argument('-v', '--verbose', action='store_true', help="Run the script in verbose mode.") +parser.add_argument('-d', '--dir', help="The root directory of ProperTree to use. The default is the parent of buildapp-linux.py's directory.") +parser.add_argument('-p', '--python', help="Select a Python executable to use. The default is the output of \"which python3\".") +parser.add_argument('-c', '--clear', help="Clear dist/linux.") +parser.add_argument('-o', '--out', help="Specify a directory to use for the output. All files will be placed in here; it will not make an extra subdirectory. Default is dist/linux relative to the root directory of ProperTree.") +parser.add_argument('--always-overwrite', action='store_true', help="Always overwrite applicable files instead of prompting.") +parser.add_argument('--use-existing-payload', action='store_true', help="Don't overwrite dist/linux/payload.") +parser.add_argument('--skip-compile', action="store_true", help="Skip compiling the script to an executable.") + +# Set up the args +args = parser.parse_args() + +dir = os.path.abspath(args.dir) if args.dir is not None else os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Directory of ProperTree. All of the refrences of the other directories are based on this, aside from data and temporary directories. +scripts = dir + "/Scripts" # /Scripts +dist = os.path.abspath(args.out) if args.out is not None else dir + "/dist" + "/linux" # /dist/linux +payload_dir = dist + "/payload" # /dist/linux/payload +payload_scripts = payload_dir + "/Scripts" # /dist/linux/payload/Scripts +result_dir = dist + "/result" # /dist/linux/result +settings = '/home/' + os.environ.get('USER') + '/.ProperTree' # /home/user/.ProperTree if platform.system() != "Linux": print("Can only be run on Linux") exit(1) +if not os.path.isdir(dir): + print("Invalid ProperTree path: {} - Directory does not exist or is not a directory. (Make sure to point to a directory, not a file.)".format(dir)) + exit(1) + +if not os.path.isfile(dir + "/ProperTree.py"): + print("Invalid ProperTree path: {} - Cannot find /ProperTree.py".format(dir)) + exit(1) + +if not os.path.isdir(dir + "/Scripts"): + print("Invalid ProperTree path: {} - Cannot find /Scripts".format(dir)) + exit(1) + # For verbose-specific logs. These will only be seen with --verbose. -def log(*args): - if "--verbose" in args: - print('\n'.join(map(str, args))) +def log(*arguments): + if args.verbose: + print('\n'.join(map(str, arguments))) # Get the version of ProperTree. -with open(scripts / 'version.json', 'r') as file: +with open(scripts + '/version.json', 'r') as file: version = json.load(file)['version'] - log(f'ProperTree version: {version}') + log('ProperTree version: {}'.format(version)) # Delete /dist if it exists. -print(f"Clearing {dist}...") +print("Clearing {}...".format(dist)) if os.path.exists(dist): - if "--use-existing-payload" in args: + if args.use_existing_payload and not args.clear: for item in os.listdir(dist): file = os.path.join(dist, item) if os.path.isdir(file): @@ -98,66 +124,71 @@ def log(*args): if not os.path.exists(dist): os.makedirs(dist) -# Only clear /dist/linux if --clear is used. -if "--clear" in args: +# Stop after clearing /dist/linux if --clear is used. +if args.clear: print("Done!") exit(0) # Tries to see if the inputted Python executable is a valid one. def is_python(path): if not os.path.isfile(path) or not os.access(path, os.X_OK): - log(f"is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") + log("is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") return False try: - result = subprocess.run([path, '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - log(f"is_python log: result: {result}") + result = subprocess.Popen([path, '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() if result.returncode == 0: - log(f"is_python success: {result.returncode}") + log("is_python success: {}".format(result.returncode)) return True + else: + log("is_python fail: {}".format(stderr)) except Exception as e: - log(f"is_python fail: exception:\n{e}") + log("is_python fail: exception:\n{}".format(e)) return False - log("is_python fail: overlow[1]") + log("is_python fail: unkown overlow") return False -if "--python" in args: - # Find the argument that they're claiming is a valid Python executable. - if args.index("--python") + 1 < len(args): - python = args[args.index("--python") + 1] - if not is_python(python): - print(f"Invalid python executable: {python}") - exit(1) - else: # Otherwise, they trolled us. - print("Invalid Python executable: no executable provided") +if args.python: + result = subprocess.Popen(["which", args.python], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() + if not stdout: + print("Invalid Python executable: {}".format(stderr)) + python = stdout.decode().strip() + if not is_python(python): + print("Invalid Python executable: {}".format(python)) exit(1) else: # If they didn't supply a Python, we take the result from "which python3". The reason we use "which python3" instead of "which python" is because there's typically not a default "python" executable, but Debian expects the user to use "python3". - result = subprocess.run(["which", "python3"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if result.stdout: - python = result.stdout.decode().strip() + result = subprocess.Popen(["which", "python3"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() + if stdout: + python = stdout.decode().strip() if not is_python(python): - print(f"Invalid python executable: {python} (auto-detected)") + print("Invalid Python executable: {} (auto-detected)".format(python)) exit(1) else: print("Invalid Python executable: no executable found") exit(1) # Success -print(f"Found Python: {python}") +print("Found Python: {}".format(python)) # Get the icon binary so we can embed it. print("Processing icon...") -with open(scripts / "icon.png", 'rb') as f: +with open(scripts + "/icon.png", 'rb') as f: content = f.read() - icon = ''.join(f'\\x{byte:02X}' for byte in content) + try: # Python 2 + icon = ''.join('\\x{0:02X}'.format(ord(byte)) for byte in content) + except: # Python 3 + icon = ''.join('\\x{:02X}'.format(byte) for byte in content) # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. # The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are copied back in /home/user/.ProperTree and the temporary directory is deleted. -script = f"""#!/bin/bash +script = """#!/bin/bash # This is an auto-generated script. -# ProperTree V. {version} +# ProperTree V. {} # Run with --clear-data to remove data. for arg in "$@"; do @@ -171,26 +202,26 @@ def is_python(path): done ID=$RANDOM -DATA=$(awk '/^BREAKER/ {{print NR + 1; exit 0; }}' "$0") +DATA=$(awk '/^BREAKER/ {{print NR + 1; exit 0;}}' "$0") mkdir "$HOME/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree" > /dev/null 2>&1 mkdir "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 tail -n+$DATA "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" cp "$HOME/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 cp "$HOME/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 -"{python}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" +"{}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 exit 0 BREAKER -""" +""".format(version, python) # Generate the install script. Also includes an uninstall option. # We embed the icon binary so we can copy it over without relying on external files. # BREAKER is now DESTROYER to avoid awk confusion. # It also handles instances where /home/$USER/.local/bin isn't in path (or when it doesn't even exist), which happens on clean installations of Debian-based distros. -install_script = f"""#!/bin/bash +install_script = """#!/bin/bash # This is an auto-generated script. echo "Preparing..." @@ -220,7 +251,7 @@ def is_python(path): mkdir "$HOME/.ProperTree" > /dev/null 2>&1 mkdir "$HOME/.local" > /dev/null 2>&1 mkdir "$HOME/.local/bin" > /dev/null 2>&1 -printf '{icon}' > "$HOME/.ProperTree/icon.png" +printf '{}' > "$HOME/.ProperTree/icon.png" echo "Extracting payload..." DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0") @@ -251,7 +282,7 @@ def is_python(path): echo "Done! Run this script with --uninstall to uninstall the ProperTree application. You can also run ProperTree with --clear-data to clear ProperTree data." exit 0 DESTROYER -""" +""".format(icon) # We're gonna put our settings into a persistent user-specific directory, since otherwise ProperTree would generate it in a temporary directory, which we don't want as it just gets deleted. if not os.path.exists(settings): @@ -259,23 +290,27 @@ def is_python(path): # Load ProperTree.py's code. This will be written later. print("Processing code...") -with open(dir / "ProperTree.py", 'r') as file: +with open(dir + "/ProperTree.py", 'r') as file: code = file.read() # Load linux-app.c's code so we can embed main.sh into it. -with open(scripts / "linux-app.c", 'r') as file: +with open(scripts + "/linux-app.c", 'r') as file: ccode = file.read() # This isn't a typo; "code" was already taken def copy_settings_json(): print("Copying settings.json...") - shutil.copy(scripts / "settings.json", settings / "settings.json") + shutil.copy(scripts + "/settings.json", settings + "/settings.json") # Here, we're gonna transfer settings.json to our new settings directory. -if os.path.exists(scripts / "settings.json"): +if os.path.exists(scripts + "/settings.json"): # If the file already exists, then ask the user if they want to overwrite it. - if os.path.exists(settings / "settings.json") and "--always-overwrite" not in args: + if os.path.exists(settings + "/settings.json") and not args.always_overwrite: while True: - response = input(f"Do you want to overwrite {settings / "settings.json"}? (y/n/c): >> ").strip().lower() + message = "Do you want to overwrite {}? (y/n/c): >> ".format(settings + "/settings.json") + try: # Python 2 + response = raw_input(message).strip().lower() + except: # Python 3 + response = input(message).strip().lower() if response == 'y': copy_settings_json() break @@ -290,7 +325,7 @@ def copy_settings_json(): copy_settings_json() # This creates /dist, /dist/linux, /dist/linux/payload, /dist/linux/payload/Scripts, /dist/linux/result all in one check. -log(f"Creating output directories... (sources: [{payload_scripts}, {result_dir}])") +log("Creating output directories...") if not os.path.exists(payload_scripts): os.makedirs(payload_scripts) if not os.path.exists(result_dir): @@ -298,25 +333,25 @@ def copy_settings_json(): # If the user decided to use the existing payload (for debugging), but ProperTree.py isn't found, then we tell them and exit. # The other scripts normally packaged into /payload/Scripts will have errors at runtime if not present, so it's not our problem. -if not os.path.exists(payload_dir / 'ProperTree.py') and "--use-existing-payload" in args: +if not os.path.exists(payload_dir + '/ProperTree.py') and args.use_existing_payload: print("No ProperTree.py given for payload.") exit(1) -if "--use-existing-payload" not in args: - with open(payload_dir / 'ProperTree.py', 'w') as file: +if not args.use_existing_payload: + with open(payload_dir + '/ProperTree.py', 'w') as file: file.write(code) print("Writing main.sh...") -with open(dist / 'main.sh', 'w') as file: +with open(dist + '/main.sh', 'w') as file: file.write(script) # Copies from the Scripts folder into the payload's Scripts folders. def copy_asset(target): - log(f"Copying asset: {target}") - shutil.copy(scripts / target, payload_scripts / target) + log("Copying asset: {}".format(target)) + shutil.copy(scripts + "/" + target, payload_scripts + "/" + target) -# Copy all the assets. -if "--use-existing-payload" not in args: +# Copy all the required assets. This includes *.py, *.plist, and version.json. +if not args.use_existing_payload: print("Copying assets...") for x in os.listdir(scripts): if x.startswith(".") or not x.lower().endswith((".py",".plist","version.json")): @@ -325,59 +360,68 @@ def copy_asset(target): # Compress the payload into a .tar.gz. print("Creating payload...") -with tarfile.open(dist / "payload.tar.gz", "w:gz") as tar: +with tarfile.open(dist + "/payload.tar.gz", "w:gz") as tar: for dirpath, dirnames, filenames in os.walk(payload_dir): for filename in filenames: filepath = os.path.join(dirpath, filename) tar.add(filepath, arcname=os.path.relpath(filepath, payload_dir)) # Here's where we add the payload. We cat payload.tar.gz into main.sh, which uses the delimiter "BREAKER" to decide where the binary is. -result = subprocess.run(f"cat {dist}/payload.tar.gz >> {dist}/main.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +result = subprocess.Popen("cat {}/payload.tar.gz >> {}/main.sh".format(dist, dist), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +stdout, stderr = result.communicate() if result.returncode != 0: - print(f"Error: {result.stderr}") + print("Error: {}".format(stderr)) + exit(1) # Here's where we create install.sh by adding a payload here too. The payload is just main.sh, uncompressed. -with open(dist / "install.sh", 'wb') as file, open(dist / "main.sh", 'rb') as main_file: +with open(dist + "/install.sh", 'wb') as file, open(dist + "/main.sh", 'rb') as main_file: file.write(install_script.encode('utf-8') + main_file.read()) # These next couple sections is processing and copying main.sh and install.sh. print("Copying scripts...") -subprocess.run(["chmod", "+x", f"{dist}/main.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +result = subprocess.Popen(["chmod", "+x", "{}/main.sh".format(dist)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +stdout, stderr = result.communicate() if result.returncode != 0: - print(f"Error: {result.stderr}") -subprocess.run(["chmod", "+x", f"{dist}/install.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print("Error: {}".format(stderr)) + exit(1) +result = subprocess.Popen(["chmod", "+x", "{}/install.sh".format(dist)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +stdout, stderr = result.communicate() if result.returncode != 0: - print(f"Error: {result.stderr}") + print("Error: {}".format(stderr)) + exit(1) -shutil.copy(dist / "main.sh", result_dir / "ProperTree.sh") -shutil.copy(dist / "install.sh", result_dir / f"ProperTree-Installer-{version}.sh") +shutil.copy(dist + "/main.sh", result_dir + "/ProperTree.sh") +shutil.copy(dist + "/install.sh", result_dir + "/ProperTree-Installer-{}.sh".format(version)) -if "--skip-compile" not in args: +if not args.skip_compile: print("Embedding script...") - with open(dist / 'main.sh', 'rb') as file: + with open(dist + '/main.sh', 'rb') as file: binary = file.read() - bytes = [f"0x{byte:02X}" for byte in binary] + try: # Python 2 + bytes = ["0x{:02X}".format(ord(byte)) for byte in binary] + except: # Python 3 + bytes = ["0x{:02X}".format(byte) for byte in binary] - with open(dist / 'main.c', 'w') as file: - file.write(ccode.replace('const unsigned char shell_script[] = {};', f'const unsigned char shell_script[] = {{\n {', '.join(bytes)}\n}};')) + with open(dist + '/main.c', 'w') as file: + file.write(ccode.replace('const unsigned char shell_script[] = {};', 'const unsigned char shell_script[] = {{\n {}\n}};'.format(', '.join(bytes)))) print("Generating executable...") try: # Run gcc, a C compiler. main.c is the generated source code, and it can be compiled manually to ARM or any other architecture. - result = subprocess.run(["gcc", "-o", "dist/linux/main", f"{dist / "main.c"}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stderr = result.stderr + result = subprocess.Popen(["gcc", "-o", dist + "/main", dist + "/main.c"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = result.communicate() except Exception as e: stderr = e # If it exists, copy it. If it doesn't, just tell the user that it couldn't make an executable. - if os.path.exists(dist / "main"): + if os.path.exists(dist + "/main"): log("Copying executable...") - shutil.copy(dist / "main", result_dir / "ProperTree") + shutil.copy(dist + "/main", result_dir + "/ProperTree") else: print("WARNING: An ELF executable was not created. Please note that in order to create an ELF executable, gcc must be installed. (For more info, run with --verbose)") - log(f"gcc error:\n{stderr}") + log("gcc error:\n{}".format(stderr)) # Done! -print(f"Done! Results are in: {result_dir}") +print("Done! Results are in: {}".format(result_dir)) exit(0) \ No newline at end of file diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c index 38657cd..931bb7c 100644 --- a/Scripts/linux-app.c +++ b/Scripts/linux-app.c @@ -5,6 +5,7 @@ #include #include #include +#include const unsigned char shell_script[] = {}; // Placeholder, as raw bytes will be written here by buildapp-linux.py const size_t shell_script_len = sizeof(shell_script); From de1ffbc59a90aa032bc2ed3f5248ba522fe8e5c8 Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:54:45 -0500 Subject: [PATCH 20/27] Update script shebang and script comments --- Scripts/buildapp-linux.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 2a5f58e..2b9bbcf 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64 Debian-based distros, but any architecture/distro is theoretically supported. +# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64, but any architecture/distro is theoretically supported. # Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--always-overwrite] [--use-existing-payload] # "--verbose" or "-v": Verbose mode. @@ -186,7 +186,7 @@ def is_python(path): # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. # The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are copied back in /home/user/.ProperTree and the temporary directory is deleted. -script = """#!/bin/bash +script = """#!/bin/sh # This is an auto-generated script. # ProperTree V. {} # Run with --clear-data to remove data. @@ -424,4 +424,4 @@ def copy_asset(target): # Done! print("Done! Results are in: {}".format(result_dir)) -exit(0) \ No newline at end of file +exit(0) From 77a71e95a18081661fbab0eaff2c9c61755b714c Mon Sep 17 00:00:00 2001 From: Caleb <150558685+Calebh101@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:48:53 -0500 Subject: [PATCH 21/27] Refine comments in buildapp-linux.py --- Scripts/buildapp-linux.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 2b9bbcf..3d08da4 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,5 +1,6 @@ -#!/usr/bin/python3 -# A Python script to compile ProperTree to run as a native Linux app. Officially supports x64, but any architecture/distro is theoretically supported. +# A Python script to compile ProperTree to run as a native Linux app. +# Officially supports x64, but any architecture/distro is theoretically supported. +# ProperTree by CorpNewt, this script by Calebh101. # Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--always-overwrite] [--use-existing-payload] # "--verbose" or "-v": Verbose mode. From 07335957da0da4789d36f7d8ba92356b27d04518 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 08:00:01 -0500 Subject: [PATCH 22/27] Update compatibility and documentation in buildapp-linux --- Scripts/buildapp-linux.py | 62 +++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 3d08da4..681d0c5 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -3,14 +3,14 @@ # ProperTree by CorpNewt, this script by Calebh101. # Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--always-overwrite] [--use-existing-payload] - # "--verbose" or "-v": Verbose mode. - # "--python PYTHON" or "-p PYTHON": Select a Python executable to use (default is output of "which python3"). - # "--clear" or "-c": Clear /dist/linux. Does not respect --use-existing-payload. - # "--dir DIR" or "-d DIR": Select the root directory of ProperTree to use. - # "--out DIR" or "-o DIR": Select an output directory to use. All files will be placed in here; it will not make an extra subdirectory. - # "--always-overwrite": Always overwrite applicable files instead of prompting. - # "--use-existing-payload": Don't overwrite /dist/linux/payload. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. - # "--skip-compile": Skip compiling the script to an ELF executable. + # '--verbose' or '-v': Verbose mode. + # '--python PYTHON' or '-p PYTHON': Select a Python executable to use (default is output of 'which python3'). + # '--clear' or '-c': Clear /dist/linux. Does not respect --use-existing-payload. + # '--dir DIR' or '-d DIR': Select the root directory of ProperTree to use. + # '--out DIR' or '-o DIR': Select an output directory to use. All files will be placed in here; it will not make an extra subdirectory. + # '--always-overwrite': Always overwrite applicable files instead of prompting. + # '--use-existing-payload': Don't overwrite /dist/linux/payload. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. + # '--skip-compile': Skip compiling the script to an ELF executable. # Generated Directories - The script will build in /dist/linux. # payload: This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app-$ID. @@ -22,8 +22,8 @@ # ProperTree-Installer-V.sh: Installs ProperTree as an application. # The Scripts - # ProperTree.sh: Runs ProperTree. Please note that it can be run with "--clear-data" to clear ProperTree data. - # ProperTree-Installer-V.sh: Installs ProperTree by adding "ProperTree" and "propertree" to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with "--uninstall" to delete these three files. + # ProperTree.sh: Runs ProperTree. Please note that it can be run with '--clear-data' to clear ProperTree data. + # ProperTree-Installer-V.sh: Installs ProperTree by adding 'ProperTree' and 'propertree' to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with '--uninstall' to delete these three files. # The Process # 1. Generate a main script and an install script. @@ -41,7 +41,7 @@ # Known Issues # ARM is not officially supported by this script, but you can compile main.c (in /dist/linux) from source. - # ProperTree does not have an icon in the taskbar. This is due to the fact that when you run the .desktop file from the launcher, it doesn't directly load a GUI; it goes through ProperTree.py, which loads a window by itself - separate from the launcher or the .desktop. + # ProperTree does not have an icon in the taskbar. This is due to the fact that when you run the .desktop file from the launcher, it doesn't directly load a GUI; it goes through the generated script, then ProperTree.py, which loads a window by itself, separate from the launcher or the .desktop. # ProperTree's taskbar window is named "Toplevel". This is on ProperTree.py's side (most likely tkinter's side), and I do not know a fix for this at the moment. import platform @@ -135,7 +135,7 @@ def is_python(path): if not os.path.isfile(path) or not os.access(path, os.X_OK): log("is_python fail: not os.path.isfile({path}) or not os.access({path}, os.X_OK)") return False - + try: result = subprocess.Popen([path, '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = result.communicate() @@ -148,7 +148,7 @@ def is_python(path): except Exception as e: log("is_python fail: exception:\n{}".format(e)) return False - + log("is_python fail: unkown overlow") return False @@ -222,7 +222,7 @@ def is_python(path): # We embed the icon binary so we can copy it over without relying on external files. # BREAKER is now DESTROYER to avoid awk confusion. # It also handles instances where /home/$USER/.local/bin isn't in path (or when it doesn't even exist), which happens on clean installations of Debian-based distros. -install_script = """#!/bin/bash +install_script = """#!/bin/sh # This is an auto-generated script. echo "Preparing..." @@ -262,7 +262,7 @@ def is_python(path): echo "$desktop" > "$HOME/.local/share/applications/ProperTree.desktop" cat << 'EOF' > "$HOME/.local/bin/propertree" -#!/bin/bash +#!/bin/sh # This is an auto-generated script. "$HOME/.local/bin/ProperTree" "$@" EOF @@ -272,12 +272,36 @@ def is_python(path): chmod +x "$HOME/.local/bin/propertree" echo "Refreshing sources..." -update-desktop-database ~/.local/share/applications -source ~/.bashrc +shell_name=$(ps -p $$ -o comm=) + +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database ~/.local/share/applications +else + echo "update-desktop-database not found; skipping..." +fi + +case "$shell_name" in + bash) + + [ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc" + ;; + zsh) + [ -f "$HOME/.zshrc" ] && . "$HOME/.zshrc" + ;; + ksh) + [ -f "$HOME/.kshrc" ] && . "$HOME/.kshrc" + ;; + fish) + fish -c "source $HOME/.config/fish/config.fish" >/dev/null 2>&1 + ;; + *) + echo "Shell '$shell_name' not supported for auto-sourcing; skipping" + ;; +esac if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then echo "WARNING: $HOME/.local/bin is not in PATH. You will not be able to run ProperTree from the command line if it's not in PATH." - echo 'Please add `PATH="$PATH:$HOME/.local/bin"` to PATH in .bashrc or whatever you use.' + echo 'Please add 'PATH="$PATH:$HOME/.local/bin"' to PATH in your environmental variables.' fi echo "Done! Run this script with --uninstall to uninstall the ProperTree application. You can also run ProperTree with --clear-data to clear ProperTree data." @@ -403,7 +427,7 @@ def copy_asset(target): bytes = ["0x{:02X}".format(ord(byte)) for byte in binary] except: # Python 3 bytes = ["0x{:02X}".format(byte) for byte in binary] - + with open(dist + '/main.c', 'w') as file: file.write(ccode.replace('const unsigned char shell_script[] = {};', 'const unsigned char shell_script[] = {{\n {}\n}};'.format(', '.join(bytes)))) From b81d5edd6adeac005c8accc6215176ddbb989c62 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 08:26:56 -0500 Subject: [PATCH 23/27] Shell compatibility in buildapp-linux --- Scripts/buildapp-linux.py | 94 +++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 681d0c5..88d50bf 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -190,10 +190,12 @@ def is_python(path): script = """#!/bin/sh # This is an auto-generated script. # ProperTree V. {} -# Run with --clear-data to remove data. +# Run with '--clear-data' to remove data. + +set -eu for arg in "$@"; do - if [ "$arg" == "--clear-data" ]; then + if [ "$arg" = "--clear-data" ]; then echo "Removing data..." rm -rf "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 rm -rf "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 @@ -202,18 +204,28 @@ def is_python(path): fi done -ID=$RANDOM +rand() {{ + seed=$(expr \\( 1103515245 \\* $seed + 12345 \\) % 2147483648) + echo $seed +}} + +randgen() {{ + seed=$(expr $$ + $(date +%s)) + echo $(( ($(rand) % (99999 - 10000 + 1)) + 10000 )) +}} + +ID=$(randgen) DATA=$(awk '/^BREAKER/ {{print NR + 1; exit 0;}}' "$0") -mkdir "$HOME/.ProperTree" > /dev/null 2>&1 -mkdir "/tmp/.ProperTree" > /dev/null 2>&1 -mkdir "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 + +mkdir -p "$HOME/.ProperTree" > /dev/null 2>&1 +mkdir -p "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 tail -n+$DATA "$0" | tar xz -C "/tmp/.ProperTree/app-$ID" -cp "$HOME/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 -cp "$HOME/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 +cp "$HOME/.ProperTree/settings.json" "/tmp/.ProperTree/app-$ID/Scripts/settings.json" > /dev/null 2>&1 || true +cp "$HOME/.ProperTree/Configuration.tex" "/tmp/.ProperTree/app-$ID/Configuration.tex" > /dev/null 2>&1 || true "{}" "/tmp/.ProperTree/app-$ID/ProperTree.py" "$@" -cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 -cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 -rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 +cp "/tmp/.ProperTree/app-$ID/Scripts/settings.json" "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 || true +cp "/tmp/.ProperTree/app-$ID/Configuration.tex" "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 || true +rm -rf "/tmp/.ProperTree/app-$ID" > /dev/null 2>&1 || true exit 0 BREAKER """.format(version, python) @@ -221,19 +233,20 @@ def is_python(path): # Generate the install script. Also includes an uninstall option. # We embed the icon binary so we can copy it over without relying on external files. # BREAKER is now DESTROYER to avoid awk confusion. -# It also handles instances where /home/$USER/.local/bin isn't in path (or when it doesn't even exist), which happens on clean installations of Debian-based distros. +# It also handles instances where /home/$USER/.local/bin isn't in path (or when it doesn't even exist). install_script = """#!/bin/sh # This is an auto-generated script. +set -eu echo "Preparing..." -rm "$HOME/.local/bin/ProperTree" > /dev/null 2>&1 -rm "$HOME/.local/bin/propertree" > /dev/null 2>&1 -rm "$HOME/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 +rm "$HOME/.local/bin/ProperTree" > /dev/null 2>&1 || true +rm "$HOME/.local/bin/propertree" > /dev/null 2>&1 || true +rm "$HOME/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 || true for arg in "$@"; do - if [ "$arg" == "--uninstall" ]; then + if [ "$arg" = "--uninstall" ]; then echo "Uninstalling..." - rm "$HOME/.ProperTree/icon.png" > /dev/null 2>&1 + rm "$HOME/.ProperTree/icon.png" > /dev/null 2>&1 || true echo "Done! ProperTree uninstalled. Your data was not affected." exit 0 fi @@ -249,9 +262,8 @@ def is_python(path): Categories=Utility MimeType=text/xml;" -mkdir "$HOME/.ProperTree" > /dev/null 2>&1 -mkdir "$HOME/.local" > /dev/null 2>&1 -mkdir "$HOME/.local/bin" > /dev/null 2>&1 +mkdir -p "$HOME/.ProperTree" > /dev/null 2>&1 +mkdir -p "$HOME/.local/bin" > /dev/null 2>&1 printf '{}' > "$HOME/.ProperTree/icon.png" echo "Extracting payload..." @@ -276,40 +288,26 @@ def is_python(path): if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database ~/.local/share/applications + echo "Updated the desktop database. Please source your shell file." else echo "update-desktop-database not found; skipping..." fi -case "$shell_name" in - bash) - - [ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc" - ;; - zsh) - [ -f "$HOME/.zshrc" ] && . "$HOME/.zshrc" - ;; - ksh) - [ -f "$HOME/.kshrc" ] && . "$HOME/.kshrc" - ;; - fish) - fish -c "source $HOME/.config/fish/config.fish" >/dev/null 2>&1 - ;; - *) - echo "Shell '$shell_name' not supported for auto-sourcing; skipping" - ;; -esac - -if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then +case ":$PATH:" in + *":$HOME/.local/bin:"*) + ;; + *) echo "WARNING: $HOME/.local/bin is not in PATH. You will not be able to run ProperTree from the command line if it's not in PATH." - echo 'Please add 'PATH="$PATH:$HOME/.local/bin"' to PATH in your environmental variables.' -fi + echo 'Please add '\''PATH="$PATH:$HOME/.local/bin"'\'" to your environmental variables." + ;; +esac echo "Done! Run this script with --uninstall to uninstall the ProperTree application. You can also run ProperTree with --clear-data to clear ProperTree data." exit 0 DESTROYER """.format(icon) -# We're gonna put our settings into a persistent user-specific directory, since otherwise ProperTree would generate it in a temporary directory, which we don't want as it just gets deleted. +# We're gonna put our settings into a persistent user-specific directory, since otherwise ProperTree would generate it in the temporary directory, which we don't want as it just gets deleted. if not os.path.exists(settings): os.makedirs(settings) @@ -320,7 +318,7 @@ def is_python(path): # Load linux-app.c's code so we can embed main.sh into it. with open(scripts + "/linux-app.c", 'r') as file: - ccode = file.read() # This isn't a typo; "code" was already taken + ccode = file.read() # This isn't a typo; 'code' was already taken def copy_settings_json(): print("Copying settings.json...") @@ -331,7 +329,7 @@ def copy_settings_json(): # If the file already exists, then ask the user if they want to overwrite it. if os.path.exists(settings + "/settings.json") and not args.always_overwrite: while True: - message = "Do you want to overwrite {}? (y/n/c): >> ".format(settings + "/settings.json") + message = "Do you want to overwrite {}? (y/n/cancel): >> ".format(settings + "/settings.json") try: # Python 2 response = raw_input(message).strip().lower() except: # Python 3 @@ -341,11 +339,11 @@ def copy_settings_json(): break elif response == 'n': break - elif response == 'c': # Quit - print("Done!") + elif response == 'c' or response == 'cancel': # Cancel + print("Cancelled") exit(0) else: - print("Invalid input. Please enter 'y' or 'n'.") + print("Invalid input.") else: copy_settings_json() From 8123e5b397ce866fdc56e72f5c5aea04a2f8e8e5 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 08:34:32 -0500 Subject: [PATCH 24/27] Executable updates to buildapp-linux --- Scripts/linux-app.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Scripts/linux-app.c b/Scripts/linux-app.c index 931bb7c..e84b506 100644 --- a/Scripts/linux-app.c +++ b/Scripts/linux-app.c @@ -13,7 +13,7 @@ const size_t shell_script_len = sizeof(shell_script); int main(int argc, char *argv[]) { DIR *entry = opendir("/tmp/.ProperTree"); if (entry == NULL) { - if (mkdir("/tmp/.ProperTree", 0777) != 0) { // Make /tmp/.ProperTree if it doesn't exist + if (mkdir("/tmp/.ProperTree", 0700) != 0) { // Make /tmp/.ProperTree if it doesn't exist perror("mkdir"); } } @@ -33,14 +33,25 @@ int main(int argc, char *argv[]) { return 1; } - close(fd); + int fl = fcntl(fd, F_GETFD); + if (fl != -1) fcntl(fd, F_SETFD, fl | FD_CLOEXEC); - if (chmod(filename, 0700) == -1) { + if (fsync(fd) != 0) { + perror("fsync"); + } + + if (chmod(filename, S_IRWXU) == -1) { perror("chmod"); unlink(filename); return 1; } + if (close(fd) != 0) { + perror("close"); + unlink(filename); + return 1; + } + char **exec_args = malloc(sizeof(char *) * (argc + 1)); if (!exec_args) { perror("malloc"); From df1cb585d9699a77342b1f690ceaecf7bf302dfb Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 13:04:40 -0500 Subject: [PATCH 25/27] Formatting stuff in buildapp Linux --- Scripts/buildapp-linux.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 88d50bf..56aaec9 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -195,13 +195,13 @@ def is_python(path): set -eu for arg in "$@"; do - if [ "$arg" = "--clear-data" ]; then - echo "Removing data..." - rm -rf "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 - rm -rf "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 - echo "Done! ProperTree data has been cleared." - exit 0 - fi + if [ "$arg" = "--clear-data" ]; then + echo "Removing data..." + rm -rf "$HOME/.ProperTree/settings.json" > /dev/null 2>&1 + rm -rf "$HOME/.ProperTree/Configuration.tex" > /dev/null 2>&1 + echo "Done! ProperTree data has been cleared." + exit 0 + fi done rand() {{ @@ -244,12 +244,12 @@ def is_python(path): rm "$HOME/.local/share/applications/ProperTree.desktop" > /dev/null 2>&1 || true for arg in "$@"; do - if [ "$arg" = "--uninstall" ]; then - echo "Uninstalling..." - rm "$HOME/.ProperTree/icon.png" > /dev/null 2>&1 || true - echo "Done! ProperTree uninstalled. Your data was not affected." - exit 0 - fi + if [ "$arg" = "--uninstall" ]; then + echo "Uninstalling..." + rm "$HOME/.ProperTree/icon.png" > /dev/null 2>&1 || true + echo "Done! ProperTree uninstalled. Your data was not affected." + exit 0 + fi done desktop="[Desktop Entry] @@ -294,12 +294,12 @@ def is_python(path): fi case ":$PATH:" in - *":$HOME/.local/bin:"*) - ;; - *) - echo "WARNING: $HOME/.local/bin is not in PATH. You will not be able to run ProperTree from the command line if it's not in PATH." - echo 'Please add '\''PATH="$PATH:$HOME/.local/bin"'\'" to your environmental variables." - ;; + *":$HOME/.local/bin:"*) + ;; + *) + echo "WARNING: $HOME/.local/bin is not in PATH. You will not be able to run ProperTree from the command line if it's not in PATH." + echo 'Please add '\''PATH="$PATH:$HOME/.local/bin"'\'" to your environmental variables." + ;; esac echo "Done! Run this script with --uninstall to uninstall the ProperTree application. You can also run ProperTree with --clear-data to clear ProperTree data." From 7972223b8901b8446ceead6ba15456be4d5e8f76 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 16:12:26 -0500 Subject: [PATCH 26/27] Documentation, new argument, and some more updates in buildapp-linux --- Scripts/buildapp-linux.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 56aaec9..9c10f2d 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -1,29 +1,30 @@ -# A Python script to compile ProperTree to run as a native Linux app. +# A Python script to compile ProperTree to run as a 'native' Linux app. # Officially supports x64, but any architecture/distro is theoretically supported. # ProperTree by CorpNewt, this script by Calebh101. -# Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--always-overwrite] [--use-existing-payload] +# Usage: python3 buildapp-linux.py [--verbose] [--python PYTHON] [--clear] [--dir DIR] [--out DIR] [--always-overwrite] [--use-existing-payload] [--skip-compile] # '--verbose' or '-v': Verbose mode. # '--python PYTHON' or '-p PYTHON': Select a Python executable to use (default is output of 'which python3'). - # '--clear' or '-c': Clear /dist/linux. Does not respect --use-existing-payload. + # '--clear' or '-c': Clear /dist/linux. Does not respect '--use-existing-payload'. # '--dir DIR' or '-d DIR': Select the root directory of ProperTree to use. # '--out DIR' or '-o DIR': Select an output directory to use. All files will be placed in here; it will not make an extra subdirectory. # '--always-overwrite': Always overwrite applicable files instead of prompting. + # '--never-overwrite': Never overwrite applicable files instead of prompting. If '--always-overwrite' is present, this argument will not be applied. # '--use-existing-payload': Don't overwrite /dist/linux/payload. This was helpful in early debugging, where I was messing around with different techniques before deciding upon this one. # '--skip-compile': Skip compiling the script to an ELF executable. # Generated Directories - The script will build in /dist/linux. - # payload: This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app-$ID. - # result: This is the directory with the results. It will have a ProperTree.sh (the raw shell file) and (maybe) an ELF executable named ProperTree. + # 'payload': This is where scripts and assets are processed and copied. This is what is extracted when the app is ran. It will extract into /tmp/.ProperTree/app-$ID. + # 'result': This is the directory with the results. It will have a ProperTree.sh (the raw shell file) and (maybe) an ELF executable named ProperTree. # Results - The script will build results in /dist/linux/result. - # ProperTree.sh: The shell script containing ProperTree. This manages all of ProperTree's files and data. - # ProperTree: The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. - # ProperTree-Installer-V.sh: Installs ProperTree as an application. + # 'ProperTree.sh': The shell script containing ProperTree. This manages all of ProperTree's files and data. + # 'ProperTree': The optional ELF executable that can be run as an application instead of as a script. This is only built for x64 systems, but for ARM-based systems, you can build main.c from source. main.c contains all the required data. + # 'ProperTree-Installer-V.sh': Installs ProperTree as an application. # The Scripts - # ProperTree.sh: Runs ProperTree. Please note that it can be run with '--clear-data' to clear ProperTree data. - # ProperTree-Installer-V.sh: Installs ProperTree by adding 'ProperTree' and 'propertree' to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with '--uninstall' to delete these three files. + # 'ProperTree.sh': Runs ProperTree. Please note that it can be run with '--clear-data' to clear ProperTree data. + # 'ProperTree-Installer-V.sh': Installs ProperTree by adding 'ProperTree' and 'propertree' to /home/$USER/.local/bin and adding ProperTree.desktop to /home/$USER/.local/share/applications. Please note that it can be run with '--uninstall' to delete these three files. # The Process # 1. Generate a main script and an install script. @@ -56,7 +57,7 @@ parser = argparse.ArgumentParser( prog='buildapp-linux.py', description='A Python script to compile ProperTree to run as a native Linux app.', - usage='python3 buildapp-linux.py [--verbose] [--dir DIR] [--out DIR] [--clear] [--python PYTHON] [--always-overwrite] [--use-existing-payload]', + usage='python3 buildapp-linux.py [--verbose] [--python PYTHON] [--clear] [--dir DIR] [--out DIR] [--always-overwrite] [--never-overwrite] [--use-existing-payload] [--skip-compile]', ) # Define script arguments. @@ -66,6 +67,7 @@ parser.add_argument('-c', '--clear', help="Clear dist/linux.") parser.add_argument('-o', '--out', help="Specify a directory to use for the output. All files will be placed in here; it will not make an extra subdirectory. Default is dist/linux relative to the root directory of ProperTree.") parser.add_argument('--always-overwrite', action='store_true', help="Always overwrite applicable files instead of prompting.") +parser.add_argument('--never-overwrite', action='store_true', help="Never overwrite applicable files instead of prompting.") parser.add_argument('--use-existing-payload', action='store_true', help="Don't overwrite dist/linux/payload.") parser.add_argument('--skip-compile', action="store_true", help="Skip compiling the script to an executable.") @@ -78,7 +80,7 @@ payload_dir = dist + "/payload" # /dist/linux/payload payload_scripts = payload_dir + "/Scripts" # /dist/linux/payload/Scripts result_dir = dist + "/result" # /dist/linux/result -settings = '/home/' + os.environ.get('USER') + '/.ProperTree' # /home/user/.ProperTree +settings = '/home/' + subprocess.check_output(["id", "-un"], text=True).strip() + '/.ProperTree' # /home/user/.ProperTree if platform.system() != "Linux": print("Can only be run on Linux") @@ -149,7 +151,7 @@ def is_python(path): log("is_python fail: exception:\n{}".format(e)) return False - log("is_python fail: unkown overlow") + log("is_python fail: unknown overflow") return False if args.python: @@ -260,7 +262,7 @@ def is_python(path): Terminal=false Type=Application Categories=Utility -MimeType=text/xml;" +MimeType=text/xml;application/xml;application/x-plist;" mkdir -p "$HOME/.ProperTree" > /dev/null 2>&1 mkdir -p "$HOME/.local/bin" > /dev/null 2>&1 @@ -327,7 +329,7 @@ def copy_settings_json(): # Here, we're gonna transfer settings.json to our new settings directory. if os.path.exists(scripts + "/settings.json"): # If the file already exists, then ask the user if they want to overwrite it. - if os.path.exists(settings + "/settings.json") and not args.always_overwrite: + if os.path.exists(settings + "/settings.json") and not args.always_overwrite and not args.never_overwrite: while True: message = "Do you want to overwrite {}? (y/n/cancel): >> ".format(settings + "/settings.json") try: # Python 2 @@ -344,7 +346,7 @@ def copy_settings_json(): exit(0) else: print("Invalid input.") - else: + elif not args.never_overwrite: copy_settings_json() # This creates /dist, /dist/linux, /dist/linux/payload, /dist/linux/payload/Scripts, /dist/linux/result all in one check. From 96b5cc8f97bc7922e7d0af4e17e024db78871136 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 25 Oct 2025 18:17:01 -0500 Subject: [PATCH 27/27] Fix issue with the icon --- Scripts/buildapp-linux.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Scripts/buildapp-linux.py b/Scripts/buildapp-linux.py index 9c10f2d..60a334c 100755 --- a/Scripts/buildapp-linux.py +++ b/Scripts/buildapp-linux.py @@ -45,6 +45,7 @@ # ProperTree does not have an icon in the taskbar. This is due to the fact that when you run the .desktop file from the launcher, it doesn't directly load a GUI; it goes through the generated script, then ProperTree.py, which loads a window by itself, separate from the launcher or the .desktop. # ProperTree's taskbar window is named "Toplevel". This is on ProperTree.py's side (most likely tkinter's side), and I do not know a fix for this at the moment. +import base64 import platform import subprocess import os @@ -181,11 +182,7 @@ def is_python(path): # Get the icon binary so we can embed it. print("Processing icon...") with open(scripts + "/icon.png", 'rb') as f: - content = f.read() - try: # Python 2 - icon = ''.join('\\x{0:02X}'.format(ord(byte)) for byte in content) - except: # Python 3 - icon = ''.join('\\x{:02X}'.format(byte) for byte in content) + icon = base64.b64encode(f.read()).decode('ascii') # Generate the extraction script. The script extracts the payload to "/tmp/.ProperTree/app-ID". "ID" is a random number between 0 and 32767. # The script works by first ensuring directories exist, then copying settings.json and Configuration.tex (if they exist) to the new temporary directory. After ProperTree runs, then settings.json and Configuration.tex are copied back in /home/user/.ProperTree and the temporary directory is deleted. @@ -266,7 +263,10 @@ def is_python(path): mkdir -p "$HOME/.ProperTree" > /dev/null 2>&1 mkdir -p "$HOME/.local/bin" > /dev/null 2>&1 -printf '{}' > "$HOME/.ProperTree/icon.png" + +base64 -d > "$HOME/.ProperTree/icon.png" << 'EOF' +{} +EOF echo "Extracting payload..." DATA=$(awk '/^DESTROYER/ {{print NR + 1; exit 0; }}' "$0")