diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 82e494f..4f70753 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -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}" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98f8290..dddbd9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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" diff --git a/build-driver-agents.sh b/build-driver-agents.sh index 352a024..f207b4f 100755 --- a/build-driver-agents.sh +++ b/build-driver-agents.sh @@ -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 diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index 8fdbcf0..477c3f4 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -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 diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go index e69cac2..2535530 100644 --- a/internal/app/methods_driver_version_test.go +++ b/internal/app/methods_driver_version_test.go @@ -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{} diff --git a/internal/db/driver_agent_revisions_gen.go b/internal/db/driver_agent_revisions_gen.go index b844e8d..12ffe96 100644 --- a/internal/db/driver_agent_revisions_gen.go +++ b/internal/db/driver_agent_revisions_gen.go @@ -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", diff --git a/tools/generate-driver-agent-revisions.sh b/tools/generate-driver-agent-revisions.sh index db42b64..acecf56 100755 --- a/tools/generate-driver-agent-revisions.sh +++ b/tools/generate-driver-agent-revisions.sh @@ -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