XtextによるVSCodeのExtensionをビルドしてみた


概要

言語ワークベンチであるXtextによるVSCodeのExtensionをビルドした際の作業メモです。
作ったではなくあくまでビルドで、基本的にはここに書いてあることをするだけです。
その手順自体は難しいものではないのですが、

  • 躓いたポイントで次は躓かない
  • xtextの日本語での情報が少ない

ことなどを加味して書き残しておくことにしました。
これらが誰かのお役に立てば幸いです。

最初に

Xtextとは

TypeFoxによる言語ワークベンチと称されるジャンルのソフトウェアで、独自のプログラミング言語の開発を容易にするもの、と捉えています。類似のものに、JetBrainsMPSがあります。

VSCodeとは

Microsoftが提供する、説明不要の人気エディタ。エディタの枠を超えて、もはやIDEとしか思えない出来です。VSCodeのExtensionとは、VSCodeの機能を拡張するVSCode専用のソフトウェア。

ビルドとは

XtextによるVSCodeのExtensionとは、Xtextで文法定義した独自のプログラミング言語向けの編集機能を提供するものであり、その提供の形態は専用の拡張子vsixを持つファイルになります。ここで言うExtensionのビルドとは、Xtextによるパーサーの生成(Java)から始まり、jarファイル生成、Extension形式へのパッケージングなどを含む工程を指します。

ビルド環境

  • Windows10 Pro 1909
  • VSCode 1.47
  • JDK1.7JDK 1.8
  • Tortoise Git 2.9.0.0

早速ビルド

作業自体、難しいことはありません。
まずは、ソースコードをGitHubのXtext Visual Studio Code Exampleからローカルの任意の場所にCloneしておきましょう。
Quickstartに書かれている通り、コマンドプロンプトを起動し、cdコマンドでClone先のフォルダへ移動、続けて、gradlew startCodeと打ち込むだけです。
しばらく待って

この画像のようにVScodeが起動すればビルド成功です。

トラブルシュートが必要な場合

ビルドの過程でいずれのエラーにも遭遇しなかったのであればラッキーです。
後述の起動後までスキップしてください。
ここからは、私が遭遇したエラーと対処についての覚書です。
おそらく、gradleやnpmなどビルドシステムに関わる代表的なエラーに、ことごとくひっかかったんじゃないでしょうか…。

JDKバージョン確認

Exception in thread "main" javax.net.ssl.SSLException: Received fatal alert: protocol_version

というエラーが出たら、JDKのバージョンと環境変数でJDKのパスを調べましょう。
当方、別のアプリケーション開発の都合でJDK1.7(古い)を使っているのですが、Cloneしたソースコード中に含まれるgradleファイルはJDK1.8を要求しているようです。
環境変数のJAVA_HOMEのパスをJDK1.8のパスに入れ替えた後は、エラーがでなくなりました。
ググると、1.7でも動くような内容の記事を見かけるので、環境変数のパスだけ設定すれば済む話かもしれませんが、ここは未検証です。
なお、環境変数のパス設定に関わる情報はここが詳しいです。

エラー(スタックトレース)
Downloading https://services.gradle.org/distributions/gradle-5.6.4-bin.zip

Exception in thread "main" javax.net.ssl.SSLException: Received fatal alert: protocol_version
        at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
        at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
        at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1961)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1077)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)
        at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:515)
        at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1299)
        at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
        at org.gradle.wrapper.Download.downloadInternal(Download.java:67)
        at org.gradle.wrapper.Download.download(Download.java:52)
        at org.gradle.wrapper.Install$1.call(Install.java:62)
        at org.gradle.wrapper.Install$1.call(Install.java:48)
        at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:69)
        at org.gradle.wrapper.Install.createDist(Install.java:48)
        at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:107)
        at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:63)

gradle向けProxy設定

a network error occurred

なエラーが出た場合は、原因の1つとして、gradleが使用するProxy設定を行っていない可能性を疑ってください。gradleが使用するProxy設定は、gradle.propertiesというファイルをgradleプロジェクトのルートフォルダに置くことで解決します。今回のビルドで言えば、README.mdと同一階層に該当するファイルがすでにあるので、ここを参考に、会社のIT部門が設定または指定している設定を追記したところ、エラーがなくなりました。

npm向けProxy設定

error code ENOTFOUND
~(略)
error network This is a problem related to network connectivity

