mit neuen venv und exe-Files

This commit is contained in:
2024-11-03 17:26:54 +01:00
parent 07c05a338a
commit 0c373ff593
15115 changed files with 1998469 additions and 0 deletions

View File

@@ -0,0 +1 @@
__version__ = "2.44.2"

View File

@@ -0,0 +1,110 @@
import argparse
import logging
import os
import shutil
import tempfile
from . import shims
shims.install_shims()
from . import __version__, config, ui, validation # noqa: E402
def start_ui(logging_level, build_directory_override):
"""Open the interface"""
# Suppress the global logger to only show error+ to the console
logging.getLogger().handlers[0].setLevel(logging_level)
# Setup the build folder
if build_directory_override is None:
config.temporary_directory = tempfile.mkdtemp()
else:
config.temporary_directory = build_directory_override
# Start UI
ui.start(config.ui_open_mode)
# Remove build folder to clean up from builds (if we created it)
if build_directory_override is None:
shutil.rmtree(config.temporary_directory)
def run():
"""Module entry point"""
# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument(
"filename", nargs="?", type=validation.argparse_file_exists, help="pass a file into the interface", default=None
)
parser.add_argument(
"-nc",
"--no-chrome",
action="store_true",
help="do not open in chrome's app mode",
)
parser.add_argument(
"-nu",
"--no-ui",
action="store_true",
help="do not open a browser to show the application and simply print out where it's being hosted from. "
"When using this option, you must manually stop the application using Ctrl+C",
)
parser.add_argument(
"-c",
"--config",
nargs="?",
type=validation.argparse_file_json,
help="provide a json file containing a UI configuration to pre-populate the ui",
default=None,
)
parser.add_argument("-o", "--output-dir", nargs="?", help="the directory to put output in", default="output")
parser.add_argument(
"-bdo",
"--build-directory-override",
nargs="?",
help="a directory for build files (overrides the default)",
default=None,
)
parser.add_argument(
"-lang",
"--language",
nargs="?",
help="hint the language to use by default - language codes can be found in the README",
default=None,
metavar="LANGUAGE_CODE",
)
parser.add_argument(
"--logging-level",
nargs="?",
type=validation.argparse_logging_level,
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="the level to use for logging - defaults to ERROR",
default="ERROR",
)
parser.add_argument("-v", "--version", action="version", version=f"auto-py-to-exe {__version__}")
args = parser.parse_args()
# Setup config from arguments
config.package_filename = args.filename
config.supplied_ui_configuration = args.config
config.default_output_directory = os.path.abspath(args.output_dir)
config.language_hint = args.language
if args.no_ui:
config.ui_open_mode = config.UIOpenMode.NONE
elif args.no_chrome:
config.ui_open_mode = config.UIOpenMode.USER_DEFAULT
else:
config.ui_open_mode = config.UIOpenMode.CHROME
# Validate --build-directory-override exists if supplied
if (args.build_directory_override is not None) and (not os.path.isdir(args.build_directory_override)):
raise ValueError("--build-directory-override must be a directory")
logging_level = getattr(logging, args.logging_level)
start_ui(logging_level, args.build_directory_override)
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,25 @@
import os
import sys
class UIOpenMode:
NONE = 0
CHROME = 1
USER_DEFAULT = 2
# Temporary directory for packaging scripts to speed up consecutive builds. Created on application start.
temporary_directory = None
# Frontend
FRONTEND_ASSET_FOLDER = os.path.join(os.path.dirname(os.path.realpath(__file__)), "web")
# Pre-defined variables by Python
DEFAULT_RECURSION_LIMIT = sys.getrecursionlimit()
# Argument-influenced configuration
package_filename = None
ui_open_mode = UIOpenMode.CHROME
supplied_ui_configuration = None
default_output_directory = os.path.abspath("output")
language_hint = None

View File

@@ -0,0 +1,87 @@
import platform
import sys
from pathlib import Path
try:
from tkinter import Tk
except ImportError:
try:
from Tkinter import Tk
except ImportError:
# If no versions of tkinter exist (most likely linux) provide a message
if sys.version_info.major < 3:
print("Error: Tkinter not found")
print('For linux, you can install Tkinter by executing: "sudo apt-get install python-tk"')
sys.exit(1)
else:
print("Error: tkinter not found")
print('For linux, you can install tkinter by executing: "sudo apt-get install python3-tk"')
sys.exit(1)
try:
from tkinter.filedialog import askdirectory, askopenfilename, askopenfilenames, asksaveasfilename
except ImportError:
from tkFileDialog import askdirectory, askopenfilename, askopenfilenames, asksaveasfilename
root = Tk()
root.withdraw()
if platform.system() == "Windows":
root.iconbitmap(str(Path(__file__).parent / "web/favicon.ico"))
root.wm_attributes("-topmost", 1)
def ask_file(file_type):
"""Ask the user to select a file"""
if (file_type is None) or (platform.system() == "Darwin"):
file_path = askopenfilename()
else:
if file_type == "python":
file_types = [("Python files", "*.py;*.pyw"), ("All files", "*")]
elif file_type == "icon":
file_types = [("Icon files", "*.ico"), ("All files", "*")]
elif file_type == "json":
file_types = [("JSON Files", "*.json"), ("All files", "*")]
else:
file_types = [("All files", "*")]
file_path = askopenfilename(title="Select a file", filetypes=file_types)
root.update()
# bool(file_path) will help filter our the negative cases; an empty string or an empty tuple
return file_path if bool(file_path) else None
def ask_files():
"""Ask the user to select one or more files"""
file_paths = askopenfilenames(title="Select one or more files")
root.update()
return file_paths if bool(file_paths) else None
def ask_folder():
"""Ask the user to select a folder"""
folder = askdirectory(title="Select a folder")
root.update()
return folder if bool(folder) else None
def ask_file_save_location(file_type):
"""Ask the user where to save a file"""
if (file_type is None) or (platform.system() == "Darwin"):
file_path = asksaveasfilename(title="Select where to save")
else:
if file_type == "json":
file_types = [("JSON Files", "*.json"), ("All files", "*")]
else:
file_types = [("All files", "*")]
file_path = asksaveasfilename(title="Select where to save", filetypes=file_types)
root.update()
if bool(file_path):
if file_type == "json":
return file_path if file_path.endswith(".json") else file_path + ".json"
else:
return file_path
else:
return None

View File

@@ -0,0 +1,151 @@
from __future__ import print_function
import argparse
import logging
import os
import shlex
import shutil
import sys
import traceback
from typing import Optional
from PyInstaller.__main__ import run as run_pyinstaller
from . import __version__ as version
from . import config
logger = logging.getLogger(__name__)
def __get_pyinstaller_argument_parser():
from PyInstaller.building.build_main import __add_options as add_build_options
from PyInstaller.building.makespec import __add_options as add_makespec_options
from PyInstaller.log import __add_options as add_log_options
parser = argparse.ArgumentParser()
add_makespec_options(parser)
add_build_options(parser)
add_log_options(parser)
parser.add_argument(
"filenames",
metavar="scriptname",
nargs="+",
help=(
"name of scriptfiles to be processed or "
"exactly one .spec-file. If a .spec-file is "
"specified, most options are unnecessary "
"and are ignored."
),
) # From PyInstaller.__main__.run
return parser
def get_pyinstaller_options():
parser = __get_pyinstaller_argument_parser()
options = []
for action in parser._actions:
# Clean out what we can't send over to the ui
# Here is what we currently have: https://github.com/python/cpython/blob/master/Lib/argparse.py#L771
del action.container
options.append(action)
return [o.__dict__ for o in options]
def will_packaging_overwrite_existing(file_path: str, manual_name: Optional[str], one_file: str, output_folder: str):
"""Checks if there is a possibility of a previous output being overwritten."""
if not os.path.exists(output_folder):
return False
no_extension = manual_name if manual_name is not None else ".".join(os.path.basename(file_path).split(".")[:-1])
if one_file and no_extension + ".exe" in os.listdir(output_folder):
return True
if (not one_file) and no_extension in os.listdir(output_folder):
return True
return False
def __move_package(src, dst):
"""Move the output package to the desired path (default is output/ - set in script.js)"""
# Make sure the destination exists
if not os.path.exists(dst):
os.makedirs(dst)
# Move all files/folders in dist/
for file_or_folder in os.listdir(src):
_dst = os.path.join(dst, file_or_folder)
# If this already exists in the destination, delete it
if os.path.exists(_dst):
if os.path.isfile(_dst):
os.remove(_dst)
else:
shutil.rmtree(_dst)
# Move file
shutil.move(os.path.join(src, file_or_folder), dst)
def package(pyinstaller_command, options):
"""
Call PyInstaller to package a script using provided arguments and options.
:param pyinstaller_command: Command to supply to PyInstaller
:param options: auto-py-to-exe specific options for setup and cleaning up
:return: Whether packaging was successful
"""
# Show current version
logger.info("Running auto-py-to-exe v" + version)
# Notify the user of the workspace and setup building to it
logger.info("Building directory: {}".format(config.temporary_directory))
# Override arguments
dist_path = os.path.join(config.temporary_directory, "application")
build_path = os.path.join(config.temporary_directory, "build")
extra_args = ["--distpath", dist_path] + ["--workpath", build_path] + ["--specpath", config.temporary_directory]
logger.info("Provided command: {}".format(pyinstaller_command))
# Setup options
increase_recursion_limit = options["increaseRecursionLimit"]
output_directory = os.path.abspath(options["outputDirectory"])
if increase_recursion_limit:
sys.setrecursionlimit(5000)
logger.info("Recursion Limit is set to 5000")
else:
sys.setrecursionlimit(config.DEFAULT_RECURSION_LIMIT)
# Run PyInstaller
fail = False
try:
pyinstaller_args = shlex.split(pyinstaller_command) + extra_args
# Display the command we are using and leave a space to separate out PyInstallers logs
logger.info("Executing: {}".format(" ".join(pyinstaller_args)))
logger.info("")
run_pyinstaller(pyinstaller_args[1:])
except: # noqa: E722
fail = True
logger.exception("An error occurred while packaging")
# Move project if there was no failure
logger.info("")
if not fail:
logger.info("Moving project to: {0}".format(output_directory))
try:
__move_package(dist_path, output_directory)
except: # noqa: E722
logger.error("Failed to move project")
logger.exception(traceback.format_exc())
else:
logger.info("Project output will not be moved to output folder")
return False
# Set complete
return True

View File

@@ -0,0 +1,45 @@
import sys
from . import utils
def install_shims():
"""Install shims to fix version incompatibility"""
install_bottle_import_redirect_shim()
def install_bottle_import_redirect_shim():
"""
https://github.com/brentvollebregt/auto-py-to-exe/issues/433 explains that a ModuleNotFoundError is raised when trying
to import bottle extensions using Python 3.12.
This shim will patch this issue with some code that is currently on bottle's main branch.
This shim is only needed on Python versions >=3.12 and bottle versions <=0.12.25 (hoping the next version fixes this issue)
"""
# First check if setting this shim is needed
if sys.version_info < (3, 12):
return
import bottle
if utils.parse_version_tuple(bottle.__version__) > (0, 12, 25):
return
if hasattr(bottle._ImportRedirect, "find_spec"):
return
print(
f"Info: Installing shim for bottle import redirects (using Python={sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} and bottle={bottle.__version__})"
)
# Add the shim
def find_spec(self, fullname, path, target=None):
if "." not in fullname:
return
if fullname.rsplit(".", 1)[0] != self.name:
return
from importlib.util import spec_from_loader
return spec_from_loader(fullname, self)
bottle._ImportRedirect.find_spec = find_spec

