Skip to content

プロジェクトCLI(onion new/build/run/test/clean)

onion コマンドは、設定より規約(convention over configuration)に基づく 軽量なプロジェクトワークフローも備えています。これはスクリプトランナーや REPLと共存します。newbuildruntestclean は、それが 最初の引数と完全に一致する場合にのみ予約語として扱われるため、既存の onion [オプション] file.on [引数...]onion repl の挙動はそのまま変わりません。

クイックスタート

onion new hello
cd hello
onion run
onion test

onion new hello は次の構成を作成します。

hello/
├── onion.toml
├── src/
│   └── main.on
└── tests/
    └── main_test.on

生成物は hello/target/ 以下に置かれ、onion clean で削除できます。 Gitリポジトリの初期化は行われず、既存のパスを上書きすることもありません。

生成されるマニフェスト:

[package]
name = "hello"
version = "0.1.0"

生成されるプログラム:

def main(): void {
  println("Hello, hello!")
}

生成されるテスト:

Assert::equals(4, 2 + 2)

コマンド

onion new <name>
onion build [--verbose]
onion run [--verbose] [-- <引数...>]
onion test [--verbose] [--report-xml <パス>]
onion clean
onion doc [-d <ディレクトリ>] [<source.on>...]
onion fmt [--check] [<パス>...]

onion test --report-xml

実行結果を JUnit XML で書き出します。CI がこれを読むことで、「何かが失敗した」だけでなく 「どのテストが失敗したか」をビルドに注釈できます。

onion test --report-xml target/test-reports/junit.xml

サマリ行と終了コードは変わりません。レポートを書けなかった場合はそれ自体をエラーとして扱います。 失敗した実行を黙って成功に変えることも、本物のテスト失敗を I/O エラーで覆い隠すこともありません。

onion fmt

空白を整えます。プロジェクト内で引数なしに実行すると、src/tests/ 配下をすべて 対象にします。

onion fmt

パスにはファイルもディレクトリも渡せます。ディレクトリを渡すと .on ファイルを再帰的に 辿るので、プロジェクトの外でも使えます。

onion fmt scripts/

--check は書き込みを一切せず、変更されるファイルを列挙して、1 つでもあれば 1 で 終了します。CI に組み込むための形です。

onion fmt --check

変更する範囲は意図的に狭くしてあります。 そして境界はすべて実測で決めました。 隣接するものに結合する句読点だけを詰め(f(a , b)f(a, b)"=" .rep(60)"=".rep(60)f(- 1)f(-1))、末尾に改行が無いファイルには改行を足します。 それ以外はタブも含めてバイト単位でそのまま再現します。

インデントの再計算はしません。Onion の継続行は括弧で囲まれていません——= の次の行に 置いた式本体、.filter から始まるメソッドチェーン、行末の + で続く式、record に付く from re"…" 節。これらを「インデントを間違えた行」と区別する手がかりが字句の上に無いため、 波括弧の深さからインデントを計算する実装は正しく書かれたコードをデデントします。 サンプルプログラムで実測したところ、その設計は全体の 36% の行を書き換え、 そのほとんどが誤りでした。

改行の移動もしません。Onion では改行が文の終わりになりうるので、改行を動かすのは 見た目の変更ではなく意味の変更です。

括弧にも、内側・外側どちらからも触れません。理由はサンプルプログラムにあります。 f( a )f(a) に詰めるのは改善に見えますが、new Employee( 1, …)new Employee(10, …) の上にある箇所では、その空白は ID 桁の右揃えです。f (1) を詰めるのも 改善に見えますが、enum の各 case を引数リストが揃うようにパディングしている箇所では、 ある行が空白5個、次の行が空白1個です。つまり「空白1個だけ詰める」という規則でも 打ち間違いと意図を区別できません。182 本のサンプルプログラムに対してこの2つの規則は 何十回も発火し、そのすべてが破壊でした。

結果として小さいツールになりました。サンプル全体にかけて変わるのは 4 行です。これが、 トークン列だけを見るフォーマッタに見える問題の正直な大きさです。残りはパーサが要ります。

ファイルを書き込む前に、整形後のテキストを再度字句解析して、コメントを含むトークン列が 元と一致するかを検査します。一致しない場合はそのファイルを書き換えず、フォーマッタ側の バグとして報告します。

onion doc

API ドキュメントを HTML で生成します。プロジェクト内で引数なしに実行すると、src/ 配下の production ソースを対象に target/doc へ出力します。

onion doc

-d <ディレクトリ> で出力先を変えられます。ソースファイルを明示すれば、プロジェクトの外でも 使えます。

onion doc -d api src/main.on src/util.on

