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)
This commit is contained in:
Nick Bolton
2024-06-24 10:36:30 +01:00
committed by GitHub
parent 8f13ce8e7a
commit 865063b77c
45 changed files with 1245 additions and 838 deletions

39
.github/actions/dist-upload/action.yml vendored Normal file
View File

@ -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 }}

View File

@ -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

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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

47
.gitignore vendored
View File

@ -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*

View File

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

View File

@ -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

View File

@ -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")

65
config.yml Normal file
View File

@ -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

25
cspell.json Normal file
View File

@ -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": []
}

View File

@ -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

View File

@ -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/

150
res/dist/macos/dmgbuild/settings.py vendored Normal file
View File

@ -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",
}

View File

@ -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) + '>')

30
scripts/github_env.py Executable file
View File

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

View File

@ -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__":

View File

@ -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

107
scripts/lib/config.py Normal file
View File

@ -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

163
scripts/lib/env.py Normal file
View File

@ -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

282
scripts/lib/mac.py Normal file
View File

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

View File

@ -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")

57
scripts/package.py Executable file
View File

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

View File

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

View File

@ -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;