diff --git a/Dockerfile b/Dockerfile index 9328f025..f1b04d2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,7 @@ RUN cd /tinygo/ && \ git submodule sync && \ git submodule update --init --recursive --force -COPY ./lib/picolibc-* /tinygo/lib/ -COPY ./lib/picolibc-include/* /tinygo/lib/picolibc-include/ +COPY ./lib/picolibc-stdio.c /tinygo/lib/picolibc-stdio.c RUN cd /tinygo/ && \ go install /tinygo/ diff --git a/Makefile b/Makefile index 19ca38ff..645b6fad 100644 --- a/Makefile +++ b/Makefile @@ -500,17 +500,16 @@ build/release: tinygo gen-device wasi-libc @cp -rp lib/picolibc/newlib/libc/string build/release/tinygo/lib/picolibc/newlib/libc @cp -rp lib/picolibc/newlib/libc/tinystdio build/release/tinygo/lib/picolibc/newlib/libc @cp -rp lib/picolibc/newlib/libm/common build/release/tinygo/lib/picolibc/newlib/libm - @cp -rp lib/picolibc-include build/release/tinygo/lib @cp -rp lib/picolibc-stdio.c build/release/tinygo/lib @cp -rp lib/wasi-libc/sysroot build/release/tinygo/lib/wasi-libc/sysroot @cp -rp src build/release/tinygo/src @cp -rp targets build/release/tinygo/targets - ./build/tinygo build-library -target=armv6m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv6m-unknown-unknown-eabi/compiler-rt.a compiler-rt - ./build/tinygo build-library -target=armv7m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7m-unknown-unknown-eabi/compiler-rt.a compiler-rt - ./build/tinygo build-library -target=armv7em-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7em-unknown-unknown-eabi/compiler-rt.a compiler-rt - ./build/tinygo build-library -target=armv6m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv6m-unknown-unknown-eabi/picolibc.a picolibc - ./build/tinygo build-library -target=armv7m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7m-unknown-unknown-eabi/picolibc.a picolibc - ./build/tinygo build-library -target=armv7em-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7em-unknown-unknown-eabi/picolibc.a picolibc + ./build/tinygo build-library -target=armv6m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv6m-unknown-unknown-eabi/compiler-rt compiler-rt + ./build/tinygo build-library -target=armv7m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7m-unknown-unknown-eabi/compiler-rt compiler-rt + ./build/tinygo build-library -target=armv7em-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7em-unknown-unknown-eabi/compiler-rt compiler-rt + ./build/tinygo build-library -target=armv6m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv6m-unknown-unknown-eabi/picolibc picolibc + ./build/tinygo build-library -target=armv7m-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7m-unknown-unknown-eabi/picolibc picolibc + ./build/tinygo build-library -target=armv7em-unknown-unknown-eabi -o build/release/tinygo/pkg/armv7em-unknown-unknown-eabi/picolibc picolibc release: build/release tar -czf build/release.tar.gz -C build/release tinygo diff --git a/builder/ar.go b/builder/ar.go index ee8ba070..aee6f00c 100644 --- a/builder/ar.go +++ b/builder/ar.go @@ -18,17 +18,12 @@ import ( // given as a parameter. It is equivalent to the following command: // // ar -rcs -func makeArchive(archivePath string, objs []string) error { +func makeArchive(arfile *os.File, objs []string) error { // Open the archive file. - arfile, err := os.Create(archivePath) - if err != nil { - return err - } - defer arfile.Close() arwriter := ar.NewWriter(arfile) - err = arwriter.WriteGlobalHeader() + err := arwriter.WriteGlobalHeader() if err != nil { - return &os.PathError{Op: "write ar header", Path: archivePath, Err: err} + return &os.PathError{Op: "write ar header", Path: arfile.Name(), Err: err} } // Open all object files and read the symbols for the symbol table. @@ -133,7 +128,7 @@ func makeArchive(archivePath string, objs []string) error { return err } if int64(int32(offset)) != offset { - return errors.New("large archives (4GB+) not supported: " + archivePath) + return errors.New("large archives (4GB+) not supported: " + arfile.Name()) } objfiles[i].archiveOffset = int32(offset) @@ -160,7 +155,7 @@ func makeArchive(archivePath string, objs []string) error { return err } if n != st.Size() { - return errors.New("file modified during ar creation: " + archivePath) + return errors.New("file modified during ar creation: " + arfile.Name()) } // File is not needed anymore. diff --git a/builder/build.go b/builder/build.go index 436d0a10..18c9d8a3 100644 --- a/builder/build.go +++ b/builder/build.go @@ -86,6 +86,30 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil } defer os.RemoveAll(dir) + // Check for a libc dependency. + // As a side effect, this also creates the headers for the given libc, if + // the libc needs them. + root := goenv.Get("TINYGOROOT") + var libcDependencies []*compileJob + switch config.Target.Libc { + case "picolibc": + libcJob, err := Picolibc.load(config, dir) + if err != nil { + return err + } + libcDependencies = append(libcDependencies, libcJob) + case "wasi-libc": + path := filepath.Join(root, "lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a") + if _, err := os.Stat(path); os.IsNotExist(err) { + return errors.New("could not find wasi-libc, perhaps you need to run `make wasi-libc`?") + } + libcDependencies = append(libcDependencies, dummyCompileJob(path)) + case "": + // no library specified, so nothing to do + default: + return fmt.Errorf("unknown libc: %s", config.Target.Libc) + } + optLevel, sizeLevel, _ := config.OptLevels() compilerConfig := &compiler.Config{ Triple: config.Triple(), @@ -489,7 +513,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil // Add compiler-rt dependency if needed. Usually this is a simple load from // a cache. if config.Target.RTLib == "compiler-rt" { - job, err := CompilerRT.load(config.Triple(), config.CPU(), dir) + job, err := CompilerRT.load(config, dir) if err != nil { return err } @@ -499,7 +523,6 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil // Add jobs to compile extra files. These files are in C or assembly and // contain things like the interrupt vector table and low level operations // such as stack switching. - root := goenv.Get("TINYGOROOT") for _, path := range config.ExtraFiles() { abspath := filepath.Join(root, path) job := &compileJob{ @@ -538,26 +561,8 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil ldflags = append(ldflags, lprogram.LDFlags...) } - // Add libc dependency if needed. - switch config.Target.Libc { - case "picolibc": - job, err := Picolibc.load(config.Triple(), config.CPU(), dir) - if err != nil { - return err - } - linkerDependencies = append(linkerDependencies, job) - case "wasi-libc": - path := filepath.Join(root, "lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a") - if _, err := os.Stat(path); os.IsNotExist(err) { - return errors.New("could not find wasi-libc, perhaps you need to run `make wasi-libc`?") - } - job := dummyCompileJob(path) - linkerDependencies = append(linkerDependencies, job) - case "": - // no library specified, so nothing to do - default: - return fmt.Errorf("unknown libc: %s", config.Target.Libc) - } + // Add libc dependencies, if they exist. + linkerDependencies = append(linkerDependencies, libcDependencies...) // Strip debug information with -no-debug. if !config.Debug() { diff --git a/builder/buildcache.go b/builder/buildcache.go deleted file mode 100644 index b673de06..00000000 --- a/builder/buildcache.go +++ /dev/null @@ -1,105 +0,0 @@ -package builder - -import ( - "io" - "os" - "path/filepath" - "time" - - "github.com/tinygo-org/tinygo/goenv" -) - -// Return the newest timestamp of all the file paths passed in. Used to check -// for stale caches. -func cacheTimestamp(paths []string) (time.Time, error) { - var timestamp time.Time - for _, path := range paths { - st, err := os.Stat(path) - if err != nil { - return time.Time{}, err - } - if timestamp.IsZero() { - timestamp = st.ModTime() - } else if timestamp.Before(st.ModTime()) { - timestamp = st.ModTime() - } - } - return timestamp, nil -} - -// Try to load a given file from the cache. Return "", nil if no cached file can -// be found (or the file is stale), return the absolute path if there is a cache -// and return an error on I/O errors. -func cacheLoad(name string, sourceFiles []string) (string, error) { - cachepath := filepath.Join(goenv.Get("GOCACHE"), name) - cacheStat, err := os.Stat(cachepath) - if os.IsNotExist(err) { - return "", nil // does not exist - } else if err != nil { - return "", err // cannot stat cache file - } - - sourceTimestamp, err := cacheTimestamp(sourceFiles) - if err != nil { - return "", err // cannot stat source files - } - - if cacheStat.ModTime().After(sourceTimestamp) { - return cachepath, nil - } else { - os.Remove(cachepath) - // stale cache - return "", nil - } -} - -// Store the file located at tmppath in the cache with the given name. The -// tmppath may or may not be gone afterwards. -func cacheStore(tmppath, name string, sourceFiles []string) (string, error) { - // get the last modified time - if len(sourceFiles) == 0 { - panic("cache: no source files") - } - - // TODO: check the config key - - dir := goenv.Get("GOCACHE") - err := os.MkdirAll(dir, 0777) - if err != nil { - return "", err - } - cachepath := filepath.Join(dir, name) - err = copyFile(tmppath, cachepath) - if err != nil { - return "", err - } - return cachepath, nil -} - -// copyFile copies the given file from src to dst. It can copy over -// a possibly already existing file at the destination. -func copyFile(src, dst string) error { - inf, err := os.Open(src) - if err != nil { - return err - } - defer inf.Close() - outpath := dst + ".tmp" - outf, err := os.Create(outpath) - if err != nil { - return err - } - - _, err = io.Copy(outf, inf) - if err != nil { - os.Remove(outpath) - return err - } - - err = outf.Close() - if err != nil { - return err - } - - return os.Rename(dst+".tmp", dst) -} diff --git a/builder/builtins.go b/builder/builtins.go index ed159e7e..33bcc99a 100644 --- a/builder/builtins.go +++ b/builder/builtins.go @@ -157,8 +157,10 @@ var aeabiBuiltins = []string{ // // For more information, see: https://compiler-rt.llvm.org/ var CompilerRT = Library{ - name: "compiler-rt", - cflags: func() []string { return []string{"-Werror", "-Wall", "-std=c11", "-nostdlibinc"} }, + name: "compiler-rt", + cflags: func(headerPath string) []string { + return []string{"-Werror", "-Wall", "-std=c11", "-nostdlibinc"} + }, sourceDir: "lib/compiler-rt/lib/builtins", sources: func(target string) []string { builtins := append([]string{}, genericBuiltins...) // copy genericBuiltins diff --git a/builder/library.go b/builder/library.go index 7f7b77a5..0e09fa44 100644 --- a/builder/library.go +++ b/builder/library.go @@ -1,10 +1,12 @@ package builder import ( + "io/ioutil" "os" "path/filepath" "strings" + "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/goenv" ) @@ -14,7 +16,11 @@ type Library struct { // The library name, such as compiler-rt or picolibc. name string - cflags func() []string + // makeHeaders creates a header include dir for the library + makeHeaders func(includeDir string) error + + // cflags returns the C flags specific to this library + cflags func(headerPath string) []string // The source directory, relative to TINYGOROOT. sourceDir string @@ -39,15 +45,15 @@ func (l *Library) sourcePaths(target string) []string { } // Load the library archive, possibly generating and caching it if needed. -// The resulting file is stored in the provided tmpdir, which is expected to be -// removed after the Load call. -func (l *Library) Load(target, tmpdir string) (path string, err error) { - job, err := l.load(target, "", tmpdir) +// The resulting directory may be stored in the provided tmpdir, which is +// expected to be removed after the Load call. +func (l *Library) Load(config *compileopts.Config, tmpdir string) (dir string, err error) { + job, err := l.load(config, tmpdir) if err != nil { return "", err } err = runJobs(job) - return job.result, err + return filepath.Dir(job.result), err } // load returns a compile job to build this library file for the given target @@ -56,29 +62,52 @@ func (l *Library) Load(target, tmpdir string) (path string, err error) { // been run. // The provided tmpdir will be used to store intermediary files and possibly the // output archive file, it is expected to be removed after use. -func (l *Library) load(target, cpu, tmpdir string) (job *compileJob, err error) { - // Try to load a precompiled library. - precompiledPath := filepath.Join(goenv.Get("TINYGOROOT"), "pkg", target, l.name+".a") - if _, err := os.Stat(precompiledPath); err == nil { +// As a side effect, this call creates the library header files if they didn't +// exist yet. +func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJob, err error) { + outdir, precompiled := config.LibcPath(l.name) + archiveFilePath := filepath.Join(outdir, "lib.a") + if precompiled { // Found a precompiled library for this OS/architecture. Return the path // directly. - return dummyCompileJob(precompiledPath), nil - } - - var outfile string - if cpu != "" { - outfile = l.name + "-" + target + "-" + cpu + ".a" - } else { - outfile = l.name + "-" + target + ".a" + return dummyCompileJob(archiveFilePath), nil } // Try to fetch this library from the cache. - if path, err := cacheLoad(outfile, l.sourcePaths(target)); path != "" || err != nil { - // Cache hit. - return dummyCompileJob(path), nil + if _, err := os.Stat(archiveFilePath); err == nil { + return dummyCompileJob(archiveFilePath), nil } // Cache miss, build it now. + // Create the destination directory where the components of this library + // (lib.a file, include directory) are placed. + outname := filepath.Base(outdir) + err = os.MkdirAll(filepath.Join(goenv.Get("GOCACHE"), outname), 0o777) + if err != nil { + // Could not create directory (and not because it already exists). + return nil, err + } + + // Make headers if needed. + headerPath := filepath.Join(outdir, "include") + if l.makeHeaders != nil { + if _, err = os.Stat(headerPath); err != nil { + temporaryHeaderPath, err := ioutil.TempDir(outdir, "include.tmp*") + if err != nil { + return nil, err + } + defer os.RemoveAll(temporaryHeaderPath) + err = l.makeHeaders(temporaryHeaderPath) + if err != nil { + return nil, err + } + err = os.Rename(temporaryHeaderPath, headerPath) + if err != nil { + return nil, err + } + } + } + remapDir := filepath.Join(os.TempDir(), "tinygo-"+l.name) dir := filepath.Join(tmpdir, "build-lib-"+l.name) err = os.Mkdir(dir, 0777) @@ -90,7 +119,9 @@ func (l *Library) load(target, cpu, tmpdir string) (job *compileJob, err error) // Note: -fdebug-prefix-map is necessary to make the output archive // reproducible. Otherwise the temporary directory is stored in the archive // itself, which varies each run. - args := append(l.cflags(), "-c", "-Oz", "-g", "-ffunction-sections", "-fdata-sections", "-Wno-macro-redefined", "--target="+target, "-fdebug-prefix-map="+dir+"="+remapDir) + target := config.Triple() + args := append(l.cflags(headerPath), "-c", "-Oz", "-g", "-ffunction-sections", "-fdata-sections", "-Wno-macro-redefined", "--target="+target, "-fdebug-prefix-map="+dir+"="+remapDir) + cpu := config.CPU() if cpu != "" { args = append(args, "-mcpu="+cpu) } @@ -107,19 +138,25 @@ func (l *Library) load(target, cpu, tmpdir string) (job *compileJob, err error) // Create job to put all the object files in a single archive. This archive // file is the (static) library file. var objs []string - arpath := filepath.Join(dir, l.name+".a") job = &compileJob{ - description: "ar " + l.name + ".a", - result: arpath, + description: "ar " + l.name + "/lib.a", + result: filepath.Join(goenv.Get("GOCACHE"), outname, "lib.a"), run: func(*compileJob) error { // Create an archive of all object files. - err := makeArchive(arpath, objs) + f, err := ioutil.TempFile(outdir, "libc.a.tmp*") + if err != nil { + return err + } + err = makeArchive(f, objs) + if err != nil { + return err + } + err = f.Close() if err != nil { return err } // Store this archive in the cache. - _, err = cacheStore(arpath, outfile, l.sourcePaths(target)) - return err + return os.Rename(f.Name(), archiveFilePath) }, } diff --git a/builder/picolibc.go b/builder/picolibc.go index 93a2639d..273c1360 100644 --- a/builder/picolibc.go +++ b/builder/picolibc.go @@ -1,6 +1,7 @@ package builder import ( + "os" "path/filepath" "github.com/tinygo-org/tinygo/goenv" @@ -10,7 +11,14 @@ import ( // based on newlib. var Picolibc = Library{ name: "picolibc", - cflags: func() []string { + makeHeaders: func(includeDir string) error { + f, err := os.Create(filepath.Join(includeDir, "picolibc.h")) + if err != nil { + return err + } + return f.Close() + }, + cflags: func(headerPath string) []string { picolibcDir := filepath.Join(goenv.Get("TINYGOROOT"), "lib/picolibc/newlib/libc") return []string{ "-Werror", @@ -22,7 +30,7 @@ var Picolibc = Library{ "-nostdlibinc", "-Xclang", "-internal-isystem", "-Xclang", picolibcDir + "/include", "-I" + picolibcDir + "/tinystdio", - "-I" + goenv.Get("TINYGOROOT") + "/lib/picolibc-include", + "-I" + headerPath, } }, sourceDir: "lib/picolibc/newlib/libc", diff --git a/compileopts/config.go b/compileopts/config.go index 0da1a2b6..9664d277 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -5,6 +5,7 @@ package compileopts import ( "errors" "fmt" + "os" "path/filepath" "regexp" "strings" @@ -197,6 +198,29 @@ func (c *Config) RP2040BootPatch() bool { return false } +// LibcPath returns the path to the libc directory. The libc path will be either +// a precompiled libc shipped with a TinyGo build, or a libc path in the cache +// directory (which might not yet be built). +func (c *Config) LibcPath(name string) (path string, precompiled bool) { + // Try to load a precompiled library. + precompiledDir := filepath.Join(goenv.Get("TINYGOROOT"), "pkg", c.Triple(), name) + if _, err := os.Stat(precompiledDir); err == nil { + // Found a precompiled library for this OS/architecture. Return the path + // directly. + return precompiledDir, true + } + + // No precompiled library found. Determine the path name that will be used + // in the build cache. + var outname string + if c.CPU() != "" { + outname = name + "-" + c.Triple() + "-" + c.CPU() + } else { + outname = name + "-" + c.Triple() + } + return filepath.Join(goenv.Get("GOCACHE"), outname), false +} + // CFlags returns the flags to pass to the C compiler. This is necessary for CGo // preprocessing. func (c *Config) CFlags() []string { @@ -208,12 +232,12 @@ func (c *Config) CFlags() []string { case "picolibc": root := goenv.Get("TINYGOROOT") picolibcDir := filepath.Join(root, "lib", "picolibc", "newlib", "libc") + path, _ := c.LibcPath("picolibc") cflags = append(cflags, - "-nostdlibinc", + "--sysroot="+path, "-Xclang", "-internal-isystem", "-Xclang", filepath.Join(picolibcDir, "include"), "-Xclang", "-internal-isystem", "-Xclang", filepath.Join(picolibcDir, "tinystdio"), ) - cflags = append(cflags, "-I"+filepath.Join(root, "lib/picolibc-include")) case "wasi-libc": root := goenv.Get("TINYGOROOT") cflags = append(cflags, "--sysroot="+root+"/lib/wasi-libc/sysroot") diff --git a/lib/picolibc-include/picolibc.h b/lib/picolibc-include/picolibc.h deleted file mode 100644 index e69de29b..00000000 diff --git a/main.go b/main.go index 77cb53c7..00b5b9bb 100644 --- a/main.go +++ b/main.go @@ -71,8 +71,8 @@ func moveFile(src, dst string) error { return os.Remove(src) } -// copyFile copies the given file from src to dst. It can copy over -// a possibly already existing file at the destination. +// copyFile copies the given file or directory from src to dst. It can copy over +// a possibly already existing file (but not directory) at the destination. func copyFile(src, dst string) error { source, err := os.Open(src) if err != nil { @@ -85,14 +85,32 @@ func copyFile(src, dst string) error { return err } - destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, st.Mode()) - if err != nil { + if st.IsDir() { + err := os.Mkdir(dst, st.Mode().Perm()) + if err != nil { + return err + } + names, err := source.Readdirnames(0) + if err != nil { + return err + } + for _, name := range names { + err := copyFile(filepath.Join(src, name), filepath.Join(dst, name)) + if err != nil { + return err + } + } + return nil + } else { + destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, st.Mode()) + if err != nil { + return err + } + defer destination.Close() + + _, err = io.Copy(destination, source) return err } - defer destination.Close() - - _, err = io.Copy(destination, source) - return err } // executeCommand is a simple wrapper to exec.Cmd @@ -1259,7 +1277,12 @@ func main() { handleCompilerError(err) } defer os.RemoveAll(tmpdir) - path, err := lib.Load(*target, tmpdir) + config := &compileopts.Config{ + Target: &compileopts.TargetSpec{ + Triple: *target, + }, + } + path, err := lib.Load(config, tmpdir) handleCompilerError(err) err = copyFile(path, outpath) if err != nil {