From a54a357e4bf697037643171a0d189b23fad1b681 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 8 Jun 2026 17:59:58 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ci):=20=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?DuckDB=20Windows=20=E5=AF=BC=E5=85=A5=E5=BA=93=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改为从 duckdb.dll 生成 MinGW 可用的导入库文件 - 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑 - 新增导入库生成命令与 buildutil 单测 --- .github/workflows/dev-build.yml | 17 +- .github/workflows/release.yml | 17 +- cmd/mingw-import-lib/main.go | 27 +++ internal/app/methods_driver.go | 29 ++- internal/buildutil/windows_import_lib.go | 229 ++++++++++++++++++ internal/buildutil/windows_import_lib_test.go | 20 ++ 6 files changed, 314 insertions(+), 25 deletions(-) create mode 100644 cmd/mingw-import-lib/main.go create mode 100644 internal/buildutil/windows_import_lib.go create mode 100644 internal/buildutil/windows_import_lib_test.go diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index f5a8d3a..e78f4a3 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07c76e1..c3887dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/cmd/mingw-import-lib/main.go b/cmd/mingw-import-lib/main.go new file mode 100644 index 0000000..e0a6bd8 --- /dev/null +++ b/cmd/mingw-import-lib/main.go @@ -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) + } +} diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index 8dce6c1..1e1ef4b 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -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 diff --git a/internal/buildutil/windows_import_lib.go b/internal/buildutil/windows_import_lib.go new file mode 100644 index 0000000..a693eed --- /dev/null +++ b/internal/buildutil/windows_import_lib.go @@ -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 +} diff --git a/internal/buildutil/windows_import_lib_test.go b/internal/buildutil/windows_import_lib_test.go new file mode 100644 index 0000000..5ed70e7 --- /dev/null +++ b/internal/buildutil/windows_import_lib_test.go @@ -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) + } +}