diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..0f0e2a3
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --frozen --package xtask --"
diff --git a/Cargo.lock b/Cargo.lock
index 5847d61..0ee1875 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
[[package]]
name = "argh"
version = "0.1.13"
@@ -248,6 +260,15 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "blocking"
version = "1.6.1"
@@ -319,12 +340,40 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
[[package]]
name = "deranged"
version = "0.4.0"
@@ -334,6 +383,16 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -433,6 +492,29 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "libz-rs-sys",
+ "miniz_oxide",
+]
+
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -497,6 +579,16 @@ dependencies = [
"slab",
]
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
[[package]]
name = "getrandom"
version = "0.3.3"
@@ -736,6 +828,17 @@ dependencies = [
"windows-targets 0.53.0",
]
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags",
+ "libc",
+ "redox_syscall",
+]
+
[[package]]
name = "libssh2-sys"
version = "0.3.1"
@@ -750,6 +853,15 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "libz-rs-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
+dependencies = [
+ "zlib-rs",
+]
+
[[package]]
name = "libz-sys"
version = "1.1.22"
@@ -804,6 +916,15 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
[[package]]
name = "mpdris"
version = "1.2.0"
@@ -992,6 +1113,15 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "rust-fuzzy-search"
version = "0.1.1"
@@ -1070,6 +1200,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1176,6 +1317,17 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "tar"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
[[package]]
name = "tempfile"
version = "3.20.0"
@@ -1313,6 +1465,12 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
[[package]]
name = "uds_windows"
version = "1.1.0"
@@ -1365,6 +1523,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
@@ -1722,6 +1886,28 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+[[package]]
+name = "xattr"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
+dependencies = [
+ "libc",
+ "rustix 1.0.7",
+]
+
+[[package]]
+name = "xtask"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "argh",
+ "flate2",
+ "hex",
+ "sha2",
+ "tar",
+]
+
[[package]]
name = "yoke"
version = "0.8.0"
@@ -1860,6 +2046,12 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "zlib-rs"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
+
[[package]]
name = "zvariant"
version = "5.5.3"
diff --git a/Cargo.toml b/Cargo.toml
index af3c8a1..35e00f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,9 @@ authors = [ "jasger9000 | jasger_" ]
license = "MIT"
repository = "https://github.com/jasger9000/mpdris"
+[workspace]
+members = [ "xtask" ]
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
diff --git a/README.md b/README.md
index 1da2a4d..a9935f6 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ __Table of Contents:__
To install this application, you can either...
- [Use the AUR package (Arch Linux only)](#use-the-aur-package)
- [Build the application yourself](#build-the-application-yourself)
-- [Install the application from a release binary](#install-using-release-binary)
+- [Install the application from a release binary](#install-using-release-binarytarball)
### Use the AUR package
> [!IMPORTANT]
@@ -80,23 +80,24 @@ You can either build the AUR-package yourself, as detailed below, or use your fa
```bash
cargo build --release
```
-3. Copy the resulting file from `target/release/mpdris` to `/usr/local/bin`
-4. Copy `resources/mpdris.service` to `/usr/local/lib/systemd/user` (You might have to create that directory first)
+3. Move the resulting file from `target/release/mpdris` to `/usr/local/bin`
+4. Copy `resources/mpdris.service.local` to `/usr/local/lib/systemd/user` (You might have to create that directory first) and rename it to `mpdris.service`
5. Enable the service to start it with MPD
```bash
systemctl --user enable mpdris.service
```
-### Install using release binary
-2. Download the correct binary for your architecture
+### Install using release binary/tarball
1. Go to the [release tab](https://github.com/jasger9000/mpdris/releases)
+2. Download the correct tarball or binary for your architecture
- If you don't know what your architecture is, you can find out by running `lscpu`
-3. Copy the file to `/usr/local/bin` and rename it to `mpdris`
+3. Move the binary to `/usr/local/bin` and rename it to `mpdris` if needed
4. Add the execute permission to the file with
```bash
chmod +x /usr/local/bin/mpdris
```
-5. Download and move [mpdris.service](https://github.com/jasger9000/mpdris/blob/main/resources/mpdris.service) to `/usr/local/lib/systemd/user` (You might have to create that directory first)
+5. Move `mpdris.service.local` to `/usr/local/lib/systemd/user` (You might have to create that directory first) and rename it to `mpdris.service`
+ - mpdris.service.local can be found in the release tarball or downloaded [here](https://github.com/jasger9000/mpdris/blob/main/resources/mpdris.service.local)
6. Enable the service to start it with MPD
```bash
systemctl --user enable mpdris.service
@@ -156,6 +157,13 @@ Contributions are always welcome!
If you feel there's something missing/wrong/something that could be improved please open an [issue](https://github.com/jasger9000/mpdris/issues).
Or if you want to add something yourself, just [open a pull request](https://github.com/jasger9000/mpdris/pulls) and I will have a look at it as soon as I can.
+## Packaging
+If you want to create a package of this application yourself, you can use the xtask cargo subcommand.
+Simply use `cargo xtask build [] [--arch ]` to build the project.
+To create an install from that build, use `cargo xtask install [--arch ]`. Note that you have to run the build command first though.
+You can also just create the manpage structure using `cargo xtask man `.
+To create release assets (tarballs, binaries, SHA256sums) for x86_64, i686 and aarch64 with `cargo xtask make-release-assets`.
+
## Licence
The Project is Licensed under the [MIT Licence](https://github.com/jasger9000/mpdris/?tab=MIT-1-ov-file)
diff --git a/resources/mpdris.service b/resources/mpdris.service
index 9f51b81..99b147d 100644
--- a/resources/mpdris.service
+++ b/resources/mpdris.service
@@ -7,7 +7,7 @@ After=mpd.service
Type=notify-reload
NotifyAccess=main
Restart=on-failure
-ExecStart=/usr/local/bin/mpdris --service
+ExecStart=/usr/bin/mpdris --service
[Install]
WantedBy=mpd.service
diff --git a/resources/mpdris.service.local b/resources/mpdris.service.local
new file mode 100644
index 0000000..9f51b81
--- /dev/null
+++ b/resources/mpdris.service.local
@@ -0,0 +1,13 @@
+[Unit]
+Description=Music Player Daemon MPRIS bridge
+BindsTo=mpd.service
+After=mpd.service
+
+[Service]
+Type=notify-reload
+NotifyAccess=main
+Restart=on-failure
+ExecStart=/usr/local/bin/mpdris --service
+
+[Install]
+WantedBy=mpd.service
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
new file mode 100644
index 0000000..099a237
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "xtask"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.98"
+argh = "0.1.13"
+tar = "0.4.44"
+sha2 = "0.10.9"
+flate2 = { version = "1.1.2", default-features = false, features = ["zlib-rs"] }
+hex = "0.4.3"
+
diff --git a/xtask/src/args.rs b/xtask/src/args.rs
new file mode 100644
index 0000000..e8195f9
--- /dev/null
+++ b/xtask/src/args.rs
@@ -0,0 +1,68 @@
+use std::{env, path::PathBuf};
+
+use argh::FromArgs;
+
+/// XTasks
+#[derive(FromArgs)]
+#[argh(help_triggers("-h", "--help"))]
+pub(crate) struct Args {
+ #[argh(subcommand)]
+ /// the task to execute
+ pub(crate) task: Task,
+}
+
+#[derive(FromArgs)]
+#[argh(subcommand)]
+pub(crate) enum Task {
+ Man(ManTask),
+ Build(BuildTask),
+ Install(InstallTask),
+ CleanDist(CleanTask),
+ MakeRelease(ReleaseTask),
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Write & compress manpages to
+#[argh(subcommand, name = "man", help_triggers("-h", "--help"))]
+pub(crate) struct ManTask {
+ #[argh(positional)]
+ /// the directory to which the compressed manpages should be written to
+ pub(crate) dir: PathBuf,
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Compile/Build all project assets for the default arch or the one provided.
+/// Result is written to target/dist/ or if provided.
+#[argh(subcommand, name = "build", help_triggers("-h", "--help"))]
+pub(crate) struct BuildTask {
+ #[argh(option, default = "env::consts::ARCH.to_string()")]
+ /// the arch to compile for
+ pub(crate) arch: String,
+ #[argh(positional)]
+ /// path to install the files to instead of target/dist/
+ pub(crate) path: Option,
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Create an install using the default arch or if provided.
+/// Result is written to target/dist/ or if provided.
+/// Note: install does NOT compile anything, for that please use build
+#[argh(subcommand, name = "install", help_triggers("-h", "--help"))]
+pub(crate) struct InstallTask {
+ #[argh(option, default = "env::consts::ARCH.to_string()")]
+ /// the arch to compile for
+ pub(crate) arch: String,
+ #[argh(positional)]
+ /// path to install the files to instead of target/dist/
+ pub(crate) path: Option,
+}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Clean the target/dist directory
+#[argh(subcommand, name = "clean-dist", help_triggers("-h", "--help"))]
+pub(crate) struct CleanTask {}
+
+#[derive(FromArgs, PartialEq, Debug)]
+/// Create release assets (tarballs, binaries and SHA256 checksums) for x86_64, aarch64, i68
+#[argh(subcommand, name = "make-release-assets", help_triggers("-h", "--help"))]
+pub(crate) struct ReleaseTask {}
diff --git a/xtask/src/dist.rs b/xtask/src/dist.rs
new file mode 100644
index 0000000..219748b
--- /dev/null
+++ b/xtask/src/dist.rs
@@ -0,0 +1,190 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::{env, fs::Permissions, io::Write, os::unix::fs::PermissionsExt, process::Command, sync::Arc};
+
+use crate::{DIST_DIR, NAME, PROJECT_ROOT, TARGET_DIR, Task, build_man};
+use anyhow::{Context, Result, anyhow};
+use flate2::{Compression, write::GzEncoder};
+use sha2::{Digest, Sha256};
+
+macro_rules! cp {
+ ($outdir:expr, $src:literal, $dst:literal, $perm:expr) => {
+ cp!(&$crate::PROJECT_ROOT, $outdir, $src, $dst, $perm)
+ };
+ ($indir:expr, $outdir:expr, $src:literal, $dst:literal, $perm:expr) => {
+ $crate::dist::copy($indir, $outdir, &::std::format!($src), &::std::format!($dst), $perm)
+ };
+ ($indir:expr, $outdir:expr, $src:literal, $dst:literal) => {
+ $crate::dist::copy_dir_all($indir.join(::std::format!($src)), $outdir.join(::std::format!($dst)))
+ };
+}
+
+fn copy>(indir: &Path, outdir: &Path, src: P, dst: P, perm: u32) -> Result<()> {
+ let (src, dst) = (indir.join(src), outdir.join(dst));
+ fs::copy(&src, &dst).with_context(|| format!("Failed to copy {}", dst.file_name().unwrap().display()))?;
+ fs::set_permissions(&dst, Permissions::from_mode(perm))?;
+ Ok(())
+}
+
+fn copy_dir_all, Q: AsRef>(src: P, dst: Q) -> Result<()> {
+ fs::create_dir_all(&dst)?;
+ for entry in fs::read_dir(src)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_dir() {
+ copy_dir_all(path, dst.as_ref().join(entry.file_name()))?;
+ } else {
+ fs::copy(path, dst.as_ref().join(entry.file_name()))?;
+ }
+ }
+ Ok(())
+}
+
+pub(crate) fn clean_dist() -> Result<()> {
+ let t = Task::new("Cleaning dist");
+
+ if DIST_DIR.exists() {
+ fs::remove_dir_all(&*DIST_DIR).with_context(|| "Failed to delete the dist directory")?;
+ }
+
+ t.success();
+ Ok(())
+}
+
+pub(crate) fn build_binary(arch: &str) -> Result<()> {
+ let t = Arc::new(Task::new(&format!("Compiling binary for {arch}")));
+ let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
+ println!();
+ let status = Command::new(cargo)
+ .current_dir(&*PROJECT_ROOT)
+ .env("CARGO_TARGET_DIR", &*TARGET_DIR)
+ .args([
+ "build",
+ "--frozen",
+ "--release",
+ &format!("--target={arch}-unknown-linux-gnu"),
+ ])
+ .status()
+ .with_context(|| "Failed to execute build command")?;
+ t.fix_text();
+
+ if !status.success() {
+ t.failure();
+ return Err(anyhow!("Failed to compile binary"));
+ }
+ t.success();
+
+ let t = Task::new("Copying binary to dist");
+ fs::create_dir_all(&*DIST_DIR).with_context(|| "Failed to create dist directory")?;
+ #[rustfmt::skip]
+ cp!(TARGET_DIR, DIST_DIR, "{arch}-unknown-linux-gnu/release/{NAME}", "{NAME}_{arch}-linux-gnu")?;
+ t.success();
+
+ Ok(())
+}
+
+pub(crate) fn build(path: Option, arch: &str) -> Result<()> {
+ let outdir = path.unwrap_or(DIST_DIR.to_path_buf());
+
+ build_binary(arch)?;
+ build_man(&outdir.join("man"))?;
+
+ Ok(())
+}
+
+pub(crate) fn install(path: Option, arch: &str) -> Result<()> {
+ let outdir = path.unwrap_or(DIST_DIR.join(arch));
+
+ install_create_dirs(&outdir).with_context(|| "Failed to create dist directory structure")?;
+ install_copy_files(&outdir, arch).with_context(|| "Failed to copy assets to install dir")?;
+
+ Ok(())
+}
+
+fn install_create_dirs(outdir: &Path) -> Result<()> {
+ let t = Task::new("Creating directory structure");
+ fs::create_dir_all(outdir)?;
+ fs::create_dir_all(outdir.join("usr/bin"))?;
+ fs::create_dir_all(outdir.join("usr/lib/systemd/user"))?;
+ fs::create_dir_all(outdir.join(format!("usr/share/doc/{NAME}")))?;
+ fs::create_dir_all(outdir.join(format!("usr/share/licenses/{NAME}")))?;
+ fs::create_dir_all(outdir.join("usr/share/man"))?;
+
+ t.success();
+ Ok(())
+}
+
+#[rustfmt::skip]
+fn install_copy_files(outdir: &Path, arch: &str) -> Result<()> {
+ let t = Task::new("Copying files to dist");
+ cp!(&DIST_DIR, outdir, "{NAME}_{arch}-linux-gnu", "usr/bin/{NAME}", 0o755)?;
+ cp!(outdir, "resources/mpdris.service", "usr/lib/systemd/user/mpdris.service", 0o644)?;
+ cp!(outdir, "resources/sample.mpdris.conf", "usr/share/doc/{NAME}/sample.mpdris.conf", 0o644)?;
+ cp!(outdir, "README.md", "usr/share/doc/{NAME}/README.md", 0o644)?;
+ cp!(outdir, "LICENSE", "usr/share/licenses/{NAME}/LICENSE", 0o644)?;
+ cp!(DIST_DIR, outdir, "man", "usr/share/man")?;
+
+ t.success();
+ Ok(())
+}
+
+pub(crate) fn make_release_assets() -> Result<()> {
+ let archs = ["x86_64", "i686", "aarch64"];
+ let mandir = DIST_DIR.join("man");
+ let mut checksums = (Vec::new(), Vec::new());
+
+ if !DIST_DIR.is_dir() {
+ fs::create_dir_all(&*DIST_DIR).with_context(|| "Failed to create dist directory")?;
+ }
+ build_man(&mandir)?;
+
+ for arch in archs {
+ println!("Making release for {arch}");
+
+ build_binary(arch)?;
+ let tarball_filename = format!("{NAME}_{arch}.tar.gz");
+ let binary_filename = format!("{NAME}_{arch}-linux-gnu");
+ let binary_outpath = PROJECT_ROOT.join(DIST_DIR.join(&binary_filename));
+
+ let t = Task::new("Making tar archive");
+ let mut builder = tar::Builder::new(Vec::new());
+ builder.mode(tar::HeaderMode::Deterministic);
+ builder.append_path_with_name(&binary_outpath, NAME)?;
+ builder.append_path_with_name(PROJECT_ROOT.join("resources/mpdris.service"), "mpdris.service")?;
+ builder.append_path_with_name(PROJECT_ROOT.join("resources/mpdris.service.local"), "mpdris.service.local")?;
+ builder.append_path_with_name(PROJECT_ROOT.join("resources/sample.mpdris.conf"), "sample.mpdris.conf")?;
+ builder.append_path_with_name(PROJECT_ROOT.join("README.md"), "README.md")?;
+ builder.append_path_with_name(PROJECT_ROOT.join("LICENSE"), "LICENSE")?;
+ builder.append_dir_all("man", &mandir)?;
+
+ let archive = builder.into_inner()?;
+ t.success();
+
+ let t = Task::new("Compressing archive");
+ let mut encoder = GzEncoder::new(Vec::new(), Compression::new(9));
+ encoder.write_all(&archive)?;
+
+ let compressed = encoder.finish()?;
+ drop(archive);
+ t.success();
+
+ let t = Task::new("Calculating checksums");
+ let binary_hash = hex::encode(Sha256::digest(fs::read(&binary_outpath)?));
+ let archive_hash = hex::encode(Sha256::digest(&compressed));
+ checksums.0.push(format!("{binary_hash} {binary_filename}"));
+ checksums.1.push(format!("{archive_hash} {tarball_filename}"));
+ t.success();
+
+ let t = Task::new("Writing tarball");
+ fs::write(DIST_DIR.join(tarball_filename), compressed).with_context(|| "failed to write compressed archive")?;
+ t.success();
+ println!();
+ }
+
+ let t = Task::new("Writing checksum file");
+ checksums.0.append(&mut checksums.1);
+ fs::write(DIST_DIR.join("SHA256sums.txt"), checksums.0.join("\n").as_bytes())?;
+ t.success();
+
+ Ok(())
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 0000000..3b88e58
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,47 @@
+use std::path::{Path, PathBuf};
+use std::{env, process::exit, sync::LazyLock};
+
+use anyhow::Result;
+use dist::{build, clean_dist, install, make_release_assets};
+
+pub(crate) use man::build_man;
+pub(crate) use task::Task;
+
+mod args;
+mod dist;
+mod man;
+mod task;
+
+static PROJECT_ROOT: LazyLock = LazyLock::new(|| {
+ Path::new(&env!("CARGO_MANIFEST_DIR"))
+ .ancestors()
+ .nth(1)
+ .unwrap()
+ .to_path_buf()
+});
+static DIST_DIR: LazyLock = LazyLock::new(|| PROJECT_ROOT.join("target/dist"));
+static TARGET_DIR: LazyLock = LazyLock::new(|| PROJECT_ROOT.join("target"));
+
+const NAME: &str = "mpdris";
+const MANPATH: &str = "resources/man";
+
+fn main() {
+ if let Err(e) = try_main() {
+ eprintln!("{e:?}");
+ exit(-1);
+ }
+}
+
+fn try_main() -> Result<()> {
+ use args::Task::*;
+
+ let args: args::Args = argh::from_env();
+
+ match args.task {
+ Man(task) => build_man(&task.dir),
+ Build(task) => build(task.path, &task.arch),
+ Install(task) => install(task.path, &task.arch),
+ CleanDist(..) => clean_dist(),
+ MakeRelease(..) => make_release_assets(),
+ }
+}
diff --git a/xtask/src/man.rs b/xtask/src/man.rs
new file mode 100644
index 0000000..2cfa3a9
--- /dev/null
+++ b/xtask/src/man.rs
@@ -0,0 +1,67 @@
+use std::fs::{self, File, Permissions, create_dir_all};
+use std::io::{BufWriter, Write};
+use std::os::unix::fs::PermissionsExt;
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+use flate2::{Compression, write::GzEncoder};
+
+use crate::{MANPATH, PROJECT_ROOT, Task};
+
+pub(crate) fn build_man(outdir: &Path) -> Result<()> {
+ let indir = PROJECT_ROOT.join(MANPATH);
+ let skip = indir.components().count();
+
+ if outdir.exists() {
+ let t = Task::new("Removing old manpage output directory");
+ fs::remove_dir_all(outdir).with_context(|| "Failed to delete manpage output directory")?;
+ fs::create_dir_all(outdir).with_context(|| "Failed to create manpage output directory")?;
+ t.success();
+ }
+
+ let t = Task::new("Building man pages");
+ for inpath in search_files_recursive(&indir)? {
+ let mut outpath: PathBuf = outdir.components().chain(inpath.components().skip(skip)).collect();
+ outpath.as_mut_os_string().push(".gz");
+
+ create_dir_all(outpath.parent().with_context(|| "Failed to get manpage output directory")?)
+ .with_context(|| "Failed to create manpage output directory")?;
+
+ let infile = fs::read(&inpath).with_context(|| "Failed to read manpage infile")?;
+ let writer = BufWriter::new(File::create(&outpath).with_context(|| "Failed to open manpage outfile")?);
+
+ let mut encoder = GzEncoder::new(writer, Compression::new(9));
+ encoder.write_all(&infile)?;
+ encoder.try_finish()?;
+
+ fs::set_permissions(&outpath, Permissions::from_mode(0o644)).with_context(|| "Failed to set manpage permissions")?;
+ }
+
+ t.success();
+ Ok(())
+}
+
+/// Finds all files present in a given start directory,
+fn search_files_recursive(start: &Path) -> Result> {
+ fn inner(start: &Path, result: &mut Vec) -> Result<()> {
+ if start.is_dir() {
+ for entry in start.read_dir().with_context(|| "failed to read dir")? {
+ let entry = entry.with_context(|| "failed to get entry")?;
+ let path = entry.path();
+
+ if path.is_dir() {
+ inner(&path, result)?;
+ } else {
+ result.push(path);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ let mut result: Vec = Vec::new();
+ inner(start, &mut result)?;
+
+ Ok(result)
+}
diff --git a/xtask/src/task.rs b/xtask/src/task.rs
new file mode 100644
index 0000000..f43ab40
--- /dev/null
+++ b/xtask/src/task.rs
@@ -0,0 +1,45 @@
+use std::io::{Write, stdout};
+use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
+
+#[derive(Debug)]
+pub(crate) struct Task {
+ running: AtomicBool,
+ text: Box,
+}
+
+impl Task {
+ pub(crate) fn new(text: &str) -> Self {
+ // hide cursor and print message
+ let mut stdout = stdout().lock();
+ stdout.write_all(text.as_bytes()).unwrap();
+ stdout.flush().unwrap();
+ Self {
+ running: AtomicBool::new(true),
+ text: text.into(),
+ }
+ }
+
+ pub(crate) fn success(&self) {
+ if self.running.load(Relaxed) {
+ println!(" - Done");
+ self.running.store(false, Relaxed);
+ }
+ }
+
+ pub(crate) fn failure(&self) {
+ if self.running.load(Relaxed) {
+ println!(" - Failed");
+ self.running.store(false, Relaxed);
+ }
+ }
+
+ pub(crate) fn fix_text(&self) {
+ print!("{}", self.text);
+ }
+}
+
+impl Drop for Task {
+ fn drop(&mut self) {
+ self.failure();
+ }
+}