@@ -7,7 +7,14 @@ package cmd
77
88import  (
99	"fmt" 
10- 
10+ 	"io" 
11+ 	"os" 
12+ 	"slices" 
13+ 	"strings" 
14+ 
15+ 	"github.com/DataDog/orchestrion/internal/binpath" 
16+ 	"github.com/DataDog/orchestrion/internal/goproxy" 
17+ 	"github.com/DataDog/orchestrion/internal/pin" 
1118	"github.com/DataDog/orchestrion/internal/report" 
1219	"github.com/urfave/cli/v2" 
1320)
@@ -33,20 +40,26 @@ var (
3340		Usage : "Also print synthetic and tracer weaved packages" ,
3441	}
3542
43+ 	buildFlag  =  cli.BoolFlag {
44+ 		Name :  "build" ,
45+ 		Usage : "Execute a build with -work before generating the diff. All remaining arguments after flags are passed to the build command." ,
46+ 	}
47+ 
3648	Diff  =  & cli.Command {
3749		Name :  "diff" ,
38- 		Usage : "Generates a diff between a nominal and orchestrion-instrumented build using a go work directory that can be  obtained running  `orchestrion go build -work -a`. This is incompatible with coverage related flags." ,
50+ 		Usage : "Generates a diff between a nominal and orchestrion-instrumented build. Use --build to execute a build first, or provide a work directory path  obtained from  `orchestrion go build -work -a`. This is incompatible with coverage related flags." ,
3951		Args :  true ,
4052		Flags : []cli.Flag {
4153			& filenameFlag ,
4254			& filterFlag ,
4355			& packageFlag ,
4456			& debugFlag ,
57+ 			& buildFlag ,
4558		},
46- 		Action : func (clictx  * cli.Context ) ( err   error )  {
47- 			workFolder   :=  clictx . Args (). First ( )
48- 			if  workFolder   ==   ""  {
49- 				return  cli . ShowSubcommandHelp ( clictx ) 
59+ 		Action : func (clictx  * cli.Context ) error  {
60+ 			workFolder ,  err   :=  getWorkFolder ( clictx )
61+ 			if  err   !=   nil  {
62+ 				return  err 
5063			}
5164
5265			report , err  :=  report .FromWorkDir (clictx .Context , workFolder )
@@ -69,25 +82,163 @@ var (
6982				}
7083			}
7184
72- 			if  clictx .Bool (packageFlag .Name ) {
73- 				for  _ , pkg  :=  range  report .Packages () {
74- 					_ , _  =  fmt .Fprintln (clictx .App .Writer , pkg )
75- 				}
76- 				return  nil 
77- 			}
85+ 			return  outputReport (clictx , report )
86+ 		},
87+ 	}
88+ )
7889
79- 			if  clictx .Bool (filenameFlag .Name ) {
80- 				for  _ , file  :=  range  report .Files () {
81- 					_ , _  =  fmt .Fprintln (clictx .App .Writer , file )
82- 				}
83- 				return  nil 
84- 			}
90+ func  getWorkFolder (clictx  * cli.Context ) (string , error ) {
91+ 	if  ! clictx .Bool (buildFlag .Name ) {
92+ 		workFolder  :=  clictx .Args ().First ()
93+ 		if  workFolder  ==  ""  {
94+ 			return  "" , cli .ShowSubcommandHelp (clictx )
95+ 		}
96+ 		return  workFolder , nil 
97+ 	}
8598
86- 			if  err  :=  report .Diff (clictx .App .Writer ); err  !=  nil  {
87- 				return  cli .Exit (fmt .Sprintf ("failed to generate diff: %s" , err ), 1 )
88- 			}
99+ 	return  executeBuildAndCaptureWorkDir (clictx , prepareBuildArgs (clictx .Args ().Slice ()))
100+ }
89101
90- 			return  nil 
91- 		},
102+ func  prepareBuildArgs (args  []string ) []string  {
103+ 	switch  {
104+ 	case  len (args ) ==  0 :
105+ 		args  =  []string {"build" , "./..." }
106+ 	case  args [0 ] !=  "build"  &&  args [0 ] !=  "install"  &&  args [0 ] !=  "test" :
107+ 		args  =  append ([]string {"build" }, args ... )
92108	}
93- )
109+ 
110+ 	var  flags  []string 
111+ 	hasWork , hasAll  :=  false , false 
112+ 	for  _ , arg  :=  range  args  {
113+ 		switch  arg  {
114+ 		case  "-work" :
115+ 			hasWork  =  true 
116+ 		case  "-a" :
117+ 			hasAll  =  true 
118+ 		}
119+ 	}
120+ 
121+ 	if  ! hasWork  {
122+ 		flags  =  append (flags , "-work" )
123+ 	}
124+ 	if  ! hasAll  {
125+ 		flags  =  append (flags , "-a" )
126+ 	}
127+ 
128+ 	if  len (flags ) >  0  {
129+ 		args  =  slices .Concat (args [:1 ], flags , args [1 :])
130+ 	}
131+ 
132+ 	return  args 
133+ }
134+ 
135+ func  outputReport (clictx  * cli.Context , rpt  report.Report ) error  {
136+ 	if  clictx .Bool (packageFlag .Name ) {
137+ 		for  _ , pkg  :=  range  rpt .Packages () {
138+ 			_ , _  =  fmt .Fprintln (clictx .App .Writer , pkg )
139+ 		}
140+ 		return  nil 
141+ 	}
142+ 
143+ 	if  clictx .Bool (filenameFlag .Name ) {
144+ 		for  _ , file  :=  range  rpt .Files () {
145+ 			_ , _  =  fmt .Fprintln (clictx .App .Writer , file )
146+ 		}
147+ 		return  nil 
148+ 	}
149+ 
150+ 	if  err  :=  rpt .Diff (clictx .App .Writer ); err  !=  nil  {
151+ 		return  cli .Exit (fmt .Sprintf ("failed to generate diff: %s" , err ), 1 )
152+ 	}
153+ 
154+ 	return  nil 
155+ }
156+ 
157+ func  executeBuildAndCaptureWorkDir (clictx  * cli.Context , buildArgs  []string ) (string , error ) {
158+ 	if  err  :=  pin .AutoPinOrchestrion (clictx .Context , clictx .App .Writer , clictx .App .ErrWriter ); err  !=  nil  {
159+ 		return  "" , cli .Exit (err , - 1 )
160+ 	}
161+ 
162+ 	tmpFile , err  :=  os .CreateTemp ("" , "orchestrion-build-output-" )
163+ 	if  err  !=  nil  {
164+ 		return  "" , fmt .Errorf ("creating temporary file for build output: %w" , err )
165+ 	}
166+ 	defer  tmpFile .Close ()
167+ 	defer  os .Remove (tmpFile .Name ())
168+ 
169+ 	originalStderr  :=  os .Stderr 
170+ 	os .Stderr  =  tmpFile 
171+ 
172+ 	buildErr  :=  goproxy .Run (clictx .Context , buildArgs , goproxy .WithToolexec (binpath .Orchestrion , "toolexec" ))
173+ 
174+ 	os .Stderr  =  originalStderr 
175+ 
176+ 	if  buildErr  !=  nil  {
177+ 		return  "" , cli .Exit (fmt .Sprintf ("build failed: %v" , buildErr ), 1 )
178+ 	}
179+ 
180+ 	output , err  :=  os .ReadFile (tmpFile .Name ())
181+ 	if  err  !=  nil  {
182+ 		return  "" , fmt .Errorf ("reading build output: %w" , err )
183+ 	}
184+ 
185+ 	wd  :=  extractWorkDirFromOutput (string (output ))
186+ 	if  wd  ==  ""  {
187+ 		return  "" , cli .Exit ("could not extract work directory from build output (did the build use -work?)" , 1 )
188+ 	}
189+ 	return  wd , nil 
190+ }
191+ 
192+ func  extractWorkDirFromOutput (output  string ) string  {
193+ 	for  line  :=  range  strings .SplitSeq (output , "\n " ) {
194+ 		if  wd , ok  :=  strings .CutPrefix (strings .TrimSpace (line ), "WORK=" ); ok  {
195+ 			return  wd 
196+ 		}
197+ 	}
198+ 	return  "" 
199+ }
200+ 
201+ // ReportInterface defines the interface for report objects used in testing 
202+ type  ReportInterface  interface  {
203+ 	IsEmpty () bool 
204+ 	WithSpecialCasesFilter () ReportInterface 
205+ 	WithRegexFilter (pattern  string ) (ReportInterface , error )
206+ 	Packages () []string 
207+ 	Files () []string 
208+ 	Diff (io.Writer ) error 
209+ }
210+ 
211+ // Test helper functions for testing internal functionality 
212+ 
213+ // PrepareBuildArgsForTest exposes prepareBuildArgs for testing 
214+ func  PrepareBuildArgsForTest (args  []string ) []string  {
215+ 	return  prepareBuildArgs (args )
216+ }
217+ 
218+ // ExtractWorkDirFromOutputForTest exposes extractWorkDirFromOutput for testing 
219+ func  ExtractWorkDirFromOutputForTest (output  string ) string  {
220+ 	return  extractWorkDirFromOutput (output )
221+ }
222+ 
223+ // OutputReportForTest exposes outputReport functionality for testing 
224+ func  OutputReportForTest (writer  io.Writer , flags  map [string ]bool , rpt  ReportInterface ) error  {
225+ 	if  flags ["package" ] {
226+ 		for  _ , pkg  :=  range  rpt .Packages () {
227+ 			_ , _  =  fmt .Fprintln (writer , pkg )
228+ 		}
229+ 		return  nil 
230+ 	}
231+ 
232+ 	if  flags ["files" ] {
233+ 		for  _ , file  :=  range  rpt .Files () {
234+ 			_ , _  =  fmt .Fprintln (writer , file )
235+ 		}
236+ 		return  nil 
237+ 	}
238+ 
239+ 	if  err  :=  rpt .Diff (writer ); err  !=  nil  {
240+ 		return  cli .Exit (fmt .Sprintf ("failed to generate diff: %s" , err ), 1 )
241+ 	}
242+ 
243+ 	return  nil 
244+ }
0 commit comments