ada_language_server

近年はLanguage Server Protocol(以下LSP)のお陰でIDEやエディタを問わず補完や宣言等へのジャンプが使えるようになりました。 それでGNAT/gcc用のLSPサーバーであるada_language_serverを使おう、という話です。

これまでAdaで宣言等を探すのはエディタに外部ツールを登録するなどでカーソル位置をgnatfindやgnatxrefに渡すことで実現していました。 gnatfindやgnatxrefはgcc 12でなくなってしまいましたので代わりが必要なのです。

バイナリの入手方法

ada_language_serverをビルドするのは大変です。 簡単にバイナリを手に入れる方法もいくつかあります。

Visual Studio Codeの拡張として入手

開発元であるAdaCore社が Ada & SPARK というVisual Studio Codeの拡張も作ってくださってますので、これをインストールすればコンパイル済みのada_language_serverも付いてきます。

ダウンロードされたものは例えば ~/.vscode/extensions/adacore.ada-25.0.20240915-linux-x64/x64/linux/ada_language_server のような位置にあります。

他の言語のLSPサーバーも事情は似たりよったりですので、もしVisual Studio Codeを使わないとしてもダウンローダーとして利用するのは有りだと思います。

githubから入手

https://github.com/AdaCore/ada_language_server/releases にもコンパイル済みバイナリが置かれています。

あれ、以前はソースコードだけでしたような。 ありがたいことです。

Alireでビルド

とはいえ自分でビルドしたい場合もあるでしょう。

本当に自力でビルドするとなりますとライブラリを揃えて.gprファイルのパスを調整してと大変な手間がかかりますので Alire の力を借りることにします。

処理系ごとに再発明されたパッケージマネージャなんて覚えたくもありませんし大抵こなれてませんから酷い仕様がありますし他にビルド方法が用意されていなければミックスランゲージやクロスコンパイルの障害にもなりますしいいことはあまりないのですが、こうした巨大プロジェクトをとりあえず手元で動くようビルドするにはやっぱり便利ですね。 くっ。

Alireの環境設定

しばらくぶりに触りましたが Alireのインストール方法は以前とあまり変わっていませんでした

ただ ~/.config の中で全てを展開するクソ仕様は流石に修正されていましたので cache.dir を設定しましょう。 cache.dir という名前ですがその下で実際にビルドも行われますので純粋なキャッシュではないです。 ビルドに失敗すれば中のファイルを修正する必要も出てきます。

また環境変数 ALIRE_SETTINGS_DIR で設定ファイルの位置も変更できますので

$ export ALIRE_SETTINGS_DIR=$PWD/alireconfig
$ alr settings --global --set cache.dir $PWD/alirecache

のようにすれば ~/.config には何も置かずに済みます。

Alireの設定が終わりましたらまずツールチェインを選びまして。

$ alr toolchain --select
Welcome to the toolchain selection assistant

In this assistant you can set up the default toolchain to be used with any crate
that does not specify its own top-level dependency on a version of gnat or
gprbuild.

If you choose "None", Alire will use whatever version is found in the
environment.

ⓘ gnat is currently not configured. (alr will use the version found in the  environment.)

Please select the gnat version for use with this configuration
  1. gnat_native=14.2.1
  2. None
  3. gnat_arm_elf=14.2.1
  4. gnat_avr_elf=14.2.1
  5. gnat_riscv64_elf=14.2.1
  6. gnat_xtensa_esp32_elf=14.2.1
  7. gnat_arm_elf=14.1.3
  8. gnat_avr_elf=14.1.3
  9. gnat_native=14.1.3
  0. gnat_riscv64_elf=14.1.3
  a. (See more choices...)
Enter your choice index (first is default):
> 1

ここでは1のネイティブコンパイラの最新版を選んでおきます。

ada_language_serverをビルド

事前にlibcの他にlibstdc++や GMP も環境にインストールされている必要がありますが、そもそもgccを動かすために必要なものばかりですので既に入っているはずです。

ではada_language_serverを取ってきてビルドしてみましょう。

$ alr get ada_language_server
$ cd ada_language_server_24.0.0_a8b6ee7c
$ alr build

コマンドとしてはこれだけです。 量が多いので時間がかかります。

