VS Codeの拡張機能で自分用のツールを作ってみた
前回のおさらい
- さてさて、前回はVS Code拡張機能のプロジェクトをとにかく「形」にするところまで進みました。
npmとyoコマンドを使って、ひな型のプロジェクトを無事作成!
作ったものの中には package.json
や extension.ts
など、それっぽいファイルたちが並んでいます。 ……が、正直なところ「なんとなくファイル名はわかったけど、中身まではピンときてない」状態だよね。
つまりは:
- プロジェクトは作れた
- でも、どこをどう直せば動くのかはまだモヤっとしてる
という地点まで来た、という感じです。
今回は、自分の必要な処理を入れながら各ファイルの役割を理解していきます。
作るツールの実装方針を決めよう
まずはこのツールの動作を整理していきます。最終的なゴールは
- VS Codeので書いたソースをVS Codeの機能であるかのようにビルドとデバックをできるようにする。
でした。
既に該当の機能は、マイクロソフトが提供してくれているのでコアな機能を作成する必要はありません。
拡張機能ID | 名称 | 用途 |
---|---|---|
ms-vscode.cpptools | C/C++ | C/C++の構文解析、コード補完、デバッグ支援 |
ms-vscode.cmake-tools | CMake Tools(※推奨) | CMakeを使う場合に便利(今回はMakefile前提なので任意) |
ms-vscode.cpptools-extension-pack | C/C++ Extension Pack(まとめパック) | 上記cpptools 含むパック。初心者向けに便利 |
マイクロソフトさんが既に出してくれているものを自分で作るなんて、劣化再生産はちょっと避けたいですね。
じゃ?お前?何を作るの?
はい。当然のご指摘です。ここまでの話では「何も作る必要はないじゃないか」そう思うことでしょう。そう思っていた時期が僕にもありました。
作るべき設定ファイル
よそ様の記事ですが。だいたいどこの書きっぷりもこんな感じです。
launch.jsonを作れ どこに?生のファイルでいいの?完全に呪文です。
実際には、vs codeで直接開いているディレクトリの直下に.vscodeディレクトリを掘って、その下に指定の名前(launch.json)でファイルを作って、そこに設定を書き込め。
という指示なんですよね。僕のような脳みそが豆腐の人間にはわかりません。
そしてそこに記載する設定のサンプルはこんな感じです。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug App",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/output/bin/app",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"preLaunchTask":
"debugbuild",
//中略
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
各設定項目の意味は以下の通りで、★についてはgccのコマンドの打ち方、Make、そして環境情報に内容に依存しているので、設定を間違うと、絶対に動きません。
設定項目 | 内容 | サンプル値 |
---|---|---|
name |
デバッグ構成名 | "Debug App" |
type |
デバッガの種類(cpptoolsではcppdbg ) |
"cppdbg" |
request |
起動方法(launch かattach ) |
"launch" |
program |
実行ファイルへのパス ★ | "${workspaceFolder}/output/bin/app" |
args ★ |
プログラムに渡す引数(不要なら空リスト) | [] |
stopAtEntry |
エントリで停止するか | true |
cwd |
作業ディレクトリ |
"${workspaceFolder}" ★ |
environment |
環境変数設定(通常空でOK) | [] |
externalConsole |
外部コンソールを使うか | false |
MIMode |
GDBなどのバックエンド指定 | "gdb" |
setupCommands |
GDBコマンドを起動時に実行 | [{ "description": "Enable pretty-printing", "text": "-enable-pretty-printing" }] |
preLaunchTask |
起動前に実行するタスク名(tasks.json で定義) |
"debugbuild" |
miDebuggerPath ★ |
GDBのパス | "/usr/bin/gdb" |
この情報を毎回。プロジェクトを新規に作るたびに間違いなく設定する必要があります。わりとムリゲーです。
また、テスト対象のプログラムバイナリを設定ファイルに記述しているので、毎回間違いなく同じ場所にプログラムが生成されるようにしなければ、期待通りの動作を望めません。つまり、ビルド品質を一定に保つことが、大前提の仕掛けなのです。
ビルドの結果を均一にしよう
先の例を見れば自明ですが、マイクロソフトのこのデバッガが正しく動作するためには「拡張機能の都合に合わせて」「毎回同じビルド結果を実現」しなくてはいけない。つまり、ビルド結果を均一に保つ必要があるわけです。
- ビルドした結果の実行ファイルの名前がちがってもだめ
- 生成先のディレクトリが異なってもだめ
- ビルド時に利用するコンパイルオプションが違ってもだめ
1回だけなら何とかなりそうでも、開発期間中ずっとこれを維持するのは難しいです。もしくは、開発終了後、プログラムを変更したくなった時、
- 生成プログラムの名前って何だけ?
- どのディレクトリに出すんだっけ?
- あれ?オプションは?
となると目も当てられません。絶対に思い出せませんよね。僕には無理です。
そうすると、せっかくC言語で書いているのに、VS Codeの設定ファイルを確認するという。なんだか訳の分からない作業になります。
Makeファイルを利用しよう
このようにビルドの作業を均一に保ちたい時に利用するのがMakeです。「bashスクリプトじゃないのか?」はい。違います。Makeを利用します。その理由は今後、どこかで整理するのでそちらをご覧ください。
今回利用するMakeファイルを示します。
CC = gcc
CFLAGS = -Wall -Wextra -Iinclude
LDFLAGS =
SRC_DIR = src
TEST_DIR = test
TARGET = output/bin/app
TEST_TARGET = output/bin/test_runner
SRC = $(wildcard $(SRC_DIR)/*.c)
SRC_NOMAIN = $(filter-out $(SRC_DIR)/main.c, $(SRC))
TEST_SRC = $(wildcard $(TEST_DIR)/*.c)
all: build
build:
mkdir -p output/bin
$(CC) $(CFLAGS) $(SRC) -o $(TARGET)
debugbuild:
mkdir -p output/bin
$(CC) $(CFLAGS) -g $(SRC) -o $(TARGET)
debug-test:
mkdir -p output/bin
$(CC) $(CFLAGS) -g $(TEST_SRC) $(SRC_NOMAIN) -o $(TEST_TARGET) -lcunit
clean:
rm -f $(TARGET) $(TEST_TARGET)
詳細については、次回の以降の記事で解説していきますが、前提としているディレクトリ構成についてはここで記載しておきます。
想定ディレクトリ
プロジェクトルート/
├── Makefile
├── include/
│ ├── *.h ← ヘッダーファイル(関数宣言など)
├── src/
│ ├── main.c ← エントリーポイント
│ ├── *.c ← その他の実装ファイル
├── test/
│ ├── *.c ← CUnitなどを使ったテストコード
├── output/
│ └── bin/
│ ├── app ← ビルドされたアプリケーション
│ └── test_runner ← ビルドされたテスト実行バイナリ
このように、ディレクトリを想定してしまえば、先のlaunch.jsonの記載内容も作るたびに考える必要はなくなります。
「ディレクトリ」を毎回同じ様に作る等、「作業を均質化」させるための仕掛けのことを「標準化」と言います。こうしてみると
標準化と拡張機能が恐ろしく相性がよい様に見えます。しかし、それは物事の原因と結果が逆なのです。
標準化しているから自動化できる
これが本当の順序です。おそらく、拡張機能の方もこのように利用者が標準化することを前提として設計しているのでしょう。
launch.jsonとmakefileの連携
ここまででlaunch.jsonとmakefile。それぞれの役割は説明してきました。ここで簡単にサンプルの記載内容を通じて、launch.jsonとmakefileがどの様に連携しているかを見てみましょう
デバッグ実行前の処理
launch.jsonを確認すると
"preLaunchTask":
"debugbuild",
の記載にぶつかります。これはpre(予め)Launch(起動)Task(仕事)ですので、起動前に予め実行する処理を指定しています。この場合はmakeコマンドのターゲットのことです。つまり。デバッグ実行する前にmake debugbuild
を実行するよと指定しているわけです。
これに対して、今回準備するmakeでは
CC = gcc
CFLAGS = -Wall -Wextra -Iinclude
SRC_DIR = src
TARGET = output/bin/app
SRC = $(wildcard $(SRC_DIR)/*.c)
debugbuild:
mkdir -p output/bin
$(CC) $(CFLAGS) -g $(SRC) -o $(TARGET)
となっていますので実際には
gcc -Wall -Wextra -Iinclude -g src/*.c -o output/bin/app
を実行するわけです。このコマンドの意味は
- include/*.hをヘッダファイルとして利用する
- -g つまりデバッグオプションを有効にする
- src配下の*.cファイルをコンパイルしoutput/bin/app として実行ファイルを作る
となりますので。output/bin/app にデバッグ情報を埋め込まれた形の実行可能ファイルが生成されることになります。
この2つのツールを媒介して、「debugbuild」を解決するのがcmake機能で、設定ファイルとしてはtasks.jsonが該当します。
{
"version": "2.0.0",
"tasks": [
{
"label": "debugbuild"★[debugbuild]の名前解決,
"type": "shell",
"command": "make debugbuild" ★実行コマンドの解決,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"]
}
]
}
デバッグ時の処理
さらにlaunch.jsonを読みこむと
"program": "${workspaceFolder}/output/bin/app"
"miDebuggerPath": "/usr/bin/gdb"
の記述から/usr/bin/gdbを利用して./output/bin/appを起動することが予想されます。これはmakeが生成する実行ファイルそのものです。このように、標準化されたディレクトリ、ファイルの「場所」で対象ファイルを指定し、実行する仕掛けとなっています。
ツールの動作をまとめると
ここまでで、このツールが何をするのかが決まりました。
- 標準ディレクトリを作成する
- launch.jsonを作成する
- tasks.jsonを作成する
- makefileを作成する
実装方式
前述の要件を満たそうとすると、2つの方式が考えられます
- 案1 ツールが必要なディレクトリ、ファイルを作成する方式
- 案2 予め用意したディレクトリ、ファイルをツールがコピーする方式
各方式のメリットデメリットを以下に示しますが、品質確保、変更容易性2の観点から案2の方式を採用するのが主流かと思います。
案 | 実装方式 | メリット | デメリット |
---|---|---|---|
案1 | ツールが必要なディレクトリ・ファイルを生成(コード内でmkdirやファイルwrite) | - 動作が柔軟でパラメータ次第で動的にファイルを変えられる - テンプレートが不要なので拡張機能のサイズが小さい |
- 生成コードのメンテが難しくなる - テンプレート変更時にコード修正が必須 - 長期運用するとソースと生成物のズレが発生しがち |
案2 | 事前に用意したテンプレートをコピー(テンプレートディレクトリから複製) | - テンプレートの内容を直接編集できる(非エンジニアでも対応可能) - 生成ロジックは単純で安定 - テンプレートを複数用意すればバリエーション展開も楽 |
- テンプレートの構成と中身を常に正しく管理する必要がある - コピー時のプレースホルダ置換処理がやや複雑になる可能性 |
実装に必要な箇所を確認しよう
さて、実装方式は決定しました。続いて、具体的な記載か所について整理しましょう。VS Code拡張機能のプロジェクトには、次のような役割を持つファイル/ディレクトリがあります。
ファイル/ディレクトリ | 役割 | このツールでの利用内容 |
---|---|---|
package.json |
拡張機能のメタ情報、ユーザーが実行できるコマンドの定義 | プロジェクトテンプレート生成用コマンドを登録 |
src/extension.ts |
拡張機能のメイン処理 | コマンド実行時にテンプレートをコピーする処理を書く |
template/ |
コピー元のテンプレート一式 | 標準ディレクトリ構成とファイルを格納 |
tsconfig.json |
TypeScriptの設定 | 特に変更なし(必要に応じて後で調整) |
具体的な作業
-
package.json
にコマンド登録- ユーザーが VS Code から「Cプロジェクトを作る!」と実行できるようにコマンドを追加。
-
extension.ts でコマンドの処理を書く
-
ユーザーがコマンドを実行したら、テンプレートをコピーする処理を呼び出す。
-
コピー時にプロジェクト名などのプレースホルダを置換する(※あとでやる)。
-
-
テンプレート(template/)を作る
-
標準ディレクトリ(src、include、test、outputなど)を含むフォルダ構成。
-
最低限のlaunch.jsonとMakefile、tasks.json をtemplateに配置する。
-
-
(任意)ユーザーにコピー先を選択させるUIを用意
-
コマンドパレットから「どの場所にプロジェクトを作りますか?」とフォルダ選択ダイアログを表示。
これで、ユーザーがコマンドを実行すると:
- template/ → 選択したフォルダに複製
- launch.jsonとMakefile → 設定済み状態で配置
され、すぐに利用可能なC言語プロジェクトを作成してくれます。
コードを書いてみよう
いよいよ、拡張機能のコードを書いてみましょう。
今回はテンプレートのコピーを行うコマンドを作っていきます。
-
package.json
にコマンドを登録
まずは事項するコマンドをpackage.jsonに登録します。<<前略>> "contributes": { "commands": [ { "command": "cProjectBootstrapper.createProject", "title": "Cプロジェクトを作成する" } ] }, <<後略>>
この設定を施すことで、VS Codeのコマンドパレットにプログラムが登録され、「表示」⇒「コマンドパレット」(Ctrl+Shift+P)で表示されるようになります。
-
extension.ts
にコマンド処理を書く
次に、実際の処理(テンプレートのコピー)をextension.tsに書きます。<<前略>> const templateOptions = [ { label: 'simple', description: 'シンプル版(最小構成)' }, { label: 'with-test', description: 'テスト付き構成' }, { label: 'advanced', description: '高度な構成' } ]; const selected = await vscode.window.showQuickPick(templateOptions, { placeHolder: '作成するプロジェクトテンプレートを選んでください' }); if (!selected) { vscode.window.showWarningMessage('テンプレートが選択されませんでした'); return; } const folders = await vscode.window.showOpenDialog({ canSelectFolders: true, openLabel: 'このフォルダに作る' }); if (!folders || folders.length === 0) { vscode.window.showWarningMessage('フォルダが選択されませんでした'); return; } const targetDir = folders[0].fsPath; const extensionPath = vscode.extensions.getExtension('yourpublisher.cProjectBootstrapper')?.extensionPath; //テンプレートを選択したタイプに切り替え const templateDir = path.join(extensionPath || '', 'template', selected.label); try { copyTemplate(templateDir, targetDir); vscode.window.showInformationMessage('Cプロジェクト(' + selected.label + ')を作成しました!'); } catch (error) { vscode.window.showErrorMessage('コピーに失敗しました: ' + error); } <<後略>>
-
テンプレートコピー関数の追加
テンプレートを再帰的にコピーする関数も用意しておきます。function copyTemplate(src: string, dest: string) { if (!fs.existsSync(src)) throw new Error('テンプレートが存在しません'); fs.readdirSync(src).forEach(item => { const srcPath = path.join(src, item); const destPath = path.join(dest, item); if (fs.lstatSync(srcPath).isDirectory()) { if (!fs.existsSync(destPath)) fs.mkdirSync(destPath); copyTemplate(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } });
これで、template/{simple,with-test,advanced}フォルダに用意したディレクトリ構成とファイル(launch.json、Makefileなど)が、選択フォルダにコピーされるようになります
現在の開発プロジェクトのディレクトリ状態をまとめます。
your-extension-root/ ├── .vscode/ │ └── launch.json(あれば) ├── node_modules/ ├── out/ ├── src/ │ └── extension.ts ← メインコード(テンプレ選択の処理や再帰コピーを記載) ├── template/ │ ├── simple/ ←コピー元のディレクトリを変更することでバリエーションを持たせる │ │ ├── Makefile │ │ ├── src/ │ │ ├── include/ │ │ └── test/ │ ├── with-test/ │ │ ├── Makefile │ │ ├── src/ │ │ ├── include/ │ │ └── test/ │ └── advanced/ │ ├── Makefile │ ├── src/ │ ├── include/ │ └── test/ ├── package.json ← コマンド定義と拡張機能情報 ├── tsconfig.json ├── package-lock.json ├── README.md └── .gitignore
テンプレートを作成する
動作確認用のテンプレートを作成します。
内容はgithubで公開していますので、そちらを利用されることをお奨めします。
なお、今回は動作確認用のため、sampleのみを対象とし、with-test/advancedはsampleと同内容とします。
テンプレートサンプル
launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug App",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/output/bin/app",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"preLaunchTask": "debugbuild",
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "debugbuild",
"type": "shell",
"command": "make debugbuild",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"]
}
]
}
Makefile
CC = gcc
CFLAGS = -Wall -Wextra -Iinclude
TARGET = output/bin/app
SRC = $(wildcard src/*.c)
all: build
build:
mkdir -p output/bin
$(CC) $(CFLAGS) $(SRC) -o $(TARGET)
debugbuild:
mkdir -p output/bin
$(CC) $(CFLAGS) -g $(SRC) -o $(TARGET)
clean:
rm -f $(TARGET)
main.c
#include <stdio.h>
#include "example.h"
int main(void) {
printf("Hello from simple template!\n");
return 0;
}
example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
void dummy_function();
#endif
実際に動かしてみよう
作成した拡張機能をパッケージする
> vsce package
WARNING A 'repository' field is missing from the 'package.json' manifest file.
Use --allow-missing-repository to bypass.
Do you want to continue? [y/N] y
WARNING LICENSE, LICENSE.md, or LICENSE.txt not found
Do you want to continue? [y/N] y
2度の警告に「y」で応えるとc-project-bootstrapper-1.0.0.vsixが生成されます。
拡張機能のインストール
-
VS Codeを起動し、拡張機能をポイントする。①
-
「…」をクリックする。②
ツールを使ってCプロジェクトを作成する
実際にプロジェクトを作ってみましょう。まだこのツールは不完全で、事前にプロジェクトディレクトリ(親ディレクトリ)だけは作成しなければなりません。事前に作成しておいてください。
- 表示⇒コマンドパレットを選択(ctrl+shift+pを押下)
- 検索ダイアログを利用して「Cプロジェクトを作成」を探し、選択する。
- プロジェクトテンプレートを選択。(ここでは「simple」を選択)
- 事前に作成したプロジェクトフォルダを選択し、「このフォルダに作る」ボタンを押下
デバッグを試す
早速、使ってみましょう。
作成したプロジェクトディレクトリ(親ディレクトリ)をVS Codeで開きます。
1.拡張機能を選択①
2.Debug APPを選択②
3.デバッガが動作して、main.cの4行目で処理がとまります。
まとめと次へのヒント
今回は、VS Code拡張機能で「Cプロジェクトひな型」を作るツールの設計と実装方針を固め、
さらに最小限のコードを書いてテンプレートコピーの自動化に成功しました。
ここまでの重要ポイントは次の3つ:
-
VS Codeの機能(launch.json、tasks.json)とビルドの仕組み(Makefile)の関係を理解
→ これが分かれば「なぜ標準化が必要か」も腑に落ちます。 -
launch.jsonとMakefileの「毎回同じ設定」の煩雑さを解消する方針を立てた
→ 人間の作業は不安定なので、作業の均質化=標準化→自動化の流れが必須。 -
テンプレートコピー型の実装方式(案2)を採用
→ 長期的に使いやすく、今後の改善・拡張もやりやすい方式です。
「標準化→自動化→楽になる」 このサイクルのスタート地点に立てました。
さて、今回雛形ディレクトリのコピーはできましたが、よく考えたらまだ改善の余地はありそうです。
次回は機能改善をしながら、より使いやすいツールを目指していこうと思いますので、お楽しみに!!
-
変更容易性。例えば標準ディレクトリ構成時にツール自体の変更をあまり修正することなく対応できる等。追加の要望に応え易いか否か ↩