From d6cee7c0cd65480feeb5111d26e5b4735a0879da Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 8 Nov 2024 19:12:51 -0500 Subject: [PATCH] Add structured output --- .gitignore | 4 + Cargo.lock | 13 ++ packages/cli/Cargo.toml | 2 +- packages/cli/src/builder/build.rs | 1 - packages/cli/src/builder/progress.rs | 11 -- packages/cli/src/builder/verify.rs | 24 ++-- packages/cli/src/bundle_utils.rs | 1 + packages/cli/src/cli/autoformat.rs | 4 +- packages/cli/src/cli/build.rs | 7 +- packages/cli/src/cli/bundle.rs | 154 +++++++++++++++++------- packages/cli/src/cli/check.rs | 4 +- packages/cli/src/cli/clean.rs | 4 +- packages/cli/src/cli/config.rs | 9 +- packages/cli/src/cli/create.rs | 6 +- packages/cli/src/cli/doctor.rs | 5 +- packages/cli/src/cli/init.rs | 6 +- packages/cli/src/cli/mod.rs | 15 +-- packages/cli/src/cli/run.rs | 6 +- packages/cli/src/cli/serve.rs | 6 +- packages/cli/src/cli/translate.rs | 6 +- packages/cli/src/cli/verbosity.rs | 16 +++ packages/cli/src/config/bundle.rs | 38 ++++-- packages/cli/src/error.rs | 3 + packages/cli/src/logging.rs | 172 ++++++++++++--------------- packages/cli/src/main.rs | 91 +++++++------- packages/cli/src/serve/mod.rs | 1 - packages/cli/src/serve/output.rs | 4 +- packages/cli/src/slog.rs | 39 ++++++ packages/dioxus/src/launch.rs | 1 + 29 files changed, 394 insertions(+), 259 deletions(-) create mode 100644 packages/cli/src/cli/verbosity.rs create mode 100644 packages/cli/src/slog.rs diff --git a/.gitignore b/.gitignore index 4974ac19b1..d087576ddd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ node_modules/ /packages/playwright-report/ /packages/playwright/.cache/ + +# ignore the output of tmps +tmp/ +bundle/ diff --git a/Cargo.lock b/Cargo.lock index bcd1d55f80..dbc5265b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11697,6 +11697,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -11707,12 +11717,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 8f5bcc7fcd..e091e249dd 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -87,7 +87,7 @@ brotli = "6.0.0" ignore = "0.4.22" env_logger = { workspace = true } -tracing-subscriber = { version = "0.3.18", features = ["std", "env-filter"] } +tracing-subscriber = { version = "0.3.18", features = ["std", "env-filter", "json"] } console-subscriber = { version = "0.3.0", optional = true } tracing = { workspace = true } wasm-opt = { version = "0.116.1", optional = true } diff --git a/packages/cli/src/builder/build.rs b/packages/cli/src/builder/build.rs index 43658fa5f1..54e4da0ff6 100644 --- a/packages/cli/src/builder/build.rs +++ b/packages/cli/src/builder/build.rs @@ -88,7 +88,6 @@ impl BuildRequest { .arg("--message-format") .arg("json-diagnostic-rendered-ansi") .args(self.build_arguments()); - // .env("RUSTFLAGS", self.rust_flags()); if let Some(target_dir) = self.custom_target_dir.as_ref() { cmd.env("CARGO_TARGET_DIR", target_dir); diff --git a/packages/cli/src/builder/progress.rs b/packages/cli/src/builder/progress.rs index 85051a601c..59b1cec420 100644 --- a/packages/cli/src/builder/progress.rs +++ b/packages/cli/src/builder/progress.rs @@ -51,17 +51,6 @@ impl BuildRequest { stage: BuildStage::RunningBindgen {}, }); } - pub(crate) fn status_wasm_opt_start(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::RunningBindgen {}, - }); - } - - pub(crate) fn status_bundle_finished(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::Bundling {}, - }); - } pub(crate) fn status_start_bundle(&self) { _ = self.progress.unbounded_send(BuildUpdate::Progress { diff --git a/packages/cli/src/builder/verify.rs b/packages/cli/src/builder/verify.rs index c25840fb8f..2c8af9321a 100644 --- a/packages/cli/src/builder/verify.rs +++ b/packages/cli/src/builder/verify.rs @@ -94,21 +94,21 @@ impl BuildRequest { /// should be installing the x86 versions. pub(crate) async fn verify_ios_tooling(&self, _rustup: RustupShow) -> Result<()> { // open the simulator - _ = tokio::process::Command::new("open") - .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app") - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .status() - .await; + // _ = tokio::process::Command::new("open") + // .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app") + // .stderr(Stdio::piped()) + // .stdout(Stdio::piped()) + // .status() + // .await; // Now xcrun to open the device // todo: we should try and query the device list and/or parse it rather than hardcode this simulator - _ = tokio::process::Command::new("xcrun") - .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"]) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .status() - .await; + // _ = tokio::process::Command::new("xcrun") + // .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"]) + // .stderr(Stdio::piped()) + // .stdout(Stdio::piped()) + // .status() + // .await; // if !rustup // .installed_toolchains diff --git a/packages/cli/src/bundle_utils.rs b/packages/cli/src/bundle_utils.rs index 1436ef82b6..2c9f2aa2ba 100644 --- a/packages/cli/src/bundle_utils.rs +++ b/packages/cli/src/bundle_utils.rs @@ -156,6 +156,7 @@ impl From for tauri_bundler::PackageType { PackageType::AppImage => Self::AppImage, PackageType::Dmg => Self::Dmg, PackageType::Updater => Self::Updater, + PackageType::Nsis => Self::Nsis, } } } diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 6d35d5c912..ab4f73a890 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -37,7 +37,7 @@ pub(crate) struct Autoformat { } impl Autoformat { - pub(crate) fn autoformat(self) -> Result<()> { + pub(crate) fn autoformat(self) -> Result { let Autoformat { check, raw, @@ -88,7 +88,7 @@ impl Autoformat { } } - Ok(()) + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 01a999ad7d..46ddb87392 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -58,9 +58,10 @@ pub(crate) struct BuildArgs { } impl BuildArgs { - pub async fn build_it(mut self) -> Result<()> { - self.build().await?; - Ok(()) + pub async fn run_cmd(mut self) -> Result { + let _bundle = self.build().await?; + + Ok(StructuredOutput::GenericSuccess) } pub(crate) async fn build(&mut self) -> Result { diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 4ca183e0fb..367bbecf60 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,9 +1,6 @@ -use crate::Builder; -use crate::DioxusCrate; -use crate::{build::BuildArgs, PackageType}; +use crate::{build::BuildArgs, AppBundle, Builder, DioxusCrate, Platform}; use anyhow::Context; -use itertools::Itertools; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; use super::*; @@ -13,8 +10,28 @@ use super::*; #[clap(name = "bundle")] pub struct Bundle { /// The package types to bundle + /// + /// Any of: + /// - macos: The macOS application bundle (.app). + /// - ios: The iOS app bundle. + /// - msi: The Windows bundle (.msi). + /// - nsis: The NSIS bundle (.exe). + /// - deb: The Linux Debian package bundle (.deb). + /// - rpm: The Linux RPM bundle (.rpm). + /// - appimage: The Linux AppImage bundle (.AppImage). + /// - dmg: The macOS DMG bundle (.dmg). + /// - updater: The Updater bundle. #[clap(long)] - pub packages: Option>, + pub package_types: Option>, + + /// The directory in which the final bundle will be placed. + /// + /// Relative paths will be placed relative to the current working directory. + /// + /// We will flatten the artifacts into this directory - there will be no differentiation between + /// artifacts produced by different platforms. + #[clap(long)] + pub outdir: Option, /// The arguments for the dioxus build #[clap(flatten)] @@ -22,7 +39,7 @@ pub struct Bundle { } impl Bundle { - pub(crate) async fn bundle(mut self) -> Result<()> { + pub(crate) async fn bundle(mut self) -> Result { tracing::info!("Bundling project..."); let krate = DioxusCrate::new(&self.build_arguments.target_args) @@ -38,6 +55,82 @@ impl Bundle { tracing::info!("Copying app to output directory..."); + // If we're building for iOS, we need to bundle the iOS bundle + if self.build_arguments.platform() == Platform::Ios && self.package_types.is_none() { + self.package_types = Some(vec![crate::PackageType::IosBundle]); + } + + let mut cmd_result = StructuredOutput::GenericSuccess; + + match self.build_arguments.platform() { + // By default, mac/win/linux work with tauri bundle + Platform::MacOS | Platform::Linux | Platform::Windows => { + let bundles = self.bundle_desktop(krate, bundle)?; + + tracing::info!("Bundled app successfully!"); + tracing::info!("App produced {} outputs:", bundles.len()); + tracing::debug!("Bundling produced bundles: {:#?}", bundles); + + // Copy the bundles to the output directory and log their locations + let mut bundle_paths = vec![]; + for bundle in bundles { + for src in bundle.bundle_paths { + let src = if let Some(outdir) = &self.outdir { + let dest = outdir.join(src.file_name().unwrap()); + crate::fastfs::copy_asset(&src, &dest)?; + dest + } else { + src.clone() + }; + + tracing::info!( + "{} - [{}]", + bundle.package_type.short_name(), + src.display() + ); + + bundle_paths.push(src); + } + } + + cmd_result = StructuredOutput::BundleOutput { + platform: self.build_arguments.platform(), + bundles: bundle_paths, + }; + } + + Platform::Web => { + tracing::info!("App available at: {}", bundle.app_dir().display()); + } + + Platform::Ios => { + tracing::warn!("Signed iOS bundles are not yet supported"); + tracing::info!("The bundle is available at: {}", bundle.app_dir().display()); + } + + Platform::Server => { + tracing::info!("Server available at: {}", bundle.app_dir().display()) + } + Platform::Liveview => tracing::info!( + "Liveview server available at: {}", + bundle.app_dir().display() + ), + + Platform::Android => { + return Err(Error::UnsupportedFeature( + "Android bundles are not yet supported".into(), + )); + } + }; + + Ok(cmd_result) + } + + fn bundle_desktop( + &self, + krate: DioxusCrate, + bundle: AppBundle, + ) -> Result, Error> { _ = std::fs::remove_dir_all(krate.bundle_dir(self.build_arguments.platform())); let package = krate.package(); @@ -45,8 +138,6 @@ impl Bundle { if cfg!(windows) { name.set_extension("exe"); } - - // Make sure we copy the exe to the bundle dir so the bundler can find it std::fs::create_dir_all(krate.bundle_dir(self.build_arguments.platform()))?; std::fs::copy( &bundle.app.exe, @@ -83,7 +174,8 @@ impl Bundle { for entry in std::fs::read_dir(bundle.asset_dir())?.flatten() { let old = entry.path().canonicalize()?; - let new = PathBuf::from("assets").join(old.file_name().unwrap()); + let new = PathBuf::from("/assets").join(old.file_name().unwrap()); + tracing::debug!("Bundled asset: {old:?} -> {new:?}"); bundle_settings .resources_map @@ -92,8 +184,6 @@ impl Bundle { .insert(old.display().to_string(), new.display().to_string()); } - // Drain any resources set in the config into the resources map. Tauri bundle doesn't let - // you set both resources and resources_map https://github.com/DioxusLabs/dioxus/issues/2941 for resource_path in bundle_settings.resources.take().into_iter().flatten() { bundle_settings .resources_map @@ -116,7 +206,7 @@ impl Bundle { .binaries(binaries) .bundle_settings(bundle_settings); - if let Some(packages) = &self.packages { + if let Some(packages) = &self.package_types { settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect()); } @@ -124,11 +214,12 @@ impl Bundle { settings = settings.target(target.to_string()); } - let settings = settings.build()?; + if self.build_arguments.platform() == Platform::Ios { + settings = settings.target("aarch64-apple-ios".to_string()); + } + let settings = settings.build()?; tracing::debug!("Bundling project with settings: {:#?}", settings); - - // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567) if cfg!(target_os = "macos") { std::env::set_var("CI", "true"); } @@ -140,35 +231,6 @@ impl Bundle { } })?; - tracing::info!("Bundled app successfully!"); - tracing::info!("App produced {} outputs:", bundles.len()); - - for bundle in bundles { - tracing::info!( - "{} - [{}]", - bundle.package_type.short_name(), - bundle.bundle_paths.iter().map(|p| p.display()).join(", ") - ); - } - - Ok(()) - } -} - -impl FromStr for PackageType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "macos" => Ok(PackageType::MacOsBundle), - "ios" => Ok(PackageType::IosBundle), - "msi" => Ok(PackageType::WindowsMsi), - "deb" => Ok(PackageType::Deb), - "rpm" => Ok(PackageType::Rpm), - "appimage" => Ok(PackageType::AppImage), - "dmg" => Ok(PackageType::Dmg), - "updater" => Ok(PackageType::Updater), - _ => Err(format!("{} is not a valid package type", s)), - } + Ok(bundles) } } diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index f32b8c38b0..ae3f373675 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -20,7 +20,7 @@ pub(crate) struct Check { impl Check { // Todo: check the entire crate - pub(crate) async fn check(self) -> Result<()> { + pub(crate) async fn check(self) -> Result { match self.file { // Default to checking the project None => { @@ -38,7 +38,7 @@ impl Check { } } - Ok(()) + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index cb69935e46..bea14ed0e9 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -9,7 +9,7 @@ pub(crate) struct Clean {} impl Clean { /// todo(jon): we should add a config option that just wipes target/dx and target/dioxus-client instead of doing a full clean - pub(crate) fn clean(self) -> anyhow::Result<()> { + pub(crate) fn clean(self) -> anyhow::Result { let output = Command::new("cargo") .arg("clean") .stdout(Stdio::piped()) @@ -20,6 +20,6 @@ impl Clean { return Err(anyhow::anyhow!("Cargo clean failed.")); } - Ok(()) + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index 9adb99263d..6656b9983a 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -76,7 +76,7 @@ impl From for bool { } impl Config { - pub(crate) fn config(self) -> Result<()> { + pub(crate) fn config(self) -> Result { let crate_root = crate_root()?; match self { Config::Init { @@ -89,7 +89,7 @@ impl Config { tracing::warn!( "config file `Dioxus.toml` already exist, use `--force` to overwrite it." ); - return Ok(()); + return Ok(StructuredOutput::GenericSuccess); } let mut file = File::create(conf_path)?; let content = String::from(include_str!("../../assets/dioxus.toml")) @@ -112,7 +112,7 @@ impl Config { tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done."); } Config::LogFile {} => { - let log_path = crate::logging::log_path(); + let log_path = crate::logging::FileAppendLayer::log_path(); tracing::info!(dx_src = ?TraceSrc::Dev, "Log file is located at {}", log_path.display()); } // Handle CLI settings. @@ -132,6 +132,7 @@ impl Config { tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 CLI setting `{setting}` has been set."); } } - Ok(()) + + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index e0cce2af47..d01a3525d7 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -39,7 +39,7 @@ pub(crate) struct Create { } impl Create { - pub(crate) fn create(mut self) -> Result<()> { + pub(crate) fn create(mut self) -> Result { let metadata = cargo_metadata::MetadataCommand::new().exec().ok(); // If we're getting pass a `.` name, that's actually a path @@ -106,7 +106,9 @@ impl Create { .expect("ctrlc::set_handler"); let path = cargo_generate::generate(args)?; - post_create(&path, metadata) + post_create(&path, metadata)?; + + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/doctor.rs b/packages/cli/src/cli/doctor.rs index 7c89c076d4..d73effea2b 100644 --- a/packages/cli/src/cli/doctor.rs +++ b/packages/cli/src/cli/doctor.rs @@ -1,10 +1,11 @@ +use crate::{Result, StructuredOutput}; use clap::Parser; #[derive(Clone, Debug, Parser)] pub struct Doctor {} impl Doctor { - pub async fn run(self) -> anyhow::Result<()> { - Ok(()) + pub async fn run(self) -> Result { + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/init.rs b/packages/cli/src/cli/init.rs index f94e39d8b0..b9b4d937b6 100644 --- a/packages/cli/src/cli/init.rs +++ b/packages/cli/src/cli/init.rs @@ -24,7 +24,7 @@ pub(crate) struct Init { } impl Init { - pub(crate) fn init(self) -> Result<()> { + pub(crate) fn init(self) -> Result { let metadata = cargo_metadata::MetadataCommand::new().exec().ok(); // Get directory name. @@ -57,6 +57,8 @@ impl Init { ..Default::default() }; let path = cargo_generate::generate(args)?; - create::post_create(&path, metadata) + create::post_create(&path, metadata)?; + + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index e19354c44e..af5462d45d 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -12,12 +12,14 @@ pub(crate) mod run; pub(crate) mod serve; pub(crate) mod target; pub(crate) mod translate; +pub(crate) mod verbosity; pub(crate) use build::*; pub(crate) use serve::*; pub(crate) use target::*; +pub(crate) use verbosity::*; -use crate::{error::Result, Error}; +use crate::{error::Result, Error, StructuredOutput}; use anyhow::Context; use clap::{Parser, Subcommand}; use html_parser::Dom; @@ -35,16 +37,11 @@ use std::{ #[derive(Parser)] #[clap(name = "dioxus", version = VERSION.as_str())] pub(crate) struct Cli { - /// Use verbose output [default: false] - #[clap(long, global = true)] - pub(crate) verbose: bool, - - /// Use trace output [default: false] - #[clap(long, global = true)] - pub(crate) trace: bool, - #[command(subcommand)] pub(crate) action: Commands, + + #[command(flatten)] + pub(crate) verbosity: Verbosity, } #[derive(Subcommand)] diff --git a/packages/cli/src/cli/run.rs b/packages/cli/src/cli/run.rs index 728e4bbe62..d0a5bc26ba 100644 --- a/packages/cli/src/cli/run.rs +++ b/packages/cli/src/cli/run.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate}; +use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate, Result}; /// Run the project with the given arguments #[derive(Clone, Debug, Parser)] @@ -10,7 +10,7 @@ pub(crate) struct RunArgs { } impl RunArgs { - pub(crate) async fn run(mut self) -> anyhow::Result<()> { + pub(crate) async fn run(mut self) -> Result { let krate = DioxusCrate::new(&self.build_args.target_args) .context("Failed to load Dioxus workspace")?; @@ -55,6 +55,6 @@ impl RunArgs { } } - Ok(()) + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 18b1d2d90a..efc8235f49 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -49,8 +49,10 @@ impl ServeArgs { /// /// Make sure not to do any intermediate logging since our tracing infra has now enabled much /// higher log levels - pub(crate) async fn serve(self) -> Result<()> { - crate::serve::serve_all(self).await + pub(crate) async fn serve(self) -> Result { + _ = crate::serve::serve_all(self).await?; + + Ok(StructuredOutput::GenericSuccess) } pub(crate) fn load_krate(&mut self) -> Result { diff --git a/packages/cli/src/cli/translate.rs b/packages/cli/src/cli/translate.rs index 2d2847d815..2711521790 100644 --- a/packages/cli/src/cli/translate.rs +++ b/packages/cli/src/cli/translate.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{Result, TraceSrc}; +use crate::{Result, StructuredOutput, TraceSrc}; use dioxus_rsx::{BodyNode, CallBody, TemplateBody}; use std::{io::IsTerminal as _, process::exit}; @@ -26,7 +26,7 @@ pub(crate) struct Translate { } impl Translate { - pub(crate) fn translate(self) -> Result<()> { + pub(crate) fn translate(self) -> Result { // Get the right input for the translation let contents = determine_input(self.file, self.raw)?; @@ -42,7 +42,7 @@ impl Translate { None => print!("{}", out), } - Ok(()) + Ok(StructuredOutput::GenericSuccess) } } diff --git a/packages/cli/src/cli/verbosity.rs b/packages/cli/src/cli/verbosity.rs new file mode 100644 index 0000000000..4c5a88e571 --- /dev/null +++ b/packages/cli/src/cli/verbosity.rs @@ -0,0 +1,16 @@ +use clap::Parser; + +#[derive(Parser, Clone, Debug)] +pub struct Verbosity { + /// Use verbose output [default: false] + #[clap(long, global = true)] + pub(crate) verbose: bool, + + /// Use trace output [default: false] + #[clap(long, global = true)] + pub(crate) trace: bool, + + /// Output logs in JSON format + #[clap(long, global = true)] + pub(crate) json_output: bool, +} diff --git a/packages/cli/src/config/bundle.rs b/packages/cli/src/config/bundle.rs index bed8eef8f8..ec9fe23b11 100644 --- a/packages/cli/src/config/bundle.rs +++ b/packages/cli/src/config/bundle.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::path::PathBuf; +use std::{collections::HashMap, str::FromStr}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub(crate) struct BundleConfig { @@ -181,11 +181,22 @@ impl Default for WebviewInstallMode { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomSignCommandSettings { + /// The command to run to sign the binary. + pub cmd: String, + /// The arguments to pass to the command. + /// + /// "%1" will be replaced with the path to the binary to be signed. + pub args: Vec, +} + #[derive(Clone, Copy, Debug)] pub(crate) enum PackageType { MacOsBundle, IosBundle, WindowsMsi, + Nsis, Deb, Rpm, AppImage, @@ -193,12 +204,21 @@ pub(crate) enum PackageType { Updater, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CustomSignCommandSettings { - /// The command to run to sign the binary. - pub cmd: String, - /// The arguments to pass to the command. - /// - /// "%1" will be replaced with the path to the binary to be signed. - pub args: Vec, +impl FromStr for PackageType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "macos" => Ok(PackageType::MacOsBundle), + "ios" => Ok(PackageType::IosBundle), + "msi" => Ok(PackageType::WindowsMsi), + "nsis" => Ok(PackageType::Nsis), + "deb" => Ok(PackageType::Deb), + "rpm" => Ok(PackageType::Rpm), + "appimage" => Ok(PackageType::AppImage), + "dmg" => Ok(PackageType::Dmg), + "updater" => Ok(PackageType::Updater), + _ => Err(format!("{} is not a valid package type", s)), + } + } } diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index f076e23344..ee4c8512f1 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -33,6 +33,9 @@ pub(crate) enum Error { #[error("Failed to bundle project: {0}")] BundleFailed(#[from] tauri_bundler::Error), + #[error("Unsupported feature: {0}")] + UnsupportedFeature(String), + #[error(transparent)] Other(#[from] anyhow::Error), } diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 60ea83e985..61b317cedd 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -14,8 +14,9 @@ //! 3. Build CLI layer for routing tracing logs to the TUI. //! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode. -use crate::{serve::ServeUpdate, Commands, Platform as TargetPlatform}; +use crate::{serve::ServeUpdate, Cli, Commands, Platform as TargetPlatform, Verbosity}; use cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage}; +use clap::Parser; use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use once_cell::sync::OnceCell; use std::{ @@ -23,12 +24,8 @@ use std::{ env, fmt::{Debug, Display, Write as _}, fs, - io::{self, Write}, path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering}, - Mutex, - }, + sync::Mutex, }; use tracing::{field::Visit, Level, Subscriber}; use tracing_subscriber::{fmt::format, prelude::*, registry::LookupSpan, EnvFilter, Layer}; @@ -37,92 +34,94 @@ const LOG_ENV: &str = "DIOXUS_LOG"; const LOG_FILE_NAME: &str = "dx.log"; const DX_SRC_FLAG: &str = "dx_src"; -pub fn log_path() -> PathBuf { - let tmp_dir = std::env::temp_dir(); - tmp_dir.join(LOG_FILE_NAME) -} - -static TUI_ENABLED: AtomicBool = AtomicBool::new(false); static TUI_TX: OnceCell> = OnceCell::new(); - -pub static VERBOSE: AtomicBool = AtomicBool::new(false); -pub static TRACE: AtomicBool = AtomicBool::new(false); +pub static VERBOSITY: OnceCell = OnceCell::new(); pub(crate) struct TraceController { pub(crate) tui_rx: UnboundedReceiver, } impl TraceController { - /// Get a handle to the trace controller. - pub fn redirect() -> Self { - let (tui_tx, tui_rx) = unbounded(); - TUI_ENABLED.store(true, Ordering::SeqCst); - TUI_TX.set(tui_tx.clone()).unwrap(); - Self { tui_rx } - } - - /// Wait for the internal logger to send a message - pub(crate) async fn wait(&mut self) -> ServeUpdate { - use futures_util::StreamExt; - let log = self.tui_rx.next().await.expect("tracer should never die"); - ServeUpdate::TracingLog { log } - } - - pub(crate) fn shutdown(&self) { - TUI_ENABLED.store(false, Ordering::SeqCst); - } - - /// Build tracing infrastructure. - pub fn initialize(args: &crate::Cli) { - let our_level = if args.verbose { "debug" } else { "info" }; + /// Initialize the CLI and set up the tracing infrastructure + pub fn initialize() -> Cli { + let args = Cli::parse(); + + VERBOSITY + .set(args.verbosity.clone()) + .expect("verbosity should only be set once"); + + // When running in interactive mode (of which serve is the only one), we want to do things slightly differently + // This involves no fmt layer or file logging + if matches!(args.action, Commands::Serve(_)) { + Self::initialize_for_serve(); + return args; + } // By default we capture ourselves at a higher tracing level when serving // This ensures we're tracing ourselves even if we end up tossing the logs - let mut filter = EnvFilter::new(match args.action { - Commands::Serve(_) => { - "error,dx=trace,dioxus-cli=trace,manganis-cli-support=debug".to_string() - } - _ => format!( - "error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}" - ), - }); - - if env::var(LOG_ENV).is_ok() { - filter = EnvFilter::from_env(LOG_ENV); - } - - // Build CLI layer - let cli_layer = CLILayer; + let filter = if env::var(LOG_ENV).is_ok() { + EnvFilter::from_env(LOG_ENV) + } else { + EnvFilter::new(format!( + "error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}", + our_level = if args.verbosity.verbose { + "debug" + } else { + "info" + } + )) + }; - // Build fmt layer let fmt_layer = tracing_subscriber::fmt::layer() - .with_target(args.verbose) + .with_target(args.verbosity.verbose) .fmt_fields( format::debug_fn(|writer, field, value| { write!(writer, "{}", format_field(field.name(), value)) }) .delimited(" "), ) - .with_timer(tracing_subscriber::fmt::time::uptime()) - .with_writer(Mutex::new(FmtLogWriter {})); + .with_timer(tracing_subscriber::fmt::time::uptime()); - // Log file - let file_append_layer = FileAppendLayer::new(log_path()) - .inspect_err( - |e| tracing::error!(dx_src = ?TraceSrc::Dev, err = ?e, "failed to init log file"), - ) - .ok(); + let fmt_layer = if args.verbosity.json_output { + fmt_layer.json().flatten_event(true).boxed() + } else { + fmt_layer.boxed() + }; let sub = tracing_subscriber::registry() .with(filter) - .with(file_append_layer) - .with(cli_layer) + .with(FileAppendLayer::new()) .with(fmt_layer); #[cfg(feature = "tokio-console")] let sub = sub.with(console_subscriber::spawn()); sub.init(); + + args + } + + /// Get a handle to the trace controller. + pub fn redirect() -> Self { + let (tui_tx, tui_rx) = unbounded(); + TUI_TX.set(tui_tx.clone()).unwrap(); + Self { tui_rx } + } + + /// Wait for the internal logger to send a message + pub(crate) async fn wait(&mut self) -> ServeUpdate { + use futures_util::StreamExt; + let log = self.tui_rx.next().await.expect("tracer should never die"); + ServeUpdate::TracingLog { log } + } + + fn initialize_for_serve() { + let filter = EnvFilter::new("error,dx=trace,dioxus-cli=trace,manganis-cli-support=trace"); + + tracing_subscriber::registry() + .with(filter) + .with(CLILayer {}) + .init(); } } @@ -130,21 +129,27 @@ impl TraceController { /// /// This layer returns on any error allowing the cli to continue work /// despite failing to log to a file. This helps in case of permission errors and similar. -struct FileAppendLayer { +pub(crate) struct FileAppendLayer { file_path: PathBuf, buffer: Mutex, } impl FileAppendLayer { - pub fn new(file_path: PathBuf) -> io::Result { + fn new() -> Self { + let file_path = Self::log_path(); + if !file_path.exists() { _ = std::fs::write(&file_path, ""); } - Ok(Self { + Self { file_path, buffer: Mutex::new(String::new()), - }) + } + } + + pub(crate) fn log_path() -> PathBuf { + std::env::temp_dir().join(LOG_FILE_NAME) } } @@ -208,15 +213,6 @@ where let mut visitor = CollectVisitor::new(); event.record(&mut visitor); - // If the TUI output is disabled we let fmt subscriber handle the logs - // EXCEPT for cargo logs which we just print. - if !TUI_ENABLED.load(Ordering::SeqCst) { - if visitor.source == TraceSrc::Cargo { - println!("{}", visitor.message); - } - return; - } - let meta = event.metadata(); let level = meta.level(); @@ -237,8 +233,6 @@ where .unbounded_send(TraceMsg::text(visitor.source, *level, final_msg)) .unwrap(); } - - // TODO: support spans? structured tui log display? } /// A record visitor that collects dx-specific info and user-provided fields for logging consumption. @@ -280,26 +274,6 @@ impl Visit for CollectVisitor { } } -struct FmtLogWriter {} - -impl Write for FmtLogWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - if !TUI_ENABLED.load(Ordering::SeqCst) { - return std::io::stdout().write(buf); - } - - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - if !TUI_ENABLED.load(Ordering::SeqCst) { - std::io::stdout().flush()?; - } - - Ok(()) - } -} - /// Formats a tracing field and value, removing any internal fields from the final output. fn format_field(field_name: &str, value: &dyn Debug) -> String { let mut out = String::new(); diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 0d8c49e18a..178b9519fa 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -3,19 +3,50 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![cfg_attr(docsrs, feature(doc_cfg))] +mod assets; +mod builder; +mod bundle_utils; +mod cli; +mod config; +mod dioxus_crate; +mod dx_build_info; +mod error; +mod fastfs; +mod filemap; +mod logging; +mod metadata; +mod platform; +mod profiles; +mod rustup; +mod serve; +mod settings; +mod slog; +mod tooling; + +pub(crate) use builder::*; +pub(crate) use cli::*; +pub(crate) use config::*; +pub(crate) use dioxus_crate::*; +pub(crate) use error::*; +pub(crate) use filemap::*; +pub(crate) use logging::*; +pub(crate) use platform::*; +pub(crate) use rustup::*; +pub(crate) use settings::*; +pub(crate) use slog::*; + #[tokio::main] async fn main() -> anyhow::Result<()> { + use anyhow::Context; + use Commands::*; + // If we're being ran as a linker (likely from ourselves), we want to act as a linker instead. if let Some(link_action) = link::LinkAction::from_env() { return link_action.run(); } - let args = Cli::parse(); - - // Start the tracer so it captures logs from the build engine before we start the builder - TraceController::initialize(&args); - - match args.action { + let args = TraceController::initialize(); + let result = match args.action { Translate(opts) => opts .translate() .context("⛔️ Translation of HTML into RSX failed:"), @@ -32,7 +63,7 @@ async fn main() -> anyhow::Result<()> { Clean(opts) => opts.clean().context("🚫 Cleaning project failed:"), - Build(opts) => opts.build_it().await.context("🚫 Building project failed:"), + Build(opts) => opts.run_cmd().await.context("🚫 Building project failed:"), Serve(opts) => opts.serve().await.context("🚫 Serving project failed:"), @@ -41,39 +72,17 @@ async fn main() -> anyhow::Result<()> { Run(opts) => opts.run().await.context("🚫 Running project failed:"), Doctor(opts) => opts.run().await.context("🚫 Checking project failed:"), + }; + + // Provide a structured output for third party tools that can consume the output of the CLI + match result { + Ok(output) => { + tracing::debug!(structured = ?output); + Ok(()) + } + Err(err) => { + tracing::debug!(structured = ?err); + Err(err) + } } } - -mod assets; -mod builder; -mod bundle_utils; -mod cli; -mod config; -mod dioxus_crate; -mod dx_build_info; -mod error; -mod fastfs; -mod filemap; -mod logging; -mod metadata; -mod platform; -mod profiles; -mod rustup; -mod serve; -mod settings; -mod tooling; - -pub(crate) use builder::*; -pub(crate) use cli::*; -pub(crate) use config::*; -pub(crate) use dioxus_crate::*; -pub(crate) use error::*; -pub(crate) use filemap::*; -pub(crate) use logging::*; -pub(crate) use platform::*; -pub(crate) use rustup::*; -pub(crate) use settings::*; - -use anyhow::Context; -use clap::Parser; -use Commands::*; diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 4d96be1b6f..c458db7b12 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -249,7 +249,6 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { _ = devserver.shutdown().await; _ = screen.shutdown(); builder.abort_all(); - tracer.shutdown(); if let Err(err) = err { eprintln!("Exiting with error: {}", err); diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 454d2fbbd4..6b385534e6 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -99,8 +99,8 @@ impl Output { more_modal_open: false, pending_logs: VecDeque::new(), throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()), - trace: crate::logging::TRACE.load(std::sync::atomic::Ordering::Relaxed), - verbose: crate::logging::VERBOSE.load(std::sync::atomic::Ordering::Relaxed), + trace: crate::logging::VERBOSITY.get().unwrap().trace, + verbose: crate::logging::VERBOSITY.get().unwrap().verbose, tick_animation: false, tick_interval: { let mut interval = tokio::time::interval(Duration::from_millis(TICK_RATE_MS)); diff --git a/packages/cli/src/slog.rs b/packages/cli/src/slog.rs new file mode 100644 index 0000000000..703966377c --- /dev/null +++ b/packages/cli/src/slog.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +use crate::Platform; + +/// The structured output for the CLI +/// +/// This is designed such that third party tools can reliably consume the output of the CLI when +/// outputting json. +/// +/// Not every log outputted will be parsable, but all structued logs should be. +/// +/// This means the debug format of this log needs to be parsable json, not the default debug format. +/// +/// We guarantee that the last line of the command represents the success of the command, such that +/// tools can simply parse the last line of the output. +/// +/// There might be intermediate lines that are parseable as structured logs (which you can put here) +/// but they are not guaranteed to be, such that we can provide better error messages for the user. +#[derive(Serialize, Deserialize)] +pub enum StructuredOutput { + BuildFinished {}, + BundleOutput { + platform: Platform, + bundles: Vec, + }, + GenericSuccess, + Error { + message: String, + }, +} + +impl std::fmt::Debug for StructuredOutput { + // todo(jon): I think to_string can write directly to the formatter? + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let out = serde_json::to_string(self).unwrap(); + f.write_str(&out) + } +} diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 97e15d90a3..199169d37c 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -16,6 +16,7 @@ pub struct LaunchBuilder { } pub type LaunchFn = fn(fn() -> Element, Vec, Vec>); + /// A context function is a Send and Sync closure that returns a boxed trait object pub type ContextFn = Box Box + Send + Sync + 'static>;