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 }