From ebc94ab4e1ee84260b1d1a0227b50f10d490db8a Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 15 Oct 2025 15:38:19 +0200 Subject: [PATCH 01/30] Add package installer tool for macos --- doc/images/fastsurfer.png | Bin 0 -> 26492 bytes doc/images/installer_background.png | Bin 0 -> 26492 bytes doc/overview/INSTRUCTIONS.md | 13 + tools/macos_build/.gitignore | 8 + tools/macos_build/FastSurfer.py.template | 14 + tools/macos_build/README.md | 4 + tools/macos_build/build_release_package.sh | 240 ++++++++++ tools/macos_build/install_fs_pruned.sh | 423 ++++++++++++++++++ .../macos_setup_fastsurfer.sh.template | 21 + .../macos_build/scripts/postinstall.template | 76 ++++ tools/macos_build/setup.py | 19 + 11 files changed, 818 insertions(+) create mode 100644 doc/images/fastsurfer.png create mode 100644 doc/images/installer_background.png create mode 100644 doc/overview/INSTRUCTIONS.md create mode 100644 tools/macos_build/.gitignore create mode 100644 tools/macos_build/FastSurfer.py.template create mode 100644 tools/macos_build/README.md create mode 100755 tools/macos_build/build_release_package.sh create mode 100755 tools/macos_build/install_fs_pruned.sh create mode 100644 tools/macos_build/macos_setup_fastsurfer.sh.template create mode 100755 tools/macos_build/scripts/postinstall.template create mode 100644 tools/macos_build/setup.py diff --git a/doc/images/fastsurfer.png b/doc/images/fastsurfer.png new file mode 100644 index 0000000000000000000000000000000000000000..5d74c972b1a59e110ab2c50e8c811f68c82b6778 GIT binary patch literal 26492 zcmV)WK(4=uP)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^Rb0u~hpD>W(ATmS$d07*naRCwC# zy?MMfSy|uvU8{yYp81@2>=C*f8`@@85D-Cvs8J&-Voa1I8sim{OLCK#pi#`nAyKc< z3_^@Z6k?2CqY@2jKoKs8qBKJ{v@}h3zeB&{nfF*#Yvqq;)vmQ`SM7Zo8k#2W{(Sc5 zoU_j!YFDk_dZyp=d*Isb+U?ry+U?ry+U?ry+U?ry+U?ry+U?ry^Yx~#oq}t(M{$kV zYp3fP_^#a^+v|G`gxA1#?V29par-E*3wsTWN4x23r{MDjaLNOXBKRg<@%WM#j(8l`cFAkE$K^GU_vk#h0^KgAbpuv!F>Jnd@n?K<2w(qH(CaX`AAb51aF_ev zw?_~J@W#=c_puef+X`#9=Q)96+Z~^_kwML`6wiR1nCDM>w?aXbp&L1rTk`HpL8$r^5 z%snr2KuOL*_71r87Wm=!%>kLIc>Ed&uYvC|O>WHl|08>^jSt;|y8V~2;nOi_4|K&n zF$2ubkHItkEj(nW-|H7&1M5EkRtdQL7g`_z`tBt|AOZrw7%&-(e;@wScfvhmX3^{a zZfd~ze>@1Uf$!5lU-Ln8{Dq3^zBZzHeSnG;nGG<&-d$tHhl-`A$AnuE-KT#6bnYNE z+w>p!Ge&y@GG}y>151`!dJkvBFL?s$`X9muFPp+kR$AsMkXgYB`@anL{uaFAJ#fZ7 z3B&z8ROc9~`7}&;9Y)`XRUIcvETn@Q*U9I0N%y z!q0mkUIX7F0o@O#ufnRYBI9}t8)S5`x=TiF?J@}JP z!aLspf9jddv;@ekbi`Ev{Jbw!sB1tw!ejq(^eR;RBN&d6p)jZ!3zP%FRxGX5z;Fjf zhY+lRUXnbAsd3cu`)CYTu0nWncBz&D*srjHw(8){1D|v*WPY&PA$Y%b_c+GG{2l%i zr?>&7z2O3jLBXP^g05K3*=ox5e39mxuD(+;^w~$yE_h7gSG$7oP?4hYJFg~CU z`_R*3Ez3*PR#>~9cU;C+xQ!#=FP!z$E~e|aL`nwiITTO(!pAwstNC}VbAX+;cIeVQ z5CBeoejI-C=i%4ygZ-a@XMQQXZU=ttU&HGY7`?qPx27Un%I9-}M*SZMw%Nyi>cJJ3 zwq6ABt3kg2rNYUx%KLBCk}&RFC*bh1`h5fEQrJr&8Hg1OR=~!y@!phon8JQIYwc`} zy~uy|-F*_XMs1o_rU8uB)fo_1Hz-!3mP{n#7!pUd**zafZT zOQ3H?D?QoP7eU(A{B6ACNT{HrA=5C9Kt-TpP-=GRz}nW#ziS&A_I+FMDBUR062vM_F2eXY`=i z6+4cGb`zfVB3K>4-5-W=3&M2Tpq9eQscTt2`|9(-GJO77xL-|Pg0;^kHTUx2clZ_!B{0j%)NVEGNFm46bE$X^^d4Dzw{&&T~!&Jw68B&+qv#_tkeJ#Ilb_B}@FifC# z5xD4*D0#f6OYvX+SpMRH18YLah;QRfJcAESSvmu(0k^xy@{>FUE<2bYxU_)k^9*#;fB%>LJ7{$N9q8v`(O4`Q3FGvC zq3u3F^h)>me85rT02!(iA$89MI0sy%x(t12;j#szXI;$9b7_sL2H)^9^dAB?JH)+1%H8C5fo9HNHJkYFN{~e%Si4N{Sc~(YV8PEv->f{os ziuI^Ufbl=I%F>w{@K&w&6VAZ&O3v^Q*skchnR+h^HgRsP1{(;|D;vY%LomKvgZM=z zefkS5v(1PKM!vIJ;?~A z6(x%Nt|kF#k*YPNE2kb001O|up!=5Z5BS!9d^2(9#TfM@;29WBVfC$83;hE~Q)n)M z3P3Ay_Z0KXx|H@(YTUlMSS-t&TWRE{Err%O06*z@eLVx0l#A2m#CFy?Jf&b#7!6@? z6-JjUc8#O>&*d*C9^W&0KVQHD6X!Hv@yn$P;Rj`jx0o`Z4czMXm@Do*#@?Rg12~rG zGk!UI&qv|Me+ABXrHLH>`CDi&jmzU1fZ^i~ct8G!0nd5C-^8kyp!6LW?wnjvyhW-l z>|TUc58@645d;nS>06gFc#cX9E;~EvSb*2tg=7R-3c3Md3!3Y)KJwQFF2Q&R>={{| zMoll9%4Cd|r8+)#ewpN8-FpD` zE`hB&_@maQ>s-#lb$B`-n7D-ja0_sf*@C+NH_qY>y}&FtSvUZ0x&NP|XjWQgR&yeP z=-F`Ro8Uqsc+Qijq4&{syUOFr{R}^+t=&)kM!<=i{wYEDI!QL*+__NdWIpI9SAP_A zmY}f&!?9SVu;~&NHEtax*L6G;t2rLKc+1G^jk9_Nh}zI<78ytth`Xtrd)wca;(8eq z8VCoF4In%wYfP0~&V1>N_Tk`)97Fw1QIYvE)E5)0UOVKYJej9+4;>e?bt}@ysv_yq z@scabL&0UY$ku=-Iv4c3dtX)BFY}}SODUEtJK#m7#3>8>6;-jVeog`~{G0_Iz^R+p z824XFv*Rqy01kIy?Krf%vunq{6vlz2KY+mj9PB}B1(uFO5YJa0vF;Fp2vjIbDoKjR z8coo#oIVw~1dEkAopq)-PoBhGcPw^xmDvo;27 z>5G+%@%9?{j#jr*3=bm?c21zRbOrSH1W0i#;45<;d`oBy3=YNO4fZ8P-@7VcSw3ET zR9%J0A{Mr}3_%m(RNh#}0!XW-FTHPx%V5q71g7}E0kno#gBpX`6^p4>rkTo9Dh_oA zY35iyI|s1-Yvlu9#yj~I{(_Sw1Fh$j7yRE_&b73qE4^maKiuM}#76A|7{DT=A$4--uAq=70mHxD3B&l{CPJ^%f zF6l_Di*n$=z-SNBb+LZbloY0FegQ-+$(~wQ;I?XU7uqC`k;!ScCB}*d8WHF~BBbn+ zSXJFFR%hbe?9oBZlBu$OJc6R5JbnwG;5EF7JGhT77?*cFGizpbSs+6Sh zFHeSW4XkrT{ebR9IPsJ44bOn%-vfVK^F^z=Bg^rh?|Z8`Zt4HA;8T^q|LM+CG5icy zuQZ7?G}_SaK&uVgmtok4&ho5RKUpnSfYWHXQ+*&7C>ugLEWo#DOw*&(E7A-o=Rt60^^P-4&8aA#3jfw|9nA`kGhypBzJ zlT)Z`YI${goDa&dWG&k+;o@gW_g&n#tl*|VOdGZ+V>&))z6ai)f8igzgWJF9 zMxJrcK7aa(ckx@ywiMe^`9arYK|d$l(C{$_-hG3c$ZUp+zLOyQJ`CaHG>(SQU6L3p z3M8W#cEuXxKv1QXV!VV)vjt&8$_`-whx=kR_aBCJ7q|KDotl&GHxvnvN%7o!-9rNy zbm0QRHeSjvdr*`cY$+PaT~*ZpJA$kS>BA77mZVJ#j1R?vPihf$&E0lEf`yp_IL%pJ z&%a=W;l$c$<}2Qo@3wUM1JA|G>wZff3U-lGv>w0>5d=BfGf+nU&X4lH!$*Pk-+KZ# zeg7Bo;?rHW2;eE3eCc~SY`*vp`1xTrwVm0TTyeSolr}&xw9CG{9K1vk7 z8m(Un=xKJ9f-n~FC?(b`P=&>af_mF&T_0VmC04G{7At6tw0e%l&|DGbiyx5qD=;f1 zaMH0WraQke-vIhUI6o4LW!=_((FHVPmwX9uR4_g5((deuWTrA;$KqB8tFrf70~bCC zX?6rFmT|OBoTAY9I{q0qu{+hH=eGtO^^S8l#}qAvIdf&zxt;;Aui-)fYZ2TKiyZGv z@aQh{U&797*7@MiJ(Xv@`Zit^hIF*6r;sP2AVH2~snXeI_Lx1R!C(4WW z8tyqm4SNwB3*mTf@ocF|pFg$=d#6d>dYTtq*I{Klfxd#%&EjAh5li2B8?XN7AK>qm zw#jm@uOAzht>I%@3k)6;-~s&NTYrdV>ve=-Joni)1#O_&fI!b^F3+rx%(HxHDz2*5 z-em~|fLS7)<^7&yQnivK&foyXeW~ARRcH#K1fV(=nwpMWVIAo8;mQE?@-ztj%RO{p zJfPrcg&MBxfNeOdIh6e{+=4+LKJf{rTYgv&B zUe$1SfH3B?x8HPuNWr)P?FMW$$tC&Q1nCV#D)%Io)lVx7*PleOrmqq+19anpQWsffx zJlaS4Y9cK)#GnJ=D(H@E5a-U$*&D0CrAsDco`np!p8Lt%K443QZm8~PtxLS*r(>C- zbx8XOm;gF$h(qV)d$z_ZFegDDmxT7@MWFUlxMEU>uegO*fBU<5cf}qN zcph+h9cDGL-)GeV>dz|hKDvD~=j}TQ^((>JNjG=heiVs+F>|$(F$@RL+lT(4Q|a;w zNNuEpJWvuhRjr~h5&$+LNwcXUmcu3nU@xV%2SPUDif zG8J~eyDD7?Ne{Z$LA)^=UisySat`B)g-cQGmF7Q zBHyH%u-+-)lOp<4o!oxS);rWoZsv=B{QZ2Q^eJT!Rs{pM!cCi2cIP81O!J^*Xxk4-o5(sRH<tNKiP6_72kV?>y?(kZb;Azhcg<;5{%jq1>9MsX~k z7Nm=~aIz0)uzv}*_bK!H3|;L@W2CZj)SkIlDNy02Rgd6G3Ujn32#qz` zf$kM(_F?G|Rub6XD2A1xU4li!Qlm)5kZVV2ujZ$r-JrSEfwz8)3#9;Nk>phsC-1Y8 zdIly4 z%H}bVG|QJ??x*RRf_W}Y-6;VtbMKXvDt7@}oab-x-*^dk5tGeiFsqbl^LNPYZqKEA z8b~)njBpN=%Sfh5Mo~m7@)fj;*)lC9)%9&y-iLTJjq)%NE4jbyj==#;2e8_3ZIy9R z+Et}gYZwwd`2?rG`gTqYQU)Ksz(Ix8n!B{Ud_l$50$21Y$46UuCsG z>Ttaw92`jEBy_Snxjssk3MH)Fs`ra>X~3W@S7lz3V*(xnySh-8wk0cNRRK8Tj)%ax z$SHhGK^j$4E9)9v3~pl?3GjA@aCRWuyPr2F@~mB5o*?rQFKuSESO%rkZpN4LhkPr) z!ZUf04%u8fNI9pGkE95^l+Tj0d@2wJ;4qL=8pqIGE?%ItxSQ<*Sib^|q)29$mv@lD z!Lpp%yr7-|%?O&I7t(t7u@P`94N*vN{VJPZc{{g%?+bXwhtBiB{Z~1pEL1alg{ZK8 zkBd5=;(@)>v|7K7B0yW`ahI%Bl`+rB(r3?!yJ<4w!Au9mGQAZ4kRKs zbF5u{-_78PD&!<8Q~&w@l~}>V5hZ#GKCWS)>x<`gz%xo{WON{F!Y2>G_$BGcyKCl~ z@G*tFNmZaZn7r=>SNJymGhfCZv&M){Md+lD0Kzhdjqu4JU2*YlZ_SZd?JFgBn z_ZMd-GR1WxMeDjUKcm)9KPpzQ)B2BSLbPM01Zt6%ba89NaC}RcC08#>(LC{*XQQY% zFRH>J7^jlzKT6Z~*urjZ)oOq@b&0f-iD<$Z4Ik1X<-VwPS2gr9bH#KTA_;FA3p|{8 zJ$d5_YgZn7XSt3Cc@=NuDSUKN2@IJZ7FU%r@jfxX<r}?+*4I=h9t)R!b5u)UZqTO;IbH zXY~LMT*IqU^QZol|98KIvq2FbGRNv=bAV`&jPCsxJgaq7jm`W+cf3_t_dz2zxWX&= zRi4O$gw92*OR&sKyp$`{z7W02BTg6Ihk;woL$Fst?RhSyrhep!ch`vPndQk9C7=qm zSU=^;J;O09YhlpfO05Hg7?e1GMiJ-;`+FbZzTN($KV@F`yZvm&f5E2(yf^>PWp2Cq zwFKr#jQbEr(;WnLhpS4-T!UP>m1U{f9UKT}(`K-F+|>dr@=*r{(u$->)hSJ^D7V^R zeulE#c6lL#bGhR6f_O4pl=TEITo$*rmT*y3yU1(o+{! zBv;^Q;z+&|>`O%WGdY(M=!_v6fa(cI^2ozD4_;LPuLQz0hMhIYyh>dL3^G|-p%yuu z$U+==j6*j-B8r6o)o4g0a(tEbC!M1CmiKaR%@uhhgE{%!0N)S(pyE4U^{q6c@8Z&h zm?@+)KhqJ}(D$6%f`%=YMZ^W>+CfUKwd*}`eq>aUgqa0jgAuRfSLl#V;<80a6}`X$ z=5e1-Ez70k$1Vf92Mk?lG*D8vJ5(@M;t=V&wg~U86zr}E2>r)1pf`rSktA87z%{M7 z_)m8Qcv0+RQ@z%sTTXJ_*&QDGzys{mf5E40^?u{GznWI-zk-c9Jdpmg z(Fk^T#8Nezv%sL7Eg!(yGjP}ws#vSzB=?laM}%&X=8deryaFstI9x-R(~mk~yh`ULrqRkw-bpBRO4^DU=t< zb=w5L^vB#g$0s?8`}vqMT;y~-c<%XhyKkN7kZf!e)G%XU=c?4nx=T{{u8QJwUCl6* z=cA;)w{>#b%`_I0v}jz#`5;LJ@Tv?(KZ9-WxrP3Lar zOqYp+;l$k%D;BV;VO14HlcU^vZAhX8UbvP@ja6{NszfXk>K}y#_34FAzK$E*-r>)L zuPO{x(at_2+|Nf&FT1?GMtA8yPcc_oO*nBvirsmlWH^LgPm(2-GU)C*w_0M^dOg^_ zDz50}G4UUag>m9!8=U1CSV{T&gGJ`e#M>{`^H|{Vm1}}o0ye7m3afY#b&1LxfbA{V z8#7D&@c{_411#t55{f7Am;7~pg(q>IekwT}c>n+)07*naRK=@&W1e$pl2V+PAH{(K zS=VJr8y+h#pS{rcK$rtLu+UaghFHm~m37NC9~_m(h9T^43emc9xA4xfwJ>sHF~Xbz z+h!W+-f2C)&icj@%U2I(Qp4Gtl(AvH0@`N{eDbx!=x-5*H_pUyK>*#3%LzN-qD)uk zjVuL7<5i#c%N<-o-ix6W(Qh%R3bOCh)Q6YL?r-CsiDT z5|$51$yud2-E7%`(5HTDprc?#c^zHevNhI~|7*In`T z7aHDCXwS{`EoH;Kmv(D^nQB+lc-~lfx< z_|P5DT6JRQ_UpLr$NmP-`e$$EeI=k(fv{?ueN^4fqZirFUtMAQ;12e7KZG$^EwHTH zlClNI7KM@-ZMGz%X{@v@hoQX3VPCo(Q~<+3<|G^e_QL9{ zWnFbZ&2c#acJ>$Sc~wef%U)?a!52z8%~?)y0fn8etT;e~E_N$dl5K_Eg<_)6mIoxi zXRj>LlsXqxBV(gC6f~S=DxcO7FjR10#VKlg2eW&-@D4(t8^T%xnn7_Iw5q;TqYOrg zH$&zSk!`FHAdN$UZ+sqi-gcS|w;#1?3h-5vk?NE*KBL)A#Rosy;P~->OSAO`n(Z~T zy#=kSUu&G~u+m4cqptX)+zf=!?X$hU#DH-VvKI8i`b0)K^`95hchz)lZeoFQ(zGe4 zWYk!)D;9R5qr0kAZl+&;NK@E7r~qFTTcx~9Y#=j#_BngPWy z%hGr*j(e-J>3(l`3ng%ms@yxj5!<*ck{OvqG#E&SLehdkI9sBa2Y``K-!oeGgetQMfHSclTwfqkLnBi`{YxB2R)Wc zgZsFZJ|PX#DdXEL9YXGakG=S>yo$OGY`#0`r+|7CvJ*Kl+g^r$Ch_FJIdoQUq@W$b zTId}>2h=*WQgM1B1+i=MTnmK}I535f+{6;P15b^_eFtNaCR}aH`=|r&083%PlW*k2 z=?zxyzc5MICSG7Y_Wlk%KvreMKCReKFSL39fA{YtSi3f{R!QP5EH#@Ws*9zRW10-T zubeN~(HQpjgxz8h8rU$J9!fGV#>;=-RmA|6@-0cFo|yLw?p}hcXW;TDVE3GWGIdeO zAeCzFyqIuamXxZpg{^Ql)SMTTsUQ}(9+`C40($gla}Q4@s5qpoq7s2B?^VJveH>hLE{dnSJMBC7TM)P zK61}%Xm|ea$wyu|FB!_vQ}{Rv_7jBmP*XkJr=r#R$%12~;lwF7qppViR<-+& zDmHav1#pAD5RRj!Bwa2V@jvDf`>V#$4YERlud)apxt>AiG)3#KV?7hsIvI&*?r!+OAitJ}Eb6lU&|1(D$Q-O^8(hb=zLl&!}mo3mZbtDkAU`kfrI^7b9_RwGVR$@V8 zowP+T%YR-}E1a5>s$}+hs-3Gns&PAY#mTE8KUV$!6g|%Jr+hJA$8S%7SB@?8EJ=i} z7Y8ntz?m1*!wTnj?sl()yBem3*Kp}nEuX)W!j;r%!Dw0WBLht(G_sYZL@b@y zW3M;}1WJUzwFBwQ##k|8;O!q^$66X*>Cy+F?{-$T8<|I=?65fI`_kv0VQ1%ih2rHB zELnE6kIZEmwzq4>1}I6aw7R0fa#cAFrmp9o*Q$Uy@!QNSKBiiJrrR#HlT@y9UyoYq z$fX>S$$#DqXB>2;*(&$1sSC;#)0kIbyo; z=O|3%A_=q2I&bI8>C>3mx2VXS`vajv=YHnh3RRP2=O)9wWjqStl1n%zRr`vw9fLKv zJcdgn*vTZTsl0tDvrfsz9f;Ph+)&y`jVmjUGNz{wsG8L+&W-(l{|R@#*U9;_@USyx$a?hOq6At?>kd0tGD6GsRxuB*))yx%nmRDUAKF5ViOub9Ds8b*Z zd;lC=fMKsb+gg6i%!%Fd&EzPV&dI9IAlaAB{A@)8DorG##l~4p7Bde}TAs5PdWp8c za|VNo%hm)=@*!^J-8`@Iv{eXoNN%w>xR9IINo2Nq%hLRbr5>cLff+-60^EwRbTt1~> zc~d$m_RdII;V7I`Hr+hz zSO;4C5B>@t*q2-7o^v(hQ)cs1!G1J8xEffwHUZte&H;Ux#s{LS}PT=vq|SEp1V zh&vPhs{C>1X0CQ%t5eKMoviG-I*6Ns8;1lpZL#*xzKc6Nu*Ee1^mo9c=AJ%n@VN;? zA3lTT15;-zP}0e8?kw!@6{EY#Ze320whL%kIhYsq8`oLD))E}M0hU)_=Ko&uh6#DqE>g%M&O z&I1p*pI0b4*UK*Fd?v#_P5wJCXNRRpqij}F%*u}>t%*0v3nD{Aa1Zhln)}$HmAG-> z+&`b&nK#+y?x%_CS86+kwsVq(1`ZOqJc8}9JI1o@shVM(GYM=BByzr-I~g0xv~Rm< z(g77$?0RLc=cAH%IXbc0*bJr9ndC*>gySdRJw)J7!@`LZ$EQh_6Gjhp>as zI1r92cx!cEM2ZLZ^90__3ugAPtW-hHG@jlV20rmP}CSUux}i!nIRi_T5e+>8ORB&r|vb3<)3MPA;-F6YKeZ`?rTFW~x`x#T=(*)2+nZ3Z8eb8&UKL zmZeoYS{x;m=zBK8Qal&9**DJe9a|Bp`Bcj?OSKG88Q6N@nI`CypxX<4IWKLJq%clV zT2Gqz>*Bt07EFD*iI<~;@27s^5Mv%>e;`Z|rK@A7IF|Y8wiG?9pnkHe#SMd0)vleX z0b-SVYCR@N+!6&eN~MGHnk|E+pGgT?Mb(lje)@0`11H6`T#qZfmB8bbC@iUKY)Me-yy?!WU{Dc=%;x`el>0ALhhPWe{aGfb$pg!DvQy)< zLoJ(5z12>3*TpfPh232TJT+CAt=t&qd4L&CBK{KC{+whB%&HZ*sgcBJI%mM_mH3>nek&3qdNba^p< zJc;Y_u`H`4C-?%>T@QZFV#%$j97i)rUP#oowFSB0fkE7w4y(2d@KE zr!zgu>!>W3%1o?ra|m;(p|X%)xnpI*5_j=lt`R+~a6Od=&H>ZYK;=N1T;w_Y8ADb% z!{$`uYv0?GGB2t$-Y5IF$UWygg=kV%&ggrGW)*ZIXa&<>+3F&k_D@GJZs4bA`8u5! zV^vTIw)}yVf*_5WPvNTiIUG-y1pIMb!-2FM=@7zhF^T9Xy?y@PVrwWzGqI(DPD}FJ zI;t1qWzI%1Reo!LYJ)z2?7)kd@V4vF16YD|&t0XE7{W{SmU}tvB!9ligFwO+);Ymt zzLvM~mAv1L!32e!1S-a>@#W;|ltMBPcLeL66-=FFERPQhY<1aEg|HmXcD85M?a`&> zIW};Z$UMFn0iO!s>cDAE`HVz=WMR3L!&Lax`L7W)mMM6j3ZD5i)?WMq*5C0iE_wiq zD&oB|?WhWX_R(`YAAInwJZbsICnUR>IA0ar=k1PTve;=JP#Vn2HTdghl-3*J@fZ%a zZVIsq#n~#j4}dxJdwM2K5yeo_zN~DTG-NxNYR&!2ue<`bunGTJ3UujAtWCU z=7z{ls*`z~mZ+j(1T7@%rAWZ%FHhT&2xDYxa<<+ziX&KYuf5tANeQ2yuBtZCDu2#% z4s8S~o^}>bouu{bXRz{)cX7D_d||~ss3`fHM<0A!r*0?9iZi2v*+mz`kRHJKbH%jS zi8f0F@w#*`)Ww3`HeB2mA*Hg9&{>gfa_5p1@>2`#HHf=RN&3wrTu%YQF?dy8f2$}b zCZhVw-Cd-*Z>Bj3SqIE1h}td*?j?u5?OV8E_kiK={2u4d zo?}=8x;pSxMVJdg7|ah(fiHjQHjU;9dc7yHyBiWUowT37yK7OVj+)|YL=E}7)s-F5!8!mZ?o3x%A{`0ngFwJIN~C`;Y91YUI@-Ay!Y76Y?!c*2Q{drQr}cS7W6qtVNcK$5Wu6c(I}qd5SpV zNS#+^1@ujYbErJ8U+IZ6q)wiobKw$u@A(t1P%o^MFe?)Bi03S=Ap%XxPjVv%iYehI|RNxGEF0cs5P(8YJ73UFmqj}D&}8(rlBui(AhP(imShqq9z z=`waM<(lKvgr(PNuc)3Y@r}KdVAJWQO!SYn#8d0B3`bF=EN5O!Yy{Gso|fcY6);RC zwu|zep(UReg~EUtxQ%g?Nphy$D)sSInGgf&*4Z+bbv#s)ImI)#zP{+$Y)w}eD+$*A zJ^fTl4)bHahYtTHoz5@HA5GWd+=QJy$w0=9`o)+3uH68XAe>KR&TP@BYb!G9x>U;v zT}hzj=J};PIeTd&S4jo)hyE&CGm|}E`&z!Mxg;y#D8fnQvcuF#*#&zM>@Edi+Ef6~ z@3zZk-n7YpTR6k_sWpf#34q!wtX5?`Ye6_4e529b zG+S2*^ah!6Qz=4NUx(gMf2?@{O9=?`$@-@K+xvlLI4yDGHK`NQVfh$f&fz;W$4bKv}8bfaY@uBoZCuUZ` zT31vh8*T#aq-Lt+6lMX0O*q~trU2A&|3fSM&H=8?v8Gh4JZ~pd!Uv5)IaWo{P!d-3 zLfH_Kld#)^?z()Yb?YcgoyB%=3AG%1e$34l;s5nrJmKHJp1W%b?NJ5KQ#0FgGS~Xe z2j8t5AK<>7kFd7-!YOW~L1Mb$P|jWu)@J|#nh_j2@gP;B5S@=5%}R+tTe5n_b3Mz{ z{GcJnb>7VgUOj+SD{-g_WQ#VLwD*jLkS^EHPtU*S(hBTcl#H%*_Q4nz2kL71`H8#J zhrtC%c3p{~ikWRH+)Sz4t#E;xI75d4Pv;)KhIeyJjk&4ifamS{sglUC+klpm(OgwN zXHG;j8MK>{?7J{>K+}blw4U`dZZ^DH1$z?Wn_(-RuB9mVnU&d5_ub-{@Xk9}t0}lgHD!p5 z`D$18sNgI1ktDeyosVj^X4Ytmy-u=1A-W2DI*=SHM4ni$Erbd&D?MC zN(Vt81*VH3Ty8*P46A(s_c0^e<>3YCJIR7-dG{%ilK?i?VE2ju&Wznv?t*nAL8dmI zk$|qZ4}$|R{s1jQeY2vkHQnc*@OqxlT||rtFf22mt?cvych)CUBG+)iLfgRgf!DC1}zMgJQ|qEBx<3SB_Z6j3ajxEfTslT0U{~bw_<_v zx?SYt-+et7V;u=w7bC@5F|4%3RkT**2GbMJxB;Rr6VX5AW!Tj`4vGi(Fho7=X>6^n z(%RW&xPb9ec0J8e3+{{SeEWNEXMOz}COP9WFR~fS@DGE?|JAWuw3a!3Vd&-+1khX( zSJqgMcT@^lbN9wjHKa$j`JR>SdFTRxRt#%n*a_jV0eem7%bb&-B;5qN0m)d-qu=yu zl&G`WpvZEzT5{#@AHsvNJg?n{ZqKy_v_Wk^vIqN@T$08xt+|@bO^0eyzJ$Nvukm)i zn7e5*o|FwDprzsZjsT%t-Zd^2Z>?1Sn!u(T#XLEGZc^+nGRk7?%CQ~CP|+>-d?S03 zYIx$aHCwaQFpl8*b@9$;!iBuB?qW=9A!sp?`)dMWI$hU`1Cf+RNp&VM(pou8g^Q7z zqD!rxKs1KtF=#&BIaWS*p+ZGC@Aa<8>rXu|3<;yS5_EOeFXa(R3oZhm;)`#6BcD9@ zm$W+@V&THswEgNb>>av!P2qgPaS)0n85?1(M4>F!#JIk{1frH`c;;@Z&&KkLB)Kcv zaVsm+#R=@h5Lt=WMizE7+yu(bw({XY{4fKY-bch zvN9Ro($6!^dyWlR>BH8NPb0>xu+200FyGG`ImO{*u0hx13o9kk=s{V$&Y?3GsLg4l zS1qhnba_vdA06eiZ^UI>cgrJOlf@aTIWfHZ+Xj$zMX^JbEUa~aH(FfbVOYrcbdR~O zpo(d7zBuk$(|u21SHnRQ)N$cdw1E@HrSLvJg#IqXCn4PMNI`Q3d0Aa>L~@WNsW@-V zrknBZZmkx<$}-JMm!{+;{60S)fMcpV+~*eFkEU3?v;A6Ho!3{E4DuvJw+s7y;b7W1 ztOM)S_2P!Cu6AcSb{dS}@Sw2j2C41o(p+@yS-=>XBCFzp!J?InA&U_`f?fc9 z1xM$Q?K5yyUu)svlQ0Zi19_f_w3MXTn3Z?01D@|%#;ox$&*lR>nS1$WexFKaHv7Wy!)e%nRz4O8682COzDo0?{F<}(D?BX_1VCodmGo_pbXSIQkad6oFU z2)D_W8hd*&jN|FyEym@eZMl8EP7$RFaK6=xS>ht!&ad-De3++l4{I1=k1Od{Z;jhK zR+>^1n{ny9D$^L5ZD5=`w-vfh0`GZaC6;cirzw!jL5Ym)TZ}UBoOnUHBpSy-sVgCE z2#YQ!I!%#AgW0y9D3F+P--(Uq0_Md`5QsNmsU;Tb;82{SK_bsxcBup9ynviatW+uT z0

zZLr&M`*93?mx72RC-UoUPFJ@{K6dY9QqiI=hwxM3dLFHx@1740_$y!Y9^%HA zNT#!0&vjG?Co+l_^B))WDv)(WNf0V^le*aFd|io;Uu z8%&pajh6jn-%SGA2w=%?UJbiYP?Z1xAOJ~3K~#&YoMdj?vIo9Ku)s_4Hd-5iU2;x{ z&rDHZI}*>SXb#P*lz>r6_Sn9Gov~BtH=AoUbnXg8yj$J3oh=+ULAx|G0S7)B*nh_y=)&IPB2oZ=@L~Ci;m3o4Ce3x zihcI|@6CAOOFzQt>%NkQ&o7aT9Wa7fOK4o|MmV2-Rj1WbSXuo%CHwru zovc{;Hj+ACk_dpa3|3a)(w1`!g3hnZx%iPc^EGm>;3# z186kbpog@Yuya|qo{bZ-MfqfC?J};J^D*Yp&lXX(%!0?`Caj;7n%&_6?6=&^hJEM{ zL<%`;fLVsD>4K^VwlXQm4=n_J*trVkxcE&eUZyge1GWO;9rjsfpDw%H%Evj)MV`f7 zd^zu<>Aj-s?!M-9AF)`?MT=E4tty|>xs(9eWbz)7u6V91pDH(aa*~@c9=W+3p}3#M zl`~b-W3Bkd(`0sxQJt!#t}ZFHBl(PmtHBxTtU?BS5;*Nr1Tl15F7iNtG)jczG?kF^ zN>xUQHC|s8;?tEKS-Qr;DQEqTpMbb6t)n0K0E6%UAwG8H3WjRA@Upmtp3~dMJhsa` z@D-Qg_5bxWcRuqMh@01O;yTzoAs`!#g&q+$izZj)0*|CjoL<0#JW6+Xng5h!^0^}g zm#vJr8STsCaaXhoMtvBZmwuA8ApwgCT!Sl;Qx6AH3^$u#S~Urk)H`K{Rj%+v?&CJ@ z<+=PRPv>rKkzes9zO1z(B)N;H0F=u4B(7lh?)l?6ifol27(7 zFKLq%gN}75)5i19?{tN4(I3duSC?gfZoBEV4_{>X5B?wSz3VOxOW!7#v%P%ELo<(N z`NGhWBuk<|W!^J{zQ+N%@ z55~OZT}wRk`FBo|BI!T^1e7FTvPi7WSUUlb7_yPn-5OfxTa!{k&aPEb3)Hdehz}ik z(aobWVUdq-BGuun#EJ=&B#3ki;x%aOz}5Sqw+`8s>$Pi3&e85WM=+Y=x820O{3zeg zo!m{65lalF{0BdYSN1QO-Wn}my`c+XN5DBN%#=o5H%9;$VmNfCNIN!5!*L5xBG0Eh z6~fGw9>$UEHOITfP$)DkEy0xl&Ygo}fy9bhQ#n;=Cd(St0I#ahzNo`(p6>J5T@sf+ zOkBJd1aNG_Z8mNt>xk*OW@SyPfSI{xjRx%ONEYWu zs@&L|pP7mk?C(HyI6dt(pYRq1vfzVIT-I9c=6R0UJjYGf|9@~jWKWTEw09_)#!E|Z z>bOg`l*d@q{q$F}E~sle1Guuo=&SxJ_uT(UhTdu!xmDFiM7Q_d3fo0Gn)lp&-#N$w z3Z<*GRN$S-cpSy@l{@gVQrb^KoBm;s@B6m@$PfJ7-{#~^D+~spJ1*d8h>lBEVaN?m z;xC#mp6B(upvE25rN8q5zG2>;q^4i*yCQK6%ctPjUV?c~%B2>h>j73NNP_G5sNST< zm+fBm0Y|z5lZOBTP&Y&Tu}q#kqqxOR&rUH5@$vox>80g z*;8%2jY2I*xTrZhRo%S-{We^_?0Wx{uu(QwJr}#8t~XU8LEO}FD#@A9OPXsfna+)4 zG&VO0@Bd__m8;XoGt@V(*+(PhtLoVu=fQ#El!o zt?H>nb&jr@b0kw{#X>rkI$ti=Tb({&{Goi)<-2HP8bJq`@WB;Rr z$&uOZ@jv3s)<5O$^)323#7^8qT`HS(Tv}$$`P>nO9K+CoE_Hxhr_ed6a;cPpD^7f< z?J}MNC82QwtmXytKt3=Giy7q`9g){t?MNmsUJ`Mj%0;i|T*-2Fw4A;4I!wZ*qu%Du zN=rE9BbQ@4L?VVr6;!Rn7W~irv0RHKv4wc9R<69-LPl2HDONn^bVe&saEg4KCeoG2@R2k_7upzs#lc=cnWa@|xbrUD|=?hUWKS$wn>A-u5b<{a0?9Ncib#s?rR=Oa%qNP#QaCz@$#cQ;THb z)rVXrbwz%jdD~Uq`orhA{p;6QzPU*mSvc4 zyaLUWj+z#EBv<4ZJ8hY-Fdo6B9f?dDjnbOZ#oAE~*^`%JShDi^{X+q5KKCzgSBfLl z^Pfle{eRBBwKEez{H#GzkyWVTOR=cXzNpSuPU@9ae#RxsH$B7Z`@Zo_{QP_VK2P|< z^~$dFGW)Gc$~}MX|9ayE9{li-SHA8h;W4?i-O?ru`j)*b(#O|sDIR#w9=rDsc<)aQ z78Nc|L-Zfv$#)3ZqtZys1D^lblDqG?=sRQq)59(Pv2%x2AmCfEt~<)r3&6SF7Hb6 zd~aXsekZpagUnEHhlcm!weZEJ)K6{pt1D=@!x+(>;c>sJ=V!rZJx}>JO z%H%nN!<5&2<}7=q8V8!H(wS!ohU72aPH%M-u&8!c<*oQ0Pv1CUFK!4 zKE{`P%Q51X;^KoNe(Ogc;?gHaZUrbV+}r19f)D1ouy)G=;OPam@ABhoK+|bi22Ny% znpnuXWJ%ztWdI!u%Z`KD^jyxPsC3m?S6}4t(!Ei7l3-+Tm)OQb(gb``Q|!C z6cmrmuaJe2NW6~r<+WPPewK+K=rXGyRTbU@t_Tmj|IgWf=eszlXiZ7y0I-VmYmY)L zn+HBykvFQ^5t0hvP5=M$xrBfGH9yC>m^TT^ZH|G*D+>29RO>jws-ZykjrNV z>|IH?|AT%0=q*<`|M6mMKz}dgH-6+He&^quqqVHreRx>YXE9H%$>1t{H$dya9qj5H zi+2>@<=x()k_=#$3(0yzL-JuAIS~k_O|s$1%|yVu>#GZtOFXn{+&@)qU2>I+iFI;6 zGv#J51hSZeD&QR@3#n?Xl&y)w>K@CVf9SsW=A4{>mDn$E+Q~CZyx|h;b`Iz$>Y%w> zn;Ps}euc}IE=_n6rUblX4)E%%UY)4QBPz118t|3(SX*vnUqTpW$a(gNXPYi>!>$o)T4QBjSsxZ3_72d<#(McNyZNH&h7}d+K%1fj$AX5Yl0@&^&?QXbn*Ts zm@cO0u3@6!N+OCLorYLjrRQ)o7FpFrtECELb(&r|0GF5^9O(UDA?%z^OZuHBhn`Dl zw-?m;D5VHpAHW~}A^*F*GmEk0s?YfERMlO3o1V=x1GceQ%o^liK!k`8uo4NOC=UN-gb!9 zx^P3vxbv^M{pHS+AA9f_9((X4L3o6y)&|;SX^YV?cZIAoDakDygM~6?VG{y{dP;FQ zw624Ld)YGKI;k$cGx(kKChoUHpb6bdm4Y_Z0!6zK%8vldDcPFQy!UU&ToC8)zl zDRY^5bSQN$mt*<&Pxa*2jR^KntDQj#>d14YZrGxtA>}UHqN-rt@9x7l>o7knyPm_* z*6&Pni;!F{F`WEga`!h8N!H`(vvB_dochQg^7O{W#8T#V(#^y%_dDmB&$f_1Mr$_u zOsT;sj&VtCr%A#v(Vt9Hd%`u1-(~hleUZ#1-0oU2Pd*Os+N<8D+=^0Yz2xbV{R`n@ z06Owi5kPH9E{U3eOEV><8^P2e1w6~xB}2~ovOv!I2sPnX$N$o*K{J8XfgDxrYROc1 zkjgPZBa}@H;UYte00Wk5Owjz*q-(|ZMaaurUK(?MD+DgZ((bO`L zyrZeks5m3L`#)Iz+)bRk_kLDNL_60Ck(qKP+etaMw^C$J?`-!MwPjta;4xcU!jXlY zMFHbLH}A5s5XOq=L(q5!yehC<*jw5^Rd+P##=Z^;s8e-GZ4K(mQM)0pHe)lXOm;m7 zA%rt9J?EHps{d8v$J1G`fm`Yz}2YZit`l$zgdd96y1#)m#xJ zul*+f5~(nmY{;%;RJZ@#S|~RXf&i9QiT~sa9DmQd+4JT%GPQdi(Ny*EMu&7|op`NB zd}5K&Q%C6@euDmIzrYC#IWf`BRi8)0$g=K|Za3xJZp~+2*HQ=Ya{JY|;FegfE{&Y0 z;sKvEg>+DVtWmE4d;;Dl9H*4-9TaQ%uyu6iig=+MK*ue~I0vG5Lz_hW|P&WO{3}6M0pV8{v zQ6L>_Po+`-ZfYG4?3HYmFw|b_{KO65fgiB)nOj)A<6EqK?G8@=Hi+5x6#?uZ^U2+n+?RAz@&q3Kslq7^Y&^e3qW%VEO10W7bV9Q+MC9} z-Zq-gm{gn9!+uf(zQ2cGeLK9x8Ob!TrEFPlHEpGhA!YgO-{3V?`M+N z88jqQB@7+4mpaal;H)xe>cp~h8U-F;xImNH@1Lqedn7%}yD=P^ksQi;RDuhY^C65E zHPh0|UM#z;gGAdvZ$WfGzUzBpX!WHQ*TQKz#Gh)*+o>gnqm=BK6ZF1z4`*(BfYmcA zQr&88o#d}R$8T^Jh2m%xL5^cuS3x(*$2%yA8CP3w*A6FWb&mL($Lq zn$h|;nQ60I*CJcaM{a6c5)H;>*-Y@6O&ym~k&FDA0O=pVwZ8=)nJHMG3yq6*Ppb0!)XlKG zBaw$zW|^4`66hv!?${Yhk)gd)Qk%0DDT1@6vY!a~zxQG}0uX9%PRJUIULr3L?UsD5 z`2w#9)H}X^T>E9rUQsnqK6W$De*RX@q$%krChc@LJyyn|PO|~67NXxL>Gj8=U20)# z*~+B8#uDr()?j{HQ@0KE{d|i-my$>=S>btiJB6d6Y)*U32R zGB`by>0lycni>Rfu%#kj=6EuR_D&bM`Q$rpIc-bGvBaKPS&OZwAe3-XU4g5iE#lUT zhNQOib!wbtl1ffyCIUo~kgj$}Zu}z0K7Z@xC;(5xE$I))`UCIQfvsm?i+VlvmUlHy z=fKkKbfujagyG@>APx}fbz$IXdL%~5>5}0Vo>`*}fUXW`dM)gog*Rsfd~V^W19NY> z01LNA)hyY(`o2T2Bv7LU@7A{gj11_ht?MY1AVk1rEh)WcJ|xi`e8NC3-V$qWBbut`m}OgXJ!z^q?rfQokjZtPT**Jp6ksDw$~}XPKFQ%@^d5Vf?#c#f zvjP8j8>bHclzz8w^-D`EAB##5*9E)?e0r1{I7rl`M-1oF?R30F6Cm^ryUlPv#BMw< zJmAx>TCQ4e>;-=<9B9HTh6RIkTdyP2Lb5QeU32EjoEnyA1=IPUfV6D+xdu{3eKnR{ z(cMkh*Vq!6MJ@lY#IQD0i@VC{b$71)sYgK{{mRGiHLCwKk{nS&wMH3flI(#Zf`?M* z=BxDc#4=Z2(CQG;6!&00$)5D_7$0nd^1EKjl%)WyTzODm&royX7 z+xxZSNqI6*m~zYDI}qt>`mPh&7On{i^xBoeS2 zLjl80zFf5JsQT^lStTi`^OG`1 z=r1B0YOg~uCF|QF5Vs>Khu9g&X3EA;e*LApSp18xv9PpGTri_eEr2sS=ZRws;JWEN zWfOen3Q^DKdYW^-Z7tHC5BSPPLQ}dn_RIr)z!Q!up)cBv0d@rJgEzDpS7;d-q>)JL zlBWXZ)Hb;=U2TI&8bEy=vc2kOw^RS%V6SMmZUV=LQqp{PQ!@m~x3!gcQY0i8_O)Q4 z2WR?{$#S|U0o#l$liI9A>&?cO47G*Sf|+2ddDsA~^hiH_7Ym=ihsCA!O+EfXVuS@~ zu>ppWqswl)XzRakXx#da+mLZ5Q}f&_eVi}&e34LKcSFw{%@>;-50*N!$Q6A;DfHD4 zeF^?(20oNovc3G>GE>iaC1{kHj!7Zb-M890tjLMQE!-5rwOEGbb*=tGyQF1-tW~mH9SkM17TAH86Mx&w(BH38JPM+M zPv26dyZr=KQ#hzcEnl*)J|ASVxwCItT7s<(V09$fD~(_q!*cA2f0n^_o?vazM7-u8 zjfG`~qXbcp2=>oWKe&sqGhp=GS$dzolM`S0E=%~iw?!Rs--f}+GXzgOU3V8FO9$b| zLn})-4>hw34Zfn$MCuqz_!2YQ>EPO67`x)!0dKkn{sp)qmzp%kU1r{qYqRVLrDc^s zML#j_PvA-`qXDe10`qDYZ@W=1?{F&DvowJHb&V)&TvIcXxsu8~!lgtyzXl1+HswIR z5m{`;e2ok;gkCHmQ`XuoWp+LI^f-uLJxf6`Wq z`#_gCBAO0{p+N8ik{z>*>u|&19po<-m&2<|Y5pRUuSi?!c zVA5(Tv5YcOM}y^@(oVticZd>Smyz7gia=*Pev|5a7>?|RLuQ^kisYI~qiFltC#|6U zT(2fqhN7C>WM8kz0eKi`LTkQtcu{blO-)+Azl~MzWo0OVIjxK6hD@qOfm`xj{p|Oj zW$o>s<#5UFr0t~kMLS#bqX=}a4fDh^hRQ6R^DS$5MS?|QoRyM~1a~FUs-hD}K(_0uek}0>a=|tcXqqL$^D25S zTkS~t;7!#H^7F*2<%oFdd8H}*`Q1E+-ICWRO{jXo5(o267Q-qObb@(}2DpedsLfF>v`T%`kxHHW+^D zA&##PNk8!myyC5gXgky2+-xd@!)NGz^4mQ3!v(sdgzVTV!_IJm%$_(>M&B}O(Fmca zu8VQo1}B#Jn`_gc$QO$X4Z0UH@|6LRIEQFg>bM=cx5JUE;Fg2%@c`+KuzLinfqZvQ zBE#aWFk~4D=5?u%JhockXL|%dTNx`2M^YHK)sP$IE_YSIR`=C4gh?Z~@nMdC>Jb)W zsXw`y0cVg55_v*>%|7(`o_nB1%STj7ne+1 zQ%_T+Y<=cy+GQ6b0PIxoZAi;8BXhFIXT$!4FGc++|`+M@ic97HWy>XRoiVhYF$*oUFzV&(U8x< zmov&*z|?i{@(;i}-wE&i8F*U*7&c@_FbGs_7RmeOln7B?4<><38JnS@q2h~twh=-d zztOO3yItM)&@vm>-Oc0LiQ*a}>uvx1`?i$d{A(`o#YURh*MQe!d33$LL*)mfk*vUA}WY<1&>F-T?;J#>oJx+ zz|#R|-uevfe&7HA2I@&fK~%V9@@yjjbKBbmysiPjo)H<%jRCWJ(sT*6-lAUnwUJUj(`=c{PQNAK%6+is>nY0u zTA6IrG}Pm3Z>qEFZB6FqLh2ux=bCzmh(ef(jZci>_tV#wkcEUvi*(G0yoe z^vuvCOX(XF`T}o$0-5a*@leXMNb{<8HlEVefzhKwhK~+e0)ki9Xk6ds&{KWZ>mkvV zO{QleqM6#3e~0fMow#2~@MS1%lwkbl?mNlK-~5>4-#N3npkbz(CMbHA&ZEwO%y0_~ z`pj^ABL>?7YK+_4;9LgXFDbz7gvjTMet{PK^toRi0rT6miiT6#eYw8qgv8;Lpng@2 z)^aM9+h#)QuWT^=;eEXJt<&t9uUXc4g!Y?^(>_zFW}5E_Dn`qb>-0YQD965enzgmj z1X)=JMBh=D=>Qx#z-JChe35TtNm&P;rb+7QRKoK-=&r!`!ls|OLsFiC>L4_Mmy7*2 zw8!e=z!OgA4MFXrhq&g$`+5D|+LlM(=B>em1R;SsLxnOxRAwkQ{e%<0^Dxh17Z5fh zkq){JlRn~l^wU$kqR5w6Mh!knnaR1#3Zh+6lO(%TPR@v(b?7IoU|hPzJY0&#E{)Mp z8)IyaGzJ;rZ7Ur4;9jnXQl{sEtu9v<7{q8rpccy8yrvo~-(P0^#={)@x~%PrBpA(3 zWXIAFu?fIp3_EPXh7^o3e4F2?r&C!3uIy4g8KWHlHkZ*$;)(udBfxAf=#einBA+Rh zA3oV*_;8QowSa2|8SJeqPd?+-@5H3XhYT-k(#!(sJoMB$o!@WVYd#v zyzReX05DHIM@9{1%MxrltTuX?&xxwcJZ{H;ZYM>)BA-E9WV8WKPw8?eB_H_mdNAsx zM1Oaj?|tej4h|w_TPf^Pulyh(T^=$z+GG767C3ge!|+v8OwH7&9cog$=QJxnT9Yyj z*bP2LMuSCL$$8;6K-S_OGAe(UU`xr4cL+$utm0mXtUxXnOQ5 zspf_e`FfUyiw%5W*G~0qfBWcP7iBG{0^dvTxtb@JnJ((!svEb6N&D*1Qupb%%^LBB z%Xs4r`+56)D?IVZV;nKMrzW6XMlm*0^T1K(Z8+*ZQ~cGo7in9JvPu3GIRs}F=yo>v zTmW=2Ud+g4M!U$8YPue;gR5bQs0|CiO?4GplO+d`KHRpkPIO*n-;#2gDPdVZ+9Lhb zD%w@u&WZXDM-7-c8Vh=g7g{=;A$Gc&4nA8d+q8T>WPF671Hjc}85qDb9d?Rbc5GlN zBj4y6idUekBHy+nplch?*2&bx`D`Dff;zIPeQX+0&~$2|pw_byooyx`E^1k70Y@44 z2Dk)iUlsUXSOB_cR-vU);{vT2{agTS8UW0{Z>F8LC~W!(=p<%yp>=2hUPAFYj!!{u zM-9cRwDZEIpn4b2OigtmUtL|kXyw%etgHEA6AIG1sgcob)WK(athqIABU0OdS7hp# zZR54909S$Ug4BVgsGe!*N5R*w298gO*=37u1D|bAZ-{?wCuV$;{k&C|E5KE0=fz4x zaipWJRF#4>bZqWtq@Z=ihxhuZ!M34pQo4#Z(-TL;J0IXwfUCgwyn@j8(N{N`UCgs;bomTa zpsT?5l6j3piO1L^EPRa^b9QJB*i9Kg+b-XBxRn*~D)3!Wub1!u(FCDMM{@yBk9wLO_$mhsa$+Y zpFe#Cyb63f`t|j7L~Zjzhac?N=M}fI0$+7(i^f@}Ix6s8s;_;S_v0nuLr@(R_;$j3P=T%jUv*rh_oM<>byP=n nR7Z7GM|D(3byP=noa^!bPGIgYRSNZX00000NkvXXu0mjfkBB&U literal 0 HcmV?d00001 diff --git a/doc/images/installer_background.png b/doc/images/installer_background.png new file mode 100644 index 0000000000000000000000000000000000000000..5d74c972b1a59e110ab2c50e8c811f68c82b6778 GIT binary patch literal 26492 zcmV)WK(4=uP)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^Rb0u~hpD>W(ATmS$d07*naRCwC# zy?MMfSy|uvU8{yYp81@2>=C*f8`@@85D-Cvs8J&-Voa1I8sim{OLCK#pi#`nAyKc< z3_^@Z6k?2CqY@2jKoKs8qBKJ{v@}h3zeB&{nfF*#Yvqq;)vmQ`SM7Zo8k#2W{(Sc5 zoU_j!YFDk_dZyp=d*Isb+U?ry+U?ry+U?ry+U?ry+U?ry+U?ry^Yx~#oq}t(M{$kV zYp3fP_^#a^+v|G`gxA1#?V29par-E*3wsTWN4x23r{MDjaLNOXBKRg<@%WM#j(8l`cFAkE$K^GU_vk#h0^KgAbpuv!F>Jnd@n?K<2w(qH(CaX`AAb51aF_ev zw?_~J@W#=c_puef+X`#9=Q)96+Z~^_kwML`6wiR1nCDM>w?aXbp&L1rTk`HpL8$r^5 z%snr2KuOL*_71r87Wm=!%>kLIc>Ed&uYvC|O>WHl|08>^jSt;|y8V~2;nOi_4|K&n zF$2ubkHItkEj(nW-|H7&1M5EkRtdQL7g`_z`tBt|AOZrw7%&-(e;@wScfvhmX3^{a zZfd~ze>@1Uf$!5lU-Ln8{Dq3^zBZzHeSnG;nGG<&-d$tHhl-`A$AnuE-KT#6bnYNE z+w>p!Ge&y@GG}y>151`!dJkvBFL?s$`X9muFPp+kR$AsMkXgYB`@anL{uaFAJ#fZ7 z3B&z8ROc9~`7}&;9Y)`XRUIcvETn@Q*U9I0N%y z!q0mkUIX7F0o@O#ufnRYBI9}t8)S5`x=TiF?J@}JP z!aLspf9jddv;@ekbi`Ev{Jbw!sB1tw!ejq(^eR;RBN&d6p)jZ!3zP%FRxGX5z;Fjf zhY+lRUXnbAsd3cu`)CYTu0nWncBz&D*srjHw(8){1D|v*WPY&PA$Y%b_c+GG{2l%i zr?>&7z2O3jLBXP^g05K3*=ox5e39mxuD(+;^w~$yE_h7gSG$7oP?4hYJFg~CU z`_R*3Ez3*PR#>~9cU;C+xQ!#=FP!z$E~e|aL`nwiITTO(!pAwstNC}VbAX+;cIeVQ z5CBeoejI-C=i%4ygZ-a@XMQQXZU=ttU&HGY7`?qPx27Un%I9-}M*SZMw%Nyi>cJJ3 zwq6ABt3kg2rNYUx%KLBCk}&RFC*bh1`h5fEQrJr&8Hg1OR=~!y@!phon8JQIYwc`} zy~uy|-F*_XMs1o_rU8uB)fo_1Hz-!3mP{n#7!pUd**zafZT zOQ3H?D?QoP7eU(A{B6ACNT{HrA=5C9Kt-TpP-=GRz}nW#ziS&A_I+FMDBUR062vM_F2eXY`=i z6+4cGb`zfVB3K>4-5-W=3&M2Tpq9eQscTt2`|9(-GJO77xL-|Pg0;^kHTUx2clZ_!B{0j%)NVEGNFm46bE$X^^d4Dzw{&&T~!&Jw68B&+qv#_tkeJ#Ilb_B}@FifC# z5xD4*D0#f6OYvX+SpMRH18YLah;QRfJcAESSvmu(0k^xy@{>FUE<2bYxU_)k^9*#;fB%>LJ7{$N9q8v`(O4`Q3FGvC zq3u3F^h)>me85rT02!(iA$89MI0sy%x(t12;j#szXI;$9b7_sL2H)^9^dAB?JH)+1%H8C5fo9HNHJkYFN{~e%Si4N{Sc~(YV8PEv->f{os ziuI^Ufbl=I%F>w{@K&w&6VAZ&O3v^Q*skchnR+h^HgRsP1{(;|D;vY%LomKvgZM=z zefkS5v(1PKM!vIJ;?~A z6(x%Nt|kF#k*YPNE2kb001O|up!=5Z5BS!9d^2(9#TfM@;29WBVfC$83;hE~Q)n)M z3P3Ay_Z0KXx|H@(YTUlMSS-t&TWRE{Err%O06*z@eLVx0l#A2m#CFy?Jf&b#7!6@? z6-JjUc8#O>&*d*C9^W&0KVQHD6X!Hv@yn$P;Rj`jx0o`Z4czMXm@Do*#@?Rg12~rG zGk!UI&qv|Me+ABXrHLH>`CDi&jmzU1fZ^i~ct8G!0nd5C-^8kyp!6LW?wnjvyhW-l z>|TUc58@645d;nS>06gFc#cX9E;~EvSb*2tg=7R-3c3Md3!3Y)KJwQFF2Q&R>={{| zMoll9%4Cd|r8+)#ewpN8-FpD` zE`hB&_@maQ>s-#lb$B`-n7D-ja0_sf*@C+NH_qY>y}&FtSvUZ0x&NP|XjWQgR&yeP z=-F`Ro8Uqsc+Qijq4&{syUOFr{R}^+t=&)kM!<=i{wYEDI!QL*+__NdWIpI9SAP_A zmY}f&!?9SVu;~&NHEtax*L6G;t2rLKc+1G^jk9_Nh}zI<78ytth`Xtrd)wca;(8eq z8VCoF4In%wYfP0~&V1>N_Tk`)97Fw1QIYvE)E5)0UOVKYJej9+4;>e?bt}@ysv_yq z@scabL&0UY$ku=-Iv4c3dtX)BFY}}SODUEtJK#m7#3>8>6;-jVeog`~{G0_Iz^R+p z824XFv*Rqy01kIy?Krf%vunq{6vlz2KY+mj9PB}B1(uFO5YJa0vF;Fp2vjIbDoKjR z8coo#oIVw~1dEkAopq)-PoBhGcPw^xmDvo;27 z>5G+%@%9?{j#jr*3=bm?c21zRbOrSH1W0i#;45<;d`oBy3=YNO4fZ8P-@7VcSw3ET zR9%J0A{Mr}3_%m(RNh#}0!XW-FTHPx%V5q71g7}E0kno#gBpX`6^p4>rkTo9Dh_oA zY35iyI|s1-Yvlu9#yj~I{(_Sw1Fh$j7yRE_&b73qE4^maKiuM}#76A|7{DT=A$4--uAq=70mHxD3B&l{CPJ^%f zF6l_Di*n$=z-SNBb+LZbloY0FegQ-+$(~wQ;I?XU7uqC`k;!ScCB}*d8WHF~BBbn+ zSXJFFR%hbe?9oBZlBu$OJc6R5JbnwG;5EF7JGhT77?*cFGizpbSs+6Sh zFHeSW4XkrT{ebR9IPsJ44bOn%-vfVK^F^z=Bg^rh?|Z8`Zt4HA;8T^q|LM+CG5icy zuQZ7?G}_SaK&uVgmtok4&ho5RKUpnSfYWHXQ+*&7C>ugLEWo#DOw*&(E7A-o=Rt60^^P-4&8aA#3jfw|9nA`kGhypBzJ zlT)Z`YI${goDa&dWG&k+;o@gW_g&n#tl*|VOdGZ+V>&))z6ai)f8igzgWJF9 zMxJrcK7aa(ckx@ywiMe^`9arYK|d$l(C{$_-hG3c$ZUp+zLOyQJ`CaHG>(SQU6L3p z3M8W#cEuXxKv1QXV!VV)vjt&8$_`-whx=kR_aBCJ7q|KDotl&GHxvnvN%7o!-9rNy zbm0QRHeSjvdr*`cY$+PaT~*ZpJA$kS>BA77mZVJ#j1R?vPihf$&E0lEf`yp_IL%pJ z&%a=W;l$c$<}2Qo@3wUM1JA|G>wZff3U-lGv>w0>5d=BfGf+nU&X4lH!$*Pk-+KZ# zeg7Bo;?rHW2;eE3eCc~SY`*vp`1xTrwVm0TTyeSolr}&xw9CG{9K1vk7 z8m(Un=xKJ9f-n~FC?(b`P=&>af_mF&T_0VmC04G{7At6tw0e%l&|DGbiyx5qD=;f1 zaMH0WraQke-vIhUI6o4LW!=_((FHVPmwX9uR4_g5((deuWTrA;$KqB8tFrf70~bCC zX?6rFmT|OBoTAY9I{q0qu{+hH=eGtO^^S8l#}qAvIdf&zxt;;Aui-)fYZ2TKiyZGv z@aQh{U&797*7@MiJ(Xv@`Zit^hIF*6r;sP2AVH2~snXeI_Lx1R!C(4WW z8tyqm4SNwB3*mTf@ocF|pFg$=d#6d>dYTtq*I{Klfxd#%&EjAh5li2B8?XN7AK>qm zw#jm@uOAzht>I%@3k)6;-~s&NTYrdV>ve=-Joni)1#O_&fI!b^F3+rx%(HxHDz2*5 z-em~|fLS7)<^7&yQnivK&foyXeW~ARRcH#K1fV(=nwpMWVIAo8;mQE?@-ztj%RO{p zJfPrcg&MBxfNeOdIh6e{+=4+LKJf{rTYgv&B zUe$1SfH3B?x8HPuNWr)P?FMW$$tC&Q1nCV#D)%Io)lVx7*PleOrmqq+19anpQWsffx zJlaS4Y9cK)#GnJ=D(H@E5a-U$*&D0CrAsDco`np!p8Lt%K443QZm8~PtxLS*r(>C- zbx8XOm;gF$h(qV)d$z_ZFegDDmxT7@MWFUlxMEU>uegO*fBU<5cf}qN zcph+h9cDGL-)GeV>dz|hKDvD~=j}TQ^((>JNjG=heiVs+F>|$(F$@RL+lT(4Q|a;w zNNuEpJWvuhRjr~h5&$+LNwcXUmcu3nU@xV%2SPUDif zG8J~eyDD7?Ne{Z$LA)^=UisySat`B)g-cQGmF7Q zBHyH%u-+-)lOp<4o!oxS);rWoZsv=B{QZ2Q^eJT!Rs{pM!cCi2cIP81O!J^*Xxk4-o5(sRH<tNKiP6_72kV?>y?(kZb;Azhcg<;5{%jq1>9MsX~k z7Nm=~aIz0)uzv}*_bK!H3|;L@W2CZj)SkIlDNy02Rgd6G3Ujn32#qz` zf$kM(_F?G|Rub6XD2A1xU4li!Qlm)5kZVV2ujZ$r-JrSEfwz8)3#9;Nk>phsC-1Y8 zdIly4 z%H}bVG|QJ??x*RRf_W}Y-6;VtbMKXvDt7@}oab-x-*^dk5tGeiFsqbl^LNPYZqKEA z8b~)njBpN=%Sfh5Mo~m7@)fj;*)lC9)%9&y-iLTJjq)%NE4jbyj==#;2e8_3ZIy9R z+Et}gYZwwd`2?rG`gTqYQU)Ksz(Ix8n!B{Ud_l$50$21Y$46UuCsG z>Ttaw92`jEBy_Snxjssk3MH)Fs`ra>X~3W@S7lz3V*(xnySh-8wk0cNRRK8Tj)%ax z$SHhGK^j$4E9)9v3~pl?3GjA@aCRWuyPr2F@~mB5o*?rQFKuSESO%rkZpN4LhkPr) z!ZUf04%u8fNI9pGkE95^l+Tj0d@2wJ;4qL=8pqIGE?%ItxSQ<*Sib^|q)29$mv@lD z!Lpp%yr7-|%?O&I7t(t7u@P`94N*vN{VJPZc{{g%?+bXwhtBiB{Z~1pEL1alg{ZK8 zkBd5=;(@)>v|7K7B0yW`ahI%Bl`+rB(r3?!yJ<4w!Au9mGQAZ4kRKs zbF5u{-_78PD&!<8Q~&w@l~}>V5hZ#GKCWS)>x<`gz%xo{WON{F!Y2>G_$BGcyKCl~ z@G*tFNmZaZn7r=>SNJymGhfCZv&M){Md+lD0Kzhdjqu4JU2*YlZ_SZd?JFgBn z_ZMd-GR1WxMeDjUKcm)9KPpzQ)B2BSLbPM01Zt6%ba89NaC}RcC08#>(LC{*XQQY% zFRH>J7^jlzKT6Z~*urjZ)oOq@b&0f-iD<$Z4Ik1X<-VwPS2gr9bH#KTA_;FA3p|{8 zJ$d5_YgZn7XSt3Cc@=NuDSUKN2@IJZ7FU%r@jfxX<r}?+*4I=h9t)R!b5u)UZqTO;IbH zXY~LMT*IqU^QZol|98KIvq2FbGRNv=bAV`&jPCsxJgaq7jm`W+cf3_t_dz2zxWX&= zRi4O$gw92*OR&sKyp$`{z7W02BTg6Ihk;woL$Fst?RhSyrhep!ch`vPndQk9C7=qm zSU=^;J;O09YhlpfO05Hg7?e1GMiJ-;`+FbZzTN($KV@F`yZvm&f5E2(yf^>PWp2Cq zwFKr#jQbEr(;WnLhpS4-T!UP>m1U{f9UKT}(`K-F+|>dr@=*r{(u$->)hSJ^D7V^R zeulE#c6lL#bGhR6f_O4pl=TEITo$*rmT*y3yU1(o+{! zBv;^Q;z+&|>`O%WGdY(M=!_v6fa(cI^2ozD4_;LPuLQz0hMhIYyh>dL3^G|-p%yuu z$U+==j6*j-B8r6o)o4g0a(tEbC!M1CmiKaR%@uhhgE{%!0N)S(pyE4U^{q6c@8Z&h zm?@+)KhqJ}(D$6%f`%=YMZ^W>+CfUKwd*}`eq>aUgqa0jgAuRfSLl#V;<80a6}`X$ z=5e1-Ez70k$1Vf92Mk?lG*D8vJ5(@M;t=V&wg~U86zr}E2>r)1pf`rSktA87z%{M7 z_)m8Qcv0+RQ@z%sTTXJ_*&QDGzys{mf5E40^?u{GznWI-zk-c9Jdpmg z(Fk^T#8Nezv%sL7Eg!(yGjP}ws#vSzB=?laM}%&X=8deryaFstI9x-R(~mk~yh`ULrqRkw-bpBRO4^DU=t< zb=w5L^vB#g$0s?8`}vqMT;y~-c<%XhyKkN7kZf!e)G%XU=c?4nx=T{{u8QJwUCl6* z=cA;)w{>#b%`_I0v}jz#`5;LJ@Tv?(KZ9-WxrP3Lar zOqYp+;l$k%D;BV;VO14HlcU^vZAhX8UbvP@ja6{NszfXk>K}y#_34FAzK$E*-r>)L zuPO{x(at_2+|Nf&FT1?GMtA8yPcc_oO*nBvirsmlWH^LgPm(2-GU)C*w_0M^dOg^_ zDz50}G4UUag>m9!8=U1CSV{T&gGJ`e#M>{`^H|{Vm1}}o0ye7m3afY#b&1LxfbA{V z8#7D&@c{_411#t55{f7Am;7~pg(q>IekwT}c>n+)07*naRK=@&W1e$pl2V+PAH{(K zS=VJr8y+h#pS{rcK$rtLu+UaghFHm~m37NC9~_m(h9T^43emc9xA4xfwJ>sHF~Xbz z+h!W+-f2C)&icj@%U2I(Qp4Gtl(AvH0@`N{eDbx!=x-5*H_pUyK>*#3%LzN-qD)uk zjVuL7<5i#c%N<-o-ix6W(Qh%R3bOCh)Q6YL?r-CsiDT z5|$51$yud2-E7%`(5HTDprc?#c^zHevNhI~|7*In`T z7aHDCXwS{`EoH;Kmv(D^nQB+lc-~lfx< z_|P5DT6JRQ_UpLr$NmP-`e$$EeI=k(fv{?ueN^4fqZirFUtMAQ;12e7KZG$^EwHTH zlClNI7KM@-ZMGz%X{@v@hoQX3VPCo(Q~<+3<|G^e_QL9{ zWnFbZ&2c#acJ>$Sc~wef%U)?a!52z8%~?)y0fn8etT;e~E_N$dl5K_Eg<_)6mIoxi zXRj>LlsXqxBV(gC6f~S=DxcO7FjR10#VKlg2eW&-@D4(t8^T%xnn7_Iw5q;TqYOrg zH$&zSk!`FHAdN$UZ+sqi-gcS|w;#1?3h-5vk?NE*KBL)A#Rosy;P~->OSAO`n(Z~T zy#=kSUu&G~u+m4cqptX)+zf=!?X$hU#DH-VvKI8i`b0)K^`95hchz)lZeoFQ(zGe4 zWYk!)D;9R5qr0kAZl+&;NK@E7r~qFTTcx~9Y#=j#_BngPWy z%hGr*j(e-J>3(l`3ng%ms@yxj5!<*ck{OvqG#E&SLehdkI9sBa2Y``K-!oeGgetQMfHSclTwfqkLnBi`{YxB2R)Wc zgZsFZJ|PX#DdXEL9YXGakG=S>yo$OGY`#0`r+|7CvJ*Kl+g^r$Ch_FJIdoQUq@W$b zTId}>2h=*WQgM1B1+i=MTnmK}I535f+{6;P15b^_eFtNaCR}aH`=|r&083%PlW*k2 z=?zxyzc5MICSG7Y_Wlk%KvreMKCReKFSL39fA{YtSi3f{R!QP5EH#@Ws*9zRW10-T zubeN~(HQpjgxz8h8rU$J9!fGV#>;=-RmA|6@-0cFo|yLw?p}hcXW;TDVE3GWGIdeO zAeCzFyqIuamXxZpg{^Ql)SMTTsUQ}(9+`C40($gla}Q4@s5qpoq7s2B?^VJveH>hLE{dnSJMBC7TM)P zK61}%Xm|ea$wyu|FB!_vQ}{Rv_7jBmP*XkJr=r#R$%12~;lwF7qppViR<-+& zDmHav1#pAD5RRj!Bwa2V@jvDf`>V#$4YERlud)apxt>AiG)3#KV?7hsIvI&*?r!+OAitJ}Eb6lU&|1(D$Q-O^8(hb=zLl&!}mo3mZbtDkAU`kfrI^7b9_RwGVR$@V8 zowP+T%YR-}E1a5>s$}+hs-3Gns&PAY#mTE8KUV$!6g|%Jr+hJA$8S%7SB@?8EJ=i} z7Y8ntz?m1*!wTnj?sl()yBem3*Kp}nEuX)W!j;r%!Dw0WBLht(G_sYZL@b@y zW3M;}1WJUzwFBwQ##k|8;O!q^$66X*>Cy+F?{-$T8<|I=?65fI`_kv0VQ1%ih2rHB zELnE6kIZEmwzq4>1}I6aw7R0fa#cAFrmp9o*Q$Uy@!QNSKBiiJrrR#HlT@y9UyoYq z$fX>S$$#DqXB>2;*(&$1sSC;#)0kIbyo; z=O|3%A_=q2I&bI8>C>3mx2VXS`vajv=YHnh3RRP2=O)9wWjqStl1n%zRr`vw9fLKv zJcdgn*vTZTsl0tDvrfsz9f;Ph+)&y`jVmjUGNz{wsG8L+&W-(l{|R@#*U9;_@USyx$a?hOq6At?>kd0tGD6GsRxuB*))yx%nmRDUAKF5ViOub9Ds8b*Z zd;lC=fMKsb+gg6i%!%Fd&EzPV&dI9IAlaAB{A@)8DorG##l~4p7Bde}TAs5PdWp8c za|VNo%hm)=@*!^J-8`@Iv{eXoNN%w>xR9IINo2Nq%hLRbr5>cLff+-60^EwRbTt1~> zc~d$m_RdII;V7I`Hr+hz zSO;4C5B>@t*q2-7o^v(hQ)cs1!G1J8xEffwHUZte&H;Ux#s{LS}PT=vq|SEp1V zh&vPhs{C>1X0CQ%t5eKMoviG-I*6Ns8;1lpZL#*xzKc6Nu*Ee1^mo9c=AJ%n@VN;? zA3lTT15;-zP}0e8?kw!@6{EY#Ze320whL%kIhYsq8`oLD))E}M0hU)_=Ko&uh6#DqE>g%M&O z&I1p*pI0b4*UK*Fd?v#_P5wJCXNRRpqij}F%*u}>t%*0v3nD{Aa1Zhln)}$HmAG-> z+&`b&nK#+y?x%_CS86+kwsVq(1`ZOqJc8}9JI1o@shVM(GYM=BByzr-I~g0xv~Rm< z(g77$?0RLc=cAH%IXbc0*bJr9ndC*>gySdRJw)J7!@`LZ$EQh_6Gjhp>as zI1r92cx!cEM2ZLZ^90__3ugAPtW-hHG@jlV20rmP}CSUux}i!nIRi_T5e+>8ORB&r|vb3<)3MPA;-F6YKeZ`?rTFW~x`x#T=(*)2+nZ3Z8eb8&UKL zmZeoYS{x;m=zBK8Qal&9**DJe9a|Bp`Bcj?OSKG88Q6N@nI`CypxX<4IWKLJq%clV zT2Gqz>*Bt07EFD*iI<~;@27s^5Mv%>e;`Z|rK@A7IF|Y8wiG?9pnkHe#SMd0)vleX z0b-SVYCR@N+!6&eN~MGHnk|E+pGgT?Mb(lje)@0`11H6`T#qZfmB8bbC@iUKY)Me-yy?!WU{Dc=%;x`el>0ALhhPWe{aGfb$pg!DvQy)< zLoJ(5z12>3*TpfPh232TJT+CAt=t&qd4L&CBK{KC{+whB%&HZ*sgcBJI%mM_mH3>nek&3qdNba^p< zJc;Y_u`H`4C-?%>T@QZFV#%$j97i)rUP#oowFSB0fkE7w4y(2d@KE zr!zgu>!>W3%1o?ra|m;(p|X%)xnpI*5_j=lt`R+~a6Od=&H>ZYK;=N1T;w_Y8ADb% z!{$`uYv0?GGB2t$-Y5IF$UWygg=kV%&ggrGW)*ZIXa&<>+3F&k_D@GJZs4bA`8u5! zV^vTIw)}yVf*_5WPvNTiIUG-y1pIMb!-2FM=@7zhF^T9Xy?y@PVrwWzGqI(DPD}FJ zI;t1qWzI%1Reo!LYJ)z2?7)kd@V4vF16YD|&t0XE7{W{SmU}tvB!9ligFwO+);Ymt zzLvM~mAv1L!32e!1S-a>@#W;|ltMBPcLeL66-=FFERPQhY<1aEg|HmXcD85M?a`&> zIW};Z$UMFn0iO!s>cDAE`HVz=WMR3L!&Lax`L7W)mMM6j3ZD5i)?WMq*5C0iE_wiq zD&oB|?WhWX_R(`YAAInwJZbsICnUR>IA0ar=k1PTve;=JP#Vn2HTdghl-3*J@fZ%a zZVIsq#n~#j4}dxJdwM2K5yeo_zN~DTG-NxNYR&!2ue<`bunGTJ3UujAtWCU z=7z{ls*`z~mZ+j(1T7@%rAWZ%FHhT&2xDYxa<<+ziX&KYuf5tANeQ2yuBtZCDu2#% z4s8S~o^}>bouu{bXRz{)cX7D_d||~ss3`fHM<0A!r*0?9iZi2v*+mz`kRHJKbH%jS zi8f0F@w#*`)Ww3`HeB2mA*Hg9&{>gfa_5p1@>2`#HHf=RN&3wrTu%YQF?dy8f2$}b zCZhVw-Cd-*Z>Bj3SqIE1h}td*?j?u5?OV8E_kiK={2u4d zo?}=8x;pSxMVJdg7|ah(fiHjQHjU;9dc7yHyBiWUowT37yK7OVj+)|YL=E}7)s-F5!8!mZ?o3x%A{`0ngFwJIN~C`;Y91YUI@-Ay!Y76Y?!c*2Q{drQr}cS7W6qtVNcK$5Wu6c(I}qd5SpV zNS#+^1@ujYbErJ8U+IZ6q)wiobKw$u@A(t1P%o^MFe?)Bi03S=Ap%XxPjVv%iYehI|RNxGEF0cs5P(8YJ73UFmqj}D&}8(rlBui(AhP(imShqq9z z=`waM<(lKvgr(PNuc)3Y@r}KdVAJWQO!SYn#8d0B3`bF=EN5O!Yy{Gso|fcY6);RC zwu|zep(UReg~EUtxQ%g?Nphy$D)sSInGgf&*4Z+bbv#s)ImI)#zP{+$Y)w}eD+$*A zJ^fTl4)bHahYtTHoz5@HA5GWd+=QJy$w0=9`o)+3uH68XAe>KR&TP@BYb!G9x>U;v zT}hzj=J};PIeTd&S4jo)hyE&CGm|}E`&z!Mxg;y#D8fnQvcuF#*#&zM>@Edi+Ef6~ z@3zZk-n7YpTR6k_sWpf#34q!wtX5?`Ye6_4e529b zG+S2*^ah!6Qz=4NUx(gMf2?@{O9=?`$@-@K+xvlLI4yDGHK`NQVfh$f&fz;W$4bKv}8bfaY@uBoZCuUZ` zT31vh8*T#aq-Lt+6lMX0O*q~trU2A&|3fSM&H=8?v8Gh4JZ~pd!Uv5)IaWo{P!d-3 zLfH_Kld#)^?z()Yb?YcgoyB%=3AG%1e$34l;s5nrJmKHJp1W%b?NJ5KQ#0FgGS~Xe z2j8t5AK<>7kFd7-!YOW~L1Mb$P|jWu)@J|#nh_j2@gP;B5S@=5%}R+tTe5n_b3Mz{ z{GcJnb>7VgUOj+SD{-g_WQ#VLwD*jLkS^EHPtU*S(hBTcl#H%*_Q4nz2kL71`H8#J zhrtC%c3p{~ikWRH+)Sz4t#E;xI75d4Pv;)KhIeyJjk&4ifamS{sglUC+klpm(OgwN zXHG;j8MK>{?7J{>K+}blw4U`dZZ^DH1$z?Wn_(-RuB9mVnU&d5_ub-{@Xk9}t0}lgHD!p5 z`D$18sNgI1ktDeyosVj^X4Ytmy-u=1A-W2DI*=SHM4ni$Erbd&D?MC zN(Vt81*VH3Ty8*P46A(s_c0^e<>3YCJIR7-dG{%ilK?i?VE2ju&Wznv?t*nAL8dmI zk$|qZ4}$|R{s1jQeY2vkHQnc*@OqxlT||rtFf22mt?cvych)CUBG+)iLfgRgf!DC1}zMgJQ|qEBx<3SB_Z6j3ajxEfTslT0U{~bw_<_v zx?SYt-+et7V;u=w7bC@5F|4%3RkT**2GbMJxB;Rr6VX5AW!Tj`4vGi(Fho7=X>6^n z(%RW&xPb9ec0J8e3+{{SeEWNEXMOz}COP9WFR~fS@DGE?|JAWuw3a!3Vd&-+1khX( zSJqgMcT@^lbN9wjHKa$j`JR>SdFTRxRt#%n*a_jV0eem7%bb&-B;5qN0m)d-qu=yu zl&G`WpvZEzT5{#@AHsvNJg?n{ZqKy_v_Wk^vIqN@T$08xt+|@bO^0eyzJ$Nvukm)i zn7e5*o|FwDprzsZjsT%t-Zd^2Z>?1Sn!u(T#XLEGZc^+nGRk7?%CQ~CP|+>-d?S03 zYIx$aHCwaQFpl8*b@9$;!iBuB?qW=9A!sp?`)dMWI$hU`1Cf+RNp&VM(pou8g^Q7z zqD!rxKs1KtF=#&BIaWS*p+ZGC@Aa<8>rXu|3<;yS5_EOeFXa(R3oZhm;)`#6BcD9@ zm$W+@V&THswEgNb>>av!P2qgPaS)0n85?1(M4>F!#JIk{1frH`c;;@Z&&KkLB)Kcv zaVsm+#R=@h5Lt=WMizE7+yu(bw({XY{4fKY-bch zvN9Ro($6!^dyWlR>BH8NPb0>xu+200FyGG`ImO{*u0hx13o9kk=s{V$&Y?3GsLg4l zS1qhnba_vdA06eiZ^UI>cgrJOlf@aTIWfHZ+Xj$zMX^JbEUa~aH(FfbVOYrcbdR~O zpo(d7zBuk$(|u21SHnRQ)N$cdw1E@HrSLvJg#IqXCn4PMNI`Q3d0Aa>L~@WNsW@-V zrknBZZmkx<$}-JMm!{+;{60S)fMcpV+~*eFkEU3?v;A6Ho!3{E4DuvJw+s7y;b7W1 ztOM)S_2P!Cu6AcSb{dS}@Sw2j2C41o(p+@yS-=>XBCFzp!J?InA&U_`f?fc9 z1xM$Q?K5yyUu)svlQ0Zi19_f_w3MXTn3Z?01D@|%#;ox$&*lR>nS1$WexFKaHv7Wy!)e%nRz4O8682COzDo0?{F<}(D?BX_1VCodmGo_pbXSIQkad6oFU z2)D_W8hd*&jN|FyEym@eZMl8EP7$RFaK6=xS>ht!&ad-De3++l4{I1=k1Od{Z;jhK zR+>^1n{ny9D$^L5ZD5=`w-vfh0`GZaC6;cirzw!jL5Ym)TZ}UBoOnUHBpSy-sVgCE z2#YQ!I!%#AgW0y9D3F+P--(Uq0_Md`5QsNmsU;Tb;82{SK_bsxcBup9ynviatW+uT z0

zZLr&M`*93?mx72RC-UoUPFJ@{K6dY9QqiI=hwxM3dLFHx@1740_$y!Y9^%HA zNT#!0&vjG?Co+l_^B))WDv)(WNf0V^le*aFd|io;Uu z8%&pajh6jn-%SGA2w=%?UJbiYP?Z1xAOJ~3K~#&YoMdj?vIo9Ku)s_4Hd-5iU2;x{ z&rDHZI}*>SXb#P*lz>r6_Sn9Gov~BtH=AoUbnXg8yj$J3oh=+ULAx|G0S7)B*nh_y=)&IPB2oZ=@L~Ci;m3o4Ce3x zihcI|@6CAOOFzQt>%NkQ&o7aT9Wa7fOK4o|MmV2-Rj1WbSXuo%CHwru zovc{;Hj+ACk_dpa3|3a)(w1`!g3hnZx%iPc^EGm>;3# z186kbpog@Yuya|qo{bZ-MfqfC?J};J^D*Yp&lXX(%!0?`Caj;7n%&_6?6=&^hJEM{ zL<%`;fLVsD>4K^VwlXQm4=n_J*trVkxcE&eUZyge1GWO;9rjsfpDw%H%Evj)MV`f7 zd^zu<>Aj-s?!M-9AF)`?MT=E4tty|>xs(9eWbz)7u6V91pDH(aa*~@c9=W+3p}3#M zl`~b-W3Bkd(`0sxQJt!#t}ZFHBl(PmtHBxTtU?BS5;*Nr1Tl15F7iNtG)jczG?kF^ zN>xUQHC|s8;?tEKS-Qr;DQEqTpMbb6t)n0K0E6%UAwG8H3WjRA@Upmtp3~dMJhsa` z@D-Qg_5bxWcRuqMh@01O;yTzoAs`!#g&q+$izZj)0*|CjoL<0#JW6+Xng5h!^0^}g zm#vJr8STsCaaXhoMtvBZmwuA8ApwgCT!Sl;Qx6AH3^$u#S~Urk)H`K{Rj%+v?&CJ@ z<+=PRPv>rKkzes9zO1z(B)N;H0F=u4B(7lh?)l?6ifol27(7 zFKLq%gN}75)5i19?{tN4(I3duSC?gfZoBEV4_{>X5B?wSz3VOxOW!7#v%P%ELo<(N z`NGhWBuk<|W!^J{zQ+N%@ z55~OZT}wRk`FBo|BI!T^1e7FTvPi7WSUUlb7_yPn-5OfxTa!{k&aPEb3)Hdehz}ik z(aobWVUdq-BGuun#EJ=&B#3ki;x%aOz}5Sqw+`8s>$Pi3&e85WM=+Y=x820O{3zeg zo!m{65lalF{0BdYSN1QO-Wn}my`c+XN5DBN%#=o5H%9;$VmNfCNIN!5!*L5xBG0Eh z6~fGw9>$UEHOITfP$)DkEy0xl&Ygo}fy9bhQ#n;=Cd(St0I#ahzNo`(p6>J5T@sf+ zOkBJd1aNG_Z8mNt>xk*OW@SyPfSI{xjRx%ONEYWu zs@&L|pP7mk?C(HyI6dt(pYRq1vfzVIT-I9c=6R0UJjYGf|9@~jWKWTEw09_)#!E|Z z>bOg`l*d@q{q$F}E~sle1Guuo=&SxJ_uT(UhTdu!xmDFiM7Q_d3fo0Gn)lp&-#N$w z3Z<*GRN$S-cpSy@l{@gVQrb^KoBm;s@B6m@$PfJ7-{#~^D+~spJ1*d8h>lBEVaN?m z;xC#mp6B(upvE25rN8q5zG2>;q^4i*yCQK6%ctPjUV?c~%B2>h>j73NNP_G5sNST< zm+fBm0Y|z5lZOBTP&Y&Tu}q#kqqxOR&rUH5@$vox>80g z*;8%2jY2I*xTrZhRo%S-{We^_?0Wx{uu(QwJr}#8t~XU8LEO}FD#@A9OPXsfna+)4 zG&VO0@Bd__m8;XoGt@V(*+(PhtLoVu=fQ#El!o zt?H>nb&jr@b0kw{#X>rkI$ti=Tb({&{Goi)<-2HP8bJq`@WB;Rr z$&uOZ@jv3s)<5O$^)323#7^8qT`HS(Tv}$$`P>nO9K+CoE_Hxhr_ed6a;cPpD^7f< z?J}MNC82QwtmXytKt3=Giy7q`9g){t?MNmsUJ`Mj%0;i|T*-2Fw4A;4I!wZ*qu%Du zN=rE9BbQ@4L?VVr6;!Rn7W~irv0RHKv4wc9R<69-LPl2HDONn^bVe&saEg4KCeoG2@R2k_7upzs#lc=cnWa@|xbrUD|=?hUWKS$wn>A-u5b<{a0?9Ncib#s?rR=Oa%qNP#QaCz@$#cQ;THb z)rVXrbwz%jdD~Uq`orhA{p;6QzPU*mSvc4 zyaLUWj+z#EBv<4ZJ8hY-Fdo6B9f?dDjnbOZ#oAE~*^`%JShDi^{X+q5KKCzgSBfLl z^Pfle{eRBBwKEez{H#GzkyWVTOR=cXzNpSuPU@9ae#RxsH$B7Z`@Zo_{QP_VK2P|< z^~$dFGW)Gc$~}MX|9ayE9{li-SHA8h;W4?i-O?ru`j)*b(#O|sDIR#w9=rDsc<)aQ z78Nc|L-Zfv$#)3ZqtZys1D^lblDqG?=sRQq)59(Pv2%x2AmCfEt~<)r3&6SF7Hb6 zd~aXsekZpagUnEHhlcm!weZEJ)K6{pt1D=@!x+(>;c>sJ=V!rZJx}>JO z%H%nN!<5&2<}7=q8V8!H(wS!ohU72aPH%M-u&8!c<*oQ0Pv1CUFK!4 zKE{`P%Q51X;^KoNe(Ogc;?gHaZUrbV+}r19f)D1ouy)G=;OPam@ABhoK+|bi22Ny% znpnuXWJ%ztWdI!u%Z`KD^jyxPsC3m?S6}4t(!Ei7l3-+Tm)OQb(gb``Q|!C z6cmrmuaJe2NW6~r<+WPPewK+K=rXGyRTbU@t_Tmj|IgWf=eszlXiZ7y0I-VmYmY)L zn+HBykvFQ^5t0hvP5=M$xrBfGH9yC>m^TT^ZH|G*D+>29RO>jws-ZykjrNV z>|IH?|AT%0=q*<`|M6mMKz}dgH-6+He&^quqqVHreRx>YXE9H%$>1t{H$dya9qj5H zi+2>@<=x()k_=#$3(0yzL-JuAIS~k_O|s$1%|yVu>#GZtOFXn{+&@)qU2>I+iFI;6 zGv#J51hSZeD&QR@3#n?Xl&y)w>K@CVf9SsW=A4{>mDn$E+Q~CZyx|h;b`Iz$>Y%w> zn;Ps}euc}IE=_n6rUblX4)E%%UY)4QBPz118t|3(SX*vnUqTpW$a(gNXPYi>!>$o)T4QBjSsxZ3_72d<#(McNyZNH&h7}d+K%1fj$AX5Yl0@&^&?QXbn*Ts zm@cO0u3@6!N+OCLorYLjrRQ)o7FpFrtECELb(&r|0GF5^9O(UDA?%z^OZuHBhn`Dl zw-?m;D5VHpAHW~}A^*F*GmEk0s?YfERMlO3o1V=x1GceQ%o^liK!k`8uo4NOC=UN-gb!9 zx^P3vxbv^M{pHS+AA9f_9((X4L3o6y)&|;SX^YV?cZIAoDakDygM~6?VG{y{dP;FQ zw624Ld)YGKI;k$cGx(kKChoUHpb6bdm4Y_Z0!6zK%8vldDcPFQy!UU&ToC8)zl zDRY^5bSQN$mt*<&Pxa*2jR^KntDQj#>d14YZrGxtA>}UHqN-rt@9x7l>o7knyPm_* z*6&Pni;!F{F`WEga`!h8N!H`(vvB_dochQg^7O{W#8T#V(#^y%_dDmB&$f_1Mr$_u zOsT;sj&VtCr%A#v(Vt9Hd%`u1-(~hleUZ#1-0oU2Pd*Os+N<8D+=^0Yz2xbV{R`n@ z06Owi5kPH9E{U3eOEV><8^P2e1w6~xB}2~ovOv!I2sPnX$N$o*K{J8XfgDxrYROc1 zkjgPZBa}@H;UYte00Wk5Owjz*q-(|ZMaaurUK(?MD+DgZ((bO`L zyrZeks5m3L`#)Iz+)bRk_kLDNL_60Ck(qKP+etaMw^C$J?`-!MwPjta;4xcU!jXlY zMFHbLH}A5s5XOq=L(q5!yehC<*jw5^Rd+P##=Z^;s8e-GZ4K(mQM)0pHe)lXOm;m7 zA%rt9J?EHps{d8v$J1G`fm`Yz}2YZit`l$zgdd96y1#)m#xJ zul*+f5~(nmY{;%;RJZ@#S|~RXf&i9QiT~sa9DmQd+4JT%GPQdi(Ny*EMu&7|op`NB zd}5K&Q%C6@euDmIzrYC#IWf`BRi8)0$g=K|Za3xJZp~+2*HQ=Ya{JY|;FegfE{&Y0 z;sKvEg>+DVtWmE4d;;Dl9H*4-9TaQ%uyu6iig=+MK*ue~I0vG5Lz_hW|P&WO{3}6M0pV8{v zQ6L>_Po+`-ZfYG4?3HYmFw|b_{KO65fgiB)nOj)A<6EqK?G8@=Hi+5x6#?uZ^U2+n+?RAz@&q3Kslq7^Y&^e3qW%VEO10W7bV9Q+MC9} z-Zq-gm{gn9!+uf(zQ2cGeLK9x8Ob!TrEFPlHEpGhA!YgO-{3V?`M+N z88jqQB@7+4mpaal;H)xe>cp~h8U-F;xImNH@1Lqedn7%}yD=P^ksQi;RDuhY^C65E zHPh0|UM#z;gGAdvZ$WfGzUzBpX!WHQ*TQKz#Gh)*+o>gnqm=BK6ZF1z4`*(BfYmcA zQr&88o#d}R$8T^Jh2m%xL5^cuS3x(*$2%yA8CP3w*A6FWb&mL($Lq zn$h|;nQ60I*CJcaM{a6c5)H;>*-Y@6O&ym~k&FDA0O=pVwZ8=)nJHMG3yq6*Ppb0!)XlKG zBaw$zW|^4`66hv!?${Yhk)gd)Qk%0DDT1@6vY!a~zxQG}0uX9%PRJUIULr3L?UsD5 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 literal 0 HcmV?d00001 diff --git a/doc/overview/INSTRUCTIONS.md b/doc/overview/INSTRUCTIONS.md new file mode 100644 index 000000000..4b58dd373 --- /dev/null +++ b/doc/overview/INSTRUCTIONS.md @@ -0,0 +1,13 @@ +-------- Running FastSurfer -------- + +- If you want to run only segmentation: + +/run_fastsurfer.sh --seg_only --sd --sid --t1 + +- To full run fastsurfer: + +/run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license + +- If some files of freesurfer files require bypassing MacOS security, +- it might take a long time to allow access to every one of them one by one. +- Instead, this command might be used:

xattr -dr com.apple.quarantine /Applications/freesurfer/* diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore new file mode 100644 index 000000000..2f2aedca8 --- /dev/null +++ b/tools/macos_build/.gitignore @@ -0,0 +1,8 @@ +raw_package/ +installer/ +*.pkg +*.gz + +scripts/postinstall +scripts_intell/postinstall +dist/ diff --git a/tools/macos_build/FastSurfer.py.template b/tools/macos_build/FastSurfer.py.template new file mode 100644 index 000000000..e6048fd5b --- /dev/null +++ b/tools/macos_build/FastSurfer.py.template @@ -0,0 +1,14 @@ +from subprocess import run + +proc = run(["/usr/bin/osascript", +"-e", +"tell app \"Terminal\"", +"-e", +"do script \"source /Applications//macos_setup_fastsurfer.sh\"", +"-e", +"activate", +"-e", +"end tell" +]) + +# to-do: print instructions, dont print export calls diff --git a/tools/macos_build/README.md b/tools/macos_build/README.md new file mode 100644 index 000000000..15f93840c --- /dev/null +++ b/tools/macos_build/README.md @@ -0,0 +1,4 @@ +creating applet: python3.10 setup.py py2app --iconfile resources/fastsurfer.png + +dependencies: XMLStarlet, aria2 +python dependency: py2app diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh new file mode 100755 index 000000000..7a00d9a25 --- /dev/null +++ b/tools/macos_build/build_release_package.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +if [ "$#" -lt 3 ] ; then + echo + echo "Usage: build_release_package.sh []" + echo + exit +fi + +# cd into directory with this script +dir=${0%/*} +if [ -d "$dir" ]; then + cd "$dir" +fi + +VERSION=$1 # version of the project +ARCH_TYPE=$2 # chip architecture - arm or intel +DIR_TO_FASTSURFER=$3 # directory to fastsurfer +if [ "$#" -gt 3 ]; then + URL_TO_FREESURFER=$4 # freesurfer install url +fi + +ARCH_TYPE_NAME="arm64" +if [ "$ARCH_TYPE" = "intel" ]; then + ARCH_TYPE_NAME="x86_64" +fi + +PACKAGE_NAME=FastSurfer$VERSION-macos-darwin_${ARCH_TYPE_NAME} # name of the package displayed in the installer +ID="com.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) +INSTALLATION_DIR="/Applications" # install location for the content of the package +OUTPUT_PKG="raw_package/$PACKAGE_NAME.pkg" # raw package file to be created +INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installater to be created + +# create temporary folder to package and copy FastSurfer over +STAGED_DIR="FastSurferPackageContent" +FASTSURFER_TO_PACKAGE="$STAGED_DIR/FastSurfer$VERSION" +mkdir $STAGED_DIR +rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ + --exclude requirements.txt \ + --exclude requirements.cpu.txt \ + --exclude Docker \ + --exclude Singularity \ + --exclude tools + +# install freesurfer into temp foler +if [ "$#" -gt 3 ]; then + ./install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER +else + ./install_fs_pruned.sh $STAGED_DIR --upx +fi + +SCRIPTS_DIR="./scripts" # directory with scripts executed during installation process (f.e. preinsatll postinstall) + +# substitute values in postinstall script +PATH_TO_FASTSURFER="$INSTALLATION_DIR/FastSurfer$VERSION" +cp $SCRIPTS_DIR/postinstall.template $SCRIPTS_DIR/postinstall + +sed -i '' -e "s||${PATH_TO_FASTSURFER}|g" $SCRIPTS_DIR/postinstall +if [ "$ARCH_TYPE" = "arm" ]; then + sed -i '' -e "s||/opt/homebrew|g" $SCRIPTS_DIR/postinstall +else + sed -i '' -e "s||/usr/local|g" $SCRIPTS_DIR/postinstall +fi + +# assemble resources +mkdir resources +cp $DIR_TO_FASTSURFER/doc/images/installer_background.png resources/ +cp $DIR_TO_FASTSURFER/doc/images/fastsurfer.png resources/ +cp $DIR_TO_FASTSURFER/doc/overview/INSTRUCTIONS.md resources/ +cp $DIR_TO_FASTSURFER/LICENSE resources/LICENSE.txt + +# create fastsurfer applet +cp FastSurfer.py.template FastSurfer.py +sed -i '' -e "s||FastSurfer${VERSION}|g" FastSurfer.py + +cp macos_setup_fastsurfer.sh.template macos_setup_fastsurfer.sh +sed -i '' -e "s||FastSurfer${VERSION}|g" macos_setup_fastsurfer.sh +if [ "$ARCH_TYPE" = "arm" ]; then + sed -i '' -e "s||1|g" macos_setup_fastsurfer.sh +else + sed -i '' -e "s||0|g" macos_setup_fastsurfer.sh +fi +mv macos_setup_fastsurfer.sh $FASTSURFER_TO_PACKAGE/ + +python3.10 setup.py py2app --iconfile resources/fastsurfer.png +mv dist/FastSurfer.app $STAGED_DIR/ + +rm -f FastSurfer.py +chmod -R 755 $STAGED_DIR/* + +# create raw package +mkdir raw_package +pkgbuild \ + --root $STAGED_DIR \ + --version $VERSION \ + --identifier $ID \ + --install-location $INSTALLATION_DIR \ + --scripts $SCRIPTS_DIR \ + $OUTPUT_PKG + +rm -f $SCRIPTS_DIR/postinstall + +# create distribution file template based on provided package +RESOURCES="./resources" +DISTRIBUTION_FILE="resources/distribution.xml" +productbuild --synthesize --package $OUTPUT_PKG $DISTRIBUTION_FILE + +# edit the distribution file +# set title to package name (f.e. package_name.pkg -> package_name) +xml ed \ + -s /installer-gui-script \ + -t elem \ + -n title \ + -v "${PACKAGE_NAME}" \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +# set background image +xml ed \ + -s /installer-gui-script \ + -t elem \ + -n background \ + -i /installer-gui-script/background \ + -t attr \ + -n file \ + -v installer_background.png \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background \ + -t attr \ + -n uti \ + -v public.png \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background \ + -t attr \ + -n scaling \ + -v proportional \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background \ + -t attr \ + -n alignment \ + -v bottomleft \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -s /installer-gui-script \ + -t elem \ + -n background-darkAqua \ + -i /installer-gui-script/background-darkAqua \ + -t attr \ + -n file \ + -v installer_background.png \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background-darkAqua \ + -t attr \ + -n uti \ + -v public.png \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background-darkAqua \ + -t attr \ + -n scaling \ + -v proportional \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/background-darkAqua \ + -t attr \ + -n alignment \ + -v bottomleft \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +# set license +xml ed \ + -s /installer-gui-script \ + -t elem \ + -n license \ + -i /installer-gui-script/license \ + -t attr \ + -n file \ + -v LICENSE.txt \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +xml ed \ + -i /installer-gui-script/license \ + -t attr \ + -n mime-type \ + -v text/txt \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +# set conclusion +xml ed \ + -s /installer-gui-script \ + -t elem \ + -n conclusion \ + -i /installer-gui-script/conclusion \ + -t attr \ + -n file \ + -v INSTRUCTIONS.md \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + + +xml ed \ + -i /installer-gui-script/conclusion \ + -t attr \ + -n mime-type \ + -v text/txt \ + $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" +mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE + +# create installer package +mkdir installer +productbuild \ + --distribution $DISTRIBUTION_FILE \ + --resources $RESOURCES \ + --package-path raw_package \ + $INSTALLER_PKG + +# get rid of temporary folder +rm -rf $STAGED_DIR +rm -rf resources diff --git a/tools/macos_build/install_fs_pruned.sh b/tools/macos_build/install_fs_pruned.sh new file mode 100755 index 000000000..5a78ac5d5 --- /dev/null +++ b/tools/macos_build/install_fs_pruned.sh @@ -0,0 +1,423 @@ +#!/bin/bash --login +# --login to read bashrc for conda inside docker + +# This file downloads the FreeSurfer tar ball and extracts from it only what is needed to run +# FastSurfer +# +# In order to update to a new FreeSurfer version you need to update the fslink and then build a +# docker with this setup. Run it and whenever it crashes/exits, find the missing file (binary, +# atlas, datafile, or dependency) and add it here or if a dependency is missing install it in the +# docker and rebuild and re-run. Repeat until recon-surf finishes successfully. Then repeat with +# all supported recon-surf flags (--hires, --fsaparc etc.). + + +# Link where to find the FreeSurfer tarball: +fslink="https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz" + + +if [[ "$#" -lt 1 ]]; then + echo + echo "Usage: install_fs_prunded install_dir [--upx] [--url freesurfer_download_url]" + echo + echo "--upx is optional, if passed, fs/bin will be packed" + echo "--url is optional, if passed, freesurfer will be downloaded from it instead of $fslink" + echo + exit 2 +fi + +where=/opt +if [[ "$#" -ge 1 ]]; then + where=$1 + shift +fi + +upx="false" +while [[ "$#" -ge 1 ]]; do + lowercase=$(echo "$1" | tr '[:upper:]' '[:lower:]') + case $lowercase in + --upx) + upx="true" + shift + ;; + --url) + if [[ "$2" != "default" ]]; then fslink=$2; fi + shift + shift + ;; + *) + echo "Invalid argument $1" + exit 1 + ;; + esac +done +fss=$where/fs-tmp +fsd=$where/freesurfer +echo +echo "Will install FreeSurfer to $fsd" +echo +echo "FreeSurfer package to download:" +echo +echo "$fslink" +echo + + +function run_parallel () +{ + # param 1 num_parallel_processes + # param 2 command (printf string) + # param 3 how many entries to consume from $@ per "run" + # param ... parameters to format, ie. we are executing $(printf $command $@...) + i=0 + pids=() + num_parallel_processes=$1 + command=$2 + num=$3 + shift + shift + shift + args=("$@") + j=0 + while [[ "$j" -lt "${#args}" ]] + do + cmd=$(printf "$command" "${args[@]:$j:$num}") + j=$((j + num)) + $cmd & + pids=("${pids[@]}" "$!") + i=$((i + 1)) + if [[ "$i" -ge "$num_parallel_processes" ]] + then + wait "${pids[0]}" + pids=("${pids[@]:1}") + fi + done + for pid in "${pids[@]}" + do + wait "$pid" + done +} + + +# get FreeSurfer and unpack (some of it) +echo "Downloading FS and unpacking portions ..." +aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" + +tar zxv --no-same-owner -C "$where" \ + --exclude='freesurfer/average/*.gca' \ + --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ + --exclude='freesurfer/average/mult-comp-cor' \ + --exclude='freesurfer/average/samseg' \ + --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ + --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/bin/freeview.bin' \ + --exclude='freesurfer/bin/freeview' \ + --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ + --exclude='freesurfer/bin/mris_decimate_gui.bin' \ + --exclude='freesurfer/bin/mris_decimate_gui' \ + --exclude='freesurfer/bin/qdec_glmfit' \ + --exclude='freesurfer/bin/qdec.bin' \ + --exclude='freesurfer/bin/qdec' \ + --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ + --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ + --exclude='freesurfer/diffusion' \ + --exclude='freesurfer/fsafd' \ + --exclude='freesurfer/fsfast' \ + --exclude='freesurfer/lib/cuda' \ + --exclude='freesurfer/lib/images' \ + --exclude='freesurfer/lib/qt' \ + --exclude='freesurfer/lib/tcl' \ + --exclude='freesurfer/lib/tktools' \ + --exclude='freesurfer/lib/vtk' \ + --exclude='freesurfer/matlab' \ + --exclude='freesurfer/mni-1.4' \ + --exclude='freesurfer/mni' \ + --exclude='freesurfer/models' \ + --exclude='freesurfer/python/bin' \ + --exclude='freesurfer/python/include' \ + --exclude='freesurfer/python/lib' \ + --exclude='freesurfer/python/share' \ + --exclude='freesurfer/subjects/bert' \ + --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ + --exclude='freesurfer/subjects/cvs_avg35' \ + --exclude='freesurfer/subjects/fsaverage_sym' \ + --exclude='freesurfer/subjects/fsaverage3' \ + --exclude='freesurfer/subjects/fsaverage4' \ + --exclude='freesurfer/subjects/fsaverage5' \ + --exclude='freesurfer/subjects/fsaverage6' \ + --exclude='freesurfer/subjects/lh.EC_average' \ + --exclude='freesurfer/subjects/rh.EC_average' \ + --exclude='freesurfer/subjects/V1_average' \ + --exclude='freesurfer/tktools' \ + --exclude='freesurfer/trctrain' \ + -f freesurfer.tar.gz + +# rm -rf freesurfer.tar.gz + +# rename download to tmp +mv $where/freesurfer $fss + +# mk directories +mkdir -p $fsd/average +mkdir -p $fsd/bin +mkdir -p $fsd/etc +mkdir -p $fsd/lib/bem +mkdir -p $fsd/python/scripts +mkdir -p $fsd/python/packages/fsbindings +mkdir -p $fsd/subjects/fsaverage/label +mkdir -p $fsd/subjects/fsaverage/surf + +# We need these +copy_files=" + ASegStatsLUT.txt + build-stamp.txt + DefectLUT.txt + FreeSurferColorLUT.txt + FreeSurferEnv.sh + SegmentNoLUT.txt + SetUpFreeSurfer.sh + Simple_surface_labels2009.txt + sources.csh + SubCorticalMassLUT.txt + WMParcStatsLUT.txt + average/3T18yoSchwartzReactN32_as_orig.4dfp.hdr + average/3T18yoSchwartzReactN32_as_orig.4dfp.ifh + average/3T18yoSchwartzReactN32_as_orig.4dfp.img + average/3T18yoSchwartzReactN32_as_orig.4dfp.img.rec + average/3T18yoSchwartzReactN32_as_orig.4dfp.mat + average/3T18yoSchwartzReactN32_as_orig.lst + average/711-2B_as_mni_average_305_mask.4dfp.hdr + average/711-2B_as_mni_average_305_mask.4dfp.ifh + average/711-2B_as_mni_average_305_mask.4dfp.img + average/711-2B_as_mni_average_305_mask.4dfp.img.rec + average/711-2C_as_mni_average_305.4dfp.hdr + average/711-2C_as_mni_average_305.4dfp.ifh + average/711-2C_as_mni_average_305.4dfp.img + average/711-2C_as_mni_average_305.4dfp.img.rec + average/711-2C_as_mni_average_305.4dfp.mat + average/colortable_BA.txt + average/colortable_desikan_killiany.txt + average/colortable_vpnl.txt + average/lh.CDaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/lh.DKaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/lh.DKTaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/lh.folding.atlas.acfb40.noaparc.i12.2016-08-02.tif + average/mni305.cor.mgz + average/mni305.mask.cor.mgz + average/rh.CDaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/rh.DKaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/rh.DKTaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs + average/rh.folding.atlas.acfb40.noaparc.i12.2016-08-02.tif + bin/analyzeto4dfp + bin/AntsDenoiseImageFs + bin/asegstats2table + bin/aparcstats2table + bin/avi2talxfm + bin/compute_vox2vox + bin/defect2seg + bin/fname2stem + bin/fspython + bin/fs_temp_dir + bin/fs_temp_file + bin/fs-check-version + bin/fsr-getxopts + bin/gauss_4dfp + bin/ifh2hdr + bin/imgreg_4dfp + bin/isanalyze + bin/isnifti + bin/lta_convert + bin/make_upright + bin/mpr2mni305 + bin/mri_add_xform_to_header + bin/mri_annotation2label + bin/mri_binarize + bin/mri_brainvol_stats + bin/mri_cc + bin/mri_concat + bin/mri_concatenate_lta + bin/mri_convert + bin/mri_coreg + bin/mri_diff + bin/mri_edit_wm_with_aseg + bin/mri_fill + bin/mri_fuse_segmentations + bin/mri_glmfit + bin/mri_info + bin/mri_label2label + bin/mri_label2vol + bin/mri_mask + bin/mri_matrix_multiply + bin/mri_mc + bin/mri_normalize + bin/mri_pretess + bin/mri_relabel_hypointensities + bin/mri_robust_register + bin/mri_robust_template + bin/mri_segment + bin/mri_segstats + bin/mri_surf2surf + bin/mri_surf2volseg + bin/mri_tessellate + bin/mri_vol2surf + bin/mri_vol2vol + bin/mris_anatomical_stats + bin/mris_autodet_gwstats + bin/mris_ca_label + bin/mris_calc + bin/mris_convert + bin/mris_curvature + bin/mris_curvature_stats + bin/mris_defects_pointset + bin/mris_diff + bin/mris_euler_number + bin/mris_extract_main_component + bin/mris_fix_topology + bin/mris_inflate + bin/mris_info + bin/mris_jacobian + bin/mris_label2annot + bin/mris_place_surface + bin/mris_preproc + bin/mris_register + bin/mris_remesh + bin/mris_remove_intersection + bin/mris_sample_parc + bin/mris_smooth + bin/mris_sphere + bin/mris_topo_fixer + bin/mris_volmask + bin/mrisp_paint + bin/pctsurfcon + bin/rca-config + bin/rca-config2csh + bin/recon-all + bin/talairach_avi + bin/UpdateNeeded + bin/vertexvol + etc/recon-config.yaml + lib/bem/ic4.tri + lib/bem/ic7.tri + python/packages/fsbindings/legacy.py + python/scripts/asegstats2table + python/scripts/aparcstats2table + python/scripts/rca-config + python/scripts/rca-config2csh + subjects/fsaverage/label/lh.aparc.annot + subjects/fsaverage/label/lh.BA1_exvivo.label + subjects/fsaverage/label/lh.BA1_exvivo.thresh.label + subjects/fsaverage/label/lh.BA2_exvivo.label + subjects/fsaverage/label/lh.BA2_exvivo.thresh.label + subjects/fsaverage/label/lh.BA3a_exvivo.label + subjects/fsaverage/label/lh.BA3a_exvivo.thresh.label + subjects/fsaverage/label/lh.BA3b_exvivo.label + subjects/fsaverage/label/lh.BA3b_exvivo.thresh.label + subjects/fsaverage/label/lh.BA44_exvivo.label + subjects/fsaverage/label/lh.BA44_exvivo.thresh.label + subjects/fsaverage/label/lh.BA45_exvivo.label + subjects/fsaverage/label/lh.BA45_exvivo.thresh.label + subjects/fsaverage/label/lh.BA4a_exvivo.label + subjects/fsaverage/label/lh.BA4a_exvivo.thresh.label + subjects/fsaverage/label/lh.BA4p_exvivo.label + subjects/fsaverage/label/lh.BA4p_exvivo.thresh.label + subjects/fsaverage/label/lh.BA6_exvivo.label + subjects/fsaverage/label/lh.BA6_exvivo.thresh.label + subjects/fsaverage/label/lh.cortex.label + subjects/fsaverage/label/lh.entorhinal_exvivo.label + subjects/fsaverage/label/lh.entorhinal_exvivo.thresh.label + subjects/fsaverage/label/lh.FG1.mpm.vpnl.label + subjects/fsaverage/label/lh.FG2.mpm.vpnl.label + subjects/fsaverage/label/lh.FG3.mpm.vpnl.label + subjects/fsaverage/label/lh.FG4.mpm.vpnl.label + subjects/fsaverage/label/lh.hOc1.mpm.vpnl.label + subjects/fsaverage/label/lh.hOc2.mpm.vpnl.label + subjects/fsaverage/label/lh.hOc3v.mpm.vpnl.label + subjects/fsaverage/label/lh.hOc4v.mpm.vpnl.label + subjects/fsaverage/label/lh.MT_exvivo.label + subjects/fsaverage/label/lh.MT_exvivo.thresh.label + subjects/fsaverage/label/lh.perirhinal_exvivo.label + subjects/fsaverage/label/lh.perirhinal_exvivo.thresh.label + subjects/fsaverage/label/lh.V1_exvivo.label + subjects/fsaverage/label/lh.V1_exvivo.thresh.label + subjects/fsaverage/label/lh.V2_exvivo.label + subjects/fsaverage/label/lh.V2_exvivo.thresh.label + subjects/fsaverage/label/rh.aparc.annot + subjects/fsaverage/label/rh.BA1_exvivo.label + subjects/fsaverage/label/rh.BA1_exvivo.thresh.label + subjects/fsaverage/label/rh.BA2_exvivo.label + subjects/fsaverage/label/rh.BA2_exvivo.thresh.label + subjects/fsaverage/label/rh.BA3a_exvivo.label + subjects/fsaverage/label/rh.BA3a_exvivo.thresh.label + subjects/fsaverage/label/rh.BA3b_exvivo.label + subjects/fsaverage/label/rh.BA3b_exvivo.thresh.label + subjects/fsaverage/label/rh.BA44_exvivo.label + subjects/fsaverage/label/rh.BA44_exvivo.thresh.label + subjects/fsaverage/label/rh.BA45_exvivo.label + subjects/fsaverage/label/rh.BA45_exvivo.thresh.label + subjects/fsaverage/label/rh.BA4a_exvivo.label + subjects/fsaverage/label/rh.BA4a_exvivo.thresh.label + subjects/fsaverage/label/rh.BA4p_exvivo.label + subjects/fsaverage/label/rh.BA4p_exvivo.thresh.label + subjects/fsaverage/label/rh.BA6_exvivo.label + subjects/fsaverage/label/rh.BA6_exvivo.thresh.label + subjects/fsaverage/label/rh.cortex.label + subjects/fsaverage/label/rh.entorhinal_exvivo.label + subjects/fsaverage/label/rh.entorhinal_exvivo.thresh.label + subjects/fsaverage/label/rh.FG1.mpm.vpnl.label + subjects/fsaverage/label/rh.FG2.mpm.vpnl.label + subjects/fsaverage/label/rh.FG3.mpm.vpnl.label + subjects/fsaverage/label/rh.FG4.mpm.vpnl.label + subjects/fsaverage/label/rh.hOc1.mpm.vpnl.label + subjects/fsaverage/label/rh.hOc2.mpm.vpnl.label + subjects/fsaverage/label/rh.hOc3v.mpm.vpnl.label + subjects/fsaverage/label/rh.hOc4v.mpm.vpnl.label + subjects/fsaverage/label/rh.MT_exvivo.label + subjects/fsaverage/label/rh.MT_exvivo.thresh.label + subjects/fsaverage/label/rh.perirhinal_exvivo.label + subjects/fsaverage/label/rh.perirhinal_exvivo.thresh.label + subjects/fsaverage/label/rh.V1_exvivo.label + subjects/fsaverage/label/rh.V1_exvivo.thresh.label + subjects/fsaverage/label/rh.V2_exvivo.label + subjects/fsaverage/label/rh.V2_exvivo.thresh.label + subjects/fsaverage/surf/lh.curv + subjects/fsaverage/surf/lh.pial + subjects/fsaverage/surf/lh.pial_semi_inflated + subjects/fsaverage/surf/lh.sphere + subjects/fsaverage/surf/lh.sphere.reg + subjects/fsaverage/surf/lh.white + subjects/fsaverage/surf/rh.curv + subjects/fsaverage/surf/rh.pial + subjects/fsaverage/surf/rh.pial_semi_inflated + subjects/fsaverage/surf/rh.sphere + subjects/fsaverage/surf/rh.sphere.reg + subjects/fsaverage/surf/rh.white" +echo +for file in $copy_files +do + echo "copying $file" + cp -r $fss/$file $fsd/$file +done + +# pack if desired with upx (do this before adding all the links +if [[ "$upx" == "true" ]] ; then + echo "finding executables in $fsd/bin/..." + exe=$(find $fsd/bin -exec file {} \; | grep ELF | cut -d: -f1) + echo "packing $fsd/bin/ executables (this can take a while) ..." + run_parallel 8 "upx -9 %s %s %s %s" 4 $exe +fi + +# Modify fsbindings Python package to allow calling scripts like asegstats2table directly: +echo "from . import legacy" > "$fsd/python/packages/fsbindings/__init__.py" + +# FS looks for them, but does not call them +touch_files="/average/RB_all_2020-01-02.gca" +echo +for file in $touch_files +do + echo "touching $file" + touch $fsd/$file +done + +#cleanup +rm -rf $fss diff --git a/tools/macos_build/macos_setup_fastsurfer.sh.template b/tools/macos_build/macos_setup_fastsurfer.sh.template new file mode 100644 index 000000000..fe42c563d --- /dev/null +++ b/tools/macos_build/macos_setup_fastsurfer.sh.template @@ -0,0 +1,21 @@ +#!/bin/bash + +export FASTSURFER_HOME=/Applications/ +export PATH=$FASTSURFER_HOME:$PATH +source $FASTSURFER_HOME/venv/bin/activate +export PYTHONPATH=$FASTSURFER_HOME/venv/lib/python3.10/site-packages:$PYTHONPATH +export PYTORCH_ENABLE_MPS_FALLBACK= +export FREESURFER_HOME=/Applications/freesurfer +source $FREESURFER_HOME/SetUpFreeSurfer.sh > /dev/null +echo +echo "This is the FastSurfer console. Call \`run_fastsurfer.sh -t1 ...\` in here! " +echo "See also https://deep-mi.org/FastSurfer/stable/overview/macos.html" + +if ! grep -q "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" ~/.bash_profile; then + echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; + echo "GNU grep has been adde to path" +fi + +if [[ -z "${FS_LICENSE}" ]]; then + echo "Set FS_LICENSE environmental variable" +fi diff --git a/tools/macos_build/scripts/postinstall.template b/tools/macos_build/scripts/postinstall.template new file mode 100755 index 000000000..6fa141db1 --- /dev/null +++ b/tools/macos_build/scripts/postinstall.template @@ -0,0 +1,76 @@ +#!/bin/bash + +FASTSURFER_HOME="" + +if [[ ! -f "$FASTSURFER_HOME/venv" ]] +then + echo "Creating FastSurfer run environment" + /bin/python3.10 -m venv --copies $FASTSURFER_HOME/venv +fi + +source $FASTSURFER_HOME/venv/bin/activate +python3.10 -m pip install --upgrade pip + +export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} + +python3.10 -m pip install -r $FASTSURFER_HOME/requirements.mac.txt + +python3.10 $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all + +FREESURFER_HOME="/Applications/freesurfer" + +ln -sf $FASTSURFER_HOME/venv/bin/python3.10 $FREESURFER_HOME/bin/fspython + +# FS calls these for version info, but we don't need them +# so we link them to mri_info to save space. +link_files=" + bin/mri_and + bin/mri_aparc2aseg + bin/mri_ca_label + bin/mri_ca_normalize + bin/mri_ca_register + bin/mri_compute_overlap + bin/mri_compute_seg_overlap + bin/mri_em_register + bin/mri_fwhm + bin/mri_gcut + bin/mri_log_likelihood + bin/mri_motion_correct.fsl + bin/mri_normalize_tp2 + bin/mri_or + bin/mri_relabel_nonwm_hypos + bin/mri_remove_neck + bin/mri_stats2seg + bin/mri_surf2vol + bin/mri_surfcluster + bin/mri_voldiff + bin/mri_watershed + bin/mris_divide_parcellation + bin/mris_left_right_register + bin/mris_surface_stats + bin/mris_thickness + bin/mris_thickness_diff + bin/nu_correct + bin/tkregister2_cmdl" + +# create target for link with ERROR message if called +ltrg=$FREESURFER_HOME/bin/not-here.sh +echo '#!/bin/bash +if [ "$1" == "-all-info" ]; then + echo "$0 not included ..." + exit 0 +fi +echo +echo "ERROR: The binary $0 is not included, your call is forwarded to not-here.sh" +echo +exit 1 +' > $ltrg +chmod a+x $ltrg +echo +for file in $link_files +do + echo "linking $file" + ln -sf $ltrg $FREESURFER_HOME/$file +done + +sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate diff --git a/tools/macos_build/setup.py b/tools/macos_build/setup.py new file mode 100644 index 000000000..dea6d97d3 --- /dev/null +++ b/tools/macos_build/setup.py @@ -0,0 +1,19 @@ +""" +This is a setup.py script generated by py2applet + +Usage: + python setup.py py2app +""" + +from setuptools import setup + +APP = ['FastSurfer.py'] +DATA_FILES = [] +OPTIONS = {} + +setup( + app=APP, + data_files=DATA_FILES, + options={'py2app': OPTIONS}, + setup_requires=['py2app'], +) From ba16a017445f8fc7df7969209fa78954b89a974d Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 15 Oct 2025 15:40:43 +0200 Subject: [PATCH 02/30] Move docker and singularity into tools --- README.md | 6 +++--- doc/overview/EXAMPLES.md | 4 ++-- doc/overview/INSTALL.md | 10 +++++----- doc/overview/docker.rst | 2 +- doc/overview/singularity.rst | 2 +- {Docker => tools/Docker}/Dockerfile | 0 {Docker => tools/Docker}/README.md | 0 {Docker => tools/Docker}/build.py | 0 {Docker => tools/Docker}/conda_pack.sh | 0 {Docker => tools/Docker}/entrypoint.sh | 0 {Docker => tools/Docker}/install_env.py | 0 {Docker => tools/Docker}/install_fs_pruned.sh | 0 {Singularity => tools/Singularity}/README.md | 0 tools/macos_build/build_release_package.sh | 4 ++-- 14 files changed, 14 insertions(+), 14 deletions(-) rename {Docker => tools/Docker}/Dockerfile (100%) rename {Docker => tools/Docker}/README.md (100%) rename {Docker => tools/Docker}/build.py (100%) rename {Docker => tools/Docker}/conda_pack.sh (100%) rename {Docker => tools/Docker}/entrypoint.sh (100%) rename {Docker => tools/Docker}/install_env.py (100%) rename {Docker => tools/Docker}/install_fs_pruned.sh (100%) rename {Singularity => tools/Singularity}/README.md (100%) diff --git a/README.md b/README.md index 379b47134..a178a31dd 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,11 @@ All installation methods use the `run_fastsurfer.sh` call interface (replace `*f ``` The `--nv` flag is needed to allow FastSurfer to run on the GPU (otherwise FastSurfer will run on the CPU). - The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](Singularity/README.md#mounting-home) for more info). + The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](tools/Singularity/README.md#mounting-home) for more info). The `-B` flag is used to tell singularity, which folders FastSurfer can read and write to. - See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](Singularity/README.md#fastsurfer-singularity-image-usage) for details on more singularity flags. + See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](tools/Singularity/README.md#fastsurfer-singularity-image-usage) for details on more singularity flags. (b) For __docker__, the syntax is ``` @@ -96,7 +96,7 @@ All installation methods use the `run_fastsurfer.sh` call interface (replace `*f The `-v` flag is used to tell docker, which folders FastSurfer can read and write to. - See also __[Example 1](doc/overview/EXAMPLES.md#example-1-fastsurfer-docker)__ for a full FastSurfer run inside a Docker container and [the Docker documentation](Docker/README.md#docker-flags) for more details on the docker flags including `--rm` and `--user`. + See also __[Example 1](doc/overview/EXAMPLES.md#example-1-fastsurfer-docker)__ for a full FastSurfer run inside a Docker container and [the Docker documentation](tools/Docker/README.md#docker-flags) for more details on the docker flags including `--rm` and `--user`. 2. For a __native install__, you need to activate your FastSurfer environment (e.g. `conda activate fastsurfer_gpu`) and make sure you have added the FastSurfer path to your `PYTHONPATH` variable, e.g. `export PYTHONPATH=$(pwd)`. diff --git a/doc/overview/EXAMPLES.md b/doc/overview/EXAMPLES.md index eb082c885..32e9a500d 100644 --- a/doc/overview/EXAMPLES.md +++ b/doc/overview/EXAMPLES.md @@ -39,10 +39,10 @@ Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory if it does not exist. So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. -If you do not have a GPU, you can also run our CPU-Docker by dropping the `--gpus all` flag and specifying `--device cpu` at the end as a FastSurfer flag, see also [FastSurfer's docker documentation](../../Docker/README.md) for more details. +If you do not have a GPU, you can also run our CPU-Docker by dropping the `--gpus all` flag and specifying `--device cpu` at the end as a FastSurfer flag, see also [FastSurfer's docker documentation](../../tools/Docker/README.md) for more details. ## Example 2: FastSurfer Singularity -After building the Singularity image (see below or [these instructions](../../Singularity/README.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. +After building the Singularity image (see below or [these instructions](../../tools/Singularity/README.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. To run FastSurfer on a given subject using the Singularity image with GPU access, execute the following commands from a directory where you want to store singularity images. This will create a singularity image from our Dockerhub image and execute it: diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 7a7c480a5..abc26b8cd 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -22,9 +22,9 @@ Assuming you have singularity installed already (by a system admin), you can bui ```bash singularity build fastsurfer-gpu.sif docker://deepmi/fastsurfer:latest ``` -Additionally, [the Singularity README](../../Singularity/README.md) contains detailed directions for building your own Singularity images from Docker. +Additionally, [the Singularity README](../../tools/Singularity/README.md) contains detailed directions for building your own Singularity images from Docker. -[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../Docker/README.md) and [Singularity](../../Singularity/README.md). +[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../tools/Docker/README.md) and [Singularity](../../tools/Singularity/README.md). ### Docker @@ -35,7 +35,7 @@ This is very similar to Singularity. Assuming you have Docker installed (by a sy docker pull deepmi/fastsurfer:latest ``` -[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](https://github.com/Deep-MI/FastSurfer/blob/dev/Docker/README.md). +[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/README.md). If you are using the **rootless mode**, you have to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and follow the [configuration for the rootless mode](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#rootless-mode). Otherwise, running FastSurfer with Docker will give you this error message ```docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]]```. @@ -97,7 +97,7 @@ conda env create -f ./env/fastsurfer.yml conda activate fastsurfer ``` -If you do not have an NVIDIA GPU, you can create appropriate ymls on the fly with `python ./Docker/install_env.py -m $MODE -i ./env/FastSurfer.yml -o ./fastsurfer_$MODE.yml`. Here `$MODE` can be for example `cpu`, see also `python ./Docker/install_env.py --help` for other options like rocm or cuda versions. Finally, replace `./env/fastsurfer.yml` with your custom environment file `./fastsurfer_$MODE.yml`. +If you do not have an NVIDIA GPU, you can create appropriate ymls on the fly with `python ./tools/Docker/install_env.py -m $MODE -i ./env/FastSurfer.yml -o ./fastsurfer_$MODE.yml`. Here `$MODE` can be for example `cpu`, see also `python ./tools/Docker/install_env.py --help` for other options like rocm or cuda versions. Finally, replace `./env/fastsurfer.yml` with your custom environment file `./fastsurfer_$MODE.yml`. If you only want to run the surface pipeline, use `./env/fastsurfer_reconsurf.yml`. Next, add the fastsurfer directory to the python path (make sure you have changed into it already): @@ -129,7 +129,7 @@ We have successfully run the segmentation on an AMD GPU (Radeon Pro W6600) using Build the Docker container with ROCm support. ```bash -python Docker/build.py --device rocm --tag my_fastsurfer:rocm +python tools/Docker/build.py --device rocm --tag my_fastsurfer:rocm ``` You will need to add a couple of flags to your docker run command for AMD, see [Example 1](EXAMPLES.md#example-1-fastsurfer-docker) for `**other-docker-flags**` or `**fastsurfer-flags**`: diff --git a/doc/overview/docker.rst b/doc/overview/docker.rst index 3cc4057e5..12328cd7a 100644 --- a/doc/overview/docker.rst +++ b/doc/overview/docker.rst @@ -1,7 +1,7 @@ Docker Support -------------- -.. include:: ../../Docker/README.md +.. include:: ../../tools/Docker/README.md :parser: fix_links.parser :relative-docs: . :relative-images: diff --git a/doc/overview/singularity.rst b/doc/overview/singularity.rst index afb11cb84..de76f9bf5 100644 --- a/doc/overview/singularity.rst +++ b/doc/overview/singularity.rst @@ -1,7 +1,7 @@ Singularity Support ------------------- -.. include:: ../../Singularity/README.md +.. include:: ../../tools/Singularity/README.md :parser: fix_links.parser :relative-docs: . :relative-images: diff --git a/Docker/Dockerfile b/tools/Docker/Dockerfile similarity index 100% rename from Docker/Dockerfile rename to tools/Docker/Dockerfile diff --git a/Docker/README.md b/tools/Docker/README.md similarity index 100% rename from Docker/README.md rename to tools/Docker/README.md diff --git a/Docker/build.py b/tools/Docker/build.py similarity index 100% rename from Docker/build.py rename to tools/Docker/build.py diff --git a/Docker/conda_pack.sh b/tools/Docker/conda_pack.sh similarity index 100% rename from Docker/conda_pack.sh rename to tools/Docker/conda_pack.sh diff --git a/Docker/entrypoint.sh b/tools/Docker/entrypoint.sh similarity index 100% rename from Docker/entrypoint.sh rename to tools/Docker/entrypoint.sh diff --git a/Docker/install_env.py b/tools/Docker/install_env.py similarity index 100% rename from Docker/install_env.py rename to tools/Docker/install_env.py diff --git a/Docker/install_fs_pruned.sh b/tools/Docker/install_fs_pruned.sh similarity index 100% rename from Docker/install_fs_pruned.sh rename to tools/Docker/install_fs_pruned.sh diff --git a/Singularity/README.md b/tools/Singularity/README.md similarity index 100% rename from Singularity/README.md rename to tools/Singularity/README.md diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 7a00d9a25..e7538121e 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -29,7 +29,7 @@ PACKAGE_NAME=FastSurfer$VERSION-macos-darwin_${ARCH_TYPE_NAME} # name of the pac ID="com.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) INSTALLATION_DIR="/Applications" # install location for the content of the package OUTPUT_PKG="raw_package/$PACKAGE_NAME.pkg" # raw package file to be created -INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installater to be created +INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installer to be created # create temporary folder to package and copy FastSurfer over STAGED_DIR="FastSurferPackageContent" @@ -42,7 +42,7 @@ rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ --exclude Singularity \ --exclude tools -# install freesurfer into temp foler +# install freesurfer into temp folder if [ "$#" -gt 3 ]; then ./install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER else From 101df1a747bf15fa1a858266da278f5b786f8c08 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 15 Oct 2025 15:43:48 +0200 Subject: [PATCH 03/30] Create workflow for MacOS package deployment --- .github/workflows/deploy.yml | 123 +++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 41 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fde08ee95..afcf19949 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,55 +1,96 @@ name: MAN deploy-docker on: -# release: -# types: -# - published - workflow_dispatch: + release: + types: + - published jobs: - deploy-gpu: - runs-on: ubuntu-latest - timeout-minutes: 120 - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Login to Docker - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build Docker image GPU - run: python Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - - name: Add additional tags - run: | - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:latest - - name: Push Docker image GPU - run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - deploy-cpu: - runs-on: ubuntu-latest + deploy-mac: + runs-on: macos-14 timeout-minutes: 120 + + env: + ARTIFACT_NAME: FastSurfer + VERSION: 242 + FREESURFER_URL: https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz + UPLOAD_ARTIFACT: true + steps: + - name: Get repository name. + run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Login to Docker - uses: docker/login-action@v2 + - name: Set up python environment + uses: actions/setup-python@v6 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build Docker image CPU - run: python Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} - - name: Add additional tags + python-version: '3.10' + - name: install dependencies run: | - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest - - name: Push Docker image CPU - run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} + python -m pip install py2app + brew update + brew upgrade || true + brew install xmlstarlet + - name: package app for arm + run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} arm ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} + - name: package app for intel + run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} intel ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} + - name: Move artifact. + if: env.UPLOAD_ARTIFACT == 'true' + run: | + mkdir artifact + mv tools/macos_build/installer/* artifact/ + - uses: actions/upload-artifact@v4 + if: env.UPLOAD_ARTIFACT == 'true' + name: Upload artifact. + with: + name: ${{ env.ARTIFACT_NAME }} + path: ${{ env.FASTSURFER_DIR }}/artifact/ + # deploy-gpu: + # runs-on: ubuntu-latest + # timeout-minutes: 120 + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + # with: + # fetch-depth: 0 + # - name: Login to Docker + # uses: docker/login-action@v2 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build Docker image GPU + # run: python Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} + # - name: Add additional tags + # run: | + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:latest + # - name: Push Docker image GPU + # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} + # deploy-cpu: + # runs-on: ubuntu-latest + # timeout-minutes: 120 + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + # with: + # fetch-depth: 0 + # - name: Login to Docker + # uses: docker/login-action@v2 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build Docker image CPU + # run: python Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} + # - name: Add additional tags + # run: | + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest + # - name: Push Docker image CPU + # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} From 6c67a96bd14f02be480645ddfb09c3d041e5a5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Sat, 18 Oct 2025 00:06:39 +0200 Subject: [PATCH 04/30] Apply suggestions from code review --- .github/workflows/deploy.yml | 11 +++++---- doc/overview/INSTALL.md | 2 +- doc/overview/INSTRUCTIONS.md | 23 ++++++++----------- tools/macos_build/build_release_package.sh | 14 ++++------- .../macos_setup_fastsurfer.sh.template | 2 +- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index afcf19949..85826075f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: release: types: - published + workflow_dispatch: jobs: deploy-mac: @@ -15,7 +16,9 @@ jobs: VERSION: 242 FREESURFER_URL: https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz UPLOAD_ARTIFACT: true - + strategy: + matrix: + arch: [intel, arm] steps: - name: Get repository name. run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -34,10 +37,8 @@ jobs: brew update brew upgrade || true brew install xmlstarlet - - name: package app for arm - run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} arm ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} - - name: package app for intel - run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} intel ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} + - name: package app for ${{ matrix.arch }} + run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} - name: Move artifact. if: env.UPLOAD_ARTIFACT == 'true' run: | diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index abc26b8cd..2f2508531 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -35,7 +35,7 @@ This is very similar to Singularity. Assuming you have Docker installed (by a sy docker pull deepmi/fastsurfer:latest ``` -[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/README.md). +[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](../../tools/Docker/README.md). If you are using the **rootless mode**, you have to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and follow the [configuration for the rootless mode](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#rootless-mode). Otherwise, running FastSurfer with Docker will give you this error message ```docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]]```. diff --git a/doc/overview/INSTRUCTIONS.md b/doc/overview/INSTRUCTIONS.md index 4b58dd373..06af22efe 100644 --- a/doc/overview/INSTRUCTIONS.md +++ b/doc/overview/INSTRUCTIONS.md @@ -1,13 +1,10 @@ --------- Running FastSurfer -------- - -- If you want to run only segmentation: - -/run_fastsurfer.sh --seg_only --sd --sid --t1 - -- To full run fastsurfer: - -/run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license - -- If some files of freesurfer files require bypassing MacOS security, -- it might take a long time to allow access to every one of them one by one. -- Instead, this command might be used:

xattr -dr com.apple.quarantine /Applications/freesurfer/* +Running FastSurfer +================== + +If you want to run only segmentation (replace paceholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): +`run_fastsurfer.sh --seg_only --sd --sid --t1 ` +To full run fastsurfer: +`run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` +Some files of **FreeSurfer** binaries require bypassing MacOS security, which is +significantly easier to do with the following command than manually and one by one. +`

xattr -dr com.apple.quarantine /Applications/freesurfer/*` diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index e7538121e..0946c9a2c 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -26,7 +26,7 @@ if [ "$ARCH_TYPE" = "intel" ]; then fi PACKAGE_NAME=FastSurfer$VERSION-macos-darwin_${ARCH_TYPE_NAME} # name of the package displayed in the installer -ID="com.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) +ID="ord.deep-mi.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) INSTALLATION_DIR="/Applications" # install location for the content of the package OUTPUT_PKG="raw_package/$PACKAGE_NAME.pkg" # raw package file to be created INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installer to be created @@ -112,11 +112,9 @@ xml ed \ -t elem \ -n title \ -v "${PACKAGE_NAME}" \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -# set background image -xml ed \ + $DISTRIBUTION_FILE | \ +{ # set background image + xml ed \ -s /installer-gui-script \ -t elem \ -n background \ @@ -124,9 +122,7 @@ xml ed \ -t attr \ -n file \ -v installer_background.png \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - +} | \ xml ed \ -i /installer-gui-script/background \ -t attr \ diff --git a/tools/macos_build/macos_setup_fastsurfer.sh.template b/tools/macos_build/macos_setup_fastsurfer.sh.template index fe42c563d..f718b9ca6 100644 --- a/tools/macos_build/macos_setup_fastsurfer.sh.template +++ b/tools/macos_build/macos_setup_fastsurfer.sh.template @@ -9,7 +9,7 @@ export FREESURFER_HOME=/Applications/freesurfer source $FREESURFER_HOME/SetUpFreeSurfer.sh > /dev/null echo echo "This is the FastSurfer console. Call \`run_fastsurfer.sh -t1 ...\` in here! " -echo "See also https://deep-mi.org/FastSurfer/stable/overview/macos.html" +echo "See also https://deep-mi.org/FastSurfer/stable/overview/MACOS.html" if ! grep -q "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" ~/.bash_profile; then echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; From f910b59d40f93224b5b21175785335e7ab2bf209 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 22 Oct 2025 16:09:09 +0200 Subject: [PATCH 05/30] Reformat scripts and fix broken paths --- .github/workflows/deploy.yml | 12 +- doc/overview/{INSTRUCTIONS.md => MACOS.md} | 2 +- recon_surf/README.md | 2 +- tools/Docker/Dockerfile | 2 +- tools/Docker/README.md | 4 +- tools/Docker/install_fs_pruned.sh | 480 ------------------ tools/{macos_build => }/install_fs_pruned.sh | 2 +- tools/macos_build/.gitignore | 2 + tools/macos_build/README.md | 42 +- tools/macos_build/build_release_package.sh | 125 +---- tools/macos_build/edit_distribution.py | 97 ++++ .../macos_setup_fastsurfer.sh.template | 8 +- tools/macos_build/resources/LICENSE.txt | 174 +++++++ tools/macos_build/resources/MACOS.md | 10 + tools/macos_build/resources/distribution.xml | 20 + .../macos_build/resources/fastsurfer.png | Bin 16 files changed, 365 insertions(+), 617 deletions(-) rename doc/overview/{INSTRUCTIONS.md => MACOS.md} (79%) delete mode 100755 tools/Docker/install_fs_pruned.sh rename tools/{macos_build => }/install_fs_pruned.sh (99%) create mode 100644 tools/macos_build/edit_distribution.py create mode 100644 tools/macos_build/resources/LICENSE.txt create mode 100644 tools/macos_build/resources/MACOS.md create mode 100644 tools/macos_build/resources/distribution.xml rename doc/images/installer_background.png => tools/macos_build/resources/fastsurfer.png (100%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 85826075f..0e47964b3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,10 +1,10 @@ name: MAN deploy-docker on: - release: - types: - - published - workflow_dispatch: + release: + types: + - published + workflow_dispatch: jobs: deploy-mac: @@ -16,9 +16,9 @@ jobs: VERSION: 242 FREESURFER_URL: https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz UPLOAD_ARTIFACT: true - strategy: + strategy: matrix: - arch: [intel, arm] + arch: [intel, arm] steps: - name: Get repository name. run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV diff --git a/doc/overview/INSTRUCTIONS.md b/doc/overview/MACOS.md similarity index 79% rename from doc/overview/INSTRUCTIONS.md rename to doc/overview/MACOS.md index 06af22efe..60d585f21 100644 --- a/doc/overview/INSTRUCTIONS.md +++ b/doc/overview/MACOS.md @@ -1,7 +1,7 @@ Running FastSurfer ================== -If you want to run only segmentation (replace paceholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): +If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): `run_fastsurfer.sh --seg_only --sd --sid --t1 ` To full run fastsurfer: `run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` diff --git a/recon_surf/README.md b/recon_surf/README.md index 281c8bb89..e452eb7dc 100644 --- a/recon_surf/README.md +++ b/recon_surf/README.md @@ -92,7 +92,7 @@ Check [Dockerhub](https://hub.docker.com/r/deepmi/fastsurfer/tags) to find out t * The `-B` commands mount your output, and directory with the FreeSurfer license file into the Singularity container. Inside the container these are visible under the name following the colon (in this case /data, /output, and /fs_license). -* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../Singularity/README.md#mounting-home)) +* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../tools/Singularity/README.md#mounting-home)) The `--t1` and `--asegdkt_segfile` flags point to the already existing conformed T1 input and segmentation from the segmentation module. Also other files from that pipeline will be reused (e.g. the `mask.mgz`, `orig_nu.mgz`). The diff --git a/tools/Docker/Dockerfile b/tools/Docker/Dockerfile index 70930c596..48b608576 100644 --- a/tools/Docker/Dockerfile +++ b/tools/Docker/Dockerfile @@ -116,7 +116,7 @@ RUN python /install/install_env.py -m ${DEVICE} -i /install/fastsurfer.yml -o /i FROM build_base AS build_freesurfer # get install scripts into docker -COPY ./Docker/install_fs_pruned.sh /install/ +COPY ./tools/install_fs_pruned.sh /install/ SHELL ["/bin/bash", "--login", "-c"] ARG FREESURFER_URL=default diff --git a/tools/Docker/README.md b/tools/Docker/README.md index d26dcfcfc..f28cb6e83 100644 --- a/tools/Docker/README.md +++ b/tools/Docker/README.md @@ -46,13 +46,13 @@ docker run --gpus all -v /home/user/my_mri_data:/data \ * The `--t1` points to the t1-weighted MRI image to analyse (full path, with mounted name inside docker: /home/user/my_mri_data => /data) * The `--sid` is the subject ID name (output folder name) * The `--sd` points to the output directory (its mounted name inside docker: /home/user/my_fastsurfer_analysis => /output) -* [more flags](../doc/overview/FLAGS.md#fastsurfer-flags) +* [more flags](../../doc/overview/FLAGS.md#fastsurfer-flags) Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ the container, not global paths on your system, so they should point to the places where you mapped these paths above with the `-v` arguments. A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory (specified via `--sd`). So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. -All other available flags are identical to the ones explained on the main page [README](../README.md). +All other available flags are identical to the ones explained on the main page [README](../../README.md). ### Docker Best Practice * Do not mount the user home directory into the docker container as the home directory. diff --git a/tools/Docker/install_fs_pruned.sh b/tools/Docker/install_fs_pruned.sh deleted file mode 100755 index 0346be306..000000000 --- a/tools/Docker/install_fs_pruned.sh +++ /dev/null @@ -1,480 +0,0 @@ -#!/bin/bash --login -# --login to read bashrc for conda inside docker - -# This file downloads the FreeSurfer tar ball and extracts from it only what is needed to run -# FastSurfer -# -# In order to update to a new FreeSurfer version you need to update the fslink and then build a -# docker with this setup. Run it and whenever it crashes/exits, find the missing file (binary, -# atlas, datafile, or dependency) and add it here or if a dependency is missing install it in the -# docker and rebuild and re-run. Repeat until recon-surf finishes successfully. Then repeat with -# all supported recon-surf flags (--hires, --fsaparc etc.). - - -# Link where to find the FreeSurfer tarball: -fslink="https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-linux-ubuntu22_amd64-7.4.1.tar.gz" - - -if [[ "$#" -lt 1 ]]; then - echo - echo "Usage: install_fs_prunded install_dir [--upx] [--url freesurfer_download_url]" - echo - echo "--upx is optional, if passed, fs/bin will be packed" - echo "--url is optional, if passed, freesurfer will be downloaded from it instead of $fslink" - echo - exit 2 -fi - -where=/opt -if [[ "$#" -ge 1 ]]; then - where=$1 - shift -fi - -upx="false" -while [[ "$#" -ge 1 ]]; do - lowercase=$(echo "$1" | tr '[:upper:]' '[:lower:]') - case $lowercase in - --upx) - upx="true" - shift - ;; - --url) - if [[ "$2" != "default" ]]; then fslink=$2; fi - shift - shift - ;; - *) - echo "Invalid argument $1" - exit 1 - ;; - esac -done -fss=$where/fs-tmp -fsd=$where/freesurfer -echo -echo "Will install FreeSurfer to $fsd" -echo -echo "FreeSurfer package to download:" -echo -echo "$fslink" -echo - - -function run_parallel () -{ - # param 1 num_parallel_processes - # param 2 command (printf string) - # param 3 how many entries to consume from $@ per "run" - # param ... parameters to format, ie. we are executing $(printf $command $@...) - i=0 - pids=() - num_parallel_processes=$1 - command=$2 - num=$3 - shift - shift - shift - args=("$@") - j=0 - while [[ "$j" -lt "${#args}" ]] - do - cmd=$(printf "$command" "${args[@]:$j:$num}") - j=$((j + num)) - $cmd & - pids=("${pids[@]}" "$!") - i=$((i + 1)) - if [[ "$i" -ge "$num_parallel_processes" ]] - then - wait "${pids[0]}" - pids=("${pids[@]:1}") - fi - done - for pid in "${pids[@]}" - do - wait "$pid" - done -} - - -# get FreeSurfer and unpack (some of it) -echo "Downloading FS and unpacking portions ..." -wget --no-check-certificate -qO- $fslink | tar zxv --no-same-owner -C $where \ - --exclude='freesurfer/average/*.gca' \ - --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ - --exclude='freesurfer/average/mult-comp-cor' \ - --exclude='freesurfer/average/samseg' \ - --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ - --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/bin/freeview.bin' \ - --exclude='freesurfer/bin/freeview' \ - --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ - --exclude='freesurfer/bin/mris_decimate_gui.bin' \ - --exclude='freesurfer/bin/mris_decimate_gui' \ - --exclude='freesurfer/bin/qdec_glmfit' \ - --exclude='freesurfer/bin/qdec.bin' \ - --exclude='freesurfer/bin/qdec' \ - --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ - --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ - --exclude='freesurfer/diffusion' \ - --exclude='freesurfer/fsafd' \ - --exclude='freesurfer/fsfast' \ - --exclude='freesurfer/lib/cuda' \ - --exclude='freesurfer/lib/images' \ - --exclude='freesurfer/lib/qt' \ - --exclude='freesurfer/lib/tcl' \ - --exclude='freesurfer/lib/tktools' \ - --exclude='freesurfer/lib/vtk' \ - --exclude='freesurfer/matlab' \ - --exclude='freesurfer/mni-1.4' \ - --exclude='freesurfer/mni' \ - --exclude='freesurfer/models' \ - --exclude='freesurfer/python/bin' \ - --exclude='freesurfer/python/include' \ - --exclude='freesurfer/python/lib' \ - --exclude='freesurfer/python/share' \ - --exclude='freesurfer/subjects/bert' \ - --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ - --exclude='freesurfer/subjects/cvs_avg35' \ - --exclude='freesurfer/subjects/fsaverage_sym' \ - --exclude='freesurfer/subjects/fsaverage3' \ - --exclude='freesurfer/subjects/fsaverage4' \ - --exclude='freesurfer/subjects/fsaverage5' \ - --exclude='freesurfer/subjects/fsaverage6' \ - --exclude='freesurfer/subjects/lh.EC_average' \ - --exclude='freesurfer/subjects/rh.EC_average' \ - --exclude='freesurfer/subjects/V1_average' \ - --exclude='freesurfer/tktools' \ - --exclude='freesurfer/trctrain' - - -# rename download to tmp -mv $where/freesurfer $fss - -# mk directories -mkdir -p $fsd/average -mkdir -p $fsd/bin -mkdir -p $fsd/etc -mkdir -p $fsd/lib/bem -mkdir -p $fsd/python/scripts -mkdir -p $fsd/python/packages/fsbindings -mkdir -p $fsd/subjects/fsaverage/label -mkdir -p $fsd/subjects/fsaverage/surf - -# We need these -copy_files=" - ASegStatsLUT.txt - build-stamp.txt - DefectLUT.txt - FreeSurferColorLUT.txt - FreeSurferEnv.sh - SegmentNoLUT.txt - SetUpFreeSurfer.sh - Simple_surface_labels2009.txt - sources.csh - SubCorticalMassLUT.txt - WMParcStatsLUT.txt - average/3T18yoSchwartzReactN32_as_orig.4dfp.hdr - average/3T18yoSchwartzReactN32_as_orig.4dfp.ifh - average/3T18yoSchwartzReactN32_as_orig.4dfp.img - average/3T18yoSchwartzReactN32_as_orig.4dfp.img.rec - average/3T18yoSchwartzReactN32_as_orig.4dfp.mat - average/3T18yoSchwartzReactN32_as_orig.lst - average/711-2B_as_mni_average_305_mask.4dfp.hdr - average/711-2B_as_mni_average_305_mask.4dfp.ifh - average/711-2B_as_mni_average_305_mask.4dfp.img - average/711-2B_as_mni_average_305_mask.4dfp.img.rec - average/711-2C_as_mni_average_305.4dfp.hdr - average/711-2C_as_mni_average_305.4dfp.ifh - average/711-2C_as_mni_average_305.4dfp.img - average/711-2C_as_mni_average_305.4dfp.img.rec - average/711-2C_as_mni_average_305.4dfp.mat - average/colortable_BA.txt - average/colortable_desikan_killiany.txt - average/colortable_vpnl.txt - average/lh.CDaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/lh.DKaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/lh.DKTaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/lh.folding.atlas.acfb40.noaparc.i12.2016-08-02.tif - average/mni305.cor.mgz - average/mni305.mask.cor.mgz - average/rh.CDaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/rh.DKaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/rh.DKTaparc.atlas.acfb40.noaparc.i12.2016-08-02.gcs - average/rh.folding.atlas.acfb40.noaparc.i12.2016-08-02.tif - bin/analyzeto4dfp - bin/AntsDenoiseImageFs - bin/asegstats2table - bin/aparcstats2table - bin/avi2talxfm - bin/compute_vox2vox - bin/defect2seg - bin/fname2stem - bin/fspython - bin/fs_temp_dir - bin/fs_temp_file - bin/fs-check-version - bin/fsr-getxopts - bin/gauss_4dfp - bin/ifh2hdr - bin/imgreg_4dfp - bin/isanalyze - bin/isnifti - bin/lta_convert - bin/make_upright - bin/mpr2mni305 - bin/mri_add_xform_to_header - bin/mri_annotation2label - bin/mri_binarize - bin/mri_brainvol_stats - bin/mri_cc - bin/mri_concat - bin/mri_concatenate_lta - bin/mri_convert - bin/mri_coreg - bin/mri_diff - bin/mri_edit_wm_with_aseg - bin/mri_fill - bin/mri_fuse_segmentations - bin/mri_glmfit - bin/mri_info - bin/mri_label2label - bin/mri_label2vol - bin/mri_mask - bin/mri_matrix_multiply - bin/mri_mc - bin/mri_normalize - bin/mri_pretess - bin/mri_relabel_hypointensities - bin/mri_robust_register - bin/mri_robust_template - bin/mri_segment - bin/mri_segstats - bin/mri_surf2surf - bin/mri_surf2volseg - bin/mri_tessellate - bin/mri_vol2surf - bin/mri_vol2vol - bin/mris_anatomical_stats - bin/mris_autodet_gwstats - bin/mris_ca_label - bin/mris_calc - bin/mris_convert - bin/mris_curvature - bin/mris_curvature_stats - bin/mris_defects_pointset - bin/mris_diff - bin/mris_euler_number - bin/mris_extract_main_component - bin/mris_fix_topology - bin/mris_inflate - bin/mris_info - bin/mris_jacobian - bin/mris_label2annot - bin/mris_place_surface - bin/mris_preproc - bin/mris_register - bin/mris_remesh - bin/mris_remove_intersection - bin/mris_sample_parc - bin/mris_smooth - bin/mris_sphere - bin/mris_topo_fixer - bin/mris_volmask - bin/mrisp_paint - bin/pctsurfcon - bin/rca-config - bin/rca-config2csh - bin/recon-all - bin/talairach_avi - bin/UpdateNeeded - bin/vertexvol - etc/recon-config.yaml - lib/bem/ic4.tri - lib/bem/ic7.tri - python/packages/fsbindings/legacy.py - python/scripts/asegstats2table - python/scripts/aparcstats2table - python/scripts/rca-config - python/scripts/rca-config2csh - subjects/fsaverage/label/lh.aparc.annot - subjects/fsaverage/label/lh.BA1_exvivo.label - subjects/fsaverage/label/lh.BA1_exvivo.thresh.label - subjects/fsaverage/label/lh.BA2_exvivo.label - subjects/fsaverage/label/lh.BA2_exvivo.thresh.label - subjects/fsaverage/label/lh.BA3a_exvivo.label - subjects/fsaverage/label/lh.BA3a_exvivo.thresh.label - subjects/fsaverage/label/lh.BA3b_exvivo.label - subjects/fsaverage/label/lh.BA3b_exvivo.thresh.label - subjects/fsaverage/label/lh.BA44_exvivo.label - subjects/fsaverage/label/lh.BA44_exvivo.thresh.label - subjects/fsaverage/label/lh.BA45_exvivo.label - subjects/fsaverage/label/lh.BA45_exvivo.thresh.label - subjects/fsaverage/label/lh.BA4a_exvivo.label - subjects/fsaverage/label/lh.BA4a_exvivo.thresh.label - subjects/fsaverage/label/lh.BA4p_exvivo.label - subjects/fsaverage/label/lh.BA4p_exvivo.thresh.label - subjects/fsaverage/label/lh.BA6_exvivo.label - subjects/fsaverage/label/lh.BA6_exvivo.thresh.label - subjects/fsaverage/label/lh.cortex.label - subjects/fsaverage/label/lh.entorhinal_exvivo.label - subjects/fsaverage/label/lh.entorhinal_exvivo.thresh.label - subjects/fsaverage/label/lh.FG1.mpm.vpnl.label - subjects/fsaverage/label/lh.FG2.mpm.vpnl.label - subjects/fsaverage/label/lh.FG3.mpm.vpnl.label - subjects/fsaverage/label/lh.FG4.mpm.vpnl.label - subjects/fsaverage/label/lh.hOc1.mpm.vpnl.label - subjects/fsaverage/label/lh.hOc2.mpm.vpnl.label - subjects/fsaverage/label/lh.hOc3v.mpm.vpnl.label - subjects/fsaverage/label/lh.hOc4v.mpm.vpnl.label - subjects/fsaverage/label/lh.MT_exvivo.label - subjects/fsaverage/label/lh.MT_exvivo.thresh.label - subjects/fsaverage/label/lh.perirhinal_exvivo.label - subjects/fsaverage/label/lh.perirhinal_exvivo.thresh.label - subjects/fsaverage/label/lh.V1_exvivo.label - subjects/fsaverage/label/lh.V1_exvivo.thresh.label - subjects/fsaverage/label/lh.V2_exvivo.label - subjects/fsaverage/label/lh.V2_exvivo.thresh.label - subjects/fsaverage/label/rh.aparc.annot - subjects/fsaverage/label/rh.BA1_exvivo.label - subjects/fsaverage/label/rh.BA1_exvivo.thresh.label - subjects/fsaverage/label/rh.BA2_exvivo.label - subjects/fsaverage/label/rh.BA2_exvivo.thresh.label - subjects/fsaverage/label/rh.BA3a_exvivo.label - subjects/fsaverage/label/rh.BA3a_exvivo.thresh.label - subjects/fsaverage/label/rh.BA3b_exvivo.label - subjects/fsaverage/label/rh.BA3b_exvivo.thresh.label - subjects/fsaverage/label/rh.BA44_exvivo.label - subjects/fsaverage/label/rh.BA44_exvivo.thresh.label - subjects/fsaverage/label/rh.BA45_exvivo.label - subjects/fsaverage/label/rh.BA45_exvivo.thresh.label - subjects/fsaverage/label/rh.BA4a_exvivo.label - subjects/fsaverage/label/rh.BA4a_exvivo.thresh.label - subjects/fsaverage/label/rh.BA4p_exvivo.label - subjects/fsaverage/label/rh.BA4p_exvivo.thresh.label - subjects/fsaverage/label/rh.BA6_exvivo.label - subjects/fsaverage/label/rh.BA6_exvivo.thresh.label - subjects/fsaverage/label/rh.cortex.label - subjects/fsaverage/label/rh.entorhinal_exvivo.label - subjects/fsaverage/label/rh.entorhinal_exvivo.thresh.label - subjects/fsaverage/label/rh.FG1.mpm.vpnl.label - subjects/fsaverage/label/rh.FG2.mpm.vpnl.label - subjects/fsaverage/label/rh.FG3.mpm.vpnl.label - subjects/fsaverage/label/rh.FG4.mpm.vpnl.label - subjects/fsaverage/label/rh.hOc1.mpm.vpnl.label - subjects/fsaverage/label/rh.hOc2.mpm.vpnl.label - subjects/fsaverage/label/rh.hOc3v.mpm.vpnl.label - subjects/fsaverage/label/rh.hOc4v.mpm.vpnl.label - subjects/fsaverage/label/rh.MT_exvivo.label - subjects/fsaverage/label/rh.MT_exvivo.thresh.label - subjects/fsaverage/label/rh.perirhinal_exvivo.label - subjects/fsaverage/label/rh.perirhinal_exvivo.thresh.label - subjects/fsaverage/label/rh.V1_exvivo.label - subjects/fsaverage/label/rh.V1_exvivo.thresh.label - subjects/fsaverage/label/rh.V2_exvivo.label - subjects/fsaverage/label/rh.V2_exvivo.thresh.label - subjects/fsaverage/surf/lh.curv - subjects/fsaverage/surf/lh.pial - subjects/fsaverage/surf/lh.pial_semi_inflated - subjects/fsaverage/surf/lh.sphere - subjects/fsaverage/surf/lh.sphere.reg - subjects/fsaverage/surf/lh.white - subjects/fsaverage/surf/rh.curv - subjects/fsaverage/surf/rh.pial - subjects/fsaverage/surf/rh.pial_semi_inflated - subjects/fsaverage/surf/rh.sphere - subjects/fsaverage/surf/rh.sphere.reg - subjects/fsaverage/surf/rh.white" -echo -for file in $copy_files -do - echo "copying $file" - cp -r $fss/$file $fsd/$file -done - -# pack if desired with upx (do this before adding all the links -if [[ "$upx" == "true" ]] ; then - echo "finding executables in $fsd/bin/..." - exe=$(find $fsd/bin -exec file {} \; | grep ELF | cut -d: -f1) - echo "packing $fsd/bin/ executables (this can take a while) ..." - run_parallel 8 "upx -9 %s %s %s %s" 4 $exe -fi - -# Modify fsbindings Python package to allow calling scripts like asegstats2table directly: -echo "from . import legacy" > "$fsd/python/packages/fsbindings/__init__.py" - -# FS looks for them, but does not call them -touch_files="/average/RB_all_2020-01-02.gca" -echo -for file in $touch_files -do - echo "touching $file" - touch $fsd/$file -done - -# FS calls these for version info, but we don't need them -# so we link them to mri_info to save space. -link_files=" - bin/mri_and - bin/mri_aparc2aseg - bin/mri_ca_label - bin/mri_ca_normalize - bin/mri_ca_register - bin/mri_compute_overlap - bin/mri_compute_seg_overlap - bin/mri_em_register - bin/mri_fwhm - bin/mri_gcut - bin/mri_log_likelihood - bin/mri_motion_correct.fsl - bin/mri_normalize_tp2 - bin/mri_or - bin/mri_relabel_nonwm_hypos - bin/mri_remove_neck - bin/mri_stats2seg - bin/mri_surf2vol - bin/mri_surfcluster - bin/mri_voldiff - bin/mri_watershed - bin/mris_divide_parcellation - bin/mris_left_right_register - bin/mris_surface_stats - bin/mris_thickness - bin/mris_thickness_diff - bin/nu_correct - bin/tkregister2_cmdl" - -# create target for link with ERROR message if called -ltrg=$fsd/bin/not-here.sh -echo '#!/bin/bash -if [ "$1" == "-all-info" ]; then - echo "$0 not included ..." - exit 0 -fi -echo -echo "ERROR: The binary $0 is not included, your call is forwarded to not-here.sh" -echo -exit 1 -' > $ltrg -chmod a+x $ltrg -echo -for file in $link_files -do - echo "linking $file" - ln -s $ltrg $fsd/$file -done - -# use our python (not really needed in recon-all anyway) -p3=$(which python3) -if [ "$p3" == "" ]; then - echo "No python3 found, please install first!" - echo - exit 1 -fi -ln -s $p3 $fsd/bin/fspython - -#cleanup -rm -rf $fss diff --git a/tools/macos_build/install_fs_pruned.sh b/tools/install_fs_pruned.sh similarity index 99% rename from tools/macos_build/install_fs_pruned.sh rename to tools/install_fs_pruned.sh index 5a78ac5d5..896254c61 100755 --- a/tools/macos_build/install_fs_pruned.sh +++ b/tools/install_fs_pruned.sh @@ -153,7 +153,7 @@ tar zxv --no-same-owner -C "$where" \ --exclude='freesurfer/trctrain' \ -f freesurfer.tar.gz -# rm -rf freesurfer.tar.gz +rm -rf freesurfer.tar.gz # rename download to tmp mv $where/freesurfer $fss diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore index 2f2aedca8..632295738 100644 --- a/tools/macos_build/.gitignore +++ b/tools/macos_build/.gitignore @@ -3,6 +3,8 @@ installer/ *.pkg *.gz +.eggs +build scripts/postinstall scripts_intell/postinstall dist/ diff --git a/tools/macos_build/README.md b/tools/macos_build/README.md index 15f93840c..b785b5fa1 100644 --- a/tools/macos_build/README.md +++ b/tools/macos_build/README.md @@ -1,4 +1,40 @@ -creating applet: python3.10 setup.py py2app --iconfile resources/fastsurfer.png +# FastSurfer MacOS packaging +## Create MacOS package -dependencies: XMLStarlet, aria2 -python dependency: py2app +In order to build the MacOS package of FastSurfer, simply run: + +```bash +./build_release_package.sh [] +``` + +Script creates release package for MacOS, where `` is the release version, `` is `arm` for arm64 arch based chips and `intel` for `x86_64` arch based chips. +`` is the directory with the fastsurfer to package. +Link to specific freesurfer distribution might be provided with `` argument. + +### Dependencies for the script + +Script is using py2app python dependency, which isn't installed through any requirements file of FastSurfer, so in order to run the script, make sure that py2app is installed. + +### Running the package + +After the script is executed, `installer` folder will be created along with the MacOS package of FastSurfer inside. +Run the package by opening it and follow instructions. + +After successful installation, FastSurfer applet and its source code will appear under the `/Applications` folder. + +### Run FastSurfer + +If you would want to run FastSurfer, you could either use terminal or FastSurfer applet. Though, running applet is recommended as it opens shell terminal and sets up environment for FastSurfer. + +#### FastSurfer Flags +* The `--fs_license` points to your FreeSurfer license which needs to be available on your computer in the `my_fs_license_dir` that was mapped above. +* The `--t1` points to the t1-weighted MRI image to analyse (full path, with mounted name inside docker: /home/user/my_mri_data => /data) +* The `--sid` is the subject ID name (output folder name) +* The `--sd` points to the output directory (its mounted name inside docker: /home/user/my_fastsurfer_analysis => /output) +* [more flags](../../doc/overview/FLAGS.md#fastsurfer-flags) + +Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ the container, not global paths on your system, so they should point to the places where you mapped these paths above with the `-v` arguments. + +A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory (specified via `--sd`). So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. + +All other available flags are identical to the ones explained on the main page [README](../../README.md). diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 0946c9a2c..d44be0e32 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -44,9 +44,9 @@ rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ # install freesurfer into temp folder if [ "$#" -gt 3 ]; then - ./install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER + ../install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER else - ./install_fs_pruned.sh $STAGED_DIR --upx + ../install_fs_pruned.sh $STAGED_DIR --upx fi SCRIPTS_DIR="./scripts" # directory with scripts executed during installation process (f.e. preinsatll postinstall) @@ -64,9 +64,8 @@ fi # assemble resources mkdir resources -cp $DIR_TO_FASTSURFER/doc/images/installer_background.png resources/ cp $DIR_TO_FASTSURFER/doc/images/fastsurfer.png resources/ -cp $DIR_TO_FASTSURFER/doc/overview/INSTRUCTIONS.md resources/ +cp $DIR_TO_FASTSURFER/doc/overview/MACOS.md resources/ cp $DIR_TO_FASTSURFER/LICENSE resources/LICENSE.txt # create fastsurfer applet @@ -107,121 +106,7 @@ productbuild --synthesize --package $OUTPUT_PKG $DISTRIBUTION_FILE # edit the distribution file # set title to package name (f.e. package_name.pkg -> package_name) -xml ed \ - -s /installer-gui-script \ - -t elem \ - -n title \ - -v "${PACKAGE_NAME}" \ - $DISTRIBUTION_FILE | \ -{ # set background image - xml ed \ - -s /installer-gui-script \ - -t elem \ - -n background \ - -i /installer-gui-script/background \ - -t attr \ - -n file \ - -v installer_background.png \ -} | \ -xml ed \ - -i /installer-gui-script/background \ - -t attr \ - -n uti \ - -v public.png \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/background \ - -t attr \ - -n scaling \ - -v proportional \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/background \ - -t attr \ - -n alignment \ - -v bottomleft \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -s /installer-gui-script \ - -t elem \ - -n background-darkAqua \ - -i /installer-gui-script/background-darkAqua \ - -t attr \ - -n file \ - -v installer_background.png \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/background-darkAqua \ - -t attr \ - -n uti \ - -v public.png \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/background-darkAqua \ - -t attr \ - -n scaling \ - -v proportional \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/background-darkAqua \ - -t attr \ - -n alignment \ - -v bottomleft \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -# set license -xml ed \ - -s /installer-gui-script \ - -t elem \ - -n license \ - -i /installer-gui-script/license \ - -t attr \ - -n file \ - -v LICENSE.txt \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -xml ed \ - -i /installer-gui-script/license \ - -t attr \ - -n mime-type \ - -v text/txt \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - -# set conclusion -xml ed \ - -s /installer-gui-script \ - -t elem \ - -n conclusion \ - -i /installer-gui-script/conclusion \ - -t attr \ - -n file \ - -v INSTRUCTIONS.md \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE - - -xml ed \ - -i /installer-gui-script/conclusion \ - -t attr \ - -n mime-type \ - -v text/txt \ - $DISTRIBUTION_FILE > "$DISTRIBUTION_FILE.temp" -mv "$DISTRIBUTION_FILE.temp" $DISTRIBUTION_FILE +python3.10 edit_distribution.py --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" # create installer package mkdir installer @@ -233,4 +118,4 @@ productbuild \ # get rid of temporary folder rm -rf $STAGED_DIR -rm -rf resources +# rm -rf resources diff --git a/tools/macos_build/edit_distribution.py b/tools/macos_build/edit_distribution.py new file mode 100644 index 000000000..7f0f852bb --- /dev/null +++ b/tools/macos_build/edit_distribution.py @@ -0,0 +1,97 @@ +# Copyright 2024 Image Analysis Lab, German Center for Neurodegenerative Diseases (DZNE), Bonn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import argparse + +from pathlib import Path +from xml.etree import ElementTree + + +def make_parser() -> argparse.ArgumentParser: + """ + Create a command line interface and return command line options. + + Returns + ------- + options + Namespace object holding options. + """ + parser = argparse.ArgumentParser( + description="Script to edit provided distribution.xml file.", + ) + parser.add_argument( + "--file", "-f", + type=Path, + dest="file", + help="path to distribution file to edit", + required=True, + ) + parser.add_argument( + "--title", "-t", + type=str, + dest="title", + help="value for the title of the distribution file", + required=True + ) + return parser + + +def edit_distribution(distribution_file: Path | str, title: str) -> None: + """ + Take path to distribution file and edit it. + + Parameters + ---------- + distribution_file : Path, str + Path of distribution file. + title : str + Value for the title tag. + """ + dist_elementtree = ElementTree.parse(distribution_file) + + root_tag = dist_elementtree.getroot() + + title_tag = ElementTree.SubElement(root_tag, "title") + title_tag.text = title + + background_tag = ElementTree.SubElement(root_tag, "background") + background_tag.attrib["file"] = "fastsurfer.png" + background_tag.attrib["uti"] = "public.png" + background_tag.attrib["scaling"] = "proportional" + background_tag.attrib["alignment"] = "bottomleft" + + background_darkAqua_tag = ElementTree.SubElement(root_tag, "background-darkAqua") + background_darkAqua_tag.attrib = background_tag.attrib + + license_tag = ElementTree.SubElement(root_tag, "license") + license_tag.attrib["file"] = "LICENSE.txt" + license_tag.attrib["mime-type"] = "text/txt" + + conclusion_tag = ElementTree.SubElement(root_tag, "conclusion") + conclusion_tag.attrib["file"] = "MACOS.md" + conclusion_tag.attrib["mime-type"] = "text/txt" + + ElementTree.indent(dist_elementtree, ' ') + + dist_elementtree.write(distribution_file, encoding="utf-8", xml_declaration=True) + + +if __name__ == "__main__": + # Command Line options are error checking done here + parser = make_parser() + args = parser.parse_args() + + print(f"Editing distribution file: {args.file} ...") + edit_distribution(args.file, args.title) + sys.exit(0) diff --git a/tools/macos_build/macos_setup_fastsurfer.sh.template b/tools/macos_build/macos_setup_fastsurfer.sh.template index f718b9ca6..dcb1ca027 100644 --- a/tools/macos_build/macos_setup_fastsurfer.sh.template +++ b/tools/macos_build/macos_setup_fastsurfer.sh.template @@ -1,7 +1,6 @@ #!/bin/bash export FASTSURFER_HOME=/Applications/ -export PATH=$FASTSURFER_HOME:$PATH source $FASTSURFER_HOME/venv/bin/activate export PYTHONPATH=$FASTSURFER_HOME/venv/lib/python3.10/site-packages:$PYTHONPATH export PYTORCH_ENABLE_MPS_FALLBACK= @@ -13,7 +12,12 @@ echo "See also https://deep-mi.org/FastSurfer/stable/overview/MACOS.html" if ! grep -q "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" ~/.bash_profile; then echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; - echo "GNU grep has been adde to path" + echo "GNU grep has been added to path" +fi + +if ! grep -q "export PATH=\"$FASTSURFER_HOME:\$PATH\"" ~/.bash_profile; then + echo "export PATH=\"$FASTSURFER_HOME:\$PATH\"" >> ~/.bash_profile; + echo "FastSurfer dir has been added to path" fi if [[ -z "${FS_LICENSE}" ]]; then diff --git a/tools/macos_build/resources/LICENSE.txt b/tools/macos_build/resources/LICENSE.txt new file mode 100644 index 000000000..dd5b3a58a --- /dev/null +++ b/tools/macos_build/resources/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/tools/macos_build/resources/MACOS.md b/tools/macos_build/resources/MACOS.md new file mode 100644 index 000000000..60d585f21 --- /dev/null +++ b/tools/macos_build/resources/MACOS.md @@ -0,0 +1,10 @@ +Running FastSurfer +================== + +If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): +`run_fastsurfer.sh --seg_only --sd --sid --t1 ` +To full run fastsurfer: +`run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` +Some files of **FreeSurfer** binaries require bypassing MacOS security, which is +significantly easier to do with the following command than manually and one by one. +`

xattr -dr com.apple.quarantine /Applications/freesurfer/*` diff --git a/tools/macos_build/resources/distribution.xml b/tools/macos_build/resources/distribution.xml new file mode 100644 index 000000000..327084849 --- /dev/null +++ b/tools/macos_build/resources/distribution.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + FastSurfer240-macos-darwin_arm64.pkg + FastSurfer240-macos-darwin_arm64 + + + + + \ No newline at end of file diff --git a/doc/images/installer_background.png b/tools/macos_build/resources/fastsurfer.png similarity index 100% rename from doc/images/installer_background.png rename to tools/macos_build/resources/fastsurfer.png From 6f50c3b2c4ab317b01dae55934fa598af21380ec Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 22 Oct 2025 16:22:47 +0200 Subject: [PATCH 06/30] Upload assets instead of artifacts in deploy.yml --- .github/workflows/deploy.yml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0e47964b3..8e1e42f89 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,13 +12,12 @@ jobs: timeout-minutes: 120 env: - ARTIFACT_NAME: FastSurfer VERSION: 242 FREESURFER_URL: https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz - UPLOAD_ARTIFACT: true - strategy: - matrix: - arch: [intel, arm] + RELEASE_ASSETS: true + strategy: + matrix: + arch: [intel, arm] steps: - name: Get repository name. run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -36,20 +35,18 @@ jobs: brew update brew upgrade || true - brew install xmlstarlet - name: package app for ${{ matrix.arch }} run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} - - name: Move artifact. - if: env.UPLOAD_ARTIFACT == 'true' + - name: Move assets. + if: env.RELEASE_ASSETS == 'true' run: | - mkdir artifact - mv tools/macos_build/installer/* artifact/ - - uses: actions/upload-artifact@v4 - if: env.UPLOAD_ARTIFACT == 'true' - name: Upload artifact. + mkdir assets + mv tools/macos_build/installer/* assets/ + - name: Upload release assets. + uses: softprops/action-gh-release@v2 + if: env.RELEASE_ASSETS == 'true' with: - name: ${{ env.ARTIFACT_NAME }} - path: ${{ env.FASTSURFER_DIR }}/artifact/ + files: ${{ env.FASTSURFER_DIR }}/assets/* # deploy-gpu: # runs-on: ubuntu-latest # timeout-minutes: 120 From 6ef6be8818efbb1b84a8d8d6480660c6a949c330 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 22 Oct 2025 16:33:00 +0200 Subject: [PATCH 07/30] Fix script imports style --- tools/macos_build/edit_distribution.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/macos_build/edit_distribution.py b/tools/macos_build/edit_distribution.py index 7f0f852bb..dbbd94e4f 100644 --- a/tools/macos_build/edit_distribution.py +++ b/tools/macos_build/edit_distribution.py @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sys import argparse - +import sys from pathlib import Path from xml.etree import ElementTree From ab056957ac384ad43e054faea0ded25ae1f4e613 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 29 Oct 2025 17:48:09 +0100 Subject: [PATCH 08/30] Add config file for tools --- .github/workflows/deploy.yml | 10 +- tools/config.yaml | 6 + tools/get_config_value.py | 83 +++++++++ tools/macos_build/.gitignore | 1 + tools/macos_build/FastSurfer.py.template | 2 - tools/macos_build/build_release_package.sh | 35 ++-- tools/macos_build/edit_distribution.py | 3 +- .../macos_setup_fastsurfer.sh.template | 6 +- tools/macos_build/resources/LICENSE.txt | 174 ------------------ tools/macos_build/resources/MACOS.md | 10 - tools/macos_build/resources/distribution.xml | 20 -- tools/macos_build/resources/fastsurfer.png | Bin 26492 -> 0 bytes .../macos_build/scripts/postinstall.template | 15 +- 13 files changed, 120 insertions(+), 245 deletions(-) create mode 100644 tools/config.yaml create mode 100644 tools/get_config_value.py delete mode 100644 tools/macos_build/resources/LICENSE.txt delete mode 100644 tools/macos_build/resources/MACOS.md delete mode 100644 tools/macos_build/resources/distribution.xml delete mode 100644 tools/macos_build/resources/fastsurfer.png diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8e1e42f89..26ad79d8e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,8 +12,6 @@ jobs: timeout-minutes: 120 env: - VERSION: 242 - FREESURFER_URL: https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz RELEASE_ASSETS: true strategy: matrix: @@ -30,13 +28,9 @@ jobs: with: python-version: '3.10' - name: install dependencies - run: | - python -m pip install py2app - - brew update - brew upgrade || true + run: python -m pip install py2app pyyaml - name: package app for ${{ matrix.arch }} - run: tools/macos_build/build_release_package.sh ${{ env.VERSION }} ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} ${{ env.FREESURFER_URL }} + run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} - name: Move assets. if: env.RELEASE_ASSETS == 'true' run: | diff --git a/tools/config.yaml b/tools/config.yaml new file mode 100644 index 000000000..3bfd7ed02 --- /dev/null +++ b/tools/config.yaml @@ -0,0 +1,6 @@ +FASTSURFER: + VERSION: '242' + PYTHON_VERSION: '3.10' + +FREESURFER_LINK_LINUX: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-linux-ubuntu22_amd64-7.4.1.tar.gz' +FREESURFER_LINK_MACOS: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz' diff --git a/tools/get_config_value.py b/tools/get_config_value.py new file mode 100644 index 000000000..55786be4e --- /dev/null +++ b/tools/get_config_value.py @@ -0,0 +1,83 @@ +# Copyright 2024 Image Analysis Lab, German Center for Neurodegenerative Diseases (DZNE), Bonn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import sys +from pathlib import Path + +import yaml + + +def make_parser() -> argparse.ArgumentParser: + """ + Create a command line interface and return command line options. + + Returns + ------- + options + Namespace object holding options. + """ + parser = argparse.ArgumentParser( + description="Tool for extracting values from configuration file", + ) + parser.add_argument( + "--file", "-f", + type=Path, + dest="file", + help="configuration file", + required=True, + ) + parser.add_argument( + "--key", "-k", + type=str, + dest="key", + help="key to lookup", + required=True, + ) + return parser + + +def extract_value(config_file: Path | str, key: str) -> str: + """ + Read configuration file and return the value for the given key. + + Parameters + ---------- + config_file : Path, str + Path to configuration file. + key : str + Key to lookup. + + Returns + ------- + value + Value under the given key. + """ + with open(config_file) as config: + try: + conf = yaml.safe_load(config) + value = conf[key.split('.')[0]] + for k in key.split('.')[1:]: + value = value[k] + return value + except yaml.YAMLError as e: + print(e) + sys.exit(1) + + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + + print(extract_value(args.file,args.key)) + sys.exit(0) diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore index 632295738..97da6dcc5 100644 --- a/tools/macos_build/.gitignore +++ b/tools/macos_build/.gitignore @@ -8,3 +8,4 @@ build scripts/postinstall scripts_intell/postinstall dist/ +resources/ \ No newline at end of file diff --git a/tools/macos_build/FastSurfer.py.template b/tools/macos_build/FastSurfer.py.template index e6048fd5b..0c8bd2225 100644 --- a/tools/macos_build/FastSurfer.py.template +++ b/tools/macos_build/FastSurfer.py.template @@ -10,5 +10,3 @@ proc = run(["/usr/bin/osascript", "-e", "end tell" ]) - -# to-do: print instructions, dont print export calls diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index d44be0e32..7bc7d30cf 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -1,8 +1,8 @@ #!/bin/bash -if [ "$#" -lt 3 ] ; then +if [ "$#" -lt 2 ] ; then echo - echo "Usage: build_release_package.sh []" + echo "Usage: build_release_package.sh " echo exit fi @@ -13,12 +13,10 @@ if [ -d "$dir" ]; then cd "$dir" fi -VERSION=$1 # version of the project -ARCH_TYPE=$2 # chip architecture - arm or intel -DIR_TO_FASTSURFER=$3 # directory to fastsurfer -if [ "$#" -gt 3 ]; then - URL_TO_FREESURFER=$4 # freesurfer install url -fi +VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.VERSION) # version of the project +URL_TO_FREESURFER=$(python3 ../get_config_value.py --file ../config.yaml --key FREESURFER_LINK_MACOS) # freesurfer install url +ARCH_TYPE=$1 # chip architecture - arm or intel +DIR_TO_FASTSURFER=$2 # directory to fastsurfer ARCH_TYPE_NAME="arm64" if [ "$ARCH_TYPE" = "intel" ]; then @@ -38,25 +36,21 @@ mkdir $STAGED_DIR rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ --exclude requirements.txt \ --exclude requirements.cpu.txt \ - --exclude Docker \ - --exclude Singularity \ --exclude tools # install freesurfer into temp folder -if [ "$#" -gt 3 ]; then - ../install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER -else - ../install_fs_pruned.sh $STAGED_DIR --upx -fi +../install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER SCRIPTS_DIR="./scripts" # directory with scripts executed during installation process (f.e. preinsatll postinstall) +PYTHON_VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.PYTHON_VERSION) # substitute values in postinstall script PATH_TO_FASTSURFER="$INSTALLATION_DIR/FastSurfer$VERSION" cp $SCRIPTS_DIR/postinstall.template $SCRIPTS_DIR/postinstall sed -i '' -e "s||${PATH_TO_FASTSURFER}|g" $SCRIPTS_DIR/postinstall -if [ "$ARCH_TYPE" = "arm" ]; then +sed -i '' -e "s||${PYTHON_VERSION}|g" $SCRIPTS_DIR/postinstall +if [ "$ARCH_TYPE" = "arm" ]; then sed -i '' -e "s||/opt/homebrew|g" $SCRIPTS_DIR/postinstall else sed -i '' -e "s||/usr/local|g" $SCRIPTS_DIR/postinstall @@ -74,6 +68,7 @@ sed -i '' -e "s||FastSurfer${VERSION}|g" FastSurfer.py cp macos_setup_fastsurfer.sh.template macos_setup_fastsurfer.sh sed -i '' -e "s||FastSurfer${VERSION}|g" macos_setup_fastsurfer.sh +sed -i '' -e "s||${PYTHON_VERSION}|g" macos_setup_fastsurfer.sh if [ "$ARCH_TYPE" = "arm" ]; then sed -i '' -e "s||1|g" macos_setup_fastsurfer.sh else @@ -81,8 +76,8 @@ else fi mv macos_setup_fastsurfer.sh $FASTSURFER_TO_PACKAGE/ -python3.10 setup.py py2app --iconfile resources/fastsurfer.png -mv dist/FastSurfer.app $STAGED_DIR/ +python3 setup.py py2app --iconfile resources/fastsurfer.png +mv dist/FastSurfer.app $STAGED_DIR/FastSurfer$VERSION.app rm -f FastSurfer.py chmod -R 755 $STAGED_DIR/* @@ -106,7 +101,7 @@ productbuild --synthesize --package $OUTPUT_PKG $DISTRIBUTION_FILE # edit the distribution file # set title to package name (f.e. package_name.pkg -> package_name) -python3.10 edit_distribution.py --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" +python3 edit_distribution.py --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" # create installer package mkdir installer @@ -118,4 +113,4 @@ productbuild \ # get rid of temporary folder rm -rf $STAGED_DIR -# rm -rf resources +rm -rf resources diff --git a/tools/macos_build/edit_distribution.py b/tools/macos_build/edit_distribution.py index dbbd94e4f..a634c4e8d 100644 --- a/tools/macos_build/edit_distribution.py +++ b/tools/macos_build/edit_distribution.py @@ -53,7 +53,7 @@ def edit_distribution(distribution_file: Path | str, title: str) -> None: Parameters ---------- distribution_file : Path, str - Path of distribution file. + Path to distribution file. title : str Value for the title tag. """ @@ -87,7 +87,6 @@ def edit_distribution(distribution_file: Path | str, title: str) -> None: if __name__ == "__main__": - # Command Line options are error checking done here parser = make_parser() args = parser.parse_args() diff --git a/tools/macos_build/macos_setup_fastsurfer.sh.template b/tools/macos_build/macos_setup_fastsurfer.sh.template index dcb1ca027..f0cff739a 100644 --- a/tools/macos_build/macos_setup_fastsurfer.sh.template +++ b/tools/macos_build/macos_setup_fastsurfer.sh.template @@ -2,7 +2,7 @@ export FASTSURFER_HOME=/Applications/ source $FASTSURFER_HOME/venv/bin/activate -export PYTHONPATH=$FASTSURFER_HOME/venv/lib/python3.10/site-packages:$PYTHONPATH +export PYTHONPATH=$FASTSURFER_HOME/venv/lib/python/site-packages:$PYTHONPATH export PYTORCH_ENABLE_MPS_FALLBACK= export FREESURFER_HOME=/Applications/freesurfer source $FREESURFER_HOME/SetUpFreeSurfer.sh > /dev/null @@ -11,12 +11,14 @@ echo "This is the FastSurfer console. Call \`run_fastsurfer.sh -t1 ...\` in here echo "See also https://deep-mi.org/FastSurfer/stable/overview/MACOS.html" if ! grep -q "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" ~/.bash_profile; then - echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; + echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; + export PATH="$(brew --prefix)/opt/grep/libexec/gnubin:$PATH" echo "GNU grep has been added to path" fi if ! grep -q "export PATH=\"$FASTSURFER_HOME:\$PATH\"" ~/.bash_profile; then echo "export PATH=\"$FASTSURFER_HOME:\$PATH\"" >> ~/.bash_profile; + export PATH="$FASTSURFER_HOME:$PATH" echo "FastSurfer dir has been added to path" fi diff --git a/tools/macos_build/resources/LICENSE.txt b/tools/macos_build/resources/LICENSE.txt deleted file mode 100644 index dd5b3a58a..000000000 --- a/tools/macos_build/resources/LICENSE.txt +++ /dev/null @@ -1,174 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. diff --git a/tools/macos_build/resources/MACOS.md b/tools/macos_build/resources/MACOS.md deleted file mode 100644 index 60d585f21..000000000 --- a/tools/macos_build/resources/MACOS.md +++ /dev/null @@ -1,10 +0,0 @@ -Running FastSurfer -================== - -If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): -`run_fastsurfer.sh --seg_only --sd --sid --t1 ` -To full run fastsurfer: -`run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` -Some files of **FreeSurfer** binaries require bypassing MacOS security, which is -significantly easier to do with the following command than manually and one by one. -`

xattr -dr com.apple.quarantine /Applications/freesurfer/*` diff --git a/tools/macos_build/resources/distribution.xml b/tools/macos_build/resources/distribution.xml deleted file mode 100644 index 327084849..000000000 --- a/tools/macos_build/resources/distribution.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - FastSurfer240-macos-darwin_arm64.pkg - FastSurfer240-macos-darwin_arm64 - - - - - \ No newline at end of file diff --git a/tools/macos_build/resources/fastsurfer.png b/tools/macos_build/resources/fastsurfer.png deleted file mode 100644 index 5d74c972b1a59e110ab2c50e8c811f68c82b6778..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26492 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%0DDT1@6vY!a~zxQG}0uX9%PRJUIULr3L?UsD5 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 diff --git a/tools/macos_build/scripts/postinstall.template b/tools/macos_build/scripts/postinstall.template index 6fa141db1..2e4c9b6e6 100755 --- a/tools/macos_build/scripts/postinstall.template +++ b/tools/macos_build/scripts/postinstall.template @@ -1,25 +1,28 @@ #!/bin/bash FASTSURFER_HOME="" +PYTHON="python" if [[ ! -f "$FASTSURFER_HOME/venv" ]] then echo "Creating FastSurfer run environment" - /bin/python3.10 -m venv --copies $FASTSURFER_HOME/venv + /bin/$PYTHON -m venv --copies $FASTSURFER_HOME/venv fi source $FASTSURFER_HOME/venv/bin/activate -python3.10 -m pip install --upgrade pip +$PYTHON -m pip install --upgrade pip export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} -python3.10 -m pip install -r $FASTSURFER_HOME/requirements.mac.txt +$PYTHON -m pip install -r $FASTSURFER_HOME/requirements.mac.txt -python3.10 $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all +$PYTHON $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all + +sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate FREESURFER_HOME="/Applications/freesurfer" -ln -sf $FASTSURFER_HOME/venv/bin/python3.10 $FREESURFER_HOME/bin/fspython +ln -sf $FASTSURFER_HOME/venv/bin/$PYTHON $FREESURFER_HOME/bin/fspython # FS calls these for version info, but we don't need them # so we link them to mri_info to save space. @@ -72,5 +75,3 @@ do echo "linking $file" ln -sf $ltrg $FREESURFER_HOME/$file done - -sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate From 7731f72aa6b0ff426e5cb0ad8a5dd1a50d289fbb Mon Sep 17 00:00:00 2001 From: David Kuegler Date: Wed, 22 Oct 2025 20:31:11 +0200 Subject: [PATCH 09/30] This commit fixes a couple of effects to the docker build settings introduced by the new tools directory structure: Fix the github docker build (used by the quicktest workflow) Update the tools directory structure Move Singularity documentation files to /doc (instead of /tools) Update docs to support the new location of the Singularity documentation file Put the MACOS documentation into the documentation tree Update the location where the docker documentation is stored in the documentation configuration file Make paths in the macos build absolute Move configuration into pyproject.toml (some is used elsewhere as well) Make changes to remove redundant configuration data Change configuration read script from yaml-based to toml-based (as most the info in the pyproject.toml file) Update macos build documentation Remove the travis file --- .dockerignore | 5 +- .github/actions/build-docker/action.yml | 2 +- .github/workflows/QUICKTEST.md | 2 +- .github/workflows/deploy.yml | 8 +- .github/workflows/quicktest.yaml | 14 +- .gitignore | 4 +- .travis.yml | 13 -- README.md | 6 +- Tutorial/Complete_FastSurfer_Tutorial.ipynb | 4 +- doc/conf.py | 2 +- doc/overview/EXAMPLES.md | 2 +- doc/overview/INSTALL.md | 4 +- doc/overview/MACOS.md | 6 +- .../README.md => doc/overview/SINGULARITY.md | 6 +- doc/overview/index.rst | 3 +- doc/overview/singularity.rst | 8 - pyproject.toml | 10 +- recon_surf/README.md | 2 +- tools/Docker/Dockerfile | 12 +- tools/Docker/README.md | 12 +- tools/Docker/build.py | 48 +++-- tools/{ => build}/install_fs_pruned.sh | 166 ++++++++++-------- .../postinstall.template => build/link_fs.sh} | 42 +++-- tools/config.yaml | 6 - {env => tools}/export_pip-r.sh | 0 tools/macos_build/README.md | 6 +- tools/macos_build/build_release_package.sh | 146 +++++++-------- .../scripts/postinstall.sh.template | 22 +++ tools/{get_config_value.py => read_toml.py} | 35 ++-- 29 files changed, 327 insertions(+), 269 deletions(-) delete mode 100644 .travis.yml rename tools/Singularity/README.md => doc/overview/SINGULARITY.md (96%) delete mode 100644 doc/overview/singularity.rst rename tools/{ => build}/install_fs_pruned.sh (73%) mode change 100755 => 100644 rename tools/{macos_build/scripts/postinstall.template => build/link_fs.sh} (59%) delete mode 100644 tools/config.yaml rename {env => tools}/export_pip-r.sh (100%) create mode 100644 tools/macos_build/scripts/postinstall.sh.template rename tools/{get_config_value.py => read_toml.py} (77%) diff --git a/.dockerignore b/.dockerignore index dd85a0e17..05aa36f38 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,14 @@ **/.git **/.github checkpoints -Singularity doc test venv Tutorial **/*.md -Docker/Dockerfile* +/tools/Docker/Dockerfile* +/tools/macos_build +/tools/export_pip-r.sh *.txt venv /srun_fastsurfer.sh diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 0c025831f..030baed11 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -54,7 +54,7 @@ runs: image_name="fastsurfer:cpu-${v/+/_}" fi if [[ -z "$(which python)" ]] ; then echo "ERROR: Cannot find python!" ; fi - cmd=(python "$FASTSURFER_HOME/Docker/build.py" --device cpu --tag "$image_name" --target "${{ inputs.target }}") + cmd=(python "$FASTSURFER_HOME/tools/Docker/build.py" --device cpu --tag "$image_name" --target "${{ inputs.target }}") if [[ "${{ inputs.freesurfer-build-image }}" != "rebuild" ]] ; then cmd+=(--freesurfer_build_image "${{ inputs.freesurfer-build-image }}") fi diff --git a/.github/workflows/QUICKTEST.md b/.github/workflows/QUICKTEST.md index b4db637dd..afb1188fa 100644 --- a/.github/workflows/QUICKTEST.md +++ b/.github/workflows/QUICKTEST.md @@ -22,7 +22,7 @@ This job sets up the necessary environments for the workflow. It depends on the ### Build Singularity Image -This job builds a Docker image and converts it to a Singularity image. It depends on the successful completion of the `prepare-job`. The Docker image is built using a Python script `Docker/build.py` with the `--device cuda --tag fastsurfer_gpu:cuda` flags. The Docker image is then converted to a Singularity image. +This job builds a Docker image and converts it to a Singularity image. It depends on the successful completion of the `prepare-job`. The Docker image is built using a Python script `tools/Docker/build.py` with the `--device cuda --tag fastsurfer_gpu:cuda` flags. The Docker image is then converted to a Singularity image. ### Run FastSurfer diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 26ad79d8e..3d4843ce7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: MAN deploy-docker +name: MAN release/deploy on: release: @@ -30,7 +30,7 @@ jobs: - name: install dependencies run: python -m pip install py2app pyyaml - name: package app for ${{ matrix.arch }} - run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} + run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} - name: Move assets. if: env.RELEASE_ASSETS == 'true' run: | @@ -57,7 +57,7 @@ jobs: # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 # - name: Build Docker image GPU - # run: python Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} + # run: python tools/Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} # - name: Add additional tags # run: | # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest @@ -80,7 +80,7 @@ jobs: # - name: Set up Docker Buildx # uses: docker/setup-buildx-action@v2 # - name: Build Docker image CPU - # run: python Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} + # run: python tools/Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} # - name: Add additional tags # run: | # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest diff --git a/.github/workflows/quicktest.yaml b/.github/workflows/quicktest.yaml index 5620dc4b0..03c4a31f4 100644 --- a/.github/workflows/quicktest.yaml +++ b/.github/workflows/quicktest.yaml @@ -32,7 +32,7 @@ on: default: build-cached type: string freesurfer-build-image: - description: 'FreeSurfer build image to build with ("" (default) => deepmi/fastsurfer-build:freesurferXXX; extract version from Docker/install_fs_pruned.sh)' + description: 'FreeSurfer build image to build with ("" (default) => deepmi/fastsurfer-build:freesurferXXX; extract version from pyproject.toml)' type: string permissions: @@ -139,10 +139,11 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ steps.parse.outputs.DOCKER_IMAGE }} - - name: Setup Python 3.10 + - name: Setup Python 3.11 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' + # python 3.11 is needed to read pyproject.toml (tomllib) architecture: 'x64' # no cache is needed, no installations and no cache is faster # cache: 'pip' # caching pip dependencies @@ -152,12 +153,9 @@ jobs: shell: bash id: parse-version run: | - # get the FreeSurfer version from install_fs_pruned.sh + # get the FreeSurfer version from pyproject.toml { - fslink="$(grep "^fslink=" ./Docker/install_fs_pruned.sh)" - fslink="${fslink:7}" - if [[ "${fslink:0:1}" == '"' ]] || [[ "${fslink:0:1}" == "'" ]] ; then fslink="${fslink:1:-1}" ; fi - fs_version="$(basename "$(dirname "$fslink")")" + fs_version="$(python ./tools/read_toml.py --file ./pyproject.toml --key tool.freesurfer.version)" fs_version_short="${fs_version//\./}" echo "FS_VERSION=$fs_version" echo "FS_VERSION_SHORT=$fs_version_short" diff --git a/.gitignore b/.gitignore index efb8526c3..fc75619a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /BUILD.info -/Docker/BUILD.info -/Docker/custom-ssl.crt +/tools/Docker/BUILD.info +/tools/Docker/custom-ssl.crt /.idea/** /rough_work/** diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ab992d5c8..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - - docker - -env: - - IMAGETAG="-t fastsurfer:gpu-beta -f ./Docker/Dockerfile" - - IMAGETAG="-t fastsurfer:cpu-beta -f ./Docker/Dockerfile_CPU" - - IMAGETAG="-t fastsurfer:segonly-gpu-beta -f ./Docker/Dockerfile_FastSurferCNN" - - IMAGETAG="-t fastsurfer:segonly-cpu-beta -f ./Docker/Dockerfile_FastSurferCNN_CPU" - - IMAGETAG="-t fastsurfer:surfonly-cpu-beta -f ./Docker/Dockerfile_reconsurf" - -script: - #- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker build --rm=true $IMAGETAG . diff --git a/README.md b/README.md index a178a31dd..835df8060 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ There are two ways to run FastSurfer (links are to installation instructions): 1. In a container ([Singularity](doc/overview/INSTALL.md#singularity) or [Docker](doc/overview/INSTALL.md#docker)) (OS: [Linux](doc/overview/INSTALL.md#linux), [Windows](doc/overview/INSTALL.md#windows), [MacOS on Intel](doc/overview/INSTALL.md#docker-currently-only-supported-for-intel-cpus)), 2. As a [native install](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204) (all OS for segmentation part). -We recommended you use Singularity or Docker on a Linux host system with a GPU. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. +We recommend you use Singularity or Docker on a Linux host system with a GPU. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. ### Usage @@ -76,11 +76,11 @@ All installation methods use the `run_fastsurfer.sh` call interface (replace `*f ``` The `--nv` flag is needed to allow FastSurfer to run on the GPU (otherwise FastSurfer will run on the CPU). - The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](tools/Singularity/README.md#mounting-home) for more info). + The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](doc/overview/SINGULARITY.md#mounting-home) for more info). The `-B` flag is used to tell singularity, which folders FastSurfer can read and write to. - See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](tools/Singularity/README.md#fastsurfer-singularity-image-usage) for details on more singularity flags. + See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](doc/overview/SINGULARITY.md#fastsurfer-singularity-image-usage) for details on more singularity flags. (b) For __docker__, the syntax is ``` diff --git a/Tutorial/Complete_FastSurfer_Tutorial.ipynb b/Tutorial/Complete_FastSurfer_Tutorial.ipynb index 94c8cd00f..ee3230266 100644 --- a/Tutorial/Complete_FastSurfer_Tutorial.ipynb +++ b/Tutorial/Complete_FastSurfer_Tutorial.ipynb @@ -1634,7 +1634,7 @@ "\n", "For [conda](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html) you are creating a [conda environment](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html#managing-environments) containing all the dependencies (fastsurfer_gpu or fastsurfer_cpu). You need to activate this environment with conda activate. To deactivate it, simply type conda deactivate. You have to activate the environment every time you want to run FastSurfer code.\n", "\n", - "For [docker](https://docs.docker.com/), you are simply downloading the fastsurfer image (deepmi/fastsurfer:gpu-v2.0.0). Alternatively, you could also download deepmi/fastsurfer:segonly-gpu-v2.0.0, deepmi/fastsurfer:segonly-cpu-v2.0.0, deepmi/fastsurfer:cpu-v2.0.0 or deepmi/fastsurfer:surfonly-cpu-v2.0.0. The first part of the name indicates what you are running (fastsurfer from the deepmi website). The tag is split into two parts. The first defines what part of the pipeline you can run with it (segonly=only segmentation, surfonly=only surface pipeline, no prefix=both options can be run) and the second defines on what you run it (cpu or gpu). If you want to modify the image, you can find the Dockerfiles the images are based on in our github page (in the [Docker folder](https://github.com/Deep-MI/FastSurfer/blob/stable/Docker/)). The [README](https://github.com/Deep-MI/FastSurfer/blob/stable/Docker/README.md) there also contains information on how to build the docker from source." + "For [docker](https://docs.docker.com/), you are simply downloading the fastsurfer image (deepmi/fastsurfer:gpu-v2.0.0). Alternatively, you could also download deepmi/fastsurfer:segonly-gpu-v2.0.0, deepmi/fastsurfer:segonly-cpu-v2.0.0, deepmi/fastsurfer:cpu-v2.0.0 or deepmi/fastsurfer:surfonly-cpu-v2.0.0. The first part of the name indicates what you are running (fastsurfer from the deepmi website). The tag is split into two parts. The first defines what part of the pipeline you can run with it (segonly=only segmentation, surfonly=only surface pipeline, no prefix=both options can be run) and the second defines on what you run it (cpu or gpu). If you want to modify the image, you can find the Dockerfiles the images are based on in our github page (in the [Docker folder](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/)). The [README](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/README.md) there also contains information on how to build the docker from source." ] }, { @@ -2192,7 +2192,7 @@ "\n", "For [conda](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html) you are creating a [conda environment](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html#managing-environments) containing all the dependencies (fastsurfer_gpu or fastsurfer_cpu). You need to activate this environment with conda activate. To deactivate it, simply type conda deactivate. You have to activate the environment every time you want to run FastSurfer code.\n", "\n", - "For [docker](https://docs.docker.com/), you are simply downloading the fastsurfer image (deepmi/fastsurfer:gpu-v2.0.0. Alternatively, you could also download deepmi/fastsurfer:segonly-gpu-v2.0.0, deepmi/fastsurfer:segonly-cpu-v2.0.0, deepmi/fastsurfer:cpu-v2.0.0 or deepmi/fastsurfer:surfonly-cpu-v2.0.0. The first part of the name indicates what you are running (fastsurfer from the deepmi website). The tag is split into two parts. The first defines what part of the pipeline you can run with it (segonly=only segmentation, surfonly=only surface pipeline, no prefix=both options can be run) and the second defines on what you run it (cpu or gpu). If you want to modify the image, you can find the Dockerfiles the images are based on in our github page (in the [Docker folder](https://github.com/Deep-MI/FastSurfer/blob/stable/Docker/)). The [README](https://github.com/Deep-MI/FastSurfer/blob/stable/Docker/README.md) there also contains information on how to build the docker from source." + "For [docker](https://docs.docker.com/), you are simply downloading the fastsurfer image (deepmi/fastsurfer:gpu-v2.0.0. Alternatively, you could also download deepmi/fastsurfer:segonly-gpu-v2.0.0, deepmi/fastsurfer:segonly-cpu-v2.0.0, deepmi/fastsurfer:cpu-v2.0.0 or deepmi/fastsurfer:surfonly-cpu-v2.0.0. The first part of the name indicates what you are running (fastsurfer from the deepmi website). The tag is split into two parts. The first defines what part of the pipeline you can run with it (segonly=only segmentation, surfonly=only surface pipeline, no prefix=both options can be run) and the second defines on what you run it (cpu or gpu). If you want to modify the image, you can find the Dockerfiles the images are based on in our github page (in the [Docker folder](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/)). The [README](https://github.com/Deep-MI/FastSurfer/blob/dev/tools/Docker/README.md) there also contains information on how to build the docker from source." ] }, { diff --git a/doc/conf.py b/doc/conf.py index a9c70804b..be1ed6383 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -271,7 +271,7 @@ def linkcode_resolve(domain, info): "^/?(.*)#(.*)ubuntu-(\\d{2})(\\d{2})": ("/\\1#\\2ubuntu-\\3-\\4",), f"{_up}readme{_end}": ("/index.rst\\1", "/overview/intro.rst\\1"), "^/overview/intro(#.*)?$": ("/overview/index.rst\\2",), - f"{_up}(singularity|docker)/readme{_end}": ("/overview/\\1.rst\\2",), + f"{_up}/tools/docker/readme{_end}": ("/overview/docker.rst\\2",), f"{_up}({_re_script_dirs})/readme{_end}": ("/scripts/\\1.rst\\2",), f"{_up}license": ("/overview/license.rst",), } diff --git a/doc/overview/EXAMPLES.md b/doc/overview/EXAMPLES.md index 32e9a500d..930ee33fb 100644 --- a/doc/overview/EXAMPLES.md +++ b/doc/overview/EXAMPLES.md @@ -42,7 +42,7 @@ A directory with the name as specified in `--sid` (here subjectX) will be create If you do not have a GPU, you can also run our CPU-Docker by dropping the `--gpus all` flag and specifying `--device cpu` at the end as a FastSurfer flag, see also [FastSurfer's docker documentation](../../tools/Docker/README.md) for more details. ## Example 2: FastSurfer Singularity -After building the Singularity image (see below or [these instructions](../../tools/Singularity/README.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. +After building the Singularity image (see below or [these instructions](SINGULARITY.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. To run FastSurfer on a given subject using the Singularity image with GPU access, execute the following commands from a directory where you want to store singularity images. This will create a singularity image from our Dockerhub image and execute it: diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 2f2508531..f7a180148 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -22,9 +22,9 @@ Assuming you have singularity installed already (by a system admin), you can bui ```bash singularity build fastsurfer-gpu.sif docker://deepmi/fastsurfer:latest ``` -Additionally, [the Singularity README](../../tools/Singularity/README.md) contains detailed directions for building your own Singularity images from Docker. +Additionally, [the Singularity README](SINGULARITY.md) contains detailed directions for building your own Singularity images from Docker. -[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../tools/Docker/README.md) and [Singularity](../../tools/Singularity/README.md). +[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../tools/Docker/README.md) and [Singularity](SINGULARITY.md). ### Docker diff --git a/doc/overview/MACOS.md b/doc/overview/MACOS.md index 60d585f21..4899f7c04 100644 --- a/doc/overview/MACOS.md +++ b/doc/overview/MACOS.md @@ -1,5 +1,7 @@ -Running FastSurfer -================== +Running FastSurfer as MacOS package +=================================== + +[Installation Instructions](INSTALL.md#macos-) If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): `run_fastsurfer.sh --seg_only --sd --sid --t1 ` diff --git a/tools/Singularity/README.md b/doc/overview/SINGULARITY.md similarity index 96% rename from tools/Singularity/README.md rename to doc/overview/SINGULARITY.md index 9638d9bd0..a379241c2 100644 --- a/tools/Singularity/README.md +++ b/doc/overview/SINGULARITY.md @@ -1,4 +1,4 @@ -# FastSurfer Singularity Support +# Singularity Support For use on HPCs (or in other cases where Docker is not available or preferred) you can easily create a Singularity image from the Docker image. Singularity uses its own image format, so the Docker images must be converted (we publish our releases as docker images available on [Dockerhub](https://hub.docker.com/r/deepmi/fastsurfer/tags)). @@ -12,7 +12,7 @@ Singularity images are files - usually with the extension `.sif`. Here, we save If you want to pick a specific FastSurfer version, you can also change the tag (`latest`) in `deepmi/fastsurfer:latest` to any tag. For example to use the cpu image hosted on [Dockerhub](https://hub.docker.com/r/deepmi/fastsurfer/tags) use the tag `cpu-latest`. ## Building your own FastSurfer Singularity Image -To build a custom FastSurfer Singularity image, the `Docker/build.py` script supports a flag for direct conversion. +To build a custom FastSurfer Singularity image, the `tools/Docker/build.py` script supports a flag for direct conversion. Simply add `--singularity /home/user/my_singlarity_images/fastsurfer-myimage.sif` to the call, which first builds the image with Docker and then converts the image to Singularity. If you want to manually convert the local Docker image `fastsurfer:myimage`, run: @@ -21,7 +21,7 @@ If you want to manually convert the local Docker image `fastsurfer:myimage`, run singularity build /home/user/my_singlarity_images/fastsurfer-myimage.sif docker-daemon://fastsurfer:myimage ``` -For more information on how to create your own Docker images, see our [Docker guide](../Docker/README.md). +For more information on how to create your own Docker images, see our [Docker guide](../../tools/Docker/README.md). ## FastSurfer Singularity Image Usage diff --git a/doc/overview/index.rst b/doc/overview/index.rst index c03b89c15..56831f195 100644 --- a/doc/overview/index.rst +++ b/doc/overview/index.rst @@ -11,7 +11,8 @@ User Guide FLAGS.md OUTPUT_FILES.md docker - singularity + SINGULARITY.md + MACOS.md EDITING.md LONG.md SECURITY.md diff --git a/doc/overview/singularity.rst b/doc/overview/singularity.rst deleted file mode 100644 index de76f9bf5..000000000 --- a/doc/overview/singularity.rst +++ /dev/null @@ -1,8 +0,0 @@ -Singularity Support -------------------- - -.. include:: ../../tools/Singularity/README.md - :parser: fix_links.parser - :relative-docs: . - :relative-images: - :start-line: 1 diff --git a/pyproject.toml b/pyproject.toml index 41da9da8c..4052b06e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,14 +101,20 @@ documentation = 'https://fastsurfer.org' source = 'https://github.com/Deep-MI/FastSurfer' tracker = 'https://github.com/Deep-MI/FastSurfer/issues' +[tool.freesurfer] +version = "7.4.1" +[tool.freesurfer.urls] +linux = "https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/{version}/freesurfer-linux-ubuntu22_amd64-{version}.tar.gz" +macOS = "https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/{version}/freesurfer-macOS-darwin_x86_64-{version}.tar.gz" + [tool.setuptools] -packages = ['FastSurferCNN','CerebNet','recon_surf'] +packages = ['FastSurferCNN','CerebNet','recon_surf','HypVINN'] [tool.pydocstyle] convention = 'numpy' ignore-decorators = '(copy_doc|property|.*setter|.*getter|pyqtSlot|Slot)' match = '^(?!setup|__init__|test_).*\.py' -match-dir = '^FastSurferCNN.*,^CerebNet.*,^recon-surf.*' +match-dir = '^FastSurferCNN.*,^CerebNet.*,^recon-surf.*,^HypVINN.*' add_ignore = 'D100,D104,D107' [tool.ruff] diff --git a/recon_surf/README.md b/recon_surf/README.md index e452eb7dc..b6a4322b9 100644 --- a/recon_surf/README.md +++ b/recon_surf/README.md @@ -92,7 +92,7 @@ Check [Dockerhub](https://hub.docker.com/r/deepmi/fastsurfer/tags) to find out t * The `-B` commands mount your output, and directory with the FreeSurfer license file into the Singularity container. Inside the container these are visible under the name following the colon (in this case /data, /output, and /fs_license). -* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../tools/Singularity/README.md#mounting-home)) +* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../doc/overview/SINGULARITY.md#mounting-home)) The `--t1` and `--asegdkt_segfile` flags point to the already existing conformed T1 input and segmentation from the segmentation module. Also other files from that pipeline will be reused (e.g. the `mask.mgz`, `orig_nu.mgz`). The diff --git a/tools/Docker/Dockerfile b/tools/Docker/Dockerfile index 48b608576..beee87992 100644 --- a/tools/Docker/Dockerfile +++ b/tools/Docker/Dockerfile @@ -87,7 +87,7 @@ FROM build_base AS build_common # get install scripts into docker ARG MAMBA_SSL_CERTIFICATE="" -COPY ./env/fastsurfer.yml ./Docker/conda_pack.sh ./Docker/install_env.py ${MAMBA_SSL_CERTIFICATE} /install/ +COPY ./env/fastsurfer.yml ./tools/Docker/conda_pack.sh ./tools/Docker/install_env.py ${MAMBA_SSL_CERTIFICATE} /install/ SHELL ["/bin/bash", "--login", "-c", "-e"] # Install conda for gpu @@ -116,7 +116,7 @@ RUN python /install/install_env.py -m ${DEVICE} -i /install/fastsurfer.yml -o /i FROM build_base AS build_freesurfer # get install scripts into docker -COPY ./tools/install_fs_pruned.sh /install/ +COPY ./tools/build/install_fs_pruned.sh ./tools/build/link_fs.sh /install/ SHELL ["/bin/bash", "--login", "-c"] ARG FREESURFER_URL=default @@ -124,8 +124,8 @@ ARG FREESURFER_URL=default # install freesurfer and point to new python location RUN /install/install_fs_pruned.sh /opt --upx --url $FREESURFER_URL && \ rm /opt/freesurfer/bin/fspython && \ - rm -R /install && \ - ln -s /venv/bin/python3 /opt/freesurfer/bin/fspython + /install/link_fs.sh /venv/bin/python3 /opt/freesurfer && \ + rm -R /install # ======================================================= @@ -201,7 +201,7 @@ ENV PYTHONPATH=/fastsurfer:/opt/freesurfer/python/packages \ RUN cd /fastsurfer ; python3 FastSurferCNN/download_checkpoints.py --all && \ python3 -m compileall * && \ python3 FastSurferCNN/version.py --sections +git+checkpoints+pip \ - --build_cache Docker/BUILD.info -o BUILD.info + --build_cache tools/Docker/BUILD.info -o BUILD.info # TODO: SBOM info of FastSurfer and FreeSurfer are missing, it is unclear how to add # those at the moment, as the buildscanner syft does not find simple package.json @@ -221,7 +221,7 @@ RUN cd /fastsurfer ; python3 FastSurferCNN/download_checkpoints.py --all && \ # the script entrypoint ensures that our conda env is active USER nonroot WORKDIR "/fastsurfer" -ENTRYPOINT ["/fastsurfer/Docker/entrypoint.sh","/fastsurfer/run_fastsurfer.sh"] +ENTRYPOINT ["/fastsurfer/tools/Docker/entrypoint.sh","/fastsurfer/run_fastsurfer.sh"] CMD ["--help"] FROM runtime AS runtime_cuda diff --git a/tools/Docker/README.md b/tools/Docker/README.md index f28cb6e83..6c8b46afe 100644 --- a/tools/Docker/README.md +++ b/tools/Docker/README.md @@ -78,7 +78,7 @@ Note, for many HPC users with limited GPUs or with very large datasets, it may b Also note, in order to run our Docker containers on a Mac, users need to increase docker memory to 10 GB by overwriting the settings under Docker Desktop --> Preferences --> Resources --> Advanced (slide the bar under Memory to 10 GB; see: [Change Docker Desktop settings on Mac](https://docs.docker.com/desktop/settings/mac/) for details). For the new Apple silicon chips (M1,etc), we noticed that a native install runs much faster than docker, because the Apple Accelerator (use the experimental MPS device via `--device mps`) can be used. There is no support for MPS-based acceleration through docker at the moment. ### General build settings -The build script `build.py` supports additional args, targets and options, see `python Docker/build.py --help`. +The build script `build.py` supports additional args, targets and options, see `python tools/Docker/build.py --help`. Note, that the build script's main function is to select parameters for build args, but also create the FastSurfer-root/BUILD.info file, which will be used by FastSurfer to document the version (including git hash of the docker container). This BUILD.info file must exist for the docker build to be successful. In general, if you specify `--dry_run` the command will not be executed but sent to stdout, so you can run `python build.py --device cuda --dry_run | bash` as well. Note, that build.py uses some dependencies from FastSurfer, so you will need to set the PYTHONPATH environment variable to the FastSurfer root (include of `FastSurferCNN` must be possible) and we only support Python 3.10. @@ -158,7 +158,7 @@ docker run --rm --security-opt seccomp=unconfined \ In conflict with the official ROCm documentation (above), we also needed to add the group render `--group-add render` (in addition to `--group-add video`). -Note, we tested on an AMD Radeon Pro W6600, which is [not officially supported](https://docs.amd.com/en/latest/release/gpu_os_support.html), but setting `HSA_OVERRIDE_GFX_VERSION=10.3.0` [inside docker did the trick](https://en.opensuse.org/SDB:AMD_GPGPU#Using_CUDA_code_with_ZLUDA_and_ROCm): +Note, we tested on an AMD Radeon Pro W6600, which is [not officially supported](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/system-requirements.html#supported-gpus), but setting `HSA_OVERRIDE_GFX_VERSION=10.3.0` [inside docker did the trick](https://en.opensuse.org/SDB:AMD_GPGPU#Using_CUDA_code_with_ZLUDA_and_ROCm): ```bash docker run --rm --security-opt seccomp=unconfined \ @@ -192,7 +192,7 @@ To build a docker image with attestation and provenance, i.e. Software Bill Of M 3. Attestation files are not supported by the standard docker image storage driver. Therefore, images cannot be tested locally. There are two solutions to this limitation. 1. Directly push to the registry: - Add `--action push` to the build script (the default is `--action load`, which loads the created image into the current docker context, and for the image name, also add the registry name. For example `... python Docker/build.py ... --attest --action push --tag docker.io//fastsurfer:latest`. + Add `--action push` to the build script (the default is `--action load`, which loads the created image into the current docker context, and for the image name, also add the registry name. For example `... python tools/Docker/build.py ... --attest --action push --tag docker.io//fastsurfer:latest`. 2. [Install the containerd image storage driver](https://docs.docker.com/storage/containerd/#enable-containerd-image-store-on-docker-engine), which supports attestation: To implement this on Linux, make sure your docker daemon config file `/etc/docker/daemon.json` includes ```json { @@ -202,7 +202,7 @@ To build a docker image with attestation and provenance, i.e. Software Bill Of M } ``` Also note, that the image storage location with containerd is not defined by the docker config file `/etc/docker/daemon.json`, but by the containerd config `/etc/containerd/config.toml`, which will likely not exist. You can [create a default config](https://github.com/containerd/containerd/blob/main/docs/getting-started.md#customizing-containerd) file with `containerd config default > /etc/containerd/config.toml`, in this config file edit the `"root"`-entry (default value is `/var/lib/containerd`). -4. Finally, you can now build the FastSurfer image with `python Docker/build.py ... --attest`. This will add the additional flags to the docker build command. +4. Finally, you can now build the FastSurfer image with `python tools/Docker/build.py ... --attest`. This will add the additional flags to the docker build command. ## Setting the ssl_verify parameter of mamba @@ -222,7 +222,7 @@ build_dir=$HOME/FastSurfer-build img=deepmi/fastsurfer # the version can be identified with: $build_dir/run_fastsurfer.sh --version version=2.4.3 -# the cuda and rocm version can be identified with: python $build_dir/Docker/build.py --help | grep -E ^[[:space:]]+--device +# the cuda and rocm version can be identified with: python $build_dir/tools/Docker/build.py --help | grep -E ^[[:space:]]+--device cuda=126 cudas=("cuda118" "cuda124" "cuda$cuda") rocm=6.2.4 @@ -236,7 +236,7 @@ all_tags=("latest" "gpu-latest" "cuda-v$version" "rocm-v$version" "cpu-latest") # build all distinct images for dev in cpu "${rocms[@]}" "${cudas[@]}" do - python3 Docker/build.py --tag $img:$dev-v$version --freesurfer_build_image $img-build:freesurfer741 --attest --device $dev + python3 tools/Docker/build.py --tag $img:$dev-v$version --freesurfer_build_image $img-build:freesurfer741 --attest --device $dev all_tags+=("$dev-v$version") done # labels that are just references diff --git a/tools/Docker/build.py b/tools/Docker/build.py index 4ab7a5ade..94247bed4 100755 --- a/tools/Docker/build.py +++ b/tools/Docker/build.py @@ -21,10 +21,22 @@ import logging import os import subprocess +import sys from collections.abc import Sequence from pathlib import Path from typing import Literal, cast, get_args +# python 3.11 supports tomllib, but we have tomli in fastsurfer +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError as e: + raise RuntimeError( + "The FastSurfer build script requires tomli or python 3.11 to load the pyproject.toml file." + ) from e + logger = logging.getLogger(__name__) Target = Literal['runtime', 'build_common', 'build_conda', 'build_freesurfer', @@ -60,10 +72,13 @@ class DEFAULTS: # torch 2.0.1 comes compiled with cu117, cu118, and rocm5.4.2 # torch 2.4 comes compiled with cu118, cu121, cu124 and rocm6.1 # torch 2.6 comes compiled with cu118, cu124, cu126 and rocm6.2.4 + CUDA="cu126" + CUDA_VERSION="12.6" + ROCM="rocm6.2.4" MapDeviceType: dict[AllDeviceType, DeviceType] = dict( ((d, d) for d in get_args(DeviceType)), - rocm="rocm6.2.4", - cuda="cu126", + rocm=ROCM, + cuda=CUDA, ) BUILD_BASE_IMAGE = "ubuntu:22.04" RUNTIME_BASE_IMAGE = "ubuntu:22.04" @@ -195,12 +210,12 @@ def make_parser() -> argparse.ArgumentParser: parser.add_argument( "--device", - choices=["cpu", "cuda", "cu118", "cu124", "cu126", "rocm", "rocm6.2.4"], + choices=list(get_args(AllDeviceType)), required=True, - help="""selection of internal build stages to build for a specific platform.
- - cuda: defaults to cu126, cuda 12.6
+ help=f"""selection of internal build stages to build for a specific platform.
+ - cuda: defaults to {DEFAULTS.CUDA}, cuda {DEFAULTS.CUDA_VERSION}
- cpu: only cpu support
- - rocm: defaults to rocm6.2.4 (experimental)""", + - rocm: defaults to {DEFAULTS.ROCM} (experimental)""", ) parser.add_argument( "--tag", @@ -648,6 +663,9 @@ def main( kwargs["cache_to"] = cache.format_cache_from() fastsurfer_home = Path(fastsurfer_home) if fastsurfer_home else default_home() + # read the freesurfer download url from pyproject.toml + with open(fastsurfer_home / "pyproject.toml", "rb") as fp: + pyproject_freesurfer = tomllib.load(fp)["tool"]["freesurfer"] if target not in get_args(Target): raise ValueError(f"Invalid target: {target}") @@ -659,7 +677,10 @@ def main( if device.startswith("cu") and target == "runtime": target = "runtime_cuda" kwargs["target"] = target - kwargs["build_arg"] = [f"DEVICE={DEFAULTS.MapDeviceType.get(device, 'cpu')}"] + kwargs["build_arg"] = [ + f"DEVICE={DEFAULTS.MapDeviceType.get(device, 'cpu')}", + f"FREESURFER_URL={pyproject_freesurfer['urls']['linux'].format(version=pyproject_freesurfer['version'])}" + ] if debug: kwargs["build_arg"].append("DEBUG=true") build_arg_list = [ @@ -677,14 +698,14 @@ def main( if ssl_verify is False: kwargs["build_arg"].append("MAMBA_SSL_VERIFY=") else: - _ssl_cert = "./Docker/custom-ssl.crt" + _ssl_cert = "tools/Docker/custom-ssl.crt" if (fastsurfer_home / _ssl_cert).exists(): (fastsurfer_home / _ssl_cert).unlink() from shutil import copy2 copy2(ssl_verify, fastsurfer_home / _ssl_cert) kwargs["build_arg"].append(f"MAMBA_SSL_CERTIFICATE={_ssl_cert}") kwargs["build_arg"].append(f"MAMBA_SSL_VERIFY=/install/{Path(_ssl_cert).name}") - build_filename = fastsurfer_home / "Docker/BUILD.info" + build_filename = fastsurfer_home / "tools" / "Docker" / "BUILD.info" if has_git(): version_sections = "+git" else: @@ -729,7 +750,7 @@ def main( logger.info("Version info added to the docker image:") logger.info(build_info["content"]) - dockerfile = fastsurfer_home / "Docker" / "Dockerfile" + dockerfile = fastsurfer_home / "tools" / "Docker" / "Dockerfile" try: docker_build_image( image_tag, @@ -759,12 +780,9 @@ def default_home() -> Path: Returns ------- Path - The FASTSURFER_HOME-path. + The FastSurfer root path belonging to this build.py file. """ - if "FASTSURFER_HOME" in os.environ: - return Path(os.environ["FASTSURFER_HOME"]) - else: - return Path(__file__).parent.parent + return Path(__file__).parents[2] if __name__ == "__main__": diff --git a/tools/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh old mode 100755 new mode 100644 similarity index 73% rename from tools/install_fs_pruned.sh rename to tools/build/install_fs_pruned.sh index 896254c61..c5f3ff02d --- a/tools/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -11,16 +11,19 @@ # all supported recon-surf flags (--hires, --fsaparc etc.). -# Link where to find the FreeSurfer tarball: -fslink="https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz" - +if [[ -z "${BASH_SOURCE[0]}" ]]; then THIS_SCRIPT="$0" +else THIS_SCRIPT="${BASH_SOURCE[0]}" +fi +# Link where to find the FreeSurfer tarball: +fslink="default" if [[ "$#" -lt 1 ]]; then echo - echo "Usage: install_fs_prunded install_dir [--upx] [--url freesurfer_download_url]" + echo "Usage: install_fs_pruned.sh install_dir [--upx] [--url freesurfer_download_url]" echo echo "--upx is optional, if passed, fs/bin will be packed" - echo "--url is optional, if passed, freesurfer will be downloaded from it instead of $fslink" + echo "--url is recommended! This is the download link for freesurfer." + echo " The link can be found in pyproject.toml:tool.freesurfer.url!" echo exit 2 fi @@ -35,23 +38,39 @@ upx="false" while [[ "$#" -ge 1 ]]; do lowercase=$(echo "$1" | tr '[:upper:]' '[:lower:]') case $lowercase in - --upx) - upx="true" - shift - ;; - --url) - if [[ "$2" != "default" ]]; then fslink=$2; fi - shift - shift - ;; - *) - echo "Invalid argument $1" - exit 1 - ;; + --upx) upx="true" ; shift ;; + --url) fslink=$2 ; shift ; shift ;; + *) echo "Invalid argument $1" ; exit 1 ;; esac done fss=$where/fs-tmp fsd=$where/freesurfer + +if [[ "$fslink" == "default" ]] +then + # --url not provided, try getting it from pyproject.toml +link=$(python3 <= (3, 11): import tomllib +else: + try: import tomli as tomllib + except Exception: sys.exit() + +for path in pathlib.Path("$THIS_SCRIPT").parents: + try: + if (path / "pyproject.toml").exists(): + with open(path / "pyproject.toml", "rb") as fp: dat = tomllib.load(fp)["tool"]["freesurfer"] + print(dat["urls"]["linux"].format(**dat)) + break + except Exception: + continue # ignore all errors +EOF +) + if [[ -n "$link" ]] ; then fslink="$link" + else echo "ERROR: Please provide the --url argument, could not find/parse pyproject.toml!" ; exit 1 + fi +fi + echo echo "Will install FreeSurfer to $fsd" echo @@ -99,61 +118,66 @@ function run_parallel () # get FreeSurfer and unpack (some of it) echo "Downloading FS and unpacking portions ..." -aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" +if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") +elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") +else aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) +fi -tar zxv --no-same-owner -C "$where" \ - --exclude='freesurfer/average/*.gca' \ - --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ - --exclude='freesurfer/average/mult-comp-cor' \ - --exclude='freesurfer/average/samseg' \ - --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ - --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/bin/freeview.bin' \ - --exclude='freesurfer/bin/freeview' \ - --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ - --exclude='freesurfer/bin/mris_decimate_gui.bin' \ - --exclude='freesurfer/bin/mris_decimate_gui' \ - --exclude='freesurfer/bin/qdec_glmfit' \ - --exclude='freesurfer/bin/qdec.bin' \ - --exclude='freesurfer/bin/qdec' \ - --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ - --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ - --exclude='freesurfer/diffusion' \ - --exclude='freesurfer/fsafd' \ - --exclude='freesurfer/fsfast' \ - --exclude='freesurfer/lib/cuda' \ - --exclude='freesurfer/lib/images' \ - --exclude='freesurfer/lib/qt' \ - --exclude='freesurfer/lib/tcl' \ - --exclude='freesurfer/lib/tktools' \ - --exclude='freesurfer/lib/vtk' \ - --exclude='freesurfer/matlab' \ - --exclude='freesurfer/mni-1.4' \ - --exclude='freesurfer/mni' \ - --exclude='freesurfer/models' \ - --exclude='freesurfer/python/bin' \ - --exclude='freesurfer/python/include' \ - --exclude='freesurfer/python/lib' \ - --exclude='freesurfer/python/share' \ - --exclude='freesurfer/subjects/bert' \ - --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ - --exclude='freesurfer/subjects/cvs_avg35' \ - --exclude='freesurfer/subjects/fsaverage_sym' \ - --exclude='freesurfer/subjects/fsaverage3' \ - --exclude='freesurfer/subjects/fsaverage4' \ - --exclude='freesurfer/subjects/fsaverage5' \ - --exclude='freesurfer/subjects/fsaverage6' \ - --exclude='freesurfer/subjects/lh.EC_average' \ - --exclude='freesurfer/subjects/rh.EC_average' \ - --exclude='freesurfer/subjects/V1_average' \ - --exclude='freesurfer/tktools' \ - --exclude='freesurfer/trctrain' \ - -f freesurfer.tar.gz +"${dl[@]}" | tar zxv --no-same-owner -C "$where" \ + --exclude='freesurfer/average/*.gca' \ + --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ + --exclude='freesurfer/average/mult-comp-cor' \ + --exclude='freesurfer/average/samseg' \ + --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ + --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/bin/freeview.bin' \ + --exclude='freesurfer/bin/freeview' \ + --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ + --exclude='freesurfer/bin/mris_decimate_gui.bin' \ + --exclude='freesurfer/bin/mris_decimate_gui' \ + --exclude='freesurfer/bin/qdec_glmfit' \ + --exclude='freesurfer/bin/qdec.bin' \ + --exclude='freesurfer/bin/qdec' \ + --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ + --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ + --exclude='freesurfer/diffusion' \ + --exclude='freesurfer/fsafd' \ + --exclude='freesurfer/fsfast' \ + --exclude='freesurfer/lib/cuda' \ + --exclude='freesurfer/lib/images' \ + --exclude='freesurfer/lib/qt' \ + --exclude='freesurfer/lib/tcl' \ + --exclude='freesurfer/lib/tktools' \ + --exclude='freesurfer/lib/vtk' \ + --exclude='freesurfer/matlab' \ + --exclude='freesurfer/mni-1.4' \ + --exclude='freesurfer/mni' \ + --exclude='freesurfer/models' \ + --exclude='freesurfer/python/bin' \ + --exclude='freesurfer/python/include' \ + --exclude='freesurfer/python/lib' \ + --exclude='freesurfer/python/share' \ + --exclude='freesurfer/subjects/bert' \ + --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ + --exclude='freesurfer/subjects/cvs_avg35' \ + --exclude='freesurfer/subjects/fsaverage_sym' \ + --exclude='freesurfer/subjects/fsaverage3' \ + --exclude='freesurfer/subjects/fsaverage4' \ + --exclude='freesurfer/subjects/fsaverage5' \ + --exclude='freesurfer/subjects/fsaverage6' \ + --exclude='freesurfer/subjects/lh.EC_average' \ + --exclude='freesurfer/subjects/rh.EC_average' \ + --exclude='freesurfer/subjects/V1_average' \ + --exclude='freesurfer/tktools' \ + --exclude='freesurfer/trctrain' -rm -rf freesurfer.tar.gz +if [[ -z "$(which wget)" ]] && [[ -z "$(which curl)" ]] +then # cleanup the file saved by aria2c + rm freesurfer.tar.gz +fi # rename download to tmp mv $where/freesurfer $fss diff --git a/tools/macos_build/scripts/postinstall.template b/tools/build/link_fs.sh similarity index 59% rename from tools/macos_build/scripts/postinstall.template rename to tools/build/link_fs.sh index 2e4c9b6e6..896060c73 100755 --- a/tools/macos_build/scripts/postinstall.template +++ b/tools/build/link_fs.sh @@ -1,28 +1,23 @@ -#!/bin/bash +#!/usr/bin/env bash -FASTSURFER_HOME="" -PYTHON="python" +# usage: link_fs.sh [ []] -if [[ ! -f "$FASTSURFER_HOME/venv" ]] +if [[ "$#" -gt 0 ]] && { [[ "${*/-h/}" != "$*" ]] || [[ "${*/--help/}" != "$*" ]] ; } ; then + echo "usage: $0 [ []]" + exit 0 +elif [[ "$#" == 1 ]] || [[ "$#" == 2 ]] then - echo "Creating FastSurfer run environment" - /bin/$PYTHON -m venv --copies $FASTSURFER_HOME/venv + if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exit!" ; exit 1 ; fi + PYTHON="$1" + if [[ "$#" == 2 ]] ; then FREESURFER_HOME="$2" ; fi +else + PYTHON=$(which python3) +fi +if [[ -z "$FREESURFER_HOME" ]] || [[ ! -d "$FREESURFER_HOME" ]] +then + echo "ERROR: FREESURFER_HOME not defined correctly!" + exit 1 fi - -source $FASTSURFER_HOME/venv/bin/activate -$PYTHON -m pip install --upgrade pip - -export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} - -$PYTHON -m pip install -r $FASTSURFER_HOME/requirements.mac.txt - -$PYTHON $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all - -sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate - -FREESURFER_HOME="/Applications/freesurfer" - -ln -sf $FASTSURFER_HOME/venv/bin/$PYTHON $FREESURFER_HOME/bin/fspython # FS calls these for version info, but we don't need them # so we link them to mri_info to save space. @@ -73,5 +68,8 @@ echo for file in $link_files do echo "linking $file" - ln -sf $ltrg $FREESURFER_HOME/$file + ln -s "$ltrg" "$FREESURFER_HOME/$file" done + +# use our python (not really needed in recon-all anyway) +ln -sf "$PYTHON" "$FREESURFER_HOME/bin/fspython" diff --git a/tools/config.yaml b/tools/config.yaml deleted file mode 100644 index 3bfd7ed02..000000000 --- a/tools/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -FASTSURFER: - VERSION: '242' - PYTHON_VERSION: '3.10' - -FREESURFER_LINK_LINUX: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-linux-ubuntu22_amd64-7.4.1.tar.gz' -FREESURFER_LINK_MACOS: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz' diff --git a/env/export_pip-r.sh b/tools/export_pip-r.sh similarity index 100% rename from env/export_pip-r.sh rename to tools/export_pip-r.sh diff --git a/tools/macos_build/README.md b/tools/macos_build/README.md index b785b5fa1..1d7a0e839 100644 --- a/tools/macos_build/README.md +++ b/tools/macos_build/README.md @@ -4,12 +4,10 @@ In order to build the MacOS package of FastSurfer, simply run: ```bash -./build_release_package.sh [] +./build_release_package.sh ``` -Script creates release package for MacOS, where `` is the release version, `` is `arm` for arm64 arch based chips and `intel` for `x86_64` arch based chips. -`` is the directory with the fastsurfer to package. -Link to specific freesurfer distribution might be provided with `` argument. +Script creates release package for MacOS, where `` is `arm` for arm64 arch based chips and `intel` for `x86_64` arch based chips. ### Dependencies for the script diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 7bc7d30cf..d5c125a5e 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -1,116 +1,124 @@ #!/bin/bash -if [ "$#" -lt 2 ] ; then +if [[ "$#" != 1 ]] || { [[ "$1" != "arm" ]] && [[ "$1" != "intel" ]] ; } ; then echo - echo "Usage: build_release_package.sh " + echo "Usage: build_release_package.sh " echo exit fi -# cd into directory with this script -dir=${0%/*} -if [ -d "$dir" ]; then - cd "$dir" -fi - -VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.VERSION) # version of the project -URL_TO_FREESURFER=$(python3 ../get_config_value.py --file ../config.yaml --key FREESURFER_LINK_MACOS) # freesurfer install url +if [[ -z "${BASH_SOURCE[0]}" ]]; then THIS_SCRIPT="$0" +else THIS_SCRIPT="${BASH_SOURCE[0]}" +fi +build_dir=$(dirname "$THIS_SCRIPT") +tools_dir=$(dirname "$build_dir") + +FASTSURFER_HOME=$(dirname "$tools_dir") # directory to fastsurfer +# version of the project +VERSION=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.yaml" --key project.version) +VERSION_NO_DOTS=${VERSION//./} +# freesurfer install url +URL_TO_FREESURFER_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.yaml" --key freesurfer.DOWNLOAD_MACOS) +sub="{version}" +URL_TO_FREESURFER="${URL_TO_FREESURFER_TEMP//$sub/$VERSION}" ARCH_TYPE=$1 # chip architecture - arm or intel -DIR_TO_FASTSURFER=$2 # directory to fastsurfer ARCH_TYPE_NAME="arm64" -if [ "$ARCH_TYPE" = "intel" ]; then - ARCH_TYPE_NAME="x86_64" -fi - -PACKAGE_NAME=FastSurfer$VERSION-macos-darwin_${ARCH_TYPE_NAME} # name of the package displayed in the installer -ID="ord.deep-mi.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) -INSTALLATION_DIR="/Applications" # install location for the content of the package -OUTPUT_PKG="raw_package/$PACKAGE_NAME.pkg" # raw package file to be created -INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installer to be created +if [[ "$ARCH_TYPE" = "intel" ]] ; then ARCH_TYPE_NAME="x86_64" ; fi + +RESOURCES_DIR="$build_dir/resources" +# name of the package displayed in the installer +PACKAGE_NAME=FastSurfer$VERSION_NO_DOTS-macos-darwin_${ARCH_TYPE_NAME} +# package identifier (f.e. com.mycompany.productid) +ID="org.deep-mi.FastSurfer.${VERSION_NO_DOTS}_${ARCH_TYPE_NAME}" +# FIXME: Must this be fixed or can this also be different? +# install location for the content of the package +INSTALLATION_DIR="/Applications" +# raw package file to be created +OUTPUT_PKG="$build_dir/raw_package/$PACKAGE_NAME.pkg" +# installer to be created +INSTALLER_PKG="$build_dir/installer/$PACKAGE_NAME.pkg" # create temporary folder to package and copy FastSurfer over STAGED_DIR="FastSurferPackageContent" FASTSURFER_TO_PACKAGE="$STAGED_DIR/FastSurfer$VERSION" mkdir $STAGED_DIR -rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ +rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ --exclude requirements.txt \ --exclude requirements.cpu.txt \ --exclude tools # install freesurfer into temp folder -../install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER +"$tools_dir/build/install_fs_pruned.sh" "$STAGED_DIR" --upx --url "$URL_TO_FREESURFER" -SCRIPTS_DIR="./scripts" # directory with scripts executed during installation process (f.e. preinsatll postinstall) -PYTHON_VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.PYTHON_VERSION) +SCRIPTS_DIR="$tools_dir/scripts" # directory with scripts executed during installation process (f.e. preinstall postinstall) +PYTHON_VERSION_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key project.requires-python) +PYTHON_VERSION="${PYTHON_VERSION_TEMP#python>=}" # substitute values in postinstall script PATH_TO_FASTSURFER="$INSTALLATION_DIR/FastSurfer$VERSION" -cp $SCRIPTS_DIR/postinstall.template $SCRIPTS_DIR/postinstall - -sed -i '' -e "s||${PATH_TO_FASTSURFER}|g" $SCRIPTS_DIR/postinstall -sed -i '' -e "s||${PYTHON_VERSION}|g" $SCRIPTS_DIR/postinstall -if [ "$ARCH_TYPE" = "arm" ]; then - sed -i '' -e "s||/opt/homebrew|g" $SCRIPTS_DIR/postinstall -else - sed -i '' -e "s||/usr/local|g" $SCRIPTS_DIR/postinstall -fi +HOMEBREW_DIR=$([[ "$ARCH_TYPE" = "arm" ]] && echo "/opt/homebrew" || echo "/usr/local") + +sed -e "s||${PATH_TO_FASTSURFER}|g" \ + -e "s||${PYTHON_VERSION}|g" \ + -e "s||$HOMEBREW_DIR|g" \ + < "$SCRIPTS_DIR/postinstall.template" \ + > "$SCRIPTS_DIR/postinstall" # assemble resources -mkdir resources -cp $DIR_TO_FASTSURFER/doc/images/fastsurfer.png resources/ -cp $DIR_TO_FASTSURFER/doc/overview/MACOS.md resources/ -cp $DIR_TO_FASTSURFER/LICENSE resources/LICENSE.txt +mkdir "$RESOURCES_DIR" +cp "$FASTSURFER_HOME/doc/images/fastsurfer.png" "$RESOURCES_DIR" +cp "$FASTSURFER_HOME/doc/overview/MACOS.md" "$RESOURCES_DIR" +cp "$FASTSURFER_HOME/LICENSE" "$RESOURCES_DIR/LICENSE.txt" # create fastsurfer applet -cp FastSurfer.py.template FastSurfer.py -sed -i '' -e "s||FastSurfer${VERSION}|g" FastSurfer.py - -cp macos_setup_fastsurfer.sh.template macos_setup_fastsurfer.sh -sed -i '' -e "s||FastSurfer${VERSION}|g" macos_setup_fastsurfer.sh -sed -i '' -e "s||${PYTHON_VERSION}|g" macos_setup_fastsurfer.sh -if [ "$ARCH_TYPE" = "arm" ]; then - sed -i '' -e "s||1|g" macos_setup_fastsurfer.sh -else - sed -i '' -e "s||0|g" macos_setup_fastsurfer.sh -fi -mv macos_setup_fastsurfer.sh $FASTSURFER_TO_PACKAGE/ +sed -e "s||FastSurfer${VERSION}|g" \ + < "$build_dir/FastSurfer.py.template" \ + > "$build_dir/FastSurfer.py" + +MPS_FALLBACK_VALUE=$([[ "$ARCH_TYPE" = "arm" ]] && echo 1 || echo 0) +sed -e "s||FastSurfer${VERSION}|g" \ + -e "s||${PYTHON_VERSION}|g" \ + -e "s||${MPS_FALLBACK_VALUE}|g" \ + < "$build_dir/macos_setup_fastsurfer.sh.template" \ + > "$build_dir/macos_setup_fastsurfer.sh" + +mv "$build_dir/macos_setup_fastsurfer.sh" "$FASTSURFER_TO_PACKAGE/" -python3 setup.py py2app --iconfile resources/fastsurfer.png -mv dist/FastSurfer.app $STAGED_DIR/FastSurfer$VERSION.app +python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" +mv "$build_dir/dist/FastSurfer.app" "$STAGED_DIR/FastSurfer$VERSION.app" -rm -f FastSurfer.py -chmod -R 755 $STAGED_DIR/* +rm -f "FastSurfer.py" +chmod -R 755 "$STAGED_DIR"/* # create raw package mkdir raw_package pkgbuild \ - --root $STAGED_DIR \ - --version $VERSION \ - --identifier $ID \ - --install-location $INSTALLATION_DIR \ - --scripts $SCRIPTS_DIR \ - $OUTPUT_PKG + --root "$STAGED_DIR" \ + --version "$VERSION" \ + --identifier "$ID" \ + --install-location "$INSTALLATION_DIR" \ + --scripts "$SCRIPTS_DIR" \ + "$OUTPUT_PKG" -rm -f $SCRIPTS_DIR/postinstall +rm -f "$SCRIPTS_DIR/postinstall" # create distribution file template based on provided package -RESOURCES="./resources" -DISTRIBUTION_FILE="resources/distribution.xml" -productbuild --synthesize --package $OUTPUT_PKG $DISTRIBUTION_FILE +DISTRIBUTION_FILE="$RESOURCES_DIR/distribution.xml" +productbuild --synthesize --package "$OUTPUT_PKG" "$DISTRIBUTION_FILE" # edit the distribution file # set title to package name (f.e. package_name.pkg -> package_name) -python3 edit_distribution.py --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" +python3 "$build_dir/edit_distribution.py" --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" # create installer package mkdir installer productbuild \ - --distribution $DISTRIBUTION_FILE \ - --resources $RESOURCES \ + --distribution "$DISTRIBUTION_FILE" \ + --resources "$RESOURCES_DIR" \ --package-path raw_package \ - $INSTALLER_PKG + "$INSTALLER_PKG" # get rid of temporary folder -rm -rf $STAGED_DIR -rm -rf resources +rm -rf "$STAGED_DIR" +rm -rf "$RESOURCES_DIR" diff --git a/tools/macos_build/scripts/postinstall.sh.template b/tools/macos_build/scripts/postinstall.sh.template new file mode 100644 index 000000000..b3f7ad602 --- /dev/null +++ b/tools/macos_build/scripts/postinstall.sh.template @@ -0,0 +1,22 @@ +#!/bin/bash + +FASTSURFER_HOME="" +PYTHON="python" + +if [[ ! -f "$FASTSURFER_HOME/venv" ]] +then + echo "Creating FastSurfer run environment" + "/bin/$PYTHON" -m venv --copies $FASTSURFER_HOME/venv +fi + +source $FASTSURFER_HOME/venv/bin/activate +$PYTHON -m pip install --upgrade pip +$PYTHON -m pip install -r $FASTSURFER_HOME/requirements.mac.txt + +export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} +$PYTHON $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all + +FREESURFER_HOME="/Applications/freesurfer" +link_fs.sh "$FASTSURFER_HOME/venv/bin/$PYTHON" "$FREESURFER_HOME" + +sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate diff --git a/tools/get_config_value.py b/tools/read_toml.py similarity index 77% rename from tools/get_config_value.py rename to tools/read_toml.py index 55786be4e..6219f3196 100644 --- a/tools/get_config_value.py +++ b/tools/read_toml.py @@ -1,3 +1,5 @@ +#!python3 + # Copyright 2024 Image Analysis Lab, German Center for Neurodegenerative Diseases (DZNE), Bonn # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,11 +13,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import argparse import sys from pathlib import Path -import yaml +# python 3.11 supports tomllib, but we have tomli in fastsurfer +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib def make_parser() -> argparse.ArgumentParser: @@ -63,21 +70,23 @@ def extract_value(config_file: Path | str, key: str) -> str: value Value under the given key. """ - with open(config_file) as config: - try: - conf = yaml.safe_load(config) - value = conf[key.split('.')[0]] - for k in key.split('.')[1:]: - value = value[k] - return value - except yaml.YAMLError as e: - print(e) - sys.exit(1) + with open(config_file, "rb") as config: + conf = tomllib.load(config) + try: + section, *keys = key.split('.') + value = conf[section] + for k in keys: + value = value[k] + return value + + except tomllib.TOMLDecodeError as e: + print(e) + sys.exit(1) + - if __name__ == "__main__": parser = make_parser() args = parser.parse_args() - print(extract_value(args.file,args.key)) + print(extract_value(args.file, args.key)) sys.exit(0) From b992ed31e51c57e644b3f80c314cad662cd34873 Mon Sep 17 00:00:00 2001 From: David Kuegler Date: Tue, 4 Nov 2025 11:12:00 +0100 Subject: [PATCH 10/30] Fix MacOS documentation: - Broken links - Formatting of MACOS.md --- doc/overview/INSTALL.md | 2 +- doc/overview/MACOS.md | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index f7a180148..426f2c123 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -141,7 +141,7 @@ docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --device=/dev/ ``` Note, that this docker image is experimental, uses a different Python version and python packages, so results can differ from our validation results. Please do visual QC. -## MacOS +## MacOS Processing on Mac CPUs is possible. On Apple Silicon, you can even use the GPU by passing ```--device mps```. diff --git a/doc/overview/MACOS.md b/doc/overview/MACOS.md index 4899f7c04..76f9ccf57 100644 --- a/doc/overview/MACOS.md +++ b/doc/overview/MACOS.md @@ -1,12 +1,22 @@ Running FastSurfer as MacOS package =================================== -[Installation Instructions](INSTALL.md#macos-) +[Installation Instructions](INSTALL.md#macos) If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): -`run_fastsurfer.sh --seg_only --sd --sid --t1 ` + +```bash +run_fastsurfer.sh --seg_only --sd --sid --t1 +``` To full run fastsurfer: -`run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` + +```bash +run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license +``` + Some files of **FreeSurfer** binaries require bypassing MacOS security, which is significantly easier to do with the following command than manually and one by one. -`

xattr -dr com.apple.quarantine /Applications/freesurfer/*` + +```bash +xattr -dr com.apple.quarantine /Applications/freesurfer/* +``` From 4406f893233f5d4c61ce269ed4e5e31189ede6bd Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Tue, 4 Nov 2025 17:34:18 +0100 Subject: [PATCH 11/30] Fix missing build paths --- .gitignore | 2 ++ tools/build/install_fs_pruned.sh | 8 +++---- tools/macos_build/build_release_package.sh | 26 +++++++++++++--------- tools/macos_build/setup.py | 4 +++- 4 files changed, 24 insertions(+), 16 deletions(-) mode change 100644 => 100755 tools/build/install_fs_pruned.sh diff --git a/.gitignore b/.gitignore index fc75619a7..9ca242572 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ __pycache__/ *.py[cod] *$py.class */stub + +.DS_Store \ No newline at end of file diff --git a/tools/build/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh old mode 100644 new mode 100755 index c5f3ff02d..86f09aa3d --- a/tools/build/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -118,10 +118,10 @@ function run_parallel () # get FreeSurfer and unpack (some of it) echo "Downloading FS and unpacking portions ..." -if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") -elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") -else aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) -fi +# if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") +# elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") +aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) +# fi "${dl[@]}" | tar zxv --no-same-owner -C "$where" \ --exclude='freesurfer/average/*.gca' \ diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index d5c125a5e..2ba854536 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -15,12 +15,14 @@ tools_dir=$(dirname "$build_dir") FASTSURFER_HOME=$(dirname "$tools_dir") # directory to fastsurfer # version of the project -VERSION=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.yaml" --key project.version) +VERSION=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key project.version) VERSION_NO_DOTS=${VERSION//./} +#version of the freesurfer +FREESURFER_VERSION=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key tool.freesurfer.version) # freesurfer install url -URL_TO_FREESURFER_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.yaml" --key freesurfer.DOWNLOAD_MACOS) +URL_TO_FREESURFER_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key tool.freesurfer.urls.macOS) sub="{version}" -URL_TO_FREESURFER="${URL_TO_FREESURFER_TEMP//$sub/$VERSION}" +URL_TO_FREESURFER="${URL_TO_FREESURFER_TEMP//$sub/$FREESURFER_VERSION}" ARCH_TYPE=$1 # chip architecture - arm or intel ARCH_TYPE_NAME="arm64" @@ -40,7 +42,7 @@ OUTPUT_PKG="$build_dir/raw_package/$PACKAGE_NAME.pkg" INSTALLER_PKG="$build_dir/installer/$PACKAGE_NAME.pkg" # create temporary folder to package and copy FastSurfer over -STAGED_DIR="FastSurferPackageContent" +STAGED_DIR="$build_dir/FastSurferPackageContent" FASTSURFER_TO_PACKAGE="$STAGED_DIR/FastSurfer$VERSION" mkdir $STAGED_DIR rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ @@ -51,7 +53,7 @@ rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ # install freesurfer into temp folder "$tools_dir/build/install_fs_pruned.sh" "$STAGED_DIR" --upx --url "$URL_TO_FREESURFER" -SCRIPTS_DIR="$tools_dir/scripts" # directory with scripts executed during installation process (f.e. preinstall postinstall) +SCRIPTS_DIR="$tools_dir/macos_build/scripts" # directory with scripts executed during installation process (f.e. preinstall postinstall) PYTHON_VERSION_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key project.requires-python) PYTHON_VERSION="${PYTHON_VERSION_TEMP#python>=}" @@ -62,7 +64,7 @@ HOMEBREW_DIR=$([[ "$ARCH_TYPE" = "arm" ]] && echo "/opt/homebrew" || echo "/usr/ sed -e "s||${PATH_TO_FASTSURFER}|g" \ -e "s||${PYTHON_VERSION}|g" \ -e "s||$HOMEBREW_DIR|g" \ - < "$SCRIPTS_DIR/postinstall.template" \ + < "$SCRIPTS_DIR/postinstall.sh.template" \ > "$SCRIPTS_DIR/postinstall" # assemble resources @@ -85,14 +87,14 @@ sed -e "s||FastSurfer${VERSION}|g" \ mv "$build_dir/macos_setup_fastsurfer.sh" "$FASTSURFER_TO_PACKAGE/" -python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" +python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" --dist-dir $build_dir/dist mv "$build_dir/dist/FastSurfer.app" "$STAGED_DIR/FastSurfer$VERSION.app" -rm -f "FastSurfer.py" +rm -f "$build_dir/FastSurfer.py" chmod -R 755 "$STAGED_DIR"/* # create raw package -mkdir raw_package +mkdir $build_dir/raw_package pkgbuild \ --root "$STAGED_DIR" \ --version "$VERSION" \ @@ -105,6 +107,8 @@ rm -f "$SCRIPTS_DIR/postinstall" # create distribution file template based on provided package DISTRIBUTION_FILE="$RESOURCES_DIR/distribution.xml" + +echo $OUTPUT_PKG and $DISTRIBUTION_FILE productbuild --synthesize --package "$OUTPUT_PKG" "$DISTRIBUTION_FILE" # edit the distribution file @@ -112,11 +116,11 @@ productbuild --synthesize --package "$OUTPUT_PKG" "$DISTRIBUTION_FILE" python3 "$build_dir/edit_distribution.py" --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" # create installer package -mkdir installer +mkdir $build_dir/installer productbuild \ --distribution "$DISTRIBUTION_FILE" \ --resources "$RESOURCES_DIR" \ - --package-path raw_package \ + --package-path $build_dir/raw_package \ "$INSTALLER_PKG" # get rid of temporary folder diff --git a/tools/macos_build/setup.py b/tools/macos_build/setup.py index dea6d97d3..b0bef70d0 100644 --- a/tools/macos_build/setup.py +++ b/tools/macos_build/setup.py @@ -6,8 +6,10 @@ """ from setuptools import setup +import os -APP = ['FastSurfer.py'] +build_dir = os.path.dirname(os.path.realpath(__file__)) +APP = [build_dir + '/FastSurfer.py'] DATA_FILES = [] OPTIONS = {} From 302092e288ac0d3661cb6c12371e025d717f02d1 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Tue, 4 Nov 2025 17:44:42 +0100 Subject: [PATCH 12/30] Fix problem with float cast --- FastSurferCNN/inference.py | 2 +- tools/macos_build/setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/FastSurferCNN/inference.py b/FastSurferCNN/inference.py index fa0a812b7..ed2a4bb68 100644 --- a/FastSurferCNN/inference.py +++ b/FastSurferCNN/inference.py @@ -382,7 +382,7 @@ def eval( # add prediction logits into the output (same as multiplying probabilities) ii[index_of_current_plane] = slice(start_index, end_index) - out[tuple(ii)].add_(pred, alpha=self.alpha.get(plane, 0.4)) + out[tuple(ii)].add_(pred.half(), alpha=self.alpha.get(plane, 0.4)) start_index = end_index except: diff --git a/tools/macos_build/setup.py b/tools/macos_build/setup.py index b0bef70d0..62fe0a37d 100644 --- a/tools/macos_build/setup.py +++ b/tools/macos_build/setup.py @@ -5,9 +5,10 @@ python setup.py py2app """ -from setuptools import setup import os +from setuptools import setup + build_dir = os.path.dirname(os.path.realpath(__file__)) APP = [build_dir + '/FastSurfer.py'] DATA_FILES = [] From e3364ff37d9c1e4a0603d19a12020c24f072a4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Tue, 4 Nov 2025 19:18:08 +0100 Subject: [PATCH 13/30] Apply suggestions from code review --- .gitignore | 2 +- README.md | 4 ++-- tools/macos_build/.gitignore | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9ca242572..41f7a2b41 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ __pycache__/ *$py.class */stub -.DS_Store \ No newline at end of file +.DS_Store diff --git a/README.md b/README.md index 835df8060..048ffd1c5 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ Notwithstanding module-specific limitations, resolution should be between 1mm an There are two ways to run FastSurfer (links are to installation instructions): 1. In a container ([Singularity](doc/overview/INSTALL.md#singularity) or [Docker](doc/overview/INSTALL.md#docker)) (OS: [Linux](doc/overview/INSTALL.md#linux), [Windows](doc/overview/INSTALL.md#windows), [MacOS on Intel](doc/overview/INSTALL.md#docker-currently-only-supported-for-intel-cpus)), -2. As a [native install](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204) (all OS for segmentation part). +2. As a native install ([Linux, Windows (install WSL)](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204), and [macOS](doc/overview/INSTALL.md#macos)). -We recommend you use Singularity or Docker on a Linux host system with a GPU. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. +We recommend you use Singularity/Apptainer on Linux (and Windows) host systems and the package installer on macOS systems with M-chip. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. ### Usage diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore index 97da6dcc5..fa74f5663 100644 --- a/tools/macos_build/.gitignore +++ b/tools/macos_build/.gitignore @@ -8,4 +8,4 @@ build scripts/postinstall scripts_intell/postinstall dist/ -resources/ \ No newline at end of file +resources/ From aa8f852030a406caccdd0ea9267a75d709199a3b Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 5 Nov 2025 11:14:42 +0100 Subject: [PATCH 14/30] Fix deployment workflow --- .github/workflows/deploy.yml | 2 +- tools/build/install_fs_pruned.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3d4843ce7..382714996 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,7 +28,7 @@ jobs: with: python-version: '3.10' - name: install dependencies - run: python -m pip install py2app pyyaml + run: python -m pip install py2app tomli yacs - name: package app for ${{ matrix.arch }} run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} - name: Move assets. diff --git a/tools/build/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh index 86f09aa3d..c5f3ff02d 100755 --- a/tools/build/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -118,10 +118,10 @@ function run_parallel () # get FreeSurfer and unpack (some of it) echo "Downloading FS and unpacking portions ..." -# if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") -# elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") -aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) -# fi +if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") +elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") +else aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) +fi "${dl[@]}" | tar zxv --no-same-owner -C "$where" \ --exclude='freesurfer/average/*.gca' \ From 82a8f2ec3213783218c2c412711304bf38e3ea81 Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Wed, 5 Nov 2025 12:31:36 +0100 Subject: [PATCH 15/30] Apply suggestion from @m-reuter --- tools/build/link_fs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build/link_fs.sh b/tools/build/link_fs.sh index 896060c73..b2fb13b6e 100755 --- a/tools/build/link_fs.sh +++ b/tools/build/link_fs.sh @@ -20,7 +20,7 @@ then fi # FS calls these for version info, but we don't need them -# so we link them to mri_info to save space. +# so we link them to not_here.sh (created below) to save space. link_files=" bin/mri_and bin/mri_aparc2aseg From 0bf71a42d0c620f542335bd5644cbeee8935976a Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Wed, 5 Nov 2025 17:40:31 +0100 Subject: [PATCH 16/30] Update installation instructions for MacOS --- README.md | 2 +- doc/overview/INSTALL.md | 51 +++++++++-------------------------------- 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 048ffd1c5..ab0ec0092 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Notwithstanding module-specific limitations, resolution should be between 1mm an There are two ways to run FastSurfer (links are to installation instructions): 1. In a container ([Singularity](doc/overview/INSTALL.md#singularity) or [Docker](doc/overview/INSTALL.md#docker)) (OS: [Linux](doc/overview/INSTALL.md#linux), [Windows](doc/overview/INSTALL.md#windows), [MacOS on Intel](doc/overview/INSTALL.md#docker-currently-only-supported-for-intel-cpus)), -2. As a native install ([Linux, Windows (install WSL)](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204), and [macOS](doc/overview/INSTALL.md#macos)). +2. As a native install ([Linux, Windows (install WSL)](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204), and [macOS](doc/overview/INSTALL.md#native)). We recommend you use Singularity/Apptainer on Linux (and Windows) host systems and the package installer on macOS systems with M-chip. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 426f2c123..f88f7179e 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -164,59 +164,30 @@ docker pull deepmi/fastsurfer:latest Continue with the example in [Example 1](EXAMPLES.md#example-1-fastsurfer-docker). - ### Native -On modern Macs with the Apple Silicon M1 or M2 ARM-based chips, we recommend a native installation as it runs much faster than Docker in our tests. Access to the built-in AI accelerator (MPS) is also only available on native installations. A native installation also works on older Intel chips. - #### 1. Dependency packages -If you do not have git, python3.10 or bash (at least 3.2) you can install these via the packet manager brew. -This installs brew and then git and python3.10: +If you do not have python3.10 or bash (at least 3.2) you can install these via the packet manager brew. +This installs brew and then python3.10: ```sh /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -brew install git python@3.10 -``` - -#### 2. Python -Create a python environment, activate it, and upgrade pip: - -```sh -python3.10 -m venv $HOME/python-envs/fastsurfer -source $HOME/python-envs/fastsurfer/bin/activate -python3.10 -m pip install --upgrade pip -``` - -#### 3. FastSurfer and Requirements -Clone FastSurfer: -```sh -git clone --branch stable https://github.com/Deep-MI/FastSurfer.git -cd FastSurfer -export PYTHONPATH="${PYTHONPATH}:$PWD" +brew install python@3.10 ``` -Install the FastSurfer requirements -```sh -python3.10 -m pip install -r requirements.mac.txt -``` +### 2. FastSurfer package +Install FastSurfer package according to your type of chip architecture and follow the installer instructions. -If this step fails, you may need to edit ```requirements.mac.txt``` and exclude version numbers that produce conflicts or break our code. -On newer M1 Macs, we also had issues with the h5py package, which could be solved by using brew for help (not sure this is needed any longer): +ARM-based chips: [FastSurfer250-dev-macos-darwin_arm64](https://github.com/OtabekRintaro/FastSurfer/releases/download/2.5.0-dev/FastSurfer250-dev-macos-darwin_arm64.pkg) +Intel chips: [FastSurfer250-dev-macos-darwin_x86_64.pkg](https://github.com/OtabekRintaro/FastSurfer/releases/download/2.5.0-dev/FastSurfer250-dev-macos-darwin_x86_64.pkg) -```sh -brew install hdf5 -export HDF5_DIR="$(brew --prefix hdf5)" -pip3 install --no-binary=h5py h5py -``` +After installation, you can find FastSurfer applet, its source code and freesurfer in the `/Applications` folder. -You can also download all network checkpoint files at this point already: -```sh -python3.10 FastSurferCNN/download_checkpoints.py --all -``` +### 3 Launching FastSurfer -Once all dependencies are installed, you can run the FastSurfer segmentation only by calling ```./run_fastsurfer.sh --seg_only ....``` with the appropriate command line flags, see the [commandline documentation](../../README.md#usage). +Launching FastSurfer applet will open terminal and setup environment for FastSurfer. -To run the full pipeline, install and source also the supported FreeSurfer version according to their [Instructions](https://surfer.nmr.mgh.harvard.edu/fswiki/rel7downloads). There is a freesurfer email list, if you run into problems during this step. Note, that currently FreeSurfer for MacOS supports no ARM, but only Intel, so on modern M-chips it will be slow due to the emulation. This is why we recommend using a Linux host system to run FastSurfer on larger datasets. +Once it is set, you can run the full FastSurfer pipeline only by calling ```run_fastsurfer.sh ....``` with the appropriate command line flags, see the [commandline documentation](../../README.md#usage). #### 4. Apple AI Accelerator support On modern M-Chips you can try the Apple Silicon AI Accelerator by setting `PYTORCH_ENABLE_MPS_FALLBACK` and passing `--device mps` for the segmentation module to make use of the fast GPU: From ff24f54fa1bdbda868723e8bc00e2f11b47fc8fd Mon Sep 17 00:00:00 2001 From: David Kuegler Date: Thu, 6 Nov 2025 15:08:38 +0100 Subject: [PATCH 17/30] Update the macOS installation documentation. --- README.md | 9 +++++---- doc/overview/INSTALL.md | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ab0ec0092..eda2acd25 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,13 @@ Notwithstanding module-specific limitations, resolution should be between 1mm an ## Getting started ### Installation -There are two ways to run FastSurfer (links are to installation instructions): +There are three ways to run FastSurfer (links are to installation instructions): -1. In a container ([Singularity](doc/overview/INSTALL.md#singularity) or [Docker](doc/overview/INSTALL.md#docker)) (OS: [Linux](doc/overview/INSTALL.md#linux), [Windows](doc/overview/INSTALL.md#windows), [MacOS on Intel](doc/overview/INSTALL.md#docker-currently-only-supported-for-intel-cpus)), -2. As a native install ([Linux, Windows (install WSL)](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204), and [macOS](doc/overview/INSTALL.md#native)). +1. For Linux and Windows users, we recommend running FastSurfer in a container [Singularity/Apptainer](doc/overview/INSTALL.md#singularity) or [Docker](doc/overview/INSTALL.md#docker): (OS: [Linux](doc/overview/INSTALL.md#linux), [Windows](doc/overview/INSTALL.md#windows), [MacOS on Intel](doc/overview/INSTALL.md#docker-currently-only-supported-for-intel-cpus)), +2. for macOS users, we recommend [installing the FastSurfer package](doc/overview/INSTALL.md#package), and +3. for developers, the native install gives full control (only documented for [Linux](doc/overview/INSTALL.md#native-ubuntu-2004-or-ubuntu-2204)). -We recommend you use Singularity/Apptainer on Linux (and Windows) host systems and the package installer on macOS systems with M-chip. The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. +The images we provide on [DockerHub](https://hub.docker.com/r/deepmi/fastsurfer) conveniently include everything needed for FastSurfer. You will also need a [FreeSurfer license](https://surfer.nmr.mgh.harvard.edu/fswiki/License) file for the [Surface pipeline](#surface-reconstruction). We have detailed per-OS Installation instructions in the [INSTALL.md](doc/overview/INSTALL.md) file. ### Usage diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index f88f7179e..9283cf649 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -164,11 +164,12 @@ docker pull deepmi/fastsurfer:latest Continue with the example in [Example 1](EXAMPLES.md#example-1-fastsurfer-docker). -### Native +### Package -#### 1. Dependency packages -If you do not have python3.10 or bash (at least 3.2) you can install these via the packet manager brew. -This installs brew and then python3.10: +#### 1. Requirements +FastSurfer requires pre-installed python3.10+ and bash (at least 3.2). +You can install these via the packet manager brew. +To install brew and then python3.10, execute the following in a Terminal: ```sh /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" @@ -176,18 +177,18 @@ brew install python@3.10 ``` ### 2. FastSurfer package -Install FastSurfer package according to your type of chip architecture and follow the installer instructions. - -ARM-based chips: [FastSurfer250-dev-macos-darwin_arm64](https://github.com/OtabekRintaro/FastSurfer/releases/download/2.5.0-dev/FastSurfer250-dev-macos-darwin_arm64.pkg) -Intel chips: [FastSurfer250-dev-macos-darwin_x86_64.pkg](https://github.com/OtabekRintaro/FastSurfer/releases/download/2.5.0-dev/FastSurfer250-dev-macos-darwin_x86_64.pkg) +From version 2.5 onward, FastSurfer ships a macOS installer package, which you can download from +[github](https://github.com/Deep-MI/FastSurfer/releases/). +There are package installers for each both the Apply M-chip architecture (`arm64`) and for legacy intel chips (`x86_64`). +To install, drag the installer into Applications and follow the installer instructions. After installation, you can find FastSurfer applet, its source code and freesurfer in the `/Applications` folder. -### 3 Launching FastSurfer +### 3. Launching FastSurfer -Launching FastSurfer applet will open terminal and setup environment for FastSurfer. +To easily get a configured FastSurfer session/terminal, start the FastSurfer applet by launching it from Applications. -Once it is set, you can run the full FastSurfer pipeline only by calling ```run_fastsurfer.sh ....``` with the appropriate command line flags, see the [commandline documentation](../../README.md#usage). +Once the terminal opens, it is already configured and you can easily run the full FastSurfer pipeline by typing and executing `run_fastsurfer.sh `, where you replace `` with the appropriate [commandline flags of FastSurfer](../../README.md#usage). #### 4. Apple AI Accelerator support On modern M-Chips you can try the Apple Silicon AI Accelerator by setting `PYTORCH_ENABLE_MPS_FALLBACK` and passing `--device mps` for the segmentation module to make use of the fast GPU: From 21feb8dd7cc6c092b937a4d94ddb8fb1ff5f3d0c Mon Sep 17 00:00:00 2001 From: David Kuegler Date: Thu, 6 Nov 2025 15:14:42 +0100 Subject: [PATCH 18/30] Fix backslash in docker installation instructions --- doc/overview/INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 9283cf649..4d38a08d2 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -249,7 +249,7 @@ docker pull deepmi/fastsurfer:latest Now you can run Fastsurfer the same way as described in [Example 1](EXAMPLES.md#example-1-fastsurfer-docker), for example: ```bash -docker run --gpus all +docker run --gpus all \ -v C:/Users/user/my_mri_data:/data \ -v C:/Users/user/my_fastsurfer_analysis:/output \ -v C:/Users/user/my_fs_license_dir:/fs_license \ From 2384a17bc4176959d5d5ddbdfce64eaf387c569d Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Thu, 6 Nov 2025 15:54:55 +0100 Subject: [PATCH 19/30] Apply suggestion from @m-reuter --- doc/overview/INSTALL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 4d38a08d2..01cf43f52 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -179,14 +179,14 @@ brew install python@3.10 ### 2. FastSurfer package From version 2.5 onward, FastSurfer ships a macOS installer package, which you can download from [github](https://github.com/Deep-MI/FastSurfer/releases/). -There are package installers for each both the Apply M-chip architecture (`arm64`) and for legacy intel chips (`x86_64`). +There are package installers for both the Apple M-chip architecture (`arm64`) and for legacy Intel chips (`x86_64`). To install, drag the installer into Applications and follow the installer instructions. -After installation, you can find FastSurfer applet, its source code and freesurfer in the `/Applications` folder. +After installation, you can find the FastSurfer applet, its source code, and selected FreeSurfer executables in the `/Applications` folder. ### 3. Launching FastSurfer -To easily get a configured FastSurfer session/terminal, start the FastSurfer applet by launching it from Applications. +To launch a configured FastSurfer terminal session, start the FastSurfer applet from Applications. Once the terminal opens, it is already configured and you can easily run the full FastSurfer pipeline by typing and executing `run_fastsurfer.sh `, where you replace `` with the appropriate [commandline flags of FastSurfer](../../README.md#usage). From ba07f37909a2447e37fcfec93f5c100742aa294c Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Fri, 14 Nov 2025 14:45:42 +0100 Subject: [PATCH 20/30] Apply suggestions from reviews --- .github/workflows/deploy.yml | 7 +- doc/overview/INSTALL.md | 2 +- doc/overview/MACOS.md | 7 -- tools/build/install_fs_pruned.sh | 21 +++--- tools/macos_build/build_release_package.sh | 10 +-- tools/macos_build/scripts/link_fs.sh | 75 +++++++++++++++++++ .../scripts/postinstall.sh.template | 19 +++-- 7 files changed, 110 insertions(+), 31 deletions(-) create mode 100755 tools/macos_build/scripts/link_fs.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 382714996..29984c541 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,7 +28,12 @@ jobs: with: python-version: '3.10' - name: install dependencies - run: python -m pip install py2app tomli yacs + run: | + python -m pip install py2app tomli yacs + + brew update + brew upgrade || true + brew install aria2 - name: package app for ${{ matrix.arch }} run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} - name: Move assets. diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 01cf43f52..255972787 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -180,7 +180,7 @@ brew install python@3.10 From version 2.5 onward, FastSurfer ships a macOS installer package, which you can download from [github](https://github.com/Deep-MI/FastSurfer/releases/). There are package installers for both the Apple M-chip architecture (`arm64`) and for legacy Intel chips (`x86_64`). -To install, drag the installer into Applications and follow the installer instructions. +To install, double-click the installer and follow the installer instructions. After installation, you can find the FastSurfer applet, its source code, and selected FreeSurfer executables in the `/Applications` folder. diff --git a/doc/overview/MACOS.md b/doc/overview/MACOS.md index 76f9ccf57..b577b7907 100644 --- a/doc/overview/MACOS.md +++ b/doc/overview/MACOS.md @@ -13,10 +13,3 @@ To full run fastsurfer: ```bash run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ``` - -Some files of **FreeSurfer** binaries require bypassing MacOS security, which is -significantly easier to do with the following command than manually and one by one. - -```bash -xattr -dr com.apple.quarantine /Applications/freesurfer/* -``` diff --git a/tools/build/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh index c5f3ff02d..75ee41c93 100755 --- a/tools/build/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -118,12 +118,17 @@ function run_parallel () # get FreeSurfer and unpack (some of it) echo "Downloading FS and unpacking portions ..." -if [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink") -elif [[ -n "$(which curl)" ]] ; then dl=(curl -L --insecure "$fslink") -else aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" ; dl=(cat freesurfer.tar.gz) + +# temp freesurfer dl filename (to save the dl) +freesurfer_dl="freesurfer_$(date +%s)" + +# dl aria2c if that exists, else wget or curl +if [[ -n "$(which aria2c)" ]] ; then aria2c -x 16 -s 16 -c --check-certificate=false -o $freesurfer_dl "$fslink" +elif [[ -n "$(which wget)" ]] ; then wget --no-check-certificate -qO- "$fslink" >> $freesurfer_dl +else curl -L --insecure "$fslink" >> $freesurfer_dl fi -"${dl[@]}" | tar zxv --no-same-owner -C "$where" \ +tar zxv --no-same-owner -C "$where" \ --exclude='freesurfer/average/*.gca' \ --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ @@ -172,12 +177,10 @@ fi --exclude='freesurfer/subjects/rh.EC_average' \ --exclude='freesurfer/subjects/V1_average' \ --exclude='freesurfer/tktools' \ - --exclude='freesurfer/trctrain' + --exclude='freesurfer/trctrain' \ + -f $freesurfer_dl -if [[ -z "$(which wget)" ]] && [[ -z "$(which curl)" ]] -then # cleanup the file saved by aria2c - rm freesurfer.tar.gz -fi + rm $freesurfer_dl # rename download to tmp mv $where/freesurfer $fss diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 2ba854536..5bbb1ae21 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -55,7 +55,7 @@ rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ SCRIPTS_DIR="$tools_dir/macos_build/scripts" # directory with scripts executed during installation process (f.e. preinstall postinstall) PYTHON_VERSION_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key project.requires-python) -PYTHON_VERSION="${PYTHON_VERSION_TEMP#python>=}" +PYTHON_VERSION="${PYTHON_VERSION_TEMP#>=}" # substitute values in postinstall script PATH_TO_FASTSURFER="$INSTALLATION_DIR/FastSurfer$VERSION" @@ -67,6 +67,8 @@ sed -e "s||${PATH_TO_FASTSURFER}|g" \ < "$SCRIPTS_DIR/postinstall.sh.template" \ > "$SCRIPTS_DIR/postinstall" +chmod +x "$SCRIPTS_DIR/postinstall" + # assemble resources mkdir "$RESOURCES_DIR" cp "$FASTSURFER_HOME/doc/images/fastsurfer.png" "$RESOURCES_DIR" @@ -87,7 +89,7 @@ sed -e "s||FastSurfer${VERSION}|g" \ mv "$build_dir/macos_setup_fastsurfer.sh" "$FASTSURFER_TO_PACKAGE/" -python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" --dist-dir $build_dir/dist +python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" --dist-dir "$build_dir/dist" mv "$build_dir/dist/FastSurfer.app" "$STAGED_DIR/FastSurfer$VERSION.app" rm -f "$build_dir/FastSurfer.py" @@ -108,7 +110,6 @@ rm -f "$SCRIPTS_DIR/postinstall" # create distribution file template based on provided package DISTRIBUTION_FILE="$RESOURCES_DIR/distribution.xml" -echo $OUTPUT_PKG and $DISTRIBUTION_FILE productbuild --synthesize --package "$OUTPUT_PKG" "$DISTRIBUTION_FILE" # edit the distribution file @@ -124,5 +125,4 @@ productbuild \ "$INSTALLER_PKG" # get rid of temporary folder -rm -rf "$STAGED_DIR" -rm -rf "$RESOURCES_DIR" +rm -rf "$STAGED_DIR" "$RESOURCES_DIR" "$build_dir/dist" "$build_dir/build" diff --git a/tools/macos_build/scripts/link_fs.sh b/tools/macos_build/scripts/link_fs.sh new file mode 100755 index 000000000..b2fb13b6e --- /dev/null +++ b/tools/macos_build/scripts/link_fs.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# usage: link_fs.sh [ []] + +if [[ "$#" -gt 0 ]] && { [[ "${*/-h/}" != "$*" ]] || [[ "${*/--help/}" != "$*" ]] ; } ; then + echo "usage: $0 [ []]" + exit 0 +elif [[ "$#" == 1 ]] || [[ "$#" == 2 ]] +then + if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exit!" ; exit 1 ; fi + PYTHON="$1" + if [[ "$#" == 2 ]] ; then FREESURFER_HOME="$2" ; fi +else + PYTHON=$(which python3) +fi +if [[ -z "$FREESURFER_HOME" ]] || [[ ! -d "$FREESURFER_HOME" ]] +then + echo "ERROR: FREESURFER_HOME not defined correctly!" + exit 1 +fi + +# FS calls these for version info, but we don't need them +# so we link them to not_here.sh (created below) to save space. +link_files=" + bin/mri_and + bin/mri_aparc2aseg + bin/mri_ca_label + bin/mri_ca_normalize + bin/mri_ca_register + bin/mri_compute_overlap + bin/mri_compute_seg_overlap + bin/mri_em_register + bin/mri_fwhm + bin/mri_gcut + bin/mri_log_likelihood + bin/mri_motion_correct.fsl + bin/mri_normalize_tp2 + bin/mri_or + bin/mri_relabel_nonwm_hypos + bin/mri_remove_neck + bin/mri_stats2seg + bin/mri_surf2vol + bin/mri_surfcluster + bin/mri_voldiff + bin/mri_watershed + bin/mris_divide_parcellation + bin/mris_left_right_register + bin/mris_surface_stats + bin/mris_thickness + bin/mris_thickness_diff + bin/nu_correct + bin/tkregister2_cmdl" + +# create target for link with ERROR message if called +ltrg=$FREESURFER_HOME/bin/not-here.sh +echo '#!/bin/bash +if [ "$1" == "-all-info" ]; then + echo "$0 not included ..." + exit 0 +fi +echo +echo "ERROR: The binary $0 is not included, your call is forwarded to not-here.sh" +echo +exit 1 +' > $ltrg +chmod a+x $ltrg +echo +for file in $link_files +do + echo "linking $file" + ln -s "$ltrg" "$FREESURFER_HOME/$file" +done + +# use our python (not really needed in recon-all anyway) +ln -sf "$PYTHON" "$FREESURFER_HOME/bin/fspython" diff --git a/tools/macos_build/scripts/postinstall.sh.template b/tools/macos_build/scripts/postinstall.sh.template index b3f7ad602..625f17966 100644 --- a/tools/macos_build/scripts/postinstall.sh.template +++ b/tools/macos_build/scripts/postinstall.sh.template @@ -2,21 +2,24 @@ FASTSURFER_HOME="" PYTHON="python" +SCRIPT_DIR="$(dirname $0)" if [[ ! -f "$FASTSURFER_HOME/venv" ]] then echo "Creating FastSurfer run environment" - "/bin/$PYTHON" -m venv --copies $FASTSURFER_HOME/venv + "/bin/$PYTHON" -m venv --copies "$FASTSURFER_HOME/venv" fi -source $FASTSURFER_HOME/venv/bin/activate -$PYTHON -m pip install --upgrade pip -$PYTHON -m pip install -r $FASTSURFER_HOME/requirements.mac.txt +source "$FASTSURFER_HOME/venv/bin/activate" +"$PYTHON" -m pip install --upgrade pip +"$PYTHON" -m pip install -r "$FASTSURFER_HOME/requirements.mac.txt" -export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} -$PYTHON $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all +export PYTHONPATH="${FASTSURFER_HOME}:${PYTHONPATH}" +"$PYTHON" "$FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py" --all FREESURFER_HOME="/Applications/freesurfer" -link_fs.sh "$FASTSURFER_HOME/venv/bin/$PYTHON" "$FREESURFER_HOME" +"$SCRIPT_DIR/link_fs.sh" "$FASTSURFER_HOME/venv/bin/$PYTHON" "$FREESURFER_HOME" -sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate +sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" "$FASTSURFER_HOME/venv/bin/activate" + +xattr -dr com.apple.quarantine "$FREESURFER_HOME"/* From f94e498dc7507515c191e9ea697473edaac11135 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Fri, 14 Nov 2025 16:07:22 +0100 Subject: [PATCH 21/30] Fix workflow missing dependency --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 29984c541..1ceebdea1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.10' - name: install dependencies run: | - python -m pip install py2app tomli yacs + python -m pip install py2app tomli yacs tqdm brew update brew upgrade || true From 2bb34162eb29f30f0caece38e44f95d472081699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Wed, 12 Nov 2025 15:00:07 +0100 Subject: [PATCH 22/30] Install aria2 (and aria2c) in the Docker base build image, so the freesurfer pruned image can use it. --- doc/overview/INSTALL.md | 9 +++++---- tools/Docker/Dockerfile | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 255972787..27f42cdcd 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -74,10 +74,11 @@ If you are using pip, make sure pip is updated as older versions will fail. We recommend to install conda as your python environment. If you don't have conda on your system, an admin needs to install it: ```bash -wget --no-check-certificate -qO ~/miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-py38_4.11.0-Linux-x86_64.sh -chmod +x ~/miniconda.sh -sudo ~/miniconda.sh -b -p /opt/conda && \ -rm ~/miniconda.sh +FORGE_VERSION=25.9.1-0 +wget --no-check-certificate -qO ~/miniforge.sh https://github.com/conda-forge/miniforge/releases/download/${FORGE_VERSION}/Miniforge3-${FORGE_VERSION}-Linux-x86_64.sh +chmod +x ~/miniforge.sh +sudo ~/miniforge.sh -b -p /opt/miniforge && \ +rm ~/miniforge.sh ``` #### 3. FastSurfer diff --git a/tools/Docker/Dockerfile b/tools/Docker/Dockerfile index beee87992..999f73ac6 100644 --- a/tools/Docker/Dockerfile +++ b/tools/Docker/Dockerfile @@ -63,19 +63,19 @@ ENV DEBIAN_FRONTEND=noninteractive # Install packages needed for build RUN apt-get update && apt-get install -y --no-install-recommends \ + aria2 \ ca-certificates \ file \ git \ - upx \ - wget && \ + upx && \ apt clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ARG FORGE_VERSION=25.3.1-0 # Install conda -RUN wget --no-check-certificate -qO ~/miniforge.sh \ - https://github.com/conda-forge/miniforge/releases/download/${FORGE_VERSION}/Miniforge3-${FORGE_VERSION}-Linux-x86_64.sh && \ +RUN aria2c -x 8 -s 8 -c --check-certificate=false -o ~/miniforge.sh \ + https://github.com/conda-forge/miniforge/releases/download/${FORGE_VERSION}/Miniforge3-${FORGE_VERSION}-Linux-x86_64.sh && \ chmod +x ~/miniforge.sh && \ ~/miniforge.sh -b -p /opt/miniforge && \ rm ~/miniforge.sh From a8f80846e873e628b38c5a7b0927c82885688a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Fri, 14 Nov 2025 16:32:51 +0100 Subject: [PATCH 23/30] terminate install_fs_pruned if download is not successful! --- tools/build/install_fs_pruned.sh | 43 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/tools/build/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh index 75ee41c93..c298376d1 100755 --- a/tools/build/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -123,9 +123,14 @@ echo "Downloading FS and unpacking portions ..." freesurfer_dl="freesurfer_$(date +%s)" # dl aria2c if that exists, else wget or curl -if [[ -n "$(which aria2c)" ]] ; then aria2c -x 16 -s 16 -c --check-certificate=false -o $freesurfer_dl "$fslink" -elif [[ -n "$(which wget)" ]] ; then wget --no-check-certificate -qO- "$fslink" >> $freesurfer_dl -else curl -L --insecure "$fslink" >> $freesurfer_dl +if [[ -n "$(which aria2c)" ]] ; then dl=(aria2c -x 16 -s 16 -c --check-certificate=false -o "$freesurfer_dl" "$fslink") +elif [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink" -O "$freesurfer_dl") +else dl=(curl -L --insecure "$fslink" -o "$freesurfer_dl") +fi + +if ! "${dl[@]}" ; then + echo "ERROR: Downloading FreeSurfer failed! This is not recoverable, see message above and retry!" + exit 1 fi tar zxv --no-same-owner -C "$where" \ @@ -178,22 +183,22 @@ tar zxv --no-same-owner -C "$where" \ --exclude='freesurfer/subjects/V1_average' \ --exclude='freesurfer/tktools' \ --exclude='freesurfer/trctrain' \ - -f $freesurfer_dl + -f "$freesurfer_dl" - rm $freesurfer_dl +rm "$freesurfer_dl" # rename download to tmp -mv $where/freesurfer $fss +mv "$where/freesurfer" "$fss" # mk directories -mkdir -p $fsd/average -mkdir -p $fsd/bin -mkdir -p $fsd/etc -mkdir -p $fsd/lib/bem -mkdir -p $fsd/python/scripts -mkdir -p $fsd/python/packages/fsbindings -mkdir -p $fsd/subjects/fsaverage/label -mkdir -p $fsd/subjects/fsaverage/surf +mkdir -p "$fsd/average" +mkdir -p "$fsd/bin" +mkdir -p "$fsd/etc" +mkdir -p "$fsd/lib/bem" +mkdir -p "$fsd/python/scripts" +mkdir -p "$fsd/python/packages/fsbindings" +mkdir -p "$fsd/subjects/fsaverage/label" +mkdir -p "$fsd/subjects/fsaverage/surf" # We need these copy_files=" @@ -423,15 +428,15 @@ echo for file in $copy_files do echo "copying $file" - cp -r $fss/$file $fsd/$file + cp -r "$fss/$file" "$fsd/$file" done # pack if desired with upx (do this before adding all the links if [[ "$upx" == "true" ]] ; then echo "finding executables in $fsd/bin/..." - exe=$(find $fsd/bin -exec file {} \; | grep ELF | cut -d: -f1) + exe=$(find "$fsd/bin" -exec file {} \; | grep ELF | cut -d: -f1) echo "packing $fsd/bin/ executables (this can take a while) ..." - run_parallel 8 "upx -9 %s %s %s %s" 4 $exe + run_parallel 8 "upx -9 %s %s %s %s" 4 "$exe" fi # Modify fsbindings Python package to allow calling scripts like asegstats2table directly: @@ -443,8 +448,8 @@ echo for file in $touch_files do echo "touching $file" - touch $fsd/$file + touch "$fsd/$file" done #cleanup -rm -rf $fss +rm -rf "$fss" From ea210639055a6adb97d1463c35ffe09262090095 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Fri, 14 Nov 2025 16:54:33 +0100 Subject: [PATCH 24/30] Run py2app directly in macos_build directory --- .github/workflows/deploy.yml | 2 +- tools/macos_build/build_release_package.sh | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1ceebdea1..0ed65afb2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: python-version: '3.10' - name: install dependencies run: | - python -m pip install py2app tomli yacs tqdm + python -m pip install py2app tomli brew update brew upgrade || true diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 5bbb1ae21..15feb0fa2 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -89,7 +89,9 @@ sed -e "s||FastSurfer${VERSION}|g" \ mv "$build_dir/macos_setup_fastsurfer.sh" "$FASTSURFER_TO_PACKAGE/" -python3 "$build_dir/setup.py" py2app --iconfile "$RESOURCES_DIR/fastsurfer.png" --dist-dir "$build_dir/dist" +pushd "$build_dir" || exit 1 +python3 "setup.py" py2app --iconfile "${RESOURCES_DIR:$((${#build_dir} + 1))}/fastsurfer.png" --dist-dir "$build_dir/dist" +popd || exit 1 mv "$build_dir/dist/FastSurfer.app" "$STAGED_DIR/FastSurfer$VERSION.app" rm -f "$build_dir/FastSurfer.py" From a4867ea69d63d3dea154e54d81006751745670dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Fri, 14 Nov 2025 17:45:11 +0100 Subject: [PATCH 25/30] Formatting changes in install_fs_pruned.sh --- tools/build/install_fs_pruned.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/build/install_fs_pruned.sh b/tools/build/install_fs_pruned.sh index c298376d1..c89617860 100755 --- a/tools/build/install_fs_pruned.sh +++ b/tools/build/install_fs_pruned.sh @@ -98,6 +98,7 @@ function run_parallel () j=0 while [[ "$j" -lt "${#args}" ]] do + # shellcheck disable=SC2059 cmd=$(printf "$command" "${args[@]:$j:$num}") j=$((j + num)) $cmd & @@ -120,10 +121,10 @@ function run_parallel () echo "Downloading FS and unpacking portions ..." # temp freesurfer dl filename (to save the dl) -freesurfer_dl="freesurfer_$(date +%s)" +freesurfer_dl="freesurfer_$(date +%s).tar.gz" # dl aria2c if that exists, else wget or curl -if [[ -n "$(which aria2c)" ]] ; then dl=(aria2c -x 16 -s 16 -c --check-certificate=false -o "$freesurfer_dl" "$fslink") +if [[ -n "$(which aria2c)" ]] ; then dl=(aria2c -x 16 -s 16 -c --check-certificate=false -o "$freesurfer_dl" "$fslink" ) elif [[ -n "$(which wget)" ]] ; then dl=(wget --no-check-certificate -qO- "$fslink" -O "$freesurfer_dl") else dl=(curl -L --insecure "$fslink" -o "$freesurfer_dl") fi From 3771eb4b5a5ad373b8447aa38ab72d551e6e2faf Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Tue, 18 Nov 2025 12:48:40 +0100 Subject: [PATCH 26/30] fix typo in error message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/build/link_fs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build/link_fs.sh b/tools/build/link_fs.sh index b2fb13b6e..1d737ad81 100755 --- a/tools/build/link_fs.sh +++ b/tools/build/link_fs.sh @@ -7,7 +7,7 @@ if [[ "$#" -gt 0 ]] && { [[ "${*/-h/}" != "$*" ]] || [[ "${*/--help/}" != "$*" ] exit 0 elif [[ "$#" == 1 ]] || [[ "$#" == 2 ]] then - if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exit!" ; exit 1 ; fi + if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exist!" ; exit 1 ; fi PYTHON="$1" if [[ "$#" == 2 ]] ; then FREESURFER_HOME="$2" ; fi else From 3a8311c610c0745236d68d7e7c4214f3c0b87be1 Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Tue, 18 Nov 2025 12:49:20 +0100 Subject: [PATCH 27/30] fix English Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- doc/overview/MACOS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/overview/MACOS.md b/doc/overview/MACOS.md index b577b7907..397f41e62 100644 --- a/doc/overview/MACOS.md +++ b/doc/overview/MACOS.md @@ -8,7 +8,7 @@ If you want to run only segmentation (replace placeholders starting with "<" and ```bash run_fastsurfer.sh --seg_only --sd --sid --t1 ``` -To full run fastsurfer: +To run the full FastSurfer: ```bash run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license From 937e0e8407f9ec8b7a697eabd36d998afa9cf7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Tue, 18 Nov 2025 13:52:04 +0100 Subject: [PATCH 28/30] Fix Copilot comments. --- .github/workflows/deploy.yml | 46 +------------------ doc/overview/INSTALL.md | 2 +- pyproject.toml | 2 + tools/macos_build/README.md | 10 ++-- tools/macos_build/build_release_package.sh | 8 ++-- tools/macos_build/scripts/link_fs.sh | 2 +- .../scripts/postinstall.sh.template | 2 +- tools/read_toml.py | 3 +- 8 files changed, 16 insertions(+), 59 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ed65afb2..4b35d10b7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,48 +46,4 @@ jobs: if: env.RELEASE_ASSETS == 'true' with: files: ${{ env.FASTSURFER_DIR }}/assets/* - # deploy-gpu: - # runs-on: ubuntu-latest - # timeout-minutes: 120 - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - # - name: Login to Docker - # uses: docker/login-action@v2 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v2 - # - name: Build Docker image GPU - # run: python tools/Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - # - name: Add additional tags - # run: | - # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest - # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:latest - # - name: Push Docker image GPU - # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - # deploy-cpu: - # runs-on: ubuntu-latest - # timeout-minutes: 120 - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - # - name: Login to Docker - # uses: docker/login-action@v2 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v2 - # - name: Build Docker image CPU - # run: python tools/Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} - # - name: Add additional tags - # run: | - # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest - # - name: Push Docker image CPU - # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} + diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 27f42cdcd..8c671e776 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -74,7 +74,7 @@ If you are using pip, make sure pip is updated as older versions will fail. We recommend to install conda as your python environment. If you don't have conda on your system, an admin needs to install it: ```bash -FORGE_VERSION=25.9.1-0 +FORGE_VERSION=25.9.1-0 # find the recent miniforge version at https://github.com/conda-forge/miniforge/releases wget --no-check-certificate -qO ~/miniforge.sh https://github.com/conda-forge/miniforge/releases/download/${FORGE_VERSION}/Miniforge3-${FORGE_VERSION}-Linux-x86_64.sh chmod +x ~/miniforge.sh sudo ~/miniforge.sh -b -p /opt/miniforge && \ diff --git a/pyproject.toml b/pyproject.toml index 4052b06e1..438bf23c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,8 @@ tracker = 'https://github.com/Deep-MI/FastSurfer/issues' [tool.freesurfer] version = "7.4.1" +# The following URLs use the {version} placeholder, which should be replaced +# with the actual version string using Python's .format(version=...). [tool.freesurfer.urls] linux = "https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/{version}/freesurfer-linux-ubuntu22_amd64-{version}.tar.gz" macOS = "https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/{version}/freesurfer-macOS-darwin_x86_64-{version}.tar.gz" diff --git a/tools/macos_build/README.md b/tools/macos_build/README.md index 1d7a0e839..dffda2000 100644 --- a/tools/macos_build/README.md +++ b/tools/macos_build/README.md @@ -25,14 +25,12 @@ After successful installation, FastSurfer applet and its source code will appear If you would want to run FastSurfer, you could either use terminal or FastSurfer applet. Though, running applet is recommended as it opens shell terminal and sets up environment for FastSurfer. #### FastSurfer Flags -* The `--fs_license` points to your FreeSurfer license which needs to be available on your computer in the `my_fs_license_dir` that was mapped above. -* The `--t1` points to the t1-weighted MRI image to analyse (full path, with mounted name inside docker: /home/user/my_mri_data => /data) -* The `--sid` is the subject ID name (output folder name) -* The `--sd` points to the output directory (its mounted name inside docker: /home/user/my_fastsurfer_analysis => /output) +* The `--fs_license` points to your FreeSurfer license. +* The `--t1` points to the t1-weighted MRI image to analyse (full, absolute path). +* The `--sid` is the subject ID name (folder name in output directory). +* The `--sd` points to the output directory. * [more flags](../../doc/overview/FLAGS.md#fastsurfer-flags) -Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ the container, not global paths on your system, so they should point to the places where you mapped these paths above with the `-v` arguments. - A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory (specified via `--sd`). So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. All other available flags are identical to the ones explained on the main page [README](../../README.md). diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 15feb0fa2..fbfcc1753 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -44,7 +44,7 @@ INSTALLER_PKG="$build_dir/installer/$PACKAGE_NAME.pkg" # create temporary folder to package and copy FastSurfer over STAGED_DIR="$build_dir/FastSurferPackageContent" FASTSURFER_TO_PACKAGE="$STAGED_DIR/FastSurfer$VERSION" -mkdir $STAGED_DIR +mkdir -p "$STAGED_DIR" rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ --exclude requirements.txt \ --exclude requirements.cpu.txt \ @@ -70,7 +70,7 @@ sed -e "s||${PATH_TO_FASTSURFER}|g" \ chmod +x "$SCRIPTS_DIR/postinstall" # assemble resources -mkdir "$RESOURCES_DIR" +mkdir -p "$RESOURCES_DIR" cp "$FASTSURFER_HOME/doc/images/fastsurfer.png" "$RESOURCES_DIR" cp "$FASTSURFER_HOME/doc/overview/MACOS.md" "$RESOURCES_DIR" cp "$FASTSURFER_HOME/LICENSE" "$RESOURCES_DIR/LICENSE.txt" @@ -98,7 +98,7 @@ rm -f "$build_dir/FastSurfer.py" chmod -R 755 "$STAGED_DIR"/* # create raw package -mkdir $build_dir/raw_package +mkdir -p "$build_dir/raw_package" pkgbuild \ --root "$STAGED_DIR" \ --version "$VERSION" \ @@ -119,7 +119,7 @@ productbuild --synthesize --package "$OUTPUT_PKG" "$DISTRIBUTION_FILE" python3 "$build_dir/edit_distribution.py" --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" # create installer package -mkdir $build_dir/installer +mkdir -p "$build_dir/installer" productbuild \ --distribution "$DISTRIBUTION_FILE" \ --resources "$RESOURCES_DIR" \ diff --git a/tools/macos_build/scripts/link_fs.sh b/tools/macos_build/scripts/link_fs.sh index b2fb13b6e..1d737ad81 100755 --- a/tools/macos_build/scripts/link_fs.sh +++ b/tools/macos_build/scripts/link_fs.sh @@ -7,7 +7,7 @@ if [[ "$#" -gt 0 ]] && { [[ "${*/-h/}" != "$*" ]] || [[ "${*/--help/}" != "$*" ] exit 0 elif [[ "$#" == 1 ]] || [[ "$#" == 2 ]] then - if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exit!" ; exit 1 ; fi + if [[ ! -e "$1" ]] ; then echo "ERROR: $1 does not exist!" ; exit 1 ; fi PYTHON="$1" if [[ "$#" == 2 ]] ; then FREESURFER_HOME="$2" ; fi else diff --git a/tools/macos_build/scripts/postinstall.sh.template b/tools/macos_build/scripts/postinstall.sh.template index 625f17966..4cdbc9897 100644 --- a/tools/macos_build/scripts/postinstall.sh.template +++ b/tools/macos_build/scripts/postinstall.sh.template @@ -4,7 +4,7 @@ FASTSURFER_HOME="" PYTHON="python" SCRIPT_DIR="$(dirname $0)" -if [[ ! -f "$FASTSURFER_HOME/venv" ]] +if [[ ! -d "$FASTSURFER_HOME/venv" ]] then echo "Creating FastSurfer run environment" "/bin/$PYTHON" -m venv --copies "$FASTSURFER_HOME/venv" diff --git a/tools/read_toml.py b/tools/read_toml.py index 6219f3196..3c6f9a83d 100644 --- a/tools/read_toml.py +++ b/tools/read_toml.py @@ -78,7 +78,8 @@ def extract_value(config_file: Path | str, key: str) -> str: for k in keys: value = value[k] return value - + except KeyError as e: + raise ValueError(f"Key {key} not found in configuration file") from e except tomllib.TOMLDecodeError as e: print(e) sys.exit(1) From b8328ed58b92848ed213dd48f850a77c44634d32 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Tue, 18 Nov 2025 14:24:03 +0100 Subject: [PATCH 29/30] Fix applet generation --- tools/macos_build/build_release_package.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index fbfcc1753..53866c652 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -48,7 +48,8 @@ mkdir -p "$STAGED_DIR" rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ --exclude requirements.txt \ --exclude requirements.cpu.txt \ - --exclude tools + --exclude tools \ + --exclude .git # install freesurfer into temp folder "$tools_dir/build/install_fs_pruned.sh" "$STAGED_DIR" --upx --url "$URL_TO_FREESURFER" @@ -90,7 +91,7 @@ sed -e "s||FastSurfer${VERSION}|g" \ mv "$build_dir/macos_setup_fastsurfer.sh" "$FASTSURFER_TO_PACKAGE/" pushd "$build_dir" || exit 1 -python3 "setup.py" py2app --iconfile "${RESOURCES_DIR:$((${#build_dir} + 1))}/fastsurfer.png" --dist-dir "$build_dir/dist" +python3 "setup.py" py2app --iconfile "${RESOURCES_DIR:$((${#build_dir} + 1))}/fastsurfer.png" popd || exit 1 mv "$build_dir/dist/FastSurfer.app" "$STAGED_DIR/FastSurfer$VERSION.app" From 9045517a7c49263752a548db0c32fc29a7106581 Mon Sep 17 00:00:00 2001 From: Atabek Mykyev Date: Tue, 18 Nov 2025 16:08:53 +0100 Subject: [PATCH 30/30] Add claryfing comments to brew update --- .github/workflows/deploy.yml | 1 + tools/macos_build/.gitignore | 1 + tools/macos_build/build_release_package.sh | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4b35d10b7..646027a85 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,6 +31,7 @@ jobs: run: | python -m pip install py2app tomli + # update system to the newest version, not pin pointing package version on purpose brew update brew upgrade || true brew install aria2 diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore index fa74f5663..862dc5753 100644 --- a/tools/macos_build/.gitignore +++ b/tools/macos_build/.gitignore @@ -9,3 +9,4 @@ scripts/postinstall scripts_intell/postinstall dist/ resources/ +FastSurferPackageContent/ diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh index 53866c652..dca74a573 100755 --- a/tools/macos_build/build_release_package.sh +++ b/tools/macos_build/build_release_package.sh @@ -33,7 +33,6 @@ RESOURCES_DIR="$build_dir/resources" PACKAGE_NAME=FastSurfer$VERSION_NO_DOTS-macos-darwin_${ARCH_TYPE_NAME} # package identifier (f.e. com.mycompany.productid) ID="org.deep-mi.FastSurfer.${VERSION_NO_DOTS}_${ARCH_TYPE_NAME}" -# FIXME: Must this be fixed or can this also be different? # install location for the content of the package INSTALLATION_DIR="/Applications" # raw package file to be created @@ -54,6 +53,12 @@ rsync -av --progress "$FASTSURFER_HOME/" "$FASTSURFER_TO_PACKAGE" \ # install freesurfer into temp folder "$tools_dir/build/install_fs_pruned.sh" "$STAGED_DIR" --upx --url "$URL_TO_FREESURFER" +if [[ ! -d "$STAGED_DIR/freesurfer" ]] +then + echo "FreeSurfer install was unsuccessful!" + exit 1 +fi + SCRIPTS_DIR="$tools_dir/macos_build/scripts" # directory with scripts executed during installation process (f.e. preinstall postinstall) PYTHON_VERSION_TEMP=$(python3 "$tools_dir/read_toml.py" --file "$FASTSURFER_HOME/pyproject.toml" --key project.requires-python) PYTHON_VERSION="${PYTHON_VERSION_TEMP#>=}"