日本の山岳一覧・百名山 API を開発しましたCLICK !

Rust 製 CLI ツールを npm で公開・配布する

Publishing Rust CLI Tools as npm Packages
  • URLをコピーしました!

mocks というモック用の API を起動できる CLI ツールを Rust で開発して crates.io で公開しています。

Rust で開発してるので、crates.io で公開・配布することはハードルが低くのですが、Rust の環境がないといけないのがネックでした。

Homebrew でもインストールする方法を整備していますが、今回は Node.js 開発者にとって馴染みのある npm エコシステムを通じて公開・配布できるようにしてみました!

目次

Rust 製 CLI ツールを npm publish する

What is mocks? Why publish it on npm?

mocks については、以下の記事で紹介しています。興味がある方はぜひ見ていってください!

なお、npm で公開された @mocks-rs/mockshttps://www.npmjs.com/package/@mocks-rs/mocks で確認できます。

モック用の API を起動するというツールの特性上、React などのフロントエンド開発時に使用するというユースケースも想定しているので npm で公開・配布できることはメリットなのかなと感じています。

Rust 製 CLI ツールを npm で公開・配布している例としては Biome CLI が好例かと思います。

npm で公開・配布する

今回の実装は biomejs/biome を参考にしました。

マルチプラットフォーム対応

各プラットフォーム向けにビルドしたバイナリを個別の npm パッケージとして作成しました。

メインパッケージ

メインパッケージである @mocks-rs/mocks がプラットフォーム検出してバイナリを実行する仕様を採用しました。

Github Actions で自動化

GitHub Actions でビルドから npm publish による公開まで自動化しました。

ソースコード一式を Github で公開しているので、合わせて確認してみてください!

ディレクトリ構成

mocks プロジェクトのディレクトリ構造です。(バージョン 1.0.4 時点)

https://github.com/mocks-rs/mocks/tree/1.0.4

mocks
├── src                        # Rust のソースコード
├── packages
│   └── @mocks-rs
│       ├── mocks              # メインパッケージ
│       ├── mocks-darwin-arm64 # 各プラットフォーム向けパッケージ
│       ├── mocks-darwin-x64
│       ├── mocks-linux-arm64
│       ├── mocks-linux-x64
│       ├── mocks-win32-arm64
│       └── mocks-win32-x64
└── scripts                    # バージョンチェックのスクリプトなどを配置

メインパッケージ:@mocks-rs/mocks

npm で公開するための情報を定義する package.json を作成します。

optionalDependencies を使用することで、ユーザーの環境に応じて必要なプラットフォーム向けパッケージのみがインストールされます。

{
  "name": "@mocks-rs/mocks",
  "version": "1.0.4",
  "description": "Mock REST APIs from JSON with zero coding within seconds.",
  "bin": {
    "mocks": "bin/mocks"
  },
  "optionalDependencies": {
    "@mocks-rs/mocks-linux-x64": "1.0.4",
    "@mocks-rs/mocks-linux-arm64": "1.0.4",
    "@mocks-rs/mocks-darwin-x64": "1.0.4",
    "@mocks-rs/mocks-darwin-arm64": "1.0.4",
    "@mocks-rs/mocks-win32-x64": "1.0.4",
    "@mocks-rs/mocks-win32-arm64": "1.0.4"
  }
}

あとは、インストール手順や使い方を記載した README.md を作成しておきましょう。

プラットフォーム別パッケージ

各プラットフォーム向けパッケージ(例:@mocks-rs/mocks-linux-x64)の package.json は以下のようになっています。

oscpu フィールドにより、npm が適切なプラットフォーム判定を行ってくれます。

{
  "name": "@mocks-rs/mocks-linux-x64",
  "version": "1.0.4",
  "description": "mocks Linux x64 binary",
  "os": ["linux"],
  "cpu": ["x64"],
  "files": ["mocks"]
}

メインパッケージのバイナリ実行の実装

メインパッケージの bin/mocks ファイルは、プラットフォーム検出とバイナリ実行を担当するコードです。

#!/usr/bin/env node