クラス・インターフェース・record・enum・メソッドに付けたドキュメントコメント(/** … */)は シグネチャだけでなく本文も引き継がれます。

buildruntestdocfmtclean はカレントディレクトリから開始して onion.toml が見つかるまで親方向に探索するため、src/tests/ など プロジェクト配下のネストしたディレクトリからでも実行できます。 プロジェクトルートに戻る必要はありません。

終了コードはすべてのプロジェクトコマンドで共通です。

  • 0 — コマンドが正常に完了した。
  • 1 — プロジェクト・マニフェスト・ビルド・テスト・実行時のいずれかの失敗。
  • 2 — コマンドライン引数の指定が不正。

マニフェスト

マニフェストには [package] テーブルが必須で、必須の文字列キーは name[A-Za-z][A-Za-z0-9_-]* に一致すること)と version(有効な SemVer 2.0 の バージョン)の2つです。未知のキー・未知のテーブル・重複キー・ 不正なTOML・無効な名前やバージョンはすべてエラーとなり、TOMLパーサーが 位置情報を提供できる場合は行・列も報告されます。ソースルートやエントリーポイントの 上書き、コンパイラフラグ、スクリプトは受け付けません。

[dependencies]

Maven の座標を "group:artifact" = "version" の形で1行ずつ書きます。

[package]
name = "report"
version = "0.1.0"

[dependencies]
"org.postgresql:postgresql" = "42.7.3"
"com.fasterxml.jackson.core:jackson-databind" = "2.17.0"

Maven の座標はコロンを含むため、キーは必ずクォートします。バージョンは厳密指定で、 範囲指定や latest は使えません。推移的依存も解決され、buildruntest の いずれでも classpath に載ります。コンパイル時に使えるライブラリは実行時にも存在します。

ビルドのフィンガープリントに入るのはマニフェストの本文だけでなく、解決後の集合 (推移的依存を含む)です。推移的依存が動けばキャッシュは無効化されて再コンパイルされ、 古い classpath でコンパイルされたクラスが現行のものとして使われることはありません。

解決できない座標があるとビルドは失敗し、coursier のメッセージがどの座標を見つけられ なかったかを示します。

[[repositories]]

追加の Maven リポジトリを、書いた順に、Maven Central より先に探索します。

[[repositories]]
url = "https://nexus.example.com/repository/maven-public"

Maven Central は置き換えではなく併用されるので、そのまま使えます。受け付けるのは絶対 URL の httphttpsfile のみです。相対パスやスペルミスはここで弾かれ、後段で 「依存が解決できない」という別の原因に見えるエラーにはなりません。

これが単なる repositories = [...] 配列ではなく配列テーブルなのは意図的です。素の キー=値のペアは直前のテーブルヘッダに属するため、[package] の後ろに書くと黙って package.repositories になってしまいます。テーブルヘッダならファイル中のどこに書いても 同じ意味になります。

まだ対応していないこと

オフラインモードはありません。解決はビルドのたびに実行され、取得済みの ものは coursier のキャッシュ(~/.cache/coursier)から読まれるため、ウォームな状態では ネットワークに出ません。ただしネットワークアクセスを禁止しているわけではありません。 Onion が組み込んでいる coursier の Java API はキャッシュポリシーを公開していないため、 --offline フラグは偽装するしかありません。保証でないものを保証のように見せないため、 あえて入れていません。

onion.lock

ビルドは onion.toml の隣に onion.lock を書き、以降のビルドはそこから解決します。 コミットしてください。

[dependencies] が書き留めるのは直接依存のバージョンだけで、それではビルドは再現しません。 推移的依存のバージョンは解決時に決まるため、onion.toml が1バイトも変わっていなくても、 推移的依存が新しい版を publish した瞬間に別の jar でコンパイルされます。この種の失敗は 「片方のマシンでだけ起き、もう片方では再現せず、プロジェクトは何も変わっていない」という 一番厄介な形で出ます。

ロックは推移的依存を含む座標集合全体を記録するので、以降のビルドはそのバージョンを 解決します。さらに各成果物の SHA-256 を記録し、コンパイル前に照合します。既に publish された版のバイト列が変わっていたらビルドを止めます。

error: Resolved dependencies do not match onion.lock:
different bytes for the same file:
  postgresql-42.7.3.jar
    locked 8f3a...
    found  1c90...
A published version's bytes should never change. Check the repository, or delete onion.lock
to accept what it is serving now.

