@@ -15,7 +15,9 @@ import (
1515 "strings"
1616
1717 "github.com/brunoga/deep"
18+ "github.com/fatih/color"
1819 "github.com/gim-home/azldev-preview/internal/global/opctx"
20+ "github.com/gim-home/azldev-preview/internal/utils/fileutils"
1921 "github.com/gim-home/azldev-preview/internal/utils/hostinfo"
2022 "github.com/kballard/go-shellquote"
2123 "github.com/samber/lo"
@@ -60,6 +62,17 @@ type Runner struct {
6062 rootDir string // optional
6163}
6264
65+ // BuildLogDetails encapsulates details extracted from mock build logs that may be relevant to
66+ // understanding the cause of a build failure.
67+ type BuildLogDetails struct {
68+ // RPMBuildErrors is a list of error messages extracted from mock build logs.
69+ RPMBuildErrors []string
70+
71+ // LastRPMBuildLogLines is a list of the last lines of the mock build log, which may contain
72+ // relevant context about why a build failure occurred.
73+ LastRPMBuildLogLines []string
74+ }
75+
6376type bindMountRequest struct {
6477 hostPath string
6578 mockRootPath string
@@ -301,7 +314,6 @@ func (r *Runner) BuildSRPM(
301314
302315 cmd := exec .CommandContext (ctx , MockBinary , args ... )
303316 cmd .Stdout = os .Stdout
304- cmd .Stderr = os .Stderr
305317
306318 extcmd , err := r .cmdFactory .Command (cmd )
307319 if err != nil {
@@ -313,7 +325,7 @@ func (r *Runner) BuildSRPM(
313325 }
314326
315327 // Watch output logs in real-time so we can asynchronously synthesize progress updates.
316- err = addMockLogListeners (r .eventListener , extcmd , outputDirPath )
328+ err = addMockCmdListeners (r .eventListener , extcmd , outputDirPath )
317329 if err != nil {
318330 return err
319331 }
@@ -383,7 +395,6 @@ func (r *Runner) BuildRPM(ctx context.Context, srpmPath, outputDirPath string, o
383395 // being passed through.
384396 cmd := exec .CommandContext (ctx , MockBinary , args ... )
385397 cmd .Stdout = os .Stdout
386- cmd .Stderr = os .Stderr
387398
388399 extcmd , err := r .cmdFactory .Command (cmd )
389400 if err != nil {
@@ -393,7 +404,7 @@ func (r *Runner) BuildRPM(ctx context.Context, srpmPath, outputDirPath string, o
393404 extcmd = extcmd .SetLongRunning ("Waiting for mock (building RPM)..." )
394405
395406 // Watch output logs in real-time so we can asynchronously synthesize progress updates.
396- err = addMockLogListeners (r .eventListener , extcmd , outputDirPath )
407+ err = addMockCmdListeners (r .eventListener , extcmd , outputDirPath )
397408 if err != nil {
398409 return err
399410 }
@@ -413,14 +424,30 @@ func (r *Runner) BuildRPM(ctx context.Context, srpmPath, outputDirPath string, o
413424// will only run for the duration of mock's invocation, allowing us to get insights
414425// into what's happening *inside* mock while we're otherwise opaquely blocking on its
415426// execution.
416- func addMockLogListeners (eventListener opctx.EventListener , extcmd opctx.Cmd , outputDirPath string ) error {
427+ func addMockCmdListeners (eventListener opctx.EventListener , extcmd opctx.Cmd , outputDirPath string ) error {
428+ // Color-code stderr lines.
429+ err := extcmd .SetRealTimeStderrListener (func (ctx context.Context , line string ) {
430+ if strings .HasPrefix (line , "No matching package to install:" ) {
431+ color .Set (color .FgHiYellow )
432+ } else {
433+ color .Set (color .FgHiBlack , color .Italic )
434+ }
435+
436+ defer color .Unset ()
437+
438+ fmt .Fprintf (os .Stderr , "%s\n " , line )
439+ })
440+ if err != nil {
441+ return fmt .Errorf ("failed to setup mock stderr listener: %w" , err )
442+ }
443+
417444 // Messages we care about in 'state.log' will look something like the following (without the indent):
418445 // 2021-05-19 16:20:11,859 - Start: rpmbuild test-1.0.0.src.rpm
419446 stateLogRe := regexp .MustCompile (`^[^ ]+ [^ ]+ - (.*)$` )
420447
421448 // Watch well-known log 'state.log', which gives us very high-level information
422449 // about where we are in the mock build process.
423- err : = extcmd .AddRealTimeFileListener (
450+ err = extcmd .AddRealTimeFileListener (
424451 filepath .Join (outputDirPath , "state.log" ),
425452 func (_ context.Context , line string ) {
426453 if matches := stateLogRe .FindStringSubmatch (line ); len (matches ) > 1 {
@@ -619,3 +646,46 @@ func (r *Runner) getBaseArgs() (args []string) {
619646
620647 return args
621648}
649+
650+ // TryGetFailureDetails makes a best-effort attempt to extract details from mock build logs that may be
651+ // relevant to understanding the cause of a build failure. This is intended to be called after a build
652+ // failure to glean any insights we can from mock's logs about why the failure might have occurred.
653+ func (r * Runner ) TryGetFailureDetails (fs opctx.FS , outputDirPath string ) (details * BuildLogDetails ) {
654+ const maxContextLinesToCapture = 10
655+
656+ details = & BuildLogDetails {}
657+
658+ // Go through build.log.
659+ buildLogPath := filepath .Join (outputDirPath , "build.log" )
660+ buildLogBytes , _ := fileutils .ReadFile (fs , buildLogPath )
661+ buildLogLines := strings .Split (string (buildLogBytes ), "\n " )
662+
663+ rpmBuildErrorsStartIndex := - 1
664+ rpmBuildErrorsEndIndex := - 1
665+
666+ for lineIndex , line := range buildLogLines {
667+ if strings .HasPrefix (line , "RPM build errors:" ) {
668+ rpmBuildErrorsStartIndex = lineIndex + 1
669+ } else if strings .HasPrefix (line , "EXCEPTION:" ) {
670+ rpmBuildErrorsEndIndex = lineIndex
671+ }
672+ }
673+
674+ // If we see evidence of "RPM build errors", then try to capture relevant lines.
675+ if rpmBuildErrorsStartIndex > 0 {
676+ contextEndIndex := rpmBuildErrorsStartIndex - 1
677+
678+ if rpmBuildErrorsEndIndex < 0 {
679+ rpmBuildErrorsEndIndex = len (buildLogLines )
680+ }
681+
682+ details .RPMBuildErrors = buildLogLines [rpmBuildErrorsStartIndex :rpmBuildErrorsEndIndex ]
683+
684+ contextLinesToCapture := min (maxContextLinesToCapture , contextEndIndex )
685+ contextStartIndex := contextEndIndex - contextLinesToCapture
686+
687+ details .LastRPMBuildLogLines = buildLogLines [contextStartIndex :contextEndIndex ]
688+ }
689+
690+ return details
691+ }
0 commit comments