// Node.jsプロセスから必要な情報を取得
// platform: 実行中のOSを示す('win32', 'darwin', 'linux'など)
// arch: CPUアーキテクチャを示す('x64', 'arm64'など)
// env: 環境変数オブジェクト
const { platform, arch, env } = process;

// 子プロセスを同期的に実行するためのspawnSync関数をインポートする
const { spawnSync } = require("child_process");

// プラットフォーム別・アーキテクチャ別のバイナリパスマッピング
const PLATFORMS = {
  // Windows用のバイナリパス(.exe拡張子付き)
  win32: {
    x64: "@mocks-rs/mocks-win32-x64/mocks.exe",      // Windows 64bit Intel/AMD
    arm64: "@mocks-rs/mocks-win32-arm64/mocks.exe",  // Windows 64bit ARM
  },
  // macOS用のバイナリパス
  darwin: {
    x64: "@mocks-rs/mocks-darwin-x64/mocks",         // macOS 64bit Intel
    arm64: "@mocks-rs/mocks-darwin-arm64/mocks",     // macOS 64bit ARM (Apple Silicon)
  },
  // Linux用のバイナリパス
  linux: {
    x64: "@mocks-rs/mocks-linux-x64/mocks",          // Linux 64bit Intel/AMD
    arm64: "@mocks-rs/mocks-linux-arm64/mocks",      // Linux 64bit ARM
  },
};

// 現在のプラットフォームとアーキテクチャに適したバイナリパスを取得する関数
function getBinaryPath() {
  // 環境変数による上書きをサポート
  if (env.MOCKS_BINARY) {
    return env.MOCKS_BINARY;
  }

  // 現在のプラットフォーム(OS)に対応するバイナリマッピングを取得
  const platformBinaries = PLATFORMS[platform];
  if (!platformBinaries) {
    // サポートされていないプラットフォームの場合はnullを返す
    return null;
  }

  return platformBinaries[arch];
}

function main() {
  // 現在の環境に適したバイナリパスを取得
  const binaryPath = getBinaryPath();
  
  // サポートされていないプラットフォーム/アーキテクチャの場合のエラーハンドリング
  if (!binaryPath) {
    console.error(`Unsupported platform: ${platform}-${arch}`);
    process.exit(1); // 異常終了コード1で終了
  }

  // require.resolve()を使用してバイナリの実際のファイルパスを解決
  let resolvedBinaryPath;
  try {
    // NPMパッケージ内のバイナリファイルの絶対パスを取得
    resolvedBinaryPath = require.resolve(binaryPath);
  } catch (error) {
    // バイナリが見つからない・NPMパッケージがインストールされていない場合のエラーハンドリング
    console.error(`Failed to find binary for ${platform}-${arch}`);
    console.error('Try running: npm install --force');
    process.exit(1);
  }

  // 子プロセスとしてバイナリを実行
  const result = spawnSync(resolvedBinaryPath, process.argv.slice(2), {
    stdio: "inherit",    // 標準入出力を親プロセスから継承(ユーザーに直接表示)
    windowsHide: false,  // Windows上でコンソールウィンドウを隠さない
  });

  // 子プロセスの終了コードで親プロセスも終了
  process.exit(result.status || 0);
}

main();

公開ワークフロー

Github Actions で npm publish するワークフローを定義していきます。

Rust セットアップは 再利用可能な Rust セットアップアクションを作成する で紹介しているので、参考にしてみてください!

# RustのCLIツールをnpmパッケージとして公開するワークフロー
name: Publish to npm

on:
  workflow_call:    # 他のワークフローから呼び出し可能
    inputs:
      version:      # 公開するバージョン(例:1.0.0)
        description: 'Version to publish (e.g., 1.0.0)'
        required: true
        type: string
      dry_run:      # 実際に公開せずテスト実行
        description: 'Dry run (do not actually publish)'
        required: false
        default: false
        type: boolean

