github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/buildpack/buildpack_test.go (about)

     1  package buildpack_test
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/buildpacks/lifecycle/api"
    15  	"github.com/heroku/color"
    16  	"github.com/pkg/errors"
    17  	"github.com/sclevine/spec"
    18  	"github.com/sclevine/spec/report"
    19  
    20  	"github.com/buildpacks/pack/pkg/archive"
    21  	"github.com/buildpacks/pack/pkg/blob"
    22  	"github.com/buildpacks/pack/pkg/buildpack"
    23  	"github.com/buildpacks/pack/pkg/dist"
    24  	"github.com/buildpacks/pack/pkg/logging"
    25  	h "github.com/buildpacks/pack/testhelpers"
    26  )
    27  
    28  func TestBuildpack(t *testing.T) {
    29  	color.Disable(true)
    30  	defer color.Disable(false)
    31  	spec.Run(t, "buildpack", testBuildpack, spec.Parallel(), spec.Report(report.Terminal{}))
    32  }
    33  
    34  func testBuildpack(t *testing.T, when spec.G, it spec.S) {
    35  	var writeBlobToFile = func(bp buildpack.BuildModule) string {
    36  		t.Helper()
    37  
    38  		bpReader, err := bp.Open()
    39  		h.AssertNil(t, err)
    40  
    41  		tmpDir, err := os.MkdirTemp("", "")
    42  		h.AssertNil(t, err)
    43  
    44  		p := filepath.Join(tmpDir, "bp.tar")
    45  		bpWriter, err := os.Create(p)
    46  		h.AssertNil(t, err)
    47  
    48  		_, err = io.Copy(bpWriter, bpReader)
    49  		h.AssertNil(t, err)
    50  
    51  		err = bpReader.Close()
    52  		h.AssertNil(t, err)
    53  
    54  		return p
    55  	}
    56  
    57  	when("#BuildpackFromRootBlob", func() {
    58  		it("parses the descriptor file", func() {
    59  			bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
    60  				openFn: func() io.ReadCloser {
    61  					tarBuilder := archive.TarBuilder{}
    62  					tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
    63  api = "0.3"
    64  
    65  [buildpack]
    66  id = "bp.one"
    67  version = "1.2.3"
    68  homepage = "http://geocities.com/cool-bp"
    69  
    70  [[stacks]]
    71  id = "some.stack.id"
    72  `))
    73  					return tarBuilder.Reader(archive.DefaultTarWriterFactory())
    74  				},
    75  			}, archive.DefaultTarWriterFactory(), nil)
    76  			h.AssertNil(t, err)
    77  
    78  			h.AssertEq(t, bp.Descriptor().API().String(), "0.3")
    79  			h.AssertEq(t, bp.Descriptor().Info().ID, "bp.one")
    80  			h.AssertEq(t, bp.Descriptor().Info().Version, "1.2.3")
    81  			h.AssertEq(t, bp.Descriptor().Info().Homepage, "http://geocities.com/cool-bp")
    82  			h.AssertEq(t, bp.Descriptor().Stacks()[0].ID, "some.stack.id")
    83  		})
    84  
    85  		it("translates blob to distribution format", func() {
    86  			bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
    87  				openFn: func() io.ReadCloser {
    88  					tarBuilder := archive.TarBuilder{}
    89  					tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
    90  api = "0.3"
    91  
    92  [buildpack]
    93  id = "bp.one"
    94  version = "1.2.3"
    95  
    96  [[stacks]]
    97  id = "some.stack.id"
    98  `))
    99  
   100  					tarBuilder.AddDir("bin", 0700, time.Now())
   101  					tarBuilder.AddFile("bin/detect", 0700, time.Now(), []byte("detect-contents"))
   102  					tarBuilder.AddFile("bin/build", 0700, time.Now(), []byte("build-contents"))
   103  					return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   104  				},
   105  			}, archive.DefaultTarWriterFactory(), nil)
   106  			h.AssertNil(t, err)
   107  
   108  			h.AssertNil(t, bp.Descriptor().EnsureTargetSupport(dist.DefaultTargetOSLinux, dist.DefaultTargetArch, "", ""))
   109  
   110  			tarPath := writeBlobToFile(bp)
   111  			defer os.Remove(tarPath)
   112  
   113  			h.AssertOnTarEntry(t, tarPath,
   114  				"/cnb/buildpacks/bp.one",
   115  				h.IsDirectory(),
   116  				h.HasFileMode(0755),
   117  				h.HasModTime(archive.NormalizedDateTime),
   118  			)
   119  
   120  			h.AssertOnTarEntry(t, tarPath,
   121  				"/cnb/buildpacks/bp.one/1.2.3",
   122  				h.IsDirectory(),
   123  				h.HasFileMode(0755),
   124  				h.HasModTime(archive.NormalizedDateTime),
   125  			)
   126  
   127  			h.AssertOnTarEntry(t, tarPath,
   128  				"/cnb/buildpacks/bp.one/1.2.3/bin",
   129  				h.IsDirectory(),
   130  				h.HasFileMode(0755),
   131  				h.HasModTime(archive.NormalizedDateTime),
   132  			)
   133  
   134  			h.AssertOnTarEntry(t, tarPath,
   135  				"/cnb/buildpacks/bp.one/1.2.3/bin/detect",
   136  				h.HasFileMode(0755),
   137  				h.HasModTime(archive.NormalizedDateTime),
   138  				h.ContentEquals("detect-contents"),
   139  			)
   140  
   141  			h.AssertOnTarEntry(t, tarPath,
   142  				"/cnb/buildpacks/bp.one/1.2.3/bin/build",
   143  				h.HasFileMode(0755),
   144  				h.HasModTime(archive.NormalizedDateTime),
   145  				h.ContentEquals("build-contents"),
   146  			)
   147  		})
   148  
   149  		it("translates blob to windows bat distribution format", func() {
   150  			bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   151  				openFn: func() io.ReadCloser {
   152  					tarBuilder := archive.TarBuilder{}
   153  					tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   154  api = "0.9"
   155  
   156  [buildpack]
   157  id = "bp.one"
   158  version = "1.2.3"
   159  `))
   160  
   161  					tarBuilder.AddDir("bin", 0700, time.Now())
   162  					tarBuilder.AddFile("bin/detect", 0700, time.Now(), []byte("detect-contents"))
   163  					tarBuilder.AddFile("bin/build.bat", 0700, time.Now(), []byte("build-contents"))
   164  					return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   165  				},
   166  			}, archive.DefaultTarWriterFactory(), nil)
   167  			h.AssertNil(t, err)
   168  
   169  			bpDescriptor := bp.Descriptor().(*dist.BuildpackDescriptor)
   170  			h.AssertTrue(t, bpDescriptor.WithWindowsBuild)
   171  			h.AssertFalse(t, bpDescriptor.WithLinuxBuild)
   172  
   173  			tarPath := writeBlobToFile(bp)
   174  			defer os.Remove(tarPath)
   175  
   176  			h.AssertOnTarEntry(t, tarPath,
   177  				"/cnb/buildpacks/bp.one/1.2.3/bin/build.bat",
   178  				h.HasFileMode(0755),
   179  				h.HasModTime(archive.NormalizedDateTime),
   180  				h.ContentEquals("build-contents"),
   181  			)
   182  		})
   183  
   184  		it("translates blob to windows exe distribution format", func() {
   185  			bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   186  				openFn: func() io.ReadCloser {
   187  					tarBuilder := archive.TarBuilder{}
   188  					tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   189  api = "0.3"
   190  
   191  [buildpack]
   192  id = "bp.one"
   193  version = "1.2.3"
   194  `))
   195  
   196  					tarBuilder.AddDir("bin", 0700, time.Now())
   197  					tarBuilder.AddFile("bin/detect", 0700, time.Now(), []byte("detect-contents"))
   198  					tarBuilder.AddFile("bin/build.exe", 0700, time.Now(), []byte("build-contents"))
   199  					return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   200  				},
   201  			}, archive.DefaultTarWriterFactory(), nil)
   202  			h.AssertNil(t, err)
   203  
   204  			bpDescriptor := bp.Descriptor().(*dist.BuildpackDescriptor)
   205  			h.AssertTrue(t, bpDescriptor.WithWindowsBuild)
   206  			h.AssertFalse(t, bpDescriptor.WithLinuxBuild)
   207  
   208  			tarPath := writeBlobToFile(bp)
   209  			defer os.Remove(tarPath)
   210  
   211  			h.AssertOnTarEntry(t, tarPath,
   212  				"/cnb/buildpacks/bp.one/1.2.3/bin/build.exe",
   213  				h.HasFileMode(0755),
   214  				h.HasModTime(archive.NormalizedDateTime),
   215  				h.ContentEquals("build-contents"),
   216  			)
   217  		})
   218  
   219  		it("surfaces errors encountered while reading blob", func() {
   220  			realBlob := &readerBlob{
   221  				openFn: func() io.ReadCloser {
   222  					tarBuilder := archive.TarBuilder{}
   223  					tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   224  api = "0.3"
   225  
   226  [buildpack]
   227  id = "bp.one"
   228  version = "1.2.3"
   229  
   230  [[stacks]]
   231  id = "some.stack.id"
   232  `))
   233  					return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   234  				},
   235  			}
   236  
   237  			bp, err := buildpack.FromBuildpackRootBlob(&errorBlob{
   238  				realBlob: realBlob,
   239  				limit:    4,
   240  			}, archive.DefaultTarWriterFactory(), nil)
   241  			h.AssertNil(t, err)
   242  
   243  			bpReader, err := bp.Open()
   244  			h.AssertNil(t, err)
   245  
   246  			_, err = io.Copy(io.Discard, bpReader)
   247  			h.AssertError(t, err, "error from errBlob (reached limit of 4)")
   248  		})
   249  
   250  		when("calculating permissions", func() {
   251  			bpTOMLData := `
   252  api = "0.3"
   253  
   254  [buildpack]
   255  id = "bp.one"
   256  version = "1.2.3"
   257  
   258  [[stacks]]
   259  id = "some.stack.id"
   260  `
   261  
   262  			when("no exec bits set", func() {
   263  				it("sets to 0755 if directory", func() {
   264  					bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   265  						openFn: func() io.ReadCloser {
   266  							tarBuilder := archive.TarBuilder{}
   267  							tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(bpTOMLData))
   268  							tarBuilder.AddDir("some-dir", 0600, time.Now())
   269  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   270  						},
   271  					}, archive.DefaultTarWriterFactory(), nil)
   272  					h.AssertNil(t, err)
   273  
   274  					tarPath := writeBlobToFile(bp)
   275  					defer os.Remove(tarPath)
   276  
   277  					h.AssertOnTarEntry(t, tarPath,
   278  						"/cnb/buildpacks/bp.one/1.2.3/some-dir",
   279  						h.HasFileMode(0755),
   280  					)
   281  				})
   282  			})
   283  
   284  			when("no exec bits set", func() {
   285  				it("sets to 0755 if 'bin/detect' or 'bin/build'", func() {
   286  					bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   287  						openFn: func() io.ReadCloser {
   288  							tarBuilder := archive.TarBuilder{}
   289  							tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(bpTOMLData))
   290  							tarBuilder.AddFile("bin/detect", 0600, time.Now(), []byte("detect-contents"))
   291  							tarBuilder.AddFile("bin/build", 0600, time.Now(), []byte("build-contents"))
   292  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   293  						},
   294  					}, archive.DefaultTarWriterFactory(), nil)
   295  					h.AssertNil(t, err)
   296  
   297  					bpDescriptor := bp.Descriptor().(*dist.BuildpackDescriptor)
   298  					h.AssertFalse(t, bpDescriptor.WithWindowsBuild)
   299  					h.AssertTrue(t, bpDescriptor.WithLinuxBuild)
   300  
   301  					tarPath := writeBlobToFile(bp)
   302  					defer os.Remove(tarPath)
   303  
   304  					h.AssertOnTarEntry(t, tarPath,
   305  						"/cnb/buildpacks/bp.one/1.2.3/bin/detect",
   306  						h.HasFileMode(0755),
   307  					)
   308  
   309  					h.AssertOnTarEntry(t, tarPath,
   310  						"/cnb/buildpacks/bp.one/1.2.3/bin/build",
   311  						h.HasFileMode(0755),
   312  					)
   313  				})
   314  			})
   315  
   316  			when("not directory, 'bin/detect', or 'bin/build'", func() {
   317  				it("sets to 0755 if ANY exec bit is set", func() {
   318  					bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   319  						openFn: func() io.ReadCloser {
   320  							tarBuilder := archive.TarBuilder{}
   321  							tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(bpTOMLData))
   322  							tarBuilder.AddFile("some-file", 0700, time.Now(), []byte("some-data"))
   323  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   324  						},
   325  					}, archive.DefaultTarWriterFactory(), nil)
   326  					h.AssertNil(t, err)
   327  
   328  					tarPath := writeBlobToFile(bp)
   329  					defer os.Remove(tarPath)
   330  
   331  					h.AssertOnTarEntry(t, tarPath,
   332  						"/cnb/buildpacks/bp.one/1.2.3/some-file",
   333  						h.HasFileMode(0755),
   334  					)
   335  				})
   336  			})
   337  
   338  			when("not directory, 'bin/detect', or 'bin/build'", func() {
   339  				it("sets to 0644 if NO exec bits set", func() {
   340  					bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   341  						openFn: func() io.ReadCloser {
   342  							tarBuilder := archive.TarBuilder{}
   343  							tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(bpTOMLData))
   344  							tarBuilder.AddFile("some-file", 0600, time.Now(), []byte("some-data"))
   345  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   346  						},
   347  					}, archive.DefaultTarWriterFactory(), nil)
   348  					h.AssertNil(t, err)
   349  
   350  					tarPath := writeBlobToFile(bp)
   351  					defer os.Remove(tarPath)
   352  
   353  					h.AssertOnTarEntry(t, tarPath,
   354  						"/cnb/buildpacks/bp.one/1.2.3/some-file",
   355  						h.HasFileMode(0644),
   356  					)
   357  				})
   358  			})
   359  		})
   360  
   361  		when("there is no descriptor file", func() {
   362  			it("returns error", func() {
   363  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   364  					openFn: func() io.ReadCloser {
   365  						tarBuilder := archive.TarBuilder{}
   366  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   367  					},
   368  				}, archive.DefaultTarWriterFactory(), nil)
   369  				h.AssertError(t, err, "could not find entry path 'buildpack.toml'")
   370  			})
   371  		})
   372  
   373  		when("there is no api field", func() {
   374  			it("assumes an api version", func() {
   375  				bp, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   376  					openFn: func() io.ReadCloser {
   377  						tarBuilder := archive.TarBuilder{}
   378  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   379  [buildpack]
   380  id = "bp.one"
   381  version = "1.2.3"
   382  
   383  [[stacks]]
   384  id = "some.stack.id"`))
   385  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   386  					},
   387  				}, archive.DefaultTarWriterFactory(), nil)
   388  				h.AssertNil(t, err)
   389  				h.AssertEq(t, bp.Descriptor().API().String(), "0.1")
   390  			})
   391  		})
   392  
   393  		when("there is no id", func() {
   394  			it("returns error", func() {
   395  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   396  					openFn: func() io.ReadCloser {
   397  						tarBuilder := archive.TarBuilder{}
   398  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   399  [buildpack]
   400  id = ""
   401  version = "1.2.3"
   402  
   403  [[stacks]]
   404  id = "some.stack.id"`))
   405  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   406  					},
   407  				}, archive.DefaultTarWriterFactory(), nil)
   408  				h.AssertError(t, err, "'buildpack.id' is required")
   409  			})
   410  		})
   411  
   412  		when("there is no version", func() {
   413  			it("returns error", func() {
   414  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   415  					openFn: func() io.ReadCloser {
   416  						tarBuilder := archive.TarBuilder{}
   417  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   418  [buildpack]
   419  id = "bp.one"
   420  version = ""
   421  
   422  [[stacks]]
   423  id = "some.stack.id"`))
   424  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   425  					},
   426  				}, archive.DefaultTarWriterFactory(), nil)
   427  				h.AssertError(t, err, "'buildpack.version' is required")
   428  			})
   429  		})
   430  
   431  		when("both stacks and order are present", func() {
   432  			it("returns error", func() {
   433  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   434  					openFn: func() io.ReadCloser {
   435  						tarBuilder := archive.TarBuilder{}
   436  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   437  [buildpack]
   438  id = "bp.one"
   439  version = "1.2.3"
   440  
   441  [[stacks]]
   442  id = "some.stack.id"
   443  
   444  [[order]]
   445  [[order.group]]
   446    id = "bp.nested"
   447    version = "bp.nested.version"
   448  `))
   449  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   450  					},
   451  				}, archive.DefaultTarWriterFactory(), nil)
   452  				h.AssertError(t, err, "cannot have both 'targets'/'stacks' and an 'order' defined")
   453  			})
   454  		})
   455  
   456  		when("missing stacks and order", func() {
   457  			it("does not return an error", func() {
   458  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   459  					openFn: func() io.ReadCloser {
   460  						tarBuilder := archive.TarBuilder{}
   461  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   462  [buildpack]
   463  id = "bp.one"
   464  version = "1.2.3"
   465  `))
   466  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   467  					},
   468  				}, archive.DefaultTarWriterFactory(), nil)
   469  				h.AssertNil(t, err)
   470  			})
   471  		})
   472  
   473  		when("hardlink is present", func() {
   474  			var bpRootFolder string
   475  
   476  			it.Before(func() {
   477  				bpRootFolder = filepath.Join("testdata", "buildpack-with-hardlink")
   478  				// create a hard link
   479  				err := os.Link(filepath.Join(bpRootFolder, "original-file"), filepath.Join(bpRootFolder, "original-file-2"))
   480  				h.AssertNil(t, err)
   481  			})
   482  
   483  			it.After(func() {
   484  				os.RemoveAll(filepath.Join(bpRootFolder, "original-file-2"))
   485  			})
   486  
   487  			it("hardlink is preserved in the output tar file", func() {
   488  				bp, err := buildpack.FromBuildpackRootBlob(blob.NewBlob(bpRootFolder), archive.DefaultTarWriterFactory(), nil)
   489  				h.AssertNil(t, err)
   490  
   491  				tarPath := writeBlobToFile(bp)
   492  				defer os.Remove(tarPath)
   493  
   494  				h.AssertOnTarEntries(t, tarPath,
   495  					"/cnb/buildpacks/bp.one/1.2.3/original-file",
   496  					"/cnb/buildpacks/bp.one/1.2.3/original-file-2",
   497  					h.AreEquivalentHardLinks(),
   498  				)
   499  			})
   500  		})
   501  
   502  		when("there are wrong things in the file", func() {
   503  			it("warns", func() {
   504  				outBuf := bytes.Buffer{}
   505  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   506  				_, err := buildpack.FromBuildpackRootBlob(&readerBlob{
   507  					openFn: func() io.ReadCloser {
   508  						tarBuilder := archive.TarBuilder{}
   509  						tarBuilder.AddFile("buildpack.toml", 0700, time.Now(), []byte(`
   510  api = "0.3"
   511  
   512  [buildpack]
   513  id = "bp.one"
   514  version = "1.2.3"
   515  homepage = "http://geocities.com/cool-bp"
   516  
   517  [[targets]]
   518  os = "some-os"
   519  arch = "some-arch"
   520  variant = "some-arch-variant"
   521  [[targets.distributions]]
   522  name = "some-distro-name"
   523  version = "some-distro-version"
   524  [[targets.distros]]
   525  name = "some-distro-name"
   526  versions = ["some-distro-version"]
   527  `))
   528  						return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   529  					},
   530  				}, archive.DefaultTarWriterFactory(), logger)
   531  				h.AssertNil(t, err)
   532  				h.AssertContains(t, outBuf.String(), "Warning: Ignoring unexpected key(s) in descriptor for buildpack bp.one: targets.distributions,targets.distributions.name,targets.distributions.version,targets.distros.versions")
   533  			})
   534  		})
   535  	})
   536  
   537  	when("#Match", func() {
   538  		it("compares, using only the id and version", func() {
   539  			other := dist.ModuleInfo{
   540  				ID:          "same",
   541  				Version:     "1.2.3",
   542  				Description: "something else",
   543  				Homepage:    "something else",
   544  				Keywords:    []string{"something", "else"},
   545  				Licenses: []dist.License{
   546  					{
   547  						Type: "MIT",
   548  						URI:  "https://example.com",
   549  					},
   550  				},
   551  			}
   552  
   553  			self := dist.ModuleInfo{
   554  				ID:      "same",
   555  				Version: "1.2.3",
   556  			}
   557  
   558  			match := self.Match(other)
   559  
   560  			h.AssertEq(t, match, true)
   561  
   562  			self.ID = "different"
   563  			match = self.Match(other)
   564  
   565  			h.AssertEq(t, match, false)
   566  		})
   567  	})
   568  
   569  	when("#Set", func() {
   570  		it("creates a set", func() {
   571  			values := []string{"a", "b", "c", "a"}
   572  			set := buildpack.Set(values)
   573  			h.AssertEq(t, len(set), 3)
   574  		})
   575  	})
   576  
   577  	when("#ToNLayerTar", func() {
   578  		var (
   579  			tmpDir     string
   580  			expectedBP []expectedBuildpack
   581  			err        error
   582  		)
   583  
   584  		it.Before(func() {
   585  			tmpDir, err = os.MkdirTemp("", "")
   586  			h.AssertNil(t, err)
   587  		})
   588  
   589  		it.After(func() {
   590  			err := os.RemoveAll(tmpDir)
   591  			if runtime.GOOS != "windows" {
   592  				// avoid "The process cannot access the file because it is being used by another process"
   593  				// error on Windows
   594  				h.AssertNil(t, err)
   595  			}
   596  		})
   597  
   598  		when("BuildModule contains only an individual buildpack (default)", func() {
   599  			it.Before(func() {
   600  				expectedBP = []expectedBuildpack{
   601  					{
   602  						id:      "buildpack-1-id",
   603  						version: "buildpack-1-version-1",
   604  					},
   605  				}
   606  			})
   607  
   608  			it("returns 1 tar files", func() {
   609  				bp := buildpack.FromBlob(
   610  					&dist.BuildpackDescriptor{
   611  						WithAPI: api.MustParse("0.3"),
   612  						WithInfo: dist.ModuleInfo{
   613  							ID:      "buildpack-1-id",
   614  							Version: "buildpack-1-version-1",
   615  							Name:    "buildpack-1",
   616  						},
   617  					},
   618  					&readerBlob{
   619  						openFn: func() io.ReadCloser {
   620  							tarBuilder := archive.TarBuilder{}
   621  
   622  							// Buildpack 1
   623  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id", 0700, time.Now())
   624  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1", 0700, time.Now())
   625  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   626  api = "0.3"
   627  
   628  [buildpack]
   629  id = "buildpack-1-id"
   630  version = "buildpack-1-version-1"
   631  
   632  `))
   633  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin", 0700, time.Now())
   634  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/detect", 0700, time.Now(), []byte("detect-contents"))
   635  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/build", 0700, time.Now(), []byte("build-contents"))
   636  
   637  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   638  						},
   639  					},
   640  				)
   641  
   642  				tarPaths, err := buildpack.ToNLayerTar(tmpDir, bp)
   643  				h.AssertNil(t, err)
   644  				h.AssertEq(t, len(tarPaths), 1)
   645  				assertBuildpacksToTar(t, tarPaths, expectedBP)
   646  			})
   647  		})
   648  
   649  		when("BuildModule contains N flattened buildpacks", func() {
   650  			it.Before(func() {
   651  				expectedBP = []expectedBuildpack{
   652  					{
   653  						id:      "buildpack-1-id",
   654  						version: "buildpack-1-version-1",
   655  					},
   656  					{
   657  						id:      "buildpack-2-id",
   658  						version: "buildpack-2-version-1",
   659  					},
   660  				}
   661  			})
   662  			when("not running on windows", func() {
   663  				it("returns N tar files", func() {
   664  					h.SkipIf(t, runtime.GOOS == "windows", "")
   665  					bp := buildpack.FromBlob(
   666  						&dist.BuildpackDescriptor{
   667  							WithAPI: api.MustParse("0.3"),
   668  							WithInfo: dist.ModuleInfo{
   669  								ID:      "buildpack-1-id",
   670  								Version: "buildpack-1-version-1",
   671  								Name:    "buildpack-1",
   672  							},
   673  						},
   674  						&readerBlob{
   675  							openFn: func() io.ReadCloser {
   676  								tarBuilder := archive.TarBuilder{}
   677  
   678  								// Buildpack 1
   679  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id", 0700, time.Now())
   680  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1", 0700, time.Now())
   681  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   682  api = "0.3"
   683  
   684  [buildpack]
   685  id = "buildpack-1-id"
   686  version = "buildpack-1-version-1"
   687  
   688  `))
   689  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin", 0700, time.Now())
   690  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/detect", 0700, time.Now(), []byte("detect-contents"))
   691  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/build", 0700, time.Now(), []byte("build-contents"))
   692  
   693  								// Buildpack 2
   694  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-2-id", 0700, time.Now())
   695  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1", 0700, time.Now())
   696  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   697  api = "0.3"
   698  
   699  [buildpack]
   700  id = "buildpack-2-id"
   701  version = "buildpack-2-version-1"
   702  
   703  `))
   704  								tarBuilder.AddDir("/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin", 0700, time.Now())
   705  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin/detect", 0700, time.Now(), []byte("detect-contents"))
   706  								tarBuilder.AddFile("/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin/build", 0700, time.Now(), []byte("build-contents"))
   707  
   708  								return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   709  							},
   710  						},
   711  					)
   712  
   713  					tarPaths, err := buildpack.ToNLayerTar(tmpDir, bp)
   714  					h.AssertNil(t, err)
   715  					h.AssertEq(t, len(tarPaths), 2)
   716  					assertBuildpacksToTar(t, tarPaths, expectedBP)
   717  				})
   718  			})
   719  
   720  			when("running on windows", func() {
   721  				it("returns N tar files", func() {
   722  					h.SkipIf(t, runtime.GOOS != "windows", "")
   723  					bp := buildpack.FromBlob(
   724  						&dist.BuildpackDescriptor{
   725  							WithAPI: api.MustParse("0.3"),
   726  							WithInfo: dist.ModuleInfo{
   727  								ID:      "buildpack-1-id",
   728  								Version: "buildpack-1-version-1",
   729  								Name:    "buildpack-1",
   730  							},
   731  						},
   732  						&readerBlob{
   733  							openFn: func() io.ReadCloser {
   734  								tarBuilder := archive.TarBuilder{}
   735  								// Windows tar format
   736  								tarBuilder.AddDir("Files", 0700, time.Now())
   737  								tarBuilder.AddDir("Hives", 0700, time.Now())
   738  								tarBuilder.AddDir("Files/cnb", 0700, time.Now())
   739  								tarBuilder.AddDir("Files/cnb/builpacks", 0700, time.Now())
   740  
   741  								// Buildpack 1
   742  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-1-id", 0700, time.Now())
   743  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1", 0700, time.Now())
   744  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   745  api = "0.3"
   746  
   747  [buildpack]
   748  id = "buildpack-1-id"
   749  version = "buildpack-1-version-1"
   750  
   751  `))
   752  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin", 0700, time.Now())
   753  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/detect.bat", 0700, time.Now(), []byte("detect-contents"))
   754  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/build.bat", 0700, time.Now(), []byte("build-contents"))
   755  
   756  								// Buildpack 2
   757  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-2-id", 0700, time.Now())
   758  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1", 0700, time.Now())
   759  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   760  api = "0.3"
   761  
   762  [buildpack]
   763  id = "buildpack-2-id"
   764  version = "buildpack-2-version-1"
   765  
   766  `))
   767  								tarBuilder.AddDir("Files/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin", 0700, time.Now())
   768  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin/detect.bat", 0700, time.Now(), []byte("detect-contents"))
   769  								tarBuilder.AddFile("Files/cnb/buildpacks/buildpack-2-id/buildpack-2-version-1/bin/build.bat", 0700, time.Now(), []byte("build-contents"))
   770  
   771  								return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   772  							},
   773  						},
   774  					)
   775  
   776  					tarPaths, err := buildpack.ToNLayerTar(tmpDir, bp)
   777  					h.AssertNil(t, err)
   778  					h.AssertEq(t, len(tarPaths), 2)
   779  					assertWindowsBuildpacksToTar(t, tarPaths, expectedBP)
   780  				})
   781  			})
   782  		})
   783  
   784  		when("BuildModule contains buildpacks with same ID but different versions", func() {
   785  			it.Before(func() {
   786  				expectedBP = []expectedBuildpack{
   787  					{
   788  						id:      "buildpack-1-id",
   789  						version: "buildpack-1-version-1",
   790  					},
   791  					{
   792  						id:      "buildpack-1-id",
   793  						version: "buildpack-1-version-2",
   794  					},
   795  				}
   796  			})
   797  
   798  			it("returns N tar files one per each version", func() {
   799  				bp := buildpack.FromBlob(
   800  					&dist.BuildpackDescriptor{
   801  						WithAPI: api.MustParse("0.3"),
   802  						WithInfo: dist.ModuleInfo{
   803  							ID:      "buildpack-1-id",
   804  							Version: "buildpack-1-version-1",
   805  							Name:    "buildpack-1",
   806  						},
   807  					},
   808  					&readerBlob{
   809  						openFn: func() io.ReadCloser {
   810  							tarBuilder := archive.TarBuilder{}
   811  
   812  							// Buildpack 1
   813  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id", 0700, time.Now())
   814  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1", 0700, time.Now())
   815  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/buildpack.toml", 0700, time.Now(), []byte(`
   816  api = "0.3"
   817  
   818  [buildpack]
   819  id = "buildpack-1-id"
   820  version = "buildpack-1-version-1"
   821  
   822  `))
   823  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin", 0700, time.Now())
   824  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/detect", 0700, time.Now(), []byte("detect-contents"))
   825  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/bin/build", 0700, time.Now(), []byte("build-contents"))
   826  
   827  							// Buildpack 2 same as before but with different version
   828  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-2", 0700, time.Now())
   829  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-2/buildpack.toml", 0700, time.Now(), []byte(`
   830  api = "0.3"
   831  
   832  [buildpack]
   833  id = "buildpack-2-id"
   834  version = "buildpack-2-version-1"
   835  
   836  `))
   837  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-2/bin", 0700, time.Now())
   838  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-2/bin/detect", 0700, time.Now(), []byte("detect-contents"))
   839  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-2/bin/build", 0700, time.Now(), []byte("build-contents"))
   840  
   841  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   842  						},
   843  					},
   844  				)
   845  
   846  				tarPaths, err := buildpack.ToNLayerTar(tmpDir, bp)
   847  				h.AssertNil(t, err)
   848  				h.AssertEq(t, len(tarPaths), 2)
   849  				assertBuildpacksToTar(t, tarPaths, expectedBP)
   850  			})
   851  		})
   852  
   853  		when("BuildModule could not be read", func() {
   854  			it("surfaces errors encountered while reading blob", func() {
   855  				_, err = buildpack.ToNLayerTar(tmpDir, &errorBuildModule{})
   856  				h.AssertError(t, err, "opening blob")
   857  			})
   858  		})
   859  
   860  		when("BuildModule is empty", func() {
   861  			it("returns a path to an empty tarball", func() {
   862  				bp := buildpack.FromBlob(
   863  					&dist.BuildpackDescriptor{
   864  						WithAPI: api.MustParse("0.3"),
   865  						WithInfo: dist.ModuleInfo{
   866  							ID:      "buildpack-1-id",
   867  							Version: "buildpack-1-version-1",
   868  							Name:    "buildpack-1",
   869  						},
   870  					},
   871  					&readerBlob{
   872  						openFn: func() io.ReadCloser {
   873  							return io.NopCloser(strings.NewReader(""))
   874  						},
   875  					},
   876  				)
   877  
   878  				tarPaths, err := buildpack.ToNLayerTar(tmpDir, bp)
   879  				h.AssertNil(t, err)
   880  				h.AssertEq(t, len(tarPaths), 1)
   881  				h.AssertNotNil(t, tarPaths[0].Path())
   882  			})
   883  		})
   884  
   885  		when("BuildModule contains unexpected elements in the tarball file", func() {
   886  			it.Before(func() {
   887  				expectedBP = []expectedBuildpack{
   888  					{
   889  						id:      "buildpack-1-id",
   890  						version: "buildpack-1-version-1",
   891  					},
   892  				}
   893  			})
   894  
   895  			it("throws an error", func() {
   896  				bp := buildpack.FromBlob(
   897  					&dist.BuildpackDescriptor{
   898  						WithAPI: api.MustParse("0.3"),
   899  						WithInfo: dist.ModuleInfo{
   900  							ID:      "buildpack-1-id",
   901  							Version: "buildpack-1-version-1",
   902  							Name:    "buildpack-1",
   903  						},
   904  					},
   905  					&readerBlob{
   906  						openFn: func() io.ReadCloser {
   907  							tarBuilder := archive.TarBuilder{}
   908  
   909  							// Buildpack 1
   910  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id", 0700, time.Now())
   911  							tarBuilder.AddDir("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1", 0700, time.Now())
   912  							tarBuilder.AddFile("/cnb/buildpacks/buildpack-1-id/buildpack-1-version-1/../hack", 0700, time.Now(), []byte("harmful content"))
   913  							return tarBuilder.Reader(archive.DefaultTarWriterFactory())
   914  						},
   915  					},
   916  				)
   917  
   918  				_, err = buildpack.ToNLayerTar(tmpDir, bp)
   919  				h.AssertError(t, err, "contains unexpected special elements")
   920  			})
   921  		})
   922  	})
   923  }
   924  
   925  type errorBlob struct {
   926  	count    int
   927  	limit    int
   928  	realBlob buildpack.Blob
   929  }
   930  
   931  func (e *errorBlob) Open() (io.ReadCloser, error) {
   932  	if e.count < e.limit {
   933  		e.count += 1
   934  		return e.realBlob.Open()
   935  	}
   936  	return nil, fmt.Errorf("error from errBlob (reached limit of %d)", e.limit)
   937  }
   938  
   939  type readerBlob struct {
   940  	openFn func() io.ReadCloser
   941  }
   942  
   943  func (r *readerBlob) Open() (io.ReadCloser, error) {
   944  	return r.openFn(), nil
   945  }
   946  
   947  type errorBuildModule struct {
   948  }
   949  
   950  func (eb *errorBuildModule) Open() (io.ReadCloser, error) {
   951  	return nil, errors.New("something happened opening the build module")
   952  }
   953  
   954  func (eb *errorBuildModule) Descriptor() buildpack.Descriptor {
   955  	return nil
   956  }
   957  
   958  type expectedBuildpack struct {
   959  	id      string
   960  	version string
   961  }
   962  
   963  func assertBuildpacksToTar(t *testing.T, actual []buildpack.ModuleTar, expected []expectedBuildpack) {
   964  	t.Helper()
   965  	for _, expectedBP := range expected {
   966  		found := false
   967  		for _, moduleTar := range actual {
   968  			if expectedBP.id == moduleTar.Info().ID && expectedBP.version == moduleTar.Info().Version {
   969  				found = true
   970  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s", expectedBP.id),
   971  					h.IsDirectory(),
   972  				)
   973  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s/%s", expectedBP.id, expectedBP.version),
   974  					h.IsDirectory(),
   975  				)
   976  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s/%s/bin", expectedBP.id, expectedBP.version),
   977  					h.IsDirectory(),
   978  				)
   979  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s/%s/bin/build", expectedBP.id, expectedBP.version),
   980  					h.HasFileMode(0700),
   981  				)
   982  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s/%s/bin/detect", expectedBP.id, expectedBP.version),
   983  					h.HasFileMode(0700),
   984  				)
   985  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("/cnb/buildpacks/%s/%s/buildpack.toml", expectedBP.id, expectedBP.version),
   986  					h.HasFileMode(0700),
   987  				)
   988  				break
   989  			}
   990  		}
   991  		h.AssertTrue(t, found)
   992  	}
   993  }
   994  
   995  func assertWindowsBuildpacksToTar(t *testing.T, actual []buildpack.ModuleTar, expected []expectedBuildpack) {
   996  	t.Helper()
   997  	for _, expectedBP := range expected {
   998  		found := false
   999  		for _, moduleTar := range actual {
  1000  			if expectedBP.id == moduleTar.Info().ID && expectedBP.version == moduleTar.Info().Version {
  1001  				found = true
  1002  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s", expectedBP.id),
  1003  					h.IsDirectory(),
  1004  				)
  1005  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s/%s", expectedBP.id, expectedBP.version),
  1006  					h.IsDirectory(),
  1007  				)
  1008  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s/%s/bin", expectedBP.id, expectedBP.version),
  1009  					h.IsDirectory(),
  1010  				)
  1011  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s/%s/bin/build.bat", expectedBP.id, expectedBP.version),
  1012  					h.HasFileMode(0700),
  1013  				)
  1014  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s/%s/bin/detect.bat", expectedBP.id, expectedBP.version),
  1015  					h.HasFileMode(0700),
  1016  				)
  1017  				h.AssertOnTarEntry(t, moduleTar.Path(), fmt.Sprintf("Files/cnb/buildpacks/%s/%s/buildpack.toml", expectedBP.id, expectedBP.version),
  1018  					h.HasFileMode(0700),
  1019  				)
  1020  				break
  1021  			}
  1022  		}
  1023  		h.AssertTrue(t, found)
  1024  	}
  1025  }