2025-11-04 2025-11-04

概要

PythonのWebアプリのデスクトップアプリ化(改良版)で作成したwebviewの部分をGoで書き直して見る。

やっている事は上記の記事(のpython版のwebview)とほぼ同じ

  • 「起動中」メッセージを表示する
  • 起動するサーバexeが信頼出来るものかどうかチェック
  • サーバ(exe)を子プロセスとして起動
  • 対象のURLをWebviewに表示する

目次

ソース

go.mod

module go_webview_sample

go 1.23.4

require (
	github.com/pelletier/go-toml/v2 v2.2.4
	github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
)

main.go

package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strings"
	"time"

	"github.com/pelletier/go-toml/v2"

	webview "github.com/webview/webview_go"
)

// 許可するサーバー実行ファイルの証明書サムプリント
var ALLOWED_THUMB_PRINTS = []string{
	"81CD829A58A13480C5...",  // 実行を許可するexeの証明書サムプリント
}

type ServerInfo struct {
	URL        string
	Host       string
	Port       string
	Path       string
	Env        map[string]string
	TempDir    string
	Cmd        *exec.Cmd
	TimeoutSec int
}

// サーバー実行ファイル名取得
func getServerExecutableName() string {
	fileName := "myapp_server"
	if runtime.GOOS == "windows" {
		fileName += ".exe"
	}
	return fileName
}

// サーバー実行ファイル 及び 設定ファイルのパス取得
func getServerPath(path string) (string, []string) {

	// サーバー実行ファイルのパス取得
	serverPath := getServerExecutablePath(path)

	// 設定ファイルのパス取得
	configFilePath := getServerConfigPaths(serverPath)

	return serverPath, configFilePath
}

// サーバー実行ファイルのパス取得
func getServerExecutablePath(path string) string {
	if path != "" {
		if _, err := os.Stat(path); err == nil {
			return path
		}
		return path
	}
	if cwd, err := os.Getwd(); err == nil {
		result := fmt.Sprintf("%s/%s", cwd, getServerExecutableName())
		if _, err := os.Stat(result); err == nil {
			return result
		}
	}

	return ""
}

// 設定ファイルのパス取得
// 以下の順に設定ファイル(toml)を読み込み環境変数を取得する(後勝ち)
//  1. ホームディレクトリの .streamlit/config.toml
//  2. exe と同じディレクトリの .streamlit/config.toml
//     または カレントディレクトリの .streamlit/config.toml (開発/テスト用)
func getServerConfigPaths(path string) []string {

	if path == "" {
		return []string{}
	}

	configPaths := []string{}
	if homeDir, err := os.UserHomeDir(); err == nil {
		filePath := fmt.Sprintf("%s/.streamlit/config.toml", homeDir)
		if _, err := os.Stat(filePath); err == nil {
			fmt.Printf("Found config file in home directory: %s\n", filePath)
			configPaths = append(configPaths, filePath)
		}
	}
	exeDir := ""
	if absPath, err := exec.LookPath(path); err == nil {
		exeDir = strings.TrimSuffix(absPath, "/"+regexp.MustCompile(`[^/]+$`).FindString(absPath))
	} else {
		exeDir = "."
	}
	filePath := fmt.Sprintf("%s/.streamlit/config.toml", exeDir)
	if _, err := os.Stat(filePath); err == nil {
		fmt.Printf("Found config file in executable directory: %s\n", filePath)
		configPaths = append(configPaths, filePath)
	} else {
		// 開発/テスト用にカレントディレクトリも確認
		if cwd, err := os.Getwd(); err == nil {
			filePath := fmt.Sprintf("%s/.streamlit/config.toml", cwd)
			if _, err := os.Stat(filePath); err == nil {
				fmt.Printf("Found config file in current working directory: %s\n", filePath)
				configPaths = append(configPaths, filePath)
			}
		}
	}
	return configPaths
}

