cargo_mobile2/
init.rs

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 = // newline
25    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            // indicate first init is ongoing, so that if we error out and exit
113            // the next init will know to still use `WildWest` filtering
114            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    // Generate the base project
131    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    // Generate Xcode project
156    #[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    // Generate Android Studio project
176    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}