cargo-ohos-app 0.1.2

Cargo subcommand for packaging Rust GUI apps as OHOS applications
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use serde::Deserialize;

use crate::cli::CommonArgs;
use crate::errors::{HarmonyAppError, Result};
use crate::project::ProjectInfo;
use crate::sdk::{
    HvigorInfo, SdkInfo, abi_to_target, discover_hvigor, discover_sdk, target_to_abi,
};

const DEFAULT_DEV_ECO_STUDIO_DIR: &str = r"D:\Apps\code\DevEco Studio";
const DEFAULT_OHPM_PATH: &str = r"D:\Apps\code\DevEco Studio\tools\ohpm\bin\ohpm.bat";
const DEFAULT_SDK_ROOT: &str = r"C:\Users\25422\AppData\Local\OpenHarmony\Sdk";
const DEFAULT_TARGET: &str = "aarch64-unknown-linux-ohos";

#[derive(Clone, Debug)]
pub struct AppContext {
    pub project: ProjectInfo,
    pub config: ResolvedConfig,
    pub sdk: SdkInfo,
    pub hvigor: HvigorInfo,
}

#[derive(Clone, Debug)]
pub struct ResolvedConfig {
    pub deveco_studio_dir: PathBuf,
    pub ohpm_path: PathBuf,
    pub sdk_root: PathBuf,
    pub sdk_version: Option<String>,
    pub target: String,
    pub abi: String,
    pub profile_dir: String,
    pub output_dir: PathBuf,
    pub bundle_name: String,
    pub module_name: String,
}

#[derive(Clone, Debug, Default, Deserialize)]
struct FileConfig {
    deveco_studio_dir: Option<PathBuf>,
    ohpm_path: Option<PathBuf>,
    sdk_root: Option<PathBuf>,
    sdk_version: Option<String>,
    bundle_name: Option<String>,
    module_name: Option<String>,
    target: Option<String>,
    abi: Option<String>,
    profile: Option<String>,
    output_dir: Option<PathBuf>,
}

impl AppContext {
    pub fn load(common: &CommonArgs, cwd: &Path) -> Result<Self> {
        let manifest_path = resolve_manifest_path(common, cwd)?;
        let project = ProjectInfo::load(&manifest_path)?;
        let file_config = load_file_config(&project.project_dir)?;

        let target = if let Some(target) = common.target.clone() {
            target
        } else if let Some(abi) = common.abi {
            abi_to_target(abi.as_str())?.to_string()
        } else if let Some(target) = env_var_any(&["OHOS_APP_TARGET", "HARMONY_APP_TARGET"]) {
            target
        } else if let Some(abi) = env_var_any(&["OHOS_APP_ABI", "HARMONY_APP_ABI"]) {
            abi_to_target(&abi)?.to_string()
        } else if let Some(target) = file_config.target.clone() {
            target
        } else if let Some(abi) = file_config.abi.clone() {
            abi_to_target(&abi)?.to_string()
        } else {
            DEFAULT_TARGET.to_string()
        };
        let abi = target_to_abi(&target)?.to_string();

        let profile_dir = if common.release {
            "release".to_string()
        } else {
            file_config
                .profile
                .clone()
                .or_else(|| env_var_any(&["OHOS_APP_PROFILE", "HARMONY_APP_PROFILE"]))
                .unwrap_or_else(|| "debug".to_string())
        };

        let output_dir = resolve_output_dir(
            common.out_dir.as_ref(),
            env_path_any(&["OHOS_APP_OUTPUT_DIR", "HARMONY_APP_OUTPUT_DIR"]).as_ref(),
            file_config.output_dir.as_ref(),
            &project.project_dir,
        );

        let default_bundle_name = default_bundle_name(&project.package_name);
        let module_name = common
            .module_name
            .clone()
            .or_else(|| env_var_any(&["OHOS_APP_MODULE_NAME", "HARMONY_APP_MODULE_NAME"]))
            .or_else(|| file_config.module_name.clone())
            .unwrap_or_else(|| "entry".to_string());
        let bundle_name = common
            .bundle_name
            .clone()
            .or_else(|| env_var_any(&["OHOS_APP_BUNDLE_NAME", "HARMONY_APP_BUNDLE_NAME"]))
            .or_else(|| file_config.bundle_name.clone())
            .unwrap_or(default_bundle_name);

        let deveco_studio_dir = common
            .deveco_studio_dir
            .clone()
            .or_else(|| {
                env_path_any(&["OHOS_APP_DEVECOSTUDIO_DIR", "HARMONY_APP_DEVECOSTUDIO_DIR"])
            })
            .or(file_config.deveco_studio_dir.clone())
            .unwrap_or_else(|| PathBuf::from(DEFAULT_DEV_ECO_STUDIO_DIR));
        let ohpm_path = common
            .ohpm_path
            .clone()
            .or_else(|| env_path_any(&["OHOS_APP_OHPM_PATH", "HARMONY_APP_OHPM_PATH"]))
            .or(file_config.ohpm_path.clone())
            .unwrap_or_else(|| PathBuf::from(DEFAULT_OHPM_PATH));
        let sdk_root = common
            .sdk_root
            .clone()
            .or_else(|| env_path_any(&["OHOS_APP_SDK_ROOT", "HARMONY_APP_SDK_ROOT"]))
            .or(file_config.sdk_root.clone())
            .unwrap_or_else(|| PathBuf::from(DEFAULT_SDK_ROOT));
        let sdk_version = common
            .sdk_version
            .clone()
            .or_else(|| env_var_any(&["OHOS_APP_SDK_VERSION", "HARMONY_APP_SDK_VERSION"]))
            .or(file_config.sdk_version.clone());

        let sdk = discover_sdk(&sdk_root, sdk_version.as_deref())?;
        let hvigor = discover_hvigor(&deveco_studio_dir)?;

        Ok(Self {
            project,
            config: ResolvedConfig {
                deveco_studio_dir,
                ohpm_path,
                sdk_root,
                sdk_version,
                target,
                abi,
                profile_dir,
                output_dir,
                bundle_name,
                module_name,
            },
            sdk,
            hvigor,
        })
    }
}

