PyInstaller 打包 FastAPI + uvicorn:能跑起来的最小写法

想把 FastAPI 项目打包成一个 exe 发给非技术用户,直接:

pyinstaller -F main.py

跑一下崩得七七八八。因为 uvicorn 走的是字符串式导入,PyInstaller 环境下拿不到;再加上一堆子模块(uvicorn.loops.*starlette.*)静态分析捞不全。

正确起步:写个 start.py

不要直接打 main.py,写个显式导入 app 对象的启动文件:

# start.py
from main import app          # 直接拿对象,不用字符串
import uvicorn

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

对应的 main.py

from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def root():
    return {"hello": "world"}

打包 start.py

pyinstaller -F start.py

Could not import module "main" 的原因

如果 start.py 里写:

uvicorn.run("main:app", host="0.0.0.0", port=8000)

main:app字符串,uvicorn 会在运行时 importlib.import_module("main")。PyInstaller 打包后 main 不在 sys.path 里,直接崩:

Error loading ASGI app. Could not import module "main"

改成直接 import + 传对象最省事:

from main import app
uvicorn.run(app, ...)

或者告诉 PyInstaller 也带上 main

pyinstaller -F start.py --hidden-import=main

uvicorn 子模块丢失

跑起来抛:

ModuleNotFoundError: No module named 'uvicorn.loops'

uvicorn 有一堆按需加载的 loop / protocol / logger 子模块,PyInstaller 全漏了。一次性带上:

pyinstaller -F start.py \
  --collect-all fastapi \
  --collect-all uvicorn \
  --collect-all starlette \
  --collect-all pydantic

--collect-all X = 把 X 及其所有子模块、数据文件、二进制全打进去。

静态文件 / 模板

用了 StaticFilesJinja2Templates 得加数据:

Windows:

pyinstaller -F start.py ^
  --add-data "templates;templates" ^
  --add-data "static;static"

Linux/macOS:

pyinstaller -F start.py \
  --add-data "templates:templates" \
  --add-data "static:static"

代码里读路径要兼容 PyInstaller 临时目录:

import sys, os
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

def base_path():
    return getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))

app.mount("/static", StaticFiles(directory=os.path.join(base_path(), "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(base_path(), "templates"))

后台运行、无 CMD 窗口

想双击不弹黑框:

pyinstaller -F -w start.py --collect-all uvicorn ...

-w 关控制台。注意:关了控制台,print / uvicorn 的日志就看不到了,改成写日志文件:

uvicorn.run(
    app,
    host="0.0.0.0",
    port=8000,
    log_config={
        "version": 1,
        "handlers": {
            "file": {"class": "logging.FileHandler", "filename": "app.log"},
        },
        "root": {"handlers": ["file"], "level": "INFO"},
    },
)

完整命令模板

一次搞定:

pyinstaller -F -w start.py ^
  --name myapi ^
  --icon app.ico ^
  --add-data "templates;templates" ^
  --add-data "static;static" ^
  --collect-all fastapi ^
  --collect-all uvicorn ^
  --collect-all starlette ^
  --collect-all pydantic

产物:dist/myapi.exe,双击就是一个后台服务。

什么时候别打包

  • 生产环境线上服务:Docker / systemd 更好
  • 要热更新:exe 每次改代码都要重打
  • 依赖 GPU / CUDA:打包体动辄几百 MB
  • 多用户 / 高并发:exe 里塞 uvicorn 效率一般

PyInstaller 更适合:内网小工具、给客户发一个”点两下就跑”的本地 API、演示 demo。

一句话总结

start.py + 直接 import app + --collect-all 四个关键字。字符串导入是坑,把 uvicorn 家族收全就通了。