// 設定ファイルを読み込み環境変数として取得する
func loadServerEnv(configFilePath []string) map[string]string {

	serverEnv := map[string]string{}
	for _, path := range configFilePath {

		// ファイルが存在しなければスキップ
		if _, err := os.Stat(path); os.IsNotExist(err) {
			continue
		}

		// ファイル読み込み 及び パース
		docBytes, err := os.ReadFile(path)
		if err != nil {
			panic(err)
		}
		doc := string(docBytes)
		var cfg map[string]interface{}
		err = toml.Unmarshal([]byte(doc), &cfg)
		if err != nil {
			panic(err)
		}

		fmt.Printf("Loaded config file: %s\n", path)
		for k, v := range cfg {
			envName := fmt.Sprintf("STREAMLIT_%s", strings.ToUpper(strings.ReplaceAll(k, ".", "_")))
			// 値がmapでない場合は無視
			if _, ok := v.(map[string]interface{}); !ok {
				continue
			}
			// キー名をキャメルケースから大文字スネークケースに変換して環境変数名を作成
			for k2, v2 := range v.(map[string]interface{}) {
				envVarName := fmt.Sprintf("%s_%s", envName, strings.ToUpper(regexp.MustCompile(`([a-z0-9])([A-Z])`).ReplaceAllString(k2, "${1}_${2}")))
				envVarValue := fmt.Sprintf("%v", v2)
				serverEnv[envVarName] = envVarValue
			}
		}
	}
	return serverEnv
}

// サーバーURLからホストとポートを抽出する
func getServerInfo(url string, path string) ServerInfo {

	serverUrl := url

	// サーバー実行ファイルのパス取得
	serverPath, configPath := getServerPath(path)

	// 設定ファイルを読み込み
	serverEnv := loadServerEnv(configPath)

	var host, port string
	if _, exist := serverEnv["STREAMLIT_SERVER_PORT"]; exist {
		host = "localhost" // 設定ファイルでポートが指定されている場合はホストを localhost に固定
		port = serverEnv["STREAMLIT_SERVER_PORT"]
		serverUrl = fmt.Sprintf("http://%s:%s", host, port)
	} else {
		// urlの頭から http:// または https:// を削除
		parsedUrl := regexp.MustCompile(`^https?://`).ReplaceAllString(serverUrl, "")
		// / 以降を削除
		hostAndPort := regexp.MustCompile(`/.*`).ReplaceAllString(parsedUrl, "")
		// ホストとポートを分割
		hostParts := strings.Split(hostAndPort, ":")
		if len(hostParts) == 2 {
			host = hostParts[0]
			port = hostParts[1]
		} else {
			host = hostAndPort
			if strings.HasPrefix(url, "https") {
				port = "443"
			} else {
				port = "80"
			}
		}
	}

	fmt.Printf("Server Info - Host: %s, Port: %s\n", host, port)
	if serverPath != "" {
		fmt.Printf("Server Executable Path: %s\n", serverPath)
		for k, v := range serverEnv {
			fmt.Printf("  Env: %s=%s\n", k, v)
		}
	}

	return ServerInfo{URL: serverUrl, Host: host, Port: port, Path: serverPath, Env: serverEnv, TimeoutSec: 30}
}

// 一時ディレクトリ作成
func createTempDir() string {

	tempDir, err := os.MkdirTemp("", "server_tmp_*")
	if err != nil {
		log.Fatal("Failed to create temp directory:", err)
	}
	fmt.Printf("Created temp directory: %s\n", tempDir)
	return tempDir
}

// サーバープロセス起動
func startServerProcess(w webview.WebView, info *ServerInfo) {

	fmt.Println("Using Server executable:", info.Path)

	// 一時ディレクトリ作成
	info.TempDir = createTempDir()

	// サーバープロセス起動準備
	cmd := exec.Command(info.Path)
	cmd.Stdout = nil
	cmd.Stderr = nil

	// 子プロセスの環境変数に TMP/TEMP を設定
	cmd.Env = append(os.Environ(),
		"TMP="+info.TempDir,
		"TEMP="+info.TempDir,
		"TMPDIR="+info.TempDir,
	)
	// 設定ファイルから取得した環境変数を追加
	for k, v := range info.Env {
		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
	}

	// プロセスグループ設定・起動
	setupProcessGroup(cmd)

	info.Cmd = cmd
}

// 指定ポートが開放されるまで待機
func waitForPort(info ServerInfo) bool {
	timeoutSec := info.TimeoutSec
	fmt.Printf("Waiting for port %s:%s to be available...\n", info.Host, info.Port)
	addr := net.JoinHostPort(info.Host, info.Port)
	start := time.Now()
	for {
		conn, err := net.Dial("tcp", addr)
		if err == nil {
			conn.Close()
			fmt.Printf("server is ready at %s\n", addr)
			return true
		}
		if time.Since(start).Seconds() > float64(timeoutSec) {
			fmt.Printf("server did not start within %d seconds\n", timeoutSec)
			return false
		}
		time.Sleep(300 * time.Millisecond)
	}
}

