2025/11/05
はじめに
最近のプログラミング言語では、プログラムの依存管理はパッケージマネージャに任せることが多く、それはRustも例に漏れずcargoを使って依存パッケージとビルド周りを一貫して行うことができる。さらにそれらのパッケージマネージャは、その言語で開発されたCLIツールなどのインストールもになってくれて、シンプルな配布方法まで提供してくれている。
それらの中には、プロジェクト固有のCLIツールをプロジェクト単位で管理する方法を提供してくれるものもあり、npmにおける npm-scriptsやnpxによる実行、Go (>=1.24)におけるtoolディレクティブなどが存在する。
一方でcargoにはその機能が存在せず、 cargo installや cargo binstallによって実行環境にインストールする必要が出てきてしまい、開発者ごとのバージョン一致の難しさやセットアップの煩雑さが生じてしまう。
そこで、xtaskパターンというのが存在することを知ったので、まとめる。 (rust-analyzerを開発されたmatkladさんが提唱しているらしい)
前提
今回はサンプルとして、sqlxコマンドをプロジェクト内で管理する手法を記載する。
実行イメージはこんな具合
$ cargo xtask sqlx help
$ cargo sqlx help
ディレクトリ構造
Rust>~/Github/rust-examples/xtask_pattern $ tree . -a
.
├── .cargo
│ └── config.toml # cargoコマンドのエイリアス設定をする
├── Cargo.toml # project設定、workspace設定を施す
├── README.md
├── src
│ └── main.rs # 好きなコードを配置
└── xtask # 名前はなんでも良いが、エイリアスと一致させる
├── Cargo.toml # 依存CLIツールを記載する場所
└── src
└── main.rs # 依存CLI実行のエントリポイント
5 directories, 6 files
設定
workspace設定
./xtaskを ./Cargo.tomlのメンバーに入れる
[package]
name = "xtask_pattern"
version = "0.1.0"
edition = "2021"
[workspace]
members = ["xtask"]
[dependencies]
alias設定
プロジェクト内で cargo xtask [args]、 cargo sqlx [args]と実行できるようにこのように設定する
[alias]
xtask = "run --package xtask -- "
sqlx = "run --package xtask -- sqlx" # Optional
(これをしないと、都度 cargo run --package xtask --から始める必要がある)
xtask設定(依存)
xtaskに組み込むCLIツールとして sqlx-cliを含ませることはもちろんのこと、今回は anyhow, clap, tokioも含ませた。このあたりはお好みで調整する。
[package]
name = "xtask"
version = "0.1.0"
edition = "2024" # sqlx-cliの都合上このようにした。project本体と合わせても良い。
[dependencies]
anyhow = { version = "1.0.100", features = ["backtrace"] }
clap = { version = "4.5.49", features = ["derive"] }
sqlx-cli = { version = "*", default-features = false, features = ["postgres"] }
tokio = "1.48.0"
xtask設定(実装)
CLIツールを入れることが目的で実装をする必要があるのはちょっと面倒ではあるが、以下のようにした。
clapを使ってargsパースを簡素にしつつも、sqlx-cliが公開しているOptsを流用する仕組みを考えた。単に Vec<String>で受け取っても問題ないはず。直依存を減らしたければそうしても良い。
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Args {
#[clap(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Sqlx(sqlx_cli::Opt),
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
match args.command {
Command::Sqlx(opt) => sqlx_cli::run(opt).await?,
}
Ok(())
}