fix: select best match from --device (#4581)
* feat: add codesigning to ios * add search for android too * install toolchains for every target * install toolchains automatically
This commit is contained in:
1 parent
b067befb05
commit
a7805b8e2a
11 files changed
+621
-364
No files matched your search
Generated
+22
@@ -5137,6 +5137,7 @@ dependencies = [
|
|||||||
"memmap",
|
"memmap",
|
||||||
"memoize",
|
"memoize",
|
||||||
"notify",
|
"notify",
|
||||||
|
"nucleo",
|
||||||
"object 0.37.1",
|
"object 0.37.1",
|
||||||
"open",
|
"open",
|
||||||
"path-absolutize",
|
"path-absolutize",
|
||||||
@@ -11006,6 +11007,27 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nucleo"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
|
||||||
|
dependencies = [
|
||||||
|
"nucleo-matcher",
|
||||||
|
"parking_lot",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nucleo-matcher"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ posthog-rs = "0.3.5"
|
|||||||
serde_json5 = "0.2.1"
|
serde_json5 = "0.2.1"
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
sentry-backtrace = "0.42.0"
|
sentry-backtrace = "0.42.0"
|
||||||
|
nucleo = "0.5.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = { version = "0.8.0", features = ["git2"] }
|
built = { version = "0.8.0", features = ["git2"] }
|
||||||
|
|||||||
+295
-316
@@ -402,6 +402,7 @@ impl AppBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
BuildStage::Bundling => tracing::info!("Bundling app..."),
|
BuildStage::Bundling => tracing::info!("Bundling app..."),
|
||||||
|
BuildStage::CodeSigning => tracing::info!("Code signing app..."),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,16 +561,16 @@ impl AppBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BundleFormat::Ios => {
|
BundleFormat::Ios => {
|
||||||
if self.build.device {
|
if let Some(device) = self.build.device_name.as_deref() {
|
||||||
self.codesign_ios().await?;
|
self.open_ios_device(device).await?
|
||||||
self.open_ios_device().await?
|
|
||||||
} else {
|
} else {
|
||||||
self.open_ios_sim(envs).await?
|
self.open_ios_sim(envs).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BundleFormat::Android => {
|
BundleFormat::Android => {
|
||||||
self.open_android_sim(false, devserver_ip, envs).await?;
|
self.open_android(false, devserver_ip, envs, self.build.device_name.clone())
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are all just basically running the main exe, but with slightly different resource dir paths
|
// These are all just basically running the main exe, but with slightly different resource dir paths
|
||||||
@@ -912,335 +913,252 @@ impl AppBuilder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We have this whole thing figured out, but we don't actually use it yet.
|
/// Upload the app to the device and launch it
|
||||||
///
|
async fn open_ios_device(&self, device_query: &str) -> Result<()> {
|
||||||
/// Launching on devices is more complicated and requires us to codesign the app, which we don't
|
let device_query = device_query.to_string();
|
||||||
/// currently do.
|
let root_dir = self.build.root_dir().clone();
|
||||||
///
|
tokio::task::spawn(async move {
|
||||||
/// Converting these commands shouldn't be too hard, but device support would imply we need
|
// 1. Find an active device
|
||||||
/// better support for codesigning and entitlements.
|
let device_uuid = Self::get_ios_device_uuid(&device_query).await?;
|
||||||
async fn open_ios_device(&self) -> Result<()> {
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
// 1. Find an active device
|
tracing::info!("Uploading app to iOS device, this might take a while...");
|
||||||
let device_uuid = get_device_uuid().await?;
|
|
||||||
|
|
||||||
// 2. Get the installation URL of the app
|
// 2. Get the installation URL of the app
|
||||||
let installation_url = get_installation_url(&device_uuid, &self.build.root_dir()).await?;
|
let installation_url = Self::get_ios_installation_url(&device_uuid, &root_dir).await?;
|
||||||
|
|
||||||
// 3. Launch the app into the background, paused
|
// 3. Launch the app into the background, paused
|
||||||
launch_app_paused(&device_uuid, &installation_url).await?;
|
Self::launch_ios_app_paused(&device_uuid, &installation_url).await?;
|
||||||
|
|
||||||
async fn get_device_uuid() -> Result<String> {
|
Result::Ok(()) as Result<()>
|
||||||
let tmpfile = tempfile::NamedTempFile::new()
|
});
|
||||||
.context("Failed to create temporary file for device list")?;
|
|
||||||
|
|
||||||
Command::new("xcrun")
|
|
||||||
.args([
|
|
||||||
"devicectl".to_string(),
|
|
||||||
"list".to_string(),
|
|
||||||
"devices".to_string(),
|
|
||||||
"--json-output".to_string(),
|
|
||||||
tmpfile.path().to_str().unwrap().to_string(),
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
|
||||||
.context("Failed to parse xcrun output")?;
|
|
||||||
let device_uuid = json["result"]["devices"][0]["identifier"]
|
|
||||||
.as_str()
|
|
||||||
.context("Failed to extract device UUID")?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(device_uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
|
|
||||||
let tmpfile = tempfile::NamedTempFile::new()
|
|
||||||
.context("Failed to create temporary file for device list")?;
|
|
||||||
|
|
||||||
// xcrun devicectl device install app --device <uuid> --path <path> --json-output
|
|
||||||
let output = Command::new("xcrun")
|
|
||||||
.args([
|
|
||||||
"devicectl",
|
|
||||||
"device",
|
|
||||||
"install",
|
|
||||||
"app",
|
|
||||||
"--device",
|
|
||||||
device_uuid,
|
|
||||||
&app_path.display().to_string(),
|
|
||||||
"--json-output",
|
|
||||||
])
|
|
||||||
.arg(tmpfile.path())
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
bail!(
|
|
||||||
"Failed to install app: {}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
|
||||||
.context("Failed to parse xcrun output")?;
|
|
||||||
let installation_url = json["result"]["installedApplications"][0]["installationURL"]
|
|
||||||
.as_str()
|
|
||||||
.context("Failed to extract installation URL from xcrun output")?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(installation_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
|
|
||||||
let tmpfile = tempfile::NamedTempFile::new()
|
|
||||||
.context("Failed to create temporary file for device list")?;
|
|
||||||
|
|
||||||
let output = Command::new("xcrun")
|
|
||||||
.args([
|
|
||||||
"devicectl",
|
|
||||||
"device",
|
|
||||||
"process",
|
|
||||||
"launch",
|
|
||||||
"--no-activate",
|
|
||||||
"--verbose",
|
|
||||||
"--device",
|
|
||||||
device_uuid,
|
|
||||||
installation_url,
|
|
||||||
"--json-output",
|
|
||||||
])
|
|
||||||
.arg(tmpfile.path())
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
bail!("Failed to launch app: {output:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
|
||||||
.context("Failed to parse xcrun output")?;
|
|
||||||
|
|
||||||
let status_pid = json["result"]["process"]["processIdentifier"]
|
|
||||||
.as_u64()
|
|
||||||
.context("Failed to extract process identifier")?;
|
|
||||||
|
|
||||||
let output = Command::new("xcrun")
|
|
||||||
.args([
|
|
||||||
"devicectl",
|
|
||||||
"device",
|
|
||||||
"process",
|
|
||||||
"resume",
|
|
||||||
"--device",
|
|
||||||
device_uuid,
|
|
||||||
"--pid",
|
|
||||||
&status_pid.to_string(),
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
bail!("Failed to resume app: {output:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn codesign_ios(&self) -> Result<()> {
|
/// Parse the xcrun output to get the device based on its name and connected state.
|
||||||
// We don't want to drop the entitlements file, until the end of the block, so we hoist it to this temporary.
|
///
|
||||||
let mut _saved_entitlements = None;
|
/// ```json, ignore
|
||||||
|
/// "connectionProperties" : {
|
||||||
|
/// "authenticationType" : "manualPairing",
|
||||||
|
/// "isMobileDeviceOnly" : false,
|
||||||
|
/// "lastConnectionDate" : "2025-08-15T01:46:43.182Z",
|
||||||
|
/// "pairingState" : "paired",
|
||||||
|
/// "potentialHostnames" : [
|
||||||
|
/// "00008130-0002058401E8001C.coredevice.local",
|
||||||
|
/// "67054C13-C6C8-5AC2-B967-24C040AD3F17.coredevice.local"
|
||||||
|
/// ],
|
||||||
|
/// "transportType" : "localNetwork",
|
||||||
|
/// "tunnelState" : "disconnected",
|
||||||
|
/// "tunnelTransportProtocol" : "tcp"
|
||||||
|
/// },
|
||||||
|
/// "deviceProperties" : {
|
||||||
|
/// "bootedFromSnapshot" : true,
|
||||||
|
/// "bootedSnapshotName" : "com.apple.os.update-A771E2B3E8C155D1B1188896B3247851B64737ACDE91A5B6F6C1F03A541406AA",
|
||||||
|
/// "ddiServicesAvailable" : false,
|
||||||
|
/// "developerModeStatus" : "enabled",
|
||||||
|
/// "hasInternalOSBuild" : false,
|
||||||
|
/// "name" : "Jon’s iPhone (2)",
|
||||||
|
/// "osBuildUpdate" : "22G86",
|
||||||
|
/// "osVersionNumber" : "18.6",
|
||||||
|
/// "rootFileSystemIsWritable" : false
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
async fn get_ios_device_uuid(device_name_query: &str) -> Result<String> {
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
let mut app_dev_name = self.build.apple_team_id.clone();
|
let tmpfile = tempfile::NamedTempFile::new()
|
||||||
if app_dev_name.is_none() {
|
.context("Failed to create temporary file for device list")?;
|
||||||
app_dev_name =
|
|
||||||
Some(self.auto_provision_signing_name().await.context(
|
|
||||||
"Failed to automatically provision signing name for iOS codesigning.",
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entitlements_file = self.build.apple_entitlements.clone();
|
Command::new("xcrun")
|
||||||
if entitlements_file.is_none() {
|
|
||||||
let entitlements_xml = self
|
|
||||||
.auto_provision_entitlements()
|
|
||||||
.await
|
|
||||||
.context("Failed to auto-provision entitlements for iOS codesigning.")?;
|
|
||||||
let entitlements_temp_file = tempfile::NamedTempFile::new()?;
|
|
||||||
std::fs::write(entitlements_temp_file.path(), entitlements_xml)?;
|
|
||||||
entitlements_file = Some(entitlements_temp_file.path().to_path_buf());
|
|
||||||
_saved_entitlements = Some(entitlements_temp_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
let entitlements_file = entitlements_file.as_ref().context(
|
|
||||||
"No entitlements file provided and could not provision entitlements to sign app.",
|
|
||||||
)?;
|
|
||||||
let app_dev_name = app_dev_name.as_ref().context(
|
|
||||||
"No Apple Development signing name provided and could not auto-provision one.",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
"Codesigning iOS app with entitlements: {} and dev name: {}",
|
|
||||||
entitlements_file.display(),
|
|
||||||
app_dev_name
|
|
||||||
);
|
|
||||||
|
|
||||||
// codesign the app
|
|
||||||
let output = Command::new("codesign")
|
|
||||||
.args([
|
.args([
|
||||||
"--force",
|
"devicectl".to_string(),
|
||||||
"--entitlements",
|
"list".to_string(),
|
||||||
entitlements_file.to_str().unwrap(),
|
"devices".to_string(),
|
||||||
"--sign",
|
"--json-output".to_string(),
|
||||||
app_dev_name,
|
tmpfile.path().to_str().unwrap().to_string(),
|
||||||
])
|
])
|
||||||
.arg(self.build.root_dir())
|
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to codesign the app - is `codesign` in your path?")?;
|
|
||||||
|
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
||||||
|
.context("Failed to parse xcrun output")?;
|
||||||
|
|
||||||
|
let devices = json
|
||||||
|
.get("result")
|
||||||
|
.context("Failed to parse xcrun output")?
|
||||||
|
.get("devices")
|
||||||
|
.context("Failed to parse xcrun output")?
|
||||||
|
.as_array()
|
||||||
|
.context("Failed to get devices from xcrun output")?;
|
||||||
|
|
||||||
|
// by default, we just pick the first available device and then look for better fits.
|
||||||
|
let mut device_idx = 0;
|
||||||
|
|
||||||
|
match device_name_query.is_empty() {
|
||||||
|
// If the user provided a query, then we look through the device list looking for the right one.
|
||||||
|
// This searches both UUIDs and names, making it possible to paste an ID or a name.
|
||||||
|
false => {
|
||||||
|
use nucleo::{chars, Config, Matcher, Utf32Str};
|
||||||
|
let normalize = |c: char| chars::to_lower_case(chars::normalize(c));
|
||||||
|
let mut matcher = Matcher::new(Config::DEFAULT);
|
||||||
|
let mut best_score = 0;
|
||||||
|
let needle = device_name_query.chars().map(normalize).collect::<String>();
|
||||||
|
for (idx, device) in devices.iter().enumerate() {
|
||||||
|
let device_name = device
|
||||||
|
.get("deviceProperties")
|
||||||
|
.and_then(|f| f.get("name"))
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let device_uuid = device
|
||||||
|
.get("identifier")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let haystack = format!("{device_name} {device_uuid}")
|
||||||
|
.chars()
|
||||||
|
.map(normalize)
|
||||||
|
.collect::<String>();
|
||||||
|
let name_score = matcher.fuzzy_match(
|
||||||
|
Utf32Str::Ascii(haystack.as_bytes()),
|
||||||
|
Utf32Str::Ascii(needle.as_bytes()),
|
||||||
|
);
|
||||||
|
if let Some(score) = name_score {
|
||||||
|
if score > best_score {
|
||||||
|
best_score = score;
|
||||||
|
device_idx = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best_score == 0 {
|
||||||
|
tracing::warn!(
|
||||||
|
"No device found matching query: {device_name_query}. Using first available device."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the query is empty, then we just find the first connected/available device
|
||||||
|
// This is somewhat based on the bundle format, since we don't want to accidentally upload
|
||||||
|
// iOS apps to watches/tvs
|
||||||
|
true => {
|
||||||
|
for (idx, device) in devices.iter().enumerate() {
|
||||||
|
let is_paired = device
|
||||||
|
.get("connectionProperties")
|
||||||
|
.and_then(|g| g.get("pairingState"))
|
||||||
|
.map(|s| s.as_str() == Some("paired"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let is_ios_device = matches!(
|
||||||
|
device.get("deviceType").and_then(|s| s.as_str()),
|
||||||
|
Some("iPhone") | Some("iPad") | Some("iPod")
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_paired && is_ios_device {
|
||||||
|
device_idx = idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices
|
||||||
|
.get(device_idx)
|
||||||
|
.context("No devices found")?
|
||||||
|
.get("identifier")
|
||||||
|
.and_then(|id| id.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.context("Failed to extract device UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_ios_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
|
||||||
|
let tmpfile = tempfile::NamedTempFile::new()
|
||||||
|
.context("Failed to create temporary file for device list")?;
|
||||||
|
|
||||||
|
// xcrun devicectl device install app --device <uuid> --path <path> --json-output
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args([
|
||||||
|
"devicectl",
|
||||||
|
"device",
|
||||||
|
"install",
|
||||||
|
"app",
|
||||||
|
"--device",
|
||||||
|
device_uuid,
|
||||||
|
&app_path.display().to_string(),
|
||||||
|
"--json-output",
|
||||||
|
])
|
||||||
|
.arg(tmpfile.path())
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
"Failed to codesign the app: {}",
|
"Failed to install app: {}",
|
||||||
String::from_utf8(output.stderr).unwrap_or_default()
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
||||||
|
.context("Failed to parse xcrun output")?;
|
||||||
|
let installation_url = json["result"]["installedApplications"][0]["installationURL"]
|
||||||
|
.as_str()
|
||||||
|
.context("Failed to extract installation URL from xcrun output")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(installation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auto_provision_signing_name(&self) -> Result<String> {
|
async fn launch_ios_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
|
||||||
let identities = Command::new("security")
|
let tmpfile = tempfile::NamedTempFile::new()
|
||||||
.args(["find-identity", "-v", "-p", "codesigning"])
|
.context("Failed to create temporary file for device list")?;
|
||||||
|
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args([
|
||||||
|
"devicectl",
|
||||||
|
"device",
|
||||||
|
"process",
|
||||||
|
"launch",
|
||||||
|
"--no-activate",
|
||||||
|
"--verbose",
|
||||||
|
"--device",
|
||||||
|
device_uuid,
|
||||||
|
installation_url,
|
||||||
|
"--json-output",
|
||||||
|
])
|
||||||
|
.arg(tmpfile.path())
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to run `security find-identity -v -p codesigning` - is `security` in your path?")
|
|
||||||
.map(|e| {
|
|
||||||
String::from_utf8(e.stdout)
|
|
||||||
.context("Failed to parse `security find-identity -v -p codesigning`")
|
|
||||||
})??;
|
|
||||||
|
|
||||||
// Parsing this:
|
if !output.status.success() {
|
||||||
// 1231231231231asdasdads123123 "Apple Development: foo@gmail.com (XYZYZY)"
|
bail!("Failed to launch app: {output:?}");
|
||||||
let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
|
|
||||||
.unwrap()
|
|
||||||
.captures(&identities)
|
|
||||||
.and_then(|caps| caps.get(1))
|
|
||||||
.map(|m| m.as_str())
|
|
||||||
.context(
|
|
||||||
"Failed to find Apple Development in `security find-identity -v -p codesigning`",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(app_dev_name.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auto_provision_entitlements(&self) -> Result<String> {
|
|
||||||
const CODESIGN_ERROR: &str = r#"This is likely because you haven't
|
|
||||||
- Created a provisioning profile before
|
|
||||||
- Accepted the Apple Developer Program License Agreement
|
|
||||||
|
|
||||||
The agreement changes frequently and might need to be accepted again.
|
|
||||||
To accept the agreement, go to https://developer.apple.com/account
|
|
||||||
|
|
||||||
To create a provisioning profile, follow the instructions here:
|
|
||||||
https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
|
|
||||||
|
|
||||||
// Check the xcode 16 location first
|
|
||||||
let mut profiles_folder = dirs::home_dir()
|
|
||||||
.context("Your machine has no home-dir")?
|
|
||||||
.join("Library/Developer/Xcode/UserData/Provisioning Profiles");
|
|
||||||
|
|
||||||
// If it doesn't exist, check the old location
|
|
||||||
if !profiles_folder.exists() {
|
|
||||||
profiles_folder = dirs::home_dir()
|
|
||||||
.context("Your machine has no home-dir")?
|
|
||||||
.join("Library/MobileDevice/Provisioning Profiles");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
|
let json: serde_json::Value =
|
||||||
tracing::error!(
|
serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
|
||||||
r#"No provisioning profiles found when trying to codesign the app.
|
.context("Failed to parse xcrun output")?;
|
||||||
We checked the folders:
|
|
||||||
- XCode16: ~/Library/Developer/Xcode/UserData/Provisioning Profiles
|
|
||||||
- XCode15: ~/Library/MobileDevice/Provisioning Profiles
|
|
||||||
|
|
||||||
{CODESIGN_ERROR}
|
let status_pid = json["result"]["process"]["processIdentifier"]
|
||||||
"#
|
.as_u64()
|
||||||
)
|
.context("Failed to extract process identifier")?;
|
||||||
|
|
||||||
|
let output = Command::new("xcrun")
|
||||||
|
.args([
|
||||||
|
"devicectl",
|
||||||
|
"device",
|
||||||
|
"process",
|
||||||
|
"resume",
|
||||||
|
"--device",
|
||||||
|
device_uuid,
|
||||||
|
"--pid",
|
||||||
|
&status_pid.to_string(),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("Failed to resume app: {output:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire the provision file
|
Ok(())
|
||||||
let provision_file = profiles_folder
|
|
||||||
.read_dir()?
|
|
||||||
.flatten()
|
|
||||||
.find(|entry| {
|
|
||||||
entry
|
|
||||||
.file_name()
|
|
||||||
.to_str()
|
|
||||||
.map(|s| s.contains("mobileprovision"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
|
|
||||||
|
|
||||||
// The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
|
|
||||||
// Let's use the plist markers to find the start and end of the plist
|
|
||||||
fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
|
|
||||||
bytes
|
|
||||||
.windows(byte_match.len())
|
|
||||||
.enumerate()
|
|
||||||
.rev()
|
|
||||||
.find(|(_, slice)| *slice == byte_match)
|
|
||||||
.map(|(i, _)| i + byte_match.len())
|
|
||||||
}
|
|
||||||
let bytes = std::fs::read(provision_file.path())?;
|
|
||||||
let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
|
|
||||||
let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
|
|
||||||
.context("Failed to parse .mobileprovision file")?;
|
|
||||||
let sub_bytes = &bytes[(cut1 - 6)..cut2];
|
|
||||||
let mbfile: ProvisioningProfile =
|
|
||||||
plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
|
||||||
struct ProvisioningProfile {
|
|
||||||
#[serde(rename = "TeamIdentifier")]
|
|
||||||
team_identifier: Vec<String>,
|
|
||||||
#[serde(rename = "Entitlements")]
|
|
||||||
entitlements: Entitlements,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[serde(rename = "ApplicationIdentifierPrefix")]
|
|
||||||
application_identifier_prefix: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
|
||||||
struct Entitlements {
|
|
||||||
#[serde(rename = "application-identifier")]
|
|
||||||
application_identifier: String,
|
|
||||||
#[serde(rename = "keychain-access-groups")]
|
|
||||||
keychain_access_groups: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(format!(
|
|
||||||
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>application-identifier</key>
|
|
||||||
<string>{APPLICATION_IDENTIFIER}</string>
|
|
||||||
<key>keychain-access-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>{APP_ID_ACCESS_GROUP}.*</string>
|
|
||||||
</array>
|
|
||||||
<key>get-task-allow</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.developer.team-identifier</key>
|
|
||||||
<string>{TEAM_IDENTIFIER}</string>
|
|
||||||
</dict></plist>
|
|
||||||
"#,
|
|
||||||
APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
|
|
||||||
APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
|
|
||||||
TEAM_IDENTIFIER = mbfile.team_identifier[0],
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Launch the Android simulator and deploy the application.
|
/// Launch the Android simulator and deploy the application.
|
||||||
@@ -1278,11 +1196,12 @@ We checked the folders:
|
|||||||
///
|
///
|
||||||
/// # Resources:
|
/// # Resources:
|
||||||
/// - <https://developer.android.com/studio/run/emulator-commandline>
|
/// - <https://developer.android.com/studio/run/emulator-commandline>
|
||||||
async fn open_android_sim(
|
async fn open_android(
|
||||||
&self,
|
&self,
|
||||||
root: bool,
|
root: bool,
|
||||||
devserver_socket: SocketAddr,
|
devserver_socket: SocketAddr,
|
||||||
envs: Vec<(String, String)>,
|
envs: Vec<(String, String)>,
|
||||||
|
device_name_query: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let apk_path = self.build.debug_apk_path();
|
let apk_path = self.build.debug_apk_path();
|
||||||
let session_cache = self.build.session_cache_dir();
|
let session_cache = self.build.session_cache_dir();
|
||||||
@@ -1298,19 +1217,14 @@ We checked the folders:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = devserver_socket.port();
|
// Try to get the transport ID for the device in case there are multiple specified devices
|
||||||
if let Err(e) = Command::new(&adb)
|
// All future commands should use this since its the most recent.
|
||||||
.arg("reverse")
|
let transport_id_args =
|
||||||
.arg(format!("tcp:{port}"))
|
Self::get_android_device_transport_id(&adb, device_name_query).await;
|
||||||
.arg(format!("tcp:{port}"))
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!("failed to forward port {port}: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for device to be ready
|
// Wait for device to be ready
|
||||||
let cmd = Command::new(&adb)
|
let cmd = Command::new(&adb)
|
||||||
|
.args(transport_id_args)
|
||||||
.arg("wait-for-device")
|
.arg("wait-for-device")
|
||||||
.arg("shell")
|
.arg("shell")
|
||||||
.arg(r#"while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"#)
|
.arg(r#"while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"#)
|
||||||
@@ -1325,6 +1239,17 @@ We checked the folders:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let port = devserver_socket.port();
|
||||||
|
if let Err(e) = Command::new(&adb)
|
||||||
|
.arg("reverse")
|
||||||
|
.arg(format!("tcp:{port}"))
|
||||||
|
.arg(format!("tcp:{port}"))
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("failed to forward port {port}: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
// Install
|
// Install
|
||||||
// adb install -r app-debug.apk
|
// adb install -r app-debug.apk
|
||||||
let res = Command::new(&adb)
|
let res = Command::new(&adb)
|
||||||
@@ -1650,4 +1575,58 @@ We checked the folders:
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_android_device_transport_id(
|
||||||
|
adb: &PathBuf,
|
||||||
|
device_name_query: Option<String>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
// If there are multiple devices, we pick the one matching the query
|
||||||
|
let mut device_specifier_args = vec![];
|
||||||
|
|
||||||
|
if let Some(device_name_query) = device_name_query {
|
||||||
|
if let Ok(res) = Command::new(adb).arg("devices").arg("-l").output().await {
|
||||||
|
let devices = String::from_utf8_lossy(&res.stdout);
|
||||||
|
let mut best_score = 0;
|
||||||
|
let mut device_identifier = "".to_string();
|
||||||
|
use nucleo::{chars, Config, Matcher, Utf32Str};
|
||||||
|
let mut matcher = Matcher::new(Config::DEFAULT);
|
||||||
|
let normalize = |c: char| chars::to_lower_case(chars::normalize(c));
|
||||||
|
let needle = device_name_query.chars().map(normalize).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for line in devices.lines() {
|
||||||
|
let device_name = line.split_whitespace().next().unwrap_or("");
|
||||||
|
let Some(transport_id) = line
|
||||||
|
.split_whitespace()
|
||||||
|
.find(|s| s.starts_with("transport_id:"))
|
||||||
|
.map(|s| s.trim_start_matches("transport_id:"))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_name = device_name.chars().map(normalize).collect::<Vec<_>>();
|
||||||
|
let score = matcher
|
||||||
|
.fuzzy_match(Utf32Str::Unicode(&device_name), Utf32Str::Unicode(&needle));
|
||||||
|
if let Some(score) = score {
|
||||||
|
if score > best_score {
|
||||||
|
best_score = score;
|
||||||
|
device_identifier = transport_id.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best_score != 0 {
|
||||||
|
device_specifier_args.push("-t".to_string());
|
||||||
|
device_specifier_args.push(device_identifier.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if device_specifier_args.is_empty() {
|
||||||
|
tracing::warn!(
|
||||||
|
"No device found matching query: {device_name_query}. Using default transport ID."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device_specifier_args
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +102,12 @@ impl BuildContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn status_codesigning(&self) {
|
||||||
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
|
stage: BuildStage::CodeSigning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn status_build_diagnostic(&self, message: Diagnostic) {
|
pub(crate) fn status_build_diagnostic(&self, message: Diagnostic) {
|
||||||
_ = self
|
_ = self
|
||||||
.tx
|
.tx
|
||||||
|
|||||||
@@ -377,7 +377,8 @@ pub(crate) struct BuildRequest {
|
|||||||
pub(crate) release: bool,
|
pub(crate) release: bool,
|
||||||
pub(crate) bundle: BundleFormat,
|
pub(crate) bundle: BundleFormat,
|
||||||
pub(crate) triple: Triple,
|
pub(crate) triple: Triple,
|
||||||
pub(crate) device: bool,
|
pub(crate) device_name: Option<String>,
|
||||||
|
pub(crate) should_codesign: bool,
|
||||||
pub(crate) package: String,
|
pub(crate) package: String,
|
||||||
pub(crate) main_target: String,
|
pub(crate) main_target: String,
|
||||||
pub(crate) features: Vec<String>,
|
pub(crate) features: Vec<String>,
|
||||||
@@ -625,7 +626,7 @@ impl BuildRequest {
|
|||||||
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
|
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
|
||||||
// For now, though, since we don't have probing, it just defaults to false
|
// For now, though, since we don't have probing, it just defaults to false
|
||||||
// Tools like xcrun/adb can detect devices
|
// Tools like xcrun/adb can detect devices
|
||||||
let device = args.device;
|
let device = args.device.clone();
|
||||||
|
|
||||||
// Resolve the target alias and renderer into a concrete target alias.
|
// Resolve the target alias and renderer into a concrete target alias.
|
||||||
// If the user didn't pass a platform, but we have a renderer, get the default platform for that renderer
|
// If the user didn't pass a platform, but we have a renderer, get the default platform for that renderer
|
||||||
@@ -639,7 +640,7 @@ impl BuildRequest {
|
|||||||
// If there is an explicit target, use it
|
// If there is an explicit target, use it
|
||||||
(Some(target), _) => target,
|
(Some(target), _) => target,
|
||||||
// If there is a platform, use it to determine the target triple
|
// If there is a platform, use it to determine the target triple
|
||||||
(None, platform) => platform.into_target(device, &workspace).await?,
|
(None, platform) => platform.into_target(device.is_some(), &workspace).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the user didn't pass a renderer, and we didn't find a good default renderer, but they are using dioxus explicitly,
|
// If the user didn't pass a renderer, and we didn't find a good default renderer, but they are using dioxus explicitly,
|
||||||
@@ -682,6 +683,10 @@ impl BuildRequest {
|
|||||||
args.release,
|
args.release,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Determine if we should codesign
|
||||||
|
let should_codesign =
|
||||||
|
args.codesign || device.is_some() || args.apple_entitlements.is_some();
|
||||||
|
|
||||||
// Determining release mode is based on the profile, actually, so we need to check that
|
// Determining release mode is based on the profile, actually, so we need to check that
|
||||||
let release = workspace.is_release_profile(&profile);
|
let release = workspace.is_release_profile(&profile);
|
||||||
|
|
||||||
@@ -807,7 +812,7 @@ impl BuildRequest {
|
|||||||
crate_target,
|
crate_target,
|
||||||
profile,
|
profile,
|
||||||
triple,
|
triple,
|
||||||
device,
|
device_name: device,
|
||||||
workspace,
|
workspace,
|
||||||
config,
|
config,
|
||||||
target_dir,
|
target_dir,
|
||||||
@@ -824,6 +829,7 @@ impl BuildRequest {
|
|||||||
main_target,
|
main_target,
|
||||||
rustflags,
|
rustflags,
|
||||||
using_dioxus_explicitly,
|
using_dioxus_explicitly,
|
||||||
|
should_codesign,
|
||||||
skip_assets: args.skip_assets,
|
skip_assets: args.skip_assets,
|
||||||
base_path: args.base_path.clone(),
|
base_path: args.base_path.clone(),
|
||||||
wasm_split: args.wasm_split,
|
wasm_split: args.wasm_split,
|
||||||
@@ -3973,28 +3979,38 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
///
|
///
|
||||||
/// This might include codesigning, zipping, creating an appimage, etc
|
/// This might include codesigning, zipping, creating an appimage, etc
|
||||||
async fn assemble(&self, ctx: &BuildContext) -> Result<()> {
|
async fn assemble(&self, ctx: &BuildContext) -> Result<()> {
|
||||||
if self.bundle == BundleFormat::Android {
|
match self.bundle {
|
||||||
ctx.status_running_gradle();
|
BundleFormat::Android => {
|
||||||
|
ctx.status_running_gradle();
|
||||||
|
|
||||||
// When the build mode is set to release and there is an Android signature configuration, use assembleRelease
|
// When the build mode is set to release and there is an Android signature configuration, use assembleRelease
|
||||||
let build_type = if self.release && self.config.bundle.android.is_some() {
|
let build_type = if self.release && self.config.bundle.android.is_some() {
|
||||||
"assembleRelease"
|
"assembleRelease"
|
||||||
} else {
|
} else {
|
||||||
"assembleDebug"
|
"assembleDebug"
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = Command::new(self.gradle_exe()?)
|
let output = Command::new(self.gradle_exe()?)
|
||||||
.arg(build_type)
|
.arg(build_type)
|
||||||
.current_dir(self.root_dir())
|
.current_dir(self.root_dir())
|
||||||
.output()
|
.output()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
"Failed to assemble apk: {}",
|
"Failed to assemble apk: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
BundleFormat::Ios => {
|
||||||
|
if self.should_codesign {
|
||||||
|
ctx.status_codesigning();
|
||||||
|
self.codesign_ios().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -4209,6 +4225,8 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> {
|
pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> {
|
||||||
ctx.status_installing_tooling();
|
ctx.status_installing_tooling();
|
||||||
|
|
||||||
|
self.verify_toolchain_installed().await?;
|
||||||
|
|
||||||
match self.bundle {
|
match self.bundle {
|
||||||
BundleFormat::Web => self.verify_web_tooling().await?,
|
BundleFormat::Web => self.verify_web_tooling().await?,
|
||||||
BundleFormat::Ios => self.verify_ios_tooling().await?,
|
BundleFormat::Ios => self.verify_ios_tooling().await?,
|
||||||
@@ -4220,25 +4238,59 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_web_tooling(&self) -> Result<()> {
|
async fn verify_toolchain_installed(&self) -> Result<()> {
|
||||||
|
let toolchain_dir = self.workspace.sysroot.join("lib/rustlib");
|
||||||
|
let triple = self.triple.to_string();
|
||||||
|
|
||||||
// Install target using rustup.
|
// Install target using rustup.
|
||||||
#[cfg(not(feature = "no-downloads"))]
|
if !toolchain_dir.join(&triple).exists() {
|
||||||
if !self.workspace.has_wasm32_unknown_unknown() {
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Web platform requires wasm32-unknown-unknown to be installed. Installing..."
|
"{} platform requires {} to be installed. Installing...",
|
||||||
|
self.bundle,
|
||||||
|
triple
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = tokio::process::Command::new("rustup")
|
let mut child = tokio::process::Command::new("rustup")
|
||||||
.args(["target", "add", "wasm32-unknown-unknown"])
|
.args(["target", "add"])
|
||||||
.output()
|
.arg(&triple)
|
||||||
.await?;
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap());
|
||||||
|
let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap());
|
||||||
|
let mut stdout_lines = stdout.lines();
|
||||||
|
let mut stderr_lines = stderr.lines();
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
line = stdout_lines.next_line() => {
|
||||||
|
match line {
|
||||||
|
Ok(Some(line)) => tracing::info!("{}", line),
|
||||||
|
Err(err) => tracing::error!("{}", err),
|
||||||
|
Ok(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line = stderr_lines.next_line() => {
|
||||||
|
match line {
|
||||||
|
Ok(Some(line)) => tracing::info!("{}", line),
|
||||||
|
Err(err) => tracing::error!("{}", err),
|
||||||
|
Ok(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure target is installed.
|
// Ensure target is installed.
|
||||||
if !self.workspace.has_wasm32_unknown_unknown() {
|
if !toolchain_dir.join(&triple).exists() {
|
||||||
bail!("Missing target wasm32-unknown-unknown.");
|
bail!("Missing rust target {}", triple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_web_tooling(&self) -> Result<()> {
|
||||||
// Wasm bindgen
|
// Wasm bindgen
|
||||||
let krate_bindgen_version =
|
let krate_bindgen_version =
|
||||||
self.workspace
|
self.workspace
|
||||||
@@ -4599,7 +4651,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn start_simulators(&self) -> Result<()> {
|
pub(crate) async fn start_simulators(&self) -> Result<()> {
|
||||||
if self.device {
|
if self.device_name.is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4793,4 +4845,197 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn codesign_ios(&self) -> Result<()> {
|
||||||
|
// We don't want to drop the entitlements file, until the end of the block, so we hoist it to this temporary.
|
||||||
|
let mut _saved_entitlements = None;
|
||||||
|
|
||||||
|
let mut app_dev_name = self.apple_team_id.clone();
|
||||||
|
if app_dev_name.is_none() {
|
||||||
|
app_dev_name =
|
||||||
|
Some(Self::auto_provision_signing_name().await.context(
|
||||||
|
"Failed to automatically provision signing name for iOS codesigning.",
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entitlements_file = self.apple_entitlements.clone();
|
||||||
|
if entitlements_file.is_none() {
|
||||||
|
let entitlements_xml = Self::auto_provision_entitlements()
|
||||||
|
.await
|
||||||
|
.context("Failed to auto-provision entitlements for iOS codesigning.")?;
|
||||||
|
let entitlements_temp_file = tempfile::NamedTempFile::new()?;
|
||||||
|
std::fs::write(entitlements_temp_file.path(), entitlements_xml)?;
|
||||||
|
entitlements_file = Some(entitlements_temp_file.path().to_path_buf());
|
||||||
|
_saved_entitlements = Some(entitlements_temp_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entitlements_file = entitlements_file.as_ref().context(
|
||||||
|
"No entitlements file provided and could not provision entitlements to sign app.",
|
||||||
|
)?;
|
||||||
|
let app_dev_name = app_dev_name.as_ref().context(
|
||||||
|
"No Apple Development signing name provided and could not auto-provision one.",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Codesigning iOS app with entitlements: {} and dev name: {}",
|
||||||
|
entitlements_file.display(),
|
||||||
|
app_dev_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// codesign the app
|
||||||
|
let output = Command::new("codesign")
|
||||||
|
.args([
|
||||||
|
"--force",
|
||||||
|
"--entitlements",
|
||||||
|
entitlements_file.to_str().unwrap(),
|
||||||
|
"--sign",
|
||||||
|
app_dev_name,
|
||||||
|
])
|
||||||
|
.arg(self.root_dir())
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to codesign the app - is `codesign` in your path?")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!(
|
||||||
|
"Failed to codesign the app: {}",
|
||||||
|
String::from_utf8(output.stderr).unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auto_provision_signing_name() -> Result<String> {
|
||||||
|
let identities = Command::new("security")
|
||||||
|
.args(["find-identity", "-v", "-p", "codesigning"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to run `security find-identity -v -p codesigning` - is `security` in your path?")
|
||||||
|
.map(|e| {
|
||||||
|
String::from_utf8(e.stdout)
|
||||||
|
.context("Failed to parse `security find-identity -v -p codesigning`")
|
||||||
|
})??;
|
||||||
|
|
||||||
|
// Parsing this:
|
||||||
|
// 1231231231231asdasdads123123 "Apple Development: foo@gmail.com (XYZYZY)"
|
||||||
|
let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
|
||||||
|
.unwrap()
|
||||||
|
.captures(&identities)
|
||||||
|
.and_then(|caps| caps.get(1))
|
||||||
|
.map(|m| m.as_str())
|
||||||
|
.context(
|
||||||
|
"Failed to find Apple Development in `security find-identity -v -p codesigning`",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(app_dev_name.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auto_provision_entitlements() -> Result<String> {
|
||||||
|
const CODESIGN_ERROR: &str = r#"This is likely because you haven't
|
||||||
|
- Created a provisioning profile before
|
||||||
|
- Accepted the Apple Developer Program License Agreement
|
||||||
|
|
||||||
|
The agreement changes frequently and might need to be accepted again.
|
||||||
|
To accept the agreement, go to https://developer.apple.com/account
|
||||||
|
|
||||||
|
To create a provisioning profile, follow the instructions here:
|
||||||
|
https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
|
||||||
|
|
||||||
|
// Check the xcode 16 location first
|
||||||
|
let mut profiles_folder = dirs::home_dir()
|
||||||
|
.context("Your machine has no home-dir")?
|
||||||
|
.join("Library/Developer/Xcode/UserData/Provisioning Profiles");
|
||||||
|
|
||||||
|
// If it doesn't exist, check the old location
|
||||||
|
if !profiles_folder.exists() {
|
||||||
|
profiles_folder = dirs::home_dir()
|
||||||
|
.context("Your machine has no home-dir")?
|
||||||
|
.join("Library/MobileDevice/Provisioning Profiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
|
||||||
|
tracing::error!(
|
||||||
|
r#"No provisioning profiles found when trying to codesign the app.
|
||||||
|
We checked the folders:
|
||||||
|
- XCode16: ~/Library/Developer/Xcode/UserData/Provisioning Profiles
|
||||||
|
- XCode15: ~/Library/MobileDevice/Provisioning Profiles
|
||||||
|
|
||||||
|
{CODESIGN_ERROR}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire the provision file
|
||||||
|
let provision_file = profiles_folder
|
||||||
|
.read_dir()?
|
||||||
|
.flatten()
|
||||||
|
.find(|entry| {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.contains("mobileprovision"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
|
||||||
|
|
||||||
|
// The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
|
||||||
|
// Let's use the plist markers to find the start and end of the plist
|
||||||
|
fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
|
||||||
|
bytes
|
||||||
|
.windows(byte_match.len())
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|(_, slice)| *slice == byte_match)
|
||||||
|
.map(|(i, _)| i + byte_match.len())
|
||||||
|
}
|
||||||
|
let bytes = std::fs::read(provision_file.path())?;
|
||||||
|
let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
|
||||||
|
let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
|
||||||
|
.context("Failed to parse .mobileprovision file")?;
|
||||||
|
let sub_bytes = &bytes[(cut1 - 6)..cut2];
|
||||||
|
let mbfile: ProvisioningProfile =
|
||||||
|
plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct ProvisioningProfile {
|
||||||
|
#[serde(rename = "TeamIdentifier")]
|
||||||
|
team_identifier: Vec<String>,
|
||||||
|
#[serde(rename = "Entitlements")]
|
||||||
|
entitlements: Entitlements,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[serde(rename = "ApplicationIdentifierPrefix")]
|
||||||
|
application_identifier_prefix: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
struct Entitlements {
|
||||||
|
#[serde(rename = "application-identifier")]
|
||||||
|
application_identifier: String,
|
||||||
|
#[serde(rename = "keychain-access-groups")]
|
||||||
|
keychain_access_groups: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
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>application-identifier</key>
|
||||||
|
<string>{APPLICATION_IDENTIFIER}</string>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>{APP_ID_ACCESS_GROUP}.*</string>
|
||||||
|
</array>
|
||||||
|
<key>get-task-allow</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.developer.team-identifier</key>
|
||||||
|
<string>{TEAM_IDENTIFIER}</string>
|
||||||
|
</dict></plist>
|
||||||
|
"#,
|
||||||
|
APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
|
||||||
|
APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
|
||||||
|
TEAM_IDENTIFIER = mbfile.team_identifier[0],
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -141,12 +141,9 @@ pub(crate) fn name_from_path(path: &Path) -> Result<String> {
|
|||||||
|
|
||||||
/// Post-creation actions for newly setup crates.
|
/// Post-creation actions for newly setup crates.
|
||||||
pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
|
pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
|
||||||
let parent_dir = path.parent();
|
let metadata = if let Some(parent_dir) = path.parent() {
|
||||||
let metadata = if parent_dir.is_none() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
match cargo_metadata::MetadataCommand::new()
|
match cargo_metadata::MetadataCommand::new()
|
||||||
.current_dir(parent_dir.unwrap())
|
.current_dir(parent_dir)
|
||||||
.exec()
|
.exec()
|
||||||
{
|
{
|
||||||
Ok(v) => Some(v),
|
Ok(v) => Some(v),
|
||||||
@@ -156,6 +153,8 @@ pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
|
|||||||
anyhow::bail!("Couldn't retrieve cargo metadata: {:?}", err)
|
anyhow::bail!("Couldn't retrieve cargo metadata: {:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Add the new project to the workspace, if it exists.
|
// 1. Add the new project to the workspace, if it exists.
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ impl RunArgs {
|
|||||||
BuildStage::RunningGradle => {
|
BuildStage::RunningGradle => {
|
||||||
tracing::info!("[{bundle_format}] Running Gradle")
|
tracing::info!("[{bundle_format}] Running Gradle")
|
||||||
}
|
}
|
||||||
|
BuildStage::CodeSigning => {
|
||||||
|
tracing::info!("[{bundle_format}] Code signing app")
|
||||||
|
}
|
||||||
BuildStage::Success => {}
|
BuildStage::Success => {}
|
||||||
BuildStage::Restarting => {}
|
BuildStage::Restarting => {}
|
||||||
BuildStage::CompressingAssets => {}
|
BuildStage::CompressingAssets => {}
|
||||||
|
|||||||
@@ -108,16 +108,22 @@ pub(crate) struct TargetArgs {
|
|||||||
#[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)]
|
#[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)]
|
||||||
pub(crate) debug_symbols: bool,
|
pub(crate) debug_symbols: bool,
|
||||||
|
|
||||||
/// Are we building for a device or just the simulator.
|
/// The name of the device we are hoping to upload to. By default, dx tries to upload to the active
|
||||||
/// If device is false, then we'll build for the simulator
|
/// simulator. If the device name is passed, we will upload to that device instead.
|
||||||
#[clap(long, default_value_t = false, help_heading = HELP_HEADING)]
|
///
|
||||||
pub(crate) device: bool,
|
/// This performs a search among devices, and fuzzy matches might be found.
|
||||||
|
#[arg(long, default_missing_value=None, num_args=0..=1)]
|
||||||
|
pub(crate) device: Option<String>,
|
||||||
|
|
||||||
/// The base path the build will fetch assets relative to. This will override the
|
/// The base path the build will fetch assets relative to. This will override the
|
||||||
/// base path set in the `dioxus` config.
|
/// base path set in the `dioxus` config.
|
||||||
#[clap(long, help_heading = HELP_HEADING)]
|
#[clap(long, help_heading = HELP_HEADING)]
|
||||||
pub(crate) base_path: Option<String>,
|
pub(crate) base_path: Option<String>,
|
||||||
|
|
||||||
|
/// Should dx attempt to codesign the app bundle?
|
||||||
|
#[clap(long, default_value_t = false, help_heading = HELP_HEADING, num_args = 0..=1)]
|
||||||
|
pub(crate) codesign: bool,
|
||||||
|
|
||||||
/// The path to the Apple entitlements file to used to sign the resulting app bundle.
|
/// The path to the Apple entitlements file to used to sign the resulting app bundle.
|
||||||
///
|
///
|
||||||
/// On iOS, this is required for deploy to a device and some configurations in the simulator.
|
/// On iOS, this is required for deploy to a device and some configurations in the simulator.
|
||||||
|
|||||||
@@ -546,6 +546,7 @@ impl Output {
|
|||||||
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
|
BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()),
|
||||||
BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
|
BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
|
||||||
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
|
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
|
||||||
|
BuildStage::CodeSigning => lines.push("Code signing app".yellow()),
|
||||||
BuildStage::Bundling => lines.push("Bundling app".yellow()),
|
BuildStage::Bundling => lines.push("Bundling app".yellow()),
|
||||||
BuildStage::CopyingAssets {
|
BuildStage::CopyingAssets {
|
||||||
current,
|
current,
|
||||||
|
|||||||
@@ -273,12 +273,6 @@ impl Workspace {
|
|||||||
.join("gcc-ld")
|
.join("gcc-ld")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_wasm32_unknown_unknown(&self) -> bool {
|
|
||||||
self.sysroot
|
|
||||||
.join("lib/rustlib/wasm32-unknown-unknown")
|
|
||||||
.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the "main" package in the workspace. There might not be one!
|
/// Find the "main" package in the workspace. There might not be one!
|
||||||
pub fn find_main_package(&self, package: Option<String>) -> Result<NodeId> {
|
pub fn find_main_package(&self, package: Option<String>) -> Result<NodeId> {
|
||||||
if let Some(package) = package {
|
if let Some(package) = package {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ pub enum BuildStage {
|
|||||||
},
|
},
|
||||||
Bundling,
|
Bundling,
|
||||||
RunningGradle,
|
RunningGradle,
|
||||||
|
CodeSigning,
|
||||||
Success,
|
Success,
|
||||||
Failed,
|
Failed,
|
||||||
Aborted,
|
Aborted,
|
||||||
|
|||||||
Reference in new issue
Block a user