1
0

request.rs 174 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271
  1. //! # [`BuildRequest`] - the core of the build process
  2. //!
  3. //! The [`BuildRequest`] object is the core of the build process. It contains all the resolved arguments
  4. //! flowing in from the CLI, dioxus.toml, env vars, and the workspace.
  5. //!
  6. //! Every BuildRequest is tied to a given workspace and BuildArgs. For simplicity's sake, the BuildArgs
  7. //! struct is used to represent the CLI arguments and all other configuration is basically just
  8. //! extra CLI arguments, but in a configuration format.
  9. //!
  10. //! When [`BuildRequest::build`] is called, it will prepare its work directory in the target folder
  11. //! and then start running the build process. A [`BuildContext`] is required to customize this
  12. //! build process, containing a channel for progress updates and the build mode.
  13. //!
  14. //! The [`BuildMode`] is extremely important since it influences how the build is performed. Most
  15. //! "normal" builds just use [`BuildMode::Base`], but we also support [`BuildMode::Fat`] and
  16. //! [`BuildMode::Thin`]. These builds are used together to power the hot-patching and fast-linking
  17. //! engine.
  18. //! - BuildMode::Base: A normal build generated using `cargo rustc`
  19. //! - BuildMode::Fat: A "fat" build where all dependency rlibs are merged into a static library
  20. //! - BuildMode::Thin: A "thin" build that dynamically links against the artifacts produced by the "fat" build
  21. //!
  22. //! The BuildRequest is also responsible for writing the final build artifacts to disk. This includes
  23. //!
  24. //! - Writing the executable
  25. //! - Processing assets from the artifact
  26. //! - Writing any metadata or configuration files (Info.plist, AndroidManifest.xml)
  27. //! - Bundle splitting (for wasm) and wasm-bindgen
  28. //!
  29. //! In some cases, the BuildRequest also handles the linking of the final executable. Specifically,
  30. //! - For Android, we use `dx` as an opaque linker to dynamically find the true android linker
  31. //! - For hotpatching, the CLI manually links the final executable with a stub file
  32. //!
  33. //! ## Build formats:
  34. //!
  35. //! We support building for the most popular platforms:
  36. //! - Web via wasm-bindgen
  37. //! - macOS via app-bundle
  38. //! - iOS via app-bundle
  39. //! - Android via gradle
  40. //! - Linux via app-image
  41. //! - Windows via exe, msi/msix
  42. //!
  43. //! Note that we are missing some setups that we *should* support:
  44. //! - PWAs, WebWorkers, ServiceWorkers
  45. //! - Web Extensions
  46. //! - Linux via flatpak/snap
  47. //!
  48. //! There are some less popular formats that we might want to support eventually:
  49. //! - TVOS, watchOS
  50. //! - OpenHarmony
  51. //!
  52. //! Also, some deploy platforms have their own bespoke formats:
  53. //! - Cloudflare workers
  54. //! - AWS Lambda
  55. //!
  56. //! Currently, we defer most of our deploy-based bundling to Tauri bundle, though we should migrate
  57. //! to just bundling everything ourselves. This would require us to implement code-signing which
  58. //! is a bit of a pain, but fortunately a solved process (<https://github.com/rust-mobile/xbuild>).
  59. //!
  60. //! ## Build Structure
  61. //!
  62. //! Builds generally follow the same structure everywhere:
  63. //! - A main executable
  64. //! - Sidecars (alternate entrypoints, framewrok plugins, etc)
  65. //! - Assets (images, fonts, etc)
  66. //! - Metadata (Info.plist, AndroidManifest.xml)
  67. //! - Glue code (java, kotlin, javascript etc)
  68. //! - Entitlements for code-signing and verification
  69. //!
  70. //! We need to be careful to not try and put a "round peg in a square hole," but most platforms follow
  71. //! the same pattern.
  72. //!
  73. //! As such, we try to assemble a build directory that's somewhat sensible:
  74. //! - A main "staging" dir for a given app
  75. //! - Per-profile dirs (debug/release)
  76. //! - A platform dir (ie web/desktop/android/ios)
  77. //! - The "bundle" dir which is basically the `.app` format or `wwww` dir.
  78. //! - The "executable" dir where the main exe is housed
  79. //! - The "assets" dir where the assets are housed
  80. //! - The "meta" dir where stuff like Info.plist, AndroidManifest.xml, etc are housed
  81. //!
  82. //! There's also some "quirky" folders that need to be stable between builds but don't influence the
  83. //! bundle itself:
  84. //! - session_cache_dir which stores stuff like window position
  85. //!
  86. //! ### Web:
  87. //!
  88. //! Create a folder that is somewhat similar to an app-image (exe + asset)
  89. //! The server is dropped into the `web` folder, even if there's no `public` folder.
  90. //! If there's no server (SPA), we still use the `web` folder, but it only contains the
  91. //! public folder.
  92. //!
  93. //! ```
  94. //! web/
  95. //! server
  96. //! assets/
  97. //! public/
  98. //! index.html
  99. //! wasm/
  100. //! app.wasm
  101. //! glue.js
  102. //! snippets/
  103. //! ...
  104. //! assets/
  105. //! logo.png
  106. //! ```
  107. //!
  108. //! ### Linux:
  109. //!
  110. //! <https://docs.appimage.org/reference/appdir.html#ref-appdir>
  111. //! current_exe.join("Assets")
  112. //! ```
  113. //! app.appimage/
  114. //! AppRun
  115. //! app.desktop
  116. //! package.json
  117. //! assets/
  118. //! logo.png
  119. //! ```
  120. //!
  121. //! ### Macos
  122. //!
  123. //! We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources`
  124. //! We put assets in an assets dir such that it generally matches every other platform and we can
  125. //! output `/assets/blah` from manganis.
  126. //! ```
  127. //! App.app/
  128. //! Contents/
  129. //! Info.plist
  130. //! MacOS/
  131. //! Frameworks/
  132. //! Resources/
  133. //! assets/
  134. //! blah.icns
  135. //! blah.png
  136. //! CodeResources
  137. //! _CodeSignature/
  138. //! ```
  139. //!
  140. //! ### iOS
  141. //!
  142. //! Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably
  143. //! since most ios apps don't ship frameworks/plugins and such.
  144. //!
  145. //! todo(jon): include the signing and entitlements in this format diagram.
  146. //! ```
  147. //! App.app/
  148. //! main
  149. //! assets/
  150. //! ```
  151. //!
  152. //! ### Android:
  153. //!
  154. //! Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since
  155. //! we need to compile kotlin and java. This pushes us into using gradle and following a structure
  156. //! similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and
  157. //! drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires
  158. //! us to manage dependencies (like kotlinc) ourselves (yuck!).
  159. //!
  160. //! <https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh>
  161. //!
  162. //! Unfortunately, it seems that while we can drop the `android` build plugin, we still will need
  163. //! gradle since kotlin is basically gradle-only.
  164. //!
  165. //! Pre-build:
  166. //! ```
  167. //! app.apk/
  168. //! .gradle
  169. //! app/
  170. //! src/
  171. //! main/
  172. //! assets/
  173. //! jniLibs/
  174. //! java/
  175. //! kotlin/
  176. //! res/
  177. //! AndroidManifest.xml
  178. //! build.gradle.kts
  179. //! proguard-rules.pro
  180. //! buildSrc/
  181. //! build.gradle.kts
  182. //! src/
  183. //! main/
  184. //! kotlin/
  185. //! BuildTask.kt
  186. //! build.gradle.kts
  187. //! gradle.properties
  188. //! gradlew
  189. //! gradlew.bat
  190. //! settings.gradle
  191. //! ```
  192. //!
  193. //! Final build:
  194. //! ```
  195. //! app.apk/
  196. //! AndroidManifest.xml
  197. //! classes.dex
  198. //! assets/
  199. //! logo.png
  200. //! lib/
  201. //! armeabi-v7a/
  202. //! libmyapp.so
  203. //! arm64-v8a/
  204. //! libmyapp.so
  205. //! ```
  206. //! Notice that we *could* feasibly build this ourselves :)
  207. //!
  208. //! ### Windows:
  209. //! <https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows>
  210. //! Windows does not provide an AppImage format, so instead we're going build the same folder
  211. //! structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources
  212. //! as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the
  213. //! Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe
  214. //! entrypoint in the associated Program Files folder.
  215. //!
  216. //! This is, in essence, the same as an installer, so we might eventually just support something like msi/msix
  217. //! which functionally do the same thing but with a sleeker UI.
  218. //!
  219. //! This means no installers are required and we can bake an updater into the host exe.
  220. //!
  221. //! ## Handling asset lookups:
  222. //! current_exe.join("assets")
  223. //! ```
  224. //! app.appimage/
  225. //! main.exe
  226. //! main.desktop
  227. //! package.json
  228. //! assets/
  229. //! logo.png
  230. //! ```
  231. //!
  232. //! Since we support just a few locations, we could just search for the first that exists
  233. //! - usr
  234. //! - ../Resources
  235. //! - assets
  236. //! - Assets
  237. //! - $cwd/assets
  238. //!
  239. //! ```
  240. //! assets::root() ->
  241. //! mac -> ../Resources/
  242. //! ios -> ../Resources/
  243. //! android -> assets/
  244. //! server -> assets/
  245. //! liveview -> assets/
  246. //! web -> /assets/
  247. //! root().join(bundled)
  248. //! ```
  249. //!
  250. //! Every dioxus app can have an optional server executable which will influence the final bundle.
  251. //! This is built in parallel with the app executable during the `build` phase and the progres/status
  252. //! of the build is aggregated.
  253. //!
  254. //! The server will *always* be dropped into the `web` folder since it is considered "web" in nature,
  255. //! and will likely need to be combined with the public dir to be useful.
  256. //!
  257. //! We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform
  258. //! can just use the build dir
  259. //!
  260. //! When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name:
  261. //! ```
  262. //! dog-app/
  263. //! build/
  264. //! web/
  265. //! server.exe
  266. //! assets/
  267. //! some-secret-asset.txt (a server-side asset)
  268. //! public/
  269. //! index.html
  270. //! assets/
  271. //! logo.png
  272. //! desktop/
  273. //! App.app
  274. //! App.appimage
  275. //! App.exe
  276. //! server/
  277. //! server
  278. //! assets/
  279. //! some-secret-asset.txt (a server-side asset)
  280. //! ios/
  281. //! App.app
  282. //! App.ipa
  283. //! android/
  284. //! App.apk
  285. //! bundle/
  286. //! build.json
  287. //! Desktop.app
  288. //! Mobile_x64.ipa
  289. //! Mobile_arm64.ipa
  290. //! Mobile_rosetta.ipa
  291. //! web.appimage
  292. //! web/
  293. //! server.exe
  294. //! assets/
  295. //! some-secret-asset.txt
  296. //! public/
  297. //! index.html
  298. //! assets/
  299. //! logo.png
  300. //! style.css
  301. //! ```
  302. //!
  303. //! When deploying, the build.json file will provide all the metadata that dx-deploy will use to
  304. //! push the app to stores, set up infra, manage versions, etc.
  305. //!
  306. //! The format of each build will follow the name plus some metadata such that when distributing you
  307. //! can easily trim off the metadata.
  308. //!
  309. //! The idea here is that we can run any of the programs in the same way that they're deployed.
  310. //!
  311. //! ## Bundle structure links
  312. //! - apple: <https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle>
  313. //! - appimage: <https://docs.appimage.org/packaging-guide/manual.html#ref-manual>
  314. //!
  315. //! ## Extra links
  316. //! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
  317. use crate::{
  318. AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, LinkerFlavor, Platform, Result,
  319. RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace,
  320. DX_RUSTC_WRAPPER_ENV_VAR,
  321. };
  322. use anyhow::Context;
  323. use cargo_metadata::diagnostic::Diagnostic;
  324. use dioxus_cli_config::format_base_path_meta_element;
  325. use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
  326. use dioxus_cli_opt::{process_file_to, AssetManifest};
  327. use itertools::Itertools;
  328. use krates::{cm::TargetKind, NodeId};
  329. use manganis::{AssetOptions, JsAssetOptions};
  330. use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
  331. use serde::{Deserialize, Serialize};
  332. use std::{
  333. collections::{BTreeMap, HashSet},
  334. io::Write,
  335. path::{Path, PathBuf},
  336. process::Stdio,
  337. sync::{
  338. atomic::{AtomicUsize, Ordering},
  339. Arc,
  340. },
  341. time::{SystemTime, UNIX_EPOCH},
  342. };
  343. use target_lexicon::{OperatingSystem, Triple};
  344. use tempfile::{NamedTempFile, TempDir};
  345. use tokio::{io::AsyncBufReadExt, process::Command};
  346. use uuid::Uuid;
  347. use super::HotpatchModuleCache;
  348. /// This struct is used to plan the build process.
  349. ///
  350. /// The point here is to be able to take in the user's config from the CLI without modifying the
  351. /// arguments in place. Creating a buildplan "resolves" their config into a build plan that can be
  352. /// introspected. For example, the users might not specify a "Triple" in the CLI but the triple will
  353. /// be guaranteed to be resolved here.
  354. ///
  355. /// Creating a buildplan also lets us introspect build requests and modularize our build process.
  356. /// This will, however, lead to duplicate fields between the CLI and the build engine. This is fine
  357. /// since we have the freedom to evolve the schema internally without breaking the API.
  358. ///
  359. /// All updates from the build will be sent on a global "BuildProgress" channel.
  360. #[derive(Clone)]
  361. pub(crate) struct BuildRequest {
  362. pub(crate) workspace: Arc<Workspace>,
  363. pub(crate) config: DioxusConfig,
  364. pub(crate) crate_package: NodeId,
  365. pub(crate) crate_target: krates::cm::Target,
  366. pub(crate) profile: String,
  367. pub(crate) release: bool,
  368. pub(crate) platform: Platform,
  369. pub(crate) enabled_platforms: Vec<Platform>,
  370. pub(crate) triple: Triple,
  371. pub(crate) device: bool,
  372. pub(crate) package: String,
  373. pub(crate) main_target: String,
  374. pub(crate) features: Vec<String>,
  375. pub(crate) rustflags: cargo_config2::Flags,
  376. pub(crate) extra_cargo_args: Vec<String>,
  377. pub(crate) extra_rustc_args: Vec<String>,
  378. pub(crate) no_default_features: bool,
  379. pub(crate) target_dir: PathBuf,
  380. pub(crate) skip_assets: bool,
  381. pub(crate) wasm_split: bool,
  382. pub(crate) debug_symbols: bool,
  383. pub(crate) inject_loading_scripts: bool,
  384. pub(crate) custom_linker: Option<PathBuf>,
  385. pub(crate) session_cache_dir: Arc<TempDir>,
  386. pub(crate) link_args_file: Arc<NamedTempFile>,
  387. pub(crate) link_err_file: Arc<NamedTempFile>,
  388. pub(crate) rustc_wrapper_args_file: Arc<NamedTempFile>,
  389. pub(crate) base_path: Option<String>,
  390. }
  391. /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin
  392. /// modes are used together to achieve binary patching and linking.
  393. ///
  394. /// Guide:
  395. /// ----------
  396. /// - Base: A normal build generated using `cargo rustc`, intended for production use cases
  397. ///
  398. /// - Fat: A "fat" build with -Wl,-all_load and no_dead_strip, keeping *every* symbol in the binary.
  399. /// Intended for development for larger up-front builds with faster link times and the ability
  400. /// to binary patch the final binary. On WASM, this also forces wasm-bindgen to generate all
  401. /// JS-WASM bindings, saving us the need to re-wasmbindgen the final binary.
  402. ///
  403. /// - Thin: A "thin" build that dynamically links against the dependencies produced by the "fat" build.
  404. /// This is generated by calling rustc *directly* and might be more fragile to construct, but
  405. /// generates *much* faster than a regular base or fat build.
  406. #[derive(Clone, Debug, PartialEq)]
  407. pub enum BuildMode {
  408. /// A normal build generated using `cargo rustc`
  409. Base,
  410. /// A "Fat" build generated with cargo rustc and dx as a custom linker without -Wl,-dead-strip
  411. Fat,
  412. /// A "thin" build generated with `rustc` directly and dx as a custom linker
  413. Thin {
  414. rustc_args: RustcArgs,
  415. changed_files: Vec<PathBuf>,
  416. aslr_reference: u64,
  417. cache: Arc<HotpatchModuleCache>,
  418. },
  419. }
  420. /// The end result of a build.
  421. ///
  422. /// Contains the final asset manifest, the executable, and metadata about the build.
  423. /// Note that the `exe` might be stale and/or overwritten by the time you read it!
  424. ///
  425. /// The patch cache is only populated on fat builds and then used for thin builds (see `BuildMode::Thin`).
  426. #[derive(Clone, Debug)]
  427. pub struct BuildArtifacts {
  428. pub(crate) platform: Platform,
  429. pub(crate) exe: PathBuf,
  430. pub(crate) direct_rustc: RustcArgs,
  431. pub(crate) time_start: SystemTime,
  432. pub(crate) time_end: SystemTime,
  433. pub(crate) assets: AssetManifest,
  434. pub(crate) mode: BuildMode,
  435. pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
  436. }
  437. impl BuildRequest {
  438. /// Create a new build request.
  439. ///
  440. /// This method consolidates various inputs into a single source of truth. It combines:
  441. /// - Command-line arguments provided by the user.
  442. /// - The crate's `Cargo.toml`.
  443. /// - The `dioxus.toml` configuration file.
  444. /// - User-specific CLI settings.
  445. /// - The workspace metadata.
  446. /// - Host-specific details (e.g., Android tools, installed frameworks).
  447. /// - The intended target platform.
  448. ///
  449. /// Fields may be duplicated from the inputs to allow for autodetection and resolution.
  450. ///
  451. /// Autodetection is performed for unspecified fields where possible.
  452. ///
  453. /// Note: Build requests are typically created only when the CLI is invoked or when significant
  454. /// changes are detected in the `Cargo.toml` (e.g., features added or removed).
  455. pub(crate) async fn new(
  456. args: &TargetArgs,
  457. main_target: Option<String>,
  458. workspace: Arc<Workspace>,
  459. ) -> Result<Self> {
  460. let crate_package = workspace.find_main_package(args.package.clone())?;
  461. let config = workspace
  462. .load_dioxus_config(crate_package)?
  463. .unwrap_or_default();
  464. let target_kind = match args.example.is_some() {
  465. true => TargetKind::Example,
  466. false => TargetKind::Bin,
  467. };
  468. let main_package = &workspace.krates[crate_package];
  469. let target_name = args
  470. .example
  471. .clone()
  472. .or(args.bin.clone())
  473. .or_else(|| {
  474. if let Some(default_run) = &main_package.default_run {
  475. return Some(default_run.to_string());
  476. }
  477. let bin_count = main_package
  478. .targets
  479. .iter()
  480. .filter(|x| x.kind.contains(&target_kind))
  481. .count();
  482. if bin_count != 1 {
  483. return None;
  484. }
  485. main_package.targets.iter().find_map(|x| {
  486. if x.kind.contains(&target_kind) {
  487. Some(x.name.clone())
  488. } else {
  489. None
  490. }
  491. })
  492. })
  493. .unwrap_or(workspace.krates[crate_package].name.clone());
  494. // Use the main_target for the client + server build if it is set, otherwise use the target name for this
  495. // specific build
  496. let main_target = main_target.unwrap_or(target_name.clone());
  497. let crate_target = main_package
  498. .targets
  499. .iter()
  500. .find(|target| {
  501. target_name == target.name.as_str() && target.kind.contains(&target_kind)
  502. })
  503. .with_context(|| {
  504. let target_of_kind = |kind|-> String {
  505. let filtered_packages = main_package
  506. .targets
  507. .iter()
  508. .filter_map(|target| {
  509. target.kind.contains(kind).then_some(target.name.as_str())
  510. }).collect::<Vec<_>>();
  511. filtered_packages.join(", ")};
  512. if let Some(example) = &args.example {
  513. let examples = target_of_kind(&TargetKind::Example);
  514. format!("Failed to find example {example}. \nAvailable examples are:\n{}", examples)
  515. } else if let Some(bin) = &args.bin {
  516. let binaries = target_of_kind(&TargetKind::Bin);
  517. format!("Failed to find binary {bin}. \nAvailable binaries are:\n{}", binaries)
  518. } else {
  519. format!("Failed to find target {target_name}. \nIt looks like you are trying to build dioxus in a library crate. \
  520. You either need to run dx from inside a binary crate or build a specific example with the `--example` flag. \
  521. Available examples are:\n{}", target_of_kind(&TargetKind::Example))
  522. }
  523. })?
  524. .clone();
  525. // The crate might enable multiple platforms or no platforms at
  526. // We collect all the platforms it enables first and then select based on the --platform arg
  527. let enabled_platforms =
  528. Self::enabled_cargo_toml_platforms(main_package, args.no_default_features);
  529. let using_dioxus_explicitly = main_package
  530. .dependencies
  531. .iter()
  532. .any(|dep| dep.name == "dioxus");
  533. let mut features = args.features.clone();
  534. let mut no_default_features = args.no_default_features;
  535. let platform: Platform = match args.platform {
  536. Some(platform) => match enabled_platforms.len() {
  537. 0 => platform,
  538. // The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml or dioxus = { features = ["abc"] }
  539. // We want to strip out the default platform and use the one they passed, setting no-default-features
  540. _ => {
  541. features.extend(Self::platformless_features(main_package));
  542. no_default_features = true;
  543. platform
  544. }
  545. },
  546. None if !using_dioxus_explicitly => Platform::autodetect_from_cargo_feature("desktop").unwrap(),
  547. None => match enabled_platforms.len() {
  548. 0 => return Err(anyhow::anyhow!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`").into()),
  549. 1 => enabled_platforms[0],
  550. _ => {
  551. return Err(anyhow::anyhow!(
  552. "Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml"
  553. )
  554. .into())
  555. }
  556. },
  557. };
  558. // Add any features required to turn on the client
  559. if using_dioxus_explicitly {
  560. features.push(Self::feature_for_platform(main_package, platform));
  561. }
  562. // Set the profile of the build if it's not already set
  563. // This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles
  564. // We might want to move some of these profiles into dioxus.toml and make them "virtual".
  565. let profile = match args.profile.clone() {
  566. Some(profile) => profile,
  567. None => platform.profile_name(args.release),
  568. };
  569. // Determining release mode is based on the profile, actually, so we need to check that
  570. let release = workspace.is_release_profile(&profile);
  571. // Determine the --package we'll pass to cargo.
  572. // todo: I think this might be wrong - we don't want to use main_package necessarily...
  573. let package = args
  574. .package
  575. .clone()
  576. .unwrap_or_else(|| main_package.name.clone());
  577. // We usually use the simulator unless --device is passed *or* a device is detected by probing.
  578. // For now, though, since we don't have probing, it just defaults to false
  579. // Tools like xcrun/adb can detect devices
  580. let device = args.device.unwrap_or(false);
  581. // We want a real triple to build with, so we'll autodetect it if it's not provided
  582. // The triple ends up being a source of truth for us later hence all this work to figure it out
  583. let triple = match args.target.clone() {
  584. Some(target) => target,
  585. None => match platform {
  586. // Generally just use the host's triple for native executables unless specified otherwise
  587. Platform::MacOS
  588. | Platform::Windows
  589. | Platform::Linux
  590. | Platform::Server
  591. | Platform::Liveview => target_lexicon::HOST,
  592. // We currently assume unknown-unknown for web, but we might want to eventually
  593. // support emscripten
  594. Platform::Web => "wasm32-unknown-unknown".parse().unwrap(),
  595. // For iOS we should prefer the actual architecture for the simulator, but in lieu of actually
  596. // figuring that out, we'll assume aarch64 on m-series and x86_64 otherwise
  597. Platform::Ios => {
  598. // use the host's architecture and sim if --device is passed
  599. use target_lexicon::{Architecture, HOST};
  600. match HOST.architecture {
  601. Architecture::Aarch64(_) if device => "aarch64-apple-ios".parse().unwrap(),
  602. Architecture::Aarch64(_) => "aarch64-apple-ios-sim".parse().unwrap(),
  603. _ if device => "x86_64-apple-ios".parse().unwrap(),
  604. _ => "x86_64-apple-ios".parse().unwrap(),
  605. }
  606. }
  607. // Same idea with android but we figure out the connected device using adb
  608. Platform::Android => {
  609. workspace
  610. .android_tools()?
  611. .autodetect_android_device_triple()
  612. .await
  613. }
  614. },
  615. };
  616. // Somethings we override are also present in the user's config.
  617. // If we can't get them by introspecting cargo, then we need to get them from the config
  618. //
  619. // This involves specifically two fields:
  620. // - The linker since we override it for Android and hotpatching
  621. // - RUSTFLAGS since we also override it for Android and hotpatching
  622. let cargo_config = cargo_config2::Config::load().unwrap();
  623. let mut custom_linker = cargo_config.linker(triple.to_string()).ok().flatten();
  624. let mut rustflags = cargo_config2::Flags::default();
  625. if matches!(platform, Platform::Android) {
  626. rustflags.flags.extend([
  627. "-Clink-arg=-landroid".to_string(),
  628. "-Clink-arg=-llog".to_string(),
  629. "-Clink-arg=-lOpenSLES".to_string(),
  630. "-Clink-arg=-Wl,--export-dynamic".to_string(),
  631. ]);
  632. }
  633. // Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
  634. for env in [
  635. "RUSTFLAGS".to_string(),
  636. format!("CARGO_TARGET_{triple}_RUSTFLAGS"),
  637. ] {
  638. if let Ok(flags) = std::env::var(env) {
  639. rustflags
  640. .flags
  641. .extend(cargo_config2::Flags::from_space_separated(&flags).flags);
  642. }
  643. }
  644. // Use the user's linker if the specify it at the target level
  645. if let Ok(target) = cargo_config.target(triple.to_string()) {
  646. if let Some(flags) = target.rustflags {
  647. rustflags.flags.extend(flags.flags);
  648. }
  649. }
  650. // If no custom linker is set, then android falls back to us as the linker
  651. if custom_linker.is_none() && platform == Platform::Android {
  652. custom_linker = Some(workspace.android_tools()?.android_cc(&triple));
  653. }
  654. let target_dir = std::env::var("CARGO_TARGET_DIR")
  655. .ok()
  656. .map(PathBuf::from)
  657. .or_else(|| cargo_config.build.target_dir.clone())
  658. .unwrap_or_else(|| workspace.workspace_root().join("target"));
  659. // Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!)
  660. let link_args_file = Arc::new(
  661. NamedTempFile::with_suffix(".txt")
  662. .context("Failed to create temporary file for linker args")?,
  663. );
  664. let link_err_file = Arc::new(
  665. NamedTempFile::with_suffix(".txt")
  666. .context("Failed to create temporary file for linker args")?,
  667. );
  668. let rustc_wrapper_args_file = Arc::new(
  669. NamedTempFile::with_suffix(".json")
  670. .context("Failed to create temporary file for rustc wrapper args")?,
  671. );
  672. let session_cache_dir = Arc::new(
  673. TempDir::new().context("Failed to create temporary directory for session cache")?,
  674. );
  675. let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default())
  676. .context("Failed to parse rustc args")?;
  677. let extra_cargo_args = shell_words::split(&args.cargo_args.clone().unwrap_or_default())
  678. .context("Failed to parse cargo args")?;
  679. tracing::debug!(
  680. r#"Log Files:
  681. • link_args_file: {},
  682. • link_err_file: {},
  683. • rustc_wrapper_args_file: {},
  684. • session_cache_dir: {}
  685. • linker: {:?}
  686. • target_dir: {:?}
  687. "#,
  688. link_args_file.path().display(),
  689. link_err_file.path().display(),
  690. rustc_wrapper_args_file.path().display(),
  691. session_cache_dir.path().display(),
  692. custom_linker,
  693. target_dir,
  694. );
  695. Ok(Self {
  696. platform,
  697. features,
  698. no_default_features,
  699. crate_package,
  700. crate_target,
  701. profile,
  702. triple,
  703. device,
  704. workspace,
  705. config,
  706. enabled_platforms,
  707. target_dir,
  708. custom_linker,
  709. link_args_file,
  710. link_err_file,
  711. session_cache_dir,
  712. rustc_wrapper_args_file,
  713. extra_rustc_args,
  714. extra_cargo_args,
  715. release,
  716. package,
  717. main_target,
  718. rustflags,
  719. skip_assets: args.skip_assets,
  720. base_path: args.base_path.clone(),
  721. wasm_split: args.wasm_split,
  722. debug_symbols: args.debug_symbols,
  723. inject_loading_scripts: args.inject_loading_scripts,
  724. })
  725. }
  726. pub(crate) async fn build(&self, ctx: &BuildContext) -> Result<BuildArtifacts> {
  727. // If we forget to do this, then we won't get the linker args since rust skips the full build
  728. // We need to make sure to not react to this though, so the filemap must cache it
  729. _ = self.bust_fingerprint(ctx);
  730. // Run the cargo build to produce our artifacts
  731. let mut artifacts = self.cargo_build(ctx).await?;
  732. // Write the build artifacts to the bundle on the disk
  733. match &ctx.mode {
  734. BuildMode::Thin {
  735. aslr_reference,
  736. cache,
  737. rustc_args,
  738. ..
  739. } => {
  740. self.write_patch(ctx, *aslr_reference, &mut artifacts, cache, rustc_args)
  741. .await?;
  742. }
  743. BuildMode::Base | BuildMode::Fat => {
  744. ctx.status_start_bundle();
  745. self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
  746. .await
  747. .context("Failed to write main executable")?;
  748. self.write_frameworks(ctx, &artifacts.direct_rustc)
  749. .await
  750. .context("Failed to write frameworks")?;
  751. self.write_assets(ctx, &artifacts.assets)
  752. .await
  753. .context("Failed to write assets")?;
  754. self.write_metadata().await?;
  755. self.optimize(ctx).await?;
  756. self.assemble(ctx)
  757. .await
  758. .context("Failed to assemble app bundle")?;
  759. tracing::debug!("Bundle created at {}", self.root_dir().display());
  760. }
  761. }
  762. // Populate the patch cache if we're in fat mode
  763. if matches!(ctx.mode, BuildMode::Fat) {
  764. artifacts.patch_cache = Some(Arc::new(self.create_patch_cache(&artifacts.exe).await?));
  765. }
  766. Ok(artifacts)
  767. }
  768. /// Run the cargo build by assembling the build command and executing it.
  769. ///
  770. /// This method needs to be very careful with processing output since errors being swallowed will
  771. /// be very confusing to the user.
  772. async fn cargo_build(&self, ctx: &BuildContext) -> Result<BuildArtifacts> {
  773. let time_start = SystemTime::now();
  774. // Extract the unit count of the crate graph so build_cargo has more accurate data
  775. // "Thin" builds only build the final exe, so we only need to build one crate
  776. let crate_count = match ctx.mode {
  777. BuildMode::Thin { .. } => 1,
  778. _ => self.get_unit_count_estimate(ctx).await,
  779. };
  780. // Update the status to show that we're starting the build and how many crates we expect to build
  781. ctx.status_starting_build(crate_count);
  782. let mut cmd = self.build_command(ctx)?;
  783. tracing::debug!(dx_src = ?TraceSrc::Build, "Executing cargo for {} using {}", self.platform, self.triple);
  784. let mut child = cmd
  785. .stdout(Stdio::piped())
  786. .stderr(Stdio::piped())
  787. .spawn()
  788. .context("Failed to spawn cargo build")?;
  789. let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap());
  790. let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap());
  791. let mut output_location: Option<PathBuf> = None;
  792. let mut stdout = stdout.lines();
  793. let mut stderr = stderr.lines();
  794. let mut units_compiled = 0;
  795. let mut emitting_error = false;
  796. loop {
  797. use cargo_metadata::Message;
  798. let line = tokio::select! {
  799. Ok(Some(line)) = stdout.next_line() => line,
  800. Ok(Some(line)) = stderr.next_line() => line,
  801. else => break,
  802. };
  803. let Some(Ok(message)) = Message::parse_stream(std::io::Cursor::new(line)).next() else {
  804. continue;
  805. };
  806. match message {
  807. Message::BuildScriptExecuted(_) => units_compiled += 1,
  808. Message::CompilerMessage(msg) => ctx.status_build_diagnostic(msg.message),
  809. Message::TextLine(line) => {
  810. // Handle the case where we're getting lines directly from rustc.
  811. // These are in a different format than the normal cargo output, though I imagine
  812. // this parsing code is quite fragile/sensitive to changes in cargo, cargo_metadata, rustc, etc.
  813. #[derive(Deserialize)]
  814. struct RustcArtifact {
  815. artifact: PathBuf,
  816. emit: String,
  817. }
  818. // These outputs look something like:
  819. //
  820. // { "artifact":"target/debug/deps/libdioxus_core-4f2a0b3c1e5f8b7c.rlib", "emit":"link" }
  821. //
  822. // There are other outputs like depinfo that we might be interested in in the future.
  823. if let Ok(artifact) = serde_json::from_str::<RustcArtifact>(&line) {
  824. if artifact.emit == "link" {
  825. output_location = Some(artifact.artifact);
  826. }
  827. }
  828. // Handle direct rustc diagnostics
  829. if let Ok(diag) = serde_json::from_str::<Diagnostic>(&line) {
  830. ctx.status_build_diagnostic(diag);
  831. }
  832. // For whatever reason, if there's an error while building, we still receive the TextLine
  833. // instead of an "error" message. However, the following messages *also* tend to
  834. // be the error message, and don't start with "error:". So we'll check if we've already
  835. // emitted an error message and if so, we'll emit all following messages as errors too.
  836. //
  837. // todo: This can lead to some really ugly output though, so we might want to look
  838. // into a more reliable way to detect errors propagating out of the compiler. If
  839. // we always wrapped rustc, then we could store this data somewhere in a much more
  840. // reliable format.
  841. if line.trim_start().starts_with("error:") {
  842. emitting_error = true;
  843. }
  844. // Note that previous text lines might have set emitting_error to true
  845. match emitting_error {
  846. true => ctx.status_build_error(line),
  847. false => ctx.status_build_message(line),
  848. }
  849. }
  850. Message::CompilerArtifact(artifact) => {
  851. units_compiled += 1;
  852. ctx.status_build_progress(units_compiled, crate_count, artifact.target.name);
  853. output_location = artifact.executable.map(Into::into);
  854. }
  855. // todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong
  856. // since that is a really bad user experience.
  857. Message::BuildFinished(finished) => {
  858. if !finished.success {
  859. return Err(anyhow::anyhow!(
  860. "Cargo build failed, signaled by the compiler. Toggle tracing mode (press `t`) for more information."
  861. )
  862. .into());
  863. }
  864. }
  865. _ => {}
  866. }
  867. }
  868. // Accumulate the rustc args from the wrapper, if they exist and can be parsed.
  869. let mut direct_rustc = RustcArgs::default();
  870. if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file.path()) {
  871. if let Ok(res) = serde_json::from_str(&res) {
  872. direct_rustc = res;
  873. }
  874. }
  875. // If there's any warnings from the linker, we should print them out
  876. if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file.path()) {
  877. if !linker_warnings.is_empty() {
  878. if output_location.is_none() {
  879. tracing::error!("Linker warnings: {}", linker_warnings);
  880. } else {
  881. tracing::debug!("Linker warnings: {}", linker_warnings);
  882. }
  883. }
  884. }
  885. // Collect the linker args from the and update the rustc args
  886. direct_rustc.link_args = std::fs::read_to_string(self.link_args_file.path())
  887. .context("Failed to read link args from file")?
  888. .lines()
  889. .map(|s| s.to_string())
  890. .collect::<Vec<_>>();
  891. let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
  892. // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds
  893. if matches!(ctx.mode, BuildMode::Fat) {
  894. let link_start = SystemTime::now();
  895. self.run_fat_link(ctx, &exe, &direct_rustc).await?;
  896. tracing::debug!(
  897. "Fat linking completed in {}us",
  898. SystemTime::now()
  899. .duration_since(link_start)
  900. .unwrap()
  901. .as_micros()
  902. );
  903. }
  904. let assets = self.collect_assets(&exe, ctx)?;
  905. let time_end = SystemTime::now();
  906. let mode = ctx.mode.clone();
  907. let platform = self.platform;
  908. tracing::debug!(
  909. "Build completed successfully in {}us: {:?}",
  910. time_end.duration_since(time_start).unwrap().as_micros(),
  911. exe
  912. );
  913. Ok(BuildArtifacts {
  914. time_end,
  915. platform,
  916. exe,
  917. direct_rustc,
  918. time_start,
  919. assets,
  920. mode,
  921. patch_cache: None,
  922. })
  923. }
  924. /// Collect the assets from the final executable and modify the binary in place to point to the right
  925. /// hashed asset location.
  926. fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
  927. // walk every file in the incremental cache dir, reading and inserting items into the manifest.
  928. let mut manifest = AssetManifest::default();
  929. // And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
  930. if !self.skip_assets {
  931. ctx.status_extracting_assets();
  932. manifest = super::assets::extract_assets_from_file(exe)?;
  933. }
  934. Ok(manifest)
  935. }
  936. /// Take the output of rustc and make it into the main exe of the bundle
  937. ///
  938. /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations
  939. /// Other platforms we might do some stripping or other optimizations
  940. /// Move the executable to the workdir
  941. async fn write_executable(
  942. &self,
  943. ctx: &BuildContext,
  944. exe: &Path,
  945. assets: &mut AssetManifest,
  946. ) -> Result<()> {
  947. match self.platform {
  948. // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder
  949. // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable".
  950. //
  951. // The wasm stuff will be in a folder called "wasm" in the workdir.
  952. //
  953. // Final output format:
  954. // ```
  955. // dx/
  956. // app/
  957. // web/
  958. // bundle/
  959. // build/
  960. // server.exe
  961. // public/
  962. // index.html
  963. // wasm/
  964. // app.wasm
  965. // glue.js
  966. // snippets/
  967. // ...
  968. // assets/
  969. // logo.png
  970. // ```
  971. Platform::Web => {
  972. self.bundle_web(ctx, exe, assets).await?;
  973. }
  974. // this will require some extra oomf to get the multi architecture builds...
  975. // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1)
  976. // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about
  977. // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path
  978. //
  979. // todo(jon): maybe just symlink this rather than copy it?
  980. // we might want to eventually use the objcopy logic to handle this
  981. //
  982. // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs
  983. // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19
  984. //
  985. // These are all super simple, just copy the exe into the folder
  986. // eventually, perhaps, maybe strip + encrypt the exe?
  987. Platform::Android
  988. | Platform::MacOS
  989. | Platform::Windows
  990. | Platform::Linux
  991. | Platform::Ios
  992. | Platform::Liveview
  993. | Platform::Server => {
  994. std::fs::create_dir_all(self.exe_dir())?;
  995. std::fs::copy(exe, self.main_exe())?;
  996. }
  997. }
  998. Ok(())
  999. }
  1000. async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
  1001. let framework_dir = self.frameworks_folder();
  1002. for arg in &direct_rustc.link_args {
  1003. // todo - how do we handle windows dlls? we don't want to bundle the system dlls
  1004. // for now, we don't do anything with dlls, and only use .dylibs and .so files
  1005. if arg.ends_with(".dylib") | arg.ends_with(".so") {
  1006. let from = PathBuf::from(arg);
  1007. let to = framework_dir.join(from.file_name().unwrap());
  1008. _ = std::fs::remove_file(&to);
  1009. tracing::debug!("Copying framework from {from:?} to {to:?}");
  1010. _ = std::fs::create_dir_all(&framework_dir);
  1011. // in dev and on normal oses, we want to symlink the file
  1012. // otherwise, just copy it (since in release you want to distribute the framework)
  1013. if cfg!(any(windows, unix)) && !self.release {
  1014. #[cfg(windows)]
  1015. std::os::windows::fs::symlink_file(from, to).with_context(|| {
  1016. "Failed to symlink framework into bundle: {from:?} -> {to:?}"
  1017. })?;
  1018. #[cfg(unix)]
  1019. std::os::unix::fs::symlink(from, to).with_context(|| {
  1020. "Failed to symlink framework into bundle: {from:?} -> {to:?}"
  1021. })?;
  1022. } else {
  1023. std::fs::copy(from, to)?;
  1024. }
  1025. }
  1026. }
  1027. Ok(())
  1028. }
  1029. fn frameworks_folder(&self) -> PathBuf {
  1030. match self.triple.operating_system {
  1031. OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => {
  1032. self.root_dir().join("Contents").join("Frameworks")
  1033. }
  1034. OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"),
  1035. OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(),
  1036. _ => self.root_dir(),
  1037. }
  1038. }
  1039. /// Copy the assets out of the manifest and into the target location
  1040. ///
  1041. /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
  1042. async fn write_assets(&self, ctx: &BuildContext, assets: &AssetManifest) -> Result<()> {
  1043. // Server doesn't need assets - web will provide them
  1044. if self.platform == Platform::Server {
  1045. return Ok(());
  1046. }
  1047. let asset_dir = self.asset_dir();
  1048. // First, clear the asset dir of any files that don't exist in the new manifest
  1049. _ = std::fs::create_dir_all(&asset_dir);
  1050. // Create a set of all the paths that new files will be bundled to
  1051. let mut keep_bundled_output_paths: HashSet<_> = assets
  1052. .assets()
  1053. .map(|a| asset_dir.join(a.bundled_path()))
  1054. .collect();
  1055. // The CLI creates a .version file in the asset dir to keep track of what version of the optimizer
  1056. // the asset was processed. If that version doesn't match the CLI version, we need to re-optimize
  1057. // all assets.
  1058. let version_file = self.asset_optimizer_version_file();
  1059. let clear_cache = std::fs::read_to_string(&version_file)
  1060. .ok()
  1061. .filter(|s| s == crate::VERSION.as_str())
  1062. .is_none();
  1063. if clear_cache {
  1064. keep_bundled_output_paths.clear();
  1065. }
  1066. tracing::trace!(
  1067. "Keeping bundled output paths: {:#?}",
  1068. keep_bundled_output_paths
  1069. );
  1070. // use walkdir::WalkDir;
  1071. // for item in WalkDir::new(&asset_dir).into_iter().flatten() {
  1072. // // If this asset is in the manifest, we don't need to remove it
  1073. // let canonicalized = dunce::canonicalize(item.path())?;
  1074. // if !keep_bundled_output_paths.contains(canonicalized.as_path()) {
  1075. // // Remove empty dirs, remove files not in the manifest
  1076. // if item.file_type().is_dir() && item.path().read_dir()?.next().is_none() {
  1077. // std::fs::remove_dir(item.path())?;
  1078. // } else {
  1079. // std::fs::remove_file(item.path())?;
  1080. // }
  1081. // }
  1082. // }
  1083. // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
  1084. let mut assets_to_transfer = vec![];
  1085. // Queue the bundled assets
  1086. for bundled in assets.assets() {
  1087. let from = PathBuf::from(bundled.absolute_source_path());
  1088. let to = asset_dir.join(bundled.bundled_path());
  1089. // prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
  1090. let from_ = from
  1091. .strip_prefix(self.workspace_dir())
  1092. .unwrap_or(from.as_path());
  1093. let to_ = from
  1094. .strip_prefix(self.workspace_dir())
  1095. .unwrap_or(to.as_path());
  1096. tracing::debug!("Copying asset {from_:?} to {to_:?}");
  1097. assets_to_transfer.push((from, to, *bundled.options()));
  1098. }
  1099. let asset_count = assets_to_transfer.len();
  1100. let started_processing = AtomicUsize::new(0);
  1101. let copied = AtomicUsize::new(0);
  1102. // Parallel Copy over the assets and keep track of progress with an atomic counter
  1103. let progress = ctx.tx.clone();
  1104. let ws_dir = self.workspace_dir();
  1105. // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
  1106. tokio::task::spawn_blocking(move || {
  1107. assets_to_transfer
  1108. .par_iter()
  1109. .try_for_each(|(from, to, options)| {
  1110. let processing = started_processing.fetch_add(1, Ordering::SeqCst);
  1111. let from_ = from.strip_prefix(&ws_dir).unwrap_or(from);
  1112. tracing::trace!(
  1113. "Starting asset copy {processing}/{asset_count} from {from_:?}"
  1114. );
  1115. let res = process_file_to(options, from, to);
  1116. if let Err(err) = res.as_ref() {
  1117. tracing::error!("Failed to copy asset {from:?}: {err}");
  1118. }
  1119. let finished = copied.fetch_add(1, Ordering::SeqCst);
  1120. BuildContext::status_copied_asset(
  1121. &progress,
  1122. finished,
  1123. asset_count,
  1124. from.to_path_buf(),
  1125. );
  1126. res.map(|_| ())
  1127. })
  1128. })
  1129. .await
  1130. .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
  1131. // Remove the wasm dir if we packaged it to an "asset"-type app
  1132. if self.should_bundle_to_asset() {
  1133. _ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir());
  1134. }
  1135. // Write the version file so we know what version of the optimizer we used
  1136. std::fs::write(self.asset_optimizer_version_file(), crate::VERSION.as_str())?;
  1137. Ok(())
  1138. }
  1139. /// Run our custom linker setup to generate a patch file in the right location
  1140. ///
  1141. /// This should be the only case where the cargo output is a "dummy" file and requires us to
  1142. /// manually do any linking.
  1143. ///
  1144. /// We also run some post processing steps here, like extracting out any new assets.
  1145. async fn write_patch(
  1146. &self,
  1147. ctx: &BuildContext,
  1148. aslr_reference: u64,
  1149. artifacts: &mut BuildArtifacts,
  1150. cache: &Arc<HotpatchModuleCache>,
  1151. rustc_args: &RustcArgs,
  1152. ) -> Result<()> {
  1153. ctx.status_hotpatching();
  1154. tracing::debug!(
  1155. "Original builds for patch: {}",
  1156. self.link_args_file.path().display()
  1157. );
  1158. let raw_args = std::fs::read_to_string(self.link_args_file.path())
  1159. .context("Failed to read link args from file")?;
  1160. let args = raw_args.lines().collect::<Vec<_>>();
  1161. // Extract out the incremental object files.
  1162. //
  1163. // This is sadly somewhat of a hack, but it might be a moderately reliable hack.
  1164. //
  1165. // When rustc links your project, it passes the args as how a linker would expect, but with
  1166. // a somewhat reliable ordering. These are all internal details to cargo/rustc, so we can't
  1167. // rely on them *too* much, but the *are* fundamental to how rust compiles your projects, and
  1168. // linker interfaces probably won't change drastically for another 40 years.
  1169. //
  1170. // We need to tear apart this command and only pass the args that are relevant to our thin link.
  1171. // Mainly, we don't want any rlibs to be linked. Occasionally some libraries like objc_exception
  1172. // export a folder with their artifacts - unsure if we actually need to include them. Generally
  1173. // you can err on the side that most *libraries* don't need to be linked here since dlopen
  1174. // satisfies those symbols anyways when the binary is loaded.
  1175. //
  1176. // Many args are passed twice, too, which can be confusing, but generally don't have any real
  1177. // effect. Note that on macos/ios, there's a special macho header that needs to be set, otherwise
  1178. // dyld will complain.
  1179. //
  1180. // Also, some flags in darwin land might become deprecated, need to be super conservative:
  1181. // - https://developer.apple.com/forums/thread/773907
  1182. //
  1183. // The format of this command roughly follows:
  1184. // ```
  1185. // clang
  1186. // /dioxus/target/debug/subsecond-cli
  1187. // /var/folders/zs/gvrfkj8x33d39cvw2p06yc700000gn/T/rustcAqQ4p2/symbols.o
  1188. // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.05stnb4bovskp7a00wyyf7l9s.rcgu.o
  1189. // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.08rgcutgrtj2mxoogjg3ufs0g.rcgu.o
  1190. // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.0941bd8fa2bydcv9hfmgzzne9.rcgu.o
  1191. // /dioxus/target/subsecond-dev/deps/libbincode-c215feeb7886f81b.rlib
  1192. // /dioxus/target/subsecond-dev/deps/libanyhow-e69ac15c094daba6.rlib
  1193. // /dioxus/target/subsecond-dev/deps/libratatui-c3364579b86a1dfc.rlib
  1194. // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libstd-019f0f6ae6e6562b.rlib
  1195. // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libpanic_unwind-7387d38173a2eb37.rlib
  1196. // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libobject-2b03cf6ece171d21.rlib
  1197. // -framework AppKit
  1198. // -lc
  1199. // -framework Foundation
  1200. // -framework Carbon
  1201. // -lSystem
  1202. // -framework CoreFoundation
  1203. // -lobjc
  1204. // -liconv
  1205. // -lm
  1206. // -arch arm64
  1207. // -mmacosx-version-min=11.0.0
  1208. // -L /dioxus/target/subsecond-dev/build/objc_exception-dc226cad0480ea65/out
  1209. // -o /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa
  1210. // -nodefaultlibs
  1211. // -Wl,-all_load
  1212. // ```
  1213. let mut dylibs = vec![];
  1214. let mut object_files = args
  1215. .iter()
  1216. .filter(|arg| arg.ends_with(".rcgu.o"))
  1217. .sorted()
  1218. .map(PathBuf::from)
  1219. .collect::<Vec<_>>();
  1220. // On non-wasm platforms, we generate a special shim object file which converts symbols from
  1221. // fat binary into direct addresses from the running process.
  1222. //
  1223. // Our wasm approach is quite specific to wasm. We don't need to resolve any missing symbols
  1224. // there since wasm is relocatable, but there is considerable pre and post processing work to
  1225. // satisfy undefined symbols that we do by munging the binary directly.
  1226. //
  1227. // todo: can we adjust our wasm approach to also use a similar system?
  1228. // todo: don't require the aslr reference and just patch the got when loading.
  1229. //
  1230. // Requiring the ASLR offset here is necessary but unfortunately might be flakey in practice.
  1231. // Android apps can take a long time to open, and a hot patch might've been issued in the interim,
  1232. // making this hotpatch a failure.
  1233. if self.platform != Platform::Web {
  1234. let stub_bytes = crate::build::create_undefined_symbol_stub(
  1235. cache,
  1236. &object_files,
  1237. &self.triple,
  1238. aslr_reference,
  1239. )
  1240. .expect("failed to resolve patch symbols");
  1241. // Currently we're dropping stub.o in the exe dir, but should probably just move to a tempfile?
  1242. let patch_file = self.main_exe().with_file_name("stub.o");
  1243. std::fs::write(&patch_file, stub_bytes)?;
  1244. object_files.push(patch_file);
  1245. // Add the dylibs/sos to the linker args
  1246. // Make sure to use the one in the bundle, not the ones in the target dir or system.
  1247. for arg in &rustc_args.link_args {
  1248. if arg.ends_with(".dylib") || arg.ends_with(".so") {
  1249. let path = PathBuf::from(arg);
  1250. dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
  1251. }
  1252. }
  1253. }
  1254. // And now we can run the linker with our new args
  1255. let linker = self.select_linker()?;
  1256. let out_exe = self.patch_exe(artifacts.time_start);
  1257. let out_arg = match self.triple.operating_system {
  1258. OperatingSystem::Windows => vec![format!("/OUT:{}", out_exe.display())],
  1259. _ => vec!["-o".to_string(), out_exe.display().to_string()],
  1260. };
  1261. tracing::trace!("Linking with {:?} using args: {:#?}", linker, object_files);
  1262. // Run the linker directly!
  1263. //
  1264. // We dump its output directly into the patch exe location which is different than how rustc
  1265. // does it since it uses llvm-objcopy into the `target/debug/` folder.
  1266. let res = Command::new(linker)
  1267. .args(object_files.iter())
  1268. .args(dylibs.iter())
  1269. .args(self.thin_link_args(&args)?)
  1270. .args(out_arg)
  1271. .env_clear()
  1272. .envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
  1273. .output()
  1274. .await?;
  1275. if !res.stderr.is_empty() {
  1276. let errs = String::from_utf8_lossy(&res.stderr);
  1277. if !self.patch_exe(artifacts.time_start).exists() || !res.status.success() {
  1278. tracing::error!("Failed to generate patch: {}", errs.trim());
  1279. } else {
  1280. tracing::trace!("Linker output during thin linking: {}", errs.trim());
  1281. }
  1282. }
  1283. // For some really weird reason that I think is because of dlopen caching, future loads of the
  1284. // jump library will fail if we don't remove the original fat file. I think this could be
  1285. // because of library versioning and namespaces, but really unsure.
  1286. //
  1287. // The errors if you forget to do this are *extremely* cryptic - missing symbols that never existed.
  1288. //
  1289. // Fortunately, this binary exists in two places - the deps dir and the target out dir. We
  1290. // can just remove the one in the deps dir and the problem goes away.
  1291. if let Some(idx) = args.iter().position(|arg| *arg == "-o") {
  1292. _ = std::fs::remove_file(PathBuf::from(args[idx + 1]));
  1293. }
  1294. // Now extract the assets from the fat binary
  1295. self.collect_assets(&self.patch_exe(artifacts.time_start), ctx)?;
  1296. // If this is a web build, reset the index.html file in case it was modified by SSG
  1297. self.write_index_html(&artifacts.assets)
  1298. .context("Failed to write index.html")?;
  1299. // Clean up the temps manually
  1300. // todo: we might want to keep them around for debugging purposes
  1301. for file in object_files {
  1302. _ = std::fs::remove_file(file);
  1303. }
  1304. Ok(())
  1305. }
  1306. /// Take the original args passed to the "fat" build and then create the "thin" variant.
  1307. ///
  1308. /// This is basically just stripping away the rlibs and other libraries that will be satisfied
  1309. /// by our stub step.
  1310. fn thin_link_args(&self, original_args: &[&str]) -> Result<Vec<String>> {
  1311. let mut out_args = vec![];
  1312. match self.linker_flavor() {
  1313. // wasm32-unknown-unknown -> use wasm-ld (gnu-lld)
  1314. //
  1315. // We need to import a few things - namely the memory and ifunc table.
  1316. //
  1317. // We can safely export everything, I believe, though that led to issues with the "fat"
  1318. // binaries that also might lead to issues here too. wasm-bindgen chokes on some symbols
  1319. // and the resulting JS has issues.
  1320. //
  1321. // We turn on both --pie and --experimental-pic but I think we only need --pie.
  1322. //
  1323. // We don't use *any* of the original linker args since they do lots of custom exports
  1324. // and other things that we don't need.
  1325. //
  1326. // The trickiest one here is -Crelocation-model=pic, which forces data symbols
  1327. // into a GOT, making it possible to import them from the main module.
  1328. //
  1329. // I think we can make relocation-model=pic work for non-wasm platforms, enabling
  1330. // fully relocatable modules with no host coordination in lieu of sending out
  1331. // the aslr slide at runtime.
  1332. LinkerFlavor::WasmLld => {
  1333. out_args.extend([
  1334. "--fatal-warnings".to_string(),
  1335. "--verbose".to_string(),
  1336. "--import-memory".to_string(),
  1337. "--import-table".to_string(),
  1338. "--growable-table".to_string(),
  1339. "--export".to_string(),
  1340. "main".to_string(),
  1341. "--allow-undefined".to_string(),
  1342. "--no-demangle".to_string(),
  1343. "--no-entry".to_string(),
  1344. "--pie".to_string(),
  1345. "--experimental-pic".to_string(),
  1346. ]);
  1347. // retain exports so post-processing has hooks to work with
  1348. for (idx, arg) in original_args.iter().enumerate() {
  1349. if *arg == "--export" {
  1350. out_args.push(arg.to_string());
  1351. out_args.push(original_args[idx + 1].to_string());
  1352. }
  1353. }
  1354. }
  1355. // This uses "cc" and these args need to be ld compatible
  1356. //
  1357. // Most importantly, we want to pass `-dylib` to both CC and the linker to indicate that
  1358. // we want to generate the shared library instead of an executable.
  1359. LinkerFlavor::Darwin => {
  1360. out_args.extend(["-Wl,-dylib".to_string()]);
  1361. // Preserve the original args. We only preserve:
  1362. // -framework
  1363. // -arch
  1364. // -lxyz
  1365. // There might be more, but some flags might break our setup.
  1366. for (idx, arg) in original_args.iter().enumerate() {
  1367. if *arg == "-framework" || *arg == "-arch" || *arg == "-L" {
  1368. out_args.push(arg.to_string());
  1369. out_args.push(original_args[idx + 1].to_string());
  1370. }
  1371. if arg.starts_with("-l") || arg.starts_with("-m") {
  1372. out_args.push(arg.to_string());
  1373. }
  1374. }
  1375. }
  1376. // android/linux need to be compatible with lld
  1377. //
  1378. // android currently drags along its own libraries and other zany flags
  1379. LinkerFlavor::Gnu => {
  1380. out_args.extend([
  1381. "-shared".to_string(),
  1382. "-Wl,--eh-frame-hdr".to_string(),
  1383. "-Wl,-z,noexecstack".to_string(),
  1384. "-Wl,-z,relro,-z,now".to_string(),
  1385. "-nodefaultlibs".to_string(),
  1386. "-Wl,-Bdynamic".to_string(),
  1387. ]);
  1388. // Preserve the original args. We only preserve:
  1389. // -L <path>
  1390. // -arch
  1391. // -lxyz
  1392. // There might be more, but some flags might break our setup.
  1393. for (idx, arg) in original_args.iter().enumerate() {
  1394. if *arg == "-L" {
  1395. out_args.push(arg.to_string());
  1396. out_args.push(original_args[idx + 1].to_string());
  1397. }
  1398. if arg.starts_with("-l")
  1399. || arg.starts_with("-m")
  1400. || arg.starts_with("-Wl,--target=")
  1401. || arg.starts_with("-Wl,-fuse-ld")
  1402. || arg.starts_with("-fuse-ld")
  1403. || arg.contains("-ld-path")
  1404. {
  1405. out_args.push(arg.to_string());
  1406. }
  1407. }
  1408. }
  1409. LinkerFlavor::Msvc => {
  1410. out_args.extend([
  1411. "shlwapi.lib".to_string(),
  1412. "kernel32.lib".to_string(),
  1413. "advapi32.lib".to_string(),
  1414. "ntdll.lib".to_string(),
  1415. "userenv.lib".to_string(),
  1416. "ws2_32.lib".to_string(),
  1417. "dbghelp.lib".to_string(),
  1418. "/defaultlib:msvcrt".to_string(),
  1419. "/DLL".to_string(),
  1420. "/DEBUG".to_string(),
  1421. "/PDBALTPATH:%_PDB%".to_string(),
  1422. "/EXPORT:main".to_string(),
  1423. "/HIGHENTROPYVA:NO".to_string(),
  1424. ]);
  1425. }
  1426. LinkerFlavor::Unsupported => {
  1427. return Err(anyhow::anyhow!("Unsupported platform for thin linking").into())
  1428. }
  1429. }
  1430. let extract_value = |arg: &str| -> Option<String> {
  1431. original_args
  1432. .iter()
  1433. .position(|a| *a == arg)
  1434. .map(|i| original_args[i + 1].to_string())
  1435. };
  1436. if let Some(vale) = extract_value("-target") {
  1437. out_args.push("-target".to_string());
  1438. out_args.push(vale);
  1439. }
  1440. if let Some(vale) = extract_value("-isysroot") {
  1441. out_args.push("-isysroot".to_string());
  1442. out_args.push(vale);
  1443. }
  1444. Ok(out_args)
  1445. }
  1446. /// Patches are stored in the same directory as the main executable, but with a name based on the
  1447. /// time the patch started compiling.
  1448. ///
  1449. /// - lib{name}-patch-{time}.(so/dll/dylib) (next to the main exe)
  1450. ///
  1451. /// Note that weirdly enough, the name of dylibs can actually matter. In some environments, libs
  1452. /// can override each other with symbol interposition.
  1453. ///
  1454. /// Also, on Android - and some Linux, we *need* to start the lib name with `lib` for the dynamic
  1455. /// loader to consider it a shared library.
  1456. ///
  1457. /// todo: the time format might actually be problematic if two platforms share the same build folder.
  1458. pub(crate) fn patch_exe(&self, time_start: SystemTime) -> PathBuf {
  1459. let path = self.main_exe().with_file_name(format!(
  1460. "lib{}-patch-{}",
  1461. self.executable_name(),
  1462. time_start
  1463. .duration_since(UNIX_EPOCH)
  1464. .map(|f| f.as_millis())
  1465. .unwrap_or(0),
  1466. ));
  1467. let extension = match self.linker_flavor() {
  1468. LinkerFlavor::Darwin => "dylib",
  1469. LinkerFlavor::Gnu => "so",
  1470. LinkerFlavor::WasmLld => "wasm",
  1471. LinkerFlavor::Msvc => "dll",
  1472. LinkerFlavor::Unsupported => "",
  1473. };
  1474. path.with_extension(extension)
  1475. }
  1476. /// When we link together the fat binary, we need to make sure every `.o` file in *every* rlib
  1477. /// is taken into account. This is the same work that the rust compiler does when assembling
  1478. /// staticlibs.
  1479. ///
  1480. /// <https://github.com/rust-lang/rust/blob/191df20fcad9331d3a948aa8e8556775ec3fe69d/compiler/rustc_codegen_ssa/src/back/link.rs#L448>
  1481. ///
  1482. /// Since we're going to be passing these to the linker, we need to make sure and not provide any
  1483. /// weird files (like the rmeta) file that rustc generates.
  1484. ///
  1485. /// We discovered the need for this after running into issues with wasm-ld not being able to
  1486. /// handle the rmeta file.
  1487. ///
  1488. /// <https://github.com/llvm/llvm-project/issues/55786>
  1489. ///
  1490. /// Also, crates might not drag in all their dependent code. The monorphizer won't lift trait-based generics:
  1491. ///
  1492. /// <https://github.com/rust-lang/rust/blob/191df20fcad9331d3a948aa8e8556775ec3fe69d/compiler/rustc_monomorphize/src/collector.rs>
  1493. ///
  1494. /// When Rust normally handles this, it uses the +whole-archive directive which adjusts how the rlib
  1495. /// is written to disk.
  1496. ///
  1497. /// Since creating this object file can be a lot of work, we cache it in the target dir by hashing
  1498. /// the names of the rlibs in the command and storing it in the target dir. That way, when we run
  1499. /// this command again, we can just used the cached object file.
  1500. ///
  1501. /// In theory, we only need to do this for every crate accessible by the current crate, but that's
  1502. /// hard acquire without knowing the exported symbols from each crate.
  1503. ///
  1504. /// todo: I think we can traverse our immediate dependencies and inspect their symbols, unless they `pub use` a crate
  1505. /// todo: we should try and make this faster with memmapping
  1506. pub(crate) async fn run_fat_link(
  1507. &self,
  1508. ctx: &BuildContext,
  1509. exe: &Path,
  1510. rustc_args: &RustcArgs,
  1511. ) -> Result<()> {
  1512. ctx.status_starting_link();
  1513. // Filter out the rlib files from the arguments
  1514. let rlibs = rustc_args
  1515. .link_args
  1516. .iter()
  1517. .filter(|arg| arg.ends_with(".rlib"))
  1518. .map(PathBuf::from)
  1519. .collect::<Vec<_>>();
  1520. // Acquire a hash from the rlib names, sizes, modified times, and dx's git commit hash
  1521. // This ensures that any changes in dx or the rlibs will cause a new hash to be generated
  1522. // The hash relies on both dx and rustc hashes, so it should be thoroughly unique. Keep it
  1523. // short to avoid long file names.
  1524. let hash_id = Uuid::new_v5(
  1525. &Uuid::NAMESPACE_OID,
  1526. rlibs
  1527. .iter()
  1528. .map(|p| {
  1529. format!(
  1530. "{}-{}-{}-{}",
  1531. p.file_name().unwrap().to_string_lossy(),
  1532. p.metadata().map(|m| m.len()).unwrap_or_default(),
  1533. p.metadata()
  1534. .ok()
  1535. .and_then(|m| m.modified().ok())
  1536. .and_then(|f| f.duration_since(UNIX_EPOCH).map(|f| f.as_secs()).ok())
  1537. .unwrap_or_default(),
  1538. crate::dx_build_info::GIT_COMMIT_HASH.unwrap_or_default()
  1539. )
  1540. })
  1541. .collect::<String>()
  1542. .as_bytes(),
  1543. )
  1544. .to_string()
  1545. .chars()
  1546. .take(8)
  1547. .collect::<String>();
  1548. // Check if we already have a cached object file
  1549. let out_ar_path = exe.with_file_name(format!("libdeps-{hash_id}.a",));
  1550. let out_rlibs_list = exe.with_file_name(format!("rlibs-{hash_id}.txt"));
  1551. let mut archive_has_contents = out_ar_path.exists();
  1552. // Use the rlibs list if it exists
  1553. let mut compiler_rlibs = std::fs::read_to_string(&out_rlibs_list)
  1554. .ok()
  1555. .map(|s| s.lines().map(PathBuf::from).collect::<Vec<_>>())
  1556. .unwrap_or_default();
  1557. // Create it by dumping all the rlibs into it
  1558. // This will include the std rlibs too, which can severely bloat the size of the archive
  1559. //
  1560. // The nature of this process involves making extremely fat archives, so we should try and
  1561. // speed up the future linking process by caching the archive.
  1562. //
  1563. // Since we're using the git hash for the CLI entropy, debug builds should always regenerate
  1564. // the archive since their hash might not change, but the logic might.
  1565. if !archive_has_contents || cfg!(debug_assertions) {
  1566. compiler_rlibs.clear();
  1567. let mut bytes = vec![];
  1568. let mut out_ar = ar::Builder::new(&mut bytes);
  1569. for rlib in &rlibs {
  1570. // Skip compiler rlibs since they're missing bitcode
  1571. //
  1572. // https://github.com/rust-lang/rust/issues/94232#issuecomment-1048342201
  1573. //
  1574. // if the rlib is not in the target directory, we skip it.
  1575. if !rlib.starts_with(self.workspace_dir()) {
  1576. compiler_rlibs.push(rlib.clone());
  1577. tracing::trace!("Skipping rlib: {:?}", rlib);
  1578. continue;
  1579. }
  1580. tracing::trace!("Adding rlib to staticlib: {:?}", rlib);
  1581. let rlib_contents = std::fs::read(rlib)?;
  1582. let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
  1583. let mut keep_linker_rlib = false;
  1584. while let Some(Ok(object_file)) = reader.next_entry() {
  1585. let name = std::str::from_utf8(object_file.header().identifier()).unwrap();
  1586. if name.ends_with(".rmeta") {
  1587. continue;
  1588. }
  1589. if object_file.header().size() == 0 {
  1590. continue;
  1591. }
  1592. // rlibs might contain dlls/sos/lib files which we don't want to include
  1593. //
  1594. // This catches .dylib, .so, .dll, .lib, .o, etc files that are not compatible with
  1595. // our "fat archive" linking process.
  1596. //
  1597. // We only trust `.rcgu.o` files to make it into the --all_load archive.
  1598. // This is a temporary stopgap to prevent issues with libraries that generate
  1599. // object files that are not compatible with --all_load.
  1600. // see https://github.com/DioxusLabs/dioxus/issues/4237
  1601. if !(name.ends_with(".rcgu.o") || name.ends_with(".obj")) {
  1602. keep_linker_rlib = true;
  1603. continue;
  1604. }
  1605. archive_has_contents = true;
  1606. out_ar
  1607. .append(&object_file.header().clone(), object_file)
  1608. .context("Failed to add object file to archive")?;
  1609. }
  1610. // Some rlibs contain weird artifacts that we don't want to include in the fat archive.
  1611. // However, we still want them around in the linker in case the regular linker can handle them.
  1612. if keep_linker_rlib {
  1613. compiler_rlibs.push(rlib.clone());
  1614. }
  1615. }
  1616. let bytes = out_ar.into_inner().context("Failed to finalize archive")?;
  1617. std::fs::write(&out_ar_path, bytes).context("Failed to write archive")?;
  1618. tracing::debug!("Wrote fat archive to {:?}", out_ar_path);
  1619. // Run the ranlib command to index the archive. This slows down this process a bit,
  1620. // but is necessary for some linkers to work properly.
  1621. // We ignore its error in case it doesn't recognize the architecture
  1622. if self.linker_flavor() == LinkerFlavor::Darwin {
  1623. if let Some(ranlib) = self.select_ranlib() {
  1624. _ = Command::new(ranlib).arg(&out_ar_path).output().await;
  1625. }
  1626. }
  1627. }
  1628. compiler_rlibs.dedup();
  1629. // We're going to replace the first rlib in the args with our fat archive
  1630. // And then remove the rest of the rlibs
  1631. //
  1632. // We also need to insert the -force_load flag to force the linker to load the archive
  1633. let mut args = rustc_args.link_args.clone();
  1634. if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) {
  1635. if archive_has_contents {
  1636. match self.linker_flavor() {
  1637. LinkerFlavor::WasmLld => {
  1638. args.insert(last_object, "--whole-archive".to_string());
  1639. args.insert(last_object + 1, out_ar_path.display().to_string());
  1640. args.insert(last_object + 2, "--no-whole-archive".to_string());
  1641. args.retain(|arg| !arg.ends_with(".rlib"));
  1642. for rlib in compiler_rlibs.iter().rev() {
  1643. args.insert(last_object + 3, rlib.display().to_string());
  1644. }
  1645. }
  1646. LinkerFlavor::Gnu => {
  1647. args.insert(last_object, "-Wl,--whole-archive".to_string());
  1648. args.insert(last_object + 1, out_ar_path.display().to_string());
  1649. args.insert(last_object + 2, "-Wl,--no-whole-archive".to_string());
  1650. args.retain(|arg| !arg.ends_with(".rlib"));
  1651. for rlib in compiler_rlibs.iter().rev() {
  1652. args.insert(last_object + 3, rlib.display().to_string());
  1653. }
  1654. }
  1655. LinkerFlavor::Darwin => {
  1656. args.insert(last_object, "-Wl,-force_load".to_string());
  1657. args.insert(last_object + 1, out_ar_path.display().to_string());
  1658. args.retain(|arg| !arg.ends_with(".rlib"));
  1659. for rlib in compiler_rlibs.iter().rev() {
  1660. args.insert(last_object + 2, rlib.display().to_string());
  1661. }
  1662. }
  1663. LinkerFlavor::Msvc => {
  1664. args.insert(
  1665. last_object,
  1666. format!("/WHOLEARCHIVE:{}", out_ar_path.display()),
  1667. );
  1668. args.retain(|arg| !arg.ends_with(".rlib"));
  1669. for rlib in compiler_rlibs.iter().rev() {
  1670. args.insert(last_object + 1, rlib.display().to_string());
  1671. }
  1672. }
  1673. LinkerFlavor::Unsupported => {
  1674. tracing::error!("Unsupported platform for fat linking");
  1675. }
  1676. };
  1677. }
  1678. }
  1679. // Add custom args to the linkers
  1680. match self.linker_flavor() {
  1681. LinkerFlavor::Gnu => {
  1682. // Export `main` so subsecond can use it for a reference point
  1683. args.push("-Wl,--export-dynamic-symbol,main".to_string());
  1684. }
  1685. LinkerFlavor::Darwin => {
  1686. args.push("-Wl,-exported_symbol,_main".to_string());
  1687. }
  1688. LinkerFlavor::Msvc => {
  1689. // Prevent alsr from overflowing 32 bits
  1690. args.push("/HIGHENTROPYVA:NO".to_string());
  1691. // Export `main` so subsecond can use it for a reference point
  1692. args.push("/EXPORT:main".to_string());
  1693. }
  1694. LinkerFlavor::WasmLld | LinkerFlavor::Unsupported => {}
  1695. }
  1696. // We also need to remove the `-o` flag since we want the linker output to end up in the
  1697. // rust exe location, not in the deps dir as it normally would.
  1698. if let Some(idx) = args
  1699. .iter()
  1700. .position(|arg| *arg == "-o" || *arg == "--output")
  1701. {
  1702. args.remove(idx + 1);
  1703. args.remove(idx);
  1704. }
  1705. // same but windows support
  1706. if let Some(idx) = args.iter().position(|arg| arg.starts_with("/OUT")) {
  1707. args.remove(idx);
  1708. }
  1709. // We want to go through wasm-ld directly, so we need to remove the -flavor flag
  1710. if self.platform == Platform::Web {
  1711. let flavor_idx = args.iter().position(|arg| *arg == "-flavor").unwrap();
  1712. args.remove(flavor_idx + 1);
  1713. args.remove(flavor_idx);
  1714. }
  1715. // And now we can run the linker with our new args
  1716. let linker = self.select_linker()?;
  1717. tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args);
  1718. tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs);
  1719. // Run the linker directly!
  1720. let out_arg = match self.triple.operating_system {
  1721. OperatingSystem::Windows => vec![format!("/OUT:{}", exe.display())],
  1722. _ => vec!["-o".to_string(), exe.display().to_string()],
  1723. };
  1724. let res = Command::new(linker)
  1725. .args(args.iter().skip(1))
  1726. .args(out_arg)
  1727. .env_clear()
  1728. .envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
  1729. .output()
  1730. .await?;
  1731. if !res.stderr.is_empty() {
  1732. let errs = String::from_utf8_lossy(&res.stderr);
  1733. if !res.status.success() {
  1734. tracing::error!("Failed to generate fat binary: {}", errs.trim());
  1735. } else {
  1736. tracing::trace!("Warnings during fat linking: {}", errs.trim());
  1737. }
  1738. }
  1739. if !res.stdout.is_empty() {
  1740. let out = String::from_utf8_lossy(&res.stdout);
  1741. tracing::trace!("Output from fat linking: {}", out.trim());
  1742. }
  1743. // Clean up the temps manually
  1744. for f in args.iter().filter(|arg| arg.ends_with(".rcgu.o")) {
  1745. _ = std::fs::remove_file(f);
  1746. }
  1747. // Cache the rlibs list
  1748. _ = std::fs::write(
  1749. &out_rlibs_list,
  1750. compiler_rlibs
  1751. .into_iter()
  1752. .map(|s| s.display().to_string())
  1753. .join("\n"),
  1754. );
  1755. Ok(())
  1756. }
  1757. /// Automatically detect the linker flavor based on the target triple and any custom linkers.
  1758. ///
  1759. /// This tries to replicate what rustc does when selecting the linker flavor based on the linker
  1760. /// and triple.
  1761. fn linker_flavor(&self) -> LinkerFlavor {
  1762. if let Some(custom) = self.custom_linker.as_ref() {
  1763. let name = custom.file_name().unwrap().to_ascii_lowercase();
  1764. match name.to_str() {
  1765. Some("lld-link") => return LinkerFlavor::Msvc,
  1766. Some("lld-link.exe") => return LinkerFlavor::Msvc,
  1767. Some("wasm-ld") => return LinkerFlavor::WasmLld,
  1768. Some("ld64.lld") => return LinkerFlavor::Darwin,
  1769. Some("ld.lld") => return LinkerFlavor::Gnu,
  1770. Some("ld.gold") => return LinkerFlavor::Gnu,
  1771. Some("mold") => return LinkerFlavor::Gnu,
  1772. Some("sold") => return LinkerFlavor::Gnu,
  1773. Some("wild") => return LinkerFlavor::Gnu,
  1774. _ => {}
  1775. }
  1776. }
  1777. match self.triple.environment {
  1778. target_lexicon::Environment::Gnu
  1779. | target_lexicon::Environment::Gnuabi64
  1780. | target_lexicon::Environment::Gnueabi
  1781. | target_lexicon::Environment::Gnueabihf
  1782. | target_lexicon::Environment::GnuLlvm => LinkerFlavor::Gnu,
  1783. target_lexicon::Environment::Musl => LinkerFlavor::Gnu,
  1784. target_lexicon::Environment::Android => LinkerFlavor::Gnu,
  1785. target_lexicon::Environment::Msvc => LinkerFlavor::Msvc,
  1786. target_lexicon::Environment::Macabi => LinkerFlavor::Darwin,
  1787. _ => match self.triple.operating_system {
  1788. OperatingSystem::Darwin(_) => LinkerFlavor::Darwin,
  1789. OperatingSystem::IOS(_) => LinkerFlavor::Darwin,
  1790. OperatingSystem::MacOSX(_) => LinkerFlavor::Darwin,
  1791. OperatingSystem::Linux => LinkerFlavor::Gnu,
  1792. OperatingSystem::Windows => LinkerFlavor::Msvc,
  1793. _ => match self.triple.architecture {
  1794. target_lexicon::Architecture::Wasm32 => LinkerFlavor::WasmLld,
  1795. target_lexicon::Architecture::Wasm64 => LinkerFlavor::WasmLld,
  1796. _ => LinkerFlavor::Unsupported,
  1797. },
  1798. },
  1799. }
  1800. }
  1801. /// Select the linker to use for this platform.
  1802. ///
  1803. /// We prefer to use the rust-lld linker when we can since it's usually there.
  1804. /// On macos, we use the system linker since macho files can be a bit finicky.
  1805. ///
  1806. /// This means we basically ignore the linker flavor that the user configured, which could
  1807. /// cause issues with a custom linker setup. In theory, rust translates most flags to the right
  1808. /// linker format.
  1809. fn select_linker(&self) -> Result<PathBuf, Error> {
  1810. if let Some(linker) = self.custom_linker.clone() {
  1811. return Ok(linker);
  1812. }
  1813. let cc = match self.linker_flavor() {
  1814. LinkerFlavor::WasmLld => self.workspace.wasm_ld(),
  1815. // On macOS, we use the system linker since it's usually there.
  1816. // We could also use `lld` here, but it might not be installed by default.
  1817. //
  1818. // Note that this is *clang*, not `lld`.
  1819. LinkerFlavor::Darwin => self.workspace.cc(),
  1820. // On Linux, we use the system linker since it's usually there.
  1821. LinkerFlavor::Gnu => self.workspace.cc(),
  1822. // On windows, instead of trying to find the system linker, we just go with the lld.link
  1823. // that rustup provides. It's faster and more stable then reyling on link.exe in path.
  1824. LinkerFlavor::Msvc => self.workspace.lld_link(),
  1825. // The rest of the platforms use `cc` as the linker which should be available in your path,
  1826. // provided you have build-tools setup. On mac/linux this is the default, but on Windows
  1827. // it requires msvc or gnu downloaded, which is a requirement to use rust anyways.
  1828. //
  1829. // The default linker might actually be slow though, so we could consider using lld or rust-lld
  1830. // since those are shipping by default on linux as of 1.86. Window's linker is the really slow one.
  1831. //
  1832. // https://blog.rust-lang.org/2024/05/17/enabling-rust-lld-on-linux.html
  1833. //
  1834. // Note that "cc" is *not* a linker. It's a compiler! The arguments we pass need to be in
  1835. // the form of `-Wl,<args>` for them to make it to the linker. This matches how rust does it
  1836. // which is confusing.
  1837. LinkerFlavor::Unsupported => self.workspace.cc(),
  1838. };
  1839. Ok(cc)
  1840. }
  1841. /// Assemble the `cargo rustc` / `rustc` command
  1842. ///
  1843. /// When building fat/base binaries, we use `cargo rustc`.
  1844. /// When building thin binaries, we use `rustc` directly.
  1845. ///
  1846. /// When processing the output of this command, you need to make sure to handle both cases which
  1847. /// both have different formats (but with json output for both).
  1848. fn build_command(&self, ctx: &BuildContext) -> Result<Command> {
  1849. match &ctx.mode {
  1850. // We're assembling rustc directly, so we need to be *very* careful. Cargo sets rustc's
  1851. // env up very particularly, and we want to match it 1:1 but with some changes.
  1852. //
  1853. // To do this, we reset the env completely, and then pass every env var that the original
  1854. // rustc process had 1:1.
  1855. //
  1856. // We need to unset a few things, like the RUSTC wrappers and then our special env var
  1857. // indicating that dx itself is the compiler. If we forget to do this, then the compiler
  1858. // ends up doing some recursive nonsense and dx is trying to link instead of compiling.
  1859. //
  1860. // todo: maybe rustc needs to be found on the FS instead of using the one in the path?
  1861. BuildMode::Thin { rustc_args, .. } => {
  1862. let mut cmd = Command::new("rustc");
  1863. cmd.current_dir(self.workspace_dir());
  1864. cmd.env_clear();
  1865. cmd.args(rustc_args.args[1..].iter());
  1866. cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
  1867. cmd.env_remove("RUSTC_WRAPPER");
  1868. cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR);
  1869. cmd.envs(self.cargo_build_env_vars(ctx)?);
  1870. cmd.arg(format!("-Clinker={}", Workspace::path_to_dx()?.display()));
  1871. if self.platform == Platform::Web {
  1872. cmd.arg("-Crelocation-model=pic");
  1873. }
  1874. tracing::debug!("Direct rustc: {:#?}", cmd);
  1875. cmd.envs(rustc_args.envs.iter().cloned());
  1876. // tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
  1877. Ok(cmd)
  1878. }
  1879. // For Base and Fat builds, we use a regular cargo setup, but we might need to intercept
  1880. // rustc itself in case we're hot-patching and need a reliable rustc environment to
  1881. // continuously recompile the top-level crate with.
  1882. //
  1883. // In the future, when we support hot-patching *all* workspace crates, we will need to
  1884. // make use of the RUSTC_WORKSPACE_WRAPPER environment variable instead of RUSTC_WRAPPER
  1885. // and then keep track of env and args on a per-crate basis.
  1886. //
  1887. // We've also had a number of issues with incorrect canonicalization when passing paths
  1888. // through envs on windows, hence the frequent use of dunce::canonicalize.
  1889. _ => {
  1890. let mut cmd = Command::new("cargo");
  1891. cmd.arg("rustc")
  1892. .current_dir(self.crate_dir())
  1893. .arg("--message-format")
  1894. .arg("json-diagnostic-rendered-ansi")
  1895. .args(self.cargo_build_arguments(ctx))
  1896. .envs(self.cargo_build_env_vars(ctx)?);
  1897. if ctx.mode == BuildMode::Fat {
  1898. cmd.env(
  1899. DX_RUSTC_WRAPPER_ENV_VAR,
  1900. dunce::canonicalize(self.rustc_wrapper_args_file.path())
  1901. .unwrap()
  1902. .display()
  1903. .to_string(),
  1904. );
  1905. cmd.env(
  1906. "RUSTC_WRAPPER",
  1907. Workspace::path_to_dx()?.display().to_string(),
  1908. );
  1909. }
  1910. tracing::debug!("Cargo: {:#?}", cmd);
  1911. Ok(cmd)
  1912. }
  1913. }
  1914. }
  1915. /// Create a list of arguments for cargo builds
  1916. ///
  1917. /// We always use `cargo rustc` *or* `rustc` directly. This means we can pass extra flags like
  1918. /// `-C` arguments directly to the compiler.
  1919. #[allow(clippy::vec_init_then_push)]
  1920. fn cargo_build_arguments(&self, ctx: &BuildContext) -> Vec<String> {
  1921. let mut cargo_args = Vec::with_capacity(4);
  1922. // Set the `--config profile.{profile}.{key}={value}` flags for the profile, filling in adhoc profile
  1923. cargo_args.extend(self.profile_args());
  1924. // Add required profile flags. --release overrides any custom profiles.
  1925. cargo_args.push("--profile".to_string());
  1926. cargo_args.push(self.profile.to_string());
  1927. // Pass the appropriate target to cargo. We *always* specify a target which is somewhat helpful for preventing thrashing
  1928. cargo_args.push("--target".to_string());
  1929. cargo_args.push(self.triple.to_string());
  1930. // We always run in verbose since the CLI itself is the one doing the presentation
  1931. cargo_args.push("--verbose".to_string());
  1932. if self.no_default_features {
  1933. cargo_args.push("--no-default-features".to_string());
  1934. }
  1935. if !self.features.is_empty() {
  1936. cargo_args.push("--features".to_string());
  1937. cargo_args.push(self.features.join(" "));
  1938. }
  1939. // We *always* set the package since that's discovered from cargo metadata
  1940. cargo_args.push(String::from("-p"));
  1941. cargo_args.push(self.package.clone());
  1942. // Set the executable
  1943. match self.executable_type() {
  1944. TargetKind::Bin => cargo_args.push("--bin".to_string()),
  1945. TargetKind::Lib => cargo_args.push("--lib".to_string()),
  1946. TargetKind::Example => cargo_args.push("--example".to_string()),
  1947. _ => {}
  1948. };
  1949. cargo_args.push(self.executable_name().to_string());
  1950. // Merge in extra args. Order shouldn't really matter.
  1951. cargo_args.extend(self.extra_cargo_args.clone());
  1952. cargo_args.push("--".to_string());
  1953. cargo_args.extend(self.extra_rustc_args.clone());
  1954. // The bundle splitter needs relocation data to create a call-graph.
  1955. // This will automatically be erased by wasm-opt during the optimization step.
  1956. if self.platform == Platform::Web && self.wasm_split {
  1957. cargo_args.push("-Clink-args=--emit-relocs".to_string());
  1958. }
  1959. // dx *always* links android and thin builds
  1960. if self.custom_linker.is_some()
  1961. || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
  1962. {
  1963. cargo_args.push(format!(
  1964. "-Clinker={}",
  1965. Workspace::path_to_dx().expect("can't find dx").display()
  1966. ));
  1967. }
  1968. // for debuggability, we need to make sure android studio can properly understand our build
  1969. // https://stackoverflow.com/questions/68481401/debugging-a-prebuilt-shared-library-in-android-studio
  1970. if self.platform == Platform::Android {
  1971. cargo_args.push("-Clink-arg=-Wl,--build-id=sha1".to_string());
  1972. }
  1973. // Handle frameworks/dylibs by setting the rpath
  1974. // This is dependent on the bundle structure - in this case, appimage and appbundle for mac/linux
  1975. // todo: we need to figure out what to do for windows
  1976. match self.triple.operating_system {
  1977. OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
  1978. cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks".to_string());
  1979. cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string());
  1980. }
  1981. OperatingSystem::Linux => {
  1982. cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string());
  1983. cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string());
  1984. }
  1985. _ => {}
  1986. }
  1987. // Our fancy hot-patching engine needs a lot of customization to work properly.
  1988. //
  1989. // These args are mostly intended to be passed when *fat* linking but are generally fine to
  1990. // pass for both fat and thin linking.
  1991. //
  1992. // We need save-temps and no-dead-strip in both cases though. When we run `cargo rustc` with
  1993. // these args, they will be captured and re-ran for the fast compiles in the future, so whatever
  1994. // we set here will be set for all future hot patches too.
  1995. if matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) {
  1996. // rustc gives us some portable flags required:
  1997. // - link-dead-code: prevents rust from passing -dead_strip to the linker since that's the default.
  1998. // - save-temps=true: keeps the incremental object files around, which we need for manually linking.
  1999. cargo_args.extend_from_slice(&[
  2000. "-Csave-temps=true".to_string(),
  2001. "-Clink-dead-code".to_string(),
  2002. ]);
  2003. // We need to set some extra args that ensure all symbols make it into the final output
  2004. // and that the linker doesn't strip them out.
  2005. //
  2006. // This basically amounts of -all_load or --whole-archive, depending on the linker.
  2007. // We just assume an ld-like interface on macos and a gnu-ld interface elsewhere.
  2008. //
  2009. // macOS/iOS use ld64 but through the `cc` interface.
  2010. // cargo_args.push("-Clink-args=-Wl,-all_load".to_string());
  2011. //
  2012. // Linux and Android fit under this umbrella, both with the same clang-like entrypoint
  2013. // and the gnu-ld interface.
  2014. //
  2015. // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string());
  2016. //
  2017. // If windows -Wl,--whole-archive is required since it follows gnu-ld convention.
  2018. // There might be other flags on windows - we haven't tested windows thoroughly.
  2019. //
  2020. // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string());
  2021. // https://learn.microsoft.com/en-us/cpp/build/reference/wholearchive-include-all-library-object-files?view=msvc-170
  2022. //
  2023. // ------------------------------------------------------------
  2024. //
  2025. // if web, -Wl,--whole-archive is required since it follows gnu-ld convention.
  2026. //
  2027. // We also use --no-gc-sections and --export-table and --export-memory to push
  2028. // said symbols into the export table.
  2029. //
  2030. // We use --emit-relocs to build up a solid call graph.
  2031. //
  2032. // rust uses its own wasm-ld linker which can be found here (it's just gcc-ld with a `-target wasm` flag):
  2033. // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld
  2034. // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld/wasm-ld
  2035. //
  2036. // Note that we can't use --export-all, unfortunately, since some symbols are internal
  2037. // to wasm-bindgen and exporting them causes the JS generation to fail.
  2038. //
  2039. // We are basically replicating what emscripten does here with its dynamic linking
  2040. // approach where the MAIN_MODULE is very "fat" and exports the necessary arguments
  2041. // for the side modules to be linked in. This guide is really helpful:
  2042. //
  2043. // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
  2044. //
  2045. // The tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref
  2046. // entries.
  2047. //
  2048. // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals
  2049. //
  2050. // It's fine that these exist in the base module but not in the patch.
  2051. if self.platform == Platform::Web
  2052. || self.triple.operating_system == OperatingSystem::Wasi
  2053. {
  2054. cargo_args.push("-Ctarget-cpu=mvp".into());
  2055. cargo_args.push("-Clink-arg=--no-gc-sections".into());
  2056. cargo_args.push("-Clink-arg=--growable-table".into());
  2057. cargo_args.push("-Clink-arg=--export-table".into());
  2058. cargo_args.push("-Clink-arg=--export-memory".into());
  2059. cargo_args.push("-Clink-arg=--emit-relocs".into());
  2060. cargo_args.push("-Clink-arg=--export=__stack_pointer".into());
  2061. cargo_args.push("-Clink-arg=--export=__heap_base".into());
  2062. cargo_args.push("-Clink-arg=--export=__data_end".into());
  2063. }
  2064. }
  2065. cargo_args
  2066. }
  2067. fn cargo_build_env_vars(&self, ctx: &BuildContext) -> Result<Vec<(&'static str, String)>> {
  2068. let mut env_vars = vec![];
  2069. // Make sure to set all the crazy android flags. Cross-compiling is hard, man.
  2070. if self.platform == Platform::Android {
  2071. env_vars.extend(self.android_env_vars()?);
  2072. };
  2073. // If this is a release build, bake the base path and title into the binary with env vars.
  2074. // todo: should we even be doing this? might be better being a build.rs or something else.
  2075. if self.release {
  2076. if let Some(base_path) = self.base_path() {
  2077. env_vars.push((ASSET_ROOT_ENV, base_path.to_string()));
  2078. }
  2079. env_vars.push((APP_TITLE_ENV, self.config.web.app.title.clone()));
  2080. }
  2081. // Assemble the rustflags by peering into the `.cargo/config.toml` file
  2082. let mut rust_flags = self.rustflags.clone();
  2083. // Disable reference types on wasm when using hotpatching
  2084. // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals
  2085. if self.platform == Platform::Web
  2086. && matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
  2087. {
  2088. rust_flags.flags.push("-Ctarget-cpu=mvp".to_string());
  2089. }
  2090. // Set the rust flags for the build if they're not empty.
  2091. if !rust_flags.flags.is_empty() {
  2092. env_vars.push((
  2093. "RUSTFLAGS",
  2094. rust_flags
  2095. .encode_space_separated()
  2096. .context("Failed to encode RUSTFLAGS")?,
  2097. ));
  2098. }
  2099. // If we're either zero-linking or using a custom linker, make `dx` itself do the linking.
  2100. if self.custom_linker.is_some()
  2101. || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat)
  2102. {
  2103. LinkAction {
  2104. triple: self.triple.clone(),
  2105. linker: self.custom_linker.clone(),
  2106. link_err_file: dunce::canonicalize(self.link_err_file.path())?,
  2107. link_args_file: dunce::canonicalize(self.link_args_file.path())?,
  2108. }
  2109. .write_env_vars(&mut env_vars)?;
  2110. }
  2111. Ok(env_vars)
  2112. }
  2113. fn android_env_vars(&self) -> Result<Vec<(&'static str, String)>> {
  2114. let mut env_vars = vec![];
  2115. let tools = self.workspace.android_tools()?;
  2116. let linker = tools.android_cc(&self.triple);
  2117. let min_sdk_version = tools.min_sdk_version();
  2118. let ar_path = tools.ar_path();
  2119. let target_cc = tools.target_cc();
  2120. let target_cxx = tools.target_cxx();
  2121. let java_home = tools.java_home();
  2122. let ndk = tools.ndk.clone();
  2123. tracing::debug!(
  2124. r#"Using android:
  2125. min_sdk_version: {min_sdk_version}
  2126. linker: {linker:?}
  2127. ar_path: {ar_path:?}
  2128. target_cc: {target_cc:?}
  2129. target_cxx: {target_cxx:?}
  2130. java_home: {java_home:?}
  2131. "#
  2132. );
  2133. env_vars.push(("ANDROID_NATIVE_API_LEVEL", min_sdk_version.to_string()));
  2134. env_vars.push(("TARGET_AR", ar_path.display().to_string()));
  2135. env_vars.push(("TARGET_CC", target_cc.display().to_string()));
  2136. env_vars.push(("TARGET_CXX", target_cxx.display().to_string()));
  2137. env_vars.push(("ANDROID_NDK_ROOT", ndk.display().to_string()));
  2138. if let Some(java_home) = java_home {
  2139. tracing::debug!("Setting JAVA_HOME to {java_home:?}");
  2140. env_vars.push(("JAVA_HOME", java_home.display().to_string()));
  2141. }
  2142. // Set the wry env vars - this is where wry will dump its kotlin files.
  2143. // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry.
  2144. env_vars.push(("WRY_ANDROID_PACKAGE", "dev.dioxus.main".to_string()));
  2145. env_vars.push(("WRY_ANDROID_LIBRARY", "dioxusmain".to_string()));
  2146. env_vars.push((
  2147. "WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
  2148. self.wry_android_kotlin_files_out_dir()
  2149. .display()
  2150. .to_string(),
  2151. ));
  2152. // todo(jon): the guide for openssl recommends extending the path to include the tools dir
  2153. // in practice I couldn't get this to work, but this might eventually become useful.
  2154. //
  2155. // https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration
  2156. //
  2157. // They recommend a configuration like this:
  2158. //
  2159. // // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570
  2160. // PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH
  2161. // ./Configure android-arm64 -D__ANDROID_API__=29
  2162. // make
  2163. //
  2164. // let tools_dir = arch.android_tools_dir(&ndk);
  2165. // let extended_path = format!(
  2166. // "{}:{}",
  2167. // tools_dir.display(),
  2168. // std::env::var("PATH").unwrap_or_default()
  2169. // );
  2170. // env_vars.push(("PATH", extended_path));
  2171. Ok(env_vars)
  2172. }
  2173. /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this
  2174. /// will return an estimate of the number of units in the crate based on cargo metadata.
  2175. ///
  2176. /// TODO: always use <https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph> once it is stable
  2177. async fn get_unit_count_estimate(&self, ctx: &BuildContext) -> usize {
  2178. // Try to get it from nightly
  2179. if let Ok(count) = self.get_unit_count(ctx).await {
  2180. return count;
  2181. }
  2182. // Otherwise, use cargo metadata
  2183. let units = self
  2184. .workspace
  2185. .krates
  2186. .krates_filtered(krates::DepKind::Dev)
  2187. .iter()
  2188. .map(|k| k.targets.len())
  2189. .sum::<usize>();
  2190. (units as f64 / 3.5) as usize
  2191. }
  2192. /// Try to get the unit graph for the crate. This is a nightly only feature which may not be
  2193. /// available with the current version of rustc the user has installed.
  2194. ///
  2195. /// It also might not be super reliable - I think in practice it occasionally returns 2x the units.
  2196. async fn get_unit_count(&self, ctx: &BuildContext) -> crate::Result<usize> {
  2197. #[derive(Debug, Deserialize)]
  2198. struct UnitGraph {
  2199. units: Vec<serde_json::Value>,
  2200. }
  2201. let output = tokio::process::Command::new("cargo")
  2202. .arg("+nightly")
  2203. .arg("build")
  2204. .arg("--unit-graph")
  2205. .arg("-Z")
  2206. .arg("unstable-options")
  2207. .args(self.cargo_build_arguments(ctx))
  2208. .envs(self.cargo_build_env_vars(ctx)?)
  2209. .output()
  2210. .await?;
  2211. if !output.status.success() {
  2212. return Err(anyhow::anyhow!("Failed to get unit count").into());
  2213. }
  2214. let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?;
  2215. let graph: UnitGraph =
  2216. serde_json::from_str(&output_text).context("Failed to get unit count")?;
  2217. Ok(graph.units.len())
  2218. }
  2219. pub(crate) fn all_target_features(&self) -> Vec<String> {
  2220. let mut features = self.features.clone();
  2221. if !self.no_default_features {
  2222. features.extend(
  2223. self.package()
  2224. .features
  2225. .get("default")
  2226. .cloned()
  2227. .unwrap_or_default(),
  2228. );
  2229. }
  2230. features.dedup();
  2231. features
  2232. }
  2233. /// returns the path to root build folder. This will be our working directory for the build.
  2234. ///
  2235. /// we only add an extension to the folders where it sorta matters that it's named with the extension.
  2236. /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc.
  2237. ///
  2238. /// for our simulator-based platforms, this is less important since they need to be zipped up anyways
  2239. /// to run in the simulator.
  2240. ///
  2241. /// For windows/linux, it's also not important since we're just running the exe directly out of the folder
  2242. ///
  2243. /// The idea of this folder is that we can run our top-level build command against it and we'll get
  2244. /// a final build output somewhere. Some platforms have basically no build command, and can simply
  2245. /// be ran by executing the exe directly.
  2246. pub(crate) fn root_dir(&self) -> PathBuf {
  2247. let platform_dir = self.platform_dir();
  2248. match self.platform {
  2249. Platform::Web => platform_dir.join("public"),
  2250. Platform::Server => platform_dir.clone(), // ends up *next* to the public folder
  2251. // These might not actually need to be called `.app` but it does let us run these with `open`
  2252. Platform::MacOS => platform_dir.join(format!("{}.app", self.bundled_app_name())),
  2253. Platform::Ios => platform_dir.join(format!("{}.app", self.bundled_app_name())),
  2254. // in theory, these all could end up directly in the root dir
  2255. Platform::Android => platform_dir.join("app"), // .apk (after bundling)
  2256. Platform::Linux => platform_dir.join("app"), // .appimage (after bundling)
  2257. Platform::Windows => platform_dir.join("app"), // .exe (after bundling)
  2258. Platform::Liveview => platform_dir.join("app"), // .exe (after bundling)
  2259. }
  2260. }
  2261. fn platform_dir(&self) -> PathBuf {
  2262. self.build_dir(self.platform, self.release)
  2263. }
  2264. fn platform_exe_name(&self) -> String {
  2265. match self.platform {
  2266. Platform::MacOS => self.executable_name().to_string(),
  2267. Platform::Ios => self.executable_name().to_string(),
  2268. Platform::Server => self.executable_name().to_string(),
  2269. Platform::Liveview => self.executable_name().to_string(),
  2270. Platform::Windows => format!("{}.exe", self.executable_name()),
  2271. // from the apk spec, the root exe is a shared library
  2272. // we include the user's rust code as a shared library with a fixed namespace
  2273. Platform::Android => "libdioxusmain.so".to_string(),
  2274. // this will be wrong, I think, but not important?
  2275. Platform::Web => format!("{}_bg.wasm", self.executable_name()),
  2276. // todo: maybe this should be called AppRun?
  2277. Platform::Linux => self.executable_name().to_string(),
  2278. }
  2279. }
  2280. /// Assemble the android app dir.
  2281. ///
  2282. /// This is a bit of a mess since we need to create a lot of directories and files. Other approaches
  2283. /// would be to unpack some zip folder or something stored via `include_dir!()`. However, we do
  2284. /// need to customize the whole setup a bit, so it's just simpler (though messier) to do it this way.
  2285. fn build_android_app_dir(&self) -> Result<()> {
  2286. use std::fs::{create_dir_all, write};
  2287. let root = self.root_dir();
  2288. // gradle
  2289. let wrapper = root.join("gradle").join("wrapper");
  2290. create_dir_all(&wrapper)?;
  2291. // app
  2292. let app = root.join("app");
  2293. let app_main = app.join("src").join("main");
  2294. let app_kotlin = app_main.join("kotlin");
  2295. let app_jnilibs = app_main.join("jniLibs");
  2296. let app_assets = app_main.join("assets");
  2297. let app_kotlin_out = self.wry_android_kotlin_files_out_dir();
  2298. create_dir_all(&app)?;
  2299. create_dir_all(&app_main)?;
  2300. create_dir_all(&app_kotlin)?;
  2301. create_dir_all(&app_jnilibs)?;
  2302. create_dir_all(&app_assets)?;
  2303. create_dir_all(&app_kotlin_out)?;
  2304. tracing::debug!(
  2305. r#"Initialized android dirs:
  2306. - gradle: {wrapper:?}
  2307. - app/ {app:?}
  2308. - app/src: {app_main:?}
  2309. - app/src/kotlin: {app_kotlin:?}
  2310. - app/src/jniLibs: {app_jnilibs:?}
  2311. - app/src/assets: {app_assets:?}
  2312. - app/src/kotlin/main: {app_kotlin_out:?}
  2313. "#
  2314. );
  2315. // handlebars
  2316. #[derive(Serialize)]
  2317. struct AndroidHandlebarsObjects {
  2318. application_id: String,
  2319. app_name: String,
  2320. android_bundle: Option<crate::AndroidSettings>,
  2321. }
  2322. let hbs_data = AndroidHandlebarsObjects {
  2323. application_id: self.full_mobile_app_name(),
  2324. app_name: self.bundled_app_name(),
  2325. android_bundle: self.config.bundle.android.clone(),
  2326. };
  2327. let hbs = handlebars::Handlebars::new();
  2328. // Top-level gradle config
  2329. write(
  2330. root.join("build.gradle.kts"),
  2331. include_bytes!("../../assets/android/gen/build.gradle.kts"),
  2332. )?;
  2333. write(
  2334. root.join("gradle.properties"),
  2335. include_bytes!("../../assets/android/gen/gradle.properties"),
  2336. )?;
  2337. write(
  2338. root.join("gradlew"),
  2339. include_bytes!("../../assets/android/gen/gradlew"),
  2340. )?;
  2341. write(
  2342. root.join("gradlew.bat"),
  2343. include_bytes!("../../assets/android/gen/gradlew.bat"),
  2344. )?;
  2345. write(
  2346. root.join("settings.gradle"),
  2347. include_bytes!("../../assets/android/gen/settings.gradle"),
  2348. )?;
  2349. // Then the wrapper and its properties
  2350. write(
  2351. wrapper.join("gradle-wrapper.properties"),
  2352. include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.properties"),
  2353. )?;
  2354. write(
  2355. wrapper.join("gradle-wrapper.jar"),
  2356. include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.jar"),
  2357. )?;
  2358. // Now the app directory
  2359. write(
  2360. app.join("build.gradle.kts"),
  2361. hbs.render_template(
  2362. include_str!("../../assets/android/gen/app/build.gradle.kts.hbs"),
  2363. &hbs_data,
  2364. )?,
  2365. )?;
  2366. write(
  2367. app.join("proguard-rules.pro"),
  2368. include_bytes!("../../assets/android/gen/app/proguard-rules.pro"),
  2369. )?;
  2370. let manifest_xml = match self.config.application.android_manifest.as_deref() {
  2371. Some(manifest) => std::fs::read_to_string(self.package_manifest_dir().join(manifest))
  2372. .context("Failed to locate custom AndroidManifest.xml")?,
  2373. _ => hbs.render_template(
  2374. include_str!("../../assets/android/gen/app/src/main/AndroidManifest.xml.hbs"),
  2375. &hbs_data,
  2376. )?,
  2377. };
  2378. write(
  2379. app.join("src").join("main").join("AndroidManifest.xml"),
  2380. manifest_xml,
  2381. )?;
  2382. // Write the main activity manually since tao dropped support for it
  2383. write(
  2384. self.wry_android_kotlin_files_out_dir()
  2385. .join("MainActivity.kt"),
  2386. hbs.render_template(
  2387. include_str!("../../assets/android/MainActivity.kt.hbs"),
  2388. &hbs_data,
  2389. )?,
  2390. )?;
  2391. // Write the res folder, containing stuff like default icons, colors, and menubars.
  2392. let res = app_main.join("res");
  2393. create_dir_all(&res)?;
  2394. create_dir_all(res.join("values"))?;
  2395. write(
  2396. res.join("values").join("strings.xml"),
  2397. hbs.render_template(
  2398. include_str!("../../assets/android/gen/app/src/main/res/values/strings.xml.hbs"),
  2399. &hbs_data,
  2400. )?,
  2401. )?;
  2402. write(
  2403. res.join("values").join("colors.xml"),
  2404. include_bytes!("../../assets/android/gen/app/src/main/res/values/colors.xml"),
  2405. )?;
  2406. write(
  2407. res.join("values").join("styles.xml"),
  2408. include_bytes!("../../assets/android/gen/app/src/main/res/values/styles.xml"),
  2409. )?;
  2410. create_dir_all(res.join("drawable"))?;
  2411. write(
  2412. res.join("drawable").join("ic_launcher_background.xml"),
  2413. include_bytes!(
  2414. "../../assets/android/gen/app/src/main/res/drawable/ic_launcher_background.xml"
  2415. ),
  2416. )?;
  2417. create_dir_all(res.join("drawable-v24"))?;
  2418. write(
  2419. res.join("drawable-v24").join("ic_launcher_foreground.xml"),
  2420. include_bytes!(
  2421. "../../assets/android/gen/app/src/main/res/drawable-v24/ic_launcher_foreground.xml"
  2422. ),
  2423. )?;
  2424. create_dir_all(res.join("mipmap-anydpi-v26"))?;
  2425. write(
  2426. res.join("mipmap-anydpi-v26").join("ic_launcher.xml"),
  2427. include_bytes!(
  2428. "../../assets/android/gen/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml"
  2429. ),
  2430. )?;
  2431. create_dir_all(res.join("mipmap-hdpi"))?;
  2432. write(
  2433. res.join("mipmap-hdpi").join("ic_launcher.webp"),
  2434. include_bytes!(
  2435. "../../assets/android/gen/app/src/main/res/mipmap-hdpi/ic_launcher.webp"
  2436. ),
  2437. )?;
  2438. create_dir_all(res.join("mipmap-mdpi"))?;
  2439. write(
  2440. res.join("mipmap-mdpi").join("ic_launcher.webp"),
  2441. include_bytes!(
  2442. "../../assets/android/gen/app/src/main/res/mipmap-mdpi/ic_launcher.webp"
  2443. ),
  2444. )?;
  2445. create_dir_all(res.join("mipmap-xhdpi"))?;
  2446. write(
  2447. res.join("mipmap-xhdpi").join("ic_launcher.webp"),
  2448. include_bytes!(
  2449. "../../assets/android/gen/app/src/main/res/mipmap-xhdpi/ic_launcher.webp"
  2450. ),
  2451. )?;
  2452. create_dir_all(res.join("mipmap-xxhdpi"))?;
  2453. write(
  2454. res.join("mipmap-xxhdpi").join("ic_launcher.webp"),
  2455. include_bytes!(
  2456. "../../assets/android/gen/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp"
  2457. ),
  2458. )?;
  2459. create_dir_all(res.join("mipmap-xxxhdpi"))?;
  2460. write(
  2461. res.join("mipmap-xxxhdpi").join("ic_launcher.webp"),
  2462. include_bytes!(
  2463. "../../assets/android/gen/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp"
  2464. ),
  2465. )?;
  2466. Ok(())
  2467. }
  2468. fn wry_android_kotlin_files_out_dir(&self) -> PathBuf {
  2469. let mut kotlin_dir = self
  2470. .root_dir()
  2471. .join("app")
  2472. .join("src")
  2473. .join("main")
  2474. .join("kotlin");
  2475. for segment in "dev.dioxus.main".split('.') {
  2476. kotlin_dir = kotlin_dir.join(segment);
  2477. }
  2478. kotlin_dir
  2479. }
  2480. /// Get the directory where this app can write to for this session that's guaranteed to be stable
  2481. /// for the same app. This is useful for emitting state like window position and size.
  2482. ///
  2483. /// The directory is specific for this app and might be
  2484. pub(crate) fn session_cache_dir(&self) -> PathBuf {
  2485. self.session_cache_dir.path().to_path_buf()
  2486. }
  2487. /// Get the outdir specified by the Dioxus.toml, relative to the crate directory.
  2488. /// We don't support workspaces yet since that would cause a collision of bundles per project.
  2489. pub(crate) fn crate_out_dir(&self) -> Option<PathBuf> {
  2490. self.config
  2491. .application
  2492. .out_dir
  2493. .as_ref()
  2494. .map(|out_dir| self.crate_dir().join(out_dir))
  2495. }
  2496. /// Compose an out directory. Represents the typical "dist" directory that
  2497. /// is "distributed" after building an application (configurable in the
  2498. /// `Dioxus.toml`).
  2499. fn internal_out_dir(&self) -> PathBuf {
  2500. let dir = self.target_dir.join("dx");
  2501. std::fs::create_dir_all(&dir).unwrap();
  2502. dir
  2503. }
  2504. /// Create a workdir for the given platform
  2505. /// This can be used as a temporary directory for the build, but in an observable way such that
  2506. /// you can see the files in the directory via `target`
  2507. ///
  2508. /// target/dx/build/app/web/
  2509. /// target/dx/build/app/web/public/
  2510. /// target/dx/build/app/web/server.exe
  2511. pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf {
  2512. self.internal_out_dir()
  2513. .join(&self.main_target)
  2514. .join(if release { "release" } else { "debug" })
  2515. .join(platform.build_folder_name())
  2516. }
  2517. /// target/dx/bundle/app/
  2518. /// target/dx/bundle/app/blah.app
  2519. /// target/dx/bundle/app/blah.exe
  2520. /// target/dx/bundle/app/public/
  2521. pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf {
  2522. self.internal_out_dir()
  2523. .join(&self.main_target)
  2524. .join("bundle")
  2525. .join(platform.build_folder_name())
  2526. }
  2527. /// Get the workspace directory for the crate
  2528. pub(crate) fn workspace_dir(&self) -> PathBuf {
  2529. self.workspace
  2530. .krates
  2531. .workspace_root()
  2532. .as_std_path()
  2533. .to_path_buf()
  2534. }
  2535. /// Get the directory of the crate
  2536. pub(crate) fn crate_dir(&self) -> PathBuf {
  2537. self.package()
  2538. .manifest_path
  2539. .parent()
  2540. .unwrap()
  2541. .as_std_path()
  2542. .to_path_buf()
  2543. }
  2544. /// Get the package we are currently in
  2545. pub(crate) fn package(&self) -> &krates::cm::Package {
  2546. &self.workspace.krates[self.crate_package]
  2547. }
  2548. /// Get the name of the package we are compiling
  2549. pub(crate) fn executable_name(&self) -> &str {
  2550. &self.crate_target.name
  2551. }
  2552. /// Get the type of executable we are compiling
  2553. pub(crate) fn executable_type(&self) -> TargetKind {
  2554. self.crate_target.kind[0]
  2555. }
  2556. /// Get the features required to build for the given platform
  2557. fn feature_for_platform(package: &krates::cm::Package, platform: Platform) -> String {
  2558. // Try to find the feature that activates the dioxus feature for the given platform
  2559. let dioxus_feature = platform.feature_name();
  2560. let res = package.features.iter().find_map(|(key, features)| {
  2561. // if the feature is just the name of the platform, we use that
  2562. if key == dioxus_feature {
  2563. return Some(key.clone());
  2564. }
  2565. // Otherwise look for the feature that starts with dioxus/ or dioxus?/ and matches the platform
  2566. for feature in features {
  2567. if let Some((_, after_dioxus)) = feature.split_once("dioxus") {
  2568. if let Some(dioxus_feature_enabled) =
  2569. after_dioxus.trim_start_matches('?').strip_prefix('/')
  2570. {
  2571. // If that enables the feature we are looking for, return that feature
  2572. if dioxus_feature_enabled == dioxus_feature {
  2573. return Some(key.clone());
  2574. }
  2575. }
  2576. }
  2577. }
  2578. None
  2579. });
  2580. res.unwrap_or_else(|| {
  2581. let fallback = format!("dioxus/{}", platform.feature_name()) ;
  2582. tracing::debug!(
  2583. "Could not find explicit feature for platform {platform}, passing `fallback` instead"
  2584. );
  2585. fallback
  2586. })
  2587. }
  2588. /// Return the version of the wasm-bindgen crate if it exists
  2589. fn wasm_bindgen_version(&self) -> Option<String> {
  2590. self.workspace
  2591. .krates
  2592. .krates_by_name("wasm-bindgen")
  2593. .next()
  2594. .map(|krate| krate.krate.version.to_string())
  2595. }
  2596. /// Return the platforms that are enabled for the package
  2597. ///
  2598. /// Ideally only one platform is enabled but we need to be able to
  2599. pub(crate) fn enabled_cargo_toml_platforms(
  2600. package: &krates::cm::Package,
  2601. no_default_features: bool,
  2602. ) -> Vec<Platform> {
  2603. let mut platforms = vec![];
  2604. // Attempt to discover the platform directly from the dioxus dependency
  2605. //
  2606. // [dependencies]
  2607. // dioxus = { features = ["web"] }
  2608. //
  2609. if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") {
  2610. for f in dxs.features.iter() {
  2611. if let Some(platform) = Platform::autodetect_from_cargo_feature(f) {
  2612. platforms.push(platform);
  2613. }
  2614. }
  2615. }
  2616. // Start searching through the default features
  2617. //
  2618. // [features]
  2619. // default = ["dioxus/web"]
  2620. //
  2621. // or
  2622. //
  2623. // [features]
  2624. // default = ["web"]
  2625. // web = ["dioxus/web"]
  2626. if no_default_features {
  2627. return platforms;
  2628. }
  2629. let Some(default) = package.features.get("default") else {
  2630. return platforms;
  2631. };
  2632. // we only trace features 1 level deep..
  2633. for feature in default.iter() {
  2634. // If the user directly specified a platform we can just use that.
  2635. if feature.starts_with("dioxus/") {
  2636. let dx_feature = feature.trim_start_matches("dioxus/");
  2637. let auto = Platform::autodetect_from_cargo_feature(dx_feature);
  2638. if let Some(auto) = auto {
  2639. platforms.push(auto);
  2640. }
  2641. }
  2642. // If the user is specifying an internal feature that points to a platform, we can use that
  2643. let internal_feature = package.features.get(feature);
  2644. if let Some(internal_feature) = internal_feature {
  2645. for feature in internal_feature {
  2646. if feature.starts_with("dioxus/") {
  2647. let dx_feature = feature.trim_start_matches("dioxus/");
  2648. let auto = Platform::autodetect_from_cargo_feature(dx_feature);
  2649. if let Some(auto) = auto {
  2650. platforms.push(auto);
  2651. }
  2652. }
  2653. }
  2654. }
  2655. }
  2656. platforms.sort();
  2657. platforms.dedup();
  2658. platforms
  2659. }
  2660. /// Gather the features that are enabled for the package
  2661. fn platformless_features(package: &krates::cm::Package) -> Vec<String> {
  2662. let Some(default) = package.features.get("default") else {
  2663. return Vec::new();
  2664. };
  2665. let mut kept_features = vec![];
  2666. // Only keep the top-level features in the default list that don't point to a platform directly
  2667. // IE we want to drop `web` if default = ["web"]
  2668. 'top: for feature in default {
  2669. // Don't keep features that point to a platform via dioxus/blah
  2670. if feature.starts_with("dioxus/") {
  2671. let dx_feature = feature.trim_start_matches("dioxus/");
  2672. if Platform::autodetect_from_cargo_feature(dx_feature).is_some() {
  2673. continue 'top;
  2674. }
  2675. }
  2676. // Don't keep features that point to a platform via an internal feature
  2677. if let Some(internal_feature) = package.features.get(feature) {
  2678. for feature in internal_feature {
  2679. if feature.starts_with("dioxus/") {
  2680. let dx_feature = feature.trim_start_matches("dioxus/");
  2681. if Platform::autodetect_from_cargo_feature(dx_feature).is_some() {
  2682. continue 'top;
  2683. }
  2684. }
  2685. }
  2686. }
  2687. // Otherwise we can keep it
  2688. kept_features.push(feature.to_string());
  2689. }
  2690. kept_features
  2691. }
  2692. pub(crate) fn mobile_org(&self) -> String {
  2693. let identifier = self.bundle_identifier();
  2694. let mut split = identifier.splitn(3, '.');
  2695. let sub = split
  2696. .next()
  2697. .expect("Identifier to have at least 3 periods like `com.example.app`");
  2698. let tld = split
  2699. .next()
  2700. .expect("Identifier to have at least 3 periods like `com.example.app`");
  2701. format!("{}.{}", sub, tld)
  2702. }
  2703. pub(crate) fn bundled_app_name(&self) -> String {
  2704. use convert_case::{Case, Casing};
  2705. self.executable_name().to_case(Case::Pascal)
  2706. }
  2707. pub(crate) fn full_mobile_app_name(&self) -> String {
  2708. format!("{}.{}", self.mobile_org(), self.bundled_app_name())
  2709. }
  2710. pub(crate) fn bundle_identifier(&self) -> String {
  2711. if let Some(identifier) = self.config.bundle.identifier.clone() {
  2712. return identifier.clone();
  2713. }
  2714. format!("com.example.{}", self.bundled_app_name())
  2715. }
  2716. /// The item that we'll try to run directly if we need to.
  2717. ///
  2718. /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod
  2719. pub(crate) fn main_exe(&self) -> PathBuf {
  2720. self.exe_dir().join(self.platform_exe_name())
  2721. }
  2722. /// Does the app specify:
  2723. ///
  2724. /// - Dioxus with "fullstack" enabled? (access to serverfns, etc)
  2725. /// - An explicit "fullstack" feature that enables said feature?
  2726. ///
  2727. /// Note that we don't detect if dependencies enable it transitively since we want to be explicit about it.
  2728. ///
  2729. /// The intention here is to detect if "fullstack" is enabled in the target's features list:
  2730. /// ```toml
  2731. /// [dependencies]
  2732. /// dioxus = { version = "0.4", features = ["fullstack"] }
  2733. /// ```
  2734. ///
  2735. /// or as an explicit feature in default:
  2736. /// ```toml
  2737. /// [features]
  2738. /// default = ["dioxus/fullstack"]
  2739. /// ```
  2740. ///
  2741. /// or as a default feature that enables the dioxus feature:
  2742. /// ```toml
  2743. /// [features]
  2744. /// default = ["fullstack"]
  2745. /// fullstack = ["dioxus/fullstack"]
  2746. /// ```
  2747. ///
  2748. /// or as an explicit feature (that enables the dioxus feature):
  2749. /// ```
  2750. /// dx serve app --features "fullstack"
  2751. /// ```
  2752. pub(crate) fn fullstack_feature_enabled(&self) -> bool {
  2753. let dioxus_dep = self
  2754. .package()
  2755. .dependencies
  2756. .iter()
  2757. .find(|dep| dep.name == "dioxus");
  2758. // If we don't have a dioxus dependency, we can't be fullstack. This shouldn't impact non-dioxus projects
  2759. let Some(dioxus_dep) = dioxus_dep else {
  2760. return false;
  2761. };
  2762. // Check if the dioxus dependency has the "fullstack" feature enabled
  2763. if dioxus_dep.features.iter().any(|f| f == "fullstack") {
  2764. return true;
  2765. }
  2766. // Check if any of the features in our feature list enables a feature that enables "fullstack"
  2767. let transitive = self
  2768. .package()
  2769. .features
  2770. .iter()
  2771. .filter(|(_name, list)| list.iter().any(|f| f == "dioxus/fullstack"));
  2772. for (name, _list) in transitive {
  2773. if self.features.contains(name) {
  2774. return true;
  2775. }
  2776. }
  2777. false
  2778. }
  2779. /// todo(jon): use handlebars templates instead of these prebaked templates
  2780. async fn write_metadata(&self) -> Result<()> {
  2781. // write the Info.plist file
  2782. match self.platform {
  2783. Platform::MacOS => {
  2784. let dest = self.root_dir().join("Contents").join("Info.plist");
  2785. let plist = self.info_plist_contents(self.platform)?;
  2786. std::fs::write(dest, plist)?;
  2787. }
  2788. Platform::Ios => {
  2789. let dest = self.root_dir().join("Info.plist");
  2790. let plist = self.info_plist_contents(self.platform)?;
  2791. std::fs::write(dest, plist)?;
  2792. }
  2793. // AndroidManifest.xml
  2794. // er.... maybe even all the kotlin/java/gradle stuff?
  2795. Platform::Android => {}
  2796. // Probably some custom format or a plist file (haha)
  2797. // When we do the proper bundle, we'll need to do something with wix templates, I think?
  2798. Platform::Windows => {}
  2799. // eventually we'll create the .appimage file, I guess?
  2800. Platform::Linux => {}
  2801. // These are served as folders, not appimages, so we don't need to do anything special (I think?)
  2802. // Eventually maybe write some secrets/.env files for the server?
  2803. // We could also distribute them as a deb/rpm for linux and msi for windows
  2804. Platform::Web => {}
  2805. Platform::Server => {}
  2806. Platform::Liveview => {}
  2807. }
  2808. Ok(())
  2809. }
  2810. /// Run the optimizers, obfuscators, minimizers, signers, etc
  2811. async fn optimize(&self, ctx: &BuildContext) -> Result<()> {
  2812. match self.platform {
  2813. Platform::Web => {
  2814. // Compress the asset dir
  2815. // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
  2816. let pre_compress = self.should_pre_compress_web_assets(self.release);
  2817. if pre_compress {
  2818. ctx.status_compressing_assets();
  2819. let asset_dir = self.asset_dir();
  2820. tokio::task::spawn_blocking(move || {
  2821. crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
  2822. })
  2823. .await
  2824. .unwrap()?;
  2825. }
  2826. }
  2827. Platform::MacOS => {}
  2828. Platform::Windows => {}
  2829. Platform::Linux => {}
  2830. Platform::Ios => {}
  2831. Platform::Android => {}
  2832. Platform::Server => {}
  2833. Platform::Liveview => {}
  2834. }
  2835. Ok(())
  2836. }
  2837. /// Check if assets should be pre_compressed. This will only be true in release mode if the user
  2838. /// has enabled pre_compress in the web config.
  2839. fn should_pre_compress_web_assets(&self, release: bool) -> bool {
  2840. self.config.web.pre_compress & release
  2841. }
  2842. /// Check if the wasm output should be bundled to an asset type app.
  2843. fn should_bundle_to_asset(&self) -> bool {
  2844. self.release && !self.wasm_split && self.platform == Platform::Web
  2845. }
  2846. /// Bundle the web app
  2847. /// - Run wasm-bindgen
  2848. /// - Bundle split
  2849. /// - Run wasm-opt
  2850. /// - Register the .wasm and .js files with the asset system
  2851. async fn bundle_web(
  2852. &self,
  2853. ctx: &BuildContext,
  2854. exe: &Path,
  2855. assets: &mut AssetManifest,
  2856. ) -> Result<()> {
  2857. use crate::{wasm_bindgen::WasmBindgen, wasm_opt};
  2858. use std::fmt::Write;
  2859. // Locate the output of the build files and the bindgen output
  2860. // We'll fill these in a second if they don't already exist
  2861. let bindgen_outdir = self.wasm_bindgen_out_dir();
  2862. let post_bindgen_wasm = self.wasm_bindgen_wasm_output_file();
  2863. let should_bundle_split: bool = self.wasm_split;
  2864. let bindgen_version = self
  2865. .wasm_bindgen_version()
  2866. .expect("this should have been checked by tool verification");
  2867. // Prepare any work dirs
  2868. _ = std::fs::remove_dir_all(&bindgen_outdir);
  2869. std::fs::create_dir_all(&bindgen_outdir)?;
  2870. // Lift the internal functions to exports
  2871. if ctx.mode == BuildMode::Fat {
  2872. let unprocessed = std::fs::read(exe)?;
  2873. let all_exported_bytes = crate::build::prepare_wasm_base_module(&unprocessed)?;
  2874. std::fs::write(exe, all_exported_bytes)?;
  2875. }
  2876. // Prepare our configuration
  2877. //
  2878. // we turn off debug symbols in dev mode but leave them on in release mode (weird!) since
  2879. // wasm-opt and wasm-split need them to do better optimizations.
  2880. //
  2881. // We leave demangling to false since it's faster and these tools seem to prefer the raw symbols.
  2882. // todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically.
  2883. let will_wasm_opt = self.release || self.wasm_split;
  2884. let keep_debug = self.config.web.wasm_opt.debug
  2885. || self.debug_symbols
  2886. || self.wasm_split
  2887. || !self.release
  2888. || will_wasm_opt
  2889. || ctx.mode == BuildMode::Fat;
  2890. let keep_names = will_wasm_opt || ctx.mode == BuildMode::Fat;
  2891. let demangle = false;
  2892. let wasm_opt_options = WasmOptConfig {
  2893. memory_packing: self.wasm_split,
  2894. debug: self.debug_symbols,
  2895. ..self.config.web.wasm_opt.clone()
  2896. };
  2897. // Run wasm-bindgen. Some of the options are not "optimal" but will be fixed up by wasm-opt
  2898. //
  2899. // There's performance implications here. Running with --debug is slower than without
  2900. // We're keeping around lld sections and names but wasm-opt will fix them
  2901. // todo(jon): investigate a good balance of wiping debug symbols during dev (or doing a double build?)
  2902. ctx.status_wasm_bindgen_start();
  2903. tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
  2904. let start = std::time::Instant::now();
  2905. WasmBindgen::new(&bindgen_version)
  2906. .input_path(exe)
  2907. .target("web")
  2908. .debug(keep_debug)
  2909. .demangle(demangle)
  2910. .keep_debug(keep_debug)
  2911. .keep_lld_sections(true)
  2912. .out_name(self.executable_name())
  2913. .out_dir(&bindgen_outdir)
  2914. .remove_name_section(!keep_names)
  2915. .remove_producers_section(!keep_names)
  2916. .run()
  2917. .await
  2918. .context("Failed to generate wasm-bindgen bindings")?;
  2919. tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
  2920. // Run bundle splitting if the user has requested it
  2921. // It's pretty expensive but because of rayon should be running separate threads, hopefully
  2922. // not blocking this thread. Dunno if that's true
  2923. if should_bundle_split {
  2924. ctx.status_splitting_bundle();
  2925. if !will_wasm_opt {
  2926. return Err(anyhow::anyhow!(
  2927. "Bundle splitting should automatically enable wasm-opt, but it was not enabled."
  2928. )
  2929. .into());
  2930. }
  2931. // Load the contents of these binaries since we need both of them
  2932. // We're going to use the default makeLoad glue from wasm-split
  2933. let original = std::fs::read(exe)?;
  2934. let bindgened = std::fs::read(&post_bindgen_wasm)?;
  2935. let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string();
  2936. // Run the emitter
  2937. let splitter = wasm_split_cli::Splitter::new(&original, &bindgened);
  2938. let modules = splitter
  2939. .context("Failed to parse wasm for splitter")?
  2940. .emit()
  2941. .context("Failed to emit wasm split modules")?;
  2942. // Write the chunks that contain shared imports
  2943. // These will be in the format of chunk_0_modulename.wasm - this is hardcoded in wasm-split
  2944. tracing::debug!("Writing split chunks to disk");
  2945. for (idx, chunk) in modules.chunks.iter().enumerate() {
  2946. let path = bindgen_outdir.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name));
  2947. wasm_opt::write_wasm(&chunk.bytes, &path, &wasm_opt_options).await?;
  2948. writeln!(
  2949. glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);",
  2950. url = assets
  2951. .register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
  2952. )?;
  2953. }
  2954. // Write the modules that contain the entrypoints
  2955. tracing::debug!("Writing split modules to disk");
  2956. for (idx, module) in modules.modules.iter().enumerate() {
  2957. let comp_name = module
  2958. .component_name
  2959. .as_ref()
  2960. .context("generated bindgen module has no name?")?;
  2961. let path = bindgen_outdir.join(format!("module_{}_{}.wasm", idx, comp_name));
  2962. wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?;
  2963. let hash_id = module
  2964. .hash_id
  2965. .as_ref()
  2966. .context("generated wasm-split bindgen module has no hash id?")?;
  2967. writeln!(
  2968. glue,
  2969. "export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/assets/{url}\", [{deps}], fusedImports);",
  2970. module = module.module_name,
  2971. // Again, register this wasm with the asset system
  2972. url = assets
  2973. .register_asset(&path, AssetOptions::Unknown)?.bundled_path(),
  2974. // This time, make sure to write the dependencies of this chunk
  2975. // The names here are again, hardcoded in wasm-split - fix this eventually.
  2976. deps = module
  2977. .relies_on_chunks
  2978. .iter()
  2979. .map(|idx| format!("__wasm_split_load_chunk_{idx}"))
  2980. .collect::<Vec<_>>()
  2981. .join(", ")
  2982. )?;
  2983. }
  2984. // Write the js binding
  2985. // It's not registered as an asset since it will get included in the main.js file
  2986. let js_output_path = bindgen_outdir.join("__wasm_split.js");
  2987. std::fs::write(&js_output_path, &glue)?;
  2988. // Make sure to write some entropy to the main.js file so it gets a new hash
  2989. // If we don't do this, the main.js file will be cached and never pick up the chunk names
  2990. let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, glue.as_bytes());
  2991. std::fs::OpenOptions::new()
  2992. .append(true)
  2993. .open(self.wasm_bindgen_js_output_file())
  2994. .context("Failed to open main.js file")?
  2995. .write_all(format!("/*{uuid}*/").as_bytes())?;
  2996. // Write the main wasm_bindgen file and register it with the asset system
  2997. // This will overwrite the file in place
  2998. // We will wasm-opt it in just a second...
  2999. std::fs::write(&post_bindgen_wasm, modules.main.bytes).unwrap();
  3000. }
  3001. if matches!(ctx.mode, BuildMode::Fat) {
  3002. // add `export { __wbg_get_imports };` to the end of the wasmbindgen js file
  3003. let mut js = std::fs::read(self.wasm_bindgen_js_output_file())?;
  3004. writeln!(js, "\nexport {{ __wbg_get_imports }};")?;
  3005. std::fs::write(self.wasm_bindgen_js_output_file(), js)?;
  3006. }
  3007. // Make sure to optimize the main wasm file if requested or if bundle splitting
  3008. if should_bundle_split || self.release {
  3009. ctx.status_optimizing_wasm();
  3010. wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
  3011. }
  3012. // In release mode, we make the wasm and bindgen files into assets so they get bundled with max
  3013. // optimizations.
  3014. if self.should_bundle_to_asset() {
  3015. // Register the main.js with the asset system so it bundles in the snippets and optimizes
  3016. assets.register_asset(
  3017. &self.wasm_bindgen_js_output_file(),
  3018. AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
  3019. )?;
  3020. }
  3021. if self.should_bundle_to_asset() {
  3022. // Make sure to register the main wasm file with the asset system
  3023. assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
  3024. }
  3025. // Write the index.html file with the pre-configured contents we got from pre-rendering
  3026. self.write_index_html(assets)?;
  3027. Ok(())
  3028. }
  3029. /// Write the index.html file to the output directory. This must be called after the wasm and js
  3030. /// assets are registered with the asset system if this is a release build.
  3031. pub(crate) fn write_index_html(&self, assets: &AssetManifest) -> Result<()> {
  3032. // Get the path to the wasm-bindgen output files. Either the direct file or the opitmized one depending on the build mode
  3033. let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
  3034. let wasm_path = if self.should_bundle_to_asset() {
  3035. let name = assets
  3036. .get_first_asset_for_source(&wasm_bindgen_wasm_out)
  3037. .expect("The wasm source must exist before creating index.html");
  3038. format!("assets/{}", name.bundled_path())
  3039. } else {
  3040. format!(
  3041. "wasm/{}",
  3042. wasm_bindgen_wasm_out.file_name().unwrap().to_str().unwrap()
  3043. )
  3044. };
  3045. let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
  3046. let js_path = if self.should_bundle_to_asset() {
  3047. let name = assets
  3048. .get_first_asset_for_source(&wasm_bindgen_js_out)
  3049. .expect("The js source must exist before creating index.html");
  3050. format!("assets/{}", name.bundled_path())
  3051. } else {
  3052. format!(
  3053. "wasm/{}",
  3054. wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
  3055. )
  3056. };
  3057. // Write the index.html file with the pre-configured contents we got from pre-rendering
  3058. std::fs::write(
  3059. self.root_dir().join("index.html"),
  3060. self.prepare_html(assets, &wasm_path, &js_path).unwrap(),
  3061. )?;
  3062. Ok(())
  3063. }
  3064. fn info_plist_contents(&self, platform: Platform) -> Result<String> {
  3065. #[derive(Serialize)]
  3066. pub struct InfoPlistData {
  3067. pub display_name: String,
  3068. pub bundle_name: String,
  3069. pub bundle_identifier: String,
  3070. pub executable_name: String,
  3071. }
  3072. // Attempt to use the user's manually specified
  3073. let _app = &self.config.application;
  3074. match platform {
  3075. Platform::MacOS => {
  3076. if let Some(macos_info_plist) = _app.macos_info_plist.as_deref() {
  3077. return Ok(std::fs::read_to_string(macos_info_plist)?);
  3078. }
  3079. }
  3080. Platform::Ios => {
  3081. if let Some(macos_info_plist) = _app.ios_info_plist.as_deref() {
  3082. return Ok(std::fs::read_to_string(macos_info_plist)?);
  3083. }
  3084. }
  3085. _ => {}
  3086. }
  3087. match platform {
  3088. Platform::MacOS => handlebars::Handlebars::new()
  3089. .render_template(
  3090. include_str!("../../assets/macos/mac.plist.hbs"),
  3091. &InfoPlistData {
  3092. display_name: self.bundled_app_name(),
  3093. bundle_name: self.bundled_app_name(),
  3094. executable_name: self.platform_exe_name(),
  3095. bundle_identifier: self.bundle_identifier(),
  3096. },
  3097. )
  3098. .map_err(|e| e.into()),
  3099. Platform::Ios => handlebars::Handlebars::new()
  3100. .render_template(
  3101. include_str!("../../assets/ios/ios.plist.hbs"),
  3102. &InfoPlistData {
  3103. display_name: self.bundled_app_name(),
  3104. bundle_name: self.bundled_app_name(),
  3105. executable_name: self.platform_exe_name(),
  3106. bundle_identifier: self.bundle_identifier(),
  3107. },
  3108. )
  3109. .map_err(|e| e.into()),
  3110. _ => Err(anyhow::anyhow!("Unsupported platform for Info.plist").into()),
  3111. }
  3112. }
  3113. /// Run any final tools to produce apks or other artifacts we might need.
  3114. ///
  3115. /// This might include codesigning, zipping, creating an appimage, etc
  3116. async fn assemble(&self, ctx: &BuildContext) -> Result<()> {
  3117. if let Platform::Android = self.platform {
  3118. ctx.status_running_gradle();
  3119. // When the build mode is set to release and there is an Android signature configuration, use assembleRelease
  3120. let build_type = if self.release && self.config.bundle.android.is_some() {
  3121. "assembleRelease"
  3122. } else {
  3123. "assembleDebug"
  3124. };
  3125. let output = Command::new(self.gradle_exe()?)
  3126. .arg(build_type)
  3127. .current_dir(self.root_dir())
  3128. .output()
  3129. .await?;
  3130. if !output.status.success() {
  3131. return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into());
  3132. }
  3133. }
  3134. Ok(())
  3135. }
  3136. /// Run bundleRelease and return the path to the `.aab` file
  3137. ///
  3138. /// <https://stackoverflow.com/questions/57072558/whats-the-difference-between-gradlewassemblerelease-gradlewinstallrelease-and>
  3139. pub(crate) async fn android_gradle_bundle(&self) -> Result<PathBuf> {
  3140. let output = Command::new(self.gradle_exe()?)
  3141. .arg("bundleRelease")
  3142. .current_dir(self.root_dir())
  3143. .output()
  3144. .await
  3145. .context("Failed to run gradle bundleRelease")?;
  3146. if !output.status.success() {
  3147. return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into());
  3148. }
  3149. let app_release = self
  3150. .root_dir()
  3151. .join("app")
  3152. .join("build")
  3153. .join("outputs")
  3154. .join("bundle")
  3155. .join("release");
  3156. // Rename it to Name-arch.aab
  3157. let from = app_release.join("app-release.aab");
  3158. let to = app_release.join(format!("{}-{}.aab", self.bundled_app_name(), self.triple));
  3159. std::fs::rename(from, &to).context("Failed to rename aab")?;
  3160. Ok(to)
  3161. }
  3162. fn gradle_exe(&self) -> Result<PathBuf> {
  3163. // make sure we can execute the gradlew script
  3164. #[cfg(unix)]
  3165. {
  3166. use std::os::unix::prelude::PermissionsExt;
  3167. std::fs::set_permissions(
  3168. self.root_dir().join("gradlew"),
  3169. std::fs::Permissions::from_mode(0o755),
  3170. )?;
  3171. }
  3172. let gradle_exec_name = match cfg!(windows) {
  3173. true => "gradlew.bat",
  3174. false => "gradlew",
  3175. };
  3176. Ok(self.root_dir().join(gradle_exec_name))
  3177. }
  3178. pub(crate) fn debug_apk_path(&self) -> PathBuf {
  3179. self.root_dir()
  3180. .join("app")
  3181. .join("build")
  3182. .join("outputs")
  3183. .join("apk")
  3184. .join("debug")
  3185. .join("app-debug.apk")
  3186. }
  3187. /// We only really currently care about:
  3188. ///
  3189. /// - app dir (.app, .exe, .apk, etc)
  3190. /// - assetas dir
  3191. /// - exe dir (.exe, .app, .apk, etc)
  3192. /// - extra scaffolding
  3193. ///
  3194. /// It's not guaranteed that they're different from any other folder
  3195. pub(crate) fn prepare_build_dir(&self) -> Result<()> {
  3196. use std::fs::{create_dir_all, remove_dir_all};
  3197. use std::sync::OnceLock;
  3198. static INITIALIZED: OnceLock<Result<()>> = OnceLock::new();
  3199. let success = INITIALIZED.get_or_init(|| {
  3200. if self.platform != Platform::Server {
  3201. _ = remove_dir_all(self.exe_dir());
  3202. }
  3203. self.flush_session_cache();
  3204. create_dir_all(self.root_dir())?;
  3205. create_dir_all(self.exe_dir())?;
  3206. create_dir_all(self.asset_dir())?;
  3207. tracing::debug!(
  3208. r#"Initialized build dirs:
  3209. • root dir: {:?}
  3210. • exe dir: {:?}
  3211. • asset dir: {:?}"#,
  3212. self.root_dir(),
  3213. self.exe_dir(),
  3214. self.asset_dir(),
  3215. );
  3216. // we could download the templates from somewhere (github?) but after having banged my head against
  3217. // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates
  3218. // by writing them here.
  3219. if let Platform::Android = self.platform {
  3220. self.build_android_app_dir()?;
  3221. }
  3222. Ok(())
  3223. });
  3224. if let Err(e) = success.as_ref() {
  3225. return Err(format!("Failed to initialize build directory: {e}").into());
  3226. }
  3227. Ok(())
  3228. }
  3229. pub(crate) fn asset_dir(&self) -> PathBuf {
  3230. match self.platform {
  3231. Platform::MacOS => self
  3232. .root_dir()
  3233. .join("Contents")
  3234. .join("Resources")
  3235. .join("assets"),
  3236. Platform::Android => self
  3237. .root_dir()
  3238. .join("app")
  3239. .join("src")
  3240. .join("main")
  3241. .join("assets"),
  3242. // everyone else is soooo normal, just app/assets :)
  3243. Platform::Web
  3244. | Platform::Ios
  3245. | Platform::Windows
  3246. | Platform::Linux
  3247. | Platform::Server
  3248. | Platform::Liveview => self.root_dir().join("assets"),
  3249. }
  3250. }
  3251. /// The directory in which we'll put the main exe
  3252. ///
  3253. /// Mac, Android, Web are a little weird
  3254. /// - mac wants to be in Contents/MacOS
  3255. /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture)
  3256. /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work)
  3257. ///
  3258. /// I think all others are just in the root folder
  3259. ///
  3260. /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do.
  3261. fn exe_dir(&self) -> PathBuf {
  3262. match self.platform {
  3263. Platform::MacOS => self.root_dir().join("Contents").join("MacOS"),
  3264. Platform::Web => self.root_dir().join("wasm"),
  3265. // Android has a whole build structure to it
  3266. Platform::Android => self
  3267. .root_dir()
  3268. .join("app")
  3269. .join("src")
  3270. .join("main")
  3271. .join("jniLibs")
  3272. .join(AndroidTools::android_jnilib(&self.triple)),
  3273. // these are all the same, I think?
  3274. Platform::Windows
  3275. | Platform::Linux
  3276. | Platform::Ios
  3277. | Platform::Server
  3278. | Platform::Liveview => self.root_dir(),
  3279. }
  3280. }
  3281. /// Get the path to the wasm bindgen temporary output folder
  3282. fn wasm_bindgen_out_dir(&self) -> PathBuf {
  3283. self.root_dir().join("wasm")
  3284. }
  3285. /// Get the path to the wasm bindgen javascript output file
  3286. pub(crate) fn wasm_bindgen_js_output_file(&self) -> PathBuf {
  3287. self.wasm_bindgen_out_dir()
  3288. .join(self.executable_name())
  3289. .with_extension("js")
  3290. }
  3291. /// Get the path to the wasm bindgen wasm output file
  3292. pub(crate) fn wasm_bindgen_wasm_output_file(&self) -> PathBuf {
  3293. self.wasm_bindgen_out_dir()
  3294. .join(format!("{}_bg", self.executable_name()))
  3295. .with_extension("wasm")
  3296. }
  3297. /// Get the path to the asset optimizer version file
  3298. pub(crate) fn asset_optimizer_version_file(&self) -> PathBuf {
  3299. self.platform_dir().join(".cli-version")
  3300. }
  3301. fn flush_session_cache(&self) {
  3302. let cache_dir = self.session_cache_dir();
  3303. _ = std::fs::remove_dir_all(&cache_dir);
  3304. _ = std::fs::create_dir_all(&cache_dir);
  3305. }
  3306. /// Check for tooling that might be required for this build.
  3307. ///
  3308. /// This should generally be only called on the first build since it takes time to verify the tooling
  3309. /// is in place, and we don't want to slow down subsequent builds.
  3310. pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> {
  3311. ctx.status_installing_tooling();
  3312. match self.platform {
  3313. Platform::Web => self.verify_web_tooling().await?,
  3314. Platform::Ios => self.verify_ios_tooling().await?,
  3315. Platform::Android => self.verify_android_tooling().await?,
  3316. Platform::Linux => self.verify_linux_tooling().await?,
  3317. Platform::MacOS | Platform::Windows | Platform::Server | Platform::Liveview => {}
  3318. }
  3319. Ok(())
  3320. }
  3321. async fn verify_web_tooling(&self) -> Result<()> {
  3322. // Install target using rustup.
  3323. #[cfg(not(feature = "no-downloads"))]
  3324. if !self.workspace.has_wasm32_unknown_unknown() {
  3325. tracing::info!(
  3326. "Web platform requires wasm32-unknown-unknown to be installed. Installing..."
  3327. );
  3328. let _ = tokio::process::Command::new("rustup")
  3329. .args(["target", "add", "wasm32-unknown-unknown"])
  3330. .output()
  3331. .await?;
  3332. }
  3333. // Ensure target is installed.
  3334. if !self.workspace.has_wasm32_unknown_unknown() {
  3335. return Err(Error::Other(anyhow::anyhow!(
  3336. "Missing target wasm32-unknown-unknown."
  3337. )));
  3338. }
  3339. // Wasm bindgen
  3340. let krate_bindgen_version = self.wasm_bindgen_version().ok_or(anyhow::anyhow!(
  3341. "failed to detect wasm-bindgen version, unable to proceed"
  3342. ))?;
  3343. WasmBindgen::verify_install(&krate_bindgen_version).await?;
  3344. Ok(())
  3345. }
  3346. /// Currently does nothing, but eventually we need to check that the mobile tooling is installed.
  3347. ///
  3348. /// For ios, this would be just aarch64-apple-ios + aarch64-apple-ios-sim, as well as xcrun and xcode-select
  3349. ///
  3350. /// We don't auto-install these yet since we're not doing an architecture check. We assume most users
  3351. /// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually
  3352. /// should be installing the x86 versions.
  3353. async fn verify_ios_tooling(&self) -> Result<()> {
  3354. // open the simulator
  3355. // _ = tokio::process::Command::new("open")
  3356. // .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app")
  3357. // .output()
  3358. // .await;
  3359. // Now xcrun to open the device
  3360. // todo: we should try and query the device list and/or parse it rather than hardcode this simulator
  3361. // _ = tokio::process::Command::new("xcrun")
  3362. // .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"])
  3363. // .output()
  3364. // .await;
  3365. // if !rustup
  3366. // .installed_toolchains
  3367. // .contains(&"aarch64-apple-ios".to_string())
  3368. // {
  3369. // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it.");
  3370. // }
  3371. // if !rustup
  3372. // .installed_toolchains
  3373. // .contains(&"aarch64-apple-ios-sim".to_string())
  3374. // {
  3375. // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it.");
  3376. // }
  3377. Ok(())
  3378. }
  3379. /// Check if the android tooling is installed
  3380. ///
  3381. /// looks for the android sdk + ndk
  3382. ///
  3383. /// will do its best to fill in the missing bits by exploring the sdk structure
  3384. /// IE will attempt to use the Java installed from android studio if possible.
  3385. async fn verify_android_tooling(&self) -> Result<()> {
  3386. let linker = self.workspace.android_tools()?.android_cc(&self.triple);
  3387. tracing::debug!("Verifying android linker: {linker:?}");
  3388. if linker.exists() {
  3389. return Ok(());
  3390. }
  3391. Err(anyhow::anyhow!(
  3392. "Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation."
  3393. ).into())
  3394. }
  3395. /// Ensure the right dependencies are installed for linux apps.
  3396. /// This varies by distro, so we just do nothing for now.
  3397. ///
  3398. /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri:
  3399. /// <https://tauri.app/start/prerequisites/>
  3400. async fn verify_linux_tooling(&self) -> Result<()> {
  3401. Ok(())
  3402. }
  3403. /// Blow away the fingerprint for this package, forcing rustc to recompile it.
  3404. ///
  3405. /// This prevents rustc from using the cached version of the binary, which can cause issues
  3406. /// with our hotpatching setup since it uses linker interception.
  3407. ///
  3408. /// This is sadly a hack. I think there might be other ways of busting the fingerprint (rustc wrapper?)
  3409. /// but that would require relying on cargo internals.
  3410. ///
  3411. /// This might stop working if/when cargo stabilizes contents-based fingerprinting.
  3412. fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
  3413. if matches!(ctx.mode, BuildMode::Fat) {
  3414. // `dx` compiles everything with `--target` which ends up with a structure like:
  3415. // target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
  3416. //
  3417. // normally you can't rely on this structure (ie with `cargo build`) but the explicit
  3418. // target arg guarantees this will work.
  3419. let fingerprint_dir = self
  3420. .target_dir
  3421. .join(self.triple.to_string())
  3422. .join(&self.profile)
  3423. .join(".fingerprint");
  3424. // split at the last `-` used to separate the hash from the name
  3425. // This causes to more aggressively bust hashes for all combinations of features
  3426. // and fingerprints for this package since we're just ignoring the hash
  3427. for entry in std::fs::read_dir(&fingerprint_dir)?.flatten() {
  3428. if let Some(fname) = entry.file_name().to_str() {
  3429. if let Some((name, _)) = fname.rsplit_once('-') {
  3430. if name == self.package().name {
  3431. _ = std::fs::remove_dir_all(entry.path());
  3432. }
  3433. }
  3434. }
  3435. }
  3436. }
  3437. Ok(())
  3438. }
  3439. async fn create_patch_cache(&self, exe: &Path) -> Result<HotpatchModuleCache> {
  3440. let exe = match self.platform {
  3441. Platform::Web => self.wasm_bindgen_wasm_output_file(),
  3442. _ => exe.to_path_buf(),
  3443. };
  3444. Ok(HotpatchModuleCache::new(&exe, &self.triple)?)
  3445. }
  3446. /// Users create an index.html for their SPA if they want it
  3447. ///
  3448. /// We always write our wasm as main.js and main_bg.wasm
  3449. ///
  3450. /// In prod we run the optimizer which bundles everything together properly
  3451. ///
  3452. /// So their index.html needs to include main.js in the scripts otherwise nothing happens?
  3453. ///
  3454. /// Seems like every platform has a weird file that declares a bunch of stuff
  3455. /// - web: index.html
  3456. /// - ios: info.plist
  3457. /// - macos: info.plist
  3458. /// - linux: appimage root thing?
  3459. /// - android: androidmanifest.xml
  3460. ///
  3461. /// You also might different variants of these files (staging / prod) and different flavors (eu/us)
  3462. ///
  3463. /// web's index.html is weird since it's not just a bundle format but also a *content* format
  3464. pub(crate) fn prepare_html(
  3465. &self,
  3466. assets: &AssetManifest,
  3467. wasm_path: &str,
  3468. js_path: &str,
  3469. ) -> Result<String> {
  3470. let mut html = {
  3471. const DEV_DEFAULT_HTML: &str = include_str!("../../assets/web/dev.index.html");
  3472. const PROD_DEFAULT_HTML: &str = include_str!("../../assets/web/prod.index.html");
  3473. let crate_root: &Path = &self.crate_dir();
  3474. let custom_html_file = crate_root.join("index.html");
  3475. let default_html = match self.release {
  3476. true => PROD_DEFAULT_HTML,
  3477. false => DEV_DEFAULT_HTML,
  3478. };
  3479. std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(default_html))
  3480. };
  3481. // Inject any resources from the config into the html
  3482. self.inject_resources(assets, wasm_path, &mut html)?;
  3483. // Inject loading scripts if they are not already present
  3484. self.inject_loading_scripts(&mut html);
  3485. // Replace any special placeholders in the HTML with resolved values
  3486. self.replace_template_placeholders(&mut html, wasm_path, js_path);
  3487. let title = self.config.web.app.title.clone();
  3488. Self::replace_or_insert_before("{app_title}", "</title", &title, &mut html);
  3489. Ok(html)
  3490. }
  3491. fn is_dev_build(&self) -> bool {
  3492. !self.release
  3493. }
  3494. // Inject any resources from the config into the html
  3495. fn inject_resources(
  3496. &self,
  3497. assets: &AssetManifest,
  3498. wasm_path: &str,
  3499. html: &mut String,
  3500. ) -> Result<()> {
  3501. use std::fmt::Write;
  3502. // Collect all resources into a list of styles and scripts
  3503. let resources = &self.config.web.resource;
  3504. let mut style_list = resources.style.clone().unwrap_or_default();
  3505. let mut script_list = resources.script.clone().unwrap_or_default();
  3506. if self.is_dev_build() {
  3507. style_list.extend(resources.dev.style.iter().cloned());
  3508. script_list.extend(resources.dev.script.iter().cloned());
  3509. }
  3510. let mut head_resources = String::new();
  3511. // Add all styles to the head
  3512. for style in &style_list {
  3513. writeln!(
  3514. &mut head_resources,
  3515. "<link rel=\"stylesheet\" href=\"{}\">",
  3516. &style.to_str().unwrap(),
  3517. )?;
  3518. }
  3519. // Add all scripts to the head
  3520. for script in &script_list {
  3521. writeln!(
  3522. &mut head_resources,
  3523. "<script src=\"{}\"></script>",
  3524. &script.to_str().unwrap(),
  3525. )?;
  3526. }
  3527. // Add the base path to the head if this is a debug build
  3528. if self.is_dev_build() {
  3529. if let Some(base_path) = &self.base_path() {
  3530. head_resources.push_str(&format_base_path_meta_element(base_path));
  3531. }
  3532. }
  3533. // Inject any resources from manganis into the head
  3534. for asset in assets.assets() {
  3535. let asset_path = asset.bundled_path();
  3536. match asset.options() {
  3537. AssetOptions::Css(css_options) => {
  3538. if css_options.preloaded() {
  3539. head_resources.push_str(&format!(
  3540. "<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  3541. ))
  3542. }
  3543. }
  3544. AssetOptions::Image(image_options) => {
  3545. if image_options.preloaded() {
  3546. head_resources.push_str(&format!(
  3547. "<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  3548. ))
  3549. }
  3550. }
  3551. AssetOptions::Js(js_options) => {
  3552. if js_options.preloaded() {
  3553. head_resources.push_str(&format!(
  3554. "<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
  3555. ))
  3556. }
  3557. }
  3558. _ => {}
  3559. }
  3560. }
  3561. // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
  3562. head_resources.push_str(&format!(
  3563. "<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
  3564. ));
  3565. Self::replace_or_insert_before("{style_include}", "</head", &head_resources, html);
  3566. Ok(())
  3567. }
  3568. /// Inject loading scripts if they are not already present
  3569. fn inject_loading_scripts(&self, html: &mut String) {
  3570. // If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
  3571. if !self.inject_loading_scripts || html.contains("__wbindgen_start") {
  3572. return;
  3573. }
  3574. // If not, insert the script
  3575. *html = html.replace(
  3576. "</body",
  3577. r#" <script>
  3578. // We can't use a module script here because we need to start the script immediately when streaming
  3579. import("/{base_path}/{js_path}").then(
  3580. ({ default: init, initSync, __wbg_get_imports }) => {
  3581. // export initSync in case a split module needs to initialize
  3582. window.__wasm_split_main_initSync = initSync;
  3583. // Actually perform the load
  3584. init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => {
  3585. // assign this module to be accessible globally
  3586. window.__dx_mainWasm = wasm;
  3587. window.__dx_mainInit = init;
  3588. window.__dx_mainInitSync = initSync;
  3589. window.__dx___wbg_get_imports = __wbg_get_imports;
  3590. if (wasm.__wbindgen_start == undefined) {
  3591. wasm.main();
  3592. }
  3593. });
  3594. }
  3595. );
  3596. </script>
  3597. </body"#,
  3598. );
  3599. }
  3600. /// Replace any special placeholders in the HTML with resolved values
  3601. fn replace_template_placeholders(&self, html: &mut String, wasm_path: &str, js_path: &str) {
  3602. let base_path = self.base_path_or_default();
  3603. *html = html.replace("{base_path}", base_path);
  3604. let app_name = &self.executable_name();
  3605. // If the html contains the old `{app_name}` placeholder, replace {app_name}_bg.wasm and {app_name}.js
  3606. // with the new paths
  3607. *html = html.replace("wasm/{app_name}_bg.wasm", wasm_path);
  3608. *html = html.replace("wasm/{app_name}.js", js_path);
  3609. // Otherwise replace the new placeholders
  3610. *html = html.replace("{wasm_path}", wasm_path);
  3611. *html = html.replace("{js_path}", js_path);
  3612. // Replace the app_name if we find it anywhere standalone
  3613. *html = html.replace("{app_name}", app_name);
  3614. }
  3615. /// Replace a string or insert the new contents before a marker
  3616. fn replace_or_insert_before(
  3617. replace: &str,
  3618. or_insert_before: &str,
  3619. with: &str,
  3620. content: &mut String,
  3621. ) {
  3622. if content.contains(replace) {
  3623. *content = content.replace(replace, with);
  3624. } else if let Some(pos) = content.find(or_insert_before) {
  3625. content.insert_str(pos, with);
  3626. }
  3627. }
  3628. /// Get the base path from the config or None if this is not a web or server build
  3629. pub(crate) fn base_path(&self) -> Option<&str> {
  3630. self.base_path
  3631. .as_deref()
  3632. .or(self.config.web.app.base_path.as_deref())
  3633. .filter(|_| matches!(self.platform, Platform::Web | Platform::Server))
  3634. }
  3635. /// Get the normalized base path for the application with `/` trimmed from both ends. If the base path is not set, this will return `.`.
  3636. pub(crate) fn base_path_or_default(&self) -> &str {
  3637. let trimmed_path = self.base_path().unwrap_or_default().trim_matches('/');
  3638. if trimmed_path.is_empty() {
  3639. "."
  3640. } else {
  3641. trimmed_path
  3642. }
  3643. }
  3644. /// Get the path to the package manifest directory
  3645. pub(crate) fn package_manifest_dir(&self) -> PathBuf {
  3646. self.workspace.krates[self.crate_package]
  3647. .manifest_path
  3648. .parent()
  3649. .unwrap()
  3650. .to_path_buf()
  3651. .into()
  3652. }
  3653. pub(crate) async fn start_simulators(&self) -> Result<()> {
  3654. if self.device {
  3655. return Ok(());
  3656. }
  3657. match self.platform {
  3658. // Boot an iOS simulator if one is not already running.
  3659. //
  3660. // We always choose the most recently opened simulator based on the xcrun list.
  3661. // Note that simulators can be running but the simulator app itself is not open.
  3662. // Calling `open::that` is always fine, even on running apps, since apps are singletons.
  3663. Platform::Ios => {
  3664. #[derive(Deserialize, Debug)]
  3665. struct XcrunListJson {
  3666. // "com.apple.CoreSimulator.SimRuntime.iOS-18-4": [{}, {}, {}]
  3667. devices: BTreeMap<String, Vec<XcrunDevice>>,
  3668. }
  3669. #[derive(Deserialize, Debug)]
  3670. struct XcrunDevice {
  3671. #[serde(rename = "lastBootedAt")]
  3672. last_booted_at: Option<String>,
  3673. udid: String,
  3674. name: String,
  3675. state: String,
  3676. }
  3677. let xcrun_list = Command::new("xcrun")
  3678. .arg("simctl")
  3679. .arg("list")
  3680. .arg("-j")
  3681. .output()
  3682. .await?;
  3683. let as_str = String::from_utf8_lossy(&xcrun_list.stdout);
  3684. let xcrun_list_json = serde_json::from_str::<XcrunListJson>(as_str.trim());
  3685. if let Ok(xcrun_list_json) = xcrun_list_json {
  3686. if xcrun_list_json.devices.is_empty() {
  3687. tracing::warn!(
  3688. "No iOS sdks installed found. Please install the iOS SDK in Xcode."
  3689. );
  3690. }
  3691. if let Some((_rt, devices)) = xcrun_list_json.devices.iter().next() {
  3692. if devices.iter().all(|device| device.state != "Booted") {
  3693. let last_booted =
  3694. devices
  3695. .iter()
  3696. .max_by_key(|device| match device.last_booted_at {
  3697. Some(ref last_booted) => last_booted,
  3698. None => "2000-01-01T01:01:01Z",
  3699. });
  3700. if let Some(device) = last_booted {
  3701. tracing::info!("Booting iOS simulator: \"{}\"", device.name);
  3702. Command::new("xcrun")
  3703. .arg("simctl")
  3704. .arg("boot")
  3705. .arg(&device.udid)
  3706. .output()
  3707. .await?;
  3708. }
  3709. }
  3710. }
  3711. }
  3712. let path_to_xcode = Command::new("xcode-select")
  3713. .arg("--print-path")
  3714. .output()
  3715. .await?;
  3716. let path_to_xcode: PathBuf = String::from_utf8_lossy(&path_to_xcode.stdout)
  3717. .as_ref()
  3718. .trim()
  3719. .into();
  3720. let path_to_sim = path_to_xcode.join("Applications").join("Simulator.app");
  3721. open::that_detached(path_to_sim)?;
  3722. }
  3723. Platform::Android => {
  3724. let tools = self.workspace.android_tools()?;
  3725. tokio::spawn(async move {
  3726. let emulator = tools.emulator();
  3727. let avds = Command::new(&emulator)
  3728. .arg("-list-avds")
  3729. .output()
  3730. .await
  3731. .unwrap();
  3732. let avds = String::from_utf8_lossy(&avds.stdout);
  3733. let avd = avds.trim().lines().next().map(|s| s.trim().to_string());
  3734. if let Some(avd) = avd {
  3735. tracing::info!("Booting Android emulator: \"{avd}\"");
  3736. Command::new(&emulator)
  3737. .arg("-avd")
  3738. .arg(avd)
  3739. .args(["-netdelay", "none", "-netspeed", "full"])
  3740. .stdout(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
  3741. .stderr(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
  3742. .output()
  3743. .await
  3744. .unwrap();
  3745. } else {
  3746. tracing::warn!("No Android emulators found. Please create one using `emulator -avd <name>`");
  3747. }
  3748. });
  3749. }
  3750. _ => {
  3751. // nothing - maybe on the web we should open the browser?
  3752. }
  3753. };
  3754. Ok(())
  3755. }
  3756. fn select_ranlib(&self) -> Option<PathBuf> {
  3757. // prefer the modern llvm-ranlib if they have it
  3758. which::which("llvm-ranlib")
  3759. .or_else(|_| which::which("ranlib"))
  3760. .ok()
  3761. }
  3762. /// Assemble a series of `--config key=value` arguments for the build command.
  3763. ///
  3764. /// This adds adhoc profiles that dx uses to isolate builds from each other. Normally if you ran
  3765. /// `cargo build --feature desktop` and `cargo build --feature server`, then both binaries get
  3766. /// the same name and overwrite each other, causing thrashing and locking issues.
  3767. ///
  3768. /// By creating adhoc profiles, we can ensure that each build is isolated and doesn't interfere with each other.
  3769. ///
  3770. /// The user can also define custom profiles in their `Cargo.toml` file, which will be used instead
  3771. /// of the adhoc profiles.
  3772. ///
  3773. /// The names of the profiles are:
  3774. /// - web-dev
  3775. /// - web-release
  3776. /// - desktop-dev
  3777. /// - desktop-release
  3778. /// - server-dev
  3779. /// - server-release
  3780. /// - ios-dev
  3781. /// - ios-release
  3782. /// - android-dev
  3783. /// - android-release
  3784. /// - liveview-dev
  3785. /// - liveview-release
  3786. ///
  3787. /// Note how every platform gets its own profile, and each platform has a dev and release profile.
  3788. fn profile_args(&self) -> Vec<String> {
  3789. // If the user defined the profile in the Cargo.toml, we don't need to add it to our adhoc list
  3790. if self
  3791. .workspace
  3792. .cargo_toml
  3793. .profile
  3794. .custom
  3795. .contains_key(&self.profile)
  3796. {
  3797. return vec![];
  3798. }
  3799. // Otherwise, we need to add the profile arguments to make it adhoc
  3800. let mut args = Vec::new();
  3801. let profile = self.profile.as_str();
  3802. let inherits = if self.release { "release" } else { "dev" };
  3803. // Add the profile definition first.
  3804. args.push(format!(r#"profile.{profile}.inherits="{inherits}""#));
  3805. // The default dioxus experience is to lightly optimize the web build, both in debug and release
  3806. // Note that typically in release builds, you would strip debuginfo, but we actually choose to do
  3807. // that with wasm-opt tooling instead.
  3808. if matches!(self.platform, Platform::Web) {
  3809. match self.release {
  3810. true => args.push(r#"profile.web.opt-level="s""#.to_string()),
  3811. false => args.push(r#"profile.web.opt-level="1""#.to_string()),
  3812. }
  3813. }
  3814. // Prepend --config to each argument
  3815. args.into_iter()
  3816. .flat_map(|arg| ["--config".to_string(), arg])
  3817. .collect()
  3818. }
  3819. }