iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 22

Day 21:跨平台預編譯與 CI +實戰案例

  • 分享至 

  • xImage
  •  

這會是 Mongory 能夠被大多數讀者輕鬆用起來的轉折點:預編譯。
筆者在公司的 pod 實際安裝時,從「本機沒 toolchain 裝不起來」的挫折出發,走過 rake-compiler-dock 的嘗試與受挫,最後落在 GitHub Actions 的原生矩陣編譯方案,才把交付真正穩下來。這篇把整段歷程與最終做法完整交代,讓讀者只需安裝即可使用,無需額外設定


目標與挑戰(真實場景)

  • 目標

    • 跨平台預編譯:提供可直接取用的建置產物(Ruby 平台 gem,內含對應平台的 .bundle/.so
    • 可觀測、可維護:CI log 透明、建置步驟簡單、錯誤能快速定位
    • 不綁工具箱:策略不使用 rake-compiler-dock,採用 matrix runner 進行原生編譯,降低黑箱感
  • 挑戰

    • Linux 的 GLIBC 相容性與多發行版差異
    • macOS Intel/Apple Silicon 的雙架構差異與部署目標設定
    • Ruby 擴充的編譯流程需與 C Core 同步,並在失敗時能回退到原始碼編譯

在公司 pod 安裝時實際遇到的第一個阻礙就是「開發機沒有完整 toolchain,安裝時直接卡住」,筆者意識到不能要求所有人都裝編譯環境,因此預編譯勢在必行


歷程:從 rake-compiler-dock 到原生矩陣編譯

一開始筆者嘗試 rake-compiler-dock 的跨平台編譯,希望一次性打包多平台產物;但在本機實驗時頻繁遇到不明失敗與版本對不上。於是將重心轉到 GitHub Actions,讓 CI 代替本機做原生編譯:

  • 矩陣原生編譯(matrix runner):用官方 macOS/Ubuntu/Windows runner 平行建置,避免跨編譯的不可預期行為
  • Artifacts 清楚可見:剛開始筆者對 artifacts 很不熟,借助 tree 輸出把目錄結構看清楚,確保每個 Ruby 版本的產物都正確歸位
  • 版本對應坑點(macOS):macOS 的 Ruby 平台字串與實際架構對應有坑,筆者踩到「universal-darwin24 對應 x86_64」的錯配,寫在發佈說明提醒使用者注意;同時用雙 runner(x86_64/arm64)確保覆蓋

最終決策清晰:放棄 dock 黑箱,全面改走「總控 workflow + 子流程」的原生矩陣編譯,讓每個平台的 log 與產物都可觀測、可驗證


平台矩陣與注意事項(最終方案)

  • 作業系統與平台三元組

    • macOS:x86_64-darwin(runner macos-13)、arm64-darwin(runner macos-14
    • Linux:x86_64-linuxaarch64-linuxx86_64-linux-muslaarch64-linux-musl
    • Windows:x64-mingw32x64-mingw-ucrt
  • Ruby 版本

    • Linux/macOS:2.63.4
    • Windows:3.03.4
  • C/C++ 工具鏈

    • macOS:系統 Clang 即可
    • Ubuntu:build-essentialcmake(若用 mongory-core/build.sh 或 CMake 流程)
  • GLIBC 與相容性:同時產出 *-linux(glibc)與 *-linux-musl(musl)平台 gem,以覆蓋主流發行版。若個別環境仍不相容,RubyGems 會自動走原始碼編譯路徑,讀者無需手動介入


GitHub Actions:現行流程(總控+可重用子流程)

採用一個總控 workflow build.yml 觸發三個可重用子流程(Linux/macOS/Windows),各自產出平台 gem,最後彙整並推送;讀者只需 gem install mongory 或在 Gemfile 指定版本,即可自動取得對應平台的預編譯套件

# .github/workflows/build.yml(節錄)
name: Release Mongory
on:
  push:
    tags:
      - "v*"
jobs:
  linux-runners:
    strategy:
      matrix:
        platform:
          [x86_64-linux-musl, aarch64-linux-musl, x86_64-linux, aarch64-linux]
    uses: ./.github/workflows/_linux_cross_compile.yml
    with:
      platform: ${{ matrix.platform }}

  macos-runners:
    strategy:
      matrix:
        include:
          - platform: x86_64-darwin
            runner: macos-13
          - platform: arm64-darwin
            runner: macos-14
    uses: ./.github/workflows/_macos_cross_compile.yml
    with:
      platform: ${{ matrix.platform }}
      runner: ${{ matrix.runner }}

  windows-runners:
    strategy:
      matrix:
        platform: [x64-mingw32, x64-mingw-ucrt]
    uses: ./.github/workflows/_windows_cross_compile.yml
    with:
      platform: ${{ matrix.platform }}

  release-gem:
    needs: [linux-runners, macos-runners, windows-runners]
    runs-on: ubuntu-latest
    steps:
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.2"
      - uses: rubygems/release-gem@v1 # 先釋出 source gem
      - uses: actions/download-artifact@v4
        with:
          pattern: gem-*
          path: ./artifacts
          merge-multiple: true
      - name: Push precompiled platform gems
        run: |
          set -euo pipefail
          shopt -s nullglob globstar
          while IFS= read -r -d '' g; do
            gem push "$g"
          done < <(find ./artifacts -type f -name '*.gem' -print0 | sort -z)

Linux 子流程(節錄):

# .github/workflows/_linux_cross_compile.yml(節錄)
env:
  PLATFORM: ${{ inputs.platform }}
  RUBIES: "2.6 2.7 3.0 3.1 3.2 3.3 3.4"
jobs:
  c-extension-compile:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - ruby: "2.6"; bundler: "2.1.4"
          - ruby: "2.7"; bundler: "2.1.4"
          - ruby: "3.0"; bundler: "2.5.6"
          - ruby: "3.1"; bundler: "2.5.6"
          - ruby: "3.2"; bundler: "2.5.6"
          - ruby: "3.3"; bundler: "2.5.6"
          - ruby: "3.4"; bundler: "2.5.6"
    steps:
      - uses: ruby/setup-ruby@v1
        with: { ruby-version: ${{ matrix.ruby }}, bundler: ${{ matrix.bundler }}, bundler-cache: true }
      - run: bundle exec rake clean && bundle exec rake compile
      - uses: actions/upload-artifact@v4
        with:
          name: compiled-${{ inputs.platform }}-${{ matrix.ruby }}
          path: lib/core/${{ matrix.ruby }}/mongory_ext.so

  build-gem:
    needs: c-extension-compile
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with: { pattern: compiled-${{ inputs.platform }}-*, path: ./artifacts }
      - name: Move files to lib/core
        run: |
          IFS=' ' read -r -a ruby_versions <<< "$RUBIES"
          for ruby in "${ruby_versions[@]}"; do
            mkdir -p lib/core/$ruby
            mv -f ./artifacts/compiled-$PLATFORM-$ruby/mongory_ext.so ./lib/core/$ruby/
          done
      - name: Native build Gem
        env: { RCD_PLATFORM: ${{ inputs.platform }}, NATIVE_BUILD: 1 }
        run: gem build mongory.gemspec
      - uses: actions/upload-artifact@v4
        with: { name: gem-${{ inputs.platform }}, path: "*.gem" }

macOS/Windows 子流程與 Linux 類似:

  • macOS 產物為 mongory_ext.bundle,runner 分別是 macos-13(x86_64)與 macos-14(arm64)
  • Windows 使用 windows-latest,目前產物檔名為 mongory_ext.so(依現況保留)

另外有一個測試用流程(維運用):

# .github/workflows/build-test.yml(節錄)
on:
  push:
    branches: ["feature/cross-platform-build"]
jobs:
  linux-runners: { uses: ./.github/workflows/_linux_cross_compile.yml, with: { platform: ${{ matrix.platform }} } }
  macos-runners: { uses: ./.github/workflows/_macos_cross_compile.yml, with: { platform: ${{ matrix.platform }}, runner: ${{ matrix.runner }} } }
  windows-runners: { uses: ./.github/workflows/_windows_cross_compile.yml, with: { platform: ${{ matrix.platform }} } }

重點說明:

  • 採「總控+子流程」結構管理多平台,便於橫向擴張與獨立排錯
  • 子流程統一將二進位放在 lib/core/<ruby>/mongory_ext.(bundle|so),再以 RCD_PLATFORMNATIVE_BUILD=1 打包平台 gem
  • 釋出順序為:先釋出 source gem,再彙整各平台 artifacts 推送平台 gem

Ruby 擴充與 C Core 的協作(讀者無需操作)

  • mongory-rb 會在 CI 自動完成 C 擴充與 C Core 的同步建置
  • 產物佈局與打包細節已在 CI 固化(平台 gem 以 lib/core/<ruby>/mongory_ext.(bundle|so) 形式封裝)
  • 讀者安裝時自動選取平台 gem;若無對應平台,RubyGems 會自動嘗試原始碼編譯

安裝行為(自動選擇與回退)

安裝時會先嘗試取得對應平台的預編譯 gem;若目標環境較舊或不相容,RubyGems 會自動改走原始碼編譯路徑。整個過程對讀者透明,無需額外設定或環境變數


維運端的回退策略(讀者無需設定)

  • 優先順序:平台 gem(glibc/musl/darwin/mingw)優先,若不相容由安裝流程自動回退原始碼編譯
  • 超時與失敗:平台產物下載或載入失敗時,自動回退原始碼編譯,不阻斷安裝
  • 可觀測性:CI 維護多平台子流程與 artifacts 驗證,持續確保各平台可用

小結

筆者選擇了「原生矩陣編譯+保留回退」的務實路線:一方面保有可觀測的 CI、降低黑箱,另一方面讓讀者在任何環境都能安裝成功。這正符合 Mongory 的精神:把複雜度藏在可控的邊界,對外呈現穩定、可預期的使用體驗


下一篇預告

Day 22:Mongory Bridge 的下一站:為什麼選 Go

專案首頁(Ruby 版)


上一篇
Day 20:關鍵優化 2:Shallow wrap
下一篇
Day 22:Mongory Bridge 的下一站:為什麼選 Go
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言