diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 17:34:01 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-23 17:42:49 +0300 |
| commit | ec2924d0d1f21417802c3efd0ed46b9db2c00582 (patch) | |
| tree | 00432d4dbb46bb82328dfb583e36293c1603bc81 | |
| parent | 0d2c20a46235ae2535d4fdef59b9529e44faa98f (diff) | |
feat: add optional nix flake support
refs #208
refs #221
refs #264
| -rw-r--r-- | .github/workflows/nix.yml | 34 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | build.rs | 16 | ||||
| -rw-r--r-- | docs/next/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | docs/next/README.md | 14 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/install.mdx | 29 | ||||
| -rw-r--r-- | flake.lock | 27 | ||||
| -rw-r--r-- | flake.nix | 82 | ||||
| -rw-r--r-- | nix/package.nix | 76 | ||||
| -rw-r--r-- | src/update.rs | 51 |
10 files changed, 329 insertions, 3 deletions
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 00000000..c8e8e6ff --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,34 @@ +name: Nix + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [master] + paths-ignore: + - "website/**" + +permissions: + contents: read + +concurrency: + group: nix-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + flake-check: + name: flake check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run Nix flake check + run: | + docker run --rm \ + -v "$PWD:/work" \ + -w /work \ + docker.io/nixos/nix:2.31.2 \ + sh -c 'nix --extra-experimental-features "nix-command flakes" flake check --print-build-logs && nix --extra-experimental-features "nix-command flakes" flake check --all-systems --no-build --print-build-logs' @@ -15,3 +15,5 @@ __pycache__/ /.pi/todos /.pi/issues /.pi/cache +/result +/result-* @@ -32,9 +32,14 @@ fn main() { println!("cargo:rerun-if-changed=vendor/libghostty-vt.vendor.json"); println!("cargo:rerun-if-changed=vendor/libghostty-vt/build.zig"); println!("cargo:rerun-if-changed=vendor/libghostty-vt/build.zig.zon"); + println!("cargo:rerun-if-changed=vendor/libghostty-vt/include"); + println!("cargo:rerun-if-changed=vendor/libghostty-vt/pkg"); + println!("cargo:rerun-if-changed=vendor/libghostty-vt/src"); println!("cargo:rerun-if-changed=vendor/libghostty-vt/VERSION"); println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_OPTIMIZE"); println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_SIMD"); + println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_ZIG_SYSTEM_DIR"); + println!("cargo:rerun-if-env-changed=ZIG"); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); let vendored_dir = manifest_dir.join("vendor/libghostty-vt"); @@ -47,13 +52,20 @@ fn main() { .trim() .to_string(); - let status = Command::new("zig") + let zig = env::var("ZIG").unwrap_or_else(|_| "zig".into()); + let mut command = Command::new(zig); + command .arg("build") .arg("-Demit-lib-vt") .arg(format!("-Doptimize={optimize}")) .arg(format!("-Dsimd={simd}")) .arg(format!("-Dtarget={zig_target}")) - .arg(format!("-Dversion-string={version_string}")) + .arg(format!("-Dversion-string={version_string}")); + if let Ok(system_dir) = env::var("LIBGHOSTTY_VT_ZIG_SYSTEM_DIR") { + command.arg("--system").arg(system_dir); + } + + let status = command .current_dir(&vendored_dir) .status() .expect("failed to execute zig build for vendored libghostty-vt"); diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index f4a8a6de..de3ada07 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- Added optional Nix flake support for building, running, installing, and developing Herdr with Nix. (#208, #221, #264) - Added `terminal.new_cwd` to choose whether new panes, tabs, and workspaces follow the source pane/workspace, start in `$HOME`, use Herdr's process directory, or use a fixed path. - Added `herdr integration install omp` for OMP's `.omp` extension directory. The extension reports OMP pane state through Herdr's socket API without relying on native `omp` process detection. - Added CLI and socket API support for Git worktrees with `herdr worktree list/create/open/remove`, optional worktree provenance on workspace responses, and client/server protocol version 10. diff --git a/docs/next/README.md b/docs/next/README.md index 16a9fd1c..99e99e6c 100644 --- a/docs/next/README.md +++ b/docs/next/README.md @@ -295,6 +295,20 @@ cargo build --release ./target/release/herdr ``` +## nix + +Herdr provides optional Nix flake outputs for users who already use Nix. The flake builds Herdr from source. + +```bash +nix run github:ogulcancelik/herdr +nix build github:ogulcancelik/herdr +nix develop github:ogulcancelik/herdr +``` + +The flake exposes `packages.<system>.default`, `apps.<system>.default`, `devShells.<system>.default`, and `overlays.default`. + +Update through the same Nix workflow you used to install Herdr. For profile installs, run `nix profile list` and then `nix profile upgrade <index-or-name>`. For flake inputs, run `nix flake update herdr` in your own flake and rebuild. + ## testing ```bash diff --git a/docs/next/website/src/content/docs/install.mdx b/docs/next/website/src/content/docs/install.mdx index dda2dc27..69834567 100644 --- a/docs/next/website/src/content/docs/install.mdx +++ b/docs/next/website/src/content/docs/install.mdx @@ -33,6 +33,35 @@ Herdr checks for new releases and notifies you in the app. You can update manual herdr update ``` +## Install with Nix + +If you already use Nix, Herdr provides a flake that builds Herdr from source: + +```bash +nix run github:ogulcancelik/herdr +nix build github:ogulcancelik/herdr +nix profile install github:ogulcancelik/herdr +``` + +The flake also exposes a development shell: + +```bash +nix develop github:ogulcancelik/herdr +``` + +Update through the same Nix workflow you used to install Herdr. For a profile install, list your profile entries and upgrade the Herdr entry: + +```bash +nix profile list +nix profile upgrade <index-or-name> +``` + +If Herdr is an input in your own flake, update that input and rebuild your system, Home Manager, or development environment: + +```bash +nix flake update herdr +``` + ## Download manually You can also download a binary from [GitHub releases](https://github.com/ogulcancelik/herdr/releases). diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..421a64fc --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1779357205, + "narHash": "sha256-cCO8aTqss5x9Ky8GWkpY0Hy5fyTZEbtifSUV8QjSzic=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f83fc3c307e74bc5fd5adb7eb6b8b13ffd2a36e1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..3a91a3ca --- /dev/null +++ b/flake.nix @@ -0,0 +1,82 @@ +{ + description = "herdr — terminal workspace manager for AI coding agents"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + lib = nixpkgs.lib; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = lib.genAttrs systems; + pkgsFor = system: import nixpkgs { inherit system; }; + in + { + packages = forAllSystems ( + system: + let + pkgs = pkgsFor system; + herdr = pkgs.callPackage ./nix/package.nix { }; + in + { + inherit herdr; + default = herdr; + } + ); + + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/herdr"; + meta.description = "Run Herdr"; + }; + }); + + checks = forAllSystems (system: { + herdr = self.packages.${system}.default; + default = self.checks.${system}.herdr; + }); + + devShells = forAllSystems ( + system: + let + pkgs = pkgsFor system; + in + { + default = pkgs.mkShell { + name = "herdr-dev"; + packages = with pkgs; [ + cargo + cargo-nextest + clippy + cmake + just + ninja + pkg-config + rustc + rustfmt + zig_0_15 + ]; + + env = { + LIBGHOSTTY_VT_OPTIMIZE = "Debug"; + LIBGHOSTTY_VT_SIMD = "true"; + }; + }; + } + ); + + formatter = forAllSystems (system: (pkgsFor system).nixfmt); + + overlays.default = final: _prev: { + herdr = final.callPackage ./nix/package.nix { }; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000..ba9206d8 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,76 @@ +{ + lib, + rustPlatform, + callPackage, + runCommand, + zig_0_15, + zstd, + pkg-config, + git, +}: + +let + manifest = lib.importTOML ../Cargo.toml; + zigDeps = callPackage ../vendor/libghostty-vt/build.zig.zon.nix { + name = "herdr-libghostty-vt-zig-cache"; + inherit zstd; + linkFarm = name: entries: + runCommand name { } '' + mkdir -p $out + ${lib.concatMapStringsSep "\n" (entry: '' + cp -rL ${entry.path} $out/${entry.name} + '') entries} + ''; + }; +in +rustPlatform.buildRustPackage { + pname = "herdr"; + version = manifest.package.version; + + src = lib.fileset.toSource { + root = ./..; + fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ./..)) ( + lib.fileset.unions [ + ../assets + ../src + ../vendor/libghostty-vt + ../vendor/libghostty-vt.vendor.json + ../build.rs + ../Cargo.lock + ../Cargo.toml + ] + ); + }; + + cargoLock.lockFile = ../Cargo.lock; + + nativeBuildInputs = [ + git + pkg-config + ]; + + env = { + LIBGHOSTTY_VT_OPTIMIZE = "ReleaseFast"; + LIBGHOSTTY_VT_SIMD = "true"; + LIBGHOSTTY_VT_ZIG_SYSTEM_DIR = zigDeps; + ZIG = lib.getExe zig_0_15; + }; + + preBuild = '' + export ZIG_GLOBAL_CACHE_DIR="$TMPDIR/zig-global-cache" + export ZIG_LOCAL_CACHE_DIR="$TMPDIR/zig-local-cache" + ''; + + # Rust tests are covered by the normal CI workflow. The Nix check is + # intentionally build-only so it validates packaging inputs without + # duplicating the full Rust test suite. + doCheck = false; + + meta = { + description = "Terminal workspace manager for AI coding agents"; + homepage = "https://herdr.dev"; + license = lib.licenses.agpl3Plus; + mainProgram = "herdr"; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; +} diff --git a/src/update.rs b/src/update.rs index f37a9265..2a2776fa 100644 --- a/src/update.rs +++ b/src/update.rs @@ -21,6 +21,7 @@ const UPDATE_MANIFEST_URL: &str = "https://herdr.dev/latest.json"; const HOMEBREW_FORMULA_API_URL: &str = "https://formulae.brew.sh/api/formula/herdr.json"; const HERDR_UPDATE_COMMAND: &str = "herdr update"; const HOMEBREW_UPDATE_COMMAND: &str = "brew update && brew upgrade herdr"; +const NIX_UPDATE_COMMAND: &str = "update through Nix"; const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); const FAKE_UPDATE_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_VERSION"; const FAKE_UPDATE_NOTES_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_NOTES_VERSION"; @@ -678,6 +679,8 @@ fn wait_for_server_shutdown(timeout: Duration) -> Result<(), String> { pub(crate) fn update_install_command() -> &'static str { if is_homebrew_managed_install() { HOMEBREW_UPDATE_COMMAND + } else if is_nix_managed_install() { + NIX_UPDATE_COMMAND } else { HERDR_UPDATE_COMMAND } @@ -697,6 +700,24 @@ fn is_homebrew_managed_install() -> bool { .is_ok_and(|path| is_homebrew_managed_exe_path(&path)) } +fn is_nix_managed_install() -> bool { + let Ok(current_exe) = env::current_exe() else { + return false; + }; + + if is_nix_store_exe_path(¤t_exe) { + return true; + } + + current_exe + .canonicalize() + .is_ok_and(|path| is_nix_store_exe_path(&path)) +} + +fn is_nix_store_exe_path(path: &Path) -> bool { + path.starts_with("/nix/store") +} + fn is_homebrew_managed_exe_path(path: &Path) -> bool { homebrew_cellar_keg_root(path).is_some() } @@ -991,6 +1012,12 @@ pub fn self_update() -> Result<Version, String> { )); } + if is_nix_managed_install() { + return Err( + "self-update is disabled for Nix installs; update with `nix profile upgrade` or update the flake input that provides Herdr".into(), + ); + } + if running_inside_herdr() { return Err("run `herdr update` outside herdr after detaching from the session".into()); } @@ -1075,6 +1102,8 @@ pub fn auto_update(events: tokio::sync::mpsc::Sender<crate::events::AppEvent>) { return; } + let nix_managed_install = is_nix_managed_install(); + let release = match check_latest() { Ok(Some(r)) => r, Ok(None) => return, @@ -1103,9 +1132,15 @@ pub fn auto_update(events: tokio::sync::mpsc::Sender<crate::events::AppEvent>) { ); // Notify the TUI — blocking_send is safe from a std::thread + let install_command = if nix_managed_install { + NIX_UPDATE_COMMAND + } else { + HERDR_UPDATE_COMMAND + }; + let _ = events.blocking_send(crate::events::AppEvent::UpdateReady { version: release.version.to_string(), - install_command: HERDR_UPDATE_COMMAND.to_string(), + install_command: install_command.to_string(), }); } @@ -1288,6 +1323,20 @@ mod tests { } #[test] + fn nix_store_path_is_detected() { + let path = Path::new("/nix/store/abc123-herdr-0.6.1/bin/herdr"); + + assert!(is_nix_store_exe_path(path)); + } + + #[test] + fn non_nix_store_path_is_not_detected() { + let path = Path::new("/usr/local/bin/herdr"); + + assert!(!is_nix_store_exe_path(path)); + } + + #[test] fn parse_homebrew_formula_stable_version_reads_versions_stable() { let version = parse_homebrew_formula_stable_version( br#"{"versions":{"stable":"0.5.10","head":"HEAD","bottle":true}}"#, |
