diff --git a/default.nix b/default.nix new file mode 100644 index 0000000000..263321cfe4 --- /dev/null +++ b/default.nix @@ -0,0 +1,177 @@ + +let + # Pin Rust 1.93.0 via rust-overlay + rustToolchain' = { pkgs, ... }: + pkgs.rust-bin.stable."1.93.0".default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "llvm-tools-preview" + ]; + } + ; + + # Create a custom rustPlatform using the pinned toolchain + rustPlatform' = { pkgs, git-ai, ... }: with git-ai.utils; + pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + + # Build the git-ai binary using the pinned Rust toolchain + git-ai-unwrapped' = {pkgs, git-ai, ...}: with git-ai.utils; + rustPlatform.buildRustPackage { + pname = "git-ai"; + version = "1.4.9"; + + src = ./.; + + cargoLock = { + lockFile = ./Cargo.lock; + }; + + # Prevent openssl-sys from vendoring OpenSSL (which requires perl). + # Instead, link against the system OpenSSL provided by buildInputs. + OPENSSL_NO_VENDOR = "1"; + + # Native build inputs needed for rusqlite with bundled SQLite + nativeBuildInputs = with pkgs; [ + pkg-config + ] ++ [ + rustPlatform.bindgenHook # For rusqlite bundled builds + ]; + + # Build inputs for runtime dependencies + buildInputs = with pkgs; [ + # rusqlite bundled mode compiles its own SQLite, but needs these headers + sqlite + # openssl-sys needs system OpenSSL headers and libraries + openssl + ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ + # macOS-specific dependencies + libiconv + apple-sdk_15 + ]; + + # Tests require git and specific setup + doCheck = false; + + meta = with pkgs.lib; { + description = "AI-powered Git wrapper that tracks AI-generated code changes"; + homepage = "https://github.com/acunniffe/git-ai"; + license = licenses.gpl3Plus; + maintainers = [ ]; + mainProgram = "git-ai"; + platforms = platforms.unix; + }; + }; + + # Wrapped version that sets up the git-ai environment properly + wrapped' = { pkgs, git, git-ai, ... }: with git-ai.packages; + pkgs.writeShellScriptBin "git-ai" '' + # Ensure config directory exists + mkdir -p "$HOME/.git-ai" + + # Create config.json if it doesn't exist + if [ ! -f "$HOME/.git-ai/config.json" ]; then + # Find the system git (not our wrapper) + GIT_PATH="${git}/bin/git" + cat > "$HOME/.git-ai/config.json" < "$HOME/.git-ai/config.json" < "$HOME/.git-ai/config.json" < "$HOME/.git-ai/config.json" < "$GITWRAP_DIR/git" <&2 + echo "Run 'cargo build' first, then retry." >&2 + exit 1 + fi + exec -a git "$BINARY" "\$@" + GITEOF + chmod +x "$GITWRAP_DIR/git" + + # Create git-ai wrapper + cat > "$GITWRAP_DIR/git-ai" <&2 + echo "Run 'cargo build' first, then retry." >&2 + exit 1 + fi + exec "$BINARY" "\$@" + GITAIEOF + chmod +x "$GITWRAP_DIR/git-ai" + + # Create git-og wrapper (bypasses git-ai, calls real git directly) + cat > "$GITWRAP_DIR/git-og" </dev/null || true + fi + + # Install lefthook git hooks (use real git, not the git-ai wrapper, + # since the dev binary may not be built yet) + PATH="${pkgs.git}/bin:$PATH" lefthook install 2>/dev/null || true + + # Set up environment for development + export RUST_BACKTRACE=1 + export RUST_LOG=debug + + echo "git-ai development environment" + echo "Rust version: $(rustc --version)" + echo "Cargo version: $(cargo --version)" + echo "" + if [ -x "$BINARY" ]; then + echo "Dev binary: $BINARY (ready)" + echo "Hooks installed." + else + echo "Dev binary: $BINARY (not built yet)" + echo "Run 'cargo build' to build, then hooks will be installed on next 'nix develop'." + fi + echo "" + echo "git, git-ai, git-og -> wrappers in $GITWRAP_DIR" + echo "Set GIT_AI_BUILD_TYPE=release for release builds." + ''; }; - }; - # Create a complete package with git wrapper (for standalone use) - # The git-wrapper script ensures argv[0] is "git" when invoked as git - git-ai-package = pkgs.symlinkJoin { - name = "git-ai-${git-ai-unwrapped.version}"; - paths = [ git-ai-wrapped git-wrapper git-ai-unwrapped git-og ]; + # Main packages + packages = default'.git-ai.packages; - # Create libexec symlink for Fork compatibility - # Fork looks for libexec relative to the git binary location - postBuild = '' - ln -s ${pkgs.git}/libexec $out/libexec - ''; + scope = default'; - meta = git-ai-unwrapped.meta // { - description = git-ai-unwrapped.meta.description + " (with git wrapper)"; + # Make app available for `nix run` + apps.default = flake-utils.lib.mkApp { + drv = self.packages.${system}.git-ai; + exePath = "/bin/git-ai"; }; - }; - - in - { - # Development shell with full Rust toolchain - devShells.default = pkgs.mkShell { - packages = [ - # Pinned Rust 1.93.0 toolchain (includes rustc, cargo, clippy, rustfmt, rust-analyzer) - rustToolchain - ] ++ (with pkgs; [ - # Build dependencies - pkg-config - - # Runtime dependencies for testing - # NOTE: git is NOT included as a package here. Instead, the - # shellHook creates wrapper scripts (git, git-ai, git-og) that - # point to the locally-built target/debug/git-ai binary, so that - # development builds are tested directly. Use `git-og` to bypass - # git-ai and call real git. - sqlite - - # Useful development tools - cargo-edit # cargo add, cargo rm, cargo upgrade - cargo-watch # Auto-rebuild on file changes - cargo-expand # Show macro expansions - cargo-llvm-cov # Code coverage via LLVM instrumentation - lefthook # Git hooks manager - go-task # Task runner (Taskfile.yml) - ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ - libiconv - apple-sdk_15 - ]); - - # Environment variables for development - shellHook = '' - # Unset DEVELOPER_DIR to avoid conflict between the default stdenv - # SDK (14.4) and apple-sdk_15 (15.5) baked into the clang wrapper. - unset DEVELOPER_DIR - - # Set up development git-ai wrappers for nix develop (Nix-specific; non-Nix devs use scripts/dev.sh) - BUILD_TYPE="''${GIT_AI_BUILD_TYPE:-debug}" - GITWRAP_DIR="$HOME/.git-ai-local-dev/gitwrap/bin" - TARGET_DIR="''${CARGO_TARGET_DIR:-$(pwd)/target}" - BINARY="$TARGET_DIR/$BUILD_TYPE/git-ai" - - mkdir -p "$GITWRAP_DIR" - - # Create git wrapper (preserves argv[0] as "git" for passthrough mode) - cat > "$GITWRAP_DIR/git" <&2 - echo "Run 'cargo build' first, then retry." >&2 - exit 1 -fi -exec -a git "$BINARY" "\$@" -GITEOF - chmod +x "$GITWRAP_DIR/git" - - # Create git-ai wrapper - cat > "$GITWRAP_DIR/git-ai" <&2 - echo "Run 'cargo build' first, then retry." >&2 - exit 1 -fi -exec "$BINARY" "\$@" -GITAIEOF - chmod +x "$GITWRAP_DIR/git-ai" - - # Create git-og wrapper (bypasses git-ai, calls real git directly) - cat > "$GITWRAP_DIR/git-og" </dev/null || true - fi - - # Install lefthook git hooks (use real git, not the git-ai wrapper, - # since the dev binary may not be built yet) - PATH="${pkgs.git}/bin:$PATH" lefthook install 2>/dev/null || true - - # Set up environment for development - export RUST_BACKTRACE=1 - export RUST_LOG=debug - - echo "git-ai development environment" - echo "Rust version: $(rustc --version)" - echo "Cargo version: $(cargo --version)" - echo "" - if [ -x "$BINARY" ]; then - echo "Dev binary: $BINARY (ready)" - echo "Hooks installed." - else - echo "Dev binary: $BINARY (not built yet)" - echo "Run 'cargo build' to build, then hooks will be installed on next 'nix develop'." - fi - echo "" - echo "git, git-ai, git-og -> wrappers in $GITWRAP_DIR" - echo "Set GIT_AI_BUILD_TYPE=release for release builds." - ''; - }; - - # Main packages - packages = { - # Unwrapped binary (just the git-ai executable) - unwrapped = git-ai-unwrapped; - - # Wrapped version with helper scripts - wrapped = git-ai-wrapped; - - # Minimal package without git symlink (for Home Manager/environments with existing git) - minimal = git-ai-minimal; - - # Complete package with git/git-og symlinks (for standalone use) - default = git-ai-package; - # Alias for clarity - git-ai = git-ai-package; - }; - - # Make app available for `nix run` - apps.default = flake-utils.lib.mkApp { - drv = git-ai-package; - exePath = "/bin/git-ai"; - }; - - # Nix flake checks: run with `nix flake check` - # Tests are not included here -- they require network access, Node.js, - # and the Graphite CLI, which are not available in the Nix sandbox. - # Tests run in CI via the existing test.yml workflow instead. - checks = - let - commonNativeBuildInputs = with pkgs; [ pkg-config ] - ++ [ rustPlatform.bindgenHook ]; - commonBuildInputs = with pkgs; [ sqlite openssl ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ - libiconv apple-sdk_15 - ]; - mkCheck = attrs: rustPlatform.buildRustPackage ({ - version = git-ai-unwrapped.version; - src = ./.; - cargoLock.lockFile = ./Cargo.lock; - OPENSSL_NO_VENDOR = "1"; - nativeBuildInputs = commonNativeBuildInputs; - buildInputs = commonBuildInputs; - installPhase = "mkdir -p $out"; - doCheck = false; - } // attrs); - in - { - # Build check - ensures the package builds - build = git-ai-unwrapped; - - # Clippy lint check with warnings as errors - clippy = mkCheck { - pname = "git-ai-clippy"; - buildPhase = '' - cargo clippy --all-targets -- -D warnings - ''; - }; + # Nix flake checks: run with `nix flake check` + # Tests are not included here -- they require network access, Node.js, + # and the Graphite CLI, which are not available in the Nix sandbox. + # Tests run in CI via the existing test.yml workflow instead. + checks = + let + commonNativeBuildInputs = with pkgs; [ pkg-config ] + ++ [ rustPlatform.bindgenHook ]; + commonBuildInputs = with pkgs; [ sqlite openssl ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + libiconv apple-sdk_15 + ]; + mkCheck = attrs: rustPlatform.buildRustPackage ({ + version = self.packages.${system}.unwrapped.version; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + OPENSSL_NO_VENDOR = "1"; + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + installPhase = "mkdir -p $out"; + doCheck = false; + } // attrs); + in + { + # Build check - ensures the package builds + build = self.packages.${system}.unwrapped; + + # Clippy lint check with warnings as errors + clippy = mkCheck { + pname = "git-ai-clippy"; + buildPhase = '' + cargo clippy --all-targets -- -D warnings + ''; + }; - # Format check - fmt = mkCheck { - pname = "git-ai-fmt"; - buildPhase = '' - cargo fmt -- --check - ''; - }; + # Format check + fmt = mkCheck { + pname = "git-ai-fmt"; + buildPhase = '' + cargo fmt -- --check + ''; + }; - # Doc check with warnings as errors - doc = mkCheck { - pname = "git-ai-doc"; - RUSTDOCFLAGS = "-D warnings"; - buildPhase = '' - cargo doc --no-deps - ''; + # Doc check with warnings as errors + doc = mkCheck { + pname = "git-ai-doc"; + RUSTDOCFLAGS = "-D warnings"; + buildPhase = '' + cargo doc --no-deps + ''; + }; }; - }; - # Formatter for `nix fmt` - formatter = pkgs.nixpkgs-fmt; - } + # Formatter for `nix fmt` + formatter = pkgs.nixpkgs-fmt; + } ) // { # System-independent outputs # Overlay for importing into other flakes - overlays.default = final: prev: { - git-ai = self.packages.${prev.stdenv.hostPlatform.system}.default; - git-ai-unwrapped = self.packages.${prev.stdenv.hostPlatform.system}.unwrapped; - }; + overlays.default = final: prev: + let + default' = defaultApplyExt final; + in + { + git-ai = default'.git-ai.packages.git-ai; + git-ai-unwrapped = default'.git-ai.packages.unwrapped; + } + ; # NixOS module for system integration nixosModules.default = { config, lib, pkgs, ... }: @@ -366,6 +222,8 @@ GITOGEOF cfg = config.programs.git-ai; jsonFormat = pkgs.formats.json { }; + default' = defaultApplyExt pkgs; + # Build the config object, filtering out null values configFile = filterAttrs (n: v: v != null) { git_path = @@ -406,11 +264,22 @@ GITOGEOF package = mkOption { type = types.package; - default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; + default = + if cfg.gitBasePackage == null + then default'.git-ai.packages.git-ai + else (default'.overrideScope (final: prev: { git = cfg.gitBasePackage; })).git-ai.packages.git-ai + ; defaultText = literalExpression "inputs.git-ai.packages.\${pkgs.system}.default"; description = "The git-ai package to use."; }; + gitBasePackage = mkOption { + type = types.nullOr types.package; + default = null; + defaultText = literalExpression "pkgs.git"; + description = "The base git package to wrap.\n If null, defaults to pkgs.git\n Does nothing if `programs.git-ai.package` is specified"; + }; + installHooks = mkOption { type = types.bool; default = true; @@ -651,6 +520,8 @@ GITOGEOF cfg = config.programs.git-ai; jsonFormat = pkgs.formats.json { }; + default' = defaultApplyExt pkgs; + # Build the config object, filtering out null values # We use explicit null checks since Nix 'or' only works for attribute access configFile = filterAttrs (n: v: v != null) { @@ -689,11 +560,22 @@ GITOGEOF package = mkOption { type = types.package; - default = self.packages.${pkgs.stdenv.hostPlatform.system}.default; + default = + if cfg.gitBasePackage == null + then default'.git-ai.packages.git-ai + else (default'.overrideScope (final: prev: { git = cfg.gitBasePackage; })).git-ai.packages.git-ai + ; defaultText = literalExpression "inputs.git-ai.packages.\${pkgs.system}.default"; description = "The git-ai package to use."; }; + gitBasePackage = mkOption { + type = types.nullOr types.package; + default = null; + defaultText = literalExpression "pkgs.git"; + description = "The base git package to wrap.\n If null, defaults to pkgs.git"; + }; + installHooks = mkOption { type = types.bool; default = true;