なエラーが出た場合、npmにもProxy設定が必要な可能性が高いです。これを参考に、gradleと同様の設定を反映したところ、エラーがなくなりました。

xtext向けProxy設定

*ATTENTION*
It is recommended to use the ANTLR 3 parser generator (BSD licence - http://www.antlr.org/license.html).
Do you agree to download it (size 1MB) from 'http://download.itemis.com/antlr-generator-3.2.0.jar'? (type 'y' or 'n' and hit enter)y
10046 [main] INFO erator.parser.antlr.AntlrToolFacade - downloading file from 'http://download.itemis.com/antlr-generator-3.2.0.jar' ...
Downloading ANTLR parser generator failed: download.itemis.com
Please install the feature 'Xtext Antlr SDK' manually using the external updatesite:

'http://download.itemis.com/updates/'.

(see http://www.eclipse.org/Xtext/download.html for details)

のように、antlr3.2がダウンロードできない的なエラーが出た場合、Proxy設定があるのかないのかわかっていないものの、回避方法でなんとか切り抜けられます。その手順は以下の通りです。
1. ここへアクセス
2. 記事内の直接ダウンロードできるリンクをクリック
3. 保存先指定を促されたら、Cloneしたソースコードのorg.xtext.example.mydslフォルダ直下に保存
4. ダウンロードしたファイルに対して、ファイル名の先頭に(.)ドットを追加

クラスファイルのバージョンが違う?

(2021/06/30追記)
2021/06/20くらいから次のエラーが出て、ビルドが完了しない現象が起きることに気づきました。

Execution failed for task ':org.xtext.example.mydsl:generateXtext'.
> com.google.common.util.concurrent.ExecutionError: com.google.common.util.concurrent.ExecutionError: com.google.common.util.concurrent.ExecutionError: com.google.common.util.concurrent.ExecutionError: com.google.common.util.concurrent.ExecutionError: java.lang.UnsupportedClassVersionError: org/eclipse/core/runtime/OperationCanceledException has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

エラーを検討した結果、Xtextが依存するorg.eclipse.equinox.common-xx.jarというファイル(xxにはバージョンが入る)では、バージョン3.15はjava11によりコンパイルされたため、他のjarファイルが利用するjava8でコンパイルされたものとバージョンの不一致、が原因のようです。同じ事例は海外のサイトで議論されている状況で、解決策は現時点ではまだないものの、Xtext Visual Studio Code Exampleでは回避策が適用されています。この回避策をbuild.gradleに適用すればビルドが成功することを確認しました。

scriptがない?

(2021/11/08追記)
2021/11/03くらいから次のエラーが出て、再びビルドが完了しない現象が起きることに気づきました。

* What went wrong:
A problem was found with the configuration of task ':vscode-extension-self-contained:vscodeExtension'.
> File (略)tflab.xtext-languageserver-example\node_modules\vsce\out\vsce' specified for property 'script' does not exist.

この時点でわかった状況は、新規にCloneしビルドした場合でのみで、既存のビルドが成功したものであれば何度ビルドしてもエラーは発生しない、ということのみです。
検討した結果、npmでインストールする(ダウンロードする)最新のvsceモジュールにはvsceファイルが保管されているパスが、fix: make it build on windowsによって、vsceモジュールのルート直下に変わったことが原因と分かりました。
上記のようにvsceファイルのパスが変更されたにもかかわらず、既存のbuild.gradleファイルでは、

    task vscodeExtension(dependsOn: [npmInstall, npmInstallVsce], type: NodeTask) {
(略)
        script = file("$npmInstallVsce.destPath/out/vsce")
        args = [ 'package', '--out', destPath ]
        execOverrides {
            workingDir = projectDir
        }
    }

と元のパスを参照するようにコーディングされています。
これではビルドが通るはずありません。
また、fix: make it build on windowsの適用バージョンは、v2.2.0 v2.1.0 v2.0.0 v1.103.1 v1.103.0 v1.102.0 v1.101.0 v1.100.2と広い範囲に渡っています。
これにより、新規にダウンロードするvsceモジュールではvsceファイルのパスが変更されたものとなり、エラーとなるのです。対策としては、

  1. gradleでのvsceファイルのパス指定を最新に合わせる
  2. vsceモジュールの変更前のバージョンをgradleで指定する

をセットで行う必要があります。
gradleでのvsceファイルのパス指定を最新に合わせる、だけでいいのでは?と思われた方もいると思いますが、ここにも罠がありました。
v2.0.0以降のバージョンでは、コード中のある箇所に演算子??が使われるようになっているのです。
この演算子は、ここにもあるとおり、比較的新しいものと分かりました。
一方、gradleで指定するNode.jsはv10であるため、??演算子は未対応です。
変更を加えたすべてのバージョンの確認は大変な作業です。もしかしてですが、まだ??を使っていないバージョンがあるかもしれないと思い、??演算子がvsceのどのバージョンから適用されたのか調査したところ、v2.0.0以降と分かりました。
以上から、gradleで指定するvsceのバージョンをv2.0.0より前で最もバージョン番号が大きい、v1.103.1

task npmInstallVsce(type: NpmTask, dependsOn: npmSetup) {
    ext.destPath = "$rootProject.projectDir/node_modules/vsce" 
    outputs.dir(destPath)
    group 'Node'
    description 'Installs the NodeJS package "Visual Studio Code Extension Manager"'
    args = [ 'install', '[email protected]' ]
}

のようにして指定することとします。
結果、無事ビルドできるようになりました。

VScodeにProxy設定

VScode起動後、おそらくProxy設定のダイアログボックスが出てくると思います。
右上の☓ボタンで閉じることもできるのですが、VScodeを起動するたびに毎回しかも2回出てくるのはちょっと鬱陶しいですよね。
Proxy設定するにはここを参照してください。

(2021/06/30追記)
VSCodeのVersion1.52からProxy設定がサポートされたようです。

VScode起動後は…

Proxy設定が必要だった人もそうでない人も、無事にVScodeが起動したでしょうか。
冒頭で示した画像のとおり、DEMOフォルダを指定して起動している状態で、また、末尾に!がない行にエラーが報知されている状態と思います。
モダンなエディタがサポートするさまざまな機能が体験できるので、試してみてください。
この手間でできたとは思えないほどのExtensionがビルドされることに感心しました。

コード補完

エラーが出ている箇所までカーソルを移動し、Ctrl+Spaceを入力すると、

のように入力候補として!が出てきます。
Enterで確定され、!が自動で入力されます。

コード整形

すでに実装されている文字列を選択し、

コンテキストメニューからFormat Selectionを選択すると、

のようになります。
整形結果は…ちょっと納得できませんが、整形はできています。

コード生成

このExtensionにはjavaソースの生成機能がついています。
DEMOフォルダの直下にsrc-genフォルダとaGreeter.javaなどのファイルががいつの間にか生成されていることが確認できたでしょうか。
aGreeter.javaを開くと

のようになっており、クラスおよびa.mydslで入力した内容をコンソールに出力するコードが生成されていることがわかると思います。

Extensionのバージョン確認

画面左にあるExtensionボタンをクリックすると、確認できます。

補足

簡単ですけど、本文中の技術に関して、記事の内容に関係する部分について、少し触れておこうと思います。

Xtext

公式サイトはこちらです。
言語ワークベンチと称されるジャンルのソフトウェアで、特定領域向けプログラミング言語の定義と、定義に基づいて入力補完やシンタックスハイライトなどエディタ向けの機能を生成するもの、と理解しています。
Eclipse向けのプラグインをメインとしているようですが、IntelliJ IDEA、ブラウザ、LSP向けなどさまざまなプラットフォーム、環境で動く様に設計されている様です。
今回の記事ではLSPを使用しています。

gradle

公式サイトはこちらです。
groovy言語でビルドのルールなどを記述することで、手元に依存するソースコードおよびライブラリがなくとも、ネットワーク上のリポジトリから、それらの取得、ビルドまでを一貫して行うことができるシステム、と理解しています。
gradleでは、文字数が少なく記述できる様に工夫されているものの、その省略された記述を読み解くのは初学者にはなかなか辛く、さらに書くとなると公式サイトを含む多数の解説サイトのお世話なっても難しいものでした。
今後、ライブラリへの依存関係を新規に設ける様な場合は、理解が必須のものとなります。

npm

公式サイトはこちらです。
Node.jsのモジュール管理ツールである、ということを除くと、謳われている機能はgradleに良く似ています。
この記事を作るにあたっては、npmをほとんど意識する必要がなかったので、機会があれば勉強してみたいと思います。