tessera-mobile 0.0.0

Rust on mobile made easy.
Documentation
use std::{
    collections::HashMap,
    fmt::{self, Display},
    path::PathBuf,
};

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
    config::app::App,
    util::{self, cli::Report},
};

const DEFAULT_MIN_SDK_VERSION: u32 = 24;
pub const DEFAULT_VULKAN_VALIDATION: bool = true;
static DEFAULT_PROJECT_DIR: &str = "gen/android";

const fn default_true() -> bool {
    true
}

#[derive(Debug, Deserialize)]
pub struct AssetPackInfo {
    pub name: String,
    pub delivery_type: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
    #[serde(default = "default_true")]
    pub supported: bool,
    #[serde(default)]
    pub no_default_features: bool,
    pub cargo_args: Option<Vec<String>>,
    pub features: Option<Vec<String>>,
    pub app_sources: Option<Vec<String>>,
    pub app_plugins: Option<Vec<String>>,
    pub project_dependencies: Option<Vec<String>>,
    pub app_dependencies: Option<Vec<String>>,
    pub app_dependencies_platform: Option<Vec<String>>,
    pub asset_packs: Option<Vec<AssetPackInfo>>,
    pub app_activity_name: Option<String>,
    pub app_permissions: Option<Vec<String>>,
    pub app_theme_parent: Option<String>,
    pub env_vars: Option<HashMap<String, String>>,
    pub vulkan_validation: Option<bool>,
}

impl Default for Metadata {
    fn default() -> Self {
        Self {
            supported: true,
            no_default_features: false,
            cargo_args: None,
            features: None,
            app_sources: None,
            app_plugins: None,
            project_dependencies: None,
            app_dependencies: None,
            app_dependencies_platform: None,
            asset_packs: None,
            app_activity_name: None,
            app_permissions: None,
            app_theme_parent: None,
            env_vars: None,
            vulkan_validation: None,
        }
    }
}

impl Metadata {
    pub const fn supported(&self) -> bool {
        self.supported
    }

    pub fn no_default_features(&self) -> bool {
        self.no_default_features
    }

    pub fn cargo_args(&self) -> Option<&[String]> {
        self.cargo_args.as_deref()
    }

    pub fn features(&self) -> Option<&[String]> {
        self.features.as_deref()
    }

    pub fn app_sources(&self) -> &[String] {
        self.app_sources.as_deref().unwrap_or(&[])
    }

    pub fn app_plugins(&self) -> Option<&[String]> {
        self.app_plugins.as_deref()
    }

    pub fn project_dependencies(&self) -> Option<&[String]> {
        self.project_dependencies.as_deref()
    }

    pub fn app_dependencies(&self) -> Option<&[String]> {
        self.app_dependencies.as_deref()
    }

    pub fn app_dependencies_platform(&self) -> Option<&[String]> {
        self.app_dependencies_platform.as_deref()
    }

    pub fn asset_packs(&self) -> Option<&[AssetPackInfo]> {
        self.asset_packs.as_deref()
    }

    pub fn app_activity_name(&self) -> Option<&str> {
        self.app_activity_name.as_deref()
    }

    pub fn app_permissions(&self) -> Option<&[String]> {
        self.app_permissions.as_deref()
    }

    pub fn app_theme_parent(&self) -> Option<&str> {
        self.app_theme_parent.as_deref()
    }

    pub fn vulkan_validation(&self) -> Option<bool> {
        self.vulkan_validation
    }
}

#[derive(Debug)]
pub enum ProjectDirInvalid {
    NormalizationFailed {
        project_dir: String,
        cause: util::NormalizationError,
    },
    OutsideOfAppRoot {
        project_dir: String,
        root_dir: PathBuf,
    },
    ContainsSpaces {
        project_dir: String,
    },
}

impl Display for ProjectDirInvalid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NormalizationFailed { project_dir, cause } => {
                write!(f, "{project_dir:?} couldn't be normalized: {cause}")
            }
            Self::OutsideOfAppRoot {
                project_dir,
                root_dir,
            } => write!(f, "{project_dir:?} is outside of the app root {root_dir:?}",),
            Self::ContainsSpaces { project_dir } => write!(
                f,
                "{project_dir:?} contains spaces, which the NDK is remarkably intolerant of"
            ),
        }
    }
}

#[derive(Debug, Error)]
pub enum Error {
    #[error("android.project-dir invalid: {0}")]
    ProjectDirInvalid(ProjectDirInvalid),
    #[error("Identifier cannot contain hyphens on Android")]
    IdentifierCannotContainHyphens,
}

impl Error {
    pub fn report(&self, msg: &str) -> Report {
        Report::error(msg, self)
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Raw {
    pub min_sdk_version: Option<u32>,
    pub project_dir: Option<String>,
    pub no_default_features: Option<bool>,
    pub features: Option<Vec<String>>,
    #[serde(default)]
    pub logcat_filter_specs: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
    #[serde(skip_serializing)]
    app: App,
    min_sdk_version: u32,
    project_dir: PathBuf,
    logcat_filter_specs: Vec<String>,
}

impl Config {
    pub fn from_raw(app: App, raw: Option<Raw>) -> Result<Self, Error> {
        let raw = raw.unwrap_or_default();

        if app.identifier().contains('-') {
            return Err(Error::IdentifierCannotContainHyphens);
        }

        let min_sdk_version = raw.min_sdk_version.unwrap_or(DEFAULT_MIN_SDK_VERSION);

        let project_dir = if let Some(project_dir) = raw.project_dir {
            if project_dir == DEFAULT_PROJECT_DIR {
                log::warn!(
                    "`{}.project-dir` is set to the default value; you can remove it from your config",
                    super::NAME
                );
            }
            if util::under_root(&project_dir, app.root_dir()).map_err(|cause| {
                Error::ProjectDirInvalid(ProjectDirInvalid::NormalizationFailed {
                    project_dir: project_dir.clone(),
                    cause,
                })
            })? {
                if !project_dir.contains(' ') {
                    Ok(project_dir.into())
                } else {
                    Err(Error::ProjectDirInvalid(
                        ProjectDirInvalid::ContainsSpaces { project_dir },
                    ))
                }
            } else {
                Err(Error::ProjectDirInvalid(
                    ProjectDirInvalid::OutsideOfAppRoot {
                        project_dir,
                        root_dir: app.root_dir().to_owned(),
                    },
                ))
            }
        } else {
            Ok(DEFAULT_PROJECT_DIR.into())
        }?;

        Ok(Self {
            app,
            min_sdk_version,
            project_dir,
            logcat_filter_specs: raw.logcat_filter_specs,
        })
    }

    pub fn app(&self) -> &App {
        &self.app
    }

    pub fn logcat_filter_specs(&self) -> &[String] {
        &self.logcat_filter_specs
    }

    pub fn so_name(&self) -> String {
        format!("lib{}.so", self.app().lib_name())
    }

    pub fn min_sdk_version(&self) -> u32 {
        self.min_sdk_version
    }

    pub fn project_dir(&self) -> PathBuf {
        self.app.prefix_path(&self.project_dir)
    }

    pub fn project_dir_exists(&self) -> bool {
        self.project_dir().is_dir()
    }
}