1
0

request.rs 161 KB

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