library.go raw

   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