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