diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml
index d2f1ce436..5856431ff 100644
--- a/.github/workflows/cppcmake-windows.yml
+++ b/.github/workflows/cppcmake-windows.yml
@@ -6,6 +6,11 @@ on:
NIGHTLY:
default: false
type: boolean
+ secrets:
+ SIGNPATH_API_TOKEN:
+ required: false
+ SIGNPATH_ORGANIZATION_ID:
+ required: false
workflow_dispatch:
jobs:
@@ -21,6 +26,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
OPENSSL_VERSION: 1.1.1.2100
QT_VERSION: 5.15.2
+ SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN || '' }}
+ SIGNPATH_ORGANIZATION_ID: ${{ secrets.SIGNPATH_ORGANIZATION_ID || '' }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -141,7 +148,7 @@ jobs:
mv DB.Browser.for.SQLite-*.msi "DB.Browser.for.SQLite-dev-$(git rev-parse --short HEAD)-${{ matrix.arch }}.msi"
}
- - if: github.event_name != 'pull_request'
+ - if: github.event_name != 'pull_request' && env.SIGNPATH_API_TOKEN != '' && env.SIGNPATH_ORGANIZATION_ID != ''
name: Upload artifacts for code signing with SignPath
id: unsigned-artifacts
uses: actions/upload-artifact@v6
@@ -150,7 +157,7 @@ jobs:
path: installer\windows\DB.Browser.for.SQLite-*.msi
# Change the signing-policy-slug when you release an RC, RTM or stable release.
- - if: github.event_name != 'pull_request'
+ - if: github.event_name != 'pull_request' && env.SIGNPATH_API_TOKEN != '' && env.SIGNPATH_ORGANIZATION_ID != ''
name: Code signing with SignPath
uses: signpath/github-action-submit-signing-request@v2
with:
@@ -177,6 +184,16 @@ jobs:
} else {
move target\System64\* "target\DB Browser for SQLite\"
}
+ $simpleExtSource = Join-Path "${{ github.workspace }}" "release-sqlcipher\Release\extensions\simple.dll"
+ $simpleExtDestDir = "target\DB Browser for SQLite\extensions"
+ if (-not (Test-Path $simpleExtDestDir)) {
+ New-Item -ItemType Directory -Path $simpleExtDestDir | Out-Null
+ }
+ if (Test-Path $simpleExtSource) {
+ Copy-Item -Path $simpleExtSource -Destination $simpleExtDestDir -Force
+ } else {
+ Write-Host "simple.dll not found at $simpleExtSource"
+ }
Compress-Archive -Path "target\DB Browser for SQLite\*" -DestinationPath $FILENAME_FORMAT
- if: github.event_name != 'pull_request' && github.workflow != 'Build (Windows)'
diff --git a/.gitignore b/.gitignore
index 87518e350..37f51157c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@ sqlitebrowser.pro.user
.qmake.stash
CMakeLists.txt.user
CMakeFiles
-*.cmake
*.cxx_parameters
# ignore any build folders
@@ -11,6 +10,23 @@ build*/
# folder with temporary test data
testdata/
+# CMake (in-source build artifacts)
+CMakeCache.txt
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+CTestTestfile.cmake
+Testing/
+DartConfiguration.tcl
+_CPack_Packages/
+CPackConfig.cmake
+CPackSourceConfig.cmake
+
+# Ninja
+.ninja_*
+build.ninja
+rules.ninja
+
src/.ui/
src/sqlitebrowser
src/Makefile*
@@ -35,5 +51,28 @@ libs/*/*/release/
libs/*/*.a
libs/*/*/*.a
+# IDEs / editors
+.idea/
+.vscode/
+.vs/
+*.swp
+*.swo
+
+# Qt Creator
+*.pro.user*
+*.qbs.user*
+*.qtc_clangd/
+
+# Local/vendor worktrees
+simple-master/
+
+# Python
+__pycache__/
+*.pyc
+.pytest_cache/
+
# Ignore .DS_Store files on OSX
.DS_Store
+*.dSYM/
+*.dmg
+*.pkg
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..aa0f66746
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,35 @@
+# AGENTS.md
+
+## Project
+DB Browser for SQLite is a C++ (C++14+) / Qt application built with CMake.
+
+## Build (out-of-source)
+Prefer an out-of-source build directory.
+
+```sh
+cmake -S . -B build
+cmake --build build
+```
+
+Resulting binaries are typically under `build/src/` (e.g. `build/src/sqlitebrowser`).
+
+## Unit tests
+Unit tests live in `src/tests` and are enabled via `ENABLE_TESTING`.
+
+```sh
+cmake -S . -B build-test -DENABLE_TESTING=ON
+cmake --build build-test
+ctest --test-dir build-test -V
+```
+
+## Common CMake options
+- `-DENABLE_TESTING=ON`: build unit tests
+- `-Dsqlcipher=1`: build with SQLCipher support (if dependencies are available)
+- `-DFORCE_INTERNAL_QSCINTILLA=ON`: use bundled QScintilla if system packages cause issues
+
+## Repo conventions (for agents)
+- Keep diffs minimal and focused; avoid drive-by refactors and mass reformatting.
+- Prefer modifying `src/` over vendored code in `libs/` unless explicitly required.
+- For `.ui` files, prefer Qt Designer edits; avoid hand-reformatting generated XML.
+- Don’t update `src/translations/*.ts` unless the change is explicitly about translations.
+- When changing build behavior, also update `BUILDING.md` (and `README.md` when user-facing).
diff --git a/BUILDING.md b/BUILDING.md
index d8805a9e3..eca235ed9 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -158,6 +158,15 @@ cmake --build .
mv DB\ Browser\ for\ SQLite.app /Applications
```
+If you see “may be damaged or incomplete” when launching the app, check that
+`/Applications/DB Browser for SQLite.app/Contents/Info.plist` has a
+`CFBundleExecutable` that matches the file in `Contents/MacOS/`. If the app was
+copied from another machine and got quarantined, clear it with:
+
+```bash
+xattr -dr com.apple.quarantine /Applications/DB\ Browser\ for\ SQLite.app
+```
+
> If you want to build universal binary, change the `cmake` command to
> `cmake -DcustomTap=1 -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" ..`
> Of course, this requires you to have an Apple Silicon Mac.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1a17f534a..02393341a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
project(sqlitebrowser
VERSION 3.13.99
DESCRIPTION "GUI editor for SQLite databases"
- LANGUAGES CXX
+ LANGUAGES C CXX
)
include(GNUInstallDirs)
@@ -60,6 +60,11 @@ endif()
include(config/platform.cmake)
+if(APPLE)
+ set_source_files_properties(src/macapp.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
+ target_sources(${PROJECT_NAME} PRIVATE src/macapp.icns)
+endif()
+
find_package(${QT_MAJOR} REQUIRED COMPONENTS Concurrent Gui LinguistTools Network PrintSupport Test Widgets Xml)
set(QT_LIBS
${QT_MAJOR}::Gui
@@ -94,6 +99,79 @@ else()
set(LIBSQLITE_NAME SQLite::SQLite3)
endif()
+if(APPLE OR WIN32)
+ set(SIMPLE_TOKENIZER_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/libs/simple_tokenizer/simple_extension.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/libs/simple_tokenizer/simple_tokenizer.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/libs/simple_tokenizer/jieba_query.c
+ )
+
+ add_library(simple_tokenizer MODULE ${SIMPLE_TOKENIZER_SOURCES})
+ # This is a plain SQLite extension (C code). Don't run Qt's automoc/uic/rcc on it.
+ set_target_properties(simple_tokenizer PROPERTIES
+ AUTOMOC OFF
+ AUTOUIC OFF
+ AUTORCC OFF
+ )
+ set(SIMPLE_TOKENIZER_INCLUDE_DIRS)
+ if(DEFINED SQLite3_INCLUDE_DIRS)
+ list(APPEND SIMPLE_TOKENIZER_INCLUDE_DIRS ${SQLite3_INCLUDE_DIRS})
+ endif()
+ if(sqlcipher)
+ list(APPEND SIMPLE_TOKENIZER_INCLUDE_DIRS ${SQLCIPHER_INCLUDE_DIR} ${SQLCIPHER_INCLUDE_DIR}/sqlcipher)
+ endif()
+
+ target_include_directories(simple_tokenizer PRIVATE ${SIMPLE_TOKENIZER_INCLUDE_DIRS})
+ target_link_libraries(simple_tokenizer PRIVATE ${LIBSQLITE_NAME})
+ target_compile_definitions(simple_tokenizer PRIVATE SQLITE_ENABLE_FTS5 SQLITE_CORE)
+
+ if(APPLE)
+ # Ensure the entry point symbols are exported even with aggressive dead-stripping,
+ # otherwise sqlite3_load_extension() cannot find them via dlsym().
+ target_link_options(simple_tokenizer PRIVATE
+ "-Wl,-exported_symbol,_sqlite3_extension_init"
+ "-Wl,-exported_symbol,_sqlite3_simple_init"
+ )
+
+ set(SIMPLE_EXT_OUTPUT_DIR "${CMAKE_BINARY_DIR}/extensions")
+ set_target_properties(simple_tokenizer PROPERTIES
+ PREFIX ""
+ OUTPUT_NAME "simple"
+ LIBRARY_OUTPUT_DIRECTORY "${SIMPLE_EXT_OUTPUT_DIR}"
+ )
+ else()
+ set(SIMPLE_EXT_OUTPUT_DIR "${CMAKE_BINARY_DIR}/$/extensions")
+ set_target_properties(simple_tokenizer PROPERTIES
+ PREFIX ""
+ OUTPUT_NAME "simple"
+ LIBRARY_OUTPUT_DIRECTORY "${SIMPLE_EXT_OUTPUT_DIR}"
+ RUNTIME_OUTPUT_DIRECTORY "${SIMPLE_EXT_OUTPUT_DIR}"
+ )
+ foreach(CONFIG_NAME IN ITEMS Release Debug RelWithDebInfo MinSizeRel)
+ set_property(TARGET simple_tokenizer PROPERTY LIBRARY_OUTPUT_DIRECTORY_${CONFIG_NAME} "${CMAKE_BINARY_DIR}/${CONFIG_NAME}/extensions")
+ set_property(TARGET simple_tokenizer PROPERTY RUNTIME_OUTPUT_DIRECTORY_${CONFIG_NAME} "${CMAKE_BINARY_DIR}/${CONFIG_NAME}/extensions")
+ endforeach()
+ endif()
+
+ install(TARGETS simple_tokenizer
+ LIBRARY DESTINATION Extensions
+ RUNTIME DESTINATION extensions
+ )
+
+ # Make local macOS builds usable without running installer scripts:
+ # copy the built tokenizer extension into the app bundle so it can be auto-loaded.
+ if(APPLE)
+ add_dependencies(${PROJECT_NAME} simple_tokenizer)
+ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E make_directory "$/Contents/Extensions"
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "$"
+ "$/Contents/Extensions/simple.dylib"
+ VERBATIM
+ )
+ endif()
+endif()
+
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/version.h
)
diff --git a/config/install.cmake b/config/install.cmake
index 5c538cf92..9c7d8cdb5 100644
--- a/config/install.cmake
+++ b/config/install.cmake
@@ -5,6 +5,18 @@ if(NOT WIN32 AND NOT APPLE)
)
endif()
+if(APPLE)
+ if(TARGET simple_tokenizer)
+ install(FILES $ DESTINATION Extensions)
+ endif()
+endif()
+
+if(WIN32)
+ if(TARGET simple_tokenizer)
+ install(FILES $ DESTINATION extensions)
+ endif()
+endif()
+
if(UNIX)
install(FILES src/icons/${PROJECT_NAME}.png
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps/
diff --git a/config/platform_apple.cmake b/config/platform_apple.cmake
index bd96ba1ef..a441c81a3 100644
--- a/config/platform_apple.cmake
+++ b/config/platform_apple.cmake
@@ -27,5 +27,6 @@ endif()
set_target_properties(${PROJECT_NAME} PROPERTIES
BUNDLE True
OUTPUT_NAME "DB Browser for SQLite"
+ MACOSX_BUNDLE_ICON_FILE "macapp.icns"
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/src/app.plist
)
diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh
index 04d8d0f56..00937f0d1 100644
--- a/installer/macos/notarize.sh
+++ b/installer/macos/notarize.sh
@@ -1,27 +1,48 @@
#!/usr/bin/env bash
-# Create a new keychain
-CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
-KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
-echo -n "$P12" | base64 --decode -o $CERTIFICATE_PATH
-security create-keychain -p "$KEYCHAIN_PW" $KEYCHAIN_PATH
-security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
-security unlock-keychain -p "$KEYCHAIN_PW" $KEYCHAIN_PATH
-security import $CERTIFICATE_PATH -P "$P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
-security list-keychain -d user -s $KEYCHAIN_PATH
+
+SIGNING_READY=true
+for VAR in P12 P12_PW KEYCHAIN_PW DEV_ID APPLE_ID APPLE_PW TEAM_ID; do
+ if [[ -z "${!VAR}" ]]; then
+ SIGNING_READY=false
+ fi
+done
+
+if [[ "$SIGNING_READY" == "false" ]]; then
+ echo "Signing credentials are missing; building unsigned DMG without notarization."
+fi
+
+# Create a new keychain when signing is available
+if [[ "$SIGNING_READY" == "true" ]]; then
+ CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ echo -n "$P12" | base64 --decode -o $CERTIFICATE_PATH
+ security create-keychain -p "$KEYCHAIN_PW" $KEYCHAIN_PATH
+ security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
+ security unlock-keychain -p "$KEYCHAIN_PW" $KEYCHAIN_PATH
+ security import $CERTIFICATE_PATH -P "$P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
+ security list-keychain -d user -s $KEYCHAIN_PATH
+fi
# Run macdeployqt
-find build -name "DB Browser for SQL*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} -sign-for-notarization=$DEV_ID \;
+if [[ "$SIGNING_READY" == "true" ]]; then
+ find build -maxdepth 1 -name "*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} -sign-for-notarization=$DEV_ID \;
+else
+ find build -maxdepth 1 -name "*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} \;
+fi
# Add the 'formats' and 'nalgeon/sqlean' extensions to the app bundle
-gh auth login --with-token <<< "$GH_TOKEN"
-gh release download --pattern "sqlean-macos-x86.zip" --repo "nalgeon/sqlean"
-unzip sqlean-macos-x86.zip -d sqlean-macos-x86
-gh release download --pattern "sqlean-macos-arm64.zip" --repo "nalgeon/sqlean"
-unzip sqlean-macos-arm64.zip -d sqlean-macos-arm64
-lipo -create sqlean-macos-x86/sqlean.dylib sqlean-macos-arm64/sqlean.dylib -output sqlean.dylib
-for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); do
- TARGET=$(echo $TARGET | sed -e 's/_/ /g')
- mkdir "$TARGET/Contents/Extensions"
+if [[ -n "$GH_TOKEN" ]]; then
+ gh auth login --with-token <<< "$GH_TOKEN"
+ gh release download --pattern "sqlean-macos-x86.zip" --repo "nalgeon/sqlean"
+ unzip sqlean-macos-x86.zip -d sqlean-macos-x86
+ gh release download --pattern "sqlean-macos-arm64.zip" --repo "nalgeon/sqlean"
+ unzip sqlean-macos-arm64.zip -d sqlean-macos-arm64
+ lipo -create sqlean-macos-x86/sqlean.dylib sqlean-macos-arm64/sqlean.dylib -output sqlean.dylib
+else
+ echo "GH_TOKEN not provided; skipping sqlean download."
+fi
+while IFS= read -r -d '' TARGET; do
+ mkdir -p "$TARGET/Contents/Extensions"
arch -x86_64 clang -I /opt/homebrew/opt/sqlb-sqlite/include -L /opt/homebrew/opt/sqlb-sqlite/lib -fno-common -dynamiclib src/extensions/extension-formats.c -o formats_x86_64.dylib
clang -I /opt/homebrew/opt/sqlb-sqlite/include -L /opt/homebrew/opt/sqlb-sqlite/lib -fno-common -dynamiclib src/extensions/extension-formats.c -o formats_arm64.dylib
@@ -32,23 +53,29 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g');
ln -s formats.dylib "$TARGET/Contents/Extensions/formats.dylib.dylib"
fi
- cp sqlean.dylib "$TARGET/Contents/Extensions/"
- if [ -f "$TARGET/Contents/Extensions/sqlean.dylib" ]; then
- install_name_tool -id "@executable_path/../Extensions/sqlean.dylib" "$TARGET/Contents/Extensions/sqlean.dylib"
- ln -s sqlean.dylib "$TARGET/Contents/Extensions/sqlean.dylib.dylib"
+ if [ -f sqlean.dylib ]; then
+ cp sqlean.dylib "$TARGET/Contents/Extensions/"
+ if [ -f "$TARGET/Contents/Extensions/sqlean.dylib" ]; then
+ install_name_tool -id "@executable_path/../Extensions/sqlean.dylib" "$TARGET/Contents/Extensions/sqlean.dylib"
+ ln -s sqlean.dylib "$TARGET/Contents/Extensions/sqlean.dylib.dylib"
+ fi
fi
-done
+
+ if [ -f "build/extensions/simple.dylib" ]; then
+ cp build/extensions/simple.dylib "$TARGET/Contents/Extensions/"
+ install_name_tool -id "@executable_path/../Extensions/simple.dylib" "$TARGET/Contents/Extensions/simple.dylib"
+ ln -s simple.dylib "$TARGET/Contents/Extensions/simple.dylib.dylib"
+ fi
+done < <(find build -maxdepth 1 -name "*.app" -print0)
# Copy the license file to the app bundle
-for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); do
- TARGET=$(echo $TARGET | sed -e 's/_/ /g')
+while IFS= read -r -d '' TARGET; do
cp LICENSE* "$TARGET/Contents/Resources/"
-done
+done < <(find build -maxdepth 1 -name "*.app" -print0)
# Copy the translation files to the app bundle
-for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); do
- TARGET=$(echo $TARGET | sed -e 's/_/ /g')
- mkdir "$TARGET/Contents/translations"
+while IFS= read -r -d '' TARGET; do
+ mkdir -p "$TARGET/Contents/translations"
for i in ar cs de en es fr it ko pl pt pt_BR ru uk zh_CN zh_TW; do
find $(brew --prefix sqlb-qt@5)/translations -name "qt_${i}.qm" 2> /dev/null -exec cp {} "$TARGET/Contents/translations/" \;
find $(brew --prefix sqlb-qt@5)/translations -name "qtbase_${i}.qm" 2> /dev/null -exec cp {} "$TARGET/Contents/translations/" \;
@@ -56,11 +83,10 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g');
find $(brew --prefix sqlb-qt@5)/translations -name "qtscript_${i}.qm" 2> /dev/null -exec cp {} "$TARGET/Contents/translations/" \;
find $(brew --prefix sqlb-qt@5)/translations -name "qtxmlpatterns_${i}.qm" 2> /dev/null -exec cp {} "$TARGET/Contents/translations/" \;
done
-done
+done < <(find build -maxdepth 1 -name "*.app" -print0)
# Copy the icon file to the app bundle
-for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); do
- TARGET=$(echo $TARGET | sed -e 's/_/ /g')
+while IFS= read -r -d '' TARGET; do
if [ "$NIGHTLY" = "false" ]; then
cp installer/macos/macapp.icns "$TARGET/Contents/Resources/"
/usr/libexec/PlistBuddy -c "Set :CFBundleIconFile macapp.icns" "$TARGET/Contents/Info.plist"
@@ -68,15 +94,21 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g');
cp installer/macos/macapp-nightly.icns "$TARGET/Contents/Resources/"
/usr/libexec/PlistBuddy -c "Set :CFBundleIconFile macapp-nightly.icns" "$TARGET/Contents/Info.plist"
fi
-done
+done < <(find build -maxdepth 1 -name "*.app" -print0)
# Sign the manually added extensions
-for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); do
- TARGET=$(echo $TARGET | sed -e 's/_/ /g')
- codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/formats.dylib"
- codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/sqlean.dylib"
- codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET"
-done
+while IFS= read -r -d '' TARGET; do
+ if [[ "$SIGNING_READY" == "true" ]]; then
+ codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/formats.dylib"
+ codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/sqlean.dylib"
+ if [ -f "$TARGET/Contents/Extensions/simple.dylib" ]; then
+ codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/simple.dylib"
+ fi
+ codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET"
+ else
+ echo "Skipping codesign for $TARGET (credentials unavailable)."
+ fi
+done < <(find build -maxdepth 1 -name "*.app" -print0)
# Move app bundle to installer folder for DMG creation
mv build/*.app installer/macos
@@ -106,11 +138,15 @@ else
appdmg --quiet installer/macos/nightly.json "$TARGET"
fi
-codesign --sign "$DEV_ID" --verbose --options=runtime --timestamp "$TARGET"
-codesign -vvv --deep --strict --verbose=4 "$TARGET"
+if [[ "$SIGNING_READY" == "true" ]]; then
+ codesign --sign "$DEV_ID" --verbose --options=runtime --timestamp "$TARGET"
+ codesign -vvv --deep --strict --verbose=4 "$TARGET"
-# Notarize the dmg
-xcrun notarytool submit *.dmg --apple-id $APPLE_ID --password $APPLE_PW --team-id $TEAM_ID --wait
+ # Notarize the dmg
+ xcrun notarytool submit *.dmg --apple-id $APPLE_ID --password $APPLE_PW --team-id $TEAM_ID --wait
-# Staple the notarization ticket
-xcrun stapler staple *.dmg
+ # Staple the notarization ticket
+ xcrun stapler staple *.dmg
+else
+ echo "Skipping signing/notarization for DMG (credentials unavailable)."
+fi
diff --git a/installer/windows/product.wxs b/installer/windows/product.wxs
index 385895a81..f60b4fa35 100644
--- a/installer/windows/product.wxs
+++ b/installer/windows/product.wxs
@@ -63,6 +63,7 @@
+
@@ -147,14 +148,17 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/installer/windows/variables.wxi b/installer/windows/variables.wxi
index 8fc77959a..5dbda56ba 100644
--- a/installer/windows/variables.wxi
+++ b/installer/windows/variables.wxi
@@ -56,5 +56,12 @@
+
+
diff --git a/libs/simple_tokenizer/jieba_query.c b/libs/simple_tokenizer/jieba_query.c
new file mode 100644
index 000000000..5e1d63380
--- /dev/null
+++ b/libs/simple_tokenizer/jieba_query.c
@@ -0,0 +1,17 @@
+#include "simple_tokenizer.h"
+
+#ifndef SQLITE_CORE
+#define SQLITE_CORE 1
+#endif
+
+#ifndef SQLITE_ENABLE_FTS5
+#define SQLITE_ENABLE_FTS5 1
+#endif
+
+int simpleRegisterJiebaModes(fts5_api* pApi)
+{
+ int rc = simpleRegisterTokenizer(pApi, "jieba", SIMPLE_TOKEN_MODE_JIEBA);
+ if(rc == SQLITE_OK)
+ rc = simpleRegisterTokenizer(pApi, "jieba_query", SIMPLE_TOKEN_MODE_JIEBA_QUERY);
+ return rc;
+}
diff --git a/libs/simple_tokenizer/simple_extension.c b/libs/simple_tokenizer/simple_extension.c
new file mode 100644
index 000000000..a4b6a322f
--- /dev/null
+++ b/libs/simple_tokenizer/simple_extension.c
@@ -0,0 +1,68 @@
+#include "simple_tokenizer.h"
+
+#include
+#include
+
+#ifndef SQLITE_CORE
+#define SQLITE_CORE 1
+#endif
+
+#ifndef SQLITE_ENABLE_FTS5
+#define SQLITE_ENABLE_FTS5 1
+#endif
+
+SQLITE_EXTENSION_INIT1
+
+#if defined(_WIN32)
+#define SQLB_EXT_EXPORT __declspec(dllexport)
+#elif defined(__GNUC__)
+#define SQLB_EXT_EXPORT __attribute__((visibility("default")))
+#else
+#define SQLB_EXT_EXPORT
+#endif
+
+#if defined(__APPLE__)
+#define SQLB_NO_DEAD_STRIP __attribute__((used))
+#else
+#define SQLB_NO_DEAD_STRIP
+#endif
+
+static int fts5ApiFromDb(sqlite3* db, fts5_api** ppApi)
+{
+ sqlite3_stmt* stmt = NULL;
+ *ppApi = NULL;
+
+ int rc = sqlite3_prepare_v2(db, "SELECT fts5(?1)", -1, &stmt, NULL);
+ if(rc != SQLITE_OK)
+ return rc;
+
+ sqlite3_bind_pointer(stmt, 1, (void*)ppApi, "fts5_api_ptr", NULL);
+ (void)sqlite3_step(stmt);
+ rc = sqlite3_finalize(stmt);
+ return rc;
+}
+
+SQLB_EXT_EXPORT SQLB_NO_DEAD_STRIP int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi)
+{
+ SQLITE_EXTENSION_INIT2(pApi);
+
+ fts5_api* api = NULL;
+ const int rc = fts5ApiFromDb(db, &api);
+ if(rc != SQLITE_OK)
+ return rc;
+
+ if(!api)
+ {
+ if(pzErrMsg)
+ *pzErrMsg = sqlite3_mprintf("FTS5 is not available in this SQLite build");
+ return SQLITE_ERROR;
+ }
+
+ return simpleRegisterTokenizers(api);
+}
+
+// Default entry point used by sqlite3_load_extension when no entry symbol is specified.
+SQLB_EXT_EXPORT SQLB_NO_DEAD_STRIP int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi)
+{
+ return sqlite3_simple_init(db, pzErrMsg, pApi);
+}
diff --git a/libs/simple_tokenizer/simple_tokenizer.c b/libs/simple_tokenizer/simple_tokenizer.c
new file mode 100644
index 000000000..e4d7e1272
--- /dev/null
+++ b/libs/simple_tokenizer/simple_tokenizer.c
@@ -0,0 +1,175 @@
+#include "simple_tokenizer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef SQLITE_CORE
+#define SQLITE_CORE 1
+#endif
+
+#ifndef SQLITE_ENABLE_FTS5
+#define SQLITE_ENABLE_FTS5 1
+#endif
+
+struct SimpleTokenizer {
+ SimpleTokenizerMode mode;
+};
+
+typedef struct SimpleTokenizer SimpleTokenizer;
+
+static int isSpaceOrControl(unsigned char c)
+{
+ return isspace((int)c) || iscntrl((int)c);
+}
+
+static int isAsciiAlpha(unsigned char c)
+{
+ return (c < 0x80) && isalpha((int)c);
+}
+
+static int isAsciiDigit(unsigned char c)
+{
+ return (c < 0x80) && isdigit((int)c);
+}
+
+static int utf8CharLen(unsigned char c)
+{
+ if(c < 0x80)
+ return 1;
+ if((c & 0xE0) == 0xC0)
+ return 2;
+ if((c & 0xF0) == 0xE0)
+ return 3;
+ if((c & 0xF8) == 0xF0)
+ return 4;
+ // Invalid UTF-8 lead byte (or continuation byte). Treat as a single byte.
+ return 1;
+}
+
+static int simpleCreate(void* pCtx, const char** azArg, int nArg, Fts5Tokenizer** ppOut)
+{
+ (void)azArg;
+ (void)nArg;
+
+ SimpleTokenizer* p = (SimpleTokenizer*)sqlite3_malloc(sizeof(SimpleTokenizer));
+ if(!p)
+ return SQLITE_NOMEM;
+
+ p->mode = (SimpleTokenizerMode)(intptr_t)pCtx;
+ *ppOut = (Fts5Tokenizer*)p;
+
+ return SQLITE_OK;
+}
+
+static void simpleDelete(Fts5Tokenizer* pTok)
+{
+ sqlite3_free(pTok);
+}
+
+static int simpleTokenize(Fts5Tokenizer* pTok, void* pCtx, int flags, const char* pText, int nText,
+ int (*xToken)(void*, int, const char*, int, int, int))
+{
+ SimpleTokenizer* p = (SimpleTokenizer*)pTok;
+ int tokenFlags = 0;
+
+ if(p->mode == SIMPLE_TOKEN_MODE_JIEBA_QUERY && (flags & FTS5_TOKENIZE_QUERY))
+ tokenFlags |= FTS5_TOKEN_COLOCATED;
+
+ // This aims to be compatible with the tokenizer used by https://github.com/wangfenjin/simple:
+ // - ASCII alphabetic runs are grouped and lowercased
+ // - ASCII digit runs are grouped
+ // - Non-ASCII UTF-8 characters are tokenized per codepoint (not per byte)
+ // - ASCII punctuation is tokenized as single-character tokens
+ int i = 0;
+ while(i < nText)
+ {
+ unsigned char c = (unsigned char)pText[i];
+
+ if(isSpaceOrControl(c))
+ {
+ i++;
+ continue;
+ }
+
+ int start = i;
+ int end = i;
+
+ if(isAsciiAlpha(c))
+ {
+ end++;
+ while(end < nText && isAsciiAlpha((unsigned char)pText[end]))
+ end++;
+
+ const int nTok = end - start;
+ char* token = (char*)sqlite3_malloc((size_t)nTok + 1);
+ if(!token)
+ return SQLITE_NOMEM;
+ for(int j = 0; j < nTok; j++)
+ token[j] = (char)tolower((unsigned char)pText[start + j]);
+ token[nTok] = '\0';
+
+ const int rc = xToken(pCtx, tokenFlags, token, nTok, start, end);
+ sqlite3_free(token);
+ if(rc != SQLITE_OK)
+ return rc;
+ i = end;
+ continue;
+ }
+
+ if(isAsciiDigit(c))
+ {
+ end++;
+ while(end < nText && isAsciiDigit((unsigned char)pText[end]))
+ end++;
+ const int rc = xToken(pCtx, tokenFlags, &pText[start], end - start, start, end);
+ if(rc != SQLITE_OK)
+ return rc;
+ i = end;
+ continue;
+ }
+
+ if(c < 0x80)
+ {
+ // ASCII non-space, non-alnum: tokenized as a single character.
+ end = start + 1;
+ const int rc = xToken(pCtx, tokenFlags, &pText[start], 1, start, end);
+ if(rc != SQLITE_OK)
+ return rc;
+ i = end;
+ continue;
+ }
+
+ // Non-ASCII: tokenize per UTF-8 codepoint.
+ const int len = utf8CharLen(c);
+ end = start + len;
+ if(end > nText)
+ end = nText;
+
+ const int rc = xToken(pCtx, tokenFlags, &pText[start], end - start, start, end);
+ if(rc != SQLITE_OK)
+ return rc;
+ i = end;
+ }
+
+ return SQLITE_OK;
+}
+
+int simpleRegisterTokenizer(fts5_api* pApi, const char* zName, SimpleTokenizerMode mode)
+{
+ static fts5_tokenizer tokenizer = { simpleCreate, simpleDelete, simpleTokenize };
+ return pApi->xCreateTokenizer(pApi, zName, (void*)(intptr_t)mode, &tokenizer, NULL);
+}
+
+int simpleRegisterTokenizers(fts5_api* pApi)
+{
+ int rc = simpleRegisterTokenizer(pApi, "simple", SIMPLE_TOKEN_MODE_BASIC);
+
+ if(rc == SQLITE_OK)
+ rc = simpleRegisterJiebaModes(pApi);
+
+ return rc;
+}
diff --git a/libs/simple_tokenizer/simple_tokenizer.h b/libs/simple_tokenizer/simple_tokenizer.h
new file mode 100644
index 000000000..bea3aac2b
--- /dev/null
+++ b/libs/simple_tokenizer/simple_tokenizer.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#ifndef SQLITE_ENABLE_FTS5
+#define SQLITE_ENABLE_FTS5 1
+#endif
+
+#ifndef SQLITE_CORE
+#define SQLITE_CORE 1
+#endif
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum SimpleTokenizerMode {
+ SIMPLE_TOKEN_MODE_BASIC = 0,
+ SIMPLE_TOKEN_MODE_JIEBA = 1,
+ SIMPLE_TOKEN_MODE_JIEBA_QUERY = 2
+} SimpleTokenizerMode;
+
+int simpleRegisterTokenizer(fts5_api* pApi, const char* zName, SimpleTokenizerMode mode);
+int simpleRegisterTokenizers(fts5_api* pApi);
+int simpleRegisterJiebaModes(fts5_api* pApi);
+
+// Extension entry points (exported symbols)
+int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi);
+int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/Settings.cpp b/src/Settings.cpp
index e9e3482df..0134bebe2 100644
--- a/src/Settings.cpp
+++ b/src/Settings.cpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -406,6 +407,10 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string&
if(group == "extensions" && name == "list")
return QStringList();
+ // extensions/builtin?
+ if(group == "extensions" && name == "builtin")
+ return defaultBuiltinExtensions();
+
// extensions/disableregex?
if(group == "extension" && name == "disableregex")
return false;
@@ -455,6 +460,24 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string&
return QVariant();
}
+QVariantMap Settings::defaultBuiltinExtensions()
+{
+ QVariantMap builtinExtensions;
+
+#ifdef Q_OS_MAC
+ const QString simpleExt = qApp->applicationDirPath() + "/../Extensions/simple.dylib";
+ if(QFile::exists(simpleExt))
+ builtinExtensions.insert(simpleExt, true);
+#endif
+#ifdef Q_OS_WIN
+ const QString simpleExt = qApp->applicationDirPath() + "/extensions/simple.dll";
+ if(QFile::exists(simpleExt))
+ builtinExtensions.insert(simpleExt, true);
+#endif
+
+ return builtinExtensions;
+}
+
QColor Settings::getDefaultColorValue(const std::string& group, const std::string& name, AppStyle style)
{
// Data Browser/NULL & Binary Fields
diff --git a/src/Settings.h b/src/Settings.h
index 5fca0a613..45e550648 100644
--- a/src/Settings.h
+++ b/src/Settings.h
@@ -27,6 +27,8 @@ class Settings
static bool importSettings(const QString& fileName);
static void sync();
+ static QVariantMap defaultBuiltinExtensions();
+
private:
Settings() = delete; // class is fully static
diff --git a/src/app.plist b/src/app.plist
index e8a024297..b456a9179 100644
--- a/src/app.plist
+++ b/src/app.plist
@@ -53,11 +53,11 @@
CFBundleExecutable
- @EXECUTABLE@
+ @MACOSX_BUNDLE_EXECUTABLE_NAME@
CFBundleGetInfoString
3.13.99
CFBundleIconFile
- @ICON@
+ @MACOSX_BUNDLE_ICON_FILE@
CFBundleIdentifier
net.sourceforge.sqlitebrowser
CFBundleInfoDictionaryVersion
diff --git a/src/sqlitedb.cpp b/src/sqlitedb.cpp
index 6e50c858d..7365fe342 100644
--- a/src/sqlitedb.cpp
+++ b/src/sqlitedb.cpp
@@ -2182,7 +2182,14 @@ void DBBrowserDB::loadExtensionsFromSettings()
QMessageBox::warning(nullptr, QApplication::applicationName(), tr("Error loading extension: %1").arg(lastError()));
}
- const QVariantMap builtinList = Settings::getValue("extensions", "builtin").toMap();
+ QVariantMap builtinList = Settings::getValue("extensions", "builtin").toMap();
+ const QVariantMap defaultBuiltins = Settings::defaultBuiltinExtensions();
+ for(const QString& ext : defaultBuiltins.keys())
+ {
+ if(!builtinList.contains(ext))
+ builtinList.insert(ext, defaultBuiltins.value(ext));
+ }
+
for(const QString& ext : builtinList.keys())
{
if(builtinList.value(ext).toBool())