diff --git a/archive.go b/archive.go new file mode 100644 index 0000000..ba5968e --- /dev/null +++ b/archive.go @@ -0,0 +1,109 @@ +//===- archive.go - Bindings for archive writing --------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines bindings for llvm::writeArchive. +// +//===----------------------------------------------------------------------===// + +package llvm + +/* +#include "llvm-c/Core.h" +#include "backports.h" +#include +*/ +import "C" + +import ( + "errors" + "fmt" + "unsafe" +) + +// ArchiveMember is an object or bitcode file to add to an archive. +// +// A memory-buffer member borrows its buffer. The caller must keep the buffer +// alive until WriteArchive returns. +type ArchiveMember struct { + name string + path string + buffer MemoryBuffer +} + +// NewArchiveMemberFromFile creates a path-backed archive member. The archive +// member name is the base name of path. +func NewArchiveMemberFromFile(path string) ArchiveMember { + return ArchiveMember{path: path} +} + +// NewArchiveMemberFromMemoryBuffer creates a memory-backed archive member. +// name is the name stored in the archive. +func NewArchiveMemberFromMemoryBuffer(name string, buffer MemoryBuffer) ArchiveMember { + return ArchiveMember{name: name, buffer: buffer} +} + +// WriteArchive creates a deterministic, non-thin archive with a symbol table. +// targetTriple determines the platform archive format. +func WriteArchive(path, targetTriple string, members []ArchiveMember) error { + if path == "" { + return errors.New("archive path is empty") + } + if len(members) == 0 { + return errors.New("archive has no members") + } + + cTriple := C.CString(targetTriple) + defer C.free(unsafe.Pointer(cTriple)) + writer := C.LLVMGoCreateArchiveWriter(cTriple) + if writer == nil { + return errors.New("failed to create archive writer") + } + defer C.LLVMGoDisposeArchiveWriter(writer) + + for i, member := range members { + var cErr *C.char + switch { + case member.path != "": + cPath := C.CString(member.path) + var cName *C.char + if member.name != "" { + cName = C.CString(member.name) + } + cErr = C.LLVMGoArchiveWriterAddFile(writer, cPath, cName) + C.free(unsafe.Pointer(cPath)) + C.free(unsafe.Pointer(cName)) + case !member.buffer.IsNil(): + if member.name == "" { + return fmt.Errorf("archive member %d has an empty name", i) + } + cName := C.CString(member.name) + cErr = C.LLVMGoArchiveWriterAddMemoryBuffer(writer, member.buffer.C, cName) + C.free(unsafe.Pointer(cName)) + default: + return fmt.Errorf("archive member %d has no file or memory buffer", i) + } + if cErr != nil { + err := errors.New(C.GoString(cErr)) + C.LLVMDisposeMessage(cErr) + label := member.name + if member.path != "" { + label = member.path + } + return fmt.Errorf("add archive member %d (%q): %w", i, label, err) + } + } + + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + if cErr := C.LLVMGoArchiveWriterWrite(writer, cPath); cErr != nil { + err := errors.New(C.GoString(cErr)) + C.LLVMDisposeMessage(cErr) + return err + } + return nil +} diff --git a/archive_test.go b/archive_test.go new file mode 100644 index 0000000..df8e876 --- /dev/null +++ b/archive_test.go @@ -0,0 +1,87 @@ +//===- archive_test.go - Tests for archive writing ------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +package llvm + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWriteArchiveMemoryAndFileMembers(t *testing.T) { + ctx := NewContext() + defer ctx.Dispose() + + mod := ctx.NewModule("archive") + defer mod.Dispose() + mod.SetTarget("x86_64-unknown-linux-gnu") + AddFunction(mod, "answer", FunctionType(ctx.Int32Type(), nil, false)) + + memoryBuf := WriteBitcodeToMemoryBuffer(mod) + defer memoryBuf.Dispose() + + fileBuf := WriteBitcodeToMemoryBuffer(mod) + defer fileBuf.Dispose() + filePath := filepath.Join(t.TempDir(), "from-file.bc") + if err := os.WriteFile(filePath, fileBuf.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + + archivePath := filepath.Join(t.TempDir(), "libarchive.a") + err := WriteArchive(archivePath, mod.Target(), []ArchiveMember{ + NewArchiveMemberFromMemoryBuffer("from-memory.bc", memoryBuf), + NewArchiveMemberFromFile(filePath), + }) + if err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(archivePath) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(data, []byte("!\n")) { + end := len(data) + if end > 8 { + end = 8 + } + t.Fatalf("archive magic = %q", data[:end]) + } + for _, name := range []string{"from-memory.bc", "from-file.bc"} { + if !bytes.Contains(data, []byte(name)) { + t.Errorf("archive does not contain member name %q", name) + } + } +} + +func TestWriteArchiveRejectsInvalidMembers(t *testing.T) { + path := filepath.Join(t.TempDir(), "invalid.a") + missing := filepath.Join(t.TempDir(), "missing.o") + tests := []struct { + name string + path string + members []ArchiveMember + want string + }{ + {name: "empty path", members: []ArchiveMember{{}}, want: "archive path is empty"}, + {name: "no members", path: path, want: "archive has no members"}, + {name: "empty member", path: path, members: []ArchiveMember{{}}, want: "has no file or memory buffer"}, + {name: "missing file", path: path, members: []ArchiveMember{NewArchiveMemberFromFile(missing)}, want: missing}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := WriteArchive(tt.path, "x86_64-unknown-linux-gnu", tt.members) + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("WriteArchive error = %v, want %q", err, tt.want) + } + }) + } +} diff --git a/backports.cpp b/backports.cpp index b7a0547..afc0354 100644 --- a/backports.cpp +++ b/backports.cpp @@ -15,10 +15,111 @@ #include "llvm/Transforms/IPO/PassManagerBuilder.h" #endif #include "llvm/IR/Module.h" +#include "llvm/Object/Archive.h" +#include "llvm/Object/ArchiveWriter.h" #include "llvm/Pass.h" +#include "llvm/Support/Error.h" +#if LLVM_VERSION_MAJOR >= 18 +#include "llvm/TargetParser/Host.h" +#else +#include "llvm/Support/Host.h" +#endif #include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#if LLVM_VERSION_MAJOR >= 16 +#include "llvm/TargetParser/Triple.h" +#else +#include "llvm/ADT/Triple.h" +#endif #include "llvm/Transforms/IPO.h" +#include "llvm-c/Core.h" #include "llvm-c/DebugInfo.h" +#include + +struct LLVMGoArchiveWriterOpaque { + llvm::object::Archive::Kind Kind; + std::vector Members; + std::deque Names; +}; + +static char *LLVMGoArchiveError(llvm::Error Err) { + if (!Err) + return nullptr; + return LLVMCreateMessage(llvm::toString(std::move(Err)).c_str()); +} + +LLVMGoArchiveWriterRef LLVMGoCreateArchiveWriter(const char *TargetTriple) { + auto TripleText = TargetTriple && TargetTriple[0] + ? llvm::Triple::normalize(TargetTriple) + : llvm::sys::getDefaultTargetTriple(); + llvm::Triple Triple(TripleText); + auto Kind = llvm::object::Archive::K_GNU; + if (Triple.isOSDarwin()) + Kind = llvm::object::Archive::K_DARWIN; + else if (Triple.isOSAIX()) + Kind = llvm::object::Archive::K_AIXBIG; + else if (Triple.isOSWindows()) + Kind = llvm::object::Archive::K_COFF; + return new LLVMGoArchiveWriterOpaque{Kind, {}, {}}; +} + +char *LLVMGoArchiveWriterAddFile(LLVMGoArchiveWriterRef Writer, + const char *Path, const char *Name) { + if (!Writer) + return LLVMCreateMessage("archive writer is nil"); + if (!Path || !Path[0]) + return LLVMCreateMessage("archive member path is empty"); + + auto MemberOrErr = llvm::NewArchiveMember::getFile(Path, true); + if (!MemberOrErr) + return LLVMGoArchiveError(MemberOrErr.takeError()); + + Writer->Names.emplace_back( + Name && Name[0] ? Name + : llvm::sys::path::filename(Path).str()); + MemberOrErr->MemberName = Writer->Names.back(); + Writer->Members.push_back(std::move(*MemberOrErr)); + return nullptr; +} + +char *LLVMGoArchiveWriterAddMemoryBuffer(LLVMGoArchiveWriterRef Writer, + LLVMMemoryBufferRef Buffer, + const char *Name) { + if (!Writer) + return LLVMCreateMessage("archive writer is nil"); + if (!Buffer) + return LLVMCreateMessage("archive member buffer is nil"); + if (!Name || !Name[0]) + return LLVMCreateMessage("archive member name is empty"); + + Writer->Names.emplace_back(Name); + Writer->Members.emplace_back(llvm::unwrap(Buffer)->getMemBufferRef()); + Writer->Members.back().MemberName = Writer->Names.back(); + return nullptr; +} + +char *LLVMGoArchiveWriterWrite(LLVMGoArchiveWriterRef Writer, + const char *ArchivePath) { + if (!Writer) + return LLVMCreateMessage("archive writer is nil"); + if (!ArchivePath || !ArchivePath[0]) + return LLVMCreateMessage("archive path is empty"); + if (Writer->Members.empty()) + return LLVMCreateMessage("archive has no members"); + +#if LLVM_VERSION_MAJOR >= 18 + auto WriteSymtab = llvm::SymtabWritingMode::NormalSymtab; +#else + auto WriteSymtab = true; +#endif + return LLVMGoArchiveError(llvm::writeArchive( + ArchivePath, Writer->Members, WriteSymtab, Writer->Kind, + /*Deterministic=*/true, /*Thin=*/false)); +} + +void LLVMGoDisposeArchiveWriter(LLVMGoArchiveWriterRef Writer) { + delete Writer; +} void LLVMGlobalObjectAddMetadata(LLVMValueRef Global, unsigned KindID, LLVMMetadataRef MD) { llvm::MDNode *N = MD ? llvm::unwrap(MD) : nullptr; diff --git a/backports.h b/backports.h index 916f653..8f54842 100644 --- a/backports.h +++ b/backports.h @@ -13,6 +13,22 @@ LLVMMemoryBufferRef LLVMGoWriteThinLTOBitcodeToMemoryBuffer(LLVMModuleRef M); LLVMMemoryBufferRef LLVMGoWriteFullLTOBitcodeToMemoryBuffer( LLVMModuleRef M, LLVMBool EnableSplitLTOUnit); +typedef struct LLVMGoArchiveWriterOpaque *LLVMGoArchiveWriterRef; + +LLVMGoArchiveWriterRef LLVMGoCreateArchiveWriter(const char *TargetTriple); + +char *LLVMGoArchiveWriterAddFile(LLVMGoArchiveWriterRef Writer, + const char *Path, const char *Name); + +char *LLVMGoArchiveWriterAddMemoryBuffer(LLVMGoArchiveWriterRef Writer, + LLVMMemoryBufferRef Buffer, + const char *Name); + +char *LLVMGoArchiveWriterWrite(LLVMGoArchiveWriterRef Writer, + const char *ArchivePath); + +void LLVMGoDisposeArchiveWriter(LLVMGoArchiveWriterRef Writer); + void LLVMGoDIBuilderInsertDbgValueRecordAtEnd( LLVMDIBuilderRef Builder, LLVMValueRef Val, LLVMMetadataRef VarInfo, LLVMMetadataRef Expr, LLVMMetadataRef DebugLoc, LLVMBasicBlockRef Block);