ohrs 1.4.1

a cli tool for ohos-rs
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io;
use std::path::Path;

use super::Arch;

#[derive(Debug, Clone, Default)]
pub struct ToolchainPaths {
  pub ranlib: String,
  pub ar: String,
  pub cc: String,
  pub cxx: String,
  pub llvm_as: String,
  pub ld: String,
  pub strip: String,
  pub objdump: String,
  pub objcopy: String,
  pub nm: String,
  pub bin_dir: String,
  pub lib_dir: String,
}

#[derive(Debug, Clone, Default)]
pub struct HmsPaths {
  pub include: Option<String>,
  pub lib: Option<String>,
}

pub fn resolve_toolchain_paths(root: &str) -> ToolchainPaths {
  let bin_dir = Path::new(root).join("bin");
  let lib_dir = Path::new(root).join("lib");

  #[cfg(target_os = "windows")]
  let exe_suffix = ".exe";
  #[cfg(not(target_os = "windows"))]
  let exe_suffix = "";

  let to_string = |p: std::path::PathBuf| p.to_string_lossy().to_string();
  let tool = |name: &str| to_string(bin_dir.join(format!("{}{}", name, exe_suffix)));

  ToolchainPaths {
    ranlib: tool("llvm-ranlib"),
    ar: tool("llvm-ar"),
    cc: tool("clang"),
    cxx: tool("clang++"),
    llvm_as: tool("llvm-as"),
    ld: tool("ld.lld"),
    strip: tool("llvm-strip"),
    objdump: tool("llvm-objdump"),
    objcopy: tool("llvm-objcopy"),
    nm: tool("llvm-nm"),
    bin_dir: to_string(bin_dir),
    lib_dir: to_string(lib_dir),
  }
}

pub fn resolve_hms_paths(hos_ndk: &str, arch: &Arch) -> HmsPaths {
  if hos_ndk.is_empty() {
    return HmsPaths::default();
  }

  let include_path = Path::new(hos_ndk)
    .join("native")
    .join("sysroot")
    .join("usr")
    .join("include");
  let lib_path = Path::new(hos_ndk)
    .join("native")
    .join("sysroot")
    .join("usr")
    .join("lib")
    .join(arch.c_target());

  HmsPaths {
    include: include_path
      .exists()
      .then(|| include_path.to_string_lossy().to_string()),
    lib: lib_path
      .exists()
      .then(|| lib_path.to_string_lossy().to_string()),
  }
}

pub fn append_hms_link_flags(base_flags: &mut Vec<String>, hms_paths: &HmsPaths) {
  if let Some(lib) = hms_paths.lib.as_ref() {
    base_flags.push(format!("-L{}", lib));
    base_flags.push(format!("-Wl,-rpath-link,{}", lib));
  }
}

pub fn apply_hms_include_env(
  prepare_env: &mut HashMap<String, String>,
  hms_paths: &HmsPaths,
  rust_target: &str,
) {
  let Some(include) = hms_paths.include.as_ref() else {
    return;
  };

  let include_flag = format!("-I{}", include);

  append_env_with_flag(prepare_env, "TARGET_CFLAGS", &include_flag);
  append_env_with_flag(prepare_env, "TARGET_CXXFLAGS", &include_flag);

  let bindgen_target = rust_target.replace('-', "_");
  append_env_with_flag(
    prepare_env,
    &format!("BINDGEN_EXTRA_CLANG_ARGS_{}", bindgen_target),
    &include_flag,
  );
  append_env_with_flag(
    prepare_env,
    &format!("BINDGEN_EXTRA_CLANG_ARGS_{}", bindgen_target.to_uppercase()),
    &include_flag,
  );
}

pub fn apply_ohos_cmake_env(
  prepare_env: &mut HashMap<String, String>,
  rust_target: &str,
  ndk: &str,
  arch: &Arch,
) -> io::Result<()> {
  #[cfg(target_os = "windows")]
  {
    let target_key = format!("CMAKE_GENERATOR_{}", rust_target);
    let target_key_alt = format!("CMAKE_GENERATOR_{}", rust_target.replace('-', "_"));
    let has_generator = env::var(&target_key).is_ok()
      || env::var(&target_key_alt).is_ok()
      || env::var("TARGET_CMAKE_GENERATOR").is_ok()
      || env::var("CMAKE_GENERATOR").is_ok();

    if !has_generator {
      prepare_env.insert(target_key_alt, String::from("Ninja"));
      prepare_env.insert(
        String::from("TARGET_CMAKE_GENERATOR"),
        String::from("Ninja"),
      );
    }
  }

  let target_toolchain_key = format!("CMAKE_TOOLCHAIN_FILE_{}", rust_target);
  let target_toolchain_key_alt = format!("CMAKE_TOOLCHAIN_FILE_{}", rust_target.replace('-', "_"));
  let has_toolchain_file = env::var(&target_toolchain_key).is_ok()
    || env::var(&target_toolchain_key_alt).is_ok()
    || env::var("TARGET_CMAKE_TOOLCHAIN_FILE").is_ok()
    || env::var("CMAKE_TOOLCHAIN_FILE").is_ok();

  if !has_toolchain_file {
    let toolchain_file = write_ohos_cmake_toolchain(ndk, rust_target, arch)?;
    prepare_env.insert(target_toolchain_key_alt, toolchain_file.clone());
    prepare_env.insert(String::from("TARGET_CMAKE_TOOLCHAIN_FILE"), toolchain_file);
  }

  Ok(())
}

fn write_ohos_cmake_toolchain(ndk: &str, rust_target: &str, arch: &Arch) -> io::Result<String> {
  let sdk_toolchain = Path::new(ndk)
    .join("native")
    .join("build")
    .join("cmake")
    .join("ohos.toolchain.cmake");

  if !sdk_toolchain.is_file() {
    return Err(io::Error::new(
      io::ErrorKind::NotFound,
      format!(
        "OHOS CMake toolchain file not found: {}",
        sdk_toolchain.to_string_lossy()
      ),
    ));
  }

  let output_dir = env::current_dir()?
    .join("target")
    .join("ohrs")
    .join("cmake");
  fs::create_dir_all(&output_dir)?;

  let wrapper = output_dir.join(format!("ohos-{}.toolchain.cmake", rust_target));
  let sdk_toolchain = cmake_path(&sdk_toolchain);
  let content = format!(
    "set(OHOS_ARCH \"{}\" CACHE STRING \"OHOS target ABI\" FORCE)\ninclude(\"{}\")\n",
    arch.to_arch(),
    sdk_toolchain
  );
  fs::write(&wrapper, content)?;

  Ok(wrapper.to_string_lossy().to_string())
}

fn cmake_path(path: &Path) -> String {
  path.to_string_lossy().replace('\\', "/")
}

fn append_env_with_flag(prepare_env: &mut HashMap<String, String>, key: &str, append: &str) {
  let current = env::var(key).unwrap_or_default();
  let merged = if current.is_empty() {
    append.to_string()
  } else {
    format!("{} {}", current, append)
  };
  prepare_env.insert(key.to_string(), merged);
}