github.com/YousefHaggyHeroku/pack@v1.5.5/internal/builder/writer/human_readable.go (about)

     1  package writer
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"text/tabwriter"
     9  	"text/template"
    10  
    11  	"github.com/YousefHaggyHeroku/pack/internal/style"
    12  
    13  	"github.com/YousefHaggyHeroku/pack/internal/dist"
    14  
    15  	pubbldr "github.com/YousefHaggyHeroku/pack/builder"
    16  
    17  	"github.com/YousefHaggyHeroku/pack/internal/config"
    18  
    19  	"github.com/YousefHaggyHeroku/pack"
    20  	"github.com/YousefHaggyHeroku/pack/internal/builder"
    21  	"github.com/YousefHaggyHeroku/pack/logging"
    22  )
    23  
    24  const (
    25  	writerMinWidth     = 0
    26  	writerTabWidth     = 0
    27  	buildpacksTabWidth = 8
    28  	defaultTabWidth    = 4
    29  	writerPadChar      = ' '
    30  	writerFlags        = 0
    31  	none               = "(none)"
    32  
    33  	outputTemplate = `
    34  {{ if ne .Info.Description "" -}}
    35  Description: {{ .Info.Description }}
    36  
    37  {{ end -}}
    38  {{- if ne .Info.CreatedBy.Name "" -}}
    39  Created By:
    40    Name: {{ .Info.CreatedBy.Name }}
    41    Version: {{ .Info.CreatedBy.Version }}
    42  
    43  {{ end -}}
    44  
    45  Trusted: {{.Trusted}}
    46  
    47  Stack:
    48    ID: {{ .Info.Stack }}
    49  {{- if .Verbose}}
    50  {{- if ne (len .Info.Mixins) 0 }}
    51    Mixins:
    52  {{- end }}
    53  {{- range $index, $mixin := .Info.Mixins }}
    54      {{ $mixin }}
    55  {{- end }}
    56  {{- end }}
    57  {{ .Lifecycle }}
    58  {{ .RunImages }}
    59  {{ .Buildpacks }}
    60  {{ .Order }}`
    61  )
    62  
    63  type HumanReadable struct{}
    64  
    65  func NewHumanReadable() *HumanReadable {
    66  	return &HumanReadable{}
    67  }
    68  
    69  func (h *HumanReadable) Print(
    70  	logger logging.Logger,
    71  	localRunImages []config.RunImage,
    72  	local, remote *pack.BuilderInfo,
    73  	localErr, remoteErr error,
    74  	builderInfo SharedBuilderInfo,
    75  ) error {
    76  	if local == nil && remote == nil {
    77  		return fmt.Errorf("unable to find builder '%s' locally or remotely", builderInfo.Name)
    78  	}
    79  
    80  	if builderInfo.IsDefault {
    81  		logger.Infof("Inspecting default builder: %s\n", style.Symbol(builderInfo.Name))
    82  	} else {
    83  		logger.Infof("Inspecting builder: %s\n", style.Symbol(builderInfo.Name))
    84  	}
    85  
    86  	logger.Info("\nREMOTE:\n")
    87  	err := writeBuilderInfo(logger, localRunImages, remote, remoteErr, builderInfo)
    88  	if err != nil {
    89  		return fmt.Errorf("writing remote builder info: %w", err)
    90  	}
    91  	logger.Info("\nLOCAL:\n")
    92  	err = writeBuilderInfo(logger, localRunImages, local, localErr, builderInfo)
    93  	if err != nil {
    94  		return fmt.Errorf("writing local builder info: %w", err)
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  func writeBuilderInfo(
   101  	logger logging.Logger,
   102  	localRunImages []config.RunImage,
   103  	info *pack.BuilderInfo,
   104  	err error,
   105  	sharedInfo SharedBuilderInfo,
   106  ) error {
   107  	if err != nil {
   108  		logger.Errorf("%s\n", err)
   109  		return nil
   110  	}
   111  
   112  	if info == nil {
   113  		logger.Info("(not present)\n")
   114  		return nil
   115  	}
   116  
   117  	var warnings []string
   118  
   119  	runImagesString, runImagesWarnings, err := runImagesOutput(info.RunImage, localRunImages, info.RunImageMirrors, sharedInfo.Name)
   120  	if err != nil {
   121  		return fmt.Errorf("compiling run images output: %w", err)
   122  	}
   123  	orderString, orderWarnings, err := detectionOrderOutput(info.Order, sharedInfo.Name)
   124  	if err != nil {
   125  		return fmt.Errorf("compiling detection order output: %w", err)
   126  	}
   127  	buildpacksString, buildpacksWarnings, err := buildpacksOutput(info.Buildpacks, sharedInfo.Name)
   128  	if err != nil {
   129  		return fmt.Errorf("compiling buildpacks output: %w", err)
   130  	}
   131  	lifecycleString, lifecycleWarnings := lifecycleOutput(info.Lifecycle, sharedInfo.Name)
   132  
   133  	warnings = append(warnings, runImagesWarnings...)
   134  	warnings = append(warnings, orderWarnings...)
   135  	warnings = append(warnings, buildpacksWarnings...)
   136  	warnings = append(warnings, lifecycleWarnings...)
   137  
   138  	outputTemplate, _ := template.New("").Parse(outputTemplate)
   139  	err = outputTemplate.Execute(
   140  		logger.Writer(),
   141  		&struct {
   142  			Info       pack.BuilderInfo
   143  			Verbose    bool
   144  			Buildpacks string
   145  			RunImages  string
   146  			Order      string
   147  			Trusted    string
   148  			Lifecycle  string
   149  		}{
   150  			*info,
   151  			logger.IsVerbose(),
   152  			buildpacksString,
   153  			runImagesString,
   154  			orderString,
   155  			stringFromBool(sharedInfo.Trusted),
   156  			lifecycleString,
   157  		},
   158  	)
   159  
   160  	for _, warning := range warnings {
   161  		logger.Warn(warning)
   162  	}
   163  
   164  	return err
   165  }
   166  
   167  type trailingSpaceStrippingWriter struct {
   168  	output io.Writer
   169  
   170  	potentialDiscard []byte
   171  }
   172  
   173  func (w *trailingSpaceStrippingWriter) Write(p []byte) (n int, err error) {
   174  	var doWrite []byte
   175  
   176  	for _, b := range p {
   177  		switch b {
   178  		case writerPadChar:
   179  			w.potentialDiscard = append(w.potentialDiscard, b)
   180  		case '\n':
   181  			w.potentialDiscard = []byte{}
   182  			doWrite = append(doWrite, b)
   183  		default:
   184  			doWrite = append(doWrite, w.potentialDiscard...)
   185  			doWrite = append(doWrite, b)
   186  			w.potentialDiscard = []byte{}
   187  		}
   188  	}
   189  
   190  	if len(doWrite) > 0 {
   191  		actualWrote, err := w.output.Write(doWrite)
   192  		if err != nil {
   193  			return actualWrote, err
   194  		}
   195  	}
   196  
   197  	return len(p), nil
   198  }
   199  
   200  func stringFromBool(subject bool) string {
   201  	if subject {
   202  		return "Yes"
   203  	}
   204  
   205  	return "No"
   206  }
   207  
   208  func runImagesOutput(
   209  	runImage string,
   210  	localRunImages []config.RunImage,
   211  	buildRunImages []string,
   212  	builderName string,
   213  ) (string, []string, error) {
   214  	output := "Run Images:\n"
   215  
   216  	tabWriterBuf := bytes.Buffer{}
   217  
   218  	localMirrorTabWriter := tabwriter.NewWriter(&tabWriterBuf, writerMinWidth, writerTabWidth, defaultTabWidth, writerPadChar, writerFlags)
   219  	err := writeLocalMirrors(localMirrorTabWriter, runImage, localRunImages)
   220  	if err != nil {
   221  		return "", []string{}, fmt.Errorf("writing local mirrors: %w", err)
   222  	}
   223  
   224  	var warnings []string
   225  
   226  	if runImage != "" {
   227  		_, err = fmt.Fprintf(localMirrorTabWriter, "  %s\n", runImage)
   228  		if err != nil {
   229  			return "", []string{}, fmt.Errorf("writing to tabwriter: %w", err)
   230  		}
   231  	} else {
   232  		warnings = append(
   233  			warnings,
   234  			fmt.Sprintf("%s does not specify a run image", builderName),
   235  			"Users must build with an explicitly specified run image",
   236  		)
   237  	}
   238  	for _, m := range buildRunImages {
   239  		_, err = fmt.Fprintf(localMirrorTabWriter, "  %s\n", m)
   240  		if err != nil {
   241  			return "", []string{}, fmt.Errorf("writing to tab writer: %w", err)
   242  		}
   243  	}
   244  	err = localMirrorTabWriter.Flush()
   245  	if err != nil {
   246  		return "", []string{}, fmt.Errorf("flushing tab writer: %w", err)
   247  	}
   248  
   249  	runImageOutput := tabWriterBuf.String()
   250  	if runImageOutput == "" {
   251  		runImageOutput = fmt.Sprintf("  %s\n", none)
   252  	}
   253  
   254  	output += runImageOutput
   255  
   256  	return output, warnings, nil
   257  }
   258  
   259  func writeLocalMirrors(logWriter io.Writer, runImage string, localRunImages []config.RunImage) error {
   260  	for _, i := range localRunImages {
   261  		if i.Image == runImage {
   262  			for _, m := range i.Mirrors {
   263  				_, err := fmt.Fprintf(logWriter, "  %s\t(user-configured)\n", m)
   264  				if err != nil {
   265  					return fmt.Errorf("writing local mirror: %s: %w", m, err)
   266  				}
   267  			}
   268  		}
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  func buildpacksOutput(buildpacks []dist.BuildpackInfo, builderName string) (string, []string, error) {
   275  	output := "Buildpacks:\n"
   276  
   277  	if len(buildpacks) == 0 {
   278  		warnings := []string{
   279  			fmt.Sprintf("%s has no buildpacks", builderName),
   280  			"Users must supply buildpacks from the host machine",
   281  		}
   282  
   283  		return fmt.Sprintf("%s  %s\n", output, none), warnings, nil
   284  	}
   285  
   286  	tabWriterBuf := bytes.Buffer{}
   287  	spaceStrippingWriter := &trailingSpaceStrippingWriter{
   288  		output: &tabWriterBuf,
   289  	}
   290  
   291  	buildpacksTabWriter := tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerPadChar, buildpacksTabWidth, writerPadChar, writerFlags)
   292  	_, err := fmt.Fprint(buildpacksTabWriter, "  ID\tVERSION\tHOMEPAGE\n")
   293  	if err != nil {
   294  		return "", []string{}, fmt.Errorf("writing to tab writer: %w", err)
   295  	}
   296  	for _, b := range buildpacks {
   297  		_, err = fmt.Fprintf(buildpacksTabWriter, "  %s\t%s\t%s\n", b.ID, b.Version, b.Homepage)
   298  		if err != nil {
   299  			return "", []string{}, fmt.Errorf("writing to tab writer: %w", err)
   300  		}
   301  	}
   302  	err = buildpacksTabWriter.Flush()
   303  	if err != nil {
   304  		return "", []string{}, fmt.Errorf("flushing tab writer: %w", err)
   305  	}
   306  
   307  	output += tabWriterBuf.String()
   308  
   309  	return output, []string{}, nil
   310  }
   311  
   312  const lifecycleFormat = `
   313  Lifecycle:
   314    Version: %s
   315    Buildpack APIs:
   316      Deprecated: %s
   317      Supported: %s
   318    Platform APIs:
   319      Deprecated: %s
   320      Supported: %s
   321  `
   322  
   323  func lifecycleOutput(lifecycleInfo builder.LifecycleDescriptor, builderName string) (string, []string) {
   324  	var warnings []string
   325  
   326  	version := none
   327  	if lifecycleInfo.Info.Version != nil {
   328  		version = lifecycleInfo.Info.Version.String()
   329  	}
   330  
   331  	if version == none {
   332  		warnings = append(warnings, fmt.Sprintf("%s does not specify a Lifecycle version", builderName))
   333  	}
   334  
   335  	supportedBuildpackAPIs := stringFromAPISet(lifecycleInfo.APIs.Buildpack.Supported)
   336  	if supportedBuildpackAPIs == none {
   337  		warnings = append(warnings, fmt.Sprintf("%s does not specify supported Lifecycle Buildpack APIs", builderName))
   338  	}
   339  
   340  	supportedPlatformAPIs := stringFromAPISet(lifecycleInfo.APIs.Platform.Supported)
   341  	if supportedPlatformAPIs == none {
   342  		warnings = append(warnings, fmt.Sprintf("%s does not specify supported Lifecycle Platform APIs", builderName))
   343  	}
   344  
   345  	return fmt.Sprintf(
   346  		lifecycleFormat,
   347  		version,
   348  		stringFromAPISet(lifecycleInfo.APIs.Buildpack.Deprecated),
   349  		supportedBuildpackAPIs,
   350  		stringFromAPISet(lifecycleInfo.APIs.Platform.Deprecated),
   351  		supportedPlatformAPIs,
   352  	), warnings
   353  }
   354  
   355  func stringFromAPISet(versions builder.APISet) string {
   356  	if len(versions) == 0 {
   357  		return none
   358  	}
   359  
   360  	return strings.Join(versions.AsStrings(), ", ")
   361  }
   362  
   363  const (
   364  	branchPrefix     = " ├ "
   365  	lastBranchPrefix = " └ "
   366  	trunkPrefix      = " │ "
   367  )
   368  
   369  func detectionOrderOutput(order pubbldr.DetectionOrder, builderName string) (string, []string, error) {
   370  	output := "Detection Order:\n"
   371  
   372  	if len(order) == 0 {
   373  		warnings := []string{
   374  			fmt.Sprintf("%s has no buildpacks", builderName),
   375  			"Users must build with explicitly specified buildpacks",
   376  		}
   377  
   378  		return fmt.Sprintf("%s  %s\n", output, none), warnings, nil
   379  	}
   380  
   381  	tabWriterBuf := bytes.Buffer{}
   382  	spaceStrippingWriter := &trailingSpaceStrippingWriter{
   383  		output: &tabWriterBuf,
   384  	}
   385  
   386  	detectionOrderTabWriter := tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerTabWidth, defaultTabWidth, writerPadChar, writerFlags)
   387  	err := writeDetectionOrderGroup(detectionOrderTabWriter, order, "")
   388  	if err != nil {
   389  		return "", []string{}, fmt.Errorf("writing detection order group: %w", err)
   390  	}
   391  	err = detectionOrderTabWriter.Flush()
   392  	if err != nil {
   393  		return "", []string{}, fmt.Errorf("flushing tab writer: %w", err)
   394  	}
   395  
   396  	output += tabWriterBuf.String()
   397  	return output, []string{}, nil
   398  }
   399  
   400  func writeDetectionOrderGroup(writer io.Writer, order pubbldr.DetectionOrder, prefix string) error {
   401  	groupNumber := 0
   402  
   403  	for i, orderEntry := range order {
   404  		lastInGroup := i == len(order)-1
   405  		includesSubGroup := len(orderEntry.GroupDetectionOrder) > 0
   406  
   407  		orderPrefix, err := writeAndUpdateEntryPrefix(writer, lastInGroup, prefix)
   408  		if err != nil {
   409  			return fmt.Errorf("writing detection group prefix: %w", err)
   410  		}
   411  
   412  		if includesSubGroup {
   413  			groupPrefix := orderPrefix
   414  
   415  			if orderEntry.ID != "" {
   416  				err = writeDetectionOrderBuildpack(writer, orderEntry)
   417  				if err != nil {
   418  					return fmt.Errorf("writing detection order buildpack: %w", err)
   419  				}
   420  
   421  				if lastInGroup {
   422  					_, err = fmt.Fprintf(writer, "%s%s", groupPrefix, lastBranchPrefix)
   423  					if err != nil {
   424  						return fmt.Errorf("writing to detection order group writer: %w", err)
   425  					}
   426  					groupPrefix = fmt.Sprintf("%s   ", groupPrefix)
   427  				} else {
   428  					_, err = fmt.Fprintf(writer, "%s%s", orderPrefix, lastBranchPrefix)
   429  					if err != nil {
   430  						return fmt.Errorf("writing to detection order group writer: %w", err)
   431  					}
   432  					groupPrefix = fmt.Sprintf("%s   ", groupPrefix)
   433  				}
   434  			}
   435  
   436  			groupNumber++
   437  			_, err = fmt.Fprintf(writer, "Group #%d:\n", groupNumber)
   438  			if err != nil {
   439  				return fmt.Errorf("writing to detection order group writer: %w", err)
   440  			}
   441  			err = writeDetectionOrderGroup(writer, orderEntry.GroupDetectionOrder, groupPrefix)
   442  			if err != nil {
   443  				return fmt.Errorf("writing detection order group: %w", err)
   444  			}
   445  		} else {
   446  			err := writeDetectionOrderBuildpack(writer, orderEntry)
   447  			if err != nil {
   448  				return fmt.Errorf("writing detection order buildpack: %w", err)
   449  			}
   450  		}
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  func writeAndUpdateEntryPrefix(writer io.Writer, last bool, prefix string) (string, error) {
   457  	if last {
   458  		_, err := fmt.Fprintf(writer, "%s%s", prefix, lastBranchPrefix)
   459  		if err != nil {
   460  			return "", fmt.Errorf("writing detection order prefix: %w", err)
   461  		}
   462  		return fmt.Sprintf("%s%s", prefix, "   "), nil
   463  	}
   464  
   465  	_, err := fmt.Fprintf(writer, "%s%s", prefix, branchPrefix)
   466  	if err != nil {
   467  		return "", fmt.Errorf("writing detection order prefix: %w", err)
   468  	}
   469  	return fmt.Sprintf("%s%s", prefix, trunkPrefix), nil
   470  }
   471  
   472  func writeDetectionOrderBuildpack(writer io.Writer, entry pubbldr.DetectionOrderEntry) error {
   473  	_, err := fmt.Fprintf(
   474  		writer,
   475  		"%s\t%s%s\n",
   476  		entry.FullName(),
   477  		stringFromOptional(entry.Optional),
   478  		stringFromCyclical(entry.Cyclical),
   479  	)
   480  
   481  	if err != nil {
   482  		return fmt.Errorf("writing buildpack in detection order: %w", err)
   483  	}
   484  
   485  	return nil
   486  }
   487  
   488  func stringFromOptional(optional bool) string {
   489  	if optional {
   490  		return "(optional)"
   491  	}
   492  
   493  	return ""
   494  }
   495  
   496  func stringFromCyclical(cyclical bool) string {
   497  	if cyclical {
   498  		return "[cyclic]"
   499  	}
   500  
   501  	return ""
   502  }