github.com/cloudfoundry/libcfbuildpack@v1.91.23/packager/cnbpackager/packager.go (about) 1 /* 2 * Copyright 2018-2020 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cnbpackager 18 19 import ( 20 "archive/tar" 21 "compress/gzip" 22 "errors" 23 "fmt" 24 "io" 25 "log" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "sort" 30 "strings" 31 templ "text/template" 32 33 "github.com/Masterminds/semver" 34 "github.com/fatih/color" 35 36 buildpackBp "github.com/buildpack/libbuildpack/buildpack" 37 layersBp "github.com/buildpack/libbuildpack/layers" 38 loggerBp "github.com/buildpack/libbuildpack/logger" 39 "github.com/cloudfoundry/libcfbuildpack/buildpack" 40 "github.com/cloudfoundry/libcfbuildpack/helper" 41 "github.com/cloudfoundry/libcfbuildpack/layers" 42 "github.com/cloudfoundry/libcfbuildpack/logger" 43 ) 44 45 const ( 46 DefaultDstDir = "packaged-cnb" 47 DefaultCacheBase = ".cnb-packager-cache" 48 ) 49 50 var identityColor = color.New(color.FgBlue) 51 52 type Packager struct { 53 buildpack buildpack.Buildpack 54 layers layers.Layers 55 logger logger.Logger 56 outputDirectory string 57 } 58 59 func New(bpDir, outputDir, version, cacheDir string) (Packager, error) { 60 l, err := loggerBp.DefaultLogger("") 61 if err != nil { 62 return Packager{}, err 63 } 64 65 if err := insertTemplateVersion(bpDir, version); err != nil { 66 return Packager{}, err 67 } 68 69 specBP, err := buildpackBp.New(bpDir, l) 70 if err != nil { 71 return Packager{}, err 72 } 73 74 log := logger.Logger{Logger: l} 75 b := buildpack.NewBuildpack(specBP, log) 76 77 depCache, err := filepath.Abs(filepath.Join(cacheDir, buildpack.CacheRoot)) 78 if err != nil { 79 return Packager{}, err 80 } 81 82 return Packager{ 83 b, 84 layers.NewLayers(layersBp.NewLayers(depCache, l), layersBp.NewLayers(depCache, l), b, log), 85 log, 86 outputDir, 87 }, nil 88 } 89 90 type pkgFile struct { 91 path string 92 packagePath string 93 } 94 95 func (p Packager) Create(cache bool) error { 96 p.logger.Title(p.buildpack) 97 98 if err := p.prePackage(); err != nil { 99 return err 100 } 101 102 includedFiles, err := p.buildpack.IncludeFiles() 103 if err != nil { 104 return err 105 } 106 107 var allFiles []pkgFile 108 for _, i := range includedFiles { 109 path, err := filepath.Abs(filepath.Join(p.buildpack.Root, i)) 110 if err != nil { 111 return err 112 } 113 f := pkgFile{ 114 path: path, 115 packagePath: i, 116 } 117 allFiles = append(allFiles, f) 118 } 119 120 if cache { 121 dependencyFiles, err := p.cacheDependencies() 122 if err != nil { 123 return err 124 } 125 allFiles = append(allFiles, dependencyFiles...) 126 } 127 128 return p.createPackage(allFiles) 129 } 130 131 func insertTemplateVersion(bpDir, version string) error { 132 bpTomlPath := filepath.Join(bpDir, "buildpack.toml") 133 v := struct { 134 Version string 135 }{ 136 Version: version, 137 } 138 139 template, err := templ.ParseFiles(bpTomlPath) 140 if err != nil { 141 return err 142 } 143 144 file, err := os.OpenFile(bpTomlPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 145 if err != nil { 146 log.Fatal(fmt.Errorf("failed to open buildpack.toml : %s", err)) 147 } 148 149 return template.Execute(file, v) 150 } 151 152 func (p Packager) cacheDependencies() ([]pkgFile, error) { 153 var files []pkgFile 154 155 deps, err := p.buildpack.Dependencies() 156 if err != nil { 157 return nil, err 158 } 159 160 for _, dep := range deps { 161 p.logger.Header("Caching %s", p.prettyIdentity(dep)) 162 163 layer := p.layers.DownloadLayer(dep) 164 165 a, err := layer.Artifact() 166 if err != nil { 167 return nil, err 168 } 169 170 f := pkgFile{ 171 path: a, 172 packagePath: filepath.Join(buildpack.CacheRoot, dep.SHA256, filepath.Base(a)), 173 } 174 175 metaF := pkgFile{ 176 path: layer.Metadata, 177 packagePath: filepath.Join(buildpack.CacheRoot, dep.SHA256+".toml"), 178 } 179 180 files = append(files, f, metaF) 181 } 182 183 return files, nil 184 } 185 186 func (Packager) prettyIdentity(v logger.Identifiable) string { 187 if v == nil { 188 return "" 189 } 190 191 name, description := v.Identity() 192 193 if description == "" { 194 return identityColor.Sprint(name) 195 } 196 197 return identityColor.Sprintf("%s %s", name, description) 198 } 199 200 func (p Packager) Archive() error { 201 defer os.RemoveAll(p.outputDirectory) 202 fileName := filepath.Base(p.outputDirectory) 203 tarFile := filepath.Join(filepath.Dir(p.outputDirectory), fileName+".tgz") 204 205 file, err := os.Create(tarFile) 206 if err != nil { 207 return err 208 } 209 defer file.Close() 210 211 gw := gzip.NewWriter(file) 212 defer gw.Close() 213 tw := tar.NewWriter(gw) 214 defer tw.Close() 215 216 filepath.Walk(p.outputDirectory, func(path string, info os.FileInfo, err error) error { 217 return p.addTarFile(tw, info, path) 218 }) 219 220 return nil 221 } 222 223 func (p Packager) addTarFile(tw *tar.Writer, info os.FileInfo, path string) error { 224 if !info.Mode().IsRegular() && !info.Mode().IsDir() { 225 return nil 226 } 227 228 if header, err := tar.FileInfoHeader(info, path); err == nil { 229 header.Name = stripBaseDirectory(p.outputDirectory, path) 230 231 if header.Name == "" { 232 return nil 233 } 234 235 if err := tw.WriteHeader(header); err != nil { 236 return err 237 } 238 239 if info.Mode().IsRegular() { 240 file, err := os.Open(path) 241 if err != nil { 242 return err 243 } 244 defer file.Close() 245 246 if _, err := io.Copy(tw, file); err != nil { 247 return err 248 } 249 } 250 } 251 return nil 252 } 253 254 func (p Packager) createPackage(files []pkgFile) error { 255 if len(files) == 0 { 256 return errors.New("no files included") 257 } 258 259 p.logger.Header("Creating package in %s", p.outputDirectory) 260 261 for _, file := range files { 262 p.logger.Body("Adding %s", file.packagePath) 263 outputDir := filepath.Dir(filepath.Join(p.outputDirectory, file.packagePath)) 264 if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { 265 return err 266 } 267 if err := helper.CopyFile(file.path, filepath.Join(p.outputDirectory, file.packagePath)); err != nil { 268 return err 269 } 270 } 271 return nil 272 } 273 274 func (p Packager) prePackage() error { 275 pp, ok := p.buildpack.PrePackage() 276 if !ok { 277 return nil 278 } 279 280 cmd := exec.Command(pp) 281 cmd.Stdout = os.Stdout 282 cmd.Stderr = os.Stderr 283 cmd.Dir = p.buildpack.Root 284 285 p.logger.Header("Pre-Package with %s", strings.Join(cmd.Args, " ")) 286 287 return cmd.Run() 288 } 289 290 func stripBaseDirectory(base, path string) string { 291 return strings.TrimPrefix(strings.Replace(path, base, "", -1), string(filepath.Separator)) 292 } 293 294 func (p Packager) Summary() (string, error) { 295 var out string 296 if err := p.depsSummary(&out); err != nil { 297 return "", err 298 } 299 300 p.defaultsSummary(&out) 301 p.stacksSummary(&out) 302 303 return out, nil 304 } 305 306 func (p Packager) depsSummary(out *string) error { 307 308 type depKey struct { 309 Idx int 310 ID string 311 Version string 312 } 313 314 bpMetadata := p.buildpack.Metadata 315 deps, ok := bpMetadata["dependencies"].([]map[string]interface{}) 316 if !ok || len(deps) == 0 { 317 return nil 318 } 319 320 *out = "\nPackaged binaries:\n\n" 321 *out += "| name | version | stacks |\n|-|-|-|\n" 322 323 depMap := map[depKey]buildpack.Stacks{} 324 for _, d := range deps { 325 dep, err := buildpack.NewDependency(d) 326 if err != nil { 327 return err 328 } 329 depKey := depKey{ 330 ID: dep.ID, 331 Version: dep.Version.Version.String(), 332 } 333 if _, ok := depMap[depKey]; !ok { 334 depMap[depKey] = dep.Stacks 335 } else { 336 depMap[depKey] = append(depMap[depKey], dep.Stacks...) 337 } 338 } 339 depKeyArray := make([]depKey, 0) 340 for key, _ := range depMap { 341 depKeyArray = append(depKeyArray, key) 342 } 343 344 sort.SliceStable(depKeyArray, func(i, j int) bool { 345 alph := strings.Compare(depKeyArray[i].ID, depKeyArray[j].ID) 346 if alph < 0 { 347 return true 348 } else if alph == 0 { 349 versionI, err := semver.NewVersion(depKeyArray[i].Version) 350 if err != nil { 351 return false 352 } 353 versionJ, err := semver.NewVersion(depKeyArray[j].Version) 354 if err != nil { 355 return false 356 } 357 return versionI.GreaterThan(versionJ) 358 } 359 return false 360 }) 361 362 for _, dKey := range depKeyArray { 363 stacks := depMap[dKey] 364 stackStringArray := []string{} 365 for _, stack := range stacks { 366 stackStringArray = append(stackStringArray, string(stack)) 367 } 368 *out += fmt.Sprintf("| %s | %s | %s |\n", dKey.ID, dKey.Version, strings.Join(stackStringArray, ", ")) 369 } 370 371 return nil 372 } 373 374 func (p Packager) defaultsSummary(out *string) { 375 bpMetadata := p.buildpack.Metadata 376 defaults, ok := bpMetadata[buildpack.DefaultVersions].(map[string]interface{}) 377 if !ok { 378 return 379 } 380 381 if len(defaults) > 0 { 382 *out += "\nDefault binary versions:\n\n" 383 *out += "| name | version |\n|-|-|\n" 384 for name, version := range defaults { 385 *out += fmt.Sprintf("| %s | %s |\n", name, version) 386 } 387 } 388 } 389 390 func (p Packager) stacksSummary(out *string) { 391 if len(p.buildpack.Stacks) < 1 { 392 return 393 } 394 395 *out += ` 396 Supported stacks: 397 398 | name | 399 |-| 400 ` 401 for _, stack := range p.buildpack.Stacks { 402 *out += fmt.Sprintf("| %s |\n", stack.ID) 403 } 404 }