-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathrequest.rs
More file actions
5500 lines (4923 loc) · 226 KB
/
request.rs
File metadata and controls
5500 lines (4923 loc) · 226 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! # [`BuildRequest`] - the core of the build process
//!
//! The [`BuildRequest`] object is the core of the build process. It contains all the resolved arguments
//! flowing in from the CLI, dioxus.toml, env vars, and the workspace.
//!
//! Every BuildRequest is tied to a given workspace and BuildArgs. For simplicity's sake, the BuildArgs
//! struct is used to represent the CLI arguments and all other configuration is basically just
//! extra CLI arguments, but in a configuration format.
//!
//! When [`BuildRequest::build`] is called, it will prepare its work directory in the target folder
//! and then start running the build process. A [`BuildContext`] is required to customize this
//! build process, containing a channel for progress updates and the build mode.
//!
//! The [`BuildMode`] is extremely important since it influences how the build is performed. Most
//! "normal" builds just use [`BuildMode::Base`], but we also support [`BuildMode::Fat`] and
//! [`BuildMode::Thin`]. These builds are used together to power the hot-patching and fast-linking
//! engine.
//! - BuildMode::Base: A normal build generated using `cargo rustc`
//! - BuildMode::Fat: A "fat" build where all dependency rlibs are merged into a static library
//! - BuildMode::Thin: A "thin" build that dynamically links against the artifacts produced by the "fat" build
//!
//! The BuildRequest is also responsible for writing the final build artifacts to disk. This includes
//!
//! - Writing the executable
//! - Processing assets from the artifact
//! - Writing any metadata or configuration files (Info.plist, AndroidManifest.xml)
//! - Bundle splitting (for wasm) and wasm-bindgen
//!
//! In some cases, the BuildRequest also handles the linking of the final executable. Specifically,
//! - For Android, we use `dx` as an opaque linker to dynamically find the true android linker
//! - For hotpatching, the CLI manually links the final executable with a stub file
//!
//! ## Build formats:
//!
//! We support building for the most popular platforms:
//! - Web via wasm-bindgen
//! - macOS via app-bundle
//! - iOS via app-bundle
//! - Android via gradle
//! - Linux via app-image
//! - Windows via exe, msi/msix
//!
//! Note that we are missing some setups that we *should* support:
//! - PWAs, WebWorkers, ServiceWorkers
//! - Web Extensions
//! - Linux via flatpak/snap
//!
//! There are some less popular formats that we might want to support eventually:
//! - TVOS, watchOS
//! - OpenHarmony
//!
//! Also, some deploy platforms have their own bespoke formats:
//! - Cloudflare workers
//! - AWS Lambda
//!
//! Currently, we defer most of our deploy-based bundling to Tauri bundle, though we should migrate
//! to just bundling everything ourselves. This would require us to implement code-signing which
//! is a bit of a pain, but fortunately a solved process (<https://github.com/rust-mobile/xbuild>).
//!
//! ## Build Structure
//!
//! Builds generally follow the same structure everywhere:
//! - A main executable
//! - Sidecars (alternate entrypoints, framewrok plugins, etc)
//! - Assets (images, fonts, etc)
//! - Metadata (Info.plist, AndroidManifest.xml)
//! - Glue code (java, kotlin, javascript etc)
//! - Entitlements for code-signing and verification
//!
//! We need to be careful to not try and put a "round peg in a square hole," but most platforms follow
//! the same pattern.
//!
//! As such, we try to assemble a build directory that's somewhat sensible:
//! - A main "staging" dir for a given app
//! - Per-profile dirs (debug/release)
//! - A platform dir (ie web/desktop/android/ios)
//! - The "bundle" dir which is basically the `.app` format or `wwww` dir.
//! - The "executable" dir where the main exe is housed
//! - The "assets" dir where the assets are housed
//! - The "meta" dir where stuff like Info.plist, AndroidManifest.xml, etc are housed
//!
//! There's also some "quirky" folders that need to be stable between builds but don't influence the
//! bundle itself:
//! - session_cache_dir which stores stuff like window position
//!
//! ### Web:
//!
//! Create a folder that is somewhat similar to an app-image (exe + asset)
//! The server is dropped into the `web` folder, even if there's no `public` folder.
//! If there's no server (SPA), we still use the `web` folder, but it only contains the
//! public folder.
//!
//! ```
//! web/
//! server
//! assets/
//! public/
//! index.html
//! wasm/
//! app.wasm
//! glue.js
//! snippets/
//! ...
//! assets/
//! logo.png
//! ```
//!
//! ### Linux:
//!
//! <https://docs.appimage.org/reference/appdir.html#ref-appdir>
//! current_exe.join("Assets")
//! ```
//! app.appimage/
//! AppRun
//! app.desktop
//! package.json
//! assets/
//! logo.png
//! ```
//!
//! ### Macos
//!
//! We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources`
//! We put assets in an assets dir such that it generally matches every other platform and we can
//! output `/assets/blah` from manganis.
//! ```
//! App.app/
//! Contents/
//! Info.plist
//! MacOS/
//! Frameworks/
//! Resources/
//! assets/
//! blah.icns
//! blah.png
//! CodeResources
//! _CodeSignature/
//! ```
//!
//! ### iOS
//!
//! Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably
//! since most ios apps don't ship frameworks/plugins and such.
//!
//! todo(jon): include the signing and entitlements in this format diagram.
//! ```
//! App.app/
//! main
//! assets/
//! ```
//!
//! ### Android:
//!
//! Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since
//! we need to compile kotlin and java. This pushes us into using gradle and following a structure
//! similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and
//! drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires
//! us to manage dependencies (like kotlinc) ourselves (yuck!).
//!
//! <https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh>
//!
//! Unfortunately, it seems that while we can drop the `android` build plugin, we still will need
//! gradle since kotlin is basically gradle-only.
//!
//! Pre-build:
//! ```
//! app.apk/
//! .gradle
//! app/
//! src/
//! main/
//! assets/
//! jniLibs/
//! java/
//! kotlin/
//! res/
//! AndroidManifest.xml
//! build.gradle.kts
//! proguard-rules.pro
//! buildSrc/
//! build.gradle.kts
//! src/
//! main/
//! kotlin/
//! BuildTask.kt
//! build.gradle.kts
//! gradle.properties
//! gradlew
//! gradlew.bat
//! settings.gradle
//! ```
//!
//! Final build:
//! ```
//! app.apk/
//! AndroidManifest.xml
//! classes.dex
//! assets/
//! logo.png
//! lib/
//! armeabi-v7a/
//! libmyapp.so
//! arm64-v8a/
//! libmyapp.so
//! x86/
//! libmyapp.so
//! x86_64/
//! libmyapp.so
//! ```
//! Notice that we *could* feasibly build this ourselves :)
//!
//! ### Windows:
//! <https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows>
//! Windows does not provide an AppImage format, so instead we're going build the same folder
//! structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources
//! as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the
//! Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe
//! entrypoint in the associated Program Files folder.
//!
//! This is, in essence, the same as an installer, so we might eventually just support something like msi/msix
//! which functionally do the same thing but with a sleeker UI.
//!
//! This means no installers are required and we can bake an updater into the host exe.
//!
//! ## Handling asset lookups:
//! current_exe.join("assets")
//! ```
//! app.appimage/
//! main.exe
//! main.desktop
//! package.json
//! assets/
//! logo.png
//! ```
//!
//! Since we support just a few locations, we could just search for the first that exists
//! - usr
//! - ../Resources
//! - assets
//! - Assets
//! - $cwd/assets
//!
//! ```
//! assets::root() ->
//! mac -> ../Resources/
//! ios -> ../Resources/
//! android -> assets/
//! server -> assets/
//! liveview -> assets/
//! web -> /assets/
//! root().join(bundled)
//! ```
//!
//! Every dioxus app can have an optional server executable which will influence the final bundle.
//! This is built in parallel with the app executable during the `build` phase and the progres/status
//! of the build is aggregated.
//!
//! The server will *always* be dropped into the `web` folder since it is considered "web" in nature,
//! and will likely need to be combined with the public dir to be useful.
//!
//! We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform
//! can just use the build dir
//!
//! When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name:
//! ```
//! dog-app/
//! build/
//! web/
//! server.exe
//! assets/
//! some-secret-asset.txt (a server-side asset)
//! public/
//! index.html
//! assets/
//! logo.png
//! desktop/
//! App.app
//! App.appimage
//! App.exe
//! server/
//! server
//! assets/
//! some-secret-asset.txt (a server-side asset)
//! ios/
//! App.app
//! App.ipa
//! android/
//! App.apk
//! bundle/
//! build.json
//! Desktop.app
//! Mobile_x64.ipa
//! Mobile_arm64.ipa
//! Mobile_rosetta.ipa
//! web.appimage
//! web/
//! server.exe
//! assets/
//! some-secret-asset.txt
//! public/
//! index.html
//! assets/
//! logo.png
//! style.css
//! ```
//!
//! When deploying, the build.json file will provide all the metadata that dx-deploy will use to
//! push the app to stores, set up infra, manage versions, etc.
//!
//! The format of each build will follow the name plus some metadata such that when distributing you
//! can easily trim off the metadata.
//!
//! The idea here is that we can run any of the programs in the same way that they're deployed.
//!
//! ## Bundle structure links
//! - apple: <https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle>
//! - appimage: <https://docs.appimage.org/packaging-guide/manual.html#ref-manual>
//!
//! ## Extra links
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
use crate::{
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
LinkAction, LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc,
WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::{bail, Context};
use cargo_metadata::diagnostic::Diagnostic;
use cargo_toml::{Profile, Profiles, StripSetting};
use depinfo::RustcDepInfo;
use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV};
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
use dioxus_cli_opt::{process_file_to, AssetManifest};
use itertools::Itertools;
use krates::{cm::TargetKind, NodeId};
use manganis::{AssetOptions, BundledAsset};
use manganis_core::{AssetOptionsBuilder, AssetVariant};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, ffi::OsString};
use std::{
collections::{BTreeMap, HashSet},
io::Write,
path::{Path, PathBuf},
process::Stdio,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::{SystemTime, UNIX_EPOCH},
};
use subsecond_types::JumpTable;
use target_lexicon::{Architecture, OperatingSystem, Triple};
use tempfile::TempDir;
use tokio::{io::AsyncBufReadExt, process::Command};
use uuid::Uuid;
use super::HotpatchModuleCache;
/// This struct is used to plan the build process.
///
/// The point here is to be able to take in the user's config from the CLI without modifying the
/// arguments in place. Creating a buildplan "resolves" their config into a build plan that can be
/// introspected. For example, the users might not specify a "Triple" in the CLI but the triple will
/// be guaranteed to be resolved here.
///
/// Creating a buildplan also lets us introspect build requests and modularize our build process.
/// This will, however, lead to duplicate fields between the CLI and the build engine. This is fine
/// since we have the freedom to evolve the schema internally without breaking the API.
///
/// All updates from the build will be sent on a global "BuildProgress" channel.
#[derive(Clone)]
pub(crate) struct BuildRequest {
pub(crate) workspace: Arc<Workspace>,
pub(crate) config: DioxusConfig,
pub(crate) crate_package: NodeId,
pub(crate) crate_target: krates::cm::Target,
pub(crate) profile: String,
pub(crate) release: bool,
pub(crate) bundle: BundleFormat,
pub(crate) triple: Triple,
pub(crate) device_name: Option<String>,
pub(crate) should_codesign: bool,
pub(crate) package: String,
pub(crate) main_target: String,
pub(crate) features: Vec<String>,
pub(crate) rustflags: cargo_config2::Flags,
pub(crate) extra_cargo_args: Vec<String>,
pub(crate) extra_rustc_args: Vec<String>,
pub(crate) no_default_features: bool,
pub(crate) all_features: bool,
pub(crate) target_dir: PathBuf,
pub(crate) skip_assets: bool,
pub(crate) wasm_split: bool,
pub(crate) debug_symbols: bool,
pub(crate) inject_loading_scripts: bool,
pub(crate) custom_linker: Option<PathBuf>,
pub(crate) base_path: Option<String>,
pub(crate) using_dioxus_explicitly: bool,
pub(crate) apple_entitlements: Option<PathBuf>,
pub(crate) apple_team_id: Option<String>,
pub(crate) session_cache_dir: PathBuf,
pub(crate) raw_json_diagnostics: bool,
pub(crate) windows_subsystem: Option<String>,
}
/// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin
/// modes are used together to achieve binary patching and linking.
///
/// Guide:
/// ----------
/// - Base: A normal build generated using `cargo rustc`, intended for production use cases
///
/// - Fat: A "fat" build with -Wl,-all_load and no_dead_strip, keeping *every* symbol in the binary.
/// Intended for development for larger up-front builds with faster link times and the ability
/// to binary patch the final binary. On WASM, this also forces wasm-bindgen to generate all
/// JS-WASM bindings, saving us the need to re-wasmbindgen the final binary.
///
/// - Thin: A "thin" build that dynamically links against the dependencies produced by the "fat" build.
/// This is generated by calling rustc *directly* and might be more fragile to construct, but
/// generates *much* faster than a regular base or fat build.
#[derive(Clone, Debug, PartialEq)]
pub enum BuildMode {
/// A normal build generated using `cargo rustc`
///
/// "run" indicates whether this build is intended to be run immediately after building.
/// This means we try to capture the build environment, saving vars like `CARGO_MANIFEST_DIR`
/// for the running executable.
Base { run: bool },
/// A "Fat" build generated with cargo rustc and dx as a custom linker without -Wl,-dead-strip
Fat,
/// A "thin" build generated with `rustc` directly and dx as a custom linker
Thin {
rustc_args: RustcArgs,
changed_files: Vec<PathBuf>,
aslr_reference: u64,
cache: Arc<HotpatchModuleCache>,
},
}
/// The end result of a build.
///
/// Contains the final asset manifest, the executable, and metadata about the build.
/// Note that the `exe` might be stale and/or overwritten by the time you read it!
///
/// The patch cache is only populated on fat builds and then used for thin builds (see `BuildMode::Thin`).
#[derive(Clone, Debug)]
pub struct BuildArtifacts {
pub(crate) root_dir: PathBuf,
pub(crate) exe: PathBuf,
pub(crate) direct_rustc: RustcArgs,
pub(crate) time_start: SystemTime,
pub(crate) time_end: SystemTime,
pub(crate) assets: AssetManifest,
pub(crate) mode: BuildMode,
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
pub(crate) depinfo: RustcDepInfo,
pub(crate) build_id: BuildId,
}
impl BuildRequest {
/// Create a new build request.
///
/// This method consolidates various inputs into a single source of truth. It combines:
/// - Command-line arguments provided by the user.
/// - The crate's `Cargo.toml`.
/// - The `dioxus.toml` configuration file.
/// - User-specific CLI settings.
/// - The workspace metadata.
/// - Host-specific details (e.g., Android tools, installed frameworks).
/// - The intended target platform.
///
/// Fields may be duplicated from the inputs to allow for autodetection and resolution.
///
/// Autodetection is performed for unspecified fields where possible.
///
/// Note: Build requests are typically created only when the CLI is invoked or when significant
/// changes are detected in the `Cargo.toml` (e.g., features added or removed).
pub(crate) async fn new(args: &TargetArgs, workspace: Arc<Workspace>) -> Result<Self> {
let crate_package = workspace.find_main_package(args.package.clone())?;
let config = workspace
.load_dioxus_config(crate_package)?
.unwrap_or_default();
let target_kind = match args.example.is_some() {
true => TargetKind::Example,
false => TargetKind::Bin,
};
let main_package = &workspace.krates[crate_package];
let target_name = args
.example
.clone()
.or(args.bin.clone())
.or_else(|| {
if let Some(default_run) = &main_package.default_run {
return Some(default_run.to_string());
}
let bin_count = main_package
.targets
.iter()
.filter(|x| x.kind.contains(&target_kind))
.count();
if bin_count != 1 {
return None;
}
main_package.targets.iter().find_map(|x| {
if x.kind.contains(&target_kind) {
Some(x.name.clone())
} else {
None
}
})
})
.unwrap_or(workspace.krates[crate_package].name.clone());
// Use the main_target for the client + server build if it is set, otherwise use the target name for this
// specific build. This is important for @client @server syntax so we use the client's output directory for the bundle.
let main_target = args.client_target.clone().unwrap_or(target_name.clone());
let crate_target = main_package
.targets
.iter()
.find(|target| {
target_name == target.name.as_str() && target.kind.contains(&target_kind)
})
.with_context(|| {
let target_of_kind = |kind|-> String {
let filtered_packages = main_package
.targets
.iter()
.filter_map(|target| {
target.kind.contains(kind).then_some(target.name.as_str())
}).collect::<Vec<_>>();
filtered_packages.join(", ")};
if let Some(example) = &args.example {
let examples = target_of_kind(&TargetKind::Example);
format!("Failed to find example {example}. \nAvailable examples are:\n{examples}")
} else if let Some(bin) = &args.bin {
let binaries = target_of_kind(&TargetKind::Bin);
format!("Failed to find binary {bin}. \nAvailable binaries are:\n{binaries}")
} else {
format!("Failed to find target {target_name}. \nIt looks like you are trying to build dioxus in a library crate. \
You either need to run dx from inside a binary crate or build a specific example with the `--example` flag. \
Available examples are:\n{}", target_of_kind(&TargetKind::Example))
}
})?
.clone();
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
// For now, though, since we don't have probing, it just defaults to false
// Tools like xcrun/adb can detect devices
let device = args.device.clone();
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
/*
Determine which features, triple, profile, etc to pass to the build.
Most of the time, users should use `dx serve --<platform>` where the platform name directly
corresponds to the feature in their cargo.toml. So,
- `dx serve --web` will enable the `web` feature
- `dx serve --mobile` will enable the `mobile` feature
- `dx serve --desktop` will enable the `desktop` feature
In this case, we set default-features to false and then add back the default features that
aren't renderers, and then add the feature for the given renderer (ie web/desktop/mobile).
We call this "no-default-features-stripped."
There are a few cases where the user doesn't need to pass a platform.
- they selected one via `dioxus = { features = ["web"] }`
- they have a single platform in their default features `default = ["web"]`
- there is only a single non-server renderer as a feature `web = ["dioxus/web"], server = ["dioxus/server"]`
- they compose the super triple via triple + bundleformat + features
Note that we only use the names of the features to correspond with the platform.
Platforms are "super triples", meaning they contain information about
- bundle format
- target triple
- how to serve
- enabled features
By default, the --platform presets correspond to:
- web: bundle(web), triple(wasm32), serve(http-serve), features("web")
- desktop: alias to mac/win/linux
- mac: bundle(mac), triple(host), serve(appbundle-open), features("desktop")
- windows: bundle(exefolder), triple(host), serve(run-exe), features("desktop")
- linux: bundle(appimage), triple(host), serve(run-exe), features("desktop")
- ios: bundle(ios), triple(arm64-apple-ios), serve(ios-simulator/xcrun), features("mobile")
- android: bundle(android), triple(arm64-apple-ios), serve(android-emulator/adb), features("mobile")
- server: bundle(server), triple(host), serve(run-exe), features("server") (and disables the client)
- liveview: bundle(liveview), triple(host), serve(run-exe), features("liveview")
- unknown: <auto or default to desktop>
Fullstack usage is inferred from the presence of the fullstack feature or --fullstack.
*/
let mut features = args.features.clone();
let no_default_features = args.no_default_features;
let all_features = args.all_features;
let mut triple = args.target.clone();
let mut renderer = args.renderer;
let mut bundle_format = args.bundle;
let mut platform = args.platform;
// the crate might be selecting renderers but the user also passes a renderer. this is weird
// ie dioxus = { features = ["web"] } but also --platform desktop
// anyways, we collect it here in the event we need it if platform is not specified.
let dioxus_direct_renderer = Self::renderer_enabled_by_dioxus_dependency(main_package);
let known_features_as_renderers = Self::features_that_enable_renderers(main_package);
// The crate might enable multiple platforms or no platforms at
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_renderers = if no_default_features {
vec![]
} else {
Self::enabled_cargo_toml_default_features_renderers(main_package)
};
// Try the easy autodetects.
// - if the user has `dioxus = { features = ["web"] }`
// - if the `default =["web"]` or `default = ["dioxus/web"]`
// - if there's only one non-server platform ie `web = ["dioxus/web"], server = ["dioxus/server"]`
// Only do this if we're explicitly using dioxus
if matches!(platform, Platform::Unknown) && using_dioxus_explicitly {
let auto = dioxus_direct_renderer
.or_else(|| {
if enabled_renderers.len() == 1 {
Some(enabled_renderers[0].clone())
} else {
None
}
})
.or_else(|| {
// If multiple renderers are enabled, pick the first non-server one
if enabled_renderers.len() == 2
&& enabled_renderers
.iter()
.any(|f| matches!(f.0, Renderer::Server))
{
return Some(
enabled_renderers
.iter()
.find(|f| !matches!(f.0, Renderer::Server))
.cloned()
.unwrap(),
);
}
None
})
.or_else(|| {
// Pick the first non-server feature in the cargo.toml
let non_server_features = known_features_as_renderers
.iter()
.filter(|f| f.1.as_str() != "server")
.collect::<Vec<_>>();
if non_server_features.len() == 1 {
Some(non_server_features[0].clone())
} else {
None
}
});
if let Some((direct, feature)) = auto {
match direct {
_ if feature == "mobile" || feature == "dioxus/mobile" => {
bail!(
"Could not autodetect mobile platform. Use --ios or --android instead."
);
}
Renderer::Webview | Renderer::Native => {
if cfg!(target_os = "macos") {
platform = Platform::MacOS;
} else if cfg!(target_os = "linux") {
platform = Platform::Linux;
} else if cfg!(target_os = "windows") {
platform = Platform::Windows;
}
}
Renderer::Server => platform = Platform::Server,
Renderer::Liveview => platform = Platform::Liveview,
Renderer::Web => platform = Platform::Web,
}
renderer = renderer.or(Some(direct));
}
}
// Set the super triple from the platform if it's provided.
// Otherwise, we attempt to guess it from the rest of their inputs.
match platform {
Platform::Unknown => {}
Platform::Web => {
if main_package.features.contains_key("web") && renderer.is_none() {
features.push("web".into());
}
renderer = renderer.or(Some(Renderer::Web));
bundle_format = bundle_format.or(Some(BundleFormat::Web));
triple = triple.or(Some("wasm32-unknown-unknown".parse()?));
}
Platform::MacOS => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::MacOS));
triple = triple.or(Some(Triple::host()));
}
Platform::Windows => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Windows));
triple = triple.or(Some(Triple::host()));
}
Platform::Linux => {
if main_package.features.contains_key("desktop") && renderer.is_none() {
features.push("desktop".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Linux));
triple = triple.or(Some(Triple::host()));
}
Platform::Ios => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Ios));
match device.is_some() {
// If targeting device, we want to build for the device which is always aarch64
true => triple = triple.or(Some("aarch64-apple-ios".parse()?)),
// If the host is aarch64, we assume the user wants to build for iOS simulator
false if matches!(Triple::host().architecture, Architecture::Aarch64(_)) => {
triple = triple.or(Some("aarch64-apple-ios-sim".parse()?))
}
// Otherwise, it's the x86_64 simulator, which is just x86_64-apple-ios
_ => triple = triple.or(Some("x86_64-apple-ios".parse()?)),
}
}
Platform::Android => {
if main_package.features.contains_key("mobile") && renderer.is_none() {
features.push("mobile".into());
}
renderer = renderer.or(Some(Renderer::Webview));
bundle_format = bundle_format.or(Some(BundleFormat::Android));
// maybe probe adb?
if let Some(_device_name) = device.as_ref() {
if triple.is_none() {
triple = Some(
crate::get_android_tools()
.context("Failed to get android tools")?
.autodetect_android_device_triple()
.await,
);
}
} else {
triple = triple.or(Some({
match Triple::host().architecture {
Architecture::X86_32(_) => "i686-linux-android".parse()?,
Architecture::X86_64 => "x86_64-linux-android".parse()?,
Architecture::Aarch64(_) => "aarch64-linux-android".parse()?,
_ => "aarch64-linux-android".parse()?,
}
}));
}
}
Platform::Server => {
if main_package.features.contains_key("server") && renderer.is_none() {
features.push("server".into());
}
renderer = renderer.or(Some(Renderer::Server));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
}
Platform::Liveview => {
if main_package.features.contains_key("liveview") && renderer.is_none() {
features.push("liveview".into());
}
renderer = renderer.or(Some(Renderer::Liveview));
bundle_format = bundle_format.or(Some(BundleFormat::Server));
triple = triple.or(Some(Triple::host()));
}
}
// If default features are enabled, we need to add the default features
// which don't enable a renderer
if !no_default_features {
features.extend(Self::rendererless_features(main_package));
features.dedup();
features.sort();
}
// The triple will be the triple passed or the host if using dioxus.
let triple = if using_dioxus_explicitly {
triple.context("Could not automatically detect target triple")?
} else {
triple.unwrap_or(Triple::host())
};
// The bundle format will be the bundle format passed or the host.
let bundle = if using_dioxus_explicitly {
bundle_format.context("Could not automatically detect bundle format")?
} else {
bundle_format.unwrap_or(BundleFormat::host())
};
// Add any features required to turn on the client
if let Some(renderer) = renderer {
if let Some(feature) =
Self::feature_for_platform_and_renderer(main_package, &triple, renderer)
{
features.push(feature);
features.dedup();
}
}
// Set the profile of the build if it's not already set
// This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles
// We might want to move some of these profiles into dioxus.toml and make them "virtual".
let profile = match args.profile.clone() {
Some(profile) => profile,
None => bundle.profile_name(args.release),
};
// Determine if we should codesign
let should_codesign =
args.codesign || device.is_some() || args.apple_entitlements.is_some();
// Determining release mode is based on the profile, actually, so we need to check that
let release = workspace.is_release_profile(&profile);
// Determine the --package we'll pass to cargo.
// todo: I think this might be wrong - we don't want to use main_package necessarily...
let package = args
.package
.clone()
.unwrap_or_else(|| main_package.name.clone());
// Somethings we override are also present in the user's config.
// If we can't get them by introspecting cargo, then we need to get them from the config
//
// This involves specifically two fields:
// - The linker since we override it for Android and hotpatching
// - RUSTFLAGS since we also override it for Android and hotpatching
let cargo_config = cargo_config2::Config::load().unwrap();
let mut custom_linker = cargo_config.linker(triple.to_string()).ok().flatten();
let mut rustflags = cargo_config2::Flags::default();
// Remove "rust-lld" as a custom linker on Windows, since that is already the linker we
// default to.
if let Some(linker) = custom_linker.as_ref() {
if (linker == "rust-lld" || linker == "rust-lld.exe") && cfg!(windows) {
// When using "rust-lld.exe" as linker on windows, it still needs to have a flavor
// given to it. rustc appears to be passing `-flavor "link"` when none is set by the
// user. If no flavor is given, it fails with 'lld is a generic driver'.
// We already use the existing lld-link by default on windows, so we can simply set the
// `custom_linker` to `None` in these cases, since we end up using "lld-link" anyway
// which is the same as "rust-lld.exe -flavor link".
custom_linker = None;
}
}
// Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
for env in [
"RUSTFLAGS".to_string(),
format!("CARGO_TARGET_{triple}_RUSTFLAGS"),
] {
if let Ok(flags) = std::env::var(env) {
rustflags
.flags
.extend(cargo_config2::Flags::from_space_separated(&flags).flags);
}
}
// Use the user's linker if the specify it at the target level
if let Ok(target) = cargo_config.target(triple.to_string()) {
if let Some(flags) = target.rustflags {
rustflags.flags.extend(flags.flags);
}
}
// When we do android builds we need to make sure we link against the android libraries
// We also `--export-dynamic` to make sure we can do shenanigans like `dlsym` the `main` symbol
if matches!(bundle, BundleFormat::Android) {
rustflags.flags.extend([
"-Clink-arg=-landroid".to_string(),
"-Clink-arg=-llog".to_string(),
"-Clink-arg=-lOpenSLES".to_string(),
"-Clink-arg=-lc++abi".to_string(),
"-Clink-arg=-Wl,--export-dynamic".to_string(),
format!(
"-Clink-arg=-Wl,--sysroot={}",
workspace.android_tools()?.sysroot().display()
),
]);
}
// Make sure we set the sysroot for ios builds in the event the user doesn't have it set
if matches!(bundle, BundleFormat::Ios) {
let xcode_path = Workspace::get_xcode_path()
.await
.unwrap_or_else(|| "/Applications/Xcode.app".to_string().into());
let sysroot_location = match triple.environment {
target_lexicon::Environment::Sim => xcode_path
.join("Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"),
_ => xcode_path.join("Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"),
};
if sysroot_location.exists() && !rustflags.flags.iter().any(|f| f == "-isysroot") {
rustflags.flags.extend([
"-Clink-arg=-isysroot".to_string(),
format!("-Clink-arg={}", sysroot_location.display()),
]);
}
}
// automatically set the getrandom backend for web builds if the user requested it
if matches!(bundle, BundleFormat::Web) && args.wasm_js_cfg {
rustflags.flags.extend(
cargo_config2::Flags::from_space_separated(r#"--cfg getrandom_backend="wasm_js""#)
.flags,
);
}
// If no custom linker is set, then android falls back to us as the linker
if custom_linker.is_none() && bundle == BundleFormat::Android {
let min_sdk_version = config.application.android_min_sdk_version.unwrap_or(28);
custom_linker = Some(
workspace
.android_tools()?
.android_cc(&triple, min_sdk_version),
);
}
let target_dir = std::env::var("CARGO_TARGET_DIR")
.ok()
.map(PathBuf::from)
.or_else(|| cargo_config.build.target_dir.clone())
.unwrap_or_else(|| workspace.workspace_root().join("target"));
// If the user provided a profile and wasm_split is enabled, we should check that LTO=true and debug=true
if args.wasm_split {
if let Some(profile_data) = workspace.cargo_toml.profile.custom.get(&profile) {
use cargo_toml::{DebugSetting, LtoSetting};
if matches!(profile_data.lto, Some(LtoSetting::None) | None) {
tracing::warn!("wasm-split requires LTO to be enabled in the profile. \
Please set `lto = true` in the `[profile.{profile}]` section of your Cargo.toml");
}
if matches!(profile_data.debug, Some(DebugSetting::None) | None) {
tracing::warn!("wasm-split requires debug symbols to be enabled in the profile. \
Please set `debug = true` in the `[profile.{profile}]` section of your Cargo.toml");
}
}
}
#[allow(deprecated)]
let session_cache_dir = args
.session_cache_dir
.clone()
.unwrap_or_else(|| TempDir::new().unwrap().into_path());
let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default())
.context("Failed to parse rustc args")?;
let extra_cargo_args = shell_words::split(&args.cargo_args.clone().unwrap_or_default())
.context("Failed to parse cargo args")?;
tracing::debug!(
r#"Target Info:
• features: {features:?}
• triple: {triple}
• bundle format: {bundle:?}
• session cache dir: {session_cache_dir:?}
• linker: {custom_linker:?}
• target_dir: {target_dir:?}"#,
);
Ok(Self {
features,
bundle,
// We hardcode passing `--no-default-features` to Cargo because dx manually enables
// the default features we want.
no_default_features: true,
all_features,