craby_build 0.1.0-rc.8

Craby build utilities
Documentation
use std::{
    fs,
    path::PathBuf,
    process::Command,
};

use crate::{
    cargo::artifact::{ArtifactType, Artifacts},
    constants::{ios::Identifier, toolchain::Target},
    platform::common::{replace_cxx_header, replace_cxx_iter_template},
};

use craby_common::{
    config::CompleteConfig,
    constants::{crate_target_dir, dest_lib_name, ios_base_path, lib_base_name},
    utils::string::SanitizedString,
};
use indoc::formatdoc;
use log::{debug, info};
use owo_colors::OwoColorize;

pub fn crate_libs(config: &CompleteConfig, build_targets: &[Target]) -> Result<(), anyhow::Error> {
    let ios_base_path = ios_base_path(&config.project_root);

    let (sims, devices): (Vec<_>, Vec<_>) = build_targets.iter().partition(|target| {
        matches!(
            target,
            Target::Ios(Identifier::Arm64Simulator) | Target::Ios(Identifier::X86_64Simulator)
        )
    });

    let sims = sims
        .into_iter()
        .map(|target| Artifacts::get_artifacts(config, target))
        .collect::<Result<Vec<_>, anyhow::Error>>()?;

    let devices = devices
        .into_iter()
        .filter(|target| matches!(target, Target::Ios(_)))
        .map(|target| Artifacts::get_artifacts(config, target))
        .collect::<Result<Vec<_>, anyhow::Error>>()?;

    let sims = if sims.len() > 1 {
        vec![create_sim_lib(sims)?]
    } else {
        sims
    };
    let xcframework_path = create_xcframework(config)?;

    for artifacts in [devices, sims].concat() {
        artifacts.path_of(ArtifactType::Lib).iter().try_for_each(
            |lib| -> Result<(), anyhow::Error> {
                info!(
                    "Optimizing library... {}",
                    format!("({})", artifacts.identifier).dimmed()
                );
                strip_lib(lib)?;
                Ok(())
            },
        )?;

        // ios/src
        artifacts.copy_to(ArtifactType::Src, &ios_base_path.join("src"))?;

        // ios/include
        artifacts.copy_to(ArtifactType::Header, &ios_base_path.join("include"))?;

        // ios/framework/lib{lib_name}.xcframework/{identifier}
        let is_sim = artifacts.identifier.contains("sim");
        artifacts.copy_to(
            ArtifactType::Lib,
            &xcframework_path.join(if is_sim {
                Identifier::Simulator.try_into_str()?
            } else {
                Identifier::Arm64.try_into_str()?
            }),
        )?;
    }

    let signal_path = ios_base_path.join("include").join("CrabySignals.h");
    if signal_path.try_exists()? {
        replace_cxx_header(&signal_path)?;
    }

    let cxx_path = ios_base_path.join("include").join("cxx.h");
    if cxx_path.try_exists()? {
        replace_cxx_iter_template(&cxx_path)?;
    }

    Ok(())
}

/// Creates a simulator library from the given artifacts
///
/// This function takes a vector of artifacts and creates a simulator library from them.
/// It uses the `lipo` command to combine the libraries into a single library.
fn create_sim_lib(sims: Vec<Artifacts>) -> Result<Artifacts, anyhow::Error> {
    let identifier = Identifier::Simulator.try_into_str()?;
    let orig = sims
        .first()
        .cloned()
        .ok_or(anyhow::anyhow!("No simulator artifacts found"))?;

    let libs = sims
        .into_iter()
        .flat_map(|artifacts| artifacts.libs)
        .collect::<Vec<_>>();

    let lib = libs.first().ok_or(anyhow::anyhow!("No library found"))?;
    let lib_name = lib
        .file_name()
        .ok_or(anyhow::anyhow!("No library name found"))?;

    let target_dir = Artifacts::try_get_target_dir()?;
    let dest_dir = crate_target_dir(&target_dir, identifier);
    let dest_path = dest_dir.join(lib_name);

    if dest_dir.try_exists()? {
        fs::remove_dir_all(&dest_dir)?;
    }
    fs::create_dir_all(&dest_dir)?;

    debug!(
        "Creating simulator library from artifacts (dest: {:?})",
        dest_path
    );

    let res = Command::new("lipo")
        .arg("-create")
        .args(libs)
        .args(["-output", dest_path.to_str().unwrap()])
        .output()?;

    if !res.status.success() {
        anyhow::bail!(
            "Failed to create simulator library: {}",
            String::from_utf8_lossy(&res.stderr)
        );
    }

    Ok(Artifacts {
        identifier: Identifier::Simulator.try_into_str()?.to_string(),
        headers: orig.headers,
        srcs: orig.srcs,
        libs: vec![dest_path],
    })
}

fn strip_lib(lib: &PathBuf) -> Result<(), anyhow::Error> {
    let res = Command::new("strip")
        .arg("-x")
        .arg("-S")
        .arg(lib)
        .output()?;

    if !res.status.success() {
        anyhow::bail!(
            "Failed to strip library: {}",
            String::from_utf8_lossy(&res.stderr)
        );
    }

    Ok(())
}

fn create_xcframework(config: &CompleteConfig) -> Result<PathBuf, anyhow::Error> {
    let name = SanitizedString::from(&config.project.name);
    let lib_base_name = lib_base_name(&name);
    let info_plist_content = info_plist(&config.project.name)?;
    let framework_path = ios_base_path(&config.project_root).join("framework");
    let xcframework_path = framework_path.join(format!("lib{}.xcframework", lib_base_name));

    if xcframework_path.try_exists()? {
        fs::remove_dir_all(&xcframework_path)?;
    }

    fs::create_dir_all(&xcframework_path)?;

    let info_plist_path = xcframework_path.join("Info.plist");
    fs::write(info_plist_path, info_plist_content)?;

    Ok(xcframework_path)
}

pub fn info_plist(name: &String) -> Result<String, anyhow::Error> {
    let lib_name = dest_lib_name(&SanitizedString::from(name));

    let content = formatdoc! {
        r#"
        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        <plist version="1.0">
        <dict>
            <key>AvailableLibraries</key>
            <array>
                <dict>
                    <key>BinaryPath</key>
                    <string>{lib_name}</string>
                    <key>LibraryIdentifier</key>
                    <string>{lib_identifier}</string>
                    <key>LibraryPath</key>
                    <string>{lib_name}</string>
                    <key>SupportedArchitectures</key>
                    <array>
                        <string>arm64</string>
                    </array>
                    <key>SupportedPlatform</key>
                    <string>ios</string>
                </dict>
                <dict>
                    <key>BinaryPath</key>
                    <string>{lib_name}</string>
                    <key>LibraryIdentifier</key>
                    <string>{lib_sim_identifier}</string>
                    <key>LibraryPath</key>
                    <string>{lib_name}</string>
                    <key>SupportedArchitectures</key>
                    <array>
                        <string>arm64</string>
                        <string>x86_64</string>
                    </array>
                    <key>SupportedPlatform</key>
                    <string>ios</string>
                    <key>SupportedPlatformVariant</key>
                    <string>simulator</string>
                </dict>
            </array>
            <key>CFBundlePackageType</key>
            <string>XFWK</string>
            <key>XCFrameworkFormatVersion</key>
            <string>1.0</string>
        </dict>
        </plist>"#,
        lib_name = lib_name,
        lib_identifier = Identifier::Arm64.try_into_str()?,
        lib_sim_identifier = Identifier::Simulator.try_into_str()?,
    };

    Ok(content)
}