fn resolve_manifest_path(common: &CommonArgs, cwd: &Path) -> Result<PathBuf> {
    let path = common
        .manifest_path
        .clone()
        .or_else(|| env_path_any(&["OHOS_APP_MANIFEST_PATH", "HARMONY_APP_MANIFEST_PATH"]))
        .unwrap_or_else(|| cwd.join("Cargo.toml"));
    if path.exists() {
        Ok(path)
    } else {
        Err(HarmonyAppError::MissingFile { path })
    }
}

fn load_file_config(project_dir: &Path) -> Result<FileConfig> {
    let mut path = project_dir.join("ohos-app.toml");
    if !path.exists() {
        path = project_dir.join("harmony-app.toml");
        if !path.exists() {
            return Ok(FileConfig::default());
        }
    }
    let text = fs::read_to_string(&path).map_err(|source| HarmonyAppError::ConfigRead {
        path: path.clone(),
        source,
    })?;
    toml::from_str(&text).map_err(|source| HarmonyAppError::ConfigParse { path, source })
}

fn resolve_output_dir(
    cli: Option<&PathBuf>,
    env: Option<&PathBuf>,
    file: Option<&PathBuf>,
    project_dir: &Path,
) -> PathBuf {
    let candidate = cli
        .cloned()
        .or_else(|| env.cloned())
        .or_else(|| file.cloned())
        .unwrap_or_else(|| PathBuf::from("ohos-app"));
    if candidate.is_absolute() {
        candidate
    } else {
        project_dir.join(candidate)
    }
}

fn env_var_any(names: &[&str]) -> Option<String> {
    names.iter().find_map(|name| env::var(name).ok())
}

fn env_path_any(names: &[&str]) -> Option<PathBuf> {
    names
        .iter()
        .find_map(|name| env::var_os(name).map(PathBuf::from))
}

fn default_bundle_name(package_name: &str) -> String {
    let normalized = package_name.replace(['-', '_'], "").to_ascii_lowercase();
    format!("com.example.{normalized}")
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::load_file_config;

    #[test]
    fn reads_flat_configuration_file() {
        let temp = TempDir::new().unwrap();
        fs::write(
            temp.path().join("ohos-app.toml"),
            r#"
deveco_studio_dir = "D:\\Apps\\code\\DevEco Studio"
sdk_root = "C:\\Users\\25422\\AppData\\Local\\OpenHarmony\\Sdk"
sdk_version = "20"
bundle_name = "com.example.demo"
module_name = "entry"
"#,
        )
        .unwrap();
        let file_config = load_file_config(temp.path()).unwrap();
        assert_eq!(file_config.sdk_version.as_deref(), Some("20"));
        assert_eq!(file_config.bundle_name.as_deref(), Some("com.example.demo"));
    }

    #[test]
    fn falls_back_to_legacy_configuration_file_name() {
        let temp = TempDir::new().unwrap();
        fs::write(
            temp.path().join("harmony-app.toml"),
            r#"sdk_version = "20""#,
        )
        .unwrap();

        let file_config = load_file_config(temp.path()).unwrap();
        assert_eq!(file_config.sdk_version.as_deref(), Some("20"));
    }
}