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  }