VSCodeで複数のコンパイラによるビルドを行うための設定

# 概要
VSCodeのtasks.jsonに複数のコンパイラでコンパイル・ビルドするタスクを登録することで，複数コンパイラの利用を容易にする方法を説明します．また，ショートカットキーの設定についても言及します．

# 背景
Fortranで開発を行っていると，様々なコンパイラを利用する機会があります．
普段（プライベート）では開発にgfortranを行い，業務ではIntel FortranあるいはNAG Fortran，スーパーコンピュータ上ではFujitsu Fortran，GPUで実行するプログラムの開発はnvfortranを使うという状況があり得ます．

各種Fortranコンパイラは，規格のサポート状況が異なっていたり，独自拡張が行われていたりするので，記述したソースファイルが一つのコンパイラでコンパイル・ビルドができたからといって，他のコンパイラでコンパイルできる（あるいはコンパイルできたとしても正しく動作する）とは限りません．また，Intel Fortranを対象に開発したコードがnvfortranで動かず，nvfortranで動くように修正したコードが，次はIntel Fortranで動かないということもあり得ます．

一つのコンパイラだけで開発を継続すると，他のコンパイラでコンパイル・ビルドする際に，非常に多くの手直しが必要になります．そのため，開発の初期段階から，（入手が可能であれば）複数のコンパイラでコンパイル・ビルドを行い，動作確認した方が後々の手間を削減できます．

VSCodeのタスクの機能を活用して，複数のコンパイラで同時にビルドを行う方法，コンパイラを切り替える方法を説明します．

# 環境
- Windows 10
- VSCode 1.52.0
- gfortran 8.1.0
- intel fortran 2021.1
- Visual Studio 2017

Windows上で，かつ無料で利用できるコンパイラのみを用いてテストしていますが，異なるのはコンパイルのコマンド，オプションのみなので，異なるコンパイラやOSでも動作します．

# サンプルプログラム
異なるコンパイラでコンパイルされてた事が確認できるように，下記のようなプログラムをサンプルとして利用します．

```Fortran
program main
    implicit none

#if __INTEL_COMPILER
    print *, "Intel Fortran"
#endif
#if __GFORTRAN__
    print *, "gfortran"
#endif
end program main
```

## プリプロセッサディレクティブ
サンプルプログラム中の`#if`～`#endif`はプリプロセッサディレクティブです．プリプロセッサとは，ソースファイルをコンパイルする前に，ソースファイルを加工するプログラムのことです．プリプロセッサディレクティブは，そのプリプロセッサにどのようにソースファイルを加工するかを指示します．

`#if`の後ろに置かれた識別子が定義され，かつ0以外の値であれば，`#if`～`#endif`で挟まれた範囲はプリプロセッサによって何も処理されません．識別子が定義されていないか，定義されていてもその値が0なら，`#if`～`#endif`で挟まれた範囲はプリプロセッサによって削除されます．

Intel Fortranでコンパイルする際は，識別子`__INTEL_COMPILER`が定義され，その値はコンパイラのバージョン（この記事の環境では2021）になります．
gfortranでコンパイルする際は，`__GFORTRAN__`が定義され，その値は1になります．

識別子の値は，`#if`～`#endif`で挟まれた範囲の中であれば，`print`文などを使って確認できます．

```Fortran
#if __INTEL_COMPILER
    print *, __INTEL_COMPILER
#endif
#if __GFORTRAN__
    print *, __GFORTRAN__
#endif
```

## コンパイルオプション
Fortranは，標準でプリプロセスが動作しないので，プリプロセスを行うにはコンパイルオプションが必要です．Intel Fortranの場合は`/fpp`，gfortranの場合は`-cpp`をコンパイルオプションとして付与します．

# ビルドの設定
## tasks.jsonの作成

