🇬🇧 English
A .py file is not a program by itself. It runs only on a computer that has Python, in a suitable version, with every library the file imports. Most people you would give it to have none of that. PyInstaller is the usual answer: it reads your script, works out what the script needs, and copies all of it, Python included, into one thing you can hand over.
This page does that on a Mac. You will build a twenty-line window four ways, read the packager's own log to see what it does, and open the app it makes to see what is inside. Then you will meet Gatekeeper, the part of macOS that decides whether an app from somebody else may open. After that come the files for a GitHub release, an installer among them, and an AI agent from Agno: a company of AI agents packed the same way.
You need a Mac, its Terminal, and Python 3 with tkinter. You type each command and compare what you get with what we got. This is the Mac sister of Python to EXE, which does the same on Windows and goes further, to a game with an AI model inside.
We do not own a Mac. Every command on this page was run on one that GitHub rents out by the minute, and each box of output is a file that Mac wrote. The last section shows the whole arrangement, so that you can check us or do the same.
🇹🇭 ไทย
ไฟล์ .py ไม่ใช่โปรแกรมที่รันได้ด้วยตัวเอง มันรันได้เฉพาะบนคอมพิวเตอร์ที่มี Python ในเวอร์ชันที่เหมาะสม และมีไลบรารีทุกตัวที่ไฟล์นั้น import คนส่วนใหญ่ที่คุณอยากส่งให้ไม่มีสิ่งเหล่านี้เลย คำตอบที่ใช้กันทั่วไปคือ PyInstaller มันอ่านสคริปต์ของคุณ หาว่าสคริปต์ต้องใช้อะไรบ้าง แล้วคัดลอกทั้งหมดนั้น รวมทั้ง Python ลงในสิ่งเดียวที่คุณส่งต่อได้
หน้านี้ทำสิ่งนั้นบนแมค คุณจะบิลด์หน้าต่างยี่สิบบรรทัดสี่แบบ อ่าน log ของตัวแพ็กเองเพื่อดูว่ามันทำอะไร และเปิดแอปที่ได้เพื่อดูว่าข้างในมีอะไร จากนั้นคุณจะได้รู้จัก Gatekeeper ซึ่งเป็นส่วนของ macOS ที่ตัดสินว่าแอปจากคนอื่นเปิดได้หรือไม่ ต่อด้วยไฟล์สำหรับ release บน GitHub ซึ่งมีตัวติดตั้งรวมอยู่ด้วย และเอเจนต์ AI จากหน้า Agno: บริษัทของเอเจนต์ AI ที่แพ็กด้วยวิธีเดียวกัน
คุณต้องมีแมค โปรแกรม Terminal ของมัน และ Python 3 ที่มี tkinter คุณพิมพ์แต่ละคำสั่งแล้วเทียบสิ่งที่ได้กับสิ่งที่เราได้ หน้านี้เป็นหน้าคู่ฝั่งแมคของ Python to EXE ซึ่งทำแบบเดียวกันบน Windows และไปไกลกว่า ถึงขั้นเกมที่มีโมเดล AI อยู่ข้างใน
เราไม่มีแมค ทุกคำสั่งในหน้านี้รันบนแมคที่ GitHub ให้เช่าเป็นรายนาที และกล่องผลลัพธ์แต่ละกล่องคือไฟล์ที่แมคเครื่องนั้นเขียน ส่วนสุดท้ายแสดงวิธีการทั้งหมด เพื่อให้คุณตรวจสอบเราหรือทำแบบเดียวกันได้
🇨🇳 中文
.py 文件本身并不是一个能独立运行的程序。它只能在装了 Python 的电脑上运行,版本要合适,文件 import 的每一个库也都得装好。而你想给的那些人,大多数一样都没有。常用的办法是 PyInstaller:它读你的脚本,弄清脚本需要的一切,然后把这些东西连同 Python 一起,复制成一个可以交给别人的东西。
这一页在 Mac 上做这件事。你会把一个二十行的窗口用四种方式构建出来,读打包工具自己的日志来看它做了什么,再打开它做出的应用,看看里面有什么。接着你会遇到 Gatekeeper,它是 macOS 里决定别人给的应用能不能打开的那一部分。然后是放到 GitHub 发布页上的文件,其中有一个安装包;还有《Agno:AI 智能体公司》里的一个 AI 智能体,用同样的办法打包。
你需要一台 Mac、它的“终端”,以及带 tkinter 的 Python 3。每条命令你都自己输入,再把得到的结果和我们的对比。这一页是 Python to EXE 在 Mac 上的姊妹篇;那一页在 Windows 上做同样的事,而且走得更远,做到了里面装着 AI 模型的游戏。
我们没有 Mac。这一页的每条命令,都运行在 GitHub 按分钟出租的一台 Mac 上,每一个输出框都是那台 Mac 写出的文件。最后一节把整套做法都摆了出来,你可以检查我们,也可以照着做。
file command calls it arm64. A Mac with an Intel chip cannot run it.PyInstaller บิลด์ให้คอมพิวเตอร์ชนิดที่มันกำลังรันอยู่เท่านั้น ไม่ใช่ชนิดอื่น เอกสารของมันบอกว่ามันไม่ใช่ cross-compiler: ถ้าจะทำแอปของ Windows ต้องรันมันบน Windows ดังนั้นแอปของแมคจึงต้องบิลด์บนแมค และยังมีข้อจำกัดอีกชั้นซ้อนอยู่ข้างใน แมคของเราใช้ชิปของ Apple และแอปที่มันบิลด์ใช้ได้กับชิปของ Apple เท่านั้น คำสั่ง file ของ macOS เรียกมันว่า arm64 แมคที่ใช้ชิป Intel รันมันไม่ได้PyInstaller 只为自己正在运行的那种电脑构建应用,别的都不行。它的文档说它不是交叉编译器:要做 Windows 应用,就得在 Windows 上运行它。所以 Mac 应用必须在 Mac 上构建。这里面还套着第二个限制。我们的 Mac 用的是 Apple 芯片,它构建出的应用也只能用在 Apple 芯片上:macOS 的 file 命令把它叫作 arm64。用 Intel 芯片的 Mac 运行不了它。
| Fileไฟล์文件 | What it isคืออะไร它是什么 | Where you use itใช้ตรงไหน用在哪里 |
|---|---|---|
| hello.py | The smallest app worth packing: one window, one sentence, one button. Twenty lines.แอปที่เล็กที่สุดที่ยังคุ้มจะแพ็ก: หน้าต่างหนึ่งบาน ประโยคหนึ่งประโยค ปุ่มหนึ่งปุ่ม ยี่สิบบรรทัด值得打包的最小应用:一个窗口、一句话、一个按钮,共二十行。 | Steps 1 to 6ขั้นที่ 1 ถึง 6第 1 到 6 步 |
| package.py | Makes the files for a release: a zip, a disk image, an installer and their fingerprints.สร้างไฟล์สำหรับ release: ไฟล์ zip ดิสก์อิมเมจ ตัวติดตั้ง และลายนิ้วมือของแต่ละไฟล์生成发布用的文件:zip、磁盘映像、安装包,以及它们的指纹。 | Steps 7 and 8ขั้นที่ 7 และ 8第 7、8 步 |
| cashier.py | The cashier from the Agno page, an AI agent. It is that page's file, unchanged.แคชเชียร์จากหน้า Agno ซึ่งเป็นเอเจนต์ AI เป็นไฟล์ของหน้านั้น ไม่ได้แก้อะไรAgno 那一页里的收银员,一个 AI 智能体。就是那一页的文件,原样没改。 | Step 9ขั้นที่ 9第 9 步 |
| Wordคำ词 | What it means on this pageความหมายในหน้านี้在这一页里的意思 |
|---|---|
| app | A program you start with a double-click. On a Mac its name ends in .app.แอป: โปรแกรมที่เปิดด้วยการดับเบิลคลิก บนแมคชื่อของมันลงท้ายด้วย .app应用:双击就能启动的程序。在 Mac 上,它的名字以 .app 结尾。 |
| bundle | What a .app really is: a folder that the Finder shows as one thing.บันเดิล: สิ่งที่ .app เป็นจริง ๆ คือโฟลเดอร์ที่ Finder แสดงเป็นของชิ้นเดียวbundle:.app 的真面目,是一个被“访达”显示成一个东西的文件夹。 |
| Terminal | The Mac program in which you type commands.โปรแกรมของแมคที่ใช้พิมพ์คำสั่งMac 上用来输入命令的程序,中文名叫“终端”。 |
| virtual environment | A separate copy of Python with its own libraries, kept in a folder.สภาพแวดล้อมเสมือน: Python อีกชุดหนึ่งที่มีไลบรารีของตัวเอง เก็บไว้ในโฟลเดอร์虚拟环境:一份独立的 Python,有自己的库,放在一个文件夹里。 |
| signature | A mark on an app that says who made it and that nobody has changed it since.ลายเซ็น: เครื่องหมายบนแอปที่บอกว่าใครเป็นผู้ทำ และไม่มีใครแก้ไขมันหลังจากนั้น签名:应用上的一个标记,说明它是谁做的,而且此后没有人改动过。 |
| Gatekeeper | The part of macOS that checks an app from somebody else before it may open.ส่วนของ macOS ที่ตรวจแอปจากคนอื่นก่อนจะยอมให้เปิดmacOS 里的一个部分:别人给的应用,先由它检查,才允许打开。 |
📦 Tutorial 1 — The smallest app · บทที่ 1 — แอปที่เล็กที่สุด · 教程 1 —— 最小的应用
🇬🇧 English
Three steps take a script to an app. The script is small on purpose, so that everything you see afterwards comes from the packager and not from your code.
🇹🇭 ไทย
สามขั้นพาสคริปต์ไปเป็นแอป สคริปต์นี้ตั้งใจให้เล็ก เพื่อให้ทุกอย่างที่คุณเห็นหลังจากนี้มาจากตัวแพ็ก ไม่ใช่จากโค้ดของคุณ
🇨🇳 中文
三步就能把脚本变成应用。这个脚本故意写得很小,这样你之后看到的一切都来自打包工具,而不是来自你的代码。
1 A Python of its own for the build · Python อีกชุดสำหรับการบิลด์ · 专门用来构建的一份 Python
🇬🇧 English
Save this file as hello.py in a new folder. It opens one window, and it prints one line to the Terminal.
🇹🇭 ไทย
บันทึกไฟล์นี้ชื่อ hello.py ในโฟลเดอร์ใหม่ มันเปิดหน้าต่างหนึ่งบาน และพิมพ์หนึ่งบรรทัดลงใน Terminal
🇨🇳 中文
把这个文件存成 hello.py,放进一个新文件夹。它会打开一个窗口,并在“终端”里打印一行字。
"""hello.py - the smallest app: one window, one sentence and one button.
python hello.py
"""
import tkinter as tk
def main():
print("hello.py is running.") # this goes to the terminal, not to the window
root = tk.Tk()
root.title("hello")
words = tk.Label(root, text="Hello from my own app.", font=("Arial", 20))
words.pack(padx=40, pady=30)
button = tk.Button(root, text="Close", command=root.destroy)
button.pack(pady=20)
root.mainloop()
if __name__ == "__main__":
main()
🇬🇧 English
PyInstaller is a library, so it has to be installed into some Python. Give it one of its own. The first command makes a virtual environment in a folder named venv, the second installs PyInstaller into it, and the third runs your script with that Python, to see the window before anything is packed.
🇹🇭 ไทย
PyInstaller เป็นไลบรารี จึงต้องติดตั้งลงใน Python สักชุดหนึ่ง ให้มันมี Python ของตัวเอง คำสั่งแรกสร้างสภาพแวดล้อมเสมือนในโฟลเดอร์ชื่อ venv คำสั่งที่สองติดตั้ง PyInstaller ลงไป และคำสั่งที่สามรันสคริปต์ของคุณด้วย Python ชุดนั้น เพื่อดูหน้าต่างก่อนจะแพ็กอะไร
🇨🇳 中文
PyInstaller 是一个库,所以得装进某一份 Python 里。给它单独准备一份。第一条命令在名叫 venv 的文件夹里建一个虚拟环境,第二条把 PyInstaller 装进去,第三条用这份 Python 运行你的脚本,在打包之前先看看那个窗口。
python3 -m venv venv
venv/bin/python -m pip install pyinstaller
venv/bin/python hello.py
venv, and -m tells that Python to run a module it has installed. So you can always see which Python does the work, and nothing has to be switched on first. It matters because PyInstaller builds the app out of the Python that runs it. Mind the capital P and I in PyInstaller.ทุกคำสั่งระบุ Python ที่อยู่ใน venv และ -m บอกให้ Python ชุดนั้นรันโมดูลที่ติดตั้งไว้ คุณจึงเห็นได้เสมอว่า Python ตัวไหนกำลังทำงาน และไม่ต้องเปิดใช้อะไรก่อน เรื่องนี้สำคัญเพราะ PyInstaller บิลด์แอปจาก Python ตัวที่รันมัน ระวังตัว P ใหญ่และตัว I ใหญ่ใน PyInstaller每条命令都点名用 venv 里的那份 Python,-m 则让这份 Python 运行它装好的一个模块。这样你随时看得见是哪一份 Python 在干活,也不用先“激活”什么。这一点要紧,因为 PyInstaller 是用运行它的那份 Python 来构建应用的。注意 PyInstaller 里大写的 P 和 I。
2 Build it: a folder · บิลด์: ได้โฟลเดอร์ · 构建:得到一个文件夹
🇬🇧 English
One command does it. PyInstaller reads hello.py, follows its imports, and finds that the script needs Python itself and tkinter, which needs the files of Tcl/Tk. It copies all of that into a new folder.
🇹🇭 ไทย
คำสั่งเดียวก็เสร็จ PyInstaller อ่าน hello.py ตามรอย import ของมัน แล้วพบว่าสคริปต์ต้องใช้ Python เองและ tkinter ซึ่งต้องใช้ไฟล์ของ Tcl/Tk มันคัดลอกทั้งหมดนั้นลงในโฟลเดอร์ใหม่
🇨🇳 中文
一条命令就够了。PyInstaller 读 hello.py,顺着它的 import 找下去,发现脚本需要 Python 本身和 tkinter,而 tkinter 又需要 Tcl/Tk 的文件。它把这些全都复制进一个新文件夹。
venv/bin/python -m PyInstaller hello.py
dist/hello/hello
| New beside your scriptของใหม่ข้างสคริปต์脚本旁边多出来的 | What it isคืออะไร它是什么 |
|---|---|
| dist | The result. Inside is a folder named hello, and inside that is a program named hello. This is the only one of the three that you give to anybody.ผลลัพธ์ ข้างในมีโฟลเดอร์ชื่อ hello และในนั้นมีโปรแกรมชื่อ hello ในสามอย่างนี้ มีอย่างนี้อย่างเดียวที่คุณส่งให้คนอื่น成品。里面有一个叫 hello 的文件夹,文件夹里是一个叫 hello 的程序。三样东西里,只有这一样是要交给别人的。 |
| build | PyInstaller's working files. You can delete this folder at any time.ไฟล์ระหว่างทำงานของ PyInstaller ลบโฟลเดอร์นี้ได้ทุกเมื่อPyInstaller 的工作文件。这个文件夹随时可以删掉。 |
| hello.spec | The recipe for this build, written out as a file. You can ignore it for everything on this page.สูตรของการบิลด์ครั้งนี้ที่เขียนออกมาเป็นไฟล์ ในหน้านี้ไม่ต้องสนใจมัน这次构建的“配方”,写成了一个文件。这一页里用不到它,可以不管。 |
🇬🇧 English
Our folder was 26.4 MB in 365 files. The second command above starts the program from the Terminal. The window opens, and the printed line appears in the Terminal, because this build is an ordinary command-line program that happens to open a window.
It is not yet an app. What you would hand over is a folder with the program and its supporting files in it, and the other person has to know which of them to start.
🇹🇭 ไทย
โฟลเดอร์ของเรามีขนาด 26.4 MB ใน 365 ไฟล์ คำสั่งที่สองข้างบนเริ่มโปรแกรมจาก Terminal หน้าต่างเปิดขึ้น และบรรทัดที่พิมพ์ก็ปรากฏใน Terminal เพราะการบิลด์แบบนี้ได้โปรแกรมบรรทัดคำสั่งธรรมดาที่บังเอิญเปิดหน้าต่างด้วย
มันยังไม่ใช่แอป สิ่งที่คุณจะส่งให้คนอื่นคือโฟลเดอร์ที่มีตัวโปรแกรมกับไฟล์ประกอบของมัน และอีกฝ่ายต้องรู้ว่าจะเริ่มไฟล์ไหน
🇨🇳 中文
我们的文件夹是 26.4 MB,共 365 个文件。上面第二条命令从“终端”里启动这个程序。窗口打开了,打印的那一行也出现在“终端”里,因为这样构建出来的是一个普通的命令行程序,只是它顺便开了个窗口。
它还算不上应用。你要交出去的是一个文件夹,里面有程序和它的配套文件,对方还得知道该启动哪一个。
3 A real app, and one file · แอปจริง และไฟล์เดียว · 真正的应用,以及单个文件
🇬🇧 English
Two options change the shape of what you get.
--windowed is the one that makes a Mac app. With it PyInstaller also writes dist/hello.app, which the Finder shows as one thing with an icon, and which opens with a double-click and no Terminal. The line that hello.py prints now goes nowhere, so a program built this way has to put its messages in its own window.
--onefile packs everything into a single file. Each time that file runs, it first unpacks itself into a temporary folder, runs from there, and removes the folder when it stops.
🇹🇭 ไทย
มีตัวเลือกสองตัวที่เปลี่ยนรูปร่างของสิ่งที่คุณได้
--windowed คือตัวที่ทำให้ได้แอปของแมค เมื่อใส่ตัวนี้ PyInstaller จะเขียน dist/hello.app เพิ่มด้วย ซึ่ง Finder แสดงเป็นของชิ้นเดียวที่มีไอคอน เปิดได้ด้วยการดับเบิลคลิก และไม่มี Terminal บรรทัดที่ hello.py พิมพ์จึงไม่ไปปรากฏที่ไหน โปรแกรมที่บิลด์แบบนี้จึงต้องแสดงข้อความในหน้าต่างของตัวเอง
--onefile แพ็กทุกอย่างรวมเป็นไฟล์เดียว ทุกครั้งที่ไฟล์นั้นรัน มันจะแตกไฟล์ตัวเองลงโฟลเดอร์ชั่วคราวก่อน รันจากที่นั่น แล้วลบโฟลเดอร์เมื่อหยุด
🇨🇳 中文
有两个选项会改变成品的样子。
--windowed 才是做出 Mac 应用的那一个。加上它,PyInstaller 还会写出 dist/hello.app:“访达”把它显示成带图标的一个东西,双击就能打开,没有“终端”。hello.py 打印的那一行这时哪儿也去不了,所以这样构建的程序,得把消息显示在自己的窗口里。
--onefile 把一切打包成单个文件。这个文件每次运行,都要先把自己解压到一个临时文件夹,从那里运行,停止时再把文件夹删掉。
venv/bin/python -m PyInstaller --windowed hello.py
open dist/hello.app
| Optionsตัวเลือก选项 | What is in distมีอะไรใน distdist 里有什么 | Sizeขนาด大小 |
|---|---|---|
| noneไม่มี不加 | the folder helloโฟลเดอร์ hello文件夹 hello | 26.4 MB in 365 files26.4 MB ใน 365 ไฟล์26.4 MB,共 365 个文件 |
--windowed | hello.app, and the folder hello againhello.app และโฟลเดอร์ hello อีกครั้งhello.app,外加又一个文件夹 hello | 26.7 MB in 369 files26.7 MB ใน 369 ไฟล์26.7 MB,共 369 个文件 |
--onefile | the file helloไฟล์ hello文件 hello | 9.7 MB in 1 file9.7 MB ใน 1 ไฟล์9.7 MB,共 1 个文件 |
--onefile --windowed | hello.app, and the file hellohello.app และไฟล์ hellohello.app,外加文件 hello | 9.8 MB in 4 files9.8 MB ใน 4 ไฟล์9.8 MB,共 4 个文件 |
🇬🇧 English
Every one of the four opened its window in about a second or less. The one-file builds are smaller because the file is compressed, and they pay for it at each start. We watched the unpacking happen: while the one-file hello was open, a folder whose name begins with _MEI sat in the temporary folder with 366 files and 40.9 MB in it, and after the program stopped it was gone.
For a Mac app, build the folder kind: --windowed without --onefile. PyInstaller's documentation says that the two together on a Mac, while possible, are not recommended, because the app then unpacks itself on every run.
What our measuring script printed is below. The script is in the checks, so you can make your own numbers.
🇹🇭 ไทย
ทั้งสี่แบบเปิดหน้าต่างได้ในราวหนึ่งวินาทีหรือน้อยกว่า แบบไฟล์เดียวเล็กกว่าเพราะไฟล์ถูกบีบอัด และต้องแลกด้วยเวลาทุกครั้งที่เริ่ม เราเฝ้าดูการแตกไฟล์เกิดขึ้นจริง: ระหว่างที่ hello แบบไฟล์เดียวเปิดอยู่ มีโฟลเดอร์ที่ชื่อขึ้นต้นด้วย _MEI อยู่ในโฟลเดอร์ชั่วคราว มี 366 ไฟล์ ขนาด 40.9 MB และเมื่อโปรแกรมหยุด โฟลเดอร์นั้นก็หายไป
สำหรับแอปของแมค ให้บิลด์แบบโฟลเดอร์: ใส่ --windowed โดยไม่ใส่ --onefile เอกสารของ PyInstaller บอกว่าการใช้สองตัวนี้ร่วมกันบนแมคนั้นทำได้ แต่ไม่แนะนำ เพราะแอปจะแตกไฟล์ตัวเองทุกครั้งที่รัน
สิ่งที่สคริปต์วัดผลของเราพิมพ์ออกมาอยู่ข้างล่าง สคริปต์นั้นอยู่ในชุดตรวจสอบ คุณจึงวัดตัวเลขของคุณเองได้
🇨🇳 中文
四种构建都在大约一秒或更短的时间里打开了窗口。单文件的构建更小,因为文件经过了压缩,代价是每次启动都要多花时间。我们亲眼看到了解压的过程:单文件的 hello 开着的时候,临时文件夹里有一个名字以 _MEI 开头的文件夹,里面有 366 个文件、40.9 MB;程序停止后,它就不见了。
做 Mac 应用,请用文件夹式的构建:加 --windowed,不加 --onefile。PyInstaller 的文档说,在 Mac 上把这两个一起用,虽然可行,但不推荐,因为那样应用每次运行都要解压自己。
我们的测量脚本打印的内容在下面。这个脚本就在检查里,你可以测出自己的数字。
Python 3.12.10 with Tk 8.6.16, PyInstaller 6.22.3, macOS 15.7.9, arm64, 3 logical cores
plain Python
venv/bin/python hello.py
window after 0.86 s 1 process(es), 77 MB of memory, stopped with code 1
hello, one folder
venv/bin/python -m PyInstaller hello.py
build 8.7 s in dist: hello/
hello: 26.4 MB in 365 files and 4 links
first start 0.37 s later starts 0.25 s
open for 5 s: 1 process(es), 80 MB of memory, stopped with code 1
as a zip: 9.7 MB, made in 1 s
hello, an app
venv/bin/python -m PyInstaller --windowed hello.py
build 5.2 s in dist: hello.app/ hello/
hello.app: 26.7 MB in 369 files and 15 links
first start 0.34 s later starts 0.22 s
open for 5 s: 1 process(es), 78 MB of memory, stopped with code 1
through "open hello.app": window after 0.57 s
as a zip: 9.8 MB, made in 1 s
hello, one file
venv/bin/python -m PyInstaller --onefile hello.py
build 6.8 s in dist: hello
hello: 9.7 MB in 1 files and 0 links
first start 0.47 s later starts 0.38 s
open for 5 s: 2 process(es), 81 MB of memory, stopped with code 1
as a zip: 9.5 MB, made in 0 s
hello, one file, and an app
venv/bin/python -m PyInstaller --onefile --windowed hello.py
build 6.2 s in dist: hello hello.app/
hello.app: 9.8 MB in 4 files and 0 links
first start 0.60 s later starts 0.60 s
open for 5 s: 2 process(es), 96 MB of memory, stopped with code 1
through "open hello.app": window after 0.66 s
as a zip: 9.6 MB, made in 0 s
4 builds in all, in 97 s.
While the app is open: a folder named _MEI plus some letters is in the temporary folder, with 366 files and 40.9 MB in it.
After the app has stopped: 0 of those folders are left.
The temporary folder is under /var/folders.
- Build with
--windowed, then look indistin the Finder. How many things are there, and which one has an icon? - Hold the Control key, click
hello.app, and choose Show Package Contents. You are now inside the app. Step 5 says what you are looking at.
- บิลด์ด้วย
--windowedแล้วดูในdistด้วย Finder มีของอยู่กี่อย่าง และอย่างไหนมีไอคอน - กดปุ่ม Control ค้างไว้ คลิกที่
hello.appแล้วเลือก Show Package Contents ตอนนี้คุณอยู่ข้างในแอปแล้ว ขั้นที่ 5 บอกว่าสิ่งที่คุณเห็นคืออะไร
- 用
--windowed构建,然后在“访达”里看dist。里面有几样东西?哪一样带图标? - 按住 Control 键点按
hello.app,选 Show Package Contents(显示包内容)。现在你就在应用里面了。第 5 步会告诉你眼前的是什么。
⚙️ Tutorial 2 — How the packager works · บทที่ 2 — ตัวแพ็กทำงานอย่างไร · 教程 2 —— 打包工具是怎样工作的
🇬🇧 English
PyInstaller does not turn your Python into another language. It makes a small starter program and puts a copy of Python and your code beside it. Its own log says so, one stage at a time.
🇹🇭 ไทย
PyInstaller ไม่ได้แปลง Python ของคุณเป็นภาษาอื่น มันสร้างโปรแกรมตัวเริ่มเล็ก ๆ ตัวหนึ่ง แล้ววางสำเนาของ Python กับโค้ดของคุณไว้ข้าง ๆ log ของมันเองบอกอย่างนั้น ทีละขั้น
🇨🇳 中文
PyInstaller 并没有把你的 Python 变成另一种语言。它做了一个很小的启动程序,再把一份 Python 和你的代码放在它旁边。它自己的日志就是这么说的,一个阶段一个阶段地说。
4 Read the build, line by line · อ่านการบิลด์ทีละบรรทัด · 一行一行读构建过程
🇬🇧 English
While it works, PyInstaller prints a line for each thing it does. The number that begins a line is the time since it started, in thousandths of a second. Our build of the app took under five seconds of that clock, and it went through six stages. The words in capitals are PyInstaller's own.
🇹🇭 ไทย
ระหว่างทำงาน PyInstaller พิมพ์หนึ่งบรรทัดต่อทุกสิ่งที่มันทำ ตัวเลขที่ขึ้นต้นบรรทัดคือเวลาตั้งแต่เริ่ม หน่วยเป็นหนึ่งในพันวินาที การบิลด์แอปของเราใช้เวลาตามนาฬิกานั้นไม่ถึงห้าวินาที และผ่านหกขั้น คำที่เป็นตัวพิมพ์ใหญ่เป็นคำของ PyInstaller เอง
🇨🇳 中文
PyInstaller 一边干活,一边为它做的每件事打印一行。行首的数字是从开始算起的时间,单位是千分之一秒。我们这次构建应用,按这个钟不到五秒,一共经过六个阶段。那些大写的词是 PyInstaller 自己的叫法。
| Stage in the logขั้นใน log日志里的阶段 | What happensเกิดอะไรขึ้น发生了什么 |
|---|---|
| Analysis | It reads hello.py and follows every import, and the imports of those files, until it has the whole list. For some libraries it has notes of its own, named hooks: the log shows hook-tkinter.py, which is how it knows that tkinter needs Tcl/Tk. This stage is most of the time.มันอ่าน hello.py และตามรอยทุก import รวมทั้ง import ของไฟล์เหล่านั้น จนได้รายการครบ สำหรับไลบรารีบางตัวมันมีบันทึกของตัวเอง เรียกว่า hook: ใน log มี hook-tkinter.py ซึ่งทำให้มันรู้ว่า tkinter ต้องใช้ Tcl/Tk ขั้นนี้ใช้เวลามากที่สุด它读 hello.py,顺着每一个 import 找下去,再顺着那些文件的 import 找,直到列出完整的清单。对有些库,它有自己的备注,叫作 hook:日志里出现了 hook-tkinter.py,它就是靠这个知道 tkinter 需要 Tcl/Tk。大部分时间花在这个阶段。 |
| PYZ | It compresses the Python modules on that list into one archive.มันบีบอัดโมดูล Python ในรายการนั้นรวมเป็นไฟล์เก็บถาวรไฟล์เดียว它把清单上的 Python 模块压缩成一个归档文件。 |
| PKG | It wraps that archive, with your script, into a package the starter program can read.มันห่อไฟล์เก็บถาวรนั้นกับสคริปต์ของคุณ เป็นแพ็กเกจที่โปรแกรมตัวเริ่มอ่านได้它把这个归档连同你的脚本,包成启动程序读得懂的一个包。 |
| EXE | It copies its starter program, which it calls the bootloader, sets it to this Mac's kind of chip (arm64), and appends the package to it. That file is the hello you start.มันคัดลอกโปรแกรมตัวเริ่มของมัน ซึ่งมันเรียกว่า bootloader ตั้งให้ตรงกับชนิดชิปของแมคเครื่องนี้ (arm64) แล้วต่อแพ็กเกจเข้าที่ท้ายไฟล์ ไฟล์นั้นคือ hello ที่คุณเริ่ม它复制自己的启动程序(它称之为 bootloader),把它设成这台 Mac 的芯片类型(arm64),再把那个包接在它后面。这个文件就是你启动的那个 hello。 |
| COLLECT | It gathers the starter, the Python library and every other file the app needs into one folder.มันรวบรวมตัวเริ่ม ไลบรารีของ Python และไฟล์อื่นทุกไฟล์ที่แอปต้องใช้ ไว้ในโฟลเดอร์เดียว它把启动程序、Python 的库和应用需要的其他所有文件,收进一个文件夹。 |
| BUNDLE | Only with --windowed: it arranges that folder as a .app, writes the app's Info.plist, and signs it. The log says Signing the BUNDLE.... Step 6 says what kind of signature that is.เฉพาะเมื่อใส่ --windowed: มันจัดโฟลเดอร์นั้นให้เป็น .app เขียน Info.plist ของแอป และเซ็นแอป ใน log เขียนว่า Signing the BUNDLE... ขั้นที่ 6 บอกว่าเป็นลายเซ็นแบบไหน只有加了 --windowed 才有:它把那个文件夹整理成 .app,写出应用的 Info.plist,并给它签名。日志里写着 Signing the BUNDLE...。第 6 步会说这是哪一种签名。 |
Show the whole log of one build · แสดง log ทั้งหมดของการบิลด์หนึ่งครั้ง · 显示一次构建的完整日志
$ venv/bin/python -m PyInstaller --windowed hello.py
36 INFO: PyInstaller: 6.22.3, contrib hooks: 2026.8
36 INFO: Python: 3.12.10
47 INFO: Platform: macOS-15.7.9-arm64-arm-64bit
47 INFO: Python environment: /Users/runner/krueng/python_to_mac_app_assets/venv
47 INFO: wrote /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/hello.spec
51 INFO: Module search paths (PYTHONPATH):
['/Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log',
'/Library/Frameworks/Python.framework/Versions/3.12/lib/python312.zip',
'/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12',
'/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload',
'/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages',
'/Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log']
160 INFO: checking Analysis
160 INFO: Building Analysis because Analysis-00.toc is non existent
160 INFO: Looking for Python shared library...
163 INFO: Using Python shared library: /Library/Frameworks/Python.framework/Versions/3.12/Python
163 INFO: Running Analysis Analysis-00.toc
163 INFO: Target bytecode optimization level: 0
164 INFO: Initializing module dependency graph...
164 INFO: Initializing module graph hook caches...
170 INFO: Analyzing modules for base_library.zip ...
814 INFO: Processing standard module hook 'hook-encodings.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
1032 INFO: Processing standard module hook 'hook-math.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
1882 INFO: Processing standard module hook 'hook-pickle.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
2639 INFO: Processing standard module hook 'hook-heapq.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
3028 INFO: Caching module dependency graph...
3054 INFO: Analyzing /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/hello.py
3054 INFO: Processing pre-find-module-path hook 'hook-tkinter.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks/pre_find_module_path'
3056 INFO: TclTkInfo: initializing cached Tcl/Tk info...
3224 INFO: Processing standard module hook 'hook-_tkinter.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
3229 INFO: Processing module hooks (post-graph stage)...
3229 INFO: Processing standard module hook 'hook-_tkinter.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks'
3235 INFO: Performing binary vs. data reclassification (322 entries)
3257 INFO: Looking for ctypes DLLs
3265 INFO: Analyzing run-time hooks ...
3266 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
3270 INFO: Including run-time hook 'pyi_rth__tkinter.py' from '/Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
3280 INFO: Creating base_library.zip...
3303 INFO: Looking for dynamic libraries
3497 INFO: Warnings written to /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/build/hello/warn-hello.txt
3503 INFO: Graph cross-reference written to /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/build/hello/xref-hello.html
3598 INFO: checking PYZ
3598 INFO: Building PYZ because PYZ-00.toc is non existent
3598 INFO: Building PYZ (ZlibArchive) /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/build/hello/PYZ-00.pyz
3728 INFO: Building PYZ (ZlibArchive) /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/build/hello/PYZ-00.pyz completed successfully.
3729 INFO: EXE target arch: arm64
3729 INFO: Code signing identity: None
3732 INFO: checking PKG
3732 INFO: Building PKG because PKG-00.toc is non existent
3732 INFO: Building PKG (CArchive) hello.pkg
3745 INFO: Building PKG (CArchive) hello.pkg completed successfully.
3745 INFO: Bootloader /Users/runner/krueng/python_to_mac_app_assets/venv/lib/python3.12/site-packages/PyInstaller/bootloader/Darwin-64bit/runw
3745 INFO: checking EXE
3746 INFO: Building EXE because EXE-00.toc is non existent
3746 INFO: Building EXE from EXE-00.toc
3746 INFO: Copying bootloader EXE to /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/build/hello/hello
3747 INFO: Converting EXE to target arch (arm64)
3768 INFO: Removing signature(s) from EXE
3783 INFO: Modifying Mach-O image UUID(s) in EXE
3785 INFO: Appending PKG archive to EXE
3785 INFO: Fixing EXE headers for code signing
3791 INFO: Rewriting the executable's macOS SDK version (26.2.0) to match the SDK version of the Python library (12.1.0) in order to avoid inconsistent behavior and potential UI issues in the frozen application.
3792 INFO: Re-signing the EXE
3810 INFO: Building EXE from EXE-00.toc completed successfully.
3813 INFO: checking COLLECT
3813 INFO: Building COLLECT because COLLECT-00.toc is non existent
3813 INFO: Building COLLECT COLLECT-00.toc
3999 INFO: Building COLLECT COLLECT-00.toc completed successfully.
4007 INFO: checking BUNDLE
4007 INFO: Building BUNDLE because BUNDLE-00.toc is non existent
4007 INFO: Building BUNDLE BUNDLE-00.toc
4195 INFO: Signing the BUNDLE...
4315 INFO: Building BUNDLE BUNDLE-00.toc completed successfully.
4319 INFO: Build complete! The results are available in: /Users/runner/krueng/python_to_mac_app_assets/checks/made/build_log/dist
exit code 0
Now in the folder: build/ dist/ hello.py hello.spec
🇬🇧 English
Two things follow from the first stage. PyInstaller packs what your code imports, not everything that is installed, so a library sitting unused in venv stays out. And it can only follow imports it can read in the code. A library that loads something by a name worked out while the program runs needs your help, with the option --hidden-import.
🇹🇭 ไทย
จากขั้นแรกมีสองเรื่องที่ตามมา PyInstaller แพ็กสิ่งที่โค้ดของคุณ import ไม่ใช่ทุกอย่างที่ติดตั้งไว้ ไลบรารีที่อยู่ใน venv แต่ไม่ได้ใช้จึงไม่ถูกแพ็ก และมันตามได้เฉพาะ import ที่อ่านได้จากโค้ด ไลบรารีที่โหลดบางอย่างด้วยชื่อที่คิดขึ้นตอนโปรแกรมรัน ต้องให้คุณช่วย ด้วยตัวเลือก --hidden-import
🇨🇳 中文
第一个阶段带来两个结论。PyInstaller 打包的是你的代码 import 的东西,不是装着的所有东西,所以 venv 里没用到的库不会被打包。另外,它只能顺着代码里读得到的 import 去找。如果一个库是在程序运行时才算出名字、再去加载某个东西,那就得你来帮忙,用 --hidden-import 选项。
5 What is inside the app · ข้างในแอปมีอะไร · 应用里面有什么
🇬🇧 English
A .app is a folder. Everything is in one folder inside it, named Contents, and macOS expects certain names there.
🇹🇭 ไทย
.app คือโฟลเดอร์ ทุกอย่างอยู่ในโฟลเดอร์เดียวข้างใน ชื่อ Contents และ macOS คาดว่าจะพบชื่อบางชื่อในนั้น
🇨🇳 中文
.app 是一个文件夹。所有东西都在它里面一个叫 Contents 的文件夹里,macOS 认定那里要有某些固定的名字。
hello.app: 26.7 MB
The folders of the app:
20.5 MB Contents/Frameworks a folder 11 things in it
0.0 MB Contents/Info.plist a file
1.5 MB Contents/MacOS a folder 1 things in it
4.5 MB Contents/Resources a folder 11 things in it
0.1 MB Contents/_CodeSignature a folder 1 things in it
The biggest things in those folders:
8.0 MB Contents/Frameworks/Python.framework/
5.8 MB Contents/Frameworks/python3__dot__12/
3.6 MB Contents/Frameworks/libcrypto.3.dylib
1.9 MB Contents/Resources/_tcl_data/
1.6 MB Contents/Frameworks/libtcl8.6.dylib
1.6 MB Contents/Frameworks/libtk8.6.dylib
1.5 MB Contents/MacOS/hello
1.3 MB Contents/Resources/base_library.zip
0.8 MB Contents/Resources/_tk_data/
0.3 MB Contents/Resources/tcl8/
Links among them: 12. The first three:
Contents/Frameworks/Python -> Python.framework/Versions/3.12/Python
Contents/Frameworks/_tcl_data -> ../Resources/_tcl_data
Contents/Frameworks/_tk_data -> ../Resources/_tk_data
Info.plist says:
CFBundleDisplayName: hello
CFBundleExecutable: hello
CFBundleIconFile: icon-windowed.icns
CFBundleIdentifier: hello
CFBundleInfoDictionaryVersion: 6.0
CFBundleName: hello
CFBundlePackageType: APPL
CFBundleShortVersionString: 0.0.0
NSHighResolutionCapable: True
| In Contentsใน ContentsContents 里的 | What it holdsเก็บอะไรไว้里面放着什么 |
|---|---|
| MacOS | One file: the starter program with your script inside it. It is 1.5 MB, and it is the only part made for you.ไฟล์เดียว: โปรแกรมตัวเริ่มที่มีสคริปต์ของคุณอยู่ข้างใน ขนาด 1.5 MB และเป็นส่วนเดียวที่ทำขึ้นเพื่อคุณ只有一个文件:里面装着你的脚本的启动程序。它是 1.5 MB,也是唯一专门为你做出来的部分。 |
| Frameworks | Python itself and the libraries it is built from. Most of the app's size is here: Python's own library is 8.0 MB.ตัว Python เองและไลบรารีที่ประกอบขึ้นเป็นมัน ขนาดส่วนใหญ่ของแอปอยู่ที่นี่: ไลบรารีของ Python เองมีขนาด 8.0 MBPython 本身,以及构成它的那些库。应用的大部分体积在这里:Python 自己的库就有 8.0 MB。 |
| Resources | Files that are not programs: the data of Tcl/Tk, the icon, and base_library.zip, the Python modules that are needed first.ไฟล์ที่ไม่ใช่โปรแกรม: ข้อมูลของ Tcl/Tk ไอคอน และ base_library.zip ซึ่งเป็นโมดูล Python ที่ต้องใช้ก่อนอย่างอื่น不是程序的文件:Tcl/Tk 的数据、图标,以及 base_library.zip,也就是最先要用到的那些 Python 模块。 |
| Info.plist | A small file in which the app tells macOS its name, its icon and which program to start. CFBundleExecutable: hello is that last one.ไฟล์เล็ก ๆ ที่แอปใช้บอก macOS ว่าตัวเองชื่ออะไร ใช้ไอคอนไหน และต้องเริ่มโปรแกรมไหน CFBundleExecutable: hello คือข้อสุดท้ายนั้น一个小文件,应用在里面告诉 macOS 自己叫什么、用哪个图标、该启动哪个程序。CFBundleExecutable: hello 说的就是最后这一项。 |
| _CodeSignature | The signature: a list of every file in the app with a fingerprint of each.ลายเซ็น: รายการของทุกไฟล์ในแอป พร้อมลายนิ้วมือของแต่ละไฟล์签名:应用里每个文件的清单,各带一个指纹。 |
🇬🇧 English
When you double-click the app, macOS reads Info.plist and starts the program in MacOS. That starter loads the Python library from Frameworks, and Python then runs your script from the package inside the starter. Your .py file is not in the app as a file you can read. It is in there as compiled Python.
Twelve of the things in those folders are links: names that point at a file kept somewhere else in the app. They are why a Mac app should be zipped with the Mac's own tools, as step 7 does.
🇹🇭 ไทย
เมื่อคุณดับเบิลคลิกแอป macOS จะอ่าน Info.plist แล้วเริ่มโปรแกรมใน MacOS ตัวเริ่มนั้นโหลดไลบรารีของ Python จาก Frameworks แล้ว Python ก็รันสคริปต์ของคุณจากแพ็กเกจที่อยู่ในตัวเริ่ม ไฟล์ .py ของคุณไม่ได้อยู่ในแอปในรูปไฟล์ที่อ่านได้ มันอยู่ในนั้นในรูป Python ที่คอมไพล์แล้ว
ของในโฟลเดอร์เหล่านั้นมีสิบสองอย่างที่เป็นลิงก์ คือชื่อที่ชี้ไปยังไฟล์ซึ่งเก็บไว้ที่อื่นในแอป นี่คือเหตุผลที่ควร zip แอปของแมคด้วยเครื่องมือของแมคเอง อย่างที่ขั้นที่ 7 ทำ
🇨🇳 中文
你双击应用时,macOS 读 Info.plist,启动 MacOS 里的程序。这个启动程序从 Frameworks 加载 Python 的库,然后 Python 从启动程序里的那个包中运行你的脚本。你的 .py 文件并不是以可读文件的形式放在应用里的,它在里面是编译过的 Python。
这些文件夹里有十二样东西是链接:也就是指向应用里别处某个文件的名字。正因为有它们,Mac 应用应该用 Mac 自己的工具来压缩,就像第 7 步做的那样。
🛂 Tutorial 3 — Give it to someone · บทที่ 3 — ส่งให้คนอื่น · 教程 3 —— 交给别人
🇬🇧 English
The app opens on the Mac that built it. On somebody else's Mac it meets Gatekeeper first, and Gatekeeper does not know you.
🇹🇭 ไทย
แอปเปิดได้บนแมคเครื่องที่บิลด์มัน แต่บนแมคของคนอื่น มันต้องเจอ Gatekeeper ก่อน และ Gatekeeper ไม่รู้จักคุณ
🇨🇳 中文
在构建它的那台 Mac 上,应用能打开。到了别人的 Mac 上,它先要过 Gatekeeper 这一关,而 Gatekeeper 并不认识你。
6 What macOS says about your app · macOS ว่าอย่างไรกับแอปของคุณ · macOS 怎么看你的应用
🇬🇧 English
macOS has commands that answer for Gatekeeper. We ran them on hello.app, and then did what a download does to a file.
🇹🇭 ไทย
macOS มีคำสั่งที่ตอบแทน Gatekeeper ได้ เรารันคำสั่งเหล่านั้นกับ hello.app แล้วทำกับไฟล์อย่างที่การดาวน์โหลดทำ
🇨🇳 中文
macOS 有几条命令可以替 Gatekeeper 回答。我们对 hello.app 运行了它们,然后又对文件做了下载会做的那件事。
$ file hello.app/Contents/MacOS/hello
hello.app/Contents/MacOS/hello: Mach-O 64-bit executable arm64
exit code 0
$ codesign --display --verbose=2 hello.app
Executable=/Users/runner/krueng/python_to_mac_app_assets/checks/made/hello_an_app/dist/hello.app/Contents/MacOS/hello
Identifier=hello
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20400 size=11838 flags=0x2(adhoc) hashes=364+3 location=embedded
Signature=adhoc
Info.plist entries=9
TeamIdentifier=not set
Sealed Resources version=2 rules=13 files=376
Internal requirements count=0 size=12
exit code 0
$ codesign --verify --deep --strict hello.app
(nothing)
exit code 0
$ spctl --assess --type execute --verbose=2 hello.app
hello.app: rejected
exit code 3
$ xattr hello.app
(nothing)
exit code 0
The app as it was built:
$ open hello.app
The open command ended with code 0.
The app's window came, after 0.6 s.
Programs that newly have something on the screen: hello
A copy with the label of a download:
$ xattr -r -w com.apple.quarantine 0083;6ac9d033;Safari; downloaded_hello.app
(nothing)
exit code 0
$ xattr downloaded_hello.app
com.apple.quarantine
exit code 0
$ spctl --assess --type execute --verbose=2 downloaded_hello.app
downloaded_hello.app: rejected
exit code 3
$ open downloaded_hello.app
The open command was still waiting after 20 s, so it was stopped.
The app's window did not come in 20 s.
Programs that newly have something on the screen: CoreServicesUIAgent
🇬🇧 English
Read it in three parts. First, the signature. Signature=adhoc and TeamIdentifier=not set mean the app is signed by nobody in particular. An Apple chip will not run a program with no signature at all, so PyInstaller gives it this empty one. It proves that the files have not changed, and it says nothing about who made them.
Second, the verdict. spctl answers rejected. That is Gatekeeper's opinion of the app, and yet the app opened on the Mac that built it, in about a second. Gatekeeper is only asked about files that carry the label com.apple.quarantine, which a browser puts on what it downloads. A file you built yourself has no such label: xattr printed nothing.
Third, the download. We made a copy of the app and put that label on it ourselves. This time the app's window did not come in twenty seconds. What appeared instead belonged to CoreServicesUIAgent, a program of macOS's own. Our script cannot read what it showed. Apple's page Open a Mac app from an unknown developer describes the message a person gets at this point.
🇹🇭 ไทย
อ่านเป็นสามส่วน ส่วนแรกคือลายเซ็น Signature=adhoc และ TeamIdentifier=not set หมายความว่าแอปนี้ไม่ได้เซ็นโดยใครเป็นพิเศษ ชิปของ Apple จะไม่รันโปรแกรมที่ไม่มีลายเซ็นเลย PyInstaller จึงใส่ลายเซ็นเปล่าแบบนี้ให้ มันพิสูจน์ว่าไฟล์ไม่ถูกเปลี่ยน แต่ไม่ได้บอกอะไรเลยว่าใครเป็นผู้ทำ
ส่วนที่สองคือคำตัดสิน spctl ตอบว่า rejected นั่นคือความเห็นของ Gatekeeper ต่อแอปนี้ แต่แอปก็ยังเปิดได้บนแมคที่บิลด์มัน ภายในราวหนึ่งวินาที Gatekeeper ถูกถามเฉพาะกับไฟล์ที่มีป้าย com.apple.quarantine ซึ่งเบราว์เซอร์ติดให้กับสิ่งที่มันดาวน์โหลด ไฟล์ที่คุณบิลด์เองไม่มีป้ายนั้น: xattr ไม่พิมพ์อะไรออกมา
ส่วนที่สามคือการดาวน์โหลด เราทำสำเนาของแอปแล้วติดป้ายนั้นเอง คราวนี้หน้าต่างของแอปไม่ขึ้นมาภายในยี่สิบวินาที สิ่งที่ปรากฏแทนเป็นของ CoreServicesUIAgent ซึ่งเป็นโปรแกรมของ macOS เอง สคริปต์ของเราอ่านไม่ได้ว่ามันแสดงอะไร หน้าของ Apple ชื่อ Open a Mac app from an unknown developer อธิบายข้อความที่คนจะเห็นในตอนนี้
🇨🇳 中文
分三部分来读。第一,签名。Signature=adhoc 和 TeamIdentifier=not set 的意思是:这个应用并不是由哪个具体的人签的名。Apple 芯片不肯运行完全没有签名的程序,所以 PyInstaller 给了它这样一个空签名。它能证明文件没有被改动过,但对是谁做的只字未提。
第二,裁决。spctl 的回答是 rejected。这是 Gatekeeper 对这个应用的看法,然而应用在构建它的那台 Mac 上还是打开了,大约用了一秒。只有带着 com.apple.quarantine 标签的文件,才会去问 Gatekeeper;这个标签是浏览器贴在下载下来的东西上的。你自己构建的文件没有这个标签:xattr 什么也没打印。
第三,下载。我们把应用复制了一份,自己给它贴上这个标签。这一次,二十秒内应用的窗口没有出现。出现的东西属于 CoreServicesUIAgent,那是 macOS 自己的一个程序。我们的脚本读不出它显示了什么。Apple 的页面 Open a Mac app from an unknown developer 描述了这时人会看到的提示。
Overriding security settings to open an app is the most common way that a Mac gets infected with malware.So only ask this of people who know you, and tell them beforehand.ส่วนนี้มาจากหน้าของ Apple ที่อ่านเมื่อวันที่ 10 ตุลาคม 2026 และเราไม่ได้รันเอง: หลังจากลองเปิดแอปแล้ว ให้เปิด System Settings ไปที่ Privacy & Security แล้วคลิก Open Anyway ซึ่งจะมีให้กดราวหนึ่งชั่วโมง จากนั้นใส่รหัสผ่าน หน้าของ Apple ยังเตือนด้วยว่า การข้ามการตั้งค่าความปลอดภัยเพื่อเปิดแอป เป็นทางที่แมคติดมัลแวร์บ่อยที่สุด ดังนั้นขอให้ทำแบบนี้เฉพาะกับคนที่รู้จักคุณ และบอกเขาไว้ก่อน这一部分来自 Apple 的页面(读于 2026 年 10 月 10 日),我们没有运行过:对方先试着打开应用,然后打开 System Settings(系统设置),进入 Privacy & Security(隐私与安全性),点 Open Anyway(仍要打开);这个按钮大约保留一小时,之后还要输入密码。Apple 的页面还提醒说,为了打开应用而绕过安全设置,是 Mac 感染恶意软件最常见的途径。所以只对认识你的人提这个要求,并且事先告诉他们。
🇬🇧 English
The way to an app that opens with no such step is to sign it with a certificate from Apple's paid developer programme and to send it to Apple to be checked, which Apple calls notarization. PyInstaller has an option for the signing, --codesign-identity. We have no such certificate, so none of that is on this page.
🇹🇭 ไทย
ทางที่จะได้แอปซึ่งเปิดได้โดยไม่ต้องทำขั้นนั้น คือเซ็นแอปด้วยใบรับรองจากโปรแกรมนักพัฒนาแบบเสียเงินของ Apple แล้วส่งให้ Apple ตรวจ ซึ่ง Apple เรียกว่า notarization PyInstaller มีตัวเลือกสำหรับการเซ็น คือ --codesign-identity เราไม่มีใบรับรองแบบนั้น หน้านี้จึงไม่มีเรื่องนั้น
🇨🇳 中文
想让应用不经过这一步就能打开,办法是用 Apple 付费开发者计划发的证书给它签名,再把它交给 Apple 检查,Apple 把这叫作 notarization(公证)。PyInstaller 有一个用于签名的选项:--codesign-identity。我们没有这样的证书,所以这一页里没有这些内容。
🚀 Tutorial 4 — Release files for GitHub · บทที่ 4 — ไฟล์สำหรับ release บน GitHub · 教程 4 —— 给 GitHub 准备发布文件
🇬🇧 English
A project on GitHub hands out its programs on its Releases page: a version number, and under it the files to download. For a Mac app people expect one of three files there. A short Python script makes all three from dist/hello.app.
🇹🇭 ไทย
โปรเจกต์บน GitHub แจกโปรแกรมของตัวเองที่หน้า Releases: มีเลขเวอร์ชัน และข้างใต้เป็นไฟล์ให้ดาวน์โหลด สำหรับแอปของแมค คนมักคาดว่าจะเจอไฟล์หนึ่งในสามแบบ สคริปต์ Python สั้น ๆ ตัวหนึ่งสร้างได้ทั้งสามแบบจาก dist/hello.app
🇨🇳 中文
GitHub 上的项目,是在它的 Releases 页面上分发程序的:一个版本号,下面是供下载的文件。对 Mac 应用,人们通常期待在那里看到三种文件之一。一个很短的 Python 脚本,就能从 dist/hello.app 把三种都做出来。
7 A zip, a disk image and an installer · ไฟล์ zip ดิสก์อิมเมจ และตัวติดตั้ง · zip、磁盘映像和安装包
| Fileไฟล์文件 | What the other person does with itอีกฝ่ายทำอะไรกับมัน对方拿它做什么 | Made byสร้างด้วย用什么做的 |
|---|---|---|
| .zip | Double-clicks it, and the app appears beside it. The plainest of the three.ดับเบิลคลิก แล้วแอปจะปรากฏข้าง ๆ เป็นแบบที่เรียบง่ายที่สุดในสามแบบ双击它,应用就出现在旁边。三种里最朴素的一种。 | ditto |
| .dmg | Double-clicks it, and a window shows the app beside a link to the Applications folder. They drag the one onto the other. This is called a disk image.ดับเบิลคลิก แล้วจะมีหน้าต่างแสดงแอปอยู่ข้างลิงก์ไปยังโฟลเดอร์ Applications จากนั้นลากแอปไปวางบนลิงก์ สิ่งนี้เรียกว่าดิสก์อิมเมจ双击它,会弹出一个窗口,里面是应用和一个指向 Applications 文件夹的链接。把前者拖到后者上就行。这叫磁盘映像。 | hdiutil |
| .pkg | Double-clicks it, and macOS's Installer walks them through a few screens and puts the app in Applications. It asks for an administrator's password.ดับเบิลคลิก แล้วโปรแกรม Installer ของ macOS จะพาไปทีละหน้าจอ และวางแอปไว้ใน Applications มันจะถามรหัสผ่านของผู้ดูแลระบบ双击它,macOS 的 Installer 会带着走过几个界面,把应用放进 Applications。它会要求输入管理员密码。 | pkgbuild |
🇬🇧 English
Nobody sits at our Mac, so nothing was double-clicked: the middle column above is how each file is meant to be used, and the box further down is what we ran. Save this file as package.py beside dist. It uses only programs that every Mac has, and it prints each command before it runs it, so you can also type them yourself. You give it the app's name and a version number.
🇹🇭 ไทย
ไม่มีใครนั่งอยู่หน้าแมคของเรา จึงไม่มีการดับเบิลคลิกอะไร คอลัมน์กลางข้างบนคือวิธีที่ตั้งใจให้ใช้แต่ละไฟล์ และกล่องถัดลงไปคือสิ่งที่เรารันจริง บันทึกไฟล์นี้ชื่อ package.py ไว้ข้าง dist มันใช้เฉพาะโปรแกรมที่แมคทุกเครื่องมี และพิมพ์แต่ละคำสั่งก่อนรัน คุณจึงพิมพ์คำสั่งเหล่านั้นเองก็ได้ คุณต้องบอกชื่อแอปและเลขเวอร์ชันให้มัน
🇨🇳 中文
没有人坐在我们那台 Mac 前,所以什么都没有被双击过:上表中间一栏说的是每种文件本来的用法,再往下的输出框才是我们真正运行的。把这个文件存成 package.py,放在 dist 旁边。它只用每台 Mac 都有的程序,而且每条命令运行之前都会先打印出来,所以你也可以自己动手输入。你要告诉它应用的名字和一个版本号。
"""package.py - turn dist/hello.app into the files you put on a GitHub release.
python3 package.py hello 1.0
Run it on a Mac, in the folder that holds dist. It makes a folder named
release with four files in it:
hello-1.0-macos-arm64.zip the app, zipped
hello-1.0-macos-arm64.dmg a disk image: open it, drag the app to Applications
hello-1.0-macos-arm64.pkg an installer: it puts the app in Applications
SHA256SUMS.txt a fingerprint of each file, to check a download
It uses only programs that every Mac has: ditto, hdiutil, pkgbuild.
"""
import hashlib
import os
import pathlib
import platform
import plistlib
import shutil
import subprocess
import sys
def run(words):
"""Run one of macOS's programs, show the command, and stop if it fails."""
print("$ " + " ".join(words))
subprocess.run(words, check=True, stdout=subprocess.DEVNULL)
def fingerprint(path):
"""The SHA-256 of a file: 64 letters and digits that change if one byte changes."""
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
if len(sys.argv) < 3:
print(__doc__)
return 2
name, version = sys.argv[1], sys.argv[2]
app = pathlib.Path("dist") / (name + ".app")
if not app.exists():
print("There is no " + str(app) + ". Build it first, with --windowed.")
return 1
# arm64 on a Mac with an Apple chip, x86_64 on a Mac with an Intel chip.
# The app runs on the kind of Mac that built it, so the file's name says which.
base = f"{name}-{version}-macos-{platform.machine()}"
release = pathlib.Path("release")
shutil.rmtree(release, ignore_errors=True)
release.mkdir()
# 1. A zip. ditto is what the Finder's "Compress" uses, and it keeps the
# links inside the app as links. An ordinary zip program can break them.
run(["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent",
str(app), str(release / (base + ".zip"))])
# 2. An installer. pkgbuild packs a folder, here one that holds only the app.
stage = release / "stage"
stage.mkdir()
run(["ditto", str(app), str(stage / app.name)])
# Left alone, the installer looks for a copy of the app that is already on
# the Mac, and installs over that one, wherever it is. This switches that off,
# so that the app always goes to Applications.
parts = release / "parts.plist"
run(["pkgbuild", "--analyze", "--root", str(stage), str(parts)])
with open(parts, "rb") as file:
described = plistlib.load(file)
described[0]["BundleIsRelocatable"] = False
with open(parts, "wb") as file:
plistlib.dump(described, file)
# The identifier is the app's name after a web address you own, written
# backwards. com.example is the address for examples.
run(["pkgbuild", "--root", str(stage), "--component-plist", str(parts),
"--install-location", "/Applications",
"--identifier", "com.example." + name, "--version", version,
str(release / (base + ".pkg"))])
parts.unlink()
# 3. A disk image. It shows the app beside a link to the Applications
# folder, so that installing is one drag.
os.symlink("/Applications", stage / "Applications")
run(["hdiutil", "create", "-volname", name, "-srcfolder", str(stage),
"-format", "UDZO", str(release / (base + ".dmg"))])
shutil.rmtree(stage)
# 4. The fingerprints, in the form the program shasum can check.
lines = []
for file in sorted(release.iterdir()):
megabytes = file.stat().st_size / 1e6
print(f" {megabytes:6.1f} MB {file.name}")
lines.append(fingerprint(file) + " " + file.name)
(release / "SHA256SUMS.txt").write_text("\n".join(lines) + "\n")
print("To put them on GitHub, with GitHub's own program gh:")
print(f" gh release create v{version} release/* --title \"{name} {version}\"")
return 0
if __name__ == "__main__":
sys.exit(main())
python3 package.py hello 1.0
🇬🇧 English
Three details in that file are worth a second look. The zip is made with ditto, which keeps the links inside the app as links. The installer is told that the app is not relocatable. Left as it is, an installer may put the app where a copy of it already is on that Mac, and not in Applications. And each file's name carries the kind of chip, because step 6 showed that the app runs on one kind only.
Then we tried each file the way a person who downloaded it would. The app from the zip opened. The app on the disk image opened. The installer put hello.app in Applications, and that one opened as well.
🇹🇭 ไทย
ในไฟล์นั้นมีสามจุดที่ควรดูอีกครั้ง ไฟล์ zip สร้างด้วย ditto ซึ่งเก็บลิงก์ในแอปไว้เป็นลิงก์ ตัวติดตั้งถูกบอกว่าแอปนี้ย้ายที่ไม่ได้ ถ้าปล่อยไว้ตามเดิม ตัวติดตั้งอาจวางแอปไว้ตรงที่ซึ่งมีสำเนาของแอปอยู่แล้วในแมคเครื่องนั้น แทนที่จะเป็น Applications และชื่อของแต่ละไฟล์มีชนิดของชิปกำกับไว้ เพราะขั้นที่ 6 แสดงแล้วว่าแอปรันได้กับชิปชนิดเดียว
จากนั้นเราลองแต่ละไฟล์แบบที่คนซึ่งดาวน์โหลดมาจะทำ แอปจากไฟล์ zip เปิดได้ แอปบนดิสก์อิมเมจเปิดได้ ตัวติดตั้งวาง hello.app ไว้ใน Applications และตัวนั้นก็เปิดได้เช่นกัน
🇨🇳 中文
这个文件里有三处细节值得再看一眼。zip 是用 ditto 做的,它会把应用里的链接保留成链接。安装包被告知这个应用不可重定位。不这样设的话,安装程序可能把应用装到那台 Mac 上已有的一份副本所在的位置,而不是 Applications。每个文件的名字里都带着芯片类型,因为第 6 步已经表明,这个应用只能在一种芯片上运行。
然后我们按下载者会做的方式,把每个文件都试了一遍。zip 里的应用打开了。磁盘映像上的应用打开了。安装包把 hello.app 放进了 Applications,那一个也打开了。
$ venv/bin/python -m PyInstaller --windowed hello.py
exit code 0
$ python3 package.py hello 1.0
$ ditto -c -k --sequesterRsrc --keepParent dist/hello.app release/hello-1.0-macos-arm64.zip
$ ditto dist/hello.app release/stage/hello.app
$ pkgbuild --analyze --root release/stage release/parts.plist
$ pkgbuild --root release/stage --component-plist release/parts.plist --install-location /Applications --identifier com.example.hello --version 1.0 release/hello-1.0-macos-arm64.pkg
$ hdiutil create -volname hello -srcfolder release/stage -format UDZO release/hello-1.0-macos-arm64.dmg
12.1 MB hello-1.0-macos-arm64.dmg
9.7 MB hello-1.0-macos-arm64.pkg
9.8 MB hello-1.0-macos-arm64.zip
To put them on GitHub, with GitHub's own program gh:
gh release create v1.0 release/* --title "hello 1.0"
exit code 0
The zip:
$ ditto -x -k hello-1.0-macos-arm64.zip unzipped
(nothing)
exit code 0
The app from the zip showed its window after 0.60 s.
The disk image:
$ hdiutil attach hello-1.0-macos-arm64.dmg -quiet -nobrowse -readonly -mountpoint mounted
(nothing)
exit code 0
$ ls -l mounted
total 0
lrwxr-xr-x 1 runner staff 13 Oct 10 05:42 Applications -> /Applications
drwxr-xr-x 3 runner staff 96 Oct 10 05:42 hello.app
exit code 0
The app on the disk image showed its window after 0.59 s.
$ hdiutil detach mounted
"disk5" ejected.
exit code 0
$ spctl --assess --type open --context context:primary-signature --verbose=2 hello-1.0-macos-arm64.dmg
hello-1.0-macos-arm64.dmg: rejected
source=no usable signature
exit code 3
The installer:
$ pkgutil --check-signature hello-1.0-macos-arm64.pkg
Package "hello-1.0-macos-arm64.pkg":
Status: no signature
exit code 1
$ spctl --assess --type install --verbose=2 hello-1.0-macos-arm64.pkg
hello-1.0-macos-arm64.pkg: rejected
source=no usable signature
exit code 3
$ sudo installer -pkg hello-1.0-macos-arm64.pkg -target /
installer: Package name is hello-1.0-macos-arm64
installer: Installing at base path /
installer: The install was successful.
exit code 0
$ ls -d /Applications/hello.app
/Applications/hello.app
exit code 0
The app the installer put in Applications showed its window after 0.43 s.
$ sudo rm -rf /Applications/hello.app
(nothing)
exit code 0
$ sudo pkgutil --forget com.example.hello
Forgot package 'com.example.hello' on '/'.
exit code 0
The fingerprints:
$ cat SHA256SUMS.txt
58efa8cde3d5ef1379a961031bcffa28f8a1a51ce6cec6abcfd3e0bf456df92d hello-1.0-macos-arm64.dmg
be84ca39c80db7dba069f84f81f6f5164fcb0c3580c3ccc24229bdede8394210 hello-1.0-macos-arm64.pkg
c54424f225c015db53bd1fccfe9fee69d843d4744e6110a633e9e2d7d3732679 hello-1.0-macos-arm64.zip
exit code 0
$ shasum -a 256 -c SHA256SUMS.txt
hello-1.0-macos-arm64.dmg: OK
hello-1.0-macos-arm64.pkg: OK
hello-1.0-macos-arm64.zip: OK
exit code 0
spctl lines in that output. macOS rejects the disk image and the installer for the same reason it rejects the app: no usable signature. Our installer ran because we started it from the Terminal as an administrator, on the Mac that made it. A person who downloads it and double-clicks it is asked the question of step 6 first.ดูสองบรรทัดของ spctl ในผลลัพธ์นั้น macOS ปฏิเสธดิสก์อิมเมจและตัวติดตั้งด้วยเหตุผลเดียวกับที่ปฏิเสธแอป: no usable signature ตัวติดตั้งของเรารันได้เพราะเราเริ่มมันจาก Terminal ในฐานะผู้ดูแลระบบ บนแมคเครื่องที่สร้างมัน คนที่ดาวน์โหลดไปแล้วดับเบิลคลิก จะถูกถามคำถามของขั้นที่ 6 ก่อน看输出里那两行 spctl。macOS 拒绝磁盘映像和安装包,理由和拒绝应用时一样:no usable signature。我们的安装包之所以能运行,是因为我们在制作它的那台 Mac 上,以管理员身份从“终端”启动了它。下载之后双击它的人,会先被问到第 6 步里的那个问题。
8 Put them on GitHub · นำขึ้น GitHub · 把它们放到 GitHub 上
🇬🇧 English
The last file, SHA256SUMS.txt, holds a fingerprint of each of the others. Put it on the release with them. Someone who has downloaded the files into one folder can then check that they arrived whole, with a program every Mac has. Our run printed OK for each.
🇹🇭 ไทย
ไฟล์สุดท้าย SHA256SUMS.txt เก็บลายนิ้วมือของไฟล์อื่นแต่ละไฟล์ ให้ใส่มันไว้ใน release พร้อมกัน คนที่ดาวน์โหลดไฟล์ทั้งหมดไว้ในโฟลเดอร์เดียวจะตรวจได้ว่าไฟล์มาถึงครบถ้วน ด้วยโปรแกรมที่แมคทุกเครื่องมี การรันของเราพิมพ์ OK ให้ทุกไฟล์
🇨🇳 中文
最后一个文件 SHA256SUMS.txt 里是其余每个文件的指纹。把它和那些文件一起放进发布。有人把这些文件下载到同一个文件夹后,就能用每台 Mac 都有的一个程序,检查它们是不是完整到达。我们运行时,每个文件都打印了 OK。
shasum -a 256 -c SHA256SUMS.txt
🇬🇧 English
There are two ways to make the release. In a browser, open your repository on GitHub, click Releases, then Draft a new release, type a tag such as v1.0, and drag the four files into the box. Or use gh, GitHub's own command-line program, which package.py prints the command for. We did not make a release: this page's repository is not a public one.
🇹🇭 ไทย
สร้าง release ได้สองทาง ทางเบราว์เซอร์: เปิด repository ของคุณบน GitHub คลิก Releases แล้วคลิก Draft a new release พิมพ์ tag เช่น v1.0 แล้วลากไฟล์ทั้งสี่ลงในกล่อง หรือใช้ gh ซึ่งเป็นโปรแกรมบรรทัดคำสั่งของ GitHub เอง และ package.py พิมพ์คำสั่งนั้นให้แล้ว เราไม่ได้สร้าง release: repository ของหน้านี้ไม่ใช่ repository สาธารณะ
🇨🇳 中文
做发布有两种办法。用浏览器:在 GitHub 上打开你的仓库,点 Releases,再点 Draft a new release,输入一个标签,比如 v1.0,然后把四个文件拖进那个框里。或者用 gh,它是 GitHub 自己的命令行程序,package.py 已经把这条命令打印出来了。我们没有做发布:这一页的仓库不是公开仓库。
🍎 TERMINAL · not run by us · · เราไม่ได้รันเอง · · 我们没有运行过
gh release create v1.0 release/* --title "hello 1.0"
- Run
package.py, then double-click the.dmg. What does its window show? - Change one letter in
SHA256SUMS.txtand run theshasumcommand again. What does it say now?
- รัน
package.pyแล้วดับเบิลคลิกไฟล์.dmgหน้าต่างของมันแสดงอะไร - แก้ตัวอักษรหนึ่งตัวใน
SHA256SUMS.txtแล้วรันคำสั่งshasumอีกครั้ง คราวนี้มันว่าอย่างไร
- 运行
package.py,然后双击那个.dmg。它的窗口里显示了什么? - 把
SHA256SUMS.txt里的一个字母改掉,再运行一次shasum命令。这回它说什么?
🤖 Tutorial 5 — An Agno agent · บทที่ 5 — เอเจนต์ของ Agno · 教程 5 —— 一个 Agno 智能体
🇬🇧 English
Agno: a company of AI agents builds a noodle shop out of AI agents, and its cashier is one Python file: an agent with one tool. The agent's model reads the order, and a Python function does the sum. Packing it shows what a packed agent carries and what it leaves outside. The Agno library goes into the program. The model does not. It stays in Ollama, a separate program that the Mac must have.
🇹🇭 ไทย
Agno: บริษัทของเอเจนต์ AI สร้างร้านก๋วยเตี๋ยวขึ้นจากเอเจนต์ AI และแคชเชียร์ของร้านคือไฟล์ Python หนึ่งไฟล์: เอเจนต์ที่มีเครื่องมือหนึ่งอย่าง โมเดลของเอเจนต์อ่านรายการสั่ง และฟังก์ชัน Python เป็นคนบวกเลข การแพ็กมันทำให้เห็นว่าเอเจนต์ที่แพ็กแล้วพกอะไรไปด้วย และทิ้งอะไรไว้ข้างนอก ไลบรารี Agno เข้าไปอยู่ในโปรแกรม แต่โมเดลไม่ได้เข้าไป มันอยู่ใน Ollama ซึ่งเป็นโปรแกรมแยกต่างหากที่แมคต้องมี
🇨🇳 中文
《Agno:AI 智能体公司》用 AI 智能体搭起了一家面馆,其中的收银员是一个 Python 文件:一个带一件工具的智能体。智能体的模型读订单,一个 Python 函数做加法。把它打包,就能看出打包后的智能体带走了什么,又把什么留在外面。Agno 这个库进了程序。模型没有进去。它留在 Ollama 里,那是 Mac 上必须另外装好的一个程序。
9 Pack the cashier · แพ็กแคชเชียร์ · 把收银员打包
🇬🇧 English
Take cashier.py from the Agno page as it is. It differs from hello.py in three ways that matter here. It has no window, so it is built without --windowed, and --onefile suits a program that is run from the Terminal. It imports agno, a library that does not come with Python. The Python that builds must have that library, with the two that Agno's Ollama model asks for, or PyInstaller cannot find them to pack. And it needs a model, qwen2.5:3b, which is 1.9 GB and which Ollama keeps and runs. PyInstaller has nothing to do with that one: the program asks Ollama for an answer over port 11434 of the same Mac.
🇹🇭 ไทย
นำ cashier.py จากหน้า Agno มาใช้ตามเดิม มันต่างจาก hello.py สามเรื่องที่มีผลตรงนี้ มันไม่มีหน้าต่าง จึงบิลด์โดยไม่ใส่ --windowed และ --onefile เหมาะกับโปรแกรมที่รันจาก Terminal มัน import agno ซึ่งเป็นไลบรารีที่ไม่ได้มากับ Python ตัว Python ที่ใช้บิลด์ต้องมีไลบรารีนั้น พร้อมอีกสองตัวที่โมเดล Ollama ของ Agno ขอ ไม่อย่างนั้น PyInstaller จะหาไม่พบและแพ็กไม่ได้ และมันต้องมีโมเดล qwen2.5:3b ซึ่งมีขนาด 1.9 GB และ Ollama เป็นผู้เก็บและรัน เรื่องนี้ PyInstaller ไม่เกี่ยวข้องเลย: โปรแกรมขอคำตอบจาก Ollama ผ่านพอร์ต 11434 ของแมคเครื่องเดียวกัน
🇨🇳 中文
把 Agno 那一页的 cashier.py 原样拿来。它和 hello.py 有三点不同,在这里都有影响。它没有窗口,所以构建时不加 --windowed,而 --onefile 适合从终端运行的程序。它 import 了 agno,这个库不是 Python 自带的。用来构建的那份 Python 必须装有这个库,还要有 Agno 的 Ollama 模型要求的另外两个,否则 PyInstaller 找不到它们,也就没法打包。它还需要一个模型 qwen2.5:3b,大小是 1.9 GB,由 Ollama 保存和运行。这一样和 PyInstaller 毫无关系:程序通过同一台 Mac 的 11434 端口向 Ollama 要答案。
venv/bin/python -m pip install agno==3.1.2 ollama openai
brew install ollama
ollama serve &
ollama pull qwen2.5:3b
venv/bin/python -m PyInstaller --onefile cashier.py
dist/cashier "2 khao soi, 1 thai iced tea"
$ venv/bin/python -m PyInstaller --onefile cashier.py
built in 34.6 s: dist/cashier, 27.0 MB in 1 file
PyInstaller used its notes on these libraries: anyio, backports, certifi, distutils, importlib_resources, jaraco, more_itertools, packaging, pydantic, pygments, rich, setuptools, setuptools._vendor.jaraco.text, tomli, typing_extensions, wheel
Ollama is not running.
$ dist/cashier "2 khao soi, 1 thai iced tea"
ERROR Error in Agent run: Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download
Traceback (most recent call last):
File "cashier.py", line 59, in <module>
File "cashier.py", line 50, in main
TypeError: 'NoneType' object is not iterable
[PYI-8240:ERROR] Failed to execute script 'cashier' due to unhandled exception!
exit code 1, after 1.41 s
$ ollama serve
Ollama answers: {"version":"0.33.3"}
$ dist/cashier "2 khao soi, 1 thai iced tea"
The agent called: bill {'order': '2 khao soi, 1 thai iced tea'}
The tool returned:
- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
The agent says:
150 baht
exit code 0, after 76.15 s
$ dist/cashier "2 khao soi, 1 thai iced tea"
The agent called: bill {'order': '2 khao soi, 1 thai iced tea'}
The tool returned:
- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
The agent says:
150 baht
exit code 0, after 19.50 s
$ dist/cashier "1 pad thai, 2 pizza"
The agent called: bill {'order': '1 pad thai, 2 pizza'}
The tool returned:
I do not know the price of pizza.
The agent says:
Sorry, I cannot provide the bill without the prices of the dishes. Could you please provide the prices for pad thai and pizza?
exit code 0, after 25.87 s
$ venv/bin/python cashier.py "2 khao soi, 1 thai iced tea"
The agent called: bill {'order': '2 khao soi, 1 thai iced tea'}
The tool returned:
- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
The agent says:
150 baht
exit code 0, after 19.92 s
🇬🇧 English
The packed agent is one file of 27.0 MB, and it took 34.6 seconds to build. The one-file hello was 9.7 MB. The log line about notes names some of what came in with Agno: pydantic, anyio, rich. The 1.9 GB model is not in those 27 MB.
Look first at the start without Ollama, because that is what somebody you give this program to will see. Agno printed the real reason, Failed to connect to Ollama
, and went on with an empty answer. Line 50 of cashier.py then tried to go through a list of tool calls that was not there, and Python stopped with a TypeError. The last line, Failed to execute script 'cashier' due to unhandled exception!
, is from the starter that PyInstaller puts in front of your script. It is how a packed program reports any error the script did not catch.
With Ollama running, the packed program did what the Python file does. The model chose the tool, Python added up 150 baht, and the program printed it. The first order took 76 seconds, because Ollama had to load the model into memory. The same order again took 20 seconds, and the plain Python file took 20. We ran all of this three times. In the other two runs the same three times were 56, 22 and 28 seconds, and 67, 26 and 14. Nearly all of the time is the model answering, on GitHub's virtual Mac with 3 cores, and it moves that much from one run to the next. So packing changed nothing that we could measure. We have not timed a real Mac.
The third order shows the model and not the packaging. The tool returned I do not know the price of pizza.
, and the model's own reply asked for the prices of both dishes, pad thai too, whose price the tool knows. The Agno page meets this in its next file, shop.py, where the tool is marked stop_after_tool_call and its words are the answer.
🇹🇭 ไทย
เอเจนต์ที่แพ็กแล้วเป็นไฟล์เดียวขนาด 27.0 MB และใช้เวลาบิลด์ 34.6 วินาที ส่วน hello แบบไฟล์เดียวมีขนาด 9.7 MB บรรทัดใน log ที่พูดถึงบันทึก บอกชื่อบางส่วนของสิ่งที่เข้ามาพร้อม Agno: pydantic, anyio, rich โมเดลขนาด 1.9 GB ไม่ได้อยู่ใน 27 MB นั้น
ดูการเริ่มโดยไม่มี Ollama ก่อน เพราะนั่นคือสิ่งที่คนที่คุณส่งโปรแกรมนี้ให้จะได้เห็น Agno พิมพ์สาเหตุจริงออกมา คือ Failed to connect to Ollama
แล้วทำงานต่อด้วยคำตอบที่ว่างเปล่า จากนั้นบรรทัดที่ 50 ของ cashier.py พยายามไล่ดูรายการการเรียกเครื่องมือซึ่งไม่มีอยู่ และ Python หยุดด้วย TypeError บรรทัดสุดท้าย Failed to execute script 'cashier' due to unhandled exception!
มาจากตัวเริ่มที่ PyInstaller วางไว้หน้าสคริปต์ของคุณ โปรแกรมที่แพ็กแล้วรายงานข้อผิดพลาดทุกอย่างที่สคริปต์ไม่ได้ดักไว้ด้วยวิธีนี้
เมื่อ Ollama ทำงานอยู่ โปรแกรมที่แพ็กแล้วทำอย่างที่ไฟล์ Python ทำ โมเดลเลือกเครื่องมือ Python บวกได้ 150 บาท แล้วโปรแกรมก็พิมพ์ออกมา รายการสั่งแรกใช้เวลา 76 วินาที เพราะ Ollama ต้องโหลดโมเดลเข้าหน่วยความจำ รายการสั่งเดิมอีกครั้งใช้ 20 วินาที และไฟล์ Python ธรรมดาใช้ 20 วินาที เรารันทั้งหมดนี้สามครั้ง ในอีกสองครั้ง สามค่าเดียวกันคือ 56, 22 และ 28 วินาที กับ 67, 26 และ 14 เวลาเกือบทั้งหมดคือโมเดลกำลังตอบ บนแมคเสมือนของ GitHub ที่มี 3 คอร์ และมันแกว่งได้มากขนาดนั้นจากการรันครั้งหนึ่งไปอีกครั้งหนึ่ง การแพ็กจึงไม่ได้เปลี่ยนอะไรที่เราวัดได้ เรายังไม่ได้จับเวลาบนแมคจริง
รายการสั่งที่สามแสดงให้เห็นโมเดล ไม่ใช่การแพ็ก เครื่องมือคืนค่า I do not know the price of pizza.
แต่คำตอบของโมเดลเองกลับขอราคาของทั้งสองจาน รวมถึง pad thai ซึ่งเครื่องมือรู้ราคาอยู่แล้ว หน้า Agno เจอเรื่องนี้ในไฟล์ถัดไป คือ shop.py ซึ่งกำหนด stop_after_tool_call ให้เครื่องมือ และคำของเครื่องมือก็คือคำตอบ
🇨🇳 中文
打包后的智能体是一个 27.0 MB 的文件,构建用了 34.6 秒。单文件的 hello 是 9.7 MB。日志里讲备注的那一行,列出了随 Agno 一起进来的一部分东西:pydantic、anyio、rich。那个 1.9 GB 的模型不在这 27 MB 里。
先看没有 Ollama 时的那次启动,因为拿到这个程序的人看到的就是它。Agno 打印了真正的原因 Failed to connect to Ollama
,然后带着一个空的回答继续往下走。接着 cashier.py 的第 50 行想遍历一个并不存在的工具调用列表,Python 就以 TypeError 停下了。最后一行 Failed to execute script 'cashier' due to unhandled exception!
来自 PyInstaller 放在你脚本前面的启动器。凡是脚本没有接住的错误,打包后的程序都是这样报告的。
Ollama 运行之后,打包后的程序做的事和 Python 文件一样。模型选了工具,Python 算出 150 泰铢,程序把它打印出来。第一份订单用了 76 秒,因为 Ollama 得先把模型载入内存。同一份订单再来一次用了 20 秒,直接运行 Python 文件用了 20 秒。这一整套我们运行了三次。另外两次里,同样的三个数是 56、22 和 28 秒,以及 67、26 和 14。这些时间几乎全是模型在回答,用的是 GitHub 的虚拟 Mac,它有 3 个核心,而两次运行之间就能差这么多。所以打包没有带来任何我们量得出的变化。我们没有在真正的 Mac 上计过时。
第三份订单暴露的是模型,而不是打包。工具返回了 I do not know the price of pizza.
,模型自己的回答却把两道菜的价格都问了一遍,连 pad thai 也问了,而工具是知道它的价格的。Agno 那一页在下一个文件 shop.py 里处理了这件事:它给工具标上 stop_after_tool_call,工具说的话就是回答。
qwen2.5:3b pulled and Ollama running, before the program can answer. And Tutorial 3 applies to this file as it does to hello.app: sent over the internet, it meets Gatekeeper.ส่วนสุดท้ายนี้เราไม่ได้รันเอง: เราไม่ได้ย้ายโปรแกรมไปยังแมคเครื่องที่สอง แมคเครื่องนั้นไม่ต้องมี Python และไม่ต้องมี Agno เพราะทั้งสองอยู่ในไฟล์แล้ว แต่ต้องมี Ollama ที่ pull qwen2.5:3b ไว้แล้วและกำลังทำงานอยู่ โปรแกรมจึงจะตอบได้ และบทที่ 3 ใช้กับไฟล์นี้เหมือนที่ใช้กับ hello.app: เมื่อส่งผ่านอินเทอร์เน็ต มันก็เจอ Gatekeeper最后这一部分我们没有运行过:我们没有把程序搬到第二台 Mac 上。那台 Mac 不需要 Python,也不需要 Agno,因为两者都在文件里。但它需要 Ollama,并且已经 pull 了 qwen2.5:3b、Ollama 正在运行,程序才能回答。教程 3 对这个文件同样适用,就像对 hello.app 一样:经互联网发出去,它也会遇到 Gatekeeper。
☁️ How we ran this without a Mac · เรารันทั้งหมดนี้โดยไม่มีแมคได้อย่างไร · 没有 Mac,我们是怎么运行这一切的
🇬🇧 English
GitHub will run commands for you on a computer of its own each time you push to a repository. The service is GitHub Actions, and one kind of computer it offers is a Mac. So this page's Mac was a GitHub one: macOS 15.7.9 on a virtual Apple M1 with 3 cores and 7 GB of memory.
The arrangement is one file, called a workflow, kept in the repository's .github/workflows folder. Ours says: when one of this page's Python files is pushed, start a Mac, fetch the repository, make the two Pythons, run every check with its output saved in a file, and commit those files back to the repository. The page's boxes of output are those files. Nobody copied a number by hand.
🇹🇭 ไทย
GitHub รันคำสั่งให้คุณได้บนคอมพิวเตอร์ของมันเอง ทุกครั้งที่คุณ push ขึ้น repository บริการนี้ชื่อ GitHub Actions และคอมพิวเตอร์ชนิดหนึ่งที่มีให้เลือกคือแมค แมคของหน้านี้จึงเป็นของ GitHub: macOS 15.7.9 บน Apple M1 เสมือน มี 3 คอร์ และหน่วยความจำ 7 GB
วิธีการทั้งหมดคือไฟล์เดียว เรียกว่า workflow เก็บไว้ในโฟลเดอร์ .github/workflows ของ repository ไฟล์ของเราบอกว่า: เมื่อมีการ push ไฟล์ Python ของหน้านี้ ให้เริ่มแมคหนึ่งเครื่อง ดึง repository มา สร้าง Python สองชุด รันทุกการตรวจสอบโดยบันทึกผลลัพธ์ลงไฟล์ แล้ว commit ไฟล์เหล่านั้นกลับเข้า repository กล่องผลลัพธ์ในหน้านี้ก็คือไฟล์เหล่านั้น ไม่มีใครคัดลอกตัวเลขด้วยมือ
🇨🇳 中文
每次你向仓库 push,GitHub 都可以在它自己的一台电脑上替你运行命令。这项服务叫 GitHub Actions,它提供的电脑里有一种就是 Mac。所以这一页用的 Mac 是 GitHub 的:macOS 15.7.9,运行在一台虚拟的 Apple M1 上,3 个核心,7 GB 内存。
整套做法只是一个文件,叫 workflow,放在仓库的 .github/workflows 文件夹里。我们的这个文件说的是:只要这一页的某个 Python 文件被 push 上来,就启动一台 Mac,取回仓库,建好两份 Python,运行每一项检查并把输出存成文件,再把这些文件 commit 回仓库。页面上的输出框就是这些文件。没有人手抄过任何一个数字。
Show the workflow · แสดง workflow · 显示这个 workflow
name: Mac app checks
# krueng.ai/python_to_mac_app.html says how big each Mac app is and how long it
# takes to build and to open. We have no Mac. This runs the page's own checks on
# one of GitHub's Macs and commits what they printed back to the branch, where
# the page reads it.
#
# It runs only when code it builds or runs is pushed to the page's branch: the
# page's Python files, or the Agno cashier it packs. Changing the page's words,
# or this file, does not start it, because a minute on one of GitHub's Macs
# costs about ten times a minute on its Linux machines. After changing the steps
# below, start it by hand: Actions, "Mac app checks", Run workflow.
#
# The page shows this file. Its copy for the page is
# HTML/python_to_mac_app_assets/mac-app-checks.yml, and the run says whether
# the two are the same.
on:
push:
branches:
- feat/python-to-mac-app
paths:
- 'HTML/python_to_mac_app_assets/**.py'
- 'HTML/agno_agents_assets/cashier.py'
workflow_dispatch:
permissions:
contents: write # to commit the outputs back to the branch
concurrency:
group: mac-app-checks
cancel-in-progress: true
jobs:
checks:
runs-on: macos-15
timeout-minutes: 30
steps:
- name: Get the page's files
uses: actions/checkout@v4
- name: Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
# The checks run in a folder of their own in the home folder, and not in
# the folder GitHub checked the repository out to. An output may show the
# folder it ran in, and this one says nothing about the repository.
- name: Copy the page's files to a folder of their own
run: |
mkdir -p ~/krueng/agno_agents_assets
cp -R HTML/python_to_mac_app_assets ~/krueng/
cp HTML/agno_agents_assets/cashier.py ~/krueng/agno_agents_assets/
- name: The two Pythons the page describes
working-directory: /Users/runner/krueng/python_to_mac_app_assets
run: |
python3 -m venv venv
venv/bin/python -m pip install --quiet pyinstaller
python3 -m venv venv_checks
venv_checks/bin/python -m pip install --quiet pyobjc-framework-Quartz
# Each check writes its output file even when it fails, and the job goes
# on, so that a failure can be read in the branch.
- name: Run the checks
working-directory: /Users/runner/krueng/python_to_mac_app_assets/checks
run: |
check() { # check <output name> <python> <script> [arguments]
out="$1_output.txt"; shift
"$@" > "$out" 2>&1 || echo "$2 ended with code $?" >> run_output.txt
}
date -u "+started %Y-%m-%d %H:%M UTC" > run_output.txt
sw_vers >> run_output.txt
sysctl -n machdep.cpu.brand_string >> run_output.txt
echo "memory: $(( $(sysctl -n hw.memsize) / 1073741824 )) GB" >> run_output.txt
echo "runner image: $ImageOS $ImageVersion" >> run_output.txt
if cmp -s "$GITHUB_WORKSPACE/.github/workflows/mac-app-checks.yml" ../mac-app-checks.yml
then echo "the page's copy of the workflow: the same as the one that ran" >> run_output.txt
else echo "the page's copy of the workflow: DIFFERENT from the one that ran" >> run_output.txt
fi
echo "in venv, for the hello builds:" >> run_output.txt
../venv/bin/python -m pip list --format=freeze | sed 's/^/ /' >> run_output.txt
echo "in venv_checks:" >> run_output.txt
../venv_checks/bin/python -m pip list --format=freeze | sed 's/^/ /' >> run_output.txt
check builds ../venv_checks/bin/python builds.py
check build_log python3 build_log.py
check inside python3 inside.py made/hello_an_app/dist/hello.app
check unpack ../venv_checks/bin/python unpack.py made/hello_one_file/dist/hello
check gatekeeper ../venv_checks/bin/python gatekeeper.py made/hello_an_app/dist/hello.app
check package ../venv_checks/bin/python package_check.py
check flags ../venv/bin/python flags.py
# The Agno agent needs its libraries in the Python that builds, and its
# model in Ollama. Ollama is stopped again: agno_app.py starts its own.
../venv/bin/python -m pip install --quiet agno==3.1.2 ollama openai
echo "in venv, for the Agno build:" >> run_output.txt
../venv/bin/python -m pip list --format=freeze | sed 's/^/ /' >> run_output.txt
brew install --quiet ollama
ollama serve > /dev/null 2>&1 &
sleep 5
ollama pull qwen2.5:3b > /dev/null 2>&1
echo "ollama: $(ollama --version 2>&1 | tail -1)" >> run_output.txt
ollama list >> run_output.txt
pkill ollama; sleep 3
check agno_app ../venv_checks/bin/python agno_app.py ../../agno_agents_assets/cashier.py
date -u "+finished %Y-%m-%d %H:%M UTC" >> run_output.txt
- name: Commit what the checks printed
run: |
cp ~/krueng/python_to_mac_app_assets/checks/*_output.txt HTML/python_to_mac_app_assets/checks/
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add HTML/python_to_mac_app_assets/checks/*_output.txt
if git diff --cached --quiet; then
echo "The outputs did not change."
exit 0
fi
git commit -m "python_to_mac_app: outputs of the checks, from GitHub's macOS runner"
git pull --rebase origin feat/python-to-mac-app
git push origin HEAD:feat/python-to-mac-app
🇬🇧 English
A few choices in it are worth knowing if you copy it. Each check writes its output even when it fails, and the job carries on, so a failure can be read afterwards. Our first try at the download check in step 6 failed in just this way, and the file showed us why. The checks run in a folder of their own in the Mac's home folder, so that no output shows where the repository was checked out. And the scripts that measure use a second Python, venv_checks, so that the library they need to find a window cannot end up inside an app.
The run also wrote down what it ran on, and what was installed in each Python.
🇹🇭 ไทย
ถ้าคุณจะนำไปใช้ มีบางจุดที่ควรรู้ การตรวจสอบแต่ละตัวเขียนผลลัพธ์ของมันแม้ตอนที่ล้มเหลว และงานก็เดินต่อ จึงอ่านความล้มเหลวได้ภายหลัง การลองครั้งแรกของเรากับการตรวจสอบการดาวน์โหลดในขั้นที่ 6 ล้มเหลวแบบนี้เอง และไฟล์ก็บอกเราว่าเพราะอะไร การตรวจสอบรันในโฟลเดอร์ของตัวเองในโฮมโฟลเดอร์ของแมค เพื่อไม่ให้ผลลัพธ์ใดแสดงว่า repository ถูกดึงมาไว้ที่ไหน และสคริปต์ที่ใช้วัดผลใช้ Python อีกชุดหนึ่ง คือ venv_checks เพื่อไม่ให้ไลบรารีที่มันใช้หาหน้าต่างเข้าไปอยู่ในแอปได้
การรันยังจดไว้ด้วยว่ารันบนเครื่องอะไร และใน Python แต่ละชุดติดตั้งอะไรไว้บ้าง
🇨🇳 中文
如果你要照搬,其中有几处做法值得知道。每项检查即使失败也会写出自己的输出,而且整个任务会继续往下走,所以事后可以读到失败的原因。第 6 步里那项下载检查,我们第一次尝试时就是这样失败的,文件告诉了我们原因。检查是在 Mac 主目录下它们自己的一个文件夹里运行的,这样任何输出都不会显示仓库被取到了哪里。负责测量的脚本用的是第二份 Python,也就是 venv_checks,这样它们用来找窗口的那个库就不可能混进应用里。
这次运行还记下了它是在什么机器上运行的,以及每份 Python 里装了什么。
started 2026-10-10 05:40 UTC
ProductName: macOS
ProductVersion: 15.7.9
BuildVersion: 24G830
Apple M1 (Virtual)
memory: 7 GB
runner image: macos15 20260907.0337.1
the page's copy of the workflow: the same as the one that ran
in venv, for the hello builds:
altgraph==0.17.5
macholib==1.16.4
packaging==26.3
pip==25.0.1
pyinstaller==6.22.3
pyinstaller-hooks-contrib==2026.8
setuptools==84.0.0
in venv_checks:
pip==25.0.1
pyobjc-core==12.2.2
pyobjc-framework-Cocoa==12.2.2
pyobjc-framework-Quartz==12.2.2
in venv, for the Agno build:
agno==3.1.2
agnoctl==0.2.1
altgraph==0.17.5
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
certifi==2026.7.22
docstring_parser==0.18.0
h11==0.16.0
h2==4.4.1
hpack==4.2.0
httpcore==1.0.9
httpcore2==2.13.1
httpx==0.28.1
httpx2==2.13.1
hyperframe==6.1.0
idna==3.20
jiter==0.17.0
macholib==1.16.4
markdown-it-py==4.2.0
mdurl==0.1.2
ollama==0.6.3
openai==3.28.0
packaging==26.3
pip==25.0.1
prompt_toolkit==3.0.53
pydantic==2.14.0
pydantic_core==2.50.0
pydantic-settings==2.15.0
Pygments==2.21.0
pyinstaller==6.22.3
pyinstaller-hooks-contrib==2026.8
python-dotenv==1.2.4
PyYAML==6.0.3
questionary==2.1.1
rich==15.0.0
setuptools==84.0.0
shellingham==1.5.4
sniffio==1.3.1
truststore==0.10.4
typer==0.27.3
typing_extensions==4.16.0
typing-inspection==0.4.4
wcwidth==0.9.2
ollama: ollama version is 0.33.3
NAME ID SIZE MODIFIED
qwen2.5:3b 357c53fb659c 1.9 GB Less than a second ago
finished 2026-10-10 05:47 UTC
🇬🇧 English
It is not free. GitHub's price list, read on 10 October 2026, gave $0.062 a minute for this kind of Mac, against $0.006 for its smallest Linux machine. One run of everything on this page took about four minutes from the push to the commit of the outputs. That is why the workflow starts only when a Python file changes, and not when a sentence does.
The scripts are on this site, each with its output beside it. README.txt says what each one checks and how to run it on a Mac of your own, where no GitHub is needed at all.
🇹🇭 ไทย
มันไม่ฟรี รายการราคาของ GitHub ที่อ่านเมื่อวันที่ 10 ตุลาคม 2026 ระบุ $0.062 ต่อนาทีสำหรับแมคชนิดนี้ เทียบกับ $0.006 สำหรับเครื่อง Linux ที่เล็กที่สุด การรันทุกอย่างในหน้านี้หนึ่งรอบใช้เวลาราวสี่นาที นับจากการ push จนถึงการ commit ผลลัพธ์ นี่คือเหตุผลที่ workflow เริ่มเฉพาะเมื่อไฟล์ Python เปลี่ยน ไม่ใช่เมื่อประโยคเปลี่ยน
สคริปต์ทั้งหมดอยู่ในเว็บไซต์นี้ แต่ละตัวมีผลลัพธ์ของมันอยู่ข้าง ๆ README.txt บอกว่าแต่ละตัวตรวจอะไร และรันบนแมคของคุณเองอย่างไร ซึ่งไม่ต้องใช้ GitHub เลย
🇨🇳 中文
这不是免费的。GitHub 的价目表(读于 2026 年 10 月 10 日)写着:这种 Mac 每分钟 $0.062,而它最小的 Linux 机器每分钟 $0.006。把这一页的所有内容运行一遍,从 push 到把输出 commit 回来,大约四分钟。这就是为什么 workflow 只在 Python 文件有改动时才启动,改一句话是不会启动的。
这些脚本都在本站,每个旁边放着它的输出。README.txt 说明了每个脚本检查什么,以及怎样在你自己的 Mac 上运行它们;那样做完全用不着 GitHub。
🇬🇧 English
Next: Python to EXE does all of this on Windows, packs a game, and puts an AI model inside it. Its last section lists every option PyInstaller takes. And Agno: a company of AI agents is where the cashier comes from.
🇹🇭 ไทย
ต่อไป: Python to EXE ทำทั้งหมดนี้บน Windows แพ็กเกมหนึ่งเกม และใส่โมเดล AI ไว้ข้างใน ส่วนสุดท้ายของหน้านั้นรวบรวมตัวเลือกทุกตัวของ PyInstaller ส่วน Agno: บริษัทของเอเจนต์ AI คือที่มาของแคชเชียร์
🇨🇳 中文
接下来:Python to EXE 在 Windows 上把这些都做了一遍,打包了一个游戏,还在里面装了一个 AI 模型。它的最后一节列出了 PyInstaller 的全部选项。而收银员来自《Agno:AI 智能体公司》。
📋 PyInstaller cheat sheet for the Mac · สรุปย่อ PyInstaller สำหรับแมค · Mac 上的 PyInstaller 速查表
The whole page on one cardทั้งหน้านี้ในการ์ดใบเดียว整页内容,一张卡片
| You wantคุณต้องการ你想要 | You typeคุณพิมพ์你输入 | On our Macบนแมคของเรา在我们的 Mac 上 |
|---|---|---|
| A Python for the buildPython สำหรับใช้บิลด์一份专门用来构建的 Python | python3 -m venv venv venv/bin/python -m pip install pyinstaller | Step 1ขั้นที่ 1第 1 步 |
| A folder with the program in itโฟลเดอร์ที่มีโปรแกรมอยู่ข้างใน一个装着程序的文件夹 | venv/bin/python -m PyInstaller hello.py | dist/hello/ 26.4 MB in 365 files. Step 226.4 MB ใน 365 ไฟล์ ขั้นที่ 226.4 MB,365 个文件。第 2 步 |
| An app that opens with a double-clickแอปที่เปิดด้วยการดับเบิลคลิก双击就能打开的应用 | venv/bin/python -m PyInstaller --windowed hello.py | dist/hello.app 26.7 MB. Step 326.7 MB ขั้นที่ 326.7 MB。第 3 步 |
| One fileไฟล์เดียว单个文件 | venv/bin/python -m PyInstaller --onefile hello.py | dist/hello 9.7 MB. Step 39.7 MB ขั้นที่ 39.7 MB。第 3 步 |
| One file, inside an appไฟล์เดียว ที่อยู่ในแอป单个文件,放在应用里 | venv/bin/python -m PyInstaller --onefile --windowed hello.py | dist/hello.app 9.8 MB. Step 39.8 MB ขั้นที่ 39.8 MB。第 3 步 |
| To know what macOS thinks of the appรู้ว่า macOS คิดอย่างไรกับแอป知道 macOS 怎么看这个应用 | codesign --display --verbose=2 hello.app spctl --assess --type execute --verbose=2 hello.app | Signature=adhoc rejected Step 6ขั้นที่ 6第 6 步 |
| The files for a releaseไฟล์สำหรับ release发布用的文件 | python3 package.py hello 1.0 | A zip of 9.8 MB, a disk image of 12.1 MB, an installer of 9.7 MB and their fingerprints. Step 7ไฟล์ zip 9.8 MB ดิสก์อิมเมจ 12.1 MB ตัวติดตั้ง 9.7 MB และลายนิ้วมือของแต่ละไฟล์ ขั้นที่ 7一个 9.8 MB 的 zip、一个 12.1 MB 的磁盘映像、一个 9.7 MB 的安装包,以及它们的指纹。第 7 步 |
| An AI agent in one fileเอเจนต์ AI ในไฟล์เดียว装进单个文件的 AI 智能体 | venv/bin/python -m PyInstaller --onefile cashier.py | dist/cashier 27.0 MB, and it needs Ollama beside it. Step 927.0 MB และต้องมี Ollama อยู่ข้างนอก ขั้นที่ 927.0 MB,旁边还得有 Ollama。第 9 步 |
🇬🇧 English
Everything you type after the name of a program is a command-line argument. A program reads its arguments and decides what to do. The command this page uses most has four parts, and it helps to know which program reads each one.
🇹🇭 ไทย
ทุกอย่างที่คุณพิมพ์ต่อจากชื่อโปรแกรมคืออาร์กิวเมนต์ของบรรทัดคำสั่ง โปรแกรมอ่านอาร์กิวเมนต์ของมันแล้วตัดสินใจว่าจะทำอะไร คำสั่งที่หน้านี้ใช้บ่อยที่สุดมีสี่ส่วน และควรรู้ว่าโปรแกรมไหนเป็นผู้อ่านแต่ละส่วน
🇨🇳 中文
你在程序名后面输入的一切,都是命令行参数。程序读取自己的参数,再决定做什么。这一页最常用的那条命令有四个部分,弄清每个部分由哪个程序来读,会很有帮助。
venv/bin/python -m PyInstaller --onefile --windowed hello.py
| Partส่วน部分 | What it isคืออะไร它是什么 |
|---|---|
venv/bin/python | The program to start: the Python inside the folder venv. On Windows it is venv\Scripts\python.โปรแกรมที่จะเริ่ม: Python ที่อยู่ในโฟลเดอร์ venv บน Windows คือ venv\Scripts\python要启动的程序:venv 文件夹里的那份 Python。在 Windows 上是 venv\Scripts\python。 |
-m PyInstaller | An option for Python, not for PyInstaller. It tells Python to find the installed module of this name and run it as a program. python -m pip and python -m venv in step 1 are the same idea.ตัวเลือกของ Python ไม่ใช่ของ PyInstaller มันบอกให้ Python หาโมดูลชื่อนี้ที่ติดตั้งไว้ แล้วรันเป็นโปรแกรม python -m pip และ python -m venv ในขั้นที่ 1 ก็เป็นแนวคิดเดียวกัน这是 Python 的选项,不是 PyInstaller 的。它让 Python 找到已安装的这个名字的模块,并把它当作程序来运行。第 1 步里的 python -m pip 和 python -m venv 是同一个道理。 |
--onefile --windowed | Options for PyInstaller. Python stops reading options at the module's name, and hands everything after it to the module.ตัวเลือกของ PyInstaller เมื่อถึงชื่อโมดูล Python จะหยุดอ่านตัวเลือก และส่งทุกอย่างที่ตามมาให้โมดูล这些是 PyInstaller 的选项。Python 读到模块名就不再读选项,后面的一切都交给这个模块。 |
hello.py | The script to pack. It has no dashes in front, so PyInstaller takes it as the thing to work on.สคริปต์ที่จะแพ็ก ไม่มีขีดนำหน้า PyInstaller จึงรู้ว่านี่คือสิ่งที่ต้องทำงานด้วย要打包的脚本。它前面没有短横线,所以 PyInstaller 把它当作要处理的对象。 |
🇬🇧 English
PyInstaller 6.22.3 takes 52 options, and all of them are in the tables below. Some stand alone, like --onefile. Some need a value after them, like --name hello, and a value with a space in it goes inside quotes. Some have a short name with one dash: -F means the same as --onefile.
This page ran two of them: --windowed and --onefile. The others we did not run. What the tables say about them comes from PyInstaller's own help text, which the check flags.py prints and which is saved as flags_output.txt. GitHub's Mac ran that check with the others. Your version may have more options than ours: run the check and compare. Python to EXE has the same tables for Windows.
🇹🇭 ไทย
PyInstaller 6.22.3 มีตัวเลือก 52 ตัว และทั้งหมดอยู่ในตารางข้างล่าง บางตัวใช้เดี่ยว ๆ ได้ เช่น --onefile บางตัวต้องมีค่าตามหลัง เช่น --name hello และค่าที่มีช่องว่างต้องอยู่ในเครื่องหมายคำพูด บางตัวมีชื่อย่อที่ใช้ขีดเดียว: -F มีความหมายเหมือน --onefile
หน้านี้รันไปสองตัว: --windowed และ --onefile ตัวอื่นเราไม่ได้รัน สิ่งที่ตารางบอกเกี่ยวกับตัวเหล่านั้นมาจากข้อความช่วยเหลือของ PyInstaller เอง ซึ่งสคริปต์ตรวจสอบ flags.py พิมพ์ออกมา และบันทึกไว้เป็น flags_output.txt แมคของ GitHub รันสคริปต์ตรวจสอบนี้พร้อมกับตัวอื่น เวอร์ชันของคุณอาจมีตัวเลือกมากกว่าของเรา ลองรันสคริปต์ตรวจสอบแล้วเทียบดู Python to EXE มีตารางเดียวกันสำหรับ Windows
🇨🇳 中文
PyInstaller 6.22.3 一共有 52 个选项,全部列在下面的表里。有的可以单独用,比如 --onefile。有的后面要跟一个值,比如 --name hello;值里有空格时,要放在引号里。有的还有一个只带一条短横线的短名字:-F 和 --onefile 是一个意思。
这一页运行过其中两个:--windowed 和 --onefile。其余的我们没有运行过。表里关于它们的说明,来自 PyInstaller 自己的帮助文本:检查脚本 flags.py 把它打印出来,并保存为 flags_output.txt。GitHub 的 Mac 把这个检查和其他检查一起运行了。你的版本可能比我们的多几个选项:运行这个检查,对比一下。Python to EXE 里有同样的表,是给 Windows 用的。
What to makeจะสร้างอะไร生成什么
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--onedir -D | Make a folder that holds the program and everything it needs. You get this when you ask for nothing. Step 2 builds this kind.สร้างโฟลเดอร์ที่มีตัวโปรแกรมและทุกอย่างที่มันต้องใช้ ถ้าไม่ระบุอะไรเลยก็จะได้แบบนี้ ขั้นที่ 2 บิลด์แบบนี้生成一个文件夹,里面是程序和它需要的一切。什么都不指定,得到的就是这个。第 2 步构建的就是这种。 |
--onefile -F | Make one file that holds everything. Step 3 runs it.สร้างไฟล์เดียวที่รวมทุกอย่างไว้ ขั้นที่ 3 รันตัวนี้生成一个装着一切的单个文件。第 3 步运行了它。 |
--name NAME -n | The name of the app and of the .spec file. Without it, the name of your first script.ชื่อของแอปและของไฟล์ .spec ถ้าไม่ใส่ จะใช้ชื่อของสคริปต์ตัวแรก应用和 .spec 文件的名字。不写的话,用第一个脚本的名字。 |
--specpath DIR | The folder where the .spec file is written. Without it, the folder you are in.โฟลเดอร์ที่จะเขียนไฟล์ .spec ถ้าไม่ใส่ จะใช้โฟลเดอร์ที่คุณอยู่写 .spec 文件的文件夹。不写的话,就是你当前所在的文件夹。 |
--contents-directory NAME | For a folder build only: the name of the folder that holds everything except the program itself. A single dot puts those files beside the program, as older versions did.ใช้กับการบิลด์แบบโฟลเดอร์เท่านั้น: ชื่อของโฟลเดอร์ที่เก็บทุกอย่างยกเว้นตัวโปรแกรม ถ้าใส่จุดตัวเดียว ไฟล์เหล่านั้นจะวางอยู่ข้างตัวโปรแกรม แบบที่เวอร์ชันเก่าทำ只用于文件夹式的构建:存放除程序本身以外所有东西的那个文件夹的名字。写一个点,这些文件就放在程序旁边,和旧版本一样。 |
The window and the iconหน้าต่างและไอคอน窗口和图标
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--windowed -w --noconsole | No console window. On a Mac this also makes the .app. Step 3 runs it.ไม่มีหน้าต่างคอนโซล บนแมค ตัวเลือกนี้ทำให้ได้ .app ด้วย ขั้นที่ 3 รันตัวนี้不要控制台窗口。在 Mac 上,它还会生成 .app。第 3 步运行了它。 |
--console -c --nowindowed | Open a console window for what the program prints and reads. You get this when you ask for nothing.เปิดหน้าต่างคอนโซลสำหรับสิ่งที่โปรแกรมพิมพ์และอ่าน ถ้าไม่ระบุอะไรเลยก็จะได้แบบนี้打开一个控制台窗口,用来显示程序打印的内容、接收它读取的内容。什么都不指定,得到的就是这个。 |
--hide-console CHOICE | Windows only. Keep the console, but hide or minimize it when the program opened it by itself. The choices are minimize-early, hide-early, minimize-late and hide-late.เฉพาะ Windows เก็บคอนโซลไว้ แต่ซ่อนหรือย่อมันเมื่อโปรแกรมเป็นผู้เปิดคอนโซลนั้นเอง ตัวเลือกคือ minimize-early, hide-early, minimize-late และ hide-late仅限 Windows。保留控制台,但如果控制台是程序自己打开的,就把它隐藏或最小化。可选 minimize-early、hide-early、minimize-late 和 hide-late。 |
--icon FILE -i | The app's icon: a .ico file on Windows, a .icns file on a Mac. The word NONE gives no icon. Without this option the app gets PyInstaller's own icon.ไอคอนของแอป: ไฟล์ .ico บน Windows และไฟล์ .icns บนแมค คำว่า NONE คือไม่ใส่ไอคอน ถ้าไม่ใช้ตัวเลือกนี้ แอปจะได้ไอคอนของ PyInstaller เอง应用的图标:Windows 上是 .ico 文件,Mac 上是 .icns 文件。写 NONE 就不加图标。不用这个选项,应用用的是 PyInstaller 自己的图标。 |
--disable-windowed-traceback | In an app with no console, do not show the details of an error that stopped the program. A message says that this is switched off.ในแอปที่ไม่มีคอนโซล ไม่ต้องแสดงรายละเอียดของข้อผิดพลาดที่ทำให้โปรแกรมหยุด จะมีข้อความบอกว่าปิดการแสดงนี้ไว้在没有控制台的应用里,不显示让程序停下的那个错误的详情,而是显示一条消息,说这项功能已关闭。 |
--splash IMAGE_FILE | Experimental. Show a picture while the app unpacks and starts.ยังอยู่ในขั้นทดลอง แสดงรูปภาพระหว่างที่แอปแตกไฟล์และเริ่มทำงาน实验性功能。在应用解压和启动的时候显示一张图片。 |
--splash-center CHOICE | How that picture is centred on the screen: default, virtual, primary or active.วิธีจัดรูปภาพนั้นให้อยู่กลางจอ: default, virtual, primary หรือ active那张图片在屏幕上怎样居中:default、virtual、primary 或 active。 |
Mac onlyเฉพาะแมค仅限 Mac
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--argv-emulation | When someone opens a file with the app, the path of that file is added to the app's arguments.เมื่อมีคนเปิดไฟล์ด้วยแอปนี้ พาธของไฟล์นั้นจะถูกเพิ่มเข้าไปในอาร์กิวเมนต์ของแอป有人用这个应用打开文件时,那个文件的路径会被加进应用的参数里。 |
--osx-bundle-identifier BUNDLE_IDENTIFIER | The identifier of the .app, a name such as com.mycompany.department.appname. Without it, the name of your first script.ตัวระบุของ .app เป็นชื่ออย่างเช่น com.mycompany.department.appname ถ้าไม่ใส่ จะใช้ชื่อของสคริปต์ตัวแรก.app 的标识符,是像 com.mycompany.department.appname 这样的名字。不写的话,用第一个脚本的名字。 |
--target-architecture ARCH --target-arch | The kind of processor to build for: x86_64, arm64 or universal2. Without it, the kind that the build runs on. Our Mac built for arm64.ชนิดของหน่วยประมวลผลที่จะบิลด์ให้: x86_64, arm64 หรือ universal2 ถ้าไม่ใส่ จะเป็นชนิดเดียวกับเครื่องที่ใช้บิลด์ แมคของเราบิลด์ให้ arm64为哪一种处理器构建:x86_64、arm64 或 universal2。不写的话,就是构建所在的那一种。我们的 Mac 构建的是 arm64。 |
--codesign-identity IDENTITY | The identity to sign the app with. Without it, the app gets an ad-hoc signature.ตัวตนที่ใช้เซ็นแอป ถ้าไม่ใส่ แอปจะได้ลายเซ็นแบบ ad-hoc给应用签名用的身份。不写的话,应用得到的是 ad-hoc 签名。 |
--osx-entitlements-file FILENAME | The entitlements file to use when the app is signed.ไฟล์ entitlements ที่ใช้ตอนเซ็นแอป给应用签名时使用的 entitlements 文件。 |
What goes insideใส่อะไรเข้าไปบ้าง把什么装进去
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--add-data SOURCE:DEST | Add a file or a folder that is not Python code. SOURCE is where it is now. DEST is the folder inside the app, and a single dot means the top one. You can use it more than once. Step 9 of Python to EXE runs it.เพิ่มไฟล์หรือโฟลเดอร์ที่ไม่ใช่โค้ด Python โดย SOURCE คือที่อยู่ตอนนี้ และ DEST คือโฟลเดอร์ข้างในแอป จุดตัวเดียวหมายถึงโฟลเดอร์บนสุด ใช้ได้มากกว่าหนึ่งครั้ง ขั้นที่ 9 ของ Python to EXE รันตัวนี้加入不是 Python 代码的文件或文件夹。SOURCE 是它现在的位置,DEST 是应用里面的文件夹,一个点表示最上层。可以用不止一次。Python to EXE 的第 9 步运行了它。 |
--add-binary SOURCE:DEST | The same, for a binary file such as a library.แบบเดียวกัน แต่ใช้กับไฟล์ไบนารี เช่นไลบรารี同上,用于二进制文件,比如库文件。 |
--paths DIR -p | One more folder to search for the modules your script imports.โฟลเดอร์อีกหนึ่งแห่งสำหรับค้นหาโมดูลที่สคริปต์ของคุณ import再加一个文件夹,用来查找脚本 import 的模块。 |
--hidden-import MODULENAME --hiddenimport | Pack a module that no import line in your code shows.แพ็กโมดูลที่ไม่มีบรรทัด import ใดในโค้ดของคุณบอกไว้把代码里没有哪一句 import 提到的模块也打包进去。 |
--collect-submodules MODULENAME | Pack every submodule of this package.แพ็กโมดูลย่อยทุกตัวของแพ็กเกจนี้把这个包的所有子模块都打包进去。 |
--collect-data MODULENAME --collect-datas | Pack every data file of this package.แพ็กไฟล์ข้อมูลทุกไฟล์ของแพ็กเกจนี้把这个包的所有数据文件都打包进去。 |
--collect-binaries MODULENAME | Pack every binary file of this package. Step 9 of Python to EXE runs it.แพ็กไฟล์ไบนารีทุกไฟล์ของแพ็กเกจนี้ ขั้นที่ 9 ของ Python to EXE รันตัวนี้把这个包的所有二进制文件都打包进去。Python to EXE 的第 9 步运行了它。 |
--collect-all MODULENAME | All three at once: the submodules, the data files and the binary files of this package.ทั้งสามอย่างพร้อมกัน: โมดูลย่อย ไฟล์ข้อมูล และไฟล์ไบนารีของแพ็กเกจนี้三样一起:这个包的子模块、数据文件和二进制文件。 |
--copy-metadata PACKAGENAME | Copy the metadata of this package into the app.คัดลอกเมทาดาทาของแพ็กเกจนี้เข้าไปในแอป把这个包的元数据复制进应用。 |
--recursive-copy-metadata PACKAGENAME | The same, for this package and for every package it depends on.แบบเดียวกัน สำหรับแพ็กเกจนี้และทุกแพ็กเกจที่มันต้องพึ่งพา同上,针对这个包和它依赖的每一个包。 |
--exclude-module EXCLUDES | Leave a module or a package out, as though it had not been found.ไม่เอาโมดูลหรือแพ็กเกจนี้ เหมือนกับว่าหามันไม่พบ不要某个模块或包,就当没有找到它。 |
--additional-hooks-dir HOOKSPATH | One more folder to search for hooks.โฟลเดอร์อีกหนึ่งแห่งสำหรับค้นหา hook再加一个文件夹,用来查找 hook。 |
--runtime-hook RUNTIME_HOOKS | A Python file that is packed into the app and runs before any other code, to prepare things for your program.ไฟล์ Python ที่ถูกแพ็กเข้าไปในแอป และรันก่อนโค้ดอื่นทั้งหมด เพื่อเตรียมสิ่งต่าง ๆ ให้โปรแกรมของคุณ一个打包进应用的 Python 文件,它在其他任何代码之前运行,替你的程序做好准备。 |
How the app is builtวิธีบิลด์แอป怎样构建
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--debug CHOICE -d | Help in finding out why a packed app does not work. The choices are all, imports, bootloader and noarchive.ช่วยหาสาเหตุที่แอปซึ่งแพ็กแล้วไม่ทำงาน ตัวเลือกคือ all, imports, bootloader และ noarchive帮你查出打包后的应用为什么不能用。可选 all、imports、bootloader 和 noarchive。 |
--optimize LEVEL | The bytecode optimization level for the Python code that is packed.ระดับการปรับแต่ง bytecode ของโค้ด Python ที่ถูกแพ็ก被打包的 Python 代码所用的字节码优化级别。 |
--python-option PYTHON_OPTION | An option to give to the Python inside the app every time it starts. It accepts v, u, W, X and hash_seed.ตัวเลือกที่จะส่งให้ Python ข้างในแอปทุกครั้งที่แอปเริ่มทำงาน รับได้ v, u, W, X และ hash_seed应用每次启动时交给里面那份 Python 的一个选项。可用 v、u、W、X 和 hash_seed。 |
--strip -s | Strip the symbol tables from the program and its libraries. Not recommended on Windows.ตัดตารางสัญลักษณ์ออกจากตัวโปรแกรมและไลบรารีของมัน ไม่แนะนำบน Windows去掉程序和它的库里的符号表。在 Windows 上不建议用。 |
--noupx | Do not use UPX, even if it is on the computer.ไม่ใช้ UPX แม้ว่าเครื่องจะมีอยู่即使电脑上有 UPX,也不用它。 |
--upx-exclude FILE | Do not let UPX compress this one file. For a file that UPX damages.ไม่ให้ UPX บีบอัดไฟล์นี้ ใช้กับไฟล์ที่ UPX ทำให้เสียหาย不让 UPX 压缩这一个文件。用于会被 UPX 压坏的文件。 |
--upx-dir UPX_DIR | The folder where UPX is. Without it, PyInstaller looks where the computer looks for programs.โฟลเดอร์ที่มี UPX ถ้าไม่ใส่ PyInstaller จะหาในที่เดียวกับที่เครื่องใช้หาโปรแกรมUPX 所在的文件夹。不写的话,PyInstaller 会到电脑查找程序的那些地方去找。 |
Folders and messagesโฟลเดอร์และข้อความ文件夹和消息
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--distpath DIR | Where the finished app is put. Without it, a folder named dist.ที่วางแอปที่บิลด์เสร็จแล้ว ถ้าไม่ใส่ จะเป็นโฟลเดอร์ชื่อ dist做好的应用放在哪里。不写的话,是名叫 dist 的文件夹。 |
--workpath WORKPATH | Where the working files are put. Without it, a folder named build.ที่วางไฟล์ระหว่างทำงาน ถ้าไม่ใส่ จะเป็นโฟลเดอร์ชื่อ build工作文件放在哪里。不写的话,是名叫 build 的文件夹。 |
--noconfirm -y | Replace the folder of the last build without asking.เขียนทับโฟลเดอร์ของการบิลด์ครั้งก่อนโดยไม่ถาม不询问,直接替换上一次构建的文件夹。 |
--clean | Empty PyInstaller's cache and remove its temporary files before the build.ล้างแคชของ PyInstaller และลบไฟล์ชั่วคราวของมันก่อนบิลด์构建之前,清空 PyInstaller 的缓存并删除它的临时文件。 |
--log-level LEVEL | How much the build prints: TRACE, DEBUG, INFO, WARN, DEPRECATION, ERROR or FATAL. Without it, INFO.การบิลด์จะพิมพ์ข้อความมากแค่ไหน: TRACE, DEBUG, INFO, WARN, DEPRECATION, ERROR หรือ FATAL ถ้าไม่ใส่ จะเป็น INFO构建时打印多少内容:TRACE、DEBUG、INFO、WARN、DEPRECATION、ERROR 或 FATAL。不写的话,是 INFO。 |
--help -h | Print the list of options and stop.พิมพ์รายการตัวเลือกแล้วหยุด打印选项列表,然后停止。 |
--version -v | Print PyInstaller's version and stop.พิมพ์เวอร์ชันของ PyInstaller แล้วหยุด打印 PyInstaller 的版本,然后停止。 |
Windows onlyเฉพาะ Windows仅限 Windows
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--version-file FILE | Add version details from a file to the .exe.เพิ่มข้อมูลเวอร์ชันจากไฟล์ลงใน .exe把一个文件里的版本信息加进 .exe。 |
--manifest FILE | Add a manifest to the .exe, from a file or written as XML.เพิ่ม manifest ลงใน .exe จากไฟล์ หรือเขียนเป็น XML给 .exe 加上 manifest,可以来自文件,也可以直接写成 XML。 |
-m FILE | An old short name for the manifest option, and no longer to be used. It is PyInstaller's own, and has nothing to do with the option of the same name that Python takes.ชื่อย่อแบบเก่าของตัวเลือก manifest และไม่ควรใช้แล้ว เป็นของ PyInstaller เอง และไม่เกี่ยวกับตัวเลือกชื่อเดียวกันของ Pythonmanifest 选项以前的短名字,已经不该再用。它是 PyInstaller 自己的,和 Python 那个同名的选项没有关系。 |
--resource RESOURCE -r | Add a resource to the .exe, or update one.เพิ่ม resource ลงใน .exe หรืออัปเดตที่มีอยู่给 .exe 加入一个资源,或更新已有的资源。 |
--uac-admin | The app asks for administrator rights when it starts.แอปจะขอสิทธิ์ผู้ดูแลระบบเมื่อเริ่มทำงาน应用启动时会请求管理员权限。 |
--uac-uiaccess | Lets an app that has administrator rights work with Remote Desktop.ทำให้แอปที่มีสิทธิ์ผู้ดูแลระบบใช้งานกับ Remote Desktop ได้让拥有管理员权限的应用能配合 Remote Desktop 使用。 |
Rarely neededไม่ค่อยได้ใช้很少用到
| Optionตัวเลือก选项 | What it doesทำอะไร它做什么 |
|---|---|
--runtime-tmpdir PATH | For a one-file app: the folder to unpack into, in place of the computer's temporary folder.สำหรับแอปแบบไฟล์เดียว: โฟลเดอร์ที่จะแตกไฟล์ลงไป แทนโฟลเดอร์ชั่วคราวของเครื่อง用于单文件应用:解压到哪个文件夹,用来代替电脑的临时文件夹。 |
--bootloader-ignore-signals | The small program that starts your app ignores signals, and does not pass them on to your program.โปรแกรมเล็ก ๆ ที่เริ่มแอปของคุณจะไม่สนใจ signal และไม่ส่งต่อให้โปรแกรมของคุณ启动你的应用的那个小程序会忽略信号,不再把它们转给你的程序。 |