View File

@@ -0,0 +1,200 @@
import argparse
import json
import logging
import os
import eel
from eel import chrome
from . import config, dialogs, packaging, utils
LOGGING_HANDLER_NAME = "auto-py-to-exe logging handler"
class UIOpenMode:
NONE = 0
CHROME = 1
USER_DEFAULT = 2
# Setup eels root folder
eel.init(config.FRONTEND_ASSET_FOLDER)
def __setup_logging_ui_forwarding():
"""Setup forwarding of logs by PyInstaller and auto-py-to-exe to the ui"""
pyinstaller_logger = logging.getLogger("PyInstaller")
# Make sure to check if the handler has already been setup so it doesn't get re-added on reload
if not any([i.get_name() == LOGGING_HANDLER_NAME for i in pyinstaller_logger.handlers]):
handler = logging.StreamHandler(utils.ForwardToFunctionStream(send_message_to_ui_output))
handler.set_name(LOGGING_HANDLER_NAME)
handler.setFormatter(logging.Formatter("%(relativeCreated)d %(levelname)s: %(message)s"))
pyinstaller_logger.addHandler(handler)
module_logger = logging.getLogger("auto_py_to_exe")
if not any([i.get_name() == LOGGING_HANDLER_NAME for i in module_logger.handlers]):
handler = logging.StreamHandler(utils.ForwardToFunctionStream(send_message_to_ui_output))
handler.set_name(LOGGING_HANDLER_NAME)
handler.setFormatter(logging.Formatter("%(message)s"))
module_logger.addHandler(handler)
def __get_pyinstaller_options():
options = packaging.get_pyinstaller_options()
# Filter out removed arguments (PyInstaller v6.0.0 removed some arguments but added proper handlers for people still using them - we need to ignore them)
options = [option for option in options if option["help"] != argparse.SUPPRESS]
# In PyInstaller v6.0.0 --hide-console options were not set correctly (like --debug), fix them here
for option in options:
if isinstance(option["choices"], set):
option["choices"] = list(option["choices"])
return options
def __can_use_chrome():
"""Identify if Chrome is available for Eel to use"""
chrome_instance_path = chrome.find_path()
return chrome_instance_path is not None and os.path.exists(chrome_instance_path)
@eel.expose
def initialise():
"""Called by the UI when opened. Used to pass initial values and setup state we couldn't set until now."""
__setup_logging_ui_forwarding()
# Pass initial values to the client
return {
"filename": config.package_filename,
"suppliedUiConfiguration": config.supplied_ui_configuration,
"options": __get_pyinstaller_options(),
"warnings": utils.get_warnings(),
"pathSeparator": os.pathsep,
"defaultOutputFolder": config.default_output_directory,
"languageHint": config.language_hint,
}
@eel.expose
def open_output_in_explorer(output_directory, input_filename, is_one_file):
"""Open a file in the local file explorer"""
utils.open_output_in_explorer(output_directory, input_filename, is_one_file)
@eel.expose
def ask_file(file_type):
"""Ask the user to select a file"""
return dialogs.ask_file(file_type)
@eel.expose
def ask_files():
return dialogs.ask_files()
@eel.expose
def ask_folder():
return dialogs.ask_folder()
@eel.expose
def does_file_exist(file_path):
"""Checks if a file exists"""
return os.path.isfile(file_path)
@eel.expose
def does_folder_exist(path):
"""Checks if a folder exists"""
return os.path.isdir(path)
@eel.expose
def is_file_an_ico(file_path):
"""Checks if a file is an ico file"""
if not os.path.isfile(file_path):
return None
# Open the file and read the first 4 bytes
with open(file_path, "rb") as f:
data = f.read(4)
if data == b"\x00\x00\x01\x00":
return True
else:
return False
@eel.expose
def convert_path_to_absolute(path: str) -> str:
"""Converts a path to an absolute path if it exists. If it doesn't exist, returns the path as is."""
if not os.path.exists(path):
return path
return os.path.abspath(path)
@eel.expose
def import_configuration():
"""Get configuration data from a file"""
file_path = dialogs.ask_file("json")
if file_path is not None:
with open(file_path) as f:
return json.load(f)
else:
return None
@eel.expose
def export_configuration(configuration):
"""Write configuration data to a file"""
file_path = dialogs.ask_file_save_location("json")
if file_path is not None:
with open(file_path, "w") as f:
json.dump(configuration, f, indent=True)
@eel.expose
def will_packaging_overwrite_existing(file_path, manual_name, one_file, output_folder):
"""Checks if there is a possibility of a previous output being overwritten"""
return packaging.will_packaging_overwrite_existing(file_path, manual_name, one_file, output_folder)
@eel.expose
def package(command, non_pyinstaller_options):
"""Package the script provided using the options selected by the user"""
packaging_options = {
"increaseRecursionLimit": non_pyinstaller_options["increaseRecursionLimit"],
"outputDirectory": non_pyinstaller_options["outputDirectory"],
}
packaging_successful = packaging.package(
pyinstaller_command=command,
options=packaging_options,
)
send_message_to_ui_output("Complete.\n")
eel.signalPackagingComplete(packaging_successful)()
def send_message_to_ui_output(message):
"""Show a message in the ui output"""
eel.putMessageInOutput(message)()
def start(open_mode):
"""Start the UI using Eel"""
try:
chrome_available = __can_use_chrome()
if open_mode == UIOpenMode.CHROME and chrome_available:
eel.start("index.html", size=(650, 672), port=0)
elif open_mode == UIOpenMode.USER_DEFAULT or (open_mode == UIOpenMode.CHROME and not chrome_available):
eel.start("index.html", size=(650, 672), port=0, mode="user default")
else:
port = utils.get_port()
print("Server starting at http://localhost:" + str(port) + "/index.html")
eel.start(
"index.html", size=(650, 672), host="localhost", port=port, mode=None, close_callback=lambda x, y: None
)
except (SystemExit, KeyboardInterrupt):
pass # This is what the bottle server raises

View File

@@ -0,0 +1,193 @@
from __future__ import print_function
import io
import os
import platform
import socket
import sys
from pathlib import Path
import requests
from PyInstaller import __version__ as pyinstaller_version_string
from . import __version__
class ForwardToFunctionStream(io.TextIOBase):
def __init__(self, output_function=print):
self.output_function = output_function
def write(self, string):
self.output_function(string)
return len(string)
def open_output_in_explorer(output_directory, input_filename, is_one_file):
"""Open the output in the local file explorer"""
folder_directory = os.path.abspath(output_directory)
if platform.system() == "Windows":
# For windows, we can highlight the file in the explorer window
target_base_to_open = Path(input_filename).stem + (".exe" if is_one_file else "")
target_path_to_open = Path(folder_directory) / target_base_to_open
if target_path_to_open.exists():
os.popen(f'explorer /select,"{target_path_to_open}"')
else:
# If the file doesn't exist, just open the folder)
os.startfile(folder_directory, "explore")
elif platform.system() == "Linux":
os.system('xdg-open "' + folder_directory + '"')
elif platform.system() == "Darwin":
os.system('open "' + folder_directory + '"')
else:
return False
return True
class PackageVersion:
def __init__(self, version_string, url) -> None:
self.version_string = version_string
self.version = parse_version_tuple(version_string)
self.url = url
def __get_latest_version_for_library(library_repo):
try:
response = requests.get(f"https://api.github.com/repos/{library_repo}/releases/latest")
response.raise_for_status()
response_data = response.json()
latest_release_tag_name = response_data["tag_name"].strip("v")
return PackageVersion(latest_release_tag_name.strip("v"), response_data["html_url"])
except requests.exceptions.RequestException:
return None
def __get_latest_auto_py_to_exe_version():
"""Get the latest version of auto-py-to-exe"""
return __get_latest_version_for_library("brentvollebregt/auto-py-to-exe")
def __get_latest_pyinstaller_version():
"""Get the latest version of PyInstaller"""
return __get_latest_version_for_library("pyinstaller/pyinstaller")
def get_warnings():
warnings = []
# Check auto-py-to-exe version is it latest
try:
current_auto_py_to_exe_version = parse_version_tuple(__version__)
latest_auto_py_to_exe_version = __get_latest_auto_py_to_exe_version()
if latest_auto_py_to_exe_version is None:
raise Exception("Unable to check for the latest version of auto-py-to-exe.")
elif latest_auto_py_to_exe_version.version > current_auto_py_to_exe_version:
message = f'<a href="{latest_auto_py_to_exe_version.url}" target="_blank">A new version of auto-py-to-exe has been released</a>: {__version__}{latest_auto_py_to_exe_version.version_string}'
message += "\nUpgrade using: python -m pip install auto-py-to-exe --upgrade"
warnings.append(message)
except Exception as e:
message = f"\nWarning: {e}"
warnings.append(message)
# Check PyInstaller version is it latest
try:
current_pyinstaller_version = parse_version_tuple(pyinstaller_version_string)
latest_pyinstaller_version = __get_latest_pyinstaller_version()
if latest_pyinstaller_version is None:
raise Exception("Unable to check for the latest version of PyInstaller.")
elif latest_pyinstaller_version.version > current_pyinstaller_version:
message = f'<a href="{latest_pyinstaller_version.url}" target="_blank">A new version of PyInstaller has been released</a>: {pyinstaller_version_string}{latest_pyinstaller_version.version_string}'
message += "\nUpgrade using: python -m pip install pyinstaller --upgrade"
warnings.append(message)
except VersionParseError:
pass # Don't warn about a new version when using a non-official release
except Exception as e:
message = f"\nWarning: {e}"
warnings.append(message)
try:
pyinstaller_version = parse_version_tuple(pyinstaller_version_string)
except ValueError:
message = "Unable to parse PyInstaller version - this may be because you aren't using an official release."
message += "\nYou are currently using PyInstaller {pyinstaller_version}.".format(
pyinstaller_version=pyinstaller_version_string
)
message += "\nIf this is an official release, please report this issue on GitHub."
warnings.append(message)
return warnings
# Make sure PyInstaller 3.4 or above is being used with Python 3.7
try:
if sys.version_info >= (3, 7) and pyinstaller_version < (3, 4):
message = "You will need PyInstaller 3.4 or above to use this tool with Python 3.7."
message += "\nYou are currently using PyInstaller {pyinstaller_version}.".format(
pyinstaller_version=pyinstaller_version_string
)
message += "\nPlease upgrade PyInstaller: python -m pip install pyinstaller --upgrade"
warnings.append(message)
except ValueError:
pass # Dev branches will have pyinstaller_version as a string in the form X.Y.devZ+HASH. Ignore it if this is the case.
# Make sure PyInstaller 4.0 or above is being used with Python 3.8 and 3.9
try:
if (
sys.version_info.major == 3
and (sys.version_info.minor == 8 or sys.version_info.minor == 9)
and pyinstaller_version < (4, 1)
):
message = "PyInstaller 4.0 and below does not officially support Python 3.8 and 3.9."
message += "\nYou are currently using PyInstaller {pyinstaller_version}.".format(
pyinstaller_version=pyinstaller_version_string
)
message += "\nIt is highly recommended to update your version of PyInstaller using: python -m pip install pyinstaller --upgrade"
warnings.append(message)
except ValueError:
pass # Dev branches will have pyinstaller_version as a string in the form X.Y.devZ+HASH. Ignore it if this is the case.
# Make sure PyInstaller 4.6 or above is being used with Python 3.10
try:
if sys.version_info.major == 3 and sys.version_info.minor == 10 and pyinstaller_version < (4, 6):
message = "You will need PyInstaller 4.6 or above to use this tool with Python 3.10."
message += "\nYou are currently using PyInstaller {pyinstaller_version}.".format(
pyinstaller_version=pyinstaller_version_string
)
message += "\nPlease upgrade PyInstaller: python -m pip install pyinstaller --upgrade"
warnings.append(message)
except ValueError:
pass # Dev branches will have pyinstaller_version as a string in the form X.Y.devZ+HASH. Ignore it if this is the case.
# If Python 3.10.0 is being used, we are probably going to see `IndexError: tuple index out of range`.
if sys.version_info.major == 3 and sys.version_info.minor == 10 and sys.version_info.micro == 0:
message = 'You are using Python 3.10.0. <a href="https://github.com/brentvollebregt/auto-py-to-exe/issues/215" target="_blank">This version of Python has a bug that causes PyInstaller to fail.</a>'
message += "\nPlease upgrade to Python 3.10.1 or above."
warnings.append(message)
# Make sure we are not using Python from the Windows Store
if r"Packages\PythonSoftwareFoundation.Python." in sys.executable:
message = "It looks like you may be using Python from the Windows Store, the Python binary you are currently using is at: "
message += '"' + sys.executable + '"'
message += "\n\nPython from the Windows Store is not supported by PyInstaller so you may get errors referencing \"win32ctypes.pywin32.pywintypes.error: (1920, 'LoadLibraryEx', 'The file cannot be accessed by the system'\"."
message += '\n<a href="https://github.com/brentvollebregt/auto-py-to-exe/issues/166#issuecomment-827492005" target="_blank">To fix this, use a distribution of Python from python.org.</a>'
warnings.append(message)
return warnings
def get_port():
"""Get an available port by starting a new server, stopping and and returning the port"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("localhost", 0))
port = sock.getsockname()[1]
sock.close()
return port
class VersionParseError(Exception):
pass
def parse_version_tuple(version_string):
"""Turn a version string into a tuple of integers e.g. "1.2.3" -> (1, 2, 3)"""
try:
return tuple(map(int, (version_string.split("."))))
except ValueError:
raise VersionParseError()

View File

@@ -0,0 +1,35 @@
import argparse
import json
import logging
import os
def argparse_file_exists(file_path):
"""Validates whether a file exists."""
if not os.path.isfile(file_path):
raise argparse.ArgumentTypeError("File does not exist")
return os.path.abspath(file_path)
def argparse_file_json(file_path):
"""Validates whether a file contains JSON parsable by Python. Raises argparse.ArgumentTypeError if not."""
if not os.path.isfile(file_path):
raise argparse.ArgumentTypeError("Provided configuration file does not exist")
try:
with open(file_path, "r") as file:
data = json.load(file)
except json.decoder.JSONDecodeError:
raise argparse.ArgumentTypeError("Provided configuration file content is not json")
except Exception as e:
raise argparse.ArgumentTypeError("Cannot parse provided configuration file:\n" + str(e))
return data
def argparse_logging_level(level):
"""Validates that a string value is a valid logging level and returns the corresponding value."""
if hasattr(logging, level.upper()):
return level.upper()
raise argparse.ArgumentTypeError("Invalid logging level: " + str(level))

View File

@@ -0,0 +1,184 @@
@font-face {
font-family: 'Nunito';
src: url('../Nunito-Light.ttf');
}
@font-face {
font-family: 'Vazirmatn';
src: url('../Vazirmatn-Light.ttf');
font-style: normal;
font-weight: 400;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
}
:root {
--background: #fbfbfb;
--primary: #458bc6;
--primary-darker: #1c79c7;
--primary-transparent: #1c79c71a;
--error: red;
--unselected: lightgrey;
--disabled: #808080;
--text: #000000;
--title: #666666;
--warning-background: #fff3cd;
--warning-border: #a0987c;
--warning-text: #000000;
--border-radius: 4px;
}
.dark-theme {
--background: #15131e;
--unselected: #5f5f5f;
--text: #ffffff;
--title: #e2e2e2;
--warning-background: #ffdc6d;
}
* {
box-sizing: border-box;
color: var(--text);
}
body {
background-color: var(--background);
font-family: 'Vazirmatn', 'Nunito', Helvetica, Arial, sans-serif;
font-weight: 100;
margin: 0 18px 10px 18px;
}
.mid {
/* Global center alignment */
margin: auto;
max-width: 800px;
}
/* Headers */
h2 {
font-weight: normal;
font-size: 25px;
margin: 10px 0 2px 0;
}
h3 {
font-size: 17px;
margin: 10px 0 4px 0;
}
.sub_header {
/* Headers in tabs */
font-size: 17px;
margin: 10px 2px 4px 2px;
}
h2 > small {
font-size: 13px;
}
/* Generic inputs */
button,
input,
select {
border: 1px solid var(--primary);
border-radius: var(--border-radius);
padding: 4px;
font-family: 'Vazirmatn', 'Nunito', Helvetica, Arial, sans-serif;
font-weight: 100;
}
input,
select,
textarea,
select option {
background-color: var(--background);
}
input:focus {
outline: none; /* Don't show outline so you can see the colour change */
}
button {
cursor: pointer;
border-radius: var(--border-radius);
background: transparent;
padding: 3px 8px;
transition: border 0.3s, background 0.3s;
border-style: solid;
border-width: 2px;
}
button:not(.selected):not(.unselected):hover {
/* Apply hovers to non-state buttons */
background: var(--primary-transparent);
}
button.selected,
button.unselected:hover {
border-color: var(--primary);
}
button.unselected {
border-color: var(--unselected);
color: var(--unselected);
}
button.large {
border-width: 3px;
padding: 8px;
}
/* Info icon */
.info_icon {
/* Information icon */
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAq0lEQVR4XnWQywrDIBBF82P+WPtXebiSbJKtq0jAtVAEwW0WIZ16p04aWjpwcbge5tU0Nfq+V13XteV9QDVX8i/Qrei5LAtt28ZCXuADf9dKxzzPhEgpsRDwKqwatCiiaZrIe09aazLGMOicAwi1qIh5xGAw50z7vtM4juyB+QFDCFzNWnt6DEpr0bquPMIwDKcnrXkZMWOMrAv0XqZufhf4C/ycR6JW/nvwF51vE+IcomqCAAAAAElFTkSuQmCC) -0px -0px
no-repeat;
height: 10px;
width: 10px;
overflow: hidden;
margin-left: 0.25em;
vertical-align: middle;
display: inline-block;
}
/* Small notes */
.note {
font-size: 14px;
font-style: italic;
margin: 8px 0 0 0;
}
/* Filepath-browse layout */
.filepath-browse-layout {
display: grid;
grid-gap: 4px;
grid-template-columns: 1fr 120px;
}
/* Icon-specific */
#icon-invalid-warning {
font-size: 14px;
padding-top: 4px;
}
/* Utils */
.noselect {
/* Don't select tab text */
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none;
}

View File

@@ -0,0 +1,363 @@
/* Header */
#header {
display: grid;
grid-template-columns: auto 1fr;
margin-top: 18px;
}
#header .title {
display: flex;
align-items: center;
}
#header .title img {
height: 41px;
}
#header .title h1 {
font-weight: 100;
font-size: 30px;
color: var(--title);
margin: 0 0 0 10px;
}
#header .title > a {
display: inherit;
text-decoration: none;
}
#header .title > a:hover {
-webkit-mask-image: linear-gradient(-75deg, rgb(0, 0, 0) 30%, rgba(0, 0, 0, 0.5) 50%, rgb(0, 0, 0) 70%);
-webkit-mask-size: 200%;
animation: shine 2s;
animation-fill-mode: forwards;
}
@-webkit-keyframes shine {
from {
-webkit-mask-position: 150%;
}
to {
-webkit-mask-position: -50%;
}
}
#header .extra-links {
display: flex;
flex-direction: column;
text-align: right;
}
#header .extra-links a {
filter: grayscale(1);
transition: filter 0.3s;
text-decoration: none;
}
#header .extra-links a span {
font-size: 15px;
color: var(--primary);
}
#header .extra-links a:hover {
filter: grayscale(0);
}
#header .extra-links a img {
height: 20px;
vertical-align: text-bottom;
}
#header .extra-links a:not(:first-child) img {
margin-top: 4px;
}
#header .ui-config {
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
justify-content: flex-end;
}
#header .ui-config [for='language-selection'] {
line-height: 0;
}
#header .ui-config #language-selection {
padding: 0 6px;
}
#header .ui-config #theme-toggle {
cursor: pointer;
user-select: none;
display: flex;
height: 18px;
}
#language-selection {
max-width: 200px;
}
/* Warnings */
#warnings {
font-size: 13px;
}
#warnings > div {
background: var(--warning-background);
border: 1px solid var(--warning-border);
border-radius: var(--border-radius);
margin: 10px 0;
padding: 8px;
}
#warnings > div > p {
margin: 0;
white-space: pre-wrap;
}
#warnings > div > p,
#warnings > div > p a {
color: var(--warning-text);
}
/* Sections */
div[id*='section'] {
margin-top: 12px;
}
div[id*='section'] .header {
display: flex;
cursor: pointer;
}
div[id*='section'] .header img {
height: 30px;
transition: transform 0.4s;
transform: rotate(180deg);
}
div[id*='section'] .header h2 {
display: inline;
margin: 0 0 0 15px;
}
div[id*='section'] .content {
display: none; /* Hide sections by default */
margin: 5px 10px 0 10px;
}
/* Additional Files */
#datas-add-buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 5px;
}
/* Advanced tab */
.option-container {
/* Option containers */
margin-top: 4px;
}
.option-container > span {
/* Option name */
margin-right: 10px;
}
.option-container.multiple-input > img {
height: 23px;
vertical-align: middle;
cursor: pointer;
}
.option-container.multiple-input > div {
margin-left: 20px;
}
.option-container.multiple-input > div > div,
#datas-list > div {
display: grid;
grid-template-columns: 1fr auto;
grid-gap: 4px;
margin-top: 2px;
}
.option-container.multiple-input > div > div.dual-value,
#datas-list > div {
grid-template-columns: 1fr 1fr auto;
}
.option-container.multiple-input > div > div > img,
#datas-list > div > img {
height: 100%;
padding: 2px 0;
cursor: pointer;
}
.option-container.input {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 5px;
}
.option-container.input.with-browse {
grid-template-columns: auto 1fr auto;
}
/* Current Command */
#current-command textarea {
width: 100%;
}
/* Output */
#output {
display: none; /* Hidden by default */
}
#output.show {
display: block;
}
#output textarea {
width: 100%;
white-space: pre;
overflow-y: hidden;
}
#output textarea.failure {
border-color: var(--error);
}
/* Common issues link formatting */
#common-issue-link {
font-size: 12px;
text-align: center;
margin-bottom: 5px;
display: none; /* Default to not shown */
}
#common-issue-link.show {
display: block;
}
#common-issue-link a {
text-decoration: none;
color: var(--primary);
}
#common-issue-link a:hover {
color: var(--primary-darker);
}
/* Package / Open Output Folder Buttons */
#package-button-wrapper {
display: flex;
justify-content: space-evenly;
}
#package-button,
#open-output-folder-button {
width: 100%;
background-color: var(--primary);
border: 1px solid var(--primary);
font-size: 15px;
color: white;
height: 38px;
padding: 0 30px;
text-align: center;
box-sizing: border-box;
letter-spacing: 0.1rem;
text-transform: uppercase;
white-space: nowrap;
transition: background-color 0.3s;
}
#package-button:hover,
#open-output-folder-button:hover {
background-color: var(--primary-darker);
}
#package-button:disabled {
background-color: var(--disabled);
border-color: var(--disabled);
cursor: not-allowed;
}
#open-output-folder-button {
display: none; /* Default to not shown */
margin-left: 4px;
}
#open-output-folder-button.show {
display: block;
}
/* Loading spinner (from https://projects.lukehaas.me/css-loaders/) */
.loading-spinner-wrapper {
display: flex;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
align-items: center;
justify-content: center;
background: rgb(0 0 0 / 75%);
}
.loading-label {
color: white;
}
.loading-spinner,
.loading-spinner:after {
border-radius: 50%;
width: 8em;
height: 8em;
}
.loading-spinner {
margin: 60px auto;
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(256, 256, 256, 0.2);
border-right: 1.1em solid rgba(256, 256, 256, 0.2);
border-bottom: 1.1em solid rgba(256, 256, 256, 0.2);
border-left: 1.1em solid #ffffff;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.1s infinite linear;
animation: load8 1.1s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,62 @@
:root {
--modal-offset: 0;
--modal-padding: 200px;
--modal-fallback-color: rgba(0, 0, 0, 0.4);
--modal-coverage-area: 100%;
}
.modal-coverage {
position: fixed;
z-index: 1;
padding-top: var(--modal-padding);
left: var(--modal-offset);
top: var(--modal-offset);
width: var(--modal-coverage-area);
height: var(--modal-coverage-area);
overflow: auto;
background-color: var(--modal-fallback-color);
}
.modal-coverage-hidden {
display: none;
}
.modal-content {
background-color: var(--background);
margin: auto;
padding: 16px;
border: 2px solid var(--primary);
border-radius: var(--border-radius);
width: 80%;
max-width: 600px;
}
.close-btn {
color: var(--primary);
float: right;
font-size: 20px;
font-weight: bold;
}
.close-btn:hover,
.close-btn:focus {
color: var(--primary-darker);
text-decoration: none;
cursor: pointer;
}
.modal-section {
padding: 4px;
}
.modal-header h2 {
margin: 0;
}
.modal-footer {
padding-top: 8px;
}
.modal-btn {
margin-right: 4px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
style="color: #458BC6;">
<path fill="currentColor"
d="M232.5 163.5l122.8 122.8c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L224 234.2l-91.7 91.7c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l122.8-122.8c4.7-4.7 12.3-4.7 17 0zM448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-48 346V86c0-3.3-2.7-6-6-6H54c-3.3 0-6 2.7-6 6v340c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z" />
</svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path fill="currentColor" d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
style="color: green;">
<path fill="currentColor"
d="M352 240v32c0 6.6-5.4 12-12 12h-88v88c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-88h-88c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h88v-88c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v88h88c6.6 0 12 5.4 12 12zm96-160v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-48 346V86c0-3.3-2.7-6-6-6H54c-3.3 0-6 2.7-6 6v340c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z" />
</svg>

After

Width:  |  Height:  |  Size: 549 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
style="color: red;">
<path fill="currentColor"
d="M108 284c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h232c6.6 0 12 5.4 12 12v32c0 6.6-5.4 12-12 12H108zM448 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h352c26.5 0 48 21.5 48 48zm-48 346V86c0-3.3-2.7-6-6-6H54c-3.3 0-6 2.7-6 6v340c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z" />
</svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" style="color: white;"><path fill="currentColor" d="M256 160c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zm246.4 80.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.4-94.8c-6.4-12.8-24.6-12.8-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4c-12.8 6.4-12.8 24.6 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.4-33.5 47.3 94.7c6.4 12.8 24.6 12.8 31 0l47.3-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3c13-6.5 13-24.7.2-31.1zm-155.9 106c-49.9 49.9-131.1 49.9-181 0-49.9-49.9-49.9-131.1 0-181 49.9-49.9 131.1-49.9 181 0 49.9 49.9 49.9 131.1 0 181z"/></svg>

After

Width:  |  Height:  |  Size: 722 B

View File

@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html>
<head>
<title>Auto Py To Exe</title>
<script>
// Provided for type checking
window.eel = {
initialise: () => ({
filename: null,
options: [],
suppliedUiConfiguration: {},
warnings: [],
pathSeparator: '',
defaultOutputFolder: '',
languageHint: '',
}),
does_file_exist: (path) => false,
does_folder_exist: (path) => false,
ask_file: (file_type) => '',
ask_files: () => [],
ask_folder: () => '',
is_file_an_ico: (file_path) => null,
convert_path_to_absolute: (path) => '',
open_output_in_explorer: (output_directory, input_filename, is_one_file) => {},
will_packaging_overwrite_existing: (file_path, manual_name, one_file, output_folder) => true,
package: (command, non_pyinstaller_options) => {},
import_configuration: () => {},
export_configuration: (configuration) => {},
};
</script>
<script type="text/javascript" src="/eel.js"></script>
<script type="text/javascript" src="/js/constants.js"></script>
<script type="text/javascript" src="/js/i18n.js"></script>
<script type="text/javascript" src="/js/initialise.js"></script>
<script type="text/javascript" src="/js/configuration.js"></script>
<script type="text/javascript" src="/js/staticEvents.js"></script>
<script type="text/javascript" src="/js/interface.js"></script>
<script type="text/javascript" src="/js/utils.js"></script>
<script type="text/javascript" src="/js/modal.js"></script>
<script type="text/javascript" src="/js/messages.js"></script>
<script type="text/javascript" src="/js/packaging.js"></script>
<script type="text/javascript" src="/js/importExport.js"></script>
<link rel="stylesheet" href="/css/general.css" />
<link rel="stylesheet" href="/css/main.css" />
<link rel="stylesheet" href="/css/modal.css" />
</head>
<body>
<div class="mid">
<div id="header">
<div class="title">
<a href="https://github.com/brentvollebregt/auto-py-to-exe" target="_blank"><img src="/favicon.ico" /></a>
<a href="https://github.com/brentvollebregt/auto-py-to-exe" target="_blank"><h1>Auto Py to Exe</h1></a>
</div>
<div>
<div class="extra-links">
<a href="https://github.com/brentvollebregt/auto-py-to-exe" target="_blank">
<span>GitHub</span>
<img src="https://github.githubassets.com/favicons/favicon.png" alt="GitHub favicon" />
</a>
<a
href="https://nitratine.net/blog/post/issues-when-using-auto-py-to-exe/?utm_source=auto_py_to_exe&utm_medium=application_link&utm_campaign=auto_py_to_exe_help&utm_content=top"
target="_blank"
>
<span data-i18n="ui.links.helpPost">Help Post</span>
<img src="https://nitratine.net/static/img/favicon-384x384.png" alt="Nitratine favicon" />
</a>
</div>
<div class="ui-config">
<label for="language-selection">
<small data-i18n="ui.title.language">Language</small><small>:</small>
</label>
<select id="language-selection"></select>
<span id="theme-toggle">
<img src="img/sun.svg" id="on-dark-theme-button" style="display: none" />
<img src="img/moon.svg" id="on-light-theme-button" style="display: inline" />
</span>
</div>
</div>
</div>
<div id="warnings"></div>
<div>
<h2 data-i18n="ui.title.scriptLocation">Script Location</h2>
<div class="filepath-browse-layout">
<input
id="entry-script"
placeholder="Path to file"
required
data-i18n_placeholder="ui.placeholders.pathToFile"
/>
<button id="entry-script-search" data-i18n="ui.button.browse">Browse</button>
</div>
</div>
<div>
<h2>
<span data-i18n="ui.title.oneFile">Onefile</span>
<small>(--onedir / --onefile)</small>
</h2>
<div>
<button id="one-directory-button" class="large" data-i18n="ui.button.oneDirectory">One Directory</button>
<button id="one-file-button" class="large" data-i18n="ui.button.oneFile">One File</button>
</div>
</div>
<div>
<h2>
<span data-i18n="ui.title.consoleWindow">Console Window</span>
<small>(--console / --windowed)</small>
</h2>
<div>
<button id="console-based-button" class="large" data-i18n="ui.button.consoleBased">Console Based</button>
<button id="window-based-button" class="large" data-i18n="ui.button.windowBased">
Window Based (hide the console)
</button>
</div>
</div>
<div id="section-icon">
<div class="header noselect" onclick="expandSection('icon')">
<img src="img/chevron-square-up.svg" alt="Icon Section Chevron" />
<h2>
<span data-i18n="ui.title.icon">Icon</span>
<small>(--icon)</small>
</h2>
</div>
<div class="content">
<div class="filepath-browse-layout">
<input id="icon-path" placeholder=".ico file" data-i18n_placeholder="ui.placeholders.icoFile" />
<button id="icon-path-search" data-i18n="ui.button.browse">Browse</button>
</div>
<div>
<span id="icon-invalid-warning" style="display: none">
⚠️
<span data-i18n="ui.notes.invalidIcoFormatWarning">Warning: this file is not a valid .ico file</span>
</span>
</div>
</div>
</div>
<div id="section-additional-files">
<div class="header noselect" onclick="expandSection('additional-files')">
<img src="img/chevron-square-up.svg" alt="Additional Files Section Chevron" />
<h2>
<span data-i18n="ui.title.additionalFiles">Additional Files</span>
<small>(--add-data)</small>
</h2>
</div>
<div class="content">
<div id="datas-add-buttons">
<button id="additional-files-add-files-button" data-i18n="ui.button.addFiles">Add Files</button>
<button id="additional-files-add-folder" data-i18n="ui.button.addFolder">Add Folder</button>
<button id="additional-files-add-blank" data-i18n="ui.button.addBlank">Add Blank</button>
</div>
<div id="datas-list"></div>
<p
id="onefileAdditionalFilesNote"
class="note"
style="display: none"
data-i18n="ui.notes.oneFileAdditionalFilesNote"
>
Be careful when using additional files with onefile mode;
<a href="https://stackoverflow.com/a/13790741/" style="text-decoration: none">read this</a>
and update your code to work with PyInstaller.
</p>
<p class="note" data-i18n="ui.notes.rootDirectory">
If you want to put files in the root directory, put a period (.) in the destination.
</p>
</div>
</div>
<div id="section-advanced">
<div class="header noselect" onclick="expandSection('advanced')">
<img src="img/chevron-square-up.svg" alt="Advanced Section Chevron" />
<h2 data-i18n="ui.title.advanced">Advanced</h2>
</div>
<div class="content"></div>
</div>
<div id="section-settings">
<div class="header noselect" onclick="expandSection('settings')">
<img src="img/chevron-square-up.svg" alt="Advanced Section Chevron" />
<h2 data-i18n="ui.title.settings">Settings</h2>
</div>
<div class="content">
<div>
<h3 data-i18n="ui.title.specificOptions">auto-py-to-exe Specific Options</h3>
<div class="option-container input">
<span>
<span data-i18n="ui.title.outputDirectory">Output Directory</span>
<span
title="The directory to put the output in. Will be created if it doesn't exist"
class="info_icon"
data-i18n_title="ui.helpText.outputDirectory"
></span>
</span>
<div class="filepath-browse-layout">
<input
id="output-directory"
placeholder="DIRECTORY"
data-i18n_placeholder="ui.placeholders.directory"
/>
<button id="output-directory-search" data-i18n="ui.button.browse">Browse</button>
</div>
</div>
<div class="option-container switch">
<span>
<span data-i18n="ui.title.increaseRecursionLimit">Increase Recursion Limit</span>
<span
title="Having this enabled will set the recursion limit to 5000 using sys.setrecursionlimit(5000)."
class="info_icon"
data-i18n_title="ui.helpText.increaseRecursionLimit"
></span>
</span>
<button id="recursion-limit-switch" data-i18n="ui.button.enable">Enable</button>
</div>
</div>
<div>
<h3 data-i18n="ui.title.manuallyProvideOptions">Manually Provide Options</h3>
<div class="option-container input">
<span>
<span data-i18n="ui.title.manualArgumentInput">Manual Argument Input</span>
<span
title="Inject raw text into the generated command."
class="info_icon"
data-i18n_title="ui.helpText.manualArgumentInput"
></span>
</span>
<input id="raw-arguments" placeholder="ARGUMENTS" data-i18n_placeholder="ui.placeholders.arguments" />
</div>
</div>
<div>
<h3 data-i18n="ui.title.configuration">Configuration</h3>
<button id="configuration-import" data-i18n="ui.button.importConfig">Import Config From JSON File</button>
<button id="configuration-export" data-i18n="ui.button.exportConfig">Export Config To JSON File</button>
</div>
</div>
</div>
<div id="current-command">
<h2 data-i18n="ui.title.currentCommand">Current Command</h2>
<textarea readonly></textarea>
</div>
<div id="output">
<h2 data-i18n="ui.title.output">Output</h2>
<textarea readonly></textarea>
</div>
<div id="common-issue-link" data-i18n="ui.notes.somethingWrongWithOutput">
Something wrong with your exe? Read
<a
href="https://nitratine.net/blog/post/issues-when-using-auto-py-to-exe/?utm_source=auto_py_to_exe&utm_medium=application_link&utm_campaign=auto_py_to_exe_help&utm_content=bottom"
target="_blank"
>
this post on how to fix common issues
</a>
for possible solutions.
</div>
<div id="package-button-wrapper">
<button id="package-button" data-i18n="ui.button.convert">Convert .py to .exe</button>
<button id="open-output-folder-button" data-i18n="ui.button.openOutputFolder">Open Output Folder</button>
</div>
</div>
<div id="modal-area" class="modal-coverage modal-coverage-hidden"></div>
<div id="spinner-root" class="loading-spinner-wrapper">
<div>
<div class="loading-spinner"></div>
<span class="loading-label">Initializing...</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,107 @@
/*
Handle configuration modifications
*/
const configurationGetters = []; // Each function in this should either return null or [option.dest, value]
const configurationSetters = {}; // dest: fn(value) => void, used to set option values
const configurationCleaners = []; // Each function in this should clear a dest value
// Get option-value pairs [[option, value], ...]
const getCurrentConfiguration = async (skipTransformations = false) => {
const currentConfiguration = [
{
optionDest: 'noconfirm',
value: true,
},
];
// Call all functions to get data
configurationGetters.forEach((getter) => {
const optionValuePair = getter();
if (optionValuePair !== null) {
currentConfiguration.push({
optionDest: optionValuePair[0],
value: optionValuePair[1],
});
}
});
if (skipTransformations) {
return currentConfiguration;
}
// Convert all relative paths to absolute paths
for (const c of currentConfiguration) {
const option = options.find((o) => o.dest === c.optionDest);
if (option === undefined) {
continue;
}
if (
[OPTION_INPUT_VALUE_FILE, OPTION_INPUT_VALUE_DIRECTORY].some((v) => option.allowedInputValues.includes(v)) ||
option.dest === 'filenames'
) {
c.value = await convertPathToAbsolute(c.value);
}
if (
[OPTION_INPUT_VALUE_DOUBLE_FILE_DEST, OPTION_INPUT_VALUE_DOUBLE_DIRECTORY_DEST].some((v) =>
option.allowedInputValues.includes(v)
)
) {
const [src, dest] = c.value.split(pathSeparator);
c.value = `${await convertPathToAbsolute(src)}${pathSeparator}${dest}`;
}
}
return currentConfiguration;
};
const getNonPyinstallerConfiguration = () => {
return {
outputDirectory: document.getElementById('output-directory').value,
increaseRecursionLimit: !document.getElementById('recursion-limit-switch').classList.contains('unselected'),
manualArguments: document.getElementById('raw-arguments').value,
};
};
const getCurrentCommand = async () => {
const currentConfiguration = await getCurrentConfiguration();
// Match configuration values with the correct flags
const optionsAndValues = currentConfiguration
.filter((c) => c.optionDest !== 'filenames')
.map((c) => {
// Identify the options
const option = options.find((o) => o.dest === c.optionDest);
if (option.nargs === 0) {
// For switches, there are some switches for false switches that we can use
const potentialOption = options.find((o) => o.dest === c.optionDest && o.const === c.value);
if (potentialOption !== undefined) {
return chooseOptionString(potentialOption.option_strings);
} else {
return null; // If there is no alternate option, skip it as it won't be required
}
} else {
const optionFlag = chooseOptionString(option.option_strings);
return `${optionFlag} "${c.value}"`;
}
})
.filter((x) => x !== null);
// Identify the entry script provided
const entryScriptConfig = currentConfiguration.find((c) => c.optionDest === 'filenames');
const entryScript = entryScriptConfig === undefined ? '' : entryScriptConfig.value;
return `pyinstaller ${optionsAndValues.join(' ')} ${
getNonPyinstallerConfiguration().manualArguments
} "${entryScript}"`;
};
const updateCurrentCommandDisplay = async () => {
document.querySelector('#current-command textarea').value = await getCurrentCommand();
};
const isCommandDefault = async () => {
return (await getCurrentCommand()) === 'pyinstaller --noconfirm --onedir --console ""';
};

View File

@@ -0,0 +1,84 @@
const options_ignored = ['help'];
const options_static = ['filenames', 'onefile', 'console', 'icon_file', 'datas'];
const options_overridden = ['specpath', 'distpath', 'workpath', 'noconfirm'];
const options_inputTypeFile = [
'runtime_hooks',
'version_file',
'manifest',
'resources',
'splash',
'entitlements_file',
'icon_file',
];
const options_inputTypeDirectory = ['upx_dir', 'pathex', 'hookspath'];
const options_inputTypeDoubleFileDest = ['datas', 'binaries'];
const options_inputTypeDoubleDirectoryDest = ['datas'];
const advancedSections = [
{
titleI18nPath: 'dynamic.title.generalOptions',
options: ['name', 'contents_directory', 'upx_dir', 'clean_build', 'loglevel'],
},
{
titleI18nPath: 'dynamic.title.whatToBundleWhereToSearch',
options: [
'binaries',
'pathex',
'hiddenimports',
'collect_submodules',
'collect_data',
'collect_binaries',
'collect_all',
'copy_metadata',
'recursive_copy_metadata',
'splash',
'hookspath',
'runtime_hooks',
'excludes',
'key',
],
},
{
titleI18nPath: 'dynamic.title.howToGenerate',
options: ['debug', 'optimize', 'python_options', 'strip', 'noupx', 'upx_exclude'],
},
{
titleI18nPath: 'dynamic.title.windowsAndMacOsXSpecificOptions',
options: ['hide_console', 'disable_windowed_traceback'],
},
{
titleI18nPath: 'dynamic.title.windowsSpecificOptions',
options: ['version_file', 'manifest', 'embed_manifest', 'resources', 'uac_admin', 'uac_uiaccess'],
},
{
titleI18nPath: 'dynamic.title.macOsxSpecificOptions',
options: ['argv_emulation', 'bundle_identifier', 'target_arch', 'codesign_identity', 'entitlements_file'],
},
{
titleI18nPath: 'dynamic.title.rarelyUsedSpecialOptions',
options: ['runtime_tmpdir', 'bootloader_ignore_signals'],
},
];
// String constants
OPTION_IGNORED = 'OPTION_IGNORED';
OPTION_STATIC = 'OPTION_STATIC';
OPTION_OVERRIDDEN = 'OPTION_OVERRIDDEN';
OPTION_SHOW = 'OPTION_SHOW';
OPTION_INPUT_TYPE_SWITCH = 'OPTION_INPUT_TYPE_SWITCH';
OPTION_INPUT_TYPE_DROPDOWN = 'OPTION_INPUT_TYPE_DROPDOWN';
OPTION_INPUT_TYPE_INPUT = 'OPTION_INPUT_TYPE_INPUT';
OPTION_INPUT_TYPE_MULTIPLE_INPUT = 'OPTION_INPUT_TYPE_MULTIPLE_INPUT';
OPTION_INPUT_TYPE_DOUBLE_MULTIPLE_INPUT = 'OPTION_INPUT_TYPE_DOUBLE_MULTIPLE_INPUT';
OPTION_INPUT_VALUE_TEXT = 'OPTION_INPUT_VALUE_TEXT';
OPTION_INPUT_VALUE_FILE = 'OPTION_INPUT_VALUE_FILE';
OPTION_INPUT_VALUE_DIRECTORY = 'OPTION_INPUT_VALUE_DIRECTORY';
OPTION_INPUT_VALUE_DOUBLE_FILE_DEST = 'OPTION_INPUT_VALUE_DOUBLE_FILE_DEST';
OPTION_INPUT_VALUE_DOUBLE_DIRECTORY_DEST = 'OPTION_INPUT_VALUE_DOUBLE_DIRECTORY_DEST';
PACKAGING_STATE_READY = 'PACKAGING_STATE_READY';
PACKAGING_STATE_PACKAGING = 'PACKAGING_STATE_PACKAGING';
PACKAGING_STATE_COMPLETE = 'PACKAGING_STATE_COMPLETE';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
const importConfiguration = (configuration) => {
// TODO Check for version to support older versions
// Re-init UI by clearing everything (copy the array first as it will be mutated during the iteration)
[...configurationCleaners].forEach((cleaner) => cleaner());
if ('pyinstallerOptions' in configuration) {
configuration.pyinstallerOptions.forEach(({ optionDest, value }) => {
if (configurationSetters.hasOwnProperty(optionDest)) {
configurationSetters[optionDest](value);
} else {
// TODO Warn user?
// TODO noconfirm is expected to come here
}
});
}
// setup nonPyinstallerOptions
if ('nonPyinstallerOptions' in configuration) {
if ('increaseRecursionLimit' in configuration.nonPyinstallerOptions) {
recursionLimitToggle(configuration.nonPyinstallerOptions.increaseRecursionLimit);
}
if ('manualArguments' in configuration.nonPyinstallerOptions) {
document.getElementById('raw-arguments').value = configuration.nonPyinstallerOptions.manualArguments;
}
if ('outputDirectory' in configuration.nonPyinstallerOptions) {
document.getElementById('output-directory').value = configuration.nonPyinstallerOptions.outputDirectory;
}
}
};
const _collectDataToExport = async () => {
const nonPyinstallerConfiguration = getNonPyinstallerConfiguration();
delete nonPyinstallerConfiguration.outputDirectory; // This does not need to be saved in the config
return {
version: 'auto-py-to-exe-configuration_v1',
pyinstallerOptions: await getCurrentConfiguration(true),
nonPyinstallerOptions: nonPyinstallerConfiguration,
};
};
const onConfigurationImport = async () => {
if (!(await isCommandDefault())) {
const response = await displayModal(
getTranslation('dynamic.modal.configModalTitle'),
getTranslation('dynamic.modal.configModalDescription'),
[
getTranslation('dynamic.modal.configModalConfirmButton'),
getTranslation('dynamic.modal.configModalCancelButton'),
]
);
if (response !== getTranslation('dynamic.modal.configModalConfirmButton')) return;
}
const data = await eel.import_configuration()();
if (data !== null) {
importConfiguration(data);
}
};
const onConfigurationExport = async () => {
const data = await _collectDataToExport();
await eel.export_configuration(data)();
};

View File

@@ -0,0 +1,109 @@
/*
Handle the initialisation of the ui
*/
let options = [];
let pathSeparator = '';
const buildUpOptions = (providedOptions) => {
return providedOptions.map((option) => {
const name = option.dest;
let placement = OPTION_SHOW;
if (options_ignored.indexOf(name) !== -1) {
placement = OPTION_IGNORED;
} else if (options_static.indexOf(name) !== -1) {
placement = OPTION_STATIC;
} else if (options_overridden.indexOf(name) !== -1) {
placement = OPTION_OVERRIDDEN;
}
let inputType = OPTION_INPUT_TYPE_INPUT;
if (option.nargs === 0) {
inputType = OPTION_INPUT_TYPE_SWITCH;
} else if (option.choices !== null) {
inputType = OPTION_INPUT_TYPE_DROPDOWN;
} else if (option.dest === 'datas' || option.dest === 'binaries') {
inputType = OPTION_INPUT_TYPE_DOUBLE_MULTIPLE_INPUT;
} else if (option.default !== null || option.dest === 'upx_exclude') {
inputType = OPTION_INPUT_TYPE_MULTIPLE_INPUT;
}
const allowedInputValues = [];
if (options_inputTypeFile.indexOf(name) !== -1) {
allowedInputValues.push(OPTION_INPUT_VALUE_FILE);
}
if (options_inputTypeDirectory.indexOf(name) !== -1) {
allowedInputValues.push(OPTION_INPUT_VALUE_DIRECTORY);
}
if (options_inputTypeDoubleFileDest.indexOf(name) !== -1) {
allowedInputValues.push(OPTION_INPUT_VALUE_DOUBLE_FILE_DEST);
}
if (options_inputTypeDoubleDirectoryDest.indexOf(name) !== -1) {
allowedInputValues.push(OPTION_INPUT_VALUE_DOUBLE_DIRECTORY_DEST);
}
if (allowedInputValues.length === 0) {
allowedInputValues.push(OPTION_INPUT_VALUE_TEXT);
}
return {
...option,
placement,
inputType,
allowedInputValues,
};
});
};
// Get initialisation data from the server and setup the ui
window.addEventListener('load', async () => {
// Get initialisation data from Python
console.log('Getting initialisation data');
const initialisationData = await eel.initialise()();
console.log('Received initialisation data');
options = buildUpOptions(initialisationData.options);
pathSeparator = initialisationData.pathSeparator;
// Setup user's default color scheme
setupTheme();
// Setup ui events (for static content) and setup initial state
setupEvents();
// Setup language selection
setupLanguageSelection();
// Setup advanced section (for dynamic content)
constructAdvancedSection(options);
// Setup json config file is supplied
if (initialisationData.suppliedUiConfiguration !== null) {
importConfiguration(initialisationData.suppliedUiConfiguration);
}
// Set the output directory to the default if it hasn't already been set by `initialisationData.suppliedUiConfiguration`
if (document.getElementById('output-directory').value === '') {
document.getElementById('output-directory').value = initialisationData.defaultOutputFolder;
}
// If a file is provided, put it in the script location
if (initialisationData.filename !== null) {
configurationSetters['filenames'](initialisationData.filename);
}
// Display any warnings provided
setupWarnings(initialisationData.warnings);
// Update the current command when setup is complete
await updateCurrentCommandDisplay();
// Try to translate to the default browser language
translate(initialisationData.languageHint);
// If the server stops, close the UI
window.eel._websocket.addEventListener('close', (e) => window.close());
console.log('Application initialised');
document.getElementById('spinner-root').style.display = 'none';
});

View File

@@ -0,0 +1,462 @@
/*
Handle visual events
*/
// Expand a section (typically triggered by clicking on a section heading)
const expandSection = (sectionName) => {
const root = document.getElementById(`section-${sectionName}`);
const chevron = root.querySelector('.header img');
const content = root.querySelector(`.content`);
if (root.getAttribute('data-expanded') === null) {
// Show the section
chevron.style.transform = 'rotate(0deg)';
content.style.display = 'block';
root.setAttribute('data-expanded', '');
} else {
// Hide the section
chevron.style.transform = 'rotate(180deg)';
content.style.display = 'none';
root.removeAttribute('data-expanded');
}
};
// Colour an input based on the "allowed" arguments. Returns whether the field is valid or not
const colourInput = async (inputNode, allowedToBeEmpty, allowedToBeFile, allowedToBeADirectory) => {
const { value } = inputNode;
if (
(allowedToBeEmpty && value === '') ||
(!allowedToBeEmpty && value !== '' && !allowedToBeFile && !allowedToBeADirectory) ||
(allowedToBeFile && (await doesFileExist(value))) ||
(allowedToBeADirectory && (await doesFolderExist(value)))
) {
inputNode.style.border = '';
return true;
} else {
inputNode.style.border = '1px solid rgb(244, 67, 54)';
return false;
}
};
const addDoubleInputForSrcDst = (
parentNode,
optionDest,
source,
destination,
sourceCanBeFile,
sourceCanBeDirectory
) => {
// Construct visible inputs
const wrapper = document.createElement('div');
parentNode.appendChild(wrapper);
const sourceInput = document.createElement('input');
wrapper.appendChild(sourceInput);
const destinationInput = document.createElement('input');
wrapper.appendChild(destinationInput);
const removeButton = document.createElement('img');
wrapper.appendChild(removeButton);
wrapper.classList.add('dual-value');
sourceInput.value = source;
sourceInput.addEventListener('input', (event) => {
colourInput(sourceInput, false, sourceCanBeFile, sourceCanBeDirectory);
void updateCurrentCommandDisplay();
});
colourInput(sourceInput, false, sourceCanBeFile, sourceCanBeDirectory);
destinationInput.value = destination;
destinationInput.addEventListener('input', (event) => {
void updateCurrentCommandDisplay();
});
// Add configurationGetter
const configurationGetter = () => [optionDest, `${sourceInput.value}${pathSeparator}${destinationInput.value}`];
configurationGetters.push(configurationGetter);
// Setup removal
const onRemove = () => {
wrapper.remove();
const configurationGetterIndex = configurationGetters.indexOf(configurationGetter);
configurationGetters.splice(configurationGetterIndex, 1);
const configurationCleanerIndex = configurationCleaners.indexOf(onRemove);
configurationCleaners.splice(configurationCleanerIndex, 1);
void updateCurrentCommandDisplay();
};
removeButton.src = 'img/remove.svg';
removeButton.addEventListener('click', onRemove);
configurationCleaners.push(onRemove);
void updateCurrentCommandDisplay();
};
const _createSubSectionInAdvanced = (title, i18nPath, options) => {
const parent = document.querySelector('#section-advanced .content');
// The div wrapping the whole section
const subSectionNode = document.createElement('div');
parent.appendChild(subSectionNode);
// Setup title
const subSectionTitleNode = document.createElement('h3');
subSectionTitleNode.textContent = title;
subSectionTitleNode.classList.add('noselect');
subSectionTitleNode.dataset.i18n = i18nPath;
subSectionNode.appendChild(subSectionTitleNode);
// Setup options
options.forEach((o) => {
// Container for option
const container = document.createElement('div');
subSectionNode.appendChild(container);
container.classList.add('option-container');
// Option title / name
const optionNode = document.createElement('span');
container.appendChild(optionNode);
optionNode.textContent = chooseOptionString(o.option_strings);
// Help icon
const helpNode = document.createElement('span');
optionNode.appendChild(helpNode); // Put the icon inside the option text
helpNode.title = o.help.replace(/R\|/, '');
helpNode.classList.add('info_icon');
if (o.inputType === OPTION_INPUT_TYPE_SWITCH) {
container.classList.add('switch');
// Add button (take note of the target argument state using `const`)
const enableButton = document.createElement('button');
container.appendChild(enableButton);
if (o.const === true) {
enableButton.dataset.i18n = 'dynamic.button.enable';
} else if (o.const === false) {
enableButton.dataset.i18n = 'dynamic.button.disable';
} else {
throw new Error('Unknown o.const value: ' + JSON.stringify(o));
}
enableButton.textContent = getTranslation(enableButton.dataset.i18n);
enableButton.classList.add('unselected');
// Function used to set the value of the switch
const setValue = (enabled) => {
if (enabled) {
enableButton.classList.remove('unselected');
enableButton.classList.add('selected');
} else {
enableButton.classList.add('unselected');
enableButton.classList.remove('selected');
}
void updateCurrentCommandDisplay();
};
// When clicked, toggle the value
enableButton.addEventListener('click', () => {
setValue(!enableButton.classList.contains('selected'));
});
// Add configurationGetter
const configurationGetter = () => [o.dest, !enableButton.classList.contains('unselected')];
configurationGetters.push(configurationGetter);
// Add configurationSetter
configurationSetters[o.dest] = setValue;
// Add configurationCleaner
configurationCleaners.push(() => setValue(false));
// Allow a default value of `true` to come through
if (o.default === true) {
setValue(true);
}
} else if (o.inputType === OPTION_INPUT_TYPE_DROPDOWN) {
container.classList.add('choice');
// Add dropdown
const selectNode = document.createElement('select');
container.appendChild(selectNode);
selectNode.addEventListener('change', (event) => {
void updateCurrentCommandDisplay();
});
// Add options (including default '')
const defaultOptionNode = document.createElement('option');
selectNode.appendChild(defaultOptionNode);
defaultOptionNode.textContent = '';
const choices = Array.isArray(o.choices) ? o.choices : Object.keys(o.choices);
choices.map((choice) => {
const optionNode = document.createElement('option');
selectNode.appendChild(optionNode);
optionNode.textContent = choice;
optionNode.value = choice;
});
// Add configurationGetter
const configurationGetter = () => {
const value = selectNode.value;
return value === '' ? null : [o.dest, value];
};
configurationGetters.push(configurationGetter);
// Add configurationSetter
configurationSetters[o.dest] = (value) => {
if (choices.indexOf(value) !== 1) {
selectNode.value = value;
} else {
selectNode.value = '';
}
selectNode.dispatchEvent(new Event('change'));
};
// Add configurationCleaner
configurationCleaners.push(() => {
selectNode.value = '';
selectNode.dispatchEvent(new Event('change'));
});
} else if (o.inputType === OPTION_INPUT_TYPE_INPUT) {
container.classList.add('input');
const isOptionFileBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_FILE) !== -1;
const isOptionDirectoryBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_DIRECTORY) !== -1;
// Add input node
const inputNode = document.createElement('input');
container.appendChild(inputNode);
inputNode.placeholder = o.metavar || 'VALUE';
inputNode.addEventListener('input', (event) => {
void updateCurrentCommandDisplay();
if (isOptionFileBased || isOptionDirectoryBased) {
colourInput(inputNode, true, isOptionFileBased, isOptionDirectoryBased);
}
});
// Show browse button if required (only file or folder - not both)
if (isOptionFileBased || isOptionDirectoryBased) {
container.classList.add('with-browse');
const searchButton = document.createElement('button');
container.appendChild(searchButton);
searchButton.dataset.i18n = isOptionFileBased
? 'dynamic.button.browseForFile'
: 'dynamic.button.browseForFolder';
searchButton.textContent = getTranslation(searchButton.dataset.i18n);
searchButton.addEventListener('click', async () => {
const value = isOptionFileBased ? await askForFile(null) : await askForFolder();
if (value !== null) {
inputNode.value = value;
inputNode.dispatchEvent(new Event('input'));
}
});
}
// Add configurationGetter
const configurationGetter = () => {
const value = inputNode.value;
return value === '' ? null : [o.dest, value];
};
configurationGetters.push(configurationGetter);
// Add configurationSetter
configurationSetters[o.dest] = (value) => {
inputNode.value = value;
inputNode.dispatchEvent(new Event('input'));
};
// Add configurationCleaner
configurationCleaners.push(() => {
inputNode.value = '';
inputNode.dispatchEvent(new Event('input'));
});
} else if (o.inputType === OPTION_INPUT_TYPE_MULTIPLE_INPUT) {
container.classList.add('multiple-input');
const isOptionFileBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_FILE) !== -1;
const isOptionDirectoryBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_DIRECTORY) !== -1;
// Add button to add an entry
const addButton = document.createElement('img');
container.appendChild(addButton);
addButton.src = 'img/plus.svg';
// Container to hold the values
const valuesContainer = document.createElement('div');
container.appendChild(valuesContainer);
const addValue = (value) => {
// Container to hold the pair
const valueContainer = document.createElement('div');
valuesContainer.appendChild(valueContainer);
// Value input
const inputNode = document.createElement('input');
valueContainer.appendChild(inputNode);
inputNode.value = value;
inputNode.placeholder = o.metavar || 'VALUE';
colourInput(inputNode, false, isOptionFileBased, isOptionDirectoryBased);
inputNode.addEventListener('input', (event) => {
colourInput(inputNode, false, isOptionFileBased, isOptionDirectoryBased);
void updateCurrentCommandDisplay();
});
// Add configurationGetter
const configurationGetter = () => [o.dest, inputNode.value];
configurationGetters.push(configurationGetter);
// Remove button
const removeButtonNode = document.createElement('img');
removeButtonNode.src = 'img/remove.svg';
valueContainer.appendChild(removeButtonNode);
const onRemove = () => {
valueContainer.remove();
const configurationGetterIndex = configurationGetters.indexOf(configurationGetter);
configurationGetters.splice(configurationGetterIndex, 1);
const configurationCleanerIndex = configurationCleaners.indexOf(onRemove);
configurationCleaners.splice(configurationCleanerIndex, 1);
void updateCurrentCommandDisplay();
};
removeButtonNode.addEventListener('click', onRemove);
// Add configurationCleaner
configurationCleaners.push(onRemove);
void updateCurrentCommandDisplay();
};
// Event to add a new input pair
addButton.addEventListener('click', async () => {
// Get initial value
let initialValue = '';
if (isOptionFileBased || isOptionDirectoryBased) {
initialValue = isOptionFileBased ? await askForFile(null) : await askForFolder();
if (initialValue === null) {
return;
}
}
addValue(initialValue);
});
// Add configurationSetter
configurationSetters[o.dest] = (value) => {
addValue(value);
};
} else if (o.inputType === OPTION_INPUT_TYPE_DOUBLE_MULTIPLE_INPUT) {
container.classList.add('multiple-input');
const isOptionFileBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_DOUBLE_FILE_DEST) !== -1;
const isOptionDirectoryBased = o.allowedInputValues.indexOf(OPTION_INPUT_VALUE_DOUBLE_DIRECTORY_DEST) !== -1;
// Add button to add an entry
const addButton = document.createElement('img');
container.appendChild(addButton);
addButton.src = 'img/plus.svg';
// Container to hold the value pairs
const valuesContainer = document.createElement('div');
container.appendChild(valuesContainer);
addButton.addEventListener('click', async () => {
// Get initial value
let initialValue = '';
if (isOptionFileBased || isOptionDirectoryBased) {
initialValue = isOptionFileBased ? await askForFile(null) : await askForFolder();
if (initialValue === null) {
return;
}
}
addDoubleInputForSrcDst(valuesContainer, o.dest, initialValue, '.', true, false);
});
// Add configurationSetter
configurationSetters[o.dest] = (value) => {
const [val1, val2] = value.split(pathSeparator);
addDoubleInputForSrcDst(valuesContainer, o.dest, val1, val2, true, false);
};
}
});
};
const constructAdvancedSection = () => {
// Setup pre-defined sections
advancedSections.forEach((section) =>
_createSubSectionInAdvanced(
getTranslation(section.titleI18nPath),
section.titleI18nPath,
options.filter((o) => section.options.indexOf(o.dest) !== -1)
)
);
// Setup extra arguments
const usedSectionOptions = flatMap(advancedSections.map((s) => s.options));
const extraOptions = options.filter(
(option) =>
usedSectionOptions.indexOf(option.dest) === -1 &&
option.placement !== OPTION_IGNORED &&
option.placement !== OPTION_STATIC &&
option.placement !== OPTION_OVERRIDDEN
);
if (extraOptions.length > 0) {
_createSubSectionInAdvanced(getTranslation('dynamic.title.other'), 'dynamic.title.other', extraOptions);
}
};
const setupWarnings = (warnings) => {
if (warnings.length === 0) {
return;
}
const warningsRootNode = document.getElementById('warnings');
warnings.forEach((warningContent) => {
// Create wrapper
const wrapperNode = document.createElement('div');
warningsRootNode.appendChild(wrapperNode);
// Create message
const messageNode = document.createElement('p');
wrapperNode.appendChild(messageNode);
messageNode.innerHTML = warningContent;
});
};
const setupLanguageSelection = () => {
const languageSelectNode = document.getElementById('language-selection');
languageSelectNode.addEventListener('change', (event) => {
translate(event.target.value);
});
supportedLanguages.forEach((language) => {
const option = document.createElement('option');
option.innerText = language.name;
option.value = language.code;
languageSelectNode.appendChild(option);
});
languageSelectNode.value = currentLanguage;
};
// Toggle theme (triggered by clicking moon or sun)
const _toggleTheme = () => {
const root = document.querySelector('body');
const onDarkThemeButton = document.querySelector('#on-dark-theme-button');
const onLightThemeButton = document.querySelector('#on-light-theme-button');
if (root.classList.contains('dark-theme')) {
onLightThemeButton.style.display = 'inline';
onDarkThemeButton.style.display = 'none';
} else {
// dark
onLightThemeButton.style.display = 'none';
onDarkThemeButton.style.display = 'inline';
}
root.classList.toggle('dark-theme');
};
// Check if user's default color scheme is dark
const setupTheme = () => {
document.getElementById('theme-toggle').addEventListener('click', _toggleTheme);
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
_toggleTheme();
}
};

View File

@@ -0,0 +1,19 @@
eel.expose(putMessageInOutput);
function putMessageInOutput(message) {
const outputNode = document.querySelector('#output textarea');
outputNode.value += message; // Add the message
if (!message.endsWith('\n')) {
outputNode.value += '\n'; // If there was no new line, add one
}
// Set the correct height to fit all the output and then scroll to the bottom
outputNode.style.height = 'auto';
outputNode.style.height = outputNode.scrollHeight + 10 + 'px';
window.scrollTo(0, document.body.scrollHeight);
}
eel.expose(signalPackagingComplete);
function signalPackagingComplete(successful) {
setPackagingComplete(successful);
window.scrollTo(0, document.body.scrollHeight);
}

View File

@@ -0,0 +1,105 @@
/*
* Renders the native JS modal over the window.
* Returns selected option from **buttonOptions** list.
*
* Input:
* - title: string
* - description: string
* - [optional] buttonOptions: string[] = ['Yes', 'No']
* - [optional]: closeEvent: string = 'Close'
*
* Returns:
* - Promise<string>
*/
const displayModal = (title, description, buttonOptions = ['Yes', 'No'], closeEvent = 'Close') => {
const buildHeader = (_title) => {
const header = document.createElement('div');
header.classList.add('modal-section', 'modal-header');
const closeButton = document.createElement('span');
closeButton.classList.add('close-btn');
closeButton.innerHTML = '&times;';
header.appendChild(closeButton);
const titleElement = document.createElement('h2');
titleElement.innerText = _title;
header.appendChild(titleElement);
return {
header: header,
closeButton: closeButton,
};
};
const buildBody = (_description) => {
const modalBody = document.createElement('div');
modalBody.classList.add('modal-section');
const descriptionElement = document.createElement('a');
descriptionElement.innerText = _description;
modalBody.appendChild(descriptionElement);
return {
body: modalBody,
};
};
const buildFooter = () => {
const footerButtons = [];
const footer = document.createElement('div');
footer.classList.add('modal-section', 'modal-footer');
for (const label of buttonOptions) {
const footerButton = document.createElement('button');
footerButton.classList.add('modal-btn');
footerButton.innerText = label;
footer.appendChild(footerButton);
footerButtons.push(footerButton);
}
return {
footer: footer,
footerButtons: footerButtons,
};
};
const modalArea = document.getElementById('modal-area');
modalArea.classList.remove('modal-coverage-hidden');
const headerElement = buildHeader(title);
const bodyElement = buildBody(description);
const footerElement = buildFooter();
const footerButtons = footerElement.footerButtons;
const clearEventListeners = () => {
headerElement.closeButton.removeEventListener('click', (_) => {});
footerButtons.forEach((button) => button.removeEventListener('click', (_) => {}));
};
const modalContent = document.createElement('div');
modalContent.classList.add('modal-content');
modalContent.appendChild(headerElement.header);
modalContent.appendChild(bodyElement.body);
modalContent.appendChild(footerElement.footer);
modalArea.appendChild(modalContent);
return new Promise((resolve) => {
headerElement.closeButton.addEventListener('click', (_) => {
clearEventListeners();
modalArea.removeChild(modalContent);
modalArea.classList.add('modal-coverage-hidden');
resolve(closeEvent);
});
for (const [label, button] of zip(buttonOptions, footerButtons)) {
button.addEventListener('click', (_) => {
clearEventListeners();
modalArea.removeChild(modalContent);
modalArea.classList.add('modal-coverage-hidden');
resolve(label);
});
}
});
};

View File

@@ -0,0 +1,61 @@
let packagingState = PACKAGING_STATE_READY;
const setPackagingState = (newState) => {
packagingState = newState;
const outputSectionNode = document.getElementById('output');
const outputTextAreaNode = outputSectionNode.querySelector('textarea');
const convertButtonNode = document.getElementById('package-button');
const openOutputButtonNode = document.getElementById('open-output-folder-button');
const commonIssueLinkNode = document.getElementById('common-issue-link');
switch (newState) {
case PACKAGING_STATE_READY:
// Clear output
outputSectionNode.classList.remove('show');
outputTextAreaNode.value = '';
outputTextAreaNode.rows = 0;
outputTextAreaNode.classList.remove('failure');
// Set the main button back to initial value
convertButtonNode.dataset.i18n = 'ui.button.convert';
convertButtonNode.innerHTML = getTranslation(convertButtonNode.dataset.i18n);
// Hide open folder button
openOutputButtonNode.classList.remove('show');
// Hide common issue link
commonIssueLinkNode.classList.remove('show');
return;
case PACKAGING_STATE_PACKAGING:
// Disable convert button
convertButtonNode.disabled = true;
convertButtonNode.dataset.i18n = 'dynamic.button.converting';
convertButtonNode.innerHTML = getTranslation(convertButtonNode.dataset.i18n);
// Show output
outputSectionNode.classList.add('show');
return;
case PACKAGING_STATE_COMPLETE:
// Re-enable convert button and re-purpose it
convertButtonNode.disabled = false;
convertButtonNode.dataset.i18n = 'dynamic.button.clearOutput';
convertButtonNode.innerHTML = getTranslation(convertButtonNode.dataset.i18n);
// Show open folder button (beside "Clear Output" button)
openOutputButtonNode.classList.add('show');
// Show common issue link
commonIssueLinkNode.classList.add('show');
return;
}
};
const startPackaging = async () => {
eel.package(await getCurrentCommand(), getNonPyinstallerConfiguration())();
setPackagingState(PACKAGING_STATE_PACKAGING);
};
const setPackagingComplete = (successful) => {
setPackagingState(PACKAGING_STATE_COMPLETE);
// Show red border around output on failure
if (!successful) {
const outputTextAreaNode = document.querySelector('#output textarea');
outputTextAreaNode.classList.add('failure');
}
};

View File

@@ -0,0 +1,261 @@
/*
Handle user events
*/
// Top level inputs
const scriptLocationChange = async (event) => {
colourInput(event.target, false, true, false);
void updateCurrentCommandDisplay();
};
const scriptLocationSearch = async (event) => {
const entryScriptNode = document.getElementById('entry-script');
const value = await askForFile('python');
if (value !== null) {
entryScriptNode.value = value;
await scriptLocationChange({ target: entryScriptNode });
}
};
const oneFileOptionChange = (option) => (event) => {
const onefileAdditionalFilesNote = document.getElementById('onefileAdditionalFilesNote');
onefileAdditionalFilesNote.style.display = option === 'one-file' ? 'block' : 'none'; // Show the note if one-file is being used
const oneFileButton = document.getElementById('one-file-button');
oneFileButton.classList.add(option === 'one-file' ? 'selected' : 'unselected');
oneFileButton.classList.remove(option !== 'one-file' ? 'selected' : 'unselected');
const oneDirectoryButton = document.getElementById('one-directory-button');
oneDirectoryButton.classList.add(option === 'one-directory' ? 'selected' : 'unselected');
oneDirectoryButton.classList.remove(option !== 'one-directory' ? 'selected' : 'unselected');
void updateCurrentCommandDisplay();
};
const consoleWindowOptionChange = (option) => (event) => {
const consoleButton = document.getElementById('console-based-button');
consoleButton.classList.add(option === 'console' ? 'selected' : 'unselected');
consoleButton.classList.remove(option !== 'console' ? 'selected' : 'unselected');
const windowButton = document.getElementById('window-based-button');
windowButton.classList.add(option === 'window' ? 'selected' : 'unselected');
windowButton.classList.remove(option !== 'window' ? 'selected' : 'unselected');
void updateCurrentCommandDisplay();
};
const iconLocationChange = async (event) => {
const valid = await colourInput(event.target, true, true, false);
void updateCurrentCommandDisplay();
// If valid and a value exists, show the message if the file is not an ico file
const warningElement = document.getElementById('icon-invalid-warning');
if (valid && event.target.value !== '') {
const isIcoFile = await isFileAnIco(event.target.value);
warningElement.style.display = isIcoFile === false ? 'block' : 'none'; // isIcoFile is boolean | null
} else {
warningElement.style.display = 'none';
}
};
const iconLocationSearch = async (event) => {
const iconPathNode = document.getElementById('icon-path');
const value = await askForFile('icon');
if (value !== null) {
iconPathNode.value = value;
await iconLocationChange({ target: iconPathNode });
}
};
const additionalFilesAddFiles = async (event) => {
const files = await askForFiles();
if (files !== null) {
const datasListNode = document.getElementById('datas-list');
files.forEach((file) => {
addDoubleInputForSrcDst(datasListNode, 'datas', file, '.', true, true);
});
}
};
const additionalFilesAddFolder = async (event) => {
const folder = await askForFolder();
if (folder !== '') {
const datasListNode = document.getElementById('datas-list');
const destinationFolder = folder.split(/[/\\]/);
addDoubleInputForSrcDst(
datasListNode,
'datas',
folder,
`${destinationFolder[destinationFolder.length - 1]}/`,
true,
true
);
}
};
const additionalFilesAddBlank = (event) => {
const datasListNode = document.getElementById('datas-list');
addDoubleInputForSrcDst(datasListNode, 'datas', '', '.', true, true);
};
// Settings section events
const outputDirectorySearch = async (event) => {
const folder = await askForFolder();
if (folder !== '') {
const outputDirectoryInput = document.getElementById('output-directory');
outputDirectoryInput.value = folder;
}
};
const recursionLimitToggle = (enabled) => {
const button = document.getElementById('recursion-limit-switch');
if (enabled) {
button.classList.add('selected');
button.classList.remove('unselected');
} else {
button.classList.remove('selected');
button.classList.add('unselected');
}
};
const rawArgumentsChange = (event) => {
void updateCurrentCommandDisplay();
};
const packageScript = async (event) => {
if (packagingState === PACKAGING_STATE_PACKAGING) {
// Do not do anything while packaging
return;
}
if (packagingState === PACKAGING_STATE_COMPLETE) {
// This is now the clear output button
setPackagingState(PACKAGING_STATE_READY);
return;
}
// Pre-checks
const currentConfiguration = await getCurrentConfiguration();
const entryScript = currentConfiguration.find((c) => c.optionDest === 'filenames').value;
if (entryScript === '') {
alert(getTranslation('nonDom.alert.noScriptsLocationProvided'));
return;
}
const willOverwrite = await eel.will_packaging_overwrite_existing(
entryScript,
currentConfiguration.find((c) => c.optionDest === 'name')?.value,
currentConfiguration.find((c) => c.optionDest === 'onefile').value,
getNonPyinstallerConfiguration().outputDirectory
)();
if (willOverwrite && !confirm(getTranslation('nonDom.alert.overwritePreviousOutput'))) {
return;
}
// If checks have passed, package the script
await startPackaging();
};
const openOutputFolder = async (event) => {
const currentConfiguration = await getCurrentConfiguration();
const entryScript = currentConfiguration.find((c) => c.optionDest === 'filenames').value;
const isOneFile = currentConfiguration.find((c) => c.optionDest === 'onefile').value;
eel.open_output_in_explorer(getNonPyinstallerConfiguration().outputDirectory, entryScript, isOneFile)();
};
const setupEvents = () => {
// Script location
document.getElementById('entry-script').addEventListener('input', scriptLocationChange);
document.getElementById('entry-script-search').addEventListener('click', scriptLocationSearch);
// Output bundle type
document.getElementById('one-directory-button').addEventListener('click', oneFileOptionChange('one-directory'));
document.getElementById('one-file-button').addEventListener('click', oneFileOptionChange('one-file'));
// Console switch
document.getElementById('console-based-button').addEventListener('click', consoleWindowOptionChange('console'));
document.getElementById('window-based-button').addEventListener('click', consoleWindowOptionChange('window'));
// Icon
document.getElementById('icon-path').addEventListener('input', iconLocationChange);
document.getElementById('icon-path-search').addEventListener('click', iconLocationSearch);
// Additional files
document.getElementById('additional-files-add-files-button').addEventListener('click', additionalFilesAddFiles);
document.getElementById('additional-files-add-folder').addEventListener('click', additionalFilesAddFolder);
document.getElementById('additional-files-add-blank').addEventListener('click', additionalFilesAddBlank);
// Settings
document.getElementById('output-directory-search').addEventListener('click', outputDirectorySearch);
document
.getElementById('recursion-limit-switch')
.addEventListener('click', (e) => recursionLimitToggle(e.target.classList.contains('unselected')));
document.getElementById('raw-arguments').addEventListener('input', rawArgumentsChange);
document.getElementById('configuration-import').addEventListener('click', () => onConfigurationImport());
document.getElementById('configuration-export').addEventListener('click', () => onConfigurationExport());
// Build buttons
document.getElementById('package-button').addEventListener('click', packageScript);
document.getElementById('open-output-folder-button').addEventListener('click', openOutputFolder);
// Add configurationGetters
const getEntryScript = () => ['filenames', document.getElementById('entry-script').value];
const getOnefile = () => [
'onefile',
document.getElementById('one-directory-button').classList.contains('unselected'),
];
const getConsole = () => ['console', document.getElementById('window-based-button').classList.contains('unselected')];
const getIcon = () => {
const path = document.getElementById('icon-path').value;
return path === '' ? null : ['icon_file', path];
};
configurationGetters.push(getEntryScript);
configurationGetters.push(getOnefile);
configurationGetters.push(getConsole);
configurationGetters.push(getIcon);
// Add configurationSetters
const setEntryScript = (value) => {
document.getElementById('entry-script').value = value;
scriptLocationChange({ target: document.getElementById('entry-script') });
};
const setOnefile = (value) => {
if (value) {
document.getElementById('one-directory-button').classList.add('unselected');
document.getElementById('one-file-button').classList.remove('unselected');
} else {
document.getElementById('one-directory-button').classList.remove('unselected');
document.getElementById('one-file-button').classList.add('unselected');
}
};
const setConsole = (value) => {
if (value) {
document.getElementById('console-based-button').classList.remove('unselected');
document.getElementById('window-based-button').classList.add('unselected');
} else {
document.getElementById('console-based-button').classList.add('unselected');
document.getElementById('window-based-button').classList.remove('unselected');
}
};
const setAdditionalFile = (value) => {
const datasListNode = document.getElementById('datas-list');
const [val1, val2] = value.split(pathSeparator);
addDoubleInputForSrcDst(datasListNode, 'datas', val1, val2, true, true);
};
const setIcon = (value) => {
document.getElementById('icon-path').value = value;
document.getElementById('icon-path').dispatchEvent(new Event('input'));
};
configurationSetters['filenames'] = setEntryScript;
configurationSetters['onefile'] = setOnefile;
configurationSetters['console'] = setConsole;
configurationSetters['datas'] = setAdditionalFile;
configurationSetters['icon_file'] = setIcon;
configurationCleaners.push(() => setEntryScript('')); // filenames
configurationCleaners.push(() => setOnefile(false)); // onefile
configurationCleaners.push(() => setConsole(true)); // console
configurationCleaners.push(() => setIcon('')); // icon_file
// Soft initialise (to trigger any required initial events)
setEntryScript('');
setOnefile(false);
setConsole(true);
};

View File

@@ -0,0 +1,50 @@
/*
Util functions
*/
const flatMap = (xs) => xs.reduce((x, y) => x.concat(y), []); // Not all browsers have Array.flatMap
/*
* Equivalent of Python zip(*args) function. Usage:
*
* for (let [var1, ..., varN] of zip(arr1, ..., arrN)) {
* ...
* }
*/
const zip = (...arrays) => [...arrays[0]].map((_, index) => arrays.map((arr) => arr[index]));
const doesFileExist = async (path) => {
return await eel.does_file_exist(path)();
};
const doesFolderExist = async (path) => {
return await eel.does_folder_exist(path)();
};
const askForFile = async (fileType) => {
return await eel.ask_file(fileType)();
};
const askForFiles = async () => {
return await eel.ask_files()();
};
const askForFolder = async () => {
return await eel.ask_folder()();
};
const isFileAnIco = async (file_path) => {
return await eel.is_file_an_ico(file_path)();
};
const convertPathToAbsolute = async (path) => {
return await eel.convert_path_to_absolute(path)();
};
const chooseOptionString = (optionStrings) => {
// Try not to use compressed flags
if (optionStrings[0].length === 2 && optionStrings.length > 1) {
return optionStrings[1];
}
return optionStrings[0];
};