library.go raw

   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