github.com/paketoio/libpak@v1.3.1/buildpack.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 libpak 18 19 import ( 20 "fmt" 21 "sort" 22 23 "github.com/Masterminds/semver/v3" 24 "github.com/buildpacks/libcnb" 25 ) 26 27 // License represents a license that a BuildpackDependency is distributed under. At least one of Name or URI MUST be 28 // specified. 29 type BuildpackDependencyLicense struct { 30 31 // Type is the type of the license. This is typically the SPDX short identifier. 32 Type string `toml:"type"` 33 34 // URI is the location where the license can be found. 35 URI string `toml:"uri"` 36 } 37 38 // BuildpackDependency describes a dependency known to the buildpack. 39 type BuildpackDependency struct { 40 // ID is the dependency ID. 41 ID string `toml:"id"` 42 43 // Name is the dependency name. 44 Name string `toml:"name"` 45 46 // Version is the dependency version. 47 Version string `toml:"version"` 48 49 // URI is the dependency URI. 50 URI string `toml:"uri"` 51 52 // SHA256 is the hash of the dependency. 53 SHA256 string `toml:"sha256"` 54 55 // Stacks are the stacks the dependency is compatible with. 56 Stacks []string `toml:"stacks"` 57 58 // Licenses are the stacks the dependency is distributed under. 59 Licenses []BuildpackDependencyLicense `toml:"licenses"` 60 } 61 62 // BuildpackMetadata is an extension to libcnb.Buildpack's metadata with opinions. 63 type BuildpackMetadata struct { 64 65 // DefaultVersions represent the default versions for dependencies keyed by Dependency.Id. 66 DefaultVersions map[string]string 67 68 // Dependencies are the dependencies known to the buildpack. 69 Dependencies []BuildpackDependency 70 71 // IncludeFiles describes the files to include in the package. 72 IncludeFiles []string 73 74 // PrePackage describes a command to invoke before packaging. 75 PrePackage string 76 } 77 78 // NewBuildpackMetadata creates a new instance of BuildpackMetadata from the contents of libcnb.Buildpack.Metadata 79 func NewBuildpackMetadata(metadata map[string]interface{}) (BuildpackMetadata, error) { 80 m := BuildpackMetadata{ 81 DefaultVersions: map[string]string{}, 82 } 83 84 if v, ok := metadata["default-versions"].(map[string]interface{}); ok { 85 for k, v := range v { 86 m.DefaultVersions[k] = v.(string) 87 } 88 } 89 90 if v, ok := metadata["dependencies"]; ok { 91 for _, v := range v.([]map[string]interface{}) { 92 var d BuildpackDependency 93 94 if v, ok := v["id"].(string); ok { 95 d.ID = v 96 } 97 98 if v, ok := v["name"].(string); ok { 99 d.Name = v 100 } 101 102 if v, ok := v["version"].(string); ok { 103 d.Version = v 104 } 105 106 if v, ok := v["uri"].(string); ok { 107 d.URI = v 108 } 109 110 if v, ok := v["sha256"].(string); ok { 111 d.SHA256 = v 112 } 113 114 if v, ok := v["stacks"].([]interface{}); ok { 115 for _, v := range v { 116 d.Stacks = append(d.Stacks, v.(string)) 117 } 118 } 119 120 if v, ok := v["licenses"].([]map[string]interface{}); ok { 121 for _, v := range v { 122 var l BuildpackDependencyLicense 123 124 if v, ok := v["type"].(string); ok { 125 l.Type = v 126 } 127 128 if v, ok := v["uri"].(string); ok { 129 l.URI = v 130 } 131 132 d.Licenses = append(d.Licenses, l) 133 } 134 } 135 136 m.Dependencies = append(m.Dependencies, d) 137 } 138 } 139 140 if v, ok := metadata["include-files"].([]interface{}); ok { 141 for _, v := range v { 142 m.IncludeFiles = append(m.IncludeFiles, v.(string)) 143 } 144 } 145 146 if v, ok := metadata["pre-package"].(string); ok { 147 m.PrePackage = v 148 } 149 150 return m, nil 151 } 152 153 // DependencyResolver provides functionality for resolving a dependency fiven a collection of constraints. 154 type DependencyResolver struct { 155 156 // Dependencies are the dependencies to resolve against. 157 Dependencies []BuildpackDependency 158 159 // StackID is the stack id of the build. 160 StackID string 161 } 162 163 // NewDependencyResolver creates a new instance from the buildpack metadata and stack id. 164 func NewDependencyResolver(context libcnb.BuildContext) (DependencyResolver, error) { 165 md, err := NewBuildpackMetadata(context.Buildpack.Metadata) 166 if err != nil { 167 return DependencyResolver{}, fmt.Errorf("unable to unmarshal buildpack metadata: %w", err) 168 } 169 170 return DependencyResolver{Dependencies: md.Dependencies, StackID: context.StackID}, nil 171 } 172 173 // NoValidDependenciesError is returned when the resolver cannot find any valid dependencies given the constraints. 174 type NoValidDependenciesError struct { 175 // Message is the error message 176 Message string 177 } 178 179 func (n NoValidDependenciesError) Error() string { 180 return n.Message 181 } 182 183 // Resolve returns the latest version of a dependency within the collection of Dependencies. The candidate set is first 184 // filtered by the constraints, then the remaining candidates are sorted for the latest result by semver semantics. 185 // Version can contain wildcards and defaults to "*" if not specified. 186 func (d *DependencyResolver) Resolve(id string, version string) (BuildpackDependency, error) { 187 if version == "" { 188 version = "*" 189 } 190 191 vc, err := semver.NewConstraint(version) 192 if err != nil { 193 return BuildpackDependency{}, fmt.Errorf("invalid constraint %s: %w", vc, err) 194 } 195 196 var candidates []BuildpackDependency 197 for _, c := range d.Dependencies { 198 v, err := semver.NewVersion(c.Version) 199 if err != nil { 200 return BuildpackDependency{}, fmt.Errorf("unable to parse version %s: %w", c.Version, err) 201 } 202 203 if c.ID == id && vc.Check(v) && d.contains(c.Stacks, d.StackID) { 204 candidates = append(candidates, c) 205 } 206 } 207 208 if len(candidates) == 0 { 209 return BuildpackDependency{}, NoValidDependenciesError{ 210 Message: fmt.Sprintf("no valid dependencies for %s, %s, and %s in %s", 211 id, version, d.StackID, DependenciesFormatter(d.Dependencies)), 212 } 213 } 214 215 sort.Slice(candidates, func(i int, j int) bool { 216 a, _ := semver.NewVersion(candidates[i].Version) 217 b, _ := semver.NewVersion(candidates[j].Version) 218 219 return a.GreaterThan(b) 220 }) 221 222 return candidates[0], nil 223 } 224 225 // Any indicates whether the collection of dependencies has any dependency that satisfies the constraints. This is 226 // used primarily to determine whether an optional dependency exists, before calling Resolve() which would throw an 227 // error if one did not. 228 func (d *DependencyResolver) Any(id string, version string) bool { 229 _, err := d.Resolve(id, version) 230 return err == nil 231 } 232 233 func (DependencyResolver) contains(candidates []string, value string) bool { 234 for _, c := range candidates { 235 if c == value { 236 return true 237 } 238 } 239 240 return false 241 }