243
background/exemaker.py
Normal file
243
background/exemaker.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Tkinter GUI to run PyInstaller and make an exe from a .py file.
|
||||
|
||||
Features:
|
||||
- choose script
|
||||
- choose onefile / dir
|
||||
- choose windowed (noconsole)
|
||||
- add icon
|
||||
- add additional data (file or folder; multiple, separated by semicolons)
|
||||
- set output folder
|
||||
- show live PyInstaller output
|
||||
- cancel build
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import threading
|
||||
import sys
|
||||
import shlex
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
class BuilderGUI(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("Python → EXE (PyInstaller GUI)")
|
||||
self.geometry("800x600")
|
||||
self.create_widgets()
|
||||
self.proc = None # subprocess handle
|
||||
self.stop_requested = False
|
||||
|
||||
def create_widgets(self):
|
||||
frame_top = tk.Frame(self)
|
||||
frame_top.pack(fill="x", padx=10, pady=8)
|
||||
|
||||
tk.Label(frame_top, text="Script:").grid(row=0, column=0, sticky="e")
|
||||
self.script_entry = tk.Entry(frame_top, width=70)
|
||||
self.script_entry.grid(row=0, column=1, padx=6)
|
||||
tk.Button(frame_top, text="Browse...", command=self.browse_script).grid(row=0, column=2)
|
||||
|
||||
tk.Label(frame_top, text="Output folder:").grid(row=1, column=0, sticky="e")
|
||||
self.out_entry = tk.Entry(frame_top, width=70)
|
||||
self.out_entry.grid(row=1, column=1, padx=6)
|
||||
tk.Button(frame_top, text="Choose...", command=self.choose_output).grid(row=1, column=2)
|
||||
|
||||
opts_frame = tk.LabelFrame(self, text="Options", padx=8, pady=8)
|
||||
opts_frame.pack(fill="x", padx=10)
|
||||
|
||||
self.onefile_var = tk.BooleanVar(value=True)
|
||||
tk.Checkbutton(opts_frame, text="Onefile (single exe)", variable=self.onefile_var).grid(row=0, column=0, sticky="w", padx=6, pady=2)
|
||||
|
||||
self.windowed_var = tk.BooleanVar(value=False)
|
||||
tk.Checkbutton(opts_frame, text="Windowed (no console) / --noconsole", variable=self.windowed_var).grid(row=0, column=1, sticky="w", padx=6, pady=2)
|
||||
|
||||
tk.Label(opts_frame, text="Icon (.ico):").grid(row=1, column=0, sticky="e")
|
||||
self.icon_entry = tk.Entry(opts_frame, width=50)
|
||||
self.icon_entry.grid(row=1, column=1, sticky="w", padx=6)
|
||||
tk.Button(opts_frame, text="Browse", command=self.browse_icon).grid(row=1, column=2)
|
||||
|
||||
tk.Label(opts_frame, text="Additional data (src;dest pairs separated by ';', e.g. resources;resources):").grid(row=2, column=0, columnspan=3, sticky="w", pady=(6,0))
|
||||
self.data_entry = tk.Entry(opts_frame, width=110)
|
||||
self.data_entry.grid(row=3, column=0, columnspan=3, padx=6, pady=4)
|
||||
|
||||
tk.Label(opts_frame, text="Extra PyInstaller args:").grid(row=4, column=0, sticky="w")
|
||||
self.extra_entry = tk.Entry(opts_frame, width=80)
|
||||
self.extra_entry.grid(row=4, column=1, columnspan=2, padx=6, pady=4, sticky="w")
|
||||
|
||||
run_frame = tk.Frame(self)
|
||||
run_frame.pack(fill="x", padx=10, pady=8)
|
||||
|
||||
self.build_btn = tk.Button(run_frame, text="Build EXE", command=self.start_build, bg="#2b7a78", fg="white")
|
||||
self.build_btn.pack(side="left", padx=(0,6))
|
||||
|
||||
self.cancel_btn = tk.Button(run_frame, text="Cancel", command=self.request_cancel, state="disabled", bg="#b00020", fg="white")
|
||||
self.cancel_btn.pack(side="left")
|
||||
|
||||
clear_btn = tk.Button(run_frame, text="Clear Log", command=self.clear_log)
|
||||
clear_btn.pack(side="left", padx=6)
|
||||
|
||||
open_out_btn = tk.Button(run_frame, text="Open Output Folder", command=self.open_output)
|
||||
open_out_btn.pack(side="right")
|
||||
|
||||
self.log = scrolledtext.ScrolledText(self, height=18, font=("Consolas", 10))
|
||||
self.log.pack(fill="both", expand=True, padx=10, pady=(0,10))
|
||||
|
||||
def browse_script(self):
|
||||
path = filedialog.askopenfilename(filetypes=[("Python files", "*.py")])
|
||||
if path:
|
||||
self.script_entry.delete(0, tk.END)
|
||||
self.script_entry.insert(0, path)
|
||||
# default output to script parent /dist
|
||||
parent = os.path.dirname(path)
|
||||
default_out = os.path.join(parent, "dist")
|
||||
self.out_entry.delete(0, tk.END)
|
||||
self.out_entry.insert(0, default_out)
|
||||
|
||||
def choose_output(self):
|
||||
path = filedialog.askdirectory()
|
||||
if path:
|
||||
self.out_entry.delete(0, tk.END)
|
||||
self.out_entry.insert(0, path)
|
||||
|
||||
def browse_icon(self):
|
||||
path = filedialog.askopenfilename(filetypes=[("Icon files", "*.ico")])
|
||||
if path:
|
||||
self.icon_entry.delete(0, tk.END)
|
||||
self.icon_entry.insert(0, path)
|
||||
|
||||
def clear_log(self):
|
||||
self.log.delete("1.0", tk.END)
|
||||
|
||||
def open_output(self):
|
||||
out = self.out_entry.get().strip()
|
||||
if not out:
|
||||
messagebox.showinfo("Output folder", "No output folder set.")
|
||||
return
|
||||
os.startfile(out) if os.name == "nt" else subprocess.run(["xdg-open", out])
|
||||
|
||||
def request_cancel(self):
|
||||
if self.proc and self.proc.poll() is None:
|
||||
self.stop_requested = True
|
||||
# try terminate politely
|
||||
try:
|
||||
self.proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
self.log_insert("\nCancellation requested...\n")
|
||||
self.cancel_btn.config(state="disabled")
|
||||
|
||||
def log_insert(self, text):
|
||||
self.log.insert(tk.END, text)
|
||||
self.log.see(tk.END)
|
||||
|
||||
def start_build(self):
|
||||
script = self.script_entry.get().strip()
|
||||
if not script or not os.path.isfile(script):
|
||||
messagebox.showerror("Error", "Please choose a valid Python script to build.")
|
||||
return
|
||||
|
||||
out_dir = self.out_entry.get().strip() or os.path.dirname(script)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
self.build_btn.config(state="disabled")
|
||||
self.cancel_btn.config(state="normal")
|
||||
self.stop_requested = False
|
||||
self.clear_log()
|
||||
|
||||
# Start build on a thread to keep UI responsive
|
||||
thread = threading.Thread(target=self.run_pyinstaller, args=(script, out_dir), daemon=True)
|
||||
thread.start()
|
||||
|
||||
def run_pyinstaller(self, script, out_dir):
|
||||
# Build the PyInstaller command
|
||||
cmd = [sys.executable, "-m", "PyInstaller"]
|
||||
|
||||
if self.onefile_var.get():
|
||||
cmd.append("--onefile")
|
||||
else:
|
||||
cmd.append("--onedir")
|
||||
|
||||
if self.windowed_var.get():
|
||||
cmd.append("--noconsole")
|
||||
|
||||
icon = self.icon_entry.get().strip()
|
||||
if icon:
|
||||
cmd.extend(["--icon", icon])
|
||||
|
||||
# add additional data: user can provide pairs like "data;data" or "assets;assets"
|
||||
data_spec = self.data_entry.get().strip()
|
||||
if data_spec:
|
||||
# support multiple separated by semicolons or vertical bars
|
||||
pairs = [p for p in (data_spec.split(";") + data_spec.split("|")) if p.strip()]
|
||||
# normalize pairs to PyInstaller format: src;dest (on Windows use ';' in CLI but PyInstaller expects src;dest as single argument)
|
||||
for p in pairs:
|
||||
# if user typed "src:dest" or "src->dest", replace with semicolon
|
||||
p_fixed = p.replace(":", ";").replace("->", ";")
|
||||
cmd.extend(["--add-data", p_fixed])
|
||||
|
||||
# user extra args (raw)
|
||||
extra = self.extra_entry.get().strip()
|
||||
if extra:
|
||||
# split carefully
|
||||
cmd.extend(shlex.split(extra))
|
||||
|
||||
# ensure output path goes to chosen dir: use --distpath
|
||||
cmd.extend(["--distpath", out_dir])
|
||||
|
||||
# entry script
|
||||
cmd.append(script)
|
||||
|
||||
self.log_insert("Running PyInstaller with command:\n" + " ".join(shlex.quote(c) for c in cmd) + "\n\n")
|
||||
|
||||
# spawn the process
|
||||
try:
|
||||
self.proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
)
|
||||
except Exception as e:
|
||||
self.log_insert(f"Failed to start PyInstaller: {e}\n")
|
||||
self.build_btn.config(state="normal")
|
||||
self.cancel_btn.config(state="disabled")
|
||||
return
|
||||
|
||||
# Stream output line by line
|
||||
try:
|
||||
for line in self.proc.stdout:
|
||||
if line:
|
||||
self.log_insert(line)
|
||||
if self.stop_requested:
|
||||
try:
|
||||
self.proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
self.proc.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log_insert("Process did not exit in time after termination.\n")
|
||||
except Exception as e:
|
||||
self.log_insert(f"Error while running PyInstaller: {e}\n")
|
||||
|
||||
retcode = self.proc.returncode if self.proc else None
|
||||
if self.stop_requested:
|
||||
self.log_insert("\nBuild cancelled by user.\n")
|
||||
elif retcode == 0:
|
||||
self.log_insert("\nBuild finished successfully.\n")
|
||||
else:
|
||||
self.log_insert(f"\nBuild finished with return code {retcode}.\n")
|
||||
|
||||
# Re-enable buttons
|
||||
self.build_btn.config(state="normal")
|
||||
self.cancel_btn.config(state="disabled")
|
||||
self.proc = None
|
||||
self.stop_requested = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = BuilderGUI()
|
||||
app.mainloop()
|
||||
143
background/experiment.py
Normal file
143
background/experiment.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import tkinter as tk
|
||||
import requests
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
|
||||
SETTINGS_FILE = "settings.json"
|
||||
|
||||
class CountdownApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Launch Control - GO/NOGO")
|
||||
|
||||
self.go_nogo_labels = {}
|
||||
self.sheet_data = {}
|
||||
self.last_data = {}
|
||||
self.running = True
|
||||
|
||||
# Load settings
|
||||
self.settings = self.load_settings()
|
||||
|
||||
tk.Label(root, text="GO/NOGO STATUS", font=("Arial", 16, "bold")).pack(pady=10)
|
||||
|
||||
# Create display area
|
||||
self.frame = tk.Frame(root)
|
||||
self.frame.pack(pady=10)
|
||||
|
||||
# Buttons
|
||||
tk.Button(root, text="Add Spreadsheet", command=self.add_spreadsheet_window).pack(pady=5)
|
||||
tk.Button(root, text="Stop", command=self.stop).pack(pady=5)
|
||||
|
||||
self.start_update_thread()
|
||||
|
||||
def load_settings(self):
|
||||
try:
|
||||
with open(SETTINGS_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {"spreadsheets": []}
|
||||
|
||||
def save_settings(self):
|
||||
with open(SETTINGS_FILE, "w") as f:
|
||||
json.dump(self.settings, f, indent=4)
|
||||
|
||||
def add_spreadsheet_window(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Add Spreadsheet")
|
||||
|
||||
tk.Label(win, text="Name:").grid(row=0, column=0)
|
||||
name_entry = tk.Entry(win)
|
||||
name_entry.grid(row=0, column=1)
|
||||
|
||||
tk.Label(win, text="Link (CSV export or share link):").grid(row=1, column=0)
|
||||
link_entry = tk.Entry(win, width=60)
|
||||
link_entry.grid(row=1, column=1)
|
||||
|
||||
tk.Label(win, text="Range cell (e.g., L2):").grid(row=2, column=0)
|
||||
range_entry = tk.Entry(win)
|
||||
range_entry.grid(row=2, column=1)
|
||||
|
||||
def save_sheet():
|
||||
name = name_entry.get().strip()
|
||||
link = link_entry.get().strip()
|
||||
cell = range_entry.get().strip().upper()
|
||||
if name and link and cell:
|
||||
self.settings["spreadsheets"].append({
|
||||
"name": name,
|
||||
"link": link,
|
||||
"cell": cell
|
||||
})
|
||||
self.save_settings()
|
||||
self.add_go_nogo_label(name)
|
||||
win.destroy()
|
||||
|
||||
tk.Button(win, text="Save", command=save_sheet).grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
def add_go_nogo_label(self, name):
|
||||
if name not in self.go_nogo_labels:
|
||||
label = tk.Label(self.frame, text=f"{name}: ---", font=("Arial", 14), width=25)
|
||||
label.pack(pady=2)
|
||||
self.go_nogo_labels[name] = label
|
||||
|
||||
def update_labels(self):
|
||||
for sheet in self.settings["spreadsheets"]:
|
||||
name = sheet["name"]
|
||||
link = sheet["link"]
|
||||
cell = sheet["cell"]
|
||||
|
||||
# Convert normal sheet link to CSV export link if needed
|
||||
if "/edit" in link and "export" not in link:
|
||||
link = link.split("/edit")[0] + "/gviz/tq?tqx=out:csv"
|
||||
|
||||
try:
|
||||
r = requests.get(link, timeout=5)
|
||||
if r.status_code == 200:
|
||||
content = r.text
|
||||
if name not in self.last_data or self.last_data[name] != content:
|
||||
self.last_data[name] = content
|
||||
# Just read raw content and extract cell text if possible
|
||||
value = self.extract_cell_value(content, cell)
|
||||
self.update_label_color(name, value)
|
||||
except Exception as e:
|
||||
print(f"Error updating {name}: {e}")
|
||||
|
||||
def extract_cell_value(self, csv_data, cell):
|
||||
# Simple CSV parser to get cell data like L2
|
||||
try:
|
||||
rows = [r.split(",") for r in csv_data.splitlines() if r.strip()]
|
||||
col = ord(cell[0]) - 65
|
||||
row = int(cell[1:]) - 1
|
||||
return rows[row][col].strip().upper()
|
||||
except Exception:
|
||||
return "ERR"
|
||||
|
||||
def update_label_color(self, name, value):
|
||||
label = self.go_nogo_labels.get(name)
|
||||
if not label:
|
||||
return
|
||||
|
||||
if "GO" in value:
|
||||
label.config(text=f"{name}: GO", bg="green", fg="white")
|
||||
elif "NO" in value:
|
||||
label.config(text=f"{name}: NO GO", bg="red", fg="white")
|
||||
else:
|
||||
label.config(text=f"{name}: ---", bg="gray", fg="black")
|
||||
|
||||
def start_update_thread(self):
|
||||
threading.Thread(target=self.update_loop, daemon=True).start()
|
||||
|
||||
def update_loop(self):
|
||||
while self.running:
|
||||
self.update_labels()
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = CountdownApp(root)
|
||||
root.mainloop()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,46 +0,0 @@
|
||||
|
||||
This file lists modules PyInstaller was not able to find. This does not
|
||||
necessarily mean this module is required for running your program. Python and
|
||||
Python 3rd-party packages include a lot of conditional or optional modules. For
|
||||
example the module 'ntpath' only exists on Windows, whereas the module
|
||||
'posixpath' only exists on Posix systems.
|
||||
|
||||
Types if import:
|
||||
* top-level: imported at the top-level - look at these first
|
||||
* conditional: imported within an if-statement
|
||||
* delayed: imported within a function
|
||||
* optional: imported within a try-except-statement
|
||||
|
||||
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
|
||||
tracking down the missing module yourself. Thanks!
|
||||
|
||||
missing module named pwd - imported by posixpath (delayed, conditional, optional), subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), netrc (delayed, conditional), getpass (delayed)
|
||||
missing module named grp - imported by subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional)
|
||||
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
|
||||
missing module named fcntl - imported by subprocess (optional)
|
||||
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
|
||||
missing module named _scproxy - imported by urllib.request (conditional)
|
||||
missing module named termios - imported by getpass (optional)
|
||||
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
|
||||
missing module named resource - imported by posix (top-level)
|
||||
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
|
||||
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
|
||||
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named pyimod02_importers - imported by C:\Users\forjn\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
|
||||
missing module named simplejson - imported by requests.compat (conditional, optional)
|
||||
missing module named dummy_threading - imported by requests.cookies (optional)
|
||||
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
|
||||
missing module named zstandard - imported by urllib3.util.request (optional), urllib3.response (optional)
|
||||
missing module named brotlicffi - imported by urllib3.util.request (optional), urllib3.response (optional)
|
||||
missing module named socks - imported by urllib3.contrib.socks (optional)
|
||||
missing module named bcrypt - imported by cryptography.hazmat.primitives.serialization.ssh (optional)
|
||||
missing module named cryptography.x509.UnsupportedExtension - imported by cryptography.x509 (optional), urllib3.contrib.pyopenssl (optional)
|
||||
missing module named 'OpenSSL.crypto' - imported by urllib3.contrib.pyopenssl (delayed, conditional)
|
||||
missing module named OpenSSL - imported by urllib3.contrib.pyopenssl (top-level)
|
||||
missing module named pyodide - imported by urllib3.contrib.emscripten.fetch (top-level)
|
||||
missing module named js - imported by urllib3.contrib.emscripten.fetch (top-level)
|
||||
File diff suppressed because it is too large
Load Diff
BIN
dist/exe/0.6.0/main.exe
vendored
BIN
dist/exe/0.6.0/main.exe
vendored
Binary file not shown.
BIN
dist/installers/0.6.0/RLCInstaller.exe
vendored
BIN
dist/installers/0.6.0/RLCInstaller.exe
vendored
Binary file not shown.
Reference in New Issue
Block a user