1use crate::android;
2#[cfg(target_os = "macos")]
3use crate::apple;
4use crate::{
5 config::{
6 self,
7 metadata::{self, Metadata},
8 Config,
9 },
10 dot_cargo,
11 os::code_command,
12 project, templating,
13 util::{
14 self,
15 cli::{Report, Reportable, TextWrapper},
16 },
17};
18use std::{
19 fs, io,
20 path::{Path, PathBuf},
21};
22
23pub static DOT_FIRST_INIT_FILE_NAME: &str = ".first-init";
24static DOT_FIRST_INIT_CONTENTS: &str = r#"The presence of this file indicates `cargo mobile init` has been called for
26the first time on a new project, but hasn't yet completed successfully once. As
27long as this file is here, `cargo mobile init` will use a more aggressive
28template generation strategy that allows it to place files that it wouldn't
29normally be able to modify.
30
31If you believe this file isn't supposed to be here, please report this, and then
32delete this file to regain normal behavior.
33
34Alternatively, if you do that and then realize that you were wrong (ouch!) and
35your project was never fully generated, then you can just create `.first-init`
36again (the contents don't matter) and run `cargo mobile init`. Just, if you do
37that, any generated files you modified will be overwritten!
38"#;
39
40#[derive(Debug)]
41pub enum Error {
42 ConfigLoadOrGenFailed(config::LoadOrGenError),
43 DotFirstInitWriteFailed {
44 path: PathBuf,
45 cause: io::Error,
46 },
47 FilterConfigureFailed(templating::FilterError),
48 ProjectInitFailed(project::Error),
49 AssetDirCreationFailed {
50 asset_dir: PathBuf,
51 cause: io::Error,
52 },
53 CodeCommandPresentFailed(std::io::Error),
54 LldbExtensionInstallFailed(std::io::Error),
55 DotCargoLoadFailed(dot_cargo::LoadError),
56 HostTargetTripleDetectionFailed(util::HostTargetTripleError),
57 MetadataFailed(metadata::Error),
58 #[cfg(target_os = "macos")]
59 AppleInitFailed(apple::project::Error),
60 AndroidEnvFailed(android::env::Error),
61 AndroidInitFailed(android::project::Error),
62 DotCargoWriteFailed(dot_cargo::WriteError),
63 DotFirstInitDeleteFailed {
64 path: PathBuf,
65 cause: io::Error,
66 },
67 OpenInEditorFailed(util::OpenInEditorError),
68}
69
70impl Reportable for Error {
71 fn report(&self) -> Report {
72 match self {
73 Self::ConfigLoadOrGenFailed(err) => err.report(),
74 Self::DotFirstInitWriteFailed { path, cause } => Report::error(format!("Failed to write first init dot file {:?}", path), cause),
75 Self::FilterConfigureFailed(err) => Report::error("Failed to configure template filter", err),
76 Self::ProjectInitFailed(err) => err.report(),
77 Self::AssetDirCreationFailed { asset_dir, cause } => Report::error(format!("Failed to create asset dir {:?}", asset_dir), cause),
78 Self::CodeCommandPresentFailed(err) => Report::error("Failed to check for presence of `code` command", err),
79 Self::LldbExtensionInstallFailed(err) => Report::error("Failed to install CodeLLDB extension", err),
80 Self::DotCargoLoadFailed(err) => err.report(),
81 Self::HostTargetTripleDetectionFailed(err) => err.report(),
82 Self::MetadataFailed(err) => err.report(),
83 Self::AndroidEnvFailed(err) => err.report(),
84 Self::AndroidInitFailed(err) => err.report(),
85 #[cfg(target_os = "macos")]
86 Self::AppleInitFailed(err) => err.report(),
87 Self::DotCargoWriteFailed(err) => err.report(),
88 Self::DotFirstInitDeleteFailed { path, cause } => Report::action_request(format!("Failed to delete first init dot file {:?}; the project generated successfully, but `cargo mobile init` will have unexpected results unless you manually delete this file!", path), cause),
89 Self::OpenInEditorFailed(err) => Report::error("Failed to open project in editor (your project generated successfully though, so no worries!)", err),
90 }
91 }
92}
93
94#[allow(clippy::too_many_arguments)]
95pub fn exec(
96 wrapper: &TextWrapper,
97 non_interactive: bool,
98 skip_dev_tools: bool,
99 skip_targets_install: bool,
100 #[cfg_attr(not(target_os = "macos"), allow(unused))] reinstall_deps: bool,
101 open_in_editor: bool,
102 submodule_commit: Option<String>,
103 cwd: impl AsRef<Path>,
104) -> Result<Config, Box<Error>> {
105 let cwd = cwd.as_ref();
106 let (config, config_origin) =
107 Config::load_or_gen(cwd, non_interactive, wrapper).map_err(Error::ConfigLoadOrGenFailed)?;
108 let dot_first_init_path = config.app().root_dir().join(DOT_FIRST_INIT_FILE_NAME);
109 let dot_first_init_exists = {
110 let dot_first_init_exists = dot_first_init_path.exists();
111 if config_origin.freshly_minted() && !dot_first_init_exists {
112 log::info!("creating first init dot file at {:?}", dot_first_init_path);
115 fs::write(&dot_first_init_path, DOT_FIRST_INIT_CONTENTS).map_err(|cause| {
116 Error::DotFirstInitWriteFailed {
117 path: dot_first_init_path.clone(),
118 cause,
119 }
120 })?;
121 true
122 } else {
123 dot_first_init_exists
124 }
125 };
126 let bike = config.build_a_bike();
127 let filter = templating::Filter::new(&config, config_origin, dot_first_init_exists)
128 .map_err(Error::FilterConfigureFailed)?;
129
130 project::gen(&config, &bike, &filter, submodule_commit).map_err(Error::ProjectInitFailed)?;
132
133 let asset_dir = config.app().asset_dir();
134 if !asset_dir.is_dir() {
135 fs::create_dir_all(&asset_dir)
136 .map_err(|cause| Error::AssetDirCreationFailed { asset_dir, cause })?;
137 }
138 if !skip_dev_tools && util::command_present("code").map_err(Error::CodeCommandPresentFailed)? {
139 code_command()
140 .before_spawn(move |cmd| {
141 cmd.args(["--install-extension", "vadimcn.vscode-lldb"]);
142 if non_interactive {
143 cmd.arg("--force");
144 }
145 Ok(())
146 })
147 .run()
148 .map_err(Error::LldbExtensionInstallFailed)?;
149 }
150 let mut dot_cargo =
151 dot_cargo::DotCargo::load(config.app()).map_err(Error::DotCargoLoadFailed)?;
152
153 let metadata = Metadata::load(config.app().root_dir()).map_err(Error::MetadataFailed)?;
154
155 #[cfg(target_os = "macos")]
157 if metadata.apple().supported() {
158 apple::project::gen(
159 config.apple(),
160 metadata.apple(),
161 config.app().template_pack().submodule_path(),
162 &bike,
163 wrapper,
164 non_interactive,
165 skip_dev_tools,
166 reinstall_deps,
167 &filter,
168 skip_targets_install,
169 )
170 .map_err(Error::AppleInitFailed)?;
171 } else {
172 println!("Skipping iOS init, since it's marked as unsupported in your Cargo.toml metadata");
173 }
174
175 if metadata.android().supported() {
177 match android::env::Env::new() {
178 Ok(env) => android::project::gen(
179 config.android(),
180 metadata.android(),
181 &env,
182 &bike,
183 wrapper,
184 &filter,
185 &mut dot_cargo,
186 skip_targets_install,
187 )
188 .map_err(Error::AndroidInitFailed)?,
189 Err(err) => {
190 if err.sdk_or_ndk_issue() {
191 Report::action_request(
192 "Failed to initialize Android environment; Android support won't be usable until you fix the issue below and re-run `cargo mobile init`!",
193 err,
194 )
195 .print(wrapper);
196 } else {
197 Err(Error::AndroidEnvFailed(err))?;
198 }
199 }
200 }
201 } else {
202 println!(
203 "Skipping Android init, since it's marked as unsupported in your Cargo.toml metadata"
204 );
205 }
206
207 dot_cargo
208 .write(config.app())
209 .map_err(Error::DotCargoWriteFailed)?;
210 if dot_first_init_exists {
211 log::info!("deleting first init dot file at {:?}", dot_first_init_path);
212 fs::remove_file(&dot_first_init_path).map_err(|cause| Error::DotFirstInitDeleteFailed {
213 path: dot_first_init_path,
214 cause,
215 })?;
216 }
217 Report::victory(
218 "Project generated successfully!",
219 "Make cool apps! 🌻 🐕 🎉",
220 )
221 .print(wrapper);
222 if open_in_editor {
223 util::open_in_editor(cwd).map_err(Error::OpenInEditorFailed)?;
224 }
225 Ok(config)
226}