From 2f01d80a39ea281feae75d99680321d910b9ad21 Mon Sep 17 00:00:00 2001 From: nab138 Date: Mon, 2 Feb 2026 22:55:14 -0500 Subject: [PATCH] continue implimenting sideloading --- Cargo.lock | 38 +++++++++++++++++ examples/minimal/Cargo.toml | 1 + examples/minimal/src/main.rs | 73 ++++++++++++++++++++------------ isideload/Cargo.toml | 2 +- isideload/src/dev/devices.rs | 22 ++++++++++ isideload/src/sideload/config.rs | 46 ++++++++++++++++++++ isideload/src/sideload/mod.rs | 39 ++++++++++++++++- 7 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 isideload/src/sideload/config.rs diff --git a/Cargo.lock b/Cargo.lock index d23a994..8a97a6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -399,6 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -407,6 +423,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-macro" version = "0.3.31" @@ -436,10 +469,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -733,6 +769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4031af51250d2f22f61a0d7fb7ea71ba8b6144b2b9dd3b7ee4a931fccbd1ec0" dependencies = [ "base64", + "futures", "plist", "plist-macro", "rustls", @@ -924,6 +961,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" name = "minimal" version = "0.1.0" dependencies = [ + "idevice", "isideload", "plist", "plist-macro", diff --git a/examples/minimal/Cargo.toml b/examples/minimal/Cargo.toml index 8b737d3..89f7a4d 100644 --- a/examples/minimal/Cargo.toml +++ b/examples/minimal/Cargo.toml @@ -10,3 +10,4 @@ plist-macro = "0.1.3" tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros"] } tracing = "0.1.44" tracing-subscriber = "0.3.22" +idevice = { version = "0.1.52", features = ["usbmuxd"]} \ No newline at end of file diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index 03f1caa..eb40aee 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -1,12 +1,11 @@ -use std::env; +use std::{env, path::PathBuf}; +use idevice::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}; use isideload::{ anisette::remote_v3::RemoteV3AnisetteProvider, auth::apple_account::AppleAccount, - dev::{ - certificates::CertificatesApi, - developer_session::{DeveloperSession, TeamsApi}, - }, + dev::developer_session::DeveloperSession, + sideload::{SideloadConfiguration, TeamSelection, sideload_app}, }; use tracing::Level; @@ -21,14 +20,15 @@ async fn main() { tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let args: Vec = env::args().collect(); - // let _app_path = PathBuf::from( - // args.get(1) - // .expect("Please provide the path to the app to install"), - // ); + let apple_id = args .get(1) .expect("Please provide the Apple ID to use for installation"); let apple_password = args.get(2).expect("Please provide the Apple ID password"); + let app_path = PathBuf::from( + args.get(3) + .expect("Please provide the path to the app to install"), + ); let get_2fa_code = || { let mut code = String::new(); @@ -53,25 +53,46 @@ async fn main() { .await .expect("Failed to create developer session"); - let teams = dev_session - .list_teams() - .await - .expect("Failed to list teams"); + let usbmuxd = UsbmuxdConnection::default().await; + if usbmuxd.is_err() { + panic!("Failed to connect to usbmuxd: {:?}", usbmuxd.err()); + } + let mut usbmuxd = usbmuxd.unwrap(); - let team = teams - .get(0) - .expect("No developer teams available for this account"); + let devs = usbmuxd.get_devices().await.unwrap(); + if devs.is_empty() { + panic!("No devices found"); + } - // let app_ids = dev_session - // .list_app_ids(team, None) - // .await - // .expect("Failed to add appid"); - // let app_id = app_ids.app_ids.get(0).cloned().unwrap(); + let provider = devs + .iter() + .next() + .unwrap() + .to_provider(UsbmuxdAddr::from_env_var().unwrap(), "isideload-demo"); - let res = dev_session - .list_all_development_certs(team, None) - .await - .expect("Failed to list dev certs"); + let sideload_config = + SideloadConfiguration::builder().team_selection(TeamSelection::Prompt(|teams| { + println!("Please select a team:"); + for (index, team) in teams.iter().enumerate() { + println!( + "{}: {} ({})", + index + 1, + team.name.as_deref().unwrap_or(""), + team.team_id + ); + } + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); + let selection = input.trim().parse::().ok()?; + if selection == 0 || selection > teams.len() { + return None; + } + Some(teams[selection - 1].team_id.clone()) + })); - println!("{:?}", res); + let result = sideload_app(&provider, &mut dev_session, app_path, &sideload_config).await; + match result { + Ok(_) => println!("App installed successfully"), + Err(e) => panic!("Failed to install app: {:?}", e), + } } diff --git a/isideload/Cargo.toml b/isideload/Cargo.toml index 45726e5..53a4db4 100644 --- a/isideload/Cargo.toml +++ b/isideload/Cargo.toml @@ -15,7 +15,7 @@ default = ["install"] install = ["dep:idevice"] [dependencies] -idevice = { version = "0.1.51", optional = true } +idevice = { version = "0.1.52", optional = true } plist = "1.8" plist-macro = "0.1.3" reqwest = { version = "0.13.1", features = ["json", "gzip"] } diff --git a/isideload/src/dev/devices.rs b/isideload/src/dev/devices.rs index 6c57559..4a5c37d 100644 --- a/isideload/src/dev/devices.rs +++ b/isideload/src/dev/devices.rs @@ -6,6 +6,7 @@ use crate::dev::{ use plist_macro::plist; use rootcause::prelude::*; use serde::Deserialize; +use tracing::info; #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] @@ -59,6 +60,27 @@ pub trait DevicesApi { Ok(device) } + + // TODO: This can be skipped if we know the device is already registered + /// Check if the device is a development device, and add it if not + async fn ensure_device_registered( + &mut self, + team: &DeveloperTeam, + name: &str, + udid: &str, + device_type: impl Into> + Send, + ) -> Result<(), Report> { + let device_type = device_type.into(); + let devices = self.list_devices(team, device_type.clone()).await?; + + if !devices.iter().any(|d| d.device_number == udid) { + info!("Registering development device"); + self.add_device(team, name, udid, device_type).await?; + } + info!("Device is a development device"); + + Ok(()) + } } impl DevicesApi for DeveloperSession { diff --git a/isideload/src/sideload/config.rs b/isideload/src/sideload/config.rs new file mode 100644 index 0000000..a09f075 --- /dev/null +++ b/isideload/src/sideload/config.rs @@ -0,0 +1,46 @@ +use std::fmt::Display; + +use crate::dev::teams::DeveloperTeam; + +/// Configuration for selecting a developer team during sideloading +/// +/// If there is only one team, it will be selected automatically regardless of this setting. +/// If there are multiple teams, the behavior will depend on this setting. +pub enum TeamSelection { + /// Select the first team automatically + First, + /// Prompt the user to select a team + Prompt(fn(&Vec) -> Option), +} + +impl Display for TeamSelection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TeamSelection::First => write!(f, "first team"), + TeamSelection::Prompt(_) => write!(f, "prompting for team"), + } + } +} + +pub struct SideloadConfiguration { + pub team_selection: TeamSelection, +} + +impl Default for SideloadConfiguration { + fn default() -> Self { + SideloadConfiguration { + team_selection: TeamSelection::First, + } + } +} + +impl SideloadConfiguration { + pub fn builder() -> Self { + Self::default() + } + + pub fn team_selection(mut self, selection: TeamSelection) -> Self { + self.team_selection = selection; + self + } +} diff --git a/isideload/src/sideload/mod.rs b/isideload/src/sideload/mod.rs index baaf66d..eb0af97 100644 --- a/isideload/src/sideload/mod.rs +++ b/isideload/src/sideload/mod.rs @@ -2,15 +2,50 @@ use std::path::PathBuf; use idevice::provider::IdeviceProvider; use rootcause::prelude::*; +use tracing::info; -use crate::dev::developer_session::DeveloperSession; +use crate::dev::teams::TeamsApi; +use crate::dev::{developer_session::DeveloperSession, devices::DevicesApi}; use crate::util::device::IdeviceInfo; +pub mod config; +pub use config::{SideloadConfiguration, TeamSelection}; + pub async fn sideload_app( device_provider: &impl IdeviceProvider, - dev_session: &DeveloperSession, + dev_session: &mut DeveloperSession, app_path: PathBuf, + config: &SideloadConfiguration, ) -> Result<(), Report> { let device_info = IdeviceInfo::from_device(device_provider).await?; + + let teams = dev_session.list_teams().await?; + let team = match teams.len() { + 0 => { + bail!("No developer teams available") + } + 1 => &teams[0], + _ => { + info!( + "Multiple developer teams found, {} as per configuration", + config.team_selection + ); + match &config.team_selection { + TeamSelection::First => &teams[0], + TeamSelection::Prompt(prompt_fn) => { + let selection = prompt_fn(&teams).ok_or_else(|| report!("No team selected"))?; + teams + .iter() + .find(|t| t.team_id == selection) + .ok_or_else(|| report!("No team found with ID {}", selection))? + } + } + } + }; + + dev_session + .ensure_device_registered(team, &device_info.name, &device_info.udid, None) + .await?; + Ok(()) }