release-tool 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426
  1. #!/usr/bin/env bash
  2. #
  3. # KeePassXC Release Preparation Helper
  4. # Copyright (C) 2017 KeePassXC team <https://keepassxc.org/>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 2 or (at your option)
  9. # version 3 of the License.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. printf "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper\n"
  19. printf "Copyright (C) 2017 KeePassXC Team <https://keepassxc.org/>\n\n"
  20. # -----------------------------------------------------------------------
  21. # global default values
  22. # -----------------------------------------------------------------------
  23. RELEASE_NAME=""
  24. APP_NAME="KeePassXC"
  25. SRC_DIR="."
  26. GPG_KEY="CFB4C2166397D0D2"
  27. GPG_GIT_KEY=""
  28. OUTPUT_DIR="release"
  29. SOURCE_BRANCH=""
  30. TARGET_BRANCH="master"
  31. TAG_NAME=""
  32. DOCKER_IMAGE=""
  33. DOCKER_CONTAINER_NAME="keepassxc-build-container"
  34. CMAKE_OPTIONS=""
  35. CPACK_GENERATORS="WIX;ZIP"
  36. COMPILER="g++"
  37. MAKE_OPTIONS="-j$(getconf _NPROCESSORS_ONLN)"
  38. BUILD_PLUGINS="all"
  39. INSTALL_PREFIX="/usr/local"
  40. ORIG_BRANCH=""
  41. ORIG_CWD="$(pwd)"
  42. MACOSX_DEPLOYMENT_TARGET=10.13
  43. GREP="grep"
  44. TIMESTAMP_SERVER="http://timestamp.sectigo.com"
  45. # -----------------------------------------------------------------------
  46. # helper functions
  47. # -----------------------------------------------------------------------
  48. printUsage() {
  49. local cmd
  50. if [ "" == "$1" ] || [ "help" == "$1" ]; then
  51. cmd="COMMAND"
  52. elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] \
  53. || [ "gpgsign" == "$1" ] || [ "appsign" == "$1" ] || [ "notarize" == "$1" ] || [ "appimage" == "$1" ]; then
  54. cmd="$1"
  55. else
  56. logError "Unknown command: '$1'\n"
  57. cmd="COMMAND"
  58. fi
  59. printf "\e[1mUsage:\e[0m $(basename $0) $cmd [options]\n"
  60. if [ "COMMAND" == "$cmd" ]; then
  61. cat << EOF
  62. Commands:
  63. check Perform a dry-run check, nothing is changed
  64. merge Merge release branch into main branch and create release tags
  65. build Build and package binary release from sources
  66. gpgsign Sign previously compiled release packages with GPG
  67. appsign Sign binaries with code signing certificates on Windows and macOS
  68. notarize Submit macOS application DMG for notarization
  69. help Show help for the given command
  70. EOF
  71. elif [ "merge" == "$cmd" ]; then
  72. cat << EOF
  73. Merge release branch into main branch and create release tags
  74. Options:
  75. -v, --version Release version number or name (required)
  76. -a, --app-name Application name (default: '${APP_NAME}')
  77. -s, --source-dir Source directory (default: '${SRC_DIR}')
  78. -k, --key GPG key used to sign the merge commit and release tag,
  79. leave empty to let Git choose your default key
  80. (default: '${GPG_GIT_KEY}')
  81. -r, --release-branch Source release branch to merge from (default: 'release/VERSION')
  82. --target-branch Target branch to merge to (default: '${TARGET_BRANCH}')
  83. -t, --tag-name Override release tag name (defaults to version number)
  84. -h, --help Show this help
  85. EOF
  86. elif [ "build" == "$cmd" ]; then
  87. cat << EOF
  88. Build and package binary release from sources
  89. Options:
  90. -v, --version Release version number or name (required)
  91. -a, --app-name Application name (default: '${APP_NAME}')
  92. -s, --source-dir Source directory (default: '${SRC_DIR}')
  93. -o, --output-dir Output directory where to build the release
  94. (default: '${OUTPUT_DIR}')
  95. -t, --tag-name Release tag to check out (defaults to version number)
  96. -b, --build Build sources after exporting release
  97. -d, --docker-image Use the specified Docker image to compile the application.
  98. The image must have all required build dependencies installed.
  99. This option has no effect if --build is not set.
  100. --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
  101. The container must not exist already
  102. --snapcraft Create and use docker image to build snapcraft distribution.
  103. This option has no effect if --docker-image is not set.
  104. --appimage Build a Linux AppImage after compilation.
  105. If this option is set, --install-prefix has no effect
  106. --appsign Perform platform specific App Signing before packaging
  107. --timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
  108. -k, --key Specify the App Signing Key/Identity
  109. -c, --cmake-options Additional CMake options for compiling the sources
  110. --compiler Compiler to use (default: '${COMPILER}')
  111. -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}')
  112. -g, --generators Additional CPack generators (default: '${CPACK_GENERATORS}')
  113. -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}')
  114. -p, --plugins Space-separated list of plugins to build
  115. (default: ${BUILD_PLUGINS})
  116. --snapshot Don't checkout the release tag
  117. -n, --no-source-tarball Don't build source tarball
  118. -h, --help Show this help
  119. EOF
  120. elif [ "gpgsign" == "$cmd" ]; then
  121. cat << EOF
  122. Sign previously compiled release packages with GPG
  123. Options:
  124. -f, --files Files to sign (required)
  125. -k, --key GPG key used to sign the files (default: '${GPG_KEY}')
  126. -h, --help Show this help
  127. EOF
  128. elif [ "appsign" == "$cmd" ]; then
  129. cat << EOF
  130. Sign binaries with code signing certificates on Windows and macOS
  131. Options:
  132. -f, --files Files to sign (required)
  133. -k, --key, -i, --identity
  134. Signing Key or Apple Developer ID (required)
  135. --timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
  136. -u, --username Apple username for notarization (required on macOS)
  137. -h, --help Show this help
  138. EOF
  139. elif [ "notarize" == "$cmd" ]; then
  140. cat << EOF
  141. Submit macOS application DMG for notarization
  142. Options:
  143. -f, --files Files to notarize (required)
  144. -u, --username Apple username for notarization (required)
  145. -c, --keychain Apple keychain entry name storing the notarization
  146. app password (default: 'AC_PASSWORD')
  147. -h, --help Show this help
  148. EOF
  149. elif [ "appimage" == "$cmd" ]; then
  150. cat << EOF
  151. Generate Linux AppImage from 'make install' AppDir
  152. Options:
  153. -a, --appdir Input AppDir (required)
  154. -v, --version KeePassXC version
  155. -o, --output-dir Output directory where to build the AppImage
  156. (default: '${OUTPUT_DIR}')
  157. -d, --docker-image Use the specified Docker image to build the AppImage.
  158. The image must have all required build dependencies installed.
  159. --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
  160. The container must not exist already
  161. --appsign Embed a PGP signature into the AppImage
  162. -k, --key The PGP Signing Key
  163. --verbosity linuxdeploy verbosity (default: 3)
  164. -h, --help Show this help
  165. EOF
  166. fi
  167. }
  168. logInfo() {
  169. printf "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1\n"
  170. }
  171. logWarn() {
  172. printf "\e[1m[ \e[33mWARNING\e[39m ]\e[0m $1\n"
  173. }
  174. logError() {
  175. printf "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1\n" >&2
  176. }
  177. init() {
  178. if [ "" == "$RELEASE_NAME" ]; then
  179. logError "Missing arguments, --version is required!\n"
  180. printUsage "check"
  181. exit 1
  182. fi
  183. if [ "" == "$TAG_NAME" ]; then
  184. TAG_NAME="$RELEASE_NAME"
  185. fi
  186. if [ "" == "$SOURCE_BRANCH" ]; then
  187. SOURCE_BRANCH="release/${RELEASE_NAME}"
  188. fi
  189. ORIG_CWD="$(pwd)"
  190. SRC_DIR="$(realpath "$SRC_DIR")"
  191. cd "$SRC_DIR" > /dev/null 2>&1
  192. ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)"
  193. cd "$ORIG_CWD"
  194. }
  195. cleanup() {
  196. logInfo "Checking out original branch..."
  197. if [ "" != "$ORIG_BRANCH" ]; then
  198. git checkout "$ORIG_BRANCH" > /dev/null 2>&1
  199. fi
  200. logInfo "Leaving source directory..."
  201. cd "$ORIG_CWD"
  202. }
  203. exitError() {
  204. logError "$1"
  205. cleanup
  206. exit 1
  207. }
  208. exitTrap() {
  209. exitError "Existing upon user request..."
  210. }
  211. cmdExists() {
  212. command -v "$1" &> /dev/null
  213. }
  214. checkGrepCompat() {
  215. if ! grep -qPzo test <(echo test) 2> /dev/null; then
  216. if [ -e /usr/local/opt/grep/libexec/gnubin/grep ]; then
  217. GREP="/usr/local/opt/grep/libexec/gnubin/grep"
  218. else
  219. exitError "Incompatible grep implementation! If on macOS, please run 'brew install grep'."
  220. fi
  221. fi
  222. }
  223. checkSourceDirExists() {
  224. if [ ! -d "$SRC_DIR" ]; then
  225. exitError "Source directory '${SRC_DIR}' does not exist!"
  226. fi
  227. }
  228. checkOutputDirDoesNotExist() {
  229. if [ -e "$OUTPUT_DIR" ]; then
  230. exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!"
  231. fi
  232. }
  233. checkGitRepository() {
  234. if [ ! -d .git ] || [ ! -f CHANGELOG.md ]; then
  235. exitError "Source directory is not a valid Git repository!"
  236. fi
  237. }
  238. checkReleaseDoesNotExist() {
  239. git tag | $GREP -q "^$TAG_NAME$"
  240. if [ $? -eq 0 ]; then
  241. exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!"
  242. fi
  243. }
  244. checkWorkingTreeClean() {
  245. git diff-index --quiet HEAD --
  246. if [ $? -ne 0 ]; then
  247. exitError "Current working tree is not clean! Please commit or unstage any changes."
  248. fi
  249. }
  250. checkSourceBranchExists() {
  251. git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1
  252. if [ $? -ne 0 ]; then
  253. exitError "Source branch '$SOURCE_BRANCH' does not exist!"
  254. fi
  255. }
  256. checkTargetBranchExists() {
  257. git rev-parse "$TARGET_BRANCH" > /dev/null 2>&1
  258. if [ $? -ne 0 ]; then
  259. exitError "Target branch '$TARGET_BRANCH' does not exist!"
  260. fi
  261. }
  262. checkVersionInCMake() {
  263. local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')"
  264. local major_num="$(echo ${RELEASE_NAME} | cut -f1 -d.)"
  265. local minor_num="$(echo ${RELEASE_NAME} | cut -f2 -d.)"
  266. local patch_num="$(echo ${RELEASE_NAME} | cut -f3 -d. | cut -f1 -d-)"
  267. $GREP -q "${app_name_upper}_VERSION_MAJOR \"${major_num}\"" CMakeLists.txt
  268. if [ $? -ne 0 ]; then
  269. exitError "${app_name_upper}_VERSION_MAJOR not updated to '${major_num}' in CMakeLists.txt!"
  270. fi
  271. $GREP -q "${app_name_upper}_VERSION_MINOR \"${minor_num}\"" CMakeLists.txt
  272. if [ $? -ne 0 ]; then
  273. exitError "${app_name_upper}_VERSION_MINOR not updated to '${minor_num}' in CMakeLists.txt!"
  274. fi
  275. $GREP -q "${app_name_upper}_VERSION_PATCH \"${patch_num}\"" CMakeLists.txt
  276. if [ $? -ne 0 ]; then
  277. exitError "${app_name_upper}_VERSION_PATCH not updated to '${patch_num}' in CMakeLists.txt!"
  278. fi
  279. }
  280. checkChangeLog() {
  281. if [ ! -f CHANGELOG.md ]; then
  282. exitError "No CHANGELOG file found!"
  283. fi
  284. $GREP -qPzo "## ${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n" CHANGELOG.md
  285. if [ $? -ne 0 ]; then
  286. exitError "'CHANGELOG.md' has not been updated to the '${RELEASE_NAME}' release!"
  287. fi
  288. }
  289. checkAppStreamInfo() {
  290. if [ ! -f share/linux/org.keepassxc.KeePassXC.appdata.xml ]; then
  291. exitError "No AppStream info file found!"
  292. fi
  293. $GREP -qPzo "<release version=\"${RELEASE_NAME}\" date=\"\d{4}-\d{2}-\d{2}\">" share/linux/org.keepassxc.KeePassXC.appdata.xml
  294. if [ $? -ne 0 ]; then
  295. exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!"
  296. fi
  297. }
  298. checkSnapcraft() {
  299. if [ ! -f snap/snapcraft.yaml ]; then
  300. echo "Could not find snap/snapcraft.yaml!"
  301. return
  302. fi
  303. $GREP -qPzo "version: ${RELEASE_NAME}" snap/snapcraft.yaml
  304. if [ $? -ne 0 ]; then
  305. exitError "'snapcraft.yaml' has not been updated to the '${RELEASE_NAME}' release!"
  306. fi
  307. $GREP -qPzo "KEEPASSXC_BUILD_TYPE=Release" snap/snapcraft.yaml
  308. if [ $? -ne 0 ]; then
  309. exitError "'snapcraft.yaml' is not set for a release build!"
  310. fi
  311. }
  312. checkTransifexCommandExists() {
  313. if ! cmdExists tx; then
  314. exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'."
  315. fi
  316. }
  317. checkSigntoolCommandExists() {
  318. if ! cmdExists signtool; then
  319. exitError "signtool command not found on the PATH! Add the Windows SDK binary folder to your PATH."
  320. fi
  321. }
  322. checkXcodeSetup() {
  323. if ! cmdExists xcrun; then
  324. exitError "xcrun command not found on the PATH! Please check that you have correctly installed Xcode."
  325. fi
  326. if ! xcrun -f codesign > /dev/null 2>&1; then
  327. exitError "codesign command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
  328. fi
  329. if ! xcrun -f altool > /dev/null 2>&1; then
  330. exitError "altool command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
  331. fi
  332. if ! xcrun -f stapler > /dev/null 2>&1; then
  333. exitError "stapler command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
  334. fi
  335. }
  336. checkQt5LUpdateExists() {
  337. if cmdExists lupdate && ! $(lupdate -version | $GREP -q "lupdate version 5\."); then
  338. if ! cmdExists lupdate-qt5; then
  339. exitError "Qt Linguist tool (lupdate-qt5) is not installed! Please install using 'apt install qttools5-dev-tools'"
  340. fi
  341. fi
  342. }
  343. performChecks() {
  344. logInfo "Performing basic checks..."
  345. checkGrepCompat
  346. checkSourceDirExists
  347. logInfo "Changing to source directory..."
  348. cd "${SRC_DIR}"
  349. logInfo "Validating toolset and repository..."
  350. checkTransifexCommandExists
  351. checkQt5LUpdateExists
  352. checkGitRepository
  353. checkReleaseDoesNotExist
  354. checkWorkingTreeClean
  355. checkSourceBranchExists
  356. checkTargetBranchExists
  357. logInfo "Checking out '${SOURCE_BRANCH}'..."
  358. git checkout "$SOURCE_BRANCH" > /dev/null 2>&1
  359. logInfo "Attempting to find '${RELEASE_NAME}' in various files..."
  360. checkVersionInCMake
  361. checkChangeLog
  362. checkAppStreamInfo
  363. checkSnapcraft
  364. logInfo "\e[1m\e[32mAll checks passed!\e[0m"
  365. }
  366. # re-implement realpath for OS X (thanks mschrag)
  367. # https://superuser.com/questions/205127/
  368. if ! cmdExists realpath; then
  369. realpath() {
  370. pushd . > /dev/null
  371. if [ -d "$1" ]; then
  372. cd "$1"
  373. dirs -l +0
  374. else
  375. cd "$(dirname "$1")"
  376. cur_dir=$(dirs -l +0)
  377. if [ "$cur_dir" == "/" ]; then
  378. echo "$cur_dir$(basename "$1")"
  379. else
  380. echo "$cur_dir/$(basename "$1")"
  381. fi
  382. fi
  383. popd > /dev/null
  384. }
  385. fi
  386. trap exitTrap SIGINT SIGTERM
  387. # -----------------------------------------------------------------------
  388. # check command
  389. # -----------------------------------------------------------------------
  390. check() {
  391. while [ $# -ge 1 ]; do
  392. local arg="$1"
  393. case "$arg" in
  394. -v|--version)
  395. RELEASE_NAME="$2"
  396. shift ;;
  397. esac
  398. shift
  399. done
  400. init
  401. performChecks
  402. cleanup
  403. logInfo "Congrats! You can successfully merge, build, and sign KeepassXC."
  404. }
  405. # -----------------------------------------------------------------------
  406. # merge command
  407. # -----------------------------------------------------------------------
  408. merge() {
  409. while [ $# -ge 1 ]; do
  410. local arg="$1"
  411. case "$arg" in
  412. -v|--version)
  413. RELEASE_NAME="$2"
  414. shift ;;
  415. -a|--app-name)
  416. APP_NAME="$2"
  417. shift ;;
  418. -s|--source-dir)
  419. SRC_DIR="$2"
  420. shift ;;
  421. -k|--key|-g|--gpg-key)
  422. GPG_GIT_KEY="$2"
  423. shift ;;
  424. --timestamp)
  425. TIMESTAMP_SERVER="$2"
  426. shift ;;
  427. -r|--release-branch)
  428. SOURCE_BRANCH="$2"
  429. shift ;;
  430. --target-branch)
  431. TARGET_BRANCH="$2"
  432. shift ;;
  433. -t|--tag-name)
  434. TAG_NAME="$2"
  435. shift ;;
  436. -h|--help)
  437. printUsage "merge"
  438. exit ;;
  439. *)
  440. logError "Unknown option '$arg'\n"
  441. printUsage "merge"
  442. exit 1 ;;
  443. esac
  444. shift
  445. done
  446. init
  447. performChecks
  448. logInfo "Updating language files..."
  449. ./share/translations/update.sh update
  450. ./share/translations/update.sh pull
  451. if [ 0 -ne $? ]; then
  452. exitError "Updating translations failed!"
  453. fi
  454. git diff-index --quiet HEAD --
  455. if [ $? -ne 0 ]; then
  456. git add -A ./share/translations/
  457. logInfo "Committing changes..."
  458. if [ "" == "$GPG_GIT_KEY" ]; then
  459. git commit -m "Update translations"
  460. else
  461. git commit -m "Update translations" -S"$GPG_GIT_KEY"
  462. fi
  463. fi
  464. CHANGELOG=$($GREP -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n\n)\n?(?:.|\n)+?\n(?=## )" CHANGELOG.md \
  465. | sed 's/^### //' | tr -d \\0)
  466. COMMIT_MSG="Release ${RELEASE_NAME}"
  467. logInfo "Checking out target branch '${TARGET_BRANCH}'..."
  468. git checkout "$TARGET_BRANCH" > /dev/null 2>&1
  469. logInfo "Merging '${SOURCE_BRANCH}' into '${TARGET_BRANCH}'..."
  470. git merge "$SOURCE_BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$SOURCE_BRANCH" -S"$GPG_GIT_KEY"
  471. logInfo "Creating tag '${TAG_NAME}'..."
  472. if [ "" == "$GPG_GIT_KEY" ]; then
  473. git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s
  474. else
  475. git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY"
  476. fi
  477. cleanup
  478. logInfo "All done!"
  479. logInfo "Please merge the release branch back into the develop branch now and then push your changes."
  480. logInfo "Don't forget to also push the tags using \e[1mgit push --tags\e[0m."
  481. }
  482. # -----------------------------------------------------------------------
  483. # appimage command
  484. # -----------------------------------------------------------------------
  485. appimage() {
  486. local appdir
  487. local build_appsign=false
  488. local build_key
  489. local verbosity="1"
  490. while [ $# -ge 1 ]; do
  491. local arg="$1"
  492. case "$arg" in
  493. -v|--version)
  494. RELEASE_NAME="$2"
  495. shift ;;
  496. -a|--appdir)
  497. appdir="$2"
  498. shift ;;
  499. -o|--output-dir)
  500. OUTPUT_DIR="$2"
  501. shift ;;
  502. -d|--docker-image)
  503. DOCKER_IMAGE="$2"
  504. shift ;;
  505. --container-name)
  506. DOCKER_CONTAINER_NAME="$2"
  507. shift ;;
  508. --appsign)
  509. build_appsign=true ;;
  510. --verbosity)
  511. verbosity=$2
  512. shift ;;
  513. -k|--key)
  514. build_key="$2"
  515. shift ;;
  516. -h|--help)
  517. printUsage "appimage"
  518. exit ;;
  519. *)
  520. logError "Unknown option '$arg'\n"
  521. printUsage "appimage"
  522. exit 1 ;;
  523. esac
  524. shift
  525. done
  526. if [ -z "${appdir}" ]; then
  527. logError "Missing arguments, --appdir is required!\n"
  528. printUsage "appimage"
  529. exit 1
  530. fi
  531. if [ ! -d "${appdir}" ]; then
  532. exitError "AppDir does not exist, please create one with 'make install'!"
  533. elif [ -e "${appdir}/AppRun" ]; then
  534. exitError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'."
  535. fi
  536. appdir="$(realpath "$appdir")"
  537. local out="${OUTPUT_DIR}"
  538. if [ "" == "$out" ]; then
  539. out="."
  540. fi
  541. mkdir -p "$out"
  542. local out_real="$(realpath "$out")"
  543. cd "$out"
  544. local linuxdeploy="linuxdeploy"
  545. local linuxdeploy_cleanup
  546. local linuxdeploy_plugin_qt="linuxdeploy-plugin-qt"
  547. local linuxdeploy_plugin_qt_cleanup
  548. local appimagetool="appimagetool"
  549. local appimagetool_cleanup
  550. logInfo "Testing for AppImage tools..."
  551. local docker_test_cmd
  552. if [ "" != "$DOCKER_IMAGE" ]; then
  553. docker_test_cmd="docker run --rm ${DOCKER_IMAGE}"
  554. fi
  555. # Test if linuxdeploy and linuxdeploy-plugin-qt are installed
  556. # on the system or inside the Docker container
  557. if ! ${docker_test_cmd} which ${linuxdeploy} &> /dev/null; then
  558. logInfo "Downloading linuxdeploy..."
  559. linuxdeploy="./linuxdeploy"
  560. linuxdeploy_cleanup="rm -f ${linuxdeploy}"
  561. if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy"; then
  562. exitError "linuxdeploy download failed."
  563. fi
  564. chmod +x "$linuxdeploy"
  565. fi
  566. if ! ${docker_test_cmd} which ${linuxdeploy_plugin_qt} &> /dev/null; then
  567. logInfo "Downloading linuxdeploy-plugin-qt..."
  568. linuxdeploy_plugin_qt="./linuxdeploy-plugin-qt"
  569. linuxdeploy_plugin_qt_cleanup="rm -f ${linuxdeploy_plugin_qt}"
  570. if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt"; then
  571. exitError "linuxdeploy-plugin-qt download failed."
  572. fi
  573. chmod +x "$linuxdeploy_plugin_qt"
  574. fi
  575. # appimagetool is always run outside a Docker container, so we can access our GPG keys
  576. if ! cmdExists ${appimagetool}; then
  577. logInfo "Downloading appimagetool..."
  578. appimagetool="./appimagetool"
  579. appimagetool_cleanup="rm -f ${appimagetool}"
  580. if ! curl -Lf "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool"; then
  581. exitError "appimagetool download failed."
  582. fi
  583. chmod +x "$appimagetool"
  584. fi
  585. # Create custom AppRun wrapper
  586. cat << EOF > "${out_real}/KeePassXC-AppRun"
  587. #!/usr/bin/env bash
  588. export PATH="\$(dirname \$0)/usr/bin:\${PATH}"
  589. export LD_LIBRARY_PATH="\$(dirname \$0)/usr/lib:\${LD_LIBRARY_PATH}"
  590. if [ "\${1}" == "cli" ]; then
  591. shift
  592. exec keepassxc-cli "\$@"
  593. elif [ "\${1}" == "proxy" ]; then
  594. shift
  595. exec keepassxc-proxy "\$@"
  596. elif [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
  597. exec keepassxc-proxy "\$@"
  598. else
  599. exec keepassxc "\$@"
  600. fi
  601. EOF
  602. chmod +x "${out_real}/KeePassXC-AppRun"
  603. # Find .desktop files, icons, and binaries to deploy
  604. local desktop_file="$(find "$appdir" -name "org.keepassxc.KeePassXC.desktop" | head -n1)"
  605. local icon="$(find "$appdir" -name 'keepassxc.png' | $GREP -P 'application/256x256/apps/keepassxc.png$' | head -n1)"
  606. local executables="$(IFS=$'\n' find "$appdir" | $GREP -P '/usr/bin/keepassxc[^/]*$' | xargs -i printf " --executable={}")"
  607. logInfo "Collecting libs and patching binaries..."
  608. if [ "" == "$DOCKER_IMAGE" ]; then
  609. "$linuxdeploy" --verbosity=${verbosity} --plugin=qt --appdir="$appdir" --desktop-file="$desktop_file" \
  610. --custom-apprun="${out_real}/KeePassXC-AppRun" --icon-file="$icon" ${executables} \
  611. --library=$(ldconfig -p | $GREP x86-64 | $GREP -oP '/[^\s]+/libgpg-error\.so\.\d+$' | head -n1)
  612. else
  613. desktop_file="${desktop_file//${appdir}/\/keepassxc\/AppDir}"
  614. icon="${icon//${appdir}/\/keepassxc\/AppDir}"
  615. executables="${executables//${appdir}/\/keepassxc\/AppDir}"
  616. docker run --name "$DOCKER_CONTAINER_NAME" --rm \
  617. --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
  618. -v "${appdir}:/keepassxc/AppDir:rw" \
  619. -v "${out_real}:/keepassxc/out:rw" \
  620. "$DOCKER_IMAGE" \
  621. bash -c "cd /keepassxc/out && ${linuxdeploy} --verbosity=${verbosity} --plugin=qt --appdir=/keepassxc/AppDir \
  622. --custom-apprun="/keepassxc/out/KeePassXC-AppRun" --desktop-file=${desktop_file} --icon-file=${icon} ${executables} \
  623. --library=\$(ldconfig -p | grep x86-64 | grep -oP '/[^\s]+/libgpg-error\.so\.\d+$' | head -n1)"
  624. fi
  625. if [ $? -ne 0 ]; then
  626. exitError "AppDir deployment failed."
  627. fi
  628. logInfo "Creating AppImage..."
  629. local appsign_flag=""
  630. local appsign_key_flag=""
  631. if ${build_appsign}; then
  632. appsign_flag="--sign"
  633. appsign_key_flag="--sign-key ${build_key}"
  634. fi
  635. local appimage_name="KeePassXC-x86_64.AppImage"
  636. if [ "" != "$RELEASE_NAME" ]; then
  637. appimage_name="KeePassXC-${RELEASE_NAME}-x86_64.AppImage"
  638. echo "X-AppImage-Version=${RELEASE_NAME}" >> "$desktop_file"
  639. fi
  640. # Run appimagetool to package (and possibly sign) AppImage
  641. # --no-appstream is required, since it may crash on newer systems
  642. # see: https://github.com/AppImage/AppImageKit/issues/856
  643. if ! "$appimagetool" --updateinformation "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-x86_64.AppImage.zsync" \
  644. ${appsign_flag} ${appsign_key_flag} --no-appstream "$appdir" "${out_real}/${appimage_name}"; then
  645. exitError "AppImage creation failed."
  646. fi
  647. logInfo "Cleaning up temporary files..."
  648. ${linuxdeploy_cleanup}
  649. ${linuxdeploy_plugin_qt_cleanup}
  650. ${appimagetool_cleanup}
  651. rm -f "${out_real}/KeePassXC-AppRun"
  652. }
  653. # -----------------------------------------------------------------------
  654. # build command
  655. # -----------------------------------------------------------------------
  656. build() {
  657. local build_source_tarball=true
  658. local build_snapshot=false
  659. local build_snapcraft=false
  660. local build_appimage=false
  661. local build_generators=""
  662. local build_appsign=false
  663. local build_key=""
  664. while [ $# -ge 1 ]; do
  665. local arg="$1"
  666. case "$arg" in
  667. -v|--version)
  668. RELEASE_NAME="$2"
  669. shift ;;
  670. -a|--app-name)
  671. APP_NAME="$2"
  672. shift ;;
  673. -s|--source-dir)
  674. SRC_DIR="$2"
  675. shift ;;
  676. -o|--output-dir)
  677. OUTPUT_DIR="$2"
  678. shift ;;
  679. -t|--tag-name)
  680. TAG_NAME="$2"
  681. shift ;;
  682. -d|--docker-image)
  683. DOCKER_IMAGE="$2"
  684. shift ;;
  685. --container-name)
  686. DOCKER_CONTAINER_NAME="$2"
  687. shift ;;
  688. --appsign)
  689. build_appsign=true ;;
  690. --timestamp)
  691. TIMESTAMP_SERVER="$2"
  692. shift ;;
  693. -k|--key)
  694. build_key="$2"
  695. shift ;;
  696. --snapcraft)
  697. build_snapcraft=true ;;
  698. --appimage)
  699. build_appimage=true ;;
  700. -c|--cmake-options)
  701. CMAKE_OPTIONS="$2"
  702. shift ;;
  703. --compiler)
  704. COMPILER="$2"
  705. shift ;;
  706. -m|--make-options)
  707. MAKE_OPTIONS="$2"
  708. shift ;;
  709. -g|--generators)
  710. build_generators="$2"
  711. shift ;;
  712. -i|--install-prefix)
  713. INSTALL_PREFIX="$2"
  714. shift ;;
  715. -p|--plugins)
  716. BUILD_PLUGINS="$2"
  717. shift ;;
  718. -n|--no-source-tarball)
  719. build_source_tarball=false ;;
  720. --snapshot)
  721. build_snapshot=true ;;
  722. -h|--help)
  723. printUsage "build"
  724. exit ;;
  725. *)
  726. logError "Unknown option '$arg'\n"
  727. printUsage "build"
  728. exit 1 ;;
  729. esac
  730. shift
  731. done
  732. init
  733. OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
  734. # Resolve appsign key to absolute path if under Windows
  735. if [[ "${build_key}" && "$(uname -o)" == "Msys" ]]; then
  736. build_key="$(realpath "${build_key}")"
  737. fi
  738. if ${build_snapshot}; then
  739. TAG_NAME="HEAD"
  740. local branch=`git rev-parse --abbrev-ref HEAD`
  741. logInfo "Using current branch ${branch} to build..."
  742. RELEASE_NAME="${RELEASE_NAME}-snapshot"
  743. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot -DOVERRIDE_VERSION=${RELEASE_NAME}"
  744. else
  745. checkGrepCompat
  746. checkWorkingTreeClean
  747. if $(echo "$TAG_NAME" | $GREP -qP "\-(alpha|beta)\\d+\$"); then
  748. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=PreRelease"
  749. logInfo "Checking out pre-release tag '${TAG_NAME}'..."
  750. else
  751. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Release"
  752. logInfo "Checking out release tag '${TAG_NAME}'..."
  753. fi
  754. git checkout "$TAG_NAME" > /dev/null 2>&1
  755. fi
  756. logInfo "Creating output directory..."
  757. mkdir -p "$OUTPUT_DIR"
  758. if [ $? -ne 0 ]; then
  759. exitError "Failed to create output directory!"
  760. fi
  761. if ${build_source_tarball}; then
  762. logInfo "Creating source tarball..."
  763. local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')"
  764. local prefix="${app_name_lower}-${RELEASE_NAME}"
  765. local tarball_name="${prefix}-src.tar"
  766. git archive --format=tar "$TAG_NAME" --prefix="${prefix}/" --output="${OUTPUT_DIR}/${tarball_name}"
  767. # add .version and .gitrev files to tarball
  768. mkdir "${prefix}"
  769. echo -n ${RELEASE_NAME} > "${prefix}/.version"
  770. echo -n `git rev-parse --short=7 HEAD` > "${prefix}/.gitrev"
  771. tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version" "${prefix}/.gitrev"
  772. rm "${prefix}/.version" "${prefix}/.gitrev"
  773. rmdir "${prefix}" 2> /dev/null
  774. local xz="xz"
  775. if ! cmdExists xz; then
  776. logWarn "xz not installed. Falling back to bz2..."
  777. xz="bzip2"
  778. fi
  779. $xz -6 "${OUTPUT_DIR}/${tarball_name}"
  780. fi
  781. if ! ${build_snapshot} && [ -e "${OUTPUT_DIR}/build-release" ]; then
  782. logInfo "Cleaning existing build directory..."
  783. rm -rf "${OUTPUT_DIR}/build-release" 2> /dev/null
  784. if [ $? -ne 0 ]; then
  785. exitError "Failed to clean existing build directory, please do it manually."
  786. fi
  787. fi
  788. logInfo "Creating build directory..."
  789. mkdir -p "${OUTPUT_DIR}/build-release"
  790. cd "${OUTPUT_DIR}/build-release"
  791. logInfo "Configuring sources..."
  792. for p in ${BUILD_PLUGINS}; do
  793. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On"
  794. done
  795. if [ "$(uname -o 2> /dev/null)" == "GNU/Linux" ] && ${build_appimage}; then
  796. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_DIST_TYPE=AppImage"
  797. # linuxdeploy requires /usr as install prefix
  798. INSTALL_PREFIX="/usr"
  799. fi
  800. # Do not build tests cases
  801. CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_TESTS=OFF"
  802. if [ "$COMPILER" == "g++" ]; then
  803. export CC=gcc
  804. elif [ "$COMPILER" == "clang++" ]; then
  805. export CC=clang
  806. fi
  807. export CXX="$COMPILER"
  808. if [ "" == "$DOCKER_IMAGE" ]; then
  809. if [ "$(uname -s)" == "Darwin" ]; then
  810. # Building on macOS
  811. export MACOSX_DEPLOYMENT_TARGET
  812. logInfo "Configuring build..."
  813. cmake -DCMAKE_BUILD_TYPE=Release \
  814. -DCMAKE_OSX_ARCHITECTURES="$(uname -m)" -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \
  815. -DCMAKE_PREFIX_PATH="/opt/homebrew/opt/qt/lib/cmake;/usr/local/opt/qt/lib/cmake" \
  816. ${CMAKE_OPTIONS} "$SRC_DIR"
  817. logInfo "Compiling and packaging sources..."
  818. make ${MAKE_OPTIONS} package
  819. # Appsign the executables if desired
  820. if ${build_appsign}; then
  821. logInfo "Signing executable files"
  822. appsign "-f" "./${APP_NAME}-${RELEASE_NAME}.dmg" "-k" "${build_key}"
  823. fi
  824. mv "./${APP_NAME}-${RELEASE_NAME}.dmg" "../${APP_NAME}-${RELEASE_NAME}-$(uname -m).dmg"
  825. elif [ "$(uname -o)" == "Msys" ]; then
  826. # Building on Windows with Msys2
  827. logInfo "Configuring build..."
  828. cmake -DCMAKE_BUILD_TYPE=Release -G"MSYS Makefiles" \
  829. -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" ${CMAKE_OPTIONS} "$SRC_DIR"
  830. logInfo "Compiling and packaging sources..."
  831. mingw32-make ${MAKE_OPTIONS} preinstall
  832. # Appsign the executables if desired
  833. if ${build_appsign} && [ -f "${build_key}" ]; then
  834. logInfo "Signing executable files"
  835. appsign "-f" $(find src | $GREP -P '\.exe$|\.dll$') "-k" "${build_key}"
  836. fi
  837. # Call cpack directly instead of calling make package.
  838. # This is important because we want to build the MSI when making a
  839. # release.
  840. cpack -G "${CPACK_GENERATORS};${build_generators}"
  841. # Inject the portable config into the zip build and rename
  842. touch .portable
  843. for filename in ${APP_NAME}-*.zip; do
  844. logInfo "Creating portable zip file"
  845. local folder=$(echo ${filename} | sed -r 's/(.*)\.zip/\1/')
  846. python -c 'import zipfile,sys ; zipfile.ZipFile(sys.argv[1],"a").write(sys.argv[2],sys.argv[3])' \
  847. ${filename} .portable ${folder}/.portable
  848. mv ${filename} ${folder}-portable.zip
  849. done
  850. rm .portable
  851. mv "${APP_NAME}-"*.* ../
  852. else
  853. mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir"
  854. # Building on Linux without Docker container
  855. logInfo "Configuring build..."
  856. cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
  857. -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR"
  858. logInfo "Compiling sources..."
  859. make ${MAKE_OPTIONS}
  860. logInfo "Installing to bin dir..."
  861. make DESTDIR="${OUTPUT_DIR}/KeePassXC.AppDir" install/strip
  862. fi
  863. else
  864. if ${build_snapcraft}; then
  865. logInfo "Building snapcraft docker image..."
  866. sudo docker image build -t "$DOCKER_IMAGE" "$(realpath "$SRC_DIR")/ci/snapcraft"
  867. logInfo "Launching Docker contain to compile snapcraft..."
  868. sudo docker run --name "$DOCKER_CONTAINER_NAME" --rm \
  869. -v "$(realpath "$SRC_DIR"):/keepassxc" -w "/keepassxc" \
  870. "$DOCKER_IMAGE" snapcraft
  871. else
  872. mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir"
  873. logInfo "Launching Docker container to compile sources..."
  874. docker run --name "$DOCKER_CONTAINER_NAME" --rm \
  875. --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
  876. -e "CC=${CC}" -e "CXX=${CXX}" \
  877. -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \
  878. -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \
  879. "$DOCKER_IMAGE" \
  880. bash -c "cd /keepassxc/out/build-release && \
  881. cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
  882. -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} /keepassxc/src && \
  883. make ${MAKE_OPTIONS} && make DESTDIR=/keepassxc/out/KeePassXC.AppDir install/strip"
  884. fi
  885. if [ 0 -ne $? ]; then
  886. exitError "Docker build failed!"
  887. fi
  888. logInfo "Build finished, Docker container terminated."
  889. fi
  890. if [ "$(uname -o 2> /dev/null)" == "GNU/Linux" ] && ${build_appimage}; then
  891. local appsign_flag=""
  892. local appsign_key_flag=""
  893. local docker_image_flag=""
  894. local docker_container_name_flag=""
  895. if ${build_appsign}; then
  896. appsign_flag="--appsign"
  897. appsign_key_flag="-k ${build_key}"
  898. fi
  899. if [ "" != "${DOCKER_IMAGE}" ]; then
  900. docker_image_flag="-d ${DOCKER_IMAGE}"
  901. docker_container_name_flag="--container-name ${DOCKER_CONTAINER_NAME}"
  902. fi
  903. appimage "-a" "${OUTPUT_DIR}/KeePassXC.AppDir" "-o" "${OUTPUT_DIR}" \
  904. ${appsign_flag} ${appsign_key_flag} ${docker_image_flag} ${docker_container_name_flag}
  905. fi
  906. cleanup
  907. logInfo "All done!"
  908. }
  909. # -----------------------------------------------------------------------
  910. # gpgsign command
  911. # -----------------------------------------------------------------------
  912. gpgsign() {
  913. local sign_files=()
  914. while [ $# -ge 1 ]; do
  915. local arg="$1"
  916. case "$arg" in
  917. -f|--files)
  918. while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
  919. sign_files+=("$2")
  920. shift
  921. done ;;
  922. -k|--key|-g|--gpg-key)
  923. GPG_KEY="$2"
  924. shift ;;
  925. -h|--help)
  926. printUsage "gpgsign"
  927. exit ;;
  928. *)
  929. logError "Unknown option '$arg'\n"
  930. printUsage "gpgsign"
  931. exit 1 ;;
  932. esac
  933. shift
  934. done
  935. if [ -z "${sign_files}" ]; then
  936. logError "Missing arguments, --files is required!\n"
  937. printUsage "gpgsign"
  938. exit 1
  939. fi
  940. for f in "${sign_files[@]}"; do
  941. if [ ! -f "$f" ]; then
  942. exitError "File '${f}' does not exist or is not a file!"
  943. fi
  944. logInfo "Signing file '${f}' using release key..."
  945. gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f"
  946. if [ 0 -ne $? ]; then
  947. exitError "Signing failed!"
  948. fi
  949. logInfo "Creating digest for file '${f}'..."
  950. local rp="$(realpath "$f")"
  951. local bname="$(basename "$f")"
  952. (cd "$(dirname "$rp")"; sha256sum "$bname" > "${bname}.DIGEST")
  953. done
  954. logInfo "All done!"
  955. }
  956. # -----------------------------------------------------------------------
  957. # appsign command
  958. # -----------------------------------------------------------------------
  959. appsign() {
  960. local sign_files=()
  961. local key
  962. while [ $# -ge 1 ]; do
  963. local arg="$1"
  964. case "$arg" in
  965. -f|--files)
  966. while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
  967. sign_files+=("$2")
  968. shift
  969. done ;;
  970. -k|--key|-i|--identity)
  971. key="$2"
  972. shift ;;
  973. -h|--help)
  974. printUsage "appsign"
  975. exit ;;
  976. *)
  977. logError "Unknown option '$arg'\n"
  978. printUsage "appsign"
  979. exit 1 ;;
  980. esac
  981. shift
  982. done
  983. if [ -z "${key}" ]; then
  984. logError "Missing arguments, --key is required!\n"
  985. printUsage "appsign"
  986. exit 1
  987. fi
  988. if [ -z "${sign_files}" ]; then
  989. logError "Missing arguments, --files is required!\n"
  990. printUsage "appsign"
  991. exit 1
  992. fi
  993. for f in "${sign_files[@]}"; do
  994. if [ ! -e "${f}" ]; then
  995. exitError "File '${f}' does not exist!"
  996. fi
  997. done
  998. if [ "$(uname -s)" == "Darwin" ]; then
  999. checkXcodeSetup
  1000. checkGrepCompat
  1001. local orig_dir="$(pwd)"
  1002. local real_src_dir="$(realpath "${SRC_DIR}")"
  1003. for f in "${sign_files[@]}"; do
  1004. if [[ ${f: -4} == '.dmg' ]]; then
  1005. logInfo "Unpacking disk image '${f}'..."
  1006. local tmp_dir="/tmp/KeePassXC_${RANDOM}"
  1007. mkdir -p ${tmp_dir}/mnt
  1008. if ! hdiutil attach -quiet -noautoopen -mountpoint ${tmp_dir}/mnt "${f}"; then
  1009. exitError "DMG mount failed!"
  1010. fi
  1011. cd ${tmp_dir}
  1012. cp -a ./mnt ./app
  1013. hdiutil detach -quiet ${tmp_dir}/mnt
  1014. local app_dir_tmp="./app/KeePassXC.app"
  1015. if [ ! -d "$app_dir_tmp" ]; then
  1016. cd "${orig_dir}"
  1017. exitError "Unpacking failed!"
  1018. fi
  1019. elif [[ ${f: -4} == '.app' ]]; then
  1020. local app_dir_tmp="$f"
  1021. else
  1022. logWarn "Skipping non-app file '${f}'..."
  1023. continue
  1024. fi
  1025. logInfo "Signing libraries and frameworks..."
  1026. if ! find "$app_dir_tmp" \( -name '*.dylib' -o -name '*.so' -o -name '*.framework' \) -print0 | xargs -0 \
  1027. xcrun codesign --sign "${key}" --verbose --force --options runtime; then
  1028. cd "${orig_dir}"
  1029. exitError "Signing failed!"
  1030. fi
  1031. logInfo "Signing executables..."
  1032. if ! find "${app_dir_tmp}/Contents/MacOS" \( -type f -not -name KeePassXC \) -print0 | xargs -0 \
  1033. xcrun codesign --sign "${key}" --verbose --force --options runtime; then
  1034. cd "${orig_dir}"
  1035. exitError "Signing failed!"
  1036. fi
  1037. # Sign main executable with additional entitlements
  1038. if ! xcrun codesign --sign "${key}" --verbose --force --options runtime --entitlements \
  1039. "${real_src_dir}/share/macosx/keepassxc.entitlements" "${app_dir_tmp}/Contents/MacOS/KeePassXC"; then
  1040. cd "${orig_dir}"
  1041. exitError "Signing failed!"
  1042. fi
  1043. if [[ ${f: -4} == '.dmg' ]]; then
  1044. logInfo "Repacking disk image..."
  1045. hdiutil create \
  1046. -volname "KeePassXC" \
  1047. -size $((1000 * ($(du -sk ./app | cut -f1) + 5000))) \
  1048. -srcfolder ./app \
  1049. -fs HFS+ \
  1050. -fsargs "-c c=64,a=16,e=16" \
  1051. -format UDBZ \
  1052. "${tmp_dir}/$(basename "${f}")"
  1053. cd "${orig_dir}"
  1054. cp -f "${tmp_dir}/$(basename "${f}")" "${f}"
  1055. rm -Rf ${tmp_dir}
  1056. fi
  1057. logInfo "File '${f}' successfully signed."
  1058. done
  1059. elif [ "$(uname -o)" == "Msys" ]; then
  1060. if [[ ! -f "${key}" ]]; then
  1061. exitError "Key file was not found!"
  1062. fi
  1063. read -s -p "Key password: " password
  1064. echo
  1065. for f in "${sign_files[@]}"; do
  1066. ext=${f: -4}
  1067. if [[ $ext == ".msi" || $ext == ".exe" || $ext == ".dll" ]]; then
  1068. # Make sure we can find the signtool
  1069. checkSigntoolCommandExists
  1070. # osslsigncode does not succeed at signing MSI files at this time...
  1071. logInfo "Signing file '${f}' using Microsoft signtool..."
  1072. signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \
  1073. -fd sha256 -tr "${TIMESTAMP_SERVER}" "${f}"
  1074. if [ 0 -ne $? ]; then
  1075. exitError "Signing failed!"
  1076. fi
  1077. else
  1078. logInfo "Skipping non-executable file '${f}'..."
  1079. fi
  1080. done
  1081. else
  1082. exitError "Unsupported platform for code signing!\n"
  1083. fi
  1084. logInfo "All done!"
  1085. }
  1086. # -----------------------------------------------------------------------
  1087. # notarize command
  1088. # -----------------------------------------------------------------------
  1089. notarize() {
  1090. local notarize_files=()
  1091. local ac_username
  1092. local ac_keychain="AC_PASSWORD"
  1093. while [ $# -ge 1 ]; do
  1094. local arg="$1"
  1095. case "$arg" in
  1096. -f|--files)
  1097. while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
  1098. notarize_files+=("$2")
  1099. shift
  1100. done ;;
  1101. -u|--username)
  1102. ac_username="$2"
  1103. shift ;;
  1104. -c|--keychain)
  1105. ac_keychain="$2"
  1106. shift ;;
  1107. -h|--help)
  1108. printUsage "notarize"
  1109. exit ;;
  1110. *)
  1111. logError "Unknown option '$arg'\n"
  1112. printUsage "notarize"
  1113. exit 1 ;;
  1114. esac
  1115. shift
  1116. done
  1117. if [ "$(uname -s)" != "Darwin" ]; then
  1118. exitError "Notarization is only supported on macOS!"
  1119. fi
  1120. if [ -z "${notarize_files}" ]; then
  1121. logError "Missing arguments, --files is required!\n"
  1122. printUsage "notarize"
  1123. exit 1
  1124. fi
  1125. if [ "$ac_username" == "" ]; then
  1126. logError "Missing arguments, --username is required!"
  1127. printUsage "notarize"
  1128. exit 1
  1129. fi
  1130. for f in "${notarize_files[@]}"; do
  1131. if [[ ${f: -4} != '.dmg' ]]; then
  1132. logWarn "Skipping non-DMG file '${f}'..."
  1133. continue
  1134. fi
  1135. logInfo "Submitting disk image '${f}' for notarization..."
  1136. local status
  1137. status="$(xcrun altool --notarize-app \
  1138. --primary-bundle-id "org.keepassxc.keepassxc" \
  1139. --username "${ac_username}" \
  1140. --password "@keychain:${ac_keychain}" \
  1141. --file "${f}" 2> /dev/null)"
  1142. if [ 0 -ne $? ]; then
  1143. logError "Submission failed!"
  1144. exitError "Error message:\n${status}"
  1145. fi
  1146. local ticket="$(echo "${status}" | $GREP -oP "[a-f0-9-]+$")"
  1147. logInfo "Submission successful. Ticket ID: ${ticket}."
  1148. logInfo "Waiting for notarization to finish (this may take a while)..."
  1149. while true; do
  1150. echo -n "."
  1151. status="$(xcrun altool --notarization-info "${ticket}" \
  1152. --username "${ac_username}" \
  1153. --password "@keychain:${ac_keychain}" 2> /dev/null)"
  1154. if echo "$status" | $GREP -q "Status Code: 0"; then
  1155. logInfo "\nNotarization successful."
  1156. break
  1157. elif echo "$status" | $GREP -q "Status Code"; then
  1158. logError "\nNotarization failed!"
  1159. exitError "Error message:\n${status}"
  1160. fi
  1161. sleep 5
  1162. done
  1163. logInfo "Stapling ticket to disk image..."
  1164. xcrun stapler staple "${f}"
  1165. if [ 0 -ne $? ]; then
  1166. exitError "Stapling failed!"
  1167. fi
  1168. logInfo "Disk image successfully notarized."
  1169. done
  1170. }
  1171. # -----------------------------------------------------------------------
  1172. # parse global command line
  1173. # -----------------------------------------------------------------------
  1174. MODE="$1"
  1175. shift
  1176. if [ "" == "$MODE" ]; then
  1177. logError "Missing arguments!\n"
  1178. printUsage
  1179. exit 1
  1180. elif [ "help" == "$MODE" ]; then
  1181. printUsage "$1"
  1182. exit
  1183. elif [ "check" == "$MODE" ] || [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] \
  1184. || [ "gpgsign" == "$MODE" ] || [ "appsign" == "$MODE" ]|| [ "notarize" == "$MODE" ] \
  1185. || [ "appimage" == "$MODE" ]; then
  1186. ${MODE} "$@"
  1187. else
  1188. printUsage "$MODE"
  1189. fi