github.com/BarDweller/libpak@v0.0.0-20230630201634-8dd5cfc15ec9/carton/package.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 carton 18 19 import ( 20 "fmt" 21 "os" 22 "path/filepath" 23 "regexp" 24 "sort" 25 "text/template" 26 27 "github.com/BurntSushi/toml" 28 "github.com/buildpacks/libcnb" 29 "github.com/heroku/color" 30 31 "github.com/BarDweller/libpak" 32 "github.com/BarDweller/libpak/bard" 33 "github.com/BarDweller/libpak/effect" 34 "github.com/BarDweller/libpak/internal" 35 ) 36 37 // Package is an object that contains the configuration for building a package. 38 type Package struct { 39 40 // CacheLocation is the location to cache downloaded dependencies. 41 CacheLocation string 42 43 // DependencyFilters indicates which filters should be applied to exclude dependencies 44 DependencyFilters []string 45 46 // StrictDependencyFilters indicates that a filter must match both the ID and version, otherwise it must only match one of the two 47 StrictDependencyFilters bool 48 49 // IncludeDependencies indicates whether to include dependencies in build package. 50 IncludeDependencies bool 51 52 // Destination is the directory to create the build package in. 53 Destination string 54 55 // Source is the source directory of the buildpack. 56 Source string 57 58 // Version is a version to substitute into an existing buildpack.toml. 59 Version string 60 } 61 62 // Create creates a package. 63 func (p Package) Create(options ...Option) { 64 config := Config{ 65 entryWriter: internal.EntryWriter{}, 66 executor: effect.NewExecutor(), 67 exitHandler: internal.NewExitHandler(), 68 } 69 70 for _, option := range options { 71 config = option(config) 72 } 73 74 var ( 75 err error 76 file string 77 ) 78 79 logger := bard.NewLogger(os.Stdout) 80 81 // Is this a buildpack or an extension? 82 bpfile := filepath.Join(p.Source, "buildpack.toml") 83 extnfile := filepath.Join(p.Source, "extension.toml") 84 var metadataMap map[string]interface{} 85 var id string 86 var name string 87 var version string 88 var homepage string 89 extension := false 90 if _, err := os.Stat(bpfile); err == nil { 91 s, err := os.ReadFile(bpfile) 92 if err != nil { 93 config.exitHandler.Error(fmt.Errorf("unable to read buildpack.toml %s\n%w", bpfile, err)) 94 return 95 } 96 var b libcnb.Buildpack 97 if err := toml.Unmarshal(s, &b); err != nil { 98 config.exitHandler.Error(fmt.Errorf("unable to decode %s\n%w", bpfile, err)) 99 return 100 } 101 metadataMap = b.Metadata 102 id = b.Info.ID 103 name = b.Info.Name 104 version = b.Info.Version 105 homepage = b.Info.Homepage 106 logger.Debug("Buildpack: %+v", b) 107 } else if _, err := os.Stat(extnfile); err == nil { 108 s, err := os.ReadFile(extnfile) 109 if err != nil { 110 config.exitHandler.Error(fmt.Errorf("unable to read extension.toml %s\n%w", extnfile, err)) 111 return 112 } 113 var e libcnb.Extension 114 if err := toml.Unmarshal(s, &e); err != nil { 115 config.exitHandler.Error(fmt.Errorf("unable to decode %s\n%w", extnfile, err)) 116 return 117 } 118 metadataMap = e.Metadata 119 id = e.Info.ID 120 name = e.Info.Name 121 version = e.Info.Version 122 homepage = e.Info.Homepage 123 extension = true 124 logger.Debug("Extension: %+v", e) 125 } else { 126 config.exitHandler.Error(fmt.Errorf("unable to read buildpack/extension.toml at %s", p.Source)) 127 return 128 } 129 130 metadata, err := libpak.NewBuildModuleMetadata(metadataMap) 131 if err != nil { 132 config.exitHandler.Error(fmt.Errorf("unable to decode metadata %s\n%w", metadataMap, err)) 133 return 134 } 135 136 entries := map[string]string{} 137 138 for _, i := range metadata.IncludeFiles { 139 entries[i] = filepath.Join(p.Source, i) 140 } 141 logger.Debug("Include files: %+v", entries) 142 143 if p.Version != "" { 144 version = p.Version 145 146 tomlName := "" 147 if extension { 148 tomlName = "extension" 149 } else { 150 tomlName = "buildpack" 151 } 152 153 file = filepath.Join(p.Source, tomlName+".toml") 154 t, err := template.ParseFiles(file) 155 if err != nil { 156 config.exitHandler.Error(fmt.Errorf("unable to parse template %s\n%w", file, err)) 157 return 158 } 159 160 out, err := os.CreateTemp("", tomlName+"-*.toml") 161 if err != nil { 162 config.exitHandler.Error(fmt.Errorf("unable to open temporary "+tomlName+".toml file\n%w", err)) 163 } 164 defer out.Close() 165 166 if err = t.Execute(out, map[string]interface{}{"version": p.Version}); err != nil { 167 config.exitHandler.Error(fmt.Errorf("unable to execute template %s with version %s\n%w", file, p.Version, err)) 168 return 169 } 170 171 entries[tomlName+".toml"] = out.Name() 172 } 173 174 logger.Title(name, version, homepage) 175 logger.Headerf("Creating package in %s", p.Destination) 176 177 if err = os.RemoveAll(p.Destination); err != nil { 178 config.exitHandler.Error(fmt.Errorf("unable to remove destination path %s\n%w", p.Destination, err)) 179 return 180 } 181 182 file = metadata.PrePackage 183 if file != "" { 184 logger.Headerf("Pre-package with %s", file) 185 execution := effect.Execution{ 186 Command: file, 187 Dir: p.Source, 188 Stdout: logger.BodyWriter(), 189 Stderr: logger.BodyWriter(), 190 } 191 192 if err = config.executor.Execute(execution); err != nil { 193 config.exitHandler.Error(fmt.Errorf("unable to execute pre-package script %s\n%w", file, err)) 194 } 195 } 196 197 if p.IncludeDependencies { 198 cache := libpak.DependencyCache{ 199 Logger: logger, 200 UserAgent: fmt.Sprintf("%s/%s", id, version), 201 } 202 203 if p.CacheLocation != "" { 204 cache.DownloadPath = p.CacheLocation 205 } else { 206 cache.DownloadPath = filepath.Join(p.Source, "dependencies") 207 } 208 209 np, err := NetrcPath() 210 if err != nil { 211 config.exitHandler.Error(fmt.Errorf("unable to determine netrc path\n%w", err)) 212 return 213 } 214 215 n, err := ParseNetrc(np) 216 if err != nil { 217 config.exitHandler.Error(fmt.Errorf("unable to read %s as netrc\n%w", np, err)) 218 return 219 } 220 221 for _, dep := range metadata.Dependencies { 222 if !p.matchDependency(dep) { 223 logger.Bodyf("Skipping [%s or %s] which matched a filter", dep.ID, dep.Version) 224 continue 225 } 226 227 logger.Headerf("Caching %s", color.BlueString("%s %s", dep.Name, dep.Version)) 228 229 f, err := cache.Artifact(dep, n.BasicAuth) 230 if err != nil { 231 config.exitHandler.Error(fmt.Errorf("unable to download %s\n%w", dep.URI, err)) 232 return 233 } 234 if err = f.Close(); err != nil { 235 config.exitHandler.Error(fmt.Errorf("unable to close %s\n%w", f.Name(), err)) 236 return 237 } 238 239 entries[fmt.Sprintf("dependencies/%s/%s", dep.SHA256, filepath.Base(f.Name()))] = f.Name() 240 entries[fmt.Sprintf("dependencies/%s.toml", dep.SHA256)] = fmt.Sprintf("%s.toml", filepath.Dir(f.Name())) 241 } 242 } 243 244 var files []string 245 for d := range entries { 246 files = append(files, d) 247 } 248 sort.Strings(files) 249 for _, d := range files { 250 logger.Bodyf("Adding %s", d) 251 file = filepath.Join(p.Destination, d) 252 if err = config.entryWriter.Write(entries[d], file); err != nil { 253 config.exitHandler.Error(fmt.Errorf("unable to write file %s to %s\n%w", entries[d], file, err)) 254 return 255 } 256 } 257 } 258 259 // matchDependency checks all filters against dependency and returns true if there is a match (or no filters) and false if there is no match 260 // There is a match if a regular expression matches against the ID or Version 261 func (p Package) matchDependency(dep libpak.BuildModuleDependency) bool { 262 if len(p.DependencyFilters) == 0 { 263 return true 264 } 265 266 for _, rawFilter := range p.DependencyFilters { 267 filter := regexp.MustCompile(rawFilter) 268 269 if (p.StrictDependencyFilters && filter.MatchString(dep.ID) && filter.MatchString(dep.Version)) || 270 (!p.StrictDependencyFilters && (filter.MatchString(dep.ID) || filter.MatchString(dep.Version))) { 271 return true 272 } 273 } 274 275 return false 276 }