Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11735] ファイルの概要

このコミットは、Goプロジェクトの継続的インテグレーション(CI)システムであるGo BuilderにおけるWindows環境でのビルドプロセスに関する複数の問題を修正することを目的としています。具体的には、コマンドの実行方法、ファイルパスの操作、および設定ファイルの検索ロジックをWindowsの慣習に合わせて調整し、クロスプラットフォーム互換性を向上させています。

コミット

commit 947ea6f750809302534bdb57afd2091767bd4038
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Thu Feb 9 14:52:01 2012 +1100

    gobuilder: fix windows builder
    
    Do not rewrite commands if they have .bash extnsion.
    Use path/filepath to manipulate file paths everywhere.
    Use all.bat on windows, not all.bash.
    Use HOMEDRIVE/HOMEPATH to find .gobuildkey on windows.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5630062

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/947ea6f750809302534bdb57afd2091767bd4038

元コミット内容

gobuilder: fix windows builder

Do not rewrite commands if they have .bash extnsion.
Use path/filepath to manipulate file paths everywhere.
Use all.bat on windows, not all.bash.
Use HOMEDRIVE/HOMEPATH to find .gobuildkey on windows.

変更の背景

GoプロジェクトのビルドシステムであるGo Builderは、様々なプラットフォームでGoのコードをビルドし、テストする役割を担っています。このコミットが作成された当時、Windows環境でのビルドプロセスにはいくつかの不具合が存在していました。

主な問題点は以下の通りです。

  1. .bash拡張子の扱い: Unix系システムではシェルスクリプトに.bash拡張子が付与されることがありますが、Windowsでは通常、バッチファイルには.bat.cmdが使用されます。Go Builderが.bash拡張子を持つコマンドを無条件にbashで実行しようとすると、Windows環境では問題が発生する可能性がありました。
  2. ファイルパスの操作: Goの標準ライブラリには、パス操作のためのpathパッケージとpath/filepathパッケージが存在します。pathパッケージは主にUnixスタイルのパス(/区切り)を扱うのに対し、path/filepathパッケージは実行環境のOSに応じたパス区切り文字(Windowsでは\、Unix系では/)を適切に処理します。Go Builderのコードベースでpathパッケージが不適切に使用されている箇所があり、Windows環境でのパス解決に問題を引き起こしていました。
  3. 設定ファイルの検索: Go Builderは.gobuildkeyという設定ファイルをユーザーのホームディレクトリから検索します。Unix系システムではHOME環境変数がホームディレクトリを指しますが、WindowsではHOMEDRIVEHOMEPATHの組み合わせがこれに相当します。HOMEのみに依存するロジックでは、Windowsで正しく設定ファイルを見つけられない問題がありました。

これらの問題により、Windows環境でのGo Builderの信頼性が低下し、ビルドが失敗する原因となっていました。このコミットは、これらのクロスプラットフォーム互換性の問題を解決し、Windows上でのGo Builderの安定稼働を保証することを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の技術的な前提知識が役立ちます。

1. Go Builder / Go Dashboard

Go Builderは、Go言語の公式プロジェクトにおける継続的インテグレーション(CI)システムの一部です。Goのソースコードが変更されるたびに、様々なオペレーティングシステム(Windows, Linux, macOSなど)やアーキテクチャ(amd64, armなど)の組み合わせで自動的にビルドとテストを実行します。その結果はGo Dashboard(build.golang.org)に表示され、Goプロジェクトの健全性を監視するために利用されます。Go Builderは、Goのリリースプロセスにおいて、安定したバイナリとテスト結果を保証する上で不可欠な役割を担っています。

2. Go言語におけるパス操作 (path vs path/filepath)

Go言語には、ファイルパスを操作するための2つの主要な標準パッケージがあります。

  • pathパッケージ: このパッケージは、スラッシュ (/) をパス区切り文字とするUnixスタイルのパスを扱います。これは、URLパスや、OSに依存しないパス表現が必要な場合に適しています。例えば、path.Join("dir", "file")は常に"dir/file"を返します。
  • path/filepathパッケージ: このパッケージは、実行中のオペレーティングシステムに固有のパス区切り文字(Windowsではバックスラッシュ \、Unix系ではスラッシュ /)を使用してパスを操作します。ファイルシステム上の実際のパスを扱う際には、このパッケージを使用することが推奨されます。例えば、Windows上でfilepath.Join("dir", "file")"dir\\file"を返し、Linux上で実行すると"dir/file"を返します。クロスプラットフォーム対応のアプリケーションを開発する際には、path/filepathの使用が不可欠です。

