プロジェクトCLI(onion new/build/run/test/clean)¶
onion コマンドは、設定より規約(convention over configuration)に基づく
軽量なプロジェクトワークフローも備えています。これはスクリプトランナーや
REPLと共存します。new・build・run・test・clean は、それが
最初の引数と完全に一致する場合にのみ予約語として扱われるため、既存の
onion [オプション] file.on [引数...] や onion repl の挙動はそのまま変わりません。
クイックスタート¶
onion new hello は次の構成を作成します。
生成物は hello/target/ 以下に置かれ、onion clean で削除できます。
Gitリポジトリの初期化は行われず、既存のパスを上書きすることもありません。
生成されるマニフェスト:
生成されるプログラム:
生成されるテスト:
コマンド¶
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 がこれを読むことで、「何かが失敗した」だけでなく 「どのテストが失敗したか」をビルドに注釈できます。
サマリ行と終了コードは変わりません。レポートを書けなかった場合はそれ自体をエラーとして扱います。 失敗した実行を黙って成功に変えることも、本物のテスト失敗を I/O エラーで覆い隠すこともありません。
onion fmt¶
空白を整えます。プロジェクト内で引数なしに実行すると、src/ と tests/ 配下をすべて
対象にします。
パスにはファイルもディレクトリも渡せます。ディレクトリを渡すと .on ファイルを再帰的に
辿るので、プロジェクトの外でも使えます。
--check は書き込みを一切せず、変更されるファイルを列挙して、1 つでもあれば 1 で
終了します。CI に組み込むための形です。
変更する範囲は意図的に狭くしてあります。 そして境界はすべて実測で決めました。
隣接するものに結合する句読点だけを詰め(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 へ出力します。
-d <ディレクトリ> で出力先を変えられます。ソースファイルを明示すれば、プロジェクトの外でも
使えます。
クラス・インターフェース・record・enum・メソッドに付けたドキュメントコメント(/** … */)は
シグネチャだけでなく本文も引き継がれます。
build・run・test・doc・fmt・clean はカレントディレクトリから開始して
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 は使えません。Maven の範囲指定([1.0,2.0)・(,1.0] など、[・]・
(・)・, を含むもの)、+ で終わる動的リビジョン(1.+)、動くエイリアス
(LATEST・RELEASE・latest.release や latest.integration などの latest.*)は、
マニフェストが依存の名前と行を示してエラーにします。SemVer のビルドメタデータ中の +
(1.0.0+build.5)と -SNAPSHOT は受け付けます。スクリプトの //> using dep も
同じ規則です。推移的依存も解決され、build・run・test の
いずれでも classpath に載ります。コンパイル時に使えるライブラリは実行時にも存在します。
ビルドのフィンガープリントに入るのはマニフェストの本文だけでなく、解決後の集合 (推移的依存を含む)です。推移的依存が動けばキャッシュは無効化されて再コンパイルされ、 古い classpath でコンパイルされたクラスが現行のものとして使われることはありません。
解決できない座標があるとビルドは失敗し、coursier のメッセージがどの座標を見つけられ なかったかを示します。
単一ファイルのスクリプトでは、同じことを先頭の //> using dep "group:artifact:version"
行で宣言します(スクリプトランナー を参照)。スクリプトには
onion.lock がないため推移的依存のバージョンは固定されません。それが問題になるなら
プロジェクトにしてください。
[[repositories]]¶
追加の Maven リポジトリを、書いた順に、Maven Central より先に探索します。
Maven Central は置き換えではなく併用されるので、そのまま使えます。受け付けるのは絶対
URL の http・https・file のみです。相対パスやスペルミスはここで弾かれ、後段で
「依存が解決できない」という別の原因に見えるエラーにはなりません。
これが単なる repositories = [...] 配列ではなく配列テーブルなのは意図的です。素の
キー=値のペアは直前のテーブルヘッダに属するため、[package] の後ろに書くと黙って
package.repositories になってしまいます。テーブルヘッダならファイル中のどこに書いても
同じ意味になります。
まだ対応していないこと¶
オフラインモードはありません。解決は、現在の onion.lock に対して記録したクラスパスを
ビルドが再利用できないとき(後述)に実行され、取得済みのものは coursier のキャッシュ(~/.cache/coursier)から読まれるため、ウォームな状態では
ネットワークに出ません。ただしネットワークアクセスを禁止しているわけではありません。
Onion が組み込んでいる coursier の Java API はキャッシュポリシーを公開していないため、
--offline フラグは偽装するしかありません。保証でないものを保証のように見せないため、
あえて入れていません。
onion.lock¶
ビルドは onion.toml の隣に onion.lock を書き、以降のビルドはそこから解決します。
コミットしてください。
[dependencies] が書き留めるのは直接依存のバージョンだけで、それではビルドは再現しません。
推移的依存のバージョンは解決時に決まるため、onion.toml が1バイトも変わっていなくても、
推移的依存が新しい版を publish した瞬間に別の jar でコンパイルされます。この種の失敗は
「片方のマシンでだけ起き、もう片方では再現せず、プロジェクトは何も変わっていない」という
一番厄介な形で出ます。
ロックは推移的依存を含む座標集合全体を記録し、以降のビルドは [dependencies] を
そのバージョンすべてを強制(force)したうえで解決します。バージョンを導き直すことは
しません。強制はマニフェスト自身の依存グラフにかけるもので、座標を平らなルートの並びとして
解決するのではありません。平らにすると、推移的依存がそのバージョンを管理している POM
(<dependencyManagement> や親 BOM)の下から外れ、別のバージョンが選ばれることがあるからです。
さらに各成果物の 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.
成果物の集合が違うのはバイト列の変化ではなく解決結果の違いなので、エラーもそう述べ、 リポジトリのせいにする代わりに動いたバージョンを列挙します。
error: Resolved dependencies do not match onion.lock:
not in the lock: lib-2.0.0.jar
missing: lib-1.0.0.jar
resolved versions differ from the lock:
com.example:lib locked 1.0.0 resolved 2.0.0
The artifacts resolved differently from the ones the lock records; no file's bytes changed.
Delete onion.lock to resolve again and record the new answer.
ロックを解決して照合し終えたビルドは、得られたクラスパスをロックの内容をキーにして
target/.onion/dependency-classpath.json に記録します。同じロックでの次の
build/run/test は、記録された jar がすべて存在し、その SHA-256 がロックと一致する限り
(サイズか更新時刻が変わった jar は再ハッシュします)、coursier に一切問い合わせずにその
クラスパスを使います。それ以外——ロックが違う、jar が無い・変わった、記録が無い——は解決に
戻ります。記録はこのマシンのキャッシュへの絶対パスを持つので target/ の下に置き、
コミットしません。onion clean で消えます。言語サーバーも同じ記録を読み取り専用で使います。
onion.toml のバージョンを変えたりリポジトリを足したりすると、ロックはもう「今きいている
問い」を説明していないので、その問いに対して強制するのではなく破棄して書き直します。
[dependencies] の並べ替えは変更に当たらず、ロックは維持されます。onion clean はロックを
消しません。これは今回のビルドの出力ではなく、次のビルドの入力だからです。
これは offline モードではありません。 coursier の埋め込み API が露出しているのは キャッシュの場所・スレッドプール・ロガーだけで、キャッシュ方針は一切ありません。したがって 「ネットワークに一切触れないビルド」を約束する誠実な方法がありません。ロックが与えるのは 毎回同じ答えであって、問いを無くすことではありません。記録されたクラスパスは、同じ問いを たいてい二度きかずに済む、というだけです。
ソースレイアウト¶
プロダクションソースは src/ 以下のすべての .on ファイル、テストは
tests/ 以下のすべての _test.on ファイルです。どちらも再帰的に探索され、
プロジェクト相対パスでソートされ、シンボリックリンクはたどりません。
プロダクションソースが空の場合はビルドエラーになりますが、tests/ が
存在しない・空の場合はテスト0件の成功として扱われます。
ビルドキャッシュ¶
build は一度だけコンパイルし、結果を target/ 以下にキャッシュします。
コンパイラの識別子・Javaのバージョン・マニフェストの正確なバイト列・
すべてのソースパスとその内容から計算したSHA-256フィンガープリントが
一致する限り、2回目以降の build(および run・test が最初に行う
ビルド)はそのキャッシュを再利用します。
ソースの追加・削除・リネーム・内容の変更はいずれもキャッシュを無効化し、 フルリビルドを引き起こします。
コンパイラの識別子は、バージョンに加えて、コンパイラを読み込んだ jar(またはクラス
ディレクトリ)のダイジェストを含みます。そのため、アップグレードやローカルで作り直した
onion.jar、エフェクトテーブルのようなコンパイラのリソースの変更など、バージョンが
同じでもコンパイラが変われば、次の build でプロジェクトが再ビルドされます。新しい
診断やエフェクト検査が onion clean なしで走ります。jar のハッシュ計算はビルドの
たびには行いません。ダイジェストはユーザーキャッシュディレクトリ
(-Donion.cache.dir・$ONION_CACHE_DIR・プラットフォームの既定)に、jar の
パス・サイズ・更新時刻をキーとしてキャッシュされます。
リビルドに失敗しても、直前の成功した
target/classes やビルド状態ファイルが壊れることはありません —
新しい出力は一時領域にステージングされ、すべての成果物が正しく
書き込まれた後にのみ昇格されます。
エントリーポイントの規約¶
コンパイラはすべてのトップレベルソースに対してJVMの main を生成する
ため、クラスファイルだけでは本物のエントリーポイントとヘルパー用の
ソースを区別できません。そこでプロジェクトのビルドは、構文解析結果を
直接見て次のように判定します。
src/main.onは、トップレベルの文(裸の式や変数宣言を含む)を 1つ以上持つ場合に候補となります。- トップレベルに
mainという名前の関数を宣言しているプロダクション ソースは、それだけで候補となります。 - トップレベルに
toolを宣言し、mainもトップレベルの文も持たない プロダクションソースは候補となります。これはちょうどコンパイラが そのソースにツールCLI用のmain(スクリプトが得るものと同じ)を 合成する条件なので、runはそれを通じて実行され、--help・--contract・--planがすべて使えます。 - それ以外のコンパイラ生成の
mainはすべて無視されます。
run は候補がちょうど1つであることを要求します。候補が0件の場合は
src/main.on に実行可能なコードを追加するか、トップレベルの main を
定義するか、トップレベルの tool を宣言するようヒントとともに失敗し、
候補が2件以上の場合はすべての候補のソースと位置を列挙して失敗するため、
あいまいさを解消できます。
-- より後ろの引数はプログラムに渡されるので、ツールのプロジェクトは
スクリプトの場合と同じように操作できます。
$ onion run -- --help
usage: meeting-summary <notes> <out>
...
$ onion run -- samples/notes.md out.json --plan
plan: `summarize` would
read derived from notes = samples/notes.md
write derived from out = out.json
...
使い方の行には、スクリプトならファイル名が入るところにプロジェクトの
パッケージ名([package].name)が入ります。
テスト¶
test はまずプロダクションソースを一度だけビルドし、その後、発見した
各テストファイルをソート順に1つずつ、順番にコンパイル・実行します。
各テストの stdout/stderr は、--verbose を指定しない限り失敗した
場合にのみ表示されます。あるテストのコンパイル失敗・アサーション失敗・
実行時例外・0以外の数値結果は、そのテスト単体の失敗として報告され、
残りのテストはすべて実行され続けます。テストが1つも無いプロジェクトは
0 tests, 0 passed, 0 failed というサマリーとともに成功します。
アサーション失敗や実行時例外は、それを起こした行 —— Assert:: の呼び出しなら、
テスト内であれ、テストから到達したプロジェクトのソース内であれ、その呼び出しの行 ——
で報告され、続けてその行が表示されます:
$ onion test
test tests/summary_test.on ... FAILED
1 tests, 0 passed, 1 failed
error: tests/summary_test.on:4: AssertionError: Expected true but was false
4 | Assert::isTrue(words.size == 4)
onion script.on でも、失敗したアサーションは同じように報告されます。
クリーン¶
clean はプロジェクトの正規の target/ ディレクトリだけを削除します。
実行には有効な、発見済みのプロジェクトであることが前提となり、
target/ が存在するかどうかにかかわらず同じメッセージで成功します。
また target 自体がシンボリックリンクである場合は、それをたどって
削除するのではなく拒否します。
スクリプト・REPLとの互換性¶
new・build・run・test・clean は、最初の引数と完全に一致する
場合にのみ予約されます。それ以外——たとえば build.on という名前の
ファイルや -- から始まるオプションなど——は既存のスクリプトランナーに
そのまま渡され、onion repl [オプション...] も従来どおり対話シェルを
起動します。
対象外の機能¶
このバージョンでは、実際の利用実績に基づく判断より先に onion.toml が
巨大なビルド言語になってしまうのを避けるため、次の機能を意図的に
対象外としています: 公開、オフライン解決、複数モジュール・
ワークスペース、ソース・テスト・出力・エントリーポイントの
パス変更、ファイル単位の差分コンパイルや並列コンパイル、
プロジェクトのウォッチモード、テスト用アノテーションや新しい
テストフレームワーク、パッケージ/アーカイブコマンド、リンター連携、
ライフサイクルフックやマニフェストスクリプト、
ターミナルの色付け、既存ディレクトリを取り込むための onion init。
次のステップ¶
- スクリプトランナー(onion) - Onionファイルを1つ直接実行
- コンパイラ(onionc) - クラスファイルへコンパイル
- REPLシェル - 対話的なプログラミング