From 8ef37ba1a388c5d6809bf83df0a64afe6400815e Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 09:28:28 +0800 Subject: [PATCH 01/18] Add Windows packaging for simple tokenizer extension --- CMakeLists.txt | 47 +++++++++++++ config/install.cmake | 12 ++++ installer/macos/notarize.sh | 9 +++ installer/windows/product.wxs | 18 +++-- installer/windows/variables.wxi | 1 + libs/simple_tokenizer/jieba_query.c | 18 +++++ libs/simple_tokenizer/simple_extension.c | 30 +++++++++ libs/simple_tokenizer/simple_tokenizer.c | 86 ++++++++++++++++++++++++ libs/simple_tokenizer/simple_tokenizer.h | 31 +++++++++ src/Settings.cpp | 19 ++++++ 10 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 libs/simple_tokenizer/jieba_query.c create mode 100644 libs/simple_tokenizer/simple_extension.c create mode 100644 libs/simple_tokenizer/simple_tokenizer.c create mode 100644 libs/simple_tokenizer/simple_tokenizer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a17f534a..dbe2b77ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,53 @@ 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}) + 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) + 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 + ) +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/installer/macos/notarize.sh b/installer/macos/notarize.sh index 04d8d0f56..1e93a28db 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -37,6 +37,12 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); 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 + + 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 # Copy the license file to the app bundle @@ -75,6 +81,9 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); 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" + 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" done 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..22f2c702a 100644 --- a/installer/windows/variables.wxi +++ b/installer/windows/variables.wxi @@ -56,5 +56,6 @@ + diff --git a/libs/simple_tokenizer/jieba_query.c b/libs/simple_tokenizer/jieba_query.c new file mode 100644 index 000000000..42c41bddd --- /dev/null +++ b/libs/simple_tokenizer/jieba_query.c @@ -0,0 +1,18 @@ +#include "simple_tokenizer.h" + +#ifndef SQLITE_CORE +#define SQLITE_CORE 1 +#endif + +#ifndef SQLITE_ENABLE_FTS5 +#define SQLITE_ENABLE_FTS5 1 +#endif + +int simpleRegisterJiebaModes(const 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..d293e43dd --- /dev/null +++ b/libs/simple_tokenizer/simple_extension.c @@ -0,0 +1,30 @@ +#include "simple_tokenizer.h" + +#include + +#ifndef SQLITE_CORE +#define SQLITE_CORE 1 +#endif + +#ifndef SQLITE_ENABLE_FTS5 +#define SQLITE_ENABLE_FTS5 1 +#endif + +SQLITE_EXTENSION_INIT1 + +int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) +{ + SQLITE_EXTENSION_INIT2(pApi); + + fts5_api* api = (fts5_api*)sqlite3_fts5_api_from_db(db); + + if(!api) + { + if(pzErrMsg) + *pzErrMsg = sqlite3_mprintf("FTS5 is not available in this SQLite build"); + return SQLITE_ERROR; + } + + return simpleRegisterJiebaModes(api); +} + diff --git a/libs/simple_tokenizer/simple_tokenizer.c b/libs/simple_tokenizer/simple_tokenizer.c new file mode 100644 index 000000000..a5a8dd6aa --- /dev/null +++ b/libs/simple_tokenizer/simple_tokenizer.c @@ -0,0 +1,86 @@ +#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 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 isTokenChar(int c) +{ + return isalnum(c) || (c & 0x80); +} + +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 i = 0; + int start = -1; + int tokenFlags = 0; + + if(p->mode == SIMPLE_TOKEN_MODE_JIEBA_QUERY && (flags & FTS5_TOKENIZE_QUERY)) + tokenFlags |= FTS5_TOKEN_COLOCATED; + + while(i <= nText) + { + unsigned char c = (unsigned char)(i < nText ? pText[i] : ' '); + + if(start >= 0 && !isTokenChar(c)) + { + int rc = xToken(pCtx, tokenFlags, &pText[start], i - start, start, i); + if(rc != SQLITE_OK) + return rc; + start = -1; + } + else if(start < 0 && isTokenChar(c)) + { + start = i; + } + i++; + } + + return SQLITE_OK; +} + +int simpleRegisterTokenizer(const fts5_api* pApi, const char* zName, SimpleTokenizerMode mode) +{ + static const fts5_tokenizer tokenizer = { simpleCreate, simpleDelete, simpleTokenize }; + return pApi->xCreateTokenizer(pApi, zName, (void*)(intptr_t)mode, &tokenizer, nullptr); +} + diff --git a/libs/simple_tokenizer/simple_tokenizer.h b/libs/simple_tokenizer/simple_tokenizer.h new file mode 100644 index 000000000..9c7a707ba --- /dev/null +++ b/libs/simple_tokenizer/simple_tokenizer.h @@ -0,0 +1,31 @@ +#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(const fts5_api* pApi, const char* zName, SimpleTokenizerMode mode); + +#ifdef __cplusplus +} +#endif + +int simpleRegisterJiebaModes(const fts5_api* pApi); +int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi); + diff --git a/src/Settings.cpp b/src/Settings.cpp index e9e3482df..74728adec 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -406,6 +407,24 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& if(group == "extensions" && name == "list") return QStringList(); + // extensions/builtin? + if(group == "extensions" && name == "builtin") + { + 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; + } + // extensions/disableregex? if(group == "extension" && name == "disableregex") return false; From 2e0102fe4db9ce120d5d20e5473945995ee6dcc5 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 11:40:29 +0800 Subject: [PATCH 02/18] Handle missing macOS signing credentials --- installer/macos/notarize.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh index 1e93a28db..aa60438c9 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -1,4 +1,11 @@ #!/usr/bin/env bash + +# Skip notarization when required secrets are not provided (e.g. CI without signing credentials) +if [[ -z "$P12" || -z "$P12_PW" || -z "$KEYCHAIN_PW" || -z "$DEV_ID" || -z "$APPLE_ID" || -z "$APPLE_PW" || -z "$TEAM_ID" || -z "$GH_TOKEN" ]]; then + echo "Notarization credentials are missing; skipping macOS signing/notarization steps." + exit 0 +fi + # Create a new keychain CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db From ebac3da105440b9027c8c78365c9bc56c42f8f35 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 12:45:41 +0800 Subject: [PATCH 03/18] Build unsigned macOS DMG when signing credentials missing --- installer/macos/notarize.sh | 95 +++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh index aa60438c9..2260c80b7 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -1,31 +1,46 @@ #!/usr/bin/env bash -# Skip notarization when required secrets are not provided (e.g. CI without signing credentials) -if [[ -z "$P12" || -z "$P12_PW" || -z "$KEYCHAIN_PW" || -z "$DEV_ID" || -z "$APPLE_ID" || -z "$APPLE_PW" || -z "$TEAM_ID" || -z "$GH_TOKEN" ]]; then - echo "Notarization credentials are missing; skipping macOS signing/notarization steps." - exit 0 +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 -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 +# 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 -name "DB Browser for SQL*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} -sign-for-notarization=$DEV_ID \; +else + find build -name "DB Browser for SQL*.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 +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 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" @@ -39,10 +54,12 @@ 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 if [ -f "build/extensions/simple.dylib" ]; then @@ -86,12 +103,16 @@ done # 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" - if [ -f "$TARGET/Contents/Extensions/simple.dylib" ]; then - codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET/Contents/Extensions/simple.dylib" + 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 - codesign --sign "$DEV_ID" --deep --force --options=runtime --strict --timestamp "$TARGET" done # Move app bundle to installer folder for DMG creation @@ -122,11 +143,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 From 164d9592ec7a0f33df512b30f84f46cf07683676 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 12:53:32 +0800 Subject: [PATCH 04/18] Guard Windows SignPath signing when secrets missing --- .github/workflows/cppcmake-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml index d2f1ce436..5bbfdfc0d 100644 --- a/.github/workflows/cppcmake-windows.yml +++ b/.github/workflows/cppcmake-windows.yml @@ -141,7 +141,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' && secrets.SIGNPATH_API_TOKEN != '' && secrets.SIGNPATH_ORGANIZATION_ID != '' name: Upload artifacts for code signing with SignPath id: unsigned-artifacts uses: actions/upload-artifact@v6 @@ -150,7 +150,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' && secrets.SIGNPATH_API_TOKEN != '' && secrets.SIGNPATH_ORGANIZATION_ID != '' name: Code signing with SignPath uses: signpath/github-action-submit-signing-request@v2 with: From d1d17df8bb54b0db1084f61fc3de3b20518985fd Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 13:01:06 +0800 Subject: [PATCH 05/18] Declare SignPath secrets for reusable workflow --- .github/workflows/cppcmake-windows.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml index 5bbfdfc0d..591551647 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: From b33171f72fc0b2d154fd9f7d01b959e146dc6f20 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 13:02:58 +0800 Subject: [PATCH 06/18] Fix SignPath secret conditions in Windows workflow --- .github/workflows/cppcmake-windows.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml index 591551647..9da238d6d 100644 --- a/.github/workflows/cppcmake-windows.yml +++ b/.github/workflows/cppcmake-windows.yml @@ -26,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 @@ -146,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' && secrets.SIGNPATH_API_TOKEN != '' && secrets.SIGNPATH_ORGANIZATION_ID != '' + - 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 @@ -155,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' && secrets.SIGNPATH_API_TOKEN != '' && secrets.SIGNPATH_ORGANIZATION_ID != '' + - 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: From 8830c5c19271741fe55d402fd1f429603b0b2cfc Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 13:37:00 +0800 Subject: [PATCH 07/18] Add sqlite3_extension_init for simple tokenizer --- libs/simple_tokenizer/simple_extension.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/simple_tokenizer/simple_extension.c b/libs/simple_tokenizer/simple_extension.c index d293e43dd..4cd034965 100644 --- a/libs/simple_tokenizer/simple_extension.c +++ b/libs/simple_tokenizer/simple_extension.c @@ -28,3 +28,9 @@ int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines return simpleRegisterJiebaModes(api); } +// Default entry point used by sqlite3_load_extension when no entry symbol is specified. +int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) +{ + return sqlite3_simple_init(db, pzErrMsg, pApi); +} + From a3cdfd61446d504f0445b10e9b79d39dbdd828d8 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 14:09:31 +0800 Subject: [PATCH 08/18] Register simple tokenizer for FTS5 --- libs/simple_tokenizer/simple_extension.c | 2 +- libs/simple_tokenizer/simple_tokenizer.c | 10 ++++++++++ libs/simple_tokenizer/simple_tokenizer.h | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/libs/simple_tokenizer/simple_extension.c b/libs/simple_tokenizer/simple_extension.c index 4cd034965..6caf85784 100644 --- a/libs/simple_tokenizer/simple_extension.c +++ b/libs/simple_tokenizer/simple_extension.c @@ -25,7 +25,7 @@ int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines return SQLITE_ERROR; } - return simpleRegisterJiebaModes(api); + return simpleRegisterTokenizers(api); } // Default entry point used by sqlite3_load_extension when no entry symbol is specified. diff --git a/libs/simple_tokenizer/simple_tokenizer.c b/libs/simple_tokenizer/simple_tokenizer.c index a5a8dd6aa..67f1b443c 100644 --- a/libs/simple_tokenizer/simple_tokenizer.c +++ b/libs/simple_tokenizer/simple_tokenizer.c @@ -84,3 +84,13 @@ int simpleRegisterTokenizer(const fts5_api* pApi, const char* zName, SimpleToken return pApi->xCreateTokenizer(pApi, zName, (void*)(intptr_t)mode, &tokenizer, nullptr); } +int simpleRegisterTokenizers(const 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 index 9c7a707ba..bba6c9387 100644 --- a/libs/simple_tokenizer/simple_tokenizer.h +++ b/libs/simple_tokenizer/simple_tokenizer.h @@ -21,6 +21,7 @@ typedef enum SimpleTokenizerMode { } SimpleTokenizerMode; int simpleRegisterTokenizer(const fts5_api* pApi, const char* zName, SimpleTokenizerMode mode); +int simpleRegisterTokenizers(const fts5_api* pApi); #ifdef __cplusplus } From e59945ddf166911aaea2b9841b34643b5c5befba Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 14:29:11 +0800 Subject: [PATCH 09/18] Package SQLCipher-built simple extension on Windows --- installer/windows/variables.wxi | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/installer/windows/variables.wxi b/installer/windows/variables.wxi index 22f2c702a..5dbda56ba 100644 --- a/installer/windows/variables.wxi +++ b/installer/windows/variables.wxi @@ -56,6 +56,12 @@ - + + From ebb1ef8635300e33325018512237ddb1c4a86a8b Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 15:04:29 +0800 Subject: [PATCH 10/18] Ensure simple extension ships in Windows ZIP --- .github/workflows/cppcmake-windows.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/cppcmake-windows.yml b/.github/workflows/cppcmake-windows.yml index 9da238d6d..5856431ff 100644 --- a/.github/workflows/cppcmake-windows.yml +++ b/.github/workflows/cppcmake-windows.yml @@ -184,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)' From 1c08cb3aaac530b9952d7bc52a48e1bc00d59e46 Mon Sep 17 00:00:00 2001 From: Andy Si Date: Mon, 29 Dec 2025 15:36:58 +0800 Subject: [PATCH 11/18] Merge default builtin extensions when loading settings --- src/Settings.cpp | 34 +++++++++++++++++++--------------- src/Settings.h | 2 ++ src/sqlitedb.cpp | 9 ++++++++- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/Settings.cpp b/src/Settings.cpp index 74728adec..0134bebe2 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -409,21 +409,7 @@ QVariant Settings::getDefaultValue(const std::string& group, const std::string& // extensions/builtin? if(group == "extensions" && name == "builtin") - { - 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; - } + return defaultBuiltinExtensions(); // extensions/disableregex? if(group == "extension" && name == "disableregex") @@ -474,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/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()) From 56cedb91e4ab38ed29cd3405a29334326ed213f0 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 20:53:54 +0800 Subject: [PATCH 12/18] =?UTF-8?q?=E5=A2=9E=E5=8A=A0simple=20tokenizer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 41 +++++++- AGENTS.md | 35 +++++++ CMakeLists.txt | 13 +++ libs/simple_tokenizer/jieba_query.c | 3 +- libs/simple_tokenizer/simple_extension.c | 21 +++- libs/simple_tokenizer/simple_tokenizer.c | 117 +++++++++++++++++++---- libs/simple_tokenizer/simple_tokenizer.h | 7 +- 7 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 AGENTS.md 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/CMakeLists.txt b/CMakeLists.txt index dbe2b77ad..a42ecb102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,6 +139,19 @@ if(APPLE OR WIN32) 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 diff --git a/libs/simple_tokenizer/jieba_query.c b/libs/simple_tokenizer/jieba_query.c index 42c41bddd..5e1d63380 100644 --- a/libs/simple_tokenizer/jieba_query.c +++ b/libs/simple_tokenizer/jieba_query.c @@ -8,11 +8,10 @@ #define SQLITE_ENABLE_FTS5 1 #endif -int simpleRegisterJiebaModes(const fts5_api* pApi) +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 index 6caf85784..1073d36ec 100644 --- a/libs/simple_tokenizer/simple_extension.c +++ b/libs/simple_tokenizer/simple_extension.c @@ -12,11 +12,29 @@ SQLITE_EXTENSION_INIT1 +static int fts5ApiFromDb(sqlite3* db, fts5_api** ppApi) +{ + sqlite3_stmt* stmt = nullptr; + *ppApi = nullptr; + + int rc = sqlite3_prepare_v2(db, "SELECT fts5(?1)", -1, &stmt, nullptr); + if(rc != SQLITE_OK) + return rc; + + sqlite3_bind_pointer(stmt, 1, (void*)ppApi, "fts5_api_ptr", nullptr); + (void)sqlite3_step(stmt); + rc = sqlite3_finalize(stmt); + return rc; +} + int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { SQLITE_EXTENSION_INIT2(pApi); - fts5_api* api = (fts5_api*)sqlite3_fts5_api_from_db(db); + fts5_api* api = nullptr; + const int rc = fts5ApiFromDb(db, &api); + if(rc != SQLITE_OK) + return rc; if(!api) { @@ -33,4 +51,3 @@ int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routi { return sqlite3_simple_init(db, pzErrMsg, pApi); } - diff --git a/libs/simple_tokenizer/simple_tokenizer.c b/libs/simple_tokenizer/simple_tokenizer.c index 67f1b443c..6e5ec5d90 100644 --- a/libs/simple_tokenizer/simple_tokenizer.c +++ b/libs/simple_tokenizer/simple_tokenizer.c @@ -21,6 +21,35 @@ struct SimpleTokenizer { 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; @@ -41,50 +70,101 @@ static void simpleDelete(Fts5Tokenizer* pTok) sqlite3_free(pTok); } -static int isTokenChar(int c) -{ - return isalnum(c) || (c & 0x80); -} - 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 i = 0; - int start = -1; int tokenFlags = 0; if(p->mode == SIMPLE_TOKEN_MODE_JIEBA_QUERY && (flags & FTS5_TOKENIZE_QUERY)) tokenFlags |= FTS5_TOKEN_COLOCATED; - while(i <= nText) + // 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)(i < nText ? pText[i] : ' '); + 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(start >= 0 && !isTokenChar(c)) + if(isAsciiDigit(c)) { - int rc = xToken(pCtx, tokenFlags, &pText[start], i - start, start, i); + 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; - start = -1; + i = end; + continue; } - else if(start < 0 && isTokenChar(c)) + + if(c < 0x80) { - start = i; + // 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; } - i++; + + // 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(const fts5_api* pApi, const char* zName, SimpleTokenizerMode mode) +int simpleRegisterTokenizer(fts5_api* pApi, const char* zName, SimpleTokenizerMode mode) { - static const fts5_tokenizer tokenizer = { simpleCreate, simpleDelete, simpleTokenize }; + static fts5_tokenizer tokenizer = { simpleCreate, simpleDelete, simpleTokenize }; return pApi->xCreateTokenizer(pApi, zName, (void*)(intptr_t)mode, &tokenizer, nullptr); } -int simpleRegisterTokenizers(const fts5_api* pApi) +int simpleRegisterTokenizers(fts5_api* pApi) { int rc = simpleRegisterTokenizer(pApi, "simple", SIMPLE_TOKEN_MODE_BASIC); @@ -93,4 +173,3 @@ int simpleRegisterTokenizers(const fts5_api* pApi) return rc; } - diff --git a/libs/simple_tokenizer/simple_tokenizer.h b/libs/simple_tokenizer/simple_tokenizer.h index bba6c9387..79d0da4a4 100644 --- a/libs/simple_tokenizer/simple_tokenizer.h +++ b/libs/simple_tokenizer/simple_tokenizer.h @@ -20,13 +20,12 @@ typedef enum SimpleTokenizerMode { SIMPLE_TOKEN_MODE_JIEBA_QUERY = 2 } SimpleTokenizerMode; -int simpleRegisterTokenizer(const fts5_api* pApi, const char* zName, SimpleTokenizerMode mode); -int simpleRegisterTokenizers(const fts5_api* pApi); +int simpleRegisterTokenizer(fts5_api* pApi, const char* zName, SimpleTokenizerMode mode); +int simpleRegisterTokenizers(fts5_api* pApi); #ifdef __cplusplus } #endif -int simpleRegisterJiebaModes(const fts5_api* pApi); +int simpleRegisterJiebaModes(fts5_api* pApi); int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi); - From 1498e30e02ef862a8a9246a61691ea0188497466 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 21:13:50 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=94=99=E8=AF=AF=20Er?= =?UTF-8?q?ror=20loading=20built-in=20extension:=20dlsym(0xb0c051c0,=20sql?= =?UTF-8?q?ite3=5Fsimple=5Finit):=20symbol=20not=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/simple_tokenizer/simple_extension.c | 11 ++++++----- libs/simple_tokenizer/simple_tokenizer.c | 2 +- libs/simple_tokenizer/simple_tokenizer.h | 8 +++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/libs/simple_tokenizer/simple_extension.c b/libs/simple_tokenizer/simple_extension.c index 1073d36ec..fb6211bbd 100644 --- a/libs/simple_tokenizer/simple_extension.c +++ b/libs/simple_tokenizer/simple_extension.c @@ -1,6 +1,7 @@ #include "simple_tokenizer.h" #include +#include #ifndef SQLITE_CORE #define SQLITE_CORE 1 @@ -14,14 +15,14 @@ SQLITE_EXTENSION_INIT1 static int fts5ApiFromDb(sqlite3* db, fts5_api** ppApi) { - sqlite3_stmt* stmt = nullptr; - *ppApi = nullptr; + sqlite3_stmt* stmt = NULL; + *ppApi = NULL; - int rc = sqlite3_prepare_v2(db, "SELECT fts5(?1)", -1, &stmt, nullptr); + 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", nullptr); + sqlite3_bind_pointer(stmt, 1, (void*)ppApi, "fts5_api_ptr", NULL); (void)sqlite3_step(stmt); rc = sqlite3_finalize(stmt); return rc; @@ -31,7 +32,7 @@ int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines { SQLITE_EXTENSION_INIT2(pApi); - fts5_api* api = nullptr; + fts5_api* api = NULL; const int rc = fts5ApiFromDb(db, &api); if(rc != SQLITE_OK) return rc; diff --git a/libs/simple_tokenizer/simple_tokenizer.c b/libs/simple_tokenizer/simple_tokenizer.c index 6e5ec5d90..e4d7e1272 100644 --- a/libs/simple_tokenizer/simple_tokenizer.c +++ b/libs/simple_tokenizer/simple_tokenizer.c @@ -161,7 +161,7 @@ static int simpleTokenize(Fts5Tokenizer* pTok, void* pCtx, int flags, const char 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, nullptr); + return pApi->xCreateTokenizer(pApi, zName, (void*)(intptr_t)mode, &tokenizer, NULL); } int simpleRegisterTokenizers(fts5_api* pApi) diff --git a/libs/simple_tokenizer/simple_tokenizer.h b/libs/simple_tokenizer/simple_tokenizer.h index 79d0da4a4..bea3aac2b 100644 --- a/libs/simple_tokenizer/simple_tokenizer.h +++ b/libs/simple_tokenizer/simple_tokenizer.h @@ -22,10 +22,12 @@ typedef enum 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 - -int simpleRegisterJiebaModes(fts5_api* pApi); -int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi); From 4f71a1980ddf4f9740763828504b5b020dc725e3 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 21:33:39 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=9A=20Error=20loading=20built-in=20exte?= =?UTF-8?q?nsion:=20dlsym(0x830c11c0,=20sqlite3=5Fsimple=5Finit):=20symbol?= =?UTF-8?q?=20not=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/simple_tokenizer/simple_extension.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/simple_tokenizer/simple_extension.c b/libs/simple_tokenizer/simple_extension.c index fb6211bbd..a4b6a322f 100644 --- a/libs/simple_tokenizer/simple_extension.c +++ b/libs/simple_tokenizer/simple_extension.c @@ -13,6 +13,20 @@ 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; @@ -28,7 +42,7 @@ static int fts5ApiFromDb(sqlite3* db, fts5_api** ppApi) return rc; } -int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) +SQLB_EXT_EXPORT SQLB_NO_DEAD_STRIP int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { SQLITE_EXTENSION_INIT2(pApi); @@ -48,7 +62,7 @@ int sqlite3_simple_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines } // Default entry point used by sqlite3_load_extension when no entry symbol is specified. -int sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) +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); } From d36e31eb1ee74041cc4ae40c1641bf562423c9de Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 21:48:57 +0800 Subject: [PATCH 15/18] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9Asymbol=20not=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a42ecb102..47011a48b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,6 +115,13 @@ if(APPLE OR WIN32) 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 "" From 7da7a20030ce3e548ee342867d4d0b76018fd9b3 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 22:08:40 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 8 +++++++- installer/macos/notarize.sh | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47011a48b..3bdb4d0dc 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) @@ -102,6 +102,12 @@ if(APPLE OR WIN32) ) 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}) diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh index 2260c80b7..18eefd5be 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -43,7 +43,7 @@ else fi 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" + 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 @@ -78,7 +78,7 @@ done # 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" + 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/" \; From 7b0c07a4294f53bed8b956432c9283b796da3847 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 22:39:09 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=94=B9=E5=90=8D?= =?UTF-8?q?=E5=AD=97=E6=97=A0=E6=B3=95=E6=89=93=E5=BC=80=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- installer/macos/notarize.sh | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/installer/macos/notarize.sh b/installer/macos/notarize.sh index 18eefd5be..00937f0d1 100644 --- a/installer/macos/notarize.sh +++ b/installer/macos/notarize.sh @@ -25,9 +25,9 @@ fi # Run macdeployqt if [[ "$SIGNING_READY" == "true" ]]; then - find build -name "DB Browser for SQL*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} -sign-for-notarization=$DEV_ID \; + find build -maxdepth 1 -name "*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} -sign-for-notarization=$DEV_ID \; else - find build -name "DB Browser for SQL*.app" -exec $(brew --prefix sqlb-qt@5)/bin/macdeployqt {} \; + 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 @@ -41,8 +41,7 @@ if [[ -n "$GH_TOKEN" ]]; then else echo "GH_TOKEN not provided; skipping sqlean download." fi -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 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 @@ -67,17 +66,15 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); 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 +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') +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/" \; @@ -86,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" @@ -98,11 +94,10 @@ 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') +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" @@ -113,7 +108,7 @@ for TARGET in $(find build -name "DB Browser for SQL*.app" | sed -e 's/ /_/g'); else echo "Skipping codesign for $TARGET (credentials unavailable)." fi -done +done < <(find build -maxdepth 1 -name "*.app" -print0) # Move app bundle to installer folder for DMG creation mv build/*.app installer/macos From b0148708a71330499ee91f44533cf6d6641a43ab Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 29 Dec 2025 22:57:27 +0800 Subject: [PATCH 18/18] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=94=B9=E5=90=8D=E5=AD=97=E6=97=A0=E6=B3=95=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BUILDING.md | 9 +++++++++ CMakeLists.txt | 5 +++++ config/platform_apple.cmake | 1 + src/app.plist | 4 ++-- 4 files changed, 17 insertions(+), 2 deletions(-) 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 3bdb4d0dc..02393341a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 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/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