Przeglądaj źródła

Merge pull request #15 from mrxiaozhuox/master

Add `tools` install & manage support
Jon Kelley 3 lat temu
rodzic
commit
a6f5da4b74

+ 7 - 2
Cargo.toml

@@ -9,10 +9,12 @@ license = "MIT/Apache-2.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+
 # cli core
 clap = { version = "3.0.14", features = ["derive"] }
 thiserror = "1.0.30"
 wasm-bindgen-cli-support = "0.2.79"
+
 # features
 log = "0.4.14"
 fern = { version = "0.6.0", features = ["colored"] }
@@ -37,10 +39,13 @@ hyper = "0.14.17"
 axum = { version = "0.4.5", features = ["ws", "headers"] }
 tower-http = { version = "0.2.2", features = ["fs", "trace"] }
 headers = "0.3.7"
+# tools download
+dirs = "4.0.0"
+reqwest = { version = "0.11", features = ["rustls-tls", "stream", "trust-dns"] }
+flate2 = "1.0.22"
+tar = "0.4.38"
 tower = "0.4.12"
 
-# hyper = { version = "0.14.11", features = ["full"] }
-
 [[bin]]
 path = "src/main.rs"
 name = "dioxus"

+ 45 - 0
Dioxus.toml

@@ -0,0 +1,45 @@
+[application]
+
+# dioxus project name
+name = "dioxus-cli"
+
+# default platfrom
+# you can also use `dioxus serve/build --platform XXX` to use other platform
+# value: web | desktop
+default_platform = "desktop"
+
+# Web `build` & `serve` dist path
+out_dir = "dist"
+
+# resource (static) file folder
+asset_dir = "public"
+
+[web.app]
+
+# HTML title tag content
+title = "dioxus | ⛺"
+
+[web.watcher]
+
+watch_path = ["src"]
+
+# include `assets` in web platform
+[web.resource]
+
+# CSS style file
+style = []
+
+# Javascript code file
+script = []
+
+[web.resource.dev]
+
+# Javascript code file
+# serve: [dev-server] only
+script = []
+
+[application.tools]
+
+# use binaryen.wasm-opt for output Wasm file
+# binaryen just will trigger in `web` platform
+binaryen = { wasm_opt = true }

+ 3 - 1
src/assets/autoreload.js