各ライブラリのビルドは cache.dir の下の builds の中で、ada_language_server本体のビルドはada_language_serverをクローンしたディレクトリ ada_language_server_24.0.0_a8b6ee7c の中で行われます。 .obj というドット付きディレクトリになっていますので探しづらいものの最終的に実行ファイルは ./.obj/server/ada_language_server に作られます。 (Alireの仕様ではなくada_language_serverの gnat/lsp_server.gpr の記述によるものです。)

実行ファイルとしての ada_language_server はデータファイルも必要とせずライブラリもAdaで書かれた分は全て静的リンクされていますので、好きな場所に移動しても動きます。

ところでこうして何もオプションをつけずにビルドした場合はデバッグ用ビルドになります。 それぞれの.gprファイル次第ではありますがだいたいは最適化オプションも -O0-Og ですので実用のためにはリリース用ビルドをしたいところです。

リリース用にビルドするにはalr buildコマンドに --release オプションを渡します。

デバッグ用ビルドとリリース用ビルドでディレクトリを分けてくれているかそれとも混ざってしまうのかも.gprファイル次第になってしまいますのでオプションを変えてビルドをやりなおすには一度 alr clean でビルドしたものを削除したほうが確実です。

$ alr clean
$ alr build --release

……実はこれだけですとまだ駄目です。

幸いにもAdaの各プロジェクトはまだAlireへの依存が低いためAlire側でビルドオプションを変えても全てのプロジェクトでそれが使われるようにはなっていません。 これはAlireの使い勝手としては悪い反面、良い傾向とも言えます。 既にパッケージマネージャを用いないビルド方法が用意されていない酷いプロジェクトなんて溢れていますからね……。

gprbuildでは -X変数名=値 の形で外から値を渡せます。 gprbuildを用いるほとんどのプロジェクトではこの方式で何らかのビルドオプションを指定できるようになっています。 alr buildコマンドでは -- に続けて渡したオプションはそのままgprbuildに引き渡されますのでこれでビルドオプションを指定できます。

これもプロジェクト次第の話ですので例えばconfigureとmakeを用いる昔ながらのプロジェクトですとalr build経由でオプションを指定する方法が無かったりします。 そういうときはそれぞれのビルドディレクトリに入って個別対応する必要があります。

今回はada_language_server自体がAdaCore社製のプロジェクトということもあって全ての依存ライブラリでもgprbuildが使われていました。

それで適当にgrepしてみました結果、ほとんどは -XBUILD_MODE=prod で良さそうです。 vss(AdaCore社のUnicodeのライブラリ、Visual SourceSafeではない)のみ -XBUILD_PROFILE=release でした。 他の例外では alire.toml にも記述がなされておりalire buildの--releaseオプションに連動してくれました。

というわけで最終的なリリース用ビルドのやり方はこうなります。

$ alr clean
$ alr build --release -- -XBUILD_MODE=prod -XBUILD_PROFILE=release

すんなりとうまくいった方はおめでとうございます。 私はリンクができませんでした。

依存ライブラリのひとつlibadasat.aの中身が存在していないかのようなエラーメッセージが出てきます。 nmしてもちゃんと必要な中身がありますのにリンカが見つけられないようです。

原因は上で選択したツールチェインのgnat_native 14.2.1に含まれるldがLTOに対応していないからでした。

adasat.gpr では BUILD_MODEprod の時に -flto が有効になっていましたのでこれを削除することで無事にリンクできました。

ファイルの位置は cache.dir の下の builds/adasat_24.0.0_46db8c9a/d497b5e492503ae325667c8793cc729c969f2a5fcfbee7c788ace2730761a029/adasat.gpr です。

こうしてアドホックな修正が許されているって素晴らしいですね。 ネイティブコンパイラにリンクエラーは付きものですから。

例えばOPAMのような最悪のパッケージマネージャではローカルでのアドホックな修正が許されておらず(以下略)

修正ついでですのでada_language_serverの gnat/lsp_server.gpr に書かれていますリンクオプションに -Wl,--gc-sections を追加しちゃいましょう。 バイナリのサイズを減らすことができます。

Kateでの利用例

Visual Studio Codeから使うぶんには拡張を入れて説明通りに settings.json を書けば使えてしまいますので、他のエディタで使ってみましょう。

