🐛 fix(duckdb): 修复 Windows 扩展下载平台不匹配问题

- 改用官方 duckdb.dll 动态库构建 Windows DuckDB driver-agent

- 安装、总包和本地导入流程同步携带运行时依赖

- 更新 DuckDB driver-agent revision 并补充安装链路测试

Refs #430
This commit is contained in:
Syngnat
2026-05-08 22:50:03 +08:00
parent ff0661d285
commit 0c2b112234
7 changed files with 573 additions and 33 deletions

View File

@@ -264,6 +264,23 @@ jobs:
DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
OUTDIR="drivers/${{ matrix.os_name }}"
mkdir -p "$OUTDIR"
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
prepare_duckdb_windows_library() {
local lib_dir="$RUNNER_TEMP/duckdb-windows-${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
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
curl -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"
unzip -qo "$zip_path" -d "$lib_dir"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
echo "$lib_dir"
}
for DRIVER in "${DRIVERS[@]}"; do
BUILD_DRIVER="$DRIVER"
@@ -275,22 +292,38 @@ jobs:
continue
fi
TAG="gonavi_${BUILD_DRIVER}_driver"
BUILD_TAGS="$TAG"
OUTPUT="${DRIVER}-driver-agent-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
OUTPUT="${OUTPUT}.exe"
fi
OUTPUT_PATH="${OUTDIR}/${OUTPUT}"
echo "🔧 构建 ${OUTPUT_PATH} (tag=${TAG})"
DUCKDB_LIB_DIR=""
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" = "amd64" ]; then
DUCKDB_LIB_DIR="$(prepare_duckdb_windows_library)"
BUILD_TAGS="${BUILD_TAGS} duckdb_use_lib"
fi
echo "🔧 构建 ${OUTPUT_PATH} (tags=${BUILD_TAGS})"
if [ "$DRIVER" = "duckdb" ]; then
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${TAG}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
if [ -n "$DUCKDB_LIB_DIR" ]; then
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CGO_LDFLAGS="-L${DUCKDB_LIB_DIR} -lduckdb" PATH="${DUCKDB_LIB_DIR}:$PATH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
cp "$DUCKDB_LIB_DIR/duckdb.dll" "$OUTDIR/duckdb.dll"
else
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
fi
else
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${TAG}" \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \

View File

