cargo-ohos 0.1.0

Cargo subcommand to cross-compile Rust for OpenHarmony
use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};

use crate::sdk::Sdk;
use crate::target::Target;
use crate::toolchain::Toolchain;

#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct Flags {
    pub cflags: Vec<String>,
    pub ldflags: Vec<String>,
    pub bindgen: Vec<String>,
    pub rustflags: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct BuildEnv {
    pub sdk: Sdk,
    pub target: Target,
    pub toolchain: Toolchain,
    pub flags: Flags,
    pub env: BTreeMap<String, String>,
}

pub fn derive(target: Target, sdk_dir: Option<&Path>) -> Result<BuildEnv, Error> {
    let sdk = Sdk::discover(sdk_dir)?;
    let toolchain = Toolchain::resolve(&sdk)?;

    let mut flags = Flags {
        cflags: vec![
            format!("--target={}", target.clang_triple),
            format!("--sysroot={}", posix(&sdk.sysroot)),
        ],
        ldflags: vec!["-fuse-ld=lld".to_owned(), "-Wl,--build-id".to_owned()],
        bindgen: Vec::new(),
        rustflags: Vec::new(),
    };
    flags.cflags.extend(target.extra_cflags());

    flags.bindgen.extend(flags.cflags.iter().cloned());
    flags.bindgen.push(format!(
        "-I{}",
        posix(
            &sdk.sysroot
                .join("usr")
                .join("include")
                .join(&target.lib_dir)
        )
    ));

    flags.rustflags = flags
        .ldflags
        .iter()
        .chain(&flags.cflags)
        .map(|f| format!("-Clink-arg={f}"))
        .collect();

    let env = build_env_map(&sdk, &target, &toolchain, &flags);

    Ok(BuildEnv {
        sdk,
        target,
        toolchain,
        flags,
        env,
    })
}

fn build_env_map(
    sdk: &Sdk,
    target: &Target,
    toolchain: &Toolchain,
    flags: &Flags,
) -> BTreeMap<String, String> {
    let mut env = BTreeMap::new();
    let rust = &target.rust_triple;
    let rust_u = target.rust_triple_underscored();
    let clang_u = target.clang_triple_underscored();
    let clang = posix(&toolchain.clang);
    let clangxx = posix(&toolchain.clangxx);

    for (tool, value) in [("CC", &clang), ("CXX", &clangxx)] {
        env.insert(format!("{tool}_{rust}"), value.clone());
        env.insert(format!("{tool}_{rust_u}"), value.clone());
    }
    env.insert(format!("CXXSTDLIB_{clang_u}"), "c++".to_owned());

    env.insert("TARGET_AR".to_owned(), posix(&toolchain.ar));
    env.insert("TARGET_RANLIB".to_owned(), posix(&toolchain.ranlib));
    env.insert("TARGET_STRIP".to_owned(), posix(&toolchain.strip));
    env.insert("TARGET_OBJCOPY".to_owned(), posix(&toolchain.objcopy));
    env.insert("TARGET_READELF".to_owned(), posix(&toolchain.readelf));

    env.insert("TARGET_CFLAGS".to_owned(), flags.cflags.join(" "));
    env.insert("TARGET_CXXFLAGS".to_owned(), flags.cflags.join(" "));

    env.insert(
        format!("CARGO_TARGET_{}_LINKER", target.rust_triple_upper()),
        clang.clone(),
    );

    env.insert(
        format!("BINDGEN_EXTRA_CLANG_ARGS_{rust_u}"),
        flags.bindgen.join(" "),
    );
    env.insert("LIBCLANG_PATH".to_owned(), posix(&toolchain.libclang_dir));
    env.insert("CLANG_PATH".to_owned(), clangxx.clone());

    if let Some(cmake) = &sdk.cmake {
        env.insert("CMAKE".to_owned(), posix(cmake));
    }
    env.insert(format!("CMAKE_C_COMPILER_{rust_u}"), clang.clone());
    env.insert(format!("CMAKE_CXX_COMPILER_{rust_u}"), clangxx.clone());
    if let Some(file) = &sdk.cmake_toolchain_file {
        env.insert(format!("CMAKE_TOOLCHAIN_FILE_{rust_u}"), posix(file));
    }

    env.insert(
        format!("PKG_CONFIG_SYSROOT_DIR_{rust_u}"),
        posix(&sdk.sysroot),
    );
    env.insert(
        format!("PKG_CONFIG_PATH_{rust_u}"),
        format!(
            "{}:{}",
            posix(&sdk.sysroot.join("usr").join("lib").join("pkgconfig")),
            posix(&sdk.sysroot.join("usr").join("share").join("pkgconfig"))
        ),
    );

    env.insert("CARGO_OHOS_SDK_NATIVE".to_owned(), posix(&sdk.native_root));
    env.insert("CARGO_OHOS_SYSROOT".to_owned(), posix(&sdk.sysroot));
    env.insert(
        "CARGO_OHOS_CLANG_TRIPLE".to_owned(),
        target.clang_triple.clone(),
    );

    env
}

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

#[derive(Debug)]
pub enum Error {
    SdkNotFound { tried: Vec<String> },
    MissingTool { path: PathBuf },
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::SdkNotFound { tried } => {
                write!(
                    f,
                    "Could not find the OpenHarmony native SDK. Point --sdk or $OHOS_SDK_NATIVE \
                     at the `native` directory of the SDK. Tried: {}",
                    tried.join("; ")
                )
            }
            Self::MissingTool { path } => write!(f, "Missing toolchain binary: {}", path.display()),
        }
    }
}

impl std::error::Error for Error {}