例として Kate を使用します。

QtCreaterやBBEdit等でも設定方法さえ見つけられれば同様に使えるはずです。

以前に使うのが難しいと愚痴ったのですけれども、うまく使えちゃえましたのでこれ書いてます。

まずはKateのLSPクライアントを有効にしてada_language_serverの存在を伝えるところからです。

設定ダイアログのPlugin ManagerからProject PluginとLSP Cientを有効にします。

../../../_images/2024_11_06_1_kateplugins.png

するとLSP Clientの設定が現れますのでUser Server Settingsに次のような内容を入力します。

{
 "servers": {
  "ada": {
   "command": ["ada_language_server"],
   "highlightingModeRegex": "^Ada$",
   "root": "%{Project:NativePath}"
  }
 }
}
../../../_images/2024_11_06_1_kateservers.png

シンタックスハイライトがAdaになっているファイルを開いたときにそのファイルが所属する「Kateのプロジェクト」のルートディレクトリをカレントディレクトリにして ada_language_server を実行する、という意味です。

Kateが「Kateのプロジェクト」と認識してくれる条件はバージョン管理されていて.git/.svn/.hg等が存在するか、または.kateprojectファイルがあることです。 大抵はバージョン管理しているでしょうから気にしなくていいです。 現在のプロジェクトはサイドバーのProjectsペインで表示できます。

さて、一応これでada_language_serverは起動してくれるようになりました。 しかしこれだけでは全機能を利用することはできません。

試しにこの状態で使ってみます。

../../../_images/2024_11_06_1_katenogpr.png

プロジェクトファイルがないと言われました。 このプロジェクトファイルとは(.kateproject のようなエディタ固有の話ではなく).gprファイルのことです。

GNAT/gccはコンパイルの過程でクロスリファレンス情報を.aliファイルに出力します。 gnatfindもgnatxrefもそしてada_language_serverもこの情報を利用します。 .aliファイルは.oと同じディレクトリに作られますのでビルドに用いているディレクトリの位置が必要になりそれはビルドツールであるgprbuildと共有するのが自然、ということなのだと思います。 gnatfindやgnatxrefであればgnatmake同様のオプションを受け取ってくれますので.gprファイルは必須ではありませんでしたが、ada_language_serverでは.gprファイルは必須です。

というわけで.gprファイルを用意します。

hello.gpr
project Hello is
   for Object_Dir use "build";
   for Source_Dirs use (".");
end Hello;

ada_language_serverだけのためであれば実際にビルドできるように書く必要はありません。 Object_DirSource_Dirs だけで充分です。

少々複雑な例として The Village of Vampire [*] の場合も載せておきます。

vampire.gpr
project Vampire is
   for Object_Dir use "x86_64-linux-gnu.noindex";
   for Source_Dirs use (
      "source",
      "lib/openssl-ada/source",
      "lib/yaml-ada/source",
      "lib/web-ada/source");
   for Runtime ("Ada") use "/opt/lib/drake/x86_64-linux-gnu/7.5.0";
end Vampire;

ソースコードのディレクトリが複数ありAdaランタイムもgcc付属とは異なるものを利用しています。 ada_language_serverが動けばそれでいいので色々ハードコードしてしまっていますがちゃんと書くならアーキテクチャやバージョンは変数になるでしょう。

こうして用意した.gprファイルを、LSPの initializationOptions を通じて位置を伝えてやる必要があります

Kateでは先程の設定にて initializationOptionsの内容を指定することができます

{
 "servers": {
  "ada": {
   "command": ["ada_language_server"],
   "highlightingModeRegex": "^Ada$",
   "root": "%{Project:NativePath}",
   "initializationOptions": {
    "projectFile": "hello.gpr"
   }
  }
 }
}

しかしこれは……正直だめですよね。 全てのプロジェクトの.gprファイル名が同じなはずがないです。

プロジェクト毎に設定を変える必要があります。

これで長いこと悩んでいたのですが、方法を2つ見つけました。

.kateproject

大抵はバージョン管理しているでしょうから気にしなくていい、と書いた .kateproject ファイルですがKateのドキュメントをよくよく読み返してみますとLSPの設定を上書きすることができます

