diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 386c16b7..8393564d 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -27,6 +27,9 @@ jobs: - name: Test release asset contracts run: | + bash tools/detect-changed-driver-agents.test.sh + bash tools/generate-driver-agent-revisions.test.sh + python3 tools/generate-driver-release-manifest.test.py python3 tools/generate-update-latest-manifest.test.py python3 tools/windows-release-artifacts.test.py @@ -645,7 +648,7 @@ jobs: open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elastic|elasticsearch) echo "elasticsearch" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$value" ;; *) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da35d75f..ecf2ca39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: - name: Test release asset contracts run: | + bash tools/detect-changed-driver-agents.test.sh + bash tools/generate-driver-agent-revisions.test.sh + python3 tools/generate-driver-release-manifest.test.py python3 tools/generate-update-latest-manifest.test.py python3 tools/windows-release-artifacts.test.py @@ -587,7 +590,7 @@ jobs: open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elastic|elasticsearch) echo "elasticsearch" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$value" ;; *) diff --git a/build-driver-agents.sh b/build-driver-agents.sh index 75745850..3967f21d 100755 --- a/build-driver-agents.sh +++ b/build-driver-agents.sh @@ -5,7 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch) +DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino) 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" @@ -44,7 +44,7 @@ normalize_driver() { open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elasticsearch|elastic) echo "elasticsearch" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$name" ;; *) diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index ee28713d..48b3a9a2 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -3355,33 +3355,6 @@ func verifyInstalledOptionalDriverAgentRevision(driverType string, executablePat return actual, nil } -func observeInstalledOptionalDriverAgentRevision(driverType string, executablePath string, selectedVersion string) string { - if !shouldVerifyOptionalDriverAgentRevision(driverType, selectedVersion) { - return "" - } - expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType)) - actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath) - if expected == "" { - return strings.TrimSpace(actual) - } - displayName := resolveDriverDisplayName(driverDefinition{Type: driverType}) - if err != nil { - logger.Warnf("%s 驱动代理版本元数据不可用,已保留安装:path=%s version=%s err=%v;建议在驱动管理中重装", - displayName, executablePath, normalizeVersion(selectedVersion), err) - return "" - } - actual = strings.TrimSpace(actual) - if !current { - actualLabel := actual - if actualLabel == "" { - actualLabel = "空" - } - logger.Warnf("%s 驱动代理 revision 不匹配,已保留安装:已安装=%s 当前需要=%s path=%s version=%s;建议在驱动管理中重装", - displayName, actualLabel, expected, executablePath, normalizeVersion(selectedVersion)) - } - return actual -} - func shouldVerifyOptionalDriverAgentRevision(driverType string, selectedVersion string) bool { switch normalizeDriverType(driverType) { case "mongodb": @@ -3451,6 +3424,132 @@ func hashFileSHA256(filePath string) (string, error) { return hex.EncodeToString(hasher.Sum(nil)), nil } +type optionalDriverInstallSnapshot struct { + path string + backupPath string + existed bool + isDir bool + mode os.FileMode +} + +func optionalDriverInstallTargetPaths(driverType string, installPath string, runtimePath string) []string { + targets := []string{installPath, runtimePath} + for _, supportName := range optionalDriverSupportFileNames(driverType) { + targets = append(targets, + filepath.Join(filepath.Dir(installPath), supportName), + filepath.Join(filepath.Dir(runtimePath), supportName), + ) + } + + unique := make([]string, 0, len(targets)) + seen := make(map[string]struct{}, len(targets)) + for _, target := range targets { + cleaned := filepath.Clean(strings.TrimSpace(target)) + if cleaned == "." || cleaned == "" { + continue + } + key := cleaned + if stdRuntime.GOOS == "windows" { + key = strings.ToLower(key) + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + unique = append(unique, cleaned) + } + return unique +} + +func snapshotOptionalDriverInstallTargets(stagingDir string, targetPaths []string) ([]optionalDriverInstallSnapshot, error) { + snapshots := make([]optionalDriverInstallSnapshot, 0, len(targetPaths)) + for index, targetPath := range targetPaths { + snapshot := optionalDriverInstallSnapshot{path: targetPath} + info, err := os.Stat(targetPath) + if os.IsNotExist(err) { + snapshots = append(snapshots, snapshot) + continue + } + if err != nil { + return nil, err + } + snapshot.existed = true + snapshot.isDir = info.IsDir() + snapshot.mode = info.Mode() + if !snapshot.isDir { + snapshot.backupPath = filepath.Join(stagingDir, fmt.Sprintf(".backup-%d", index)) + if err := copyOptionalDriverSupportFile(targetPath, snapshot.backupPath); err != nil { + return nil, err + } + } + snapshots = append(snapshots, snapshot) + } + return snapshots, nil +} + +func restoreOptionalDriverInstallTargets(snapshots []optionalDriverInstallSnapshot) error { + var restoreErrs []error + for index := len(snapshots) - 1; index >= 0; index-- { + snapshot := snapshots[index] + if !snapshot.existed { + if err := os.RemoveAll(snapshot.path); err != nil { + restoreErrs = append(restoreErrs, err) + } + continue + } + if snapshot.isDir { + if info, err := os.Stat(snapshot.path); err == nil && info.IsDir() { + continue + } + if err := os.RemoveAll(snapshot.path); err != nil { + restoreErrs = append(restoreErrs, err) + continue + } + if err := os.MkdirAll(snapshot.path, snapshot.mode.Perm()); err != nil { + restoreErrs = append(restoreErrs, err) + } + continue + } + if err := os.RemoveAll(snapshot.path); err != nil { + restoreErrs = append(restoreErrs, err) + continue + } + if err := copyOptionalDriverSupportFile(snapshot.backupPath, snapshot.path); err != nil { + restoreErrs = append(restoreErrs, err) + continue + } + if err := os.Chmod(snapshot.path, snapshot.mode.Perm()); err != nil && stdRuntime.GOOS != "windows" { + restoreErrs = append(restoreErrs, err) + } + } + return errors.Join(restoreErrs...) +} + +func promoteOptionalDriverAgentFromStaging(driverType string, stagingPath string, installPath string, runtimePath string, selectedVersion string) error { + targetPaths := optionalDriverInstallTargetPaths(driverType, installPath, runtimePath) + snapshots, err := snapshotOptionalDriverInstallTargets(filepath.Dir(stagingPath), targetPaths) + if err != nil { + return err + } + rollback := func(installErr error) error { + if restoreErr := restoreOptionalDriverInstallTargets(snapshots); restoreErr != nil { + return errors.Join(installErr, fmt.Errorf("restore previous driver installation: %w", restoreErr)) + } + return installErr + } + + if err := activateOptionalDriverAgentBinary(driverType, stagingPath, installPath); err != nil { + return rollback(err) + } + if err := activateOptionalDriverAgentBinary(driverType, installPath, runtimePath); err != nil { + return rollback(err) + } + if _, err := verifyInstalledOptionalDriverAgentRevision(driverType, runtimePath, selectedVersion); err != nil { + return rollback(err) + } + return nil +} + func installOptionalDriverAgentPackage(a *App, definition driverDefinition, selectedVersion string, resolvedDir string, downloadURL string) (installedDriverPackage, error) { driverType := normalizeDriverType(definition.Type) installPath, err := db.ResolveOptionalDriverAgentExecutablePathForVersion(resolvedDir, driverType, selectedVersion) @@ -3461,23 +3560,36 @@ func installOptionalDriverAgentPackage(a *App, definition driverDefinition, sele if err != nil { return installedDriverPackage{}, err } - downloadSource, hash, err := ensureOptionalDriverAgentBinary(a, definition, installPath, downloadURL, selectedVersion) + if err := os.MkdirAll(filepath.Dir(installPath), 0o755); err != nil { + return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.create_named_directory_failed", map[string]any{"name": resolveDriverDisplayName(definition)}, err) + } + stagingDir, err := os.MkdirTemp(filepath.Dir(installPath), ".gonavi-driver-install-*") + if err != nil { + return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.create_named_directory_failed", map[string]any{"name": resolveDriverDisplayName(definition)}, err) + } + defer os.RemoveAll(stagingDir) + stagingPath := filepath.Join(stagingDir, filepath.Base(installPath)) + + downloadSource, hash, err := ensureOptionalDriverAgentBinary(a, definition, stagingPath, downloadURL, selectedVersion) if err != nil { return installedDriverPackage{}, err } - if activateErr := activateOptionalDriverAgentBinary(driverType, installPath, runtimePath); activateErr != nil { - return installedDriverPackage{}, fmt.Errorf("activate %s driver agent failed: %w", resolveDriverDisplayName(definition), activateErr) + agentRevision, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, stagingPath, selectedVersion) + if revisionErr != nil { + return installedDriverPackage{}, revisionErr } if strings.TrimSpace(hash) == "" { - hash, err = hashFileSHA256(installPath) + hash, err = hashFileSHA256(stagingPath) if err != nil { return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.named_agent_hash_failed", map[string]any{"name": resolveDriverDisplayName(definition)}, err) } } + if activateErr := promoteOptionalDriverAgentFromStaging(driverType, stagingPath, installPath, runtimePath, selectedVersion); activateErr != nil { + return installedDriverPackage{}, fmt.Errorf("activate %s driver agent failed: %w", resolveDriverDisplayName(definition), activateErr) + } if strings.TrimSpace(downloadSource) == "" { downloadSource = strings.TrimSpace(downloadURL) } - agentRevision := observeInstalledOptionalDriverAgentRevision(driverType, runtimePath, selectedVersion) return installedDriverPackage{ DriverType: driverType, Version: strings.TrimSpace(selectedVersion), @@ -3506,13 +3618,23 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.read_local_package_failed", nil, statErr) } - executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(resolvedDir, driverType) + installPath, err := db.ResolveOptionalDriverAgentExecutablePathForVersion(resolvedDir, driverType, selectedVersion) if err != nil { return installedDriverPackage{}, err } - if mkErr := os.MkdirAll(filepath.Dir(executablePath), 0o755); mkErr != nil { + runtimePath, err := db.ResolveOptionalDriverAgentExecutablePath(resolvedDir, driverType) + if err != nil { + return installedDriverPackage{}, err + } + if mkErr := os.MkdirAll(filepath.Dir(installPath), 0o755); mkErr != nil { return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.create_named_directory_failed", map[string]any{"name": displayName}, mkErr) } + stagingDir, err := os.MkdirTemp(filepath.Dir(installPath), ".gonavi-driver-install-*") + if err != nil { + return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.create_named_directory_failed", map[string]any{"name": displayName}, err) + } + defer os.RemoveAll(stagingDir) + stagingPath := filepath.Join(stagingDir, filepath.Base(installPath)) sourcePath := pathText sourceName := filepath.Base(pathText) @@ -3531,7 +3653,7 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa } if !info.IsDir() && strings.EqualFold(filepath.Ext(pathText), ".zip") { - entryName, extractErr := installOptionalDriverAgentFromLocalZip(pathText, definition, executablePath, selectedVersion) + entryName, extractErr := installOptionalDriverAgentFromLocalZip(pathText, definition, stagingPath, selectedVersion) if extractErr != nil { return installedDriverPackage{}, extractErr } @@ -3539,29 +3661,35 @@ func installOptionalDriverAgentFromLocalPath(definition driverDefinition, filePa downloadSource = downloadSource + "#" + entryName } } else { - if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil { + if copyErr := copyAgentBinary(sourcePath, stagingPath); copyErr != nil { return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.import_local_agent_failed", nil, copyErr) } - if supportErr := copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(sourcePath), filepath.Dir(executablePath)); supportErr != nil { + if supportErr := copyOptionalDriverSupportFilesFromDirectory(driverType, filepath.Dir(sourcePath), stagingDir); supportErr != nil { return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.import_local_agent_runtime_failed", nil, supportErr) } } - if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, executablePath); validateErr != nil { + if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, stagingPath); validateErr != nil { return installedDriverPackage{}, validateErr } - agentRevision := observeInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion) - hash, hashErr := hashFileSHA256(executablePath) + agentRevision, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, stagingPath, selectedVersion) + if revisionErr != nil { + return installedDriverPackage{}, revisionErr + } + hash, hashErr := hashFileSHA256(stagingPath) if hashErr != nil { return installedDriverPackage{}, newLocalizedDriverBackendError("driver_manager.backend.error.named_agent_hash_failed", map[string]any{"name": displayName}, hashErr) } + if activateErr := promoteOptionalDriverAgentFromStaging(driverType, stagingPath, installPath, runtimePath, selectedVersion); activateErr != nil { + return installedDriverPackage{}, fmt.Errorf("activate %s driver agent failed: %w", displayName, activateErr) + } return installedDriverPackage{ DriverType: driverType, Version: strings.TrimSpace(selectedVersion), AgentRevision: agentRevision, FilePath: sourcePath, FileName: sourceName, - ExecutablePath: executablePath, + ExecutablePath: runtimePath, DownloadURL: downloadSource, SHA256: hash, DownloadedAt: time.Now().Format(time.RFC3339), @@ -3940,9 +4068,20 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut if a != nil { a.emitDriverDownloadProgress(driverType, "downloading", 10, 100, planMessage) } - observeInstalledCandidateRevision := func() { - observeInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion) + cleanupCandidate := func() { + _ = os.Remove(executablePath) + for _, supportName := range optionalDriverSupportFileNames(driverType) { + _ = os.Remove(filepath.Join(filepath.Dir(executablePath), supportName)) + } } + validateCandidateRevision := func() error { + if _, revisionErr := verifyInstalledOptionalDriverAgentRevision(driverType, executablePath, selectedVersion); revisionErr != nil { + cleanupCandidate() + return revisionErr + } + return nil + } + var downloadErrs []string if !skipReuseCandidate { if sourcePath, ok := findExistingOptionalDriverAgentCandidate(definition, executablePath); ok { if copyErr := copyAgentBinary(sourcePath, executablePath); copyErr != nil { @@ -3956,12 +4095,15 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut if hashErr != nil { return "", "", newLocalizedDriverBackendError("driver_manager.backend.error.bundled_agent_hash_failed", map[string]any{"name": displayName}, hashErr) } - observeInstalledCandidateRevision() - return "file://" + sourcePath, hash, nil + if revisionErr := validateCandidateRevision(); revisionErr != nil { + logger.Warnf("预置 %s 驱动代理 revision 校验失败,source=%s err=%v", displayName, sourcePath, revisionErr) + downloadErrs = appendOptionalDriverAttemptError(a, downloadErrs, "file://"+sourcePath, revisionErr) + } else { + return "file://" + sourcePath, hash, nil + } } } - var downloadErrs []string var sourceBuildAttempted bool var sourceBuildErr error @@ -3972,7 +4114,13 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut } hash, buildErr := buildOptionalDriverAgentFromSource(definition, executablePath, selectedVersion) if buildErr == nil { - return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil + if revisionErr := validateCandidateRevision(); revisionErr == nil { + return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil + } else { + buildErr = revisionErr + } + } else { + cleanupCandidate() } sourceBuildErr = buildErr if requireSourceBuildBeforeDownload { @@ -3991,7 +4139,11 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut } hash, dlErr := downloadOptionalDriverAgentBinary(a, definition, candidateURL, executablePath) if dlErr == nil { - observeInstalledCandidateRevision() + if revisionErr := validateCandidateRevision(); revisionErr != nil { + logger.Warnf("预编译 %s 驱动代理 revision 校验失败,url=%s err=%v", displayName, candidateURL, revisionErr) + downloadErrs = appendOptionalDriverAttemptError(a, downloadErrs, candidateURL, revisionErr) + continue + } return candidateURL, hash, nil } logger.Warnf("下载预编译 %s 驱动代理失败,url=%s err=%v", displayName, candidateURL, dlErr) @@ -4010,7 +4162,11 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut } source, hash, bundleErr := downloadOptionalDriverAgentFromBundle(a, definition, bundleURL, executablePath) if bundleErr == nil { - observeInstalledCandidateRevision() + if revisionErr := validateCandidateRevision(); revisionErr != nil { + logger.Warnf("驱动总包 %s 代理 revision 校验失败,source=%s err=%v", displayName, source, revisionErr) + downloadErrs = appendOptionalDriverAttemptError(a, downloadErrs, source, revisionErr) + continue + } return source, hash, nil } logger.Warnf("从驱动总包提取 %s 驱动代理失败,url=%s err=%v", displayName, bundleURL, bundleErr) @@ -4035,7 +4191,13 @@ func ensureOptionalDriverAgentBinary(a *App, definition driverDefinition, execut hash, runErr := buildOptionalDriverAgentFromSource(definition, executablePath, selectedVersion) buildErr = runErr if buildErr == nil { - return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil + if revisionErr := validateCandidateRevision(); revisionErr == nil { + return fmt.Sprintf("local://go-build/%s-driver-agent", driverType), hash, nil + } else { + buildErr = revisionErr + } + } else { + cleanupCandidate() } } diff --git a/internal/app/methods_driver_assets.go b/internal/app/methods_driver_assets.go index 9627e46b..9b2c99a8 100644 --- a/internal/app/methods_driver_assets.go +++ b/internal/app/methods_driver_assets.go @@ -613,7 +613,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe if validateErr := validateOptionalDriverAgentExecutableFunc(driverType, absPath); validateErr != nil { continue } - if !isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType, absPath) { + if !isReusableOptionalDriverAgentRevisionCurrent(driverType, absPath) { continue } return absPath, true @@ -621,7 +621,7 @@ func findExistingOptionalDriverAgentCandidate(definition driverDefinition, targe return "", false } -func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, executablePath string) bool { +func isReusableOptionalDriverAgentRevisionCurrent(driverType string, executablePath string) bool { expected := strings.TrimSpace(db.OptionalDriverAgentRevision(driverType)) if expected == "" { return true @@ -629,16 +629,12 @@ func isReusableOptionalDriverAgentCandidateRevisionAcceptable(driverType string, actual, current, err := optionalDriverAgentRevisionCurrent(driverType, executablePath) displayName := resolveDriverDisplayName(driverDefinition{Type: driverType}) if err != nil { - logger.Warnf("可复用 %s 驱动代理候选版本元数据不可用,仍允许安装:path=%s err=%v;建议在驱动管理中重装", displayName, executablePath, err) - return true + logger.Warnf("跳过可复用 %s 驱动代理候选:版本元数据不可用 path=%s err=%v", displayName, executablePath, err) + return false } if !current { - actualLabel := strings.TrimSpace(actual) - if actualLabel == "" { - actualLabel = "空" - } - logger.Warnf("可复用 %s 驱动代理候选 revision 不匹配,仍允许安装:path=%s actual=%s expected=%s;建议在驱动管理中重装", displayName, executablePath, actualLabel, expected) - return true + logger.Warnf("跳过可复用 %s 驱动代理候选:revision 不匹配 path=%s actual=%s expected=%s", displayName, executablePath, strings.TrimSpace(actual), expected) + return false } return true } diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go index 1280a0e4..f5f30039 100644 --- a/internal/app/methods_driver_version_test.go +++ b/internal/app/methods_driver_version_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -1774,21 +1775,55 @@ func TestDownloadOptionalDriverAgentFromBundleLocalizesInvalidBundleDetail(t *te } } -func TestInstallOptionalDriverAgentPackageAcceptsStaleDownloadRevision(t *testing.T) { +func TestDownloadDriverPackageRejectsStaleRevisionAndPreservesInstalledDriver(t *testing.T) { originalProbe := optionalDriverAgentMetadataProbe + originalValidate := validateOptionalDriverAgentExecutableFunc + originalLookPath := goBinaryLookPath + originalStat := goBinaryStat + originalCommandOutput := goBinaryCommandOutput t.Cleanup(func() { optionalDriverAgentMetadataProbe = originalProbe + validateOptionalDriverAgentExecutableFunc = originalValidate + goBinaryLookPath = originalLookPath + goBinaryStat = originalStat + goBinaryCommandOutput = originalCommandOutput }) tmpDir := t.TempDir() - staleAgent := filepath.Join(tmpDir, "stale-driver-agent") - if runtime.GOOS == "windows" { - staleAgent += ".exe" + driverRoot := filepath.Join(tmpDir, "drivers") + executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(driverRoot, "sqlserver") + if err != nil { + t.Fatalf("resolve installed driver path: %v", err) + } + if err := os.MkdirAll(filepath.Dir(executablePath), 0o755); err != nil { + t.Fatalf("create installed driver directory: %v", err) + } + previousBinary := []byte("previous-sqlserver-driver") + if err := os.WriteFile(executablePath, previousBinary, 0o755); err != nil { + t.Fatalf("write previous driver: %v", err) + } + previousMeta := installedDriverPackage{ + DriverType: "sqlserver", + Version: "1.9.6", + AgentRevision: db.OptionalDriverAgentRevision("sqlserver"), + FilePath: executablePath, + FileName: filepath.Base(executablePath), + ExecutablePath: executablePath, + DownloadURL: "https://example.test/previous-driver", + SHA256: "previous-sha256", + DownloadedAt: "2026-07-15T12:00:00+08:00", + } + if err := writeInstalledDriverPackage(driverRoot, "sqlserver", previousMeta); err != nil { + t.Fatalf("write previous driver metadata: %v", err) + } + metaPath := installedDriverMetaPath(driverRoot, "sqlserver") + previousMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read previous driver metadata: %v", err) } - writeSelfExecutable(t, staleAgent) staleServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, staleAgent) + _, _ = w.Write([]byte("stale-sqlserver-driver")) })) defer staleServer.Close() proxySnapshot := currentGlobalProxyConfig() @@ -1805,33 +1840,331 @@ func TestInstallOptionalDriverAgentPackageAcceptsStaleDownloadRevision(t *testin AgentRevision: "src-stale-agent", }, nil } + validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error { + return nil + } + goBinaryLookPath = func(file string) (string, error) { + return "", os.ErrNotExist + } + goBinaryStat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + goBinaryCommandOutput = func(cmd *exec.Cmd) ([]byte, error) { + return nil, os.ErrNotExist + } - meta, err := installOptionalDriverAgentPackage( - nil, - driverDefinition{Type: "sqlserver", Name: "SQL Server"}, - "1.9.6", - filepath.Join(tmpDir, "drivers"), - staleServer.URL, - ) + app := NewApp() + result := app.DownloadDriverPackage("sqlserver", "1.9.7", staleServer.URL, driverRoot) + if result.Success { + t.Fatal("expected stale driver reinstall to fail") + } + + installedBinary, err := os.ReadFile(executablePath) if err != nil { - t.Fatalf("expected stale direct download to be installed with an update hint, got %v", err) + t.Fatalf("read installed driver after failed reinstall: %v", err) } - if meta.DownloadURL != staleServer.URL { - t.Fatalf("expected direct download source to be preserved, got %q", meta.DownloadURL) + if string(installedBinary) != string(previousBinary) { + t.Fatalf("failed reinstall replaced the previous driver: got %q", string(installedBinary)) } - if meta.AgentRevision != "src-stale-agent" { - t.Fatalf("expected stale agent revision to be recorded, got %q", meta.AgentRevision) + installedMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read driver metadata after failed reinstall: %v", err) } - if _, err := os.Stat(meta.ExecutablePath); err != nil { - t.Fatalf("expected runtime executable to stay installed, got %v", err) + if string(installedMetaBytes) != string(previousMetaBytes) { + t.Fatalf("failed reinstall changed installed metadata:\n%s", string(installedMetaBytes)) } - needsUpdate, reason, expectedRevision := optionalDriverAgentRevisionStatus("sqlserver", meta, true) - if !needsUpdate { - t.Fatalf("expected stale installed revision to be surfaced as needsUpdate; expected=%q", expectedRevision) + assertNoDriverInstallStagingDirs(t, filepath.Dir(executablePath)) +} + +func TestInstallLocalDriverPackageRejectsStaleRevisionAndPreservesInstalledDriver(t *testing.T) { + originalProbe := optionalDriverAgentMetadataProbe + originalValidate := validateOptionalDriverAgentExecutableFunc + t.Cleanup(func() { + optionalDriverAgentMetadataProbe = originalProbe + validateOptionalDriverAgentExecutableFunc = originalValidate + }) + + tmpDir := t.TempDir() + driverRoot := filepath.Join(tmpDir, "drivers") + executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(driverRoot, "sqlserver") + if err != nil { + t.Fatalf("resolve installed driver path: %v", err) } - if !strings.Contains(reason, "强烈建议重装") { - t.Fatalf("expected advisory reinstall reason, got %q", reason) + if err := os.MkdirAll(filepath.Dir(executablePath), 0o755); err != nil { + t.Fatalf("create installed driver directory: %v", err) } + previousBinary := []byte("previous-local-sqlserver-driver") + if err := os.WriteFile(executablePath, previousBinary, 0o755); err != nil { + t.Fatalf("write previous driver: %v", err) + } + previousMeta := installedDriverPackage{ + DriverType: "sqlserver", + Version: "1.9.6", + AgentRevision: db.OptionalDriverAgentRevision("sqlserver"), + FilePath: executablePath, + FileName: filepath.Base(executablePath), + ExecutablePath: executablePath, + DownloadURL: "local://previous-driver", + SHA256: "previous-sha256", + DownloadedAt: "2026-07-15T12:00:00+08:00", + } + if err := writeInstalledDriverPackage(driverRoot, "sqlserver", previousMeta); err != nil { + t.Fatalf("write previous driver metadata: %v", err) + } + metaPath := installedDriverMetaPath(driverRoot, "sqlserver") + previousMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read previous driver metadata: %v", err) + } + stalePackage := filepath.Join(tmpDir, "stale-sqlserver-driver") + if runtime.GOOS == "windows" { + stalePackage += ".exe" + } + if err := os.WriteFile(stalePackage, []byte("stale-local-sqlserver-driver"), 0o755); err != nil { + t.Fatalf("write stale local driver package: %v", err) + } + validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error { + return nil + } + optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) { + return db.OptionalDriverAgentMetadata{ + DriverType: driverType, + AgentRevision: "src-stale-local-agent", + }, nil + } + + app := NewApp() + result := app.InstallLocalDriverPackage("sqlserver", stalePackage, driverRoot, "1.9.6") + if result.Success { + t.Fatal("expected stale local driver import to fail") + } + installedBinary, err := os.ReadFile(executablePath) + if err != nil { + t.Fatalf("read installed driver after failed local import: %v", err) + } + if string(installedBinary) != string(previousBinary) { + t.Fatalf("failed local import replaced the previous driver: got %q", string(installedBinary)) + } + installedMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read metadata after failed local import: %v", err) + } + if string(installedMetaBytes) != string(previousMetaBytes) { + t.Fatalf("failed local import changed installed metadata:\n%s", string(installedMetaBytes)) + } + assertNoDriverInstallStagingDirs(t, filepath.Dir(executablePath)) +} + +func TestDownloadDriverPackageFallsBackAfterStaleRevision(t *testing.T) { + originalProbe := optionalDriverAgentMetadataProbe + originalValidate := validateOptionalDriverAgentExecutableFunc + originalLookPath := goBinaryLookPath + t.Cleanup(func() { + optionalDriverAgentMetadataProbe = originalProbe + validateOptionalDriverAgentExecutableFunc = originalValidate + goBinaryLookPath = originalLookPath + }) + + tmpDir := t.TempDir() + staleServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("stale-kingbase-driver")) + })) + defer staleServer.Close() + proxySnapshot := currentGlobalProxyConfig() + if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil { + t.Fatalf("disable global proxy failed: %v", err) + } + t.Cleanup(func() { + _, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy) + }) + + projectRoot := filepath.Join(tmpDir, "project") + if err := os.MkdirAll(filepath.Join(projectRoot, "cmd", "optional-driver-agent"), 0o755); err != nil { + t.Fatalf("create project root: %v", err) + } + if err := os.WriteFile(filepath.Join(projectRoot, "go.mod"), []byte("module GoNavi-Wails\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } + if err := os.WriteFile(filepath.Join(projectRoot, "cmd", "optional-driver-agent", "main.go"), []byte("package main\n"), 0o644); err != nil { + t.Fatalf("write optional driver agent source: %v", err) + } + currentAgent := filepath.Join(tmpDir, "current-kingbase-driver") + if runtime.GOOS == "windows" { + currentAgent += ".exe" + } + if err := os.WriteFile(currentAgent, []byte("current-kingbase-driver"), 0o755); err != nil { + t.Fatalf("write current driver fixture: %v", err) + } + fakeGo := filepath.Join(tmpDir, "fake-go") + if runtime.GOOS == "windows" { + fakeGo += ".bat" + if err := os.WriteFile(fakeGo, []byte("@echo off\r\nsetlocal\r\nset \"out=\"\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif \"%~1\"==\"-o\" goto capture\r\nshift\r\ngoto loop\r\n:capture\r\nset \"out=%~2\"\r\nshift\r\nshift\r\ngoto loop\r\n:done\r\nif \"%out%\"==\"\" exit /b 1\r\ncopy /Y \"%GONAVI_TEST_BUILT_AGENT%\" \"%out%\" >nul\r\n"), 0o755); err != nil { + t.Fatalf("write fake go command: %v", err) + } + } else if err := os.WriteFile(fakeGo, []byte("#!/usr/bin/env sh\nout=\"\"\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = \"-o\" ]; then out=\"$2\"; shift 2; continue; fi\n shift\ndone\ncp \"$GONAVI_TEST_BUILT_AGENT\" \"$out\"\n"), 0o755); err != nil { + t.Fatalf("write fake go command: %v", err) + } + t.Setenv("GONAVI_TEST_BUILT_AGENT", currentAgent) + goBinaryLookPath = func(file string) (string, error) { + return fakeGo, nil + } + validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error { + return nil + } + optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) { + content, err := os.ReadFile(executablePath) + if err != nil { + return db.OptionalDriverAgentMetadata{}, err + } + revision := "src-stale-agent" + if string(content) == "current-kingbase-driver" { + revision = db.OptionalDriverAgentRevision(driverType) + } + return db.OptionalDriverAgentMetadata{DriverType: driverType, AgentRevision: revision}, nil + } + + workingDir, err := os.Getwd() + if err != nil { + t.Fatalf("get working directory: %v", err) + } + if err := os.Chdir(projectRoot); err != nil { + t.Fatalf("change to project root: %v", err) + } + t.Cleanup(func() { + if err := os.Chdir(workingDir); err != nil { + t.Fatalf("restore working directory: %v", err) + } + }) + + driverRoot := filepath.Join(tmpDir, "drivers") + app := NewApp() + result := app.DownloadDriverPackage("kingbase", "0.0.0-test", staleServer.URL, driverRoot) + if !result.Success { + t.Fatalf("expected current source fallback to install successfully, got %q", result.Message) + } + executablePath, err := db.ResolveOptionalDriverAgentExecutablePath(driverRoot, "kingbase") + if err != nil { + t.Fatalf("resolve installed driver path: %v", err) + } + installedBinary, err := os.ReadFile(executablePath) + if err != nil { + t.Fatalf("read installed fallback driver: %v", err) + } + if string(installedBinary) != "current-kingbase-driver" { + t.Fatalf("unexpected installed fallback driver: %q", string(installedBinary)) + } + pkg, ok := readInstalledDriverPackage(driverRoot, "kingbase") + if !ok { + t.Fatal("expected installed metadata after fallback") + } + if pkg.AgentRevision != db.OptionalDriverAgentRevision("kingbase") { + t.Fatalf("unexpected installed revision: %q", pkg.AgentRevision) + } + if pkg.DownloadURL != "local://go-build/kingbase-driver-agent" { + t.Fatalf("unexpected fallback source: %q", pkg.DownloadURL) + } + assertNoDriverInstallStagingDirs(t, filepath.Dir(executablePath)) +} + +func TestDownloadDriverPackageRollsBackWhenRuntimeActivationFails(t *testing.T) { + originalProbe := optionalDriverAgentMetadataProbe + originalValidate := validateOptionalDriverAgentExecutableFunc + t.Cleanup(func() { + optionalDriverAgentMetadataProbe = originalProbe + validateOptionalDriverAgentExecutableFunc = originalValidate + }) + + tmpDir := t.TempDir() + driverRoot := filepath.Join(tmpDir, "drivers") + installPath, err := db.ResolveOptionalDriverAgentExecutablePathForVersion(driverRoot, "mongodb", "2.99.0") + if err != nil { + t.Fatalf("resolve versioned driver path: %v", err) + } + runtimePath, err := db.ResolveOptionalDriverAgentExecutablePath(driverRoot, "mongodb") + if err != nil { + t.Fatalf("resolve runtime driver path: %v", err) + } + if err := os.MkdirAll(filepath.Dir(installPath), 0o755); err != nil { + t.Fatalf("create driver directory: %v", err) + } + previousBinary := []byte("previous-mongodb-driver") + if err := os.WriteFile(installPath, previousBinary, 0o755); err != nil { + t.Fatalf("write previous versioned driver: %v", err) + } + if err := os.MkdirAll(runtimePath, 0o755); err != nil { + t.Fatalf("create occupied runtime path: %v", err) + } + if err := os.WriteFile(filepath.Join(runtimePath, "keep"), []byte("occupied"), 0o644); err != nil { + t.Fatalf("occupy runtime path: %v", err) + } + previousMeta := installedDriverPackage{ + DriverType: "mongodb", + Version: "2.98.0", + AgentRevision: db.OptionalDriverAgentRevision("mongodb"), + FilePath: installPath, + FileName: filepath.Base(installPath), + ExecutablePath: installPath, + DownloadURL: "https://example.test/previous-mongodb-driver", + SHA256: "previous-sha256", + DownloadedAt: "2026-07-15T12:00:00+08:00", + } + if err := writeInstalledDriverPackage(driverRoot, "mongodb", previousMeta); err != nil { + t.Fatalf("write previous driver metadata: %v", err) + } + metaPath := installedDriverMetaPath(driverRoot, "mongodb") + previousMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read previous driver metadata: %v", err) + } + + currentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("current-mongodb-driver")) + })) + defer currentServer.Close() + proxySnapshot := currentGlobalProxyConfig() + if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil { + t.Fatalf("disable global proxy failed: %v", err) + } + t.Cleanup(func() { + _, _ = setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy) + }) + validateOptionalDriverAgentExecutableFunc = func(driverType string, executablePath string) error { + return nil + } + optionalDriverAgentMetadataProbe = func(driverType string, executablePath string) (db.OptionalDriverAgentMetadata, error) { + return db.OptionalDriverAgentMetadata{ + DriverType: driverType, + AgentRevision: db.OptionalDriverAgentRevision(driverType), + }, nil + } + + app := NewApp() + result := app.DownloadDriverPackage("mongodb", "2.99.0", currentServer.URL, driverRoot) + if result.Success { + t.Fatal("expected runtime activation failure") + } + installedBinary, err := os.ReadFile(installPath) + if err != nil { + t.Fatalf("read versioned driver after rollback: %v", err) + } + if string(installedBinary) != string(previousBinary) { + t.Fatalf("failed activation did not restore previous driver: got %q", string(installedBinary)) + } + installedMetaBytes, err := os.ReadFile(metaPath) + if err != nil { + t.Fatalf("read metadata after failed activation: %v", err) + } + if string(installedMetaBytes) != string(previousMetaBytes) { + t.Fatalf("failed activation changed installed metadata:\n%s", string(installedMetaBytes)) + } + occupiedMarker, err := os.ReadFile(filepath.Join(runtimePath, "keep")) + if err != nil { + t.Fatalf("read occupied runtime marker after rollback: %v", err) + } + if string(occupiedMarker) != "occupied" { + t.Fatalf("runtime marker changed after rollback: %q", string(occupiedMarker)) + } + assertNoDriverInstallStagingDirs(t, filepath.Dir(installPath)) } func seedReleaseAssetSizeCache(t *testing.T, cacheKey string, sizeByKey map[string]int64) { @@ -1965,6 +2298,20 @@ func chdirTemp(t *testing.T) { }) } +func assertNoDriverInstallStagingDirs(t *testing.T, driverDir string) { + t.Helper() + + entries, err := os.ReadDir(driverDir) + if err != nil { + t.Fatalf("read driver directory: %v", err) + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".gonavi-driver-install-") { + t.Fatalf("driver install staging directory was not cleaned up: %s", entry.Name()) + } + } +} + func mongoVersionedReleaseAssetName(major int) string { name := fmt.Sprintf("mongodb-driver-agent-v%d-%s-%s", major, runtime.GOOS, runtime.GOARCH) if runtime.GOOS == "windows" { diff --git a/tools/complete-driver-release-assets.py b/tools/complete-driver-release-assets.py index 005fd18c..d6303624 100644 --- a/tools/complete-driver-release-assets.py +++ b/tools/complete-driver-release-assets.py @@ -34,6 +34,7 @@ DRIVERS = [ "iotdb", "clickhouse", "elasticsearch", + "trino", ] BUNDLE_NAME = "GoNavi-DriverAgents.zip" diff --git a/tools/detect-changed-driver-agents.sh b/tools/detect-changed-driver-agents.sh index b74c9bc3..edc46075 100644 --- a/tools/detect-changed-driver-agents.sh +++ b/tools/detect-changed-driver-agents.sh @@ -7,7 +7,7 @@ cd "$SCRIPT_DIR" SCRIPT_DIR_WINDOWS="$(pwd -W 2>/dev/null || true)" SCRIPT_DIR_WINDOWS="${SCRIPT_DIR_WINDOWS//\\//}" -DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch) +DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino) TARGET_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64) usage() { @@ -54,7 +54,7 @@ normalize_driver() { open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elastic|elasticsearch) echo "elasticsearch" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$value" ;; *) @@ -165,6 +165,7 @@ driver_tokens_from_text() { case "$text" in *iotdb*|*apache-iotdb*|*apache_iotdb*) emit_driver_token iotdb ;; esac case "$text" in *clickhouse*) emit_driver_token clickhouse ;; esac case "$text" in *elasticsearch*) emit_driver_token elasticsearch ;; esac + case "$text" in *trino*) emit_driver_token trino ;; esac case "$text" in *github.com/go-sql-driver/mysql*) @@ -194,6 +195,7 @@ driver_tokens_from_text() { case "$text" in *github.com/apache/iotdb-client-go*) emit_driver_token iotdb ;; esac case "$text" in *github.com/clickhouse/clickhouse-go/v2*|*github.com/clickhouse/ch-go*) emit_driver_token clickhouse ;; esac case "$text" in *github.com/elastic/go-elasticsearch/v8*) emit_driver_token elasticsearch ;; esac + case "$text" in *github.com/trinodb/trino-go-client*) emit_driver_token trino ;; esac } emit_driver_token() { @@ -351,7 +353,7 @@ revision_file_changed_drivers() { is_ignored_driver_agent_source_file() { case "$1" in - *_test.go|frontend/*|internal/app/*|internal/appdata/*|internal/connection/*|internal/logger/*) + *_test.go|frontend/*|internal/app/*|internal/appdata/*|internal/connection/*|internal/logger/*|internal/db/driver_agent_revisions_gen.go) return 0 ;; esac @@ -372,12 +374,6 @@ attribute_source_file_change() { return 0 fi - tokens="$(source_file_driver_tokens "$file")" - if [[ -n "$tokens" ]]; then - add_forced_drivers_from_tokens "$tokens" - return 0 - fi - case "$file" in cmd/optional-driver-agent/*.go|internal/db/*.go) add_all_forced_drivers @@ -385,6 +381,12 @@ attribute_source_file_change() { ;; esac + tokens="$(source_file_driver_tokens "$file")" + if [[ -n "$tokens" ]]; then + add_forced_drivers_from_tokens "$tokens" + return 0 + fi + return 1 } diff --git a/tools/detect-changed-driver-agents.test.sh b/tools/detect-changed-driver-agents.test.sh index 8200e6d2..9cced3e0 100755 --- a/tools/detect-changed-driver-agents.test.sh +++ b/tools/detect-changed-driver-agents.test.sh @@ -13,6 +13,7 @@ cd "$SCRIPT_DIR" tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-driver-revisions.XXXXXX")" tmpdir_connection="" tmpdir_script="" +tmpdir_shared_source="" cleanup() { rm -rf "$tmpdir" if [[ -n "$tmpdir_connection" ]]; then @@ -21,6 +22,9 @@ cleanup() { if [[ -n "$tmpdir_script" ]]; then rm -rf "$tmpdir_script" fi + if [[ -n "$tmpdir_shared_source" ]]; then + rm -rf "$tmpdir_shared_source" + fi } trap cleanup EXIT @@ -54,6 +58,36 @@ GOEOF fi ) +tmpdir_shared_source="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-shared-source.XXXXXX")" +git init -q "$tmpdir_shared_source" +mkdir -p "$tmpdir_shared_source/tools" "$tmpdir_shared_source/internal/db" +cp tools/detect-changed-driver-agents.sh "$tmpdir_shared_source/tools/detect-changed-driver-agents.sh" +cat >"$tmpdir_shared_source/internal/db/database.go" <<'GOEOF' +package db + +func normalizeDriverType(driverType string) string { + return driverType +} +GOEOF + +( + cd "$tmpdir_shared_source" + git add . + git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial + base="$(git rev-parse HEAD)" + + perl -0pi -e 's/return driverType/if driverType == "sqlite3" { return "sqlite" }\n\treturn driverType/' internal/db/database.go + git add internal/db/database.go + git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m 'update shared database logic' + + actual="$(bash ./tools/detect-changed-driver-agents.sh --base "$base" --head HEAD)" + expected="mariadb,oceanbase,doris,starrocks,sphinx,sqlserver,sqlite,duckdb,dameng,kingbase,highgo,vastbase,opengauss,gaussdb,iris,mongodb,tdengine,iotdb,clickhouse,elasticsearch,trino" + if [[ "$actual" != "$expected" ]]; then + echo "expected shared internal/db source change to trigger all driver builds, got: ${actual:-}" >&2 + exit 1 + fi +) + tmpdir_connection="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-detect-connection-change.XXXXXX")" git init -q "$tmpdir_connection" mkdir -p "$tmpdir_connection/tools" "$tmpdir_connection/internal/connection" diff --git a/tools/diff-driver-agent-revisions.sh b/tools/diff-driver-agent-revisions.sh index 4fdb0d47..f9b495c0 100644 --- a/tools/diff-driver-agent-revisions.sh +++ b/tools/diff-driver-agent-revisions.sh @@ -5,7 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$SCRIPT_DIR" -DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch) +DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino) usage() { cat <<'EOF' @@ -38,7 +38,7 @@ normalize_driver_name() { value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" case "$value" in doris|diros) echo "diros" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|elasticsearch) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|elasticsearch|trino) echo "$value" ;; *) diff --git a/tools/diff-driver-agent-revisions.test.sh b/tools/diff-driver-agent-revisions.test.sh index e7cf7a40..0172594d 100644 --- a/tools/diff-driver-agent-revisions.test.sh +++ b/tools/diff-driver-agent-revisions.test.sh @@ -22,6 +22,7 @@ rsync -a --exclude .git ./ "$tmpdir/" >/dev/null ( cd "$tmpdir" git init -q + git add -f cmd/optional-driver-agent git add . git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial base="$(git rev-parse HEAD)" @@ -60,6 +61,7 @@ rsync -a --exclude .git ./ "$tmpdir_frontend/" >/dev/null ( cd "$tmpdir_frontend" git init -q + git add -f cmd/optional-driver-agent git add . git -c user.name=GoNavi -c user.email=gonavi@example.test commit -q -m initial base="$(git rev-parse HEAD)" diff --git a/tools/generate-driver-agent-revisions.sh b/tools/generate-driver-agent-revisions.sh index 5fb20c37..403535a6 100755 --- a/tools/generate-driver-agent-revisions.sh +++ b/tools/generate-driver-agent-revisions.sh @@ -272,7 +272,7 @@ fi fingerprint_driver() { local driver="$1" - local build_driver tag cgo_enabled tmp file identity file_hash revision + local build_driver tag cgo_enabled tmp dependency_files file identity file_hash revision build_driver="$(build_driver_name "$driver")" tag="$(driver_build_tags "$driver")" cgo_enabled=0 @@ -288,6 +288,17 @@ fingerprint_driver() { printf 'goarch=%s\n' "$goarch" } >"$tmp" + dependency_files="$(mktemp "${TMPDIR:-/tmp}/gonavi-agent-dependencies.XXXXXX")" + if ! CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \ + go list -deps \ + -tags "$tag" \ + -f '{{if not .Standard}}{{range .GoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .CgoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{end}}' \ + ./cmd/optional-driver-agent | sort -u >"$dependency_files"; then + rm -f "$tmp" "$dependency_files" + echo "driver-agent dependency enumeration failed: $driver ($goos/$goarch)" >&2 + return 1 + fi + while IFS= read -r file; do file="${file//\\//}" [[ -n "$file" && -f "$file" ]] || continue @@ -314,13 +325,8 @@ fingerprint_driver() { fi file_hash="$(hash_file "$file")" printf '%s %s\n' "$file_hash" "$identity" - done < <( - CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \ - go list -deps \ - -tags "$tag" \ - -f '{{if not .Standard}}{{range .GoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{range .CgoFiles}}{{$.Dir}}/{{.}}{{"\n"}}{{end}}{{end}}' \ - ./cmd/optional-driver-agent | sort -u - ) >>"$tmp" + done <"$dependency_files" >>"$tmp" + rm -f "$dependency_files" revision="$(hash_file "$tmp" | cut -c1-16)" rm -f "$tmp" diff --git a/tools/generate-driver-agent-revisions.test.sh b/tools/generate-driver-agent-revisions.test.sh index 379ef7db..78d01ab6 100644 --- a/tools/generate-driver-agent-revisions.test.sh +++ b/tools/generate-driver-agent-revisions.test.sh @@ -16,16 +16,53 @@ copy_repo_to_tmp() { git ls-files -z | tar --null -T - -cf - | (cd "$target" && tar -xf -) } +tmpdir_failure="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-generate-driver-revisions-failure.XXXXXX")" tmpdir_platform="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-generate-driver-revisions-platform.XXXXXX")" tmpdir_connection="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-generate-driver-revisions-connection.XXXXXX")" darwin_file="$(mktemp "${TMPDIR:-/tmp}/gonavi-darwin-revisions.XXXXXX")" windows_file="$(mktemp "${TMPDIR:-/tmp}/gonavi-windows-revisions.XXXXXX")" cleanup() { - rm -rf "$tmpdir_platform" "$tmpdir_connection" + rm -rf "$tmpdir_failure" "$tmpdir_platform" "$tmpdir_connection" rm -f "$darwin_file" "$windows_file" } trap cleanup EXIT +copy_repo_to_tmp "$tmpdir_failure" + +( + cd "$tmpdir_failure" + cp internal/db/driver_agent_revisions_gen.go driver_agent_revisions.before.go + mkdir -p fake-bin + cat >fake-bin/go <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == "list" ]]; then + printf '%s\n' "$PWD/cmd/optional-driver-agent/main.go" + exit 42 +fi + +exec "${REAL_GO:?}" "$@" +EOF + chmod +x fake-bin/go + + if REAL_GO="$(command -v go)" PATH="$PWD/fake-bin:$PATH" GONAVI_DRIVER_REVISION_JOBS=1 \ + bash ./tools/generate-driver-agent-revisions.sh --platform darwin/arm64 --drivers mariadb \ + >generator.stdout 2>generator.stderr; then + echo "expected revision generation to fail when go list returns a partial result" >&2 + exit 1 + fi + if ! cmp -s driver_agent_revisions.before.go internal/db/driver_agent_revisions_gen.go; then + echo "expected failed revision generation to preserve the existing revision file" >&2 + exit 1 + fi + if ! grep -Fq "driver-agent dependency enumeration failed: mariadb (darwin/arm64)" generator.stderr; then + echo "expected failed revision generation to report the dependency enumeration error" >&2 + cat generator.stderr >&2 + exit 1 + fi +) + copy_repo_to_tmp "$tmpdir_platform" ( diff --git a/tools/generate-driver-release-manifest.test.py b/tools/generate-driver-release-manifest.test.py index 4d6cae1d..35e39bd9 100644 --- a/tools/generate-driver-release-manifest.test.py +++ b/tools/generate-driver-release-manifest.test.py @@ -129,7 +129,7 @@ class GenerateDriverReleaseManifestTest(unittest.TestCase): check=True, ) - self.assertIn("asset count: 5", proc.stdout) + self.assertIn("asset count: 6", proc.stdout) manifest = json.loads(output.read_text(encoding="utf-8")) assets = manifest["assets"] darwin_revision_file = self._generate_revision_file("darwin/arm64") diff --git a/tools/verify-driver-agent-revisions.sh b/tools/verify-driver-agent-revisions.sh index 662932c6..0e82a998 100755 --- a/tools/verify-driver-agent-revisions.sh +++ b/tools/verify-driver-agent-revisions.sh @@ -71,7 +71,7 @@ normalize_driver() { opengauss|open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elasticsearch|elastic) echo "elasticsearch" ;; - mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$value" ;; *)