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())