From 10c4436a85a63eeeed887a200165130f1205948a Mon Sep 17 00:00:00 2001 From: William Villeneuve Date: Sat, 21 Oct 2023 18:10:45 -0400 Subject: [PATCH] files-windows-support: switched files plugin to rust --- .github/workflows/release.yml | 116 +++++++- .gitignore | 6 +- .rtx.toml | 1 + .rustfmt.toml | 1 + Cargo.lock | 471 +++++++++++++++++++++++++++++++ Cargo.toml | 3 + bin/dev | 29 ++ files/Cargo.toml | 18 ++ files/files.sh | 220 --------------- files/plugin.yml | 27 +- files/src/args.rs | 35 +++ files/src/main.rs | 514 ++++++++++++++++++++++++++++++++++ rust-toolchain.toml | 3 + 13 files changed, 1205 insertions(+), 239 deletions(-) create mode 100644 .rtx.toml create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 bin/dev create mode 100644 files/Cargo.toml delete mode 100755 files/files.sh create mode 100644 files/src/args.rs create mode 100644 files/src/main.rs create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 068a964..609b07c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,15 +3,66 @@ on: push: branches: - main + # TODO: remove + - files-windows-support jobs: + build: + name: Build + strategy: + matrix: + build: + - os: macos-latest + target: x86_64-apple-darwin + - os: macos-latest + target: aarch64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + # TODO: + # - os: windows-latest + # target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.build.os }} + steps: + - uses: actions/checkout@v4 + - name: Setup musl + run: | + rustup target add x86_64-unknown-linux-musl + sudo apt-get install -y musl-tools + echo 'LDFLAGS=-Wl,--copy-dt-needed-entries' >> $GITHUB_ENV + echo 'RUSTFLAGS=-C target-feature=+crt-static' >> $GITHUB_ENV + if: matrix.build.target == 'x86_64-unknown-linux-musl' + - name: Setup Apple Silicon + run: rustup target add aarch64-apple-darwin + if: matrix.build.target == 'aarch64-apple-darwin' + - uses: Swatinem/rust-cache@v2 + # TODO: fix for windows... + - name: Build + env: + TARGET: ${{ matrix.build.target }} + run: | + declare output + output="$( + cargo build \ + --workspace \ + --release \ + --target "$TARGET" \ + --message-format=json \ + | jq -r '.message.rendered // .executable // empty' + )" + echo "ARTIFACT_PATH=$output" >> $GITHUB_ENV + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.build.target }} + path: ${{ env.ARTIFACT_PATH }} release: name: Release + needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Determine New Version id: version - uses: zwaldowski/semver-release-action@v2 + uses: zwaldowski/semver-release-action@v3 with: dry_run: true bump: minor @@ -27,6 +78,20 @@ jobs: # add to path echo "${HOME}/.nk/bin" >> $GITHUB_PATH + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + - name: Move executables + run: | + for file in artifacts/*/*; do + plugin="$(basename "$file")" + target="$(basename "$(dirname "$file")")" + parent="${plugin}/assets/${target}" + + mkdir -p "$parent" + mv "$file" "${parent}/${plugin}" + done - name: Build assets env: REPOSITORY_NAME: ${{ github.event.repository.name }} @@ -36,13 +101,40 @@ jobs: --repo "$REPOSITORY_NAME" \ --version "$TAG" \ ./*/plugin.yml - - name: Create Release - env: - GITHUB_USER: ${{ github.repository_owner }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create \ - --title "$TAG" \ - --notes '' \ - "$TAG" \ - manifest.yml *.tar.gz + + # TODO: DEBUG + - run: 'find . -type f' + - run: 'false' + + # TODO: DEBUGGING + # - name: Create Release + # env: + # GITHUB_USER: ${{ github.repository_owner }} + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # run: | + # gh release create \ + # --title "$TAG" \ + # --notes '' \ + # "$TAG" \ + # manifest.yml *.tar.gz + + # TODO: DEBUG + # - name: Create Release + # env: + # GITHUB_USER: ${{ github.repository_owner }} + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # run: | + # mkdir assets + + # # move binaries to assets directory + # for path in artifacts/*; do + # declare artifact="$(basename "$path")" + # mv "${path}/nk"* "assets/${artifact}" + # done + + # # create release + # gh release create \ + # --title "$GITHUB_REF_NAME" \ + # --notes '' \ + # "$GITHUB_REF_NAME" \ + # "assets/"* diff --git a/.gitignore b/.gitignore index 21271f8..7c8460d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -# nix -/.direnv/ - -# release assets +/target/ *.tar.gz +manifest.yml diff --git a/.rtx.toml b/.rtx.toml new file mode 100644 index 0000000..99c5022 --- /dev/null +++ b/.rtx.toml @@ -0,0 +1 @@ +env_path = ['./bin'] diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc2ffd7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,471 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags", + "libc", + "winapi", +] + +[[package]] +name = "file-id" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6584280525fb2059cba3db2c04abf947a1a29a45ddae89f3870f8281704fafc9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "files" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "clap", + "dirs", + "faccess", + "file-id", + "serde", + "serde_json", + "shellexpand", + "walkdir", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..203c088 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["files"] diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..45ee664 --- /dev/null +++ b/bin/dev @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e + +# TODO: initial setup: cargo install cargo-watch +# TODO: reset: git clean ... +# TODO: lint: cargo clippy +# TODO: lint fix: cargo clippy --fix + +# TODO: start: cargo watch -x "build --workspace" -s ' +# cp target/debug/{plugin}(.exe) {plugin}/assets/{target}/{plugin}(.exe) +# ' +# - {plugin}/assets/{target}/{plugin}(.exe) +# - ie. files/assets/x86_64-apple-darwin/files +# - ie. files/assets/aarch64-apple-darwin/files +# - ie. files/assets/aarch64-pc-windows-msvc/files.exe +# - plugin.yml +# exe: ./assets/aarch64-pc-windows-msvc/files.exe +# % targets +# - x86_64-apple-darwin +# - aarch64-apple-darwin +# - x86_64-unknown-linux-musl +# - x86_64-pc-windows-msvc + +# TODO: debug +# - ie. cargo run -p 'files' -q -- provision '{"sources": ["/Users/william/Projects/dotfiles/.", "/Users/william/Projects/dotfiles/../dotfiles-private"], "vars": {}}' <<< ' +# {"declaration": "files", "state": {"source": "home", "destination": "~/", "link_files": true}} +# {"declaration": "files", "state": {"source": "readme.md", "destination": "~/temp-readme.md"}} +# ' diff --git a/files/Cargo.toml b/files/Cargo.toml new file mode 100644 index 0000000..3ed37ca --- /dev/null +++ b/files/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "files" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +camino = { version = "1.1.6", features = ["serde1"] } +clap = { version = "4.4.6", features = ["derive"] } +dirs = "5.0.1" +faccess = "0.2.4" +file-id = "0.2.1" +serde = { version = "1.0.189", features = ["derive"] } +serde_json = "1.0.107" +shellexpand = "3.1.0" +walkdir = "2.4.0" diff --git a/files/files.sh b/files/files.sh deleted file mode 100755 index dca1b1e..0000000 --- a/files/files.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env bash - -# NOTE: DOES NOT APPLY TO FUNCTIONS CALLED INSIDE IF CONDITIONS OR WITH ||/&& CHAINS -set -e - -eval "$(nk plugin helper bash 2>/dev/null)" - -files::_perms() { - if [[ "$OSTYPE" == darwin* ]]; then - stat -f "%A" "$@" - else - stat -c "%a" "$@" - fi -} - -files::_file_identity() { - if [[ "$OSTYPE" == darwin* ]]; then - stat -Lf "%d:%i" "$1" 2>/dev/null - else - stat -Lc "%d:%i" "$1" 2>/dev/null - fi -} - -files::_provision_file() { - # TODO: consider refactoring to add the parent directories to the list of files to create? - # create parent directory - declare destination_parent - destination_parent="$(dirname "$destination_file")" - - if [[ ! -d "$destination_parent" ]]; then - # create directory - mkdir -p "$destination_parent" \ - || return "$(nk::error "$?" "failed creating parent directory: ${destination_parent}")" - changed='true' - - # chmod directory - if [[ "$(files::_perms "$destination_parent")" != '700' ]]; then - chmod 700 "$destination_parent" \ - || return "$(nk::error "$?" "failed chmod'ing parent directory: ${destination_parent}")" - changed='true' - fi - fi - - if [[ -d "$source_file" ]]; then - # create directory - if [[ ! -d "$destination_file" ]]; then - # delete existing first - if [[ -e "$destination_file" ]]; then - rm -rf "$destination_file" || return "$?" - changed='true' - fi - - # create directory - mkdir "$destination_file" || return "$?" - changed='true' - fi - - # chmod directory - if [[ "$(files::_perms "$destination_file")" != '700' ]]; then - chmod 700 "$destination_file" || return "$?" - changed='true' - fi - elif [[ "$link_files" == 'true' ]]; then - # create link - if [[ "$(files::_file_identity "$destination_file")" != "$(files::_file_identity "$source_file")" ]]; then - # delete existing first - if [[ -d "$destination_file" ]]; then - rm -rf "$destination_file" || return "$?" - changed='true' - fi - - # link file - ln -sf "${PWD}/${source_file}" "$destination_file" || return "$?" - changed='true' - fi - else - declare source_contents - source_contents="$(cat "$source_file")" || return "$?" - declare destination_contents - destination_contents="$(cat "$destination_file" 2>/dev/null)" # failures intentionally ignored - - # create file - if [[ ! -f "$destination_file" || "$destination_contents" != "$source_contents" || -L "$destination_file" ]]; then - # delete existing first - if [[ -d "$destination_file" || -L "$destination_file" ]]; then - rm -rf "$destination_file" || return "$?" - changed='true' - fi - - # copy file - cp "$source_file" "$destination_file" || return "$?" - changed='true' - fi - - # determine perms to set - if [[ -x "$source_file" ]]; then - declare perms='700' - else - declare perms='600' - fi - - # chmod file - if [[ "$(files::_perms "$destination_file")" != "$perms" ]]; then - chmod "$perms" "$destination_file" || return "$?" - changed='true' - fi - fi -} - -files::_exists_under_any() { - declare source_="$1" - declare -a nk_sources=("${@:2}") - - for nk_source in "${nk_sources[@]}"; do - # exists, and if it's a directory, it's also listable - declare possible_source="${nk_source}/${source_}" - if [[ -e "$possible_source" && (! -d "$possible_source" || -x "$possible_source") ]]; then - return 0 - fi - done - - return 1 -} - -files::provision() -{ - declare info="$1" - declare -a nk_sources=() - while read -r nk_source; do - nk_sources+=("$nk_source") - done <<< "$(jq -r --compact-output '.sources[]' <<<"$info")" - - while read -r state; do - declare source_ - source_="$(jq -r '.source' <<< "$state")" - declare destination - destination="$(jq -r '.destination' <<< "$state")" - destination="${destination%/}" # drop optional trailing slash - declare link_files - link_files="$(jq -r 'if has("link_files") then .link_files else false end' <<< "$state")" - - # replace tilde - declare resolved_destination - resolved_destination="${destination/#~/$HOME}" - - # if source does not existing or isn't listable - if ! files::_exists_under_any "$source_" "${nk_sources[@]}"; then - declare output='' - if [[ ! -e "$source_" ]]; then - output="${source_} does not exist" - else - output="${source_} is not listable" - fi - - nk::log_result \ - 'failed' \ - 'false' \ - "$destination" \ - "$output" - continue - fi - - # search relative to all nk sources - for nk_source in "${nk_sources[@]}"; do - declare nk_source_relative_source="${nk_source}/${source_}" - if [[ ! -e "$nk_source_relative_source" ]]; then - # NOTE: we check above if any of the sources exist, so this is fine - continue - fi - - # iterate all files/directories in source - while read -r source_file; do - # figure out destination file path - declare nested_source_file="${source_file#"${nk_source_relative_source}/"}" - if [[ "$nested_source_file" == "$source_file" ]]; then - # source file is the source - declare destination_file="$resolved_destination" - declare destination_file_tilde="$destination" - else - # source file is a child of the source - declare destination_file="${resolved_destination}/${nested_source_file}" - declare destination_file_tilde="${destination}/${nested_source_file}" - fi - - # provision file - declare status='success' - declare changed='false' - declare output='' - if ! nk::run_for_output output "files::_provision_file"; then - status='failed' - fi - - # build description - if [[ "$link_files" == 'false' || -d "$source_file" ]]; then - declare action='create' - else - declare action='link' - fi - declare description="${action} ${destination_file_tilde}" - - # log state details - nk::log_result \ - "$status" \ - "$changed" \ - "$description" \ - "$output" - done <<< "$(find "$nk_source_relative_source")" - done - done <<< "$(jq --compact-output '.[].state')" -} - -case "$1" in - provision) - files::provision "${@:2}" - ;; - *) - echo "files: unrecognized subcommand ${1}" 1>&2 - exit 1 - ;; -esac diff --git a/files/plugin.yml b/files/plugin.yml index b8e9672..846fbcc 100644 --- a/files/plugin.yml +++ b/files/plugin.yml @@ -1,11 +1,8 @@ name: files -when: family == "unix" provision: when: declaration == "files" -executable: files.sh - schema: $schema: https://json-schema.org/draft/2020-12/schema type: object @@ -19,3 +16,27 @@ schema: required: - source - destination + +--- +when: + - os == "macos" + - arch == "x86_64" +executable: assets/x86_64-apple-darwin/files + +--- +when: + - os == "macos" + - arch == "aarch64" +executable: assets/aarch64-apple-darwin/files + +--- +when: + - os == "linux" + - arch == "x86_64" +executable: assets/x86_64-unknown-linux-musl/files + +--- +when: + - family == "windows" + - arch == "x86_64" +executable: assets/x86_64-pc-windows-msvc/files.exe diff --git a/files/src/args.rs b/files/src/args.rs new file mode 100644 index 0000000..3855acf --- /dev/null +++ b/files/src/args.rs @@ -0,0 +1,35 @@ +use anyhow::Error; +use camino::Utf8PathBuf; +use clap::{arg, Args, Parser, Subcommand}; +use serde::Deserialize; + +#[derive(Debug, Parser)] +#[command(about, version)] +pub struct Arguments { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + Provision(Provision), +} + +#[derive(Debug, Args)] +pub struct Provision { + /// Provision info as json + #[arg(value_name = "info", value_parser = ProvisionInfo::value_parser)] + pub info: ProvisionInfo, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ProvisionInfo { + pub sources: Vec, + // pub vars: serde_json::Map, +} + +impl ProvisionInfo { + fn value_parser(value: &str) -> Result { + Ok(serde_json::from_str(value)?) + } +} diff --git a/files/src/main.rs b/files/src/main.rs new file mode 100644 index 0000000..f9dc7a2 --- /dev/null +++ b/files/src/main.rs @@ -0,0 +1,514 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow(clippy::cargo_common_metadata)] +#![allow(clippy::too_many_lines)] + +mod args; + +use anyhow::{anyhow, Result}; +use args::{Arguments, Commands, Provision}; +use camino::{Utf8Path, Utf8PathBuf}; +use clap::Parser; +use faccess::PathExt; +use file_id::get_file_id; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; +use std::{ + fs::{copy, create_dir_all, remove_dir_all, remove_file, File}, + io::{stdin, Read}, + path::Path, + str::FromStr, +}; +use walkdir::WalkDir; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case", tag = "declaration", content = "state")] +enum State { + Files(FileState), +} + +#[derive(Debug, Deserialize)] +struct FileState { + source: Utf8PathBuf, + #[serde(deserialize_with = "expand_path")] + destination: Utf8PathBuf, + #[serde(default)] + link_files: bool, +} + +fn expand_path<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let path: String = Deserialize::deserialize(deserializer)?; + // TODO: maybe std::path::absolute once stable? + Utf8PathBuf::from_str(&shellexpand::tilde(&path)).map_err(D::Error::custom) +} + +fn main() -> Result<()> { + let args = Arguments::parse(); + + match args.command { + Commands::Provision(args) => provision(args), + } +} + +fn display_path_with_tilde(path: &Utf8Path) -> String { + let mut path_string = path.to_string(); + + if let Some(home_dir) = dirs::home_dir() { + if let Ok(home) = Utf8PathBuf::try_from(home_dir) { + let home_str = home.as_str(); + if path_string.starts_with(home_str) { + path_string.replace_range(0..home_str.len(), "~"); + } + } + } + + path_string +} + +fn provision(args: Provision) -> Result<()> { + let nk_sources = args.info.sources; + + let states = serde_json::Deserializer::from_reader(stdin()) + .into_iter::() + .collect::, serde_json::Error>>()?; + + for state in states { + match state { + State::Files(state) => { + if let Err(result) = provision_file(&nk_sources, &state) { + // fallback error handler for the provision + println!( + "{}", + serde_json::to_string(&NkProvisionStateResult { + status: NkProvisionStateStatus::Failed, + changed: false, + description: display_path_with_tilde( + &state.destination + ), + output: result.to_string() + })? + ); + } + } + }; + } + + Ok(()) +} + +#[derive(Debug, Serialize)] +struct NkProvisionStateResult { + status: NkProvisionStateStatus, + changed: bool, + description: String, + output: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +enum NkProvisionStateStatus { + Failed, + Success, +} + +fn provision_file(nk_sources: &[Utf8PathBuf], state: &FileState) -> Result<()> { + let FileState { + source, + destination, + link_files, + } = state; + // find sources + let nk_source_relative_sources = nk_sources + .iter() + .map(|nk_source| nk_source.join(source)) + .filter(|p| p.exists()) + .collect::>(); + + // need at least one source to proceed + if nk_source_relative_sources.is_empty() { + return Err(anyhow!("{source}: does not exist")); + } + + // check if any sources aren't listable + let nk_source_relative_sources = nk_source_relative_sources + .iter() + .map(|p| { + if p.is_dir() && !p.as_std_path().executable() { + return Err(anyhow!("{p}: is not listable")); + } + + Ok(p) + }) + .collect::>>()?; + + // walk each source + for nk_source_relative_source in nk_source_relative_sources { + for entry in WalkDir::new(nk_source_relative_source).sort_by_file_name() + { + let entry = entry?; + let source_file = + Utf8PathBuf::from_path_buf(entry.path().into()).unwrap(); + + // figure out destination file path + let destination_file = if source_file == *nk_source_relative_source + { + // root of the source + destination.clone() + } else { + // child of the source + destination + .join(source_file.strip_prefix(nk_source_relative_source)?) + }; + + let output = provision_sub_file( + &source_file, + &destination_file, + *link_files, + ); + + println!("{}", serde_json::to_string(&output)?); + } + } + + Ok(()) +} + +fn provision_sub_file( + source_file: &Utf8Path, + destination_file: &Utf8Path, + link_files: bool, +) -> NkProvisionStateResult { + let action = if !link_files || source_file.is_dir() { + "create" + } else { + "link" + }; + + let mut result = NkProvisionStateResult { + status: NkProvisionStateStatus::Success, + changed: false, + description: format!( + "{action} {}", + display_path_with_tilde(destination_file) + ), + output: String::new(), + }; + + // create parent directory + if let Some(destination_parent) = destination_file.parent() { + if !destination_parent.exists() { + // create directory + if let Err(e) = create_dir_all(destination_parent) { + // failed creating parent directory + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed creating parent directory: {destination_parent}", + ); + + return result; + }; + result.changed = true; + } + + // TODO: should support files.settings or something that we can configure a umask with, then configure that first (assuming it'll apply immediately, if not, use it to calculate perms) + #[cfg(unix)] + { + use std::fs::set_permissions; + use std::os::unix::prelude::MetadataExt; + use std::os::unix::prelude::PermissionsExt; + + // TODO: fix unwrap + let metadata = destination_parent.metadata().unwrap(); + let mut permissions = metadata.permissions(); + let existing_mode = permissions.mode() & 0o777; + + // chmod parent directory + // TODO: uid != 0 is to ensure we don't try to chmod /Users or other system folders... (might be a better way of handling this...) + if existing_mode != 0o700 && metadata.uid() != 0 { + permissions.set_mode(0o700); + if let Err(e) = set_permissions(destination_parent, permissions) + { + // failed changing permissions of parent + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed changing permissions of parent: {destination_parent}", + ); + + return result; + }; + result.changed = true; + } + } + } + + // create/link + + if source_file.is_dir() { + // create directory + + if !destination_file.is_dir() { + // delete existing first + if destination_file.exists() { + if let Err(e) = remove_file(destination_file) { + // failed deleting existing file + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed deleting existing file: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + + // create directory + if let Err(e) = create_dir_all(destination_file) { + // failed creating directory + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed creating directory: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + + // TODO: should support files.settings or something that we can configure a umask with, then configure that first (assuming it'll apply immediately, if not, use it to calculate perms) + #[cfg(unix)] + { + use std::fs::set_permissions; + use std::os::unix::prelude::PermissionsExt; + + // TODO: fix unwrap + let metadata = destination_file.metadata().unwrap(); + let mut permissions = metadata.permissions(); + let existing_mode = permissions.mode() & 0o777; + + // chmod directory + if existing_mode != 0o700 { + permissions.set_mode(0o700); + if let Err(e) = set_permissions(destination_file, permissions) { + // failed changing permissions of parent + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed changing permissions of directory: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + } + } else if link_files { + // link file + + let is_linked_to = match is_linked_to(destination_file, source_file) { + Ok(v) => v, + Err(e) => { + // failed linking file + result.status = NkProvisionStateStatus::Failed; + result.output = + format!("{e}: failed checking link: {destination_file}"); + + return result; + } + }; + + if !is_linked_to { + // delete existing first + if destination_file.is_dir() { + if let Err(e) = remove_dir_all(destination_file) { + // failed deleting existing directory + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed deleting existing directory: {destination_file}", + ); + + return result; + }; + result.changed = true; + } else if destination_file.is_symlink() { + if let Err(e) = remove_file(destination_file) { + // failed deleting existing symlink + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed deleting existing symlink: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + + // link file + if let Err(e) = symlink_file(source_file, destination_file) { + // failed linking file + result.status = NkProvisionStateStatus::Failed; + result.output = + format!("{e}: failed linking file: {destination_file}"); + + return result; + }; + result.changed = true; + } + } else { + // create file + + let file_matches = + match file_contents_match(source_file, destination_file) { + Ok(v) => v, + Err(e) => { + // failed linking file + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed linking file: {destination_file}", + ); + + return result; + } + }; + if !file_matches { + // delete existing first + if destination_file.is_dir() { + if let Err(e) = remove_dir_all(destination_file) { + // failed deleting existing directory + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed deleting existing directory: {destination_file}", + ); + + return result; + }; + result.changed = true; + } else if destination_file.is_symlink() { + if let Err(e) = remove_file(destination_file) { + // failed deleting existing symlink + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed deleting existing symlink: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + + // copy file + if let Err(e) = copy(source_file, destination_file) { + // failed copying file + result.status = NkProvisionStateStatus::Failed; + result.output = + format!("{e}: failed copying file: {destination_file}"); + + return result; + }; + result.changed = true; + } + + // TODO: should support files.settings or something that we can configure a umask with, then configure that first (assuming it'll apply immediately, if not, use it to calculate perms) + #[cfg(unix)] + { + use std::fs::set_permissions; + use std::os::unix::prelude::PermissionsExt; + + // TODO: fix unwrap + let metadata = destination_file.metadata().unwrap(); + let mut permissions = metadata.permissions(); + let existing_mode = permissions.mode() & 0o777; + + // determine perms to set + let perms = if source_file.as_std_path().executable() { + 0o700 + } else { + 0o600 + }; + + // chmod file + if existing_mode != perms { + permissions.set_mode(perms); + if let Err(e) = set_permissions(destination_file, permissions) { + // failed changing permissions of parent + result.status = NkProvisionStateStatus::Failed; + result.output = format!( + "{e}: failed changing permissions of file: {destination_file}", + ); + + return result; + }; + result.changed = true; + } + } + } + + result +} + +fn is_linked_to( + destination_file: &Utf8Path, + source_file: &Utf8Path, +) -> std::io::Result { + let destination_file_id = get_file_id(destination_file)?; + let source_file_id = get_file_id(source_file)?; + + Ok(destination_file_id == source_file_id) +} + +#[cfg(unix)] +fn symlink_file, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::unix::fs::symlink(original, link) +} + +#[cfg(windows)] +fn symlink_file, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::windows::fs::symlink_file(original, link) +} + +fn file_contents_match( + source: &Utf8Path, + destination: &Utf8Path, +) -> std::io::Result { + if !destination.exists() || destination.is_dir() { + return Ok(false); + } + + let mut source_file = File::open(source)?; + let mut destination_file = File::open(destination)?; + + // check file size + if source_file.metadata()?.len() != destination_file.metadata()?.len() { + return Ok(false); + } + + // check file contents + let mut source_contents = String::new(); + let mut destination_contents = String::new(); + + source_file.read_to_string(&mut source_contents)?; + destination_file.read_to_string(&mut destination_contents)?; + + if source_contents != destination_contents { + return Ok(false); + } + + // TODO: allow this to handle large files (maybe have a size limit where it still does the simple thing of loading the whole thing? since that's likely fairly fast...) + // // check file contents + // let mut source_buf_reader = BufReader::new(source_file); + // let mut destination_buf_reader = BufReader::new(destination_file); + + // const SIZE: usize = 8192; + // let mut source_buffer = [0u8; SIZE]; + // let mut destination_buffer = [0u8; SIZE]; + + // source_buf_reader.read_exact(&mut source_buffer)?; + // destination_buf_reader.read_exact(&mut destination_buffer)?; + + Ok(true) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ac7d502 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rust-src"]