このコミットでは、pathパッケージからpath/filepathパッケージへの移行が主要な変更点の一つとなっています。

3. WindowsとUnix系OSにおけるシェルスクリプトと環境変数

  • シェルスクリプトの拡張子:
    • Unix系OS (Linux, macOSなど): シェルスクリプトは通常、拡張子を持たないか、.sh.bashなどの拡張子を持ちます。実行時には、ファイルの先頭にあるシバン(#!)行によって使用するインタプリタ(例: #!/bin/bash)が指定されます。
    • Windows: バッチファイルは通常、.batまたは.cmdの拡張子を持ちます。これらはWindowsのコマンドプロンプト(cmd.exe)によって直接実行されます。Git BashなどのUnix系シェル環境がインストールされている場合、.bashスクリプトも実行できますが、ネイティブなWindows環境では.batが一般的です。
  • ホームディレクトリを示す環境変数:
    • Unix系OS: ユーザーのホームディレクトリは通常、HOME環境変数によって指定されます。
    • Windows: ユーザーのホームディレクトリは、通常HOMEDRIVE(例: C:)とHOMEPATH(例: \Users\YourUsername)の組み合わせによって構成されます。HOME環境変数が設定されている場合もありますが、これはGitなどのツールによって設定されることが多く、ネイティブなWindowsアプリケーションではHOMEDRIVEHOMEPATHの組み合わせを使用するのがより堅牢です。

このコミットは、これらのOS間の違いを吸収し、Go BuilderがWindows環境で正しく動作するように修正しています。

技術的詳細

このコミットは、Go BuilderのWindows環境での動作を改善するために、以下の主要な技術的変更を導入しています。

  1. .bash拡張子を持つコマンドの自動書き換えの停止:

    • 以前のmisc/dashboard/builder/exec.goにはuseBashという関数が存在し、実行しようとするコマンドの最初の引数が.bashで終わる場合、そのコマンドの前にbashを自動的に追加していました。これは、Unix系システムで.bashスクリプトを明示的にbashで実行するためのロジックでした。
    • しかし、Windows環境では、.bash拡張子を持つファイルが必ずしもbashで実行されるとは限りません。また、Windowsのビルドプロセスでは.batファイルが使用されることが想定されており、この自動書き換えが不必要な、あるいは誤ったコマンド実行を引き起こす可能性がありました。
    • このコミットでは、useBash関数とその呼び出しが完全に削除されました。これにより、コマンドは指定された通りに実行され、Windows環境での.batスクリプトの実行が妨げられることがなくなりました。
  2. path/filepathパッケージの全面的使用:

    • misc/dashboard/builder/main.go内のファイルパス操作において、pathパッケージ(Unixスタイルのパスを扱う)からpath/filepathパッケージ(OS固有のパスを扱う)への移行が行われました。
    • 具体的には、flag.Stringで定義されるbuildrootbuildCmdのデフォルト値、gorootのパス結合、.gobuildkeyファイルのパス構築、作業ディレクトリのパス、ソースディレクトリのパス、ログファイルのパスなど、ほぼ全てのパス操作がfilepath.Joinを使用するように変更されました。
    • これにより、Windows環境ではパス区切り文字として\が、Unix系環境では/が自動的に使用されるようになり、クロスプラットフォームでのパス解決の堅牢性が大幅に向上しました。
  3. Windowsでのビルドコマンドの動的選択 (all.batの使用):

    • 以前は、ビルドコマンドとして./all.bashがハードコードされていました。
    • このコミットでは、main.goallCmdcleanCmdsuffixという新しいグローバル変数が導入されました。
    • defaultSuffix()関数が追加され、runtime.GOOSwindowsであれば.batを、そうでなければ.bashを返すようになりました。
    • これにより、allCmdはWindowsではall.batに、それ以外のOSではall.bashに解決されるようになり、buildCmdcleanCmdの実行時に適切なスクリプトが選択されるようになりました。
  4. Windowsでの.gobuildkeyファイルの検索ロジックの改善:

    • 以前は、.gobuildkeyファイルを検索する際にos.Getenv("HOME")に依存していました。
    • Windows環境ではHOME環境変数が常に設定されているとは限らず、またその値がユーザーのホームディレクトリを正確に指さない場合がありました。
    • このコミットでは、runtime.GOOS == "windows"の場合にos.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")を使用してホームディレクトリのパスを構築するように変更されました。これにより、Windowsの標準的な方法でユーザーのホームディレクトリを特定し、.gobuildkeyファイルを確実に検索できるようになりました。

これらの変更は、Go BuilderがWindows環境でより安定して動作するための基盤を強化し、クロスプラットフォーム開発におけるGoの強みをさらに引き出すものです。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は、misc/dashboard/builder/exec.gomisc/dashboard/builder/main.goの2つのファイルに集中しています。

misc/dashboard/builder/exec.go

  • run関数とrunLog関数から、argv = useBash(argv)の行が削除されました。
  • useBash関数自体がファイルから完全に削除されました。
--- a/misc/dashboard/builder/exec.go
+++ b/misc/dashboard/builder/exec.go
@@ -10,7 +10,6 @@ import (
  	"log"
  	"os"
  	"os/exec"
-	"strings"
  )
  
  // run is a simple wrapper for exec.Run/Close
@@ -18,7 +17,6 @@ func run(envv []string, dir string, argv ...string) error {
  	if *verbose {
  		log.Println("run", argv)
  	}
-	argv = useBash(argv)
  	cmd := exec.Command(argv[0], argv[1:]...)
  	cmd.Dir = dir
  	cmd.Env = envv
@@ -35,7 +33,6 @@ func runLog(envv []string, logfile, dir string, argv ...string) (string, int, er
  	if *verbose {
  		log.Println("runLog", argv)
  	}
-	argv = useBash(argv)
  
  	b := new(bytes.Buffer)
  	var w io.Writer = b
@@ -62,13 +59,3 @@ func runLog(envv []string, logfile, dir string, argv ...string) (string, int, er
  	}
  	return b.String(), 0, err
  }
-
-// useBash prefixes a list of args with 'bash' if the first argument
-// is a bash script.
-func useBash(argv []string) []string {
-	// TODO(brainman): choose a more reliable heuristic here.
-	if strings.HasSuffix(argv[0], ".bash") {
-		argv = append([]string{"bash"}, argv...)
-	}
-	return argv
-}

misc/dashboard/builder/main.go

  • pathパッケージのインポートが削除され、path/filepathパッケージが使用されるようになりました。
  • buildrootgorootbuildCmdの定義でpath.Joinfilepath.Joinに置き換えられました。
  • allCmdcleanCmdsuffixという新しいグローバル変数が追加され、defaultSuffix()関数が導入されました。
  • .gobuildkeyファイルのパスを決定するロジックが、Windows環境変数を考慮するように変更されました。
  • ビルド時の作業パス、ソースパス、ログファイルパスの生成にfilepath.Joinが使用されるようになりました。
  • ビルドコマンドの実行時に、buildCmdが絶対パスでない場合にsrcDirと結合するロジックが追加されました。
  • クリーンアップコマンドの実行時に、cleanCmdが使用されるようになりました。
  • リリースアーカイブのアップロードスクリプトのパスにfilepath.Joinが使用されるようになりました。
  • commitPoll関数内のpkgRootのパス結合にfilepath.Joinが使用されるようになりました。
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -12,7 +12,6 @@ import (
  	"io/ioutil"
  	"log"
  	"os"
-	"path"
  	"path/filepath"
  	"regexp"
  	"runtime"
@@ -49,12 +48,12 @@ type Builder struct {
  }
  
  var (
-	buildroot     = flag.String("buildroot", path.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
+	buildroot     = flag.String("buildroot", filepath.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
  	commitFlag    = flag.Bool("commit", false, "upload information about new commits")
  	dashboard     = flag.String("dashboard", "build.golang.org", "Go Dashboard Host")
  	buildRelease  = flag.Bool("release", false, "Build and upload binary release archives")
  	buildRevision = flag.String("rev", "", "Build specified revision and exit")
-	buildCmd      = flag.String("cmd", "./all.bash", "Build command (specify absolute or relative to go/src/)")
+	buildCmd      = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)")
  	external      = flag.Bool("external", false, "Build external packages")
  	parallel      = flag.Bool("parallel", false, "Build multiple targets in parallel")
  	verbose       = flag.Bool("v", false, "verbose")
@@ -64,6 +63,9 @@ var (
  	goroot      string
  	binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`)
  	releaseRe   = regexp.MustCompile(`^release\.r[0-9\-.]+`)
+	allCmd      = "all" + suffix
+	cleanCmd    = "clean" + suffix
+	suffix      = defaultSuffix()
  )
  
  func main() {
@@ -76,7 +78,7 @@ func main() {
  	if len(flag.Args()) == 0 && !*commitFlag {
  		flag.Usage()
  	}
-	goroot = path.Join(*buildroot, "goroot")
+	goroot = filepath.Join(*buildroot, "goroot")
  	builders := make([]*Builder, len(flag.Args()))
  	for i, builder := range flag.Args() {
  		b, err := NewBuilder(builder)
@@ -171,7 +173,13 @@ func NewBuilder(builder string) (*Builder, error) {
  	}
  
  	// read keys from keyfile
-	fn := path.Join(os.Getenv("HOME"), ".gobuildkey")
+	fn := ""
+	if runtime.GOOS == "windows" {
+		fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
+	} else {
+		fn = os.Getenv("HOME")
+	}
+	fn = filepath.Join(fn, ".gobuildkey")
  	if s := fn + "-" + b.name; isFile(s) { // builder-specific file
  		fn = s
  	}
@@ -257,7 +265,7 @@ func (b *Builder) buildHash(hash string) error {
  	log.Println(b.name, "building", hash)
  
  	// create place in which to do work
-	workpath := path.Join(*buildroot, b.name+"-"+hash[:12])
+	workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12])
  	if err := os.Mkdir(workpath, mkdirPerm); err != nil {
  		return err
  	}
@@ -269,16 +277,20 @@ func (b *Builder) buildHash(hash string) error {
  	}
  
  	// update to specified revision
-	if err := run(nil, path.Join(workpath, "go"), "hg", "update", hash); err != nil {
+	if err := run(nil, filepath.Join(workpath, "go"), "hg", "update", hash); err != nil {
  		return err
  	}
  
-	srcDir := path.Join(workpath, "go", "src")
+	srcDir := filepath.Join(workpath, "go", "src")
  
  	// build
-	logfile := path.Join(workpath, "build.log")
+	logfile := filepath.Join(workpath, "build.log")
+	cmd := *buildCmd
+	if !filepath.IsAbs(cmd) {
+		cmd = filepath.Join(srcDir, cmd)
+	}
  	startTime := time.Now()
-	buildLog, status, err := runLog(b.envv(), logfile, srcDir, *buildCmd)
+	buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd)
  	runTime := time.Now().Sub(startTime)
  	if err != nil {
  		return fmt.Errorf("%s: %s", *buildCmd, err)
@@ -314,15 +326,16 @@ func (b *Builder) buildHash(hash string) error {
  	if hash == releaseHash {
  		// clean out build state
-		if err := run(b.envv(), srcDir, "./clean.bash", "--nopkg"); err != nil {
-			return fmt.Errorf("clean.bash: %s", err)
+		cmd := filepath.Join(srcDir, cleanCmd)
+		if err := run(b.envv(), srcDir, cmd, "--nopkg"); err != nil {
+			return fmt.Errorf("%s: %s", cleanCmd, err)
  		}
  		// upload binary release
  		fn := fmt.Sprintf("go.%s.%s-%s.tar.gz", release, b.goos, b.goarch)
  		if err := run(nil, workpath, "tar", "czf", fn, "go"); err != nil {
  			return fmt.Errorf("tar: %s", err)
  		}
-		err := run(nil, workpath, path.Join(goroot, codePyScript),
+		err := run(nil, workpath, filepath.Join(goroot, codePyScript),
  			"-s", release,
  			"-p", codeProject,
  			"-u", b.codeUsername,
@@ -556,7 +569,7 @@ func commitPoll(key, pkg string) {
  	pkgRoot := goroot
  
  	if pkg != "" {
-		pkgRoot = path.Join(*buildroot, pkg)
+		pkgRoot = filepath.Join(*buildroot, pkg)
  		if !hgRepoExists(pkgRoot) {
  			if err := hgClone(repoURL(pkg), pkgRoot); err != nil {
  				log.Printf("%s: hg clone failed: %v", pkg, err)
@@ -719,3 +732,12 @@ func repoURL(importPath string) string {
  	}
  	return "https://code.google.com/p/\" + m[1]
  }
+
+// defaultSuffix returns file extension used for command files in
+// current os environment.
+func defaultSuffix() string {
+	if runtime.GOOS == "windows" {
+		return ".bat"
+	}
+	return ".bash"
+}

コアとなるコードの解説

misc/dashboard/builder/exec.goの変更

  • useBash関数の削除:
    • この関数は、コマンドが.bashで終わる場合に、そのコマンドの前にbashを付加して実行しようとするものでした。これはUnix系システムでは有効な場合がありますが、Windowsでは.batファイルが標準的な実行形式であり、.bash拡張子を持つファイルが必ずしもbashで実行されるとは限りません。
    • この関数を削除することで、Go Builderはコマンドをより直接的に、OSのデフォルトの関連付けに従って実行するようになります。これにより、Windows上でall.batのようなバッチファイルが正しく実行されるようになります。

misc/dashboard/builder/main.goの変更

  • pathからpath/filepathへの移行:

    • pathパッケージはUnixスタイルのパス(/区切り)を前提としており、Windowsのパス(\区切り)とは互換性がありません。
    • filepathパッケージは、実行中のOSに応じて適切なパス区切り文字を自動的に使用するため、クロスプラットフォーム対応のアプリケーションには必須です。
    • buildrootgorootworkpathsrcDirlogfilepkgRootなど、ファイルシステム上の実際のパスを扱う全ての箇所でfilepath.Joinを使用するように変更されたことで、Windows環境でのパス解決の不具合が解消されました。例えば、filepath.Join("C:\\", "Users", "gobuilder")はWindowsでC:\Users\gobuilderを生成し、Linuxで/Users/gobuilderを生成します。
  • ビルドコマンドの動的選択 (all.bat / all.bash):

    • defaultSuffix()関数が導入され、runtime.GOOS(実行中のOS)がwindowsであれば.batを、そうでなければ.bashを返すようになりました。
    • このsuffix変数を使ってallCmdcleanCmdが定義されるため、Windowsではall.batclean.batが、Unix系ではall.bashclean.bashが自動的に選択されます。
    • これにより、OSごとに異なるビルドスクリプトの拡張子を適切に処理できるようになり、Windowsでのビルドが正しく起動するようになりました。
  • .gobuildkeyファイルの検索ロジックの改善:

    • WindowsではHOME環境変数が常にユーザーのホームディレクトリを指すとは限りません。より信頼性の高い方法は、HOMEDRIVEHOMEPATH環境変数を組み合わせることです。
    • この変更により、Windows環境ではHOMEDRIVEHOMEPATHを結合してホームディレクトリのパスを構築し、そこに.gobuildkeyファイルを結合するようになりました。これにより、Windows上での設定ファイルの検索がより堅牢になりました。
  • ビルドコマンドの絶対パス解決:

    • buildCmdが相対パスで指定された場合(例: all.bat)、それがsrcDir(Goのソースコードルート)からの相対パスとして解釈されるように、filepath.Join(srcDir, cmd)というロジックが追加されました。これにより、ビルドコマンドが常に正しい場所から実行されることが保証されます。

これらの変更は、Go BuilderがWindows環境で直面していたパス解決、コマンド実行、および設定ファイル検索に関する根本的な問題を解決し、クロスプラットフォームでの一貫した動作を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語 pathパッケージのドキュメント: https://pkg.go.dev/path
  • Go言語 path/filepathパッケージのドキュメント: https://pkg.go.dev/path/filepath
  • Go言語 osパッケージのドキュメント (環境変数関連): https://pkg.go.dev/os
  • Go言語 runtimeパッケージのドキュメント (runtime.GOOS): https://pkg.go.dev/runtime
  • Windowsの環境変数 HOMEDRIVEHOMEPATH に関する情報 (一般的なWindowsのドキュメントやフォーラム)
  • Unix系OSにおけるシェルスクリプトのシバンに関する情報 (一般的なLinux/Unixのドキュメント)