├── .gitignore ├── .rust-toolchain ├── rust-toolchain ├── install.sh ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── tests.yml ├── CHANGES.md ├── default.nix ├── shell.nix ├── src ├── actions │ ├── mod.rs │ ├── prune.rs │ ├── revert.rs │ ├── goal.rs │ ├── sync.rs │ └── add.rs ├── file_actions.rs ├── tests.rs ├── goals.rs ├── packages.rs ├── util.rs ├── config.rs ├── main.rs └── link.rs ├── Cargo.toml ├── flake.nix ├── LICENSE ├── README.md ├── flake.lock └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly 2 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | curl -LsSf "https://github.com/AusCyberman/dotfile-sync/releases/download/v0.3.6/dots" > /tmp/dots 4 | chmod +x /tmp/dots 5 | /tmp/dots $@ sync 6 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/rust:latest 2 | RUN rustup toolchain install nightly 3 | RUN rustup component add clippy --toolchain "nightly" 4 | RUN cargo install cargo-edit cargo-expand -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | # Files stored in repository root 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGES 2 | * Breaking: 3 | - Change .links.toml format 4 | * Non Breaking 5 | - Change License to Anti-Capitalist Software License 6 | - Add goals 7 | - Use new structopt 8 | - Added completion 9 | - Make sync async 10 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | let 3 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; } 7 | ) { 8 | src = ./.; 9 | }).defaultNix 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { 2 | overlays = [ 3 | (import "${fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz"}/overlay.nix") 4 | (self: super: { 5 | clippy-preview = super.fenix.latest.clippy-preview; 6 | rustc = super.fenix.latest.rustc; 7 | cargo = super.fenix.latest.cargo; 8 | rust-src = super.fenix.latest.rust-src; 9 | } 10 | ) 11 | ]; 12 | } }: 13 | with pkgs; 14 | mkShell { 15 | buildInputs = [ 16 | cargo 17 | rustc 18 | gcc 19 | rust-analyzer 20 | rustfmt 21 | lldb 22 | # clippy 23 | cargo-edit 24 | clippy-preview 25 | ]; 26 | 27 | RUST_SRC_PATH = "${pkgs.rust-src}/lib/rustlib/src/rust/library"; 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::SystemConfig; 2 | use anyhow::*; 3 | use log::*; 4 | 5 | mod add; 6 | pub mod goal; 7 | mod prune; 8 | mod revert; 9 | mod sync; 10 | 11 | pub use add::add; 12 | pub use prune::prune; 13 | pub use revert::revert; 14 | pub use sync::sync; 15 | 16 | pub fn manage(ctx: &super::ProjectContext, make_default: bool) -> Result { 17 | let mut sysconfig = ctx.system_config.clone(); 18 | if !ctx.project_config_path.exists() { 19 | bail!("Project path does not exist"); 20 | } 21 | sysconfig.add_project(ctx.project.name.clone(), ctx.project_config_path.clone()); 22 | if make_default { 23 | sysconfig.default = Some(ctx.project_config_path.clone()); 24 | info!("Set as default"); 25 | } 26 | 27 | Ok(sysconfig) 28 | } 29 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "build": { 7 | // Path is relataive to the devcontainer.json file. 8 | "dockerfile": "Dockerfile" 9 | }, 10 | "features": { 11 | "ghcr.io/devcontainers/features/github-cli:1": {}, 12 | "ghcr.io/devcontainers-contrib/features/ccache-asdf:2": {} 13 | } 14 | 15 | // Features to add to the dev container. More info: https://containers.dev/features. 16 | // "features": {}, 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | // "forwardPorts": [], 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | // "postCreateCommand": "rustc --version", 23 | 24 | // Configure tool-specific properties. 25 | // "customizations": {}, 26 | 27 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 28 | // "remoteUser": "root" 29 | } 30 | -------------------------------------------------------------------------------- /src/file_actions.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use futures::future::{BoxFuture, FutureExt}; 5 | use tokio::fs; 6 | 7 | pub fn check_path(path: &Path) -> Result { 8 | if !path.exists() { 9 | anyhow::bail!("File does not exist: {}", path.display()); 10 | } 11 | if path 12 | .metadata() 13 | .map(|x| x.permissions().readonly()) 14 | .unwrap_or(true) 15 | { 16 | anyhow::bail!("Invalid permissions for file: {}", path.display()); 17 | } 18 | 19 | Ok(path.to_path_buf()) 20 | } 21 | pub fn recurse_copy<'a>(src: &'a Path, output_dest: &'a Path) -> BoxFuture<'a, Result<()>> { 22 | async move { 23 | fs::create_dir(&output_dest).await?; 24 | let mut files = fs::read_dir(src).await?; 25 | while let Some(entry) = files.next_entry().await? { 26 | let path = entry.path(); 27 | if path.is_file() { 28 | fs::copy(&path, output_dest.join(&path.file_name().unwrap())).await?; 29 | } else { 30 | recurse_copy(&path, &output_dest.join(&path.file_name().unwrap())).await?; 31 | } 32 | } 33 | Ok(()) 34 | } 35 | .boxed() 36 | } 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dots" 3 | version = "0.3.6" 4 | authors = ["AusCyber "] 5 | repository = "https://github.com/auscyberman/dotfile-sync" 6 | description = "a dotfile syncing software" 7 | exclude = [ 8 | ".github", 9 | "**.nix", 10 | "flake.lock", 11 | ] 12 | readme = "README.md" 13 | edition = "2021" 14 | 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | futures = "0.3.18" 20 | tokio = { version = "1.11.0", features = ["full"] } 21 | # git2 = "0.13" 22 | anyhow = "1.0.52" 23 | toml = "0.5.8" 24 | serde = { version = "1.0", features = ["derive"] } 25 | directories = "4.0" 26 | log = "0.4.14" 27 | env_logger = "0.9.0" 28 | regex = "1.4.5" 29 | lazy_static = "1.4.0" 30 | itertools = "0.10.0" 31 | cascade = "1.0.0" 32 | same-file = "1.0.6" 33 | colored = "2" 34 | derive_more = "0.99.16" 35 | async-trait = "0.1.51" 36 | futures-util = "0.3.18" 37 | clap_generate = { version = "3.0.0-beta.5", git = "https://github.com/clap-rs/clap", rev = "3a697af253b5fdeeda7078cd247555d0ea7e6e37" } 38 | clap = { version = "3.0.0-beta.5", git = "https://github.com/clap-rs/clap", rev = "3a697af253b5fdeeda7078cd247555d0ea7e6e37" } 39 | 40 | 41 | [patch.crates-io] 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: workflow_dispatch 4 | 5 | 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Cache Dependencies 13 | id: cache-deps 14 | uses: actions/cache@v2 15 | with: 16 | path: | 17 | ~/.cargo/bin/ 18 | ~/.cargo/registry/index/ 19 | ~/.cargo/registry/cache 20 | ~/.cargo/git/db 21 | target/ 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 23 | - name: Check release version 24 | id: version-check 25 | uses: thebongy/version-check@v1 26 | with: 27 | file: Cargo.toml 28 | tagFormat: v${version} 29 | 30 | - run: rustup default nightly 31 | - run: cargo build --release 32 | - uses: actions/upload-artifact@v2 33 | with: 34 | name: dots-binary 35 | path: target/release/dots 36 | - name: Draft release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | files: target/release/dots 40 | draft: true 41 | tag_name: ${{ steps.version-check.outputs.releaseVersion }} 42 | body_path: CHANGES.md 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | flake-compat = { 4 | url = "github:edolstra/flake-compat"; 5 | flake = false; 6 | }; 7 | nixpkgs.url = "nixpkgs/nixpkgs-unstable"; 8 | flake-utils.url = "github:numtide/flake-utils"; 9 | fenix = { 10 | url = "github:nix-community/fenix"; 11 | inputs.nixpkgs.follows = "nixpkgs"; 12 | }; 13 | naersk.url = "github:nmattia/naersk"; 14 | }; 15 | outputs = { self, flake-utils, fenix, nixpkgs, naersk, ... }: 16 | flake-utils.lib.eachDefaultSystem (system: 17 | let 18 | # Add rust nightly to pkgs 19 | pkgs = nixpkgs.legacyPackages.${system} // { inherit (fenix.packages.${system}.latest) cargo rustc rust-src clippy-preview; inherit (fenix.packages.${system}) rust-analyzer; }; 20 | 21 | naersk-lib = (naersk.lib."${system}".override { 22 | cargo = pkgs.cargo; 23 | rustc = pkgs.rustc; 24 | }); 25 | 26 | dots = naersk-lib.buildPackage { 27 | pname = "dots"; 28 | doCheck = true; 29 | cargoTestCommands = a: [ 30 | ''USER=test-user cargo $cargo_options test $cargo_test_options'' 31 | ''cargo clippy -- -D warnings'']; 32 | nativeBuildInputs = with pkgs; [ pkg-config clippy-preview rustc]; 33 | root = ./.; 34 | }; 35 | 36 | 37 | in 38 | rec { 39 | packages.dots = dots; 40 | defaultPackage = dots; 41 | 42 | devShell = import ./shell.nix { inherit pkgs; }; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - 'Cargo.toml' 8 | - 'src/**.rs' 9 | - 'Cargo.lock' 10 | pull_request: 11 | branches: [ master ] 12 | paths: 13 | - 'Cargo.toml' 14 | - 'src/**.rs' 15 | - 'Cargo.lock' 16 | 17 | env: 18 | CARGO_TERM_COLOR: always 19 | 20 | jobs: 21 | clippy: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Cache Dependencies 26 | id: cache-deps 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.cargo/bin/ 31 | ~/.cargo/registry/index/ 32 | ~/.cargo/registry/cache/ 33 | ~/.cargo/git/db/ 34 | target/ 35 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 36 | - run: rustup default nightly 37 | - run: rustup component add clippy 38 | - run: cargo clippy -- -D warnings 39 | 40 | 41 | tests: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | - name: Cache Dependencies 47 | id: cache-deps 48 | uses: actions/cache@v2 49 | with: 50 | path: | 51 | ~/.cargo/bin/ 52 | ~/.cargo/registry/index/ 53 | ~/.cargo/registry/cache/ 54 | ~/.cargo/git/db/ 55 | target/ 56 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 57 | - run: rustup default nightly 58 | - run: cargo test --verbose 59 | 60 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | mod parse_vars { 2 | use crate::util::parse_vars; 3 | use std::env; 4 | 5 | #[test] 6 | fn simple_input() { 7 | let input_text = "$PWD/very cool${USER}"; 8 | let expected_text = format!( 9 | "{}/very cool{}", 10 | env::var("PWD").unwrap(), 11 | env::var("USER").unwrap() 12 | ); 13 | assert_eq!(expected_text, parse_vars(true, None, input_text).unwrap()); 14 | } 15 | #[test] 16 | fn no_vars() { 17 | let input_text = "Hi well"; 18 | 19 | assert_eq!( 20 | input_text.to_string(), 21 | parse_vars(true, None, input_text).unwrap() 22 | ); 23 | } 24 | } 25 | 26 | //mod goals { 27 | // use crate::goals::Goal; 28 | // 29 | // #[test] 30 | // fn goal_test() { 31 | // let mut map = std::collections::HashMap::new(); 32 | // macro_rules! goal { 33 | // ($name:expr) => { 34 | // map.insert($name,Goal { 35 | // enabled: true, 36 | // links: Vec::new(), 37 | // required_goals: None, 38 | // }); 39 | // 40 | // }; 41 | // ($name:expr , $($depends:expr),*) => { 42 | // map.insert($name,Goal { 43 | // enabled: true, 44 | // links: Vec::new(), 45 | // required_goals: Some(vec![ $($depends.into(),)*]), 46 | // }); 47 | // }; 48 | // } 49 | // 50 | // goal!("hi", "lol"); 51 | // goal!("lol", "bob", "jim"); 52 | // goal!("jim"); 53 | // goal!("bob"); 54 | // println!() 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) 2 | 3 | Copyright © 2021 Will Pierlot 4 | 5 | This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. 6 | 7 | Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: 8 | 9 | 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. 10 | 11 | 2. The User is one of the following: 12 | a. An individual person, laboring for themselves 13 | b. A non-profit organization 14 | c. An educational institution 15 | d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor 16 | 17 | 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. 18 | 19 | 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | Translations: 24 | -------------------------------------------------------------------------------- /src/actions/prune.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ProjectConfig; 2 | use crate::link::*; 3 | use crate::ProjectContext; 4 | use anyhow::Result; 5 | use log::*; 6 | use std::fs::remove_file; 7 | 8 | pub fn prune(ctx: &ProjectContext) -> Result { 9 | let project = &ctx.project; 10 | let mut new_project = project.clone(); 11 | new_project.links = new_project 12 | .links 13 | .into_iter() 14 | .filter_map(|mut link| { 15 | match convert_iter_to_source(link.src.into_iter().filter_map(|x| { 16 | ctx.project_config_path.join(&x.2).canonicalize().ok()?; 17 | Some(x) 18 | })) { 19 | None => { 20 | info!("removing link {}", &link.name); 21 | match remove_file( 22 | link.destination 23 | .to_path_buf(ctx.project.variables.as_ref()) 24 | .ok()?, 25 | ) { 26 | Ok(_) => debug!("Successfully removed link {}", &link.name), 27 | Err(e) => error!("Failed to remove link {}", e.to_string()), 28 | } 29 | None 30 | } 31 | Some(src) => { 32 | link.src = src; 33 | Some(link) 34 | } 35 | } 36 | }) 37 | .collect(); 38 | let new_links_len = new_project.links.len(); 39 | let old_links_len = project.links.len(); 40 | if new_links_len == old_links_len { 41 | debug!("no links removed"); 42 | } else { 43 | debug!("removed {} links", old_links_len - new_links_len); 44 | } 45 | Ok(new_project) 46 | } 47 | -------------------------------------------------------------------------------- /src/actions/revert.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::*, file_actions::recurse_copy, link::*}; 2 | use log::*; 3 | 4 | use anyhow::*; 5 | use std::path::{Path, PathBuf}; 6 | use tokio::fs; 7 | 8 | pub async fn revert(ctx: &crate::ProjectContext, path: &Path) -> Result { 9 | debug!("path is {}", path.display()); 10 | let ac_path = path.canonicalize().context("could not find file")?; 11 | let mut new_project = ctx.project.clone(); 12 | let mut dest_path: Option = None; 13 | let new_links = new_project 14 | .links 15 | .iter() 16 | .filter_map(|link| { 17 | let mut new_link = link.clone(); 18 | let link_dest = link 19 | .destination 20 | .to_path_buf(new_project.variables.as_ref()) 21 | .ok(); 22 | 23 | debug!("link_dest = {:?} {:?}", link_dest, ac_path); 24 | new_link.src = convert_iter_to_source(link.src.clone().into_iter().filter(|x| { 25 | debug!("src = {}", ctx.project_config_path.join(&x.2).display()); 26 | if same_file::is_same_file(ctx.project_config_path.join(&x.2), &ac_path) 27 | .unwrap_or(false) 28 | { 29 | debug!("found it"); 30 | dest_path = link_dest.clone(); 31 | false 32 | } else { 33 | true 34 | } 35 | }))?; 36 | Some(new_link) 37 | }) 38 | .collect(); 39 | let dest = dest_path.context("could not find path in links")?; 40 | debug!("dest is {}", dest.display()); 41 | 42 | fs::remove_file(&dest).await?; 43 | if ac_path.is_file() { 44 | fs::copy(&ac_path, dest).await?; 45 | fs::remove_file(&ac_path).await?; 46 | } else { 47 | recurse_copy(&ac_path, &dest).await?; 48 | fs::remove_dir_all(&ac_path).await?; 49 | } 50 | new_project.links = new_links; 51 | Ok(new_project) 52 | } 53 | -------------------------------------------------------------------------------- /src/actions/goal.rs: -------------------------------------------------------------------------------- 1 | use crate::goals::Goal; 2 | use crate::ProjectContext; 3 | use anyhow::{Context, Result}; 4 | use clap::Parser; 5 | 6 | use std::collections::HashMap; 7 | use std::path::PathBuf; 8 | 9 | #[derive(Parser, Clone)] 10 | pub enum GoalSubCommand { 11 | List, 12 | Add { name: String, depends: Vec }, 13 | AddFile { goal: String, files: Vec }, 14 | } 15 | 16 | pub async fn goals( 17 | ctx: &ProjectContext, 18 | command: GoalSubCommand, 19 | ) -> Result { 20 | let mut project_config = ctx.project.clone(); 21 | use GoalSubCommand::*; 22 | match command { 23 | List => match ctx.project.goals { 24 | Some(ref goals) => { 25 | println!("Goals: \n"); 26 | for (name, goal) in goals { 27 | print!("Name: {} \n {}", name, goal); 28 | } 29 | } 30 | None => anyhow::bail!("No goals for project"), 31 | }, 32 | AddFile { goal, files } => { 33 | for file in files { 34 | anyhow::ensure!( 35 | ctx.in_project( 36 | &crate::config::ProjectConfig::remove_start( 37 | &ctx.project_config_path, 38 | &file 39 | ) 40 | .context("does not start with config_path")? 41 | )?, 42 | "File not in project" 43 | ); 44 | project_config 45 | .goals 46 | .as_mut() 47 | .context("No Goals for project".to_string())? 48 | .get_mut(&goal) 49 | .context(format!("Could not find goal {}", goal))? 50 | .links 51 | .push(file.to_str().unwrap().to_string()); 52 | } 53 | } 54 | Add { name, depends } => { 55 | let _ = project_config 56 | .goals 57 | .get_or_insert_with(HashMap::new) 58 | .insert(name, Goal::new(depends)); 59 | } 60 | } 61 | Ok(project_config) 62 | } 63 | -------------------------------------------------------------------------------- /src/goals.rs: -------------------------------------------------------------------------------- 1 | use crate::{link::Link, ProjectContext}; 2 | use anyhow::{Context, Result}; 3 | use itertools::Itertools; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use std::collections::HashMap; 7 | 8 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 9 | pub struct Goal { 10 | pub enabled: bool, 11 | pub links: Vec, 12 | pub required_goals: Option>, 13 | } 14 | 15 | impl Goal { 16 | pub fn new(required_goals: Vec) -> Goal { 17 | Goal { 18 | enabled: true, 19 | links: Vec::new(), 20 | required_goals: if required_goals.is_empty() { 21 | None 22 | } else { 23 | Some(required_goals) 24 | }, 25 | } 26 | } 27 | pub fn get_links(&self, ctx: &ProjectContext) -> Result> { 28 | let all_goals = ctx 29 | .project 30 | .goals 31 | .clone() 32 | .context("No goals set for project")?; 33 | let hash_map = ctx 34 | .project 35 | .links 36 | .clone() 37 | .into_iter() 38 | .map(|x| (x.name.clone(), x)) 39 | .collect(); 40 | Ok(self 41 | .to_links(&hash_map, &all_goals)? 42 | .into_iter() 43 | .cloned() 44 | .collect()) 45 | } 46 | pub fn to_links<'a>( 47 | &self, 48 | all_links: &'a HashMap, 49 | all_goals: &'a HashMap, 50 | ) -> Result> { 51 | let mut links = self.links.clone(); 52 | if let Some(ref x) = self.required_goals { 53 | links.extend( 54 | x.iter() 55 | .map(|x| { 56 | Ok(all_goals 57 | .get(x) 58 | .context(format!("Could not find {}", x))? 59 | .links 60 | .clone()) 61 | }) 62 | .try_collect::<_, Vec<_>, anyhow::Error>()? 63 | .concat(), 64 | ); 65 | } 66 | links 67 | .into_iter() 68 | .dedup() 69 | .map(|x| all_links.get(&x).context(format!("Could not find {}", &x))) 70 | .try_collect::<_, Vec<_>, _>() 71 | } 72 | } 73 | 74 | impl std::fmt::Display for Goal { 75 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 76 | writeln!(f, "Enabled: {}", self.enabled)?; 77 | writeln!(f, " Links: ")?; 78 | for link in &self.links { 79 | writeln!(f, " {}", link)?; 80 | } 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/packages.rs: -------------------------------------------------------------------------------- 1 | use crate::{goals::Goal, link::Link}; 2 | use anyhow::{Context, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::env; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Debug)] 8 | #[serde(untagged)] 9 | enum GoalType { 10 | InlineGoal(Goal), 11 | GoalName { goal: String }, 12 | LinkName { link_name: String }, 13 | Link(Link), 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug, Clone)] 17 | pub struct ProgramConfig { 18 | app_name: String, 19 | app_aliases: Option>, 20 | checker_script: Option, 21 | #[serde(flatten)] 22 | goal: GoalType, 23 | } 24 | 25 | impl ProgramConfig { 26 | pub fn get_goal(&self, ctx: &crate::ProjectContext) -> Result> { 27 | match &self.goal { 28 | GoalType::InlineGoal(goal) => Ok(goal.get_links(ctx)?), 29 | GoalType::GoalName { goal: goal_name } => ctx 30 | .project 31 | .goals 32 | .as_ref() 33 | .context("no goals from project".to_string())? 34 | .get(goal_name) 35 | .context(format!("Goal could not be found {}", &goal_name))? 36 | .clone() 37 | .get_links(ctx), 38 | GoalType::LinkName { link_name } => Ok(ctx 39 | .project 40 | .links 41 | .iter() 42 | .find(|x| &x.name == link_name) 43 | .cloned() 44 | .into_iter() 45 | .collect()), 46 | GoalType::Link(link) => Ok(vec![link.clone()]), 47 | } 48 | } 49 | pub async fn package_installed(&self) -> Result { 50 | log::debug!("Checking {}", self.app_name); 51 | Ok(if let Some(ref script) = self.checker_script { 52 | let result = tokio::process::Command::new("sh") 53 | .args(["-c", script]) 54 | .spawn()? 55 | .wait() 56 | .await?; 57 | log::debug!("Pacman result: {:?}", result); 58 | result.success() 59 | } else { 60 | false 61 | } || env::var("PATH") 62 | .context("Could not get PATH variable")? 63 | .split(';') 64 | .any(|x| { 65 | let path = PathBuf::from(x); 66 | match path.read_dir() { 67 | Ok(mut dir) => dir 68 | .find_map(|x| { 69 | let file_name = x.ok()?.file_name(); 70 | let file_name = file_name.to_str()?; 71 | Some( 72 | file_name == self.app_name 73 | || self 74 | .app_aliases 75 | .as_ref() 76 | .and_then(|aliases| { 77 | aliases.iter().find(|y| file_name == *y) 78 | }) 79 | .is_some(), 80 | ) 81 | }) 82 | .is_some(), 83 | _ => false, 84 | } 85 | })) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::{collections::hash_map, env}; 3 | 4 | #[derive(Debug)] 5 | pub enum ParsingVarError { 6 | VarNotFound(std::env::VarError), 7 | Other(anyhow::Error), 8 | } 9 | 10 | impl From for ParsingVarError { 11 | fn from(error: std::env::VarError) -> Self { 12 | ParsingVarError::VarNotFound(error) 13 | } 14 | } 15 | impl From for ParsingVarError { 16 | fn from(e: anyhow::Error) -> Self { 17 | ParsingVarError::Other(e) 18 | } 19 | } 20 | 21 | impl From for anyhow::Error { 22 | fn from(e: ParsingVarError) -> anyhow::Error { 23 | match e { 24 | ParsingVarError::VarNotFound(e) => anyhow::Error::new(e), 25 | ParsingVarError::Other(e) => e, 26 | } 27 | } 28 | } 29 | 30 | pub fn parse_vars( 31 | use_env: bool, 32 | extra_map: Option<&hash_map::HashMap>, 33 | text: &str, 34 | ) -> Result { 35 | lazy_static::lazy_static! { 36 | static ref GET_VARIABLES: regex::Regex = regex::Regex::new(r"(?:\$(\w+))|(?:\$\{(\w+?)\})").unwrap(); 37 | } 38 | let mut extra_offset: i32 = 0; 39 | let mut output = text.to_string(); 40 | for captures in GET_VARIABLES.captures_iter(text) { 41 | let matches = captures.get(1).or_else(|| captures.get(2)).unwrap(); 42 | let offsets = captures.get(0).or_else(|| captures.get(1)).unwrap(); 43 | let text = matches.as_str(); 44 | let variable_value = extra_map 45 | .as_ref() 46 | .and_then(|x| x.get(text).cloned()) 47 | .context("Could not get extra variables from map") 48 | .or_else(|_| { 49 | if use_env { 50 | Ok(env::var(text)?) 51 | } else { 52 | Err(ParsingVarError::Other(anyhow::Error::msg(format!( 53 | "Could not find extra variable {}", 54 | text 55 | )))) 56 | } 57 | })?; 58 | output.replace_range( 59 | ((offsets.start() as i32 + extra_offset) as usize) 60 | ..((offsets.end() as i32 + extra_offset) as usize), 61 | &variable_value, 62 | ); 63 | extra_offset += variable_value.as_str().len() as i32 - offsets.as_str().len() as i32; 64 | } 65 | Ok(output) 66 | } 67 | 68 | use serde::{de::DeserializeOwned, Serialize}; 69 | use std::fs; 70 | use std::path::Path; 71 | #[async_trait::async_trait] 72 | pub trait WritableConfig: Sized { 73 | fn write_to_file(&self, file_name: &Path) -> Result<()>; 74 | fn read_from_file(file_name: &Path) -> Result; 75 | } 76 | 77 | impl WritableConfig for T { 78 | fn write_to_file(&self, path: &Path) -> Result<()> { 79 | let data = toml::to_vec(self)?; 80 | fs::write(path, &data)?; 81 | Ok(()) 82 | } 83 | fn read_from_file(file_name: &Path) -> Result { 84 | let data = fs::read(file_name)?; 85 | let val = toml::from_slice(&data)?; 86 | Ok(val) 87 | } 88 | } 89 | 90 | use std::ffi::OsStr; 91 | use std::process::Stdio; 92 | use tokio::process::Command; 93 | pub fn run_command(program: &str, args: I) -> Command 94 | where 95 | I: IntoIterator, 96 | S: AsRef, 97 | { 98 | let mut command = Command::new(program); 99 | command.args(args).stdin(Stdio::inherit()); 100 | command 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Dotfile Sync 3 | ###### For UNIX-based systems 4 | 5 | ![Build](https://img.shields.io/github/workflow/status/auscyberman/dotfile-sync/tests) 6 | ![Issues](https://img.shields.io/github/issues/auscyberman/dotfile-sync?color=pink) 7 | 8 | 9 | **Syncing dotfiles or other folders with symlinks can be a bit annoying to manage. Especially when you have multiple systems to setup.** 10 | 11 | You can create system dependent links to alleviate all your stress 12 | 13 | ## Table of Contents 14 | 15 | 1. [Features](#features) 16 | 2. [Installation](#installation) 17 | 3. [Configuration](#configuration) 18 | 4. [Usage](#usage) 19 | 20 | 21 | ## Features 22 | 23 | * Easy initalisation of configurations 24 | `dots init` 25 | * Addition of links 26 | `dots add file1 file2` 27 | `dots add file1 --destination files/file2linked` 28 | `dots add file1 file2 --destination files` 29 | * Manage globally 30 | `dots manage` 31 | `dots manage --default` 32 | * Only link on specific system 33 | `dots --system laptop add file1laptop` 34 | `dots --system desktop add file1desktop file2desktop --destination files` 35 | * Read from environment variables 36 | `dots add $HOME/file1` 37 | * Project-wide variables 38 | ```toml 39 | [variables] 40 | user = "auscyber" 41 | password = "1234" 42 | ``` 43 | ![List example](https://i.imgur.com/EMem4sN.png) 44 | * Revert link 45 | `dots revert file1` 46 | 47 | 48 | ## Installation 49 | 50 | 1. Install rust-toolchain with if you don't already have it installed 51 | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 52 | 3. Clone the repository into a folder of your choice 53 | `git clone https://github.com/auscyberman/dotfile-sync` 54 | 4. Run `cargo build --release` and add `target/build/release/dots` to your `PATH` 55 | 56 | **Now It's installed. Time to configure it** 57 | 58 | ## Configuration 59 | 60 | ### Create base `.links.toml` file 61 | 1. Move to folder where you want to initalise project 62 | 2. Initalise Project `dots init` 63 | 3. `dots manage --default` *Note: `default` flag not required, just simplifies later tasks* 64 | 65 | **Now you are all setup, just run `dots add file1 file2 ... ` and `dots sync` to sync everything on another computer** 66 | *Note: adding files requires `--project` or `--project-path` flag to command if `--default` wasn't added to `manage`* 67 | 68 | **Dotfile Sync uses [TOML](https://github.com/toml-lang/toml) for configuration** 69 | 70 | * To add a link to the project, add a new section 71 | ```toml 72 | [[links]] 73 | ``` 74 | with these attributes 75 | **These first three are required** 76 | 77 | * `name`: The user side name for the link 78 | * `src`: The relative location of the actual file in the project 79 | * `destination`: The location for the `src` to be linked to 80 | 81 | **These are *not* required** 82 | * `system`: System to link on *can be null* 83 | * `default_path`: When `source_map` exists, the default path to link if the current `system` cannot be found in `source_map` 84 | * `default_system`: The same as above, however, the default system to search for in `source_map` 85 | * `source_map`: Map of systems to relative locations 86 | 87 | ## Usage 88 | #### Adding multiple files 89 | To add the files `file1` `file2` 90 | 91 | `dots add file1 file2` 92 | 93 | #### Set destination of files 94 | To put the file `file1` in project/`files/file2` 95 | 96 | `dots add file1 -d files/file2` 97 | 98 | #### Add file dependent on system 99 | To only link `file` when the system is `desktop` 100 | `dots add file1 --system "desktop"` 101 | 102 | #### On another computer 103 | To sync with no extra paremeters 104 | `curl -Ls https://git.io/JBB45 | sh -s ` 105 | To add extra paremeters `--system "desktop"` 106 | `curl -Ls https://git.io/JBB45 | sh -s -- --system "desktop"` 107 | 108 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1639808710, 12 | "narHash": "sha256-OKDHt4D14puuqfVHptQ6EvjIR9RaHXyPzMh8Rjo8vzA=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "9b391fc1831ece6c245a4eafe7b52f5c806df28c", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-compat": { 25 | "flake": false, 26 | "locked": { 27 | "lastModified": 1627913399, 28 | "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=", 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "edolstra", 36 | "repo": "flake-compat", 37 | "type": "github" 38 | } 39 | }, 40 | "flake-utils": { 41 | "locked": { 42 | "lastModified": 1638122382, 43 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "naersk": { 56 | "inputs": { 57 | "nixpkgs": "nixpkgs" 58 | }, 59 | "locked": { 60 | "lastModified": 1639731602, 61 | "narHash": "sha256-5u/J/7KrY/fYL/OeBLxP4E9NdNdQrriHpOyPBleKp5I=", 62 | "owner": "nmattia", 63 | "repo": "naersk", 64 | "rev": "5415c7045366bb53db1a33cf7d975942b5553a28", 65 | "type": "github" 66 | }, 67 | "original": { 68 | "owner": "nmattia", 69 | "repo": "naersk", 70 | "type": "github" 71 | } 72 | }, 73 | "nixpkgs": { 74 | "locked": { 75 | "lastModified": 1639837534, 76 | "narHash": "sha256-zAZoVtvVfrs41e+kEEumyptQ4DOkcXQIYgxmaJ51+hs=", 77 | "owner": "NixOS", 78 | "repo": "nixpkgs", 79 | "rev": "e1f9e754a40b645a39f6592d45df943cb5f59dcf", 80 | "type": "github" 81 | }, 82 | "original": { 83 | "id": "nixpkgs", 84 | "type": "indirect" 85 | } 86 | }, 87 | "nixpkgs_2": { 88 | "locked": { 89 | "lastModified": 1639837534, 90 | "narHash": "sha256-zAZoVtvVfrs41e+kEEumyptQ4DOkcXQIYgxmaJ51+hs=", 91 | "owner": "NixOS", 92 | "repo": "nixpkgs", 93 | "rev": "e1f9e754a40b645a39f6592d45df943cb5f59dcf", 94 | "type": "github" 95 | }, 96 | "original": { 97 | "id": "nixpkgs", 98 | "ref": "nixpkgs-unstable", 99 | "type": "indirect" 100 | } 101 | }, 102 | "root": { 103 | "inputs": { 104 | "fenix": "fenix", 105 | "flake-compat": "flake-compat", 106 | "flake-utils": "flake-utils", 107 | "naersk": "naersk", 108 | "nixpkgs": "nixpkgs_2" 109 | } 110 | }, 111 | "rust-analyzer-src": { 112 | "flake": false, 113 | "locked": { 114 | "lastModified": 1639175515, 115 | "narHash": "sha256-Yj38u9BpKfyGrcSEaoSEnOns885xn/Ask6lR5rsxS8k=", 116 | "owner": "rust-analyzer", 117 | "repo": "rust-analyzer", 118 | "rev": "d03397fe1173eaeb2e04c9e55ac223289e7e08ee", 119 | "type": "github" 120 | }, 121 | "original": { 122 | "owner": "rust-analyzer", 123 | "ref": "nightly", 124 | "repo": "rust-analyzer", 125 | "type": "github" 126 | } 127 | } 128 | }, 129 | "root": "root", 130 | "version": 7 131 | } 132 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::goals::Goal; 2 | use crate::link::{Link, System}; 3 | use crate::packages::ProgramConfig; 4 | use crate::util::WritableConfig; 5 | use anyhow::{bail, Context, Result}; 6 | use directories::ProjectDirs; 7 | use serde::{Deserialize, Serialize}; 8 | use std::{ 9 | collections::{hash_map::DefaultHasher, HashMap}, 10 | env, fs, 11 | hash::{Hash, Hasher}, 12 | path::{Path, PathBuf}, 13 | }; 14 | 15 | #[derive(Deserialize, Serialize, Debug, Clone)] 16 | pub struct ProjectConfig { 17 | pub name: String, 18 | pub id: String, 19 | pub default: Option, 20 | pub systems: Vec, 21 | pub variables: Option>, 22 | pub goals: Option>, 23 | pub programs: Option>, 24 | pub links: Vec, 25 | } 26 | 27 | impl ProjectConfig { 28 | pub fn remove_start(proj_path: &Path, path: &Path) -> Option { 29 | Some(path.strip_prefix(proj_path).ok()?.to_str()?.to_string()) 30 | } 31 | 32 | pub fn new(name: String, path: &Path) -> ProjectConfig { 33 | let mut hasher = DefaultHasher::new(); 34 | name.hash(&mut hasher); 35 | path.hash(&mut hasher); 36 | std::time::Instant::now().hash(&mut hasher); 37 | ProjectConfig { 38 | default: None, 39 | name, 40 | id: format!("{}", hasher.finish()), 41 | systems: Vec::new(), 42 | links: Vec::new(), 43 | variables: None, 44 | goals: None, 45 | programs: None, 46 | } 47 | } 48 | pub fn save(&self, ctx: &crate::ProjectContext) -> Result<()> { 49 | self.write_to_file(&ctx.project_config_path.join(".links.toml")) 50 | } 51 | } 52 | 53 | pub fn get_config_loc() -> Option { 54 | ProjectDirs::from("com", "AusCyber", "dotfile-sync").map(|x| x.config_dir().to_path_buf()) 55 | } 56 | 57 | pub fn get_sys_config(config_path: Option>) -> Result<(PathBuf, SystemConfig)> { 58 | match config_path { 59 | Some(x) => Ok(( 60 | x.as_ref().to_path_buf(), 61 | SystemConfig::read_from_file(x.as_ref())?, 62 | )), 63 | None => match get_config_loc() 64 | .context("Failed to get config location") 65 | .and_then(|x| Ok(x.join("config.toml").canonicalize()?)) 66 | { 67 | Ok(x) => Ok((x.clone(), SystemConfig::read_from_file(x.as_ref())?)), 68 | _ => { 69 | let par_dir = get_config_loc().context("Failed to get config location")?; 70 | let loc = par_dir.join("config.toml"); 71 | fs::create_dir_all(par_dir)?; 72 | Ok((loc, SystemConfig::new())) 73 | } 74 | }, 75 | } 76 | } 77 | 78 | pub fn get_project_config(config_path: Option<&PathBuf>) -> Result<(PathBuf, ProjectConfig)> { 79 | match config_path { 80 | Some(x) => { 81 | if !x.is_file() { 82 | Ok(( 83 | x.clone(), 84 | ProjectConfig::read_from_file(&x.join(".links.toml"))?, 85 | )) 86 | } else { 87 | Ok(( 88 | x.parent() 89 | .context("Could not get parent folder of config file") 90 | .map(Path::to_path_buf)?, 91 | ProjectConfig::read_from_file(x)?, 92 | )) 93 | } 94 | } 95 | None => { 96 | let proj_path = env::current_dir()?; 97 | let file_path = proj_path.join(".links.toml"); 98 | if !file_path.exists() { 99 | bail!("No config file in current directory") 100 | } 101 | Ok((proj_path, ProjectConfig::read_from_file(&file_path)?)) 102 | } 103 | } 104 | } 105 | 106 | #[derive(Deserialize, Serialize, Debug, Clone)] 107 | pub struct ProjectOutput { 108 | pub system: Option, 109 | pub path: PathBuf, 110 | } 111 | 112 | #[derive(Deserialize, Serialize, Debug, Clone)] 113 | pub struct SystemConfig { 114 | pub default: Option, 115 | pub projects: HashMap, 116 | pub sudo_program: Option, 117 | } 118 | 119 | impl Default for SystemConfig { 120 | fn default() -> Self { 121 | Self::new() 122 | } 123 | } 124 | impl SystemConfig { 125 | pub fn new() -> SystemConfig { 126 | SystemConfig { 127 | default: None, 128 | projects: HashMap::new(), 129 | sudo_program: None, 130 | } 131 | } 132 | 133 | pub fn get_project(&self, name: &str) -> Option<&ProjectOutput> { 134 | self.projects.get(name) 135 | } 136 | 137 | pub fn add_project(&mut self, name: String, path: PathBuf) { 138 | self.projects 139 | .insert(name, ProjectOutput { system: None, path }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(result_flattening)] 2 | #![feature(in_band_lifetimes)] 3 | use anyhow::{Context, Result}; 4 | use clap_generate::{generate, Shell}; 5 | use log::*; 6 | use std::{ 7 | env, 8 | path::{Path, PathBuf}, 9 | }; 10 | //use structopt::StructOpt; 11 | 12 | use clap::{IntoApp, Parser}; 13 | use colored::*; 14 | use std::convert::TryInto; 15 | 16 | mod actions; 17 | mod config; 18 | mod file_actions; 19 | mod goals; 20 | mod link; 21 | mod packages; 22 | #[cfg(test)] 23 | mod tests; 24 | mod util; 25 | 26 | use config::*; 27 | use link::{Link, System}; 28 | use util::WritableConfig; 29 | 30 | #[derive(Parser, Clone)] 31 | #[clap(about = "Manage dotfiles")] 32 | pub struct Args { 33 | #[clap(short, long, about = "Location of system config file", global = true)] 34 | config_file: Option, 35 | #[clap(long, global = true, about = "Location of project config file")] 36 | project_path: Option, 37 | #[clap( 38 | long, 39 | short, 40 | about = "Locate project from system projects", 41 | global = true 42 | )] 43 | project: Option, 44 | #[clap(long, short, global = true)] 45 | system: Option, 46 | #[clap(subcommand)] 47 | command: Command, 48 | } 49 | 50 | pub struct ProjectContext { 51 | pub args: Args, 52 | pub project: ProjectConfig, 53 | pub project_config_path: PathBuf, 54 | pub system_config: SystemConfig, 55 | pub system_config_path: PathBuf, 56 | pub system: Option, 57 | } 58 | 59 | impl ProjectContext { 60 | pub fn get_link_for_file<'a>(&'a mut self, file: &Path) -> Option<&'a mut Link> { 61 | let stripped_path = file.to_str()?; 62 | self.project 63 | .links 64 | .iter_mut() 65 | .find(|x| x.src.contains_path(stripped_path)) 66 | } 67 | 68 | pub fn in_project(&self, path: &str) -> Result { 69 | Ok(self 70 | .project_config_path 71 | .join(path) 72 | .canonicalize() 73 | .map(|x| x.exists()) 74 | .unwrap_or(false) 75 | || self.project.links.iter().any(|x| x.src.contains_path(path))) 76 | } 77 | } 78 | 79 | impl TryInto for Args { 80 | type Error = anyhow::Error; 81 | fn try_into(self) -> Result { 82 | let (system_config_file, system_config) = get_sys_config(self.config_file.as_ref())?; 83 | let current = std::env::current_dir()?; 84 | let (path, proj_config) = get_project_config( 85 | self.project_path 86 | .as_ref() 87 | .or_else(|| { 88 | if current.join(".links.toml").exists() { 89 | Some(¤t) 90 | } else { 91 | None 92 | } 93 | }) 94 | .or_else(|| { 95 | self.project 96 | .clone() 97 | .and_then(|y| system_config.projects.get(&y)) 98 | .map(|x| &x.path) 99 | }) 100 | .or(system_config.default.as_ref()), 101 | )?; 102 | 103 | let system = self 104 | .system 105 | .as_ref() 106 | .or_else(|| { 107 | system_config 108 | .get_project(&proj_config.name)? 109 | .system 110 | .as_ref() 111 | }) 112 | .or(proj_config.default.as_ref()) 113 | .cloned(); 114 | Ok(ProjectContext { 115 | // command: self.command.clone(), 116 | args: self, 117 | project: proj_config, 118 | project_config_path: path, 119 | system_config, 120 | system_config_path: system_config_file, 121 | system, 122 | }) 123 | } 124 | } 125 | 126 | impl Args { 127 | fn try_to_context(self) -> Result { 128 | self.try_into() 129 | } 130 | } 131 | 132 | #[derive(Parser, Clone)] 133 | enum Command { 134 | #[clap(about = "Link all files in project")] 135 | Sync { 136 | #[clap(long = "installed-programs")] 137 | installed_programs: bool, 138 | #[clap(short = 'g')] 139 | goal: Option, 140 | }, 141 | #[clap(about = "Move and link project")] 142 | Add { 143 | src: Vec, 144 | #[clap(short, long)] 145 | destination: Option, 146 | #[clap(short, long)] 147 | name: Option, 148 | }, 149 | #[clap(about = "Initalise project")] 150 | Init { name: Option }, 151 | #[clap(about = "Revert path")] 152 | Revert { file: PathBuf }, 153 | #[clap(about = "Add project to system configuration")] 154 | Manage { 155 | #[clap(short, long)] 156 | default: bool, 157 | }, 158 | #[clap(about = "Prune all removed files in the project")] 159 | Prune, 160 | #[clap(about = "Work with Goals", subcommand)] 161 | Goals(actions::goal::GoalSubCommand), 162 | Completion { 163 | #[clap(long, value_name = "SHELL", arg_enum)] 164 | shell: Shell, 165 | }, 166 | #[clap(about = "List all links in the project")] 167 | List, 168 | } 169 | 170 | #[tokio::main] 171 | pub async fn main() -> Result<()> { 172 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 173 | let args = Args::parse(); 174 | match args.command.clone() { 175 | Command::Completion { shell } => { 176 | generate(shell, &mut Args::into_app(), "dots", &mut std::io::stdout()) 177 | } 178 | Command::Sync { 179 | goal, 180 | installed_programs, 181 | } => { 182 | actions::sync(args.try_into()?, goal, installed_programs).await?; 183 | } 184 | Command::Manage { default } => { 185 | let ctx = args.try_to_context()?; 186 | let config = actions::manage(&ctx, default).context(format!( 187 | "Failure managing {}", 188 | ctx.project_config_path.display() 189 | ))?; 190 | config.write_to_file(&ctx.system_config_path)?; 191 | info!("Managed {}", ctx.project.name); 192 | } 193 | Command::Add { 194 | src, 195 | destination, 196 | name, 197 | } => { 198 | let ctx = args.try_to_context()?; 199 | let config = actions::add(&ctx, src, destination, name) 200 | .await 201 | .context("Failure adding link")?; 202 | config.save(&ctx)?; 203 | } 204 | Command::Init { name } => { 205 | let dir = env::current_dir()?; 206 | let project = ProjectConfig::new( 207 | name.unwrap_or( 208 | dir.file_name() 209 | .and_then(|x| x.to_str()) 210 | .map(|x| x.into()) 211 | .context("Invalid name")?, 212 | ), 213 | &dir, 214 | ); 215 | project.write_to_file(&dir.join(".links.toml"))?; 216 | } 217 | Command::List => { 218 | let ctx = args.try_to_context()?; 219 | println!("{} {}", "Links for".bold(), ctx.project.name.bold()); 220 | for link in ctx.project.links { 221 | print!("{}", link); 222 | } 223 | } 224 | Command::Revert { file } => { 225 | let ctx = args.try_to_context()?; 226 | let config = actions::revert(&ctx, &file).await?; 227 | config.save(&ctx)?; 228 | } 229 | Command::Prune => { 230 | let ctx = args.try_to_context()?; 231 | actions::prune(&ctx)?.save(&ctx)?; 232 | } 233 | Command::Goals(command) => { 234 | let ctx = args.try_to_context()?; 235 | let config = actions::goal::goals(&ctx, command).await?; 236 | config.save(&ctx)?; 237 | } 238 | }; 239 | Ok(()) 240 | } 241 | -------------------------------------------------------------------------------- /src/actions/sync.rs: -------------------------------------------------------------------------------- 1 | use crate::{link::Link, ProjectContext}; 2 | use anyhow::{Context, Result}; 3 | use futures::TryStreamExt; 4 | use log::*; 5 | use std::sync::Arc; 6 | use tokio::fs; 7 | 8 | pub async fn sync( 9 | ctx: ProjectContext, 10 | goal: Option, 11 | installed_programs: bool, 12 | ) -> Result<()> { 13 | let links = match goal { 14 | Some(goal) => { 15 | let all_goals = ctx 16 | .project 17 | .goals 18 | .clone() 19 | .context("No goals set for project")?; 20 | let hash_map = ctx 21 | .project 22 | .links 23 | .clone() 24 | .into_iter() 25 | .map(|x| (x.name.clone(), x)) 26 | .collect(); 27 | all_goals 28 | .get(&goal) 29 | .context("Could not find goal")? 30 | .to_links(&hash_map, &all_goals)? 31 | .into_iter() 32 | .cloned() 33 | .collect() 34 | } 35 | None => { 36 | if installed_programs { 37 | let packages = ctx 38 | .project 39 | .programs 40 | .as_ref() 41 | .context("Could not find any packages")?; 42 | let ctx = Arc::new(&ctx); 43 | packages 44 | .iter() 45 | .map(|x| async { 46 | x.package_installed().await.and_then(|y| { 47 | if y { 48 | x.get_goal(&ctx) 49 | } else { 50 | Ok::, anyhow::Error>(vec![]) 51 | } 52 | }) 53 | }) 54 | .collect::>() 55 | .try_collect::>() 56 | .await?; 57 | todo!(); 58 | } else { 59 | ctx.project.links.clone() 60 | } 61 | } 62 | }; 63 | 64 | link_links(ctx, links).await 65 | } 66 | 67 | pub async fn link_links(ctx: ProjectContext, links: Vec) -> Result<()> { 68 | let ctx = Arc::new(ctx); 69 | let threads = links.into_iter().map( 70 | |link| -> tokio::task::JoinHandle> { 71 | let ctx = ctx.clone(); 72 | //Create async threads to link 73 | tokio::spawn(async move { 74 | async { 75 | let project_path = &ctx.project_config_path; 76 | let source = match link.src.resolve(&ctx.system) { 77 | Some(d) => project_path.join(d), 78 | None => return Ok(()), 79 | } 80 | .canonicalize()?; 81 | 82 | //Normalise destination 83 | let destination = 84 | { 85 | //Parse in environment variables 86 | let mut temp_dest = link 87 | .destination 88 | .to_path_buf(ctx.project.variables.as_ref())?; 89 | if temp_dest.is_dir() 90 | && temp_dest.exists() 91 | && !same_file::is_same_file(&temp_dest, &source)? 92 | { 93 | temp_dest.push(source.file_name().context(format!( 94 | "Could not get file name for {}", 95 | link.name 96 | ))?); 97 | } 98 | //If the destination exists, and links back to the original location, then already 99 | //linked 100 | if temp_dest.exists() && same_file::is_same_file(&temp_dest, &source)? { 101 | info!(r#""{}" already linked"#, source.display()); 102 | return Ok(()); 103 | } else if temp_dest.exists() { 104 | error!("{} file already exists", temp_dest.display()); 105 | return Ok(()); 106 | } 107 | temp_dest 108 | }; 109 | // If sudo is required to pass then set perms 110 | if link.sudo_required.unwrap_or(false) { 111 | let sudo_program = 112 | ctx.system_config.sudo_program.as_deref().unwrap_or("sudo"); 113 | com_run( 114 | sudo_program, 115 | &[ 116 | "mkdir", 117 | "-p", 118 | destination 119 | .parent() 120 | .and_then(|x| x.to_str()) 121 | .context("Could not get parent folder")?, 122 | ], 123 | ) 124 | .await?; 125 | let dest_str = destination 126 | .to_str() 127 | .context("Could not convert destination to string")?; 128 | com_run( 129 | sudo_program, 130 | &[ 131 | "ln", 132 | "-s", 133 | source 134 | .to_str() 135 | .context("Could not convert source to string")?, 136 | dest_str, 137 | ], 138 | ) 139 | .await?; 140 | if let Some(perms) = link.perms { 141 | if perms.user_owner.is_some() || perms.group_owner.is_some() { 142 | let owner_loc = format!( 143 | "{}:{}", 144 | perms.user_owner.unwrap_or_else(|| "".to_string()), 145 | perms.group_owner.unwrap_or_else(|| "".to_string()) 146 | ); 147 | com_run(sudo_program, &["chown", "-h", "-R", &owner_loc, dest_str]) 148 | .await?; 149 | } 150 | if let Some(user_code) = perms.user_code { 151 | com_run(sudo_program, &["chmod", "-R", &user_code, dest_str]) 152 | .await?; 153 | } 154 | let source_parent = source 155 | .parent() 156 | .and_then(|x| x.to_str()) 157 | .context("Could not get destination parent")?; 158 | log::debug!("dest_parent: {}", source_parent); 159 | com_run(sudo_program, &["chmod", "o+rx", source_parent]).await?; 160 | } 161 | } else { 162 | fs::create_dir_all( 163 | &destination 164 | .parent() 165 | .context("Could not get parent folder")?, 166 | ) 167 | .await 168 | .context(format!( 169 | "Failed creating folder hierchy for {}", 170 | &destination.display() 171 | ))?; 172 | 173 | fs::symlink(source, &destination).await?; 174 | if let Some(perms) = link.perms { 175 | let dest_str = destination 176 | .to_str() 177 | .context("Could not convert destination to string")?; 178 | if let Some(user_code) = perms.user_code { 179 | com_run("chmod", &["-R", &user_code, dest_str]).await?; 180 | } 181 | if perms.user_owner.is_some() || perms.group_owner.is_some() { 182 | let owner_str = format!( 183 | "{}:{}", 184 | perms.user_owner.unwrap_or_default(), 185 | perms.group_owner.unwrap_or_default() 186 | ); 187 | com_run("chown", &["-R", &owner_str, dest_str]).await?; 188 | } 189 | } 190 | } 191 | Ok::<_, anyhow::Error>(()) 192 | } 193 | .await 194 | .context(format!("Failed linking {}", &link.name)) 195 | }) 196 | }, 197 | ); 198 | 199 | for res in threads { 200 | if let Err(e) = res.await.map_err(Into::into).flatten() { 201 | log::error!("Error syncing : {}", e) 202 | } 203 | } 204 | Ok(()) 205 | } 206 | 207 | async fn com_run(com: &str, args: I) -> Result<()> 208 | where 209 | I: IntoIterator, 210 | S: AsRef, 211 | { 212 | crate::util::run_command(com, args).spawn()?.wait().await?; 213 | Ok(()) 214 | } 215 | -------------------------------------------------------------------------------- /src/actions/add.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::ProjectConfig, file_actions::recurse_copy, link::*, ProjectContext}; 2 | use anyhow::{bail, Context, Result}; 3 | use cascade::cascade; 4 | use itertools::Itertools; 5 | use log::*; 6 | use std::path::{Path, PathBuf}; 7 | use tokio::fs; 8 | 9 | pub async fn add( 10 | ctx: &ProjectContext, 11 | //File to copy 12 | original_locations: Vec, 13 | //Location of where to place it in the project 14 | destination: Option, 15 | name: Option, 16 | ) -> Result { 17 | if original_locations.is_empty() { 18 | bail!("No files defined to link"); 19 | } 20 | if original_locations.len() == 1 { 21 | add_individual_link( 22 | ctx, 23 | original_locations.first().unwrap().clone(), 24 | destination, 25 | name, 26 | ) 27 | .await 28 | } else { 29 | manage_list(ctx, original_locations, destination).await 30 | } 31 | } 32 | 33 | async fn add_individual_link( 34 | ctx: &ProjectContext, 35 | mut original_location: String, 36 | destination: Option, 37 | name: Option, 38 | ) -> Result { 39 | //Append current directory if it is a generic location 40 | let original_location = { 41 | let p = PathBuf::from(&original_location); 42 | if p.exists() && !p.has_root() { 43 | original_location = format!( 44 | "{}/{}", 45 | std::env::current_dir()?.display(), 46 | &original_location 47 | ) 48 | }; 49 | VariablePath::from(original_location) 50 | }; 51 | 52 | //clean and realise path 53 | let original_location_cleaned = original_location 54 | .to_path_buf(ctx.project.variables.as_ref())? 55 | .canonicalize() 56 | .context(format!( 57 | r#"file "{}" could not be found"#, 58 | &original_location 59 | ))?; 60 | 61 | let output_dest = match destination.map(PathBuf::from) { 62 | Some(destination) => { 63 | let mut output = match destination.strip_prefix(&ctx.project_config_path) { 64 | Ok(x) => x.to_path_buf(), 65 | _ => destination, 66 | }; 67 | if ctx.project_config_path.join(&output).is_dir() { 68 | output = output.join(original_location_cleaned.file_name().context(format!( 69 | "Could not get filename for {}", 70 | original_location_cleaned.to_str().unwrap() 71 | ))?) 72 | } 73 | output.to_string_lossy().to_string() 74 | } 75 | 76 | None => original_location_cleaned 77 | .file_name() 78 | .map(|x| x.to_string_lossy().into()) 79 | .context("Could not get file name")?, 80 | }; 81 | 82 | anyhow::ensure!( 83 | !(ctx 84 | .project 85 | .links 86 | .iter() 87 | .filter_map(|x| same_file::is_same_file( 88 | &original_location_cleaned, 89 | &x.destination 90 | .to_path_buf(ctx.project.variables.as_ref()) 91 | .ok()? 92 | ) 93 | .ok()) 94 | .any(|x| x) 95 | || ctx 96 | .project_config_path 97 | .join(&output_dest) 98 | .canonicalize() 99 | .map(|x| { 100 | println!("{}", x.display()); 101 | x.exists() 102 | }) 103 | .unwrap_or(false) 104 | || ctx 105 | .project 106 | .links 107 | .iter() 108 | .any(|x| x.src.contains_path(&output_dest))), 109 | "Destination {} already exists", 110 | original_location 111 | ); 112 | 113 | let name = name.unwrap_or( 114 | original_location_cleaned 115 | .file_name() 116 | .map(|x| x.to_string_lossy().into()) 117 | .context("Could not get file name")?, 118 | ); 119 | anyhow::ensure!( 120 | !ctx.project.links.iter().any(|x| x.name == name), 121 | "links already contain link of that name" 122 | ); 123 | 124 | let get_system = || ctx.args.system.to_owned().context("could not get system"); 125 | let mut found = false; 126 | let mut completed_links = ctx 127 | .project 128 | .links 129 | .iter() 130 | .map(|link| { 131 | if link 132 | .destination 133 | .to_path_buf(ctx.project.variables.as_ref()) 134 | .and_then(|x| Ok(x.canonicalize()? != original_location_cleaned)) 135 | .unwrap_or(true) 136 | { 137 | return Ok(link.clone()); 138 | } 139 | found = true; 140 | 141 | let mut link = link.clone(); 142 | let sys = get_system()?; 143 | link.src = link.src.insert_link(&sys, &output_dest)?; 144 | Ok(link) 145 | }) 146 | .collect::, anyhow::Error>>()?; 147 | 148 | if !found { 149 | let source = SourceFile::Source { 150 | system: ctx.args.system.clone(), 151 | src: output_dest.clone(), 152 | }; 153 | debug!("name is orig: {}, source: {}", original_location, source); 154 | completed_links.push(Link::new(name.clone(), original_location, source)); 155 | }; 156 | 157 | let output_dest = ctx.project_config_path.join(output_dest); 158 | let final_project_config = cascade! { 159 | ctx.project.clone(); 160 | ..links = completed_links; 161 | }; 162 | fs::create_dir_all( 163 | PathBuf::from(&output_dest) 164 | .parent() 165 | .context("Could not get parent folder")?, 166 | ) 167 | .await?; 168 | move_link(&original_location_cleaned, &output_dest).await?; 169 | info!("Added {}", name); 170 | Ok(final_project_config) 171 | } 172 | 173 | async fn manage_list( 174 | ctx: &ProjectContext, 175 | locations: Vec, 176 | destination: Option, 177 | ) -> Result { 178 | let dest = destination.unwrap_or_else(|| String::from(".")); 179 | fs::create_dir_all(ctx.project_config_path.join(&dest)).await?; 180 | let mut triples: Vec<_> = locations 181 | .into_iter() 182 | .dedup() 183 | .map(|mut path| { 184 | let p = PathBuf::from(&path); 185 | if p.exists() && !p.has_root() { 186 | path = format!("{}/{}", std::env::current_dir()?.display(), path) 187 | }; 188 | let variable_path: VariablePath = path.clone().into(); 189 | let cleaned = variable_path 190 | .to_path_buf(ctx.project.variables.as_ref())? 191 | .canonicalize() 192 | .context(format!(r#"file "{}" could not be found"#, &path))?; 193 | 194 | let file_name: String = cleaned 195 | .file_name() 196 | .map(|x| x.to_string_lossy()) 197 | .context("Could not get file name")? 198 | .into(); 199 | let dest_file = format!("{}/{}", dest, file_name); 200 | anyhow::ensure!( 201 | ctx.project_config_path.join(&dest_file).exists(), 202 | "file {} already exists", 203 | dest_file 204 | ); 205 | 206 | Ok((false, cleaned, dest_file, variable_path, file_name)) 207 | }) 208 | .try_collect()?; 209 | 210 | let get_system = || ctx.args.system.to_owned().context("could not get system"); 211 | 212 | let mut new_links: Vec<_> = ctx 213 | .project 214 | .links 215 | .iter() 216 | .cloned() 217 | .map(|mut link| { 218 | let (_, _, dest_file, _, _) = match triples 219 | .iter_mut() 220 | .find(|(f, _, _, p, _)| p == &link.destination && !f) 221 | { 222 | None => return Ok(link), 223 | Some(x) => { 224 | x.0 = true; 225 | x 226 | } 227 | }; 228 | let sys = get_system()?; 229 | link.src = link.src.insert_link(&sys, dest_file)?; 230 | 231 | Ok::<_, anyhow::Error>(link) 232 | }) 233 | .try_collect()?; 234 | 235 | for (_, cleaned, dest_file, _, name) in &triples { 236 | info!("Linked {}", name); 237 | debug!( 238 | "cleaned = {}, dest_file = {}", 239 | cleaned.display(), 240 | ctx.project_config_path.join(dest_file).display() 241 | ); 242 | move_link(cleaned, &ctx.project_config_path.join(dest_file)).await?; 243 | } 244 | 245 | for (_, _, dest_file, p, name) in triples.into_iter().filter(|x| x.0) { 246 | let source = SourceFile::Source { 247 | system: ctx.args.system.clone(), 248 | src: dest_file, 249 | }; 250 | 251 | new_links.push(Link::new(name.to_string(), p, source)); 252 | } 253 | 254 | let new_project = cascade! { 255 | ctx.project.clone(); 256 | ..links = new_links; 257 | }; 258 | Ok(new_project) 259 | } 260 | 261 | async fn move_link(original_locaction_cleaned: &Path, output_dest: &Path) -> Result<()> { 262 | if original_locaction_cleaned.is_dir() { 263 | recurse_copy(original_locaction_cleaned, output_dest).await?; 264 | } else { 265 | fs::copy(original_locaction_cleaned, output_dest).await?; 266 | } 267 | if fs::metadata(original_locaction_cleaned).await?.is_dir() { 268 | fs::remove_dir_all(original_locaction_cleaned).await?; 269 | } else { 270 | fs::remove_file(original_locaction_cleaned).await?; 271 | } 272 | debug!( 273 | "loc = {} \n dest = {}", 274 | original_locaction_cleaned.display(), 275 | output_dest.display() 276 | ); 277 | 278 | fs::symlink(output_dest, original_locaction_cleaned).await?; 279 | Ok(()) 280 | } 281 | -------------------------------------------------------------------------------- /src/link.rs: -------------------------------------------------------------------------------- 1 | use crate::file_actions::check_path; 2 | use anyhow::{bail, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | collections::HashMap, 6 | fmt, 7 | path::{Path, PathBuf}, 8 | string::ParseError, 9 | }; 10 | 11 | use cascade::cascade; 12 | use colored::*; 13 | use derive_more::Display; 14 | use itertools::Itertools; 15 | 16 | #[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, Display)] 17 | #[serde(transparent)] 18 | pub struct System(String); 19 | 20 | impl std::str::FromStr for System { 21 | type Err = ParseError; 22 | 23 | fn from_str(s: &str) -> Result { 24 | Ok(System(s.into())) 25 | } 26 | } 27 | 28 | #[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone, Display)] 29 | #[serde(transparent)] 30 | pub struct VariablePath(String); 31 | 32 | impl> From for VariablePath { 33 | fn from(a: T) -> Self { 34 | VariablePath(a.as_ref().to_owned()) 35 | } 36 | } 37 | 38 | impl VariablePath { 39 | pub fn from_path(a: impl AsRef) -> Result { 40 | Ok(VariablePath( 41 | a.as_ref().canonicalize()?.to_string_lossy().into(), 42 | )) 43 | } 44 | 45 | pub fn to_path_buf( 46 | &self, 47 | extra_variables: Option<&HashMap>, 48 | ) -> Result { 49 | Ok(PathBuf::from(crate::util::parse_vars( 50 | true, 51 | extra_variables, 52 | self.0.as_str(), 53 | )?)) 54 | } 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 58 | pub struct Perms { 59 | pub user_owner: Option, 60 | pub group_owner: Option, 61 | pub user_code: Option, 62 | } 63 | 64 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] 65 | pub struct Link { 66 | pub name: String, 67 | pub destination: VariablePath, 68 | #[serde(flatten)] 69 | pub src: SourceFile, 70 | pub sudo_required: Option, 71 | #[serde(flatten)] 72 | pub perms: Option, 73 | } 74 | 75 | impl Link { 76 | pub fn new(name: String, src: VariablePath, destination: SourceFile) -> Link { 77 | Link { 78 | name, 79 | destination: src, 80 | src: destination, 81 | sudo_required: None, 82 | perms: None, 83 | } 84 | } 85 | } 86 | impl std::fmt::Display for Link { 87 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 88 | write!( 89 | f, 90 | "Name: {} Destination: {} Link Sources: \n {}", 91 | self.name.yellow(), 92 | format!("\"{}\"", self.destination).green(), 93 | format!("{}", self.src).red() 94 | )?; 95 | Ok(()) 96 | } 97 | } 98 | 99 | #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)] 100 | #[serde(transparent)] 101 | pub struct SourceFileUrl(String); 102 | 103 | #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)] 104 | #[serde(untagged)] 105 | pub enum SourceFile { 106 | Source { 107 | system: Option, 108 | src: String, 109 | }, 110 | DynamicSource { 111 | default_path: Option, 112 | default_system: Option, 113 | source_map: HashMap, 114 | }, 115 | } 116 | 117 | //impl<'de> Deserialize<'de> for SourceFile { 118 | // fn deserialize(deserializer: D) -> Result 119 | // where 120 | // D: serde::Deserializer<'de>, 121 | // { 122 | // struct 123 | // } 124 | //} 125 | //impl<'se> Serialize<'se> for SourceFile {} 126 | 127 | impl IntoIterator for SourceFile { 128 | type Item = (bool, Option, String); 129 | type IntoIter = Box>; 130 | fn into_iter(self) -> Self::IntoIter { 131 | use SourceFile::*; 132 | match self { 133 | Source { src: path, system } => Box::new(Some((false, system, path)).into_iter()), 134 | DynamicSource { 135 | default_path, 136 | source_map: map, 137 | default_system, 138 | } => Box::new(default_path.map(|s| (false, None, s)).into_iter().chain( 139 | map.into_iter().map(move |(sys, path)| { 140 | ( 141 | default_system 142 | .as_ref() 143 | .map(|system| system == &sys) 144 | .unwrap_or(false), 145 | Some(sys), 146 | path, 147 | ) 148 | }), 149 | )), 150 | } 151 | } 152 | } 153 | impl fmt::Display for SourceFile { 154 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 155 | match self { 156 | SourceFile::Source { system, src: path } => { 157 | write!(f, "\t")?; 158 | system 159 | .as_ref() 160 | .map_or(Ok(()), |sys| write!(f, "System: {} ", sys))?; 161 | writeln!(f, "Path: {}", path) 162 | } 163 | SourceFile::DynamicSource { 164 | default_system, 165 | default_path, 166 | source_map: map, 167 | } => { 168 | write!(f, "\t")?; 169 | default_path 170 | .as_ref() 171 | .map_or(Ok(()), |p| write!(f, "Default Path: {} ", p))?; 172 | default_system 173 | .as_ref() 174 | .map_or(Ok(()), |s| write!(f, "Default System: {}", s))?; 175 | if default_path.is_some() || default_system.is_some() { 176 | writeln!(f)?; 177 | } 178 | map.iter().try_for_each(|(system, path)| { 179 | writeln!(f, "\tSystem: {}, Path: {}", system, path) 180 | }) 181 | } 182 | } 183 | } 184 | } 185 | 186 | pub fn convert_iter_to_source, String)>>( 187 | iter: T, 188 | ) -> Option { 189 | let mut total = 0; 190 | let vec: Vec<_> = iter 191 | .map(|(b, sys, path)| { 192 | total += 1; 193 | (b, sys, path) 194 | }) 195 | .collect(); 196 | match (total, vec) { 197 | (0, _) => None, 198 | (1, mut vec) => { 199 | let (_, system, path) = vec.pop()?; 200 | Some(SourceFile::Source { system, src: path }) 201 | } 202 | (_, vec) => Some(SourceFile::DynamicSource { 203 | default_path: vec.iter().find_map(|x| { 204 | if x.1.is_none() { 205 | Some(x.2.clone()) 206 | } else { 207 | None 208 | } 209 | }), 210 | default_system: vec.iter().find_map(|x| { 211 | if x.0 { 212 | Some(x.1.as_ref()?.clone()) 213 | } else { 214 | None 215 | } 216 | }), 217 | source_map: vec 218 | .iter() 219 | .filter_map(|x| { 220 | if !x.0 { 221 | Some((x.1.as_ref()?.clone(), x.2.clone())) 222 | } else { 223 | None 224 | } 225 | }) 226 | .collect(), 227 | }), 228 | } 229 | } 230 | 231 | impl SourceFile { 232 | pub fn insert_link(self, sys: &System, dest_string: &str) -> Result { 233 | let dest_string = dest_string.to_owned(); 234 | let sys = sys.clone(); 235 | Ok(match self { 236 | SourceFile::Source { system, src: path } => { 237 | if !system.as_ref().map_or(false, |x| x == &sys) { 238 | let map = cascade! { 239 | HashMap::new(); 240 | ..insert(sys, dest_string); 241 | }; 242 | SourceFile::DynamicSource { 243 | default_system: system, 244 | default_path: Some(path), 245 | source_map: map, 246 | } 247 | } else { 248 | bail!( 249 | r#"System "{:?}" already defined for output file "{}" "#, 250 | sys, 251 | dest_string 252 | ); 253 | } 254 | } 255 | SourceFile::DynamicSource { 256 | default_path, 257 | default_system, 258 | source_map: mut map, 259 | } => { 260 | if map.contains_key(&sys) || default_system.as_ref().map_or(false, |x| x == &sys) { 261 | bail!( 262 | r#"System "{}" already defined for output file "{}" "#, 263 | sys, 264 | dest_string 265 | ); 266 | } 267 | map.insert(sys, dest_string); 268 | SourceFile::DynamicSource { 269 | default_path, 270 | default_system, 271 | source_map: map, 272 | } 273 | } 274 | }) 275 | } 276 | pub fn contains_path(&self, path: &str) -> bool { 277 | self.clone().into_iter().any(|(_, _, x)| x == path) 278 | } 279 | 280 | pub fn resolve(&self, system: &Option) -> Option { 281 | let system = match system { 282 | None => match self { 283 | SourceFile::Source { src: path, .. } => return Some(path.clone()), 284 | SourceFile::DynamicSource { 285 | default_path: Some(default_path), 286 | .. 287 | } => return Some(default_path.clone()), 288 | _ => return None, 289 | }, 290 | Some(x) => x, 291 | }; 292 | match self { 293 | SourceFile::Source { 294 | src: path, 295 | system: sys, 296 | } => { 297 | if sys.as_ref().map_or(true, |x| x == system) { 298 | Some(path.clone()) 299 | } else { 300 | None 301 | } 302 | } 303 | SourceFile::DynamicSource { 304 | default_path, 305 | default_system, 306 | source_map: map, 307 | } => map 308 | .get(system) 309 | .or_else(|| map.get(default_system.as_ref()?)) 310 | .or(default_path.as_ref()) 311 | .cloned(), 312 | } 313 | } 314 | 315 | pub fn remove_link(self, search_path: &str) -> Option { 316 | match self { 317 | SourceFile::Source { src: path, system } => { 318 | if search_path != path { 319 | Some(SourceFile::Source { src: path, system }) 320 | } else { 321 | None 322 | } 323 | } 324 | SourceFile::DynamicSource { 325 | default_path, 326 | source_map: map, 327 | default_system, 328 | } => { 329 | if default_path.as_ref().map_or(false, |x| x == search_path) { 330 | return Some(SourceFile::DynamicSource { 331 | default_path: None, 332 | source_map: map, 333 | default_system, 334 | }); 335 | } 336 | let map: HashMap = map 337 | .iter() 338 | .filter_map(|(a, x)| { 339 | if search_path != x { 340 | Some((a.clone(), x.clone())) 341 | } else { 342 | None 343 | } 344 | }) 345 | .collect(); 346 | if map.is_empty() { 347 | None 348 | } else { 349 | Some(SourceFile::DynamicSource { 350 | default_path, 351 | default_system, 352 | source_map: map, 353 | }) 354 | } 355 | } 356 | } 357 | } 358 | 359 | pub fn with_default( 360 | base_url: &Path, 361 | default: String, 362 | system_map: HashMap, 363 | ) -> Result { 364 | let new_map: HashMap = system_map 365 | .into_iter() 366 | .map(move |(key, elem)| { 367 | check_path(&base_url.join(&elem))?; 368 | Ok((key, elem)) 369 | }) 370 | .try_collect::<_, _, anyhow::Error>()?; 371 | 372 | Ok(SourceFile::DynamicSource { 373 | default_path: None, 374 | default_system: Some(System(default)), 375 | source_map: new_map, 376 | }) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.52" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.52" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "atty" 33 | version = "0.2.14" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 36 | dependencies = [ 37 | "hermit-abi", 38 | "libc", 39 | "winapi", 40 | ] 41 | 42 | [[package]] 43 | name = "autocfg" 44 | version = "1.0.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 47 | 48 | [[package]] 49 | name = "bitflags" 50 | version = "1.3.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 53 | 54 | [[package]] 55 | name = "bytes" 56 | version = "1.1.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 59 | 60 | [[package]] 61 | name = "cascade" 62 | version = "1.0.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "f18c6a921baae2d947e4cf96f6ef1b5774b3056ae8edbdf5c5cfce4f33260921" 65 | 66 | [[package]] 67 | name = "cfg-if" 68 | version = "1.0.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 71 | 72 | [[package]] 73 | name = "clap" 74 | version = "3.0.0-beta.5" 75 | source = "git+https://github.com/clap-rs/clap?rev=3a697af253b5fdeeda7078cd247555d0ea7e6e37#3a697af253b5fdeeda7078cd247555d0ea7e6e37" 76 | dependencies = [ 77 | "atty", 78 | "bitflags", 79 | "clap_derive", 80 | "indexmap", 81 | "lazy_static", 82 | "os_str_bytes", 83 | "strsim", 84 | "termcolor", 85 | "textwrap", 86 | "unicase", 87 | ] 88 | 89 | [[package]] 90 | name = "clap_derive" 91 | version = "3.0.0-beta.5" 92 | source = "git+https://github.com/clap-rs/clap?rev=3a697af253b5fdeeda7078cd247555d0ea7e6e37#3a697af253b5fdeeda7078cd247555d0ea7e6e37" 93 | dependencies = [ 94 | "heck", 95 | "proc-macro-error", 96 | "proc-macro2", 97 | "quote", 98 | "syn", 99 | ] 100 | 101 | [[package]] 102 | name = "clap_generate" 103 | version = "3.0.0-beta.5" 104 | source = "git+https://github.com/clap-rs/clap?rev=3a697af253b5fdeeda7078cd247555d0ea7e6e37#3a697af253b5fdeeda7078cd247555d0ea7e6e37" 105 | dependencies = [ 106 | "clap", 107 | ] 108 | 109 | [[package]] 110 | name = "colored" 111 | version = "2.0.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 114 | dependencies = [ 115 | "atty", 116 | "lazy_static", 117 | "winapi", 118 | ] 119 | 120 | [[package]] 121 | name = "convert_case" 122 | version = "0.4.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 125 | 126 | [[package]] 127 | name = "derive_more" 128 | version = "0.99.17" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 131 | dependencies = [ 132 | "convert_case", 133 | "proc-macro2", 134 | "quote", 135 | "rustc_version", 136 | "syn", 137 | ] 138 | 139 | [[package]] 140 | name = "directories" 141 | version = "4.0.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" 144 | dependencies = [ 145 | "dirs-sys", 146 | ] 147 | 148 | [[package]] 149 | name = "dirs-sys" 150 | version = "0.3.6" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" 153 | dependencies = [ 154 | "libc", 155 | "redox_users", 156 | "winapi", 157 | ] 158 | 159 | [[package]] 160 | name = "dots" 161 | version = "0.3.6" 162 | dependencies = [ 163 | "anyhow", 164 | "async-trait", 165 | "cascade", 166 | "clap", 167 | "clap_generate", 168 | "colored", 169 | "derive_more", 170 | "directories", 171 | "env_logger", 172 | "futures", 173 | "futures-util", 174 | "itertools", 175 | "lazy_static", 176 | "log", 177 | "regex", 178 | "same-file", 179 | "serde", 180 | "tokio", 181 | "toml", 182 | ] 183 | 184 | [[package]] 185 | name = "either" 186 | version = "1.6.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 189 | 190 | [[package]] 191 | name = "env_logger" 192 | version = "0.9.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 195 | dependencies = [ 196 | "atty", 197 | "humantime", 198 | "log", 199 | "regex", 200 | "termcolor", 201 | ] 202 | 203 | [[package]] 204 | name = "futures" 205 | version = "0.3.19" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" 208 | dependencies = [ 209 | "futures-channel", 210 | "futures-core", 211 | "futures-executor", 212 | "futures-io", 213 | "futures-sink", 214 | "futures-task", 215 | "futures-util", 216 | ] 217 | 218 | [[package]] 219 | name = "futures-channel" 220 | version = "0.3.19" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" 223 | dependencies = [ 224 | "futures-core", 225 | "futures-sink", 226 | ] 227 | 228 | [[package]] 229 | name = "futures-core" 230 | version = "0.3.19" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" 233 | 234 | [[package]] 235 | name = "futures-executor" 236 | version = "0.3.19" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" 239 | dependencies = [ 240 | "futures-core", 241 | "futures-task", 242 | "futures-util", 243 | ] 244 | 245 | [[package]] 246 | name = "futures-io" 247 | version = "0.3.19" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" 250 | 251 | [[package]] 252 | name = "futures-macro" 253 | version = "0.3.19" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" 256 | dependencies = [ 257 | "proc-macro2", 258 | "quote", 259 | "syn", 260 | ] 261 | 262 | [[package]] 263 | name = "futures-sink" 264 | version = "0.3.19" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" 267 | 268 | [[package]] 269 | name = "futures-task" 270 | version = "0.3.19" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" 273 | 274 | [[package]] 275 | name = "futures-util" 276 | version = "0.3.19" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" 279 | dependencies = [ 280 | "futures-channel", 281 | "futures-core", 282 | "futures-io", 283 | "futures-macro", 284 | "futures-sink", 285 | "futures-task", 286 | "memchr", 287 | "pin-project-lite", 288 | "pin-utils", 289 | "slab", 290 | ] 291 | 292 | [[package]] 293 | name = "getrandom" 294 | version = "0.2.3" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 297 | dependencies = [ 298 | "cfg-if", 299 | "libc", 300 | "wasi", 301 | ] 302 | 303 | [[package]] 304 | name = "hashbrown" 305 | version = "0.11.2" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 308 | 309 | [[package]] 310 | name = "heck" 311 | version = "0.3.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 314 | dependencies = [ 315 | "unicode-segmentation", 316 | ] 317 | 318 | [[package]] 319 | name = "hermit-abi" 320 | version = "0.1.19" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 323 | dependencies = [ 324 | "libc", 325 | ] 326 | 327 | [[package]] 328 | name = "humantime" 329 | version = "2.1.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 332 | 333 | [[package]] 334 | name = "indexmap" 335 | version = "1.7.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 338 | dependencies = [ 339 | "autocfg", 340 | "hashbrown", 341 | ] 342 | 343 | [[package]] 344 | name = "instant" 345 | version = "0.1.12" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 348 | dependencies = [ 349 | "cfg-if", 350 | ] 351 | 352 | [[package]] 353 | name = "itertools" 354 | version = "0.10.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 357 | dependencies = [ 358 | "either", 359 | ] 360 | 361 | [[package]] 362 | name = "lazy_static" 363 | version = "1.4.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 366 | 367 | [[package]] 368 | name = "libc" 369 | version = "0.2.112" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 372 | 373 | [[package]] 374 | name = "lock_api" 375 | version = "0.4.5" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" 378 | dependencies = [ 379 | "scopeguard", 380 | ] 381 | 382 | [[package]] 383 | name = "log" 384 | version = "0.4.14" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 387 | dependencies = [ 388 | "cfg-if", 389 | ] 390 | 391 | [[package]] 392 | name = "memchr" 393 | version = "2.4.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 396 | 397 | [[package]] 398 | name = "mio" 399 | version = "0.7.14" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" 402 | dependencies = [ 403 | "libc", 404 | "log", 405 | "miow", 406 | "ntapi", 407 | "winapi", 408 | ] 409 | 410 | [[package]] 411 | name = "miow" 412 | version = "0.3.7" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 415 | dependencies = [ 416 | "winapi", 417 | ] 418 | 419 | [[package]] 420 | name = "ntapi" 421 | version = "0.3.6" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 424 | dependencies = [ 425 | "winapi", 426 | ] 427 | 428 | [[package]] 429 | name = "num_cpus" 430 | version = "1.13.0" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 433 | dependencies = [ 434 | "hermit-abi", 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "once_cell" 440 | version = "1.9.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" 443 | 444 | [[package]] 445 | name = "os_str_bytes" 446 | version = "4.2.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" 449 | dependencies = [ 450 | "memchr", 451 | ] 452 | 453 | [[package]] 454 | name = "parking_lot" 455 | version = "0.11.2" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 458 | dependencies = [ 459 | "instant", 460 | "lock_api", 461 | "parking_lot_core", 462 | ] 463 | 464 | [[package]] 465 | name = "parking_lot_core" 466 | version = "0.8.5" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 469 | dependencies = [ 470 | "cfg-if", 471 | "instant", 472 | "libc", 473 | "redox_syscall", 474 | "smallvec", 475 | "winapi", 476 | ] 477 | 478 | [[package]] 479 | name = "pin-project-lite" 480 | version = "0.2.7" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" 483 | 484 | [[package]] 485 | name = "pin-utils" 486 | version = "0.1.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 489 | 490 | [[package]] 491 | name = "proc-macro-error" 492 | version = "1.0.4" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 495 | dependencies = [ 496 | "proc-macro-error-attr", 497 | "proc-macro2", 498 | "quote", 499 | "syn", 500 | "version_check", 501 | ] 502 | 503 | [[package]] 504 | name = "proc-macro-error-attr" 505 | version = "1.0.4" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 508 | dependencies = [ 509 | "proc-macro2", 510 | "quote", 511 | "version_check", 512 | ] 513 | 514 | [[package]] 515 | name = "proc-macro2" 516 | version = "1.0.34" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" 519 | dependencies = [ 520 | "unicode-xid", 521 | ] 522 | 523 | [[package]] 524 | name = "quote" 525 | version = "1.0.10" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 528 | dependencies = [ 529 | "proc-macro2", 530 | ] 531 | 532 | [[package]] 533 | name = "redox_syscall" 534 | version = "0.2.10" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 537 | dependencies = [ 538 | "bitflags", 539 | ] 540 | 541 | [[package]] 542 | name = "redox_users" 543 | version = "0.4.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 546 | dependencies = [ 547 | "getrandom", 548 | "redox_syscall", 549 | ] 550 | 551 | [[package]] 552 | name = "regex" 553 | version = "1.5.4" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 556 | dependencies = [ 557 | "aho-corasick", 558 | "memchr", 559 | "regex-syntax", 560 | ] 561 | 562 | [[package]] 563 | name = "regex-syntax" 564 | version = "0.6.25" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 567 | 568 | [[package]] 569 | name = "rustc_version" 570 | version = "0.4.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 573 | dependencies = [ 574 | "semver", 575 | ] 576 | 577 | [[package]] 578 | name = "same-file" 579 | version = "1.0.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 582 | dependencies = [ 583 | "winapi-util", 584 | ] 585 | 586 | [[package]] 587 | name = "scopeguard" 588 | version = "1.1.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 591 | 592 | [[package]] 593 | name = "semver" 594 | version = "1.0.4" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" 597 | 598 | [[package]] 599 | name = "serde" 600 | version = "1.0.133" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" 603 | dependencies = [ 604 | "serde_derive", 605 | ] 606 | 607 | [[package]] 608 | name = "serde_derive" 609 | version = "1.0.133" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" 612 | dependencies = [ 613 | "proc-macro2", 614 | "quote", 615 | "syn", 616 | ] 617 | 618 | [[package]] 619 | name = "signal-hook-registry" 620 | version = "1.4.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 623 | dependencies = [ 624 | "libc", 625 | ] 626 | 627 | [[package]] 628 | name = "slab" 629 | version = "0.4.5" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" 632 | 633 | [[package]] 634 | name = "smallvec" 635 | version = "1.7.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" 638 | 639 | [[package]] 640 | name = "strsim" 641 | version = "0.10.0" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 644 | 645 | [[package]] 646 | name = "syn" 647 | version = "1.0.82" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" 650 | dependencies = [ 651 | "proc-macro2", 652 | "quote", 653 | "unicode-xid", 654 | ] 655 | 656 | [[package]] 657 | name = "termcolor" 658 | version = "1.1.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 661 | dependencies = [ 662 | "winapi-util", 663 | ] 664 | 665 | [[package]] 666 | name = "textwrap" 667 | version = "0.14.2" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" 670 | dependencies = [ 671 | "unicode-width", 672 | ] 673 | 674 | [[package]] 675 | name = "tokio" 676 | version = "1.15.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" 679 | dependencies = [ 680 | "bytes", 681 | "libc", 682 | "memchr", 683 | "mio", 684 | "num_cpus", 685 | "once_cell", 686 | "parking_lot", 687 | "pin-project-lite", 688 | "signal-hook-registry", 689 | "tokio-macros", 690 | "winapi", 691 | ] 692 | 693 | [[package]] 694 | name = "tokio-macros" 695 | version = "1.7.0" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" 698 | dependencies = [ 699 | "proc-macro2", 700 | "quote", 701 | "syn", 702 | ] 703 | 704 | [[package]] 705 | name = "toml" 706 | version = "0.5.8" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 709 | dependencies = [ 710 | "serde", 711 | ] 712 | 713 | [[package]] 714 | name = "unicase" 715 | version = "2.6.0" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 718 | dependencies = [ 719 | "version_check", 720 | ] 721 | 722 | [[package]] 723 | name = "unicode-segmentation" 724 | version = "1.8.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 727 | 728 | [[package]] 729 | name = "unicode-width" 730 | version = "0.1.9" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 733 | 734 | [[package]] 735 | name = "unicode-xid" 736 | version = "0.2.2" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 739 | 740 | [[package]] 741 | name = "version_check" 742 | version = "0.9.3" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 745 | 746 | [[package]] 747 | name = "wasi" 748 | version = "0.10.2+wasi-snapshot-preview1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 751 | 752 | [[package]] 753 | name = "winapi" 754 | version = "0.3.9" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 757 | dependencies = [ 758 | "winapi-i686-pc-windows-gnu", 759 | "winapi-x86_64-pc-windows-gnu", 760 | ] 761 | 762 | [[package]] 763 | name = "winapi-i686-pc-windows-gnu" 764 | version = "0.4.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 767 | 768 | [[package]] 769 | name = "winapi-util" 770 | version = "0.1.5" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 773 | dependencies = [ 774 | "winapi", 775 | ] 776 | 777 | [[package]] 778 | name = "winapi-x86_64-pc-windows-gnu" 779 | version = "0.4.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 782 | --------------------------------------------------------------------------------