Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,43 @@ func (c *context) linker() *clang.Cmd {
return cmd
}

// normalizeToArchive creates an archive from object files and updates LLFiles to use it.
// This ensures the link step always consumes .a archives regardless of cache state.
func normalizeToArchive(ctx *context, aPkg *aPackage, verbose bool) error {
if ctx.buildConf.Goarch == "wasm" || strings.Contains(ctx.crossCompile.LLVMTarget, "wasm") {
return nil
}
if len(aPkg.LLFiles) == 0 {
return nil
}

var objFiles []string
for _, f := range aPkg.LLFiles {
switch filepath.Ext(f) {
case ".o", ".ll":
objFiles = append(objFiles, f)
}
}
if len(objFiles) == 0 {
return nil
}

archiveFile, err := os.CreateTemp("", "pkg-*.a")
if err != nil {
return fmt.Errorf("create temp archive: %w", err)
}
archiveFile.Close()
archivePath := archiveFile.Name()

if err := ctx.createArchiveFile(archivePath, objFiles, verbose); err != nil {
os.Remove(archivePath)
return fmt.Errorf("create archive for %s: %w", aPkg.PkgPath, err)
}

aPkg.LLFiles = []string{archivePath}
return nil
}

func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, error) {
built := ctx.built

Expand Down Expand Up @@ -597,6 +634,9 @@ func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, er
return err
}
if !aPkg.CacheHit {
if err := normalizeToArchive(ctx, aPkg, verbose); err != nil {
return err
}
if kind == cl.PkgLinkExtern {
appendExternalLinkArgs(ctx, aPkg, param)
}
Expand All @@ -622,6 +662,9 @@ func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, er
needRuntime = needRuntime || aPkg.NeedRt
needPyInit = needPyInit || aPkg.NeedPyInit
if !aPkg.CacheHit {
if err := normalizeToArchive(ctx, aPkg, verbose); err != nil {
return err
}
if err := ctx.saveToCache(aPkg); err != nil && verbose {
fmt.Fprintf(os.Stderr, "warning: failed to save cache for %s: %v\n", pkg.PkgPath, err)
}
Expand Down Expand Up @@ -921,7 +964,7 @@ func isRuntimePkg(pkgPath string) bool {
func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error {
// Handle c-archive mode differently - use ar tool instead of linker
if ctx.buildConf.BuildMode == BuildModeCArchive {
return createArchiveFile(app, objFiles, verbose)
return ctx.createArchiveFile(app, objFiles, verbose)
}

buildArgs := []string{"-o", app}
Expand Down Expand Up @@ -967,9 +1010,26 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose
return cmd.Link(buildArgs...)
}

// archiver returns the archiving tool to use for the current context.
func (c *context) archiver() string {
if c != nil && c.crossCompile.CC != "" {
clangDir := filepath.Dir(c.crossCompile.CC)
if clangDir != "" {
llvmAr := filepath.Join(clangDir, "llvm-ar")
if _, err := os.Stat(llvmAr); err == nil {
return llvmAr
}
}
}
if ar := os.Getenv("LLGO_AR"); ar != "" {
return ar
}
return "ar"
}

// createArchiveFile builds an archive at archivePath atomically to avoid races when
// multiple builds target the same output concurrently.
func createArchiveFile(archivePath string, objFiles []string, verbose ...bool) error {
func (c *context) createArchiveFile(archivePath string, objFiles []string, verbose ...bool) error {
if len(objFiles) == 0 {
return fmt.Errorf("no object files provided for archive %s", archivePath)
}
Expand All @@ -988,7 +1048,7 @@ func createArchiveFile(archivePath string, objFiles []string, verbose ...bool) e
_ = os.Remove(tmpName)

args := append([]string{"rcs", tmpName}, objFiles...)
cmd := exec.Command("ar", args...)
cmd := exec.Command(c.archiver(), args...)
if len(verbose) > 0 && verbose[0] {
fmt.Fprintf(os.Stderr, "ar %s\n", strings.Join(args, " "))
}
Expand Down
83 changes: 69 additions & 14 deletions internal/build/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package build

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -437,26 +438,39 @@ func (c *context) saveToCache(pkg *aPackage) error {
return err
}

// Collect object files to cache
// Deduplicate by full path first
var objectFiles []string
seenPath := make(map[string]bool)
var archiveSource string
for _, f := range pkg.LLFiles {
if filepath.Ext(f) == ".o" || filepath.Ext(f) == ".ll" {
if !seenPath[f] {
seenPath[f] = true
if filepath.Ext(f) == ".a" {
archiveSource = f
break
}
}

if archiveSource != "" {
if err := copyFileAtomic(archiveSource, paths.Archive); err != nil {
return err
}
} else {
var objectFiles []string
seenPath := make(map[string]bool)
for _, f := range pkg.LLFiles {
if seenPath[f] {
continue
}
seenPath[f] = true
switch filepath.Ext(f) {
case ".o", ".ll":
objectFiles = append(objectFiles, f)
}
}
}

if len(objectFiles) == 0 {
return nil
}
if len(objectFiles) == 0 {
return nil
}

// Create .a archive from object files (atomic write to avoid races)
if err := createArchiveFile(paths.Archive, objectFiles); err != nil {
return err
if err := c.createArchiveFile(paths.Archive, objectFiles); err != nil {
return err
}
}

// Append metadata to existing manifest (pkg.Manifest was built in collectFingerprint).
Expand Down Expand Up @@ -493,3 +507,44 @@ func (c *context) saveToCache(pkg *aPackage) error {

return nil
}

// copyFileAtomic copies src to dst using a temp file for atomicity.
func copyFileAtomic(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}

in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()

tmp, err := os.CreateTemp(filepath.Dir(dst), filepath.Base(dst)+".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
cleanup := true
defer func() {
if cleanup {
tmp.Close()
os.Remove(tmpName)
}
}()

if _, err := io.Copy(tmp, in); err != nil {
return err
}

if err := tmp.Close(); err != nil {
return err
}

if err := os.Rename(tmpName, dst); err != nil {
return err
}

cleanup = false
return nil
}
Loading