Files
MyGoNavi/internal/buildutil/windows_import_lib.go
Syngnat a54a357e4b 🐛 fix(ci): 修复 DuckDB Windows 导入库生成链路
- 改为从 duckdb.dll 生成 MinGW 可用的导入库文件
- 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑
- 新增导入库生成命令与 buildutil 单测
2026-06-08 17:59:58 +08:00

230 lines
6.3 KiB
Go

package buildutil
import (
"bytes"
"debug/pe"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
)
const peExportDirectoryIndex = 0
type imageExportDirectory struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
Name uint32
Base uint32
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
func GenerateWindowsImportLibraryFromDLL(dllPath string, dlltoolPath string, outputLibPath string) error {
trimmedDLLPath := strings.TrimSpace(dllPath)
if trimmedDLLPath == "" {
return fmt.Errorf("dll path is empty")
}
trimmedOutput := strings.TrimSpace(outputLibPath)
if trimmedOutput == "" {
return fmt.Errorf("output import library path is empty")
}
if strings.TrimSpace(dlltoolPath) == "" {
resolved, err := exec.LookPath("dlltool")
if err != nil {
return fmt.Errorf("locate dlltool failed: %w", err)
}
dlltoolPath = resolved
}
exportNames, err := readPEExportNames(trimmedDLLPath)
if err != nil {
return err
}
if len(exportNames) == 0 {
return fmt.Errorf("no export symbols found in %s", trimmedDLLPath)
}
if err := os.MkdirAll(filepath.Dir(trimmedOutput), 0o755); err != nil {
return fmt.Errorf("create output dir failed: %w", err)
}
defPath := strings.TrimSuffix(trimmedOutput, filepath.Ext(trimmedOutput)) + ".def"
defContent := buildModuleDefinition(filepath.Base(trimmedDLLPath), exportNames)
if err := os.WriteFile(defPath, []byte(defContent), 0o644); err != nil {
return fmt.Errorf("write module definition failed: %w", err)
}
cmd := exec.Command(
dlltoolPath,
"--input-def", defPath,
"--dllname", filepath.Base(trimmedDLLPath),
"--output-lib", trimmedOutput,
)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("generate import library failed: %w; output=%s", err, strings.TrimSpace(string(output)))
}
return nil
}
func buildModuleDefinition(dllName string, exportNames []string) string {
seen := make(map[string]struct{}, len(exportNames))
normalized := make([]string, 0, len(exportNames))
for _, name := range exportNames {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
normalized = append(normalized, trimmed)
}
slices.Sort(normalized)
var builder strings.Builder
builder.WriteString("LIBRARY ")
builder.WriteString(dllName)
builder.WriteString("\nEXPORTS\n")
for _, name := range normalized {
builder.WriteString(" ")
builder.WriteString(name)
builder.WriteString("\n")
}
return builder.String()
}
func readPEExportNames(dllPath string) ([]string, error) {
file, err := pe.Open(dllPath)
if err != nil {
return nil, fmt.Errorf("open dll failed: %w", err)
}
defer file.Close()
exportDirectoryRVA, err := resolveExportDirectoryRVA(file)
if err != nil {
return nil, err
}
if exportDirectoryRVA == 0 {
return nil, fmt.Errorf("dll has no export directory: %s", dllPath)
}
payload, err := readBytesAtRVA(file, exportDirectoryRVA, binary.Size(imageExportDirectory{}))
if err != nil {
return nil, fmt.Errorf("read export directory failed: %w", err)
}
var directory imageExportDirectory
if err := binary.Read(bytes.NewReader(payload), binary.LittleEndian, &directory); err != nil {
return nil, fmt.Errorf("decode export directory failed: %w", err)
}
if directory.NumberOfNames == 0 {
return nil, nil
}
nameTable, err := readBytesAtRVA(file, directory.AddressOfNames, int(directory.NumberOfNames)*4)
if err != nil {
return nil, fmt.Errorf("read export name table failed: %w", err)
}
names := make([]string, 0, directory.NumberOfNames)
for index := uint32(0); index < directory.NumberOfNames; index++ {
offset := index * 4
nameRVA := binary.LittleEndian.Uint32(nameTable[offset : offset+4])
name, err := readCStringAtRVA(file, nameRVA)
if err != nil {
return nil, fmt.Errorf("read export name failed: %w", err)
}
names = append(names, name)
}
return names, nil
}
func resolveExportDirectoryRVA(file *pe.File) (uint32, error) {
switch header := file.OptionalHeader.(type) {
case *pe.OptionalHeader32:
if len(header.DataDirectory) <= peExportDirectoryIndex {
return 0, fmt.Errorf("optional header has no export directory")
}
return header.DataDirectory[peExportDirectoryIndex].VirtualAddress, nil
case *pe.OptionalHeader64:
if len(header.DataDirectory) <= peExportDirectoryIndex {
return 0, fmt.Errorf("optional header has no export directory")
}
return header.DataDirectory[peExportDirectoryIndex].VirtualAddress, nil
default:
return 0, fmt.Errorf("unsupported optional header type %T", file.OptionalHeader)
}
}
func readBytesAtRVA(file *pe.File, rva uint32, size int) ([]byte, error) {
if size < 0 {
return nil, fmt.Errorf("invalid size %d", size)
}
for _, section := range file.Sections {
start := section.VirtualAddress
length := maxUint32(section.VirtualSize, section.Size)
end := start + length
if rva < start || rva >= end {
continue
}
offset := int(rva - start)
data, err := section.Data()
if err != nil {
return nil, err
}
if offset > len(data) {
return nil, fmt.Errorf("rva %d out of section bounds", rva)
}
if size == 0 {
return []byte{}, nil
}
if offset+size > len(data) {
return nil, fmt.Errorf("rva %d with size %d exceeds section size", rva, size)
}
return data[offset : offset+size], nil
}
return nil, fmt.Errorf("rva %d not found in any section", rva)
}
func readCStringAtRVA(file *pe.File, rva uint32) (string, error) {
for _, section := range file.Sections {
start := section.VirtualAddress
length := maxUint32(section.VirtualSize, section.Size)
end := start + length
if rva < start || rva >= end {
continue
}
offset := int(rva - start)
data, err := section.Data()
if err != nil {
return "", err
}
if offset >= len(data) {
return "", fmt.Errorf("string rva %d out of section bounds", rva)
}
endIndex := offset
for endIndex < len(data) && data[endIndex] != 0 {
endIndex++
}
return string(data[offset:endIndex]), nil
}
return "", fmt.Errorf("string rva %d not found in any section", rva)
}
func maxUint32(left uint32, right uint32) uint32 {
if left > right {
return left
}
return right
}