メニューバーの**ターミナル**→**ビルドタスクの実行**を選択するか，ショートカットキー`Ctrl+Shift+B`を入力すると，コマンドパレットに**実行するビルドタスクはありません．ビルドタスクを構成する**というメッセージが現れるので，それを選択→**テンプレートからtasks.jsonを生成**→**Others**と選んでいくと，`tasks.json`が作られます．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/70343fa7-d7c5-a214-0b9d-bc39a5d23a83.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/2ed86ed2-f900-5e43-d009-8c574d778aed.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/07d99d78-35b7-73a7-7432-957b0187ab8b.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/a074f4f2-4db4-271c-0dc1-423c24572f3d.png)

`tasks.json`に，gfortranでビルドするタスクと，Intel Fortranでビルドするタスクを追加します．

### gfortranを使ったビルド
gfortranを使ってビルドするためのタスクを，下記のように設定しました．

```json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build using gfortran",
            "type": "shell",
            "options": {
                "cwd": "${workspaceRoot}",
                "env": {
                    "PATH": "C:\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\bin"
                },
                "shell": {
                    "executable": "${env:windir}\\system32\\cmd.exe",
                    "args": [
                        "/d",
                        "/c"
                    ]
                }
            },
            "command": "gfortran.exe",
            "args": [
                "-cpp",
                "-o",
                "${workspaceFolder}\\${fileBasenameNoExtension}_gnu.exe",
                "${fileBasename}",
            ],
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": true,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "problemMatcher": [],
        },
    ]
}
```

`"label"`はタスクの名前です．

`"type"`は`"shell"`にして，gfortran.exeがあるフォルダにPATHを通すために`"options"`で指定します．
`"cwd"`はタスクを行うフォルダです．VSCodeの変数を用いて，VSCodeで開いているフォルダ名を指定しています．
`"env"`で環境変数を指定します．ここでは，環境変数`PATH`の値として，gfortran.exeのあるフォルダのパスを与えました．
`"shell"`は，使用するシェルを指定します．

`"command"`がコンパイラの実行ファイル名，`"args"`がコンパイルオプションです．コンパイルオプションには，プリプロセスを有効化するオプションを忘れないように与えます．`"-o"`で出力される実行ファイル名を指定しています．これは，他のコンパイラで作成される実行ファイルに上書きされないようにするためです．

`"presentation"`は必須の設定ではありませんが，`"focus": true`を設定しておくと，ビルド時に統合ターミナルにフォーカスが移るので，ビルドタスクが終了した後，Enterキーを押せばターミナルを閉じつつエディタに戻れます．

`"problemMatcher"`は，タスクのエラーが発生した際に，エラーメッセージをどのような形式で処理するかを指定します．ここでは何も指定しません．何も指定はしないのですが，この設定を書いておかないと，タスクを実行する度に，スキャンするタスク出力のエラーと警告の種類を選択する必要が生じます．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/07fd7ff7-f578-0349-b2b5-6e0756ee80a3.png)

### Intel Fortranを使ったビルド
Intel Fortran (+Visual Studio 2017)を使ってビルドするためのタスクを，下記のように設定しました．PATHの設定が非常に長くなるので省略しています．

```json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build using gfortran",
            省略...
        },
        {
            "label": "build using intel fortran",
            "type": "shell",
            "options": {
                "cwd": "${workspaceRoot}",
                "env": {
                    "PATH": "省略...",
                    "LIB": "省略...",
                    "INCLUDE": "省略..."
                },
                "shell": {
                    "executable": "${env:windir}\\system32\\cmd.exe",
                    "args": [
                        "/d",
                        "/c"
                    ]
                }
            },
            "command": "ifort.exe",
            "args": [
                "/fpp",
                "/o",
                "${workspaceFolder}\\${fileBasenameNoExtension}_intel.exe",
                "${fileBasename}",
            ],
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": true,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "problemMatcher": [],
        },
    ]
}
```

WindowsでIntel Fortranを使う場合は，プログラムをビルドするのにVisual Studioが必要です．そして，環境変数として，PATH以外にLIBとINCLUDEも必要です．

