github.com/YousefHaggyHeroku/pack@v1.5.5/internal/dist/buildpack.go (about) 1 package dist 2 3 import ( 4 "archive/tar" 5 "io" 6 "path" 7 8 "github.com/BurntSushi/toml" 9 "github.com/buildpacks/lifecycle/api" 10 "github.com/pkg/errors" 11 12 "github.com/YousefHaggyHeroku/pack/internal/archive" 13 "github.com/YousefHaggyHeroku/pack/internal/style" 14 ) 15 16 const AssumedBuildpackAPIVersion = "0.1" 17 const BuildpacksDir = "/cnb/buildpacks" 18 19 type Blob interface { 20 // Open returns a io.ReadCloser for the contents of the Blob in tar format. 21 Open() (io.ReadCloser, error) 22 } 23 24 type buildpack struct { 25 descriptor BuildpackDescriptor 26 Blob `toml:"-"` 27 } 28 29 func (b *buildpack) Descriptor() BuildpackDescriptor { 30 return b.descriptor 31 } 32 33 //go:generate mockgen -package testmocks -destination testmocks/mock_buildpack.go github.com/YousefHaggyHeroku/pack/internal/dist Buildpack 34 type Buildpack interface { 35 // Open returns a reader to a tar with contents structured as per the distribution spec 36 // (currently '/cnbs/buildpacks/{ID}/{version}/*', all entries with a zeroed-out 37 // timestamp and root UID/GID). 38 Open() (io.ReadCloser, error) 39 Descriptor() BuildpackDescriptor 40 } 41 42 type BuildpackInfo struct { 43 ID string `toml:"id,omitempty" json:"id,omitempty" yaml:"id,omitempty"` 44 Version string `toml:"version,omitempty" json:"version,omitempty" yaml:"version,omitempty"` 45 Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty" yaml:"homepage,omitempty"` 46 } 47 48 func (b BuildpackInfo) FullName() string { 49 if b.Version != "" { 50 return b.ID + "@" + b.Version 51 } 52 return b.ID 53 } 54 55 // Satisfy stringer 56 func (b BuildpackInfo) String() string { return b.FullName() } 57 58 // Match compares two buildpacks by ID and Version 59 func (b BuildpackInfo) Match(o BuildpackInfo) bool { 60 return b.ID == o.ID && b.Version == o.Version 61 } 62 63 type Stack struct { 64 ID string `json:"id"` 65 Mixins []string `json:"mixins,omitempty"` 66 } 67 68 // BuildpackFromBlob constructs a buildpack from a blob. It is assumed that the buildpack 69 // contents are structured as per the distribution spec (currently '/cnbs/buildpacks/{ID}/{version}/*'). 70 func BuildpackFromBlob(bpd BuildpackDescriptor, blob Blob) Buildpack { 71 return &buildpack{ 72 Blob: blob, 73 descriptor: bpd, 74 } 75 } 76 77 // BuildpackFromRootBlob constructs a buildpack from a blob. It is assumed that the buildpack contents reside at the 78 // root of the blob. The constructed buildpack contents will be structured as per the distribution spec (currently 79 // a tar with contents under '/cnbs/buildpacks/{ID}/{version}/*'). 80 func BuildpackFromRootBlob(blob Blob, layerWriterFactory archive.TarWriterFactory) (Buildpack, error) { 81 bpd := BuildpackDescriptor{} 82 rc, err := blob.Open() 83 if err != nil { 84 return nil, errors.Wrap(err, "open buildpack") 85 } 86 defer rc.Close() 87 88 _, buf, err := archive.ReadTarEntry(rc, "buildpack.toml") 89 if err != nil { 90 return nil, errors.Wrap(err, "reading buildpack.toml") 91 } 92 93 bpd.API = api.MustParse(AssumedBuildpackAPIVersion) 94 _, err = toml.Decode(string(buf), &bpd) 95 if err != nil { 96 return nil, errors.Wrap(err, "decoding buildpack.toml") 97 } 98 99 err = validateDescriptor(bpd) 100 if err != nil { 101 return nil, errors.Wrap(err, "invalid buildpack.toml") 102 } 103 104 return &buildpack{ 105 descriptor: bpd, 106 Blob: &distBlob{ 107 openFn: func() io.ReadCloser { 108 return archive.GenerateTarWithWriter( 109 func(tw archive.TarWriter) error { 110 return toDistTar(tw, bpd, blob) 111 }, 112 layerWriterFactory, 113 ) 114 }, 115 }, 116 }, nil 117 } 118 119 type distBlob struct { 120 openFn func() io.ReadCloser 121 } 122 123 func (b *distBlob) Open() (io.ReadCloser, error) { 124 return b.openFn(), nil 125 } 126 127 func toDistTar(tw archive.TarWriter, bpd BuildpackDescriptor, blob Blob) error { 128 ts := archive.NormalizedDateTime 129 130 if err := tw.WriteHeader(&tar.Header{ 131 Typeflag: tar.TypeDir, 132 Name: path.Join(BuildpacksDir, bpd.EscapedID()), 133 Mode: 0755, 134 ModTime: ts, 135 }); err != nil { 136 return errors.Wrapf(err, "writing buildpack id dir header") 137 } 138 139 baseTarDir := path.Join(BuildpacksDir, bpd.EscapedID(), bpd.Info.Version) 140 if err := tw.WriteHeader(&tar.Header{ 141 Typeflag: tar.TypeDir, 142 Name: baseTarDir, 143 Mode: 0755, 144 ModTime: ts, 145 }); err != nil { 146 return errors.Wrapf(err, "writing buildpack version dir header") 147 } 148 149 rc, err := blob.Open() 150 if err != nil { 151 return errors.Wrap(err, "reading buildpack blob") 152 } 153 defer rc.Close() 154 155 tr := tar.NewReader(rc) 156 for { 157 header, err := tr.Next() 158 if err == io.EOF { 159 break 160 } 161 if err != nil { 162 return errors.Wrap(err, "failed to get next tar entry") 163 } 164 165 archive.NormalizeHeader(header, true) 166 header.Name = path.Clean(header.Name) 167 if header.Name == "." || header.Name == "/" { 168 continue 169 } 170 171 header.Mode = calcFileMode(header) 172 header.Name = path.Join(baseTarDir, header.Name) 173 err = tw.WriteHeader(header) 174 if err != nil { 175 return errors.Wrapf(err, "failed to write header for '%s'", header.Name) 176 } 177 178 _, err = io.Copy(tw, tr) 179 if err != nil { 180 return errors.Wrapf(err, "failed to write contents to '%s'", header.Name) 181 } 182 } 183 184 return nil 185 } 186 187 func calcFileMode(header *tar.Header) int64 { 188 switch { 189 case header.Typeflag == tar.TypeDir: 190 return 0755 191 case nameOneOf(header.Name, 192 path.Join("bin", "detect"), 193 path.Join("bin", "build"), 194 ): 195 return 0755 196 case anyExecBit(header.Mode): 197 return 0755 198 } 199 200 return 0644 201 } 202 203 func nameOneOf(name string, paths ...string) bool { 204 for _, p := range paths { 205 if name == p { 206 return true 207 } 208 } 209 return false 210 } 211 212 func anyExecBit(mode int64) bool { 213 return mode&0111 != 0 214 } 215 216 func validateDescriptor(bpd BuildpackDescriptor) error { 217 if bpd.Info.ID == "" { 218 return errors.Errorf("%s is required", style.Symbol("buildpack.id")) 219 } 220 221 if bpd.Info.Version == "" { 222 return errors.Errorf("%s is required", style.Symbol("buildpack.version")) 223 } 224 225 if len(bpd.Order) == 0 && len(bpd.Stacks) == 0 { 226 return errors.Errorf( 227 "buildpack %s: must have either %s or an %s defined", 228 style.Symbol(bpd.Info.FullName()), 229 style.Symbol("stacks"), 230 style.Symbol("order"), 231 ) 232 } 233 234 if len(bpd.Order) >= 1 && len(bpd.Stacks) >= 1 { 235 return errors.Errorf( 236 "buildpack %s: cannot have both %s and an %s defined", 237 style.Symbol(bpd.Info.FullName()), 238 style.Symbol("stacks"), 239 style.Symbol("order"), 240 ) 241 } 242 243 return nil 244 }