🐛 fix(ci): 修复 DuckDB Windows 导入库生成链路

- 改为从 duckdb.dll 生成 MinGW 可用的导入库文件
- 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑
- 新增导入库生成命令与 buildutil 单测
This commit is contained in:
Syngnat
2026-06-08 17:59:58 +08:00
parent 2e5c3473e1
commit a54a357e4b
6 changed files with 314 additions and 25 deletions

View File

@@ -381,6 +381,7 @@ jobs:
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-binutils
- name: Configure DuckDB CGO Toolchain (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
@@ -546,14 +547,14 @@ jobs:
local lib_dir="$RUNNER_TEMP/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local extract_dir="$RUNNER_TEMP/duckdb-windows-extract-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local zip_path="$RUNNER_TEMP/libduckdb-windows-amd64.zip"
if [ -f "$lib_dir/duckdb.dll" ] && [ -f "$lib_dir/duckdb.lib" ]; then
if [ -f "$lib_dir/duckdb.dll" ] && [ -f "$lib_dir/libduckdb.dll.a" ] && [ -f "$lib_dir/libduckdb.a" ]; then
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
rm -rf "$extract_dir"
rm -f "$zip_path"
local attempt dll_path lib_path
local attempt dll_path
for attempt in 1 2 3; do
echo "📥 下载 DuckDB Windows 动态库 (${attempt}/3): ${DUCKDB_WINDOWS_LIBRARY_URL}"
if curl --retry 3 --retry-delay 2 --retry-all-errors --connect-timeout 20 --max-time 300 -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"; then
@@ -564,17 +565,17 @@ jobs:
mkdir -p "$extract_dir"
unzip -qo "$zip_path" -d "$extract_dir"
dll_path="$(find "$extract_dir" -type f -name duckdb.dll | head -n 1 || true)"
lib_path="$(find "$extract_dir" -type f -name duckdb.lib | head -n 1 || true)"
if [ -n "$dll_path" ] && [ -n "$lib_path" ]; then
if [ -n "$dll_path" ]; then
cp "$dll_path" "$lib_dir/duckdb.dll"
cp "$lib_path" "$lib_dir/duckdb.lib"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
go run ./cmd/mingw-import-lib \
--dll "$lib_dir/duckdb.dll" \
--output-lib "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/libduckdb.dll.a" "$lib_dir/libduckdb.a"
rm -rf "$extract_dir"
echo "$lib_dir"
return 0
fi
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll 或 duckdb.lib,准备重试"
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll准备重试"
else
echo "⚠️ DuckDB Windows 动态库压缩包校验失败,准备重试"
fi

View File

@@ -390,6 +390,7 @@ jobs:
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-binutils
- name: Configure DuckDB CGO Toolchain (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
@@ -544,14 +545,14 @@ jobs:
local lib_dir="$RUNNER_TEMP/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local extract_dir="$RUNNER_TEMP/duckdb-windows-extract-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local zip_path="$RUNNER_TEMP/libduckdb-windows-amd64.zip"
if [ -f "$lib_dir/duckdb.dll" ] && [ -f "$lib_dir/duckdb.lib" ]; then
if [ -f "$lib_dir/duckdb.dll" ] && [ -f "$lib_dir/libduckdb.dll.a" ] && [ -f "$lib_dir/libduckdb.a" ]; then
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
rm -rf "$extract_dir"
rm -f "$zip_path"
local attempt dll_path lib_path
local attempt dll_path
for attempt in 1 2 3; do
echo "📥 下载 DuckDB Windows 动态库 (${attempt}/3): ${DUCKDB_WINDOWS_LIBRARY_URL}"
if curl --retry 3 --retry-delay 2 --retry-all-errors --connect-timeout 20 --max-time 300 -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"; then
@@ -562,17 +563,17 @@ jobs:
mkdir -p "$extract_dir"
unzip -qo "$zip_path" -d "$extract_dir"
dll_path="$(find "$extract_dir" -type f -name duckdb.dll | head -n 1 || true)"
lib_path="$(find "$extract_dir" -type f -name duckdb.lib | head -n 1 || true)"
if [ -n "$dll_path" ] && [ -n "$lib_path" ]; then
if [ -n "$dll_path" ]; then
cp "$dll_path" "$lib_dir/duckdb.dll"
cp "$lib_path" "$lib_dir/duckdb.lib"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
go run ./cmd/mingw-import-lib \
--dll "$lib_dir/duckdb.dll" \
--output-lib "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/libduckdb.dll.a" "$lib_dir/libduckdb.a"
rm -rf "$extract_dir"
echo "$lib_dir"
return 0
fi
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll 或 duckdb.lib,准备重试"
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll准备重试"
else
echo "⚠️ DuckDB Windows 动态库压缩包校验失败,准备重试"
fi

View File

@@ -0,0 +1,27 @@
package main
import (
"flag"
"fmt"
"os"
"GoNavi-Wails/internal/buildutil"
)
func main() {
var (
dllPath string
dlltoolPath string
outputLib string
)
flag.StringVar(&dllPath, "dll", "", "Path to the source DLL")
flag.StringVar(&dlltoolPath, "dlltool", "", "Optional path to dlltool executable")
flag.StringVar(&outputLib, "output-lib", "", "Output import library path")
flag.Parse()
if err := buildutil.GenerateWindowsImportLibraryFromDLL(dllPath, dlltoolPath, outputLib); err != nil {
fmt.Fprintf(os.Stderr, "generate mingw import library failed: %v\n", err)
os.Exit(1)
}
}

View File

@@ -25,6 +25,7 @@ import (
"sync"
"time"
"GoNavi-Wails/internal/buildutil"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"GoNavi-Wails/internal/logger"
@@ -4187,7 +4188,7 @@ func resolveDuckDBWindowsCGOToolchainBinFromCandidates(candidates []string) (str
}
}
installHint := `请先安装 MSYS2 UCRT64 工具链winget install --id MSYS2.MSYS2 -e然后执行 C:\msys64\usr\bin\bash.exe -lc "pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-gcc"`
installHint := `请先安装 MSYS2 UCRT64 工具链winget install --id MSYS2.MSYS2 -e然后执行 C:\msys64\usr\bin\bash.exe -lc "pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils"`
if len(checked) == 0 {
return "", fmt.Errorf("未找到可用的 gcc.exe/g++.exe%s", installHint)
}
@@ -4266,7 +4267,6 @@ func prepareDuckDBWindowsDynamicLibraryForBuild() (string, func(), error) {
required := map[string]bool{
"duckdb.dll": false,
"duckdb.lib": false,
}
for _, file := range reader.File {
baseName := strings.ToLower(filepath.Base(filepath.ToSlash(file.Name)))
@@ -4291,13 +4291,24 @@ func prepareDuckDBWindowsDynamicLibraryForBuild() (string, func(), error) {
return "", nil, fmt.Errorf("DuckDB 官方动态库包缺少文件:%s", strings.Join(missing, ", "))
}
importLibPath := filepath.Join(workDir, "duckdb.lib")
for _, alias := range []string{"libduckdb.dll.a", "libduckdb.a"} {
aliasPath := filepath.Join(workDir, alias)
if err := copyOptionalDriverSupportFile(importLibPath, aliasPath); err != nil {
cleanup()
return "", nil, err
}
toolchainBin, err := resolveDuckDBWindowsCGOToolchainBin()
if err != nil {
cleanup()
return "", nil, fmt.Errorf("定位 DuckDB Windows dlltool 失败:%w", err)
}
dllPath := filepath.Join(workDir, "duckdb.dll")
importLibPath := filepath.Join(workDir, "libduckdb.dll.a")
if err := buildutil.GenerateWindowsImportLibraryFromDLL(
dllPath,
filepath.Join(toolchainBin, "dlltool.exe"),
importLibPath,
); err != nil {
cleanup()
return "", nil, err
}
if err := copyOptionalDriverSupportFile(importLibPath, filepath.Join(workDir, "libduckdb.a")); err != nil {
cleanup()
return "", nil, err
}
return workDir, cleanup, nil

View File

@@ -0,0 +1,229 @@
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
}

View File

@@ -0,0 +1,20 @@
package buildutil
import (
"strings"
"testing"
)
func TestBuildModuleDefinition(t *testing.T) {
content := buildModuleDefinition("duckdb.dll", []string{"duckdb_close", "duckdb_open", "duckdb_open", ""})
if !strings.Contains(content, "LIBRARY duckdb.dll") {
t.Fatalf("expected dll header, got %q", content)
}
if strings.Count(content, "duckdb_open") != 1 {
t.Fatalf("expected duplicate exports to be collapsed: %q", content)
}
if !strings.Contains(content, "EXPORTS\n duckdb_close\n duckdb_open\n") {
t.Fatalf("expected sorted export list, got %q", content)
}
}