FR-063 · Nuitka Packaging(Linux 容器版)— 需求討論稿 · 2026-08-14
產品進入落地版(on-premise)交付階段。本案用 Nuitka standalone 把整個 BE 編成機器碼,配合 FR-062 既有的 license 執法形成雙保險。2026-08-13 夜間已在 STG 機完成第一次冷 build 實測(1h27m / 808MB 產物),並撞出第一個 import 期炸點(dependency-injector)。本稿整理十一項已定案決策(D1–D11,全數定案、無待確認項)、三路掃描的現況盤點(不修就死/靜默失效/路徑類/外部依賴)、build 管線設計與階段拆分草案。
產品進入落地版(on-premise)交付階段——客戶主機上不可再出現可讀原始碼。這是一條三階段戰線,本案是第一棒:
把 BE 整包編成機器碼 binary,客戶主機上沒有 .py 可讀。範圍限定 Linux 容器版。
下兩棒:防竄改機制、安裝器。Windows 支援也在下一階段——傾向用 Docker Desktop / WSL2 跑同一個 Linux image,不另編 Windows binary。
比較過四條路:.pyc(compileall)/ Cython / PyArmor / Nuitka。值得記一筆的是:Python 3.11 的反編譯工具鏈已經斷裂(uncompyle6 / decompyle3 都停在 3.8 前後),純 .pyc 交付其實已有基礎保護力。但決策者拍板採 Nuitka standalone 編成機器碼,取得最高保護等級,對齊 Go binary 陣營的交付形態。
業界對照:
| 陣營 | 代表 | 交付形態 | 保護手段 |
|---|---|---|---|
| 源碼可見 | GitLab、Sentry | Ruby / Python 原始碼直接落地 | 靠 license 執法 |
| 編譯 binary | Grafana、HashiCorp | Go 編譯後 binary | 機器碼天然不可讀 |
| 本案 | Guidant AI 落地版 | Nuitka binary | license 執法(FR-062 既有)+ binary 交付,雙保險 |
環境:STG 機 /opt/guidant_ai,Nuitka 4.1.3 / Python 3.11,4 core / 32GB。
1h27m冷 build 時間(4 core)
808MB產物 main_app.dist
8,613C 檔全編
34minpymupdf.mupdf.c 單檔耗時(平行化不可壓縮地板)
🔴 試跑結果:import 期就炸
SystemError: dynamic module 'dependency_injector.providers'
not initialized properly from def
炸點是 dependency-injector 套件本身——整個 DI 體系的地基。根因分析與修法已定案為 D2(見下節)。
以下十一項均已定案,不是 pending。每項附理由。D7–D10 為原「待確認 Q1–Q4」拍板後升格;D11 為 2026-08-14 config 盤查後新增拍板。
D1 api 模式先驗,socketio 模式隨後(單一 binary 兩模式,見 D9)
api 模式路徑完全不碰 eventlet(已掃描確認)——先驗它就等於 90% 業務程式碼到位。socketio 模式隨後跟進,唯一的增量風險是 eventlet monkey-patch × Nuitka 相容性,必須實測。
兩模式是獨立 process(port 8000 / 8002),不是線程,可以分開驗證。依 D9 統一入口決策,兩模式編在同一個 binary、由 RUN_MODE 環境變數切換,不再分兩支入口各自編譯。
保底方案:若 eventlet 實測卡關,socketio 模式換 async 承載(gevent 等——flask-socketio 支援換 async_mode)。入口薄殼只有十幾行、無祕密,業務模組仍全數受編譯保護。
D2 dependency-injector 私房 wheel,rebuild 納入 compile 管線前置步驟
炸因:官方 wheel 以 Limited API(abi3)模式編譯 → 強制走 PEP 489 多相初始化 → 與 Nuitka 靜態嵌入環境不相容。
已驗證:PyPI 有 sdist(1MB,BSD 授權),setup.py 會現場 cythonize 從 .pyx 重生 C 碼。兩條修法候選:
-DCYTHON_PEP489_MULTI_PHASE_INIT=0 強制關多相初始化管線設計:每次 build 照 pyproject.toml 鎖定版本現場拉 sdist 重編;版本感知快取(wheel 檔名含版號,同版直接用快取)+ sdist tar.gz 留檔防 PyPI 不可用。升版自動觸發重編,零漂移。
D3 建 resource_path() 統一資源定位入口
開發模式回原始碼樹、打包模式回 dist 根,所有資料檔讀取收斂到這一個入口。影響範圍=盤點表 #4 的四處資料檔(translations .mo/SSP docx 範本/cmmc_l1_canon.json/cmmc_l1_aos.json)。
順帶清理四件事:
docs/(release 產物不該讀 docs/)static/(見盤點 #8 雙重身分衝突)api/__init__.py 死碼 load_routes()D4 證據分類 docker CLI 依賴:落地版功能開關降級停用
classifier_container_runner.py 跑 cmmc-classifier:latest 需要 docker CLI——BE 自己容器化之後這變成 DinD(Docker-in-Docker)問題。
落地版第一批以功能開關停用此能力,避免安裝複雜度暴增。日後客戶真有需求再議:掛 docker.sock 或改分類器執行形態。
D5 專用 build 機 24 core / 32GB(user 準備)
24 core 預估冷 build ~35–40 分鐘(pymupdf 單檔 34 分鐘是不可壓縮地板),配 ccache 熱 build 10 分內。需 100GB+ SSD。
build 流程全 script 化——build 機只是算力,搬家零成本。
D6 版本號+git commit hash build 期 bake 進產物
/api/1.0/version 現在讀 pyproject.toml——產物內沒有這個檔,會回 unknown。改為 build 期生成 version.py 常數檔一起編進去。
擴充(定案):release build 把 git commit hash 一併 bake 進 version 資訊,/api/1.0/version 回 version + commit。動機:編譯後 traceback 只剩函式名、無原始碼行號,support 只能靠「版號 → git tag 原始碼」對照定位問題——log 內的版本資訊必須足以唯一鎖定 commit,光有版號不夠(同版號可能有 hotfix 重 build)。
D7 main_app 承載換 gunicorn 內嵌(原 Q1)
Flask dev server 不上 production——這是落地版對外交付,執行形態在此定案。但 Nuitka 下不能走 gunicorn main:app 的 CLI import 模式:binary 產物內沒有可供 gunicorn 外部 import 的模組。改採 gunicorn BaseApplication API 內嵌——在我們的程式內起 master/worker,入口仍是我們的 binary,gunicorn 作為依賴一起編入。worker 數走環境變數。開發模式保留 dev server(開關切換,開發體驗不變)。
D8 production image 非 root 執行(原 Q2)
定案非 root。理由:合規產品的客戶會掃 image,「root 執行」是標準紅字,賣合規產品自己先被掃出紅字說不過去;非 root 亦降低容器被攻破後的權限面。
做法:image 內建固定 uid(如 1000)專用使用者+entrypoint 啟動時 chown volume 目錄(解掛載目錄 uid/gid 不合的老問題)+安裝文件寫明。此為 FR-065 installer 要一起承接的細節。
D9 統一入口+按模式裁剪載入,三支舊入口全退役(原 Q3,拍板並加碼裁剪需求)
新增統一入口(骨架以 main_app.py 邏輯為主——它較新、註解完整),RUN_MODE 環境變數切換 api(預設)/ socketio 模式;main.py(舊版,會 create_all 建表)/ main_app.py / main_socketio.py 三支全退役。單一 binary,build 一次,容器 command 只差環境變數,無縫接現行「一 image 兩 entrypoint」部署慣例。
eventlet monkey_patch 收進 if mode == "socketio": 條件塊,且必須在其他 import 之前——Python import 是執行到才發生,Nuitka 保留模組執行順序語意,此 pattern 編譯後行為不變。
PyCharm debug 痛點不回歸(歷史脈絡,決策時特別確認過):當初拆兩檔正是因為 eventlet monkey_patch 與 pydevd debugger 衝突(斷點中斷/exception)。統一入口下 RUN_MODE 不設或 =api 時,import eventlet 那行根本不執行、stdlib 零觸碰,debug 體驗與現行 main_app.py 完全等價;且 patch 收進條件塊後,誤 import 觸發 patch 的面比現狀更小。
模式裁剪載入表——現況是 main_socketio.py 與 main_app.py 幾乎孿生:socketio 進程背著全部 REST blueprint+全部 4 個 APScheduler job 雙跑,靠 CAS 冪等擋。統一入口順勢裁剪:
| 載入項 | api 模式 | socketio 模式 |
|---|---|---|
| REST blueprints(REGISTERED_APPS) | 全載 | 不載(僅留 health check) |
| socketio init + Redis message queue | 不載 | 載 |
| APScheduler 4 jobs | 載(唯一持有者) | 不載——解掉雙跑 |
| eventlet monkey_patch | 無 | 有 |
| DI container | 全量 | 第一版照全量(求穩),瘦身留第二步 |
| 承載 | gunicorn 內嵌(D7) | socketio.run(eventlet) |
紅利:socketio 進程瘦身(啟動快/記憶體省/8002 不再暴露整套 REST API)、scheduler 雙跑收斂到 api 進程單一持有、eventlet × Nuitka 的實測覆蓋面縮小。
工程注意:socketio 模式的 DI 最小集邊界要實際盤(socket handler 依賴回追),第一版採「不載 blueprint、不起 scheduler、DI 照全量」求穩——裁過頭會踩隱性依賴。
D10 jedi-file-upload 硬編 LibreOffice 路徑修正(原 Q4)
套件內 local_file_adapter.py / minio_adapter.py 硬編 platform 判斷、不吃 LIBREOFFICE_CMD,與主專案解析邏輯不一致(盤點#12)。定案修正:改為與主專案同邏輯(LIBREOFFICE_CMD env → PATH → mac fallback)。
屬 jedi-* 套件異動,照規範走:開發期 poetry path dependency、完成後 pin 版發佈。納入 .1 清障子需求。
D11 Config 三層重整+死項清理(新增子任務 .1g)
動機:打包成 binary 後「改 config=重編譯交付」,設定放錯層代價放大;現行六套環境 class 是雲端多環境時代的設計,落地版每個客戶都是新環境,不可能為客戶加 class 重編譯。分層判準是「誰、多常、需要改它」:
| 層 | 判準 | 改動代價 |
|---|---|---|
| 編譯進 binary | 產品常數 | 改=出新版 |
| 環境變數 | 每部署點不同 | 裝機定一次 |
DB system_config |
營運中會調 | 線上改即生效 |
第 1 刀:六套環境 class 塌成一套 Config
BaseConfig 為底),六套子 class(DevelopmentPremise / StagingPremise / StagingAwsBillowS / ProductionAwsBillowS / StagingAwsNics / ProductionAwsNics)全刪WEBSITE_URL→刪(見死項)、CORS_ALLOWED_ORIGINS→env 逗號分隔(預設 *)、DB_PORT→env(預設 5432)、LOGGING_LOCATION→env LOG_DIR(預設 ./log)、SQLALCHEMY_TRACK_MODIFICATIONS→死項刪、JWT 效期→第 3 刀進 DBconfig_util.py 的 ENV→class 對照表退役;ENV 變數降級為純顯示標籤(log 印一行,不驅動行為),不破壞現有部署腳本AWD_DEFAULT_URL、BaseConfig.DB_NAME="audit_manager".env 翻譯是機械操作;STG / POC 實際更新等部署放行(環境鐵律)第 2 刀:Secret 拆 JSON 包裝+啟動期集中驗證
config/config_loader.py:支援新格式平鋪變數(DB_USER / DB_PASSWORD / REDIS_USER / REDIS_PASSWORD / JWT_SECRET_KEY),向後相容舊格式(偵測 DB_SECRET 存在照舊 json.loads,現有 .env 零改動)json.loads 直接 tracebackjson.loads 移進 loader,由統一入口(D9 / .1c)最早階段呼叫DB_USERNAME / DB_PASSWORD 命名分歧一併對齊收斂(盤查發現 jedi 讀的變數名與主專案 .env 是兩套)第 3 刀:營運參數遷移 DB system_config(內建預設+DB 覆寫)
LOGIN_MAX_LOCK_COUNT / LOGIN_USER_LOCK_TIME(客戶資安政策各異)、CHANGE_PASSWORD_THRESHOLD_DAYS、LOGIN_INACTIVITY_THRESHOLD_DAYS、JWT_ACCESS/REFRESH_TOKEN_EXPIRES、MFA_REQUIREDinfra/system_config/system_config_root_reader.py 現成);編譯內建預設值,DB 有值覆寫,讀不到照預設跑(無啟動依賴)ON CONFLICT DO NOTHING),只套 DEV死項清理(2026-08-14 盤查定案,附判定依據)
TOKEN_TTL、USER_LOCK_COUNT、USER_PASSWORD_LIMIT_TIME(舊代,新代 LOGIN_* / CHANGE_PASSWORD_* 活著)、PROJECT_EXCEL_UPLOAD_DIR / PROJECT_EXCEL_SAMPLE_DIR / PROJECT_EXCEL_SAMPLE_FILENAME、USER_EXCEL_SAMPLE_DIR / USER_EXCEL_SAMPLE_FILENAME、ANSWER_SAMPLE_DIR / ANSWER_SAMPLE_FILENAME、LOGGING_FORMAT、FEEDBACK_ALLOWED_EXTENSIONS(另兩處同名模組常數是別的東西不動)、SCHEDULER_API_ENABLED(裝的是純 apscheduler 非 flask-apscheduler)、WEBSITE_URL(唯一用途餵 CORS,inline 後刪)SQLALCHEMY_* 四支 ×7 份全刪(user 拍板):盤查發現專案未裝 flask_sqlalchemy,app_factory 自組 URI,SQLALCHEMY_DATABASE_URI / BINDS / POOL_RECYCLE / TRACK_MODIFICATIONS 全無消費者——連帶事實:read replica 綁定無人消費,DB_READ_HOST 設了未生效,讀寫分離從未實際運作。落地版單機 PG 無 replica 需求;真要讀寫分離是獨立 feature 不夾帶。DB_READ_HOST 保留 env 定義,文件標註「目前未生效」APISPEC_* 三支刪(user 拍板):docs.init_app 被註解、Swagger 從未啟用;落地版不應對客戶暴露 Swagger。Swagger 補齊列 follow-up 不進本案.env 死變數 4 項清:REDIS_PORT(port 硬寫 6379)、UPLOAD_DIR / UPLOAD_FILE_DIR / UPLOAD_STATIC_DIR(僅 test fixture 用).env 整理:TEST_* / FR0*_SEED_* 六項測試帳密移 .env.test;NOTION_API_TOKEN 留(工具鏈用)加註釋分區;LICENSE_ENFORCEMENT_ENABLED / LICENSE_READONLY_GATE_ENABLED 顯式寫入 .env(現在靠預設全開,曾是 DEV 踩雷點)並列入安裝設定清單必填BUNDLE_ERRORS(flask_restful 隱式讀)、PROPAGATE_EXCEPTIONS(Flask core 讀)、CHANGE_PASSWORD_REQUEST_EXPIRATION_DAYS(餵 jedi-login DTO)、USER_EXCEL_UPLOAD_DIR、ANSWER_UPLOAD_DIR、REDIS_URL(flask_redis 讀)app.config(已驗證),只吃 env+DTO,死項判定無漏掃風險.1g 驗收條件:①三環境用舊 .env 原封不動起服務,行為零差異(向後相容)②新格式 .env 起服務行為相同 ③故意漏設必填→啟動一次列全缺項非 traceback ④DB system_config 改鎖定次數生效、刪除回內建預設 ⑤既有測試全綠
影響面:讀 app.config[...] 的呼叫點全不用動(key 名不變,值來源變);要動:config.py 大改、config_util.py 退役對照表、新增 config_loader.py、統一入口接線、一支 migration、密碼政策 / JWT 讀取點改走 system_config reader(約 3–5 個 service 檔)。
「編得出來」≠「功能活著」——本案盤點出的地雷多為靜默失效型(不炸但功能悄悄壞),驗收必須逐雷探針,缺一不可:
| 探針 | 探的雷 |
|---|---|
| 登入 | 基本 API/DI wire 全量 |
| 任一分頁列表 | blueprint 載入完整 |
| i18n 中文回應 | .mo 檔有打包(盤點#4) |
| SSP docx 匯出 | 範本檔有打包(盤點#4) |
| 摘要報告 PDF 開檔驗中文 | WeasyPrint 系統庫+字型(盤點#5/#13) |
| 問卷上傳 | static/上傳目錄可寫(盤點#8/#9) |
| socketio 通知 | eventlet 編譯後可用(D1/D9) |
以下是「動態 import/__file__ 路徑/外部依賴」三路掃描的完整結果,按嚴重度分級。
| # | 位置 | 問題 | 修法 |
|---|---|---|---|
| 1 | config/di_modules.py:49,72 |
rglob('*.py') 掃 DI wire 模組——產物內沒有 .py,掃空後 wire 只剩 2 支固定模組 → 所有 route 的 @inject 失效。最陰的是啟動不報錯,第一個 request 才炸 |
build 期 dump 靜態模組清單:gen script 從現行掃描結果產 config/di_modules_static.py;開發模式照掃、release 模式用靜態清單 |
| 2 | dependency_injector 套件 |
import 期 SystemError(abi3 × Nuitka 不相容) |
見 D2 私房 wheel |
| # | 位置 | 問題 | 修法 |
|---|---|---|---|
| 3 | main_app.py:26 / main_socketio.py:28 / main.py:21 |
importlib.import_module 動態載 REGISTERED_APPS blueprint,except ImportError: logger.warning 靜默吞錯——編譯後若缺 blueprint 只噴一行 warning,整個模組的 API 消失 |
改 fail-fast;並以靜態 import 補強(--include-package=api 理論上已涵蓋,但要驗證) |
| 4 | 資料檔 ×4(__file__ 相對定位) |
Nuitka 不自動打包非 .py 檔,四處全滅(明細見下) |
併入 D3 resource_path() |
| 5 | 中文字型 | fc-match build 期斷言要在 production Dockerfile 重建——缺字型時 PDF 照樣回 200 但中文整段消失 |
Dockerfile 裝 fonts-noto-cjk + build 期 fc-match 'Noto Sans CJK TC' 斷言 |
| 6 | api/version/routes/version_route.py:17 |
讀 pyproject.toml,產物內無 → 回 unknown |
見 D6 version bake |
#4 的四處資料檔明細:
| 位置 | 讀的檔 | 壞法 |
|---|---|---|
config/config.py:46 TRANSLATIONS_DIR |
config/translations 下的 .mo |
i18n 全滅;.mo 是 build 產物不入版控,管線要記得先 pybabel compile |
app/oscal/service/export/ssp_docx_generator.py:27-29 |
app/oscal/templates/ssp/ssp_cmmc_template.docx |
SSP 匯出壞;且用 ../../ 相對跳躍定位,特別脆 |
app/evidence_classification/service/report/report_common.py:16 |
resources/cmmc_l1_canon.json |
缺檔回 None 靜默降級,四處裡最陰——完全無錯誤訊息 |
app/evidence_classification/service/catalog_builder.py:11 |
parents[3] + docs/reference/CMMC-Level 1-Evidences/cmmc_l1_aos.json |
讀 docs/ 下的檔——現行 image 可能已經是壞的;要搬出 docs/(D3 順帶) |
| # | 位置 | 問題 | 修法 |
|---|---|---|---|
| 7 | log/app.log |
cwd 相對路徑;且 import jedi_issue 期就 makedirs('log')(在 jedi_common 內)——cwd 不對就在奇怪位置長出 log/ |
entrypoint 必先 chdir 到固定工作目錄 |
| 8 | static/ 雙重身分 |
同一目錄同時是上傳落地處(要 volume、可寫)與範本下載處(要隨版本、唯讀)——容器化後兩個需求直接打架 | 拆開:範本移出 static(併入 D3) |
| 9 | 上傳根目錄 | DB STORAGE_CONFIG.base_dir 未設時 fallback /tmp/upload/ → 容器重啟即丟 |
落地版必須明確設定並掛載 |
| 10 | volume 清單 | 見下表 | Dockerfile / compose 明確宣告 |
| 11 | core/app_factory.py:35 template_folder='../templates' 指向不存在目錄;api/module_frame/__init__.py:83 等三處 Blueprint static_folder='static' 指向不存在目錄,且 module_frame_import_route.py:32 拿它 join 下載範本路徑(這條現在就是壞的) |
死宣告+壞功能 | D3 一併清理 |
#10 需要外掛 volume 的完整清單:
| 掛載點 | 用途 | 備註 |
|---|---|---|
上傳目錄(STORAGE_CONFIG.base_dir) |
使用者上傳檔 | 見 #9,必須明確設定 |
log/ |
app log | 配合 entrypoint chdir |
$HOME/.cm-jobs |
job 狀態 | |
~/.config/libreoffice |
LibreOffice profile | 首次轉檔會建 |
TMPDIR |
LibreOffice 轉檔中繼 | 容量要夠,大檔轉檔會吃 |
| agent 憑證目錄 | AGENT_* env 指的憑證檔 |
pki/ 已在 dockerignore,檔案只能外部掛入 |
| # | 項目 | 說明 |
|---|---|---|
| 12 | 外部 binary ×4 | LibreOffice(解析順序 LIBREOFFICE_CMD → PATH → mac 絕對路徑;⚠️ jedi-file-upload 套件內另有一份硬編路徑不吃 LIBREOFFICE_CMD,不一致要注意);cinc-auditor(275MB,CINC_AUDITOR_CMD);docker CLI(見 D4 停用);tesseract(優雅降級,現行不裝) |
| 13 | WeasyPrint 系統庫 | import 期 dlopen pango / harfbuzz / gobject / fontconfig——Nuitka 不自動收 dlopen 的 .so。image 內 apt 裝齊即可(與現行 Dockerfile 相同做法),不必打進 binary |
| 14 | 環境變數 60+ 個 | DB_SECRET / JWT_SECRET / REDIS_SECRET import 期就 json.loads——缺了 import 階段直接炸;jedi-* 套件內另有 16 個主專案 grep 不到(清單見下);AGENT_* 值是憑證檔路徑,檔案要外部掛載;正式進入點不載 .env,全靠外部注入 |
| 15 | APScheduler 4 jobs | drive_sync_worker(5s)/ webhook_channel_renewer(6h)/ framework_parse_job_cleanup(每日)/ license_expiry_state_machine(每日)——現況兩個進入點都會起(雙跑靠 CAS 冪等擋),無額外 binary 依賴,編譯面無風險。統一入口後收斂為 api 模式單一持有(見 D9) |
#14 jedi-* 套件內、主專案 grep 不到的環境變數(落地版部署文件必列):
| 類別 | 變數 |
|---|---|
| 執行環境 | ENABLE_MULTI_TENANT、RUN_ENV、TZ |
| DB | DB_USERNAME、DB_PASSWORD、DB_SCHEMA、DEFAULT_SCHEMA |
| Redis | REDIS_DB、REDIS_SSL |
| i18n | DEFAULT_LOCALE |
| 外部整合 | GITLAB_*、GITHUB_PRIVATE_TOKEN |
| Auth | OTP_COOLDOWN_SECONDS、AUTH_CHANGE_SECRET_COOLING_HOURS |
(主專案自身的 60+ 個以現行各 Config class 為準,.1 子需求收斂時產完整對照表。config 死項盤查結論見 D11。)
inspect.getsource:零命中__import__("uuid") 常量參數,Nuitka 可靜態解析scheduler_report_route 的 listdir 掃的是 runtime 目錄,安全%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
S1["Step 1<br/>dependency-injector<br/>私房 wheel rebuild<br/>(D2,版本感知快取)"] --> S2["Step 2<br/>poetry install<br/>+覆蓋私房 wheel"]
S2 --> S3["Step 3<br/>gen 靜態 DI 清單<br/>+ version bake<br/>(盤點#1 / D6)"]
S3 --> S4["Step 4<br/>Nuitka 編譯統一入口<br/>(單 binary 兩模式,D9;<br/>api 模式先驗,D1)"]
S4 --> S5["Step 5<br/>production Dockerfile<br/>打 image<br/>apt 依賴+fc-match 斷言"]
S5 --> S6["Step 6<br/>smoke 驗證<br/>啟動+關鍵 API 巡檢"]
Step 5 的 OS 依賴清單沿用現行 E2E Dockerfile 的 apt 清單(LibreOffice / WeasyPrint 系統庫 / fonts-noto-cjk 等)+ fc-match 斷言;執行形態=gunicorn 內嵌(D7)、非 root 執行(D8),均已定案。Step 6 的 smoke 巡檢照上節「Smoke 驗收清單」逐雷探針。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
subgraph DEV["開發模式(現況不變)"]
D_SRC["原始碼樹 .py"] --> D_SCAN["di_modules.py<br/>rglob 動態掃"]
D_SRC --> D_RES["resource_path()<br/>回原始碼樹"]
end
subgraph REL["release 模式(Nuitka 產物)"]
R_BIN["統一入口 dist<br/>binary+隨附資料檔(D9)"] --> R_STATIC["di_modules_static.py<br/>build 期產的靜態清單"]
R_BIN --> R_RES["resource_path()<br/>回 dist 根"]
end
D_SCAN -. "gen script<br/>build 期 dump" .-> R_STATIC
雙軌的原則:同一份程式碼,兩個模式行為等價——開發模式維持動態掃描(加檔即生效,開發體驗不變),release 模式吃 build 期凍結的靜態產物。gen script 是兩軌之間唯一的橋,掃描結果 dump 下來就是靜態清單,不存在第二套手維護名單。
給後續拆卡參考,粗粒度四個子需求:
DI 靜態清單機制(盤點#1)、resource_path() 重構含全部影響面(D3、盤點#4/#8/#11)、blueprint fail-fast(盤點#3)、死碼清理、version + commit bake(D6)、統一入口實作(D9,三支舊入口退役)、jedi-file-upload LibreOffice 路徑修正(D10)、g. config 三層重整+死項清理(D11)——排序 .1b 之後、與 .1c 可平行;第 2 刀 loader 與統一入口(.1c)同檔作業。
驗收草案:開發模式行為零變化(既有測試綠、PyCharm debug 體驗不變);grep 全 codebase 無殘留 __file__ 相對資料檔定位;blueprint 缺模組時啟動即炸而非 warning;RUN_MODE 兩模式各自可起。
wheel rebuild script(D2 含快取)、Nuitka build script、build 機環境建置(D5)。
驗收草案:build 機上一鍵跑完 Step 1–4 產出可啟動的統一入口 dist;私房 wheel 快取命中時跳過重編;產物 import 期不炸、DI wire 全量。
volume 佈局(盤點#10)、環境變數文件化(盤點#14 完整對照表)、fc-match 斷言(盤點#5)、entrypoint chdir(盤點#7)、gunicorn 內嵌承載(D7)、非 root 執行+entrypoint chown(D8)。
驗收草案:image 啟動後過完整「Smoke 驗收清單」(見決策節,逐雷探針:登入/列表/i18n/SSP docx/PDF 中文/上傳/socketio);重啟後上傳檔不丟;容器內找不到任何 .py 業務原始碼;process 以非 root uid 執行。
單 binary 已含 socketio 模式(D9),本階段重點不是編譯而是:eventlet monkey-patch × Nuitka 相容性實測+裁剪載入實作(不載 blueprint、不起 scheduler,照 D9 載入表);卡關則啟動 D1 保底(換 async 承載)。
驗收草案:RUN_MODE=socketio 進程可建立連線並收發事件、不載 REST blueprint、scheduler 不雙跑;或保底方案落地且業務模組仍在編譯保護內。