diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8af38df4..92d5ed26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,8 +11,6 @@ on: pull_request: branches: - master - release: - types: [created] defaults: run: @@ -106,32 +104,43 @@ jobs: name: ${{ env.ARCHIVE_NAME }} path: ${{ env.ARCHIVE_NAME }} - upload-releases: - name: Upload the created artifacts to the releases page - runs-on: ubuntu-latest - needs: [build-bionic, build-focal] - if: ${{ github.event_name == 'release' }} + build-yabridgectl: + name: Build yabridgectl + runs-on: ubuntu-18.04 + outputs: + artifact-name: ${{ env.ARCHIVE_NAME }} + defaults: + run: + working-directory: tools/yabridgectl steps: - # They don't allow you to specify multiple file names for these actions - - uses: actions/download-artifact@v2 + - uses: actions/checkout@v2 + # Needed for git-describe to do anything useful + - name: Fetch all git history + run: git fetch --force --prune --tags --unshallow + - name: Determine build archive name + run: | + export ARCHIVE_NAME=yabridgectl-$(git describe --always).tar.gz + echo ::set-env "name=ARCHIVE_NAME::$ARCHIVE_NAME" + - name: Set up Rust toolchain + uses: actions-rs/toolchain@v1 with: - name: ${{ needs.build-bionic.outputs.artifact-name }} - - uses: actions/download-artifact@v2 + toolchain: stable + profile: minimal + default: true + - name: Build the binaries + run: cargo build --release + - name: Strip remaining debug symbols + run: strip target/release/yabridgectl + - name: Create an archive for the binaries + run: | + mkdir yabridgectl + cp target/release/yabridgectl README.md yabridgectl + + tar -caf "$ARCHIVE_NAME" yabridgectl + rm -rf yabridgectl + - uses: actions/upload-artifact@v2 with: - name: ${{ needs.build-focal.outputs.artifact-name }} - - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./${{ needs.build-bionic.outputs.artifact-name }} - asset_name: $${{ needs.build-bionic.outputs.artifact-name }} - asset_content_type: application/x-compressed-tar - - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./${{ needs.build-focal.outputs.artifact-name }} - asset_name: $${{ needs.build-focal.outputs.artifact-name }} - asset_content_type: application/x-compressed-tar + name: ${{ env.ARCHIVE_NAME }} + # For some reason there's no way to tell GitHub actions to run actions + # in a subdirectory + path: tools/yabridgectl/${{ env.ARCHIVE_NAME }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 888d0384..743b4d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- By somewhat popular demand, yabridge now comes with yabridgectl, a utility + that can automatically set up and manage yabridge for you. It also performs + some basic checks to ensure that everything has been set up correctly. + Yabridgectl can be downloaded separately from the GitHub releases page and its + use is completely optional, so you don't have to use it if you don't want to. + Check out the + [readme](https://github.com/robbert-vdh/yabridge/tree/feature/cli-tools/tools/yabridgectl/README.md) + for more information on how it works. + ## [1.2.1] - 2020-06-20 ### Changed diff --git a/README.md b/README.md index 3cd92624..4428f7c4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ Please let me know if there are any issues with other VST hosts. ## Usage +TODO: Refactor these sections to refer to yabridgectl for most of the setup. If +you are reading this, then you can either follow the instructions below or you +can download a preview version of yabridgectl from the [automated +builds](https://github.com/robbert-vdh/yabridge/actions?query=workflow%3A%22Automated+builds%22+branch%3Amaster) +page. + You can either download a prebuilt version of yabridge through the GitHub [releases](https://github.com/robbert-vdh/yabridge/releases) section, or you can compile it from source using the instructions in the [build](#Building) section diff --git a/tools/yabridgectl/.gitignore b/tools/yabridgectl/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/tools/yabridgectl/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/tools/yabridgectl/Cargo.lock b/tools/yabridgectl/Cargo.lock new file mode 100644 index 00000000..8c91f2d0 --- /dev/null +++ b/tools/yabridgectl/Cargo.lock @@ -0,0 +1,507 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clap" +version = "3.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "860643c53f980f0d38a5e25dfab6c3c93b2cb3aa1fe192643d17a293c6c41936" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "term_size 1.0.0-beta1", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb51c9e75b94452505acd21d929323f5a5c6c4735a852adbd39ef5fb1b014f30" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" +dependencies = [ + "autocfg", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "memoffset" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "os_str_bytes" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510" + +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[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 = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "term_size" +version = "1.0.0-beta1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a17d8699e154863becdf18e4fd28bd0be27ca72856f54daf75c00f2566898f" +dependencies = [ + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "term_size 0.3.2", + "unicode-width", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[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 = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" + +[[package]] +name = "yabridgectl" +version = "1.2.1" +dependencies = [ + "aho-corasick", + "clap", + "colored", + "lazy_static", + "rayon", + "serde", + "serde_derive", + "toml", + "walkdir", + "xdg", +] diff --git a/tools/yabridgectl/Cargo.toml b/tools/yabridgectl/Cargo.toml new file mode 100644 index 00000000..d3d7f579 --- /dev/null +++ b/tools/yabridgectl/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "yabridgectl" +# This version is linked to yabridge's version for clarity's sake and because +# there's not a lot going on here +version = "1.2.1" +authors = ["Robbert van der Helm "] +edition = "2018" +description = "Optional utility to help set up yabridge" +repository = "https://github.com/robbert-vdh/yabridge" +license = "GPL-3.0-or-later" + +[dependencies] +aho-corasick = "0.7.13" +colored = "2.0.0" +clap = { version = "3.0.0-beta.1", features = ["wrap_help"] } +lazy_static = "1.4.0" +rayon = "1.3.1" +serde = "1.0.114" +serde_derive = "1.0.114" +toml = "0.5.6" +walkdir = "2.3.1" +xdg = "2.2.0" diff --git a/tools/yabridgectl/README.md b/tools/yabridgectl/README.md new file mode 100644 index 00000000..6a2e17e9 --- /dev/null +++ b/tools/yabridgectl/README.md @@ -0,0 +1,103 @@ +# yabridgectl + +A small, optional utility to set up +[yabridge](https://github.com/robbert-vdh/yabridge) for several directories at +once and to keep them updated. + +## Usage + +Yabridgectl can be downloaded from the GitHub releases section, and it can run +from anywhere. All of the information below can also be found by running +`yabridgectl --help`. + +### Yabridge path + +Yabrdgectl will need to know where it can find `libyabridge.so`. By default it +will look for it in both `~/.local/share/yabridge` (the recommended installation +directory when using the prebuilt binaries) and in `/usr/lib` (used when using +the AUR packages). You can use the command below to override this behaviour and +to use a custom installation directory instead. + +```shell +yabridgectl set --path= +``` + +### Installation methods + +By default, yabridgectl will use the copy-based installation method for yabridge +since this installation method works everywhere. If you are using a DAW that +supports individually sandboxed plugins, then you can choose between using +copies and symlinks using the command below. + +```shell +yabridgectl set --method= +``` + +### Managing directories + +Yabridgectl manage Windows VST plugin install locations for you. To add, remove +and list directories, or to list the plugins currently installed inside one of +those directories, you can use the command below. + +```shell +# Add a directory to watch +# For instance, use the below command for the most common VST2 plugin directory +# yabridgectl add "$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins" +yabridgectl add +# No longer watch a directory, this will ask you if you want to remove any leftover yabridge files +yabridgectl rm +# List the currently watched directories +yabridgectl list +# Show the current settings and the installation status for all plugins in the watched directories +yabridgectl status +``` + +### Installing and updating + +Finally you can set up or update yabridge for all of your plugins at once using +the command below. By default yabridgectl will warn you if it finds `.so` files +without an accompanying `.dll` file, but it will only delete those when using +the `--prune` option. + +```shell +# Set up copies or symlinks of yabridge for all plugins under the watched directories +yabridgectl sync +# Set up yabridge, and also remove any '.so' still leftover after removing a plugin +yabridgectl sync --prune +``` + +## Alternatives + +If you want to script your own installation behaviour and don't feel like using +yabridgectl, then you could use one of the below bash snippets as a base. This +approach is slightly less robust and does not do any problem detection or status +reporting, but it will get you started. + +```shell +# For use with symlinks +yabridge_home=$HOME/.local/share/yabridge +plugin_dir="$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins" + +find -L "$plugin_dir" -type f -iname '*.dll' -print0 | + xargs -0 -P$(nproc) -I{} bash -c "(winedump -j export '{}' | grep -qE 'VSTPluginMain|main|main_plugin') && printf '{}\0'" | + sed -z 's/\.dll$/.so/' | + xargs -0 -n1 ln -sf "$yabridge_home/libyabridge.so" + +# For use with copies +yabridge_home=$HOME/.local/share/yabridge +plugin_dir="$HOME/.wine/drive_c/Program Files/Steinberg/VstPlugins" + +find -L "$plugin_dir" -type f -iname '*.dll' -print0 | + xargs -0 -P$(nproc) -I{} bash -c "(winedump -j export '{}' | grep -qE 'VSTPluginMain|main|main_plugin') && printf '{}\0'" | + sed -z 's/\.dll$/.so/' | + xargs -0 -n1 cp "$yabridge_home/libyabridge.so" +``` + +## Building + +After installing [Rust](https://rustup.rs/), simply run the below to compile and +run: + +```shell +cargo run --release +``` diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs new file mode 100644 index 00000000..d09b0318 --- /dev/null +++ b/tools/yabridgectl/src/config.rs @@ -0,0 +1,200 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use rayon::prelude::*; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Display; +use std::fs; +use std::path::{Path, PathBuf}; +use xdg::BaseDirectories; + +use crate::files::{self, SearchResults}; + +/// The name of the config file, relative to `$XDG_CONFIG_HOME/CONFIG_PREFIX`. +const CONFIG_FILE_NAME: &str = "config.toml"; +/// The name of the XDG base directory prefix for yabridgectl, relative to `$XDG_CONFIG_HOME` and +/// `$XDG_DATA_HOME`. +const YABRIDGECTL_PREFIX: &str = "yabridgectl"; + +/// The name of the library file we're searching for. +const LIBYABRIDGE_NAME: &str = "libyabridge.so"; +/// The name of the XDG base directory prefix for yabridge's own files, relative to +/// `$XDG_CONFIG_HOME` and `$XDG_DATA_HOME`. +const YABRIDGE_PREFIX: &str = "yabridge"; + +/// The configuration used for yabridgectl. This will be serialized to and deserialized from +/// `$XDG_CONFIG_HOME/yabridge/config.toml`. +#[derive(Deserialize, Serialize, Debug)] +pub struct Config { + /// The installation method to use. We will default to creating copies since that works + /// everywehre. + pub method: InstallationMethod, + /// The path to the directory containing `libyabridge.so`. If not set, then yabridgectl will + /// look in `/usr/lib` and `$XDG_DATA_HOME/yabridge` since those are the expected locations for + /// yabridge to be installed in. + pub yabridge_home: Option, + /// Directories to search for Windows VST plugins. We're using an ordered set here out of + /// convenience so we can't get duplicates and the config file is always sorted. + pub plugin_dirs: BTreeSet, +} + +/// Specifies how yabridge will be set up for the found plugins. +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum InstallationMethod { + /// Create a copy of `libyabridge.so` for every Windows VST2 plugin .dll file found. After + /// updating yabridge, the user will have to rerun `yabridgectl sync` to copy over the new + /// version. + Copy, + /// This will create a symlink to `libyabridge.so` for every VST2 .dll file in the plugin + /// directories. As explained in the readme, this makes updating easier and remvoes the need to + /// modify the `PATH` environment variable. + Symlink, +} + +impl InstallationMethod { + /// The plural term for this installation methodd, using in string formatting. + pub fn plural(&self) -> &str { + match &self { + InstallationMethod::Copy => "copies", + InstallationMethod::Symlink => "symlinks", + } + } +} + +impl Display for InstallationMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + InstallationMethod::Copy => write!(f, "copy"), + InstallationMethod::Symlink => write!(f, "symlink"), + } + } +} + +impl Config { + /// Try to read the config file, creating a new default file if necessary. This will fail if the + /// file could not be created or if it could not be parsed. + pub fn read() -> Result { + match yabridgectl_directories()?.find_config_file(CONFIG_FILE_NAME) { + Some(path) => { + let toml_str = fs::read_to_string(&path).map_err(|err| { + format!( + "Could not read config file at '{}': {}", + path.display(), + err + ) + })?; + + Ok(toml::from_str(&toml_str) + .map_err(|err| format!("Could not parse TOML: {}", err))?) + } + None => { + let defaults = Config { + method: InstallationMethod::Copy, + yabridge_home: None, + plugin_dirs: BTreeSet::new(), + }; + + // If no existing config file exists, then write a new config file with default + // values + defaults.write()?; + + Ok(defaults) + } + } + } + + /// Write the config to disk, creating the file if it does not yet exist. + pub fn write(&self) -> Result<(), String> { + let toml_str = toml::to_string_pretty(&self) + .map_err(|err| format!("Could not format TOML: {}", err))?; + let config_file = yabridgectl_directories()? + .place_config_file(CONFIG_FILE_NAME) + .map_err(|err| format!("Could not write config file: {}", err))?; + + fs::write(&config_file, toml_str).map_err(|err| { + format!( + "Could not write config file to '{}': {}", + config_file.display(), + err + ) + }) + } + + /// Return the path to `libyabridge.so`, or a descriptive error if it can't be found. If + /// `yabridge_home` is `None`, then we'll search in both `/usr/lib` and + /// `$XDG_DATA_HOME/yabridge`. + pub fn libyabridge(&self) -> Result { + match &self.yabridge_home { + Some(directory) => { + let candidate = directory.join(LIBYABRIDGE_NAME); + if candidate.exists() { + Ok(candidate) + } else { + Err(format!( + "Could not find '{}' in '{}'.", + LIBYABRIDGE_NAME, + directory.display() + )) + } + } + None => { + // Search in the two common installation locations if no path was set explicitely + let system_path = Path::new("/usr/lib"); + let user_path = yabridge_directories()?.get_data_home(); + for directory in &[system_path, &user_path] { + let candidate = directory.join(LIBYABRIDGE_NAME); + if candidate.exists() { + return Ok(candidate); + } + } + + Err(format!( + "Could not find '{}' in either '{}' or '{}'. You can tell yabridgectl where \ + to search for it using 'yabridgectl set --path='.", + LIBYABRIDGE_NAME, + system_path.display(), + user_path.display() + )) + } + } + } + + /// Search for VST2 plugins in all of the registered plugins directories. This will return an + /// error if `winedump` could not be called. + pub fn index_directories(&self) -> Result, std::io::Error> { + self.plugin_dirs + .par_iter() + .map(|path| files::index(path).map(|search_results| (path.as_path(), search_results))) + .collect() + } +} + +/// Fetch the XDG base directories for yabridge's own files, converting any error messages if this +/// somehow fails into a printable string to reduce boiler plate. This is only used when searching +/// for `libyabridge.so` when no explicit search path has been set. +fn yabridge_directories() -> Result { + BaseDirectories::with_prefix(YABRIDGE_PREFIX) + .map_err(|err| format!("Error while parsing base directories: {}", err)) +} + +/// Fetch the XDG base directories used for yabridgectl, converting any error messages if this +/// somehow fails into a printable string to reduce boiler plate. +fn yabridgectl_directories() -> Result { + BaseDirectories::with_prefix(YABRIDGECTL_PREFIX) + .map_err(|err| format!("Error while parsing base directories: {}", err)) +} diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs new file mode 100644 index 00000000..05c2dc9e --- /dev/null +++ b/tools/yabridgectl/src/files.rs @@ -0,0 +1,162 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Functions to index plugins and to set up yabridge for those plugins. + +use aho_corasick::AhoCorasick; +use lazy_static::lazy_static; +use rayon::prelude::*; +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use walkdir::WalkDir; + +/// Stores the results from searching for Windows VST plugin `.dll` files and native Linux `.so` +/// files inside of a directory. These `.so` files are kept track of so we can report the current +/// installation status and to be able to prune orphan files. +#[derive(Debug)] +pub struct SearchResults { + /// Absolute paths to the found VST2 `.dll` files. + pub vst2_files: Vec, + /// `.dll` files skipped over during the serach. Used for printing statistics and shown when + /// running `yabridgectl sync --verbose`. + pub skipped_files: Vec, + /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or + /// a regular file. + pub so_files: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FoundFile { + Symlink(PathBuf), + Regular(PathBuf), +} + +impl SearchResults { + /// For every found VST2 plugin, find the associated copy or symlink of `libyabridge.so`. The + /// returned hashmap will contain a `None` value for plugins that have not yet been set up. + /// + /// These two functions could be combined into a single function, but speed isn't really an + /// issue here and it's a bit more organized this way. + pub fn installation_status(&self) -> BTreeMap<&Path, Option<&FoundFile>> { + let so_files: HashMap<&Path, &FoundFile> = self + .so_files + .iter() + .map(|file| (file.path(), file)) + .collect(); + + self.vst2_files + .iter() + .map( + |path| match so_files.get(path.with_extension("so").as_path()) { + Some(&file) => (path.as_path(), Some(file)), + None => (path.as_path(), None), + }, + ) + .collect() + } + + /// Find all `.so` files in the search results that do not belong to a VST2 plugin `.dll` file. + pub fn orphans(&self) -> Vec<&FoundFile> { + // We need to store these in a map so we can easily entries with corresponding `.dll` files + let mut orphans: HashMap<&Path, &FoundFile> = self + .so_files + .iter() + .map(|file| (file.path(), file)) + .collect(); + for vst2_path in &self.vst2_files { + orphans.remove(vst2_path.with_extension("so").as_path()); + } + + orphans.into_iter().map(|(_, file)| file).collect() + } +} + +impl FoundFile { + /// Return the path of a found `.so` file. + pub fn path(&self) -> &Path { + match &self { + FoundFile::Symlink(path) => path, + FoundFile::Regular(path) => path, + } + } +} + +/// Search for Windows VST2 plugins and .so files under a directory. This will return an error if +/// the directory does not exist, or if `winedump` could not be found. +pub fn index(directory: &Path) -> Result { + // First we'll find all .dll and .so files in the directory + let mut dll_files: Vec = Vec::new(); + let mut so_files: Vec = Vec::new(); + // XXX: We're silently skipping directories and files we don't have permission to read. This + // sounds like the expected behavior, but I"m not entirely sure. + for entry in WalkDir::new(directory) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|x| !x.file_type().is_dir()) + { + match entry.path().extension().and_then(|os| os.to_str()) { + Some("dll") => dll_files.push(entry.into_path()), + Some("so") => { + if entry.path_is_symlink() { + so_files.push(FoundFile::Symlink(entry.into_path())); + } else { + so_files.push(FoundFile::Regular(entry.into_path())); + } + } + _ => (), + } + } + + lazy_static! { + static ref VST2_AUTOMATON: AhoCorasick = + AhoCorasick::new_auto_configured(&["VSTPluginMain", "main", "main_plugin"]); + } + + // THne we'll figure out which `.dll` files are VST2 plugins and which should be skipped by + // checking whether the file contains one of the VST2 entry point functions. The boolean flag in + // this vector indicates whether it is a VST2 plugin. + let dll_files: Vec<(PathBuf, bool)> = dll_files + .into_par_iter() + .map(|path| { + let exported_functions = Command::new("winedump") + .arg("-j") + .arg("export") + .arg(&path) + .output()? + .stdout; + + Ok((path, VST2_AUTOMATON.is_match(exported_functions))) + }) + .collect::>()?; + + let mut vst2_files = Vec::new(); + let mut skipped_files = Vec::new(); + for (path, is_vst2_plugin) in dll_files { + if is_vst2_plugin { + vst2_files.push(path); + } else { + skipped_files.push(path); + } + } + + Ok(SearchResults { + vst2_files, + skipped_files, + so_files, + }) +} diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs new file mode 100644 index 00000000..187cb36e --- /dev/null +++ b/tools/yabridgectl/src/main.rs @@ -0,0 +1,364 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clap::{app_from_crate, App, AppSettings, Arg, ArgMatches}; +use colored::Colorize; +use std::fs; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::process::exit; + +use crate::config::{Config, InstallationMethod}; +use crate::files::FoundFile; + +mod config; +mod files; + +// TODO: Naming and descriptions could be made clearer +// TODO: When creating copies, check whether `yabridge-host.exe` is in the PATH for the login shell +// TODO: Check for left over files when removing directory +// TODO: Reward parts of the readme + +fn main() { + let mut config = match Config::read() { + Ok(config) => config, + Err(err) => { + eprintln!("Error while reading config:\n\n{}", err); + std::process::exit(1); + } + }; + + // Used for validation in `yabridgectl rm ` + let plugin_directories: Vec<&str> = config + .plugin_dirs + .iter() + .map(|path| path.to_str().expect("Path contains invalid unicode")) + .collect(); + + let matches = app_from_crate!() + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + App::new("add").about("Add a plugin install location").arg( + Arg::with_name("path") + .about("Path to a directory containing Windows VST plugins") + .validator(validate_path) + .takes_value(true) + .required(true), + ), + ) + .subcommand( + App::new("rm") + .about("Remove a plugin install location") + .arg( + Arg::with_name("path") + .about("Path to a directory") + .possible_values(&plugin_directories) + .takes_value(true) + .required(true), + ), + ) + .subcommand(App::new("list").about("List the plugin install locations")) + .subcommand(App::new("status").about("Show the installation status for all plugins")) + .subcommand( + App::new("set") + .about("Change installation method or yabridge path") + .setting(AppSettings::ArgRequiredElseHelp) + .arg( + Arg::with_name("method") + .long("method") + .about("The installation method to use") + .long_about("The installation method to use.") + .possible_values(&["copy", "symlink"]) + .takes_value(true), + ) + .arg( + Arg::with_name("path") + .long("path") + .about("Path to the directory containing 'libyabridge.so'") + .long_about("Path to the directory containing 'libyabridge.so'. If this is not set, then yabridgectl will look in both '/usr/lib' and '~/.local/share/yabridge' by default.") + .validator(validate_path) + .takes_value(true), + ), + ) + .subcommand( + App::new("sync") + .about("Set up or update yabridge for all plugins") + .arg( + Arg::with_name("prune") + .short('p') + .long("prune") + .about("Remove unrelated or leftover '.so' files"), + ) + .arg( + Arg::with_name("verbose") + .short('v') + .long("verbose") + .about("Print information about plugins being set up or skipped"), + ), + ) + .get_matches(); + + match matches.subcommand() { + ("add", Some(options)) => add_directory(&mut config, options.value_of_t_or_exit("path")), + ("rm", Some(options)) => { + remove_directory(&mut config, &options.value_of_t_or_exit::("path")) + } + ("list", _) => list_directories(&config), + ("status", _) => show_status(&config), + ("set", Some(options)) => set_settings(&mut config, options), + ("sync", Some(options)) => do_sync( + &config, + options.is_present("prune"), + options.is_present("verbose"), + ), + _ => unreachable!(), + } +} + +/// Add a direcotry to the plugin locations. Duplicates get ignord because we're using ordered sets. +fn add_directory(config: &mut Config, path: PathBuf) { + config.plugin_dirs.insert(path); + if let Err(err) = config.write() { + eprintln!("Error while writing config file: {}", err); + exit(1); + }; +} + +/// Remove a direcotry to the plugin locations. The path is assumed to be part of +/// `config.plugin_dirs`, otherwise this si silently ignored. +fn remove_directory(config: &mut Config, path: &Path) { + // We've already verified that this path is in `config.plugin_dirs` + // XXS: Would it be a good idea to warn about leftover .so files? + config.plugin_dirs.remove(path); + if let Err(err) = config.write() { + eprintln!("Error while writing config file: {}", err); + exit(1); + }; +} + +/// List the plugin locations. +fn list_directories(config: &Config) { + for directory in &config.plugin_dirs { + println!("{}", directory.display()); + } +} + +/// Print the current configuration and the installation status for all found plugins. +fn show_status(config: &Config) { + let results = match config.index_directories() { + Ok(results) => results, + Err(err) => { + eprintln!("Error while searching for plugins: {}", err); + exit(1); + } + }; + + println!( + "yabridge path: {}", + config + .yabridge_home + .as_ref() + .map(|path| format!("'{}'", path.display())) + .unwrap_or_else(|| String::from("")) + ); + println!( + "libyabridge.so: {}", + config + .libyabridge() + .map(|path| format!("'{}'", path.display())) + .unwrap_or_else(|_| format!("{}", "".red())) + ); + println!("installation method: {}", config.method); + + for (path, search_results) in results { + println!("\n{}:", path.display()); + + for (plugin, status) in search_results.installation_status() { + let status_str = match status { + Some(FoundFile::Regular(_)) => "copy".green(), + Some(FoundFile::Symlink(_)) => "symlink".green(), + None => "not installed".red(), + }; + + println!(" {} :: {}", plugin.display(), status_str); + } + } +} + +/// Change configuration settings. The actual options are defined in the clap [app](clap::App). +fn set_settings(config: &mut Config, options: &ArgMatches) { + match options.value_of("method") { + Some("copy") => config.method = InstallationMethod::Copy, + Some("symlink") => config.method = InstallationMethod::Symlink, + Some(s) => unimplemented!("Unexpected installation method '{}'", s), + None => (), + } + + match options.value_of_t("path") { + Ok(path) => config.yabridge_home = Some(path), + Err(clap::Error { + kind: clap::ErrorKind::ArgumentNotFound, + .. + }) => (), + // I don't think we can get any parsing errors here since we already validated that the + // argument has to be a valid path, but you never know + Err(err) => err.exit(), + } + + if let Err(err) = config.write() { + eprintln!("Error while writing config file: {}", err); + exit(1); + }; +} + +/// Set up yabridge for all Windows VST2 plugins in the plugin directories. Will also remove orphan +/// `.so` files if the prune option is set. +fn do_sync(config: &Config, prune: bool, verbose: bool) { + let libyabridge_path = match config.libyabridge() { + Ok(path) => { + println!("Using '{}'\n", path.display()); + path + } + Err(err) => { + // The error messages here are already formatted + eprintln!("{}", err); + exit(1); + } + }; + + let results = match config.index_directories() { + Ok(results) => results, + Err(err) => { + eprintln!("Error while searching for plugins: {}", err); + exit(1); + } + }; + + // Keep track of some global statistics + let mut num_installed = 0; + let mut skipped_dll_files: Vec = Vec::new(); + let mut orphan_so_files: Vec = Vec::new(); + for (path, search_results) in results { + num_installed += search_results.vst2_files.len(); + orphan_so_files.extend(search_results.orphans().into_iter().cloned()); + skipped_dll_files.extend(search_results.skipped_files); + + if verbose { + println!("{}:", path.display()); + } + for plugin in search_results.vst2_files { + // If the target file already exists, we'll remove it first to prevent issues with + // mixing symlinks and regular files + let target_path = plugin.with_extension("so"); + if target_path.exists() { + fs::remove_file(&target_path).unwrap_or_else(|err| { + eprintln!("Could not remove '{}': {}", target_path.display(), err); + exit(1); + }); + } + + match config.method { + InstallationMethod::Copy => { + fs::copy(&libyabridge_path, &target_path).unwrap_or_else(|err| { + eprintln!( + "Error copying '{}' to '{}': {}", + libyabridge_path.display(), + target_path.display(), + err + ); + exit(1); + }); + } + InstallationMethod::Symlink => { + symlink(&libyabridge_path, &target_path).unwrap_or_else(|err| { + eprintln!( + "Error symlinking '{}' to '{}': {}", + libyabridge_path.display(), + target_path.display(), + err + ); + exit(1); + }); + } + } + + if verbose { + println!(" {}", plugin.display()); + } + } + if verbose { + println!(); + } + } + + // We'll print the skipped files all at once to prevetn clutter + let num_skipped_files = skipped_dll_files.len(); + if verbose && !skipped_dll_files.is_empty() { + println!("Skipped files:"); + for path in skipped_dll_files { + println!("- {}", path.display()); + } + println!(); + } + + // Always warn about leftover files sicne those might cause warnings or errors when a VST host + // tries to load them + if !orphan_so_files.is_empty() { + if prune { + println!("Removing {} leftover '.so' file(s):", orphan_so_files.len()); + } else { + println!( + "Found {} leftover '.so' file(s), rerun with the '--prune' option to remove them:", + orphan_so_files.len() + ); + } + + for file in orphan_so_files { + let path = file.path(); + + println!("- {}", path.display()); + if prune { + fs::remove_file(path).unwrap_or_else(|err| { + eprintln!("Error while trying to remove '{}': {}", path.display(), err); + exit(1); + }); + } + } + + println!(); + } + + println!( + "Finished setting up {} plugins using {}, skipped {} non-plugin '.dll' files.", + num_installed, + config.method.plural(), + num_skipped_files + ) +} + +/// Verify that a path exists, used for validating arguments. +fn validate_path(path: &str) -> Result<(), String> { + let path = Path::new(path); + + if path.exists() { + Ok(()) + } else { + Err(format!( + "File or directory '{}' could not be found", + path.display() + )) + } +}