Merge branch 'feature/cli-tools'

This commit is contained in:
Robbert van der Helm
2020-07-15 17:47:48 +02:00
10 changed files with 1415 additions and 28 deletions
+37 -28
View File
@@ -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 }}
+13
View File
@@ -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
+6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
target/
+507
View File
@@ -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",
]
+22
View File
@@ -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 <mail@robbertvanderhelm.nl>"]
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"
+103
View File
@@ -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=<path/to/directory/containing/yabridge>
```
### 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=<copy|symlink>
```
### 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 <path/to/plugins>
# No longer watch a directory, this will ask you if you want to remove any leftover yabridge files
yabridgectl rm <path/to/plugins>
# 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
```
+200
View File
@@ -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 <https://www.gnu.org/licenses/>.
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<PathBuf>,
/// 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<PathBuf>,
}
/// 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<Config, String> {
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<PathBuf, String> {
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=<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<BTreeMap<&Path, SearchResults>, 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, String> {
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, String> {
BaseDirectories::with_prefix(YABRIDGECTL_PREFIX)
.map_err(|err| format!("Error while parsing base directories: {}", err))
}
+162
View File
@@ -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 <https://www.gnu.org/licenses/>.
//! 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<PathBuf>,
/// `.dll` files skipped over during the serach. Used for printing statistics and shown when
/// running `yabridgectl sync --verbose`.
pub skipped_files: Vec<PathBuf>,
/// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or
/// a regular file.
pub so_files: Vec<FoundFile>,
}
#[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<SearchResults, std::io::Error> {
// First we'll find all .dll and .so files in the directory
let mut dll_files: Vec<PathBuf> = Vec::new();
let mut so_files: Vec<FoundFile> = 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::<Result<_, std::io::Error>>()?;
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,
})
}
+364
View File
@@ -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 <https://www.gnu.org/licenses/>.
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 <path>`
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::<PathBuf>("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("<auto>"))
);
println!(
"libyabridge.so: {}",
config
.libyabridge()
.map(|path| format!("'{}'", path.display()))
.unwrap_or_else(|_| format!("{}", "<not found>".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<PathBuf> = Vec::new();
let mut orphan_so_files: Vec<FoundFile> = 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()
))
}
}