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  }