🦕

Denoで単独実行できるプログラムを作る

2023/03/04に公開

WSLをそんなに使っていないのですが、ふと複数使うことはできるのだろうか?っと思い調べたところ以下の記事が見つかりました。

https://zenn.dev/takajun/articles/50044292cf6060

なるほどできるらしいということで読んでいくと、以下のような流れでできるようです。

  • 雛形となるWSL distroをエクスポート
  • さっきのファイルをインポートしてどこか設置する場所を指定する

エクスポートとインポート面倒くさい……。そういえば以前Denoで使ってみたい機能にcompile(Deno+書いたTypeScriptを一つのバイナリにまとめて実行可能ファイルにするらしい)があったな。対話式で複製とかできるコマンドを作ってみたら良いのでは?っと思ったので、作ってみることにしました。

今回はWindowsにしかない機能を使うのでWindowsのPowerShell向けで作ることにします。

コマンドを実行して標準出力の結果を得る

Denoでコマンドを実行するには --allow-run の権限が必要です。権限があれば以下のようなコードが動きます。

function outputToString(output: Promise<Uint8Array>) {
	return output.then((result: Uint8Array | Uint16Array) => {
		return new TextDecoder().decode(result);
	});
}

const process = Deno.run({
	cmd: ['wsl', '--list'],
	stdout: 'piped',
});

await process.status();
const stdout = await outputToString(process.output());
console.log(stdout);

これでいい感じに標準出力をプログラムで扱えるように取得できます。さぁ実行しましょう。

おっと文字化けですね。Uint8Arrayのデータを見ればわかりますが1文字のアルファベットの後に必ず0があります。つまり8bitや可変長のUTF-8ではない、UTF-16であると思われます。

return new TextDecoder('utf-16').decode(result);

これでコマンドの出力を得ることに成功しましたので、後はコマンドを作って実行して……みたいなのをいい感じにやっていけばコマンドを作れそうですね。

ユーザーの入力

今回のコマンドはとりあえず一覧の表示後にどれを複製する?削除する?と聞いていく対話形式にしようと思います。
その場合ユーザーの入力処理が必要ですが若干面倒です。

ですがDenoには良い機能があります。

https://deno.land/api@v1.29.2?s=prompt

これはブラウザに実装された prompt と同じです。メッセージを表示し、ユーザーから何か1行入力を受け付けるとその結果が得られます。非常に便利なのでこれを使っていきましょう。

とりあえず以下のような感じでコマンドを出力し入力をコマンド文字列に変換し、変換できない場合は全部終了の exit にする処理を書きます。

function nextCommand() {
	const next = prompt('What next? [list(l)/clone(c)/delete(d)/exit]', 'exit');
	switch (next) {
		case 'list':
		case 'l':
			return 'list';
		case 'clone':
		case 'c':
			return 'clone';
		case 'delete':
		case 'd':
			return 'delete';
	}
	return 'exit';
}

これでループさせればいい感じですし、名前の入力なんかもできそうで良いですね。

変数展開

今回はファイルのエクスポート・インポートが発生します。とりあえずユーザーディレクトリにWSLというディレクトリを作りたいところです。がそのパスはどうやって入手するかが問題です。

やり方の一つとして、例えばWindowsでは特定の文字列をパスに指定すると、例えば %UserProfile% は実行しているユーザーディレクトリに変換されたりします。これが使えると便利そうです。
ただし、この文字列をDenoで使っても意味はありません。このデータを展開する必要があります。

今回はPowerShellを使うので Write-Output $env:userprofile\\WSL とすれば C:\Users\ユーザー名\WSL の文字列を出力してくれます。このコマンド実行結果を取得すればよさそうです。

しかしPowerShellの文字コードは日本語だとShiftJISになったりしてるらしく文字コード関連はぐちゃぐちゃなようです。ヤメテ!!

ここでちょっと一工夫します。PowerShellで使う文字コードを指定する chcp というコマンドがあり、 chcp 65001 でUTF-8を指定できます。
このコマンドを実行すると変更結果が標準出力に出てしまうのでそれの対応を合わせると以下のようなコマンドを実行すればいい感じに結果が得られそうです。

powershell chcp 65001 > $null ; Write-Output $env:userprofile\\WSL

仕様文字コードをUTF-8にして標準出力は /dev/null 的なところに投げて消し、後は変数展開を echo 的なコマンドにやらせています。これをDenoで書くと以下のようになります。

const process = Deno.run({
	cmd: ['powershell', 'chcp', '65001', '>', '$null', ';', 'Write-Output', ファイルパス],
	stdout: 'piped',
});

これでディレクトリ名をいい感じに取得できそうですね。存在しない場合は作って、存在する場合は無視するような処理を書けばエクスポート周りもなんとかなりそうです。

async function createDirectory() {
	const process = Deno.run({
		cmd: ['powershell', 'chcp', '65001', '>', '$null', ';', 'Write-Output', this.distributionDirectory],
		stdout: 'piped',
	});

	const path = (await process.status().then(() => {
		return outputToString(process.output(), 'utf-8');
	})).trim();

	return Deno.mkdir(path).catch((error) => {
		if (error && error.name === 'AlreadyExists') {
			return;
		}
		throw error;
	});
}

単体実行ファイルとして出力

さて、実際の出力についてです。まずは今までの実行は以下のような感じでした。

deno run --allow-read --allow-write --allow-run ./src/main.ts

これを bin/wsldm.exe に出力する場合は以下のようなコマンドを実行します。

deno compile --unstable --allow-read --allow-write --allow-run --output bin/wsldm ./src/main.ts

まず目につくのは --unstable でしょう。まだ正式機能ではないのでこのオプションが必要です。
そして次は権限周りです。使う権限をこうやって指定すれば生成物を実行しても正常に実行できますが、権限を付与しない場合以下のようになります。

PS > .\bin\wsldm.exe
⚠️  ┌ Deno requests run access to "wsl".
   ├ Requested by `Deno.run()` API
   ├ Run again with --allow-run to bypass this prompt.
   └ Allow? [y/n] (y = yes, allow; n = no, deny) >

権限はちゃんと与えてからコンパイルしましょう。

完成品

主な変わった部分は以上の通りです。成果物はこちらです。

https://github.com/azulamb/wsldm

途中PowerShellやWSLの出力の罠に苦しめられたりしたものの、とりあえず一日でサクッと対話型CLIを作ることができました。
最低限しか作ってないもののWSLのエクスポート・インポート周りの辛さはなくなって嬉しいです。今後はもう少しWSLも使っていきたいですね。

その他

以下にDenoにpromptを実装した話があります。便利な機能をありがとうございます。

https://qiita.com/kt3k/items/2cf8c220c1c5552448ac

Discussion