// サーバー起動を待ち、成功でNavigate / 失敗でタイムアウト画面
func startWait(w webview.WebView, info ServerInfo) {
	fmt.Printf("Waiting for server at %s:%s ...\n", info.Host, info.Port)
	go func() {
		if waitForPort(info) {
			w.Dispatch(func() { w.Navigate(info.URL) })
		} else {
			w.Dispatch(func() { w.SetHtml(timeoutHTML()) })
		}
	}()
}

// サーバープロセス終了時の後処理
func serverPostProcess(info *ServerInfo) {

	fmt.Println("serverPostProcess")
	fmt.Printf("TempDir: %s\n", info.TempDir)

	killProcessTree(info.Cmd)

	// 一時ディレクトリが残っていれば削除
	if info.TempDir != "" {
		if _, err := os.Stat(info.TempDir); err == nil {
			_ = os.RemoveAll(info.TempDir)
		}
	}
}

func main() {

	// 引数取得
	serverUrl := "http://localhost:8501"
	serverExePath := ""

	if len(os.Args) >= 2 {
		serverUrl = os.Args[1]
	}
	if len(os.Args) >= 3 {
		serverExePath = os.Args[2]
	}

	// WebView 起動: まずは起動中メッセージを表示
	debug := true
	w := webview.New(debug)
	defer w.Destroy()
	w.SetTitle("Sample WebView")
	w.SetSize(800, 600, webview.HintNone)
	//setupWindowStyle(w)  // タイトルバー色などの調整
	w.SetHtml(loadingHTML())

	// サーバー情報取得
	info := getServerInfo(serverUrl, serverExePath)

	// [起動] 押下時の関数をバインド(JS から window.forceStartApp() で呼び出し可)
	_ = w.Bind("forceStartApp", func() {
		//w.SetHtml(loadingHTML())
		w.Dispatch(func() { w.SetHtml(loadingHTML()) })
		startServerProcess(w, &info)
		startWait(w, info)
	})

	// [再試行] 押下時の関数をバインド(JS から window.reloadApp() で呼び出し可)
	_ = w.Bind("reloadApp", func() {
		//reloadApp(w, info)
		w.Dispatch(func() { w.SetHtml(loadingHTML()) })
		startWait(w, info)
	})

	go func() {

		// サーバーexe証明書チェック
		isCertified := false
		if info.Path != "" {
			isCertified = checkServerCertificate(info.Path)
		}

		if isCertified {
			// サーバプロセス起動
			startServerProcess(w, &info)
			startWait(w, info)
		} else if info.Path != "" && !isCertified {
			// 証明書エラー画面表示
			w.Dispatch(func() { w.SetHtml(certificateErrorHTML()) })
		} else {
			startWait(w, info)
		}
	}()

	// プロセス終了時は子プロセスも終了
	defer serverPostProcess(&info)

	// イベントループ開始
	w.Run()
}

webview_contents.go

package main

// 起動中に表示する簡易HTML
func loadingHTML() string {
	return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
  text-align: center;
  margin-top: 20%;
}
.loading {
  position: relative;
}
.loading::after {
  content: "";
  display: inline-block;
  width: 30px;
  text-align: left;
  animation: dots 1.5s steps(3, end) infinite;
}
@keyframes dots {
  0%  { content: ""; }
  25% { content: "."; }
  50% { content: ".."; }
  75% { content: "..."; }
}
</style>
</head>
<body>
<h1 class="loading">アプリケーションを起動しています</h1>
</body>
</html>`
}

// タイムアウト時に表示する簡易HTML
func timeoutHTML() string {
	return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { text-align: center; margin-top: 20%; }
.msg { color: #b00; }
.btn { margin-top: 16px; padding: 8px 16px; font-size: 14px; }
</style>
</head>
<body>
<div>
	<h1 class="msg">内部サーバーが規定時間内に起動しませんでした</h1>
	<button id="retryBtn" class="btn">再試行</button>
</div>
<script>
(function(){
  var btn = document.getElementById('retryBtn');
  if(btn){
    btn.addEventListener('click', function(){
      btn.disabled = true; btn.textContent = '再試行中…';
      if(window.reloadApp){ window.reloadApp(); }
    });
  }
})();
</script>
</body>
</html>`
}

