builder: use flock to avoid double-compiles

This change uses flock (when available) to acquire locks for build operations.
This allows multiple tinygo processes to run concurrently without building the same thing twice.
This commit is contained in:
Nia Waldvogel
2021-12-19 15:23:04 -05:00
committed by Ron Evans
parent 38305399a3
commit f9293645af
5 changed files with 68 additions and 22 deletions
+31 -11
View File
@@ -23,6 +23,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/gofrs/flock"
"github.com/tinygo-org/tinygo/cgo" "github.com/tinygo-org/tinygo/cgo"
"github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/compiler" "github.com/tinygo-org/tinygo/compiler"
@@ -97,17 +98,19 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
var libcDependencies []*compileJob var libcDependencies []*compileJob
switch config.Target.Libc { switch config.Target.Libc {
case "musl": case "musl":
job, err := Musl.load(config, dir) job, unlock, err := Musl.load(config, dir)
if err != nil { if err != nil {
return err return err
} }
defer unlock()
libcDependencies = append(libcDependencies, dummyCompileJob(filepath.Join(filepath.Dir(job.result), "crt1.o"))) libcDependencies = append(libcDependencies, dummyCompileJob(filepath.Join(filepath.Dir(job.result), "crt1.o")))
libcDependencies = append(libcDependencies, job) libcDependencies = append(libcDependencies, job)
case "picolibc": case "picolibc":
libcJob, err := Picolibc.load(config, dir) libcJob, unlock, err := Picolibc.load(config, dir)
if err != nil { if err != nil {
return err return err
} }
defer unlock()
libcDependencies = append(libcDependencies, libcJob) libcDependencies = append(libcDependencies, libcJob)
case "wasi-libc": case "wasi-libc":
path := filepath.Join(root, "lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a") path := filepath.Join(root, "lib/wasi-libc/sysroot/lib/wasm32-wasi/libc.a")
@@ -116,10 +119,11 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
} }
libcDependencies = append(libcDependencies, dummyCompileJob(path)) libcDependencies = append(libcDependencies, dummyCompileJob(path))
case "mingw-w64": case "mingw-w64":
_, err := MinGW.load(config, dir) _, unlock, err := MinGW.load(config, dir)
if err != nil { if err != nil {
return err return err
} }
unlock()
libcDependencies = append(libcDependencies, makeMinGWExtraLibs(dir)...) libcDependencies = append(libcDependencies, makeMinGWExtraLibs(dir)...)
case "": case "":
// no library specified, so nothing to do // no library specified, so nothing to do
@@ -228,17 +232,19 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
bitcodePath := filepath.Join(cacheDir, "pkg-"+hex.EncodeToString(hash[:])+".bc") bitcodePath := filepath.Join(cacheDir, "pkg-"+hex.EncodeToString(hash[:])+".bc")
packageBitcodePaths[pkg.ImportPath] = bitcodePath packageBitcodePaths[pkg.ImportPath] = bitcodePath
// Check whether this package has been compiled before, and if so don't
// compile it again.
if _, err := os.Stat(bitcodePath); err == nil {
// Already cached, don't recreate this package.
continue
}
// The package has not yet been compiled, so create a job to do so. // The package has not yet been compiled, so create a job to do so.
job := &compileJob{ job := &compileJob{
description: "compile package " + pkg.ImportPath, description: "compile package " + pkg.ImportPath,
run: func(*compileJob) error { run: func(*compileJob) error {
// Acquire a lock (if supported).
unlock := lock(bitcodePath + ".lock")
defer unlock()
if _, err := os.Stat(bitcodePath); err == nil {
// Already cached, don't recreate this package.
return nil
}
// Compile AST to IR. The compiler.CompilePackage function will // Compile AST to IR. The compiler.CompilePackage function will
// build the SSA as needed. // build the SSA as needed.
mod, errs := compiler.CompilePackage(pkg.ImportPath, pkg, program.Package(pkg.Pkg), machine, compilerConfig, config.DumpSSA()) mod, errs := compiler.CompilePackage(pkg.ImportPath, pkg, program.Package(pkg.Pkg), machine, compilerConfig, config.DumpSSA())
@@ -533,10 +539,11 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
// Add compiler-rt dependency if needed. Usually this is a simple load from // Add compiler-rt dependency if needed. Usually this is a simple load from
// a cache. // a cache.
if config.Target.RTLib == "compiler-rt" { if config.Target.RTLib == "compiler-rt" {
job, err := CompilerRT.load(config, dir) job, unlock, err := CompilerRT.load(config, dir)
if err != nil { if err != nil {
return err return err
} }
defer unlock()
linkerDependencies = append(linkerDependencies, job) linkerDependencies = append(linkerDependencies, job)
} }
@@ -1181,3 +1188,16 @@ func patchRP2040BootCRC(executable string) error {
// Update the .boot2 section to included the CRC // Update the .boot2 section to included the CRC
return replaceElfSection(executable, ".boot2", bytes) return replaceElfSection(executable, ".boot2", bytes)
} }
// lock may acquire a lock at the specified path.
// It returns a function to release the lock.
// If flock is not supported, it does nothing.
func lock(path string) func() {
flock := flock.New(path)
err := flock.Lock()
if err != nil {
return func() {}
}
return func() { flock.Close() }
}
+4
View File
@@ -63,6 +63,10 @@ func compileAndCacheCFile(abspath, tmpdir string, cflags []string, printCommands
return "", err return "", err
} }
// Acquire a lock (if supported).
unlock := lock(filepath.Join(goenv.Get("GOCACHE"), fileHash+".c.lock"))
defer unlock()
// Create cache key for the dependencies file. // Create cache key for the dependencies file.
buf, err := json.Marshal(struct { buf, err := json.Marshal(struct {
Path string Path string
+30 -11
View File
@@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/goenv"
@@ -37,10 +38,11 @@ type Library struct {
// The resulting directory may be stored in the provided tmpdir, which is // The resulting directory may be stored in the provided tmpdir, which is
// expected to be removed after the Load call. // expected to be removed after the Load call.
func (l *Library) Load(config *compileopts.Config, tmpdir string) (dir string, err error) { func (l *Library) Load(config *compileopts.Config, tmpdir string) (dir string, err error) {
job, err := l.load(config, tmpdir) job, unlock, err := l.load(config, tmpdir)
if err != nil { if err != nil {
return "", err return "", err
} }
defer unlock()
err = runJobs(job, config.Options.Parallelism) err = runJobs(job, config.Options.Parallelism)
return filepath.Dir(job.result), err return filepath.Dir(job.result), err
} }
@@ -53,28 +55,38 @@ func (l *Library) Load(config *compileopts.Config, tmpdir string) (dir string, e
// output archive file, it is expected to be removed after use. // output archive file, it is expected to be removed after use.
// As a side effect, this call creates the library header files if they didn't // As a side effect, this call creates the library header files if they didn't
// exist yet. // exist yet.
func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJob, err error) { func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJob, abortLock func(), err error) {
outdir, precompiled := config.LibcPath(l.name) outdir, precompiled := config.LibcPath(l.name)
archiveFilePath := filepath.Join(outdir, "lib.a") archiveFilePath := filepath.Join(outdir, "lib.a")
if precompiled { if precompiled {
// Found a precompiled library for this OS/architecture. Return the path // Found a precompiled library for this OS/architecture. Return the path
// directly. // directly.
return dummyCompileJob(archiveFilePath), nil return dummyCompileJob(archiveFilePath), func() {}, nil
} }
// Create a lock on the output (if supported).
// This is a bit messy, but avoids a deadlock because it is ordered consistently with other library loads within a build.
outname := filepath.Base(outdir)
unlock := lock(filepath.Join(goenv.Get("GOCACHE"), outname+".lock"))
var ok bool
defer func() {
if !ok {
unlock()
}
}()
// Try to fetch this library from the cache. // Try to fetch this library from the cache.
if _, err := os.Stat(archiveFilePath); err == nil { if _, err := os.Stat(archiveFilePath); err == nil {
return dummyCompileJob(archiveFilePath), nil return dummyCompileJob(archiveFilePath), func() {}, nil
} }
// Cache miss, build it now. // Cache miss, build it now.
// Create the destination directory where the components of this library // Create the destination directory where the components of this library
// (lib.a file, include directory) are placed. // (lib.a file, include directory) are placed.
outname := filepath.Base(outdir)
err = os.MkdirAll(filepath.Join(goenv.Get("GOCACHE"), outname), 0o777) err = os.MkdirAll(filepath.Join(goenv.Get("GOCACHE"), outname), 0o777)
if err != nil { if err != nil {
// Could not create directory (and not because it already exists). // Could not create directory (and not because it already exists).
return nil, err return nil, nil, err
} }
// Make headers if needed. // Make headers if needed.
@@ -84,12 +96,12 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
if _, err = os.Stat(headerPath); err != nil { if _, err = os.Stat(headerPath); err != nil {
temporaryHeaderPath, err := ioutil.TempDir(outdir, "include.tmp*") temporaryHeaderPath, err := ioutil.TempDir(outdir, "include.tmp*")
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
defer os.RemoveAll(temporaryHeaderPath) defer os.RemoveAll(temporaryHeaderPath)
err = l.makeHeaders(target, temporaryHeaderPath) err = l.makeHeaders(target, temporaryHeaderPath)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
err = os.Rename(temporaryHeaderPath, headerPath) err = os.Rename(temporaryHeaderPath, headerPath)
if err != nil { if err != nil {
@@ -108,7 +120,7 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
fallthrough fallthrough
default: default:
return nil, err return nil, nil, err
} }
} }
} }
@@ -118,7 +130,7 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
dir := filepath.Join(tmpdir, "build-lib-"+l.name) dir := filepath.Join(tmpdir, "build-lib-"+l.name)
err = os.Mkdir(dir, 0777) err = os.Mkdir(dir, 0777)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// Precalculate the flags to the compiler invocation. // Precalculate the flags to the compiler invocation.
@@ -146,6 +158,8 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
args = append(args, "-march=rv64gc", "-mabi=lp64") args = append(args, "-march=rv64gc", "-mabi=lp64")
} }
var once sync.Once
// Create job to put all the object files in a single archive. This archive // Create job to put all the object files in a single archive. This archive
// file is the (static) library file. // file is the (static) library file.
var objs []string var objs []string
@@ -153,6 +167,8 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
description: "ar " + l.name + "/lib.a", description: "ar " + l.name + "/lib.a",
result: filepath.Join(goenv.Get("GOCACHE"), outname, "lib.a"), result: filepath.Join(goenv.Get("GOCACHE"), outname, "lib.a"),
run: func(*compileJob) error { run: func(*compileJob) error {
defer once.Do(unlock)
// Create an archive of all object files. // Create an archive of all object files.
f, err := ioutil.TempFile(outdir, "libc.a.tmp*") f, err := ioutil.TempFile(outdir, "libc.a.tmp*")
if err != nil { if err != nil {
@@ -224,5 +240,8 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
}) })
} }
return job, nil ok = true
return job, func() {
once.Do(unlock)
}, nil
} }
+1
View File
@@ -7,6 +7,7 @@ require (
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2
github.com/chromedp/cdproto v0.0.0-20210113043257-dabd2f2e7693 github.com/chromedp/cdproto v0.0.0-20210113043257-dabd2f2e7693
github.com/chromedp/chromedp v0.6.4 github.com/chromedp/chromedp v0.6.4
github.com/gofrs/flock v0.8.1 // indirect
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
github.com/marcinbor85/gohex v0.0.0-20200531091804-343a4b548892 github.com/marcinbor85/gohex v0.0.0-20200531091804-343a4b548892
github.com/mattn/go-colorable v0.1.8 github.com/mattn/go-colorable v0.1.8
+2
View File
@@ -17,6 +17,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs= github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs=
github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=