mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-21 04:41:48 +08:00
🐛 fix(duckdb): 修复 Windows 扩展下载平台不匹配问题
- 改用官方 duckdb.dll 动态库构建 Windows DuckDB driver-agent - 安装、总包和本地导入流程同步携带运行时依赖 - 更新 DuckDB driver-agent revision 并补充安装链路测试 Refs #430
This commit is contained in:
49
.github/workflows/dev-build.yml
vendored
49
.github/workflows/dev-build.yml
vendored
@@ -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}" \
|
||||
|
||||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user