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 管線設計與階段拆分草案。

D1–D11 全定案 夜間實測已完成一輪 範圍:Linux 容器版 後續:FR-064 防竄改 · FR-065 Installer
§1

需求背景:為什麼要編譯

產品進入落地版(on-premise)交付階段——客戶主機上不可再出現可讀原始碼。這是一條三階段戰線,本案是第一棒:

FR-063 Nuitka 編譯(本案)

把 BE 整包編成機器碼 binary,客戶主機上沒有 .py 可讀。範圍限定 Linux 容器版

FR-064 防竄改 → FR-065 Installer

下兩棒:防竄改機制、安裝器。Windows 支援也在下一階段——傾向用 Docker Desktop / WSL2 跑同一個 Linux image,不另編 Windows binary。

1.1 方案選型脈絡(前期討論已定)

比較過四條路:.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 交付,雙保險
§2

2026-08-13 夜間實測

環境: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(見下節)。

§3

決策定案 D1–D11

以下十一項均已定案,不是 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 碼。兩條修法候選:

  1. 從 sdist 重編、不開 Limited API(預設即不開,可能已直接相容,15 分鐘可驗)
  2. CFLAGS 帶 -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.jsoncmmc_l1_aos.json)。

順帶清理四件事:

  • catalog seed 搬出 docs/(release 產物不該讀 docs/)
  • 下載範本移出 static/(見盤點 #8 雙重身分衝突)
  • 清掉三處指向不存在目錄的 Blueprint static 宣告(盤點 #11)
  • api/__init__.py 死碼 load_routes()

D4 證據分類 docker CLI 依賴:落地版功能開關降級停用

classifier_container_runner.pycmmc-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.pymain_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

  • 保留單一 Config(以現 BaseConfig 為底),六套子 class(DevelopmentPremise / StagingPremise / StagingAwsBillowS / ProductionAwsBillowS / StagingAwsNics / ProductionAwsNics)全刪
  • 差異值 env 化:WEBSITE_URL→刪(見死項)、CORS_ALLOWED_ORIGINS→env 逗號分隔(預設 *)、DB_PORT→env(預設 5432)、LOGGING_LOCATION→env LOG_DIR(預設 ./log)、SQLALCHEMY_TRACK_MODIFICATIONS→死項刪、JWT 效期→第 3 刀進 DB
  • config_util.py 的 ENV→class 對照表退役;ENV 變數降級為純顯示標籤(log 印一行,不驅動行為),不破壞現有部署腳本
  • 檔頭死值清理:AWD_DEFAULT_URLBaseConfig.DB_NAME="audit_manager"
  • 遷移配套:產「六 class 差異→env 對照表」,三環境 .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 零改動)
  • 開機必填檢查,缺項一次列全(「缺少:DB_HOST, JWT_SECRET_KEY(或 JWT_SECRET)」),取代 import 期 json.loads 直接 traceback
  • import 期 json.loads 移進 loader,由統一入口(D9 / .1c)最早階段呼叫
  • 附帶效益:新格式命名與 jedi-* 套件期待的 DB_USERNAME / DB_PASSWORD 命名分歧一併對齊收斂(盤查發現 jedi 讀的變數名與主專案 .env 是兩套)

第 3 刀:營運參數遷移 DB system_config(內建預設+DB 覆寫)

  • 第一批(刻意保守):LOGIN_MAX_LOCK_COUNT / LOGIN_USER_LOCK_TIME(客戶資安政策各異)、CHANGE_PASSWORD_THRESHOLD_DAYSLOGIN_INACTIVITY_THRESHOLD_DAYSJWT_ACCESS/REFRESH_TOKEN_EXPIRESMFA_REQUIRED
  • 機制:沿用既有 system_config root reader pattern(infra/system_config/system_config_root_reader.py 現成);編譯內建預設值,DB 有值覆寫,讀不到照預設跑(無啟動依賴)
  • 本期不做 UI(root 管理介面留後續);migration 一支 seed(ON CONFLICT DO NOTHING),只套 DEV
  • 風險註記:JWT 效期改 DB 後的快取生效時機,與既有 system_config reader 快取行為一致,不另發明

死項清理(2026-08-14 盤查定案,附判定依據)

  • config.py 確定死掉 14 項直接刪:TOKEN_TTLUSER_LOCK_COUNTUSER_PASSWORD_LIMIT_TIME(舊代,新代 LOGIN_* / CHANGE_PASSWORD_* 活著)、PROJECT_EXCEL_UPLOAD_DIR / PROJECT_EXCEL_SAMPLE_DIR / PROJECT_EXCEL_SAMPLE_FILENAMEUSER_EXCEL_SAMPLE_DIR / USER_EXCEL_SAMPLE_FILENAMEANSWER_SAMPLE_DIR / ANSWER_SAMPLE_FILENAMELOGGING_FORMATFEEDBACK_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.testNOTION_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_DIRANSWER_UPLOAD_DIRREDIS_URL(flask_redis 讀)
  • jedi-* 套件不讀 Flask 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 檔)。

