github.com/bazelbuild/rules_go@v0.47.2-0.20240515105122-e7ddb9ea474e/go/tools/bzltestutil/chdir/init.go (about)

     1  // Copyright 2020 The Bazel Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package chdir provides an init function that changes the current working
    16  // directory to RunDir when the test executable is started by Bazel
    17  // (when TEST_SRCDIR and TEST_WORKSPACE are set).
    18  //
    19  // This hides a difference between Bazel and 'go test': 'go test' starts test
    20  // executables in the package source directory, while Bazel starts test
    21  // executables in a directory made to look like the repository root directory.
    22  // Tests frequently refer to testdata files using paths relative to their
    23  // package directory, so open source tests frequently break unless they're
    24  // written with Bazel specifically in mind (using go/runfiles).
    25  //
    26  // For this init function to work, it must be called before init functions
    27  // in all user packages.
    28  //
    29  // In Go 1.20 and earlier, the package initialization order was underspecified,
    30  // other than a requirement that each package is initialized after all its
    31  // transitively imported packages. We relied on the linker initializing
    32  // packages in the order their imports appeared in source, so we import
    33  // bzltestutil (and transitively, this package) from the generated test main
    34  // before other packages.
    35  //
    36  // In Go 1.21, the package initialization order was clarified, and the
    37  // linker implementation was changed. See
    38  // https://go.dev/ref/spec#Program_initialization or
    39  // https://go.dev/doc/go1.21#language.
    40  //
    41  // > Given the list of all packages, sorted by import path, in each step the
    42  // > first uninitialized package in the list for which all imported packages
    43  // > (if any) are already initialized is initialized. This step is repeated
    44  // > until all packages are initialized.
    45  //
    46  // To ensure this package is initialized before user code without injecting
    47  // edges into the dependency graph, we implement the following hack:
    48  //
    49  // 1. Add the prefix '+initfirst/' to this package's path with the 'importmap'
    50  //    attribute. '+' is the first allowed character that sorts higher than
    51  //    letters. Because we're using 'importmap' and not 'importpath', this
    52  //    package may be imported in .go files without the prefix.
    53  // 2. Put this init function in a separate package that only imports "os".
    54  //    Previously, this function was in bzltestutil, but bzltest util imports
    55  //    several other std packages may be get initialized later. For example,
    56  //    the "sync" package is initialized after a user package named
    57  //    "github.com/a/b" that only imports "os", and because bzltestutil imports
    58  //    "sync", it would get initialized even later. A user package that imports
    59  //    nothing may still be initialized before "os", but we assume "os"
    60  //    is needed to observe the current directory.
    61  package chdir
    62  
    63  // This package should import nothing other than "os"
    64  // and packages imported by "os" (run 'go list -deps os').
    65  import "os"
    66  
    67  var (
    68  	// Initialized by linker.
    69  	RunDir string
    70  
    71  	// Initial working directory.
    72  	TestExecDir string
    73  )
    74  
    75  const isWindows = os.PathSeparator == '\\'
    76  
    77  func init() {
    78  	var err error
    79  	TestExecDir, err = os.Getwd()
    80  	if err != nil {
    81  		panic(err)
    82  	}
    83  
    84  	// Check if we're being run by Bazel and change directories if so.
    85  	// TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent proxy.
    86  	testSrcDir, hasSrcDir := os.LookupEnv("TEST_SRCDIR")
    87  	testWorkspace, hasWorkspace := os.LookupEnv("TEST_WORKSPACE")
    88  	if hasSrcDir && hasWorkspace && RunDir != "" {
    89  		abs := RunDir
    90  		if !filepathIsAbs(RunDir) {
    91  			abs = filepathJoin(testSrcDir, testWorkspace, RunDir)
    92  		}
    93  		err := os.Chdir(abs)
    94  		// Ignore the Chdir err when on Windows, since it might have have runfiles symlinks.
    95  		// https://github.com/bazelbuild/rules_go/pull/1721#issuecomment-422145904
    96  		if err != nil && !isWindows {
    97  			panic("could not change to test directory: " + err.Error())
    98  		}
    99  		if err == nil {
   100  			os.Setenv("PWD", abs)
   101  		}
   102  	}
   103  }
   104  
   105  // filepathIsAbs is a primitive version of filepath.IsAbs. It handles the
   106  // cases we are likely to encounter but is not specialized at compile time
   107  // and does not support DOS device paths (\\.\UNC\host\share\...) nor
   108  // Plan 9 absolute paths (starting with #).
   109  func filepathIsAbs(path string) bool {
   110  	if isWindows {
   111  		// Drive-letter path
   112  		if len(path) >= 3 &&
   113  			('A' <= path[0] && path[0] <= 'Z' || 'a' <= path[0] && path[0] <= 'z') &&
   114  			path[1] == ':' &&
   115  			(path[2] == '\\' || path[2] == '/') {
   116  			return true
   117  		}
   118  
   119  		// UNC path
   120  		if len(path) >= 2 && path[0] == '\\' && path[1] == '\\' {
   121  			return true
   122  		}
   123  		return false
   124  	}
   125  
   126  	return len(path) > 0 && path[0] == '/'
   127  }
   128  
   129  // filepathJoin is a primitive version of filepath.Join. It only joins
   130  // its arguments with os.PathSeparator. It does not clean arguments first.
   131  func filepathJoin(base string, parts ...string) string {
   132  	n := len(base)
   133  	for _, part := range parts {
   134  		n += 1 + len(part)
   135  	}
   136  	buf := make([]byte, 0, n)
   137  	buf = append(buf, []byte(base)...)
   138  	for _, part := range parts {
   139  		buf = append(buf, os.PathSeparator)
   140  		buf = append(buf, []byte(part)...)
   141  	}
   142  	return string(buf)
   143  }