@@ -10,7 +10,6 @@ import (
1010 "errors"
1111 "fmt"
1212 "io"
13- "io/fs"
1413 "os"
1514 "path/filepath"
1615 "sort"
@@ -25,37 +24,21 @@ import (
2524// Compiling the same file again (if nothing changed, including included header
2625// files) the output is loaded from the build cache instead.
2726//
28- // Its operation is a bit complex (more complex than Go package build caching)
29- // because the list of file dependencies is only known after the file is
30- // compiled. However, luckily compilers have a flag to write a list of file
31- // dependencies in Makefile syntax which can be used for caching .
27+ // Its operation is a bit complex (more complex than Go package build caching),
28+ // because the list of file dependencies depends on C include path resolution.
29+ // TinyGo asks Clang for the current dependency list before looking for an object
30+ // cache hit, then uses the hashes of those dependencies in the object key .
3231//
33- // Because of this complexity, every file has in fact two cached build outputs:
34- // the file itself, and the list of dependencies. Its operation is as follows:
35- //
36- // depfile = hash(path, compiler, cflags, ...)
37- // if depfile exists:
38- // outfile = hash of all files and depfile name
39- // if outfile exists:
40- // # cache hit
41- // return outfile
42- // # cache miss
32+ // dependencies = clang -M source
33+ // outfile = hash(path, compiler, cflags, dependencies, ...)
34+ // if outfile exists:
35+ // # cache hit
36+ // return outfile
4337// tmpfile = compile file
44- // read dependencies (side effect of compile)
45- // write depfile
46- // outfile = hash of all files and depfile name
4738// rename tmpfile to outfile
4839//
49- // There are a few edge cases that are not handled:
50- // - If a file is added to an include path, that file may be included instead of
51- // some other file. This would be fixed by also including lookup failures in the
52- // dependencies file, but I'm not aware of a compiler which does that.
53- // - The Makefile syntax that compilers output has issues, see readDepFile for
54- // details.
55- // - A header file may be changed to add/remove an include. This invalidates the
56- // depfile but without invalidating its name. For this reason, the depfile is
57- // written on each new compilation (even when it seems unnecessary). However, it
58- // could in rare cases lead to a stale file fetched from the cache.
40+ // The Makefile syntax that compilers output has issues, see readDepFile for
41+ // details.
5942func compileAndCacheCFile (abspath , tmpdir string , cflags []string , printCommands func (string , ... string )) (string , error ) {
6043 // Hash input file.
6144 fileHash , err := hashFile (abspath )
@@ -67,65 +50,47 @@ func compileAndCacheCFile(abspath, tmpdir string, cflags []string, printCommands
6750 unlock := lock (filepath .Join (goenv .Get ("GOCACHE" ), fileHash + ".c.lock" ))
6851 defer unlock ()
6952
70- // Create cache key for the dependencies file.
71- buf , err := json .Marshal (struct {
72- Path string
73- Hash string
74- Flags []string
75- LLVMVersion string
76- }{
77- Path : abspath ,
78- Hash : fileHash ,
79- Flags : cflags ,
80- LLVMVersion : llvm .Version ,
81- })
53+ compilerID , err := clangCompilerIdentity ()
8254 if err != nil {
83- panic ( err ) // shouldn't happen
55+ return "" , err
8456 }
85- depfileNameHashBuf := sha512 .Sum512_224 (buf )
86- depfileNameHash := hex .EncodeToString (depfileNameHashBuf [:])
87-
88- // Load dependencies file, if possible.
89- depfileName := "dep-" + depfileNameHash + ".json"
90- depfileCachePath := filepath .Join (goenv .Get ("GOCACHE" ), depfileName )
91- depfileBuf , err := os .ReadFile (depfileCachePath )
92- var dependencies []string // sorted list of dependency paths
93- if err == nil {
94- // There is a dependency file, that's great!
95- // Parse it first.
96- err := json .Unmarshal (depfileBuf , & dependencies )
97- if err != nil {
98- return "" , fmt .Errorf ("could not parse dependencies JSON: %w" , err )
99- }
10057
101- // Obtain hashes of all the files listed as a dependency.
102- outpath , err := makeCFileCachePath ( dependencies , depfileNameHash )
103- if err == nil {
104- if _ , err := os . Stat ( outpath ); err == nil {
105- return outpath , nil
106- } else if ! errors . Is ( err , fs . ErrNotExist ) {
107- return "" , err
108- }
109- }
110- } else if ! errors . Is ( err , fs . ErrNotExist ) {
111- // expected either nil or IsNotExist
58+ dependencies , err := scanCFileDependencies ( abspath , tmpdir , cflags , printCommands )
59+ if err != nil {
60+ return "" , err
61+ }
62+ outpath , err := makeCFileCachePath ( abspath , cFileCompileArgs ( abspath , "$OBJ" , cflags ), compilerID , dependencies )
63+ if err != nil {
64+ return "" , err
65+ }
66+ if _ , err := os . Stat ( outpath ); err == nil {
67+ return outpath , nil
68+ } else if ! errors . Is ( err , os . ErrNotExist ) {
11269 return "" , err
11370 }
11471
115- objTmpFile , err := os . CreateTemp (goenv .Get ("GOCACHE" ), "tmp-*.bc" )
72+ objTmpFile , err := compileCFile (goenv .Get ("GOCACHE" ), abspath , cflags , printCommands )
11673 if err != nil {
11774 return "" , err
11875 }
119- objTmpFile .Close ()
120- depTmpFile , err := os .CreateTemp (tmpdir , "dep-*.d" )
76+ err = os .Rename (objTmpFile , outpath )
12177 if err != nil {
12278 return "" , err
12379 }
80+ return outpath , nil
81+ }
82+
83+ func scanCFileDependencies (abspath , tmpdir string , cflags []string , printCommands func (string , ... string )) ([]string , error ) {
84+ depTmpFile , err := os .CreateTemp (tmpdir , "dep-*.d" )
85+ if err != nil {
86+ return nil , err
87+ }
12488 depTmpFile .Close ()
125- flags := append ([]string {}, cflags ... ) // copy cflags
126- flags = append (flags , "-MD" , "-MV" , "-MTdeps" , "-MF" , depTmpFile .Name (), "-flto=thin" ) // autogenerate dependencies
127- flags = append (flags , "-c" , "-o" , objTmpFile .Name (), abspath )
128- if strings .ToLower (filepath .Ext (abspath )) == ".s" {
89+ defer os .Remove (depTmpFile .Name ())
90+
91+ flags := append ([]string {}, cflags ... )
92+ flags = append (flags , "-M" , "-MV" , "-MTdeps" , "-MF" , depTmpFile .Name (), abspath )
93+ if isAssemblyFile (abspath ) {
12994 // If this is an assembly file (.s or .S, lowercase or uppercase), then
13095 // we'll need to add -Qunused-arguments because many parameters are
13196 // relevant to C, not assembly. And with -Werror, having meaningless
@@ -137,13 +102,12 @@ func compileAndCacheCFile(abspath, tmpdir string, cflags []string, printCommands
137102 }
138103 err = runCCompiler (flags ... )
139104 if err != nil {
140- return "" , & commandError {"failed to build " , abspath , err }
105+ return nil , & commandError {"failed to scan dependencies " , abspath , err }
141106 }
142107
143- // Create sorted and uniqued slice of dependencies.
144108 dependencyPaths , err := readDepFile (depTmpFile .Name ())
145109 if err != nil {
146- return "" , err
110+ return nil , err
147111 }
148112 dependencyPaths = append (dependencyPaths , abspath ) // necessary for .s files
149113 dependencySet := make (map [string ]struct {}, len (dependencyPaths ))
@@ -156,50 +120,44 @@ func compileAndCacheCFile(abspath, tmpdir string, cflags []string, printCommands
156120 dependencySlice = append (dependencySlice , path )
157121 }
158122 sort .Strings (dependencySlice )
123+ return dependencySlice , nil
124+ }
159125
160- // Write dependencies file.
161- f , err := os .CreateTemp (filepath .Dir (depfileCachePath ), depfileName )
162- if err != nil {
163- return "" , err
126+ func cFileCompileArgs (abspath , objpath string , cflags []string ) []string {
127+ flags := append ([]string {}, cflags ... )
128+ flags = append (flags , "-flto=thin" )
129+ flags = append (flags , "-c" , "-o" , objpath , abspath )
130+ if isAssemblyFile (abspath ) {
131+ flags = append (flags , "-Qunused-arguments" )
164132 }
133+ return flags
134+ }
165135
166- buf , err = json .MarshalIndent (dependencySlice , "" , "\t " )
167- if err != nil {
168- panic (err ) // shouldn't happen
169- }
170- _ , err = f .Write (buf )
171- if err != nil {
172- return "" , err
173- }
174- err = f .Close ()
175- if err != nil {
176- return "" , err
177- }
178- err = os .Rename (f .Name (), depfileCachePath )
136+ func compileCFile (cacheDir , abspath string , cflags []string , printCommands func (string , ... string )) (string , error ) {
137+ objTmpFile , err := os .CreateTemp (cacheDir , "tmp-*.bc" )
179138 if err != nil {
180139 return "" , err
181140 }
141+ objTmpFile .Close ()
182142
183- // Move temporary object file to final location.
184- outpath , err := makeCFileCachePath (dependencySlice , depfileNameHash )
185- if err != nil {
186- return "" , err
143+ flags := cFileCompileArgs (abspath , objTmpFile .Name (), cflags )
144+ if printCommands != nil {
145+ printCommands ("clang" , flags ... )
187146 }
188- err = os . Rename ( objTmpFile . Name (), outpath )
147+ err = runCCompiler ( flags ... )
189148 if err != nil {
190- return "" , err
149+ return "" , & commandError { "failed to build" , abspath , err }
191150 }
192-
193- return outpath , nil
151+ return objTmpFile .Name (), nil
194152}
195153
196154// Create a cache path (a path in GOCACHE) to store the output of a compiler
197- // job. This path is based on the dep file name (which is a hash of metadata
198- // including compiler flags) and the hash of all input files in the paths slice .
199- func makeCFileCachePath (paths []string , depfileNameHash string ) (string , error ) {
155+ // job. This path is based on the compiler identity, compiler flags, and the
156+ // hash of all dependency files.
157+ func makeCFileCachePath (path string , flags []string , compilerID string , dependencies [] string ) (string , error ) {
200158 // Hash all input files.
201- fileHashes := make (map [string ]string , len (paths ))
202- for _ , path := range paths {
159+ fileHashes := make (map [string ]string , len (dependencies ))
160+ for _ , path := range dependencies {
203161 hash , err := hashFile (path )
204162 if err != nil {
205163 return "" , err
@@ -209,11 +167,17 @@ func makeCFileCachePath(paths []string, depfileNameHash string) (string, error)
209167
210168 // Calculate a cache key based on the above hashes.
211169 buf , err := json .Marshal (struct {
212- DepfileHash string
213- FileHashes map [string ]string
170+ Path string
171+ Flags []string
172+ LLVMVersion string
173+ CompilerIdentity string
174+ FileHashes map [string ]string
214175 }{
215- DepfileHash : depfileNameHash ,
216- FileHashes : fileHashes ,
176+ Path : path ,
177+ Flags : flags ,
178+ LLVMVersion : llvm .Version ,
179+ CompilerIdentity : compilerID ,
180+ FileHashes : fileHashes ,
217181 })
218182 if err != nil {
219183 panic (err ) // shouldn't happen
@@ -225,6 +189,10 @@ func makeCFileCachePath(paths []string, depfileNameHash string) (string, error)
225189 return outpath , nil
226190}
227191
192+ func isAssemblyFile (path string ) bool {
193+ return strings .ToLower (filepath .Ext (path )) == ".s"
194+ }
195+
228196// hashFile hashes the given file path and returns the hash as a hex string.
229197func hashFile (path string ) (string , error ) {
230198 f , err := os .Open (path )
0 commit comments