どのフォルダにPATHが通されるかはすぐには判りませんが，確認する方法があります．
まずはコマンドプロンプトを起動して，`echo %PATH%`で登録されているPATHを表示します．
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/d1ef1979-5041-1bcf-f9ca-6dd9b108d6e2.png)

次に，スタートメニューのIntel oneAPIのフォルダにあるコマンドプロンプト（Intel oneAPI command prompt for Intel 64 for Visual Studio 2017）を起動し，同様に`echo %PATH%`で登録されているPATHを表示した後，通常のコマンドプロンプトで表示されたPATHとの差を抽出します．
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/7d94d5bd-36bd-91d3-1a81-44681864da10.png)

通常のコマンドプロンプトのPATHに登録されておらず，Intel oneAPI command prompt for Intel 64 for Visual Studio 2017のPATHに登録されているフォルダが，Intel Fortranでコンパイルするのに必要なフォルダということです．
そのフォルダのパスを，上のタスクにある`options.env.PATH`に登録します．

同様に，Intel oneAPI command prompt for Intel 64 for Visual Studio 2017上で`echo %LIB%`，`echo %INCLUDE%`を実行して，表示されたフォルダパスを，それぞれ`options.env.LIB`，`options.env.INCLUDE`に登録します．

### 登録したタスクの実行
タスクを二つ登録したので，実行してみます．

ショートカットキー`Ctrl+Shift+P`を入力してコマンドパレットを呼出し，`runtask`と入力すると，タスク：タスクの実行というメニューが表示されるので，それを選択すると，登録した二つのタスクの名前が表示されます．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/cf27a674-c3ff-2361-290e-6e0678dcd34f.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/ec87e5f0-7479-2906-219f-b881091918ca.png)

"build using gfortran"を選択するとgfortranでコンパイル・ビルドされ，"build using intel fortran"を選択するとIntel Fortranでコンパイル・ビルドされます．

作成された実行ファイルを実行すると，プリプロセッサディレクティブによって表示が切り替わっていることが確認できます．

```console
>main_gnu.exe
 gfortran

>main_intel.exe
 Intel Fortran
```

## 複数のタスクの設定
一つの`tasks.json`ファイルに，異なるコンパイラでコンパイル・ビルドする設定を書くことができました．しかし，一つずつタスクを呼び出すのも億劫ですし，そのうち一つのタスクしか呼び出されなくなることは想像に難くありません．また，上記の`tasks.json`は，標準のビルドタスクが登録されていないので，いちいちコマンドバレットの呼出し→タスクの選択をする必要があります．

メニューの**ターミナル**→**ビルドタスクの実行**を選択するか，ショートカットキー`Ctrl+Shift+B`を入力したときに，ここで登録した二つのビルドタスクを同時に呼び出せるようにします．

```json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build using gfortran",
            省略...
        },
        {
            "label": "build using intel fortran",
            省略...
        },
        {
            "label": "build all",
            "type": "shell",
            "options": {
                "cwd": "${workspaceRoot}"
            },
            "command": "echo",
            "args": [
                "Build using all compilers done."
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": true,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "problemMatcher": [],
            "dependsOn": [
                "build using gfortran",
                "build using intel fortran"
            ]
        }
    ]
}
```

`"build all"`というタスクを登録しました．このタスクでは，実際には`Build using all compilers done.`を表示するだけです．このタスクで重要なのは，`"group"`と`"dependsOn"`です．

`"group"`のプロパティで`"kind": "build"`，`"isDefault": true`とすることで，このタスクが標準のビルドタスクという扱いになります．メニューの**ターミナル**→**ビルドタスクの実行**を選択するか，ショートカットキー`Ctrl+Shift+B`を入力したときに，このタスクが実行されます．

`"dependsOn"`は，このタスクを実行する前に実行されるタスクを指定します．ここでは，`"build using gfortran"`と`"build using intel fortran"`を指定しているので，この二つのタスクが実行されます．二つのタスクに依存性が無い場合は，二つのタスクが同時に実行されます．

