mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
- 改为从 duckdb.dll 生成 MinGW 可用的导入库文件 - 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑 - 新增导入库生成命令与 buildutil 单测
230 lines
6.3 KiB
Go
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
|
|
}
|