From 865063b77cd8d3439c4e3b7f0c659a504ffadf2e Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Mon, 24 Jun 2024 10:36:30 +0100 Subject: [PATCH] Re-implement packaging for GitHub workflows (macOS) (#7353) * Restore Azure macOS dist scripts * Move steps to workflow for testing * Always upload to GitHub * Add codesign ID * Echo codesign ID * Add cert import code * Stub file for Mac * Self-install pyyaml and choco * Auto add env var on Windows * Auto add CMAKE_PREFIX_PATH to .zshrc * Shorter var names * Append env var instead of replace * Only set env var if not CI * Improve function names and print output * Simplify Linux package command * Support continuation sequence * Add note about Windows * Remove dead doc file * Tidy up version file and move to .env format * Use Python venv for deps * Only use venv on Mac * Rename package script for all OS * Add package and dist steps, and use common upload * Remove version source * Fixed vars not available * Fixed python paths * Use RuntimeError which is sufficient * Remove dead code * Add extras command for Linux * Always install deps on Linux * Move Python deps to CI * More env bootstrapping, ugh * Forgot to return! * Simplify code * Use shell * Simplify command * Skip sudo if no sudo * Update package managers * Fixed Fedora package name * Tidy up commands * Use newer upload artifact * Strip don't trim! * Check for version file and reduce log verbosity * Remove CentOS 7.6 * Print more info about return code and log more to stderr * Install certificate on macOS * Better errors for no env var * Implement Mac signing and notary * Move dmgbuild load * Simplify notary * Rename dist files to same as dest * Fixed paths for dist * Move checked-in dist files to res (dist is meant to be a temp dir) * Fixed Mac path in CMake * Fixed dmg path * Format Python * Ignore import warnings and move function * Fixed cmake paths * Add missing env var secrets * Remove extensions from GH upload * Make deps.yml general purpose config * Add cspell config * Pass codesign ID * Use new general config file * Sign bundle on Mac * Move imports to functions * Escape chars in docs * Fixed config key accessor * Change module import order * Move file to tmp dir in workflow dir * Persist temp dir * Add tmp dir to ignore * Flush stdio before running process * Trying quotes around env values * Add codesigning certificate validation for Mac signing * Revert "Trying quotes around env values" This reverts commit 0dd741e8cd6fde21e69d4fb871e835a5f4fa1a23. * Extract codesign verify * Fixed version number * Ignore .cache dir * Fix macro name * Package name with version number and arch * Improve package function readability * Change order of vars * Testing upload to GDrive * Add missing return code * Use positional args and declare error * Use machine instead of arch and remove build from filename * Remove redundant build jobs * Replace massively over-complicated `build_version.py` script * Move version info to env module * Use version info script * Fixed: too many values to unpack * Chmod version script * Use shebang * Don't check return code on Linux * Fixed function name * Convert to GitHub specific script * Env vars must be after configure * Fixed Windows env var command * Remove && from deps command so it's not conditional * Fixed position of set env * Change order of env script * Only upload when not draft * Test * Tweak config * Fixed if condition * Don't package in draft (Windows and Linux) --- .github/actions/dist-upload/action.yml | 39 +++ .github/workflows/ci.yml | 72 +++-- .github/workflows/job-build-mac-10.13.yaml | 128 -------- .github/workflows/job-build-mac-11.yaml | 134 --------- .github/workflows/sonarcloud-analysis.yml | 1 - .gitignore | 47 +-- CMakeLists.txt | 52 ++-- ChangeLog | 1 + cmake/Version.cmake | 25 +- config.yml | 65 ++++ cspell.json | 25 ++ deps.yml | 96 ------ doc/MacReadme.txt | 18 -- .../bundle/Synergy.app/Contents/Info.plist.in | 0 .../macos/bundle/Synergy.app/Contents/PkgInfo | 0 .../Contents/Resources/.background.tiff | Bin .../Contents/Resources/Synergy.icns | Bin .../Contents/Resources/VolumeIcon.icns | Bin res/dist/macos/dmgbuild/settings.py | 150 ++++++++++ .../dist}/rpm/synergy-business.spec.in | 0 .../dist}/rpm/synergy-enterprise.spec.in | 0 {dist => res/dist}/rpm/synergy.spec.in | 0 {dist => res/dist}/wix/Include.wxi.in | 0 {dist => res/dist}/wix/Product.wxs | 0 {dist => res/dist}/wix/Synergy.sln | 0 {dist => res/dist}/wix/Synergy.wixproj | 0 {dist => res/dist}/wix/SynergyBrowseDlg.wxs | 0 {dist => res/dist}/wix/SynergyDlgSequence.wxs | 0 {dist => res/dist}/wix/SynergyInstallDlg.wxs | 0 .../dist}/wix/SynergyMaintenanceTypeDlg.wxs | 0 .../dist}/wix/SynergyVerifyReadyDlg.wxs | 0 {dist => res/dist}/wix/SynergyWelcome.wxs | 0 .../dist}/wix/msm/Microsoft_VC142_CRT_x64.msm | Bin .../dist}/wix/msm/Microsoft_VC142_CRT_x86.msm | Bin scripts/build_version.py | 137 --------- scripts/github_env.py | 30 ++ scripts/install_deps.py | 184 +++--------- scripts/lib/cmd_utils.py | 91 +++++- scripts/lib/config.py | 107 +++++++ scripts/lib/env.py | 163 ++++++++++ scripts/lib/mac.py | 282 ++++++++++++++++++ scripts/lib/windows.py | 56 ++-- scripts/package.py | 57 ++++ scripts/windows_daemon.py | 121 ++++---- src/lib/common/Version.cpp | 2 +- 45 files changed, 1245 insertions(+), 838 deletions(-) create mode 100644 .github/actions/dist-upload/action.yml delete mode 100644 .github/workflows/job-build-mac-10.13.yaml delete mode 100644 .github/workflows/job-build-mac-11.yaml create mode 100644 config.yml create mode 100644 cspell.json delete mode 100644 deps.yml delete mode 100755 doc/MacReadme.txt rename {dist => res/dist}/macos/bundle/Synergy.app/Contents/Info.plist.in (100%) rename {dist => res/dist}/macos/bundle/Synergy.app/Contents/PkgInfo (100%) rename dist/macos/bundle/Synergy.app/Contents/Resources/.installer_background.tiff => res/dist/macos/bundle/Synergy.app/Contents/Resources/.background.tiff (100%) rename {dist => res/dist}/macos/bundle/Synergy.app/Contents/Resources/Synergy.icns (100%) rename dist/macos/bundle/Synergy.app/Contents/Resources/Drive.icns => res/dist/macos/bundle/Synergy.app/Contents/Resources/VolumeIcon.icns (100%) create mode 100644 res/dist/macos/dmgbuild/settings.py rename {dist => res/dist}/rpm/synergy-business.spec.in (100%) rename {dist => res/dist}/rpm/synergy-enterprise.spec.in (100%) rename {dist => res/dist}/rpm/synergy.spec.in (100%) rename {dist => res/dist}/wix/Include.wxi.in (100%) rename {dist => res/dist}/wix/Product.wxs (100%) rename {dist => res/dist}/wix/Synergy.sln (100%) rename {dist => res/dist}/wix/Synergy.wixproj (100%) rename {dist => res/dist}/wix/SynergyBrowseDlg.wxs (100%) rename {dist => res/dist}/wix/SynergyDlgSequence.wxs (100%) rename {dist => res/dist}/wix/SynergyInstallDlg.wxs (100%) rename {dist => res/dist}/wix/SynergyMaintenanceTypeDlg.wxs (100%) rename {dist => res/dist}/wix/SynergyVerifyReadyDlg.wxs (100%) rename {dist => res/dist}/wix/SynergyWelcome.wxs (100%) rename {dist => res/dist}/wix/msm/Microsoft_VC142_CRT_x64.msm (100%) rename {dist => res/dist}/wix/msm/Microsoft_VC142_CRT_x86.msm (100%) delete mode 100644 scripts/build_version.py create mode 100755 scripts/github_env.py create mode 100644 scripts/lib/config.py create mode 100644 scripts/lib/env.py create mode 100644 scripts/lib/mac.py create mode 100755 scripts/package.py diff --git a/.github/actions/dist-upload/action.yml b/.github/actions/dist-upload/action.yml new file mode 100644 index 000000000..60c51a19d --- /dev/null +++ b/.github/actions/dist-upload/action.yml @@ -0,0 +1,39 @@ +name: "Distribute upload" +description: "Uploads the package from the dist dir to GitHub artifacts or Google Drive" +inputs: + service: + description: "Where to upload the package (github or gdrive)" + default: "github" + + github-target-filename: + description: "The filename to upload (only used by GitHub artifacts)" + + gdrive-target-dir: + description: "The directory to upload (only used by Google Drive)" + + gdrive-secret-key: + description: "The Google Drive secret key" + + gdrive-parent-folder-id: + description: "The Google Drive parent folder ID" + +runs: + using: "composite" + + steps: + # Upload to GitHub + - if: ${{ inputs.service == 'github' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.github-target-filename }} + path: ./dist + retention-days: 3 + + # Upload to Google Drive + - if: ${{ inputs.service == 'gdrive' }} + uses: symless/gdrive-upload@target-glob + with: + credentials: ${{ inputs.gdrive-secret-key }} + target: "./dist/*" + parent_folder_id: ${{ inputs.gdrive-parent-folder-id }} + child_folder: Snapshots/${{ inputs.gdrive-target-dir }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5847fa2c0..49d5f2bac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,13 +9,16 @@ on: - synchronize - ready_for_review push: - branches: [master*, release*] + branches: + - master* + - release* env: GIT_COMMIT: ${{ github.sha }} jobs: - windows-2022: + windows: + name: windows-2022 runs-on: windows-2022 timeout-minutes: 20 @@ -41,9 +44,7 @@ jobs: run: python ./scripts/install_deps.py --only qt - name: Install dependencies - run: | - pip install pyyaml - python ./scripts/install_deps.py + run: python ./scripts/install_deps.py - name: Setup VC++ environment uses: ilammy/msvc-dev-cmd@v1 @@ -53,12 +54,19 @@ jobs: CMAKE_PREFIX_PATH: "${{ env.QT_BASE_DIR }}\\${{ env.QT_VERSION }}\\msvc2019_64\\" run: cmake -B build --preset=windows-release + - name: Set env vars + run: python ./scripts/github_env.py + - name: Build run: cmake --build build - name: Test run: ./build/bin/unittests + - name: Package + if: ${{ !github.event.pull_request.draft }} + run: python ./scripts/package.py + macos: runs-on: ${{ matrix.runtime.os }} timeout-minutes: ${{ matrix.runtime.timeout }} @@ -102,21 +110,49 @@ jobs: submodules: "recursive" - name: Install dependencies - run: | - pip install pyyaml - python ./scripts/install_deps.py + run: ./scripts/install_deps.py - name: Configure env: CMAKE_OSX_DEPLOYMENT_TARGET: ${{ matrix.runtime.target }} run: cmake -B build --preset=macos-release -DCMAKE_PREFIX_PATH=$(brew --prefix qt@5) + - name: Set env vars + run: ./scripts/github_env.py + - name: Build run: cmake --build build - name: Test run: ./build/bin/unittests + - name: Package + if: ${{ !github.event.pull_request.draft }} + run: ./scripts/package.py + env: + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }} + APPLE_P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }} + APPLE_NOTARY_USER: ${{ secrets.APPLE_NOTARY_USER }} + APPLE_NOTARY_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Upload to GitHub + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }} + uses: ./.github/actions/dist-upload + with: + service: "github" + github-target-filename: "synergy-macos-${{ matrix.runtime.target }}" + + - name: Upload to Google Drive + if: github.event_name != 'pull_request' + uses: ./.github/actions/dist-upload + with: + service: "gdrive" + gdrive-target-dir: "synergy1/v1-core-standard/${{ env.SYNERGY_VERSION }}" + gdrive-secret-key: ${{ secrets.GOOGLE_DRIVE_KEY }} + gdrive-parent-folder-id: ${{ secrets.GOOGLE_DRIVE_TECH_DRIVE }} + linux: runs-on: ${{ matrix.distro.runs-on }} timeout-minutes: 10 @@ -126,11 +162,6 @@ jobs: strategy: matrix: distro: - - name: centos-7.6 - container: symless/synergy-core:centos7.6 - runs-on: ubuntu-latest - legacy-cmake: true - - name: centos-8 container: symless/synergy-core:centos8 runs-on: ubuntu-latest @@ -166,7 +197,6 @@ jobs: - name: ubuntu-24.04 runs-on: ubuntu-24.04 - install-deps: true steps: # Use @v3 since some older Linux distro versions don't support @v4 @@ -176,10 +206,11 @@ jobs: submodules: "recursive" - name: Install dependencies - if: ${{ matrix.distro.install-deps }} - run: python ./scripts/install_deps.py + run: | + ${{ matrix.distro.python-deps }} + ./scripts/install_deps.py - - name: Configure + - name: Configure (modern) if: ${{ !matrix.distro.legacy-cmake }} run: cmake -B build --preset=linux-release @@ -188,8 +219,15 @@ jobs: if: ${{ matrix.distro.legacy-cmake }} run: cmake -B build -DCMAKE_BUILD_TYPE=Release + - name: Set env vars + run: ./scripts/github_env.py + - name: Build run: cmake --build build - name: Test run: ./build/bin/unittests + + - name: Package + if: ${{ !github.event.pull_request.draft }} + run: ./scripts/package.py diff --git a/.github/workflows/job-build-mac-10.13.yaml b/.github/workflows/job-build-mac-10.13.yaml deleted file mode 100644 index a769df2a6..000000000 --- a/.github/workflows/job-build-mac-10.13.yaml +++ /dev/null @@ -1,128 +0,0 @@ -name: "Build macOS 10.13" - -on: - release: - types: [created] - -jobs: - build-mac-10: - runs-on: "macos-latest-xlarge" - timeout-minutes: 10 - - strategy: - matrix: - runtime: - - name: "synergy" - remote_folder: "v1-core-standard" - enterprise: "" - business: "" - - name: "synergy-enterprise" - remote_folder: "v1-core-enterprise" - enterprise: "1" - business: "" - - name: "synergy-business" - remote_folder: "v1-core-business" - enterprise: "" - business: "1" - - env: - GIT_COMMIT: ${{ github.sha }} - SYNERGY_ENTERPRISE: ${{ matrix.runtime.enterprise }} - SYNERGY_BUSINESS: ${{ matrix.runtime.business }} - Qt5_DIR: /usr/local/opt/qt/5.15.2/clang_64 - OpenSSL_DIR: /usr/local/ssl - CODESIGN_ID: "Developer ID Application: Symless Ltd (4HX897Y6GJ)" - - steps: - - name: Checkout git repo - uses: actions/checkout@v3 - with: - submodules: "recursive" - - - name: Setup Environment - run: | - python3 -m pip install dmgbuild - - - name: Build - env: - CMAKE_PREFIX_PATH: "${{ env.Qt5_DIR }};${{ env.OpenSSL_DIR }}" - run: | - export PATH="$Qt5_DIR/bin:$PATH" - python3 scripts/build_version.py - mkdir build - cd build - cmake \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \ - -DCMAKE_OSX_ARCHITECTURES=x86_64 \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CONFIGURATION_TYPES=Release .. - . ./version - make -j - make install/strip - - - name: Version Info - id: version - run: | - . ./build/version - SYNERGY_VERSION="${SYNERGY_VERSION_MAJOR}.${SYNERGY_VERSION_MINOR}.${SYNERGY_VERSION_PATCH}" - SYNERGY_REVISION=$(git rev-parse --short=8 HEAD) - SYNERGY_DMG_VERSION="${SYNERGY_VERSION}-${SYNERGY_VERSION_STAGE}.${SYNERGY_REVISION}" - echo "::set-output name=SYNERGY_VERSION_MAJOR::$SYNERGY_VERSION_MAJOR" - echo "::set-output name=SYNERGY_VERSION_MINOR::$SYNERGY_VERSION_MINOR" - echo "::set-output name=SYNERGY_VERSION_PATCH::$SYNERGY_VERSION_PATCH" - echo "::set-output name=SYNERGY_VERSION_STAGE::$SYNERGY_VERSION_STAGE" - echo "::set-output name=SYNERGY_VERSION_BUILD::$SYNERGY_VERSION_BUILD" - echo "::set-output name=SYNERGY_VERSION::$SYNERGY_VERSION" - echo "::set-output name=SYNERGY_REVISION::$SYNERGY_REVISION" - echo "::set-output name=SYNERGY_DMG_VERSION::$SYNERGY_DMG_VERSION" - SYNERGY_PACKAGE_NAME=${{ matrix.runtime.name }} - SYNERGY_REMOTE_FOLDER="${{ matrix.runtime.remote_folder }}/${SYNERGY_VERSION}/${SYNERGY_VERSION_STAGE}/b${SYNERGY_VERSION_BUILD}-${SYNERGY_REVISION}" - SYNERGY_DMG_FILENAME="${SYNERGY_PACKAGE_NAME}_${SYNERGY_DMG_VERSION}_macos-10.13_x86-64.dmg" - echo "SYNERGY_REMOTE_FOLDER: $SYNERGY_REMOTE_FOLDER" - echo "::set-output name=SYNERGY_REMOTE_FOLDER::$SYNERGY_REMOTE_FOLDER" - echo "::set-output name=SYNERGY_PACKAGE_NAME::$SYNERGY_PACKAGE_NAME" - echo "::set-output name=SYNERGY_DMG_FILENAME::$SYNERGY_DMG_FILENAME" - - - name: Sign applicaiton - run: | - export PATH="$Qt5_DIR/bin:$PATH" - macdeployqt ${{ github.workspace }}/build/bundle/Synergy.app -codesign="$CODESIGN_ID" - codesign -f --options runtime --deep -s "$CODESIGN_ID" ${{ github.workspace }}/build/bundle/Synergy.app - ln -s /Applications ${{ github.workspace }}/build/bundle/Applications - - - name: Create Installer - env: - SYNERGY_DMG_FILENAME: ${{ steps.version.outputs.SYNERGY_DMG_FILENAME }} - run: | - dmgbuild \ - -s CI/MacOS/installator_settings.py \ - -D app=${{ github.workspace }}/build/bundle/Synergy.app \ - -D background=${{ github.workspace }}/build/bundle/Synergy.app/Contents/Resources/.installer_background.tiff \ - "Synergy" \ - $SYNERGY_DMG_FILENAME - mkdir pkg - mv $SYNERGY_DMG_FILENAME pkg/ - cd pkg - md5 -r $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - shasum $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - shasum -a 256 $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - - - name: Submit for Notarization - env: - ASC_USERNAME: ${{ secrets.ASC_USERNAME }} - NOTORY_APP_PASSWORD: ${{ secrets.NOTORY_APP_PASSWORD }} - SYNERGY_VERSION: ${{ steps.version.outputs.SYNERGY_VERSION }} - SYNERGY_REVISION: ${{ steps.version.outputs.SYNERGY_REVISION }} - SYNERGY_DMG_FILENAME: ${{ steps.version.outputs.SYNERGY_DMG_FILENAME }} - run: | - cd pkg - ../CI/MacOS/notorize.sh - - - name: Send package to Binary Storage - uses: garygrossgarten/github-action-scp@v0.7.3 - with: - local: "${{ github.workspace }}/pkg/" - remote: "${{ secrets.BINARIES_SSH_DIR }}/${{ steps.version.outputs.SYNERGY_REMOTE_FOLDER }}/" - host: ${{ secrets.BINARIES_SSH_HOST }} - username: ${{ secrets.BINARIES_SSH_USER }} - privateKey: ${{ secrets.BINARIES_SSH_KEY }} diff --git a/.github/workflows/job-build-mac-11.yaml b/.github/workflows/job-build-mac-11.yaml deleted file mode 100644 index 9eb1c5e7e..000000000 --- a/.github/workflows/job-build-mac-11.yaml +++ /dev/null @@ -1,134 +0,0 @@ -name: "Build macOS 11" - -on: - release: - types: [created] - -defaults: - run: - shell: "/usr/bin/arch -arch arm64e /bin/bash --noprofile --norc -eo pipefail {0}" - -jobs: - build-mac-11: - runs-on: "macos-latest-xlarge" - timeout-minutes: 10 - - strategy: - matrix: - runtime: - - name: "synergy" - remote_folder: "v1-core-standard" - enterprise: "" - business: "" - - name: "synergy-enterprise" - remote_folder: "v1-core-enterprise" - enterprise: "1" - business: "" - - name: "synergy-business" - remote_folder: "v1-core-business" - enterprise: "" - business: "1" - - env: - GIT_COMMIT: ${{ github.sha }} - SYNERGY_ENTERPRISE: ${{ matrix.runtime.enterprise }} - SYNERGY_BUSINESS: ${{ matrix.runtime.business }} - CODESIGN_ID: "Developer ID Application: Symless Ltd (4HX897Y6GJ)" - - steps: - - name: Setup PATH - run: echo "/opt/homebrew/bin" >> $GITHUB_PATH - env: - ARCH: ${{ matrix.runtime.arch }} - - - name: Checkout git repo - uses: actions/checkout@v3 - with: - submodules: "recursive" - - - name: Setup Environment - run: | - python3 -m pip install dmgbuild - brew bundle --file=- <<< "brew 'qt5'; brew 'openssl'" - - - name: Build - env: - CMAKE_BUILD_TYPE: Release - run: | - python3 scripts/build_version.py - mkdir build - cd build - cmake \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=11 \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH="$(brew --prefix qt5);$(brew --prefix openssl)" .. - . ./version - make -j - make install/strip - - - name: Version Info - id: version - run: | - . ./build/version - SYNERGY_VERSION="${SYNERGY_VERSION_MAJOR}.${SYNERGY_VERSION_MINOR}.${SYNERGY_VERSION_PATCH}" - SYNERGY_REVISION=$(git rev-parse --short=8 HEAD) - SYNERGY_DMG_VERSION="${SYNERGY_VERSION}-${SYNERGY_VERSION_STAGE}.${SYNERGY_REVISION}" - echo "::set-output name=SYNERGY_VERSION_MAJOR::$SYNERGY_VERSION_MAJOR" - echo "::set-output name=SYNERGY_VERSION_MINOR::$SYNERGY_VERSION_MINOR" - echo "::set-output name=SYNERGY_VERSION_PATCH::$SYNERGY_VERSION_PATCH" - echo "::set-output name=SYNERGY_VERSION_STAGE::$SYNERGY_VERSION_STAGE" - echo "::set-output name=SYNERGY_VERSION_BUILD::$SYNERGY_VERSION_BUILD" - echo "::set-output name=SYNERGY_VERSION::$SYNERGY_VERSION" - echo "::set-output name=SYNERGY_REVISION::$SYNERGY_REVISION" - echo "::set-output name=SYNERGY_DMG_VERSION::$SYNERGY_DMG_VERSION" - SYNERGY_PACKAGE_NAME=${{ matrix.runtime.name }} - SYNERGY_REMOTE_FOLDER="${{ matrix.runtime.remote_folder }}/${SYNERGY_VERSION}/${SYNERGY_VERSION_STAGE}/b${SYNERGY_VERSION_BUILD}-${SYNERGY_REVISION}" - SYNERGY_DMG_FILENAME="${SYNERGY_PACKAGE_NAME}_${SYNERGY_DMG_VERSION}_macos-arm64.dmg" - echo "SYNERGY_REMOTE_FOLDER: $SYNERGY_REMOTE_FOLDER" - echo "::set-output name=SYNERGY_REMOTE_FOLDER::$SYNERGY_REMOTE_FOLDER" - echo "::set-output name=SYNERGY_PACKAGE_NAME::$SYNERGY_PACKAGE_NAME" - echo "::set-output name=SYNERGY_DMG_FILENAME::$SYNERGY_DMG_FILENAME" - - - name: Sign applicaiton - run: | - export PATH="$(brew --prefix qt5)/bin:$PATH" - macdeployqt ${{ github.workspace }}/build/bundle/Synergy.app -codesign="$CODESIGN_ID" - codesign -f --options runtime --deep -s "$CODESIGN_ID" ${{ github.workspace }}/build/bundle/Synergy.app - ln -s /Applications ${{ github.workspace }}/build/bundle/Applications - - - name: Create Installer - env: - SYNERGY_DMG_FILENAME: ${{ steps.version.outputs.SYNERGY_DMG_FILENAME }} - run: | - dmgbuild \ - -s CI/MacOS/installator_settings.py \ - -D app=${{ github.workspace }}/build/bundle/Synergy.app \ - -D background=${{ github.workspace }}/build/bundle/Synergy.app/Contents/Resources/.installer_background.tiff \ - "Synergy" \ - $SYNERGY_DMG_FILENAME - mkdir pkg - mv $SYNERGY_DMG_FILENAME pkg/ - cd pkg - md5 -r $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - shasum $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - shasum -a 256 $SYNERGY_DMG_FILENAME >> $SYNERGY_DMG_FILENAME.checksum.txt - - - name: Submit for Notarization - env: - ASC_USERNAME: ${{ secrets.ASC_USERNAME }} - NOTORY_APP_PASSWORD: ${{ secrets.NOTORY_APP_PASSWORD }} - SYNERGY_VERSION: ${{ steps.version.outputs.SYNERGY_VERSION }} - SYNERGY_REVISION: ${{ steps.version.outputs.SYNERGY_REVISION }} - SYNERGY_DMG_FILENAME: ${{ steps.version.outputs.SYNERGY_DMG_FILENAME }} - run: | - cd pkg - ../CI/MacOS/notorize.sh - - - name: Send package to Binary Storage - uses: garygrossgarten/github-action-scp@v0.7.3 - with: - local: "${{ github.workspace }}/pkg/" - remote: "${{ secrets.BINARIES_SSH_DIR }}/${{ steps.version.outputs.SYNERGY_REMOTE_FOLDER }}/" - host: ${{ secrets.BINARIES_SSH_HOST }} - username: ${{ secrets.BINARIES_SSH_USER }} - privateKey: ${{ secrets.BINARIES_SSH_KEY }} diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml index 11e65cf99..4a5b9f6cb 100644 --- a/.github/workflows/sonarcloud-analysis.yml +++ b/.github/workflows/sonarcloud-analysis.yml @@ -49,7 +49,6 @@ jobs: mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON .. - . ./version build-wrapper-linux-x86-64 --out-dir bw-output make -j - name: Running coverage diff --git a/.gitignore b/.gitignore index 862e2cb00..56067f54d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,15 @@ -.vscode -config.h -.DS_Store -*.pyc -*.o -*~ -\.*.swp -*build-gui-Desktop_Qt* -/bin -/lib -/build* -/CMakeFiles -/ext/cryptopp562 -/ext/openssl -/src/gui/Makefile* -/src/gui/object_script* -/src/gui/tmp -/src/gui/ui_* -src/gui/gui.pro.user* -src/gui/.qmake.stash -src/gui/.rnd -src/setup/win32/synergy.suo -/.idea -/cmake-build-* -/CMakeLists.txt.user -/.vs -/CMakeLists.txt.* -/doxygen/ +# temp dirs created during build +/build +/dist /deps -flatpak/.flatpak-builder -flatpak/build -flatpak/export -flatpak/synergy.desktop -flatpak/*.flatpak -*.code-workspace -*.idx +/tmp +/scripts/**/*.pyc aqtinstall.log Brewfile.lock.json +/.cache + +# typical developer-created files +.vscode +.DS_Store +*.code-workspace +.env* diff --git a/CMakeLists.txt b/CMakeLists.txt index eeb8cc655..8a2e7a26a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ cmake_minimum_required (VERSION 3.5) project (synergy-core C CXX) +include (cmake/Version.cmake) # use response files so that ninja can compile on windows, # otherwise you get an error when linking qt: @@ -59,6 +60,18 @@ else() option (SYNERGY_BUSINESS "Build Business" OFF) endif() +if (SYNERGY_DEVELOPER_MODE) + add_definitions (-DSYNERGY_DEVELOPER_MODE=1) +endif() + +if (SYNERGY_ENTERPRISE) + add_definitions (-DSYNERGY_ENTERPRISE=1) +endif() + +if (SYNERGY_BUSINESS) + add_definitions(-DSYNERGY_BUSINESS=1) +endif() + set (CMAKE_CXX_STANDARD 20) set (CMAKE_CXX_EXTENSIONS OFF) set (CMAKE_CXX_STANDARD_REQUIRED ON) @@ -81,11 +94,6 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions (-DNDEBUG) endif() -# -# Synergy version -# -include (cmake/Version.cmake) - # TODO: Find out why we need these, and remove them if (COMMAND cmake_policy) cmake_policy (SET CMP0003 NEW) @@ -386,7 +394,7 @@ endif() # -# Configure_file... but for directories, recursively. +# Same as the `configure_file` command but for directories recursively. # macro (configure_files srcDir destDir) message (STATUS "Configuring directory ${destDir}") @@ -415,33 +423,14 @@ macro (configure_files srcDir destDir) endforeach (templateFile) endmacro (configure_files) -macro(generate_versionfile) - if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin" OR ${CMAKE_SYSTEM_NAME} MATCHES "Linux|.*BSD|DragonFly") - FILE(WRITE ${CMAKE_BINARY_DIR}/version - "export SYNERGY_VERSION_MAJOR=\"${SYNERGY_VERSION_MAJOR}\"\n" - "export SYNERGY_VERSION_MINOR=\"${SYNERGY_VERSION_MINOR}\"\n" - "export SYNERGY_VERSION_PATCH=\"${SYNERGY_VERSION_PATCH}\"\n" - "export SYNERGY_VERSION_BUILD=\"${SYNERGY_VERSION_BUILD}\"\n" - "export SYNERGY_VERSION_STAGE=\"${SYNERGY_VERSION_STAGE}\"\n") - elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - FILE(WRITE ${CMAKE_BINARY_DIR}/version.bat - "SET SYNERGY_VERSION_MAJOR=${SYNERGY_VERSION_MAJOR}\n" - "SET SYNERGY_VERSION_MINOR=${SYNERGY_VERSION_MINOR}\n" - "SET SYNERGY_VERSION_PATCH=${SYNERGY_VERSION_PATCH}\n" - "SET SYNERGY_VERSION_BUILD=${SYNERGY_VERSION_BUILD}\n" - "SET SYNERGY_VERSION_STAGE=${SYNERGY_VERSION_STAGE}\n") - endif() -endmacro(generate_versionfile) - -generate_versionfile() - if (${SYNERGY_BUILD_INSTALLER}) + # -# macOS app Bundle +# macOS app bundle # if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set (CMAKE_INSTALL_RPATH "@loader_path/../Libraries;@loader_path/../Frameworks") - set (SYNERGY_BUNDLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/dist/macos/bundle) + set (SYNERGY_BUNDLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/res/dist/macos/bundle) set (SYNERGY_BUNDLE_DIR ${CMAKE_BINARY_DIR}/bundle) set (SYNERGY_BUNDLE_APP_DIR ${SYNERGY_BUNDLE_DIR}/Synergy.app) set (SYNERGY_BUNDLE_BINARY_DIR ${SYNERGY_BUNDLE_APP_DIR}/Contents/MacOS) @@ -454,14 +443,14 @@ endif() if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") message (STATUS "Configuring the v1 installer") set(QT_PATH $ENV{CMAKE_PREFIX_PATH}) - configure_files (${CMAKE_CURRENT_SOURCE_DIR}/dist/wix ${CMAKE_BINARY_DIR}/installer) + configure_files (${CMAKE_CURRENT_SOURCE_DIR}/res/dist/wix ${CMAKE_BINARY_DIR}/installer) endif() # # Linux installation # if (${CMAKE_SYSTEM_NAME} MATCHES "Linux|.*BSD|DragonFly") - configure_files (${CMAKE_CURRENT_SOURCE_DIR}/dist/rpm ${CMAKE_BINARY_DIR}/rpm) + configure_files (${CMAKE_CURRENT_SOURCE_DIR}/res/dist/rpm ${CMAKE_BINARY_DIR}/rpm) install(FILES res/synergy.svg DESTINATION share/icons/hicolor/scalable/apps) if("${VERSION_MAJOR}" STREQUAL "2") install(FILES res/synergy2.desktop DESTINATION share/applications) @@ -471,6 +460,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux|.*BSD|DragonFly") endif() else() - message (STATUS "NOT configuring the v1 installer") + message (STATUS "NOT configuring the installer") endif() + add_subdirectory (src) diff --git a/ChangeLog b/ChangeLog index 0efc54147..85a8dd138 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,6 +33,7 @@ Enhancements: - #7336 Add C++ and LLDB to VS Code recommendations - #7351 Use deps script to make life easier for contribs - #7352 Combine GitHub workflows to reduce config duplication +- #7353 Re-implement packaging for GitHub workflows (macOS) # 1.14.6 diff --git a/cmake/Version.cmake b/cmake/Version.cmake index b51240089..189e12be6 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -59,25 +59,20 @@ endif() string (TIMESTAMP SYNERGY_BUILD_DATE "%Y%m%d" UTC) set (SYNERGY_SNAPSHOT_INFO "${SYNERGY_VERSION_STAGE}.${SYNERGY_REVISION}") -set (SYNERGY_VERSION_TAG "${SYNERGY_VERSION_STAGE}.b${SYNERGY_VERSION_BUILD}-${SYNERGY_REVISION}") +set (SYNERGY_VERSION_TAG "${SYNERGY_VERSION_STAGE}+build-${SYNERGY_VERSION_BUILD}+${SYNERGY_REVISION}") set (SYNERGY_VERSION "${SYNERGY_VERSION_MAJOR}.${SYNERGY_VERSION_MINOR}.${SYNERGY_VERSION_PATCH}") -set (SYNERGY_VERSION_STRING "${SYNERGY_VERSION}-${SYNERGY_VERSION_TAG}") -message (STATUS "Full Synergy version string is '" ${SYNERGY_VERSION_STRING} "'") +set (SYNERGY_VERSION_LONG "${SYNERGY_VERSION}-${SYNERGY_VERSION_TAG}") +message (STATUS "Long version number: " ${SYNERGY_VERSION_LONG}) add_definitions (-DSYNERGY_VERSION="${SYNERGY_VERSION}") -add_definitions (-DSYNERGY_VERSION_STRING="${SYNERGY_VERSION_STRING}") +add_definitions (-DSYNERGY_VERSION_LONG="${SYNERGY_VERSION_LONG}") add_definitions (-DSYNERGY_REVISION="${SYNERGY_REVISION}") add_definitions (-DSYNERGY_BUILD_DATE="${SYNERGY_BUILD_DATE}") add_definitions (-DSYNERGY_VERSION_BUILD=${SYNERGY_VERSION_BUILD}) -if (SYNERGY_DEVELOPER_MODE) - add_definitions (-DSYNERGY_DEVELOPER_MODE=1) -endif() - -if (SYNERGY_ENTERPRISE) - add_definitions (-DSYNERGY_ENTERPRISE=1) -endif() - -if (SYNERGY_BUSINESS) - add_definitions(-DSYNERGY_BUSINESS=1) -endif() +file(WRITE ${CMAKE_BINARY_DIR}/.env.version + "SYNERGY_VERSION_MAJOR=${SYNERGY_VERSION_MAJOR}\n" + "SYNERGY_VERSION_MINOR=${SYNERGY_VERSION_MINOR}\n" + "SYNERGY_VERSION_PATCH=${SYNERGY_VERSION_PATCH}\n" + "SYNERGY_VERSION_BUILD=${SYNERGY_VERSION_BUILD}\n" + "SYNERGY_VERSION_STAGE=${SYNERGY_VERSION_STAGE}\n") diff --git a/config.yml b/config.yml new file mode 100644 index 000000000..e97c19943 --- /dev/null +++ b/config.yml @@ -0,0 +1,65 @@ +config: + windows: + dependencies: + command: choco install Chocolatey.config -y + qt: + version: 5.15.2 + mirror: https://qt.mirror.constant.com/ + install-dir: C:\Qt + ci: + skip: + edit-config: Chocolatey.config + packages: + - cmake + - visualstudio2022buildtools + + mac: + qt-prefix-command: brew --prefix qt@5 + dependencies: + command: brew bundle --file=Brewfile + + # arch, opensuse, etc, patches welcome! :) + linux: + debian: &debian + dependencies: + command: sudo apt-get update; sudo apt-get install -y \ + cmake \ + make \ + g++ \ + xorg-dev \ + libx11-dev \ + libxtst-dev \ + libssl-dev \ + pkg-config \ + libglib2.0-dev \ + libgdk-pixbuf-2.0-dev \ + libnotify-dev \ + libxkbfile-dev \ + qtbase5-dev \ + qttools5-dev \ + libgtk-3-dev \ + rpm + + ubuntu: + <<: *debian + + fedora: &fedora + dependencies: + command: sudo dnf check-update; sudo dnf install -y \ + cmake \ + make \ + gcc-c++ \ + openssl-devel \ + libXtst-devel \ + glib2-devel \ + gdk-pixbuf2-devel \ + libnotify-devel \ + qt5-qtbase-devel \ + qt5-qttools-devel \ + libxkbfile-devel \ + gtk3-devel \ + rpm-build + + centos: + <<: *fedora + command: sudo yum install -y diff --git a/cspell.json b/cspell.json new file mode 100644 index 000000000..6d9602fb4 --- /dev/null +++ b/cspell.json @@ -0,0 +1,25 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "aqtinstall", + "codesign", + "codesigning", + "distros", + "dmgbuild", + "dotenv", + "gdrive", + "keychain", + "Keychains", + "macdeployqt", + "msvc", + "notarytool", + "outputdir", + "runas", + "winget" + ], + "ignoreWords": [], + "import": [] +} diff --git a/deps.yml b/deps.yml deleted file mode 100644 index 6f1489232..000000000 --- a/deps.yml +++ /dev/null @@ -1,96 +0,0 @@ -dependencies: - windows: - command: choco install Chocolatey.config -y - qt: - version: 5.15.2 - mirror: https://qt.mirror.constant.com/ - install-dir: C:\Qt - ci: - skip: - edit-config: Chocolatey.config - packages: - - cmake - - visualstudio2022buildtools - - mac: - command: brew bundle --file=Brewfile - - linux: - debian: &debian - command: sudo apt-get update && sudo apt-get install -y - packages: - - cmake - - make - - g++ - - xorg-dev - - libx11-dev - - libxtst-dev - - libssl-dev - - pkg-config - - libglib2.0-dev - - libgdk-pixbuf-2.0-dev - - libnotify-dev - - libxkbfile-dev - - qtbase5-dev - - qttools5-dev - - libgtk-3-dev - - rpm - - ubuntu: - <<: *debian - - fedora: &fedora - command: sudo dnf install -y - packages: - - cmake - - make - - gcc-c++ - - openssl-devel - - libXtst-devel - - glib2-devel - - gdk-pixbuf2-devel - - libnotify-devel - - qt5-qtbase-devel - - qt5-qttools-devel - - libxkbfile-devel - - gtk3-devel - - rpm-build - - centos: - <<: *fedora - command: sudo yum install -y - - # Warning: AI generated (untested) - arch: - command: sudo pacman -Syu --noconfirm - packages: - - cmake - - make - - gcc - - libx11 - - libxtst - - openssl - - pkg-config - - glib2 - - gdk-pixbuf2 - - libnotify - - libxkbfile - - qt5-base - - gtk3 - - rpm - - # Warning: AI generated (untested) - opensuse: - command: sudo zypper install -y - packages: - - cmake - - make - - gcc-c++ - - libXtst-devel - - glib2-devel - - gdk-pixbuf-devel - - libnotify-devel - - libxkbfile-devel - - libqt5-qtbase-devel - - gtk3-devel - - rpm-build diff --git a/doc/MacReadme.txt b/doc/MacReadme.txt deleted file mode 100755 index dfa33fa92..000000000 --- a/doc/MacReadme.txt +++ /dev/null @@ -1,18 +0,0 @@ -Mac OS X Readme -=============== - -To install on Mac OS X with the .zip distribution (first seen in 1.3.6) you must follow these steps: - - 1. Extract the zip file to any location (usually double click will do this) - 2. Open Terminal, and cd to the extracted directory (e.g. /Users/my-name/Downloads/extracted-dir/) - 3. Copy the binaries to /usr/bin using: sudo cp synergy* /usr/bin - 4. Correct the permissions and ownership: sudo chown root:wheel /usr/bin/synergy*; sudo chmod 555 /usr/bin/synergy* - -Alternatively, you can copy the binaries as root. How to enable the root user in Mac OS X: - http://support.apple.com/en-us/ht1528 - -Once the binaries have been copied to /usr/bin, you should follow the configuration guide: - http://synergy2.sourceforge.net/configuration.html - -If you have any problems, see the [[Support]] page: - http://symless.com/help/ diff --git a/dist/macos/bundle/Synergy.app/Contents/Info.plist.in b/res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in similarity index 100% rename from dist/macos/bundle/Synergy.app/Contents/Info.plist.in rename to res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in diff --git a/dist/macos/bundle/Synergy.app/Contents/PkgInfo b/res/dist/macos/bundle/Synergy.app/Contents/PkgInfo similarity index 100% rename from dist/macos/bundle/Synergy.app/Contents/PkgInfo rename to res/dist/macos/bundle/Synergy.app/Contents/PkgInfo diff --git a/dist/macos/bundle/Synergy.app/Contents/Resources/.installer_background.tiff b/res/dist/macos/bundle/Synergy.app/Contents/Resources/.background.tiff similarity index 100% rename from dist/macos/bundle/Synergy.app/Contents/Resources/.installer_background.tiff rename to res/dist/macos/bundle/Synergy.app/Contents/Resources/.background.tiff diff --git a/dist/macos/bundle/Synergy.app/Contents/Resources/Synergy.icns b/res/dist/macos/bundle/Synergy.app/Contents/Resources/Synergy.icns similarity index 100% rename from dist/macos/bundle/Synergy.app/Contents/Resources/Synergy.icns rename to res/dist/macos/bundle/Synergy.app/Contents/Resources/Synergy.icns diff --git a/dist/macos/bundle/Synergy.app/Contents/Resources/Drive.icns b/res/dist/macos/bundle/Synergy.app/Contents/Resources/VolumeIcon.icns similarity index 100% rename from dist/macos/bundle/Synergy.app/Contents/Resources/Drive.icns rename to res/dist/macos/bundle/Synergy.app/Contents/Resources/VolumeIcon.icns diff --git a/res/dist/macos/dmgbuild/settings.py b/res/dist/macos/dmgbuild/settings.py new file mode 100644 index 000000000..3b282dfa3 --- /dev/null +++ b/res/dist/macos/dmgbuild/settings.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os.path + +# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg +# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg + +# .. Useful stuff .............................................................. + +app = defines.get("app") +app_basename = os.path.basename(app) + +# .. Basics .................................................................... + +# Volume format (see hdiutil create -help) +format = defines.get("format", "UDBZ") + +# Volume size +size = defines.get("size", None) + +# Files to include +files = [app] + +# Symlinks to create +symlinks = {"Applications": "/Applications"} + +# Volume icon +# +# You can either define icon, in which case that icon file will be copied to the +# image, *or* you can define badge_icon, in which case the icon file you specify +# will be used to badge the system's Removable Disk icon +# +icon = os.path.join(app, "Contents/Resources/VolumeIcon.icns") + +# Where to put the icons +icon_locations = { + app_basename: (144, 190), + "Applications": (455, 190), + ".background.tiff": (150, 450), + ".VolumeIcon.icns": (455, 450), +} + +# .. Window configuration ...................................................... + +# Background +# +# This is a STRING containing any of the following: +# +# #3344ff - web-style RGB color +# #34f - web-style RGB color, short form (#34f == #3344ff) +# rgb(1,0,0) - RGB color, each value is between 0 and 1 +# hsl(120,1,.5) - HSL (hue saturation lightness) color +# hwb(300,0,0) - HWB (hue whiteness blackness) color +# cmyk(0,1,0,0) - CMYK color +# goldenrod - X11/SVG named color +# builtin-arrow - A simple built-in background with a blue arrow +# /foo/bar/baz.png - The path to an image file +# +# The hue component in hsl() and hwb() may include a unit; it defaults to +# degrees ('deg'), but also supports radians ('rad') and gradians ('grad' +# or 'gon'). +# +# Other color components may be expressed either in the range 0 to 1, or +# as percentages (e.g. 60% is equivalent to 0.6). +background = os.path.join(app, "Contents/Resources/.background.tiff") + +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 180 + +# Window position in ((x, y), (w, h)) format +window_rect = ((200, 120), (620, 420)) + +# Select the default view; must be one of +# +# 'icon-view' +# 'list-view' +# 'column-view' +# 'coverflow' +# +default_view = "icon-view" + +# General view configuration +show_icon_preview = False + +# Set these to True to force inclusion of icon/list view settings (otherwise +# we only include settings for the default view) +include_icon_view_settings = "auto" +include_list_view_settings = "auto" + +# .. Icon view configuration ................................................... + +arrange_by = None +grid_offset = (0, 0) +grid_spacing = 100 +scroll_position = (0, 0) +label_pos = "bottom" # or 'right' +text_size = 16 +icon_size = 100 + +# .. List view configuration ................................................... + +# Column names are as follows: +# +# name +# date-modified +# date-created +# date-added +# date-last-opened +# size +# kind +# label +# version +# comments +# +list_icon_size = 16 +list_text_size = 12 +list_scroll_position = (0, 0) +list_sort_by = "name" +list_use_relative_dates = True +list_calculate_all_sizes = (False,) +list_columns = ("name", "date-modified", "size", "kind", "date-added") +list_column_widths = { + "name": 300, + "date-modified": 181, + "date-created": 181, + "date-added": 181, + "date-last-opened": 181, + "size": 97, + "kind": 115, + "label": 100, + "version": 75, + "comments": 300, +} +list_column_sort_directions = { + "name": "ascending", + "date-modified": "descending", + "date-created": "descending", + "date-added": "descending", + "date-last-opened": "descending", + "size": "descending", + "kind": "ascending", + "label": "ascending", + "version": "ascending", + "comments": "ascending", +} diff --git a/dist/rpm/synergy-business.spec.in b/res/dist/rpm/synergy-business.spec.in similarity index 100% rename from dist/rpm/synergy-business.spec.in rename to res/dist/rpm/synergy-business.spec.in diff --git a/dist/rpm/synergy-enterprise.spec.in b/res/dist/rpm/synergy-enterprise.spec.in similarity index 100% rename from dist/rpm/synergy-enterprise.spec.in rename to res/dist/rpm/synergy-enterprise.spec.in diff --git a/dist/rpm/synergy.spec.in b/res/dist/rpm/synergy.spec.in similarity index 100% rename from dist/rpm/synergy.spec.in rename to res/dist/rpm/synergy.spec.in diff --git a/dist/wix/Include.wxi.in b/res/dist/wix/Include.wxi.in similarity index 100% rename from dist/wix/Include.wxi.in rename to res/dist/wix/Include.wxi.in diff --git a/dist/wix/Product.wxs b/res/dist/wix/Product.wxs similarity index 100% rename from dist/wix/Product.wxs rename to res/dist/wix/Product.wxs diff --git a/dist/wix/Synergy.sln b/res/dist/wix/Synergy.sln similarity index 100% rename from dist/wix/Synergy.sln rename to res/dist/wix/Synergy.sln diff --git a/dist/wix/Synergy.wixproj b/res/dist/wix/Synergy.wixproj similarity index 100% rename from dist/wix/Synergy.wixproj rename to res/dist/wix/Synergy.wixproj diff --git a/dist/wix/SynergyBrowseDlg.wxs b/res/dist/wix/SynergyBrowseDlg.wxs similarity index 100% rename from dist/wix/SynergyBrowseDlg.wxs rename to res/dist/wix/SynergyBrowseDlg.wxs diff --git a/dist/wix/SynergyDlgSequence.wxs b/res/dist/wix/SynergyDlgSequence.wxs similarity index 100% rename from dist/wix/SynergyDlgSequence.wxs rename to res/dist/wix/SynergyDlgSequence.wxs diff --git a/dist/wix/SynergyInstallDlg.wxs b/res/dist/wix/SynergyInstallDlg.wxs similarity index 100% rename from dist/wix/SynergyInstallDlg.wxs rename to res/dist/wix/SynergyInstallDlg.wxs diff --git a/dist/wix/SynergyMaintenanceTypeDlg.wxs b/res/dist/wix/SynergyMaintenanceTypeDlg.wxs similarity index 100% rename from dist/wix/SynergyMaintenanceTypeDlg.wxs rename to res/dist/wix/SynergyMaintenanceTypeDlg.wxs diff --git a/dist/wix/SynergyVerifyReadyDlg.wxs b/res/dist/wix/SynergyVerifyReadyDlg.wxs similarity index 100% rename from dist/wix/SynergyVerifyReadyDlg.wxs rename to res/dist/wix/SynergyVerifyReadyDlg.wxs diff --git a/dist/wix/SynergyWelcome.wxs b/res/dist/wix/SynergyWelcome.wxs similarity index 100% rename from dist/wix/SynergyWelcome.wxs rename to res/dist/wix/SynergyWelcome.wxs diff --git a/dist/wix/msm/Microsoft_VC142_CRT_x64.msm b/res/dist/wix/msm/Microsoft_VC142_CRT_x64.msm similarity index 100% rename from dist/wix/msm/Microsoft_VC142_CRT_x64.msm rename to res/dist/wix/msm/Microsoft_VC142_CRT_x64.msm diff --git a/dist/wix/msm/Microsoft_VC142_CRT_x86.msm b/res/dist/wix/msm/Microsoft_VC142_CRT_x86.msm similarity index 100% rename from dist/wix/msm/Microsoft_VC142_CRT_x86.msm rename to res/dist/wix/msm/Microsoft_VC142_CRT_x86.msm diff --git a/scripts/build_version.py b/scripts/build_version.py deleted file mode 100644 index 65883e44a..000000000 --- a/scripts/build_version.py +++ /dev/null @@ -1,137 +0,0 @@ -import subprocess - -class VersionPart: - - def __init__(self, part = ''): - self.prefix = '' - self.suffix = '' - self.number = 0 - - if part: - self.__parsePreffix(part) - self.__parseNumber(part) - self.__parseSuffix(part) - - def __parsePreffix(self, part): - if not part[0].isdigit(): - for i in part: - if not i.isdigit(): - self.prefix += i - else: - break - - def __parseNumber(self, part): - start = len(self.prefix) - end = part.find('-') - if end > 0: - self.number = int(part[start:end]) - else: - self.number = int(part[start:]) - - def __parseSuffix(self, part): - items = part.split('-') - if len(items) == 2: - self.suffix = '-' + items[1] - - def __str__(self): - return self.prefix + str(self.number) + self.suffix -class Version: - - def __init__(self, version): - versionParts = version.split('.') - - if len(versionParts) == 3: - self.major = VersionPart(versionParts[0]) - self.minor = VersionPart(versionParts[1]) - self.build = VersionPart(versionParts[2]) - self.patch = VersionPart() - self.patch.number = self.build.number - elif len(versionParts) == 4: - self.major = VersionPart(versionParts[0]) - self.minor = VersionPart(versionParts[1]) - self.patch = VersionPart(versionParts[2]) - self.build = VersionPart(versionParts[3]) - else: - print('ERROR: Wrong version number') - - def isSamePatch(self, version): - return (self.major.number == version.major.number and - self.minor.number == version.minor.number and - self.patch.number == version.patch.number) - - def __str__(self): - result = str(self.major) + '.' - result += str(self.minor) + '.' - result += str(self.patch) + '.' - result += str(self.build) - return result - -class VersionFile: - def __init__(self, file): - self.file = file - - def setOption(self, name, value): - fp = open(self.file, 'rt+') - content = '' - for line in fp: - print(line) - if line.find(name) != -1: - line = 'set ('+ name + ' ' + value + ')' - content += line - fp.write(content) - fp.close() - -def findVersion(versions, cmakeVersion): - gitVersion = Version(versions[0]) - for version in versions: - ver = Version(version) - if ver.isSamePatch(cmakeVersion): - gitVersion = ver - break - print('INFO: Version '+ str(gitVersion) + ' has been read from git') - return gitVersion - -def getVesionFromGit(cmakeVersion): - try: - taggedRevision = subprocess.check_output(('git rev-list --tags --max-count=100').split(), universal_newlines=True) - cmd = ('git describe --tags ' + taggedRevision).split() - versions = subprocess.check_output(cmd, universal_newlines=True).split() - return findVersion(versions, cmakeVersion) - except subprocess.CalledProcessError: - print('ERROR: Unable to get version from git') - exit(1) - -def updateVersionFile(number): - fp = open('cmake/Version.cmake') - content = fp.read() - fp.close() - - fp = open('cmake/Version.cmake', 'wt') - fp.write(content.replace('set (SYNERGY_VERSION_BUILD 1)', 'set (SYNERGY_VERSION_BUILD ' + str(number) + ')')) - fp.close() - -def getOptionValue(source, option): - start = source.find(option) - if (start != -1): - start += len(option) + 1 - end = source.find(')', start) - return source[start : end] - else: - print("ERROR: Can't find option <" + option + ">"); - -def getVersionFromFile(): - fp = open('cmake/Version.cmake') - content = fp.read() - fp.close() - - major = getOptionValue(content, 'SYNERGY_VERSION_MAJOR') - minor = getOptionValue(content, 'SYNERGY_VERSION_MINOR') - patch = getOptionValue(content, 'SYNERGY_VERSION_PATCH') - - return Version(major + '.' + minor + '.' + patch) - -if __name__ == '__main__': - cmakeVersion = getVersionFromFile() - gitVersion = getVesionFromGit(cmakeVersion) - updateVersionFile(gitVersion.build.number) - print('INFO: Build number is: <' + str(gitVersion) + '>') \ No newline at end of file diff --git a/scripts/github_env.py b/scripts/github_env.py new file mode 100755 index 000000000..3990f53e6 --- /dev/null +++ b/scripts/github_env.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import os +from lib import env + +# important: load venv before loading modules that install deps. +env.ensure_in_venv(__file__) + +github_key = "GITHUB_ENV" +version_key = "SYNERGY_VERSION" + + +def main(): + env_file = os.getenv(github_key) + if not env_file: + raise RuntimeError(f"Env var {github_key} not set") + + if not os.path.exists(env_file): + raise RuntimeError(f"File not found: {env_file}") + + major, minor, patch, stage, _build = env.get_version_info() + version_value = f"{major}.{minor}.{patch}-{stage}" + + print(f"Setting env var: {version_key}={version_value}") + with open(env_file, "a") as env_file: + env_file.write(f"{version_key}={version_value}\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/install_deps.py b/scripts/install_deps.py index 0f5b0bad1..f9b319e2c 100755 --- a/scripts/install_deps.py +++ b/scripts/install_deps.py @@ -1,38 +1,13 @@ #!/usr/bin/env python3 -import os -from lib import windows, cmd_utils -import sys -import argparse -import traceback +import os, sys, argparse, traceback +from lib import env, cmd_utils -config_file = "deps.yml" - - -class YamlError(Exception): - pass - - -class PlatformError(Exception): - pass - - -class PathError(Exception): - pass - - -try: - import yaml # type: ignore -except ImportError: - # this is fairly common in earlier versions of python3, - # which is normally what you find on mac and windows. - print("Python yaml module missing, please install: pip install pyyaml") - sys.exit(1) +# important: load venv before loading modules that install deps. +env.ensure_in_venv(__file__) def main(): - """Entry point for the script.""" - parser = argparse.ArgumentParser() parser.add_argument( "--pause-on-exit", action="store_true", help="Useful on Windows" @@ -42,171 +17,110 @@ def main(): ) args = parser.parse_args() + error = False try: deps = Dependencies(args.only) deps.install() except Exception: traceback.print_exc() + error = True if args.pause_on_exit: input("Press enter to continue...") - -def get_os(): - """Detects the operating system.""" - if sys.platform == "win32": - return "windows" - elif sys.platform == "darwin": - return "mac" - elif sys.platform.startswith("linux"): - return "linux" - else: - raise PlatformError(f"Unsupported platform: {sys.platform}") - - -def get_linux_distro(): - """Detects the Linux distro.""" - os_file = "/etc/os-release" - if os.path.isfile(os_file): - with open(os_file) as f: - for line in f: - if line.startswith("ID="): - return line.strip().split("=")[1].strip('"') - return None - - -class Config: - """Reads the dependencies configuration file.""" - - def __init__(self): - with open(config_file, "r") as f: - data = yaml.safe_load(f) - - os_name = get_os() - try: - root = data["dependencies"] - except KeyError: - raise YamlError(f"Nothing found in {config_file} for: dependencies") - - try: - self.os = root[os_name] - except KeyError: - raise YamlError(f"Nothing found in {config_file} for: {os_name}") - - def get_qt_config(self): - try: - return self.os["qt"] - except KeyError: - raise YamlError(f"Nothing found in {config_file} for: qt") - - def get_packages_file(self): - try: - return self.os["packages"] - except KeyError: - raise YamlError(f"Nothing found in {config_file} for: packages") - - def get_linux_package_command(self, distro): - try: - distro_data = self.os[distro] - except KeyError: - raise YamlError(f"Nothing found in {config_file} for: {distro}") - - try: - command_base = distro_data["command"] - except KeyError: - raise YamlError(f"No package command found in {config_file} for: {distro}") - - try: - package_data = distro_data["packages"] - except KeyError: - raise YamlError(f"No package list found in {config_file} for: {distro}") - - packages = " ".join(package_data) - return f"{command_base} {packages}" + if error: + sys.exit(1) class Dependencies: def __init__(self, only): + from lib.config import Config + self.config = Config() self.only = only + self.ci_env = env.is_running_in_ci() + + if self.ci_env: + print("CI environment detected") def install(self): """Installs dependencies for the current platform.""" - os = get_os() - if os == "windows": + if env.is_windows(): self.windows() - elif os == "mac": + elif env.is_mac(): self.mac() - elif os == "linux": + elif env.is_linux(): self.linux() else: - raise PlatformError(f"Unsupported platform: {os}") + raise RuntimeError(f"Unsupported platform: {os}") def windows(self): """Installs dependencies on Windows.""" + from lib import windows if not windows.is_admin(): windows.relaunch_as_admin(__file__) sys.exit() - ci_env = os.environ.get("CI") - if ci_env: - print("CI environment detected") - only_qt = self.only == "qt" # for ci, skip qt; we install qt separately so we can cache it. - if not ci_env or only_qt: - qt = windows.WindowsQt(self.config.get_qt_config(), config_file) + if not self.ci_env or only_qt: + qt = windows.WindowsQt(*self.config.get_qt_config()) qt_install_dir = qt.get_install_dir() if qt_install_dir: print(f"Skipping Qt, already installed at: {qt_install_dir}") else: qt.install() + if not self.ci_env: + qt.set_env_vars() + if only_qt: return choco = windows.WindowsChoco() - if ci_env: + if self.ci_env: choco.config_ci_cache() - - try: - ci_skip = self.config.os["ci"]["skip"] - choco_config_file = ci_skip["edit-config"] - remove_packages = ci_skip["packages"] - except KeyError: - raise YamlError(f"Bad mapping in {config_file} on Windows for: ci") - + choco_config_file, remove_packages = self.config.get_choco_config() choco.remove_from_config(choco_config_file, remove_packages) - try: - command = self.config.os["command"] - except KeyError: - raise YamlError(f"Nothing found in {config_file} on Windows for: command") - - choco.install(command, ci_env) + command = self.config.get_deps_command() + choco.install(command, self.ci_env) def mac(self): """Installs dependencies on macOS.""" - try: - command = self.config.os["command"] - except KeyError: - raise YamlError(f"Nothing found in {config_file} on Mac for: command") + from lib import mac + command = self.config.get_os_deps_value("command") cmd_utils.run(command) + if not self.ci_env: + mac.set_cmake_prefix_env_var(self.config.get_os_value("qt-prefix-command")) + def linux(self): """Installs dependencies on Linux.""" - distro = get_linux_distro() + distro = env.get_linux_distro() if not distro: - raise PlatformError("Unable to detect Linux distro") + raise RuntimeError("Unable to detect Linux distro") - command = self.config.get_linux_package_command(distro) - cmd_utils.run(command) + command = self.config.get_linux_deps_command(distro) + + has_sudo = cmd_utils.has_command("sudo") + if "sudo" in command and not has_sudo: + # assume we're running as root if sudo is not found (common on older distros). + # a space char is intentionally added after "sudo" for intentionality. + # possible limitation with stripping "sudo" is that if any packages with "sudo" in the + # name are added to the list (probably very unlikely), this will have undefined behavior. + print("The 'sudo' command was not found, stripping sudo from command") + command = command.replace("sudo ", "").strip() + + # don't check the return code, as some package managers return non-zero exit codes + # under normal circumstances (e.g. dnf returns 100 when there are updates available). + cmd_utils.run(command, check=False) if __name__ == "__main__": diff --git a/scripts/lib/cmd_utils.py b/scripts/lib/cmd_utils.py index a923877c7..00ae2c0c8 100644 --- a/scripts/lib/cmd_utils.py +++ b/scripts/lib/cmd_utils.py @@ -2,18 +2,91 @@ import subprocess import sys -def run(command, check=True): - """Runs a shell command and by default asserts that the return code is 0.""" +def has_command(command): + platform = sys.platform + if platform == "win32": + cmd = f"where {command}" + else: + cmd = f"which {command}" + try: + subprocess.check_output(cmd, shell=True) + return True + except subprocess.CalledProcessError: + return False + +def strip_continuation_sequences(command): + """ + Remove the continuation sequences (\\) from a command. + + To spread strings over multiple lines in YAML files, like in bash, a backslash is used at + the end of each line as continuation character. + When a YAML file is parsed, this becomes "\\ " (without a new line char), so this character + sequence must be removed before running the command. + This doesn't seem to be an issue on Windows, since the \\ path separator is rarely followed + by a space. + """ + cmd_continuation = "\\ " + + if isinstance(command, list): + return [c.replace(cmd_continuation, "") for c in command] + else: + return command.replace(cmd_continuation, "") + + +# TODO: fix bug: often when using this function, only the first arg element is sent to subprocess.run +def run( + command, + check=True, + shell=True, + get_output=False, + print_cmd=True, +): + """ + Convenience wrapper around `subprocess.run` to: + - print the command before running it + - pipe/capture the output instead of printing it + + This differs to `subprocess.run` in that by default it: + - checks the return code by default + - uses a shell by default (sometimes a bad idea for security) + + Warning: This code is used by CI and prints the command before running it; + never use this function with sensitive information such as passwords, + unless you want the world to know. + + Args: + command (str or list): The command to run. + check (bool): Raise an exception if the command fails. + shell (bool): Run the command in a shell. + get_output (bool): Return the output of the command. + print_cmd (bool): Print the command before running it. + """ + + # create string version of list command, only for debugging purposes command_str = command if isinstance(command, list): command_str = " ".join(command) - print(f"Running: {command_str}") - sys.stdout.flush() + if print_cmd: + print(f"Running: {command_str}") + sys.stdout.flush() - try: - subprocess.run(command, shell=True, check=check) - except subprocess.CalledProcessError as e: - print(f"Command failed: {command_str}", file=sys.stderr) - raise e + if get_output: + result = subprocess.run( + command, + shell=shell, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + else: + result = subprocess.run(command, check=check, shell=shell) + + if print_cmd and result.returncode != 0: + print( + f"Command exited with code {result.returncode}: {command_str}", + file=sys.stderr, + ) + return result diff --git a/scripts/lib/config.py b/scripts/lib/config.py new file mode 100644 index 000000000..31336b078 --- /dev/null +++ b/scripts/lib/config.py @@ -0,0 +1,107 @@ +from lib import env, cmd_utils + +env.ensure_module("yaml", "pyyaml") +import yaml + +config_file = "config.yml" +deps_key = "dependencies" + + +class ConfigError(RuntimeError): + pass + + +class Config: + """Reads the project configuration YAML file.""" + + def __init__(self): + with open(config_file, "r") as f: + data = yaml.safe_load(f) + + self.os_name = env.get_os() + root_key = "config" + try: + root = data[root_key] + except KeyError: + raise ConfigError(f"Nothing found in {config_file} for: {root_key}") + + try: + self.os = root[self.os_name] + except KeyError: + raise ConfigError(f"Nothing found in {config_file} for: {self.os_name}") + + def get_os_value(self, key): + try: + return self.os[key] + except KeyError: + raise ConfigError( + f"Nothing found in {config_file} for: {self.os_name}:{key}" + ) + + def get_qt_config(self): + qt = self.get_os_deps_value("qt") + + try: + mirror_url = qt["mirror"] + except KeyError: + raise ConfigError(f"Qt mirror not set in {self.config_file}") + + try: + default_version = qt["version"] + except KeyError: + raise ConfigError(f"Qt version not set in {self.config_file}") + + try: + default_base_dir = qt["install-dir"] + except KeyError: + raise ConfigError(f"Qt install-dir not set in {self.config_file}") + + return mirror_url, default_version, default_base_dir + + def get_os_deps_value(self, key): + deps = self.get_os_value(deps_key) + try: + return deps[key] + except KeyError: + raise ConfigError( + f"Nothing found in {config_file} for: {self.os_name}:{deps_key}:{key}" + ) + + def get_deps_command(self): + dependencies = self.get_os_value(deps_key) + try: + command = dependencies["command"] + except KeyError: + raise ConfigError( + f"No dependencies command found in {config_file} for: {self.os_name}" + ) + return cmd_utils.strip_continuation_sequences(command) + + def get_linux_deps_command(self, distro): + distro_data = self.get_os_value(distro) + + try: + deps = distro_data[deps_key] + except KeyError: + raise ConfigError( + f"No dependencies config found in {config_file} for: {distro}" + ) + + try: + command = deps["command"] + return cmd_utils.strip_continuation_sequences(command) + except KeyError: + + raise ConfigError( + f"No dependencies command found in {config_file} for: {self.os_name}:{distro}" + ) + + def get_choco_config(self): + ci = self.get_os_deps_value("ci") + try: + ci_skip = ci["skip"] + choco_config_file = ci_skip["edit-config"] + remove_packages = ci_skip["packages"] + except KeyError: + raise ConfigError(f"Bad structure in {config_file} under: ci") + return choco_config_file, remove_packages diff --git a/scripts/lib/env.py b/scripts/lib/env.py new file mode 100644 index 000000000..a454f9e18 --- /dev/null +++ b/scripts/lib/env.py @@ -0,0 +1,163 @@ +import os, sys, subprocess, platform +from lib import env, cmd_utils + +venv_path = "build/python" +version_env = "build/.env.version" + + +def check_module(module): + try: + __import__(module) + return True + except ImportError: + print(f"Python is missing {module} module", file=sys.stderr) + return False + + +def get_os(): + """Detects the operating system.""" + if sys.platform == "win32": + return "windows" + elif sys.platform == "darwin": + return "mac" + elif sys.platform.startswith("linux"): + return "linux" + else: + raise RuntimeError(f"Unsupported platform: {sys.platform}") + + +def is_windows(): + return get_os() == "windows" + + +def is_mac(): + return get_os() == "mac" + + +def is_linux(): + return get_os() == "linux" + + +def is_running_in_ci(): + """Returns True if running in a CI environment.""" + return os.environ.get("CI") + + +def get_linux_distro(): + """Detects the Linux distro.""" + os_file = "/etc/os-release" + if os.path.isfile(os_file): + with open(os_file) as f: + for line in f: + if line.startswith("ID="): + return line.strip().split("=")[1].strip('"') + return None + + +def get_env_var(name): + """Returns an env var or raises an error if it is not set.""" + value = os.getenv(name) + if not value: + raise ValueError(f"Environment variable not set: {name}") + return value + + +def get_python_executable(binary="python"): + if sys.platform == "win32": + return os.path.join(venv_path, "Scripts", binary) + else: + return os.path.join(venv_path, "bin", binary) + + +def in_venv(): + """Returns True if the script is running in a Python virtual environment.""" + return sys.prefix != sys.base_prefix + + +def ensure_in_venv(script): + """ + Ensures the script is running in a Python virtual environment (venv). + If the script is not running in a venv, it will create one and re-run the script in the venv. + """ + + ensure_dependencies() + import venv + + if not in_venv(): + if not os.path.exists(venv_path): + print(f"Creating virtual environment at {venv_path}") + venv.create(venv_path, with_pip=True) + + script_file = os.path.basename(script) + print(f"Using virtual environment for {script_file}") + sys.stdout.flush() + python_executable = get_python_executable() + result = subprocess.run([python_executable, script] + sys.argv[1:]) + sys.exit(result.returncode) + + +# TODO: Use pyproject.toml to specify dependencies +def ensure_module(module, package): + """ + Ensures that a Python module is available, and installs the package if it is not. + """ + + ensure_dependencies() + + try: + __import__(module) + except ImportError: + print(f"Python missing {module}, installing {package}...", file=sys.stderr) + cmd_utils.run([sys.executable, "-m", "pip", "install", package], shell=False) + + +def ensure_dependencies(): + """ + Ensures that pip and venv are available, and installs them if they are not. + This is normally only required on Linux. + """ + + has_pip = check_module("pip") + has_venv = check_module("venv") + + if has_pip and has_venv: + return + + print("Installing Python dependencies...") + + os = get_os() + if os != "linux": + # should not be a problem, since windows and mac come with pip and venv + raise RuntimeError(f"Unable to install Python dependencies on {os}") + + has_sudo = cmd_utils.has_command("sudo") + sudo = "sudo" if has_sudo else "" + + distro = get_linux_distro() + if distro == "ubuntu" or distro == "debian": + cmd_utils.run(f"{sudo} apt update".strip(), check=False) + cmd_utils.run(f"{sudo} apt install -y python3-pip python3-venv".strip()) + elif distro == "fedora" or distro == "centos": + cmd_utils.run(f"{sudo} dnf check-update".strip(), check=False) + cmd_utils.run(f"{sudo} dnf install -y python3-pip python3-virtualenv".strip()) + else: + # arch, opensuse, etc, patches welcome! :) + raise RuntimeError(f"Unable to install Python dependencies on {distro}") + + +def get_version_info(): + env.ensure_module("dotenv", "python-dotenv") + from dotenv import load_dotenv # type: ignore + + if not os.path.isfile(version_env): + raise RuntimeError(f"Version file not found: {version_env}") + + load_dotenv(dotenv_path=version_env) + + major = os.getenv("SYNERGY_VERSION_MAJOR") + minor = os.getenv("SYNERGY_VERSION_MINOR") + patch = os.getenv("SYNERGY_VERSION_PATCH") + stage = os.getenv("SYNERGY_VERSION_STAGE") + build = os.getenv("SYNERGY_VERSION_BUILD") + + return major, minor, patch, stage, build diff --git a/scripts/lib/mac.py b/scripts/lib/mac.py new file mode 100644 index 000000000..968e08d49 --- /dev/null +++ b/scripts/lib/mac.py @@ -0,0 +1,282 @@ +import os, subprocess, base64, time, json, shutil, sys +from lib import cmd_utils, env + +cmake_env_var = "CMAKE_PREFIX_PATH" +shell_rc = "~/.zshrc" +cert_path = "tmp/codesign.p12" +dist_dir = "dist" +product_name = "Synergy" +settings_file = "res/dist/macos/dmgbuild/settings.py" +app_path = "build/bundle/Synergy.app" +security_path = "/usr/bin/security" +sudo_path = "/usr/bin/sudo" +notarytool_path = "/usr/bin/notarytool" +codesign_path = "/usr/bin/codesign" +xcode_select_path = "/usr/bin/xcode-select" +keychain_path = "/Library/Keychains/System.keychain" + + +def set_env_var(name, value): + text = f'export {name}="${name}:{value}"' + file = os.path.expanduser(shell_rc) + with open(file, "r") as f: + if text in f.read(): + return + + print(f"Setting environment variable: {name}={name}") + with open(file, "a") as f: + f.write(f"\n{text}") + print(f"Appended to {shell_rc}: {text}") + + +def set_cmake_prefix_env_var(cmake_prefix_command): + result = cmd_utils.run(cmake_prefix_command, get_output=True) + cmake_prefix = result.stdout.strip() + set_env_var(cmake_env_var, cmake_prefix) + + +def package(filename_base): + codesign_id = env.get_env_var("APPLE_CODESIGN_ID") + certificate = env.get_env_var("APPLE_P12_CERTIFICATE") + password = env.get_env_var("APPLE_P12_PASSWORD") + + build_bundle() + install_certificate(certificate, password) + assert_certificate_installed(codesign_id) + sign_bundle(codesign_id) + dmg_path = build_dmg(filename_base) + notarize_package(dmg_path) + + +def build_bundle(): + print("Building bundle...") + # cmake build install target should run macdeployqt + cmd_utils.run("cmake --build build --target install") + + +def sign_bundle(codesign_id): + print(f"Signing bundle {app_path}...") + sys.stdout.flush() + subprocess.run( + [ + codesign_path, + "-f", + "--options", + "runtime", + "--deep", + "-s", + codesign_id, + app_path, + ], + check=True, + ) + + +def assert_certificate_installed(codesign_id): + installed = cmd_utils.run( + "security find-identity -v -p codesigning", get_output=True + ) + + if codesign_id not in installed.stdout: + raise RuntimeError("Code signing certificate not installed or has expired") + + +def build_dmg(filename_base): + env.ensure_module("dmgbuild", "dmgbuild") + import dmgbuild # type: ignore + + settings_file_abs = os.path.abspath(settings_file) + app_path_abs = os.path.abspath(app_path) + + # cwd for dmgbuild, since setting the dmg filename to a path (include the dist dir) seems to + # make the dmg disappear and never writes to the specified path. the dmgbuild module also + # creates a temporary file in cwd, so it makes sense to change to the dist dir. + print(f"Changing directory to: {os.path.abspath(dist_dir)}") + cwd = os.getcwd() + os.makedirs(dist_dir, exist_ok=True) + os.chdir(dist_dir) + + try: + dmg_filename = f"{filename_base}.dmg" + dmg_path = os.path.join(dist_dir, dmg_filename) + print(f"Building package {dmg_path}...") + dmgbuild.build_dmg( + dmg_filename, + product_name, + settings_file=settings_file_abs, + defines={ + "app": app_path_abs, + }, + ) + finally: + print(f"Changing directory back to: {cwd}") + os.chdir(cwd) + + return dmg_path + + +def install_certificate(cert_base64, cert_password): + if not cert_base64: + raise ValueError("Certificate base 64 not provided") + + if not cert_password: + raise ValueError("Certificate password not provided") + + print(f"Decoding certificate to: {cert_path}") + cert_bytes = base64.b64decode(cert_base64) + os.makedirs(os.path.dirname(cert_path), exist_ok=True) + with open(cert_path, "wb") as cert_file: + cert_file.write(cert_bytes) + + print(f"Installing certificate: {cert_path}") + sys.stdout.flush() + + try: + # warning: contains private key password, never print this command + subprocess.run( + [ + sudo_path, + security_path, + "import", + cert_path, + "-k", + keychain_path, + "-P", + cert_password, + "-T", + codesign_path, + "-T", + security_path, + ], + check=True, + ) + except subprocess.CalledProcessError as e: + # important: suppress the original args with `from None` to avoid leaking the password + raise subprocess.CalledProcessError(e.returncode, security_path) from None + except Exception as e: + # important: suppress the original args with `from None` to avoid leaking the password + raise RuntimeError(f"Command failed: {security_path}") from None + finally: + # not strictly necessary for ci, but when run on a dev machine, it reduces the risk + # that private keys are left on the filesystem + print(f"Removing temporary certificate file: {cert_path}") + os.remove(cert_path) + + +def notarize_package(dmg_path): + print(f"Notarizing package {dmg_path}...") + notary_tool = NotaryTool() + notary_tool.store_credentials( + env.get_env_var("APPLE_NOTARY_USER"), + env.get_env_var("APPLE_NOTARY_PASSWORD"), + env.get_env_var("APPLE_TEAM_ID"), + ) + + notary_tool.submit_and_wait(dmg_path) + + +def get_xcode_path(): + result = cmd_utils.run([xcode_select_path, "-p"], get_output=True, shell=False) + return result.stdout.strip() + + +class NotaryTool: + """ + Provides a wrapper around the notarytool command line tool. + """ + + def __init__(self): + self.xcode_path = get_xcode_path() + + def get_path(self): + return f"{self.xcode_path}{notarytool_path}" + + def store_credentials(self, user, password, team_id): + print("Storing credentials for notary tool...") + sys.stdout.flush() + + notarytool_path = self.get_path() + try: + # warning: contains password, never print this command + subprocess.run( + [ + notarytool_path, + "store-credentials", + "notarytool-password", + "--apple-id", + user, + "--team-id", + team_id, + "--password", + password, + ], + check=True, + ) + except subprocess.CalledProcessError as e: + # important: suppress the original args with `from None` to avoid leaking the password + raise subprocess.CalledProcessError(e.returncode, notarytool_path) from None + except Exception as e: + # important: suppress the original args with `from None` to avoid leaking the password + raise RuntimeError(f"Command failed: {notarytool_path}") from None + + def submit_and_wait(self, dmg_filename): + print("Submitting notarization request...") + submit_result = self.run_submit_command(dmg_filename) + request_id = submit_result["id"] + + print(f"Notary submitted, waiting for request: {request_id}") + start = time.time() + wait_result = self.run_wait_command(request_id) + status = wait_result["status"] + + time_taken = time.time() - start + print(f"Notary complete in {time_taken:.2f}s, status: {status}") + if status == "Accepted": + print("Notarization successful.") + elif status == "Invalid" or status == "Rejected": + raise ValueError(f"Notarization failed, status: {status}") + else: + raise ValueError(f"Unknown status: {status}") + + def run_submit_command(self, dmg_filename): + if not os.path.exists(dmg_filename): + raise FileNotFoundError(f"File not found: {dmg_filename}") + + result = cmd_utils.run( + [ + self.get_path(), + "submit", + dmg_filename, + "--keychain-profile", + "notarytool-password", + "--output-format", + "json", + ], + get_output=True, + shell=False, + ) + + if result.stderr: + return json.loads(result.stderr) + else: + return json.loads(result.stdout) + + def run_wait_command(self, request_id): + result = cmd_utils.run( + [ + self.get_path(), + "wait", + request_id, + "--keychain-profile", + "notarytool-password", + "--output-format", + "json", + ], + get_output=True, + shell=False, + ) + + if result.stderr: + return json.loads(result.stderr) + else: + return json.loads(result.stdout) diff --git a/scripts/lib/windows.py b/scripts/lib/windows.py index 4d7aa1d46..c2d176e5e 100644 --- a/scripts/lib/windows.py +++ b/scripts/lib/windows.py @@ -4,9 +4,7 @@ import os import xml.etree.ElementTree as ET from lib import cmd_utils - -class EnvError(Exception): - pass +cmake_env_var = "CMAKE_PREFIX_PATH" def relaunch_as_admin(script): @@ -24,6 +22,24 @@ def is_admin(): return False +def set_env_var(name, value): + """ + Sets or updates an environment variable. Appends the value if it doesn't already exist. + + Args: + name (str): The name of the environment variable. + value (str): The value of the environment variable. + """ + + current_value = os.getenv(name, "") + + if value not in current_value: + new_value = f"{current_value}{os.pathsep}{value}" if current_value else value + os.environ[name] = new_value + print(f"Setting environment variable: {name}={value}") + cmd_utils.run(["setx", name, new_value], check=True) + + class WindowsChoco: """Chocolatey for Windows.""" @@ -33,6 +49,7 @@ class WindowsChoco: # don't show noisy choco progress bars in ci env cmd_utils.run(f"{command} --no-progress") else: + cmd_utils.run("winget install chocolatey", check=False) cmd_utils.run(command) def config_ci_cache(self): @@ -48,10 +65,10 @@ class WindowsChoco: else: print(f"Warning: CI environment variable {runner_temp_key} not set") - def remove_from_config(self, config_file, remove_packages): + def remove_from_config(self, choco_config_file, remove_packages): """Removes a package from the Chocolatey configuration.""" - tree = ET.parse(config_file) + tree = ET.parse(choco_config_file) root = tree.getroot() for remove in remove_packages: for package in root.findall("package"): @@ -59,33 +76,22 @@ class WindowsChoco: root.remove(package) print(f"Removed package from choco config: {remove}") - tree.write(config_file) + tree.write(choco_config_file) class WindowsQt: """Qt for Windows.""" - def __init__(self, config, config_file): - self.config = config - self.config_file = config_file + def __init__(self, mirror_url, default_version, default_base_dir): + self.mirror_url = mirror_url self.version = os.environ.get("QT_VERSION") if not self.version: - try: - default_version = config["version"] - except KeyError: - raise EnvError(f"Qt version not set in {config_file}") - print(f"QT_VERSION not set, using: {default_version}") self.version = default_version self.base_dir = os.environ.get("QT_BASE_DIR") if not self.base_dir: - try: - default_base_dir = config["install-dir"] - except KeyError: - raise EnvError(f"Qt install-dir not set in {config_file}") - print(f"QT_BASE_DIR not set, using: {default_base_dir}") self.base_dir = default_base_dir @@ -100,17 +106,15 @@ class WindowsQt: cmd_utils.run(["pip", "install", "aqtinstall"]) - try: - mirror_url = self.config["mirror"] - except KeyError: - raise EnvError(f"Qt mirror not set in {self.config_file}") - args = ["python", "-m", "aqt", "install-qt"] args.extend(["--outputdir", self.base_dir]) - args.extend(["--base", mirror_url]) + args.extend(["--base", self.mirror_url]) args.extend(["windows", "desktop", self.version, "win64_msvc2019_64"]) cmd_utils.run(args) install_dir = self.get_install_dir() if not install_dir: - raise EnvError(f"Qt not installed, path not found: {install_dir}") + raise RuntimeError(f"Qt not installed, path not found: {install_dir}") + + def set_env_vars(self): + set_env_var(cmake_env_var, f"{self.get_install_dir()}\\msvc2019_64") diff --git a/scripts/package.py b/scripts/package.py new file mode 100755 index 000000000..fc8701c38 --- /dev/null +++ b/scripts/package.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import platform +from lib import env + +# important: load venv before loading modules that install deps. +env.ensure_in_venv(__file__) + +env_file = ".env" +package_filename_product = "synergy" + + +def main(): + env.ensure_module("dotenv", "python-dotenv") + from dotenv import load_dotenv # type: ignore + + load_dotenv(dotenv_path=env_file) + + major, minor, patch, stage, _build = env.get_version_info() + version = f"{major}.{minor}.{patch}-{stage}" + filename_base = get_filename_base(version) + print(f"Package filename base: {filename_base}") + + if env.is_windows(): + windows_package(filename_base) + elif env.is_mac(): + mac_package(filename_base) + elif env.is_linux(): + linux_package(filename_base) + else: + raise RuntimeError(f"Unsupported platform: {env.get_os()}") + + +def get_filename_base(version): + os = env.get_os() + machine = platform.machine().lower() + return f"{package_filename_product}-{version}-{os}-{machine}" + + +def windows_package(filename_base): + """TODO: Windows packaging""" + pass + + +def mac_package(filename_base): + from lib import mac + + mac.package(filename_base) + + +def linux_package(filename_base): + """TODO: Linux packaging""" + pass + + +if __name__ == "__main__": + main() diff --git a/scripts/windows_daemon.py b/scripts/windows_daemon.py index 69d9e9306..3341029d4 100644 --- a/scripts/windows_daemon.py +++ b/scripts/windows_daemon.py @@ -5,79 +5,90 @@ import argparse import glob from lib import windows -BIN_NAME = 'synergyd' -SOURCE_BIN_DIR = os.path.join('build', 'bin') -TARGET_BIN_DIR = 'bin' +BIN_NAME = "synergyd" +SOURCE_BIN_DIR = os.path.join("build", "bin") +TARGET_BIN_DIR = "bin" SERVICE_NOT_RUNNING_ERROR = 2 + def main(): - """Entry point for the script.""" + """Entry point for the script.""" - parser = argparse.ArgumentParser() - parser.add_argument('--pause-on-exit', action='store_true') - parser.add_argument('--source-bin-dir', default=SOURCE_BIN_DIR) - parser.add_argument('--target-bin-dir', default=TARGET_BIN_DIR) - parser.add_argument('--source-bin-name', default=BIN_NAME) - parser.add_argument('--target-bin-name', default=BIN_NAME) - args = parser.parse_args() + parser = argparse.ArgumentParser() + parser.add_argument("--pause-on-exit", action="store_true") + parser.add_argument("--source-bin-dir", default=SOURCE_BIN_DIR) + parser.add_argument("--target-bin-dir", default=TARGET_BIN_DIR) + parser.add_argument("--source-bin-name", default=BIN_NAME) + parser.add_argument("--target-bin-name", default=BIN_NAME) + args = parser.parse_args() - if not windows.is_admin(): - windows.relaunch_as_admin(__file__) - sys.exit() + if not windows.is_admin(): + windows.relaunch_as_admin(__file__) + sys.exit() + + try: + reinstall( + args.source_bin_dir, + args.target_bin_dir, + args.source_bin_name, + args.target_bin_name, + ) + except Exception as e: + print(f"Error: {e}") + + if args.pause_on_exit: + input("Press enter to continue...") - try: - reinstall(args.source_bin_dir, args.target_bin_dir, args.source_bin_name, args.target_bin_name) - except Exception as e: - print(f'Error: {e}') - - if (args.pause_on_exit): - input('Press enter to continue...') def reinstall(source_bin_dir, target_bin_dir, source_bin_name, target_bin_name): - """Stops the running daemon service, copies files, and reinstalls.""" - - print('Stopping daemon service') - try: - subprocess.run(['net', 'stop', 'synergy'], shell=True, check=True) - except subprocess.CalledProcessError as e: - if (e.returncode == SERVICE_NOT_RUNNING_ERROR): - print('Daemon service not running') - else: - raise e + """Stops the running daemon service, copies files, and reinstalls.""" - copy_bin_files(source_bin_dir, target_bin_dir, source_bin_name, target_bin_name) + print("Stopping daemon service") + try: + subprocess.run(["net", "stop", "synergy"], shell=True, check=True) + except subprocess.CalledProcessError as e: + if e.returncode == SERVICE_NOT_RUNNING_ERROR: + print("Daemon service not running") + else: + raise e - target_bin_file = f'{os.path.join(target_bin_dir, target_bin_name)}.exe' + copy_bin_files(source_bin_dir, target_bin_dir, source_bin_name, target_bin_name) - print('Removing old daemon service') - subprocess.run([target_bin_file, '/uninstall'], shell=True, check=True) + target_bin_file = f"{os.path.join(target_bin_dir, target_bin_name)}.exe" + + print("Removing old daemon service") + subprocess.run([target_bin_file, "/uninstall"], shell=True, check=True) + + print("Installing daemon service") + subprocess.run([target_bin_file, "/install"], shell=True, check=True) - print('Installing daemon service') - subprocess.run([target_bin_file, '/install'], shell=True, check=True) def copy_bin_files(source_bin_dir, target_bin_dir, source_bin_name, target_bin_name): - if not os.path.isdir(source_bin_dir): - raise Exception(f'Invalid source bin dir: {source_bin_dir}') + if not os.path.isdir(source_bin_dir): + raise RuntimeError(f"Invalid source bin dir: {source_bin_dir}") - print(f'Persisting dir: {target_bin_dir}') - os.makedirs(target_bin_dir, exist_ok=True) + print(f"Persisting dir: {target_bin_dir}") + os.makedirs(target_bin_dir, exist_ok=True) - source_bin_glob = f'{source_bin_name}*' - source_files = glob.glob(os.path.join(source_bin_dir, source_bin_glob)) + source_bin_glob = f"{source_bin_name}*" + source_files = glob.glob(os.path.join(source_bin_dir, source_bin_glob)) - if not source_files: - raise Exception(f'No files found in {source_bin_dir} matching {source_bin_glob}') + if not source_files: + raise RuntimeError( + f"No files found in {source_bin_dir} matching {source_bin_glob}" + ) + + for source_file in source_files: + base_name = os.path.basename(source_file) + base_name = base_name.replace(source_bin_name, target_bin_name) + target_file = os.path.join(target_bin_dir, base_name) + print(f"Copying {source_file} to {target_file}") + # use the copy command; shutil.copy gives us a permission denied error. + try: + subprocess.run(["copy", source_file, target_file], shell=True, check=True) + except subprocess.CalledProcessError as e: + print(f"Copy failed: {e}") - for source_file in source_files: - base_name = os.path.basename(source_file) - base_name = base_name.replace(source_bin_name, target_bin_name) - target_file = os.path.join(target_bin_dir, base_name) - print(f'Copying {source_file} to {target_file}') - # use the copy command; shutil.copy gives us a permission denied error. - try: - subprocess.run(['copy', source_file, target_file], shell=True, check=True) - except subprocess.CalledProcessError as e: - print(f'Copy failed: {e}') main() diff --git a/src/lib/common/Version.cpp b/src/lib/common/Version.cpp index ca6e66851..3cbc13eee 100644 --- a/src/lib/common/Version.cpp +++ b/src/lib/common/Version.cpp @@ -25,5 +25,5 @@ const char* kCopyright = "Copyright (C) 2012-%s Symless Ltd.\n" "Copyright (C) 2002-2014 Chris Schoeneman"; const char* kContact = "Email: engineering@symless.com"; const char* kWebsite = "https://symless.com/"; -const char* kVersion = SYNERGY_VERSION_STRING; +const char* kVersion = SYNERGY_VERSION_LONG; const char* kAppVersion = "Synergy " SYNERGY_VERSION;