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  }