@@ -255,6 +255,23 @@ jobs:
DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
OUTDIR="drivers/${{ matrix.os_name }}"
mkdir -p "$OUTDIR"
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
prepare_duckdb_windows_library() {
local lib_dir="$RUNNER_TEMP/duckdb-windows-${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
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
curl -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"
unzip -qo "$zip_path" -d "$lib_dir"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
echo "$lib_dir"
}
for DRIVER in "${DRIVERS[@]}"; do
BUILD_DRIVER="$DRIVER"
@@ -266,22 +283,38 @@ jobs:
continue
fi
TAG="gonavi_${BUILD_DRIVER}_driver"
BUILD_TAGS="$TAG"
OUTPUT="${DRIVER}-driver-agent-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
OUTPUT="${OUTPUT}.exe"
fi
OUTPUT_PATH="${OUTDIR}/${OUTPUT}"
echo "🔧 构建 ${OUTPUT_PATH} (tag=${TAG})"
DUCKDB_LIB_DIR=""
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" = "amd64" ]; then
DUCKDB_LIB_DIR="$(prepare_duckdb_windows_library)"
BUILD_TAGS="${BUILD_TAGS} duckdb_use_lib"
fi
echo "🔧 构建 ${OUTPUT_PATH} (tags=${BUILD_TAGS})"
if [ "$DRIVER" = "duckdb" ]; then
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${TAG}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
if [ -n "$DUCKDB_LIB_DIR" ]; then
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CGO_LDFLAGS="-L${DUCKDB_LIB_DIR} -lduckdb" PATH="${DUCKDB_LIB_DIR}:$PATH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
cp "$DUCKDB_LIB_DIR/duckdb.dll" "$OUTDIR/duckdb.dll"
else
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
fi
else
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${TAG}" \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
@@ -534,6 +567,7 @@ jobs:
REQUIRED_FILES=(
"drivers/Windows/duckdb-driver-agent-windows-amd64.exe"
"drivers/Windows/duckdb.dll"
"drivers/MacOS/duckdb-driver-agent-darwin-amd64"
"drivers/MacOS/duckdb-driver-agent-darwin-arm64"
"drivers/Linux/duckdb-driver-agent-linux-amd64"

View File

@@ -7,6 +7,9 @@ cd "$SCRIPT_DIR"
DEFAULT_DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
DEFAULT_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64)
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
DUCKDB_WINDOWS_SUPPORT_DLL="duckdb.dll"
usage() {
cat <<'EOF'
@@ -139,6 +142,54 @@ PY
fi
}
prepare_duckdb_windows_library() {
local cache_root="$1"
local lib_dir="$cache_root/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local zip_path="$cache_root/libduckdb-windows-amd64.zip"
if [[ -f "$lib_dir/duckdb.dll" && -f "$lib_dir/duckdb.lib" ]]; then
printf '%s\n' "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
echo "⬇️ 下载 DuckDB Windows 官方动态库:$DUCKDB_WINDOWS_LIBRARY_URL" >&2
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"
elif command -v wget >/dev/null 2>&1; then
wget -q "$DUCKDB_WINDOWS_LIBRARY_URL" -O "$zip_path"
else
echo "❌ 未找到 curl 或 wget无法下载 DuckDB Windows 动态库。" >&2
return 1
fi
if command -v unzip >/dev/null 2>&1; then
unzip -qo "$zip_path" -d "$lib_dir"
elif command -v python3 >/dev/null 2>&1; then
DUCKDB_LIB_ZIP="$zip_path" DUCKDB_LIB_DIR="$lib_dir" python3 - <<'PY'
import os
import zipfile
zip_path = os.environ["DUCKDB_LIB_ZIP"]
target = os.environ["DUCKDB_LIB_DIR"]
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(target)
PY
else
echo "❌ 未找到 unzip 或 python3无法解压 DuckDB Windows 动态库。" >&2
return 1
fi
if [[ ! -f "$lib_dir/duckdb.dll" || ! -f "$lib_dir/duckdb.lib" ]]; then
echo "❌ DuckDB Windows 动态库包缺少 duckdb.dll 或 duckdb.lib。" >&2
return 1
fi
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
printf '%s\n' "$lib_dir"
}
join_by_comma() {
local IFS=,
echo "$*"
@@ -282,6 +333,7 @@ for platform in "${platforms[@]}"; do
build_driver="$(build_driver_name "$driver")"
tag="gonavi_${build_driver}_driver"
build_tags="$tag"
asset_name="${driver}-driver-agent-${goos}-${goarch}"
if [[ "$goos" == "windows" ]]; then
asset_name="${asset_name}.exe"
@@ -292,11 +344,22 @@ for platform in "${platforms[@]}"; do
if [[ "$driver" == "duckdb" ]]; then
cgo_enabled=1
fi
duckdb_lib_dir=""
if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" == "amd64" ]]; then
duckdb_lib_dir="$(prepare_duckdb_windows_library "$bundle_stage_dir")"
build_tags="$build_tags duckdb_use_lib"
fi
echo "🔧 构建 $driver -> $asset_name (platform=$platform, tag=$tag, CGO_ENABLED=$cgo_enabled)"
echo "🔧 构建 $driver -> $asset_name (platform=$platform, tags=$build_tags, CGO_ENABLED=$cgo_enabled)"
set +e
CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \
go build -tags "$tag" -trimpath -ldflags "-s -w" -o "$output_path" ./cmd/optional-driver-agent
if [[ -n "$duckdb_lib_dir" ]]; then
CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \
CGO_LDFLAGS="-L${duckdb_lib_dir} -lduckdb" PATH="${duckdb_lib_dir}:$PATH" \
go build -tags "$build_tags" -trimpath -ldflags "-s -w" -o "$output_path" ./cmd/optional-driver-agent
else
CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \
go build -tags "$build_tags" -trimpath -ldflags "-s -w" -o "$output_path" ./cmd/optional-driver-agent
fi
build_exit=$?
set -e
@@ -310,6 +373,11 @@ for platform in "${platforms[@]}"; do
fi
cp "$output_path" "$bundle_platform_dir/$asset_name"
if [[ -n "$duckdb_lib_dir" ]]; then
cp "$duckdb_lib_dir/$DUCKDB_WINDOWS_SUPPORT_DLL" "$output_dir_abs/$DUCKDB_WINDOWS_SUPPORT_DLL"
cp "$duckdb_lib_dir/$DUCKDB_WINDOWS_SUPPORT_DLL" "$bundle_platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL"
built_assets+=("$platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL")
fi
built_assets+=("$platform_dir/$asset_name")
done
done

View File

@@ -306,6 +306,9 @@ const (
driverChecksumPolicyOff = "off"
driverEngineGo = "go"
driverEngineExternal = "external"
duckDBWindowsLibraryVersion = "v1.4.4"
duckDBWindowsLibraryArchiveURL = "https://github.com/duckdb/duckdb/releases/download/" + duckDBWindowsLibraryVersion + "/libduckdb-windows-amd64.zip"
duckDBWindowsSupportDLLName = "duckdb.dll"
)
const builtinDriverManifestJSON = `{
@@ -1842,7 +1845,7 @@ func optionalDriverSourceBuildAvailable(definition driverDefinition, selectedVer
if driverType == "" || !db.IsOptionalGoDriver(driverType) {
return false
}
if _, err := optionalDriverBuildTag(driverType, selectedVersion); err != nil {
if _, err := optionalDriverBuildTags(driverType, selectedVersion); err != nil {
return false
}
if _, err := exec.LookPath("go"); err != nil {
@@ -2878,7 +2881,7 @@ func installOptionalDriverAgentPackage(a *App, definition driverDefinition, sele
if err != nil {
return installedDriverPackage{}, err
}
if activateErr := activateOptionalDriverAgentBinary(installPath, runtimePath); activateErr != nil {
if activateErr := activateOptionalDriverAgentBinary(driverType, installPath, runtimePath); activateErr != nil {
return installedDriverPackage{}, fmt.Errorf("activate %s driver agent failed: %w", resolveDriverDisplayName(definition), activateErr)
}
if strings.TrimSpace(hash) == "" {
@@ -2958,6 +2961,9 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa
if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil {
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理失败:%w", copyErr)
}
if supportErr := copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(sourcePath), filepath.Dir(executablePath)); supportErr != nil {
return installedDriverPackage{}, fmt.Errorf("导入本地驱动代理运行时依赖失败:%w", supportErr)
}
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
return installedDriverPackage{}, validateErr
@@ -3226,6 +3232,9 @@ func installOptionalDriverAgentFromLocalZip(zipPath string, definition driverDef
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
if supportErr := extractOptionalDriverSupportFilesFromZip(reader.File, driverType, entry.Name, filepath.Dir(executablePath)); supportErr != nil {
return "", supportErr
}
return filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(entry.Name), "./")), nil
}
@@ -3419,6 +3428,9 @@ func downloadOptionalDriverAgentBinary(a *App, definition driverDefinition, urlT
if trimmedURL == "" {
return "", fmt.Errorf("下载地址为空")
}
if len(optionalDriverSupportFileNames(driverType)) > 0 {
return "", fmt.Errorf("%s 当前平台需要随包提供运行时依赖(%s不能安装单文件代理请使用驱动总包或本地源码构建", displayName, strings.Join(optionalDriverSupportFileNames(driverType), ", "))
}
tempPath := executablePath + ".tmp"
_ = os.Remove(tempPath)
@@ -3557,6 +3569,10 @@ func downloadOptionalDriverAgentFromBundle(a *App, definition driverDefinition,
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", "", fmt.Errorf("设置驱动代理权限失败:%w", chmodErr)
}
if supportErr := extractOptionalDriverSupportFilesFromZip(reader.File, driverType, entry.Name, filepath.Dir(executablePath)); supportErr != nil {
_ = os.Remove(executablePath)
return "", "", supportErr
}
if validateErr := db.ValidateOptionalDriverAgentExecutable(driverType, executablePath); validateErr != nil {
_ = os.Remove(executablePath)
return "", "", validateErr
@@ -3577,7 +3593,7 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
return "", fmt.Errorf("当前环境未安装 Go且未找到可用的 %s 预编译代理包", displayName)
}
tagName, tagErr := optionalDriverBuildTag(driverType, selectedVersion)
tagName, tagErr := optionalDriverBuildTags(driverType, selectedVersion)
if tagErr != nil {
return "", tagErr
}
@@ -3586,13 +3602,36 @@ func buildOptionalDriverAgentFromSource(definition driverDefinition, executableP
if rootErr != nil {
return "", rootErr
}
env := append([]string{}, os.Environ()...)
env = withEnvValue(env, "GOTOOLCHAIN", "auto")
var duckDBLibDir string
var cleanupDuckDBLib func()
if normalizeDriverType(driverType) == "duckdb" {
env = withEnvValue(env, "CGO_ENABLED", "1")
}
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
libDir, cleanup, prepErr := prepareDuckDBWindowsDynamicLibraryForBuild()
if prepErr != nil {
return "", fmt.Errorf("准备 DuckDB Windows 动态库失败:%w", prepErr)
}
duckDBLibDir = libDir
cleanupDuckDBLib = cleanup
defer cleanupDuckDBLib()
env = withEnvValue(env, "CGO_LDFLAGS", fmt.Sprintf("-L\"%s\" -lduckdb", filepath.ToSlash(duckDBLibDir)))
env = prependPathEnv(env, duckDBLibDir)
}
cmd := exec.Command(goPath, "build", "-tags", tagName, "-trimpath", "-ldflags", "-s -w", "-o", executablePath, "./cmd/optional-driver-agent")
cmd.Dir = projectRoot
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=auto")
cmd.Env = env
output, buildErr := cmd.CombinedOutput()
if buildErr != nil {
return "", fmt.Errorf("构建 %s 驱动代理失败:%v输出%s", displayName, buildErr, strings.TrimSpace(string(output)))
}
if strings.TrimSpace(duckDBLibDir) != "" {
if copyErr := copyOptionalDriverSupportFilesFromDirectory(driverType, duckDBLibDir, filepath.Dir(executablePath)); copyErr != nil {
return "", fmt.Errorf("复制 %s 运行时依赖失败:%w", displayName, copyErr)
}
}
if chmodErr := os.Chmod(executablePath, 0o755); chmodErr != nil && stdRuntime.GOOS != "windows" {
return "", fmt.Errorf("设置 %s 驱动代理权限失败:%w", displayName, chmodErr)
}
@@ -3638,10 +3677,7 @@ func shouldForceSourceBuildForResolvedDownload(driverType string, selectedVersio
func shouldPreferSourceBuildBeforeDownload(driverType string, selectedVersion string) bool {
_ = selectedVersion
switch normalizeDriverType(driverType) {
default:
return false
}
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
}
func shouldSkipReusableAgentCandidate(driverType string, selectedVersion string) bool {
@@ -3650,10 +3686,25 @@ func shouldSkipReusableAgentCandidate(driverType string, selectedVersion string)
case "mongodb", "kingbase":
return true
default:
return false
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
}
}
func shouldUseDuckDBWindowsDynamicLibrary(driverType string) bool {
return normalizeDriverType(driverType) == "duckdb" && stdRuntime.GOOS == "windows" && stdRuntime.GOARCH == "amd64"
}
func shouldSkipDirectOptionalDriverDownloads(driverType string) bool {
return shouldUseDuckDBWindowsDynamicLibrary(driverType)
}
func optionalDriverSupportFileNames(driverType string) []string {
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
return []string{duckDBWindowsSupportDLLName}
}
return nil
}
func optionalDriverBuildTag(driverType string, selectedVersion string) (string, error) {
switch normalizeDriverType(driverType) {
case "mysql":
@@ -3696,6 +3747,17 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string,
}
}
func optionalDriverBuildTags(driverType string, selectedVersion string) (string, error) {
tagName, err := optionalDriverBuildTag(driverType, selectedVersion)
if err != nil {
return "", err
}
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
return strings.TrimSpace(tagName + " duckdb_use_lib"), nil
}
return tagName, nil
}
func locateProjectRootForAgentBuild() (string, error) {
wd, err := os.Getwd()
if err != nil {
@@ -3720,6 +3782,89 @@ func fileExists(path string) bool {
return err == nil && !info.IsDir()
}
func withEnvValue(env []string, key string, value string) []string {
normalizedKey := strings.ToUpper(strings.TrimSpace(key))
entry := normalizedKey + "=" + value
for i, item := range env {
name, _, ok := strings.Cut(item, "=")
if ok && strings.ToUpper(strings.TrimSpace(name)) == normalizedKey {
env[i] = entry
return env
}
}
return append(env, entry)
}
func prependPathEnv(env []string, dir string) []string {
trimmedDir := strings.TrimSpace(dir)
if trimmedDir == "" {
return env
}
currentPath := os.Getenv("PATH")
return withEnvValue(env, "PATH", trimmedDir+string(os.PathListSeparator)+currentPath)
}
func prepareDuckDBWindowsDynamicLibraryForBuild() (string, func(), error) {
workDir, err := os.MkdirTemp("", "gonavi-duckdb-lib-*")
if err != nil {
return "", nil, err
}
cleanup := func() {
_ = os.RemoveAll(workDir)
}
archivePath := filepath.Join(workDir, "libduckdb-windows-amd64.zip")
if _, err := downloadFileWithHash(duckDBWindowsLibraryArchiveURL, archivePath, nil); err != nil {
cleanup()
return "", nil, err
}
reader, err := zip.OpenReader(archivePath)
if err != nil {
cleanup()
return "", nil, err
}
defer reader.Close()
required := map[string]bool{
"duckdb.dll": false,
"duckdb.lib": false,
}
for _, file := range reader.File {
baseName := strings.ToLower(filepath.Base(filepath.ToSlash(file.Name)))
if _, ok := required[baseName]; !ok {
continue
}
if err := extractZipFileToPath(file, filepath.Join(workDir, baseName)); err != nil {
cleanup()
return "", nil, err
}
required[baseName] = true
}
var missing []string
for name, found := range required {
if !found {
missing = append(missing, name)
}
}
if len(missing) > 0 {
sort.Strings(missing)
cleanup()
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
}
}
return workDir, cleanup, nil
}
func optionalDriverPublicTypeName(driverType string) string {
switch normalizeDriverType(driverType) {
case "diros":
@@ -3980,6 +4125,10 @@ func resolveOptionalDriverAgentDownloadURLs(definition driverDefinition, rawURL
candidates = append(candidates, trimmed)
}
if shouldSkipDirectOptionalDriverDownloads(definition.Type) {
return candidates
}
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil {
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https":
@@ -4138,7 +4287,7 @@ func resolveDriverDisplayName(definition driverDefinition) string {
return "未知"
}
func activateOptionalDriverAgentBinary(installPath string, runtimePath string) error {
func activateOptionalDriverAgentBinary(driverType string, installPath string, runtimePath string) error {
source := strings.TrimSpace(installPath)
target := strings.TrimSpace(runtimePath)
if source == "" || target == "" {
@@ -4159,7 +4308,10 @@ func activateOptionalDriverAgentBinary(installPath string, runtimePath string) e
if strings.EqualFold(absSource, absTarget) {
return nil
}
return copyAgentBinary(source, target)
if err := copyAgentBinary(source, target); err != nil {
return err
}
return copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(source), filepath.Dir(target))
}
func copyAgentBinary(sourcePath, targetPath string) error {
@@ -4193,7 +4345,7 @@ func copyAgentBinary(sourcePath, targetPath string) error {
_ = os.Remove(tempPath)
return chmodErr
}
if err := os.Rename(tempPath, targetPath); err != nil {
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
_ = os.Remove(tempPath)
return err
}
@@ -4203,6 +4355,173 @@ func copyAgentBinary(sourcePath, targetPath string) error {
return nil
}
func extractZipFileToPath(file *zip.File, targetPath string) error {
if file == nil {
return fmt.Errorf("zip 条目为空")
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
tempPath := targetPath + ".tmp"
_ = os.Remove(tempPath)
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return err
}
dst, err := os.Create(tempPath)
if err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return err
}
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
_ = os.Remove(tempPath)
return err
}
return nil
}
func copyOptionalDriverSupportFile(sourcePath, targetPath string) error {
src, err := os.Open(sourcePath)
if err != nil {
return err
}
defer src.Close()
tempPath := targetPath + ".tmp"
_ = os.Remove(tempPath)
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return err
}
dst, err := os.Create(tempPath)
if err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Sync(); err != nil {
dst.Close()
_ = os.Remove(tempPath)
return err
}
if err := dst.Close(); err != nil {
_ = os.Remove(tempPath)
return err
}
if err := renameTempFileOverTarget(tempPath, targetPath); err != nil {
_ = os.Remove(tempPath)
return err
}
return nil
}
func renameTempFileOverTarget(tempPath, targetPath string) error {
if err := os.Rename(tempPath, targetPath); err == nil {
return nil
} else {
firstErr := err
if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) {
return firstErr
}
if retryErr := os.Rename(tempPath, targetPath); retryErr != nil {
return retryErr
}
return nil
}
}
func copyOptionalDriverSupportFilesFromDirectory(driverType string, sourceDir string, targetDir string) error {
names := optionalDriverSupportFileNames(driverType)
if len(names) == 0 {
return nil
}
sourceRoot := strings.TrimSpace(sourceDir)
targetRoot := strings.TrimSpace(targetDir)
if sourceRoot == "" || targetRoot == "" {
return fmt.Errorf("运行时依赖目录为空")
}
for _, name := range names {
sourcePath := filepath.Join(sourceRoot, name)
targetPath := filepath.Join(targetRoot, name)
if err := copyOptionalDriverSupportFile(sourcePath, targetPath); err != nil {
return fmt.Errorf("复制 %s 失败:%w", name, err)
}
}
return nil
}
func findOptionalDriverSupportFileInZip(files []*zip.File, agentEntryName string, supportName string) *zip.File {
normalizedAgent := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(agentEntryName), "./"))
agentDir := filepath.ToSlash(filepath.Dir(normalizedAgent))
if agentDir == "." {
agentDir = ""
}
candidatePaths := []string{}
if agentDir != "" {
candidatePaths = append(candidatePaths, filepath.ToSlash(filepath.Join(agentDir, supportName)))
}
candidatePaths = append(candidatePaths, supportName)
for _, candidate := range candidatePaths {
for _, file := range files {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if name == candidate {
return file
}
}
for _, file := range files {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(name, candidate) {
return file
}
}
}
for _, file := range files {
name := filepath.ToSlash(strings.TrimPrefix(strings.TrimSpace(file.Name), "./"))
if strings.EqualFold(filepath.Base(name), supportName) {
return file
}
}
return nil
}
func extractOptionalDriverSupportFilesFromZip(files []*zip.File, driverType string, agentEntryName string, targetDir string) error {
names := optionalDriverSupportFileNames(driverType)
if len(names) == 0 {
return nil
}
targetRoot := strings.TrimSpace(targetDir)
if targetRoot == "" {
return fmt.Errorf("运行时依赖目标目录为空")
}
for _, name := range names {
entry := findOptionalDriverSupportFileInZip(files, agentEntryName, name)
if entry == nil {
return fmt.Errorf("驱动包缺少运行时依赖:%s", name)
}
if err := extractZipFileToPath(entry, filepath.Join(targetRoot, name)); err != nil {
return fmt.Errorf("解压运行时依赖 %s 失败:%w", name, err)
}
}
return nil
}
func scaleProgress(downloaded, total, start, end int64) (int64, int64) {
if end <= start {
return end, 100

View File

@@ -198,6 +198,81 @@ func TestBuildOptionalDriverFallbackProgressMessageReportsBundleFallback(t *test
}
}
func TestDuckDBWindowsBuildUsesDynamicLibraryTag(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB Windows dynamic library flow only applies on windows/amd64")
}
tags, err := optionalDriverBuildTags("duckdb", "")
if err != nil {
t.Fatalf("resolve DuckDB build tags failed: %v", err)
}
if !strings.Contains(tags, "gonavi_duckdb_driver") || !strings.Contains(tags, "duckdb_use_lib") {
t.Fatalf("expected DuckDB Windows build tags to include dynamic library tag, got %q", tags)
}
if !shouldPreferSourceBuildBeforeDownload("duckdb", "") {
t.Fatal("expected DuckDB Windows install to try local dynamic-library build before downloads")
}
if !shouldSkipReusableAgentCandidate("duckdb", "") {
t.Fatal("expected DuckDB Windows install to skip reusable static agent candidates")
}
urls := resolveOptionalDriverAgentDownloadURLs(driverDefinition{Type: "duckdb"}, "https://example.com/duckdb-driver-agent-windows-amd64.exe", "")
if len(urls) != 0 {
t.Fatalf("expected DuckDB Windows install to skip single-file direct downloads, got %v", urls)
}
}
func TestInstallOptionalDriverAgentFromLocalZipExtractsDuckDBDLL(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("DuckDB DLL support file is only required on windows/amd64")
}
tmpDir := t.TempDir()
zipPath := filepath.Join(tmpDir, "duckdb-driver.zip")
zipFile, err := os.Create(zipPath)
if err != nil {
t.Fatalf("create zip failed: %v", err)
}
zw := zip.NewWriter(zipFile)
for name, content := range map[string]string{
"Windows/duckdb-driver-agent-windows-amd64.exe": "agent",
"Windows/duckdb.dll": "dll",
} {
w, err := zw.Create(name)
if err != nil {
t.Fatalf("create zip entry %s failed: %v", name, err)
}
if _, err := w.Write([]byte(content)); err != nil {
t.Fatalf("write zip entry %s failed: %v", name, err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("close zip writer failed: %v", err)
}
if err := zipFile.Close(); err != nil {
t.Fatalf("close zip file failed: %v", err)
}
target := filepath.Join(tmpDir, "install", "duckdb-driver-agent.exe")
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
t.Fatalf("create install dir failed: %v", err)
}
entryName, err := installOptionalDriverAgentFromLocalZip(zipPath, driverDefinition{Type: "duckdb", Name: "DuckDB"}, target, "")
if err != nil {
t.Fatalf("install local DuckDB zip failed: %v", err)
}
if entryName != "Windows/duckdb-driver-agent-windows-amd64.exe" {
t.Fatalf("unexpected extracted agent entry: %q", entryName)
}
dllBytes, err := os.ReadFile(filepath.Join(filepath.Dir(target), "duckdb.dll"))
if err != nil {
t.Fatalf("expected duckdb.dll to be extracted: %v", err)
}
if string(dllBytes) != "dll" {
t.Fatalf("unexpected duckdb.dll content: %q", string(dllBytes))
}
}
func TestDownloadDriverPackageRejectsUnsupportedMongoVersion(t *testing.T) {
app := &App{}

View File

@@ -10,7 +10,7 @@ func init() {
"sphinx": "src-4f9ec83df79bc8f7",
"sqlserver": "src-172613975f6f18d2",
"sqlite": "src-2ff8c7eb368b324b",
"duckdb": "src-6d20adc5b77a9ed6",
"duckdb": "src-8af9c516b81fd5ee",
"dameng": "src-659f5656149e216c",
"kingbase": "src-82ff6ff9440233cd",
"highgo": "src-a3915194d9a50d5d",

View File

@@ -40,6 +40,17 @@ build_driver_name() {
echo "$1"
}
driver_build_tags() {
local driver="$1"
local build_driver tag
build_driver="$(build_driver_name "$driver")"
tag="gonavi_${build_driver}_driver"
if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" == "amd64" ]]; then
tag="$tag duckdb_use_lib"
fi
echo "$tag"
}
hash_file() {
local target="$1"
if command -v sha256sum >/dev/null 2>&1; then
@@ -205,7 +216,7 @@ fingerprint_driver() {
local driver="$1"
local build_driver tag cgo_enabled tmp file identity file_hash revision
build_driver="$(build_driver_name "$driver")"
tag="gonavi_${build_driver}_driver"
tag="$(driver_build_tags "$driver")"
cgo_enabled=0
if [[ "$driver" == "duckdb" ]]; then
cgo_enabled=1