Fix bundling/path errors, sidecar support, iOS/Android simulator support, asset hotreload fixes, serverfn hotrelad, native tailwind support (#2779)
* wip: fix manganis import path * upgrade tauri bundler, convert todo!() to unimplemented!() * feat: mobile hotreloading + ios asset configuration * remove oid demo and its associated env vars * nuke all the things that cause cache thrashing * swap to Asset type * add some more logs * display impl for attrvalue * dont panic on collect failure * cut down web deps to 150 * clean up deps in a lot of places, simplify build scripts * clean up asset resolution and cli-dev profile * wire up absolute paths for manganis asset in a particular mode * move document related things around to shorten compile times * move most things to `document::Item` and then decompose the cli-config * switch link to stylesheet for clarity in examples * move manganis workspace example to examples folder * decompose manganis-cli-support * dont need mobile demo anymore * remove gloo dialogs * html doesnt need document * rename hotreload to devtools * really clean up the html crate * fix weird rsx spacing * clean up desktop to use callback * clean up document a bit * re-wire up devsocket * fix utf css * yeeeet that js out of here * synthetic web system works * web crate almost done being cleaned up * desktop mostly cleaned up too * clean up cli a bit * more cli cleanup * cli builds again * clean up cli, inline structs, cut down number of unique types where possible * hotreload works again for desktop and this time, mobile too * cfg out tungstenite * devserver err * more refactor to cli builder * switch to unbounded_send * new structure is much cleaner * add http serve subcommand * bundled hotreload * kick stylsheets * clean up a bit more, split up eventloop * amazingly serve seems done * change from pub to pub(crate) in cli * remove tools * bit more polish to cli * fix issue with join * gracefully handle fullstack without a server * fullstack mobile demo * fix launch function, move projects into example-projects folder * hoist examples * add "run" command * clean up launch * remove old manual websocket receiver * doctor command * allow desktop to scroll * cut apart router crate * dont put launch in prelude * use dioxus::launch where possible * rename rsx, cut out hotreload tests * remove liveview project * bump native * fix compile for renderers * move sync event response out of interpreter * move render in serve * rollback settings change * cli compiles, huzzah! * change uris for asset * fix asset * new tui screen * new cargo-like tui works * very very verty close * it works! very small bug with incorrect grapheme calc * Clean up devserver a bit * status sytem * tidy up debug filters * clean up logging situation * Fix a number of bugs with log printing * new printing system is more reliable * wire up more stuff * things working but fullstack is having issues * fullstack works again! * hotreloading bundled assets works again * bundled hotreload and beginnings of macos bundling * Hotreload desktop * combined server builds * add build handle * fix fullstack assets * make open async, add some hooks for ios * migrate filemap to runner * wip global crate system * fixup bundles + organize asset * fix asset location bug * all the bundled reloading! * open ios simulator! * full hotreload support for mobile + serverfn * basic cleanups * clean up dx * Move filemap * fix cutting of newlines * assets workibg, some android * hoist wry/tao * use sync locks and headers to fix issues with android * desktop -> mac/win/linux with alias * better logging * feat: workspace (entire computer!) hotreload * should rebuild toggle, ios simulator bootup * proper mobile support in launch * more robust handling of assets * fix cargo * bring back some of tauri bundle * make warnings go away, clippy happy on cli * some final clippy cleanups * fmt * move manganis to its own folder * upgrade bundle to stable * drastically slim down manganis, prepping for merge * typos, failing test, docsrs config * remove static gen test * nix static gen test * we use --platform web instead of --platform fullstack now * only bind dev urls in desktop/mobile * install gtk * nix static gen * split build dir by app name
This commit is contained in:
1 parent
ef436e4ed0
commit
7ec3453ca3
156 files changed
+11544
-9005
No files matched your search
@@ -0,0 +1,451 @@
|
||||
use crate::{AppBundle, Platform, Result};
|
||||
use anyhow::Context;
|
||||
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.
|
||||
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 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,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn open(
|
||||
&mut self,
|
||||
devserver_ip: SocketAddr,
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
open_browser: bool,
|
||||
) -> Result<()> {
|
||||
if let Some(addr) = fullstack_address {
|
||||
tracing::debug!("Proxying fullstack server from port {:?}", addr);
|
||||
}
|
||||
|
||||
// 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_ENABLED", "true".to_string()),
|
||||
(
|
||||
dioxus_cli_config::DEVSERVER_RAW_ADDR_ENV,
|
||||
devserver_ip.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()),
|
||||
];
|
||||
|
||||
if let Some(addr) = fullstack_address {
|
||||
envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string()));
|
||||
envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()));
|
||||
}
|
||||
|
||||
// Launch the server if we have one and consume its stdout/stderr
|
||||
if let Some(server) = self.app.server_exe() {
|
||||
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(envs, devserver_ip);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
Platform::Ios => Some(self.open_ios_sim(envs).await?),
|
||||
|
||||
// https://developer.android.com/studio/run/emulator-commandline
|
||||
Platform::Android => {
|
||||
tracing::error!("Android is not yet supported, sorry!");
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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) 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.asset_dir(),
|
||||
};
|
||||
|
||||
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
|
||||
|
||||
// If the asset shares the same name in the bundle, reload that
|
||||
let 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}");
|
||||
}
|
||||
}
|
||||
|
||||
// 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 res = std::fs::copy(changed_file, asset_dir.join(&resource.bundled));
|
||||
bundled_name = Some(PathBuf::from(&resource.bundled));
|
||||
if let Err(e) = res {
|
||||
tracing::debug!("Failed to hotreload asset {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> {
|
||||
let child = Command::new(self.app.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, _envs: Vec<(&str, String)>, 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.app_dir());
|
||||
|
||||
let res = Command::new("xcrun")
|
||||
.arg("simctl")
|
||||
.arg("install")
|
||||
.arg("booted")
|
||||
.arg(self.app.app_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("com.dioxuslabs")
|
||||
.envs(ios_envs)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
tracing::debug!("Launched app on simulator with exit code: {child:?}");
|
||||
|
||||
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<()> {
|
||||
// APP_PATH="target/aarch64-apple-ios/debug/bundle/ios/DioxusApp.app"
|
||||
|
||||
// # get the device id by jq-ing the json of the device list
|
||||
// xcrun devicectl list devices --json-output target/deviceid.json
|
||||
// DEVICE_UUID=$(jq -r '.result.devices[0].identifier' target/deviceid.json)
|
||||
|
||||
// xcrun devicectl device install app --device "${DEVICE_UUID}" "${APP_PATH}" --json-output target/xcrun.json
|
||||
|
||||
// # get the installation url by jq-ing the json of the device install
|
||||
// INSTALLATION_URL=$(jq -r '.result.installedApplications[0].installationURL' target/xcrun.json)
|
||||
|
||||
// # launch the app
|
||||
// # todo: we can just background it immediately and then pick it up for loading its logs
|
||||
// xcrun devicectl device process launch --device "${DEVICE_UUID}" "${INSTALLATION_URL}"
|
||||
|
||||
// # # launch the app and put it in background
|
||||
// # xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}" --json-output "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}"
|
||||
|
||||
// # # Extract background PID of status app
|
||||
// # STATUS_PID=$(jq -r '.result.process.processIdentifier' "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}")
|
||||
// # "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1
|
||||
|
||||
// # # now that metro is ready, resume the app from background
|
||||
// # xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1
|
||||
|
||||
use serde_json::Value;
|
||||
let app_path = self.app.app_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> {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user