Merge pull request #25239 from graelo/quicklook-clean-pixi

feat(macos): add quicklook extensions
This commit is contained in:
Chris Hennes
2026-01-21 15:27:22 -06:00
committed by GitHub
19 changed files with 1761 additions and 58 deletions

View File

@@ -96,6 +96,13 @@ function run_codesign {
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements entitlements.plist "$1"
}
function run_codesign_extension {
local target="$1"
local entitlements_file="$2"
echo "Signing extension $target with entitlements $entitlements_file"
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements "$entitlements_file" "$target"
}
IFS=$'\n'
dylibs=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.dylib"))
shared_objects=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.so"))
@@ -108,13 +115,42 @@ signed_files=("${dylibs[@]}" "${shared_objects[@]}" "${bundles[@]}" "${executabl
# This list of files is generated from:
# file `find . -type f -perm +111 -print` | grep "Mach-O 64-bit executable" | sed 's/:.*//g'
for exe in ${signed_files}; do
run_codesign "${exe}"
# Skip .appex executables as they will be signed separately with their bundles
if [[ "$exe" != */Contents/PlugIns/*.appex/* ]]; then
run_codesign "${exe}"
fi
done
# Two additional files that must be signed that aren't caught by the above searches:
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/packages.txt"
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd"
# Sign new Swift QuickLook extensions (macOS 15.0+) with their specific entitlements
# These must be signed before the app itself to avoid overriding the extension signatures
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns" ]; then
# Find the entitlements files relative to script location
# Script is in package/scripts/, entitlements are in src/MacAppBundle/QuickLook/modern/
SCRIPT_DIR="${0:A:h}" # zsh equivalent of dirname with full path resolution
PREVIEW_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/PreviewExtension.entitlements"
THUMBNAIL_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/ThumbnailExtension.entitlements"
# Sign individual executables within .appex bundles first
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension" ]; then
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension"
fi
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension" ]; then
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension"
fi
# Then sign the .appex bundles themselves with extension-specific entitlements
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" ] && [ -f "$THUMBNAIL_ENTITLEMENTS" ]; then
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" "$THUMBNAIL_ENTITLEMENTS"
fi
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" ] && [ -f "$PREVIEW_ENTITLEMENTS" ]; then
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" "$PREVIEW_ENTITLEMENTS"
fi
fi
# Finally, sign the app itself (must be done last)
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}"

View File

@@ -40,6 +40,7 @@
#include <QMimeData>
#include <QOpenGLWidget>
#include <QPainter>
#include <QProcess>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QScreen>
@@ -1793,8 +1794,67 @@ void MainWindow::delayedStartup()
);
safeModePopup.exec();
}
#ifdef Q_OS_MAC
// Register QuickLook extensions on first launch
registerQuickLookExtensions();
#endif
}
#ifdef Q_OS_MAC
void MainWindow::registerQuickLookExtensions()
{
// Only check once per session
static bool quickLookChecked = false;
if (quickLookChecked) {
return;
}
quickLookChecked = true;
// Get the path to FreeCAD.app/Contents/PlugIns
QString appPath = QApplication::applicationDirPath();
QString plugInsPath = appPath + "/../PlugIns";
QString thumbnailExt = plugInsPath + "/FreeCADThumbnailExtension.appex";
QString previewExt = plugInsPath + "/FreeCADPreviewExtension.appex";
// Check if extensions exist before attempting registration
if (!QFileInfo::exists(thumbnailExt) || !QFileInfo::exists(previewExt)) {
return;
}
// Check if extensions are already registered with pluginkit
QProcess checkProcess;
checkProcess.start("pluginkit", QStringList() << "-m");
checkProcess.waitForFinished();
QString registeredPlugins = QString::fromUtf8(checkProcess.readAllStandardOutput());
const QString thumbnailId = QStringLiteral("org.freecad.FreeCAD.quicklook.thumbnail");
const QString previewId = QStringLiteral("org.freecad.FreeCAD.quicklook.preview");
bool thumbnailRegistered = registeredPlugins.contains(thumbnailId);
bool previewRegistered = registeredPlugins.contains(previewId);
if (thumbnailRegistered && previewRegistered) {
Base::Console().log("QuickLook extensions already registered\n");
return;
}
// Register and activate only the extensions that are not yet registered
if (!thumbnailRegistered) {
QProcess::execute("pluginkit", QStringList() << "-a" << thumbnailExt);
QProcess::execute("pluginkit", QStringList() << "-e" << "use" << "-i" << thumbnailId);
}
if (!previewRegistered) {
QProcess::execute("pluginkit", QStringList() << "-a" << previewExt);
QProcess::execute("pluginkit", QStringList() << "-e" << "use" << "-i" << previewId);
}
Base::Console().log("QuickLook extensions registered successfully\n");
}
#endif
void MainWindow::appendRecentFile(const QString& filename)
{
auto recent = this->findChild<RecentFilesAction*>(QStringLiteral("recentFiles"));

View File

@@ -370,6 +370,12 @@ private Q_SLOTS:
* \internal
*/
void delayedStartup();
#ifdef Q_OS_MAC
/**
* \internal
*/
void registerQuickLookExtensions();
#endif
/**
* \internal
*/

View File

@@ -6,14 +6,7 @@
#
if(FREECAD_CREATE_MAC_APP OR (APPLE AND BUILD_WITH_CONDA))
add_subdirectory(QuickLook)
install(
DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/QuickLook/QuicklookFCStd.qlgenerator"
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook"
)
install(
PROGRAMS "${PROJECT_BINARY_DIR}/src/MacAppBundle/QuickLook/QuicklookFCStd.framework/Versions/A/QuicklookFCStd"
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/"
)
# Installation handled by QuickLook/CMakeLists.txt
endif()
@@ -25,13 +18,21 @@ if(FREECAD_CREATE_MAC_APP)
set(PYTHON_DIR_BASENAME python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR})
if(PYTHON_LIBRARY MATCHES "(.*Python\\.framework).*")
#framework
set(PYTHON_DIR ${CMAKE_MATCH_1}/Versions/${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/lib/${PYTHON_DIR_BASENAME})
if(Python3_LIBRARY_DIRS)
# Get first library directory
list(GET Python3_LIBRARY_DIRS 0 FIRST_PYTHON_LIB_DIR)
if(FIRST_PYTHON_LIB_DIR MATCHES "(.*Python\\.framework).*")
#framework
set(PYTHON_DIR ${CMAKE_MATCH_1}/Versions/${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/lib/${PYTHON_DIR_BASENAME})
else()
#unix
set(PYTHON_DIR ${FIRST_PYTHON_LIB_DIR}/${PYTHON_DIR_BASENAME})
endif()
else()
#unix
get_filename_component(PYTHON_DIR ${PYTHON_LIBRARY} PATH)
set(PYTHON_DIR ${PYTHON_DIR}/${PYTHON_DIR_BASENAME})
# Fallback: derive from Python3_EXECUTABLE
get_filename_component(PYTHON_BIN_DIR ${Python3_EXECUTABLE} DIRECTORY)
get_filename_component(PYTHON_PREFIX ${PYTHON_BIN_DIR} DIRECTORY)
set(PYTHON_DIR ${PYTHON_PREFIX}/lib/${PYTHON_DIR_BASENAME})
endif()
message(" PYTHON_DIR is ${PYTHON_DIR} --------------------ipatch--")
@@ -51,7 +52,7 @@ if(HOMEBREW_PREFIX)
message(" PYTHON_DIR is ${PYTHON_DIR} --------------------ipatch--")
file(GLOB HOMEBREW_PTH_FILES "${HOMEBREW_PREFIX}/lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/freecad*.pth")
file(GLOB HOMEBREW_PTH_FILES "${HOMEBREW_PREFIX}/lib/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/freecad*.pth")
message(STATUS "HOMEBREW_PTH_FILES are ${HOMEBREW_PTH_FILES} -----------------------ipatch--")
@@ -120,13 +121,13 @@ if(HOMEBREW_PREFIX)
if(IS_DIRECTORY "${CONTENT_ITEM}")
# Install directory
install(DIRECTORY "${CONTENT_ITEM}/"
install(DIRECTORY "${CONTENT_ITEM}/"
DESTINATION "${BUNDLE_SITE_PACKAGES_DIR}/${ITEM_NAME}"
USE_SOURCE_PERMISSIONS)
message(STATUS " Installing directory: ${ITEM_NAME}")
else()
# Install file
install(FILES "${CONTENT_ITEM}"
install(FILES "${CONTENT_ITEM}"
DESTINATION "${BUNDLE_SITE_PACKAGES_DIR}")
message(STATUS " Installing file: ${ITEM_NAME}")
endif()
@@ -149,7 +150,15 @@ if(HOMEBREW_PREFIX)
endforeach(PTH_FILE)
endif()
set(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins")
set(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../plugins")
# Fallback for Homebrew Qt6 layout
if(NOT EXISTS "${QT_PLUGINS_DIR}")
set(QT_PLUGINS_DIR "/opt/homebrew/share/qt/plugins")
endif()
set(QT_ASSISTANT_PATH "${Qt6Core_DIR}/../../../libexec/Assistant.app/Contents/MacOS/Assistant")
set(QT_PLUGIN_SUBDIRS "platforms;imageformats;styles;iconengines")
execute_process(COMMAND "xcode-select" "--print-path"
OUTPUT_VARIABLE XCODE_PATH
ERROR_QUIET
@@ -159,22 +168,33 @@ string(STRIP ${XCODE_PATH} XCODE_PATH)
set(XCTEST_PATH "${XCODE_PATH}/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework/Versions/Current")
# add qt assistant to bundle
install(PROGRAMS "${Qt5Core_DIR}/../../../libexec/Assistant.app/Contents/MacOS/Assistant" DESTINATION ${CMAKE_INSTALL_PREFIX}/MacOS)
if(EXISTS "${QT_ASSISTANT_PATH}")
install(PROGRAMS "${QT_ASSISTANT_PATH}" DESTINATION ${CMAKE_INSTALL_PREFIX}/MacOS)
else()
message(WARNING "Qt Assistant not found at ${QT_ASSISTANT_PATH}, skipping installation")
endif()
# Ensure the actual plugin files are installed instead of symlinks.
file(GLOB _subdirs RELATIVE "${QT_PLUGINS_DIR}" "${QT_PLUGINS_DIR}/*")
foreach(_subdir ${_subdirs})
# Install specific Qt plugin subdirectories that FreeCAD needs
foreach(_subdir ${QT_PLUGIN_SUBDIRS})
if(IS_DIRECTORY "${QT_PLUGINS_DIR}/${_subdir}")
set(_resolved_files "")
file(GLOB _plugin_files RELATIVE "${QT_PLUGINS_DIR}/${_subdir}" "${QT_PLUGINS_DIR}/${_subdir}/*")
file(GLOB _plugin_files "${QT_PLUGINS_DIR}/${_subdir}/*.dylib")
foreach(_plugin_file ${_plugin_files})
get_filename_component(_resolved_file "${QT_PLUGINS_DIR}/${_subdir}/${_plugin_file}" REALPATH)
list(APPEND _resolved_files ${_resolved_file})
if(EXISTS "${_plugin_file}" AND NOT IS_DIRECTORY "${_plugin_file}")
get_filename_component(_resolved_file "${_plugin_file}" REALPATH)
list(APPEND _resolved_files ${_resolved_file})
endif()
endforeach()
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_LIBDIR}/qtplugins/${_subdir}")
if(_resolved_files)
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_LIBDIR}/qtplugins/${_subdir}")
# Also install platform plugins to standard Qt location for runtime loading
if("${_subdir}" STREQUAL "platforms")
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns/platforms")
endif()
endif()
endif()
endforeach()
@@ -194,9 +214,65 @@ install(CODE
${HOMEBREW_PREFIX}${MACPORTS_PREFIX}/lib
${HOMEBREW_PREFIX}/opt
${HOMEBREW_PREFIX}/opt/*/lib
${Qt5Core_DIR}/../../..
${Qt${FREECAD_QT_MAJOR_VERSION}Core_DIR}/../../..
${XCTEST_PATH}
)"
)
# Fix Qt platform plugin dependencies after bundle relocation
# This addresses the SIGKILL issue where Qt plugins reference framework paths
# but the Qt libraries are installed as flat dylibs
install(CODE "
message(STATUS \"Fixing Qt platform plugin dependencies...\")
# Find platform plugins
file(GLOB_RECURSE PLATFORM_PLUGINS \"${CMAKE_INSTALL_PREFIX}/PlugIns/platforms/*.dylib\")
foreach(PLUGIN \${PLATFORM_PLUGINS})
message(STATUS \"Processing plugin: \${PLUGIN}\")
# Get current dependencies
execute_process(
COMMAND otool -L \"\${PLUGIN}\"
OUTPUT_VARIABLE PLUGIN_DEPS
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Fix framework-style Qt dependencies to use flat dylib names
if(FREECAD_QT_MAJOR_VERSION STREQUAL \"6\")
set(QT_FRAMEWORKS \"QtCore;QtGui;QtWidgets;QtOpenGL;QtPrintSupport;QtSvg;QtNetwork\")
else()
set(QT_FRAMEWORKS \"QtCore;QtGui;QtWidgets;QtOpenGL;QtPrintSupport;QtSvg;QtNetwork\")
endif()
foreach(FRAMEWORK \${QT_FRAMEWORKS})
# Change @rpath/QtXXX.framework/Versions/A/QtXXX to @rpath/QtXXX
execute_process(
COMMAND install_name_tool -change
\"@rpath/\${FRAMEWORK}.framework/Versions/A/\${FRAMEWORK}\"
\"@rpath/\${FRAMEWORK}\"
\"\${PLUGIN}\"
OUTPUT_QUIET
ERROR_QUIET
)
# Also handle versioned framework paths for Qt5
if(FREECAD_QT_MAJOR_VERSION STREQUAL \"5\")
execute_process(
COMMAND install_name_tool -change
\"@rpath/\${FRAMEWORK}.framework/Versions/5/\${FRAMEWORK}\"
\"@rpath/\${FRAMEWORK}\"
\"\${PLUGIN}\"
OUTPUT_QUIET
ERROR_QUIET
)
endif()
endforeach()
message(STATUS \"Fixed dependencies for: \${PLUGIN}\")
endforeach()
message(STATUS \"Qt platform plugin dependency fixing complete.\")
")
endif(FREECAD_CREATE_MAC_APP)

View File

@@ -1,36 +1,156 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#cmake_minimum_required(VERSION 3.23)
#project(FCQuickLook)
cmake_minimum_required(VERSION 3.22)
project(FreeCADQuickLook)
add_library(
QuicklookFCStd
SHARED
GeneratePreviewForURL.m
GenerateThumbnailForURL.m
main.c
# Configuration options for QuickLook support
option(FREECAD_QUICKLOOK_LEGACY_SUPPORT "Build legacy .qlgenerator for older macOS compatibility" ON)
option(FREECAD_QUICKLOOK_MODERN_SUPPORT "Build modern .appex extensions for macOS 15.0+" ON)
# Only build QuickLook extensions with Unix Makefiles or Ninja generator
if(NOT CMAKE_GENERATOR MATCHES "Unix Makefiles|Ninja")
message(STATUS "QuickLook extensions only supported with Unix Makefiles or Ninja generators (current: ${CMAKE_GENERATOR})")
add_custom_target(FreeCADQuickLook
COMMAND ${CMAKE_COMMAND} -E echo "QuickLook extensions require Unix Makefiles or Ninja generator"
COMMENT "QuickLook placeholder target"
)
return()
endif()
# Validate CMAKE_OSX_DEPLOYMENT_TARGET is set (or use system default)
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
# If not explicitly set, query the system default
execute_process(
COMMAND xcrun --show-sdk-version
OUTPUT_VARIABLE MACOS_SDK_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(MACOS_SDK_VERSION)
set(CMAKE_OSX_DEPLOYMENT_TARGET ${MACOS_SDK_VERSION})
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET not set, using SDK version: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
else()
message(WARNING "CMAKE_OSX_DEPLOYMENT_TARGET not set and could not determine SDK version. Defaulting to 15.0")
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0")
endif()
endif()
# Determine which QuickLook implementations to build
set(BUILD_MODERN_EXTENSIONS OFF)
set(BUILD_LEGACY_GENERATOR OFF)
if(FREECAD_QUICKLOOK_MODERN_SUPPORT)
if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_GREATER_EQUAL "15.0")
set(BUILD_MODERN_EXTENSIONS ON)
message(STATUS "Building modern Swift .appex QuickLook extensions (macOS 15.0+)")
else()
message(STATUS "Modern QuickLook extensions require macOS 15.0+, current target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
endif()
if(FREECAD_QUICKLOOK_LEGACY_SUPPORT)
set(BUILD_LEGACY_GENERATOR ON)
message(STATUS "Building legacy .qlgenerator QuickLook support")
endif()
if(NOT BUILD_MODERN_EXTENSIONS AND NOT BUILD_LEGACY_GENERATOR)
message(WARNING "No QuickLook implementations enabled. Enable FREECAD_QUICKLOOK_MODERN_SUPPORT or FREECAD_QUICKLOOK_LEGACY_SUPPORT")
add_custom_target(FreeCADQuickLook
COMMAND ${CMAKE_COMMAND} -E echo "No QuickLook implementations enabled"
COMMENT "QuickLook disabled target"
)
return()
endif()
# Global configuration
set(FREECAD_BUNDLE_ID "org.freecad.FreeCAD")
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
# Detect target architecture
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
set(TARGET_ARCH "arm64")
else()
set(TARGET_ARCH "x86_64")
endif()
# Build subdirectories conditionally
set(QUICKLOOK_TARGETS "")
if(BUILD_MODERN_EXTENSIONS)
add_subdirectory(modern)
list(APPEND QUICKLOOK_TARGETS FreeCADModernQuickLook)
endif()
if(BUILD_LEGACY_GENERATOR)
add_subdirectory(legacy)
list(APPEND QUICKLOOK_TARGETS FreeCADLegacyQuickLook)
endif()
# Main target that coordinates all QuickLook implementations
add_custom_target(FreeCADQuickLook ALL
DEPENDS ${QUICKLOOK_TARGETS}
COMMENT "FreeCAD QuickLook support integrated into main app"
)
set_target_properties(
QuicklookFCStd
PROPERTIES
FRAMEWORK TRUE
MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/QuicklookFCStd.qlgenerator/Contents/Info.plist"
#SUFFIX .qlgenerator
# Ensure QuickLook is built after main FreeCAD app if it's available as a target
if(TARGET FreeCAD)
add_dependencies(FreeCADQuickLook FreeCAD)
endif()
# Combined verification target
set(VERIFY_COMMANDS "")
if(BUILD_MODERN_EXTENSIONS)
list(APPEND VERIFY_COMMANDS
COMMAND ${CMAKE_COMMAND} --build . --target verify_modern_build
)
endif()
if(BUILD_LEGACY_GENERATOR)
list(APPEND VERIFY_COMMANDS
COMMAND ${CMAKE_COMMAND} --build . --target verify_legacy_build
)
endif()
add_custom_target(verify_build
${VERIFY_COMMANDS}
DEPENDS FreeCADQuickLook
COMMENT "Verifying all QuickLook implementations"
)
target_link_libraries(
QuicklookFCStd
"-framework AppKit"
"-framework ApplicationServices"
"-framework CoreData"
"-framework CoreFoundation"
"-framework CoreServices"
"-framework Foundation"
"-framework QuickLook"
# Combined test registration target
set(REGISTRATION_COMMANDS "")
if(BUILD_MODERN_EXTENSIONS)
list(APPEND REGISTRATION_COMMANDS
COMMAND ${CMAKE_COMMAND} --build . --target test_modern_registration
)
endif()
if(BUILD_LEGACY_GENERATOR)
list(APPEND REGISTRATION_COMMANDS
COMMAND ${CMAKE_COMMAND} --build . --target test_legacy_registration
)
endif()
add_custom_target(test_registration
${REGISTRATION_COMMANDS}
DEPENDS FreeCADQuickLook
COMMENT "Testing all QuickLook registrations"
)
set_target_properties(
QuicklookFCStd
PROPERTIES LINK_FLAGS "-Wl,-F/Library/Frameworks"
)
# Installation components
if(BUILD_MODERN_EXTENSIONS)
# Modern extensions install themselves via their CMakeLists.txt
endif()
if(BUILD_LEGACY_GENERATOR)
# Legacy generator installs itself via its CMakeLists.txt
endif()
# Status summary
message(STATUS "FreeCAD QuickLook Configuration Summary:")
message(STATUS " Generator: ${CMAKE_GENERATOR}")
message(STATUS " Target Architecture: ${TARGET_ARCH}")
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
message(STATUS " Target FreeCAD.app: ${TARGET_APP_BUNDLE}")
message(STATUS " Modern Extensions (.appex): ${BUILD_MODERN_EXTENSIONS}")
message(STATUS " Legacy Generator (.qlgenerator): ${BUILD_LEGACY_GENERATOR}")
message(STATUS " Code Signing: Handled by official src/Tools/macos_sign_and_notarize.sh script")

View File

@@ -0,0 +1,97 @@
cmake_minimum_required(VERSION 3.22)
# Legacy Objective-C QuickLook Generator for older macOS compatibility
# This file handles building .qlgenerator bundles using Objective-C/C
# Configuration options (inherited from parent)
# CMAKE_OSX_DEPLOYMENT_TARGET
# Paths (inherited from parent)
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
set(LEGACY_QUICKLOOK_DIR "${TARGET_APP_BUNDLE}/Contents/Library/QuickLook")
# Build legacy QuickLook generator using traditional CMake approach
add_library(
QuicklookFCStd
SHARED
GeneratePreviewForURL.m
GenerateThumbnailForURL.m
main.c
)
set_target_properties(
QuicklookFCStd
PROPERTIES
FRAMEWORK TRUE
MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/QuicklookFCStd.qlgenerator/Contents/Info.plist"
)
target_link_libraries(
QuicklookFCStd
"-framework AppKit"
"-framework ApplicationServices"
"-framework CoreData"
"-framework CoreFoundation"
"-framework CoreServices"
"-framework Foundation"
"-framework QuickLook"
)
set_target_properties(
QuicklookFCStd
PROPERTIES LINK_FLAGS "-Wl,-F/Library/Frameworks"
)
# Create bundle directory structure
add_custom_target(create_legacy_bundle_dirs ALL
COMMAND ${CMAKE_COMMAND} -E make_directory ${LEGACY_QUICKLOOK_DIR}
COMMENT "Creating legacy QuickLook directory structure"
)
# Install legacy generator to the system QuickLook directory within the app bundle
add_custom_target(embed_legacy_generator ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory
"$<TARGET_BUNDLE_DIR:QuicklookFCStd>"
"${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator"
DEPENDS
QuicklookFCStd
create_legacy_bundle_dirs
COMMENT "Embedding legacy QuickLook generator in main FreeCAD.app"
)
# Main legacy generator target
add_custom_target(FreeCADLegacyQuickLook ALL
DEPENDS embed_legacy_generator
COMMENT "Legacy Objective-C QuickLook generator built"
)
# Install target for legacy generator
install(DIRECTORY ${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook"
COMPONENT LegacyQuickLook
USE_SOURCE_PERMISSIONS
)
# Verification target
add_custom_target(verify_legacy_build
COMMAND test -f ${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd
COMMAND echo "Legacy generator successfully embedded in FreeCAD.app"
DEPENDS FreeCADLegacyQuickLook
COMMENT "Verifying legacy QuickLook generator integration"
)
# Test registration target
add_custom_target(test_legacy_registration
COMMAND echo "Legacy QuickLook generator will be registered automatically by the system"
COMMAND qlmanage -r || echo "QuickLook cache reset attempted"
DEPENDS FreeCADLegacyQuickLook
COMMENT "Testing legacy QuickLook generator registration"
)
# Status messages
message(STATUS "Legacy Objective-C QuickLook Generator Configuration:")
message(STATUS " Target Library: QuicklookFCStd")
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
message(STATUS " Legacy QuickLook Directory: ${LEGACY_QUICKLOOK_DIR}")
message(STATUS " Bundle ID: org.freecad.qlgenerator.QuicklookFCStd")
message(STATUS " Code Signing: Handled by official macos_sign_and_notarize.sh script")

View File

@@ -210,5 +210,4 @@ void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID)
}
/* If the requested type is incorrect, return NULL. */
return NULL;
}
}

View File

@@ -0,0 +1,220 @@
cmake_minimum_required(VERSION 3.22)
# Modern Swift QuickLook Extensions for macOS 15.0+
# This file handles building .appex extensions using Swift
# Configuration options (inherited from parent)
# CMAKE_OSX_DEPLOYMENT_TARGET
# Check for required tools
find_program(SWIFTC_EXECUTABLE swiftc REQUIRED)
find_program(PLUTIL_EXECUTABLE plutil REQUIRED)
# Find macOS SDK
execute_process(
COMMAND xcrun --show-sdk-path --sdk macosx
OUTPUT_VARIABLE MACOS_SDK_PATH
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE SDK_RESULT
)
if(NOT SDK_RESULT EQUAL 0 OR NOT EXISTS "${MACOS_SDK_PATH}")
message(FATAL_ERROR "macOS SDK not found. Please install Xcode Command Line Tools.")
endif()
message(STATUS "Modern QuickLook: Using macOS SDK: ${MACOS_SDK_PATH}")
# Bundle identifiers (inherited from parent)
set(FREECAD_BUNDLE_ID "org.freecad.FreeCAD")
set(THUMBNAIL_BUNDLE_ID "${FREECAD_BUNDLE_ID}.quicklook.thumbnail")
set(PREVIEW_BUNDLE_ID "${FREECAD_BUNDLE_ID}.quicklook.preview")
# Paths (inherited from parent)
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
set(EXTENSIONS_DIR "${TARGET_APP_BUNDLE}/Contents/PlugIns")
# Build directories
set(BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(THUMBNAIL_EXT_DIR "${BUILD_DIR}/FreeCADThumbnailExtension.appex")
set(PREVIEW_EXT_DIR "${BUILD_DIR}/FreeCADPreviewExtension.appex")
# Detect target architecture
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
set(TARGET_ARCH "arm64")
else()
set(TARGET_ARCH "x86_64")
endif()
# Swift compilation flags
set(SWIFT_FLAGS
-target ${TARGET_ARCH}-apple-macosx${CMAKE_OSX_DEPLOYMENT_TARGET}
-sdk ${MACOS_SDK_PATH}
-O
-whole-module-optimization
-enable-library-evolution
-swift-version 5
)
# Function to process Info.plist files
function(process_plist input_plist output_plist bundle_id executable_name product_name principal_class)
add_custom_command(
OUTPUT ${output_plist}
COMMAND ${CMAKE_COMMAND} -E copy ${input_plist} ${output_plist}
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleIdentifier -string ${bundle_id} ${output_plist}
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleExecutable -string ${executable_name} ${output_plist}
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleName -string ${product_name} ${output_plist}
COMMAND ${PLUTIL_EXECUTABLE} -replace NSExtension.NSExtensionPrincipalClass -string ${principal_class} ${output_plist}
DEPENDS ${input_plist}
COMMENT "Processing extension ${input_plist} -> ${output_plist}"
)
endfunction()
# Create bundle directory structure
add_custom_target(create_modern_bundle_dirs ALL
COMMAND ${CMAKE_COMMAND} -E make_directory ${EXTENSIONS_DIR}
COMMAND ${CMAKE_COMMAND} -E make_directory ${THUMBNAIL_EXT_DIR}/Contents/MacOS
COMMAND ${CMAKE_COMMAND} -E make_directory ${THUMBNAIL_EXT_DIR}/Contents/Resources
COMMAND ${CMAKE_COMMAND} -E make_directory ${PREVIEW_EXT_DIR}/Contents/MacOS
COMMAND ${CMAKE_COMMAND} -E make_directory ${PREVIEW_EXT_DIR}/Contents/Resources
COMMENT "Creating modern extensions directory structure"
)
# Process Info.plist files
process_plist(
"${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailExtensionInfo.plist"
"${THUMBNAIL_EXT_DIR}/Contents/Info.plist"
${THUMBNAIL_BUNDLE_ID}
"FreeCADThumbnailExtension"
"FreeCADThumbnailExtension"
"FreeCADThumbnailExtension.ThumbnailProvider"
)
process_plist(
"${CMAKE_CURRENT_SOURCE_DIR}/PreviewExtensionInfo.plist"
"${PREVIEW_EXT_DIR}/Contents/Info.plist"
${PREVIEW_BUNDLE_ID}
"FreeCADPreviewExtension"
"FreeCADPreviewExtension"
"FreeCADPreviewExtension.PreviewProvider"
)
# Compile Thumbnail Extension
add_custom_target(compile_thumbnail_extension ALL
COMMAND ${SWIFTC_EXECUTABLE}
${SWIFT_FLAGS}
-emit-executable
-module-name FreeCADThumbnailExtension
-parse-as-library
-Xlinker -e -Xlinker _NSExtensionMain
-Xlinker -rpath -Xlinker @executable_path/../Frameworks
-Xlinker -rpath -Xlinker @executable_path/../../../../Frameworks
-framework Foundation
-framework CoreGraphics
-framework ImageIO
-framework QuickLookThumbnailing
-framework AppKit
-lcompression
-o ${THUMBNAIL_EXT_DIR}/Contents/MacOS/FreeCADThumbnailExtension
${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailProvider.swift
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
DEPENDS
create_modern_bundle_dirs
${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailProvider.swift
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
${THUMBNAIL_EXT_DIR}/Contents/Info.plist
COMMENT "Compiling Thumbnail Extension"
)
# Compile Preview Extension
add_custom_target(compile_preview_extension ALL
COMMAND ${SWIFTC_EXECUTABLE}
${SWIFT_FLAGS}
-emit-executable
-module-name FreeCADPreviewExtension
-parse-as-library
-Xlinker -e -Xlinker _NSExtensionMain
-Xlinker -rpath -Xlinker @executable_path/../Frameworks
-Xlinker -rpath -Xlinker @executable_path/../../../../Frameworks
-framework Foundation
-framework CoreGraphics
-framework ImageIO
-framework Quartz
-framework Cocoa
-framework UniformTypeIdentifiers
-lcompression
-o ${PREVIEW_EXT_DIR}/Contents/MacOS/FreeCADPreviewExtension
${CMAKE_CURRENT_SOURCE_DIR}/PreviewProvider.swift
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
DEPENDS
create_modern_bundle_dirs
${CMAKE_CURRENT_SOURCE_DIR}/PreviewProvider.swift
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
${PREVIEW_EXT_DIR}/Contents/Info.plist
COMMENT "Compiling Preview Extension"
)
# Embed unsigned extensions in main FreeCAD.app
add_custom_target(embed_modern_extensions ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory
${THUMBNAIL_EXT_DIR}
${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
COMMAND ${CMAKE_COMMAND} -E copy_directory
${PREVIEW_EXT_DIR}
${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
DEPENDS
compile_thumbnail_extension
compile_preview_extension
COMMENT "Embedding modern extensions in main FreeCAD.app"
)
# Main modern extensions target
add_custom_target(FreeCADModernQuickLook ALL
DEPENDS embed_modern_extensions
COMMENT "Modern Swift QuickLook extensions built"
)
# Install targets for modern extensions
install(DIRECTORY ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns"
COMPONENT ModernQuickLook
USE_SOURCE_PERMISSIONS
)
install(DIRECTORY ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns"
COMPONENT ModernQuickLook
USE_SOURCE_PERMISSIONS
)
# Verification target
add_custom_target(verify_modern_build
COMMAND test -f ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension
COMMAND test -f ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension
COMMAND echo "Modern extensions successfully embedded in FreeCAD.app"
DEPENDS FreeCADModernQuickLook
COMMENT "Verifying modern QuickLook extension integration"
)
# Test registration target
add_custom_target(test_modern_registration
COMMAND pluginkit -a ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
COMMAND pluginkit -a ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
COMMAND pluginkit -e use -i ${THUMBNAIL_BUNDLE_ID}
COMMAND pluginkit -e use -i ${PREVIEW_BUNDLE_ID}
COMMAND pluginkit -m -v -i ${THUMBNAIL_BUNDLE_ID} || echo "Thumbnail extension registration status unknown"
COMMAND pluginkit -m -v -i ${PREVIEW_BUNDLE_ID} || echo "Preview extension registration status unknown"
DEPENDS FreeCADModernQuickLook
COMMENT "Testing modern QuickLook extension registration"
)
# Status messages
message(STATUS "Modern Swift QuickLook Extensions Configuration:")
message(STATUS " Swift Compiler: ${SWIFTC_EXECUTABLE}")
message(STATUS " Target Architecture: ${TARGET_ARCH}")
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
message(STATUS " Thumbnail Bundle ID: ${THUMBNAIL_BUNDLE_ID}")
message(STATUS " Preview Bundle ID: ${PREVIEW_BUNDLE_ID}")
message(STATUS " Extensions Directory: ${EXTENSIONS_DIR}")
message(STATUS " Code Signing: Handled by official macos_sign_and_notarize.sh script")

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
<key>com.apple.security.files.downloads.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>QuickLookPreviewExtension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLIsDataBasedPreview</key>
<true/>
<key>QLSupportedContentTypes</key>
<array>
<string>org.freecad.fcstd</string>
</array>
<key>QLSupportsSearchableItems</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.preview</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PreviewProvider</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,77 @@
import Cocoa
import Quartz
import UniformTypeIdentifiers
import os.log
private let logger = Logger(
subsystem: Bundle(for: PreviewProvider.self).bundleIdentifier
?? "org.freecad.quicklook.fallback",
category: "PreviewProvider"
)
class PreviewProvider: QLPreviewProvider, QLPreviewingController {
func providePreview(for request: QLFilePreviewRequest) async throws
-> QLPreviewReply
{
logger.debug(
"--- PreviewProvider: providePreview CALLED for \(request.fileURL.lastPathComponent) ---"
)
let fileURL = request.fileURL
logger.info("Received file URL: \(fileURL.path)")
guard let image = try? SwiftZIPParser.extractThumbnail(from: fileURL) else {
let errorMessage =
"Failed to extract thumbnail from FreeCAD file: \(fileURL.lastPathComponent)"
logger.error("\(errorMessage)")
throw NSError(
domain: Bundle(for: PreviewProvider.self).bundleIdentifier
?? "org.freecad.quicklook.fallback",
code: 1001,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
)
}
logger.info(
"Thumbnail extracted successfully. Image size: \(image.width)x\(image.height)"
)
let imageSize = CGSize(
width: CGFloat(image.width),
height: CGFloat(image.height)
)
logger.debug(
"Preview contextSize will be: \(imageSize.width)x\(imageSize.height)"
)
// Ensure imageSize is valid and positive
if imageSize.width <= 0 || imageSize.height <= 0 {
let errorMessage =
"Cannot create preview with zero or negative dimensions: \(imageSize) for file: \(fileURL.lastPathComponent)"
logger.error("\(errorMessage)")
throw NSError(
domain: Bundle(for: PreviewProvider.self).bundleIdentifier
?? "org.freecad.quicklook.fallback",
code: 1003,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
)
}
let reply = QLPreviewReply(contextSize: imageSize, isBitmap: true) {
context,
_ in
logger.info("Drawing block started. Drawing extracted thumbnail.")
// Draw the extracted thumbnail image
context.draw(image, in: CGRect(origin: .zero, size: imageSize))
logger.debug("Thumbnail image drawn in context.")
logger.info("Drawing block finished.")
return
}
logger.notice("QLPreviewReply created. Returning reply.")
return reply
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
<key>com.apple.security.files.downloads.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>QuickLookThumbnailExtension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLIsDataBasedThumbnail</key>
<true/>
<key>QLSupportedContentTypes</key>
<array>
<string>org.freecad.fcstd</string>
</array>
<key>QLSupportsSearchableItems</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.thumbnail</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ThumbnailProvider</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,42 @@
import AppKit
import QuickLookThumbnailing
import os.log
private let logger = Logger(
subsystem: Bundle(for: ThumbnailProvider.self).bundleIdentifier
?? "org.freecad.quicklook.thumbnail.fallback",
category: "ThumbnailProvider"
)
class ThumbnailProvider: QLThumbnailProvider {
override func provideThumbnail(
for request: QLFileThumbnailRequest,
_ handler: @escaping (QLThumbnailReply?, Error?) -> Void
) {
logger.debug(
"Providing thumbnail for: \(request.fileURL.path)"
)
guard
let cgImage = try? SwiftZIPParser.extractThumbnail(
from: request.fileURL, maxSize: request.maximumSize)
else {
logger.warning("No valid thumbnail found; returning empty reply.")
handler(nil, nil)
return
}
let reply = QLThumbnailReply(
contextSize: request.maximumSize,
currentContextDrawing: { () -> Bool in
let image = NSImage(cgImage: cgImage, size: request.maximumSize)
image.draw(in: CGRect(origin: .zero, size: request.maximumSize))
return true
}
)
handler(reply, nil)
}
}

View File

@@ -0,0 +1,474 @@
//
// ZipExtractor.swift
// FreeCAD QuickLook Swift Implementation
//
// Pure Swift ZIP parser for extracting thumbnails from FreeCAD (.FCStd) files
// This removes external dependencies while maintaining modern Swift APIs
//
// Created for integration with FreeCAD upstream
//
import Compression
import CoreGraphics
import Foundation
import ImageIO
import os.log
// MARK: - ZIP File Format Constants
private struct ZIPConstants {
static let localFileSignature: UInt32 = 0x0403_4b50
static let centralDirSignature: UInt32 = 0x0201_4b50
static let endOfCentralDirSignature: UInt32 = 0x0605_4b50
static let compressionStored: UInt16 = 0
static let compressionDeflate: UInt16 = 8
}
// MARK: - ZIP Structures
private struct ZIPLocalFileHeader {
let signature: UInt32
let version: UInt16
let flags: UInt16
let compression: UInt16
let modTime: UInt16
let modDate: UInt16
let crc32: UInt32
let compressedSize: UInt32
let uncompressedSize: UInt32
let filenameLength: UInt16
let extraFieldLength: UInt16
static let size = 30
}
private struct ZIPCentralDirHeader {
let signature: UInt32
let versionMadeBy: UInt16
let versionNeeded: UInt16
let flags: UInt16
let compression: UInt16
let modTime: UInt16
let modDate: UInt16
let crc32: UInt32
let compressedSize: UInt32
let uncompressedSize: UInt32
let filenameLength: UInt16
let extraFieldLength: UInt16
let commentLength: UInt16
let diskNumber: UInt16
let internalAttributes: UInt16
let externalAttributes: UInt32
let localHeaderOffset: UInt32
static let size = 46
}
private struct ZIPEndOfCentralDir {
let signature: UInt32
let diskNumber: UInt16
let centralDirDisk: UInt16
let entriesOnDisk: UInt16
let totalEntries: UInt16
let centralDirSize: UInt32
let centralDirOffset: UInt32
let commentLength: UInt16
static let size = 22
}
// MARK: - Error Types
enum ZIPParserError: Error, LocalizedError {
case fileNotFound
case invalidZipFile
case corruptedZipFile
case thumbnailNotFound
case compressionUnsupported
case decompressionFailed
var errorDescription: String? {
switch self {
case .fileNotFound:
return "FCStd file not found"
case .invalidZipFile:
return "Invalid ZIP file format"
case .corruptedZipFile:
return "Corrupted ZIP file"
case .thumbnailNotFound:
return "No thumbnail found in FCStd file"
case .compressionUnsupported:
return "Unsupported compression method"
case .decompressionFailed:
return "Failed to decompress file data"
}
}
}
// MARK: - Pure Swift ZIP Parser
struct SwiftZIPParser {
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "org.freecad.quicklook",
category: "SwiftZIPParser"
)
/// Extract thumbnail from FCStd file
static func extractThumbnail(from fileURL: URL, maxSize: CGSize? = nil) throws -> CGImage {
logger.debug("Extracting thumbnail from file: \(fileURL.path)")
logger.info("=== SwiftZIPParser.extractThumbnail called ===")
logger.info("File URL: \(fileURL.path)")
logger.info("File exists: \(FileManager.default.fileExists(atPath: fileURL.path))")
do {
let isReachable = try fileURL.checkResourceIsReachable()
logger.info("File is readable: \(isReachable)")
} catch {
logger.info("File is readable: false (error: \(error.localizedDescription))")
}
// Handle security scoped resources
let didStartAccessing = fileURL.startAccessingSecurityScopedResource()
logger.info("Started accessing security scoped resource: \(didStartAccessing)")
defer {
if didStartAccessing {
fileURL.stopAccessingSecurityScopedResource()
logger.debug("Stopped accessing security scoped resource")
}
}
// Read file data
let zipData: Data
do {
zipData = try Data(contentsOf: fileURL, options: .mappedIfSafe)
logger.info("Successfully read \(zipData.count) bytes from file")
} catch {
logger.error(
"Failed to read file data: \(error.localizedDescription)")
logger.error("Failed to read file data: \(error.localizedDescription)")
throw ZIPParserError.fileNotFound
}
return try extractThumbnail(from: zipData, maxSize: maxSize)
}
/// Extract thumbnail from ZIP data
static func extractThumbnail(from zipData: Data, maxSize: CGSize? = nil) throws -> CGImage {
logger.info("=== Processing ZIP data ===")
logger.info("ZIP data size: \(zipData.count) bytes")
guard zipData.count >= ZIPLocalFileHeader.size else {
logger.error("ZIP data too small")
logger.error("ZIP data too small: \(zipData.count) < \(ZIPLocalFileHeader.size)")
throw ZIPParserError.invalidZipFile
}
// Verify ZIP signature
let signature = zipData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
logger.info("ZIP signature: 0x\(String(signature, radix: 16))")
guard signature == ZIPConstants.localFileSignature else {
logger.error("Invalid ZIP signature: 0x\(String(signature, radix: 16))")
logger.error(
"Invalid ZIP signature: 0x\(String(signature, radix: 16)), expected: 0x\(String(ZIPConstants.localFileSignature, radix: 16))"
)
throw ZIPParserError.invalidZipFile
}
// Find end of central directory
logger.info("Searching for end of central directory...")
guard let endOfCentralDir = findEndOfCentralDirectory(in: zipData) else {
logger.error("Could not find end of central directory")
throw ZIPParserError.corruptedZipFile
}
logger.info("Found end of central directory with \(endOfCentralDir.totalEntries) entries")
// Look for thumbnail files
let thumbnailPaths = ["thumbnails/Thumbnail.png", "Thumbnail.png"]
logger.info("Searching for thumbnail files: \(thumbnailPaths)")
for thumbnailPath in thumbnailPaths {
logger.info("Trying path: \(thumbnailPath)")
if let thumbnailData = try? extractFile(
from: zipData,
endOfCentralDir: endOfCentralDir,
filename: thumbnailPath
) {
logger.debug("Found thumbnail at path: \(thumbnailPath)")
logger.info(
"Found thumbnail at path: \(thumbnailPath), size: \(thumbnailData.count) bytes")
if let image = createImage(from: thumbnailData, maxSize: maxSize) {
logger.debug("Successfully created CGImage from thumbnail data")
logger.info("Successfully created CGImage from thumbnail data")
return image
} else {
logger.warning(
"Failed to create CGImage from thumbnail data at path: \(thumbnailPath)")
}
} else {
logger.info("No thumbnail found at path: \(thumbnailPath)")
}
}
logger.info("No thumbnail found in FCStd file")
logger.warning("No valid thumbnail found in FCStd file")
throw ZIPParserError.thumbnailNotFound
}
/// Validate if file is a valid FCStd (ZIP) file
static func isValidFCStdFile(at url: URL) -> Bool {
guard let headerData = try? Data(contentsOf: url, options: .uncached),
headerData.count >= 4
else {
return false
}
let signature = headerData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
return signature == ZIPConstants.localFileSignature
}
}
// MARK: - Private Extensions
extension SwiftZIPParser {
/// Find the end of central directory record
private static func findEndOfCentralDirectory(in data: Data) -> ZIPEndOfCentralDir? {
// Search backwards from the end of the file
let minOffset = max(0, data.count - 65557) // Max comment length + EOCD size
for offset in stride(from: data.count - ZIPEndOfCentralDir.size, through: minOffset, by: -1)
{
let signature = readUInt32(from: data, at: offset)
if signature == ZIPConstants.endOfCentralDirSignature {
return parseEndOfCentralDir(from: data, at: offset)
}
}
return nil
}
/// Parse end of central directory record
private static func parseEndOfCentralDir(from data: Data, at offset: Int) -> ZIPEndOfCentralDir
{
return ZIPEndOfCentralDir(
signature: readUInt32(from: data, at: offset),
diskNumber: readUInt16(from: data, at: offset + 4),
centralDirDisk: readUInt16(from: data, at: offset + 6),
entriesOnDisk: readUInt16(from: data, at: offset + 8),
totalEntries: readUInt16(from: data, at: offset + 10),
centralDirSize: readUInt32(from: data, at: offset + 12),
centralDirOffset: readUInt32(from: data, at: offset + 16),
commentLength: readUInt16(from: data, at: offset + 20)
)
}
/// Extract a specific file from the ZIP archive
private static func extractFile(
from zipData: Data,
endOfCentralDir: ZIPEndOfCentralDir,
filename: String
) throws -> Data {
logger.debug("Searching for file: \(filename)")
// Search through central directory
var offset = Int(endOfCentralDir.centralDirOffset)
for _ in 0..<endOfCentralDir.totalEntries {
let centralHeader = parseCentralDirHeader(from: zipData, at: offset)
// Extract filename
let filenameStart = offset + ZIPCentralDirHeader.size
let filenameData = zipData.subdata(
in: filenameStart..<(filenameStart + Int(centralHeader.filenameLength)))
if let foundFilename = String(data: filenameData, encoding: .utf8),
foundFilename == filename
{
logger.debug("Found matching file: \(foundFilename)")
return try extractFileData(from: zipData, centralHeader: centralHeader)
}
// Move to next entry
offset +=
ZIPCentralDirHeader.size + Int(centralHeader.filenameLength)
+ Int(centralHeader.extraFieldLength) + Int(centralHeader.commentLength)
}
throw ZIPParserError.thumbnailNotFound
}
/// Parse central directory file header
private static func parseCentralDirHeader(from data: Data, at offset: Int)
-> ZIPCentralDirHeader
{
return ZIPCentralDirHeader(
signature: readUInt32(from: data, at: offset),
versionMadeBy: readUInt16(from: data, at: offset + 4),
versionNeeded: readUInt16(from: data, at: offset + 6),
flags: readUInt16(from: data, at: offset + 8),
compression: readUInt16(from: data, at: offset + 10),
modTime: readUInt16(from: data, at: offset + 12),
modDate: readUInt16(from: data, at: offset + 14),
crc32: readUInt32(from: data, at: offset + 16),
compressedSize: readUInt32(from: data, at: offset + 20),
uncompressedSize: readUInt32(from: data, at: offset + 24),
filenameLength: readUInt16(from: data, at: offset + 28),
extraFieldLength: readUInt16(from: data, at: offset + 30),
commentLength: readUInt16(from: data, at: offset + 32),
diskNumber: readUInt16(from: data, at: offset + 34),
internalAttributes: readUInt16(from: data, at: offset + 36),
externalAttributes: readUInt32(from: data, at: offset + 38),
localHeaderOffset: readUInt32(from: data, at: offset + 42)
)
}
/// Extract file data using central directory header
private static func extractFileData(from zipData: Data, centralHeader: ZIPCentralDirHeader)
throws -> Data
{
let localHeaderOffset = Int(centralHeader.localHeaderOffset)
let localHeader = parseLocalFileHeader(from: zipData, at: localHeaderOffset)
guard localHeader.signature == ZIPConstants.localFileSignature else {
throw ZIPParserError.corruptedZipFile
}
let dataStart =
localHeaderOffset + ZIPLocalFileHeader.size + Int(localHeader.filenameLength)
+ Int(localHeader.extraFieldLength)
let dataEnd = dataStart + Int(localHeader.compressedSize)
guard dataEnd <= zipData.count else {
throw ZIPParserError.corruptedZipFile
}
let compressedData = zipData.subdata(in: dataStart..<dataEnd)
switch localHeader.compression {
case ZIPConstants.compressionStored:
return compressedData
case ZIPConstants.compressionDeflate:
return try deflateDecompress(
data: compressedData, expectedSize: Int(localHeader.uncompressedSize))
default:
throw ZIPParserError.compressionUnsupported
}
}
/// Parse local file header
private static func parseLocalFileHeader(from data: Data, at offset: Int) -> ZIPLocalFileHeader
{
return ZIPLocalFileHeader(
signature: readUInt32(from: data, at: offset),
version: readUInt16(from: data, at: offset + 4),
flags: readUInt16(from: data, at: offset + 6),
compression: readUInt16(from: data, at: offset + 8),
modTime: readUInt16(from: data, at: offset + 10),
modDate: readUInt16(from: data, at: offset + 12),
crc32: readUInt32(from: data, at: offset + 14),
compressedSize: readUInt32(from: data, at: offset + 18),
uncompressedSize: readUInt32(from: data, at: offset + 22),
filenameLength: readUInt16(from: data, at: offset + 26),
extraFieldLength: readUInt16(from: data, at: offset + 28)
)
}
/// Decompress deflate-compressed data
private static func deflateDecompress(data: Data, expectedSize: Int) throws -> Data {
guard expectedSize > 0 else {
throw ZIPParserError.decompressionFailed
}
return try data.withUnsafeBytes { bytes in
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: expectedSize)
defer { buffer.deallocate() }
let actualSize = compression_decode_buffer(
buffer, expectedSize,
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
nil, COMPRESSION_ZLIB
)
guard actualSize == expectedSize else {
throw ZIPParserError.decompressionFailed
}
return Data(bytes: buffer, count: expectedSize)
}
}
/// Create CGImage from PNG data with optional size constraints
private static func createImage(from pngData: Data, maxSize: CGSize? = nil) -> CGImage? {
guard let dataProvider = CGDataProvider(data: pngData as CFData),
let image = CGImage(
pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: true,
intent: .defaultIntent)
else {
return nil
}
// If no max size specified, return original
guard let maxSize = maxSize else {
return image
}
// Calculate scaled size
let originalSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height))
let scaledSize = scaleToFit(originalSize: originalSize, maxSize: maxSize)
// If no scaling needed, return original
if scaledSize == originalSize {
return image
}
// Scale the image
return scaleImage(image, to: scaledSize)
}
/// Safely read UInt32 from Data at offset
fileprivate static func readUInt32(from data: Data, at offset: Int) -> UInt32 {
let subdata = data.subdata(in: offset..<(offset + 4))
return subdata.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
}
/// Safely read UInt16 from Data at offset
fileprivate static func readUInt16(from data: Data, at offset: Int) -> UInt16 {
let subdata = data.subdata(in: offset..<(offset + 2))
return subdata.withUnsafeBytes { $0.loadUnaligned(as: UInt16.self) }
}
/// Calculate size that fits within maxSize while maintaining aspect ratio
fileprivate static func scaleToFit(originalSize: CGSize, maxSize: CGSize) -> CGSize {
let widthRatio = maxSize.width / originalSize.width
let heightRatio = maxSize.height / originalSize.height
let scaleFactor = min(widthRatio, heightRatio)
return CGSize(
width: originalSize.width * scaleFactor,
height: originalSize.height * scaleFactor
)
}
/// Scale CGImage to specified size
fileprivate static func scaleImage(_ image: CGImage, to size: CGSize) -> CGImage? {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
guard
let context = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue
)
else {
logger.error("Failed to create CGContext for image scaling")
return nil
}
context.interpolationQuality = .high
context.draw(image, in: CGRect(origin: .zero, size: size))
return context.makeImage()
}
}

View File

@@ -0,0 +1,371 @@
#!/bin/bash
# FreeCAD QuickLook Extensions - Integration Test Script
# This script tests the QuickLook extension integration in FreeCAD.app
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Look for FreeCAD.app in common install locations
if [[ -d "${SCRIPT_DIR}/../../../FreeCAD.app" ]]; then
FREECAD_APP="${SCRIPT_DIR}/../../../FreeCAD.app"
elif [[ -d "${SCRIPT_DIR}/../../../../FreeCAD.app" ]]; then
FREECAD_APP="${SCRIPT_DIR}/../../../../FreeCAD.app"
else
# Default to relative path from script
FREECAD_APP="${SCRIPT_DIR}/../../../FreeCAD.app"
fi
EXTENSIONS_DIR="${FREECAD_APP}/Contents/PlugIns"
THUMBNAIL_EXT="${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex"
PREVIEW_EXT="${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex"
THUMBNAIL_BUNDLE_ID="org.freecad.FreeCAD.quicklook.thumbnail"
PREVIEW_BUNDLE_ID="org.freecad.FreeCAD.quicklook.preview"
# Function to print colored output
print_status() {
local status=$1
local message=$2
case $status in
"OK")
echo -e "${GREEN}${NC} $message"
;;
"FAIL")
echo -e "${RED}${NC} $message"
;;
"WARN")
echo -e "${YELLOW}${NC} $message"
;;
"INFO")
echo -e "${BLUE}${NC} $message"
;;
esac
}
# Main test function
main() {
echo "FreeCAD QuickLook Extensions - Integration Test"
echo "=============================================="
echo "Testing app at: $FREECAD_APP"
echo
local total_tests=0
local passed_tests=0
# Test 1: Check if FreeCAD.app exists
((total_tests++))
if [[ -d "$FREECAD_APP" ]]; then
print_status "OK" "FreeCAD.app exists at: $FREECAD_APP"
((passed_tests++))
else
print_status "FAIL" "FreeCAD.app not found at: $FREECAD_APP"
print_status "INFO" "Please build and install FreeCAD first with: make install"
exit 1
fi
# Test 2: Check if main FreeCAD executable exists
((total_tests++))
if [[ -f "$FREECAD_APP/Contents/MacOS/FreeCAD" ]]; then
print_status "OK" "FreeCAD executable exists"
((passed_tests++))
else
print_status "FAIL" "FreeCAD executable not found"
fi
# Test 3: Check if PlugIns directory exists
((total_tests++))
if [[ -d "$EXTENSIONS_DIR" ]]; then
print_status "OK" "Extensions directory exists: $EXTENSIONS_DIR"
((passed_tests++))
else
print_status "FAIL" "Extensions directory not found: $EXTENSIONS_DIR"
print_status "INFO" "QuickLook extensions may not have been built. Check cmake configuration."
fi
# Test 4: Check if thumbnail extension exists
((total_tests++))
if [[ -d "$THUMBNAIL_EXT" ]]; then
print_status "OK" "Thumbnail extension exists"
((passed_tests++))
else
print_status "FAIL" "Thumbnail extension not found: $THUMBNAIL_EXT"
fi
# Test 5: Check if preview extension exists
((total_tests++))
if [[ -d "$PREVIEW_EXT" ]]; then
print_status "OK" "Preview extension exists"
((passed_tests++))
else
print_status "FAIL" "Preview extension not found: $PREVIEW_EXT"
fi
# Test 6: Check if thumbnail extension executable exists
((total_tests++))
if [[ -f "$THUMBNAIL_EXT/Contents/MacOS/FreeCADThumbnailExtension" ]]; then
print_status "OK" "Thumbnail extension executable exists"
((passed_tests++))
else
print_status "FAIL" "Thumbnail extension executable not found"
fi
# Test 7: Check if preview extension executable exists
((total_tests++))
if [[ -f "$PREVIEW_EXT/Contents/MacOS/FreeCADPreviewExtension" ]]; then
print_status "OK" "Preview extension executable exists"
((passed_tests++))
else
print_status "FAIL" "Preview extension executable not found"
fi
# Test 8: Check if thumbnail extension Info.plist exists
((total_tests++))
if [[ -f "$THUMBNAIL_EXT/Contents/Info.plist" ]]; then
print_status "OK" "Thumbnail extension Info.plist exists"
((passed_tests++))
else
print_status "FAIL" "Thumbnail extension Info.plist not found"
fi
# Test 9: Check if preview extension Info.plist exists
((total_tests++))
if [[ -f "$PREVIEW_EXT/Contents/Info.plist" ]]; then
print_status "OK" "Preview extension Info.plist exists"
((passed_tests++))
else
print_status "FAIL" "Preview extension Info.plist not found"
fi
# Test 10: Check thumbnail extension bundle ID
((total_tests++))
if [[ -f "$THUMBNAIL_EXT/Contents/Info.plist" ]]; then
local bundle_id=$(plutil -extract CFBundleIdentifier raw "$THUMBNAIL_EXT/Contents/Info.plist" 2>/dev/null)
if [[ "$bundle_id" == "$THUMBNAIL_BUNDLE_ID" ]]; then
print_status "OK" "Thumbnail extension has correct bundle ID: $bundle_id"
((passed_tests++))
else
print_status "FAIL" "Thumbnail extension bundle ID incorrect: $bundle_id (expected: $THUMBNAIL_BUNDLE_ID)"
fi
fi
# Test 11: Check preview extension bundle ID
((total_tests++))
if [[ -f "$PREVIEW_EXT/Contents/Info.plist" ]]; then
local bundle_id=$(plutil -extract CFBundleIdentifier raw "$PREVIEW_EXT/Contents/Info.plist" 2>/dev/null)
if [[ "$bundle_id" == "$PREVIEW_BUNDLE_ID" ]]; then
print_status "OK" "Preview extension has correct bundle ID: $bundle_id"
((passed_tests++))
else
print_status "FAIL" "Preview extension bundle ID incorrect: $bundle_id (expected: $PREVIEW_BUNDLE_ID)"
fi
fi
# Test 12: Basic FreeCAD launch test
((total_tests++))
print_status "INFO" "Testing FreeCAD launch (--version)..."
if timeout 10 "$FREECAD_APP/Contents/MacOS/FreeCAD" --version >/dev/null 2>&1; then
print_status "OK" "FreeCAD launches successfully"
((passed_tests++))
else
print_status "FAIL" "FreeCAD failed to launch or crashed"
print_status "WARN" "This will prevent QuickLook registration from working"
fi
# Extension signing tests (optional - don't count toward pass/fail)
echo
print_status "INFO" "Code Signing Status:"
if codesign -v "$THUMBNAIL_EXT" >/dev/null 2>&1; then
print_status "OK" "Thumbnail extension is signed"
else
print_status "WARN" "Thumbnail extension is unsigned (normal for development builds)"
fi
if codesign -v "$PREVIEW_EXT" >/dev/null 2>&1; then
print_status "OK" "Preview extension is signed"
else
print_status "WARN" "Preview extension is unsigned (normal for development builds)"
fi
# App signing status
if codesign -v "$FREECAD_APP" >/dev/null 2>&1; then
print_status "OK" "FreeCAD.app is signed"
else
print_status "WARN" "FreeCAD.app is unsigned (normal for development builds)"
fi
# Optional tests (don't count toward pass/fail)
echo
print_status "INFO" "Additional Information:"
# Show signing details if available
if command -v codesign >/dev/null 2>&1; then
echo
print_status "INFO" "Signing Details:"
echo " FreeCAD.app:"
codesign -dv "$FREECAD_APP" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
echo " Thumbnail Extension:"
codesign -dv "$THUMBNAIL_EXT" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
echo " Preview Extension:"
codesign -dv "$PREVIEW_EXT" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
fi
# Show current registration status (if pluginkit is available)
if command -v pluginkit >/dev/null 2>&1; then
echo
print_status "INFO" "Current Registration Status:"
if pluginkit -m -v -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1; then
print_status "OK" "Thumbnail extension is registered with system"
else
print_status "WARN" "Thumbnail extension not registered (normal before first successful FreeCAD launch)"
fi
if pluginkit -m -v -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1; then
print_status "OK" "Preview extension is registered with system"
else
print_status "WARN" "Preview extension not registered (normal before first successful FreeCAD launch)"
fi
fi
# Check for gatekeeper issues
echo
print_status "INFO" "Security Status:"
if command -v spctl >/dev/null 2>&1; then
if spctl -a -v "$FREECAD_APP" >/dev/null 2>&1; then
print_status "OK" "FreeCAD.app passes Gatekeeper checks"
else
print_status "WARN" "FreeCAD.app rejected by Gatekeeper (normal for unsigned development builds)"
print_status "INFO" "You may need to: sudo xattr -rd com.apple.quarantine '$FREECAD_APP'"
fi
fi
# Check for quarantine attributes
if xattr "$FREECAD_APP" 2>/dev/null | grep -q quarantine; then
print_status "WARN" "FreeCAD.app has quarantine attributes"
print_status "INFO" "Remove with: sudo xattr -rd com.apple.quarantine '$FREECAD_APP'"
else
print_status "OK" "No quarantine attributes found"
fi
# Summary
echo
echo "Test Results:"
echo "============"
echo "Passed: $passed_tests/$total_tests core tests"
if [[ $passed_tests -eq $total_tests ]]; then
print_status "OK" "All core tests passed! QuickLook extensions are properly built and integrated."
echo
echo "Next Steps:"
echo " 1. Ensure FreeCAD launches successfully to trigger extension registration"
echo " 2. Test QuickLook functionality with .FCStd files in Finder"
echo " 3. Look for system notification about Quick Look extensions being added"
return 0
else
print_status "FAIL" "Some core tests failed. Please check the build configuration."
echo
echo "Troubleshooting:"
echo " 1. Ensure you're using Unix Makefiles generator: cmake -G 'Unix Makefiles'"
echo " 2. Check that FREECAD_CREATE_MAC_APP=ON in cmake configuration"
echo " 3. Run 'make install' to build and install FreeCAD with QuickLook extensions"
echo " 4. If FreeCAD crashes, try removing quarantine attributes or ad-hoc signing"
return 1
fi
}
# Test registration functionality (optional)
test_registration() {
if [[ "$1" == "--test-registration" ]]; then
echo
print_status "INFO" "Testing extension registration..."
if command -v pluginkit >/dev/null 2>&1; then
print_status "INFO" "Attempting to register extensions manually..."
# Try to register extensions
if pluginkit -a "$THUMBNAIL_EXT" >/dev/null 2>&1; then
print_status "OK" "Thumbnail extension registration command succeeded"
else
print_status "WARN" "Thumbnail registration command failed (may already be registered)"
fi
if pluginkit -a "$PREVIEW_EXT" >/dev/null 2>&1; then
print_status "OK" "Preview extension registration command succeeded"
else
print_status "WARN" "Preview registration command failed (may already be registered)"
fi
# Try to enable extensions
pluginkit -e use -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1 || print_status "WARN" "Thumbnail activation command failed"
pluginkit -e use -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1 || print_status "WARN" "Preview activation command failed"
sleep 2 # Give system time to process
# Check final status
if pluginkit -m -v -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1; then
print_status "OK" "Thumbnail extension successfully registered and active"
else
print_status "FAIL" "Thumbnail extension registration failed"
fi
if pluginkit -m -v -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1; then
print_status "OK" "Preview extension successfully registered and active"
else
print_status "FAIL" "Preview extension registration failed"
fi
echo
print_status "INFO" "Try testing with a .FCStd file in Finder now"
else
print_status "WARN" "pluginkit not available for registration testing"
fi
fi
}
# Show usage information
show_usage() {
echo "Usage: $0 [--test-registration] [--help]"
echo
echo "Options:"
echo " --test-registration Also test extension registration with pluginkit"
echo " --help Show this help message"
echo
echo "This script tests the QuickLook extension integration in FreeCAD.app."
echo "Run this after building and installing FreeCAD: 'make install'"
echo
echo "The script will look for FreeCAD.app in common install locations relative to the script."
}
# Handle command line arguments
case "${1:-}" in
--help)
show_usage
exit 0
;;
--test-registration)
main
test_registration "$1"
;;
"")
main
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac