release-tool 47 KB

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