123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850 |
- use crate::{AppBundle, DioxusCrate, Platform, Result};
- use anyhow::Context;
- use dioxus_cli_opt::process_file_to;
- use std::{
- net::SocketAddr,
- path::{Path, PathBuf},
- process::Stdio,
- };
- use tokio::{
- io::{AsyncBufReadExt, BufReader, Lines},
- process::{Child, ChildStderr, ChildStdout, Command},
- };
- /// A handle to a running app.
- ///
- /// Also includes a handle to its server if it exists.
- /// The actual child processes might not be present (web) or running (died/killed).
- ///
- /// The purpose of this struct is to accumulate state about the running app and its server, like
- /// any runtime information needed to hotreload the app or send it messages.
- ///
- /// We might want to bring in websockets here too, so we know the exact channels the app is using to
- /// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super
- /// duper useful.
- ///
- /// todo: restructure this such that "open" is a running task instead of blocking the main thread
- pub(crate) struct AppHandle {
- pub(crate) app: AppBundle,
- // These might be None if the app died or the user did not specify a server
- pub(crate) app_child: Option<Child>,
- pub(crate) server_child: Option<Child>,
- // stdio for the app so we can read its stdout/stderr
- // we don't map stdin today (todo) but most apps don't need it
- pub(crate) app_stdout: Option<Lines<BufReader<ChildStdout>>>,
- pub(crate) app_stderr: Option<Lines<BufReader<ChildStderr>>>,
- pub(crate) server_stdout: Option<Lines<BufReader<ChildStdout>>>,
- pub(crate) server_stderr: Option<Lines<BufReader<ChildStderr>>>,
- /// The executables but with some extra entropy in their name so we can run two instances of the
- /// same app without causing collisions on the filesystem.
- pub(crate) entropy_app_exe: Option<PathBuf>,
- pub(crate) entropy_server_exe: Option<PathBuf>,
- /// The virtual directory that assets will be served from
- /// Used mostly for apk/ipa builds since they live in simulator
- pub(crate) runtime_asst_dir: Option<PathBuf>,
- }
- impl AppHandle {
- pub async fn new(app: AppBundle) -> Result<Self> {
- Ok(AppHandle {
- app,
- runtime_asst_dir: None,
- app_child: None,
- app_stderr: None,
- app_stdout: None,
- server_child: None,
- server_stdout: None,
- server_stderr: None,
- entropy_app_exe: None,
- entropy_server_exe: None,
- })
- }
- pub(crate) async fn open(
- &mut self,
- devserver_ip: SocketAddr,
- start_fullstack_on_address: Option<SocketAddr>,
- open_browser: bool,
- ) -> Result<()> {
- let krate = &self.app.build.krate;
- // Set the env vars that the clients will expect
- // These need to be stable within a release version (ie 0.6.0)
- let mut envs = vec![
- (dioxus_cli_config::CLI_ENABLED_ENV, "true".to_string()),
- (
- dioxus_cli_config::ALWAYS_ON_TOP_ENV,
- krate.settings.always_on_top.unwrap_or(true).to_string(),
- ),
- (
- dioxus_cli_config::APP_TITLE_ENV,
- krate.config.web.app.title.clone(),
- ),
- ("RUST_BACKTRACE", "1".to_string()),
- (
- dioxus_cli_config::DEVSERVER_IP_ENV,
- devserver_ip.ip().to_string(),
- ),
- (
- dioxus_cli_config::DEVSERVER_PORT_ENV,
- devserver_ip.port().to_string(),
- ),
- // unset the cargo dirs in the event we're running `dx` locally
- // since the child process will inherit the env vars, we don't want to confuse the downstream process
- ("CARGO_MANIFEST_DIR", "".to_string()),
- (
- dioxus_cli_config::SESSION_CACHE_DIR,
- self.app
- .build
- .krate
- .session_cache_dir()
- .display()
- .to_string(),
- ),
- ];
- if let Some(base_path) = &krate.config.web.app.base_path {
- envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.clone()));
- }
- // Launch the server if we were given an address to start it on, and the build includes a server. After we
- // start the server, consume its stdout/stderr.
- if let (Some(addr), Some(server)) = (start_fullstack_on_address, self.server_exe()) {
- tracing::debug!("Proxying fullstack server from port {:?}", addr);
- envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string()));
- envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()));
- tracing::debug!("Launching server from path: {server:?}");
- let mut child = Command::new(server)
- .envs(envs.clone())
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .kill_on_drop(true)
- .spawn()?;
- let stdout = BufReader::new(child.stdout.take().unwrap());
- let stderr = BufReader::new(child.stderr.take().unwrap());
- self.server_stdout = Some(stdout.lines());
- self.server_stderr = Some(stderr.lines());
- self.server_child = Some(child);
- }
- // We try to use stdin/stdout to communicate with the app
- let running_process = match self.app.build.build.platform() {
- // Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead
- // use use the websocket to communicate with it. I wish we could merge the concepts here,
- // like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else.
- Platform::Web => {
- // Only the first build we open the web app, after that the user knows it's running
- if open_browser {
- self.open_web(devserver_ip);
- }
- None
- }
- Platform::Ios => Some(self.open_ios_sim(envs).await?),
- // https://developer.android.com/studio/run/emulator-commandline
- Platform::Android => {
- self.open_android_sim(devserver_ip, envs).await;
- None
- }
- // These are all just basically running the main exe, but with slightly different resource dir paths
- Platform::Server
- | Platform::MacOS
- | Platform::Windows
- | Platform::Linux
- | Platform::Liveview => Some(self.open_with_main_exe(envs)?),
- };
- // If we have a running process, we need to attach to it and wait for its outputs
- if let Some(mut child) = running_process {
- let stdout = BufReader::new(child.stdout.take().unwrap());
- let stderr = BufReader::new(child.stderr.take().unwrap());
- self.app_stdout = Some(stdout.lines());
- self.app_stderr = Some(stderr.lines());
- self.app_child = Some(child);
- }
- Ok(())
- }
- /// Gracefully kill the process and all of its children
- ///
- /// Uses the `SIGTERM` signal on unix and `taskkill` on windows.
- /// This complex logic is necessary for things like window state preservation to work properly.
- ///
- /// Also wipes away the entropy executables if they exist.
- pub(crate) async fn cleanup(&mut self) {
- // Soft-kill the process by sending a sigkill, allowing the process to clean up
- self.soft_kill().await;
- // Wipe out the entropy executables if they exist
- if let Some(entropy_app_exe) = self.entropy_app_exe.take() {
- _ = std::fs::remove_file(entropy_app_exe);
- }
- if let Some(entropy_server_exe) = self.entropy_server_exe.take() {
- _ = std::fs::remove_file(entropy_server_exe);
- }
- }
- /// Kill the app and server exes
- pub(crate) async fn soft_kill(&mut self) {
- use futures_util::FutureExt;
- // Kill any running executables on Windows
- let server_process = self.server_child.take();
- let client_process = self.app_child.take();
- let processes = [server_process, client_process]
- .into_iter()
- .flatten()
- .collect::<Vec<_>>();
- for mut process in processes {
- let Some(pid) = process.id() else {
- _ = process.kill().await;
- continue;
- };
- // on unix, we can send a signal to the process to shut down
- #[cfg(unix)]
- {
- _ = Command::new("kill")
- .args(["-s", "TERM", &pid.to_string()])
- .spawn();
- }
- // on windows, use the `taskkill` command
- #[cfg(windows)]
- {
- _ = Command::new("taskkill")
- .args(["/F", "/PID", &pid.to_string()])
- .spawn();
- }
- // join the wait with a 100ms timeout
- futures_util::select! {
- _ = process.wait().fuse() => {}
- _ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {}
- };
- }
- }
- /// Hotreload an asset in the running app.
- ///
- /// This will modify the build dir in place! Be careful! We generally assume you want all bundles
- /// to reflect the latest changes, so we will modify the bundle.
- ///
- /// However, not all platforms work like this, so we might also need to update a separate asset
- /// dir that the system simulator might be providing. We know this is the case for ios simulators
- /// and haven't yet checked for android.
- ///
- /// This will return the bundled name of the asset such that we can send it to the clients letting
- /// them know what to reload. It's not super important that this is robust since most clients will
- /// kick all stylsheets without necessarily checking the name.
- pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option<PathBuf> {
- let mut bundled_name = None;
- // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
- // we won't actually be using the build dir.
- let asset_dir = match self.runtime_asst_dir.as_ref() {
- Some(dir) => dir.to_path_buf().join("assets/"),
- None => self.app.build.asset_dir(),
- };
- tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
- // If the asset shares the same name in the bundle, reload that
- if let Some(legacy_asset_dir) = self.app.build.krate.legacy_asset_dir() {
- if changed_file.starts_with(&legacy_asset_dir) {
- tracing::debug!("Hotreloading legacy asset {changed_file:?}");
- let trimmed = changed_file.strip_prefix(legacy_asset_dir).unwrap();
- let res = std::fs::copy(changed_file, asset_dir.join(trimmed));
- bundled_name = Some(trimmed.to_path_buf());
- if let Err(e) = res {
- tracing::debug!("Failed to hotreload legacy asset {e}");
- }
- }
- }
- // Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
- let changed_file = dunce::canonicalize(changed_file)
- .inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
- .ok()?;
- // The asset might've been renamed thanks to the manifest, let's attempt to reload that too
- if let Some(resource) = self.app.app.assets.assets.get(&changed_file).as_ref() {
- let output_path = asset_dir.join(resource.bundled_path());
- // Remove the old asset if it exists
- _ = std::fs::remove_file(&output_path);
- // And then process the asset with the options into the **old** asset location. If we recompiled,
- // the asset would be in a new location because the contents and hash have changed. Since we are
- // hotreloading, we need to use the old asset location it was originally written to.
- let options = *resource.options();
- let res = process_file_to(&options, &changed_file, &output_path);
- bundled_name = Some(PathBuf::from(resource.bundled_path()));
- if let Err(e) = res {
- tracing::debug!("Failed to hotreload asset {e}");
- }
- }
- // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
- if self.app.build.build.platform() == Platform::Android {
- if let Some(bundled_name) = bundled_name.as_ref() {
- let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name);
- tracing::debug!("Pushing asset to device: {target:?}");
- let res = tokio::process::Command::new(DioxusCrate::android_adb())
- .arg("push")
- .arg(&changed_file)
- .arg(target)
- .output()
- .await
- .context("Failed to push asset to device");
- if let Err(e) = res {
- tracing::debug!("Failed to push asset to device: {e}");
- }
- }
- }
- // Now we can return the bundled asset name to send to the hotreload engine
- bundled_name
- }
- /// Open the native app simply by running its main exe
- ///
- /// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths,
- /// but for now, we just run the exe directly. Very few users should be caring about `dylib` search
- /// paths right now, but they will when we start to enable things like swift integration.
- ///
- /// Server/liveview/desktop are all basically the same, though
- fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
- // Create a new entropy app exe if we need to
- let main_exe = self.app_exe();
- let child = Command::new(main_exe)
- .envs(envs)
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .kill_on_drop(true)
- .spawn()?;
- Ok(child)
- }
- /// Open the web app by opening the browser to the given address.
- /// Check if we need to use https or not, and if so, add the protocol.
- /// Go to the basepath if that's set too.
- fn open_web(&self, address: SocketAddr) {
- let base_path = self.app.build.krate.config.web.app.base_path.clone();
- let https = self
- .app
- .build
- .krate
- .config
- .web
- .https
- .enabled
- .unwrap_or_default();
- let protocol = if https { "https" } else { "http" };
- let base_path = match base_path.as_deref() {
- Some(base_path) => format!("/{}", base_path.trim_matches('/')),
- None => "".to_owned(),
- };
- _ = open::that(format!("{protocol}://{address}{base_path}"));
- }
- /// Use `xcrun` to install the app to the simulator
- /// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning
- /// or entitlements, or anything like that.
- ///
- /// However, if there's no simulator running, this *might* fail.
- ///
- /// TODO(jon): we should probably check if there's a simulator running before trying to install,
- /// and open the simulator if we have to.
- async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
- tracing::debug!(
- "Installing app to simulator {:?}",
- self.app.build.root_dir()
- );
- let res = Command::new("xcrun")
- .arg("simctl")
- .arg("install")
- .arg("booted")
- .arg(self.app.build.root_dir())
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .output()
- .await?;
- tracing::debug!("Installed app to simulator with exit code: {res:?}");
- // Remap the envs to the correct simctl env vars
- // iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX"
- let ios_envs = envs
- .iter()
- .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone()));
- let child = Command::new("xcrun")
- .arg("simctl")
- .arg("launch")
- .arg("--console")
- .arg("booted")
- .arg(self.app.build.krate.bundle_identifier())
- .envs(ios_envs)
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .kill_on_drop(true)
- .spawn()?;
- Ok(child)
- }
- /// We have this whole thing figured out, but we don't actually use it yet.
- ///
- /// Launching on devices is more complicated and requires us to codesign the app, which we don't
- /// currently do.
- ///
- /// Converting these commands shouldn't be too hard, but device support would imply we need
- /// better support for codesigning and entitlements.
- #[allow(unused)]
- async fn open_ios_device(&self) -> Result<()> {
- use serde_json::Value;
- let app_path = self.app.build.root_dir();
- install_app(&app_path).await?;
- // 2. Determine which device the app was installed to
- let device_uuid = get_device_uuid().await?;
- // 3. Get the installation URL of the app
- let installation_url = get_installation_url(&device_uuid, &app_path).await?;
- // 4. Launch the app into the background, paused
- launch_app_paused(&device_uuid, &installation_url).await?;
- // 5. Pick up the paused app and resume it
- resume_app(&device_uuid).await?;
- async fn install_app(app_path: &PathBuf) -> Result<()> {
- let output = Command::new("xcrun")
- .args(["simctl", "install", "booted"])
- .arg(app_path)
- .output()
- .await?;
- if !output.status.success() {
- return Err(format!("Failed to install app: {:?}", output).into());
- }
- Ok(())
- }
- async fn get_device_uuid() -> Result<String> {
- let output = Command::new("xcrun")
- .args([
- "devicectl",
- "list",
- "devices",
- "--json-output",
- "target/deviceid.json",
- ])
- .output()
- .await?;
- let json: Value =
- serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?)
- .context("Failed to parse xcrun output")?;
- let device_uuid = json["result"]["devices"][0]["identifier"]
- .as_str()
- .ok_or("Failed to extract device UUID")?
- .to_string();
- Ok(device_uuid)
- }
- async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
- // 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",
- "target/xcrun.json",
- ])
- .output()
- .await?;
- if !output.status.success() {
- return Err(format!("Failed to install app: {:?}", output).into());
- }
- let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?)
- .context("Failed to parse xcrun output")?;
- let installation_url = json["result"]["installedApplications"][0]["installationURL"]
- .as_str()
- .ok_or("Failed to extract installation URL")?
- .to_string();
- Ok(installation_url)
- }
- async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
- let output = Command::new("xcrun")
- .args([
- "devicectl",
- "device",
- "process",
- "launch",
- "--no-activate",
- "--verbose",
- "--device",
- device_uuid,
- installation_url,
- "--json-output",
- "target/launch.json",
- ])
- .output()
- .await?;
- if !output.status.success() {
- return Err(format!("Failed to launch app: {:?}", output).into());
- }
- Ok(())
- }
- async fn resume_app(device_uuid: &str) -> Result<()> {
- let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?)
- .context("Failed to parse xcrun output")?;
- let status_pid = json["result"]["process"]["processIdentifier"]
- .as_u64()
- .ok_or("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() {
- return Err(format!("Failed to resume app: {:?}", output).into());
- }
- Ok(())
- }
- unimplemented!("dioxus-cli doesn't support ios devices yet.")
- }
- #[allow(unused)]
- async fn codesign_ios(&self) -> Result<()> {
- 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"#;
- let 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 folder: {}
- {CODESIGN_ERROR}
- "#,
- profiles_folder.display()
- )
- }
- let identities = Command::new("security")
- .args(["find-identity", "-v", "-p", "codesigning"])
- .output()
- .await
- .context("Failed to run `security find-identity -v -p codesigning`")
- .map(|e| {
- String::from_utf8(e.stdout)
- .context("Failed to parse `security find-identity -v -p codesigning`")
- })??;
- // Parsing this:
- // 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@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`",
- )?;
- // 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 = "ApplicationIdentifierPrefix")]
- application_identifier_prefix: Vec<String>,
- #[serde(rename = "Entitlements")]
- entitlements: Entitlements,
- }
- #[derive(serde::Deserialize, Debug)]
- struct Entitlements {
- #[serde(rename = "application-identifier")]
- application_identifier: String,
- #[serde(rename = "keychain-access-groups")]
- keychain_access_groups: Vec<String>,
- }
- let entielements_xml = 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],
- );
- // write to a temp file
- let temp_file = tempfile::NamedTempFile::new()?;
- std::fs::write(temp_file.path(), entielements_xml)?;
- // codesign the app
- let output = Command::new("codesign")
- .args([
- "--force",
- "--entitlements",
- temp_file.path().to_str().unwrap(),
- "--sign",
- app_dev_name,
- ])
- .arg(self.app.build.root_dir())
- .output()
- .await
- .context("Failed to codesign the app")?;
- if !output.status.success() {
- let stderr = String::from_utf8(output.stderr).unwrap_or_default();
- return Err(format!("Failed to codesign the app: {stderr}").into());
- }
- Ok(())
- }
- async fn open_android_sim(
- &self,
- devserver_socket: SocketAddr,
- envs: Vec<(&'static str, String)>,
- ) {
- let apk_path = self.app.apk_path();
- let session_cache = self.app.build.krate.session_cache_dir();
- let full_mobile_app_name = self.app.build.krate.full_mobile_app_name();
- // Start backgrounded since .open() is called while in the arm of the top-level match
- tokio::task::spawn(async move {
- let port = devserver_socket.port();
- if let Err(e) = Command::new("adb")
- .arg("reverse")
- .arg(format!("tcp:{}", port))
- .arg(format!("tcp:{}", port))
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .output()
- .await
- {
- tracing::error!("failed to forward port {port}: {e}");
- }
- // Install
- // adb install -r app-debug.apk
- if let Err(e) = Command::new(DioxusCrate::android_adb())
- .arg("install")
- .arg("-r")
- .arg(apk_path)
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .output()
- .await
- {
- tracing::error!("Failed to install apk with `adb`: {e}");
- };
- // Write the env vars to a .env file in our session cache
- let env_file = session_cache.join(".env");
- let contents: String = envs
- .iter()
- .map(|(key, value)| format!("{key}={value}"))
- .collect::<Vec<_>>()
- .join("\n");
- _ = std::fs::write(&env_file, contents);
- // Push the env file to the device
- if let Err(e) = tokio::process::Command::new(DioxusCrate::android_adb())
- .arg("push")
- .arg(env_file)
- .arg(dioxus_cli_config::android_session_cache_dir().join(".env"))
- .output()
- .await
- .context("Failed to push asset to device")
- {
- tracing::error!("Failed to push .env file to device: {e}");
- }
- // eventually, use the user's MainActivity, not our MainActivity
- // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
- let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
- if let Err(e) = Command::new(DioxusCrate::android_adb())
- .arg("shell")
- .arg("am")
- .arg("start")
- .arg("-n")
- .arg(activity_name)
- .stderr(Stdio::piped())
- .stdout(Stdio::piped())
- .output()
- .await
- {
- tracing::error!("Failed to start app with `adb`: {e}");
- };
- });
- }
- fn make_entropy_path(exe: &PathBuf) -> PathBuf {
- let id = uuid::Uuid::new_v4();
- let name = id.to_string();
- let some_entropy = name.split('-').next().unwrap();
- // Make a copy of the server exe with a new name
- let entropy_server_exe = exe.with_file_name(format!(
- "{}-{}",
- exe.file_name().unwrap().to_str().unwrap(),
- some_entropy
- ));
- std::fs::copy(exe, &entropy_server_exe).unwrap();
- entropy_server_exe
- }
- fn server_exe(&mut self) -> Option<PathBuf> {
- let mut server = self.app.server_exe()?;
- // Create a new entropy server exe if we need to
- if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
- // If we already have an entropy server exe, return it - this is useful for re-opening the same app
- if let Some(existing_server) = self.entropy_server_exe.clone() {
- return Some(existing_server);
- }
- // Otherwise, create a new entropy server exe and save it for re-opning
- let entropy_server_exe = Self::make_entropy_path(&server);
- self.entropy_server_exe = Some(entropy_server_exe.clone());
- server = entropy_server_exe;
- }
- Some(server)
- }
- fn app_exe(&mut self) -> PathBuf {
- let mut main_exe = self.app.main_exe();
- // The requirement here is based on the platform, not necessarily our current architecture.
- let requires_entropy = match self.app.build.build.platform() {
- // When running "bundled", we don't need entropy
- Platform::Web => false,
- Platform::MacOS => false,
- Platform::Ios => false,
- Platform::Android => false,
- // But on platforms that aren't running as "bundled", we do.
- Platform::Windows => true,
- Platform::Linux => true,
- Platform::Server => true,
- Platform::Liveview => true,
- };
- if requires_entropy || std::env::var("DIOXUS_ENTROPY").is_ok() {
- // If we already have an entropy app exe, return it - this is useful for re-opening the same app
- if let Some(existing_app_exe) = self.entropy_app_exe.clone() {
- return existing_app_exe;
- }
- let entropy_app_exe = Self::make_entropy_path(&main_exe);
- self.entropy_app_exe = Some(entropy_app_exe.clone());
- main_exe = entropy_app_exe;
- }
- main_exe
- }
- }
|