mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-17 03:59:41 +08:00
🐛 fix(ci): 修复 DuckDB Windows 导入库生成链路
- 改为从 duckdb.dll 生成 MinGW 可用的导入库文件 - 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑 - 新增导入库生成命令与 buildutil 单测
This commit is contained in:
17
.github/workflows/dev-build.yml
vendored
17
.github/workflows/dev-build.yml
vendored
@@ -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
|
||||
|
||||
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
27
cmd/mingw-import-lib/main.go
Normal file
27
cmd/mingw-import-lib/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
229
internal/buildutil/windows_import_lib.go
Normal file
229
internal/buildutil/windows_import_lib.go
Normal 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
|
||||
}
|
||||
20
internal/buildutil/windows_import_lib_test.go
Normal file
20
internal/buildutil/windows_import_lib_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user