88 "os"
99 "os/exec"
1010 "strings"
11+ "sync"
1112
1213 "github.com/hashicorp/go-version"
1314
@@ -34,6 +35,14 @@ type OSBuildOptions struct {
3435 Checkpoints []string
3536 ExtraEnv []string
3637
38+ // If specified, the mutex is used for the syncwriter so the caller may write to the build
39+ // log as well. Also note that in case BuildLog is specified, stderr will be combined into
40+ // stdout.
41+ BuildLog io.Writer
42+ BuildLogMu * sync.Mutex
43+ Stdout io.Writer
44+ Stderr io.Writer
45+
3746 Monitor MonitorType
3847 MonitorFD uintptr
3948
@@ -73,10 +82,44 @@ func NewOSBuildCmd(manifest []byte, optsPtr *OSBuildOptions) *exec.Cmd {
7382 if opts .MonitorFD != 0 {
7483 cmd .Args = append (cmd .Args , fmt .Sprintf ("--monitor-fd=%d" , opts .MonitorFD ))
7584 }
85+
7686 if opts .JSONOutput {
7787 cmd .Args = append (cmd .Args , "--json" )
7888 }
7989
90+ // Default to os stdout/stderr. This is for maximum compatibility with the existing
91+ // bootc-image-builder in "verbose" mode where stdout, stderr come directly from osbuild.
92+ var stdout , stderr io.Writer
93+ stdout = os .Stdout
94+ if opts .Stdout != nil {
95+ stdout = opts .Stdout
96+ }
97+ cmd .Stdout = stdout
98+ stderr = os .Stderr
99+ if opts .Stderr != nil {
100+ stderr = opts .Stderr
101+ }
102+ cmd .Stderr = stderr
103+
104+ if opts .BuildLog != nil {
105+ // There is a slight wrinkle here: when requesting a buildlog we can no longer write
106+ // to separate stdout/stderr streams without being racy and give potential
107+ // out-of-order output (which is very bad and confusing in a log). The reason is
108+ // that if cmd.Std{out,err} are different "go" will start two go-routine to
109+ // monitor/copy those are racy when both stdout,stderr output happens close together
110+ // (TestRunOSBuildWithBuildlog demos that). We cannot have our cake and eat it so
111+ // here we need to combine osbuilds stderr into our stdout.
112+ // stdout → syncw → multiw → stdoutw or os stdout
113+ // stderr ↗↗↗ → buildlog
114+ var mw io.Writer
115+ if opts .BuildLogMu == nil {
116+ opts .BuildLogMu = new (sync.Mutex )
117+ }
118+ mw = newSyncedWriter (opts .BuildLogMu , io .MultiWriter (stdout , opts .BuildLog ))
119+ cmd .Stdout = mw
120+ cmd .Stderr = mw
121+ }
122+
80123 cmd .Env = append (os .Environ (), opts .ExtraEnv ... )
81124 cmd .Stdin = bytes .NewBuffer (manifest )
82125 return cmd
@@ -87,7 +130,7 @@ func NewOSBuildCmd(manifest []byte, optsPtr *OSBuildOptions) *exec.Cmd {
87130// Note that osbuild returns non-zero when the pipeline fails. This function
88131// does not return an error in this case. Instead, the failure is communicated
89132// with its corresponding logs through osbuild.Result.
90- func RunOSBuild (manifest []byte , errorWriter io. Writer , optsPtr * OSBuildOptions ) (* Result , error ) {
133+ func RunOSBuild (manifest []byte , optsPtr * OSBuildOptions ) (* Result , error ) {
91134 opts := common .ValueOrEmpty (optsPtr )
92135
93136 if err := CheckMinimumOSBuildVersion (); err != nil {
@@ -100,11 +143,7 @@ func RunOSBuild(manifest []byte, errorWriter io.Writer, optsPtr *OSBuildOptions)
100143
101144 if opts .JSONOutput {
102145 cmd .Stdout = & stdoutBuffer
103- } else {
104- cmd .Stdout = os .Stdout
105146 }
106- cmd .Stderr = errorWriter
107-
108147 err := cmd .Start ()
109148 if err != nil {
110149 return nil , fmt .Errorf ("error starting osbuild: %v" , err )
@@ -186,3 +225,19 @@ func OSBuildInspect(manifest []byte) ([]byte, error) {
186225
187226 return out , nil
188227}
228+
229+ type syncedWriter struct {
230+ mu * sync.Mutex
231+ w io.Writer
232+ }
233+
234+ func newSyncedWriter (mu * sync.Mutex , w io.Writer ) io.Writer {
235+ return & syncedWriter {mu : mu , w : w }
236+ }
237+
238+ func (sw * syncedWriter ) Write (p []byte ) (n int , err error ) {
239+ sw .mu .Lock ()
240+ defer sw .mu .Unlock ()
241+
242+ return sw .w .Write (p )
243+ }
0 commit comments