github.com/paketo-buildpacks/packit@v1.3.2-0.20211206231111-86b75c657449/build.go (about) 1 package packit 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 11 "github.com/BurntSushi/toml" 12 "github.com/Masterminds/semver/v3" 13 "github.com/paketo-buildpacks/packit/internal" 14 ) 15 16 // BuildFunc is the definition of a callback that can be invoked when the Build 17 // function is executed. Buildpack authors should implement a BuildFunc that 18 // performs the specific build phase operations for a buildpack. 19 type BuildFunc func(BuildContext) (BuildResult, error) 20 21 // BuildContext provides the contextual details that are made available by the 22 // buildpack lifecycle during the build phase. This context is populated by the 23 // Build function and passed to BuildFunc during execution. 24 type BuildContext struct { 25 // BuildpackInfo includes the details of the buildpack parsed from the 26 // buildpack.toml included in the buildpack contents. 27 BuildpackInfo BuildpackInfo 28 29 // CNBPath is the absolute path location of the buildpack contents. 30 // This path is useful for finding the buildpack.toml or any other 31 // files included in the buildpack. 32 CNBPath string 33 34 // Platform includes the platform context according to the specification: 35 // https://github.com/buildpacks/spec/blob/main/buildpack.md#build 36 Platform Platform 37 38 // Layers provides access to layers managed by the buildpack. It can be used 39 // to create new layers or retrieve cached layers from previous builds. 40 Layers Layers 41 42 // Plan includes the BuildpackPlan provided by the lifecycle as specified in 43 // the specification: 44 // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpack-plan-toml. 45 Plan BuildpackPlan 46 47 // Stack is the value of the chosen stack. This value is populated from the 48 // $CNB_STACK_ID environment variable. 49 Stack string 50 51 // WorkingDir is the location of the application source code as provided by 52 // the lifecycle. 53 WorkingDir string 54 } 55 56 // BuildResult allows buildpack authors to indicate the result of the build 57 // phase for a given buildpack. This result, returned in a BuildFunc callback, 58 // will be parsed and persisted by the Build function and returned to the 59 // lifecycle at the end of the build phase execution. 60 type BuildResult struct { 61 // Plan is the set of refinements to the Buildpack Plan that were performed 62 // during the build phase. 63 Plan BuildpackPlan 64 65 // Layers is a list of layers that will be persisted by the lifecycle at the 66 // end of the build phase. Layers not included in this list will not be made 67 // available to the lifecycle. 68 Layers []Layer 69 70 // Launch is the metadata that will be persisted as launch.toml according to 71 // the buildpack lifecycle specification: 72 // https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml 73 Launch LaunchMetadata 74 75 // Build is the metadata that will be persisted as build.toml according to 76 // the buildpack lifecycle specification: 77 // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildtoml-toml 78 Build BuildMetadata 79 } 80 81 // Build is an implementation of the build phase according to the Cloud Native 82 // Buildpacks specification. Calling this function with a BuildFunc will 83 // perform the build phase process. 84 func Build(f BuildFunc, options ...Option) { 85 config := OptionConfig{ 86 exitHandler: internal.NewExitHandler(), 87 args: os.Args, 88 tomlWriter: internal.NewTOMLWriter(), 89 envWriter: internal.NewEnvironmentWriter(), 90 fileWriter: internal.NewFileWriter(), 91 } 92 93 for _, option := range options { 94 config = option(config) 95 } 96 97 var ( 98 layersPath = config.args[1] 99 platformPath = config.args[2] 100 planPath = config.args[3] 101 ) 102 103 pwd, err := os.Getwd() 104 if err != nil { 105 config.exitHandler.Error(err) 106 return 107 } 108 109 var plan BuildpackPlan 110 _, err = toml.DecodeFile(planPath, &plan) 111 if err != nil { 112 config.exitHandler.Error(err) 113 return 114 } 115 116 cnbPath, ok := os.LookupEnv("CNB_BUILDPACK_DIR") 117 if !ok { 118 cnbPath = filepath.Clean(strings.TrimSuffix(config.args[0], filepath.Join("bin", "build"))) 119 } 120 121 var buildpackInfo struct { 122 APIVersion string `toml:"api"` 123 Buildpack BuildpackInfo `toml:"buildpack"` 124 } 125 126 _, err = toml.DecodeFile(filepath.Join(cnbPath, "buildpack.toml"), &buildpackInfo) 127 if err != nil { 128 config.exitHandler.Error(err) 129 return 130 } 131 132 apiV05, _ := semver.NewVersion("0.5") 133 apiV06, _ := semver.NewVersion("0.6") 134 apiVersion, err := semver.NewVersion(buildpackInfo.APIVersion) 135 if err != nil { 136 config.exitHandler.Error(err) 137 return 138 } 139 140 result, err := f(BuildContext{ 141 CNBPath: cnbPath, 142 Platform: Platform{ 143 Path: platformPath, 144 }, 145 Stack: os.Getenv("CNB_STACK_ID"), 146 WorkingDir: pwd, 147 Plan: plan, 148 Layers: Layers{ 149 Path: layersPath, 150 }, 151 BuildpackInfo: buildpackInfo.Buildpack, 152 }) 153 if err != nil { 154 config.exitHandler.Error(err) 155 return 156 } 157 158 if len(result.Plan.Entries) > 0 { 159 if apiVersion.GreaterThan(apiV05) || apiVersion.Equal(apiV05) { 160 config.exitHandler.Error(errors.New("buildpack plan is read only since Buildpack API v0.5")) 161 return 162 } 163 164 err = config.tomlWriter.Write(planPath, result.Plan) 165 if err != nil { 166 config.exitHandler.Error(err) 167 return 168 } 169 } 170 171 layerTomls, err := filepath.Glob(filepath.Join(layersPath, "*.toml")) 172 if err != nil { 173 config.exitHandler.Error(err) 174 return 175 } 176 177 if apiVersion.LessThan(apiV06) { 178 for _, file := range layerTomls { 179 if filepath.Base(file) != "launch.toml" && filepath.Base(file) != "store.toml" && filepath.Base(file) != "build.toml" { 180 err = os.Remove(file) 181 if err != nil { 182 config.exitHandler.Error(fmt.Errorf("failed to remove layer toml: %w", err)) 183 return 184 } 185 } 186 } 187 } 188 189 for _, layer := range result.Layers { 190 err = config.tomlWriter.Write(filepath.Join(layersPath, fmt.Sprintf("%s.toml", layer.Name)), formattedLayer{layer, apiVersion}) 191 if err != nil { 192 config.exitHandler.Error(err) 193 return 194 } 195 196 err = config.envWriter.Write(filepath.Join(layer.Path, "env"), layer.SharedEnv) 197 if err != nil { 198 config.exitHandler.Error(err) 199 return 200 } 201 202 err = config.envWriter.Write(filepath.Join(layer.Path, "env.launch"), layer.LaunchEnv) 203 if err != nil { 204 config.exitHandler.Error(err) 205 return 206 } 207 208 err = config.envWriter.Write(filepath.Join(layer.Path, "env.build"), layer.BuildEnv) 209 if err != nil { 210 config.exitHandler.Error(err) 211 return 212 } 213 214 for process, processEnv := range layer.ProcessLaunchEnv { 215 err = config.envWriter.Write(filepath.Join(layer.Path, "env.launch", process), processEnv) 216 if err != nil { 217 config.exitHandler.Error(err) 218 return 219 } 220 } 221 222 if layer.SBOM != nil { 223 if apiVersion.GreaterThan(apiV06) { 224 for _, format := range layer.SBOM.Formats() { 225 err = config.fileWriter.Write(filepath.Join(layersPath, fmt.Sprintf("%s.sbom.%s", layer.Name, format.Extension)), format.Content) 226 if err != nil { 227 config.exitHandler.Error(err) 228 return 229 } 230 } 231 } else { 232 config.exitHandler.Error(fmt.Errorf("%s.sbom.* output is only supported with Buildpack API v0.7 or higher", layer.Name)) 233 return 234 } 235 } 236 } 237 238 if !result.Launch.isEmpty() { 239 if apiVersion.LessThan(apiV05) && len(result.Launch.BOM) > 0 { 240 config.exitHandler.Error(errors.New("BOM entries in launch.toml is only supported with Buildpack API v0.5 or higher")) 241 return 242 } 243 244 type label struct { 245 Key string `toml:"key"` 246 Value string `toml:"value"` 247 } 248 249 var launch struct { 250 Processes []Process `toml:"processes"` 251 Slices []Slice `toml:"slices"` 252 Labels []label `toml:"labels"` 253 BOM []BOMEntry `toml:"bom"` 254 } 255 256 launch.Processes = result.Launch.Processes 257 if apiVersion.LessThan(apiV06) { 258 for _, process := range launch.Processes { 259 if process.Default { 260 config.exitHandler.Error(errors.New("processes can only be marked as default with Buildpack API v0.6 or higher")) 261 return 262 } 263 } 264 } 265 266 launch.Slices = result.Launch.Slices 267 launch.BOM = result.Launch.BOM 268 if len(result.Launch.Labels) > 0 { 269 launch.Labels = []label{} 270 for k, v := range result.Launch.Labels { 271 launch.Labels = append(launch.Labels, label{Key: k, Value: v}) 272 } 273 274 sort.Slice(launch.Labels, func(i, j int) bool { 275 return launch.Labels[i].Key < launch.Labels[j].Key 276 }) 277 } 278 279 err = config.tomlWriter.Write(filepath.Join(layersPath, "launch.toml"), launch) 280 if err != nil { 281 config.exitHandler.Error(err) 282 return 283 } 284 285 if result.Launch.SBOM != nil { 286 if apiVersion.GreaterThan(apiV06) { 287 for _, format := range result.Launch.SBOM.Formats() { 288 err = config.fileWriter.Write(filepath.Join(layersPath, fmt.Sprintf("launch.sbom.%s", format.Extension)), format.Content) 289 if err != nil { 290 config.exitHandler.Error(err) 291 return 292 } 293 } 294 } else { 295 config.exitHandler.Error(fmt.Errorf("launch.sbom.* output is only supported with Buildpack API v0.7 or higher")) 296 return 297 } 298 } 299 } 300 301 if !result.Build.isEmpty() { 302 if apiVersion.LessThan(apiV05) { 303 config.exitHandler.Error(fmt.Errorf("build.toml is only supported with Buildpack API v0.5 or higher")) 304 return 305 } 306 307 if result.Build.SBOM != nil { 308 if apiVersion.GreaterThan(apiV06) { 309 for _, format := range result.Build.SBOM.Formats() { 310 err = config.fileWriter.Write(filepath.Join(layersPath, fmt.Sprintf("build.sbom.%s", format.Extension)), format.Content) 311 if err != nil { 312 config.exitHandler.Error(err) 313 return 314 } 315 } 316 } else { 317 config.exitHandler.Error(fmt.Errorf("build.sbom.* output is only supported with Buildpack API v0.7 or higher")) 318 return 319 } 320 } 321 err = config.tomlWriter.Write(filepath.Join(layersPath, "build.toml"), result.Build) 322 if err != nil { 323 config.exitHandler.Error(err) 324 return 325 } 326 } 327 }