diff --git a/build.go b/build.go index 1fe7880c29..c59223e06d 100644 --- a/build.go +++ b/build.go @@ -334,7 +334,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { GID: opts.GroupID, PreviousImage: opts.PreviousImage, Interactive: opts.Interactive, - Termui: termui.NewTermui(), + Termui: termui.NewTermui(imageRef.Name(), bldr, runImageName), } lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 87393976eb..dae1f91e85 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -189,6 +189,11 @@ func (b *Builder) Order() dist.Order { return b.order } +// BaseImageName returns the name of the builder base image +func (b *Builder) BaseImageName() string { + return b.baseImageName +} + // Name returns the name of the builder func (b *Builder) Name() string { return b.image.Name() diff --git a/internal/termui/dashboard.go b/internal/termui/dashboard.go new file mode 100644 index 0000000000..9f96c2e015 --- /dev/null +++ b/internal/termui/dashboard.go @@ -0,0 +1,139 @@ +package termui + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + + "github.com/buildpacks/pack/internal/builder" + "github.com/buildpacks/pack/internal/dist" +) + +type Dashboard struct { + app app + imagesView *tview.Flex + planList *tview.List + logsView *tview.TextView + logs string +} + +func NewDashboard(app app, appName string, bldr *builder.Builder, runImageName string, buildpackInfo []dist.BuildpackInfo) *Dashboard { + imagesView, planList, logsView := initDashboard(appName, bldr, runImageName, buildpackInfo) + + d := &Dashboard{ + app: app, + imagesView: imagesView, + planList: planList, + logsView: logsView, + } + + leftPane := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(imagesView, 11, 0, false). + AddItem(planList, 0, 1, true) + + screen := tview.NewFlex(). + SetDirection(tview.FlexColumn). + AddItem(leftPane, 0, 1, true). + AddItem(logsView, 0, 1, false) + + d.app.SetRoot(screen, true) + return d +} + +func (d *Dashboard) Handle(txt string) { + d.app.QueueUpdateDraw(func() { + d.logs = d.logs + txt + "\n" + d.logsView.SetText(tview.TranslateANSI(d.logs)) + }) +} + +func (d *Dashboard) Stop() { + panic("implement me") +} + +func initDashboard(appName string, bldr *builder.Builder, runImageName string, buildpackInfos []dist.BuildpackInfo) (*tview.Flex, *tview.List, *tview.TextView) { + appTree, builderTree := initTrees(appName, bldr, runImageName) + imagesView := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(appTree, 0, 1, false). + AddItem(builderTree, 0, 1, true) + imagesView.SetBorder(true). + SetTitleAlign(tview.AlignLeft). + SetTitle("| [::b]images[::-] |"). + SetBackgroundColor(backgroundColor) + + planList := tview.NewList() + planList.SetMainTextColor(tcell.ColorMediumTurquoise). + SetSelectedTextColor(tcell.ColorMediumTurquoise). + SetSelectedBackgroundColor(tcell.ColorDarkSlateGray). + SetSecondaryTextColor(tcell.ColorDimGray). + SetBorder(true). + SetBorderPadding(1, 1, 1, 1). + SetTitle("| [::b]plan[::-] |"). + SetTitleAlign(tview.AlignLeft). + SetBackgroundColor(backgroundColor) + + for _, buildpackInfo := range buildpackInfos { + planList.AddItem( + buildpackInfo.FullName(), + info(buildpackInfo), + '✔', + func() {}, + ) + } + + logsView := tview.NewTextView() + logsView.SetDynamicColors(true). + SetTextAlign(tview.AlignLeft). + SetBorderPadding(1, 1, 3, 1). + SetTitleAlign(tview.AlignLeft). + SetBackgroundColor(backgroundColor) + + return imagesView, planList, logsView +} + +func initTrees(appName string, bldr *builder.Builder, runImageName string) (*tview.TreeView, *tview.TreeView) { + var ( + appImage = tview.NewTreeNode(fmt.Sprintf("app: [white::b]%s", appName)).SetColor(tcell.ColorDimGray) + appRunImage = tview.NewTreeNode(fmt.Sprintf(" run: [white::b]%s", runImageName)).SetColor(tcell.ColorDimGray) + builderImage = tview.NewTreeNode(fmt.Sprintf("builder: [white::b]%s", bldr.BaseImageName())).SetColor(tcell.ColorDimGray) + lifecycle = tview.NewTreeNode(fmt.Sprintf(" lifecycle: [white::b]%s", bldr.LifecycleDescriptor().Info.Version.String())).SetColor(tcell.ColorDimGray) + runImage = tview.NewTreeNode(fmt.Sprintf(" run: [white::b]%s", bldr.Stack().RunImage.Image)).SetColor(tcell.ColorDimGray) + buildpacks = tview.NewTreeNode(" [mediumturquoise::b]buildpacks") + ) + + appImage.AddChild(appRunImage) + builderImage.AddChild(lifecycle) + builderImage.AddChild(runImage) + builderImage.AddChild(buildpacks) + + appTree := tview.NewTreeView() + appTree. + SetRoot(appImage). + SetGraphics(true). + SetGraphicsColor(tcell.ColorMediumTurquoise). + SetTitleAlign(tview.AlignLeft). + SetBorderPadding(1, 0, 4, 0). + SetBackgroundColor(backgroundColor) + + builderTree := tview.NewTreeView() + builderTree. + SetRoot(builderImage). + SetGraphics(true). + SetGraphicsColor(tcell.ColorMediumTurquoise). + SetTitleAlign(tview.AlignLeft). + SetBorderPadding(0, 0, 4, 0). + SetBackgroundColor(backgroundColor) + + return appTree, builderTree +} + +func info(buildpackInfo dist.BuildpackInfo) string { + if buildpackInfo.Description != "" { + return buildpackInfo.Description + } + + return buildpackInfo.Homepage +} diff --git a/internal/termui/detect.go b/internal/termui/detect.go index ad7836b28b..703529aaec 100644 --- a/internal/termui/detect.go +++ b/internal/termui/detect.go @@ -1,22 +1,32 @@ package termui import ( + "regexp" "time" "github.com/rivo/tview" + + "github.com/buildpacks/pack/internal/builder" + "github.com/buildpacks/pack/internal/dist" ) type Detect struct { - app app - textView *tview.TextView - doneChan chan bool + app app + textView *tview.TextView + buildpackRegex *regexp.Regexp + buildpackChan chan dist.BuildpackInfo + doneChan chan bool + bldr *builder.Builder } -func NewDetect(app app) *Detect { +func NewDetect(app app, buildpackChan chan dist.BuildpackInfo, bldr *builder.Builder) *Detect { d := &Detect{ - app: app, - textView: detectStatusTV(app), - doneChan: make(chan bool, 1), + app: app, + textView: detectStatusTV(), + buildpackRegex: regexp.MustCompile(`(\S+\/\S+)\s+([\d\.]+)`), + buildpackChan: buildpackChan, + doneChan: make(chan bool, 1), + bldr: bldr, } go d.start() @@ -25,6 +35,13 @@ func NewDetect(app app) *Detect { return d } +func (d *Detect) Handle(txt string) { + m := d.buildpackRegex.FindStringSubmatch(txt) + if len(m) == 3 { + d.buildpackChan <- d.find(m[1], m[2]) + } +} + func (d *Detect) Stop() { d.doneChan <- true } @@ -45,7 +62,9 @@ func (d *Detect) start() { for { select { case <-ticker.C: - d.textView.SetText(texts[i]) + d.app.QueueUpdateDraw(func() { + d.textView.SetText(texts[i]) + }) i++ if i == len(texts) { @@ -53,16 +72,28 @@ func (d *Detect) start() { } case <-d.doneChan: ticker.Stop() - d.textView.SetText(doneText) + + d.app.QueueUpdateDraw(func() { + d.textView.SetText(doneText) + }) return } } } -func detectStatusTV(app app) *tview.TextView { +func (d *Detect) find(buildpackID, buildpackVersion string) dist.BuildpackInfo { + for _, buildpack := range d.bldr.Buildpacks() { + if buildpack.ID == buildpackID && buildpack.Version == buildpackVersion { + return buildpack + } + } + + return dist.BuildpackInfo{} +} + +func detectStatusTV() *tview.TextView { tv := tview.NewTextView() tv.SetBackgroundColor(backgroundColor) - tv.SetChangedFunc(func() { app.Draw() }) return tv } diff --git a/internal/termui/termui.go b/internal/termui/termui.go index 08609789e0..dafda4cbf5 100644 --- a/internal/termui/termui.go +++ b/internal/termui/termui.go @@ -11,7 +11,9 @@ import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" + "github.com/buildpacks/pack/internal/builder" "github.com/buildpacks/pack/internal/container" + "github.com/buildpacks/pack/internal/dist" ) var ( @@ -21,34 +23,49 @@ var ( type app interface { SetRoot(root tview.Primitive, fullscreen bool) *tview.Application Draw() *tview.Application + QueueUpdateDraw(f func()) *tview.Application Run() error } type page interface { + Handle(txt string) Stop() } type Termui struct { - app app - currentPage page - textChan chan string + appName string + runImageName string + bldr *builder.Builder + exitCode int64 + + app app + currentPage page + textChan chan string + buildpackChan chan dist.BuildpackInfo } -func NewTermui() *Termui { +func NewTermui(appName string, bldr *builder.Builder, runImageName string) *Termui { return &Termui{ - app: tview.NewApplication(), - textChan: make(chan string, 10), + appName: appName, + bldr: bldr, + runImageName: runImageName, + app: tview.NewApplication(), + buildpackChan: make(chan dist.BuildpackInfo, 50), + textChan: make(chan string, 10), } } // Run starts the terminal UI process in the foreground // and the passed in function in the background func (s *Termui) Run(funk func()) error { - go funk() + go func() { + funk() + s.showBuildStatus() + }() go s.handle() defer s.stop() - s.currentPage = NewDetect(s.app) + s.currentPage = NewDetect(s.app, s.buildpackChan, s.bldr) return s.app.Run() } @@ -61,8 +78,11 @@ func (s *Termui) handle() { switch { case strings.Contains(txt, "===> ANALYZING"): s.currentPage.Stop() + + s.currentPage = NewDashboard(s.app, s.appName, s.bldr, s.runImageName, collect(s.buildpackChan)) + s.currentPage.Handle(txt) default: - // no-op + s.currentPage.Handle(txt) } } } @@ -93,18 +113,39 @@ func (s *Termui) Handler() container.Handler { return err case err := <-errChan: return err + case body := <-bodyChan: + s.exitCode = body.StatusCode + return nil default: - if !scanner.Scan() { - err := scanner.Err() - if err != nil { - return err - } - - return nil + if scanner.Scan() { + s.textChan <- scanner.Text() + continue } - s.textChan <- scanner.Text() + if err := scanner.Err(); err != nil { + return err + } } } } } + +func (s *Termui) showBuildStatus() { + if s.exitCode == 0 { + s.textChan <- "[green::b]\n\nBUILD SUCCEEDED" + return + } + + s.textChan <- "[red::b]\n\nBUILD FAILED" +} + +func collect(buildpackChan chan dist.BuildpackInfo) []dist.BuildpackInfo { + close(buildpackChan) + + var result []dist.BuildpackInfo + for txt := range buildpackChan { + result = append(result, txt) + } + + return result +}