エグゼクティブサマリー
Goで作ったWindows向けの小さなexeツールを配布するとき、右クリックで作る通常のショートカットと、mklinkで作るシンボリックリンクは別物でした。
特に、exe起動時にログフォルダや設定フォルダを作るようなツールでは、相対パスやカレントディレクトリを基準にすると、想定と違う場所にフォルダが作られることがあります。
今回試した範囲では、mklinkで作ったexeリンクからGoのos.Executable()を呼ぶと、リンク元のパスが取得できました。
結論
Go製ツールで「exeが起動された場所」を基準にしたいなら、まずはos.Executable()で取得したパスを使うのがよさそうです。
右クリックで作るショートカットは.lnkファイルで、アプリから見ると「ショートカット自身の場所」ではなく、リンク先exeや作業フォルダの影響を受けます。
一方、mklinkで作るファイルシンボリックリンクは、ファイルシステム上ではその場所にexeがあるように見えます。
今回の検証では、以下のようにC:\git\test.exeから起動した場合、Go側でもC:\git\test.exeが取得できました。
C:\git>test.exe
C:\git\test.exe
ただし、Goの公式ドキュメントでは、シンボリックリンク経由で起動した場合にリンク側パスが返るか実体側パスが返るかはOSに依存し得るとされています。
背景
Goで小さな業務ツールを作って配布するとき、exeの隣にlogsやconfigのようなフォルダを作りたくなることがあります。
たとえば、こんな構成です。
tool.exe
logs\
config\
ただ、ここで何も考えずに相対パスでフォルダを作ると、exeの場所ではなく、カレントディレクトリ側に作られることがあります。
ショートカット起動の場合、このカレントディレクトリはショートカットの「作業フォルダー」の設定に左右されます。
そのため、ユーザーはexeを起動したつもりでも、ログや設定ファイルが思った場所に出ない、ということが起きます。
補足:右クリックのショートカットは.lnk
Windowsで右クリックから作るショートカットは、基本的に.lnkファイルです。
これはファイルシステム上のリンクというより、Windows Shellがリンク先、作業ディレクトリ、引数、アイコンなどを保持して起動するためのファイルです。
つまり、アプリ側から見ると「どの.lnkから起動されたか」を自然に知るものではありません。
やったこと
Goで、自分自身のexeパスを表示するだけのプログラムを作りました。
package main
import (
"fmt"
"os"
)
func main() {
exePath, err := os.Executable()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(exePath)
}
ビルドします。
go build test.go -o test.exe
その後、実体を別フォルダ側に置き、C:\git\test.exeとしてmklinkを作りました。
mklink C:\git\test.exe C:\demo\tools\test.exe
作成結果は以下です。
C:\git\test.exe <<===>> C:\demo\tools\test.exe のシンボリック リンクが作成されました
そして、C:\gitから起動しました。
C:\git>test.exe
C:\git\test.exe
今回の環境では、Goのos.Executable()はmklink側の入口パスを返しました。
補足:mklinkとショートカットの違い
今回の整理は以下です。
| 種類 | 作り方 | 特徴 |
|---|---|---|
| 通常のショートカット | 右クリックから作成 |
.lnkファイル。リンク先や作業フォルダーを持つ |
| ファイルシンボリックリンク | mklink link.exe target.exe |
ファイルシステム上でリンク先を参照する |
| ディレクトリシンボリックリンク | mklink /d linkdir targetdir |
ディレクトリ用のシンボリックリンク |
| ハードリンク | mklink /h link.exe target.exe |
同じファイル実体に別名を付ける。元・先の概念が薄い |
| ジャンクション | mklink /j linkdir targetdir |
ディレクトリ用。exe単体には使えない |
/jはディレクトリジャンクションなので、exe単体には使えません。
exe単体をリンクしたい場合は、通常のmklinkを使います。
mklink C:\git\test.exe C:\path\to\real\test.exe
最終形
自分の用途では、通常のショートカットだけに頼るより、mklinkで入口パスを固定するほうが扱いやすそうでした。
たとえば、ユーザーには以下のパスを起動してもらいます。
C:\git\mytool.exe
実体は別の場所に置きます。
C:\tools\mytool\versions\1.0.0\mytool.exe
リンクはこうです。
mklink C:\git\mytool.exe C:\tools\mytool\versions\1.0.0\mytool.exe
Go側では、os.Executable()で取得したパスのディレクトリを基準にします。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
exePath, err := os.Executable()
if err != nil {
fmt.Println("error:", err)
return
}
exeDir := filepath.Dir(exePath)
logDir := filepath.Join(exeDir, "logs")
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Println("mkdir error:", err)
return
}
fmt.Println("exePath:", exePath)
fmt.Println("logDir :", logDir)
}
この形なら、少なくとも今回の検証環境では、C:\git\mytool.exeを入口として扱えます。
補足:本番利用時の注意点
os.Executable()は便利ですが、シンボリックリンク経由の場合の挙動は環境差があり得ます。
そのため、本番で使うなら最低限以下は試しておいたほうがよいです。
# 絶対パスで起動
C:\git\test.exe
# カレントディレクトリから起動
cd C:\git
.\test.exe
# PATH経由で起動
test.exe
また、ログや設定ファイルをexe直下に置く設計は、更新時に少し面倒になります。
配布や更新まで考えるなら、以下のように分けるほうが安全です。
本体:
%LOCALAPPDATA%\Company\MyTool\versions\1.0.0\mytool.exe
ログ:
%LOCALAPPDATA%\Company\MyTool\logs\
設定:
%APPDATA%\Company\MyTool\config.json
exeの隣に何かを作る設計は分かりやすいですが、ツール更新や複数ユーザー利用を考えると、ログや設定は別ディレクトリに逃がしたほうが無難です。
まとめ
今回の気づきは、右クリックのショートカットとmklinkはまったく別物だということです。
右クリックのショートカットは、あくまで.lnkという起動用ファイルです。
一方でmklinkは、ファイルシステム上のリンクとして扱われます。
Go製ツールで「exeの場所を基準にログやフォルダを作りたい」ときは、カレントディレクトリではなく、os.Executable()から取得したパスを基準にするのがよさそうです。
ただし、シンボリックリンク経由の挙動は環境差があり得るため、実際に配布する環境で検証してから使うのが安全です。