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  }