タスクを実行すると，ターミナルが3個に増えます．一つ目が通常のターミナル，二つ目と三つ目が`"dependsOn"`で指定したタスクを行っているターミナルです．これらのタスクは，終了後にEnterキーの入力を待つような設定になっているので，Enterキーを入力すればターミナルごと終了します．
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/8f0fd438-fbb0-ab82-6e43-9fa027ba56f7.png)

`"dependsOn"`には2個以上のタスクを書けるので，タスクを増やしても問題ありません．


# ショートカットキーの設定
複数のタスクを設定して異なるコンパイラでコンパイル・ビルド出来るようになりました．一方で，一つのコンパイラでのコンパイル・ビルドが非常にやりにくくなりました．

例えば，gfortranでビルドしようとすると，`Ctrl+Shift+P`でコマンドパレットを呼び出し，runtaskを入力して**タスク：タスクの実行**を呼び出してから，`build using gfortran`タスクを選択する必要があります．

ショートカットキーを選択しようにも，項目が見当たりません．**タスク：タスクの実行**は，内部的にはworkbench.action.tasks.runTaskという名前が付けられています．**ファイル**→**ユーザー設定**→**キーボードショートカット**でショートカットキーの一覧を開き，runtaskを検索しても見つかりません．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/955066c7-a53a-7205-d756-c83c1c3247e5.png)

workbench.action.tasks.runTaskにショートカットキーを設定するには，`keybindings.json`を直接編集する必要があります．キーボードショートカットの一覧を開き，右上の方にある書類マークをクリックすると`keybindings.json`が開くので，キー入力と実行するコマンドを入力します．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/94d04c73-4926-b997-556f-6256e00b0c26.png)


```json
    {
        "key": "ctrl+t ctrl+b",
        "command": "workbench.action.tasks.runTask",
    },
```

ここでは，`Ctrl+t, Ctrl+b`を割り当てました．`workbench.action.tasks.runTask`のショートカットキーの設定には，本来はもう一つ`arg`というプロパティを設定する必要があります[^arg]．標準で使うコンパイラを定め，そのコンパイラでビルドするタスクを`arg`に設定すれば，ショートカットキー一発でコンパイル・ビルドできるようになります．`arg`を設定しないと，タスクを選択するメニューが呼び出されます．どちらのコンパイラを使うかを予め決めたくない，あるいはビルド毎に切り替えたい等の場合は，`arg`を設定しない方が都合がよいでしょう．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/dddeb86f-f72f-b3cb-ffb1-0e79c4af541a.png)


`workbench.action.tasks.runTask`に`arg`を設定しない場合は，このショートカットキーに加えて，**タスク：最後のタスクを再実行**（`workbench.action.tasks.reRunTask`）にもショートカットキーを設定しておくと，さらに便利になります．設定は，ショートカットキーの一覧を開き，rerunを検索します．

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224040/9cce2101-b528-9e02-81ac-022edfec88aa.png)

ある機能を開発していて，しばらくはgfortranでコンパイルをしたいと思ったときは，まずショートカットキーで**タスク：タスクの実行**（`workbench.action.tasks.runTask`）を実行して，`build using gfortran`タスクを実行します．それ以降は，**タスク：最後のタスクを再実行**（`workbench.action.tasks.reRunTask`）に割り当てたショートカットキーを入力すれば，常に`build using gfortran`が実行されます．

[^arg]: https://code.visualstudio.com/docs/editor/tasks#_binding-keyboard-shortcuts-to-tasks

# まとめ
Fortranを用いた開発において複数のコンパイラを用いる場面がある背景を述べ，VSCodeで複数のコンパイラによるビルドを簡単に行う方法を説明しました．
fpm(Fortran package manager)では，まだコンパイラの切替には対応していないので，fpmの開発を待つ間，VSCodeを上手く使って開発の効率化を図っていきましょう．