diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1180a62..01ae239 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,26 +22,56 @@ jobs: os_name: MacOS arch_name: Amd64 build_name: gonavi-build-darwin-amd64 + wails_tags: "" + artifact_suffix: "" + build_optional_agents: true + linux_webkit: "" - os: macos-latest platform: darwin/arm64 os_name: MacOS arch_name: Arm64 build_name: gonavi-build-darwin-arm64 + wails_tags: "" + artifact_suffix: "" + build_optional_agents: true + linux_webkit: "" - os: windows-latest platform: windows/amd64 os_name: Windows arch_name: Amd64 build_name: gonavi-build-windows-amd64 + wails_tags: "" + artifact_suffix: "" + build_optional_agents: true + linux_webkit: "" - os: windows-latest platform: windows/arm64 os_name: Windows arch_name: Arm64 build_name: gonavi-build-windows-arm64 + wails_tags: "" + artifact_suffix: "" + build_optional_agents: true + linux_webkit: "" - os: ubuntu-22.04 platform: linux/amd64 os_name: Linux arch_name: Amd64 build_name: gonavi-build-linux-amd64 + wails_tags: "" + artifact_suffix: "" + build_optional_agents: true + linux_webkit: "4.0" + # Debian 13 (trixie) 默认仓库已切到 WebKitGTK 4.1:单独提供 4.1 变体产物 + - os: ubuntu-24.04 + platform: linux/amd64 + os_name: Linux + arch_name: Amd64 + build_name: gonavi-build-linux-amd64-webkit41 + wails_tags: "webkit2_41" + artifact_suffix: "-WebKit41" + build_optional_agents: false + linux_webkit: "4.1" steps: - name: Checkout code @@ -63,7 +93,17 @@ jobs: if: contains(matrix.platform, 'linux') run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libfuse2 + sudo apt-get install -y libgtk-3-dev + + # WebKitGTK 4.1 需要 libsoup3;4.0 使用 libsoup2(通常由 webkit2gtk dev 包拉起) + if [ "${{ matrix.linux_webkit }}" = "4.1" ]; then + sudo apt-get install -y libwebkit2gtk-4.1-dev libsoup-3.0-dev + else + sudo apt-get install -y libwebkit2gtk-4.0-dev + fi + + # AppImage 运行/打包可能需要 FUSE2。不同发行版/版本包名不同,做兼容兜底。 + sudo apt-get install -y libfuse2 || sudo apt-get install -y libfuse2t64 || true # Download linuxdeploy tools for AppImage packaging LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" @@ -94,9 +134,15 @@ jobs: - name: Build shell: bash run: | - wails build -platform ${{ matrix.platform }} -clean -o ${{ matrix.build_name }} -ldflags "-s -w -X GoNavi-Wails/internal/app.AppVersion=${{ github.ref_name }}" + set -euo pipefail + TAG_ARGS=() + if [ -n "${{ matrix.wails_tags }}" ]; then + TAG_ARGS+=(-tags "${{ matrix.wails_tags }}") + fi + wails build -platform ${{ matrix.platform }} -clean -o ${{ matrix.build_name }} "${TAG_ARGS[@]}" -ldflags "-s -w -X GoNavi-Wails/internal/app.AppVersion=${{ github.ref_name }}" - name: Build Optional Driver Agents + if: ${{ matrix.build_optional_agents }} shell: bash run: | set -euo pipefail @@ -160,7 +206,7 @@ jobs: codesign --force --options runtime --deep --sign - "$APP_NAME" DMG_NAME="${{ matrix.build_name }}.dmg" - FINAL_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}.dmg" + FINAL_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.dmg" echo "📦 正在生成 DMG: $DMG_NAME..." create-dmg \ @@ -187,8 +233,8 @@ jobs: $version = $version.Substring(1) } $target = "${{ matrix.build_name }}" - $finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}.exe" - $finalZipName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}.zip" + $finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe" + $finalZipName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.zip" if (Test-Path "$target.exe") { $finalExe = "$target.exe" @@ -214,8 +260,8 @@ jobs: VERSION="${VERSION#v}" cd build/bin TARGET="${{ matrix.build_name }}" - TAR_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}.tar.gz" - APPIMAGE_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}.AppImage" + TAR_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.tar.gz" + APPIMAGE_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.AppImage" if [ ! -f "$TARGET" ]; then echo "❌ 未找到构建产物 '$TARGET'!" diff --git a/README.md b/README.md index 2596349..d773de9 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ wails build -clean 支持构建: * macOS (AMD64 / ARM64) * Windows (AMD64) +* Linux (AMD64,提供 WebKitGTK 4.0 与 4.1 变体产物) --- @@ -146,6 +147,27 @@ wails build -clean ``` 4. 或者:在 Finder 中右键点击应用图标,按住 `Control` 键选择 **打开**,然后在弹出的窗口中再次点击 **打开**。 +### Linux 启动报错缺少 `libwebkit2gtk` / `libjavascriptcoregtk` + +GoNavi 的 Linux 二进制依赖系统 WebKitGTK 运行库。不同发行版默认版本不同: + +- Debian 13 / Ubuntu 24.04 及更新版本:通常为 WebKitGTK 4.1 +- Ubuntu 22.04 / Debian 12 等:通常为 WebKitGTK 4.0 + +如果启动时报错(如 `libwebkit2gtk-4.0.so.37: cannot open shared object file`),请按系统安装对应依赖后重试: + +```bash +# Debian 13 / Ubuntu 24.04+ +sudo apt-get update +sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.1-0 libjavascriptcoregtk-4.1-0 + +# Ubuntu 22.04 / Debian 12 +sudo apt-get update +sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18 +``` + +如果你使用的是 Release 中带 `-WebKit41` 后缀的 Linux 产物,请优先在 Debian 13 / Ubuntu 24.04+ 上使用;普通 Linux 产物更适合 WebKitGTK 4.0 运行环境。 + --- ## 🤝 贡献指南 diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 5d8b382..47587f6 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -465,6 +465,7 @@ func (a *App) DBGetTables(config connection.ConnectionConfig, dbName string) con func (a *App) DBShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult { runConfig := normalizeRunConfig(config, dbName) + dbType := resolveDDLDBType(config) dbInst, err := a.getDatabase(runConfig) if err != nil { @@ -478,10 +479,120 @@ func (a *App) DBShowCreateTable(config connection.ConnectionConfig, dbName strin logger.Error(err, "DBShowCreateTable 获取建表语句失败:%s 表=%s", formatConnSummary(runConfig), tableName) return connection.QueryResult{Success: false, Message: err.Error()} } + if shouldFallbackCreateStatement(dbType, sqlStr) { + columns, colErr := dbInst.GetColumns(schemaName, pureTableName) + if colErr != nil { + logger.Error(colErr, "DBShowCreateTable 兜底加载字段失败:%s 表=%s", formatConnSummary(runConfig), tableName) + return connection.QueryResult{Success: false, Message: colErr.Error()} + } + fallbackDDL, buildErr := buildFallbackCreateStatement(dbType, schemaName, pureTableName, columns) + if buildErr != nil { + logger.Error(buildErr, "DBShowCreateTable 兜底生成 DDL 失败:%s 表=%s", formatConnSummary(runConfig), tableName) + return connection.QueryResult{Success: false, Message: buildErr.Error()} + } + sqlStr = fallbackDDL + } return connection.QueryResult{Success: true, Data: sqlStr} } +func shouldFallbackCreateStatement(dbType string, ddl string) bool { + switch dbType { + case "postgres", "kingbase", "highgo", "vastbase": + default: + return false + } + + trimmed := strings.TrimSpace(ddl) + if trimmed == "" { + return true + } + if hasCreateTableHead(trimmed) { + return false + } + + lower := strings.ToLower(trimmed) + if strings.Contains(lower, "not fully supported") || + strings.Contains(lower, "not directly supported") || + strings.Contains(lower, "not supported") { + return true + } + return true +} + +func hasCreateTableHead(sqlText string) bool { + lines := strings.Split(sqlText, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.HasPrefix(line, "--") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") { + continue + } + return strings.HasPrefix(strings.ToLower(line), "create table") + } + return false +} + +func buildFallbackCreateStatement(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition) (string, error) { + table := strings.TrimSpace(tableName) + if table == "" { + return "", fmt.Errorf("表名不能为空") + } + if len(columns) == 0 { + return "", fmt.Errorf("未获取到字段定义,无法生成建表语句") + } + + qualifiedTable := quoteTableIdentByType(dbType, schemaName, table) + columnLines := make([]string, 0, len(columns)+1) + primaryKeys := make([]string, 0, 2) + + for _, col := range columns { + colNameRaw := strings.TrimSpace(col.Name) + if colNameRaw == "" { + continue + } + colType := strings.TrimSpace(col.Type) + if colType == "" { + colType = "text" + } + + colName := quoteIdentByType(dbType, colNameRaw) + defParts := []string{fmt.Sprintf("%s %s", colName, colType)} + + if strings.EqualFold(strings.TrimSpace(col.Nullable), "NO") { + defParts = append(defParts, "NOT NULL") + } + if col.Default != nil { + defVal := strings.TrimSpace(*col.Default) + if defVal != "" { + defParts = append(defParts, "DEFAULT "+defVal) + } + } + + columnLines = append(columnLines, " "+strings.Join(defParts, " ")) + if strings.EqualFold(strings.TrimSpace(col.Key), "PRI") { + primaryKeys = append(primaryKeys, colName) + } + } + + if len(columnLines) == 0 { + return "", fmt.Errorf("字段定义为空,无法生成建表语句") + } + if len(primaryKeys) > 0 { + columnLines = append(columnLines, " PRIMARY KEY ("+strings.Join(primaryKeys, ", ")+")") + } + + ddl := strings.Builder{} + ddl.WriteString("CREATE TABLE ") + ddl.WriteString(qualifiedTable) + ddl.WriteString(" (\n") + ddl.WriteString(strings.Join(columnLines, ",\n")) + ddl.WriteString("\n);") + return ddl.String(), nil +} + func (a *App) DBGetColumns(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult { runConfig := normalizeRunConfig(config, dbName) diff --git a/internal/db/agent_process_stub.go b/internal/db/agent_process_stub.go new file mode 100644 index 0000000..0b0e3b2 --- /dev/null +++ b/internal/db/agent_process_stub.go @@ -0,0 +1,9 @@ +//go:build !windows + +package db + +import "os/exec" + +func configureAgentProcess(cmd *exec.Cmd) { + _ = cmd +} diff --git a/internal/db/agent_process_windows.go b/internal/db/agent_process_windows.go new file mode 100644 index 0000000..c268037 --- /dev/null +++ b/internal/db/agent_process_windows.go @@ -0,0 +1,20 @@ +//go:build windows + +package db + +import ( + "os/exec" + "syscall" +) + +const windowsCreateNoWindow = 0x08000000 + +func configureAgentProcess(cmd *exec.Cmd) { + if cmd == nil { + return + } + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: windowsCreateNoWindow, + } +} diff --git a/internal/db/mysql_agent_impl.go b/internal/db/mysql_agent_impl.go index c5b53f6..3ad540d 100644 --- a/internal/db/mysql_agent_impl.go +++ b/internal/db/mysql_agent_impl.go @@ -67,11 +67,16 @@ func newMySQLAgentClient(executablePath string) (*mysqlAgentClient, error) { if pathText == "" { return nil, fmt.Errorf("MySQL 驱动代理路径为空") } - if info, err := os.Stat(pathText); err != nil || info.IsDir() { + info, err := os.Stat(pathText) + if err != nil { return nil, fmt.Errorf("MySQL 驱动代理不存在:%s", pathText) } + if info.IsDir() { + return nil, fmt.Errorf("MySQL 驱动代理路径是目录:%s", pathText) + } cmd := exec.Command(pathText) + configureAgentProcess(cmd) stdin, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("创建 MySQL 驱动代理 stdin 失败:%w", err) diff --git a/internal/db/optional_driver_agent_impl.go b/internal/db/optional_driver_agent_impl.go index 18f88ce..cde793a 100644 --- a/internal/db/optional_driver_agent_impl.go +++ b/internal/db/optional_driver_agent_impl.go @@ -68,11 +68,16 @@ func newOptionalDriverAgentClient(driverType string, executablePath string) (*op if pathText == "" { return nil, fmt.Errorf("%s 驱动代理路径为空", driverDisplayName(driverType)) } - if info, err := os.Stat(pathText); err != nil || info.IsDir() { + info, err := os.Stat(pathText) + if err != nil { return nil, fmt.Errorf("%s 驱动代理不存在:%s", driverDisplayName(driverType), pathText) } + if info.IsDir() { + return nil, fmt.Errorf("%s 驱动代理路径是目录:%s", driverDisplayName(driverType), pathText) + } cmd := exec.Command(pathText) + configureAgentProcess(cmd) stdin, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("创建 %s 驱动代理 stdin 失败:%w", driverDisplayName(driverType), err)