onion.toml のバージョンを変えたりリポジトリを足したりすると、ロックはもう「今きいている 問い」を説明していないので、その問いに対して強制するのではなく破棄して書き直します。 [dependencies] の並べ替えは変更に当たらず、ロックは維持されます。onion clean はロックを 消しません。これは今回のビルドの出力ではなく、次のビルドの入力だからです。

これは offline モードではありません。 coursier の埋め込み API が露出しているのは キャッシュの場所・スレッドプール・ロガーだけで、キャッシュ方針は一切ありません。したがって 「ネットワークに一切触れないビルド」を約束する誠実な方法がありません。ロックが与えるのは 毎回同じ答えであって、問いを無くすことではありません。

ソースレイアウト

プロダクションソースは src/ 以下のすべての .on ファイル、テストは tests/ 以下のすべての _test.on ファイルです。どちらも再帰的に探索され、 プロジェクト相対パスでソートされ、シンボリックリンクはたどりません。 プロダクションソースが空の場合はビルドエラーになりますが、tests/ が 存在しない・空の場合はテスト0件の成功として扱われます。

ビルドキャッシュ

build は一度だけコンパイルし、結果を target/ 以下にキャッシュします。 コンパイラのバージョン・Javaのバージョン・マニフェストの正確なバイト列・ すべてのソースパスとその内容から計算したSHA-256フィンガープリントが 一致する限り、2回目以降の build(および runtest が最初に行う ビルド)はそのキャッシュを再利用します。

$ onion build
Built hello (1 classes)
$ onion build
Built hello (cached)

ソースの追加・削除・リネーム・内容の変更はいずれもキャッシュを無効化し、 フルリビルドを引き起こします。リビルドに失敗しても、直前の成功した target/classes やビルド状態ファイルが壊れることはありません — 新しい出力は一時領域にステージングされ、すべての成果物が正しく 書き込まれた後にのみ昇格されます。

エントリーポイントの規約

コンパイラはすべてのトップレベルソースに対してJVMの main を生成する ため、クラスファイルだけでは本物のエントリーポイントとヘルパー用の ソースを区別できません。そこでプロジェクトのビルドは、構文解析結果を 直接見て次のように判定します。

  1. src/main.on は、トップレベルの文(裸の式や変数宣言を含む)を 1つ以上持つ場合に候補となります。
  2. トップレベルに main という名前の関数を宣言しているプロダクション ソースは、それだけで候補となります。
  3. それ以外のコンパイラ生成の main はすべて無視されます。

run は候補がちょうど1つであることを要求します。候補が0件の場合は src/main.on に実行可能なコードを追加するか、トップレベルの main を 定義するようヒントとともに失敗し、候補が2件以上の場合はすべての候補の ソースと位置を列挙して失敗するため、あいまいさを解消できます。

テスト

test はまずプロダクションソースを一度だけビルドし、その後、発見した 各テストファイルをソート順に1つずつ、順番にコンパイル・実行します。

$ onion test
test tests/main_test.on ... ok

1 tests, 1 passed, 0 failed

各テストの stdout/stderr は、--verbose を指定しない限り失敗した 場合にのみ表示されます。あるテストのコンパイル失敗・アサーション失敗・ 実行時例外・0以外の数値結果は、そのテスト単体の失敗として報告され、 残りのテストはすべて実行され続けます。テストが1つも無いプロジェクトは 0 tests, 0 passed, 0 failed というサマリーとともに成功します。

クリーン

clean はプロジェクトの正規の target/ ディレクトリだけを削除します。

$ onion clean
Cleaned target

実行には有効な、発見済みのプロジェクトであることが前提となり、 target/ が存在するかどうかにかかわらず同じメッセージで成功します。 また target 自体がシンボリックリンクである場合は、それをたどって 削除するのではなく拒否します。

スクリプト・REPLとの互換性

newbuildruntestclean は、最初の引数と完全に一致する 場合にのみ予約されます。それ以外——たとえば build.on という名前の ファイルや -- から始まるオプションなど——は既存のスクリプトランナーに そのまま渡され、onion repl [オプション...] も従来どおり対話シェルを 起動します。

対象外の機能

このバージョンでは、実際の利用実績に基づく判断より先に onion.toml が 巨大なビルド言語になってしまうのを避けるため、次の機能を意図的に 対象外としています: 公開、オフライン解決、複数モジュール・ ワークスペース、ソース・テスト・出力・エントリーポイントの パス変更、ファイル単位の差分コンパイルや並列コンパイル、 プロジェクトのウォッチモード、テスト用アノテーションや新しい テストフレームワーク、パッケージ/アーカイブコマンド、フォーマッター・ リンター連携、ライフサイクルフックやマニフェストスクリプト、 ターミナルの色付け、既存ディレクトリを取り込むための onion init

次のステップ