11package preflight
22
33import (
4- "encoding/json"
5- "errors"
6- "fmt"
7- "os"
8- "os/exec"
9- "path/filepath"
10- "sort"
11- "strings"
12-
134 "github.com/grafana/codejen"
145
156 "github.com/grafana/grafana-app-sdk/codegen/config"
@@ -18,289 +9,38 @@ import (
189// GeneratedGoCodeCompiles runs a preflight compilation check on generated Go files.
1910func GeneratedGoCodeCompiles (cfg * config.Config , files codejen.Files ) error {
2011 if cfg == nil {
21- return generatedGoCodeCompilesWithOverlay (files )
12+ return compileGeneratedGoCodeWithOverlay (files )
2213 }
2314
24- cwd , err := os . Getwd ()
15+ cwd , err := getWorkingDirectory ()
2516 if err != nil {
2617 return err
2718 }
2819
2920 currentModule , _ := getGoModule ("go.mod" )
30- goModule := cfg .Codegen .GoModule
31- if goModule == "" {
32- goModule = currentModule
33- }
21+ goModule := resolveGoModule (cfg , currentModule )
3422 if useOverlayCompilationPreflight (goModule , currentModule ) {
35- return generatedGoCodeCompilesWithOverlay (files )
36- }
37-
38- goGenRoot := normalizeAbsolutePath (cfg .Codegen .GoGenPath , cwd )
39-
40- moduleGenRoot := cfg .Codegen .GoModGenPath
41- if moduleGenRoot == "" {
42- moduleGenRoot = cfg .Codegen .GoGenPath
43- }
44- moduleGenRoot = normalizeRelativePath (moduleGenRoot )
45- if filepath .IsAbs (moduleGenRoot ) {
46- return generatedGoCodeCompilesWithOverlay (files )
47- }
48-
49- generatedFiles := make ([]generatedGoFile , 0 , len (files ))
50- generatedPackageDirs := make (map [string ]struct {})
51- manifestFileCountByDir := make (map [string ]int )
52-
53- for _ , f := range files {
54- if filepath .Ext (f .RelativePath ) != ".go" {
55- continue
56- }
57-
58- absTargetPath := normalizeAbsolutePath (f .RelativePath , cwd )
59-
60- relPath , err := filepath .Rel (goGenRoot , absTargetPath )
61- if err != nil || strings .HasPrefix (relPath , ".." ) {
62- return generatedGoCodeCompilesWithOverlay (files )
63- }
64-
65- generatedFiles = append (generatedFiles , generatedGoFile {
66- absDir : filepath .Dir (absTargetPath ),
67- relPath : filepath .Join (moduleGenRoot , relPath ),
68- data : f .Data ,
69- })
70- generatedPackageDirs [filepath .Dir (absTargetPath )] = struct {}{}
71- if strings .HasSuffix (absTargetPath , "_manifest.go" ) {
72- manifestFileCountByDir [filepath .Dir (absTargetPath )]++
73- }
74- }
75-
76- if len (generatedFiles ) == 0 {
77- return nil
78- }
79-
80- skipPackages := make (map [string ]struct {})
81- for dir , count := range manifestFileCountByDir {
82- // Multiple generated manifest files share package-level identifiers by design.
83- // Skip those packages in preflight and continue validating generated resource code.
84- if count > 1 {
85- skipPackages [dir ] = struct {}{}
86- }
87- }
88- if len (skipPackages ) > 0 {
89- filtered := generatedFiles [:0 ]
90- for _ , f := range generatedFiles {
91- if _ , skip := skipPackages [f .absDir ]; skip {
92- continue
93- }
94- filtered = append (filtered , f )
95- }
96- generatedFiles = filtered
97- for dir := range skipPackages {
98- delete (generatedPackageDirs , dir )
99- }
100- if len (generatedFiles ) == 0 {
101- return nil
102- }
103- }
104-
105- tempDir , err := os .MkdirTemp ("" , "grafana-app-sdk-generate-preflight-*" )
106- if err != nil {
107- return err
108- }
109- defer os .RemoveAll (tempDir )
110-
111- moduleRoot := filepath .Join (tempDir , "module" )
112- if err := os .MkdirAll (moduleRoot , 0o755 ); err != nil {
113- return err
114- }
115-
116- // Copy existing on-disk files from generated package directories, then overwrite with in-memory generated files.
117- for packageDir := range generatedPackageDirs {
118- relDir , err := filepath .Rel (goGenRoot , packageDir )
119- if err != nil || strings .HasPrefix (relDir , ".." ) {
120- return generatedGoCodeCompilesWithOverlay (files )
121- }
122- tempPackageDir := filepath .Join (moduleRoot , moduleGenRoot , relDir )
123- if err := os .MkdirAll (tempPackageDir , 0o755 ); err != nil {
124- return err
125- }
126-
127- entries , err := os .ReadDir (packageDir )
128- if err != nil {
129- if os .IsNotExist (err ) {
130- continue
131- }
132- return err
133- }
134- for _ , entry := range entries {
135- if entry .IsDir () || filepath .Ext (entry .Name ()) != ".go" {
136- continue
137- }
138- src := filepath .Join (packageDir , entry .Name ())
139- data , err := os .ReadFile (src )
140- if err != nil {
141- return err
142- }
143- dst := filepath .Join (tempPackageDir , entry .Name ())
144- if err := os .WriteFile (dst , data , 0o600 ); err != nil {
145- return err
146- }
147- }
148- }
149-
150- for _ , f := range generatedFiles {
151- tempFilePath := filepath .Join (moduleRoot , f .relPath )
152- if err := os .MkdirAll (filepath .Dir (tempFilePath ), 0o755 ); err != nil {
153- return err
154- }
155- if err := os .WriteFile (tempFilePath , f .data , 0o600 ); err != nil {
156- return err
157- }
158- }
159-
160- goModContents := fmt .Sprintf ("module %s\n \n go 1.24.0\n " , goModule )
161- if currentModule != "" && currentModule != goModule {
162- goModContents += fmt .Sprintf ("\n require %s v0.0.0\n replace %s => %s\n " , currentModule , currentModule , cwd )
163- }
164- if err := os .WriteFile (filepath .Join (moduleRoot , "go.mod" ), []byte (goModContents ), 0o600 ); err != nil {
165- return err
23+ return compileGeneratedGoCodeWithOverlay (files )
16624 }
16725
168- out , err := runPreflightGoBuild (tempDir , moduleRoot , "build" , "-mod=mod" , "./..." )
169- if err != nil {
170- return preflightBuildError (out , err )
171- }
172-
173- return nil
174- }
175-
176- func generatedGoCodeCompilesWithOverlay (files codejen.Files ) error {
177- overlay := goBuildOverlay {
178- Replace : make (map [string ]string ),
179- }
180- generatedPackages := make (map [string ]struct {})
181- cwd , err := os .Getwd ()
26+ ctx , shouldUseTempModule , err := buildTempModuleContext (cfg , cwd , currentModule , goModule , files )
18227 if err != nil {
18328 return err
18429 }
185- tempDir , err := os .MkdirTemp ("" , "grafana-app-sdk-generate-preflight-*" )
186- if err != nil {
187- return err
30+ if ! shouldUseTempModule {
31+ return compileGeneratedGoCodeWithOverlay (files )
18832 }
189- defer os .RemoveAll (tempDir )
190-
191- fileIndex := 0
192- for _ , f := range files {
193- if filepath .Ext (f .RelativePath ) != ".go" {
194- continue
195- }
196-
197- absTargetPath := normalizeAbsolutePath (f .RelativePath , cwd )
198-
199- generatedPackages [filepath .Dir (absTargetPath )] = struct {}{}
200-
201- tempFilePath := filepath .Join (tempDir , fmt .Sprintf ("file-%06d.go" , fileIndex ))
202- fileIndex ++
203-
204- if err := os .WriteFile (tempFilePath , f .Data , 0o600 ); err != nil {
205- return err
206- }
207- overlay .Replace [absTargetPath ] = tempFilePath
208- }
209-
210- if len (generatedPackages ) == 0 {
33+ if len (ctx .generatedFiles ) == 0 {
21134 return nil
21235 }
21336
214- packages := make ([]string , 0 , len (generatedPackages ))
215- for pkg := range generatedPackages {
216- packages = append (packages , pkg )
217- }
218- sort .Strings (packages )
219-
220- overlayBytes , err := json .Marshal (overlay )
221- if err != nil {
222- return err
223- }
224- overlayPath := filepath .Join (tempDir , "overlay.json" )
225- if err := os .WriteFile (overlayPath , overlayBytes , 0o600 ); err != nil {
226- return err
227- }
228-
229- buildArgs := append ([]string {"build" , "-overlay" , overlayPath }, packages ... )
230- out , err := runPreflightGoBuild (tempDir , "" , buildArgs ... )
231- if err != nil {
232- return preflightBuildError (out , err )
233- }
234-
235- return nil
236- }
237-
238- type generatedGoFile struct {
239- absDir string
240- relPath string
241- data []byte
242- }
243-
244- type goBuildOverlay struct {
245- Replace map [string ]string `json:"Replace"`
37+ return compileGeneratedGoCodeWithTempModule (ctx )
24638}
24739
248- func useOverlayCompilationPreflight (goModule , currentModule string ) bool {
40+ func resolveGoModule (cfg * config.Config , currentModule string ) string {
41+ goModule := cfg .Codegen .GoModule
24942 if goModule == "" {
250- return true
251- }
252- return currentModule != "" && goModule == currentModule
253- }
254-
255- func normalizeAbsolutePath (path , cwd string ) string {
256- if path == "" {
257- path = "."
258- }
259- if ! filepath .IsAbs (path ) {
260- path = filepath .Join (cwd , path )
43+ return currentModule
26144 }
262- return filepath .Clean (path )
263- }
264-
265- func normalizeRelativePath (path string ) string {
266- if path == "" {
267- return "."
268- }
269- return filepath .Clean (path )
270- }
271-
272- func runPreflightGoBuild (tempDir , dir string , args ... string ) ([]byte , error ) {
273- buildCmd := exec .Command ("go" , args ... )
274- buildCmd .Dir = dir
275- buildCmd .Env = append (os .Environ (),
276- "GOSUMDB=off" ,
277- fmt .Sprintf ("GOCACHE=%s" , filepath .Join (tempDir , "gocache" )),
278- )
279- return buildCmd .CombinedOutput ()
280- }
281-
282- func preflightBuildError (out []byte , err error ) error {
283- return fmt .Errorf ("generated code contains compilation errors, this is likely a bug in sdk. If you'd like to bypass the compilation check, please set skipPreflightCompilationCheck to true.\n \n %s\n %w" , strings .TrimSpace (string (out )), err )
284- }
285-
286- type goModJSON struct {
287- Module struct {
288- Path string `json:"Path"`
289- } `json:"Module"`
290- }
291-
292- func getGoModule (goModPath string ) (string , error ) {
293- cmd := exec .Command ("go" , "mod" , "edit" , "-json" )
294- cmd .Dir = filepath .Dir (goModPath )
295- out , err := cmd .Output ()
296- if err != nil {
297- return "" , fmt .Errorf ("unable to run go mod edit --json: %w" , err )
298- }
299-
300- var mod goModJSON
301- if err := json .Unmarshal (out , & mod ); err == nil {
302- return mod .Module .Path , nil
303- }
304-
305- return "" , errors .New ("unable to locate module in go.mod file" )
45+ return goModule
30646}
0 commit comments