coliru

A minimal, flexible, dotfile installer
git clone https://git.ashermorgan.net/coliru/
Log | Files | Refs | README

commit 0058b790ce465c8e3c3be7a5b7b03317db310024
parent f49508720c82a8504f9405947e933311a086db0c
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Thu, 20 Jun 2024 16:05:00 -0700

Implement link command

Diffstat:
Mexamples/2.yml | 4++--
Msrc/core.rs | 23++++++++++++++++++++---
Msrc/manifest.rs | 24++++++++++++++++--------
Msrc/utils.rs | 41++++++++++++++++++++++++++++++++++++-----
4 files changed, 74 insertions(+), 18 deletions(-)

diff --git a/examples/2.yml b/examples/2.yml @@ -1,7 +1,7 @@ # Valid manifest steps: - - copy: + - link: - src: foo dst: ~/foo - src: bar @@ -11,4 +11,4 @@ steps: - copy: - src: baz dst: /baz - tags: [ b, c ] + # tags default to [] diff --git a/src/core.rs b/src/core.rs @@ -1,8 +1,8 @@ use std::env::set_current_dir; use std::path::Path; -use super::manifest::{CopyOptions, Manifest, parse_manifest_file}; +use super::manifest::{CopyLinkOptions, Manifest, parse_manifest_file}; use super::tags::tags_match; -use super::utils::copy_file; +use super::utils::{copy_file, link_file}; /// Execute the steps in a coliru manifest file according to a set of tag rules pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, dry_run: bool) @@ -29,11 +29,12 @@ fn execute_manifest(manifest: Manifest, tag_rules: Vec<String>, dry_run: bool) { println!(""); execute_copies(&step.copy, dry_run); + execute_links(&step.link, dry_run); } } /// Execute the copy commands specified in a coliru manifest step -fn execute_copies(copies: &[CopyOptions], dry_run: bool) { +fn execute_copies(copies: &[CopyLinkOptions], dry_run: bool) { for copy in copies { print!(" Copy {} to {}", copy.src, copy.dst); if dry_run { @@ -47,3 +48,19 @@ fn execute_copies(copies: &[CopyOptions], dry_run: bool) { } } } + +/// Execute the link commands specified in a coliru manifest step +fn execute_links(copies: &[CopyLinkOptions], dry_run: bool) { + for copy in copies { + print!(" Link {} to {}", copy.src, copy.dst); + if dry_run { + println!(" (skipped due to --dry-run)"); + return; + } + println!(""); + + if let Err(why) = link_file(&copy.src, &copy.dst) { + eprintln!(" Error: {}", why); + } + } +} diff --git a/src/manifest.rs b/src/manifest.rs @@ -4,14 +4,20 @@ use std::fs::read_to_string; use std::path::{Path, PathBuf}; #[derive(Debug, PartialEq, Deserialize)] -pub struct CopyOptions { +pub struct CopyLinkOptions { pub src: String, pub dst: String, } #[derive(Debug, PartialEq, Deserialize)] pub struct Step { - pub copy: Vec<CopyOptions>, + #[serde(default)] + pub copy: Vec<CopyLinkOptions>, + + #[serde(default)] + pub link: Vec<CopyLinkOptions>, + + #[serde(default)] pub tags: Vec<String>, } @@ -53,7 +59,7 @@ mod tests { #[test] fn parse_manifest_file_invalid() { - let expected = "steps[0]: missing field `copy` at line 4 column 5"; + let expected = "steps[1].copy[0]: missing field `src` at line 12 column 7"; let actual = parse_manifest_file(Path::new("examples/1.yml")); assert_eq!(actual, Err(String::from(expected))); } @@ -63,12 +69,13 @@ mod tests { let expected = Manifest { steps: vec![ Step { - copy: vec![ - CopyOptions{ + copy: vec![], + link: vec![ + CopyLinkOptions{ src: String::from("foo"), dst: String::from("~/foo"), }, - CopyOptions{ + CopyLinkOptions{ src: String::from("bar"), dst: String::from("~/test/bar"), }, @@ -77,12 +84,13 @@ mod tests { }, Step { copy: vec![ - CopyOptions{ + CopyLinkOptions{ src: String::from("baz"), dst: String::from("/baz"), }, ], - tags: vec![String::from("b"), String::from("c")], + link: vec![], + tags: vec![], } ], base_dir: PathBuf::from("examples"), diff --git a/src/utils.rs b/src/utils.rs @@ -2,17 +2,48 @@ extern crate expanduser; use expanduser::expanduser; use std::io::Result; -use std::fs::{copy, create_dir_all}; +use std::fs; +#[cfg(target_family = "unix")] +use std::os::unix::fs::symlink; +use std::path::PathBuf; + /// Copies the contents of a local file to another local file. /// /// Tildes are expanded if present and the destination file is overwritten if /// necessary. pub fn copy_file(src: &str, dst: &str) -> Result<()> { - let _dst = expanduser(dst)?; - if let Some(path) = _dst.parent() { - create_dir_all(path)?; + let _dst = prepare_path(dst)?; + fs::copy(src, _dst)?; + Ok(()) +} + +/// Creates a symbolic link to a local file. +/// +/// Tildes are expanded if present and the destination file is overwritten if +/// necessary. On non-Unix platforms, a hard link will be created instead. +pub fn link_file(src: &str, dst: &str) -> Result<()> { + let _dst = prepare_path(dst)?; + + if cfg!(target_family = "unix") { + symlink(fs::canonicalize(src)?, _dst)?; + } else { + fs::hard_link(src, _dst)?; } - copy(src, _dst)?; + Ok(()) } + +/// Create the parent directories of a path and return the path with tildes +/// expanded. +fn prepare_path(path: &str) -> Result<PathBuf> { + let _dst = expanduser(path)?; + if let Some(_path) = _dst.parent() { + fs::create_dir_all(_path)?; + } + if fs::symlink_metadata(&_dst).is_ok() { + // Check for existing files, including broken symlinks + fs::remove_file(&_dst)?; + } + Ok(_dst) +}