jobs:
  build-and-publish:
    # 複数プラットフォーム対応する
    strategy:
      fail-fast: false
      matrix:
        include:
          # Linux x64(musl静的リンク)
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            platform: linux-x64
          # Linux ARM64(musl静的リンク)
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            platform: linux-arm64
          # macOS x64
          - target: x86_64-apple-darwin
            os: macos-latest
            platform: darwin-x64
          # macOS ARM64(Apple Silicon)
          - target: aarch64-apple-darwin
            os: macos-latest
            platform: darwin-arm64
          # Windows x64(GNU toolchain)
          - target: x86_64-pc-windows-gnu
            os: ubuntu-latest
            platform: win32-x64
          # Windows ARM64(MSVC toolchain)
          - target: aarch64-pc-windows-msvc
            os: windows-latest
            platform: win32-arm64

    steps:
      # Rust セットアップ
      - name: Setup Rust
        uses: ./.github/actions/setup-rust
        with:
          targets: ${{ matrix.target }}

      # musl(静的リンク)用ツールチェーンのインストール
      - name: Install musl toolchain
        if: contains(matrix.target, 'musl')
        run: |
          sudo apt-get install -y musl-tools
          
      # ARM64クロスコンパイル用のGCCツールチェーン
      - name: Install Windows cross-compilation tools
        if: matrix.target == 'x86_64-pc-windows-gnu'
        run: |
          sudo apt-get install -y gcc-mingw-w64-x86-64

      # セキュリティ監査(Linux x64でのみ実行)
      - name: Security audit
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: cargo audit

      # Rustバイナリのリリースビルド
      - name: Build binary
        timeout-minutes: 30
        run: cargo build --release --target ${{ matrix.target }}
        env:
          # ARM64クロスコンパイル用リンカー設定
          CC_aarch64_unknown_linux_musl: aarch64-linux-gnu-gcc
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-gnu-gcc

      # プラットフォーム固有のnpmパッケージ準備
      - name: Prepare platform package
        shell: bash
        run: |
          mkdir -p dist/${{ matrix.platform }}
          
          # 既存のパッケージテンプレートをコピー
          cp -r packages/@mocks-rs/mocks-${{ matrix.platform }}/* dist/${{ matrix.platform }}/
          
          # ビルドしたバイナリをパッケージにコピー
          if [ "${{ matrix.platform }}" = "win32-x64" ] || [ "${{ matrix.platform }}" = "win32-arm64" ]; then
            cp target/${{ matrix.target }}/release/mocks.exe dist/${{ matrix.platform }}/
          else
            cp target/${{ matrix.target }}/release/mocks dist/${{ matrix.platform }}/
          fi

      # npmレジストリへの公開
      - name: Publish platform package
        if: github.event.inputs.dry_run == 'false' || inputs.dry_run == false
        shell: bash
        run: |
          cd dist/${{ matrix.platform }}
          npm publish --access public  # 公開パッケージとして公開

  # メインパッケージの公開(全プラットフォームビルド完了後)
  publish-main:
    needs: build-and-publish  # プラットフォーム固有パッケージの公開完了を待機
    runs-on: ubuntu-latest
    
    steps:
      # メインパッケージ(プラットフォーム固有パッケージの依存関係管理)の公開
      - name: Publish main package
        if: github.event.inputs.dry_run == 'false' || inputs.dry_run == false
        shell: bash
        run: |
          cd packages/@mocks-rs/mocks
          npm publish --access public

mocks のワークフローでは release.yml でテストやバージョンチェックを行った後に、バージョン変更があれば publish-to-npm.yml を呼び出しています。

release.yml

Node.js 環境でのインストール

npm で公開・配布したことで、以下の方法でインストールできるようになりました。

npm install -g @mocks-rs/mocks

グローバルにインストールしたくない場合は、package.jsonmocks コマンドを定義して実行できます。

npm install @mocks-rs/mocks
{
  "scripts": {
    "mocks:init": "mocks init storage.json",
    "mocks:run": "mocks run storage.json"
  }
}

また、npx 経由でも実行できるようになります。

npx @mocks-rs/mocks init storage.json
npx @mocks-rs/mocks run storage.json

まとめ

Rust 製 CLI ツールを npm で公開・配布することで、Node.js 環境での利用がしやすくなります。

ユーザーの拡大という意味では、npm エコシステムの領域に進出するメリットは非常に大きいと思います!

参考

Publishing Rust CLI Tools as npm Packages

この記事が気に入ったら
フォローしてね!

  • URLをコピーしました!

コメント

コメントする

目次