diff --git a/.github/workflows/build-dmg-qt6-macos.yml b/.github/workflows/build-dmg-qt6-macos.yml new file mode 100644 index 00000000..4f04ebae --- /dev/null +++ b/.github/workflows/build-dmg-qt6-macos.yml @@ -0,0 +1,149 @@ +name: Build macOS DMG (Qt6, Universal) + +on: + push: + tags: + - "*" + workflow_dispatch: + inputs: + branch: + description: 'Checkout branch' + required: false + default: 'dev' + tag: + description: 'Checkout tag' + required: false + +permissions: + contents: write + +jobs: + build-omodsim: + name: Build OpenModSim macOS DMG version '${{ github.event.inputs.tag || github.event.inputs.branch || github.ref_name }}' with Qt6 + runs-on: macos-latest + + env: + QT_VERSION: "6.9.3" + QT_HOST: "mac" + QT_TARGET: "desktop" + QT_ARCH: "clang_64" + QT_INSTALL_DIR: "${{ github.workspace }}/Qt" + CMAKE_GENERATOR: "Ninja" + BUILD_TYPE: "Release" + DMG_PACKAGE_NAME: "qt6-omodsim" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.event.inputs.branch || github.ref_name }} + + - name: Determine ref_type and ref_name + run: | + if [ "${{ github.ref_type }}" = "tag" ] || [ -n "${{ github.event.inputs.tag }}" ]; then + REF_TYPE="tags" + if [ -n "${{ github.event.inputs.tag }}" ]; then + REF_NAME="${{ github.event.inputs.tag }}" + else + REF_NAME="${{ github.ref_name }}" + fi + else + REF_TYPE="heads" + REF_NAME="${{ github.ref_name }}" + fi + echo "REF_TYPE=$REF_TYPE" >> $GITHUB_ENV + echo "REF_NAME=$REF_NAME" >> $GITHUB_ENV + + - name: Extract version from CMakeLists.txt + run: | + FULL_VERSION=$(grep -oE 'VERSION\s+[0-9]+\.[0-9]+\.[0-9]+' src/CMakeLists.txt | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + + if [ "${GITHUB_REF_NAME}" = "dev" ] || [ "${{ github.event.inputs.branch }}" = "dev" ]; then + MAJOR_MINOR=$(echo "$FULL_VERSION" | cut -d. -f1,2) + APP_VERSION="${MAJOR_MINOR}~dev" + else + APP_VERSION="$FULL_VERSION" + fi + + echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV + echo "Extracted version: $APP_VERSION" + + - name: Install dependencies + run: | + brew install ninja cmake + + - name: Install Python (for aqtinstall) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install aqtinstall + run: python -m pip install aqtinstall + + - name: Download Qt + run: | + mkdir -p ${{ env.QT_INSTALL_DIR }} + aqt install-qt \ + ${{ env.QT_HOST }} \ + ${{ env.QT_TARGET }} \ + ${{ env.QT_VERSION }} \ + ${{ env.QT_ARCH }} \ + -m qt5compat qtpdf qtserialport qtserialbus \ + -O ${{ env.QT_INSTALL_DIR }} + + - name: Add Qt to PATH + run: echo "${{ env.QT_INSTALL_DIR }}/${{ env.QT_VERSION }}/macos/bin" >> $GITHUB_PATH + + - name: Set BUILD_DIR + run: echo "BUILD_DIR=build-omodsim-Qt_${{ env.QT_VERSION }}_clang_64-${{ env.BUILD_TYPE }}" >> $GITHUB_ENV + + - name: Configure project + run: | + cmake src -B ${{ env.BUILD_DIR }} \ + -G "${{ env.CMAKE_GENERATOR }}" \ + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DCMAKE_PREFIX_PATH=${{ env.QT_INSTALL_DIR }}/${{ env.QT_VERSION }}/macos \ + -DUSE_QT6=ON + + - name: Build + run: cmake --build ${{ env.BUILD_DIR }} --config ${{ env.BUILD_TYPE }} --parallel + + - name: Deploy Qt dependencies using macdeployqt + run: | + "${{ env.QT_INSTALL_DIR }}/${{ env.QT_VERSION }}/macos/bin/macdeployqt" \ + "${{ env.BUILD_DIR }}/omodsim.app" \ + -always-overwrite + + - name: Copy docs into app bundle + run: | + mkdir -p "${{ env.BUILD_DIR }}/omodsim.app/Contents/Resources/docs" + cp -R "${{ env.BUILD_DIR }}/docs/"* "${{ env.BUILD_DIR }}/omodsim.app/Contents/Resources/docs/" + + - name: Create DMG + run: | + DMG_NAME="${{ env.DMG_PACKAGE_NAME }}_${{ env.APP_VERSION }}_macos.dmg" + echo "DMG_NAME=$DMG_NAME" >> $GITHUB_ENV + + hdiutil create -volname "Open ModSim ${{ env.APP_VERSION }}" \ + -srcfolder "${{ env.BUILD_DIR }}/omodsim.app" \ + -ov -format UDZO \ + "$DMG_NAME" + + - name: Upload DMG + uses: actions/upload-artifact@v4 + if: success() + with: + name: ${{ env.DMG_NAME }} + path: ${{ env.DMG_NAME }} + + - name: Create or update GitHub Release and upload DMG + if: success() && github.event_name == 'push' && github.ref_type == 'tag' + uses: softprops/action-gh-release@v2 + with: + draft: true + tag_name: ${{ github.ref_name }} + name: Open ModSim ${{ env.APP_VERSION }} + files: | + ${{ env.DMG_NAME }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index cfd0228b..649df8e5 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Script.onInit(init); ``` # Building - Building is available via cmake (with installed Qt version 5.15 and above) or Qt Creator. Supports both OS Microsoft Windows and Linux. + Building is available via cmake (with installed Qt version 5.15 and above) or Qt Creator. Supports Microsoft Windows, Linux and macOS. ## Microsoft Windows Building @@ -176,10 +176,41 @@ cd OpenModSim If you need to specify Qt framework major version (5 or 6), you can do it in the parameters - `./build.sh -qt5` or `./build.sh -qt6` +## macOS Building + +The minimum supported version of macOS for building OpenModSim from sources is macOS 11 (Big Sur). + +1. Install [Homebrew](https://brew.sh) if not already installed +2. Install required dependencies: +```bash +brew install qt@6 cmake ninja +``` +3. Clone OpenModSim sources from github repository: +```bash +git clone https://github.com/sanny32/OpenModSim.git +``` +4. Go to OpenModSim folder: +```bash +cd OpenModSim +``` +5. Run the build script: +```bash +./build-macos.sh +``` + +If you need to specify Qt framework major version (5 or 6), you can do it in the parameters + - `./build-macos.sh -qt5` or `./build-macos.sh -qt6` + +The build script generates a macOS application bundle (`omodsim.app`). To run the application: +```bash +open build-omodsim-Qt_*/omodsim.app +``` + # About supported operating systems The following minimum operating system versions are supported for OpenModSim: +- **macOS 11 (Big Sur)** and later (Intel and Apple Silicon) - **Microsoft Windows 7** - **Debian Linux 11** - **Ubuntu Linux 22.04** diff --git a/build-macos.sh b/build-macos.sh new file mode 100755 index 00000000..6064e6f0 --- /dev/null +++ b/build-macos.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -e + +echo "==================================" +echo " OpenModSim build script (macOS) " +echo "==================================" +echo "" + +# ========================== +# Check macOS +# ========================== +if [[ "$(uname)" != "Darwin" ]]; then + echo "Error: This script is for macOS only." + exit 1 +fi + +# ========================== +# Parse script arguments +# ========================== +QT_CHOICE="" +for arg in "$@"; do + case "$arg" in + -qt5|qt5) + QT_CHOICE="qt5" + ;; + -qt6|qt6) + QT_CHOICE="qt6" + ;; + *) + ;; + esac +done + +if [ -z "$QT_CHOICE" ]; then + QT_CHOICE="qt6" +fi + +# ========================== +# Check Xcode Command Line Tools +# ========================== +echo "Checking for Xcode Command Line Tools..." +if ! xcode-select -p >/dev/null 2>&1; then + echo "Error: Xcode Command Line Tools not found." + echo "Install with: xcode-select --install" + exit 1 +fi +echo " Found: $(xcode-select -p)" + +# ========================== +# Check Homebrew +# ========================== +echo "Checking for Homebrew..." +if ! command -v brew >/dev/null 2>&1; then + echo "Error: Homebrew not found." + echo "Install from: https://brew.sh" + exit 1 +fi +echo " Found: $(brew --prefix)" + +# ========================== +# Check CMake and Ninja +# ========================== +echo "Checking for CMake..." +if ! command -v cmake >/dev/null 2>&1; then + echo "CMake not found. Installing..." + brew install cmake +fi +echo " Found: $(cmake --version | head -1)" + +echo "Checking for Ninja..." +if ! command -v ninja >/dev/null 2>&1; then + echo "Ninja not found. Installing..." + brew install ninja +fi +echo " Found: ninja $(ninja --version)" + +# ========================== +# Check Qt +# ========================== +echo "Checking for Qt..." + +QT_PREFIX="" +QT_VERSION="" + +if [ "$QT_CHOICE" = "qt6" ]; then + QT_PREFIX="$(brew --prefix qt@6 2>/dev/null || brew --prefix qt 2>/dev/null || true)" + if [ -z "$QT_PREFIX" ] || [ ! -d "$QT_PREFIX" ]; then + echo "Qt6 not found. Installing..." + brew install qt@6 + QT_PREFIX="$(brew --prefix qt@6 2>/dev/null || brew --prefix qt 2>/dev/null)" + fi + QT_VERSION=$("${QT_PREFIX}/bin/qmake" -query QT_VERSION 2>/dev/null || true) +elif [ "$QT_CHOICE" = "qt5" ]; then + QT_PREFIX="$(brew --prefix qt@5 2>/dev/null || true)" + if [ -z "$QT_PREFIX" ] || [ ! -d "$QT_PREFIX" ]; then + echo "Qt5 not found. Installing..." + brew install qt@5 + QT_PREFIX="$(brew --prefix qt@5 2>/dev/null)" + fi + QT_VERSION=$("${QT_PREFIX}/bin/qmake" -query QT_VERSION 2>/dev/null || true) +fi + +if [ -z "$QT_VERSION" ]; then + echo "Error: Cannot detect Qt version from ${QT_PREFIX}" + exit 1 +fi + +echo " Found: Qt ${QT_VERSION} at ${QT_PREFIX}" + +# ========================== +# Check minimum Qt version +# ========================== +MIN_QT_VERSION="5.15.0" +verlte() { + [ "$1" = "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" ] +} +verlt() { + [ "$1" != "$2" ] && verlte "$1" "$2" +} + +if verlt "$QT_VERSION" "$MIN_QT_VERSION"; then + echo "Error: Qt >= $MIN_QT_VERSION is required, but found $QT_VERSION" + exit 1 +fi + +# ========================== +# Setup cmake options +# ========================== +CMAKE_QT_OPTION="-DUSE_QT5=OFF -DUSE_QT6=OFF" +if [ "$QT_CHOICE" = "qt5" ]; then + CMAKE_QT_OPTION="-DUSE_QT5=ON" +elif [ "$QT_CHOICE" = "qt6" ]; then + CMAKE_QT_OPTION="-DUSE_QT6=ON" +fi + +# ========================== +# Detect architecture +# ========================== +ARCH=$(uname -m) +BUILD_TYPE=Release + +# ========================== +# Build project +# ========================== +SANITIZED_QT_VERSION=$(echo "$QT_VERSION" | tr '.' '_') +BUILD_DIR="build-omodsim-Qt_${SANITIZED_QT_VERSION}_clang_${ARCH}-${BUILD_TYPE}" +echo "" +echo "Starting build in: ${BUILD_DIR}" +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +cmake ../src -GNinja \ + -DCMAKE_PREFIX_PATH="${QT_PREFIX}" \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + ${CMAKE_QT_OPTION} + +ninja + +echo "" +echo "Build finished successfully!" +echo "Application bundle: ${BUILD_DIR}/omodsim.app" +echo "" +echo "To run:" +echo " open ${BUILD_DIR}/omodsim.app" +echo "" diff --git a/docs/icons/logo_apple.svg b/docs/icons/logo_apple.svg new file mode 100644 index 00000000..6a01cb14 --- /dev/null +++ b/docs/icons/logo_apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9aace2f2..1df07fbf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -338,6 +338,15 @@ file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/docs) find_program(QHELP_GENERATOR_EXECUTABLE qhelpgenerator HINTS "${QT_BINARY_DIR}" "${QT_LIBEXEC_DIR}" "${QT_INSTALL_LIBEXECS}") +if(NOT QHELP_GENERATOR_EXECUTABLE) + # Homebrew on macOS installs qhelpgenerator in share/qt/libexec + get_filename_component(_qt_share_libexec "${QT_BINARY_DIR}/../share/qt/libexec" REALPATH) + find_program(QHELP_GENERATOR_EXECUTABLE qhelpgenerator + HINTS "${_qt_share_libexec}" + "${CMAKE_PREFIX_PATH}/share/qt/libexec" + "${CMAKE_PREFIX_PATH}/libexec") +endif() + if(NOT QHELP_GENERATOR_EXECUTABLE) find_program(QHELP_GENERATOR_EXECUTABLE qhelpgenerator) endif() @@ -386,6 +395,25 @@ if(WIN32) target_sources(${PROJECT_NAME} PRIVATE ${ICON_PATH} "${CMAKE_BINARY_DIR}/omodsim.rc") set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE ON) +elseif(APPLE) + set(MACOSX_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/omodsim.icns") + set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + target_sources(${PROJECT_NAME} PRIVATE ${MACOSX_ICON}) + + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/omodsim.plist.in" + "${CMAKE_BINARY_DIR}/Info.plist" + @ONLY + ) + + set_target_properties(${PROJECT_NAME} PROPERTIES + MACOSX_BUNDLE ON + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_BINARY_DIR}/Info.plist" + MACOSX_BUNDLE_ICON_FILE omodsim.icns + MACOSX_BUNDLE_BUNDLE_NAME "${PRODUCT_NAME}" + MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" + ) elseif(LINUX) target_link_options(${PROJECT_NAME} PRIVATE -static-libgcc -static-libstdc++) endif() @@ -405,7 +433,9 @@ add_custom_command( add_custom_target(helpgenerator ALL DEPENDS ${JSHELP_QCH} ${JSHELP_QHC}) add_dependencies(${PROJECT_NAME} helpgenerator) -if(LINUX) +if(APPLE) + install(TARGETS ${PROJECT_NAME} BUNDLE DESTINATION .) +elseif(LINUX) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/omodsim.desktop.in" "${CMAKE_CURRENT_BINARY_DIR}/omodsim.desktop" diff --git a/src/dialogs/dialogabout.cpp b/src/dialogs/dialogabout.cpp index e01ad0e5..bbef64d3 100644 --- a/src/dialogs/dialogabout.cpp +++ b/src/dialogs/dialogabout.cpp @@ -109,6 +109,13 @@ DialogAbout::DialogAbout(QWidget *parent) : tr("Underlying platform.")); #endif + #ifdef Q_OS_MAC + addComponent(vboxLayout, + QSysInfo::prettyProductName(), + QSysInfo::currentCpuArchitecture(), + tr("Underlying platform.")); + #endif + vboxLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); ui->scrollAreaComponentsWidget->setLayout(vboxLayout); } diff --git a/src/omodsim.plist.in b/src/omodsim.plist.in new file mode 100644 index 00000000..17dfd1b0 --- /dev/null +++ b/src/omodsim.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleExecutable + @PROJECT_NAME@ + CFBundleIconFile + omodsim.icns + CFBundleIdentifier + org.ananev.omodsim + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + @PRODUCT_NAME@ + CFBundlePackageType + APPL + CFBundleShortVersionString + @PROJECT_VERSION@ + CFBundleVersion + @VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@ + NSHighResolutionCapable + + NSHumanReadableCopyright + Copyright 2023-@CURRENT_YEAR@ Alexandr Ananev. All rights reserved. + LSMinimumSystemVersion + 11.0 + + diff --git a/src/res/omodsim.icns b/src/res/omodsim.icns new file mode 100644 index 00000000..3154ca11 Binary files /dev/null and b/src/res/omodsim.icns differ