release-tool 47 KB

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