release-tool 44 KB

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