diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d28f417..e389254 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,6 +17,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install prerequisites run: | + cargo install cargo-workspaces + sudo apt-get update sudo apt-get install -y \ libudev-dev \ diff --git a/.gitignore b/.gitignore index dc5e6fd..c09504c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,11 +19,11 @@ tarpaulin-report.html # Jetbrain .idea/ -/docs/handbook/.obsidian/app.json -/docs/handbook/.obsidian/appearance.json -/docs/handbook/.obsidian/hotkeys.json -/docs/handbook/.obsidian/workspace.json -/docs/handbook/.obsidian/plugins -!/docs/handbook/.obsidian/plugins/obsidian-git/data.json -!/docs/handbook/.obsidian/plugins/obsidian-linter/data.json +.obsidian/app.json +.obsidian/appearance.json +.obsidian/hotkeys.json +.obsidian/workspace.json +.obsidian/plugins +!.obsidian/plugins/obsidian-git/data.json +!.obsidian/plugins/obsidian-linter/data.json diff --git a/docs/handbook/.obsidian/community-plugins.json b/.obsidian/community-plugins.json similarity index 100% rename from docs/handbook/.obsidian/community-plugins.json rename to .obsidian/community-plugins.json diff --git a/docs/handbook/.obsidian/core-plugins.json b/.obsidian/core-plugins.json similarity index 100% rename from docs/handbook/.obsidian/core-plugins.json rename to .obsidian/core-plugins.json diff --git a/docs/handbook/.obsidian/graph.json b/.obsidian/graph.json similarity index 100% rename from docs/handbook/.obsidian/graph.json rename to .obsidian/graph.json diff --git a/docs/handbook/.obsidian/themes/GitHub theme/manifest.json b/.obsidian/themes/GitHub theme/manifest.json similarity index 100% rename from docs/handbook/.obsidian/themes/GitHub theme/manifest.json rename to .obsidian/themes/GitHub theme/manifest.json diff --git a/docs/handbook/.obsidian/themes/GitHub theme/theme.css b/.obsidian/themes/GitHub theme/theme.css similarity index 100% rename from docs/handbook/.obsidian/themes/GitHub theme/theme.css rename to .obsidian/themes/GitHub theme/theme.css diff --git a/docs/handbook/.obsidian/themes/Minimal/manifest.json b/.obsidian/themes/Minimal/manifest.json similarity index 100% rename from docs/handbook/.obsidian/themes/Minimal/manifest.json rename to .obsidian/themes/Minimal/manifest.json diff --git a/docs/handbook/.obsidian/themes/Minimal/theme.css b/.obsidian/themes/Minimal/theme.css similarity index 100% rename from docs/handbook/.obsidian/themes/Minimal/theme.css rename to .obsidian/themes/Minimal/theme.css diff --git a/Cargo.lock b/Cargo.lock index a8391ef..952e0de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,6 +852,31 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cmd_lib" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62" +dependencies = [ + "cmd_lib_macros", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cocoa" version = "0.24.0" @@ -1261,6 +1286,7 @@ dependencies = [ "clap", "config", "dioxus", + "dip_bundle", "dip_cli", "dip_core", "dip_desktop", @@ -1272,6 +1298,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "dip_bundle" +version = "0.1.0" +dependencies = [ + "anyhow", + "bevy", + "cmd_lib", + "convert_case 0.5.0", + "dip_core", + "dirs", + "pathdiff", + "reqwest", + "tempfile", + "walkdir", +] + [[package]] name = "dip_cli" version = "0.1.0" @@ -1451,6 +1493,17 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags", + "libc", + "winapi", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -2826,6 +2879,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "os_pipe" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "os_str_bytes" version = "6.3.0" diff --git a/Cargo.toml b/Cargo.toml index 63c6683..6828727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "plugins/bundle", "plugins/core", "plugins/macro", "plugins/task", @@ -23,18 +24,24 @@ keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"] anyhow = "1.0" bevy = { version = "0.8", default-features = false } bevy_ecs = "0.8" +cmd_lib = "1" config = "0.13" +convert_case = "0.5" dioxus = { version = "0.2", features = ["fermi"] } dip = { version = "0.1", path = ".", features = ["desktop"] } +dip_bundle = { version = "0.1", path = "./plugins/bundle", features = ["full"] } dip_core = { version = "0.1", path = "./plugins/core" } dip_macro = { version = "0.1", path = "./plugins/macro" } dip_task = { version = "0.1", path = "./plugins/task" } dirs = "4.0" +pathdiff = "0.2" reqwest = { version = "0.11", features = ["json", "blocking"] } tokio = { version = "1.18", features = ["rt-multi-thread", "sync", "macros", "fs"], default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" +tempfile = "3" +walkdir = "2" [package] name = "dip" @@ -57,6 +64,7 @@ bevy.workspace = true clap = { version = "3.2", features = ["derive"], optional = true } config.workspace = true dioxus.workspace = true +dip_bundle.workspace = true dip_cli = { version = "0.1", path = "./plugins/ui/cli", optional = true } dip_core = { version = "0.1", path = "./plugins/core" } dip_desktop = { version = "0.1", path = "./plugins/ui/desktop", optional = true } diff --git a/README.md b/README.md index cc4bb06..683270c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +

dip

diff --git a/docs/handbook/.obsidian/plugins/obsidian-git/data.json b/docs/handbook/.obsidian/plugins/obsidian-git/data.json deleted file mode 100644 index 9ce6101..0000000 --- a/docs/handbook/.obsidian/plugins/obsidian-git/data.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "commitMessage": "vault backup: {{date}}", - "autoCommitMessage": "vault backup: {{date}}", - "commitDateFormat": "YYYY-MM-DD HH:mm:ss", - "autoSaveInterval": 0, - "autoPushInterval": 0, - "autoPullInterval": 0, - "autoPullOnBoot": false, - "disablePush": false, - "pullBeforePush": true, - "disablePopups": false, - "listChangedFilesInMessageBody": false, - "showStatusBar": true, - "updateSubmodules": true, - "syncMethod": "merge", - "customMessageOnAutoBackup": false, - "autoBackupAfterFileChange": false, - "treeStructure": false, - "refreshSourceControl": true, - "basePath": "../..", - "differentIntervalCommitAndPush": false, - "changedFilesInStatusBar": false, - "username": "", - "showedMobileNotice": true, - "refreshSourceControlTimer": 7000, - "showBranchStatusBar": true -} diff --git a/docs/handbook/Internal b/docs/handbook/Internal index 8a1510f..3f793af 160000 --- a/docs/handbook/Internal +++ b/docs/handbook/Internal @@ -1 +1 @@ -Subproject commit 8a1510f3cab5829051f2c859779909995cf7768a +Subproject commit 3f793af89d53c67b626443be7f16d98efb89b256 diff --git a/docs/handbook/Product/Framework/CLI/Installation.md b/docs/handbook/Product/Framework/CLI/Installation.md index 973e9cd..4c236c6 100644 --- a/docs/handbook/Product/Framework/CLI/Installation.md +++ b/docs/handbook/Product/Framework/CLI/Installation.md @@ -1,3 +1,4 @@ + ## Installation ### From Crates.io diff --git a/plugins/bundle/Cargo.toml b/plugins/bundle/Cargo.toml new file mode 100644 index 0000000..13fb180 --- /dev/null +++ b/plugins/bundle/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dip_bundle" +version.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +bevy.workspace = true +dip_core.workspace = true +dirs.workspace = true +cmd_lib.workspace = true +convert_case.workspace = true +pathdiff.workspace = true +reqwest.workspace = true +tempfile.workspace = true +walkdir.workspace = true + +[features] +# default = ["full"] +full = ["dotfiles", "brew", "tailwind"] +dotfiles = [] +brew = [] +tailwind = [] diff --git a/plugins/bundle/src/config.rs b/plugins/bundle/src/config.rs new file mode 100644 index 0000000..1b364a9 --- /dev/null +++ b/plugins/bundle/src/config.rs @@ -0,0 +1,43 @@ +use bevy::app::{App, Plugin}; +use std::{fs, path::PathBuf}; + +pub struct BundleConfigPlugin; + +impl Plugin for BundleConfigPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + } +} + +pub struct BundleConfig { + app_path: PathBuf, +} + +impl Default for BundleConfig { + fn default() -> Self { + Self { + app_path: dirs::home_dir().unwrap().join(".dip"), + } + } +} + +impl BundleConfig { + pub fn app_path(&self) -> PathBuf { + Self::ensure_dir(&self.app_path); + + self.app_path.clone() + } + + pub fn install_path(&self) -> PathBuf { + let p = self.app_path().join("installs"); + Self::ensure_dir(&p); + + p + } + + fn ensure_dir(p: &PathBuf) { + if !&p.is_dir() { + fs::create_dir_all(&p).unwrap(); + } + } +} diff --git a/plugins/bundle/src/lib.rs b/plugins/bundle/src/lib.rs new file mode 100644 index 0000000..7c652be --- /dev/null +++ b/plugins/bundle/src/lib.rs @@ -0,0 +1,48 @@ +// mod config; +mod schedule; +mod tool; + +use bevy::{ + app::{App, Plugin}, + ecs::event::{EventReader, EventWriter}, +}; +// pub use config::BundleConfigPlugin; +pub use schedule::{BundleSchedulePlugin, BundleStage}; +use std::path::PathBuf; +use tool::{InstallTools, ToolPlugin}; + +pub struct BundlePlugin; + +impl Plugin for BundlePlugin { + fn build(&self, app: &mut App) { + app.add_plugin(BundleSchedulePlugin) + .add_event::() + .add_event::() + // .add_plugin(BundleConfigPlugin) + .add_plugin(ToolPlugin) + .add_system_to_stage(BundleStage::First, apply_bundle); + } +} + +// Events + +#[derive(Clone)] +pub struct ApplyBundle { + pub path: PathBuf, +} + +#[derive(Clone)] +pub struct CleanBundle { + pub path: PathBuf, +} + +fn apply_bundle( + mut events: EventReader, + mut install_tools: EventWriter, +) { + events.iter().for_each(|e| { + install_tools.send(InstallTools { + path: e.path.clone(), + }); + }); +} diff --git a/plugins/bundle/src/schedule.rs b/plugins/bundle/src/schedule.rs new file mode 100644 index 0000000..c92ee93 --- /dev/null +++ b/plugins/bundle/src/schedule.rs @@ -0,0 +1,46 @@ +use bevy::{ + app::{App, Plugin}, + ecs::schedule::{StageLabel, SystemStage}, +}; +use dip_core::schedule::DipStage; + +pub struct BundleSchedulePlugin; + +impl Plugin for BundleSchedulePlugin { + fn build(&self, app: &mut App) { + app.add_stage_after( + DipStage::Render, + BundleStage::First, + SystemStage::parallel(), + ) + .add_stage_after( + BundleStage::First, + BundleStage::Clean, + SystemStage::parallel(), + ) + .add_stage_after( + BundleStage::Clean, + BundleStage::Install, + SystemStage::parallel(), + ) + .add_stage_after( + BundleStage::Install, + BundleStage::Apply, + SystemStage::parallel(), + ) + .add_stage_after( + BundleStage::Apply, + BundleStage::Last, + SystemStage::parallel(), + ); + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] +pub enum BundleStage { + First, + Clean, + Install, + Apply, + Last, +} diff --git a/plugins/bundle/src/tool.rs b/plugins/bundle/src/tool.rs new file mode 100644 index 0000000..fc460b4 --- /dev/null +++ b/plugins/bundle/src/tool.rs @@ -0,0 +1,35 @@ +mod dotfiles; +mod homebrew; +mod script; +mod tailwind; +mod unix; + +pub use self::unix::UnixToolPlugin; +use bevy::{ + app::{App, Plugin}, + ecs::component::Component, +}; +use std::path::PathBuf; + +pub struct ToolPlugin; + +impl Plugin for ToolPlugin { + fn build(&self, app: &mut App) { + app.add_event::(); + + #[cfg(target_family = "unix")] + app.add_plugin(UnixToolPlugin); + } +} + +// Events + +#[derive(Clone)] +pub struct InstallTools { + pub path: PathBuf, +} + +// Commponents + +#[derive(Component)] +pub struct Tool; diff --git a/plugins/bundle/src/tool/dotfiles.rs b/plugins/bundle/src/tool/dotfiles.rs new file mode 100644 index 0000000..44d0989 --- /dev/null +++ b/plugins/bundle/src/tool/dotfiles.rs @@ -0,0 +1,171 @@ +use crate::{ApplyBundle, BundleStage, CleanBundle}; +use bevy::{ + app::{App, Plugin}, + ecs::event::EventReader, +}; +use pathdiff::diff_paths; +use std::{ + fs::{self, DirEntry}, + os, + path::PathBuf, +}; +use walkdir::WalkDir; + +// Plugin + +pub struct DotfilesPlugin; + +impl Plugin for DotfilesPlugin { + fn build(&self, app: &mut App) { + app.add_system_to_stage(BundleStage::Apply, apply) + .add_system_to_stage(BundleStage::Clean, clean); + } +} + +// Systems + +fn apply(mut events: EventReader) { + events.iter().for_each(|e| { + let dotfiles = Dotfiles::from(e.clone()); + + if dotfiles.bundle_exists() { + println!("📌 Apply dotfiles"); + + dotfiles.symlinks().for_each(|sym| sym.apply()); + } else { + println!("🟡 Skip: Apply dotfiles"); + println!("bundle/dotfiles directory is empty",); + } + + println!("✅ Apply dotfiles"); + }); +} + +fn clean(mut events: EventReader) { + events.iter().for_each(|e| { + let dotfiles = Dotfiles::from(e.clone()); + + if dotfiles.bundle_exists() { + println!("📌 Clean dotfiles"); + + dotfiles.symlinks().for_each(|sym| sym.clean()); + } else { + println!("🟡 Skip: Clean dotfiles"); + println!("bundle/dotfiles directory is empty",); + } + + println!("✅ Clean dotfiles"); + }); +} + +struct Dotfiles { + pub path: PathBuf, +} + +impl Dotfiles { + fn bundle_path(&self) -> PathBuf { + self.path.join("bundle/dotfiles") + } + + fn bundle_exists(&self) -> bool { + self.bundle_path().is_dir() + } + + fn symlinks(&self) -> std::boxed::Box + '_> { + Box::new( + self.packages() + .flat_map(|dir| WalkDir::new(&dir.path().into_iter())) + .filter_map(Result::ok) + .filter_map(|dir| { + let original = dir.path().to_path_buf().canonicalize().unwrap(); + let diff = diff_paths(dir.path(), &self.bundle_path()).unwrap(); + let dotfile_bundle_name = diff.iter().next().unwrap(); + let stripped = diff.strip_prefix(dotfile_bundle_name).unwrap(); + let link = dirs::home_dir().unwrap().join(stripped); + + if dir.file_type().is_dir() { + fs::create_dir_all(link).unwrap(); + None + } else { + Some(Symlink { original, link }) + } + }), + ) + } + + fn packages(&self) -> std::boxed::Box + '_> { + let dir = fs::read_dir(&self.bundle_path()) + .unwrap() + .filter_map(Result::ok); + + Box::new(dir) + } +} + +impl From for Dotfiles { + fn from(ApplyBundle { path }: ApplyBundle) -> Self { + Self { path } + } +} + +impl From for Dotfiles { + fn from(CleanBundle { path }: CleanBundle) -> Self { + Self { path } + } +} + +struct Symlink { + original: PathBuf, + link: PathBuf, +} + +impl Symlink { + fn apply(&self) { + if self.link.is_symlink() { + // println!( + // "{}", + // &self.format("🟡 Skip: File is already symlinked") + // ); + } else if self.link.is_file() { + // println!("{}", &self.format("🟡 Skip: File already exists")); + } else { + #[cfg(target_family = "unix")] + let res = os::unix::fs::symlink(&self.original, &self.link); + + #[cfg(target_family = "windows")] + let res = os::windows::fs::symlink(&self.original, &self.link); + + match res { + Ok(_) => { + println!("{}", &self.format("Symlink created")); + } + Err(e) => { + eprintln!("{}", &self.format(&e.to_string())); + } + } + } + } + + fn clean(&self) { + if self.link.is_symlink() { + match fs::remove_file(&self.link) { + Ok(_) => { + println!("{}", &self.format("Symlink removed")); + } + Err(e) => { + eprintln!("{}", &self.format(&e.to_string())); + } + } + } + } + + fn format<'a>(&self, message: &'a str) -> String { + format!( + "----------------------------------------------------------\n\ + {message}\n\ + original : {:?}\n\ + link : {:?}", + &self.original, &self.link, + ) + } +} diff --git a/plugins/bundle/src/tool/homebrew.rs b/plugins/bundle/src/tool/homebrew.rs new file mode 100644 index 0000000..42c2464 --- /dev/null +++ b/plugins/bundle/src/tool/homebrew.rs @@ -0,0 +1,154 @@ +use crate::{tool::InstallTools, ApplyBundle, BundleStage}; +use bevy::{ + app::{App, Plugin}, + ecs::event::EventReader, +}; +use cmd_lib::spawn_with_output; +use std::{ + fs::File, + io::{self, BufRead, BufReader, Write}, + path::{Path, PathBuf}, +}; +use tempfile::tempdir; + +// Plugin + +pub struct HomebrewPlugin; + +impl Plugin for HomebrewPlugin { + fn build(&self, app: &mut App) { + app.add_system_to_stage(BundleStage::Install, install) + .add_system_to_stage(BundleStage::Apply, apply); + } +} + +// Systems + +fn install(mut events: EventReader) { + events.iter().for_each(|e| { + let brew = Homebrew::from(e.clone()); + + match &brew.brewfile_path() { + Ok(_brewfile_path) => { + if brew.installed() { + println!("🟡 Skip: Install Homebrew"); + println!("brew is already installed"); + } else { + println!("📌 Install Homebrew bundle"); + + if let Err(e) = brew.install() { + println!("Failed to run brew install."); + eprintln!("{e}"); + } else { + println!("✅ Install Homebrew"); + } + } + } + Err(_e) => { + println!("🟡 Skip: Install Homebrew"); + println!("bundle/homebrew/Brewfile does not exists.",); + } + } + }); +} + +fn apply(mut events: EventReader) { + events.iter().for_each(|e| { + let brew = Homebrew::from(e.clone()); + + match &brew.brewfile_path() { + Ok(brewfile_path) => { + if brew.installed() { + println!("📌 Apply Homebrew bundle"); + + if let Err(e) = brew.apply(&brewfile_path) { + println!("Failed to run brew bundle."); + eprintln!("{e}"); + } else { + println!("✅ Apply Homebrew bundle"); + } + } else { + eprintln!("Could not find homebrew binary."); + } + } + Err(_e) => { + println!("🟡 Skip: Apply Homebrew bundle"); + println!("bundle/homebrew/Brewfile does not exists."); + } + } + }); +} + +struct Homebrew { + pub path: PathBuf, +} + +impl Homebrew { + fn homebrew_path() -> &'static str { + "/opt/homebrew/bin/brew" + } + + fn bundle_path(&self) -> PathBuf { + self.path.join("bundle/homebrew") + } + + fn brewfile_path(&self) -> io::Result { + self.bundle_path().join("Brewfile").canonicalize() + } + + fn installed(&self) -> bool { + Path::new(Self::homebrew_path()).exists() + } + + fn install(&self) -> anyhow::Result<()> { + let install_sh = reqwest::blocking::get( + "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh", + )? + .text()?; + + let dir = tempdir()?; + let file_path = dir.path().join("brew-install.sh"); + let file_path_str = file_path.display().to_string(); + let mut file = File::create(file_path)?; + file.write_all(install_sh.as_bytes()) + .expect("Unable to write file"); + + let mut install_brew = spawn_with_output!(NONINTERACTIVE=1 /bin/bash -C $file_path_str)?; + + let result = install_brew.wait_with_pipe(&mut |pipe| { + BufReader::new(pipe) + .lines() + .filter_map(|line| line.ok()) + .for_each(|f| println!("{f}")); + })?; + + Ok(result) + } + + fn apply(&self, brewfile_path: &PathBuf) -> anyhow::Result<()> { + let brewfile_path_str = &brewfile_path.display(); + + let mut brew_bundle = spawn_with_output!(brew bundle --cleanup --file $brewfile_path_str)?; + + let result = brew_bundle.wait_with_pipe(&mut |pipe| { + BufReader::new(pipe) + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{:?}", line)); + })?; + + Ok(result) + } +} + +impl From for Homebrew { + fn from(InstallTools { path }: InstallTools) -> Self { + Self { path } + } +} + +impl From for Homebrew { + fn from(ApplyBundle { path }: ApplyBundle) -> Self { + Self { path } + } +} diff --git a/plugins/bundle/src/tool/script.rs b/plugins/bundle/src/tool/script.rs new file mode 100644 index 0000000..4fabe02 --- /dev/null +++ b/plugins/bundle/src/tool/script.rs @@ -0,0 +1,117 @@ +use bevy::{ + app::{App, Plugin}, + ecs::event::EventReader, +}; +use cmd_lib::spawn_with_output; +use convert_case::{Case, Casing}; +use std::{ + io::{self, BufRead, BufReader}, + path::PathBuf, +}; + +use crate::{ApplyBundle, BundleStage}; + +pub struct ScriptPlugin; + +impl Plugin for ScriptPlugin { + fn build(&self, app: &mut App) { + app.add_system_to_stage(BundleStage::First, pre_script) + .add_system_to_stage(BundleStage::Last, post_script); + } +} + +fn pre_script(mut events: EventReader) { + events.iter().for_each(|e| Script::pre(e.clone()).run()); +} + +fn post_script(mut events: EventReader) { + events.iter().for_each(|e| Script::post(e.clone()).run()); +} + +struct Script { + event: ApplyBundle, + schedule: ScriptSchedule, +} + +impl Script { + fn pre(event: ApplyBundle) -> Self { + Self { + event, + schedule: ScriptSchedule::Pre, + } + } + + fn post(event: ApplyBundle) -> Self { + Self { + event, + schedule: ScriptSchedule::Post, + } + } + + fn run(&self) { + match self.find_file() { + Ok(file_path) => { + println!("📌 {} script", self.schedule.to_upper_camel()); + + let file_path_str = file_path.display(); + let mut script = spawn_with_output!(/bin/bash -C $file_path_str).unwrap(); + + let result = script.wait_with_pipe(&mut |pipe| { + BufReader::new(pipe) + .lines() + .filter_map(|line| line.ok()) + .for_each(|f| println!("{f}")); + }); + + if let Err(e) = result { + println!("Failed to run {} script.", self.schedule.to_string()); + eprintln!("{e}"); + } else { + println!("✅ {} script", self.schedule.to_upper_camel()); + } + } + Err(_e) => { + self.skip(); + } + } + } + + fn skip(&self) { + println!("🟡 Skip: {} script", &self.schedule.to_upper_camel()); + println!("{} does not exists.", &self.file_path().display()); + } + + fn find_file(&self) -> io::Result { + self.file_path().canonicalize() + } + + fn file_path(&self) -> PathBuf { + self.event + .path + .join(format!("bundle/scripts/{}", &self.file_name())) + } + + fn file_name(&self) -> String { + format!("{}.sh", self.schedule.to_string()) + } +} + +enum ScriptSchedule { + Pre, + Post, +} + +impl ScriptSchedule { + fn to_upper_camel(&self) -> String { + self.to_string().to_case(Case::UpperCamel) + } +} + +impl ToString for ScriptSchedule { + fn to_string(&self) -> String { + match self { + ScriptSchedule::Pre => "pre".into(), + ScriptSchedule::Post => "post".into(), + } + } +} diff --git a/plugins/bundle/src/tool/tailwind.rs b/plugins/bundle/src/tool/tailwind.rs new file mode 100644 index 0000000..3206487 --- /dev/null +++ b/plugins/bundle/src/tool/tailwind.rs @@ -0,0 +1,27 @@ +use crate::tool::InstallTools; +use bevy::{ + app::{App, Plugin}, + ecs::event::{EventReader, EventWriter}, + log, +}; + +// Plugin +pub struct TailwindPlugin; + +impl Plugin for TailwindPlugin { + fn build(&self, app: &mut App) { + app.add_event::().add_system(install); + } +} + +fn install(mut events: EventReader, mut installed: EventWriter) { + for _e in events.iter() { + log::warn!("TODO: Install Tool"); + + installed.send(TailwindInstalled); + } +} + +// Events + +pub struct TailwindInstalled; diff --git a/plugins/bundle/src/tool/unix.rs b/plugins/bundle/src/tool/unix.rs new file mode 100644 index 0000000..c42b110 --- /dev/null +++ b/plugins/bundle/src/tool/unix.rs @@ -0,0 +1,22 @@ +pub use crate::tool::{ + dotfiles::DotfilesPlugin, homebrew::HomebrewPlugin, script::ScriptPlugin, + tailwind::TailwindPlugin, +}; +use bevy::app::{App, Plugin}; + +pub struct UnixToolPlugin; + +impl Plugin for UnixToolPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(ScriptPlugin); + + #[cfg(feature = "dotfiles")] + app.add_plugin(DotfilesPlugin); + + #[cfg(feature = "brew")] + app.add_plugin(HomebrewPlugin); + + #[cfg(feature = "tailwind")] + app.add_plugin(TailwindPlugin); + } +} diff --git a/plugins/core/src/schedule.rs b/plugins/core/src/schedule.rs index 863b137..7f9e223 100644 --- a/plugins/core/src/schedule.rs +++ b/plugins/core/src/schedule.rs @@ -44,6 +44,6 @@ impl Plugin for UiSchedulePlugin { SystemStage::parallel(), ) .add_stage_after(DipStage::Prepare, DipStage::Apply, SystemStage::parallel()) - .add_stage_after(DipStage::Prepare, DipStage::Render, SystemStage::parallel()); + .add_stage_after(DipStage::Apply, DipStage::Render, SystemStage::parallel()); } } diff --git a/plugins/macro/Cargo.toml b/plugins/macro/Cargo.toml index c02ae57..5a60532 100644 --- a/plugins/macro/Cargo.toml +++ b/plugins/macro/Cargo.toml @@ -13,7 +13,7 @@ keywords.workspace = true proc-macro = true [dependencies] -convert_case = "0.5" +convert_case.workspace = true dirs.workspace = true quote = "1.0" proc-macro2 = "1.0" diff --git a/plugins/macro/src/cli.rs b/plugins/macro/src/cli.rs index 13b8097..15714d4 100644 --- a/plugins/macro/src/cli.rs +++ b/plugins/macro/src/cli.rs @@ -40,6 +40,7 @@ impl CliParser { &format!("handle_{}", subcommand_name), ) .unwrap(); + token.add_subcommand_handler = quote! { .add_startup_system_to_stage( ::dip::core::schedule::DipStartupStage::Action, diff --git a/plugins/macro/src/subcommand.rs b/plugins/macro/src/subcommand.rs index bba96ca..c23ae3d 100644 --- a/plugins/macro/src/subcommand.rs +++ b/plugins/macro/src/subcommand.rs @@ -43,7 +43,30 @@ impl SubcommandParser { } fn add_system(&self) -> TokenStream2 { - let mut subsubcommand_handler_names = vec![]; + let handler_with_system_order = self.handler_with_system_order(); + + let gen = quote! { + .add_startup_system_to_stage(::dip::core::schedule::DipStartupStage::Action, #handler_with_system_order); + }; + + gen + } + + fn handler_with_system_order(&self) -> TokenStream2 { + let gen_str = self + .child_names() + .iter() + .map(|name| format!("handle_{}", name.to_case(Case::Snake))) + .fold(self.handler_name().to_string(), |acc, name| { + format!("{acc}.before({})", name) + }); + let gen = TokenStream2::from_str(&gen_str).unwrap(); + + gen + } + + fn child_names(&self) -> Vec { + let mut child_names = vec![]; for v in self.commands_enum.variants.iter() { for a in v.attrs.iter() { for t in a.tokens.clone().into_iter() { @@ -54,15 +77,13 @@ impl SubcommandParser { TokenTree::Ident(ident) => { if ident.to_string() == "subcommand" { if let syn::Fields::Unnamed(f) = &v.fields { - let subsubcommand_ty = &f.unnamed[0].ty; - let subsubcommand_ty_quote = - quote! { #subsubcommand_ty }; - let subsubcommand_name = &subsubcommand_ty_quote + let child_ty = &f.unnamed[0].ty; + let child_ty_quote = quote! { #child_ty }; + let child_name = child_ty_quote .to_string() - .to_case(Case::Snake); + .to_case(Case::UpperCamel); - subsubcommand_handler_names - .push(format!("handle_{}", subsubcommand_name)); + child_names.push(child_name); } } } @@ -76,16 +97,7 @@ impl SubcommandParser { } } - let mut handler = self.handler_name().to_string(); - for n in subsubcommand_handler_names { - handler = format!("{}.before({})", handler, n); - } - - let handler_token = TokenStream2::from_str(&handler).unwrap(); - - quote! { - .add_startup_system_to_stage(::dip::core::schedule::DipStartupStage::Action, #handler_token); - } + child_names } fn subcommand_ty_name(&self) -> TokenStream2 { diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..201a382 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,46 @@ +mod action; + +use crate::cli::action::{ + ActionPlugin, ApplyBundleAction, BundleActionPlugin, CleanBundleAction, CliPlugin, +}; +use dip::{ + bevy::{ + app::{App, Plugin}, + ecs::event::{EventReader, EventWriter}, + }, + bundle::{ApplyBundle, BundlePlugin, CleanBundle}, + core::task::NoAsyncAction, +}; +use std::path::PathBuf; + +pub struct DipCliPlugin; + +impl Plugin for DipCliPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(CliPlugin::::oneshot()) + .add_plugin(ActionPlugin) + .add_plugin(BundleActionPlugin) + .add_plugin(BundlePlugin) + .add_system(install_bundle) + .add_system(clean_bundle); + } +} + +fn install_bundle( + mut actions: EventReader, + mut apply: EventWriter, +) { + actions.iter().for_each(|a| { + apply.send(ApplyBundle { + path: PathBuf::from(&a.path), + }); + }); +} + +fn clean_bundle(mut actions: EventReader, mut clean: EventWriter) { + actions.iter().for_each(|a| { + clean.send(CleanBundle { + path: PathBuf::from(&a.path), + }) + }); +} diff --git a/src/plugin/cli.rs b/src/cli/action.rs similarity index 65% rename from src/plugin/cli.rs rename to src/cli/action.rs index 9489719..2cb1922 100644 --- a/src/plugin/cli.rs +++ b/src/cli/action.rs @@ -1,3 +1,7 @@ +// mod build; + +// pub use build::*; + use dip::cli::{CliPlugin, SubcommandPlugin}; #[derive(CliPlugin, clap::Parser)] @@ -12,7 +16,7 @@ pub enum Action { Build(BuildArgs), #[clap(subcommand)] - Tool(ToolAction), + Bundle(BundleAction), } #[derive(clap::Args, Clone, Debug)] @@ -34,7 +38,19 @@ pub struct BuildArgs { } #[derive(SubcommandPlugin, clap::Subcommand, Clone, Debug)] -pub enum ToolAction { - List, - Add { name: String }, +pub enum BundleAction { + Apply(ApplyBundleArgs), + Clean(CleanBundleArgs), +} + +#[derive(clap::Args, Clone, Debug)] +pub struct ApplyBundleArgs { + #[clap(short, long, default_value_t = String::from("."))] + pub path: String, +} + +#[derive(clap::Args, Clone, Debug)] +pub struct CleanBundleArgs { + #[clap(short, long, default_value_t = String::from("."))] + pub path: String, } diff --git a/src/plugin/handler.rs b/src/cli/action/build.rs similarity index 89% rename from src/plugin/handler.rs rename to src/cli/action/build.rs index b9e45bc..ee7adfb 100644 --- a/src/plugin/handler.rs +++ b/src/cli/action/build.rs @@ -1,25 +1,21 @@ use crate::{ - plugin::{AsyncAction, BuildAction}, + cli::{action::BuildAction, async_action::AsyncAction}, resource::tool::Tool, }; use dip::{ bevy::{ - app::AppExit, - ecs::{ - event::{EventReader, EventWriter}, - schedule::ParallelSystemDescriptorCoercion, - system::Res, - }, + app::{App, AppExit, Plugin}, + ecs::prelude::*, log, }, - prelude::{AsyncActionPool, Plugin}, + core::task::AsyncActionPool, }; use std::{fs, path::PathBuf, process::Command}; -pub struct HandlerPlugin; +pub struct BuildActionPlugin; -impl Plugin for HandlerPlugin { - fn build(&self, app: &mut dip::prelude::App) { +impl Plugin for BuildActionPlugin { + fn build(&self, app: &mut App) { app.add_event::() .add_event::() .add_system(handle_build) @@ -27,7 +23,6 @@ impl Plugin for HandlerPlugin { .add_system(build_app.after(handle_build).after(compile_css)); } } - fn handle_build( mut actions: EventReader, mut build_app: EventWriter, @@ -56,6 +51,38 @@ fn handle_build( } } +fn build_app(mut events: EventReader, mut app_exit: EventWriter) { + for BuildApp { action } in events.iter() { + let mut cmd = Command::new("cargo"); + + cmd.current_dir(fs::canonicalize(&action.path).unwrap()) + .args(["build"]); + + let output = cmd.output().expect("Could not execute cargo build"); + log::trace!("{output:?}"); + + if output.status.success() { + println!("Build finished"); + } else { + println!("Failed to build project"); + println!("{}", String::from_utf8(output.stderr).unwrap()); + } + + app_exit.send(AppExit); + } +} + +#[derive(Clone, Debug)] +struct BuildApp { + pub action: BuildAction, +} + +#[derive(Clone, Debug)] +struct CompileCss { + pub action: BuildAction, + pub tool: Tool, +} + fn compile_css( mut events: EventReader, mut build_app: EventWriter, @@ -97,35 +124,3 @@ fn compile_css( } } } - -fn build_app(mut events: EventReader, mut app_exit: EventWriter) { - for BuildApp { action } in events.iter() { - let mut cmd = Command::new("cargo"); - - cmd.current_dir(fs::canonicalize(&action.path).unwrap()) - .args(["build"]); - - let output = cmd.output().expect("Could not execute cargo build"); - log::trace!("{output:?}"); - - if output.status.success() { - println!("Build finished"); - } else { - println!("Failed to build project"); - println!("{}", String::from_utf8(output.stderr).unwrap()); - } - - app_exit.send(AppExit); - } -} - -#[derive(Clone, Debug)] -struct BuildApp { - pub action: BuildAction, -} - -#[derive(Clone, Debug)] -struct CompileCss { - pub action: BuildAction, - pub tool: Tool, -} diff --git a/src/lib.rs b/src/lib.rs index c37415a..1b6d5b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ pub use dip_cli as cli; #[cfg(feature = "desktop")] pub use dip_desktop as desktop; +pub use dip_bundle as bundle; + pub use bevy; #[cfg(feature = "desktop")] diff --git a/src/main.rs b/src/main.rs index 2be3737..ca12c8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,10 @@ -mod plugin; -mod resource; +mod cli; -use crate::plugin::DipCliPlugin; -use dip::bevy::{ - app::App, - log::{LogPlugin, LogSettings}, -}; +use crate::cli::DipCliPlugin; +use dip::bevy::app::App; fn main() { let mut app = App::new(); - #[cfg(debug_assertions)] - app.insert_resource(LogSettings { - filter: "info,dip=debug".into(), - level: bevy::log::Level::DEBUG, - }); - - #[cfg(not(debug_assertions))] - app.insert_resource(LogSettings { - filter: "warn".into(), - level: bevy::log::Level::WARN, - }); - - app.add_plugin(DipCliPlugin).add_plugin(LogPlugin).run(); + app.add_plugin(DipCliPlugin).run(); } diff --git a/src/plugin.rs b/src/plugin.rs deleted file mode 100644 index f77f849..0000000 --- a/src/plugin.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod async_action; -mod cli; -mod handler; -mod tool; - -use crate::plugin::{async_action::*, cli::*, handler::*, tool::*}; -use dip::bevy::app::{App, Plugin}; - -pub struct DipCliPlugin; - -impl Plugin for DipCliPlugin { - fn build(&self, app: &mut App) { - app.add_plugin(CliPlugin::::application()) - .add_plugin(ActionPlugin) - .add_plugin(AsyncActionPlugin) - .add_plugin(ToolPlugin) - .add_plugin(HandlerPlugin); - } -} diff --git a/src/plugin/async_action.rs b/src/plugin/async_action.rs deleted file mode 100644 index 70ab215..0000000 --- a/src/plugin/async_action.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{ - plugin::BuildAction, - resource::tool::{Tool, ToolResult}, -}; -use dip::{bevy::ecs::event::EventReader, core::task::async_action}; - -#[async_action] -impl AsyncActionCreator { - async fn install(tool: Tool) -> ToolResult { - tool.install().await?; - - Ok(Install) - } - - async fn install_and_build(tool: &Tool, action: BuildAction) -> BuildAction { - tool.install().await.unwrap(); - - action - } -} - -#[derive(Clone, Debug)] -pub struct Install; diff --git a/src/plugin/tool.rs b/src/plugin/tool.rs deleted file mode 100644 index c961806..0000000 --- a/src/plugin/tool.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{ - plugin::{async_action::*, cli::*}, - resource::tool::{Tool, ToolResult}, -}; -use dip::{ - bevy::{ - app::{App, AppExit, Plugin}, - ecs::{ - event::{EventReader, EventWriter}, - system::Res, - }, - }, - core::task::AsyncActionPool, -}; - -pub struct ToolPlugin; - -impl Plugin for ToolPlugin { - fn build(&self, app: &mut App) { - app.add_plugin(ToolActionPlugin) - .add_system(list_tool) - .add_system(add_tool) - .add_system(install_result); - } -} - -fn list_tool(mut events: EventReader, mut app_exit: EventWriter) { - for _ in events.iter() { - for t in Tool::list().iter() { - println!("- {t}"); - } - app_exit.send(AppExit); - } -} - -fn add_tool( - mut events: EventReader, - async_action: Res>, -) { - for e in events.iter() { - let name = e.name.as_str(); - let tool = Tool::from_str(name).expect(&format!("Could not find tool: {name}")); - - match tool { - Tool::Tailwind => async_action.send(AsyncAction::install(tool)), - } - } -} - -fn install_result( - mut events: EventReader>, - mut app_exit: EventWriter, -) { - for e in events.iter() { - if let Err(e) = e { - println!("{:?}", e.error); - } - app_exit.send(AppExit); - } -}