func certificateErrorHTML() string {
	return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { text-align: center; margin-top: 20%; }
.msg { color: #b00; }
.btn { margin-top: 16px; padding: 8px 16px; font-size: 14px; }
</style>
</head>
<body>
<div>
	<h2 class="msg">内部サーバ証明書が存在しないか許可された証明書と一致しません</h2>
  <div>このまま起動しますか?</div>
	<button id="forceStartBtn" class="btn">起動する</button>
</div>
<script>
(function(){
  var btn = document.getElementById('forceStartBtn');
  if(btn){
    btn.addEventListener('click', function(){
      btn.disabled = true; btn.textContent = '起動中...';
      if(window.forceStartApp){ window.forceStartApp(); }
    });
  }
})();
</script>
</body>
</html>`
}

proc_unix.go

//go:build !windows

package main

import (
	"log"
	"os/exec"
	"syscall"
)

// プロセスグループ設定・起動
func setupProcessGroup(cmd *exec.Cmd) {
	// 子プロセスがさらに孫プロセスを生成するケースに備え、
	// Unix ではプロセスグループを切り出して終了時にまとめて Kill する。
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	err := cmd.Start()
	if err != nil {
		log.Fatal("Failed to start Streamlit:", err)
	}
}

// プロセスツリーごと終了
func killProcessTree(cmd *exec.Cmd) {
	if cmd == nil || cmd.Process == nil {
		return
	}
	if pgid, err := syscall.Getpgid(cmd.Process.Pid); err == nil {
		_ = syscall.Kill(-pgid, syscall.SIGKILL)
		return
	}
	_ = cmd.Process.Kill()
}

// サーバー実行ファイルの証明書サムプリントを確認
func checkServerCertificate(path string) bool {
	// 非対応
	return false
}

proc_windows.go

//go:build windows

package main

import (
	"fmt"
	"log"
	"os/exec"
	"strings"
	"syscall"
	"unsafe"
)

// Windows では Job Object に子プロセスをぶら下げ、
// ハンドル Close によりツリー全体を終了させる。

var jobObject syscall.Handle

const createNoWindow = 0x08000000 // CREATE_NO_WINDOW

// プロセスグループ設定・起動
func setupProcessGroup(cmd *exec.Cmd) {
	cmd.SysProcAttr = &syscall.SysProcAttr{
		HideWindow:    true,
		CreationFlags: createNoWindow,
	}
	err := cmd.Start()
	if err != nil {
		log.Fatal("Failed to start Streamlit:", err)
	}

	// Windows では Start 後に Job Object を割り当て、終了時にツリー全体を終了できるようにする。
	postStartManage(cmd)
}

func postStartManage(cmd *exec.Cmd) {
	if cmd == nil || cmd.Process == nil {
		return
	}
	// 既に作成済みならスキップ
	if jobObject != 0 {
		return
	}
	h, err := createKillOnCloseJob()
	if err != nil {
		return
	}
	// プロセスハンドルを開く
	const PROCESS_ALL_ACCESS = 0x1F0FFF
	ph, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, false, uint32(cmd.Process.Pid))
	if err != nil {
		syscall.CloseHandle(h)
		return
	}
	if !assignProcessToJobObject(h, ph) {
		syscall.CloseHandle(ph)
		syscall.CloseHandle(h)
		return
	}
	syscall.CloseHandle(ph)
	jobObject = h
}

func killProcessTree(cmd *exec.Cmd) {
	// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE により配下プロセスをまとめて終了する。
	if jobObject != 0 {
		syscall.CloseHandle(jobObject)
		jobObject = 0
		return
	}
	if cmd != nil && cmd.Process != nil {
		_ = cmd.Process.Kill()
	}
}

// --- Win32 API bindings ---

var (
	modkernel32                  = syscall.NewLazyDLL("kernel32.dll")
	procCreateJobObjectW         = modkernel32.NewProc("CreateJobObjectW")
	procSetInformationJobObject  = modkernel32.NewProc("SetInformationJobObject")
	procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
	moddwmapi                    = syscall.NewLazyDLL("dwmapi.dll")
	procDwmSetWindowAttribute    = moddwmapi.NewProc("DwmSetWindowAttribute")
)

const (
	jobObjectInfoExtendedLimit   = 9
	jobObjectLimitKillOnJobClose = 0x00002000
)

type ioCounters struct {
	ReadOperationCount  uint64
	WriteOperationCount uint64
	OtherOperationCount uint64
	ReadTransferCount   uint64
	WriteTransferCount  uint64
	OtherTransferCount  uint64
}

type jobObjectBasicLimitInformation struct {
	PerProcessUserTimeLimit int64
	PerJobUserTimeLimit     int64
	LimitFlags              uint32
	MinimumWorkingSetSize   uintptr
	MaximumWorkingSetSize   uintptr
	ActiveProcessLimit      uint32
	Affinity                uintptr
	PriorityClass           int32
	SchedulingClass         uint32
}

type jobObjectExtendedLimitInformation struct {
	BasicLimitInformation jobObjectBasicLimitInformation
	IoInfo                ioCounters
	ProcessMemoryLimit    uintptr
	JobMemoryLimit        uintptr
	PeakProcessMemoryUsed uintptr
	PeakJobMemoryUsed     uintptr
}

func createKillOnCloseJob() (syscall.Handle, error) {
	r1, _, e1 := procCreateJobObjectW.Call(0, 0)
	if r1 == 0 {
		return 0, e1
	}
	h := syscall.Handle(r1)
	var info jobObjectExtendedLimitInformation
	info.BasicLimitInformation.LimitFlags = jobObjectLimitKillOnJobClose
	size := uint32(unsafe.Sizeof(info))
	ok, _, e2 := procSetInformationJobObject.Call(
		uintptr(h),
		uintptr(jobObjectInfoExtendedLimit),
		uintptr(unsafe.Pointer(&info)),
		uintptr(size),
	)
	if ok == 0 {
		syscall.CloseHandle(h)
		return 0, e2
	}
	return h, nil
}

func assignProcessToJobObject(hJob syscall.Handle, hProcess syscall.Handle) bool {
	r1, _, _ := procAssignProcessToJobObject.Call(uintptr(hJob), uintptr(hProcess))
	return r1 != 0
}

// サーバーファイルの証明書チェック(PowerShell版)
func checkServerCertificate(path string) bool {

	// パス未指定は失敗扱い
	if strings.TrimSpace(path) == "" {
		return false
	}

	// PowerShell 経由で Authenticode 署名の Thumbprint を取得
	psPath := strings.ReplaceAll(path, "'", "''")
	psScript := fmt.Sprintf("(Get-AuthenticodeSignature -FilePath '%s').SignerCertificate.Thumbprint", psPath)

	getThumb := func(cmdName string) (string, error) {
		out, err := exec.Command(cmdName, "-NoProfile", "-NonInteractive", "-Command", psScript).Output()
		if err != nil {
			return "", err
		}
		t := strings.TrimSpace(string(out))
		if t == "" {
			return "", fmt.Errorf("empty thumbprint")
		}
		// 正規化: 大文字化・空白/コロン除去
		t = strings.ToUpper(t)
		t = strings.ReplaceAll(t, " ", "")
		t = strings.ReplaceAll(t, ":", "")
		return t, nil
	}

	// まず Windows 既定の PowerShell、次に pwsh を試す
	thumb, err := getThumb("powershell")
	if err != nil {
		if t2, err2 := getThumb("pwsh"); err2 == nil {
			thumb = t2
		} else {
			fmt.Printf("Failed to get certificate thumbprint: %v\n", err)
			return false
		}
	}

	// 取得したサムプリントが許可リストに存在するか確認
	for _, allowed := range ALLOWED_THUMB_PRINTS {
		a := strings.ToUpper(strings.TrimSpace(allowed))
		a = strings.ReplaceAll(a, " ", "")
		a = strings.ReplaceAll(a, ":", "")
		if thumb == a {
			return true
		}
	}

	fmt.Printf("Certificate thumbprint not allowed: %s\n", thumb)
	return false
}

ビルド

Linux/Mac環境でLinux/Mac用にビルドする

x_build.sh

#!/bin/bash

go build -o ./dist/myapp_go

Linux/Mac環境でWindows用にビルドする

x_build_win.sh

#!/bin/bash

set -euo pipefail

# Windows 用の exe をビルドします。
# 既定: amd64 向け、GUI(コンソール非表示)
# 使い方:
#   ./x_build_win.sh [amd64|arm64]

ARCH="${1:-amd64}"
case "$ARCH" in
  amd64|arm64) ;;
  *) echo "[ERR] ARCH は amd64 か arm64 を指定してください" >&2; exit 1 ;;
esac

OUT="./dist/myapp_go.exe"
echo "[INFO] Building Windows ${ARCH} -> ${OUT}"

# webview は cgo を利用するため、クロスコンパイルには mingw-w64 が必要
# 例 (macOS/Homebrew): brew install mingw-w64
# amd64:  x86_64-w64-mingw32-gcc
# arm64:  aarch64-w64-mingw32-gcc

TRIPLET=""
if [[ "$ARCH" == "amd64" ]]; then
  TRIPLET="x86_64-w64-mingw32"
else
  TRIPLET="aarch64-w64-mingw32"
fi

GCC_BIN="${TRIPLET}-gcc"
GPP_BIN="${TRIPLET}-g++"

if ! command -v "$GCC_BIN" >/dev/null 2>&1 || ! command -v "$GPP_BIN" >/dev/null 2>&1; then
  echo "[ERR] ${TRIPLET} のコンパイラが見つかりません。mingw-w64 をインストールし、PATH を通してください。" >&2
  echo "      例: brew install mingw-w64" >&2
  echo "          export PATH=\"$(brew --prefix 2>/dev/null || echo /opt/homebrew)/opt/mingw-w64/bin:\$PATH\"" >&2
  exit 1
fi

echo "[INFO] Using CC=$GCC_BIN, CXX=$GPP_BIN"

# -ldflags "-H windowsgui" でコンソールを非表示化
env GOOS=windows GOARCH="$ARCH" CGO_ENABLED=1 CC="$GCC_BIN" CXX="$GPP_BIN" \
  go build -ldflags "-H windowsgui -s -w" -o "$OUT" ./

echo "[OK] Build completed: ${OUT}"

Windows環境でWindows用にビルドする

x_build_win.bat

@echo off

REM ###################################################################
REM CGO_ENABLED=1 でビルドする必要がある為、以下のビルド環境が必要
REM ###################################################################
REM (1) Build Tools for Visual Studio のインストール
REM   (1-1) https://visualstudio.microsoft.com/ja/downloads/ からダウンロードし、
REM       「C++によるデスクトップ開発」にチェックを入れてインストール
REM   (1-2) cl.exe, link.exe に PATHを通す
REM       例) C:\Program Files (x86)\Microsoft Visual Studio\XXXX\BuildTools\VC\Tools\MSVC\XX.XX.XXXXX\bin\Hostx64\x64
REM (2) gccのインストール
REM 「Build Tools for Visual Studio」にはGCCは含まれていない為、別途 gcc をインストールする必要がある。
REM  ※Build Tools で提供されるのはMSVCという別のコンパイラ
REM   (2-1) msys2 (https://www.msys2.org) をインストール
REM   (2-2) MSYS2 UCRT64 ターミナル から「pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain」を実行
REM         ※MSYS2/MinGW-w64 UCRT64(ユニバーサルCランタイム)向け GCC/リンカをまとめて導入
REM   (2-3) gcc, g++ へのPATHを通しておく
REM       例) C:\msys64\ucrt64\bin
REM ###################################################################

set OUT="dist\myapp_go.exe"

REM set CONSOLE_FLAG="-H windowsgui"
set CONSOLE_FLAG=
set GOOS=windows
set GOARCH=amd64
set CGO_ENABLED=1
REM set CC=gcc
REM set CXX=g++

go build -ldflags %CONSOLE_FLAG% " -s -w" -o %OUT% ./

おまけ(PowerShell非依存の証明書チェック)

証明書をチェックをPowerShellに依存しない形でAIに書かせてみた。

proc_windows.go

var (
	modcrypt32                      = syscall.NewLazyDLL("crypt32.dll")
	procCryptQueryObject            = modcrypt32.NewProc("CryptQueryObject")
	procCryptMsgGetParam            = modcrypt32.NewProc("CryptMsgGetParam")
	procCryptMsgClose               = modcrypt32.NewProc("CryptMsgClose")
	procCertFindCertificateInStore  = modcrypt32.NewProc("CertFindCertificateInStore")
	procCertFreeCertificateContext  = modcrypt32.NewProc("CertFreeCertificateContext")
	procCertCloseStore              = modcrypt32.NewProc("CertCloseStore")
	procCertOpenStore               = modcrypt32.NewProc("CertOpenStore")
	procCertEnumCertificatesInStore = modcrypt32.NewProc("CertEnumCertificatesInStore")
)

const (
	CERT_QUERY_OBJECT_FILE                     = 1
	CERT_QUERY_OBJECT_BLOB                     = 2
	CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED       = 0x00000008
	CERT_QUERY_CONTENT_FLAG_PKCS7_UNSIGNED     = 0x00000010
	CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED = 0x00000020
	CERT_QUERY_FORMAT_FLAG_BINARY              = 0x00000002

	CMSG_SIGNER_INFO_PARAM = 6

	CERT_COMPARE_SUBJECT_CERT = 11
)

// CERT_FIND_SUBJECT_CERT = (CERT_COMPARE_SUBJECT_CERT<<16) | 0
const CERT_FIND_SUBJECT_CERT = (CERT_COMPARE_SUBJECT_CERT << 16) | 0

const (
	X509_ASN_ENCODING   = 0x00000001
	PKCS_7_ASN_ENCODING = 0x00010000
)

type cryptBlob struct {
	cbData uint32
	pbData *byte
}

type certNameBlob = cryptBlob

type cryptAlgorithmIdentifier struct {
	pszObjId   *byte
	Parameters cryptBlob
}

type cryptAttrBlob = cryptBlob

type cryptAttribute struct {
	pszObjId *byte
	cValue   uint32
	rgValue  *cryptAttrBlob
}

type cryptAttributes struct {
	cAttr  uint32
	rgAttr *cryptAttribute
}

type cmsgSignerInfo struct {
	dwVersion               uint32
	Issuer                  certNameBlob
	SerialNumber            cryptBlob
	HashAlgorithm           cryptAlgorithmIdentifier
	HashEncryptionAlgorithm cryptAlgorithmIdentifier
	EncryptedHash           cryptBlob
	AuthAttrs               cryptAttributes
	UnauthAttrs             cryptAttributes
}

type fileTime struct {
	dwLowDateTime  uint32
	dwHighDateTime uint32
}

type certInfo struct {
	dwVersion          uint32
	SerialNumber       cryptBlob
	SignatureAlgorithm cryptAlgorithmIdentifier
	Issuer             certNameBlob
	NotBefore          fileTime
	NotAfter           fileTime
	Subject            certNameBlob
}

type certContext struct {
	dwCertEncodingType uint32
	pbCertEncoded      *byte
	cbCertEncoded      uint32
	pCertInfo          *certInfo
	hCertStore         syscall.Handle
}

// サーバーファイルの証明書チェック(PowerShell非依存版)
func checkServerCertificate(path string) bool {

	// パス未指定は失敗扱い
	if strings.TrimSpace(path) == "" {
		fmt.Print("checkServerCertificate: false(1)")
		return false
	}

	// 署名は PE の Attribute Certificate Table に格納されるため、PKCS#7 BLOB を抽出
	f, err := os.Open(path)
	if err != nil {
		fmt.Print("checkServerCertificate: false(2)")
		return false
	}
	defer f.Close()
	data, err := io.ReadAll(f)
	if err != nil {
		fmt.Print("checkServerCertificate: false(3)")
		return false
	}
	if len(data) < 0x40 || string(data[:2]) != "MZ" {
		fmt.Print("checkServerCertificate: false(4)")
		return false
	}
	peOff := int(uint32(data[0x3C]) | uint32(data[0x3D])<<8 | uint32(data[0x3E])<<16 | uint32(data[0x3F])<<24)
	if peOff <= 0 || peOff+4 > len(data) || string(data[peOff:peOff+4]) != "PE\x00\x00" {
		fmt.Print("checkServerCertificate: false(5)")
		return false
	}
	coff := peOff + 4
	if coff+20 > len(data) {
		fmt.Print("checkServerCertificate: false(6)")
		return false
	}
	sizeOpt := int(uint32(data[coff+16]) | uint32(data[coff+17])<<8)
	optOff := coff + 20
	if optOff+sizeOpt > len(data) || sizeOpt < 2 {
		fmt.Print("checkServerCertificate: false(7)")
		return false
	}
	magic := uint16(data[optOff]) | uint16(data[optOff+1])<<8
	ddStart := 0
	switch magic {
	case 0x10b:
		ddStart = optOff + 96
	case 0x20b:
		ddStart = optOff + 112
	default:
		fmt.Print("checkServerCertificate: false(8)")
		return false
	}
	if ddStart+8*5 > optOff+sizeOpt {
		fmt.Print("checkServerCertificate: false(9)")
		return false
	}
	secDir := ddStart + 8*4
	va := uint32(data[secDir]) | uint32(data[secDir+1])<<8 | uint32(data[secDir+2])<<16 | uint32(data[secDir+3])<<24
	sz := uint32(data[secDir+4]) | uint32(data[secDir+5])<<8 | uint32(data[secDir+6])<<16 | uint32(data[secDir+7])<<24
	if va == 0 || sz < 8 || int(va+sz) > len(data) {
		fmt.Print("checkServerCertificate: false(10)")
		return false
	}
	off := int(va)
	end := off + int(sz)
	var pkcs7 []byte
	for off+8 <= end {
		dwLen := int(uint32(data[off]) | uint32(data[off+1])<<8 | uint32(data[off+2])<<16 | uint32(data[off+3])<<24)
		if dwLen < 8 || off+dwLen > end {
			break
		}
		certType := int(uint16(data[off+6]) | uint16(data[off+7])<<8)
		if certType == 2 { // WIN_CERT_TYPE_PKCS_SIGNED_DATA
			pkcs7 = data[off+8 : off+dwLen]
			break
		}
		off = (off + ((dwLen + 7) &^ 7))
	}
	if len(pkcs7) == 0 {
		fmt.Print("checkServerCertificate: false(11)")
		return false
	}

	// Open a temporary certificate store from the PKCS#7 blob
	var hStore syscall.Handle
	blob := cryptBlob{cbData: uint32(len(pkcs7))}
	if len(pkcs7) > 0 {
		blob.pbData = &pkcs7[0]
	}
	provider := []byte("PKCS7\x00")
	rStore, _, _ := procCertOpenStore.Call(
		uintptr(unsafe.Pointer(&provider[0])),
		uintptr(X509_ASN_ENCODING|PKCS_7_ASN_ENCODING),
		0,
		0,
		uintptr(unsafe.Pointer(&blob)),
	)
	hStore = syscall.Handle(rStore)
	if hStore == 0 {
		fmt.Print("checkServerCertificate: false(12)")
		return false
	}
	defer procCertCloseStore.Call(uintptr(hStore), 0)

	// Enumerate certificates in the PKCS#7 and compare thumbprints
	var pCtx uintptr
	for {
		rCtx, _, _ := procCertEnumCertificatesInStore.Call(uintptr(hStore), pCtx)
		if rCtx == 0 {
			break
		}
		// Free previous context if any
		if pCtx != 0 {
			procCertFreeCertificateContext.Call(pCtx)
		}
		pCtx = rCtx
		cc := (*certContext)(unsafe.Pointer(pCtx))
		if cc.pbCertEncoded == nil || cc.cbCertEncoded == 0 {
			continue
		}
		n := int(cc.cbCertEncoded)
		der := make([]byte, n)
		copy(der, (*[1 << 30]byte)(unsafe.Pointer(cc.pbCertEncoded))[:n:n])
		sum := sha1.Sum(der)
		thumb := strings.ToUpper(hex.EncodeToString(sum[:]))
		for _, a := range ALLOWED_THUMB_PRINTS {
			aa := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(a, " ", ""), ":", ""))
			if thumb == aa {
				// Free current context and return
				procCertFreeCertificateContext.Call(pCtx)
				fmt.Print("checkServerCertificate: OK")
				return true
			}
		}
	}
	// Free last context if not already freed
	if pCtx != 0 {
		procCertFreeCertificateContext.Call(pCtx)
	}
	fmt.Print("checkServerCertificate: false(16)")
	return false
}

おまけ(アイコンなど)

syso を作成して アイコンやアプリケーションの情報を設定する。

参考: https://github.com/josephspurrier/goversioninfo

アイコンファイルの作成

icoファイルを準備する

versioninfo.json の作成

参考

{
	"FixedFileInfo": {
		"FileVersion": {
			"Major": 1,
			"Minor": 2,
			"Patch": 3,
			"Build": 1
		}
	},
	"StringFileInfo":
	{
		"Comments": "",
		"CompanyName": "Sample Company",
		"FileDescription": "MyApp",
		"InternalName": "",
		"LegalCopyright": "Copyright (C) 2014-2025 Sample Company",
		"LegalTrademarks": "",
		"OriginalFilename": "myapp_go.exe",
		"PrivateBuild": "",
		"ProductName": "Sample MyApp",
		"ProductVersion": "v1.2.3.20251101_1",
		"SpecialBuild": ""
	}
}

syso の作成

go run github.com/hymkor/goversioninfo/cmd/goversioninfo@master -64 -icon=icon.ico -o versioninfo.syso

あとは、普通に go build するだけで、アイコン等が exe に設定される。


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-12-01 (月) 08:22:04 (4d)