Repository: uber/makisu
Pull request #325 (2020-04-26 21:01:39)

Allow for global ARG substitution between stages
See: https://github.com/moby/moby/issues/37345#issuecomment-400245466

While doing this I saw that the entrypoint on the image is different than docker (from Dockerfile of testdata/build-context/global-arg/Dockerfile):  
Docker:  
`if [ -z \"$version\" -a \"$version2\" = \"v2\" ]; then echo \"This is correct\"; exit 0; else exit 1; fi`
Makisu:
`if [ -z \"$version\" -a \"v2\" = \"v2\" ] ; then echo \"This is correct\" ; exit 0 ; else exit 1 ; fi`

Makisu should not do substitution in the `CMD` and `ENTRYPOINT` directive (as seen in the list here: https://docs.docker.com/engine/reference/builder/#environment-replacement)

Rowern: Integration tests are failing until https://github.com/uber/makisu/pull/323 is merged.
I need the fix on ENTRYPOINT PARSING.
CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/uber/makisu?pullRequest=325) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/uber/makisu?pullRequest=325) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/uber/makisu?pullRequest=325) it.</sub>
Rowern: > [![CLA assistant check](https://camo.githubusercontent.com/62718f75b7272a189ef1deb563c17f1e9424023e/68747470733a2f2f636c612d617373697374616e742e696f2f70756c6c2f62616467652f7369676e6564)](https://cla-assistant.io/uber/makisu?pullRequest=325)
> All committers have signed the CLA.

Weird that I had to sign this again...
Rowern: Integration tests are failing but the test is ok on my end...  
If anyone can rety them to confirm it's not a Travis error it would be great!
yiranwang52: ha we were trying to copy docker's buggy behavior earlier.

Patch:
diff --git a/bin/makisu/cmd/diff.go b/bin/makisu/cmd/diff.go
new file mode 100644
index 0000000..d57ad4b
--- /dev/null
+++ b/bin/makisu/cmd/diff.go
@@ -0,0 +1,104 @@
+package cmd
+
+import (
+	"archive/tar"
+	"errors"
+	"fmt"
+	"os"
+
+	"github.com/andres-erbsen/clock"
+	"github.com/spf13/cobra"
+	"github.com/uber/makisu/lib/docker/image"
+	"github.com/uber/makisu/lib/log"
+	"github.com/uber/makisu/lib/registry"
+	"github.com/uber/makisu/lib/snapshot"
+	"github.com/uber/makisu/lib/storage"
+	"github.com/uber/makisu/lib/tario"
+)
+
+type diffCmd struct {
+	*cobra.Command
+	ignoreModTime bool
+}
+
+func getDiffCmd() *diffCmd {
+	diffCmd := &diffCmd{
+		Command: &cobra.Command{
+			Use:                   "diff <image name> <image name>",
+			DisableFlagsInUseLine: true,
+			Short:                 "Compare docker images from registry",
+		},
+	}
+
+	diffCmd.Args = func(cmd *cobra.Command, args []string) error {
+		if len(args) != 2 {
+			return errors.New("Requires two image names as arguments")
+		}
+		return nil
+	}
+
+	diffCmd.Run = func(cmd *cobra.Command, args []string) {
+		if err := diffCmd.Diff(args); err != nil {
+			log.Error(err)
+			os.Exit(1)
+		}
+	}
+
+	diffCmd.PersistentFlags().BoolVar(&diffCmd.ignoreModTime, "ignoreModTime", true, "Ignore mod time of image files when comparing images")
+	return diffCmd
+}
+
+func (cmd *diffCmd) Diff(imagesFullName []string) error {
+	log.Infof("ingore time? :%t", cmd.ignoreModTime)
+	var pullImages []image.Name
+	for _, imageFullName := range imagesFullName {
+		pullImage, err := image.ParseNameForPull(imageFullName)
+		if err != nil {
+			return fmt.Errorf("parse image %s: %s", pullImage, err)
+		}
+		pullImages = append(pullImages, pullImage)
+	}
+
+	if err := initRegistryConfig(""); err != nil {
+		return fmt.Errorf("failed to initialize registry configuration: %s", err)
+	}
+
+	store, err := storage.NewImageStore("/tmp/makisu-storage/")
+	if err != nil {
+		panic(err)
+	}
+
+	var memFSArr []*snapshot.MemFS
+	for i, pullImage := range pullImages {
+		client := registry.New(store, pullImage.GetRegistry(), pullImage.GetRepository())
+		manifest, err := client.Pull(pullImage.GetTag())
+		if err != nil {
+			panic(err)
+		}
+
+		memfs, err := snapshot.NewMemFS(clock.New(), "/tmp/makisu-storage/", nil)
+		if err != nil {
+			panic(err)
+		}
+
+		for _, descriptor := range manifest.Layers {
+			reader, err := store.Layers.GetStoreFileReader(descriptor.Digest.Hex())
+			if err != nil {
+				panic(fmt.Errorf("get reader from image %d layer: %s", i+1, err))
+			}
+			gzipReader, err := tario.NewGzipReader(reader)
+			if err != nil {
+				panic(fmt.Errorf("create gzip reader for layer: %s", err))
+			}
+			if err = memfs.UpdateFromTarReader(tar.NewReader(gzipReader), false); err != nil {
+				panic(fmt.Errorf("untar image %d layer reader: %s", i+1, err))
+			}
+		}
+		memFSArr = append(memFSArr, memfs)
+	}
+
+	log.Infof("* Diff two images")
+	// TODO: compare the image config.
+	snapshot.CompareFS(memFSArr[0], memFSArr[1], pullImages[0], pullImages[1], cmd.ignoreModTime)
+	return nil
+}
diff --git a/bin/makisu/cmd/root.go b/bin/makisu/cmd/root.go
index 66e3504..f909bcb 100644
--- a/bin/makisu/cmd/root.go
+++ b/bin/makisu/cmd/root.go
@@ -76,6 +76,7 @@ func Execute() {
 	rootCmd.AddCommand(getVersionCmd())
 	rootCmd.AddCommand(getPullCmd().Command)
 	rootCmd.AddCommand(getPushCmd().Command)
+	rootCmd.AddCommand(getDiffCmd().Command)
 	if err := rootCmd.Execute(); err != nil {
 		log.Error(err)
 		os.Exit(1)
diff --git a/lib/parser/dockerfile/arg.go b/lib/parser/dockerfile/arg.go
index 0516131..b4f7f2c 100644
--- a/lib/parser/dockerfile/arg.go
+++ b/lib/parser/dockerfile/arg.go
@@ -76,6 +76,13 @@ func (d *ArgDirective) update(state *parsingState) error {
 		d.ResolvedVal = &d.DefaultVal
 	}
 	if !global {
+		// If no value is provided to this arg, we try to replace it using the global scope
+		// (see the testdata/build-context/global-arg/Dockerfile)
+		if val, ok := state.globalArgs[d.Name]; ok {
+			vars[d.Name] = val
+			d.ResolvedVal = &val
+		}
+
 		return state.addToCurrStage(d)
 	}
 	return nil
diff --git a/lib/snapshot/mem_fs.go b/lib/snapshot/mem_fs.go
index 7c44540..0b95b60 100644
--- a/lib/snapshot/mem_fs.go
+++ b/lib/snapshot/mem_fs.go
@@ -25,6 +25,7 @@ import (
 	"time"
 
 	"github.com/andres-erbsen/clock"
+	"github.com/uber/makisu/lib/docker/image"
 	"github.com/uber/makisu/lib/fileio"
 	"github.com/uber/makisu/lib/log"
 	"github.com/uber/makisu/lib/mountutils"
@@ -489,7 +490,7 @@ func (fs *MemFS) isUpdated(p string, hdr *tar.Header) (bool, *memFSNode, error)
 		}
 	}
 
-	similar, err := tario.IsSimilarHeader(curr.hdr, hdr)
+	similar, err := tario.IsSimilarHeader(curr.hdr, hdr, false)
 	if err != nil {
 		return false, nil, fmt.Errorf("compare header %s: %s", p, err)
 	}
@@ -599,7 +600,7 @@ func (fs *MemFS) untarOneItem(path string, header *tar.Header, r *tar.Reader) er
 		}
 
 		// If the file is already on disk, nothing needs to be done.
-		if similar, err := tario.IsSimilarHeader(localHeader, header); err != nil {
+		if similar, err := tario.IsSimilarHeader(localHeader, header, false); err != nil {
 			return fmt.Errorf("compare headers %s: %s", path, err)
 		} else if similar {
 			return nil
@@ -710,3 +711,75 @@ func (fs *MemFS) untarFile(path string, header *tar.Header, r *tar.Reader) error
 	}
 	return nil
 }
+
+// CompareFS is the public API for comparing merged layers of two images for differences.
+func CompareFS(fs1, fs2 *MemFS, image1Name, image2Name image.Name, ignoreModTime bool) {
+	missing1 := make(map[string]*memFSNode)
+	missing2 := make(map[string]*memFSNode)
+	diff1 := make(map[string]*memFSNode)
+	diff2 := make(map[string]*memFSNode)
+
+	compareNode(fs1.tree, fs2.tree, missing1, missing2, diff1, diff2, "/", ignoreModTime)
+	image1Format := image1Name.GetRepository() + ":" + image1Name.GetTag()
+	image2Format := image2Name.GetRepository() + ":" + image2Name.GetTag()
+	// Files missing in first image but appeared in second image.
+	log.Infof("===== file missing in first image %s =====", image1Format)
+	for _, node := range missing1 {
+		logMemFSNodeInfo(node)
+	}
+
+	// Files missing in second image but appeared in first image.
+	log.Infof("===== file missing in second image %s =====", image2Format)
+	for _, node := range missing2 {
+		logMemFSNodeInfo(node)
+	}
+
+	// File differences in two images.
+	log.Infof("===== difference between two images %s and %s =====", image1Format, image2Format)
+	for path := range diff1 {
+		hdr1, hdr2 := diff1[path].hdr, diff2[path].hdr
+		log.Infof("%s %s %d %d %d", path, hdr1.FileInfo().Mode(), hdr1.Uid, hdr1.Gid, hdr1.Size)
+		log.Infof("%s %s %d %d %d", path, hdr2.FileInfo().Mode(), hdr2.Uid, hdr2.Gid, hdr2.Size)
+		log.Infof("xxxxxxxxxxxxxxxxxxxxxxxxxxx")
+	}
+}
+
+// compareNode compares two memFSNodes for differences.
+func compareNode(node1, node2 *memFSNode, missing1, missing2, diff1, diff2 map[string]*memFSNode, path string, ignoreModTime bool) {
+	if isSimilar, _ := tario.IsSimilarHeader(node1.hdr, node2.hdr, ignoreModTime); !isSimilar {
+		diff1[path] = node1
+		diff2[path] = node2
+	}
+
+	allChildren := make(map[string]bool)
+	for child := range node1.children {
+		allChildren[child] = true
+	}
+
+	for child := range node2.children {
+		allChildren[child] = true
+	}
+
+	for child := range allChildren {
+		nextNode1, ok1 := node1.children[child]
+		nextNode2, ok2 := node2.children[child]
+		updatedPath := filepath.Join(path, child)
+		if ok1 && ok2 {
+			compareNode(nextNode1, nextNode2, missing1, missing2, diff1, diff2, updatedPath, ignoreModTime)
+			continue
+		} else if ok1 {
+			missing2[updatedPath] = node1.children[child]
+		} else if ok2 {
+			missing1[updatedPath] = node2.children[child]
+		}
+	}
+}
+
+//logMemFSNodeInfo logs the info of a memFSNode.
+func logMemFSNodeInfo(node *memFSNode) {
+	hdr := node.hdr
+	log.Infof("%s %s %d %d %d", hdr.Name, hdr.FileInfo().Mode(), hdr.Uid, hdr.Gid, hdr.Size)
+	for _, nxtNode := range node.children {
+		logMemFSNodeInfo(nxtNode)
+	}
+}
diff --git a/lib/tario/compare.go b/lib/tario/compare.go
index 50e3959..9734f43 100644
--- a/lib/tario/compare.go
+++ b/lib/tario/compare.go
@@ -21,7 +21,7 @@ import (
 )
 
 // IsSimilarHeader returns if the given headers are describing similar entries.
-func IsSimilarHeader(h *tar.Header, nh *tar.Header) (bool, error) {
+func IsSimilarHeader(h *tar.Header, nh *tar.Header, ignoreTime bool) (bool, error) {
 	// Don't support modifying "/".
 	if h.Name == "" && nh.Name == "" {
 		return true, nil
@@ -37,17 +37,17 @@ func IsSimilarHeader(h *tar.Header, nh *tar.Header) (bool, error) {
 		if nh.Typeflag != tar.TypeLink {
 			return false, nil
 		}
-		return isSimilarHardLink(h, nh)
+		return isSimilarHardLink(h, nh, ignoreTime)
 	case tar.TypeDir:
 		if nh.Typeflag != tar.TypeDir {
 			return false, nil
 		}
-		return isSimilarDirectory(h, nh)
+		return isSimilarDirectory(h, nh, ignoreTime)
 	case tar.TypeReg, tar.TypeRegA:
 		if nh.Typeflag != tar.TypeReg && nh.Typeflag != tar.TypeRegA {
 			return false, nil
 		}
-		return isSimilarRegularFile(h, nh)
+		return isSimilarRegularFile(h, nh, ignoreTime)
 	default:
 		return false, fmt.Errorf("unsupported type %b", h.Typeflag)
 	}
@@ -61,10 +61,15 @@ func isSimilarSymlink(h *tar.Header, nh *tar.Header) (bool, error) {
 
 // isSimilarHardLink returns if the given headers are describing similar hard
 // links. It only checks mtime and link target.
-func isSimilarHardLink(h *tar.Header, nh *tar.Header) (bool, error) {
-	hMtime := h.ModTime.Truncate(1 * time.Second)
-	nhMtime := nh.ModTime.Truncate(1 * time.Second)
-	if hMtime.Equal(nhMtime) &&
+func isSimilarHardLink(h *tar.Header, nh *tar.Header, ignoreTime bool) (bool, error) {
+	timeIsEqual := true
+	if !ignoreTime {
+		hMtime := h.ModTime.Truncate(1 * time.Second)
+		nhMtime := nh.ModTime.Truncate(1 * time.Second)
+		timeIsEqual = hMtime.Equal(nhMtime)
+	}
+
+	if timeIsEqual &&
 		h.Linkname == nh.Linkname &&
 		h.Uid == nh.Uid &&
 		h.Gid == nh.Gid &&
@@ -76,10 +81,15 @@ func isSimilarHardLink(h *tar.Header, nh *tar.Header) (bool, error) {
 
 // isSimilarDirectory returns if the given headers are describing similar
 // directories. It only checks mtime and owner, ignoring size, path and content.
-func isSimilarDirectory(h *tar.Header, nh *tar.Header) (bool, error) {
-	hMtime := h.ModTime.Truncate(1 * time.Second)
-	nhMtime := nh.ModTime.Truncate(1 * time.Second)
-	if hMtime.Equal(nhMtime) &&
+func isSimilarDirectory(h *tar.Header, nh *tar.Header, ignoreTime bool) (bool, error) {
+	timeIsEqual := true
+	if !ignoreTime {
+		hMtime := h.ModTime.Truncate(1 * time.Second)
+		nhMtime := nh.ModTime.Truncate(1 * time.Second)
+		timeIsEqual = hMtime.Equal(nhMtime)
+	}
+
+	if timeIsEqual &&
 		h.Uid == nh.Uid &&
 		h.Gid == nh.Gid &&
 		h.FileInfo().Mode() == nh.FileInfo().Mode() {
@@ -91,10 +101,15 @@ func isSimilarDirectory(h *tar.Header, nh *tar.Header) (bool, error) {
 // isSimilarRegularFile returns if the given headers are describing similar
 // regular files. It only checks mtime, size, and owner, ignoring path and
 // content.
-func isSimilarRegularFile(h *tar.Header, nh *tar.Header) (bool, error) {
-	hMtime := h.ModTime.Truncate(1 * time.Second)
-	nhMtime := nh.ModTime.Truncate(1 * time.Second)
-	if hMtime.Equal(nhMtime) &&
+func isSimilarRegularFile(h *tar.Header, nh *tar.Header, ignoreTime bool) (bool, error) {
+	timeIsEqual := true
+	if !ignoreTime {
+		hMtime := h.ModTime.Truncate(1 * time.Second)
+		nhMtime := nh.ModTime.Truncate(1 * time.Second)
+		timeIsEqual = hMtime.Equal(nhMtime)
+	}
+
+	if timeIsEqual &&
 		h.Uid == nh.Uid &&
 		h.Gid == nh.Gid &&
 		h.Size == nh.Size &&


Test patch:
diff --git a/lib/snapshot/mem_fs_test.go b/lib/snapshot/mem_fs_test.go
index 42d84a2..1eb9573 100644
--- a/lib/snapshot/mem_fs_test.go
+++ b/lib/snapshot/mem_fs_test.go
@@ -20,12 +20,12 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"sort"
 	"testing"
 
-	"github.com/uber/makisu/lib/pathutils"
-
 	"github.com/andres-erbsen/clock"
 	"github.com/stretchr/testify/require"
+	"github.com/uber/makisu/lib/pathutils"
 )
 
 func TestUntarFromPath(t *testing.T) {
@@ -1194,3 +1194,86 @@ func TestAddLayersEqual(t *testing.T) {
 	require.Equal(b3, b1)
 	require.Equal(b2, b1)
 }
+
+func TestCompareFS(t *testing.T) {
+	require := require.New(t)
+
+	tmpRoot, err := ioutil.TempDir("/tmp", "makisu-test")
+	require.NoError(err)
+	defer os.RemoveAll(tmpRoot)
+
+	clk := clock.NewMock()
+	fs1, err := NewMemFS(clk, tmpRoot, pathutils.DefaultBlacklist)
+	require.NoError(err)
+
+	l1 := newMemLayer()
+	dst11 := "/common"
+	require.NoError(addDirectoryToLayer(l1, tmpRoot, dst11, 0755))
+	dst12 := "/common/test1"
+	require.NoError(addDirectoryToLayer(l1, tmpRoot, dst12, 0755))
+	dst13 := "/common/world"
+	require.NoError(addRegularFileToLayer(l1, tmpRoot, dst13, "hello", 0711))
+	require.NoError(fs1.merge(l1))
+
+	fs2, err := NewMemFS(clk, tmpRoot, pathutils.DefaultBlacklist)
+	require.NoError(err)
+
+	l2 := newMemLayer()
+	dst21 := "/common"
+	require.NoError(addDirectoryToLayer(l2, tmpRoot, dst21, 0755))
+	dst22 := "/common/test2"
+	require.NoError(addDirectoryToLayer(l2, tmpRoot, dst22, 0755))
+	dst23 := "/common/world"
+	require.NoError(addRegularFileToLayer(l2, tmpRoot, dst23, "hello", 0755))
+	require.NoError(fs2.merge(l2))
+
+	missing1 := make(map[string]*memFSNode)
+	missing2 := make(map[string]*memFSNode)
+	diff1 := make(map[string]*memFSNode)
+	diff2 := make(map[string]*memFSNode)
+
+	compareNode(fs1.tree, fs2.tree, missing1, missing2, diff1, diff2, "/", true)
+	// Check Missing paths.
+	expectedMissing1Nums := 1
+	require.Equal(expectedMissing1Nums, len(missing1))
+
+	expectedMissing1Paths := []string{"/common/test2"}
+	actualMissing1Paths := make([]string, 0)
+	for path1 := range missing1 {
+		actualMissing1Paths = append(actualMissing1Paths, path1)
+	}
+
+	sort.Strings(actualMissing1Paths)
+	sort.Strings(expectedMissing1Paths)
+	require.Equal(actualMissing1Paths, expectedMissing1Paths)
+
+	expectedMissing2Nums := 1
+	require.Equal(expectedMissing2Nums, len(missing2))
+	expectedMissing2Paths := []string{"/common/test1"}
+	actualMissing2Paths := make([]string, 0)
+	for path2 := range missing2 {
+		actualMissing2Paths = append(actualMissing2Paths, path2)
+	}
+
+	sort.Strings(actualMissing2Paths)
+	sort.Strings(expectedMissing2Paths)
+	require.Equal(actualMissing2Paths,
+		expectedMissing2Paths)
+
+	// Check diff paths.
+	expectedDiff := []string{"/common/world"}
+	actualDiff1 := make([]string, 0)
+	for path := range diff1 {
+		actualDiff1 = append(actualDiff1, path)
+	}
+
+	actualDiff2 := make([]string, 0)
+	for path := range diff2 {
+		actualDiff2 = append(actualDiff2, path)
+	}
+
+	sort.Strings(actualDiff1)
+	sort.Strings(actualDiff2)
+	require.Equal(expectedDiff, actualDiff1)
+	require.Equal(expectedDiff, actualDiff2)
+}
diff --git a/lib/snapshot/testutils_test.go b/lib/snapshot/testutils_test.go
index cdf6506..004485b 100644
--- a/lib/snapshot/testutils_test.go
+++ b/lib/snapshot/testutils_test.go
@@ -51,14 +51,14 @@ func requireEqualLayers(require *require.Assertions, l1 *memLayer, l2 *memLayer)
 			content2, ok := mf2.(*contentMemFile)
 			require.True(ok)
 			require.Equal(content1.dst, content2.dst, "%s dsts not equal:\nexpected=%s, actual=%s", p1, content1.dst, content2.dst)
-			similar, err := tario.IsSimilarHeader(content1.hdr, content2.hdr)
+			similar, err := tario.IsSimilarHeader(content1.hdr, content2.hdr, false)
 			require.NoError(err)
 			require.True(similar, "%s headers not similar:\nexpected=%+v\nactual=  %+v", p1, content1.hdr, content2.hdr)
 		} else if whiteout1, ok := mf1.(*whiteoutMemFile); ok {
 			whiteout2, ok := mf2.(*whiteoutMemFile)
 			require.True(ok)
 			require.Equal(whiteout1.del, whiteout2.del)
-			similar, err := tario.IsSimilarHeader(whiteout1.hdr, whiteout2.hdr)
+			similar, err := tario.IsSimilarHeader(whiteout1.hdr, whiteout2.hdr, false)
 			require.NoError(err)
 			require.True(similar)
 		} else {
diff --git a/lib/tario/compare_test.go b/lib/tario/compare_test.go
index 068ee86..bf51106 100644
--- a/lib/tario/compare_test.go
+++ b/lib/tario/compare_test.go
@@ -47,7 +47,7 @@ func TestIsSimilar(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := IsSimilarHeader(h, newH)
+		similar, err := IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -75,7 +75,7 @@ func TestIsSimilar(t *testing.T) {
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
 		newH.Name = ""
-		similar, err := IsSimilarHeader(h, newH)
+		similar, err := IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -102,7 +102,7 @@ func TestIsSimilar(t *testing.T) {
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
 		newH.Linkname = testDir1
-		similar, err := IsSimilarHeader(h, newH)
+		similar, err := IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -135,7 +135,7 @@ func TestIsSimilar(t *testing.T) {
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
 		newH.Linkname = testFile1.Name()
-		similar, err := IsSimilarHeader(h, newH)
+		similar, err := IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -171,7 +171,7 @@ func TestIsSimilar(t *testing.T) {
 		newH.Linkname = testFile1.Name()
 		newH.Size = 0
 
-		similar, err := IsSimilarHeader(h, newH)
+		similar, err := IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -211,7 +211,7 @@ func TestIsSimilarSymlink(t *testing.T) {
 		similar, err := isSimilarSymlink(h, newH)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -251,7 +251,7 @@ func TestIsSimilarSymlink(t *testing.T) {
 		similar, err := isSimilarSymlink(h, newH)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -292,10 +292,10 @@ func TestIsSimilarHardlink(t *testing.T) {
 		newH.Typeflag = tar.TypeLink
 		newH.Linkname = testFile.Name()
 		newH.Size = 0
-		similar, err := isSimilarHardLink(h, newH)
+		similar, err := isSimilarHardLink(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -336,10 +336,8 @@ func TestIsSimilarHardlink(t *testing.T) {
 		newH.Typeflag = tar.TypeLink
 		newH.Linkname = testFile2.Name()
 		newH.Size = 0
-		similar, err := isSimilarHardLink(h, newH)
-		require.False(similar)
-		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err := isSimilarHardLink(h, newH, false)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -367,10 +365,10 @@ func TestIsSimilarDirectory(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarDirectory(h, newH)
+		similar, err := isSimilarDirectory(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -399,10 +397,10 @@ func TestIsSimilarDirectory(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarDirectory(h, newH)
+		similar, err := isSimilarDirectory(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -432,14 +430,47 @@ func TestIsSimilarDirectory(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarDirectory(h, newH)
+		similar, err := isSimilarDirectory(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
 
+	t.Run("DifferentModTimeConsideredSimilarIfIgnored", func(t *testing.T) {
+		require := require.New(t)
+
+		tmpRoot, err := ioutil.TempDir("/tmp", "makisu-test")
+		require.NoError(err)
+		defer os.RemoveAll(tmpRoot)
+
+		testDir1, err := ioutil.TempDir(tmpRoot, "test1")
+		require.NoError(err)
+		testDir2, err := ioutil.TempDir(tmpRoot, "test2")
+		require.NoError(err)
+
+		atime := time.Now().Add(-time.Hour)
+		mtime := time.Now().Add(-time.Hour * 2)
+		require.NoError(os.Chtimes(testDir1, atime, mtime))
+
+		fi1, err := os.Lstat(testDir1)
+		require.NoError(err)
+		fi2, err := os.Lstat(testDir2)
+		require.NoError(err)
+
+		h, err := tar.FileInfoHeader(fi1, "")
+		require.NoError(err)
+		newH, err := tar.FileInfoHeader(fi2, "")
+		require.NoError(err)
+		similar, err := isSimilarDirectory(h, newH, true)
+		require.True(similar)
+		require.NoError(err)
+		similar, err = IsSimilarHeader(h, newH, true)
+		require.True(similar)
+		require.NoError(err)
+	})
+
 	t.Run("DifferentModeConsideredDifferent", func(t *testing.T) {
 		require := require.New(t)
 
@@ -463,10 +494,10 @@ func TestIsSimilarDirectory(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarDirectory(h, newH)
+		similar, err := isSimilarDirectory(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -499,10 +530,10 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -535,10 +566,10 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
 		newH.Typeflag = tar.TypeRegA
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -573,10 +604,10 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.True(similar)
 		require.NoError(err)
 	})
@@ -608,10 +639,10 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
@@ -641,14 +672,47 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
 
+	t.Run("DifferentModTimeConsideredSimilarIfIgnored", func(t *testing.T) {
+		require := require.New(t)
+
+		tmpRoot, err := ioutil.TempDir("/tmp", "makisu-test")
+		require.NoError(err)
+		defer os.RemoveAll(tmpRoot)
+
+		testFile1, err := ioutil.TempFile(tmpRoot, "test1")
+		require.NoError(err)
+		testFile2, err := ioutil.TempFile(tmpRoot, "test2")
+		require.NoError(err)
+
+		atime := time.Now().Add(-time.Hour)
+		mtime := time.Now().Add(-time.Hour * 2)
+		require.NoError(os.Chtimes(testFile1.Name(), atime, mtime))
+
+		fi1, err := os.Lstat(testFile1.Name())
+		require.NoError(err)
+		fi2, err := os.Lstat(testFile2.Name())
+		require.NoError(err)
+
+		h, err := tar.FileInfoHeader(fi1, "")
+		require.NoError(err)
+		newH, err := tar.FileInfoHeader(fi2, "")
+		require.NoError(err)
+		similar, err := isSimilarRegularFile(h, newH, true)
+		require.True(similar)
+		require.NoError(err)
+		similar, err = IsSimilarHeader(h, newH, true)
+		require.True(similar)
+		require.NoError(err)
+	})
+
 	t.Run("DifferentModeConsideredDifferent", func(t *testing.T) {
 		require := require.New(t)
 
@@ -672,10 +736,10 @@ func TestIsSimilarRegularFile(t *testing.T) {
 		require.NoError(err)
 		newH, err := tar.FileInfoHeader(fi2, "")
 		require.NoError(err)
-		similar, err := isSimilarRegularFile(h, newH)
+		similar, err := isSimilarRegularFile(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
-		similar, err = IsSimilarHeader(h, newH)
+		similar, err = IsSimilarHeader(h, newH, false)
 		require.False(similar)
 		require.NoError(err)
 	})
diff --git a/test/python/test_build.py b/test/python/test_build.py
index fd040f4..ec69e4d 100644
--- a/test/python/test_build.py
+++ b/test/python/test_build.py
@@ -287,3 +287,16 @@ def test_build_entrypoint(registry1, storage_dir):
         new_image, context_dir, storage_dir, registry=registry1.addr)
     code, err = utils.docker_run_image(registry1.addr, new_image)
     assert code == 0, err
+
+def test_build_global_arg(registry1, storage_dir):
+    new_image = utils.new_image_name()
+    context_dir = os.path.join(
+        os.getcwd(), 'testdata/build-context/global-arg')
+    docker_build_args = [
+        "version_default=v2",
+    ]
+    utils.makisu_build_image(
+        new_image, context_dir, storage_dir,
+        registry=registry1.addr, docker_args=docker_build_args)
+    code, err = utils.docker_run_image(registry1.addr, new_image)
+    assert code == 0, err
diff --git a/testdata/build-context/global-arg/Dockerfile b/testdata/build-context/global-arg/Dockerfile
new file mode 100644
index 0000000..7d205c4
--- /dev/null
+++ b/testdata/build-context/global-arg/Dockerfile
@@ -0,0 +1,11 @@
+ARG version_default=v1
+
+FROM alpine:latest as base1
+ARG version_default
+ENV version=$version_default
+
+FROM alpine:latest as base2
+ARG version_default
+ENV version2=$version_default
+
+ENTRYPOINT if [ -z "$version" -a "$version2" = "v2" ]; then echo "This is correct"; exit 0; else exit 1; fi
\ No newline at end of file