1// Copyright 2019 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 15package reproducibility_test 16 17import ( 18 "bytes" 19 "crypto/sha256" 20 "encoding/hex" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "strings" 28 "sync" 29 "testing" 30 31 "github.com/bazelbuild/rules_go/go/tools/bazel_testing" 32) 33 34func TestMain(m *testing.M) { 35 bazel_testing.TestMain(m, bazel_testing.Args{ 36 Main: ` 37-- BUILD.bazel -- 38load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 39 40go_library( 41 name = "empty_lib", 42 srcs = [], 43 importpath = "empty_lib", 44) 45 46go_binary( 47 name = "hello", 48 srcs = ["hello.go"], 49) 50 51go_binary( 52 name = "adder", 53 srcs = [ 54 "adder_main.go", 55 "adder.go", 56 "add.c", 57 "add.cpp", 58 "add.h", 59 ], 60 cgo = True, 61 linkmode = "c-archive", 62) 63 64-- hello.go -- 65package main 66 67import "fmt" 68 69func main() { 70 fmt.Println("hello") 71} 72 73-- add.h -- 74#ifdef __cplusplus 75extern "C" { 76#endif 77 78int add_c(int a, int b); 79int add_cpp(int a, int b); 80 81#ifdef __cplusplus 82} 83#endif 84 85-- add.c -- 86#include "add.h" 87#include "_cgo_export.h" 88 89int add_c(int a, int b) { return add(a, b); } 90 91-- add.cpp -- 92#include "add.h" 93#include "_cgo_export.h" 94 95int add_cpp(int a, int b) { return add(a, b); } 96 97-- adder.go -- 98package main 99 100/* 101#include "add.h" 102*/ 103import "C" 104 105func AddC(a, b int32) int32 { 106 return int32(C.add_c(C.int(a), C.int(b))) 107} 108 109func AddCPP(a, b int32) int32 { 110 return int32(C.add_cpp(C.int(a), C.int(b))) 111} 112 113//export add 114func add(a, b int32) int32 { 115 return a + b 116} 117 118-- adder_main.go -- 119package main 120 121import "fmt" 122 123func main() { 124 // Depend on some stdlib function. 125 fmt.Println("In C, 2 + 2 = ", AddC(2, 2)) 126 fmt.Println("In C++, 2 + 2 = ", AddCPP(2, 2)) 127} 128 129`, 130 }) 131} 132 133func Test(t *testing.T) { 134 wd, err := os.Getwd() 135 if err != nil { 136 t.Fatal(err) 137 } 138 139 // Copy the workspace to three other directories. 140 // We'll run bazel commands in those directories, not here. We clean those 141 // workspaces at the end of the test, but we don't want to clean this 142 // directory because it's shared with other tests. 143 dirs := []string{wd + "0", wd + "1", wd + "2"} 144 for _, dir := range dirs { 145 if err := copyTree(dir, wd); err != nil { 146 t.Fatal(err) 147 } 148 defer func() { 149 cmd := bazel_testing.BazelCmd("clean", "--expunge") 150 cmd.Dir = dir 151 cmd.Run() 152 os.RemoveAll(dir) 153 }() 154 } 155 defer func() { 156 var wg sync.WaitGroup 157 wg.Add(len(dirs)) 158 for _, dir := range dirs { 159 go func(dir string) { 160 defer wg.Done() 161 cmd := bazel_testing.BazelCmd("clean", "--expunge") 162 cmd.Dir = dir 163 cmd.Run() 164 os.RemoveAll(dir) 165 }(dir) 166 } 167 wg.Wait() 168 }() 169 170 // Change the source file in dir2. We should detect a difference here. 171 hello2Path := filepath.Join(dirs[2], "hello.go") 172 hello2File, err := os.OpenFile(hello2Path, os.O_WRONLY|os.O_APPEND, 0666) 173 if err != nil { 174 t.Fatal(err) 175 } 176 defer hello2File.Close() 177 if _, err := hello2File.WriteString(`func init() { fmt.Println("init") }`); err != nil { 178 t.Fatal(err) 179 } 180 if err := hello2File.Close(); err != nil { 181 t.Fatal(err) 182 } 183 184 // Build the targets in each directory. 185 var wg sync.WaitGroup 186 wg.Add(len(dirs)) 187 for _, dir := range dirs { 188 go func(dir string) { 189 defer wg.Done() 190 cmd := bazel_testing.BazelCmd("build", 191 "//:all", 192 "@io_bazel_rules_go//go/tools/builders:go_path", 193 "@go_sdk//:builder", 194 ) 195 cmd.Dir = dir 196 if err := cmd.Run(); err != nil { 197 t.Fatalf("in %s, error running %s: %v", dir, strings.Join(cmd.Args, " "), err) 198 } 199 }(dir) 200 } 201 wg.Wait() 202 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 { 216 t.Fatal(err) 217 } 218 } 219 220 // Compare dir0 and dir1. They should be identical. 221 if err := compareHashes(dirHashes[0], dirHashes[1]); err != nil { 222 t.Fatal(err) 223 } 224 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 } 229 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 } 246} 247 248func copyTree(dstRoot, srcRoot string) error { 249 return filepath.Walk(srcRoot, func(srcPath string, info os.FileInfo, err error) error { 250 if err != nil { 251 return err 252 } 253 254 rel, err := filepath.Rel(srcRoot, srcPath) 255 if err != nil { 256 return err 257 } 258 var dstPath string 259 if rel == "." { 260 dstPath = dstRoot 261 } else { 262 dstPath = filepath.Join(dstRoot, rel) 263 } 264 265 if info.IsDir() { 266 return os.Mkdir(dstPath, 0777) 267 } 268 r, err := os.Open(srcPath) 269 if err != nil { 270 return nil 271 } 272 defer r.Close() 273 w, err := os.Create(dstPath) 274 if err != nil { 275 return err 276 } 277 defer w.Close() 278 if _, err := io.Copy(w, r); err != nil { 279 return err 280 } 281 return w.Close() 282 }) 283} 284 285func compareHashes(lhs, rhs []fileHash) error { 286 buf := &bytes.Buffer{} 287 for li, ri := 0, 0; li < len(lhs) || ri < len(rhs); { 288 if li < len(lhs) && (ri == len(rhs) || lhs[li].rel < rhs[ri].rel) { 289 fmt.Fprintf(buf, "%s only in left\n", lhs[li].rel) 290 li++ 291 continue 292 } 293 if ri < len(rhs) && (li == len(lhs) || rhs[ri].rel < lhs[li].rel) { 294 fmt.Fprintf(buf, "%s only in right\n", rhs[ri].rel) 295 ri++ 296 continue 297 } 298 if lhs[li].hash != rhs[ri].hash { 299 fmt.Fprintf(buf, "%s is different: %s %s\n", lhs[li].rel, lhs[li].hash, rhs[ri].hash) 300 } 301 li++ 302 ri++ 303 } 304 if errStr := buf.String(); errStr != "" { 305 return errors.New(errStr) 306 } 307 return nil 308} 309 310type fileHash struct { 311 rel, hash string 312} 313 314func hashFiles(dir string) ([]fileHash, error) { 315 // Follow top-level symbolic link 316 root := dir 317 for { 318 info, err := os.Lstat(root) 319 if err != nil { 320 return nil, err 321 } 322 if info.Mode()&os.ModeType != os.ModeSymlink { 323 break 324 } 325 rel, err := os.Readlink(root) 326 if err != nil { 327 return nil, err 328 } 329 if filepath.IsAbs(rel) { 330 root = rel 331 } else { 332 root = filepath.Join(filepath.Dir(dir), rel) 333 } 334 } 335 336 // Gather hashes of files within the tree. 337 var hashes []fileHash 338 var sum [16]byte 339 err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 340 if err != nil { 341 return err 342 } 343 344 // Skip directories and symbolic links to directories. 345 if info.Mode()&os.ModeType == os.ModeSymlink { 346 info, err = os.Stat(path) 347 if err != nil { 348 return err 349 } 350 } 351 if info.IsDir() { 352 return nil 353 } 354 355 // Skip MANIFEST, runfiles_manifest, and .lo files. 356 // TODO(jayconrod): find out why .lo files are not reproducible. 357 base := filepath.Base(path) 358 if base == "MANIFEST" || strings.HasSuffix(base, ".runfiles_manifest") || strings.HasSuffix(base, ".lo") { 359 return nil 360 } 361 362 rel, err := filepath.Rel(root, path) 363 if err != nil { 364 return err 365 } 366 367 r, err := os.Open(path) 368 if err != nil { 369 return err 370 } 371 defer r.Close() 372 h := sha256.New() 373 if _, err := io.Copy(h, r); err != nil { 374 return err 375 } 376 hashes = append(hashes, fileHash{rel: rel, hash: hex.EncodeToString(h.Sum(sum[:0]))}) 377 378 return nil 379 }) 380 if err != nil { 381 return nil, err 382 } 383 return hashes, nil 384} 385