From 30fdffae03be85896238cd8a8b040eaa50dbcb53 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 23 Sep 2023 15:43:12 +1000 Subject: [PATCH] Updater: Mac support --- .github/workflows/rolling-release.yml | 16 ++ README.md | 1 - src/CMakeLists.txt | 2 +- src/duckstation-qt/autoupdaterdialog.cpp | 85 +++++++- src/updater/CMakeLists.txt | 26 +++ src/updater/Info.plist.in | 24 +++ src/updater/Updater.icns | Bin 0 -> 8521 bytes src/updater/cocoa_main.mm | 135 +++++++++++++ src/updater/cocoa_progress_callback.h | 64 ++++++ src/updater/cocoa_progress_callback.mm | 244 +++++++++++++++++++++++ src/updater/updater.cpp | 118 +++++++++-- src/updater/updater.h | 10 +- src/updater/win32_main.cpp | 14 +- 13 files changed, 703 insertions(+), 36 deletions(-) create mode 100644 src/updater/Info.plist.in create mode 100644 src/updater/Updater.icns create mode 100644 src/updater/cocoa_main.mm create mode 100644 src/updater/cocoa_progress_callback.h create mode 100644 src/updater/cocoa_progress_callback.mm diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 336f648b9..538ab5e12 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -334,6 +334,22 @@ jobs: if: steps.cache-deps-mac.outputs.cache-hit != 'true' run: scripts/build-dependencies-mac.sh + - name: Tag as preview build + if: github.ref == 'refs/heads/master' + run: | + echo '#pragma once' > src/scmversion/tag.h + echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h + echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h + echo '#define SCM_RELEASE_TAG "preview"' >> src/scmversion/tag.h + + - name: Tag as dev build + if: github.ref == 'refs/heads/dev' + run: | + echo '#pragma once' > src/scmversion/tag.h + echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h + echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h + echo '#define SCM_RELEASE_TAG "latest"' >> src/scmversion/tag.h + - name: Compile and zip .app shell: bash run: | diff --git a/README.md b/README.md index 5bbe19f22..c2c06cc9e 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,6 @@ Requirements (Debian/Ubuntu package names): 5. Run the binary, located in the build directory under `bin/duckstation-qt`. ### macOS -**NOTE:** macOS is highly experimental and not tested by the developer. Use at your own risk; things may be horribly broken. Vulkan support may be unstable, so sticking to OpenGL or software renderer is recommended. Requirements: - CMake diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b130b827..879d46bc7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,7 +3,7 @@ add_subdirectory(util) add_subdirectory(core) add_subdirectory(scmversion) -if(WIN32) +if(WIN32 OR APPLE) add_subdirectory(updater) endif() diff --git a/src/duckstation-qt/autoupdaterdialog.cpp b/src/duckstation-qt/autoupdaterdialog.cpp index 3d5e96356..04bb138a3 100644 --- a/src/duckstation-qt/autoupdaterdialog.cpp +++ b/src/duckstation-qt/autoupdaterdialog.cpp @@ -11,6 +11,7 @@ #include "common/file_system.h" #include "common/log.h" #include "common/minizip_helpers.h" +#include "common/path.h" #include "common/string_util.h" #include @@ -28,18 +29,18 @@ #include #include -Log_SetChannel(AutoUpdaterDialog); +#ifdef __APPLE__ +#include "common/cocoa_tools.h" +#endif // Logic to detect whether we can use the auto updater. -// Currently Windows and Linux-only, and requires that the channel be defined by the buildbot. -#if defined(_WIN32) || defined(__linux__) +// Requires that the channel be defined by the buildbot. #if defined(__has_include) && __has_include("scmversion/tag.h") #include "scmversion/tag.h" #ifdef SCM_RELEASE_TAGS #define AUTO_UPDATER_SUPPORTED #endif #endif -#endif #ifdef AUTO_UPDATER_SUPPORTED @@ -52,6 +53,8 @@ static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG; #endif +Log_SetChannel(AutoUpdaterDialog); + AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent /* = nullptr */) : QDialog(parent), m_host_interface(host_interface) { @@ -81,7 +84,7 @@ bool AutoUpdaterDialog::isSupported() return true; #else - // Windows - always supported. + // Windows/Mac - always supported. return true; #endif #else @@ -582,6 +585,78 @@ void AutoUpdaterDialog::cleanupAfterUpdate() { } +#elif defined(__APPLE__) + +bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +{ + std::optional bundle_path = CocoaTools::GetNonTranslocatedBundlePath(); + if (!bundle_path.has_value()) + { + reportError("Couldn't obtain non-translocated bundle path."); + return false; + } + + QFileInfo info(QString::fromStdString(bundle_path.value())); + if (!info.isBundle()) + { + reportError("Application %s isn't a bundle.", bundle_path->c_str()); + return false; + } + if (info.suffix() != QStringLiteral("app")) + { + reportError("Unexpected application suffix %s on %s.", info.suffix().toUtf8().constData(), bundle_path->c_str()); + return false; + } + + // Use the updater from this version to unpack the new version. + const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app"); + if (!FileSystem::DirectoryExists(updater_app.c_str())) + { + reportError("Failed to find updater at %s.", updater_app.c_str()); + return false; + } + + // We use the user data directory to temporarily store the update zip. + const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip"); + const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING"); + if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str())) + { + reportError("Failed to remove old update zip."); + return false; + } + + // Save update. + { + QFile zip_file(QString::fromStdString(zip_path)); + if (!zip_file.open(QIODevice::WriteOnly) || zip_file.write(update_data) != update_data.size()) + { + reportError("Writing update zip to '%s' failed", zip_path.c_str()); + return false; + } + zip_file.close(); + } + + Log_InfoFmt("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}", + updater_app, zip_path, staging_directory, bundle_path.value()); + + const std::string_view args[] = { + zip_path, + staging_directory, + bundle_path.value(), + }; + + // Kick off updater! + CocoaTools::DelayedLaunch(updater_app, args); + return true; +} + +void AutoUpdaterDialog::cleanupAfterUpdate() +{ + const QString zip_path = QString::fromStdString(Path::Combine(EmuFolders::DataRoot, "update.zip")); + if (QFile::exists(zip_path)) + QFile::remove(zip_path); +} + #elif defined(__linux__) bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) diff --git a/src/updater/CMakeLists.txt b/src/updater/CMakeLists.txt index 80af88967..438a9da1f 100644 --- a/src/updater/CMakeLists.txt +++ b/src/updater/CMakeLists.txt @@ -14,3 +14,29 @@ if(WIN32) target_link_libraries(updater PRIVATE "Comctl32.lib") set_target_properties(updater PROPERTIES WIN32_EXECUTABLE TRUE) endif() + +if(APPLE) + set(MAC_SOURCES + cocoa_main.mm + cocoa_progress_callback.mm + cocoa_progress_callback.h + ) + target_sources(updater PRIVATE ${MAC_SOURCES}) + set_source_files_properties(${MAC_SOURCES} PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE) + find_library(COCOA_LIBRARY Cocoa REQUIRED) + target_link_libraries(updater PRIVATE ${COCOA_LIBRARY}) + + if(NOT CMAKE_GENERATOR MATCHES "Xcode" AND NOT SKIP_POSTPROCESS_BUNDLE) + set_target_properties(updater PROPERTIES OUTPUT_NAME "Updater") + set_target_properties(updater PROPERTIES + MACOSX_BUNDLE true + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in + OUTPUT_NAME Updater + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app/Contents/Resources + ) + + # Copy icon into the bundle + target_sources(updater PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Updater.icns") + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/Updater.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + endif() +endif() diff --git a/src/updater/Info.plist.in b/src/updater/Info.plist.in new file mode 100644 index 000000000..3e0a98227 --- /dev/null +++ b/src/updater/Info.plist.in @@ -0,0 +1,24 @@ + + + + + CFBundleExecutable + Updater + CFBundleIconFile + Updater.icns + CFBundleIdentifier + com.github.stenzek.duckstation.updater + CFBundleDevelopmentRegion + English + CFBundlePackageType + APPL + NSHumanReadableCopyright + Licensed under GPL version 3 + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHighResolutionCapable + + CSResourcesFileMapped + + + diff --git a/src/updater/Updater.icns b/src/updater/Updater.icns new file mode 100644 index 0000000000000000000000000000000000000000..49b90d2ef269ef5cdbdba03b8cb14c65ecbde461 GIT binary patch literal 8521 zcmc~y&MRhMQ1r~qFg9Xfkg#Mqa^UcR{mln?4jn#p`0$|vM|Q8@wDw@*fA-IhZ(O{5 z<;tao%gk49T)*CMgYEiFgxbT04jw#kV*C81^A0usXMc6-^0h0MFJn{7bm;J*hJ!+f zPV8E-dg0Ok|Ns4cb>-&u>sPTD+VjtyL#pF zWmGq`9h5nA^yu*ur?2endHDbThwFFm-MMq?=FJ<|ucE1JIVgST$kF4+kDNMl@$jnW z|NnoyclZANyLZ8=uU&0G@>tVB1(5A0jvl*w?9BNqS3mv#_wU!UyAK}RzXx{7jqBHt zR5u>vI}CEd@pJG0KRbW%@`?9N|9RitzyA=f{93~e7EqWSYCOmRHu&VJ6R#Tnv%ETU z;qjm5|3crN-g|^ledGG|n}-_@3P4q!IeX^)gWvz(pZwhRU+~}CM~@LouV24$wBev2 z*!EMW&z?Ph`v2{B|KC3S(*9rl-{YrGU`p@Yx_NiU!9z!m9y@XJ)S0vAFI+tL>HC}i zfB(O{^}Y4K;-6>FpFtEq0ENee<;S23&t159>B`k}kAF7)XS#OdP0N3Ue=lCXc>e6! zlgE!i?!CWz)yb15PJ(Q{aOv`uYxi!xZTiph_S*GFKbro_fB*XW)ytPJo;`c|T>US69u)s2E|204DShRY<`~%NEz5Mt8)2ELw-hO)W?9=nE zxSE#cmfCs$|KGcF=lbKH|NlR_+xlO7*OJvMr>uJM`2NjPCyzXN@#^FE&!3;KuPAD1 z1u6Y>?cUuhFaH1kdiVeKz26)EOP^Y_WbwW`Pwrp8c>e5($IoBCefj#;xBs{26jj&H zZ}`u0=g!@mFB|@=y!iiq%f{#b|KDD*V9V8aFCX52aOK>&laHUjeEaOh%U7>{|2*E4 zGpFG{%lqqhZ@y~y&+xn9zrwCVe-2GsdFI8NhoC%g_2RjcPhLHG`s~^B7cXDF{r_&w zoW}pmx9{G3)$(8Q(uR$*4nBPM<8w{P9Ne&g)PN6#NTe)0sO_|u<*%MUd;kCK)mKgbwSGRm_xce??adq4u7K2@ zJ#+fTjazr`-G2Zw{xMkn%crkCU47m7zyAHb+m9cB9eeHS<%<{2pFMr*%$b`vuHU$Q z=OM_3M~@#rdHU?xi{~#MJpcdy%ahv=?%%z0`{s>nSFc>WaN**qQzuTHJ#+KMjq5j_ z+DY6sX0uU>xs`0(xnsOrm?E}pw|`Q*hDQ1$m899wnG<=-+TV-sk3Kp+)hq*x1757=nlvy4<9{x_U7Q?ip9_Ffy%|p z7tWo#dSO$4{ghd&4_&@+`q9n(OBSwJw`23V-4`F+2kE%~_{D)`8@sC(J-m7O(!~pB zFJ9d{tGTmh)}k46HlMw6YVE?6t0h)$*tKK(#*K%sKfHVA&VwfhR&L(h+grEr&eeu< z0#}bO?P%|x&^vqa!uc~6?_090VKvjr4SO4Q%5UGe{p9U?cOM>HyLHRf-oE~(h1V}! zKD%LJYyYH)lltc_ZCK1WZ%)%1*;SkN@7cY3*RJiG_g=hrXv6mH+qU=hPng)U@Y3E{ z?Y#|?SSC-NFmGAo5{AW1>sZ!oJ+yak;~v)C+qUoDva4Yy=Z*;zCr+BwIj66?VRGXX zrit^GH!W@2z_Nbx!9)8S_lfM?yL%_d^xeBAPMS1%@|505Q>IR7oXR|T%Howx%Ud=u zZ`pJBaKizH{f+zB_U_%YXZPgE4O1GYHcn%mI(5$CWh<@0&6~Gw+p_(@@#9At4mTcRnm%Jj(@d@zGv_Z|v3wa!6*EXx<2L3! zM@}4XINET8VMgOD*4eXW%v-v$X&DpLC^nFyZQHgt>|ow^?8NcLV+^wzXS2R`CypIIHf#3mnX?wIT+^_e0ZAb{Okv|L-kp06pEz-B)~uQH zmaSR25{J5mU3|ND@7THj*ohgl7q48kas^(6yLRo|eQ?2wRV!AYDP-;L>F(|6Zt4-~ z?(FL9?CNZtJa^WNMTaI;)l8T%Z(+myhQ*9a7B?>9T)cSca+q4??#|AxhHinLCEGWy z-|}j4e(9XeOBXGik5w^KH%P5mZ}*(Ft5$5fv!tea=GtXTmn>emXz^kUlf}Ecy1RS& zCQO_(ZP}tTXC7Q#0P@=MrAsiK(b&`2E!I0>;^Zk)Cof-e;pFRQi`TDNvwG!nu6Po&uUor%)yfsimw{ZNfTX&+y9?xm$&)6{ zpFC^soJr?SpWbGFnd;jZqUzW#}mr%anZb5UaIifwzR?>pVHU+CbW?VC4m+PGmo$VDqxEM2<1 zt)W{4?2KvCXU>|lB-^?D?8#|WTlTJR+EBk`)#{zwwr<(9aXrNN<%_3*19|e4X)|Wd zo-=P*gGc-AbL-tw=I`IKZvCcB%VwVXV=ah+qZ4q0`|h1Lo3=Rf}A`HqMdBp50(eHt*WKf7OQ7Q^2M#TCrnswp;4-y_+}9jmu1ojY?g&c2x?? zO8=JKmk#gQvwPRhom=)B9zD2f^@>>w<}FybWXrmGAIso`z=o}d=f!2D#YW}LbX>V` z_WH=6r0K`c@87#;&yGzy_wH#}S-*PuyhZD`O^mj3^$pI6^-Wv0CN@1YA+kQ0`PjPp znv}G-pu&|GPVGCie)-BZYgVsWvwGRWrPFg9?0tQGg0oX%gClCv)6&x-OKSRNCMTuj zrY5IHMl^3Za(Mm9l}nba+q8M}*4^7f)ZF}h{d_~RGUDQ*LXt9K(_+%M<~Rie26%+| z<)vmMyJzg$wru^XDO1*M+_Y`?-s3xJ9ISo)K^l@{;^IQ;r^iQR?9ddjvp02Vt4xlG zZCbr`-O}}|rcGP7ansh_d-j~Tx;o9u**`QZJ-s2WKDhDp!tBT`%Pd5!quSb&B1;w> z+Pz`@n)Rze>NjuQxoh|CJ?G9$4Ydl)PR~e=i3_RQvu9n;(dV}U)f>vgGN$Z0uyOs` zHLKRHoHlLUhHYE7ZQrqT*RK7S_SgBwq-UhZg4A!=aQVuaSBnBtVp}&J+5%F&V)@dg z)26Q9uzB+qW{{SKoytcpuB{G_%Swt3soS%5>y>lo9!!iVUvgmQ`gLnotysQv@uGz@ zXEv;_-?(Yh=B?YdZQr?b*U4jZv%`}?7OXmU@$99$6MA;-1*Lwl>IL)W%$c=%&D!-F zHf-Fqc?-yr9eYmi=}8I>s^7Kw()p{WH?7;hdF`52E0-@_vS`7)xwB_ZpT2tasx@oZ zt%vH^wqwVE(;I4B%61&Nabf@3)mzrBS-p}UrhL}4Y13z{UcItmRsF{G>(+1Bw0ZNE zt=qTnIC^YK+= z`Xy7dBAVySnm%L3stwy0Rz|0!6--#SdCi)&>o#oMxM}wbcFftZe8K#Ab7sz4*pcWN z5E7dd5mPyH(bSyiw9Jg$(&D1btd^yl*Q{E-VS8hOVqR`Yh)edWMYCtkUNkA!&nGNA zG%h(QAw0D$Avr5EGb6XWp+vGMv#5X7hP9g;Qws9)LqfycGndR+Ftak;JG3E!DJ(v< zA%!_ECOfAgn<*>5qM=N_w6q|rYWBLOw4$QI!qBj=aL>$n?TJ3Ykr5G*k>QCMO=(Se zEV)J16%~!;Y-M?cwK*jv4aJOM;SCYYe#s#LjgbveOc9A`O{q<}%sBh4&kxfxe(QGkM zQ3=UexlK7u+3Z=lg@vWn^|g)F4OJ|a6_piHP0`IUY|(M4nJF1L?b%#exkbfAMdfu3 zwY3d3jn#}*(M>Upu^cgR$r+jH$si?6S-C|G#Vmy-)r}1ebq%#lH8n9!v21a1v9U?% znGNYoa8-@PtVP8Ybxn;8^$m3lv5j%8@o}+<=~+!kYMP4KN=r(s8=4v$8yMmmp zQnRu$(i&0Ju$PvVm6q2wHa0YH$Hm9T#wBNFXJn$Q5-Tk$D=#mrsB3I&h>MF&%E->l z#G$I9qO7c{p(!RlEhihK2D@1m73Jk6Wi`o}IaygKs+zJ{gF=GBLPMLvm_kEB8bbMk zeEfWUgUpnQ+8S$0b21t-8*=z_b22mY@(T*`a$$4Hm7SA=X);4-Lx^x_cw|&mVsw%M1FKbM zNpVqOL0&EvcQk}Cgf@nVg+)fi#KfgU#z`?S7)~xND=jGkxwRq38r9ihVIU`mg-1rm zWyB|@#N{Y4Fxzx@mY0`-t;o;K$;?1_vmsP3BsduClC+HAqKuS`Mr{UG4vB(EHRa`H zr6t9M1^Ic|xrks1ZV2HE3y+G9O^jD!G|kS)?$c~!P-gV&>8Pj#TTxV)pPL7ASx`t& zNJwx9$n2Qd_*~Hj1{TBmI$!o?1|f}#i4~O9jEjutm1jR9972fSgiPke{F17{U#6qg5wL_%y$Nlnkl%4!K@ z>|jt~WMB}T+ycr9b+t8Bl@;a1jlrQYFS$|gX|8W*;0p}&4G7E5 zF0YH$;1ZP+>ts-7(PCs^U{O(*dJG=3Ukx)^E-Q*+FB>hDK=ymk{6r6AR{OzVv^O@ zT3JyFk#Fm)Z)$02oi;gMkxQtDL4kvTMN(QLqOGf{qO7#Iq%b!vFTbI#zM-+Hxw&Ij zn-5P9gOs$qn6gJzZ!JhgVL@JYL3&|PZA~pmRYMbV@9a|bZU$vOW$T=t=JN8=;=+Qw zob2@6)bfgos_N>Ry88Ntrl#hpQ=<4g8C2vFT020=2&^bGJuNx0vaGbcva+fgVqjz2 zoHpln26emI?wYdF;-Z4Q-0aNsw3MWTr0VLj($ew@sM7kzhMw8QTFne9s#y~o!TAuX zG&vz5DW$5qsa2{k)b#kcn1rO{s_M$B znzo$U8YCNKkI3i5MvvNFs6}{6^5|WZD8$0Xs?4*?BwL>cEN=wViE32xi8@rsuxXtTJ zK?PJsT1s+unv1-EjEqT0PFhk|t)G&VhL)b1oOxnhRdH!qd3jx%lbD7qw`pZTW=48y zQbu;5rl7d2lA5%R zCV4J7IYm_sbv1Q$h$>!)DnlbfLsKgo>qaXExkh;wS$P!=jRrMlm?}1?ssjo=MIeB?GIc0SXHFYfN8jaWt4NNSpt*sj5*c8bxTuEJ19pr8#gEA~~vl$pTIT+AD*->PHW7Wt!Ot$Os|Nnas!kz#BpY}v> z(OEKc{)2!GOxy~@`@c{Y#z&)zzk%id|6qnlFn)*dzn4JxsHD$1i0uFW|93)Uw!sB1 z_(DVwr1BED|NsBRBKCisCIf?LEjAfkV)f!ja7keoKjw^G9G9pILm4h9Y~oc6 z3=D^mCH|s&^dGY95l{$mA%uQ^|AQWzD|JT=V2zP;{5yanjufPBQ{|K&} zLFU=-uNW>zsQ>@}+w0Fy!WeB+zsD>tl5PnuG zSObx!gN$lpU|{G3F^I*z4AY2J3Q;#R{tSdc9C<#Ofg$fLacckn|5Ob2urQ=pBdBY! zBv?5E1B2xqf{Op&bcCs9U|{GVtiB9MIl~mf>YI?&PbI7#mKwm?2Alfjgxx;{>~JVi zOjvyiR4IfCzKUPR1&nI15l3kPGUsyz#8ebg5nDX~QvH9a6pBJP$NK_U8jU#P3Ri%^ zsQ8Sg`2YJN3`NKS(kS)h|Njd_k!7*5olhgII1aDsv8jZK6}*G!c$EngAwY}F2Wgne RPe2)xwDsk)rbt{QCIB7@ +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "cocoa_progress_callback.h" +#include "updater.h" + +#include "common/file_system.h" +#include "common/log.h" +#include "common/path.h" +#include "common/scoped_guard.h" +#include "common/string_util.h" +#include "common/timer.h" + +#include +#include + +static void LaunchApplication(const char* path) +{ + @autoreleasepool + { + NSTask* task = [[[NSTask alloc] init] autorelease]; + [task setLaunchPath:[NSString stringWithUTF8String:path]]; + [task launch]; + } +} + +int main(int argc, char* argv[]) +{ + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + // Needed for keyboard in put. + const ProcessSerialNumber psn = {0, kCurrentProcess}; + TransformProcessType(&psn, kProcessTransformToForegroundApplication); + + Log::SetConsoleOutputParams(true, "", LOGLEVEL_DEBUG); + + CocoaProgressCallback progress; + + if (argc != 4) + { + progress.ModalError("Expected 3 arguments: update zip, staging directory, output directory.\n\nThis program is not " + "intended to be run manually, please use the Qt frontend and click Help->Check for Updates."); + return EXIT_FAILURE; + } + + std::string zip_path = argv[1]; + std::string staging_directory = argv[2]; + std::string destination_directory = argv[3]; + + if (zip_path.empty() || staging_directory.empty() || destination_directory.empty()) + { + progress.ModalError("One or more parameters is empty."); + return EXIT_FAILURE; + } + + if (const char* home_dir = getenv("HOME")) + { + static constexpr char log_file[] = "Library/Application Support/DuckStation/updater.log"; + std::string log_path = Path::Combine(home_dir, log_file); + Log::SetFileOutputParams(true, log_path.c_str()); + } + + std::string program_to_launch = Path::Combine(destination_directory, "Contents/MacOS/DuckStation"); + int result = EXIT_SUCCESS; + + std::thread worker([&progress, zip_path = std::move(zip_path), + destination_directory = std::move(destination_directory), + staging_directory = std::move(staging_directory), &result]() { + ScopedGuard app_stopper([]() { dispatch_async(dispatch_get_main_queue(), []() { [NSApp stop:nil]; }); }); + + Updater updater(&progress); + if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory))) + { + progress.ModalError("Failed to initialize updater."); + result = EXIT_FAILURE; + return; + } + + if (!updater.OpenUpdateZip(zip_path.c_str())) + { + progress.DisplayFormattedModalError("Could not open update zip '%s'. Update not installed.", zip_path.c_str()); + result = EXIT_FAILURE; + return; + } + + if (!updater.PrepareStagingDirectory()) + { + progress.ModalError("Failed to prepare staging directory. Update not installed."); + result = EXIT_FAILURE; + return; + } + + if (!updater.StageUpdate()) + { + progress.ModalError("Failed to stage update. Update not installed."); + result = EXIT_FAILURE; + return; + } + + if (!updater.ClearDestinationDirectory()) + { + progress.ModalError("Failed to clear destination directory. Your installation may be corrupted, please " + "re-download a fresh version from GitHub."); + result = EXIT_FAILURE; + return; + } + + if (!updater.CommitUpdate()) + { + progress.ModalError( + "Failed to commit update. Your installation may be corrupted, please re-download a fresh version from GitHub."); + result = EXIT_FAILURE; + return; + } + + updater.CleanupStagingDirectory(); + + progress.ModalInformation("Update complete."); + + result = EXIT_SUCCESS; + }); + + [NSApp run]; + + worker.join(); + + if (result == EXIT_SUCCESS) + { + progress.DisplayFormattedInformation("Launching '%s'...", program_to_launch.c_str()); + LaunchApplication(program_to_launch.c_str()); + } + + return result; +} diff --git a/src/updater/cocoa_progress_callback.h b/src/updater/cocoa_progress_callback.h new file mode 100644 index 000000000..689c3045b --- /dev/null +++ b/src/updater/cocoa_progress_callback.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include "common/progress_callback.h" + +#include +#include + +#ifndef __OBJC__ +#error This file needs to be compiled with Objective C++. +#endif + +#if __has_feature(objc_arc) +#error ARC should not be enabled. +#endif + +class CocoaProgressCallback final : public BaseProgressCallback +{ +public: + CocoaProgressCallback(); + ~CocoaProgressCallback(); + + void PushState() override; + void PopState() override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + +private: + enum : int + { + WINDOW_WIDTH = 600, + WINDOW_HEIGHT = 300, + WINDOW_MARGIN = 20, + SUBWINDOW_PADDING = 10, + SUBWINDOW_WIDTH = WINDOW_WIDTH - WINDOW_MARGIN - WINDOW_MARGIN, + }; + + bool Create(); + void Destroy(); + void UpdateProgress(); + void AppendMessage(const char* message); + + NSWindow* m_window = nil; + NSView* m_view = nil; + NSTextField* m_status = nil; + NSProgressIndicator* m_progress = nil; + NSScrollView* m_text_scroll = nil; + NSTextView* m_text = nil; +}; diff --git a/src/updater/cocoa_progress_callback.mm b/src/updater/cocoa_progress_callback.mm new file mode 100644 index 000000000..a64a9ed96 --- /dev/null +++ b/src/updater/cocoa_progress_callback.mm @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "cocoa_progress_callback.h" + +#include "common/log.h" + +Log_SetChannel(CocoaProgressCallback); + +CocoaProgressCallback::CocoaProgressCallback() : BaseProgressCallback() +{ + Create(); +} + +CocoaProgressCallback::~CocoaProgressCallback() +{ + Destroy(); +} + +void CocoaProgressCallback::PushState() +{ + BaseProgressCallback::PushState(); +} + +void CocoaProgressCallback::PopState() +{ + BaseProgressCallback::PopState(); + UpdateProgress(); +} + +void CocoaProgressCallback::SetCancellable(bool cancellable) +{ + BaseProgressCallback::SetCancellable(cancellable); +} + +void CocoaProgressCallback::SetTitle(const char* title) +{ + dispatch_async(dispatch_get_main_queue(), [this, title = [[NSString alloc] initWithUTF8String:title]]() { + [m_window setTitle:title]; + [title release]; + }); +} + +void CocoaProgressCallback::SetStatusText(const char* text) +{ + BaseProgressCallback::SetStatusText(text); + dispatch_async(dispatch_get_main_queue(), [this, title = [[NSString alloc] initWithUTF8String:text]]() { + [m_status setStringValue:title]; + [title release]; + }); +} + +void CocoaProgressCallback::SetProgressRange(u32 range) +{ + BaseProgressCallback::SetProgressRange(range); + UpdateProgress(); +} + +void CocoaProgressCallback::SetProgressValue(u32 value) +{ + BaseProgressCallback::SetProgressValue(value); + UpdateProgress(); +} + +bool CocoaProgressCallback::Create() +{ + @autoreleasepool + { + const NSRect window_rect = + NSMakeRect(0.0f, 0.0f, static_cast(WINDOW_WIDTH), static_cast(WINDOW_HEIGHT)); + constexpr NSWindowStyleMask style = NSWindowStyleMaskTitled; + m_window = [[NSWindow alloc] initWithContentRect:window_rect + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + + NSView* m_view; + m_view = [[NSView alloc] init]; + [m_window setContentView:m_view]; + + int x = WINDOW_MARGIN; + int y = WINDOW_HEIGHT - WINDOW_MARGIN; + + y -= 16 + SUBWINDOW_PADDING; + m_status = [NSTextField labelWithString:@"Initializing..."]; + [m_status setFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 16)]; + [m_view addSubview:m_status]; + + y -= 16 + SUBWINDOW_PADDING; + m_progress = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 16)]; + [m_progress setMinValue:0]; + [m_progress setMaxValue:100]; + [m_progress setDoubleValue:0]; + [m_progress setIndeterminate:NO]; + [m_view addSubview:m_progress]; + + y -= 170 + SUBWINDOW_PADDING; + m_text_scroll = [[NSScrollView alloc] initWithFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 170)]; + [m_text_scroll setBorderType:NSBezelBorder]; + [m_text_scroll setHasVerticalScroller:YES]; + [m_text_scroll setHasHorizontalScroller:NO]; + + const NSSize content_size = [m_text_scroll contentSize]; + m_text = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, content_size.width, content_size.height)]; + [m_text setMinSize:NSMakeSize(0, content_size.height)]; + [m_text setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [m_text setVerticallyResizable:YES]; + [m_text setHorizontallyResizable:NO]; + [m_text setAutoresizingMask:NSViewWidthSizable]; + [m_text setUsesAdaptiveColorMappingForDarkAppearance:YES]; + [[m_text textContainer] setContainerSize:NSMakeSize(content_size.width, FLT_MAX)]; + [[m_text textContainer] setWidthTracksTextView:YES]; + [m_text_scroll setDocumentView:m_text]; + [m_view addSubview:m_text_scroll]; + + [m_window center]; + [m_window setIsVisible:TRUE]; + [m_window makeKeyAndOrderFront:nil]; + [m_window setReleasedWhenClosed:NO]; + } + + return true; +} + +void CocoaProgressCallback::Destroy() +{ + if (m_window == nil) + return; + + [m_window close]; + + m_text = nil; + m_progress = nil; + m_status = nil; + + [m_view release]; + m_view = nil; + + [m_window release]; + m_window = nil; +} + +void CocoaProgressCallback::UpdateProgress() +{ + const float percent = (static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f; + dispatch_async(dispatch_get_main_queue(), [this, percent]() { + [m_progress setDoubleValue:percent]; + }); +} + +void CocoaProgressCallback::DisplayError(const char* message) +{ + Log_ErrorPrint(message); + AppendMessage(message); +} + +void CocoaProgressCallback::DisplayWarning(const char* message) +{ + Log_WarningPrint(message); + AppendMessage(message); +} + +void CocoaProgressCallback::DisplayInformation(const char* message) +{ + Log_InfoPrint(message); + AppendMessage(message); +} + +void CocoaProgressCallback::AppendMessage(const char* message) +{ + @autoreleasepool + { + NSString* nsmessage = [[[NSString stringWithUTF8String:message] stringByAppendingString:@"\n"] retain]; + dispatch_async(dispatch_get_main_queue(), [this, nsmessage]() { + @autoreleasepool + { + NSAttributedString* attr = [[[NSAttributedString alloc] initWithString:nsmessage] autorelease]; + [[m_text textStorage] appendAttributedString:attr]; + [m_text scrollRangeToVisible:NSMakeRange([[m_text string] length], 0)]; + [nsmessage release]; + } + }); + } +} + +void CocoaProgressCallback::DisplayDebugMessage(const char* message) +{ + Log_DevPrint(message); +} + +void CocoaProgressCallback::ModalError(const char* message) +{ + if (![NSThread isMainThread]) + { + dispatch_sync(dispatch_get_main_queue(), [this, message]() { ModalError(message); }); + return; + } + + @autoreleasepool + { + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + [alert setMessageText:[NSString stringWithUTF8String:message]]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert runModal]; + } +} + +bool CocoaProgressCallback::ModalConfirmation(const char* message) +{ + if (![NSThread isMainThread]) + { + bool result; + dispatch_sync(dispatch_get_main_queue(), [this, message, &result]() { result = ModalConfirmation(message); }); + return result; + } + + bool result; + @autoreleasepool + { + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + [alert setMessageText:[NSString stringWithUTF8String:message]]; + [alert addButtonWithTitle:@"Yes"]; + [alert addButtonWithTitle:@"No"]; + result = ([alert runModal] == NSAlertFirstButtonReturn); + } + + return result; +} + +void CocoaProgressCallback::ModalInformation(const char* message) +{ + if (![NSThread isMainThread]) + { + dispatch_sync(dispatch_get_main_queue(), [this, message]() { ModalInformation(message); }); + return; + } + + @autoreleasepool + { + NSAlert* alert = [[[NSAlert alloc] init] autorelease]; + [alert setMessageText:[NSString stringWithUTF8String:message]]; + [alert runModal]; + } +} diff --git a/src/updater/updater.cpp b/src/updater/updater.cpp index 930f42858..ccb054ad9 100644 --- a/src/updater/updater.cpp +++ b/src/updater/updater.cpp @@ -1,12 +1,14 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "updater.h" -#include "win32_progress_callback.h" +#include "common/error.h" #include "common/file_system.h" #include "common/log.h" #include "common/minizip_helpers.h" +#include "common/path.h" +#include "common/progress_callback.h" #include "common/string_util.h" #include @@ -18,7 +20,14 @@ #include #ifdef _WIN32 +#include "common/windows_headers.h" #include +#else +#include +#endif + +#ifdef __APPLE__ +#include "common/cocoa_tools.h" #endif Updater::Updater(ProgressCallback* progress) : m_progress(progress) @@ -32,19 +41,12 @@ Updater::~Updater() unzClose(m_zf); } -bool Updater::Initialize(std::string destination_directory) +bool Updater::Initialize(std::string staging_directory, std::string destination_directory) { + m_staging_directory = std::move(staging_directory); m_destination_directory = std::move(destination_directory); - m_staging_directory = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s", - m_destination_directory.c_str(), "UPDATE_STAGING"); m_progress->DisplayFormattedInformation("Destination directory: '%s'", m_destination_directory.c_str()); m_progress->DisplayFormattedInformation("Staging directory: '%s'", m_staging_directory.c_str()); - - // log everything to file as well - Log::SetFileOutputParams( - true, StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "updater.log", m_destination_directory.c_str()) - .c_str()); - return true; } @@ -58,9 +60,12 @@ bool Updater::OpenUpdateZip(const char* path) return ParseZip(); } -bool Updater::RecursiveDeleteDirectory(const char* path) +bool Updater::RecursiveDeleteDirectory(const char* path, bool remove_dir) { #ifdef _WIN32 + if (!remove_dir) + return false; + // making this safer on Win32... std::wstring wpath(StringUtil::UTF8StringToWideString(path)); wpath += L'\0'; @@ -72,7 +77,31 @@ bool Updater::RecursiveDeleteDirectory(const char* path) return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted); #else - return FileSystem::DeleteDirectory(path, true); + FileSystem::FindResultsArray results; + if (FileSystem::FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, + &results)) + { + for (const FILESYSTEM_FIND_DATA& fd : results) + { + if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + { + if (!RecursiveDeleteDirectory(fd.FileName.c_str(), true)) + return false; + } + else + { + m_progress->DisplayFormattedInformation("Removing directory '%s'.", fd.FileName.c_str()); + if (!FileSystem::DeleteFile(fd.FileName.c_str())) + return false; + } + } + } + + if (!remove_dir) + return true; + + m_progress->DisplayFormattedInformation("Removing directory '%s'.", path); + return FileSystem::DeleteDirectory(path); #endif } @@ -110,13 +139,33 @@ bool Updater::ParseZip() while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER) std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len); +#ifdef _WIN32 + entry.file_mode = 0; +#else + // Preserve permissions on Unix. + static constexpr u32 PERMISSION_MASK = (S_IRWXO | S_IRWXG | S_IRWXU); + entry.file_mode = + ((file_info.external_fa >> 16) & 0x01FFu) & PERMISSION_MASK; // https://stackoverflow.com/a/28753385 +#endif + // skip directories (we sort them out later) if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER) { + bool process_file = true; + const char* filename_to_add = zip_filename_buffer; +#ifdef _WIN32 // skip updater itself, since it was already pre-extracted. - if (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0) + process_file = process_file && (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0); +#elif defined(__APPLE__) + // on MacOS, we want to remove the DuckStation.app prefix. + static constexpr const char* PREFIX_PATH = "DuckStation.app/"; + const size_t prefix_length = std::strlen(PREFIX_PATH); + process_file = process_file && (std::strncmp(zip_filename_buffer, PREFIX_PATH, prefix_length) == 0); + filename_to_add += prefix_length; +#endif + if (process_file) { - entry.destination_filename = zip_filename_buffer; + entry.destination_filename = filename_to_add; m_progress->DisplayFormattedInformation("Found file in zip: '%s'", entry.destination_filename.c_str()); m_update_paths.push_back(std::move(entry)); } @@ -167,7 +216,7 @@ bool Updater::PrepareStagingDirectory() if (FileSystem::DirectoryExists(m_staging_directory.c_str())) { m_progress->DisplayFormattedWarning("Update staging directory already exists, removing"); - if (!RecursiveDeleteDirectory(m_staging_directory.c_str()) || + if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true) || FileSystem::DirectoryExists(m_staging_directory.c_str())) { m_progress->ModalError("Failed to remove old staging directory"); @@ -204,7 +253,8 @@ bool Updater::StageUpdate() for (const FileToUpdate& ftu : m_update_paths) { - m_progress->SetFormattedStatusText("Extracting '%s'...", ftu.original_zip_filename.c_str()); + m_progress->SetFormattedStatusText("Extracting '%s' (mode %o)...", ftu.original_zip_filename.c_str(), + ftu.file_mode); if (unzLocateFile(m_zf, ftu.original_zip_filename.c_str(), 0) != UNZ_OK) { @@ -258,6 +308,23 @@ bool Updater::StageUpdate() } } +#ifndef _WIN32 + if (ftu.file_mode != 0) + { + const int fd = fileno(fp); + const int res = (fd >= 0) ? fchmod(fd, ftu.file_mode) : -1; + if (res < 0) + { + m_progress->DisplayFormattedModalError("Failed to set mode for file '%s' (fd %d) to %u: errno %d", + destination_file.c_str(), fd, res, errno); + std::fclose(fp); + FileSystem::DeleteFile(destination_file.c_str()); + unzCloseCurrentFile(m_zf); + return false; + } + } +#endif + std::fclose(fp); unzCloseCurrentFile(m_zf); m_progress->IncrementProgressValue(); @@ -291,17 +358,23 @@ bool Updater::CommitUpdate() const std::string dest_file_name = StringUtil::StdStringFromFormat( "%s" FS_OSPATH_SEPARATOR_STR "%s", m_destination_directory.c_str(), ftu.destination_filename.c_str()); m_progress->DisplayFormattedInformation("Moving '%s' to '%s'", staging_file_name.c_str(), dest_file_name.c_str()); + + Error error; #ifdef _WIN32 const bool result = MoveFileExW(StringUtil::UTF8StringToWideString(staging_file_name).c_str(), StringUtil::UTF8StringToWideString(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING); + if (!result) + error.SetWin32(GetLastError()); +#elif defined(__APPLE__) + const bool result = CocoaTools::MoveFile(staging_file_name.c_str(), dest_file_name.c_str(), &error); #else const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0); #endif if (!result) { - m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s'", staging_file_name.c_str(), - dest_file_name.c_str()); + m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s': %s", staging_file_name.c_str(), + dest_file_name.c_str(), error.GetDescription().c_str()); return false; } } @@ -312,6 +385,11 @@ bool Updater::CommitUpdate() void Updater::CleanupStagingDirectory() { // remove staging directory itself - if (!RecursiveDeleteDirectory(m_staging_directory.c_str())) + if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true)) m_progress->DisplayFormattedError("Failed to remove staging directory '%s'", m_staging_directory.c_str()); } + +bool Updater::ClearDestinationDirectory() +{ + return RecursiveDeleteDirectory(m_destination_directory.c_str(), false); +} diff --git a/src/updater/updater.h b/src/updater/updater.h index a925ff70b..3aed0ac55 100644 --- a/src/updater/updater.h +++ b/src/updater/updater.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -13,27 +13,29 @@ public: Updater(ProgressCallback* progress); ~Updater(); - bool Initialize(std::string destination_directory); + bool Initialize(std::string staging_directory, std::string destination_directory); bool OpenUpdateZip(const char* path); bool PrepareStagingDirectory(); bool StageUpdate(); bool CommitUpdate(); void CleanupStagingDirectory(); + bool ClearDestinationDirectory(); private: - static bool RecursiveDeleteDirectory(const char* path); + bool RecursiveDeleteDirectory(const char* path, bool remove_dir); struct FileToUpdate { std::string original_zip_filename; std::string destination_filename; + u32 file_mode; }; bool ParseZip(); - std::string m_destination_directory; std::string m_staging_directory; + std::string m_destination_directory; std::vector m_update_paths; std::vector m_update_directories; diff --git a/src/updater/win32_main.cpp b/src/updater/win32_main.cpp index c2a3ecbff..70009f25e 100644 --- a/src/updater/win32_main.cpp +++ b/src/updater/win32_main.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "updater.h" @@ -6,6 +6,7 @@ #include "common/file_system.h" #include "common/log.h" +#include "common/path.h" #include "common/string_util.h" #include "common/windows_headers.h" @@ -42,9 +43,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi } const int parent_process_id = StringUtil::FromChars(StringUtil::WideStringToUTF8String(argv[0])).value_or(0); - const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]); - const std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]); - const std::wstring program_to_launch(argv[3]); + std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]); + std::string staging_directory = Path::Combine(destination_directory, "UPDATE_STAGING"); + std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]); + std::wstring program_to_launch(argv[3]); LocalFree(argv); if (parent_process_id <= 0 || destination_directory.empty() || zip_path.empty() || program_to_launch.empty()) @@ -53,11 +55,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi return 1; } + Log::SetFileOutputParams(true, Path::Combine(destination_directory, "updater.log").c_str()); + progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id); WaitForProcessToExit(parent_process_id); Updater updater(&progress); - if (!updater.Initialize(destination_directory)) + if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory))) { progress.ModalError("Failed to initialize updater."); return 1;