tessera-mobile 0.0.0

Rust on mobile made easy.
Documentation
use std::{env::temp_dir, fs::read_to_string, path::PathBuf};

use serde::Deserialize;
use thiserror::Error;

use crate::{
    DuctExpressionExt,
    apple::{
        config::Config,
        deps::{GemCache, LIBIMOBILE_DEVICE_PACKAGE},
    },
    env::{Env, ExplicitEnv as _},
    opts::NoiseLevel,
    util::cli::{Report, Reportable},
};

#[derive(Debug, Error)]
pub enum RunError {
    #[error("failed to run {command}: {error}")]
    CommandFailed {
        command: String,
        error: std::io::Error,
    },
    #[error("failed to read {path}: {cause}")]
    ReadFailed {
        path: PathBuf,
        cause: std::io::Error,
    },
    #[error("failed to write {path}: {cause}")]
    WriteFailed {
        path: PathBuf,
        cause: std::io::Error,
    },
    #[error("`devicectl` returned an invalid JSON: {0}")]
    InvalidDevicectlJson(#[from] serde_json::Error),
    #[error("`devicectl` did not return the installed application metadata")]
    MissingInstalledApplication,
}

impl Reportable for RunError {
    fn report(&self) -> Report {
        match self {
            Self::CommandFailed { command, error } => {
                Report::error(format!("Failed to run {command}"), error)
            }
            Self::ReadFailed { path, cause } => {
                Report::error(format!("Failed to read {}", path.display()), cause)
            }
            Self::WriteFailed { path, cause } => {
                Report::error(format!("Failed to write {}", path.display()), cause)
            }
            Self::InvalidDevicectlJson(err) => {
                Report::error("Failed to read `devicectl` output", err)
            }
            Self::MissingInstalledApplication => Report::error(
                "Failed to deploy application",
                "`devicectl` did not return the installed application metadata",
            ),
        }
    }
}

#[derive(Deserialize)]
struct InstalledApplication {
    #[serde(rename = "bundleID")]
    bundle_id: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct InstallResult {
    installed_applications: Vec<InstalledApplication>,
}

#[derive(Deserialize)]
struct InstallOutput {
    result: InstallResult,
}

pub fn run(
    config: &Config,
    env: &Env,
    non_interactive: bool,
    id: &str,
    paired: bool,
    noise_level: NoiseLevel,
) -> Result<duct::Handle, RunError> {
    if !paired {
        println!("Pairing with device...");

        let cmd = duct::cmd("xcrun", ["devicectl", "manage", "pair", "--device", id])
            .vars(env.explicit_env())
            .dup_stdio();
        cmd.run().map_err(|error| RunError::CommandFailed {
            command: format!("{cmd:?}"),
            error,
        })?;
    }

    println!("Deploying app to device...");

    let app_dir = config
        .export_dir()
        .join(format!("{}_iOS.xcarchive", config.app().name()))
        .join("Products/Applications")
        .join(format!("{}.app", config.app().stylized_name()));
    let json_output_path = temp_dir().join("deviceinstall.json");
    let json_output_path_ = json_output_path.clone();
    std::fs::write(&json_output_path, "").map_err(|error| RunError::WriteFailed {
        path: json_output_path.clone(),
        cause: error,
    })?;
    let cmd = duct::cmd(
        "xcrun",
        ["devicectl", "device", "install", "app", "--device", id],
    )
    .vars(env.explicit_env())
    .before_spawn(move |cmd| {
        cmd.arg(&app_dir)
            .arg("--json-output")
            .arg(&json_output_path_);
        Ok(())
    })
    .dup_stdio();

    cmd.run().map_err(|error| RunError::CommandFailed {
        command: format!("{cmd:?}"),
        error,
    })?;

    let install_output_json =
        read_to_string(&json_output_path).map_err(|error| RunError::ReadFailed {
            path: json_output_path,
            cause: error,
        })?;
    let install_output = serde_json::from_str::<InstallOutput>(&install_output_json)?;
    let installed_application = install_output
        .result
        .installed_applications
        .into_iter()
        .next()
        .ok_or(RunError::MissingInstalledApplication)?;
    let app_id = installed_application.bundle_id;

    let launcher_cmd = duct::cmd(
        "xcrun",
        [
            "devicectl",
            "device",
            "process",
            "launch",
            "--device",
            id,
            &app_id,
        ],
    )
    .vars(env.explicit_env())
    .dup_stdio();

    if non_interactive {
        launcher_cmd
            .start()
            .map_err(|error| RunError::CommandFailed {
                command: format!("{cmd:?}"),
                error,
            })
    } else {
        launcher_cmd
            .start()
            .map_err(|error| RunError::CommandFailed {
                command: format!("{cmd:?}"),
                error,
            })?
            .wait()
            .map_err(|error| RunError::CommandFailed {
                command: format!("{cmd:?}"),
                error,
            })?;

        let app_name = config.app().stylized_name().to_string();

        LIBIMOBILE_DEVICE_PACKAGE
            .install(false, &mut GemCache::new())
            .map_err(|e| RunError::CommandFailed {
                command: "`brew install libimobiledevice`".to_string(),
                error: std::io::Error::other(e.to_string()),
            })?;

        let cmd = duct::cmd("idevicesyslog", ["--process", &app_name])
            .before_spawn(move |cmd| {
                if !noise_level.pedantic() {
                    // when not in pedantic log mode, filter out logs that are not from the actual
                    // app e.g. `App Name(UIKitCore)[processID]: message` vs
                    // `App Name[processID]: message`
                    cmd.arg("--match").arg(format!("{app_name}["));
                }
                Ok(())
            })
            .vars(env.explicit_env())
            .dup_stdio();
        cmd.start().map_err(|error| RunError::CommandFailed {
            command: format!("{cmd:?}"),
            error,
        })
    }
}