mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-11-26 09:36:33 -05:00
Integrate macOS code signing into CMake
Moves code signing from the release-tool to CMake and unifies the Windows-equivalent code.
This commit is contained in:
parent
c09ba0113b
commit
4e59c1c579
8 changed files with 301 additions and 232 deletions
|
|
@ -60,10 +60,17 @@ option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
|
|||
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
|
||||
if(UNIX AND NOT APPLE)
|
||||
option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF)
|
||||
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps")
|
||||
endif()
|
||||
option(WITH_XC_DOCS "Enable building of documentation" ON)
|
||||
|
||||
set(WITH_XC_X11 ON CACHE BOOL "Enable building with X11 deps")
|
||||
if(WIN32 OR APPLE)
|
||||
set(WITH_XC_CODESIGN_IDENTITY "" CACHE STRING "Certificate to be used for signing binaries before packaging.")
|
||||
if(WIN32)
|
||||
set(WITH_XC_CODESIGN_TIMESTAMP_URL "http://timestamp.sectigo.com" CACHE STRING "Timestamp URL for Windows code signing.")
|
||||
elseif(APPLE)
|
||||
set(WITH_XC_NOTARY_KEYCHAIN_PROFILE "" CACHE STRING "Keychain profile name for stored Apple notarization credentials.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Perform the platform checks before applying the stricter compiler flags.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ find_library(
|
|||
NAMES ${BOTAN_NAMES}
|
||||
PATH_SUFFIXES release/lib lib
|
||||
DOC "The Botan (release) library")
|
||||
if(MSVC)
|
||||
if(WIN32 AND NOT MINGW)
|
||||
find_library(
|
||||
BOTAN_LIBRARY_DEBUG
|
||||
NAMES ${BOTAN_NAMES_DEBUG}
|
||||
|
|
@ -55,7 +55,7 @@ endif()
|
|||
|
||||
if(BOTAN_FOUND)
|
||||
set(BOTAN_INCLUDE_DIRS ${BOTAN_INCLUDE_DIR})
|
||||
if(MSVC)
|
||||
if(WIN32 AND NOT MINGW)
|
||||
set(BOTAN_LIBRARIES optimized ${BOTAN_LIBRARY} debug ${BOTAN_LIBRARY_DEBUG})
|
||||
else()
|
||||
set(BOTAN_LIBRARIES ${BOTAN_LIBRARY})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
find_path(QRENCODE_INCLUDE_DIR NAMES qrencode.h)
|
||||
|
||||
if(WIN32 AND MSVC)
|
||||
if(WIN32 AND NOT MINGW)
|
||||
find_library(QRENCODE_LIBRARY_RELEASE qrencode)
|
||||
find_library(QRENCODE_LIBRARY_DEBUG qrencoded)
|
||||
set(QRENCODE_LIBRARY optimized ${QRENCODE_LIBRARY_RELEASE} debug ${QRENCODE_LIBRARY_DEBUG})
|
||||
|
|
|
|||
102
cmake/MacOSCodesign.cmake.in
Normal file
102
cmake/MacOSCodesign.cmake.in
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 or (at your option)
|
||||
# version 3 of the License.
|
||||
#
|
||||
# This program 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# CPACK_PACKAGE_FILES is set only during POST_BUILD
|
||||
if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries
|
||||
|
||||
set(PROGNAME "@PROGNAME@")
|
||||
set(CODESIGN_IDENTITY "@WITH_XC_CODESIGN_IDENTITY@")
|
||||
set(ENTITLEMENTS @MACOSX_BUNDLE_APPLE_ENTITLEMENTS@)
|
||||
set(APP_DIR "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/ALL_IN_ONE/${PROGNAME}.app")
|
||||
|
||||
if(NOT CODESIGN_IDENTITY)
|
||||
message(FATAL_ERROR "No codesign identity specified.")
|
||||
endif()
|
||||
|
||||
message(STATUS "Codesign identity used: ${CODESIGN_IDENTITY}")
|
||||
message(STATUS "Signing ${PROGNAME}.app, this may take while...")
|
||||
|
||||
# Sign all binaries
|
||||
execute_process(
|
||||
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep ${APP_DIR}
|
||||
RESULT_VARIABLE SIGN_RESULT
|
||||
OUTPUT_VARIABLE SIGN_OUTPUT
|
||||
ERROR_VARIABLE SIGN_ERROR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if (NOT SIGN_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Signing binaries failed: ${SIGN_ERROR}")
|
||||
endif()
|
||||
|
||||
# (Re-)Sign main executable with --entitlements
|
||||
execute_process(
|
||||
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep --entitlements=${ENTITLEMENTS} ${APP_DIR}
|
||||
RESULT_VARIABLE SIGN_RESULT
|
||||
OUTPUT_VARIABLE SIGN_OUTPUT
|
||||
ERROR_VARIABLE SIGN_ERROR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if (NOT SIGN_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Signing main binary failed: ${SIGN_ERROR}")
|
||||
endif()
|
||||
|
||||
message(STATUS "${PROGNAME}.app signed successfully.")
|
||||
|
||||
else() # POST_BUILD: Notarize DMG
|
||||
set(KEYCHAIN_PROFILE "@WITH_XC_NOTARY_KEYCHAIN_PROFILE@")
|
||||
file(GLOB_RECURSE DMG_FILE "${CPACK_PACKAGE_DIRECTORY}/${CPACK_PACKAGE_FILE_NAME}.dmg")
|
||||
|
||||
if(NOT KEYCHAIN_PROFILE)
|
||||
message(FATAL_ERROR "No notarization credentials keychain profile specified.")
|
||||
endif()
|
||||
|
||||
# Submit for notarization
|
||||
message(STATUS "Submitting DMG bundle for notarization, this may take while...")
|
||||
execute_process(
|
||||
COMMAND xcrun notarytool submit --keychain-profile=${KEYCHAIN_PROFILE} --wait ${DMG_FILE}
|
||||
RESULT_VARIABLE NOTARIZE_RESULT
|
||||
OUTPUT_VARIABLE NOTARIZE_OUTPUT
|
||||
ERROR_VARIABLE NOTARIZE_ERROR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if (NOT NOTARIZE_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Notarization failed: ${NOTARIZE_ERROR}")
|
||||
endif()
|
||||
message(STATUS "DMG bundle notarized successfully.")
|
||||
|
||||
# Staple tickets
|
||||
message(STATUS "Stapling notarization ticket...")
|
||||
execute_process(
|
||||
COMMAND xcrun stapler staple ${DMG_FILE} && xcrun stapler validate ${DMG_FILE}
|
||||
RESULT_VARIABLE STAPLE_RESULT
|
||||
OUTPUT_VARIABLE STAPLE_OUTPUT
|
||||
ERROR_VARIABLE STAPLE_ERROR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if (NOT STAPLE_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Stapling failed: ${STAPLE_ERROR}")
|
||||
endif()
|
||||
message(STATUS "DMG bundle notarization ticket stapled successfully.")
|
||||
|
||||
endif()
|
||||
79
cmake/WindowsCodesign.cmake.in
Normal file
79
cmake/WindowsCodesign.cmake.in
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 or (at your option)
|
||||
# version 3 of the License.
|
||||
#
|
||||
# This program 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
set(INSTALL_DIR ${CPACK_TEMPORARY_INSTALL_DIRECTORY})
|
||||
set(CODESIGN_IDENTITY @WITH_XC_CODESIGN_IDENTITY@)
|
||||
set(TIMESTAMP_URL @WITH_XC_CODESIGN_TIMESTAMP_URL@)
|
||||
|
||||
if(CPACK_PACKAGE_FILES)
|
||||
# This variable is set only during POST_BUILD, reset SIGN_FILES first
|
||||
set(SIGN_FILES "")
|
||||
foreach(PACKAGE_FILE ${CPACK_PACKAGE_FILES})
|
||||
# Check each package file to see if it can be signed
|
||||
if(PACKAGE_FILE MATCHES "\\.msix?$" OR PACKAGE_FILE MATCHES "\\.exe$")
|
||||
message(STATUS "Adding ${PACKAGE_FILE} for signature")
|
||||
list(APPEND SIGN_FILES "${PACKAGE_FILE}")
|
||||
endif()
|
||||
endforeach()
|
||||
else()
|
||||
# Setup portable zip file if building one
|
||||
if(INSTALL_DIR MATCHES "/ZIP/")
|
||||
file(TOUCH "${INSTALL_DIR}/.portable")
|
||||
message(STATUS "Injected portable marker into ZIP file.")
|
||||
endif()
|
||||
|
||||
# Find all dll and exe files in the install directory
|
||||
file(GLOB_RECURSE SIGN_FILES
|
||||
RELATIVE "${INSTALL_DIR}"
|
||||
"${INSTALL_DIR}/*.dll"
|
||||
"${INSTALL_DIR}/*.exe"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Sign relevant binaries if requested
|
||||
if(CODESIGN_IDENTITY AND SIGN_FILES)
|
||||
# Find signtool in PATH or error out
|
||||
find_program(SIGNTOOL signtool.exe QUIET)
|
||||
if(NOT SIGNTOOL)
|
||||
message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_CODESIGN_IDENTITY")
|
||||
endif()
|
||||
|
||||
# Check that a certificate thumbprint was provided or error out
|
||||
if(CODESIGN_IDENTITY STREQUAL "auto")
|
||||
message(STATUS "Signing using best available certificate.")
|
||||
set(CERT_OPTS /a)
|
||||
else ()
|
||||
message(STATUS "Signing using certificate with fingerprint ${CODESIGN_IDENTITY}.")
|
||||
set(CERT_OPTS /sha1 ${CODESIGN_IDENTITY})
|
||||
endif()
|
||||
|
||||
message(STATUS "Signing binary files, this may take a while...")
|
||||
# Use cmd /c to enable pop-up for pin entry if needed
|
||||
execute_process(
|
||||
COMMAND cmd /c ${SIGNTOOL} sign /fd SHA256 ${CERT_OPTS} /tr ${TIMESTAMP_URL} /td SHA256 /d ${CPACK_PACKAGE_FILE_NAME} ${SIGN_FILES}
|
||||
WORKING_DIRECTORY "${INSTALL_DIR}"
|
||||
RESULT_VARIABLE SIGN_RESULT
|
||||
OUTPUT_VARIABLE SIGN_OUTPUT
|
||||
ERROR_VARIABLE SIGN_ERROR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if(NOT SIGN_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Signing binary files failed: ${SIGN_ERROR}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Binary files signed successfully.")
|
||||
endif()
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
# Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 or (at your option)
|
||||
# version 3 of the License.
|
||||
#
|
||||
# This program 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
set(_installdir ${CPACK_TEMPORARY_INSTALL_DIRECTORY})
|
||||
set(_sign @WITH_XC_SIGNINSTALL@)
|
||||
set(_cert_thumbprint @WITH_XC_SIGNINSTALL_CERT@)
|
||||
set(_timestamp_url @WITH_XC_SIGNINSTALL_TIMESTAMP_URL@)
|
||||
|
||||
# Setup portable zip file if building one
|
||||
if(_installdir MATCHES "/ZIP/")
|
||||
file(TOUCH "${_installdir}/.portable")
|
||||
message(STATUS "Injected portable zip file.")
|
||||
endif()
|
||||
|
||||
# Find all dll and exe files in the install directory
|
||||
file(GLOB_RECURSE _sign_files
|
||||
RELATIVE "${_installdir}"
|
||||
"${_installdir}/*.dll"
|
||||
"${_installdir}/*.exe"
|
||||
)
|
||||
|
||||
# Sign relevant binaries if requested
|
||||
if(_sign AND _sign_files)
|
||||
# Find signtool in PATH or error out
|
||||
find_program(_signtool signtool.exe QUIET)
|
||||
if(NOT _signtool)
|
||||
message(FATAL_ERROR "signtool.exe not found in PATH, correct or unset WITH_XC_SIGNINSTALL")
|
||||
endif()
|
||||
|
||||
# Set a default timestamp URL if none was provided
|
||||
if (NOT _timestamp_url)
|
||||
set(_timestamp_url "http://timestamp.sectigo.com")
|
||||
endif()
|
||||
|
||||
# Check that a certificate thumbprint was provided or error out
|
||||
if (NOT _cert_thumbprint)
|
||||
message(STATUS "Signing using best available certificate.")
|
||||
set(_certopt /a)
|
||||
else()
|
||||
message(STATUS "Signing using certificate with thumbprint ${_cert_thumbprint}.")
|
||||
set(_certopt /sha1 ${_cert_thumbprint})
|
||||
endif()
|
||||
|
||||
message(STATUS "Signing binary files with signtool, this may take a while...")
|
||||
# Use cmd /c to enable pop-up for pin entry if needed
|
||||
execute_process(
|
||||
COMMAND cmd /c ${_signtool} sign /fd SHA256 ${_certopt} /tr ${_timestamp_url} /td SHA256 ${_sign_files}
|
||||
WORKING_DIRECTORY "${_installdir}"
|
||||
RESULT_VARIABLE sign_result
|
||||
OUTPUT_VARIABLE sign_output
|
||||
ERROR_VARIABLE sign_error
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_STRIP_TRAILING_WHITESPACE
|
||||
ECHO_OUTPUT_VARIABLE
|
||||
)
|
||||
if (NOT sign_result EQUAL 0)
|
||||
message(FATAL_ERROR "signtool failed: ${sign_error}")
|
||||
endif()
|
||||
endif()
|
||||
242
release-tool.py
242
release-tool.py
|
|
@ -342,6 +342,48 @@ def _capture_vs_env(arch='amd64'):
|
|||
return env
|
||||
|
||||
|
||||
def _macos_get_codesigning_identity(user_choice=None):
|
||||
"""
|
||||
Select an Apple codesigning certificate to be used for signing the macOS binaries.
|
||||
If only one identity was found on the system, it is returned automatically. If multiple identities are
|
||||
found, an interactive selection is shown. A user choice can be supplied to skip the selection.
|
||||
If the user choice refers to an invalid identity, an error is raised.
|
||||
"""
|
||||
Check.check_xcode_setup()
|
||||
result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True)
|
||||
identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]]
|
||||
identities = [i.split(' ', 2)[1:] for i in identities]
|
||||
if not identities:
|
||||
raise Error('No codesigning identities found.')
|
||||
|
||||
if not user_choice and len(identities) == 1:
|
||||
logger.info('Using codesigning identity %s.', identities[0][1])
|
||||
return identities[0][0]
|
||||
elif not user_choice:
|
||||
return identities[_choice_prompt(
|
||||
'The following code signing identities were found. Which one do you want to use?',
|
||||
[' '.join(i) for i in identities])][0]
|
||||
else:
|
||||
for i in identities:
|
||||
# Exact match of ID or substring match of description
|
||||
if user_choice == i[0] or user_choice in i[1]:
|
||||
return i[0]
|
||||
raise Error('Invalid identity: %s', user_choice)
|
||||
|
||||
|
||||
def _macos_validate_keychain_profile(keychain_profile):
|
||||
"""
|
||||
Validate that a given keychain profile with stored notarization credentials exists and is valid.
|
||||
If no such profile is found, an error is raised with instructions on how to set one up.
|
||||
"""
|
||||
if _run(['security', 'find-generic-password', '-a',
|
||||
f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0:
|
||||
raise Error(f'Keychain profile "%s" not found! Run\n'
|
||||
f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n'
|
||||
f'to store your Apple notary service credentials in a keychain as "%s".',
|
||||
keychain_profile, keychain_profile)
|
||||
|
||||
|
||||
###########################################################################################
|
||||
# CLI Commands
|
||||
###########################################################################################
|
||||
|
|
@ -585,6 +627,13 @@ class Build(Command):
|
|||
help='macOS deployment target version (default: %(default)s).')
|
||||
parser.add_argument('-p', '--platform-target', default=platform.uname().machine,
|
||||
help='Build target platform (default: %(default)s).', choices=['x86_64', 'arm64'])
|
||||
parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true')
|
||||
parser.add_argument('--sign-identity',
|
||||
help='Apple Developer identity name used for signing binaries (default: ask).')
|
||||
parser.add_argument('--notarize', help='Notarize signed file(s).', action='store_true')
|
||||
parser.add_argument('--keychain-profile', default='notarization-creds',
|
||||
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
||||
parser.set_defaults(cmake_generator='Ninja')
|
||||
elif sys.platform == 'linux':
|
||||
parser.add_argument('-d', '--docker-image', help='Run build in Docker image (overrides --use-system-deps).')
|
||||
parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).',
|
||||
|
|
@ -594,8 +643,10 @@ class Build(Command):
|
|||
parser.add_argument('-p', '--platform-target', help='Build target platform (default: %(default)s).',
|
||||
choices=['amd64', 'arm64'], default='amd64')
|
||||
parser.add_argument('--sign', help='Sign binaries prior to packaging.', action='store_true')
|
||||
parser.add_argument('--sign-cert', help='SHA1 fingerprint of the signing certificate (optional).')
|
||||
parser.set_defaults(cmake_generator='Ninja', no_source_tarball=True)
|
||||
parser.add_argument('--sign-identity', help='SHA1 fingerprint of the signing certificate.')
|
||||
parser.add_argument('--sign-timestamp-url', help='Timestamp URL for signing binaries.',
|
||||
default='http://timestamp.sectigo.com')
|
||||
parser.set_defaults(cmake_generator='Ninja')
|
||||
|
||||
parser.add_argument('-c', '--cmake-opts', nargs=argparse.REMAINDER,
|
||||
help='Additional CMake options (no other arguments can be specified after this).')
|
||||
|
|
@ -674,15 +725,15 @@ class Build(Command):
|
|||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def build_windows(self, version, src_dir, output_dir, *, parallelism, cmake_opts, platform_target,
|
||||
sign, sign_cert, with_tests, **_):
|
||||
sign, sign_identity, sign_timestamp_url, with_tests, **_):
|
||||
# Check for required tools
|
||||
if not _cmd_exists('candle.exe') or not _cmd_exists('light.exe') or not _cmd_exists('heat.exe'):
|
||||
raise Error('WiX Toolset not found on the PATH (candle.exe, light.exe, heat.exe).')
|
||||
|
||||
# Setup build signing if requested
|
||||
if sign:
|
||||
cmake_opts.append('-DWITH_XC_SIGNINSTALL=ON')
|
||||
cmake_opts.append(f'-DWITH_XC_SIGNINSTALL_CERT={sign_cert}')
|
||||
cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
|
||||
cmake_opts.append(f'-WITH_XC_CODESIGN_TIMESTAMP_URL={sign_timestamp_url}')
|
||||
# Use vcpkg for dependency deployment
|
||||
cmake_opts.append('-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON')
|
||||
|
||||
|
|
@ -716,13 +767,20 @@ class Build(Command):
|
|||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def build_macos(self, version, src_dir, output_dir, *, use_system_deps, parallelism, cmake_opts,
|
||||
macos_target, platform_target, with_tests, **_):
|
||||
macos_target, platform_target, with_tests, sign, sign_identity, notarize, keychain_profile, **_):
|
||||
if not use_system_deps:
|
||||
cmake_opts.append(f'-DVCPKG_TARGET_TRIPLET={platform_target.replace("86_", "")}-osx-dynamic-release')
|
||||
cmake_opts.append(f'-DCMAKE_OSX_DEPLOYMENT_TARGET={macos_target}')
|
||||
cmake_opts.append(f'-DCMAKE_OSX_ARCHITECTURES={platform_target}')
|
||||
|
||||
with tempfile.TemporaryDirectory() as build_dir:
|
||||
if sign:
|
||||
sign_identity = _macos_get_codesigning_identity(sign_identity)
|
||||
cmake_opts.append(f'-DWITH_XC_CODESIGN_IDENTITY={sign_identity}')
|
||||
if notarize:
|
||||
_macos_validate_keychain_profile(keychain_profile)
|
||||
cmake_opts.append(f'-DWITH_XC_NOTARY_KEYCHAIN_PROFILE={keychain_profile}')
|
||||
|
||||
logger.info('Configuring build...')
|
||||
_run(['cmake', *cmake_opts, str(src_dir)], cwd=build_dir, capture_output=False)
|
||||
|
||||
|
|
@ -736,9 +794,13 @@ class Build(Command):
|
|||
_run(['cpack', '-G', 'DragNDrop'], cwd=build_dir, capture_output=False)
|
||||
|
||||
output_file = Path(build_dir) / f'KeePassXC-{version}.dmg'
|
||||
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}-unsigned.dmg')
|
||||
unsigned_suffix = '-unsigned' if not sign else ''
|
||||
output_file.rename(output_dir / f'KeePassXC-{version}-{platform_target}{unsigned_suffix}.dmg')
|
||||
|
||||
logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
|
||||
if sign:
|
||||
logger.info('All done!')
|
||||
else:
|
||||
logger.info('All done! Please don\'t forget to sign the binaries before distribution.')
|
||||
|
||||
@staticmethod
|
||||
def _download_tools_if_not_available(toolname, bin_dir, url, docker_args=None):
|
||||
|
|
@ -888,162 +950,37 @@ class BuildSrc(Command):
|
|||
tmp_comp.rename(output_file)
|
||||
|
||||
|
||||
class AppSign(Command):
|
||||
"""Sign binaries with code signing certificates on Windows and macOS."""
|
||||
class Notarize(Command):
|
||||
"""Notarize a signed macOS DMG app bundle."""
|
||||
|
||||
@classmethod
|
||||
def setup_arg_parser(cls, parser: argparse.ArgumentParser):
|
||||
parser.add_argument('file', help='Input file(s) to sign.', nargs='+')
|
||||
parser.add_argument('-i', '--identity', help='Key or identity used for the signature (default: ask).')
|
||||
parser.add_argument('-s', '--src-dir', help='Source directory (default: %(default)s).', default='.')
|
||||
parser.add_argument('file', help='Input DMG file(s) to notarize.', nargs='+')
|
||||
parser.add_argument('-p', '--keychain-profile', default='notarization-creds',
|
||||
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
parser.add_argument('-n', '--notarize', help='Notarize signed file(s).', action='store_true')
|
||||
parser.add_argument('-c', '--keychain-profile', default='notarization-creds',
|
||||
help='Read Apple credentials for notarization from a keychain (default: %(default)s).')
|
||||
def run(self, file, keychain_profile, **_):
|
||||
if sys.platform != 'darwin':
|
||||
raise Error('Unsupported platform.')
|
||||
|
||||
def run(self, file, identity, src_dir, **kwargs):
|
||||
logger.warning('This tool is meant primarily for testing purposes. '
|
||||
'For production use, add the --notarize flag to the build command.')
|
||||
|
||||
_macos_validate_keychain_profile(keychain_profile)
|
||||
for i, f in enumerate(file):
|
||||
f = Path(f)
|
||||
if not f.exists():
|
||||
raise Error('Input file does not exist: %s', f)
|
||||
if f.suffix != '.dmg':
|
||||
raise Error('Input file is not a DMG image: %s', f)
|
||||
file[i] = f
|
||||
|
||||
if sys.platform == 'win32':
|
||||
for f in file:
|
||||
self.sign_windows(f, identity, Path(src_dir))
|
||||
|
||||
elif sys.platform == 'darwin':
|
||||
Check.check_xcode_setup()
|
||||
if kwargs['notarize']:
|
||||
self._macos_validate_keychain_profile(kwargs['keychain_profile'])
|
||||
identity = self._macos_get_codesigning_identity(identity)
|
||||
for f in file:
|
||||
out_file = self.sign_macos(f, identity, Path(src_dir))
|
||||
if out_file and kwargs['notarize'] and out_file.suffix == '.dmg':
|
||||
self.notarize_macos(out_file, kwargs['keychain_profile'])
|
||||
|
||||
else:
|
||||
raise Error('Unsupported platform.')
|
||||
self.notarize_macos(f, keychain_profile)
|
||||
|
||||
logger.info('All done.')
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def sign_windows(self, file, identity, src_dir):
|
||||
# Check for signtool
|
||||
if not _cmd_exists('signtool.exe'):
|
||||
raise Error('signtool was not found on the PATH.')
|
||||
|
||||
signtool_args = ['signtool', 'sign', '/fd', 'sha256', '/tr', 'http://timestamp.digicert.com', '/td', 'sha256']
|
||||
if not identity:
|
||||
logger.info('Using automatic selection of signing certificate.')
|
||||
signtool_args += ['/a']
|
||||
else:
|
||||
logger.info('Using specified signing certificate: %s', identity)
|
||||
signtool_args += ['/sha1', identity]
|
||||
signtool_args += ['/d', file.name, str(file.resolve())]
|
||||
|
||||
_run(signtool_args, cwd=src_dir, capture_output=False)
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _macos_validate_keychain_profile(self, keychain_profile):
|
||||
if _run(['security', 'find-generic-password', '-a',
|
||||
f'com.apple.gke.notary.tool.saved-creds.{keychain_profile}'], cwd=None, check=False).returncode != 0:
|
||||
raise Error(f'Keychain profile "%s" not found! Run\n'
|
||||
f' {fmt.bold("xcrun notarytool store-credentials %s [...]" % keychain_profile)}\n'
|
||||
f'to store your Apple notary service credentials in a keychain as "%s".',
|
||||
keychain_profile, keychain_profile)
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _macos_get_codesigning_identity(self, user_choice=None):
|
||||
result = _run(['security', 'find-identity', '-v', '-p', 'codesigning'], cwd=None, text=True)
|
||||
identities = [i.strip() for i in result.stdout.strip().split('\n')[:-1]]
|
||||
identities = [i.split(' ', 2)[1:] for i in identities]
|
||||
if not identities:
|
||||
raise Error('No codesigning identities found.')
|
||||
|
||||
if not user_choice and len(identities) == 1:
|
||||
logger.info('Using codesigning identity %s.', identities[0][1])
|
||||
return identities[0][0]
|
||||
elif not user_choice:
|
||||
return identities[_choice_prompt(
|
||||
'The following code signing identities were found. Which one do you want to use?',
|
||||
[' '.join(i) for i in identities])][0]
|
||||
else:
|
||||
for i in identities:
|
||||
# Exact match of ID or substring match of description
|
||||
if user_choice == i[0] or user_choice in i[1]:
|
||||
return i[0]
|
||||
raise Error('Invalid identity: %s', user_choice)
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def sign_macos(self, file, identity, src_dir):
|
||||
logger.info('Signing "%s"', file)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp).absolute()
|
||||
app_dir = tmp / 'app'
|
||||
out_file = file.parent / file.name.replace('-unsigned', '')
|
||||
|
||||
if file.is_file() and file.suffix == '.dmg':
|
||||
logger.debug('Unpacking disk image...')
|
||||
mnt = tmp / 'mnt'
|
||||
mnt.mkdir()
|
||||
try:
|
||||
_run(['hdiutil', 'attach', '-noautoopen', '-mountpoint', mnt.as_posix(), file.as_posix()], cwd=None)
|
||||
shutil.copytree(mnt, app_dir, symlinks=True)
|
||||
finally:
|
||||
_run(['hdiutil', 'detach', mnt.as_posix()], cwd=None)
|
||||
elif file.is_dir() and file.suffix == '.app':
|
||||
logger.debug('Copying .app directory...')
|
||||
shutil.copytree(file, app_dir, symlinks=True)
|
||||
else:
|
||||
logger.warning('Skipping non-app file "%s"', file)
|
||||
return None
|
||||
|
||||
app_dir_app = list(app_dir.glob('*.app'))[0]
|
||||
|
||||
logger.debug('Signing libraries and frameworks...')
|
||||
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime', '--deep',
|
||||
app_dir_app.as_posix()], cwd=None)
|
||||
|
||||
# (Re-)Sign main executable with --entitlements
|
||||
logger.debug('Signing main executable...')
|
||||
_run(['xcrun', 'codesign', f'--sign={identity}', '--force', '--options=runtime',
|
||||
'--entitlements', (src_dir / 'share/macosx/keepassxc.entitlements').as_posix(),
|
||||
(app_dir_app / 'Contents/MacOS/KeePassXC').as_posix()], cwd=None)
|
||||
|
||||
tmp_out = out_file.with_suffix(f'.{"".join(random.choices(string.ascii_letters, k=8))}{file.suffix}')
|
||||
try:
|
||||
if file.suffix == '.dmg':
|
||||
logger.debug('Repackaging disk image...')
|
||||
dmg_size = sum(f.stat().st_size for f in app_dir.rglob('*'))
|
||||
_run(['hdiutil', 'create', '-volname', 'KeePassXC', '-srcfolder', app_dir.as_posix(),
|
||||
'-fs', 'HFS+', '-fsargs', '-c c=64,a=16,e=16', '-format', 'UDBZ',
|
||||
'-size', f'{dmg_size}k', tmp_out.as_posix()],
|
||||
cwd=None)
|
||||
elif file.suffix == '.app':
|
||||
shutil.copytree(app_dir, tmp_out, symlinks=True)
|
||||
except Exception:
|
||||
if tmp_out.is_file():
|
||||
tmp_out.unlink()
|
||||
elif tmp_out.is_dir():
|
||||
shutil.rmtree(tmp_out, ignore_errors=True)
|
||||
raise
|
||||
finally:
|
||||
# Replace original file if all went well
|
||||
if tmp_out.exists():
|
||||
if tmp_out.is_dir():
|
||||
shutil.rmtree(file)
|
||||
else:
|
||||
file.unlink()
|
||||
tmp_out.rename(out_file)
|
||||
|
||||
logger.info('File signed successfully and written to: "%s".', out_file)
|
||||
return out_file
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def notarize_macos(self, file, keychain_profile):
|
||||
|
||||
logger.info('Submitting "%s" for notarization...', file)
|
||||
_run(['xcrun', 'notarytool', 'submit', f'--keychain-profile={keychain_profile}', '--wait',
|
||||
file.as_posix()], cwd=None, capture_output=False)
|
||||
|
|
@ -1271,9 +1208,10 @@ def main():
|
|||
BuildSrc.setup_arg_parser(build_src_parser)
|
||||
build_src_parser.set_defaults(_cmd=BuildSrc)
|
||||
|
||||
appsign_parser = subparsers.add_parser('appsign', help=AppSign.__doc__)
|
||||
AppSign.setup_arg_parser(appsign_parser)
|
||||
appsign_parser.set_defaults(_cmd=AppSign)
|
||||
if sys.platform == 'darwin':
|
||||
notarize_parser = subparsers.add_parser('notarize', help=Notarize.__doc__)
|
||||
Notarize.setup_arg_parser(notarize_parser)
|
||||
notarize_parser.set_defaults(_cmd=Notarize)
|
||||
|
||||
gpgsign_parser = subparsers.add_parser('gpgsign', help=GPGSign.__doc__)
|
||||
GPGSign.setup_arg_parser(gpgsign_parser)
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ target_link_libraries(${PROGNAME} keepassxc_gui)
|
|||
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(${PROGNAME} PROPERTIES WIN32 ON)
|
||||
set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON)
|
||||
include(GenerateProductVersion)
|
||||
generate_product_version(
|
||||
WIN32_ResourceFiles
|
||||
|
|
@ -442,6 +442,7 @@ if(WIN32)
|
|||
elseif(APPLE AND WITH_APP_BUNDLE)
|
||||
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc)
|
||||
set(MACOSX_BUNDLE_ICON_NAME keepassxc)
|
||||
set(MACOSX_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
|
||||
configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR})
|
||||
set(MACOSX_BUNDLE_RESOURCE_FILES
|
||||
|
|
@ -451,7 +452,7 @@ elseif(APPLE AND WITH_APP_BUNDLE)
|
|||
set_target_properties(${PROGNAME} PROPERTIES
|
||||
MACOSX_BUNDLE ON
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist"
|
||||
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements"
|
||||
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${MACOSX_BUNDLE_APPLE_ENTITLEMENTS}"
|
||||
RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}"
|
||||
)
|
||||
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
|
||||
|
|
@ -461,6 +462,18 @@ elseif(APPLE AND WITH_APP_BUNDLE)
|
|||
DESTINATION "${DATA_INSTALL_DIR}")
|
||||
endif()
|
||||
|
||||
# Sign binaries
|
||||
if(WITH_XC_CODESIGN_IDENTITY)
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY)
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake")
|
||||
if(WITH_XC_NOTARY_KEYCHAIN_PROFILE)
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/MacOSCodesign.cmake.in" "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake" @ONLY)
|
||||
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/MacOSCodesign.cmake")
|
||||
else()
|
||||
message(INFO "Do not forget to notarize DMG package before distribution!")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CPACK_GENERATOR "DragNDrop")
|
||||
set(CPACK_DMG_FORMAT "UDBZ")
|
||||
set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/share/macosx/DS_Store.in")
|
||||
|
|
@ -492,8 +505,9 @@ if(WIN32)
|
|||
"${CMAKE_CURRENT_BINARY_DIR}/INSTALLER_LICENSE.txt")
|
||||
|
||||
# Prepare post-install script and set to run prior to building cpack installers
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsPostInstall.cmake.in" "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake" @ONLY)
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsPostInstall.cmake")
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/WindowsCodesign.cmake.in" "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake" @ONLY)
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
|
||||
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/WindowsCodesign.cmake")
|
||||
|
||||
string(REGEX REPLACE "-.*$" "" KEEPASSXC_VERSION_CLEAN ${KEEPASSXC_VERSION})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue