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() {
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,
})
}
}