tauri-cli 2.10.1

Command line interface for building Tauri apps
Documentation
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use crate::{
  error::Context,
  helpers::{config::Config as TauriConfig, template},
  mobile::ios::LIB_OUTPUT_FILE_NAME,
  Error, ErrorExt, Result,
};
use cargo_mobile2::{
  apple::{
    config::{Config, Metadata},
    deps,
    target::Target,
  },
  config::app::DEFAULT_ASSET_DIR,
  target::TargetTrait as _,
  util::{self, cli::TextWrapper},
};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use std::{
  ffi::OsString,
  fs::{create_dir_all, OpenOptions},
  path::{Component, PathBuf},
};

const TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/mobile/ios");

// unprefixed app_root seems pretty dangerous!!
// TODO: figure out what cargo-mobile meant by that
#[allow(clippy::too_many_arguments)]
pub fn gen(
  tauri_config: &TauriConfig,
  config: &Config,
  metadata: &Metadata,
  (handlebars, mut map): (Handlebars, template::JsonMap),
  wrapper: &TextWrapper,
  non_interactive: bool,
  reinstall_deps: bool,
  skip_targets_install: bool,
) -> Result<()> {
  if !skip_targets_install {
    let installed_targets =
      crate::interface::rust::installation::installed_targets().unwrap_or_default();
    let missing_targets = Target::all()
      .values()
      .filter(|t| !installed_targets.contains(&t.triple().into()))
      .collect::<Vec<&Target>>();

    if !missing_targets.is_empty() {
      log::info!("Installing iOS Rust targets...");
      for target in missing_targets {
        log::info!("Installing target {}", target.triple());
        target.install().map_err(|error| Error::CommandFailed {
          command: "rustup target add".to_string(),
          error,
        })?;
      }
    }
  }

  deps::install_all(wrapper, non_interactive, true, reinstall_deps).map_err(|error| {
    Error::CommandFailed {
      command: "pod install".to_string(),
      error: std::io::Error::other(error),
    }
  })?;

  let dest = config.project_dir();
  let rel_prefix = util::relativize_path(config.app().root_dir(), &dest);
  let source_dirs = vec![rel_prefix.join("src")];

  let asset_catalogs = metadata.ios().asset_catalogs().unwrap_or_default();
  let ios_pods = metadata.ios().pods().unwrap_or_default();
  let macos_pods = metadata.macos().pods().unwrap_or_default();

  #[cfg(target_arch = "aarch64")]
  let default_archs = ["arm64"];
  #[cfg(not(target_arch = "aarch64"))]
  let default_archs = ["arm64", "x86_64"];

  map.insert("lib-output-file-name", LIB_OUTPUT_FILE_NAME);

  map.insert("file-groups", &source_dirs);
  map.insert("ios-frameworks", metadata.ios().frameworks());
  map.insert("ios-valid-archs", default_archs);
  map.insert("ios-vendor-frameworks", metadata.ios().vendor_frameworks());
  map.insert("ios-vendor-sdks", metadata.ios().vendor_sdks());
  map.insert("macos-frameworks", metadata.macos().frameworks());
  map.insert(
    "macos-vendor-frameworks",
    metadata.macos().vendor_frameworks(),
  );
  map.insert("macos-vendor-sdks", metadata.macos().vendor_frameworks());
  map.insert("asset-catalogs", asset_catalogs);
  map.insert("ios-pods", ios_pods);
  map.insert("macos-pods", macos_pods);
  map.insert(
    "ios-additional-targets",
    metadata.ios().additional_targets(),
  );
  map.insert(
    "macos-additional-targets",
    metadata.macos().additional_targets(),
  );
  map.insert("ios-pre-build-scripts", metadata.ios().pre_build_scripts());
  map.insert(
    "ios-post-compile-scripts",
    metadata.ios().post_compile_scripts(),
  );
  map.insert(
    "ios-post-build-scripts",
    metadata.ios().post_build_scripts(),
  );
  map.insert(
    "macos-pre-build-scripts",
    metadata.macos().pre_build_scripts(),
  );
  map.insert(
    "macos-post-compile-scripts",
    metadata.macos().post_compile_scripts(),
  );
  map.insert(
    "macos-post-build-scripts",
    metadata.macos().post_build_scripts(),
  );
  map.insert(
    "ios-command-line-arguments",
    metadata.ios().command_line_arguments(),
  );
  map.insert(
    "macos-command-line-arguments",
    metadata.macos().command_line_arguments(),
  );

  let mut created_dirs = Vec::new();
  template::render_with_generator(
    &handlebars,
    map.inner(),
    &TEMPLATE_DIR,
    &dest,
    &mut |path| {
      let mut components: Vec<_> = path.components().collect();
      let mut new_component = None;
      for component in &mut components {
        if let Component::Normal(c) = component {
          let c = c.to_string_lossy();
          if c.contains("{{app.name}}") {
            new_component.replace(OsString::from(
              &c.replace("{{app.name}}", config.app().name()),
            ));
            *component = Component::Normal(new_component.as_ref().unwrap());
            break;
          }
        }
      }
      let path = dest.join(components.iter().collect::<PathBuf>());

      let parent = path.parent().unwrap().to_path_buf();
      if !created_dirs.contains(&parent) {
        create_dir_all(&parent)?;
        created_dirs.push(parent);
      }

      let mut options = OpenOptions::new();
      options.write(true);

      if !path.exists() {
        options.create(true).open(path).map(Some)
      } else {
        Ok(None)
      }
    },
  )
  .with_context(|| "failed to process template")?;

  if let Some(template_path) = tauri_config.bundle.ios.template.as_ref() {
    let template = std::fs::read_to_string(template_path).fs_context(
      "failed to read custom Xcode project template",
      template_path.to_path_buf(),
    )?;
    let mut output_file = std::fs::File::create(dest.join("project.yml")).fs_context(
      "failed to create project.yml file",
      dest.join("project.yml"),
    )?;
    handlebars
      .render_template_to_write(&template, map.inner(), &mut output_file)
      .expect("Failed to render template");
  }

  let mut dirs_to_create = asset_catalogs.to_vec();
  dirs_to_create.push(dest.join(DEFAULT_ASSET_DIR));
  dirs_to_create.push(dest.join("Externals"));
  dirs_to_create.push(dest.join(format!("{}_iOS", config.app().name())));

  // Create all required project directories if they don't already exist
  for dir in &dirs_to_create {
    std::fs::create_dir_all(dir).fs_context("failed to create directory", dir.to_path_buf())?;
  }

  // Note that Xcode doesn't always reload the project nicely; reopening is
  // often necessary.
  println!("Generating Xcode project...");
  duct::cmd(
    "xcodegen",
    [
      "generate",
      "--spec",
      &dest.join("project.yml").to_string_lossy(),
    ],
  )
  .stdout_file(os_pipe::dup_stdout().unwrap())
  .stderr_file(os_pipe::dup_stderr().unwrap())
  .run()
  .map_err(|error| Error::CommandFailed {
    command: "xcodegen".to_string(),
    error,
  })?;

  if !ios_pods.is_empty() || !macos_pods.is_empty() {
    duct::cmd(
      "pod",
      [
        "install",
        &format!("--project-directory={}", dest.display()),
      ],
    )
    .stdout_file(os_pipe::dup_stdout().unwrap())
    .stderr_file(os_pipe::dup_stderr().unwrap())
    .run()
    .map_err(|error| Error::CommandFailed {
      command: "pod install".to_string(),
      error,
    })?;
  }
  Ok(())
}