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