1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::fs::create_dir_all;

use camino::{Utf8Path, Utf8PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use uniffi_bindgen::{bindings::KotlinBindingGenerator, library_mode::generate_bindings};
use xshell::{cmd, pushd};

use crate::{workspace, Result};

struct PackageValues {
    name: &'static str,
}

#[derive(ValueEnum, Clone)]
enum Package {
    CryptoSDK,
    FullSDK,
}

impl Package {
    fn values(self) -> PackageValues {
        match self {
            Package::CryptoSDK => PackageValues { name: "matrix-sdk-crypto-ffi" },
            Package::FullSDK => PackageValues { name: "matrix-sdk-ffi" },
        }
    }
}

#[derive(Args)]
pub struct KotlinArgs {
    #[clap(subcommand)]
    cmd: KotlinCommand,
}

#[derive(Subcommand)]
enum KotlinCommand {
    /// Builds the SDK for Android as an AAR.
    BuildAndroidLibrary {
        #[clap(value_enum, long)]
        package: Package,

        /// Build with the release profile
        #[clap(long)]
        release: bool,

        /// Build with a custom profile, takes precedence over `--release`
        #[clap(long)]
        profile: Option<String>,

        /// Build the given target only
        #[clap(long)]
        only_target: Option<String>,

        /// Move the generated files into the given src directory
        #[clap(long)]
        src_dir: Utf8PathBuf,
    },
}

impl KotlinArgs {
    pub fn run(self) -> Result<()> {
        let _p = pushd(workspace::root_path()?)?;

        match self.cmd {
            KotlinCommand::BuildAndroidLibrary {
                release,
                profile,
                src_dir,
                only_target,
                package,
            } => {
                let profile = profile.as_deref().unwrap_or(if release { "release" } else { "dev" });
                build_android_library(profile, only_target, src_dir, package)
            }
        }
    }
}

fn build_android_library(
    profile: &str,
    only_target: Option<String>,
    src_dir: Utf8PathBuf,
    package: Package,
) -> Result<()> {
    let package_values = package.values();
    let package_name = package_values.name;

    let jni_libs_dir = src_dir.join("jniLibs");
    let jni_libs_dir_str = jni_libs_dir.as_str();

    let kotlin_generated_dir = src_dir.join("kotlin");
    create_dir_all(kotlin_generated_dir.clone())?;

    let uniffi_lib_path = if let Some(target) = only_target {
        println!("-- Building for {target} [1/1]");
        build_for_android_target(target.as_str(), profile, jni_libs_dir_str, package_name)?
    } else {
        println!("-- Building for x86_64-linux-android[1/4]");
        build_for_android_target("x86_64-linux-android", profile, jni_libs_dir_str, package_name)?;
        println!("-- Building for aarch64-linux-android[2/4]");
        build_for_android_target("aarch64-linux-android", profile, jni_libs_dir_str, package_name)?;
        println!("-- Building for armv7-linux-androideabi[3/4]");
        build_for_android_target(
            "armv7-linux-androideabi",
            profile,
            jni_libs_dir_str,
            package_name,
        )?;
        println!("-- Building for i686-linux-android[4/4]");
        build_for_android_target("i686-linux-android", profile, jni_libs_dir_str, package_name)?
    };

    println!("-- Generate uniffi files");
    generate_uniffi_bindings(&uniffi_lib_path, &kotlin_generated_dir)?;

    println!("-- All done and hunky dory. Enjoy!");
    Ok(())
}

fn generate_uniffi_bindings(library_path: &Utf8Path, ffi_generated_dir: &Utf8Path) -> Result<()> {
    println!("-- library_path = {library_path}");

    generate_bindings(library_path, None, &KotlinBindingGenerator, None, ffi_generated_dir, false)?;
    Ok(())
}

fn build_for_android_target(
    target: &str,
    profile: &str,
    dest_dir: &str,
    package_name: &str,
) -> Result<Utf8PathBuf> {
    cmd!("cargo ndk --target {target} -o {dest_dir} build --profile {profile} -p {package_name}")
        .run()?;

    // The builtin dev profile has its files stored under target/debug, all
    // other targets have matching directory names
    let profile_dir_name = if profile == "dev" { "debug" } else { profile };
    let package_camel = package_name.replace('-', "_");
    let lib_name = format!("lib{package_camel}.so");
    Ok(workspace::target_path()?.join(target).join(profile_dir_name).join(lib_name))
}