Skip to content

Commit 64759ee

Browse files
Lazy expand absolute path to avoid including them in the output (#4009)
**What type of PR is this?** Bug fix **What does this PR do? Why is it needed?** It fixes reproducibility issues reported in #3994 by lazy-expanding paths to absolute paths **Which issues(s) does this PR fix?** Fixes #3994
1 parent 29d4e5d commit 64759ee

File tree

7 files changed

+179
-49
lines changed

7 files changed

+179
-49
lines changed

go/tools/builders/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ filegroup(
6363
"ar.go",
6464
"asm.go",
6565
"builder.go",
66+
"cc.go",
6667
"cgo2.go",
6768
"compilepkg.go",
6869
"constants.go",

go/tools/builders/builder.go

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func main() {
5454
action = stdlib
5555
case "stdliblist":
5656
action = stdliblist
57+
case "cc":
58+
action = cc
5759
default:
5860
log.Fatalf("unknown action: %s", verb)
5961
}

go/tools/builders/cc.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
"strings"
10+
"syscall"
11+
)
12+
13+
func cc(args []string) error {
14+
cc := os.Getenv("GO_CC")
15+
if cc == "" {
16+
errors.New("GO_CC environment variable not set")
17+
}
18+
ccroot := os.Getenv("GO_CC_ROOT")
19+
if ccroot == "" {
20+
errors.New("GO_CC_ROOT environment variable not set")
21+
}
22+
normalized := []string{cc}
23+
normalized = append(normalized, args...)
24+
transformArgs(normalized, cgoAbsEnvFlags, func(s string) string {
25+
if strings.HasPrefix(s, cgoAbsPlaceholder) {
26+
trimmed := strings.TrimPrefix(s, cgoAbsPlaceholder)
27+
abspath := filepath.Join(ccroot, trimmed)
28+
if _, err := os.Stat(abspath); err == nil {
29+
// Only return the abspath if it exists, otherwise it
30+
// means that either it won't have any effect or the original
31+
// value was not a relpath (e.g. a path with a XCODE placehold from
32+
// macos cc_wrapper)
33+
return abspath
34+
}
35+
return trimmed
36+
}
37+
return s
38+
})
39+
if runtime.GOOS == "windows" {
40+
cmd := exec.Command(normalized[0], normalized[1:]...)
41+
cmd.Stdout = os.Stdout
42+
cmd.Stderr = os.Stderr
43+
return cmd.Run()
44+
} else {
45+
return syscall.Exec(normalized[0], normalized, os.Environ())
46+
}
47+
}

go/tools/builders/env.go

+35-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ var (
3535
cgoEnvVars = []string{"CGO_CFLAGS", "CGO_CXXFLAGS", "CGO_CPPFLAGS", "CGO_LDFLAGS"}
3636
// cgoAbsEnvFlags are all the flags that need absolute path in cgoEnvVars
3737
cgoAbsEnvFlags = []string{"-I", "-L", "-isysroot", "-isystem", "-iquote", "-include", "-gcc-toolchain", "--sysroot", "-resource-dir", "-fsanitize-blacklist", "-fsanitize-ignorelist"}
38+
// cgoAbsPlaceholder is placed in front of flag values that must be absolutized
39+
cgoAbsPlaceholder = "__GO_BAZEL_CC_PLACEHOLDER__"
3840
)
3941

4042
// env holds a small amount of Go environment and toolchain information
@@ -160,10 +162,33 @@ func (e *env) runCommandToFile(out, err io.Writer, args []string) error {
160162
return runAndLogCommand(cmd, e.verbose)
161163
}
162164

163-
func absEnv(envNameList []string, argList []string) error {
165+
// absCCCompiler modifies CGO flags to workaround relative paths.
166+
// Because go is having its own sandbox, all CGO flags should use
167+
// absolute paths. However, CGO flags are embedded in the output
168+
// so we cannot use absolute paths directly. Instead, use a placeholder
169+
// for the absolute path and we replace CC with this builder so that
170+
// we can expand the placeholder later.
171+
func absCCCompiler(envNameList []string, argList []string) error {
172+
err := os.Setenv("GO_CC", os.Getenv("CC"))
173+
if err != nil {
174+
return err
175+
}
176+
err = os.Setenv("GO_CC_ROOT", abs("."))
177+
if err != nil {
178+
return err
179+
}
180+
err = os.Setenv("CC", abs(os.Args[0])+" cc")
181+
if err != nil {
182+
return err
183+
}
164184
for _, envName := range envNameList {
165185
splitedEnv := strings.Fields(os.Getenv(envName))
166-
absArgs(splitedEnv, argList)
186+
transformArgs(splitedEnv, argList, func(s string) string {
187+
if filepath.IsAbs(s) {
188+
return s
189+
}
190+
return cgoAbsPlaceholder + s
191+
})
167192
if err := os.Setenv(envName, strings.Join(splitedEnv, " ")); err != nil {
168193
return err
169194
}
@@ -320,10 +345,16 @@ func abs(path string) string {
320345
// absArgs applies abs to strings that appear in args. Only paths that are
321346
// part of options named by flags are modified.
322347
func absArgs(args []string, flags []string) {
348+
transformArgs(args, flags, abs)
349+
}
350+
351+
// transformArgs applies fn to strings that appear in args. Only paths that are
352+
// part of options named by flags are modified.
353+
func transformArgs(args []string, flags []string, fn func(string) string) {
323354
absNext := false
324355
for i := range args {
325356
if absNext {
326-
args[i] = abs(args[i])
357+
args[i] = fn(args[i])
327358
absNext = false
328359
continue
329360
}
@@ -341,7 +372,7 @@ func absArgs(args []string, flags []string) {
341372
possibleValue = possibleValue[1:]
342373
separator = "="
343374
}
344-
args[i] = fmt.Sprintf("%s%s%s", f, separator, abs(possibleValue))
375+
args[i] = fmt.Sprintf("%s%s%s", f, separator, fn(possibleValue))
345376
break
346377
}
347378
}

go/tools/builders/stdlib.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,7 @@ You may need to use the flags --cpu=x64_windows --compiler=mingw-gcc.`)
158158
installArgs = append(installArgs, "-ldflags="+allSlug+strings.Join(ldflags, " "))
159159
installArgs = append(installArgs, "-asmflags="+allSlug+strings.Join(asmflags, " "))
160160

161-
// Modify CGO flags to use only absolute path
162-
// because go is having its own sandbox, all CGO flags must use absolute path
163-
if err := absEnv(cgoEnvVars, cgoAbsEnvFlags); err != nil {
161+
if err := absCCCompiler(cgoEnvVars, cgoAbsEnvFlags); err != nil {
164162
return fmt.Errorf("error modifying cgo environment to absolute path: %v", err)
165163
}
166164

go/tools/builders/stdliblist.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,7 @@ func stdliblist(args []string) error {
257257
}
258258
os.Setenv("CC", quotePathIfNeeded(abs(ccEnv)))
259259

260-
// Modify CGO flags to use only absolute path
261-
// because go is having its own sandbox, all CGO flags must use absolute path
262-
if err := absEnv(cgoEnvVars, cgoAbsEnvFlags); err != nil {
260+
if err := absCCCompiler(cgoEnvVars, cgoAbsEnvFlags); err != nil {
263261
return fmt.Errorf("error modifying cgo environment to absolute path: %v", err)
264262
}
265263

tests/integration/reproducibility/reproducibility_test.go

+92-39
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ func Test(t *testing.T) {
189189
defer wg.Done()
190190
cmd := bazel_testing.BazelCmd("build",
191191
"//:all",
192+
"--copt=-Irelative/path/does/not/exist",
193+
"--linkopt=-Lrelative/path/does/not/exist",
192194
"@io_bazel_rules_go//go/tools/builders:go_path",
193195
"@go_sdk//:builder",
194196
)
@@ -200,49 +202,92 @@ func Test(t *testing.T) {
200202
}
201203
wg.Wait()
202204

203-
// Hash files in each bazel-bin directory.
204-
dirHashes := make([][]fileHash, len(dirs))
205-
errs := make([]error, len(dirs))
206-
wg.Add(len(dirs))
207-
for i := range dirs {
208-
go func(i int) {
209-
defer wg.Done()
210-
dirHashes[i], errs[i] = hashFiles(filepath.Join(dirs[i], "bazel-bin"))
211-
}(i)
212-
}
213-
wg.Wait()
214-
for _, err := range errs {
215-
if err != nil {
205+
t.Run("Check Hashes", func(t *testing.T) {
206+
// Hash files in each bazel-bin directory.
207+
dirHashes := make([][]fileHash, len(dirs))
208+
errs := make([]error, len(dirs))
209+
wg.Add(len(dirs))
210+
for i := range dirs {
211+
go func(i int) {
212+
defer wg.Done()
213+
dirHashes[i], errs[i] = hashFiles(filepath.Join(dirs[i], "bazel-out/"), func(root, path string) bool {
214+
// Hash everything but actions stdout/stderr and the volatile-status.txt file
215+
// as they are expected to change
216+
return strings.HasPrefix(path, filepath.Join(root, "_tmp")) ||
217+
path == filepath.Join(root, "volatile-status.txt")
218+
})
219+
}(i)
220+
}
221+
wg.Wait()
222+
for _, err := range errs {
223+
if err != nil {
224+
t.Fatal(err)
225+
}
226+
}
227+
228+
// Compare dir0 and dir1. They should be identical.
229+
if err := compareHashes(dirHashes[0], dirHashes[1]); err != nil {
216230
t.Fatal(err)
217231
}
218-
}
219232

220-
// Compare dir0 and dir1. They should be identical.
221-
if err := compareHashes(dirHashes[0], dirHashes[1]); err != nil {
222-
t.Fatal(err)
223-
}
233+
// Compare dir0 and dir2. They should be different.
234+
if err := compareHashes(dirHashes[0], dirHashes[2]); err == nil {
235+
t.Fatalf("dir0 and dir2 are the same)", len(dirHashes[0]))
236+
}
237+
})
224238

225-
// Compare dir0 and dir2. They should be different.
226-
if err := compareHashes(dirHashes[0], dirHashes[2]); err == nil {
227-
t.Fatalf("dir0 and dir2 are the same)", len(dirHashes[0]))
228-
}
239+
t.Run("Check builder", func(t *testing.T) {
240+
// Check that the go_sdk path doesn't appear in the builder binary. This path is different
241+
// nominally different per workspace (but in these tests, the go_sdk paths are all set to the same
242+
// path in WORKSPACE) -- so if this path is in the builder binary, then builds between workspaces
243+
// would be partially non cacheable.
244+
builder_file, err := os.Open(filepath.Join(dirs[0], "bazel-bin", "external", "go_sdk", "builder"))
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
defer builder_file.Close()
249+
builder_data, err := ioutil.ReadAll(builder_file)
250+
if err != nil {
251+
t.Fatal(err)
252+
}
253+
if bytes.Index(builder_data, []byte("go_sdk")) != -1 {
254+
t.Fatalf("Found go_sdk path in builder binary, builder tool won't be reproducible")
255+
}
256+
})
229257

230-
// Check that the go_sdk path doesn't appear in the builder binary. This path is different
231-
// nominally different per workspace (but in these tests, the go_sdk paths are all set to the same
232-
// path in WORKSPACE) -- so if this path is in the builder binary, then builds between workspaces
233-
// would be partially non cacheable.
234-
builder_file, err := os.Open(filepath.Join(dirs[0], "bazel-bin", "external", "go_sdk", "builder"))
235-
if err != nil {
236-
t.Fatal(err)
237-
}
238-
defer builder_file.Close()
239-
builder_data, err := ioutil.ReadAll(builder_file)
240-
if err != nil {
241-
t.Fatal(err)
242-
}
243-
if bytes.Index(builder_data, []byte("go_sdk")) != -1 {
244-
t.Fatalf("Found go_sdk path in builder binary, builder tool won't be reproducible")
245-
}
258+
t.Run("Check stdlib", func(t *testing.T) {
259+
for _, dir := range dirs {
260+
found := false
261+
root, err := os.Readlink(filepath.Join(dir, "bazel-out/"))
262+
if err != nil {
263+
t.Fatal(err)
264+
}
265+
266+
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
267+
if err != nil {
268+
return err
269+
}
270+
if !strings.Contains(path, "stdlib_/pkg") {
271+
return nil
272+
}
273+
if !strings.HasSuffix(path, ".a") {
274+
return nil
275+
}
276+
data, err := ioutil.ReadFile(path)
277+
if err != nil {
278+
t.Fatal(err)
279+
}
280+
if bytes.Index(data, []byte("__GO_BAZEL_CC_PLACEHOLDER__")) == -1 {
281+
found = true
282+
return filepath.SkipAll
283+
}
284+
return nil
285+
})
286+
if !found {
287+
t.Fatal("Placeholder not found in stdlib of " + dir)
288+
}
289+
}
290+
})
246291
}
247292

248293
func copyTree(dstRoot, srcRoot string) error {
@@ -311,7 +356,7 @@ type fileHash struct {
311356
rel, hash string
312357
}
313358

314-
func hashFiles(dir string) ([]fileHash, error) {
359+
func hashFiles(dir string, exclude func(root, path string) bool) ([]fileHash, error) {
315360
// Follow top-level symbolic link
316361
root := dir
317362
for {
@@ -348,7 +393,15 @@ func hashFiles(dir string) ([]fileHash, error) {
348393
return err
349394
}
350395
}
396+
351397
if info.IsDir() {
398+
if exclude(root, path) {
399+
return filepath.SkipDir
400+
}
401+
return nil
402+
}
403+
404+
if exclude(root, path) {
352405
return nil
353406
}
354407

0 commit comments

Comments
 (0)