github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/builder/library.go (about) 1 package builder 2 3 import ( 4 "errors" 5 "io/fs" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "sync" 11 12 "github.com/tinygo-org/tinygo/compileopts" 13 "github.com/tinygo-org/tinygo/goenv" 14 ) 15 16 // Library is a container for information about a single C library, such as a 17 // compiler runtime or libc. 18 type Library struct { 19 // The library name, such as compiler-rt or picolibc. 20 name string 21 22 // makeHeaders creates a header include dir for the library 23 makeHeaders func(target, includeDir string) error 24 25 // cflags returns the C flags specific to this library 26 cflags func(target, headerPath string) []string 27 28 // The source directory. 29 sourceDir func() string 30 31 // The source files, relative to sourceDir. 32 librarySources func(target string) ([]string, error) 33 34 // The source code for the crt1.o file, relative to sourceDir. 35 crt1Source string 36 } 37 38 // Load the library archive, possibly generating and caching it if needed. 39 // The resulting directory may be stored in the provided tmpdir, which is 40 // expected to be removed after the Load call. 41 func (l *Library) Load(config *compileopts.Config, tmpdir string) (dir string, err error) { 42 job, unlock, err := l.load(config, tmpdir) 43 if err != nil { 44 return "", err 45 } 46 defer unlock() 47 err = runJobs(job, config.Options.Semaphore) 48 return filepath.Dir(job.result), err 49 } 50 51 // load returns a compile job to build this library file for the given target 52 // and CPU. It may return a dummy compileJob if the library build is already 53 // cached. The path is stored as job.result but is only valid after the job has 54 // been run. 55 // The provided tmpdir will be used to store intermediary files and possibly the 56 // output archive file, it is expected to be removed after use. 57 // As a side effect, this call creates the library header files if they didn't 58 // exist yet. 59 func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJob, abortLock func(), err error) { 60 outdir, precompiled := config.LibcPath(l.name) 61 archiveFilePath := filepath.Join(outdir, "lib.a") 62 if precompiled { 63 // Found a precompiled library for this OS/architecture. Return the path 64 // directly. 65 return dummyCompileJob(archiveFilePath), func() {}, nil 66 } 67 68 // Create a lock on the output (if supported). 69 // This is a bit messy, but avoids a deadlock because it is ordered consistently with other library loads within a build. 70 outname := filepath.Base(outdir) 71 unlock := lock(filepath.Join(goenv.Get("GOCACHE"), outname+".lock")) 72 var ok bool 73 defer func() { 74 if !ok { 75 unlock() 76 } 77 }() 78 79 // Try to fetch this library from the cache. 80 if _, err := os.Stat(archiveFilePath); err == nil { 81 return dummyCompileJob(archiveFilePath), func() {}, nil 82 } 83 // Cache miss, build it now. 84 85 // Create the destination directory where the components of this library 86 // (lib.a file, include directory) are placed. 87 err = os.MkdirAll(filepath.Join(goenv.Get("GOCACHE"), outname), 0o777) 88 if err != nil { 89 // Could not create directory (and not because it already exists). 90 return nil, nil, err 91 } 92 93 // Make headers if needed. 94 headerPath := filepath.Join(outdir, "include") 95 target := config.Triple() 96 if l.makeHeaders != nil { 97 if _, err = os.Stat(headerPath); err != nil { 98 temporaryHeaderPath, err := os.MkdirTemp(outdir, "include.tmp*") 99 if err != nil { 100 return nil, nil, err 101 } 102 defer os.RemoveAll(temporaryHeaderPath) 103 err = l.makeHeaders(target, temporaryHeaderPath) 104 if err != nil { 105 return nil, nil, err 106 } 107 err = os.Chmod(temporaryHeaderPath, 0o755) // TempDir uses 0o700 by default 108 if err != nil { 109 return nil, nil, err 110 } 111 err = os.Rename(temporaryHeaderPath, headerPath) 112 if err != nil { 113 switch { 114 case errors.Is(err, fs.ErrExist): 115 // Another invocation of TinyGo also seems to have already created the headers. 116 117 case runtime.GOOS == "windows" && errors.Is(err, fs.ErrPermission): 118 // On Windows, a rename with a destination directory that already 119 // exists does not result in an IsExist error, but rather in an 120 // access denied error. To be sure, check for this case by checking 121 // whether the target directory exists. 122 if _, err := os.Stat(headerPath); err == nil { 123 break 124 } 125 fallthrough 126 127 default: 128 return nil, nil, err 129 } 130 } 131 } 132 } 133 134 remapDir := filepath.Join(os.TempDir(), "tinygo-"+l.name) 135 dir := filepath.Join(tmpdir, "build-lib-"+l.name) 136 err = os.Mkdir(dir, 0777) 137 if err != nil { 138 return nil, nil, err 139 } 140 141 // Precalculate the flags to the compiler invocation. 142 // Note: -fdebug-prefix-map is necessary to make the output archive 143 // reproducible. Otherwise the temporary directory is stored in the archive 144 // itself, which varies each run. 145 args := append(l.cflags(target, headerPath), "-c", "-Oz", "-gdwarf-4", "-ffunction-sections", "-fdata-sections", "-Wno-macro-redefined", "--target="+target, "-fdebug-prefix-map="+dir+"="+remapDir) 146 resourceDir := goenv.ClangResourceDir(false) 147 if resourceDir != "" { 148 args = append(args, "-resource-dir="+resourceDir) 149 } 150 cpu := config.CPU() 151 if cpu != "" { 152 // X86 has deprecated the -mcpu flag, so we need to use -march instead. 153 // However, ARM has not done this. 154 if strings.HasPrefix(target, "i386") || strings.HasPrefix(target, "x86_64") { 155 args = append(args, "-march="+cpu) 156 } else if strings.HasPrefix(target, "avr") { 157 args = append(args, "-mmcu="+cpu) 158 } else { 159 args = append(args, "-mcpu="+cpu) 160 } 161 } 162 if config.ABI() != "" { 163 args = append(args, "-mabi="+config.ABI()) 164 } 165 if strings.HasPrefix(target, "arm") || strings.HasPrefix(target, "thumb") { 166 if strings.Split(target, "-")[2] == "linux" { 167 args = append(args, "-fno-unwind-tables", "-fno-asynchronous-unwind-tables") 168 } else { 169 args = append(args, "-fshort-enums", "-fomit-frame-pointer", "-mfloat-abi=soft", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables") 170 } 171 } 172 if strings.HasPrefix(target, "avr") { 173 // AVR defaults to C float and double both being 32-bit. This deviates 174 // from what most code (and certainly compiler-rt) expects. So we need 175 // to force the compiler to use 64-bit floating point numbers for 176 // double. 177 args = append(args, "-mdouble=64") 178 } 179 if strings.HasPrefix(target, "riscv32-") { 180 args = append(args, "-march=rv32imac", "-fforce-enable-int128") 181 } 182 if strings.HasPrefix(target, "riscv64-") { 183 args = append(args, "-march=rv64gc") 184 } 185 186 var once sync.Once 187 188 // Create job to put all the object files in a single archive. This archive 189 // file is the (static) library file. 190 var objs []string 191 job = &compileJob{ 192 description: "ar " + l.name + "/lib.a", 193 result: filepath.Join(goenv.Get("GOCACHE"), outname, "lib.a"), 194 run: func(*compileJob) error { 195 defer once.Do(unlock) 196 197 // Create an archive of all object files. 198 f, err := os.CreateTemp(outdir, "libc.a.tmp*") 199 if err != nil { 200 return err 201 } 202 err = makeArchive(f, objs) 203 if err != nil { 204 return err 205 } 206 err = f.Close() 207 if err != nil { 208 return err 209 } 210 err = os.Chmod(f.Name(), 0o644) // TempFile uses 0o600 by default 211 if err != nil { 212 return err 213 } 214 // Store this archive in the cache. 215 return os.Rename(f.Name(), archiveFilePath) 216 }, 217 } 218 219 sourceDir := l.sourceDir() 220 221 // Create jobs to compile all sources. These jobs are depended upon by the 222 // archive job above, so must be run first. 223 paths, err := l.librarySources(target) 224 if err != nil { 225 return nil, nil, err 226 } 227 for _, path := range paths { 228 // Strip leading "../" parts off the path. 229 cleanpath := path 230 for strings.HasPrefix(cleanpath, "../") { 231 cleanpath = cleanpath[3:] 232 } 233 srcpath := filepath.Join(sourceDir, path) 234 objpath := filepath.Join(dir, cleanpath+".o") 235 os.MkdirAll(filepath.Dir(objpath), 0o777) 236 objs = append(objs, objpath) 237 job.dependencies = append(job.dependencies, &compileJob{ 238 description: "compile " + srcpath, 239 run: func(*compileJob) error { 240 var compileArgs []string 241 compileArgs = append(compileArgs, args...) 242 compileArgs = append(compileArgs, "-o", objpath, srcpath) 243 if config.Options.PrintCommands != nil { 244 config.Options.PrintCommands("clang", compileArgs...) 245 } 246 err := runCCompiler(compileArgs...) 247 if err != nil { 248 return &commandError{"failed to build", srcpath, err} 249 } 250 return nil 251 }, 252 }) 253 } 254 255 // Create crt1.o job, if needed. 256 // Add this as a (fake) dependency to the ar file so it gets compiled. 257 // (It could be done in parallel with creating the ar file, but it probably 258 // won't make much of a difference in speed). 259 if l.crt1Source != "" { 260 srcpath := filepath.Join(sourceDir, l.crt1Source) 261 job.dependencies = append(job.dependencies, &compileJob{ 262 description: "compile " + srcpath, 263 run: func(*compileJob) error { 264 var compileArgs []string 265 compileArgs = append(compileArgs, args...) 266 tmpfile, err := os.CreateTemp(outdir, "crt1.o.tmp*") 267 if err != nil { 268 return err 269 } 270 tmpfile.Close() 271 compileArgs = append(compileArgs, "-o", tmpfile.Name(), srcpath) 272 if config.Options.PrintCommands != nil { 273 config.Options.PrintCommands("clang", compileArgs...) 274 } 275 err = runCCompiler(compileArgs...) 276 if err != nil { 277 return &commandError{"failed to build", srcpath, err} 278 } 279 return os.Rename(tmpfile.Name(), filepath.Join(outdir, "crt1.o")) 280 }, 281 }) 282 } 283 284 ok = true 285 return job, func() { 286 once.Do(unlock) 287 }, nil 288 }