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