From b14bda39d7e2ca85c02196a5a9d02128edb8fb2b Mon Sep 17 00:00:00 2001 From: nash_su Date: Wed, 1 Apr 2026 18:34:35 +0800 Subject: [PATCH 1/4] feat: add macOS build support Add macOS platform support with .app bundle packaging, build script, and documentation. All changes are additive and do not affect existing Windows or Linux build paths. --- README.md | 33 +++++++- build-macos.sh | 165 ++++++++++++++++++++++++++++++++++++ docs/icons/logo_apple.svg | 3 + src/CMakeLists.txt | 32 ++++++- src/dialogs/dialogabout.cpp | 7 ++ src/omodsim.plist.in | 28 ++++++ src/res/omodsim.icns | Bin 0 -> 29947 bytes 7 files changed, 266 insertions(+), 2 deletions(-) create mode 100755 build-macos.sh create mode 100644 docs/icons/logo_apple.svg create mode 100644 src/omodsim.plist.in create mode 100644 src/res/omodsim.icns 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 0000000000000000000000000000000000000000..3154ca11a604f4e3532e4a3795f8fc78d32f5e17 GIT binary patch literal 29947 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%0DC4!L9qv_9b)t-HEkhUP;Rez2CZ{M5A(RP=nn; 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 literal 0 HcmV?d00001 From 9c772281bb4f887979c79fd88e48078a401f13a7 Mon Sep 17 00:00:00 2001 From: nash_su Date: Thu, 2 Apr 2026 06:46:13 +0800 Subject: [PATCH 2/4] ci: add macOS DMG build workflow Add GitHub Actions workflow for building macOS DMG package with Qt6 using aqtinstall and macdeployqt. --- .github/workflows/build-dmg-qt6-macos.yml | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/build-dmg-qt6-macos.yml 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 }} From 5f5f47cc2c812547854d148138e9d28136a7026f Mon Sep 17 00:00:00 2001 From: nash_su Date: Thu, 2 Apr 2026 06:56:52 +0800 Subject: [PATCH 3/4] feat: bump version to 1.13.0, add macOS CI workflow - Bump version from 1.12.0 to 1.13.0 for macOS platform support - Add GitHub Actions workflow for building macOS DMG - Update README version references --- README.md | 14 +++++++------- src/CMakeLists.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 649df8e5..7d9a97aa 100644 --- a/README.md +++ b/README.md @@ -230,18 +230,18 @@ Below are the methods for installing the OpenModSim for different OS ## Microsoft Windows Run the installer: -- For 32-bit Windows: `qt5-omodsim_1.12.0_x86.exe` -- For 64-bit Windows: `qt5-omodsim_1.12.0_x64.exe` or `qt6-omodsim_1.12.0_x64.exe` +- For 32-bit Windows: `qt5-omodsim_1.13.0_x86.exe` +- For 64-bit Windows: `qt5-omodsim_1.13.0_x64.exe` or `qt6-omodsim_1.13.0_x64.exe` ## Debian / Ubuntu / Mint / Zorin / Astra Linux ### Install Install the DEB package from the command line: ```bash -sudo apt install ./qt6-omodsim_1.12.0-1_amd64.deb +sudo apt install ./qt6-omodsim_1.13.0-1_amd64.deb ``` or if you want to use Qt5 libraries: ```bash -sudo apt install ./qt5-omodsim_1.12.0-1_amd64.deb +sudo apt install ./qt5-omodsim_1.13.0-1_amd64.deb ``` ### Remove @@ -258,7 +258,7 @@ sudo apt remove qt5-omodsim ### Install Install the RPM package from the command line: ```bash -sudo dnf install ./qt6-omodsim-1.12.0-1.x86_64.rpm +sudo dnf install ./qt6-omodsim-1.13.0-1.x86_64.rpm ``` ### Remove @@ -271,7 +271,7 @@ sudo dnf remove qt6-omodsim ### Install Install the RPM package from the command line as root user: ```bash -apt-get install ./qt6-omodsim-1.12.0-1.x86_64.rpm +apt-get install ./qt6-omodsim-1.13.0-1.x86_64.rpm ``` ### Remove @@ -288,7 +288,7 @@ sudo rpm --import qt6-omodsim.rpm.pubkey ``` Install the RPM package using Zypper: ```bash -sudo zypper install ./qt6-omodsim-1.12.0-1.x86_64.rpm +sudo zypper install ./qt6-omodsim-1.13.0-1.x86_64.rpm ``` ### Remove diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1df07fbf..15623612 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) project(omodsim - VERSION 1.12.0 + VERSION 1.13.0 DESCRIPTION "An Open Source Modbus Slave (Server) Utility" LANGUAGES CXX) From bafec2fd92e74ebc55b7a814048ec7189d9613cb Mon Sep 17 00:00:00 2001 From: nash_su Date: Fri, 3 Apr 2026 06:45:22 +0800 Subject: [PATCH 4/4] fix: revert version bump, leave versioning to maintainer --- README.md | 14 +++++++------- src/CMakeLists.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7d9a97aa..649df8e5 100644 --- a/README.md +++ b/README.md @@ -230,18 +230,18 @@ Below are the methods for installing the OpenModSim for different OS ## Microsoft Windows Run the installer: -- For 32-bit Windows: `qt5-omodsim_1.13.0_x86.exe` -- For 64-bit Windows: `qt5-omodsim_1.13.0_x64.exe` or `qt6-omodsim_1.13.0_x64.exe` +- For 32-bit Windows: `qt5-omodsim_1.12.0_x86.exe` +- For 64-bit Windows: `qt5-omodsim_1.12.0_x64.exe` or `qt6-omodsim_1.12.0_x64.exe` ## Debian / Ubuntu / Mint / Zorin / Astra Linux ### Install Install the DEB package from the command line: ```bash -sudo apt install ./qt6-omodsim_1.13.0-1_amd64.deb +sudo apt install ./qt6-omodsim_1.12.0-1_amd64.deb ``` or if you want to use Qt5 libraries: ```bash -sudo apt install ./qt5-omodsim_1.13.0-1_amd64.deb +sudo apt install ./qt5-omodsim_1.12.0-1_amd64.deb ``` ### Remove @@ -258,7 +258,7 @@ sudo apt remove qt5-omodsim ### Install Install the RPM package from the command line: ```bash -sudo dnf install ./qt6-omodsim-1.13.0-1.x86_64.rpm +sudo dnf install ./qt6-omodsim-1.12.0-1.x86_64.rpm ``` ### Remove @@ -271,7 +271,7 @@ sudo dnf remove qt6-omodsim ### Install Install the RPM package from the command line as root user: ```bash -apt-get install ./qt6-omodsim-1.13.0-1.x86_64.rpm +apt-get install ./qt6-omodsim-1.12.0-1.x86_64.rpm ``` ### Remove @@ -288,7 +288,7 @@ sudo rpm --import qt6-omodsim.rpm.pubkey ``` Install the RPM package using Zypper: ```bash -sudo zypper install ./qt6-omodsim-1.13.0-1.x86_64.rpm +sudo zypper install ./qt6-omodsim-1.12.0-1.x86_64.rpm ``` ### Remove diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 15623612..1df07fbf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) project(omodsim - VERSION 1.13.0 + VERSION 1.12.0 DESCRIPTION "An Open Source Modbus Slave (Server) Utility" LANGUAGES CXX)