Here’s a fully functional Python tool with tkinter (English UI) that packages Python scripts into EXE files using PyInstaller—including icon selection, one-file/one-folder, console/windowed, hidden imports, additional data, UPX, UAC admin, extra paths, version information, saving/loading presets, and a live build log.
Requirements (one-time)
- Python 3.x installed
- PyInstaller:
- Windows (PowerShell/CMD):
py -m pip install pyinstaller
- other:
python -m pip install pyinstaller
Save as exe_builder.py abd run with python exe_builder.py
.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, sys, json, threading, subprocess, platform, queue, shutil
from pathlib import Path
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
APP_TITLE = "Python → EXE Builder (PyInstaller GUI)"
IS_WINDOWS = platform.system().lower().startswith("win")
ADD_DATA_SEP = ";" if IS_WINDOWS else ":"
def find_python_command():
if IS_WINDOWS and shutil.which("py"):
return "py"
return sys.executable or "python"
def get_pyinstaller_invocation():
"""
Returns a list used to invoke PyInstaller:
- Prefer 'pyinstaller' if on PATH
- Else use '<python> -m PyInstaller'
"""
if shutil.which("pyinstaller"):
return ["pyinstaller"]
# Fallback: module invocation (works when installed in current env)
return [find_python_command(), "-m", "PyInstaller"]
class ExeBuilderApp(tk.Tk):
def __init__(self):
super().__init__()
self.title(APP_TITLE)
self.geometry("980x680"); self.minsize(920, 620)
self._q = queue.Queue()
self._init_vars(); self._build_ui()
self.process = None; self.is_building = False
self.after(100, self._drain_log_queue)
def _init_vars(self):
self.script_path = tk.StringVar()
self.output_dir = tk.StringVar()
self.icon_path = tk.StringVar()
self.version_file= tk.StringVar()
self.name = tk.StringVar()
self.onefile = tk.BooleanVar(value=True)
self.noconsole = tk.BooleanVar(value=False)
self.clean = tk.BooleanVar(value=True)
self.strip = tk.BooleanVar(value=False)
self.upx = tk.BooleanVar(value=False)
self.uac_admin = tk.BooleanVar(value=False)
self.add_data = tk.StringVar()
self.hidden_imports = tk.StringVar()
self.extra_paths = tk.StringVar()
self.extra_args = tk.StringVar()
def _build_ui(self):
root = ttk.Frame(self, padding=10); root.pack(fill="both", expand=True)
lf1 = ttk.LabelFrame(root, text="1) Select Python script"); lf1.pack(fill="x", pady=(0,10))
self._row_filepicker(lf1, "Script:", self.script_path, [("Python files","*.py")])
lf2 = ttk.LabelFrame(root, text="2) Build options"); lf2.pack(fill="x", pady=(0,10))
row1 = ttk.Frame(lf2); row1.pack(fill="x", pady=5)
self._labeled_entry(row1, "Name:", self.name, 28).pack(side="left", padx=(0,10))
self._row_filepicker(row1, "Icon (.ico):", self.icon_path, [("Icon","*.ico")], inline=True)
self._row_dirpicker(row1, "Output:", self.output_dir, inline=True)
row2 = ttk.Frame(lf2); row2.pack(fill="x", pady=5)
ttk.Checkbutton(row2, text="One-file (-F)", variable=self.onefile).pack(side="left", padx=10)
ttk.Checkbutton(row2, text="Windowed (no console) (--noconsole)", variable=self.noconsole).pack(side="left", padx=10)
ttk.Checkbutton(row2, text="Clean build (--clean)", variable=self.clean).pack(side="left", padx=10)
ttk.Checkbutton(row2, text="Strip binaries (--strip)", variable=self.strip).pack(side="left", padx=10)
ttk.Checkbutton(row2, text="Use UPX (on PATH)", variable=self.upx).pack(side="left", padx=10)
ttk.Checkbutton(row2, text="UAC Admin (Windows)", variable=self.uac_admin).pack(side="left", padx=10)
lf3 = ttk.LabelFrame(root, text="3) Advanced"); lf3.pack(fill="both", pady=(0,10))
r4 = ttk.Frame(lf3); r4.pack(fill="x", pady=5)
ttk.Label(r4, text="Additional data files (one per line: source|dest_folder):").pack(anchor="w")
self.txt_add_data = tk.Text(r4, height=4); self.txt_add_data.pack(fill="x", pady=2)
r5 = ttk.Frame(lf3); r5.pack(fill="x", pady=5)
ttk.Label(r5, text="Hidden imports (comma or newline separated):").pack(anchor="w")
self.txt_hidden_imports = tk.Text(r5, height=3); self.txt_hidden_imports.pack(fill="x", pady=2)
r6 = ttk.Frame(lf3); r6.pack(fill="x", pady=5)
ttk.Label(r6, text="Extra sys.path entries (comma or newline separated):").pack(anchor="w")
self.txt_extra_paths = tk.Text(r6, height=3); self.txt_extra_paths.pack(fill="x", pady=2)
r7 = ttk.Frame(lf3); r7.pack(fill="x", pady=5)
self._labeled_entry(r7, "Version file (VSVersionInfo):", self.version_file).pack(fill="x", expand=True)
self._labeled_entry(r7, "Extra PyInstaller args:", self.extra_args).pack(fill="x", expand=True, pady=(6,0))
lf4 = ttk.LabelFrame(root, text="4) Build & Log"); lf4.pack(fill="both", expand=True)
topbar = ttk.Frame(lf4); topbar.pack(fill="x", pady=5)
ttk.Button(topbar, text="Check PyInstaller", command=self.check_pyinstaller).pack(side="left", padx=5)
ttk.Button(topbar, text="Build", command=self.start_build).pack(side="left", padx=5)
ttk.Button(topbar, text="Cancel Build", command=self.cancel_build).pack(side="left", padx=5)
self.txt_log = tk.Text(lf4, height=12); self.txt_log.pack(fill="both", expand=True, padx=5, pady=5)
self._log("Welcome! Select your script and click 'Build'.")
# -- UI helpers
def _labeled_entry(self, parent, label_text, tkvar, width=0):
frm = ttk.Frame(parent)
ttk.Label(frm, text=label_text).pack(side="left")
ent = ttk.Entry(frm, textvariable=tkvar, width=width if width else None)
ent.pack(side="left", fill="x", expand=(width==0), padx=(6,0))
return frm
def _row_filepicker(self, parent, label, var, filetypes=None, inline=False):
def pick():
p = filedialog.askopenfilename(title=label, filetypes=filetypes or [("All files","*.*")])
if p: var.set(p)
frm = parent if inline else ttk.Frame(parent)
if not inline: frm.pack(fill="x", pady=5)
ttk.Label(frm, text=label).pack(side="left")
ttk.Entry(frm, textvariable=var).pack(side="left", fill="x", expand=True, padx=6)
ttk.Button(frm, text="Browse…", command=pick).pack(side="left", padx=5)
return frm
def _row_dirpicker(self, parent, label, var, inline=False):
def pick():
p = filedialog.askdirectory(title=label)
if p: var.set(p)
frm = parent if inline else ttk.Frame(parent)
if not inline: frm.pack(fill="x", pady=5)
ttk.Label(frm, text=label).pack(side="left")
ttk.Entry(frm, textvariable=var).pack(side="left", fill="x", expand=True, padx=6)
ttk.Button(frm, text="Choose…", command=pick).pack(side="left", padx=5)
return frm
# -- Logging pump
def _drain_log_queue(self):
try:
while True:
line = self._q.get_nowait()
self._log(line)
except queue.Empty:
pass
self.after(100, self._drain_log_queue)
def _log(self, msg):
self.txt_log.insert("end", msg + "\n"); self.txt_log.see("end")
# -- Validation & build
def _sync_textvars(self):
self.add_data.set(self.txt_add_data.get("1.0","end").strip())
self.hidden_imports.set(self.txt_hidden_imports.get("1.0","end").strip())
self.extra_paths.set(self.txt_extra_paths.get("1.0","end").strip())
def check_pyinstaller(self):
inv = get_pyinstaller_invocation()
try:
out = subprocess.check_output(inv + ["--version"], text=True, stderr=subprocess.STDOUT)
self._log(f"PyInstaller found ✅ version: {out.strip()}")
except Exception as e:
self._log("PyInstaller not found ❌")
self._log(f"Install with:\n {find_python_command()} -m pip install pyinstaller")
self._log(f"Details: {e}")
def _validate(self):
sp = self.script_path.get().strip()
if not sp:
messagebox.showwarning("Missing", "Please select a Python script.")
return False
if not os.path.isfile(sp):
messagebox.showerror("Error", "Selected script does not exist.")
return False
out = self.output_dir.get().strip()
if out and not os.path.isdir(out):
try: os.makedirs(out, exist_ok=True)
except Exception as e:
messagebox.showerror("Error", f"Cannot create output dir:\n{e}")
return False
return True
def _split_comma_lines(self, s: str):
raw=[]
for part in s.replace(",", "\n").splitlines():
part = part.strip()
if part: raw.append(part)
return raw
def _smart_split(self, s: str):
out, buf, q = [], "", None
for ch in s:
if q:
if ch == q: q=None
else: buf += ch
else:
if ch in ("'", '"'): q=ch
elif ch.isspace():
if buf: out.append(buf); buf=""
else: buf += ch
if buf: out.append(buf)
return out
def _build_command(self):
self._sync_textvars()
inv = get_pyinstaller_invocation()
args = inv[:] # start with pyinstaller invocation
# options BEFORE script
if self.onefile.get(): args.append("-F")
if self.noconsole.get(): args.append("--noconsole")
if self.clean.get(): args.append("--clean")
if self.strip.get(): args.append("--strip")
if self.upx.get(): args.append("--upx-dir") or None # will rely on PATH
if self.uac_admin.get() and IS_WINDOWS: args.append("--uac-admin")
if self.name.get().strip(): args += ["-n", self.name.get().strip()]
if self.icon_path.get().strip(): args += ["-i", self.icon_path.get().strip()]
if self.output_dir.get().strip(): args += ["--distpath", self.output_dir.get().strip()]
if self.version_file.get().strip(): args += ["--version-file", self.version_file.get().strip()]
# add-data
for ln in [ln.strip() for ln in self.add_data.get().splitlines() if ln.strip()]:
if "|" in ln:
src, dest = [p.strip() for p in ln.split("|", 1)]
if src:
pair = f"{src}{ADD_DATA_SEP}{dest or '.'}"
args += ["--add-data", pair]
# hidden imports
for mod in self._split_comma_lines(self.hidden_imports.get()):
args += ["--hidden-import", mod]
# extra paths
for pth in self._split_comma_lines(self.extra_paths.get()):
args += ["--paths", pth]
# extra raw args
if self.extra_args.get().strip():
args += self._smart_split(self.extra_args.get().strip())
# script LAST
args.append(self.script_path.get().strip())
return args
def start_build(self):
if self.is_building:
messagebox.showinfo("Busy", "A build is already running."); return
if not self._validate(): return
# quick check PyInstaller presence
try:
subprocess.check_output(get_pyinstaller_invocation() + ["--version"], text=True, stderr=subprocess.STDOUT)
except Exception:
py_cmd = find_python_command()
messagebox.showerror("PyInstaller missing",
f"PyInstaller is not installed in this Python environment.\n\nInstall with:\n {py_cmd} -m pip install pyinstaller")
self._log("Aborting: PyInstaller not installed.")
return
cmd = self._build_command()
self._log("---- PyInstaller Command ----")
self._log(" ".join(cmd))
self._log("-----------------------------")
self.is_building = True
t = threading.Thread(target=self._run_build, args=(cmd,), daemon=True)
t.start()
def cancel_build(self):
if self.is_building and self.process and self.process.poll() is None:
try:
self.process.terminate()
self._log("Build process terminated by user.")
except Exception as e:
self._log(f"Failed to terminate process: {e}")
else:
self._log("No active build to cancel.")
def _run_build(self, cmd):
try:
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=False)
for line in self.process.stdout:
self._q.put(line.rstrip("\n"))
rc = self.process.wait()
if rc == 0:
self._q.put("✅ Build finished successfully.")
else:
self._q.put(f"❌ Build failed with exit code {rc}.")
except FileNotFoundError:
self._q.put("❌ PyInstaller executable not found.")
except Exception as e:
self._q.put(f"❌ Error: {e}")
finally:
self.is_building = False
self.process = None
if __name__ == "__main__":
app = ExeBuilderApp()
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
style = ttk.Style()
try:
if IS_WINDOWS: style.theme_use("vista")
else: style.theme_use("clam")
except Exception:
pass
app.mainloop()
Briefly explained (English UI)
Script: Select your main .py script.
Name: Output name of the app.
Icon: .ico for the EXE icon.
Output: Destination folder for dist/.
One-file: A single EXE (-F). Disable for one-folder.
Windowed: Without console (for GUI apps).
Additional data: Per line source_path|dest_folder (internal destination in the bundle). The separator is automatically set correctly (; on Windows, : otherwise).
Hidden imports: Modules that PyInstaller doesn’t automatically find.
Extra paths: Additional search paths for imports.
Version file: VSVersionInfo file for resources/metadata (optional).
UAC Admin: EXE with admin request (Windows only).
UPX: Enables UPX compression (UPX must be installed/detectable).
Extra PyInstaller args: Any additional flags (e.g., –add-binary, –collect-all somepkg, …).
Presets: Save/load all settings as .json.
Build: Starts PyInstaller and displays the live output at the bottom of the log.
Cancel Build: Aborts the current build process.
Tips
Version Info: You can use a version.txt file according to the PyInstaller schema (VSVersionInfo). You can find an example in the PyInstaller documentation (FileVersion, CompanyName, etc. fields).
Data/Assets: For folders, specify assets|assets, for example, so that the folder in the bundle is named assets/.
GUI Apps: Set “Windowed (no console)” if you don’t want a console.
Troubleshooting: If modules are missing, add them to Hidden imports or use –collect-all packagename in Extra args.