Smoke 驗收清單(.3 子需求驗收條件,FR-064/065 復用)

「編得出來」≠「功能活著」——本案盤點出的地雷多為靜默失效型(不炸但功能悄悄壞),驗收必須逐雷探針,缺一不可:

探針 探的雷
登入 基本 API/DI wire 全量
任一分頁列表 blueprint 載入完整
i18n 中文回應 .mo 檔有打包(盤點#4)
SSP docx 匯出 範本檔有打包(盤點#4)
摘要報告 PDF 開檔驗中文 WeasyPrint 系統庫+字型(盤點#5/#13)
問卷上傳 static/上傳目錄可寫(盤點#8/#9)
socketio 通知 eventlet 編譯後可用(D1/D9)
§4

現況盤點:三路掃描結果

以下是「動態 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_TENANTRUN_ENVTZ
DB DB_USERNAMEDB_PASSWORDDB_SCHEMADEFAULT_SCHEMA
Redis REDIS_DBREDIS_SSL
i18n DEFAULT_LOCALE
外部整合 GITLAB_*GITHUB_PRIVATE_TOKEN
Auth OTP_COOLDOWN_SECONDSAUTH_CHANGE_SECRET_COOLING_HOURS

(主專案自身的 60+ 個以現行各 Config class 為準,.1 子需求收斂時產完整對照表。config 死項盤查結論見 D11。)

✅ 掃過確認安全

  • getattr / registry 字串解類:零風險
  • inspect.getsource:零命中
  • 匯出多走記憶體串流(BytesIO),不落地也不讀源
  • __import__("uuid") 常量參數,Nuitka 可靜態解析
  • scheduler_report_routelistdir 掃的是 runtime 目錄,安全
§5

Build 管線設計

%%{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 巡檢"]
圖 1 — FR-063 build 管線六步

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
圖 2 — 開發模式 vs release 模式雙軌

雙軌的原則:同一份程式碼,兩個模式行為等價——開發模式維持動態掃描(加檔即生效,開發體驗不變),release 模式吃 build 期凍結的靜態產物。gen script 是兩軌之間唯一的橋,掃描結果 dump 下來就是靜態清單,不存在第二套手維護名單。

§6

階段拆分草案

給後續拆卡參考,粗粒度四個子需求:

.1 清障與程式碼改造

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 兩模式各自可起。

.2 Build 管線

wheel rebuild script(D2 含快取)、Nuitka build script、build 機環境建置(D5)。

驗收草案:build 機上一鍵跑完 Step 1–4 產出可啟動的統一入口 dist;私房 wheel 快取命中時跳過重編;產物 import 期不炸、DI wire 全量。

.3 Production Dockerfile 與 image

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 執行。

.4 socketio 模式驗證與裁剪

單 binary 已含 socketio 模式(D9),本階段重點不是編譯而是:eventlet monkey-patch × Nuitka 相容性實測裁剪載入實作(不載 blueprint、不起 scheduler,照 D9 載入表);卡關則啟動 D1 保底(換 async 承載)。

驗收草案RUN_MODE=socketio 進程可建立連線並收發事件、不載 REST blueprint、scheduler 不雙跑;或保底方案落地且業務模組仍在編譯保護內。

FR-063 · Nuitka 落地版打包 — 需求討論稿 · 2026-08-14 · 三階段戰線第一棒(FR-063 編譯 → FR-064 防竄改 → FR-065 Installer)· 實測環境:STG 機 /opt/guidant_ai(Nuitka 4.1.3 / Python 3.11,4 core / 32GB)· Notion 母案 CM-1187