@@ -1,3 +1,6 @@
+// Dioxus-CLI
+// https://github.com/DioxusLabs/cli
+
 (function () {
   var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
   var url = protocol + '//' + window.location.host + '/_dioxus/ws';
@@ -15,7 +18,6 @@
   var ws = new WebSocket(url);
   ws.onmessage = (ev) => {
       if (ev.data == "reload") {
-        //   alert("reload!!!");
           window.location.reload();
       }
   };

+ 8 - 2
src/assets/dioxus.toml

@@ -6,7 +6,7 @@ name = "{{project-name}}"
 # default platfrom
 # you can also use `dioxus serve/build --platform XXX` to use other platform
 # value: web | desktop
-default_platform = "web"
+default_platform = "{{default-platform}}"
 
 # Web `build` & `serve` dist path
 out_dir = "dist"
@@ -36,4 +36,10 @@ script = []
 
 # Javascript code file
 # serve: [dev-server] only
-script = []
+script = []
+
+[application.tools]
+
+# use binaryen.wasm-opt for output Wasm file
+# binaryen just will trigger in `web` platform
+binaryen = { wasm_opt = [".PROJECT_OUT"] }

+ 0 - 4
src/assets/index.html

@@ -1,14 +1,11 @@
 <html>
-
 <head>
   <title>{app_title}</title>
   <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta charset="UTF-8" />
   {style_include}
-
 </head>
-
 <body>
   <div id="main"></div>
   <!-- Note the usage of `type=module` here as this is an ES6 module -->
@@ -22,5 +19,4 @@
   </script>
   {script_include}
 </body>
-
 </html>

+ 50 - 2
src/builder.rs

@@ -4,7 +4,8 @@ use crate::{
     DioxusConfig,
 };
 use std::{
-    fs::{copy, create_dir_all, remove_dir_all},
+    fs::{copy, create_dir_all, remove_dir_all, File},
+    io::Read,
     panic,
     path::PathBuf,
     process::Command,
@@ -95,8 +96,43 @@ pub fn build(config: &CrateConfig) -> Result<()> {
     });
     if bindgen_result.is_err() {
         log::error!("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.79` Bindgen crate.");
+        return Ok(());
     }
 
+    // check binaryen:wasm-opt tool
+    let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
+    if dioxus_tools.contains_key("binaryen") {
+        let info = dioxus_tools.get("binaryen").unwrap();
+        let binaryen = crate::tools::Tool::Binaryen;
+
+        if binaryen.is_installed() {
+            if let Some(sub) = info.as_table() {
+                if sub.contains_key("wasm_opt")
+                    && sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
+                {
+                    let target_file = out_dir
+                        .join("assets")
+                        .join("dioxus")
+                        .join(format!("{}_bg.wasm", dioxus_config.application.name));
+                    if target_file.is_file() {
+                        binaryen.call(
+                            "wasm-opt",
+                            vec![
+                                target_file.to_str().unwrap(),
+                                "-o",
+                                target_file.to_str().unwrap(),
+                            ],
+                        )?;
+                    }
+                }
+            }
+        } else {
+            log::warn!(
+                "Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
+            );
+        }
+    }
+  
     // this code will copy all public file to the output dir
     let copy_options = fs_extra::dir::CopyOptions {
         overwrite: true,
@@ -237,7 +273,19 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
 }
 
 pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
-    let mut html = String::from(include_str!("./assets/index.html"));
+    let crate_root = crate::cargo::crate_root().unwrap();
+    let custom_html_file = crate_root.join("index.html");
+    let mut html = if custom_html_file.is_file() {
+        let mut buf = String::new();
+        let mut file = File::open(custom_html_file).unwrap();
+        if file.read_to_string(&mut buf).is_ok() {
+            buf
+        } else {
+            String::from(include_str!("./assets/index.html"))
+        }
+    } else {
+        String::from(include_str!("./assets/index.html"))
+    };
 
     let resouces = config.web.resource.clone();
 

+ 1 - 0
src/cli/build/mod.rs

@@ -12,6 +12,7 @@ impl Build {
     pub fn build(self) -> Result<()> {
         let mut crate_config = crate::CrateConfig::new()?;
 
+
         // change the release state.
         crate_config.with_release(self.build.release);
 

+ 25 - 2
src/cli/config/mod.rs

@@ -13,14 +13,26 @@ pub enum Config {
         #[clap(long)]
         #[serde(default)]
         force: bool,
+
+        /// Project default platform
+        #[clap(long, default_value = "web")]
+        platform: String,
     },
+    /// Format print Dioxus config.
+    FormatPrint {},
+    /// Create a custom html file.
+    CustomHtml {}
 }
 
 impl Config {
     pub fn config(self) -> Result<()> {
         let crate_root = crate::cargo::crate_root()?;
         match self {
-            Config::Init { name, force } => {
+            Config::Init {
+                name,
+                force,
+                platform,
+            } => {
                 let conf_path = crate_root.join("Dioxus.toml");
                 if conf_path.is_file() && !force {
                     log::warn!(
@@ -30,10 +42,21 @@ impl Config {
                 }
                 let mut file = File::create(conf_path)?;
                 let content = String::from(include_str!("../../assets/dioxus.toml"))
-                    .replace("{{project-name}}", &name);
+                    .replace("{{project-name}}", &name)
+                    .replace("{{default-platform}}", &platform);
                 file.write_all(content.as_bytes())?;
                 log::info!("🚩 Init config file completed.");
             }
+            Config::FormatPrint {} => {
+                println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
+            }
+            Config::CustomHtml {} => {
+                let html_path = crate_root.join("index.html");
+                let mut file = File::create(html_path)?;
+                let content = include_str!("../../assets/index.html");
+                file.write_all(content.as_bytes())?;
+                log::info!("🚩 Create custom html file done.");
+            }
         }
         Ok(())
     }

+ 4 - 0
src/cli/mod.rs

@@ -5,6 +5,7 @@ pub mod config;
 pub mod create;
 pub mod serve;
 pub mod translate;
+pub mod tool;
 
 use crate::{
     cfg::{ConfigOptsBuild, ConfigOptsServe},
@@ -55,4 +56,7 @@ pub enum Commands {
     /// Dioxus config file controls.
     #[clap(subcommand)]
     Config(config::Config),
+    /// Install  & Manage tools for Dioxus-cli.
+    #[clap(subcommand)]
+    Tool(tool::Tool),
 }

+ 66 - 0
src/cli/tool/mod.rs

@@ -0,0 +1,66 @@
+use crate::tools;
+
+use super::*;
+
+/// Build the Rust WASM app and all of its assets.
+#[derive(Clone, Debug, Deserialize, Subcommand)]
+#[clap(name = "tool")]
+pub enum Tool {
+    /// Return all dioxus-cli support tools.
+    List {},
+    /// Get default app install path.
+    AppPath {},
+    /// Install a new tool.
+    Add {
+        name: String
+    }
+}
+
+impl Tool {
+    pub async fn tool(self) -> Result<()> {
+        match self {
+            Tool::List {} => {
+                for item in tools::tool_list() {
+                    if tools::Tool::from_str(item).unwrap().is_installed() {
+                        println!("{item} [installed]");
+                    } else {
+                        println!("{item}");
+                    }
+                }
+            }
+            Tool::AppPath {} => {
+                println!("{}", tools::tools_path().to_str().unwrap());
+            }
+            Tool::Add { name } => {
+                let tool_list = tools::tool_list();
+                
+                if !tool_list.contains(&name.as_str()) {
+                    log::error!("Tool {name} not found.");
+                    return Ok(());
+                }
+                let target_tool = tools::Tool::from_str(&name).unwrap();
+
+                if target_tool.is_installed() {
+                    log::warn!("Tool {name} is installed.");
+                    return Ok(());
+                }
+
+                log::info!("Start to download tool package...");
+                if let Err(e) = target_tool.download_package().await {
+                    log::error!("Tool download failed: {e}");
+                    return Ok(());
+                }
+
+                log::info!("Start to install tool package...");
+                if let Err(e) = target_tool.install_package().await {
+                    log::error!("Tool install failed: {e}");
+                    return Ok(());
+                }
+
+                log::info!("Tool {name} install successfully!");
+            }
+        }
+
+        Ok(())
+    }
+}

+ 3 - 1
src/config.rs

@@ -1,6 +1,6 @@
 use crate::error::Result;
 use serde::{Deserialize, Serialize};
-use std::{fs::File, io::Read, path::PathBuf};
+use std::{fs::File, io::Read, path::PathBuf, collections::HashMap};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct DioxusConfig {
@@ -34,6 +34,7 @@ impl Default for DioxusConfig {
                 default_platform: "web".to_string(),
                 out_dir: Some(PathBuf::from("dist")),
                 asset_dir: Some(PathBuf::from("public")),
+                tools: None,
                 sub_package: None,
             },
             web: WebConfig {
@@ -65,6 +66,7 @@ pub struct ApplicationConfig {
     pub default_platform: String,
     pub out_dir: Option<PathBuf>,
     pub asset_dir: Option<PathBuf>,
+    pub tools: Option<HashMap<String, toml::Value>>,
     pub sub_package: Option<String>,
 }
 

+ 2 - 0
src/lib.rs

@@ -1,5 +1,7 @@
 pub mod builder;
 pub mod server;
+pub mod tools;
+
 pub use builder::*;
 
 pub mod cargo;

+ 6 - 0
src/main.rs

@@ -42,6 +42,12 @@ async fn main() -> Result<()> {
                 log::error!("config error: {}", e);
             }
         }
+
+        Commands::Tool(opts) => {
+            if let Err(e) = opts.tool().await {
+                log::error!("tool error: {}", e);
+            }
+        }
     }
 
     Ok(())

+ 16 - 16
src/server/mod.rs

@@ -35,23 +35,23 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
     // file watcher: check file change
     let watcher_conf = config.clone();
     let mut watcher = RecommendedWatcher::new(move |_: notify::Result<notify::Event>| {
-        if chrono::Local::now().timestamp() > last_update_time
-            && builder::build(&watcher_conf).is_ok()
-        {
-            // change the websocket reload state to true;
-            // the page will auto-reload.
-            if watcher_conf
-                .dioxus_config
-                .web
-                .watcher
-                .reload_html
-                .unwrap_or(false)
-            {
-                let _ = Serve::regen_dev_page(&watcher_conf);
+        if chrono::Local::now().timestamp() > last_update_time {
+            log::info!("Start to rebuild project...");
+            if builder::build(&watcher_conf).is_ok() {
+                // change the websocket reload state to true;
+                // the page will auto-reload.
+                if watcher_conf
+                    .dioxus_config
+                    .web
+                    .watcher
+                    .reload_html
+                    .unwrap_or(false)
+                {
+                    let _ = Serve::regen_dev_page(&watcher_conf);
+                }
+                let _ = reload_tx.send("reload".into());
+                last_update_time = chrono::Local::now().timestamp();
             }
-            println!("watcher send reload");
-            reload_tx.send("reload".into()).unwrap();
-            last_update_time = chrono::Local::now().timestamp();
         }
     })
     .unwrap();

+ 187 - 0
src/tools.rs

@@ -0,0 +1,187 @@
+use std::{
+    fs::{create_dir_all, File},
+    path::PathBuf,
+    process::Command,
+};
+
+use anyhow::Context;
+use flate2::read::GzDecoder;
+use futures::StreamExt;
+use tar::Archive;
+use tokio::io::AsyncWriteExt;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Tool {
+    Binaryen,
+}
+
+pub fn tool_list() -> Vec<&'static str> {
+    vec!["binaryen"]
+}
+
+pub fn app_path() -> PathBuf {
+    let data_local = dirs::data_local_dir().unwrap();
+    let dioxus_dir = data_local.join("dioxus");
+    if !dioxus_dir.is_dir() {
+        create_dir_all(&dioxus_dir).unwrap();
+    }
+    dioxus_dir
+}
+
+pub fn temp_path() -> PathBuf {
+    let app_path = app_path();
+    let temp_path = app_path.join("temp");
+    if !temp_path.is_dir() {
+        create_dir_all(&temp_path).unwrap();
+    }
+    temp_path
+}
+
+pub fn tools_path() -> PathBuf {
+    let app_path = app_path();
+    let temp_path = app_path.join("tools");
+    if !temp_path.is_dir() {
+        create_dir_all(&temp_path).unwrap();
+    }
+    temp_path
+}
+
+#[allow(clippy::should_implement_trait)]
+impl Tool {
+    /// from str to tool enum
+    pub fn from_str(name: &str) -> Option<Self> {
+        match name {
+            "binaryen" => Some(Self::Binaryen),
+            _ => None,
+        }
+    }
+
+    /// get current tool name str
+    pub fn name(&self) -> &str {
+        match self {
+            Self::Binaryen => "binaryen",
+        }
+    }
+
+    /// get tool bin dir path
+    pub fn bin_path(&self) -> &str {
+        match self {
+            Self::Binaryen => "bin",
+        }
+    }
+
+    /// get target platform
+    pub fn target_platform(&self) -> &str {
+        match self {
+            Self::Binaryen => {
+                if cfg!(target_os = "windows") {
+                    "windows"
+                } else if cfg!(target_os = "macos") {
+                    "macos"
+                } else if cfg!(target_os = "linux") {
+                    "linux"
+                } else {
+                    panic!("unsupported platformm");
+                }
+            }
+        }
+    }
+
+    /// get tool package download url
+    pub fn download_url(&self) -> String {
+        match self {
+            Self::Binaryen => {
+                format!(
+                    "https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-{target}.tar.gz",
+                    target = self.target_platform()
+                )
+            }
+        }
+    }
+
+    /// get package extension name
+    pub fn extension(&self) -> &str {
+        match self {
+            Self::Binaryen => "tar.gz",
+        }
+    }
+
+    /// check tool state
+    pub fn is_installed(&self) -> bool {
+        tools_path().join(self.name()).is_dir()
+    }
+
+    /// get download temp path
+    pub fn temp_out_path(&self) -> PathBuf {
+        temp_path().join(format!("{}-tool.tmp", self.name()))
+    }
+
+    /// start to download package
+    pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
+        let download_url = self.download_url();
+        let temp_out = self.temp_out_path();
+        let mut file = tokio::fs::File::create(&temp_out)
+            .await
+            .context("failed creating temporary output file")?;
+
+        let resp = reqwest::get(download_url).await.unwrap();
+
+        let mut res_bytes = resp.bytes_stream();
+        while let Some(chunk_res) = res_bytes.next().await {
+            let chunk = chunk_res.context("error reading chunk from download")?;
+            let _ = file.write(chunk.as_ref()).await;
+        }
+
+        Ok(temp_out)
+    }
+
+    /// start to install package
+    pub async fn install_package(&self) -> anyhow::Result<()> {
+        let temp_path = self.temp_out_path();
+        let tool_path = tools_path();
+
+        let dir_name = if self == &Tool::Binaryen {
+            "binaryen-version_105"
+        } else {
+            ""
+        };
+
+        if self.extension() == "tar.gz" {
+            let tar_gz = File::open(temp_path)?;
+            let tar = GzDecoder::new(tar_gz);
+            let mut archive = Archive::new(tar);
+            archive.unpack(&tool_path)?;
+            // println!("{:?} -> {:?}", tool_path.join(dir_name), tool_path.join(self.name()));
+            std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
+        }
+
+        Ok(())
+    }
+
+    pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
+        let bin_path = tools_path().join(self.name()).join(self.bin_path());
+
+        let command_file = match self {
+            Tool::Binaryen => {
+                if cfg!(target_os = "windows") {
+                    format!("{}.exe", command)
+                } else {
+                    command.to_string()
+                }
+            }
+        };
+
+        if !bin_path.join(&command_file).is_file() {
+            return Err(anyhow::anyhow!("Command file not found."));
+        }
+
+        let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
+
+        let output = command
+            .args(&args[..])
+            .stdout(std::process::Stdio::inherit())
+            .stderr(std::process::Stdio::inherit())
+            .output()?;
+        Ok(output.stdout)
+    }
+}