diff --git a/.env.example b/.env.example index 1c9c9b745..0501fdd9b 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,12 @@ # Packaging (optional) # +# [Windows] Base64 encoded PFX code signing certificate +# WINDOWS_PFX_CERTIFICATE="very-long-base64-encoded-string" + +# [Windows] Password for the PFX code signing certificate +# WINDOWS_PFX_PASSWORD="super-secret-password" + # [macOS] Apple ID used to notarize the app # APPLE_NOTARY_USER="example@example.com" diff --git a/.github/actions/dist-upload/action.yml b/.github/actions/dist-upload/action.yml index e310192dd..15eb1ac22 100644 --- a/.github/actions/dist-upload/action.yml +++ b/.github/actions/dist-upload/action.yml @@ -26,6 +26,12 @@ runs: using: "composite" steps: + - if: ${{ inputs.use_gdrive == 'true' && !inputs.package-version }} + run: | + echo "Input 'package-version' is required when uploading to Google Drive" + exit 1 + shell: bash + - if: ${{ inputs.use_gdrive == 'true' }} run: | SHORT_VERSION=$(echo "${{ inputs.package-version }}" | cut -d'-' -f1) @@ -41,7 +47,7 @@ runs: retention-days: 3 # Upload to Google Drive - - if: ${{ inputs.use_gdrive }} + - if: ${{ inputs.use_gdrive == 'true' }} uses: symless/gdrive-upload@target-glob with: credentials: ${{ inputs.gdrive-secret-key }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d14ecdef..31b696363 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: inputs: version: description: Synergy version number + required: true pull_request: types: - opened @@ -68,6 +69,21 @@ jobs: - name: Package if: ${{ !github.event.pull_request.draft }} run: python ./scripts/package.py + env: + WINDOWS_PFX_CERTIFICATE: ${{ secrets.WINDOWS_PFX }} + WINDOWS_PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASS }} + + - name: Upload + if: + uses: ./.github/actions/dist-upload + with: + use_github: ${{ env.UPLOAD_TO_GITHUB }} + use_gdrive: ${{ env.UPLOAD_TO_GDRIVE }} + github-target-filename: "synergy-windows-x64" + gdrive-target-base-dir: "synergy1/personal" + gdrive-secret-key: ${{ secrets.GOOGLE_DRIVE_KEY }} + gdrive-parent-folder-id: ${{ secrets.GOOGLE_DRIVE_TECH_DRIVE }} + package-version: ${{ env.SYNERGY_VERSION }} macos: runs-on: ${{ matrix.runtime.os }} @@ -88,7 +104,7 @@ jobs: target: "10.14" shell: "bash" - - name: "macos-11-m1" + - name: "macos-11-arm64" timeout: 10 os: "macos-14" arch: arm64 @@ -132,7 +148,7 @@ jobs: with: use_github: ${{ env.UPLOAD_TO_GITHUB }} use_gdrive: ${{ env.UPLOAD_TO_GDRIVE }} - github-target-filename: "synergy-macos-${{ matrix.runtime.target }}" + github-target-filename: "synergy-${{ matrix.runtime.name }}" gdrive-target-base-dir: "synergy1/personal" gdrive-secret-key: ${{ secrets.GOOGLE_DRIVE_KEY }} gdrive-parent-folder-id: ${{ secrets.GOOGLE_DRIVE_TECH_DRIVE }} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 761251ce2..761027053 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,10 @@ "command": "build", "targets": ["all"], "preset": "${command:cmake.activeBuildPresetName}", - "group": "build" + "group": { + "kind": "build", + "isDefault": true + } }, { "label": "reinstall windows daemon", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a2e7a26a..e28171e37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ # Synergy -- mouse and keyboard sharing utility -# Copyright (C) 2012-2016 Symless Ltd. -# Copyright (C) 2009 Nick Bolton +# Copyright (C) 2012-2024 Symless Ltd. +# Copyright (C) 2009-2012 Nick Bolton # # This package is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,7 +16,9 @@ cmake_minimum_required (VERSION 3.5) project (synergy-core C CXX) + include (cmake/Version.cmake) +set_version() # use response files so that ninja can compile on windows, # otherwise you get an error when linking qt: @@ -339,7 +341,7 @@ if (UNIX) add_definitions (-DWINAPI_XWINDOWS=1) endif() -elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows") +elseif (WIN32) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /D _BIND_TO_CURRENT_VCLIBS_VERSION=1") set (CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD /O2 /Ob2") @@ -354,6 +356,11 @@ elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows") /D_XKEYCHECK_H ) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/res/win/version.rc.in + ${CMAKE_BINARY_DIR}/src/version.rc + @ONLY) + endif() # @@ -376,11 +383,12 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") option(GIT_SUBMODULE "Check submodules during build" ON) if(GIT_SUBMODULE) message(STATUS "Submodule update") - execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE GIT_SUBMOD_RESULT) - if(NOT GIT_SUBMOD_RESULT EQUAL "0") - message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMODULE_RESULT) + if(NOT GIT_SUBMODULE_RESULT EQUAL "0") + message(FATAL_ERROR "Git submodule update failed: ${GIT_SUBMODULE_RESULT}") endif() endif() endif() @@ -389,78 +397,10 @@ endif() # Google Test # if(BUILD_TESTS AND NOT EXISTS "${PROJECT_SOURCE_DIR}/ext/googletest/CMakeLists.txt") - message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.") + message(FATAL_ERROR "Git submodule for Google Test is missing") endif() - -# -# Same as the `configure_file` command but for directories recursively. -# -macro (configure_files srcDir destDir) - message (STATUS "Configuring directory ${destDir}") - make_directory (${destDir}) - - file (GLOB_RECURSE sourceFiles RELATIVE ${srcDir} ${srcDir}/*) - file (GLOB_RECURSE templateFiles LIST_DIRECTORIES false RELATIVE ${srcDir} ${srcDir}/*.in) - list (REMOVE_ITEM sourceFiles ${templateFiles}) - - foreach (sourceFile ${sourceFiles}) - set (sourceFilePath ${srcDir}/${sourceFile}) - if (IS_DIRECTORY ${sourceFilePath}) - message (STATUS "Copying directory ${sourceFile}") - make_directory (${destDir}/${sourceFile}) - else() - message (STATUS "Copying file ${sourceFile}") - configure_file (${sourceFilePath} ${destDir}/${sourceFile} COPYONLY) - endif() - endforeach (sourceFile) - - foreach (templateFile ${templateFiles}) - set (sourceTemplateFilePath ${srcDir}/${templateFile}) - string (REGEX REPLACE "\.in$" "" templateFile ${templateFile}) - message (STATUS "Configuring file ${templateFile}") - configure_file (${sourceTemplateFilePath} ${destDir}/${templateFile} @ONLY) - endforeach (templateFile) -endmacro (configure_files) - -if (${SYNERGY_BUILD_INSTALLER}) - -# -# 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}/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) - configure_files (${SYNERGY_BUNDLE_SOURCE_DIR} ${SYNERGY_BUNDLE_DIR}) -endif() - -# -# Windows installer -# -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}/res/dist/wix ${CMAKE_BINARY_DIR}/installer) -endif() - -# -# Linux installation -# -if (${CMAKE_SYSTEM_NAME} MATCHES "Linux|.*BSD|DragonFly") - 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) - else() - install(FILES res/synergy.desktop DESTINATION share/applications) - endif() -endif() - -else() - message (STATUS "NOT configuring the installer") -endif() +include(cmake/Packaging.cmake) +configure_packaging() add_subdirectory (src) diff --git a/ChangeLog b/ChangeLog index f7518f441..99369c6f4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,8 +33,9 @@ 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) - #7354 Re-implement CI auto version increment for packaging +- #7353 Re-implement packaging for GitHub workflows (macOS) +- #7360 Re-implement packaging for GitHub workflows (Windows) # 1.14.6 diff --git a/cmake/Packaging.cmake b/cmake/Packaging.cmake new file mode 100644 index 000000000..bd4331df3 --- /dev/null +++ b/cmake/Packaging.cmake @@ -0,0 +1,120 @@ +# Synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012-2024 Symless Ltd. +# Copyright (C) 2009-2012 Nick Bolton +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# +# If enabled, configure packaging based on OS. +# +macro(configure_packaging) + if (${SYNERGY_BUILD_INSTALLER}) + if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + configure_macos_packaging() + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + configure_windows_packaging() + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux|.*BSD|DragonFly") + configure_linux_packaging() + endif() + else() + message (STATUS "Not configuring installer") + endif() +endmacro() + +# +# macOS app bundle +# +macro(configure_macos_packaging) + 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}/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) + configure_files (${SYNERGY_BUNDLE_SOURCE_DIR} ${SYNERGY_BUNDLE_DIR}) + endif() +endmacro() + +function(find_openssl_dir_win32 result) + execute_process( + COMMAND where openssl + OUTPUT_VARIABLE OPENSSL_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # It's possible that there are multiple OpenSSL installations on the system, + # which is the case on GitHub runners. + # For now we'll pick the first one, but that's probably not very robust. + # Maybe our choco config could install to a specific location? + string(REGEX REPLACE "\r?\n" ";" OPENSSL_PATH_LIST ${OPENSSL_PATH}) + message(STATUS "Found OpenSSL binaries at: ${OPENSSL_PATH_LIST}") + + list(GET OPENSSL_PATH_LIST 0 OPENSSL_FIRST_PATH) + message(STATUS "First OpenSSL binary: ${OPENSSL_FIRST_PATH}") + + get_filename_component(OPENSSL_BIN_DIR ${OPENSSL_FIRST_PATH} DIRECTORY) + message(STATUS "OpenSSL bin dir: ${OPENSSL_BIN_DIR}") + + get_filename_component(OPENSSL_DIR ${OPENSSL_BIN_DIR} DIRECTORY) + message(STATUS "OpenSSL install root dir: ${OPENSSL_DIR}") + + set(${result} ${OPENSSL_DIR} PARENT_SCOPE) +endfunction() + +# +# Windows installer +# +macro(configure_windows_packaging) + find_openssl_dir_win32(OPENSSL_PATH) + set(QT_PATH $ENV{CMAKE_PREFIX_PATH}) + configure_files (${CMAKE_CURRENT_SOURCE_DIR}/res/dist/wix ${CMAKE_BINARY_DIR}/installer) +endmacro() + +# +# Linux packages (including BSD and DragonFly) +# +macro(configure_linux_packaging) + configure_files (${CMAKE_CURRENT_SOURCE_DIR}/res/dist/rpm ${CMAKE_BINARY_DIR}/rpm) + install(FILES res/synergy.svg DESTINATION share/icons/hicolor/scalable/apps) + install(FILES res/synergy.desktop DESTINATION share/applications) +endmacro() + +# +# Same as the `configure_file` command but for directories recursively. +# +macro (configure_files srcDir destDir) + message (STATUS "Configuring directory ${destDir}") + make_directory (${destDir}) + + file (GLOB_RECURSE sourceFiles RELATIVE ${srcDir} ${srcDir}/*) + file (GLOB_RECURSE templateFiles LIST_DIRECTORIES false RELATIVE ${srcDir} ${srcDir}/*.in) + list (REMOVE_ITEM sourceFiles ${templateFiles}) + + foreach (sourceFile ${sourceFiles}) + set (sourceFilePath ${srcDir}/${sourceFile}) + if (IS_DIRECTORY ${sourceFilePath}) + message (STATUS "Copying directory ${sourceFile}") + make_directory (${destDir}/${sourceFile}) + else() + message (STATUS "Copying file ${sourceFile}") + configure_file (${sourceFilePath} ${destDir}/${sourceFile} COPYONLY) + endif() + endforeach (sourceFile) + + foreach (templateFile ${templateFiles}) + set (sourceTemplateFilePath ${srcDir}/${templateFile}) + string (REGEX REPLACE "\.in$" "" templateFile ${templateFile}) + message (STATUS "Configuring file ${templateFile}") + configure_file (${sourceTemplateFilePath} ${destDir}/${templateFile} @ONLY) + endforeach (templateFile) +endmacro (configure_files) diff --git a/cmake/Version.cmake b/cmake/Version.cmake index 488b2d7eb..4a05f8d38 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -1,14 +1,58 @@ +# Synergy -- mouse and keyboard sharing utility +# Copyright (C) 2012-2024 Symless Ltd. +# Copyright (C) 2009-2012 Nick Bolton # -# Gets the version number either from an env var or from the VERSION file. +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. # +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . -set (VERSION $ENV{SYNERGY_VERSION}) -string(STRIP "${VERSION}" VERSION) +# Either get the version number from the environment or from the VERSION file. +# On Windows, we also set a special 4-digit MSI version number. +macro(set_version) -if (NOT VERSION) - file(READ "${CMAKE_SOURCE_DIR}/VERSION" VERSION) - string(STRIP "${VERSION}" VERSION) -endif() + set (SYNERGY_VERSION $ENV{SYNERGY_VERSION}) + string(STRIP "${SYNERGY_VERSION}" SYNERGY_VERSION) -message (STATUS "Version number: " ${VERSION}) -add_definitions (-DSYNERGY_VERSION="${VERSION}") + if (NOT SYNERGY_VERSION) + file(READ "${CMAKE_SOURCE_DIR}/VERSION" SYNERGY_VERSION) + string(STRIP "${SYNERGY_VERSION}" SYNERGY_VERSION) + endif() + + message (STATUS "Version number: " ${SYNERGY_VERSION}) + add_definitions (-DSYNERGY_VERSION="${SYNERGY_VERSION}") + + # Useful for copyright (e.g. in macOS bundle .plist.in and Windows version .rc file) + string(TIMESTAMP SYNERGY_BUILD_YEAR "%Y") + + # MSI requires a 4-digit number and doesn't accept semver. + if (WIN32) + string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" _ "${SYNERGY_VERSION}") + set(VERSION_MAJOR "${CMAKE_MATCH_1}") + set(VERSION_MINOR "${CMAKE_MATCH_2}") + set(VERSION_PATCH "${CMAKE_MATCH_3}") + + # Find the revision number, which is the number after the 'r'. + string(REGEX MATCH "r([0-9]+)$" _ "${SYNERGY_VERSION}") + set(VERSION_REVISION "${CMAKE_MATCH_1}") + if (NOT VERSION_REVISION) + set(VERSION_REVISION "0") + endif() + + # Dot-separated version number for MSI and Windows version .rc file. + set(SYNERGY_VERSION_MS "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_REVISION}") + message (STATUS "Version number for Microsoft (dots): " ${SYNERGY_VERSION_MS}) + + # CSV version number for Windows version .rc file. + set(SYNERGY_VERSION_MS_CSV "${VERSION_MAJOR},${VERSION_MINOR},${VERSION_PATCH},${VERSION_REVISION}") + message (STATUS "Version number for Microsoft (CSV): " ${SYNERGY_VERSION_MS_CSV}) + endif() + +endmacro() diff --git a/res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in b/res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in index e681046f7..555c18d04 100644 --- a/res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in +++ b/res/dist/macos/bundle/Synergy.app/Contents/Info.plist.in @@ -11,7 +11,6 @@ Synergy.icns CFBundleIdentifier synergy - CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -25,7 +24,7 @@ CFBundleVersion @SYNERGY_VERSION@ NSHumanReadableCopyright - © 2012-2016, Symless Ltd + © 2012-@SYNERGY_BUILD_YEAR@ Symless Ltd LSMinimumSystemVersion 10.9.0 diff --git a/res/dist/wix/Include.wxi.in b/res/dist/wix/Include.wxi.in index 11bf1f13d..44d520de4 100644 --- a/res/dist/wix/Include.wxi.in +++ b/res/dist/wix/Include.wxi.in @@ -1,24 +1,23 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/dist/wix/Product.wxs b/res/dist/wix/Product.wxs index 0a7b0f18d..f4bd5f861 100644 --- a/res/dist/wix/Product.wxs +++ b/res/dist/wix/Product.wxs @@ -38,9 +38,9 @@ = 602)]]> - + - + common_background @@ -90,11 +90,11 @@ - - + + - - + + @@ -112,7 +112,7 @@ - + @@ -142,11 +142,11 @@ - - + + - - + + diff --git a/res/win/version.rc.in b/res/win/version.rc.in new file mode 100644 index 000000000..af7c63e2a --- /dev/null +++ b/res/win/version.rc.in @@ -0,0 +1,54 @@ +/* +Based on example from: +https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource?redirectedfrom=MSDN +*/ + +#include +#include + +#define VER_FILEVERSION @SYNERGY_VERSION_MS_CSV@ +#define VER_FILEVERSION_STR "@SYNERGY_VERSION_MS@\0" + +#define VER_PRODUCTVERSION @SYNERGY_VERSION_MS_CSV@ +#define VER_PRODUCTVERSION_STR "@SYNERGY_VERSION_MS@\0" + +#define VER_COMPANYNAME_STR "Symless\0" +#define VER_FILEDESCRIPTION_STR "Synergy\0" +#define VER_INTERNALNAME_STR "Synergy\0" +#define VER_LEGALCOPYRIGHT_STR "Copyright (C) Symless Ltd. @SYNERGY_BUILD_YEAR@\0" +#define VER_LEGALTRADEMARKS1_STR "All Rights Reserved\0" +#define VER_LEGALTRADEMARKS2_STR "\0" +#define VER_ORIGINALFILENAME_STR "\0" +#define VER_PRODUCTNAME_STR "Synergy\0" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VER_FILEVERSION + PRODUCTVERSION VER_PRODUCTVERSION + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK + FILEFLAGS 0 // No flags + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", VER_COMPANYNAME_STR + VALUE "FileDescription", VER_FILEDESCRIPTION_STR + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "InternalName", VER_INTERNALNAME_STR + VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR + VALUE "LegalTrademarks1", VER_LEGALTRADEMARKS1_STR + VALUE "LegalTrademarks2", VER_LEGALTRADEMARKS2_STR + VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR + VALUE "ProductName", VER_PRODUCTNAME_STR + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END \ No newline at end of file diff --git a/scripts/github_env.py b/scripts/github_env.py index 77e133afb..ac7246968 100755 --- a/scripts/github_env.py +++ b/scripts/github_env.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import argparse -from lib import env, github +import lib.env as env +import lib.github as github from lib.config import Config qt_version_key = "QT_VERSION" diff --git a/scripts/install_deps.py b/scripts/install_deps.py index 1b5635485..40dde2e4d 100755 --- a/scripts/install_deps.py +++ b/scripts/install_deps.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import os, sys, argparse, traceback -from lib import env, cmd_utils +import lib.env as env +import lib.cmd_utils as cmd_utils def main(): @@ -61,7 +62,7 @@ class Dependencies: def windows(self): """Installs dependencies on Windows.""" - from lib import windows + import lib.windows as windows if not windows.is_admin(): windows.relaunch_as_admin(__file__) @@ -95,10 +96,10 @@ class Dependencies: def mac(self): """Installs dependencies on macOS.""" - from lib import mac + import lib.mac as mac command = self.config.get_os_deps_value("command") - cmd_utils.run(command) + cmd_utils.run(command, shell=True, print_cmd=True) if not self.ci_env: mac.set_cmake_prefix_env_var(self.config.get_os_value("qt-prefix-command")) @@ -123,7 +124,7 @@ class Dependencies: # 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) + cmd_utils.run(command, check=False, shell=True, print_cmd=True) if __name__ == "__main__": diff --git a/scripts/lib/certificate.py b/scripts/lib/certificate.py new file mode 100644 index 000000000..6cef50b00 --- /dev/null +++ b/scripts/lib/certificate.py @@ -0,0 +1,40 @@ +import os, base64 + +temp_path = "tmp/certificate" + + +class Certificate: + """ + Installs a certificate from a base64 string, and returns the path to the certificate. + Once the context is exited, the certificate is removed from the filesystem. + + Example usage: + with Certificate(base64) as cert_path: + print(f"Certificate path: {cert_path}") + """ + + def __init__(self, base64, file_ext): + self.base64 = base64 + self.temp_filename = f"{temp_path}.{file_ext}" + + def __enter__(self): + print(f"Decoding certificate to temporary path: {self.temp_filename}") + try: + cert_bytes = base64.b64decode(self.base64) + except Exception as e: + raise ValueError("Failed to decode certificate base64") from e + + os.makedirs(os.path.dirname(self.temp_filename), exist_ok=True) + with open(self.temp_filename, "wb") as cert_file: + cert_file.write(cert_bytes) + + return self.temp_filename + + def __exit__(self, _exc_type, _exc_value, _traceback): + # 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: {self.temp_filename}") + os.remove(self.temp_filename) + + # propagate exceptions + return False diff --git a/scripts/lib/cmd_utils.py b/scripts/lib/cmd_utils.py index 00ae2c0c8..49395a690 100644 --- a/scripts/lib/cmd_utils.py +++ b/scripts/lib/cmd_utils.py @@ -37,30 +37,28 @@ def strip_continuation_sequences(command): # 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, + check=True, # true by default to fail fast + shell=False, # false by default for security get_output=False, - print_cmd=True, + print_cmd=False, # false by default for security ): """ Convenience wrapper around `subprocess.run` to: - - print the command before running it - - pipe/capture the output instead of printing it + - print the command before running it (if `print_cmd` is True) 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) + - prints list commands as a readable string on failure - 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. + This is the same as `subprocess.run` in that it: + - does not use shell by default for security (shell is less secure) 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. + shell (bool): Run the command in a shell (false by default for security) get_output (bool): Return the output of the command. - print_cmd (bool): Print the command before running it. + print_cmd (bool): Print the command before running it (false by default for security) """ # create string version of list command, only for debugging purposes @@ -70,23 +68,44 @@ def run( if print_cmd: print(f"Running: {command_str}") - sys.stdout.flush() - - 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) + print("Running command...") + command_str = "***" - if print_cmd and result.returncode != 0: + # Flush the output to ensure the command is printed before the output of the command, + # which seems to happen in the GitHub runner logs. + sys.stdout.flush() + sys.stderr.flush() + + try: + 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) + except subprocess.CalledProcessError as e: + # Take control of how failed commands are printed: + # - if `print_cmd` is false, it will print `***` instead of the command + # - if the command was a list, the command is printed as a readable string + raise RuntimeError( + f"Command exited with code {e.returncode}: {command_str}" + ) from None + except Exception: + # Take control of how failed commands are printed: + # - if `print_cmd` is false, it will print `***` instead of the command + # - if the command was a list, the command is printed as a readable string + raise RuntimeError(f"Command failed: {command_str}") from None + + if 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 index 8848c0435..0516b061c 100644 --- a/scripts/lib/config.py +++ b/scripts/lib/config.py @@ -1,4 +1,5 @@ -from lib import env, cmd_utils +import lib.env as env +import lib.cmd_utils as cmd_utils config_file = "config.yml" root_key = "config" diff --git a/scripts/lib/env.py b/scripts/lib/env.py index 3218821bd..ce3fa9e84 100644 --- a/scripts/lib/env.py +++ b/scripts/lib/env.py @@ -1,5 +1,5 @@ -import os, sys, subprocess, platform -from lib import env, cmd_utils +import os, sys, subprocess +import lib.cmd_utils as cmd_utils venv_path = "build/python" version_file = "VERSION" @@ -75,7 +75,7 @@ def in_venv(): return sys.prefix != sys.base_prefix -def ensure_in_venv(script): +def ensure_in_venv(script_file): """ 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. @@ -89,11 +89,9 @@ def ensure_in_venv(script): 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() + print(f"Using virtual environment for {script_file}", flush=True) python_executable = get_python_executable() - result = subprocess.run([python_executable, script] + sys.argv[1:]) + result = subprocess.run([python_executable, script_file] + sys.argv[1:]) sys.exit(result.returncode) @@ -109,7 +107,11 @@ def ensure_module(module, package): __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) + cmd_utils.run( + [sys.executable, "-m", "pip", "install", package], + shell=False, + print_cmd=True, + ) def assert_dependencies(raise_error=True): @@ -150,11 +152,23 @@ def ensure_dependencies(): 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()) + cmd_utils.run( + f"{sudo} apt update".strip(), check=False, shell=True, print_cmd=True + ) + cmd_utils.run( + f"{sudo} apt install -y python3-pip python3-venv".strip(), + shell=True, + print_cmd=True, + ) 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()) + cmd_utils.run( + f"{sudo} dnf check-update".strip(), check=False, shell=True, print_cmd=True + ) + cmd_utils.run( + f"{sudo} dnf install -y python3-pip python3-virtualenv".strip(), + shell=True, + print_cmd=True, + ) else: # arch, opensuse, etc, patches welcome! :) raise RuntimeError(f"Unable to install Python dependencies on {distro}") @@ -165,7 +179,9 @@ def get_app_version(): Returns the version either from the env var, or from the version file. """ if version_env_var in os.environ: - return os.environ[version_env_var] + version = os.environ[version_env_var].strip() + if version: + return version with open(version_file, "r") as f: return f.read().strip() diff --git a/scripts/lib/mac.py b/scripts/lib/mac.py index 968e08d49..c8c91cb25 100644 --- a/scripts/lib/mac.py +++ b/scripts/lib/mac.py @@ -1,9 +1,10 @@ -import os, subprocess, base64, time, json, shutil, sys -from lib import cmd_utils, env +import os, time, json +import lib.cmd_utils as cmd_utils +import lib.env as env +from lib.certificate import Certificate 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" @@ -30,18 +31,20 @@ def set_env_var(name, value): def set_cmake_prefix_env_var(cmake_prefix_command): - result = cmd_utils.run(cmake_prefix_command, get_output=True) + result = cmd_utils.run( + cmake_prefix_command, get_output=True, shell=True, print_cmd=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") + cert_base64 = env.get_env_var("APPLE_P12_CERTIFICATE") + cert_password = env.get_env_var("APPLE_P12_PASSWORD") build_bundle() - install_certificate(certificate, password) + install_certificate(cert_base64, cert_password) assert_certificate_installed(codesign_id) sign_bundle(codesign_id) dmg_path = build_dmg(filename_base) @@ -51,13 +54,12 @@ def package(filename_base): def build_bundle(): print("Building bundle...") # cmake build install target should run macdeployqt - cmd_utils.run("cmake --build build --target install") + cmd_utils.run("cmake --build build --target install", shell=True, print_cmd=True) def sign_bundle(codesign_id): print(f"Signing bundle {app_path}...") - sys.stdout.flush() - subprocess.run( + cmd_utils.run( [ codesign_path, "-f", @@ -67,14 +69,16 @@ def sign_bundle(codesign_id): "-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 + "security find-identity -v -p codesigning", + get_output=True, + shell=True, + print_cmd=True, ) if codesign_id not in installed.stdout: @@ -122,18 +126,11 @@ def install_certificate(cert_base64, cert_password): 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) + with Certificate(cert_base64, "p12") as cert_path: + print(f"Installing certificate: {cert_path}") - print(f"Installing certificate: {cert_path}") - sys.stdout.flush() - - try: - # warning: contains private key password, never print this command - subprocess.run( + # WARNING: contains private key password, never print this command + cmd_utils.run( [ sudo_path, security_path, @@ -148,19 +145,7 @@ def install_certificate(cert_base64, cert_password): "-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): @@ -176,7 +161,9 @@ def notarize_package(dmg_path): def get_xcode_path(): - result = cmd_utils.run([xcode_select_path, "-p"], get_output=True, shell=False) + result = cmd_utils.run( + [xcode_select_path, "-p"], get_output=True, shell=False, print_cmd=True + ) return result.stdout.strip() @@ -193,31 +180,22 @@ class NotaryTool: 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 + + # WARNING: contains password, never print this command + cmd_utils.run( + [ + notarytool_path, + "store-credentials", + "notarytool-password", + "--apple-id", + user, + "--team-id", + team_id, + "--password", + password, + ] + ) def submit_and_wait(self, dmg_filename): print("Submitting notarization request...") @@ -254,6 +232,7 @@ class NotaryTool: ], get_output=True, shell=False, + print_cmd=True, ) if result.stderr: @@ -274,6 +253,7 @@ class NotaryTool: ], get_output=True, shell=False, + print_cmd=True, ) if result.stderr: diff --git a/scripts/lib/windows.py b/scripts/lib/windows.py index d3ef629bb..2dcab5526 100644 --- a/scripts/lib/windows.py +++ b/scripts/lib/windows.py @@ -1,12 +1,19 @@ -import ctypes -import sys -import os +import ctypes, sys, os, shutil import xml.etree.ElementTree as ET -from lib import cmd_utils +import lib.cmd_utils as cmd_utils +import lib.env as env +from lib.certificate import Certificate +msbuild_cmd = "msbuild" +signtool_cmd = "signtool" +certutil_cmd = "certutil" cmake_env_var = "CMAKE_PREFIX_PATH" runner_temp_env_var = "RUNNER_TEMP" qt_base_dir_env_var = "QT_BASE_DIR" +dist_dir = "dist" +build_dir = "build" +wix_solution_file = f"{build_dir}/installer/Synergy.sln" +installer_file = f"{build_dir}/installer/bin/Release/Synergy.msi" def relaunch_as_admin(script): @@ -39,7 +46,89 @@ def set_env_var(name, 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) + cmd_utils.run(["setx", name, new_value], check=True, shell=True, print_cmd=True) + + +def package(filename_base): + cert_base64 = env.get_env_var("WINDOWS_PFX_CERTIFICATE") + cert_password = env.get_env_var("WINDOWS_PFX_PASSWORD") + + sign_binaries(cert_base64, cert_password) + build_msi(filename_base) + sign_msi(filename_base, cert_base64, cert_password) + + +def assert_vs_cmd(cmd): + has_cmd = cmd_utils.has_command(cmd) + if not has_cmd: + raise RuntimeError( + f"The '{cmd}' command was not found, " + "re-run from 'Developer Command Prompt for VS'" + ) + + +def build_msi(filename_base): + print("Building MSI installer...") + configuration = "Release" + platform = "x64" + + assert_vs_cmd(msbuild_cmd) + cmd_utils.run( + [ + msbuild_cmd, + wix_solution_file, + f"/p:Configuration={configuration}", + f"/p:Platform={platform}", + ], + shell=True, + print_cmd=True, + ) + + path = get_package_path(filename_base) + print(f"Copying MSI installer to {dist_dir}") + os.makedirs(dist_dir, exist_ok=True) + shutil.copy(installer_file, path) + + +def get_package_path(filename_base): + return f"{dist_dir}/{filename_base}.msi" + + +def sign_binaries(cert_base64, cert_password): + exe_pattern = f"{build_dir}/bin/*.exe" + run_codesign(exe_pattern, cert_base64, cert_password) + + +def sign_msi(filename_base, cert_base64, cert_password): + path = get_package_path(filename_base) + run_codesign(path, cert_base64, cert_password) + + +def run_codesign(path, cert_base64, cert_password): + time_server = "http://timestamp.digicert.com" + hashing_algorithm = "SHA256" + + with Certificate(cert_base64, "pfx") as cert_path: + print("Signing MSI installer...") + assert_vs_cmd(signtool_cmd) + + # WARNING: contains private key password, never print this command + cmd_utils.run( + [ + signtool_cmd, + "sign", + "/f", + cert_path, + "/p", + cert_password, + "/t", + time_server, + "/fd", + hashing_algorithm, + path, + ], + shell=True, + ) class WindowsChoco: @@ -49,10 +138,23 @@ class WindowsChoco: """Installs packages using Chocolatey.""" if ci_env: # don't show noisy choco progress bars in ci env - cmd_utils.run(f"{command} --no-progress") + cmd_utils.run( + f"{command} --no-progress", + shell=True, + print_cmd=True, + ) else: - cmd_utils.run("winget install chocolatey", check=False) - cmd_utils.run(command) + cmd_utils.run( + "winget install chocolatey", + check=False, + shell=True, + print_cmd=True, + ) + cmd_utils.run( + command, + shell=True, + print_cmd=True, + ) def config_ci_cache(self): """Configures Chocolatey cache for CI.""" @@ -62,7 +164,11 @@ class WindowsChoco: # sets the choco cache dir, which should match the dir in the ci cache action. key_arg = '--name="cacheLocation"' value_arg = f'--value="{runner_temp}/choco"' - cmd_utils.run(["choco", "config", "set", key_arg, value_arg]) + cmd_utils.run( + ["choco", "config", "set", key_arg, value_arg], + shell=True, + print_cmd=True, + ) else: print(f"Warning: CI environment variable {runner_temp_env_var} not set") @@ -102,13 +208,21 @@ class WindowsQt: def install(self): """Installs Qt on Windows.""" - cmd_utils.run(["pip", "install", "aqtinstall"]) + cmd_utils.run( + ["pip", "install", "aqtinstall"], + shell=True, + print_cmd=True, + ) args = ["python", "-m", "aqt", "install-qt"] args.extend(["--outputdir", self.base_dir]) args.extend(["--base", self.mirror_url]) args.extend(["windows", "desktop", self.version, "win64_msvc2019_64"]) - cmd_utils.run(args) + cmd_utils.run( + args, + shell=True, + print_cmd=True, + ) install_dir = self.get_install_dir() if not install_dir: diff --git a/scripts/package.py b/scripts/package.py index 91cc9d107..747b492da 100755 --- a/scripts/package.py +++ b/scripts/package.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import platform -from lib import env +import lib.env as env env_file = ".env" package_filename_product = "synergy" @@ -33,16 +33,17 @@ def main(): def get_filename_base(version): os = env.get_os() machine = platform.machine().lower() - return f"{package_filename_product}-{version}-{os}-{machine}" + return f"{package_filename_product}-{os}-{machine}-{version}" def windows_package(filename_base): - """TODO: Windows packaging""" - pass + import lib.windows as windows + + windows.package(filename_base) def mac_package(filename_base): - from lib import mac + import lib.mac as mac mac.package(filename_base) diff --git a/scripts/windows_daemon.py b/scripts/windows_daemon.py index 3341029d4..2bea265fe 100644 --- a/scripts/windows_daemon.py +++ b/scripts/windows_daemon.py @@ -3,7 +3,7 @@ import subprocess import sys import argparse import glob -from lib import windows +import lib.windows as windows BIN_NAME = "synergyd" SOURCE_BIN_DIR = os.path.join("build", "bin") diff --git a/src/cmd/synergy-core/CMakeLists.txt b/src/cmd/synergy-core/CMakeLists.txt index b8e4d6315..5ab1c488d 100644 --- a/src/cmd/synergy-core/CMakeLists.txt +++ b/src/cmd/synergy-core/CMakeLists.txt @@ -14,21 +14,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target synergy-core) + set(sources - synergy-core.cpp + ${target}.cpp ) if (WIN32) - list(APPEND sources synergy-core.exe.manifest) + list(APPEND sources + ${target}.exe.manifest + ${CMAKE_BINARY_DIR}/src/version.rc + ) endif() -add_executable(synergy-core ${sources}) -target_link_libraries(synergy-core +add_executable(${target} ${sources}) +target_link_libraries(${target} arch base client common io mt net ipc platform server synlib ${libs}) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - install (TARGETS synergy-core DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) + install (TARGETS ${target} DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") - install (TARGETS synergy-core DESTINATION bin) + install (TARGETS ${target} DESTINATION bin) endif() diff --git a/src/cmd/synergyc/CMakeLists.txt b/src/cmd/synergyc/CMakeLists.txt index c01609c9d..556c9f2ea 100644 --- a/src/cmd/synergyc/CMakeLists.txt +++ b/src/cmd/synergyc/CMakeLists.txt @@ -14,8 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target synergyc) + set(sources - synergyc.cpp + ${target}.cpp ) if (WIN32) @@ -23,13 +25,14 @@ if (WIN32) file(GLOB arch_sources "MSWindows*.cpp") list(APPEND sources resource.h - synergyc.ico - synergyc.rc + ${target}.ico + ${target}.rc tb_error.ico tb_idle.ico tb_run.ico tb_wait.ico - synergyc.exe.manifest + ${target}.exe.manifest + ${CMAKE_BINARY_DIR}/src/version.rc ) elseif (APPLE) file(GLOB arch_headers "OSX*.h") @@ -46,13 +49,13 @@ if (SYNERGY_ADD_HEADERS) list(APPEND sources ${headers}) endif() -add_executable(synergyc ${sources}) -target_link_libraries(synergyc +add_executable(${target} ${sources}) +target_link_libraries(${target} arch base client common io mt net ipc platform server synlib ${libs}) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - install (TARGETS synergyc DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) + install (TARGETS ${target} DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") - install (TARGETS synergyc DESTINATION bin) + install (TARGETS ${target} DESTINATION bin) endif() diff --git a/src/cmd/synergyd/CMakeLists.txt b/src/cmd/synergyd/CMakeLists.txt index 240cdfd07..dcd4e108a 100644 --- a/src/cmd/synergyd/CMakeLists.txt +++ b/src/cmd/synergyd/CMakeLists.txt @@ -14,14 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target synergyd) + file(GLOB headers "*.h") file(GLOB sources "*.cpp") if (WIN32) - add_executable (synergyd WIN32 ${sources}) + add_executable ( + ${target} + WIN32 + ${sources} + ${CMAKE_BINARY_DIR}/src/version.rc + ) else() - add_executable (synergyd ${sources}) + add_executable (${target} ${sources}) endif() -target_link_libraries (synergyd +target_link_libraries (${target} arch base common io ipc mt net platform synlib shared ${libs}) diff --git a/src/cmd/synergys/CMakeLists.txt b/src/cmd/synergys/CMakeLists.txt index 47b237407..ca084ef5f 100644 --- a/src/cmd/synergys/CMakeLists.txt +++ b/src/cmd/synergys/CMakeLists.txt @@ -14,8 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target synergys) + set(sources - synergys.cpp + ${target}.cpp ) if (WIN32) @@ -23,13 +25,14 @@ if (WIN32) file(GLOB arch_sources "MSWindows*.cpp") list(APPEND sources resource.h - synergys.ico - synergys.rc + ${target}.ico + ${target}.rc tb_error.ico tb_idle.ico tb_run.ico tb_wait.ico - synergys.exe.manifest + ${target}.exe.manifest + ${CMAKE_BINARY_DIR}/src/version.rc ) elseif (APPLE) file(GLOB arch_headers "OSX*.h") @@ -46,14 +49,14 @@ if (SYNERGY_ADD_HEADERS) list(APPEND sources ${headers}) endif() -add_executable(synergys ${sources}) -target_link_libraries(synergys +add_executable(${target} ${sources}) +target_link_libraries(${target} arch base client common io mt net ipc platform server synlib ${libs}) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - install (TARGETS synergys DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) + install (TARGETS ${target} DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") - install (TARGETS synergys DESTINATION bin) + install (TARGETS ${target} DESTINATION bin) endif() diff --git a/src/cmd/syntool/CMakeLists.txt b/src/cmd/syntool/CMakeLists.txt index 5f841b7e9..f6b79299b 100644 --- a/src/cmd/syntool/CMakeLists.txt +++ b/src/cmd/syntool/CMakeLists.txt @@ -13,15 +13,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target syntool) + file(GLOB headers "*.h") file(GLOB sources "*.cpp") -add_executable(syntool ${sources}) -target_link_libraries(syntool +if (WIN32) + list(APPEND sources ${CMAKE_BINARY_DIR}/src/version.rc) +endif() + +add_executable(${target} ${sources}) +target_link_libraries(${target} synlib arch base client common io ipc mt net platform server ${libs}) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - install (TARGETS syntool DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) + install (TARGETS ${target} DESTINATION ${SYNERGY_BUNDLE_BINARY_DIR}) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") - install (TARGETS syntool DESTINATION bin) + install (TARGETS ${target} DESTINATION bin) endif() diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 7a100ae31..5e7aab45a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -4,28 +4,28 @@ set (CMAKE_AUTORCC ON) set (CMAKE_AUTOUIC ON) set (CMAKE_INCLUDE_CURRENT_DIR ON) -file (GLOB LEGACY_GUI_SOURCE_FILES +file (GLOB GUI_SOURCE_FILES src/*.cpp src/*.h src/validators/* src/widgets/* ) -file (GLOB LEGACY_GUI_UI_FILES src/*.ui) -file (GLOB LEGACY_ACTIVATION_FILES src/*Activation* src/*License*) -file (GLOB LEGACY_ZEROCONF_FILES src/Zeroconf*) -file (GLOB LEGACY_LANGUAGE_FILES res/lang/*.ts) +file (GLOB GUI_UI_FILES src/*.ui) +file (GLOB ACTIVATION_FILES src/*Activation* src/*License*) +file (GLOB ZEROCONF_FILES src/Zeroconf*) +file (GLOB LANGUAGE_FILES res/lang/*.ts) configure_file(res/Languages.qrc res/Languages.qrc) -set_source_files_properties(${LEGACY_LANGUAGE_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/res/lang") +set_source_files_properties(${LANGUAGE_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/res/lang") -qt5_add_translation(QM_FILES ${LEGACY_LANGUAGE_FILES}) +qt5_add_translation(QM_FILES ${LANGUAGE_FILES}) add_custom_target(translations ALL DEPENDS ${QM_FILES}) if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - file (GLOB LEGACY_GUI_MAC_SOURCE_FILES src/*.mm) - list (APPEND LEGACY_GUI_SOURCE_FILES ${LEGACY_GUI_MAC_SOURCE_FILES}) + file (GLOB GUI_MAC_SOURCE_FILES src/*.mm) + list (APPEND GUI_SOURCE_FILES ${GUI_MAC_SOURCE_FILES}) endif() # Retrieve the absolute path to qmake and then use that path to find @@ -36,21 +36,24 @@ find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${_qt_bin_dir}") find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${_qt_bin_dir}") if (SYNERGY_ENTERPRISE) - list (REMOVE_ITEM LEGACY_GUI_SOURCE_FILES ${LEGACY_ACTIVATION_FILES}) - list (REMOVE_ITEM LEGACY_GUI_UI_FILES ${LEGACY_ACTIVATION_FILES}) + list (REMOVE_ITEM GUI_SOURCE_FILES ${ACTIVATION_FILES}) + list (REMOVE_ITEM GUI_UI_FILES ${ACTIVATION_FILES}) endif () -list (REMOVE_ITEM LEGACY_GUI_SOURCE_FILES ${LEGACY_ZEROCONF_FILES}) +list (REMOVE_ITEM GUI_SOURCE_FILES ${ZEROCONF_FILES}) if (WIN32) - set (LEGACY_GUI_RC_FILES res/win/Synergy.rc) + set (GUI_RC_FILES + res/win/Synergy.rc + ${CMAKE_BINARY_DIR}/src/version.rc + ) endif() add_executable (synergy WIN32 - ${LEGACY_GUI_SOURCE_FILES} - ${LEGACY_GUI_UI_FILES} - ${LEGACY_GUI_RC_FILES} + ${GUI_SOURCE_FILES} + ${GUI_UI_FILES} + ${GUI_RC_FILES} res/Synergy.qrc ${CMAKE_CURRENT_BINARY_DIR}/res/Languages.qrc ${QM_FILES} @@ -59,7 +62,7 @@ add_executable (synergy WIN32 include_directories (./src) target_link_libraries (synergy shared) -qt5_use_modules (synergy Core Widgets Network) +target_link_libraries(synergy Qt5::Core Qt5::Widgets Qt5::Network) target_compile_definitions (synergy PRIVATE -DSYNERGY_VERSION_STAGE="${SYNERGY_VERSION_STAGE}") target_compile_definitions (synergy PRIVATE -DSYNERGY_REVISION="${SYNERGY_REVISION}") diff --git a/src/gui/src/AboutDialog.cpp b/src/gui/src/AboutDialog.cpp index 9e25fafa5..d79b9a78d 100644 --- a/src/gui/src/AboutDialog.cpp +++ b/src/gui/src/AboutDialog.cpp @@ -26,8 +26,7 @@ AboutDialog::AboutDialog(MainWindow* parent, const AppConfig& config) : setupUi(this); m_versionChecker.setApp(parent->appPath(config.synergycName())); - QString version = m_versionChecker.getVersion(); - m_pLabelSynergyVersion->setText(version); + m_pLabelSynergyVersion->setText(SYNERGY_VERSION); QString buildDateString = QString::fromLocal8Bit(__DATE__).simplified(); QDate buildDate = QLocale("en_US").toDate(buildDateString, "MMM d yyyy"); diff --git a/src/gui/src/MainWindow.cpp b/src/gui/src/MainWindow.cpp index 5af04a173..1524322a3 100644 --- a/src/gui/src/MainWindow.cpp +++ b/src/gui/src/MainWindow.cpp @@ -188,11 +188,10 @@ MainWindow::MainWindow (AppConfig& appConfig, updateWindowTitle(); QString lastVersion = m_AppConfig->lastVersion(); - QString currentVersion = m_VersionChecker.getVersion(); - if (lastVersion != currentVersion) { - m_AppConfig->setLastVersion (currentVersion); + if (lastVersion != SYNERGY_VERSION) { + m_AppConfig->setLastVersion (SYNERGY_VERSION); #ifndef SYNERGY_ENTERPRISE - m_LicenseManager->notifyUpdate (lastVersion, currentVersion); + m_LicenseManager->notifyUpdate (lastVersion, SYNERGY_VERSION); #endif } diff --git a/src/gui/src/VersionChecker.cpp b/src/gui/src/VersionChecker.cpp index 4933c56a6..e60304113 100644 --- a/src/gui/src/VersionChecker.cpp +++ b/src/gui/src/VersionChecker.cpp @@ -24,12 +24,8 @@ #include #include -#define VERSION_REGEX "(\\d+\\.\\d+\\.\\d+-[a-z1-9]*)" -#define VERSION_REGEX_SECTIONED "(\\d+)\\.(\\d+)\\.(\\d+)-([a-z1-9]*)" -#define VERSION_SEGMENT_COUNT 4 #define VERSION_URL "https://api.symless.com/version" - VersionChecker::VersionChecker() { m_manager = new QNetworkAccessManager(this); @@ -46,53 +42,52 @@ VersionChecker::~VersionChecker() void VersionChecker::checkLatest() { auto request = QNetworkRequest(QUrl(VERSION_URL)); - request.setHeader(QNetworkRequest::UserAgentHeader, QString("Synergy (") + getVersion() + ") " + QSysInfo::prettyProductName()); - request.setRawHeader("X-Synergy-Version", getVersion().toStdString().c_str() ); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Synergy (") + SYNERGY_VERSION + ") " + QSysInfo::prettyProductName()); + request.setRawHeader("X-Synergy-Version", SYNERGY_VERSION ); request.setRawHeader("X-Synergy-Language", QLocale::system().name().toStdString().c_str() ); m_manager->get(request); } void VersionChecker::replyFinished(QNetworkReply* reply) { - QString newestVersion = QString(reply->readAll()); - if (!newestVersion.isEmpty()) - { - QString currentVersion = getVersion(); - if (currentVersion != "Unknown") { - if (compareVersions(currentVersion, newestVersion) > 0) - emit updateFound(newestVersion); - } + auto newestVersion = QString(reply->readAll()); + if (!newestVersion.isEmpty() && compareVersions(SYNERGY_VERSION, newestVersion) > 0) { + emit updateFound(newestVersion); } } -int VersionChecker::getStageVersion(QString stage) +int VersionChecker::getStageVersion(QString stage) const { - const int valueStable = INT_MAX; //Stable will always be considered the highest value - const int valueRC = 2; - const int valueSnapshot = 1; - const int valueOther = 0; + const char* stableName = "stable"; + const char* rcName = "rc"; + const char* betaName = "beta"; - //Stable should always be considered highest, followed by rc[0-9] then snapshots with everything else at the end - //HACK There is probably a much better way of doing this - if (stage == "stable") + // use max int for stable so it's always the highest value. + const int stableValue = INT_MAX; + const int rcValue = 2; + const int betaValue = 1; + const int otherValue = 0; + + if (stage == stableName) { - return valueStable; + return stableValue; } - else if (stage.startsWith("rc") || stage.startsWith("RC")) + else if (stage.toLower().startsWith(rcName)) { QRegExp rx("\\d*", Qt::CaseInsensitive); if (rx.indexIn(stage) != -1) { - //Return the RC value plus the RC version as in int - return valueRC + rx.cap(1).toInt(); + // return the rc value plus the rc number (e.g. 2 + 1) + // this should be ok since stable is max int. + return rcValue + rx.cap(1).toInt(); } } - else if (stage == "snapshot") + else if (stage == betaName) { - return valueSnapshot; + return betaValue; } - - return valueOther; + + return otherValue; } int VersionChecker::compareVersions(const QString& left, const QString& right) @@ -100,48 +95,34 @@ int VersionChecker::compareVersions(const QString& left, const QString& right) if (left.compare(right) == 0) return 0; // versions are same. - QStringList leftSplit = left.split(QRegExp("[\\.-]")); - if (leftSplit.size() != VERSION_SEGMENT_COUNT) - return 1; // assume right wins. + QStringList leftParts = left.split("-"); + QStringList rightParts = right.split("-"); - QStringList rightSplit = right.split(QRegExp("[\\.-]")); - if (rightSplit.size() != VERSION_SEGMENT_COUNT) - return -1; // assume left wins. + if (leftParts.size() < 1 || rightParts.size() < 1) + return 0; // versions are same. - const int leftMajor = leftSplit.at(0).toInt(); - const int leftMinor = leftSplit.at(1).toInt(); - const int leftRev = leftSplit.at(2).toInt(); - const int leftStage = getStageVersion(leftSplit.at(3)); + QString leftNumber = leftParts.at(0); + QString rightNumber = rightParts.at(0); - const int rightMajor = rightSplit.at(0).toInt(); - const int rightMinor = rightSplit.at(1).toInt(); - const int rightRev = rightSplit.at(2).toInt(); - const int rightStage = getStageVersion(rightSplit.at(3)); + QStringList leftNumberParts = left.split("."); + QStringList rightNumberParts = right.split("."); + + const int leftMajor = leftNumberParts.at(0).toInt(); + const int leftMinor = leftNumberParts.at(1).toInt(); + const int leftPatch = leftNumberParts.at(2).toInt(); + const int leftStage = leftParts.size() > 1 ? getStageVersion(leftParts.at(1)) : 0; + + const int rightMajor = rightNumberParts.at(0).toInt(); + const int rightMinor = rightNumberParts.at(1).toInt(); + const int rightPatch = rightNumberParts.at(2).toInt(); + const int rightStage = rightParts.size() > 1 ? getStageVersion(rightParts.at(1)) : 0; const bool rightWins = ( rightMajor > leftMajor) || ((rightMajor >= leftMajor) && (rightMinor > leftMinor)) || - ((rightMajor >= leftMajor) && (rightMinor >= leftMinor) && (rightRev > leftRev)) || - ((rightMajor >= leftMajor) && (rightMinor >= leftMinor) && (rightRev >= leftRev) && (rightStage > leftStage)); + ((rightMajor >= leftMajor) && (rightMinor >= leftMinor) && (rightPatch > leftPatch)) || + ((rightMajor >= leftMajor) && (rightMinor >= leftMinor) && (rightPatch >= leftPatch) && (rightStage > leftStage)); return rightWins ? 1 : -1; } -QString VersionChecker::getVersion() -{ - QProcess process; - process.start(m_app, QStringList() << "--version"); - - process.setReadChannel(QProcess::StandardOutput); - if (process.waitForStarted() && process.waitForFinished()) - { - QRegExp rx(VERSION_REGEX,Qt::CaseInsensitive); - QString text = process.readLine(); - if (rx.indexIn(text) != -1) - { - return rx.cap(1); - } - } - - return tr("Unknown"); -} diff --git a/src/gui/src/VersionChecker.h b/src/gui/src/VersionChecker.h index 2048d3d8a..cce00ad22 100644 --- a/src/gui/src/VersionChecker.h +++ b/src/gui/src/VersionChecker.h @@ -31,7 +31,6 @@ public: VersionChecker(); virtual ~VersionChecker(); void checkLatest(); - QString getVersion(); void setApp(const QString& app) { m_app = app; } int compareVersions(const QString& left, const QString& right); public slots: @@ -47,5 +46,5 @@ private: * \param stage The string containing the stage version * \return An integer representation of the stage, the higher the number the more recent the version */ - int getStageVersion(QString stage); + int getStageVersion(QString stage) const; }; diff --git a/src/test/integtests/CMakeLists.txt b/src/test/integtests/CMakeLists.txt index 9ff8027a3..e85b93111 100644 --- a/src/test/integtests/CMakeLists.txt +++ b/src/test/integtests/CMakeLists.txt @@ -53,6 +53,10 @@ if (SYNERGY_ADD_HEADERS) list(APPEND sources ${headers}) endif() +if (WIN32) + list(APPEND sources ${CMAKE_BINARY_DIR}/src/version.rc) +endif() + include_directories( ../../ ../../lib/ diff --git a/src/test/unittests/CMakeLists.txt b/src/test/unittests/CMakeLists.txt index 197537f9f..00fcf6b5e 100644 --- a/src/test/unittests/CMakeLists.txt +++ b/src/test/unittests/CMakeLists.txt @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +set(target unittests) + file(GLOB_RECURSE headers "*.h" "languages/*.h") file(GLOB_RECURSE sources "*.cpp" "languages/*.cpp") @@ -60,6 +62,10 @@ if (SYNERGY_ADD_HEADERS) list(APPEND sources ${headers}) endif() -add_executable(unittests ${sources}) -target_link_libraries(unittests +if (WIN32) + list(APPEND sources ${CMAKE_BINARY_DIR}/src/version.rc) +endif() + +add_executable(${target} ${sources}) +target_link_libraries(${target} arch base client server common io net platform server synlib mt ipc gtest gmock shared ${libs}) diff --git a/test b/test deleted file mode 100644 index e69de29bb..000000000