.kateproject
{
 "name": "hello",
 "files": [{
  "directory": "."
 }],
 "lspclient": {
  "servers": {
   "ada": {
    "command": ["ada_language_server"],
    "highlightingModeRegex": "^Ada$",
    "root": "%{Project:NativePath}",
    "initializationOptions": {
     "projectFile": "hello.gpr"
    }
   }
  }
 }
}

他にもサイドバーのProjectsペインに表示するファイルをフィルタするような便利な機能もありますので .kateproject ファイルを作ることにしてしまえば何かと捗ります。

ada_language_serverの--configオプション

ada_language_serverには initializationOptions の内容を書いたファイルをコマンドライン引数で渡すことができます。

$ ada_language_server --help
Usage: ada_language_server [options]

Options:
  --tracefile <ARG>          Full path to a file containing traces
                               configuration
  --config <ARG>             Full path to a JSON file containing
                               initialization options for the server (i.e: all
                               the settings that can be specified through LSP
                               'initialize' request's initializattionOptions)
  --language-gpr             Handle GPR language instead of Ada
  --version                  Display the program version
  -h, --help                 Display help information

ですのでKate側の設定はこのようにしておいて

{
 "servers": {
  "ada": {
   "command": [
    "ada_language_server", "--config", "initializationOptions.json"
   ],
   "highlightingModeRegex": "^Ada$",
   "root": "%{Project:NativePath}"
  }
 }
}

各プロジェクトのディレクトリに initializationOptions.json を置く、という方法です。

initializationOptions.json
{
 "defaultCharset" : "UTF-8",
 "enableIndexing" : false,
 "projectFile" : "hello.gpr"
}

ハードコードしてしまいますと initializationOptions.json が無いとada_language_serverの起動に失敗することになりますので、ファイルがあるときだけオプションを足すようなラッパースクリプトを間に入れても良いかもしれません。

余計なファイルを置けないディレクトリ

余計なファイルを置きたくない、置けないディレクトリではどうしようもありませんのでその場合はラッパースクリプトに頼ることになります。

KateではOpen Folder...で任意のディレクトリをプロジェクト扱いで開くことができます。 root%{Project:NativePath} と指定していますのでLSPサーバーはプロジェクトとして開いたディレクトリをカレントディレクトリにして起動されます。 command にラッパースクリプトを指定してその中で $PWD で分岐すればよいでしょう。

ラッパースクリプトを挟めば --config オプションに渡すファイルの置き場所や環境変数等の調整も含めて融通が利くようになります。

コンパイラの使い分けとPATH環境変数

その他の注意事項としましてはada_language_serverは起動時点でPATHが通っているコンパイラを使います。 例えばAdaランタイムのソースコードにジャンプした場合はPATHの通っているコンパイラの lib 内に飛びます。 ランタイムライブラリと.aliファイルの内容が食い違えばうまく動かない機能もありそうです。

/usr/bin/gcc だけを使うなら気にしなくてよいのですが、複数のバージョンを使い分けたり(Alire等で)他の場所にインストールしたツールチェインを使いたければ少し考える必要があります。

方法は色々あるとは思いますが .kateproject ファイルで解決する場合はenvコマンドが利用できます。 %{ENV:...} と書くことで環境変数を参照できます。

.kateproject
{
 "name": "hello",
 "files": [{
  "directory": "."
 }],
 "lspclient": {
  "servers": {
   "ada": {
    "command": [
     "env", "PATH=/opt/ytomino/gcc-7.5.0/bin:%{ENV:PATH}", "ada_language_server"
    ],
    "highlightingModeRegex": "^Ada$",
    "root": "%{Project:NativePath}",
    "initializationOptions": {
     "projectFile": "hello.gpr"
    }
   }
  }
 }
}
../../../_images/2024_11_06_1_kategcc7.png

Text_IO を選択してGo to Declarationでgcc-7.5.0のランタイムライブラリにジャンプできました。

まとめ

Kateとada_language_serverを例にしてきましたが、このようにLSPサーバーと initializationOptions を指定する方法さえあればKateに限らずお好きなエディタでAdaに限らずお好きな言語の補完やジャンプが使えるようになるはずです、たぶんきっと。