Mac下PyInstaller打包App双击无响应,Shell运行正常的原因及解决
基于Tkinter的学校日程应用Mac打包问题修复
问题描述
开发了一款基于Tkinter的学校日程生成Python应用,使用PyInstaller打包成Mac应用后,双击生成的School Schedule App无任何反应,但进入应用包内容的MacOS文件夹,运行其中的可执行文件可正常启动应用。
应用代码
import tkinter as tk from tkinter import ttk import os import json import logging import datetime import sys logging.basicConfig(level=logging.DEBUG, filename='app.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s') if getattr(sys, 'frozen', False): application_path = os.path.dirname(sys.executable) else: application_path = os.path.dirname(__file__) PREFERENCES_FILE = os.path.join(application_path, "user_preferences.json") def save_preferences(data): try: with open(PREFERENCES_FILE, 'w') as f: json.dump(data, f) logging.info("Preferences saved successfully.") except Exception as e: logging.error(f"Failed to save preferences: {e}") def load_preferences(): try: if os.path.exists(PREFERENCES_FILE): with open(PREFERENCES_FILE, 'r') as f: return json.load(f) except Exception as e: logging.error(f"Error reading preferences: {e}") return None def delete_preferences(): try: if os.path.exists(PREFERENCES_FILE): os.remove(PREFERENCES_FILE) logging.info("Preferences deleted successfully.") except Exception as e: logging.error(f"Failed to delete preferences: {e}") def show_schedule(day, preferences): schedule = preferences['schedule'] day_schedule = schedule.get(day, []) schedule_text = f"Schedule for {day}:\n" for time_range, subject_key in day_schedule: subject = preferences['subjects'].get(subject_key, "Free") schedule_text += f"{time_range} - {subject}\n" schedule_label.config(text=schedule_text) def setup_preferences(): def save_and_close(): subjects = { 'MESH1': mesh1_var.get(), 'MESH2': mesh2_var.get(), 'MESH3': mesh3_var.get(), 'MESH4': mesh4_var.get(), 'WL': wl_var.get(), 'WELLNESS': 'Wellness', 'ELECTIVE1': elective1_entry.get(), 'ELECTIVE2': elective2_entry.get(), 'CLUB': club_entry.get(), 'GEOMETRY': 'Geometry' if geometry_var.get() else None, 'RECESS': 'Recess', 'LUNCH': 'Lunch', 'ACADEMIC_COACHING': 'Academic Coaching', 'ADVISORY': 'Advisory' } preferences = { 'subjects': subjects, 'schedule': load_default_schedule(geometry_var.get()) } save_preferences(preferences) setup_root.destroy() main_app() setup_root = tk.Tk() setup_root.title("Setup Preferences") mesh_options = ["Math", "English", "Science", "History"] wl_options = ["Spanish", "French", "FOL"] mesh1_var, mesh2_var, mesh3_var, mesh4_var, wl_var = tk.StringVar(), tk.StringVar(), tk.StringVar(), tk.StringVar(), tk.StringVar() geometry_var = tk.BooleanVar() ttk.Label(setup_root, text="Select MESH1:").pack() ttk.Combobox(setup_root, textvariable=mesh1_var, values=mesh_options).pack() ttk.Label(setup_root, text="Select MESH2:").pack() ttk.Combobox(setup_root, textvariable=mesh2_var, values=mesh_options).pack() ttk.Label(setup_root, text="Select MESH3:").pack() ttk.Combobox(setup_root, textvariable=mesh3_var, values=mesh_options).pack() ttk.Label(setup_root, text="Select MESH4:").pack() ttk.Combobox(setup_root, textvariable=mesh4_var, values=mesh_options).pack() ttk.Label(setup_root, text="Select WL:").pack() ttk.Combobox(setup_root, textvariable=wl_var, values=wl_options).pack() ttk.Checkbutton(setup_root, text="Include Geometry", variable=geometry_var).pack() ttk.Label(setup_root, text="Enter AM ICE:").pack() elective1_entry = ttk.Entry(setup_root) elective1_entry.pack() ttk.Label(setup_root, text="Enter PM ICE:").pack() elective2_entry = ttk.Entry(setup_root) elective2_entry.pack() ttk.Label(setup_root, text="Enter Club:").pack() club_entry = ttk.Entry(setup_root) club_entry.pack() ttk.Button(setup_root, text="Save and Start", command=save_and_close).pack(pady=10) setup_root.mainloop() def load_default_schedule(include_geometry): today = datetime.datetime.today().weekday() # Monday is 0 and Sunday is 6 geometry_days = [0, 2, 4] # Monday, Wednesday, Friday schedule = { 'A': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "CLUB"), ("9:20-10:10", "MESH1"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH3"), ("11:20-12:10", "MESH2"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "WL"), ("1:30-2:20", "MESH4"), ("2:20-3:10", "ELECTIVE2")], 'B': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "ELECTIVE1"), ("9:20-10:10", "WL"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH2"), ("11:20-12:10", "MESH3"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "MESH4"), ("1:30-2:20", "MESH1"), ("2:20-3:10", "ELECTIVE2")], 'C': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "ACADEMIC_COACHING"), ("9:20-10:10", "MESH2"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH4"), ("11:20-12:10", "MESH1"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "WL"), ("1:30-2:20", "MESH3"), ("2:20-3:10", "ELECTIVE2")], 'D': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "ELECTIVE1"), ("9:20-10:10", "WL"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH4"), ("11:20-12:10", "MESH1"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "MESH3"), ("1:30-2:20", "MESH2"), ("2:20-3:10", "ACADEMIC_COACHING")], 'E': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "ELECTIVE1"), ("9:20-10:10", "MESH1"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH3"), ("11:20-12:10", "MESH2"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "WELLNESS"), ("1:30-2:20", "MESH4"), ("2:20-3:10", "ELECTIVE2")], 'F': [("8:10-8:30", "ADVISORY"), ("8:30-9:20", "ELECTIVE1"), ("9:20-10:10", "WL"), ("10:10-10:30", "RECESS"), ("10:30-11:20", "MESH2"), ("11:20-12:10", "MESH3"), ("12:10-12:40", "LUNCH"), ("12:40-1:30", "MESH4"), ("1:30-2:20", "MESH1"), ("2:20-3:10", "ACADEMIC_COACHING")] } if include_geometry and today in geometry_days: geometry_class = [("7:00-8:00", "GEOMETRY")] for key in schedule: schedule[key].insert(0, geometry_class[0]) return schedule def main_app(): preferences = load_preferences() if not preferences: setup_preferences() return root = tk.Tk() root.title("School Schedule") day_var = tk.StringVar(value='A') day_dropdown = ttk.Combobox(root, textvariable=day_var, values=list('ABCDEF')) day_dropdown.pack(pady=10) show_button = ttk.Button(root, text="Show Schedule", command=lambda: show_schedule(day_var.get(), preferences)) show_button.pack(pady=5) reset_button = ttk.Button(root, text="Reset Preferences", command=lambda: [delete_preferences(), root.destroy(), setup_preferences()]) reset_button.pack(pady=5) global schedule_label schedule_label = tk.Label(root, text="", justify=tk.LEFT) schedule_label.pack(pady=10) root.mainloop() if __name__ == "__main__": main_app()
使用的打包命令
pyinstaller --windowed --onefile --icon=/Users/jacob/\ icon.iconset/857457.icns --name="School Schedule App" /Users/jacob/School_Schedule_App.py
问题原因
- 工作目录与权限冲突:双击.app启动时,应用的工作目录为系统级路径(如
/Applications),代码中把配置文件user_preferences.json存放在应用可执行文件所在的Contents/MacOS目录,此时应用没有权限在该目录下创建或写入文件,导致启动崩溃。而终端运行时,工作目录是当前终端路径,且有足够读写权限,因此能正常启动。 - --windowed参数隐藏错误输出:该参数禁用终端窗口,应用启动时的报错信息无法显示,无法直观排查问题。
- 打包命令路径错误:原命令中icon路径包含多余反斜杠,可能导致图标加载异常,间接影响启动流程。
修复方案
1. 修改配置文件存储路径
将配置文件改为存储在用户专属的应用支持目录(Mac标准路径),确保无论启动方式如何都有读写权限。修改代码中的路径部分:
import tkinter as tk from tkinter import ttk import os import json import logging import datetime import sys logging.basicConfig(level=logging.DEBUG, filename='app.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s') if getattr(sys, 'frozen', False): # 打包后使用用户应用支持目录 application_path = os.path.expanduser("~/Library/Application Support/SchoolScheduleApp") else: application_path = os.path.dirname(__file__) # 确保目录存在,不存在则创建 os.makedirs(application_path, exist_ok=True) PREFERENCES_FILE = os.path.join(application_path, "user_preferences.json")
2. 修正打包命令
去掉icon路径中的多余反斜杠,确保路径正确:
pyinstaller --windowed --onefile --icon="/Users/jacob/icon.iconset/857457.icns" --name="School Schedule App" /Users/jacob/School_Schedule_App.py
3. 修复应用权限(可选)
如果重新打包后仍无法启动,检查并修复.app包的权限:
- 右键点击
School Schedule App.app-> 显示简介 -> 确认当前用户有“读与写”权限。 - 或在终端执行:
chmod -R +x "/Applications/School Schedule App.app"
4. 调试排查(可选)
如果仍有问题,暂时去掉--windowed参数打包,启动时会弹出终端窗口,查看具体错误信息:
pyinstaller --onefile --icon="/Users/jacob/icon.iconset/857457.icns" --name="School Schedule App" /Users/jacob/School_Schedule_App.py
内容的提问来源于stack exchange,提问作者JSB10
相关产品推荐
相关产品推荐

