Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Nix.Cache.Niks3 binary cache backend

Add niks3 cache backend that delegates to the niks3 CLI tool for
uploading store paths to S3-compatible binary caches. Config comes
entirely from the environment (NIKS3_SERVER_URL, auth token files).

Register niks3:// URL scheme in SowerCli.Cache.parse_url.

sow-88

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+115
+110
apps/nix/lib/nix/cache/niks3.ex
··· 1 + defmodule Nix.Cache.Niks3 do 2 + @moduledoc """ 3 + Binary cache backend using niks3. 4 + 5 + niks3 uploads Nix store paths to an S3-compatible binary cache. 6 + Server URL and auth token are read from the environment 7 + (`NIKS3_SERVER_URL`, `NIKS3_AUTH_TOKEN_FILE` or 8 + `$XDG_CONFIG_HOME/niks3/auth-token`). 9 + 10 + ## Configuration 11 + 12 + %{} 13 + 14 + No configuration is required — all settings come from the environment. 15 + 16 + ## Example 17 + 18 + {:ok, result} = Nix.Cache.Niks3.upload([ 19 + "/nix/store/abc123-foo", 20 + "/nix/store/xyz789-bar" 21 + ], %{}) 22 + """ 23 + 24 + use Nix.Cache 25 + 26 + require Logger 27 + 28 + @impl Nix.Cache 29 + def name(), do: "niks3" 30 + 31 + @impl Nix.Cache 32 + def validate_config(%{}), do: :ok 33 + 34 + @impl Nix.Cache 35 + def upload(path, config) when not is_list(path), do: upload([path], config) 36 + 37 + def upload(paths, _config) when is_list(paths) do 38 + if length(paths) == 0 do 39 + {:ok, %{uploaded: [], failed: []}} 40 + else 41 + paths = ensure_store_paths(paths) 42 + 43 + niks3_cmd = System.find_executable("niks3") 44 + 45 + if is_nil(niks3_cmd) do 46 + {:error, "niks3 command not found in PATH"} 47 + else 48 + do_upload(niks3_cmd, paths) 49 + end 50 + end 51 + end 52 + 53 + defp do_upload(niks3_cmd, paths) do 54 + args = ["push"] ++ paths 55 + 56 + Logger.debug( 57 + msg: "Uploading to cache", 58 + backend: "niks3", 59 + path_count: to_string(length(paths)) 60 + ) 61 + 62 + case System.cmd(niks3_cmd, args, stderr_to_stdout: true) do 63 + {_output, 0} -> 64 + Logger.info( 65 + msg: "Upload succeeded", 66 + backend: "niks3", 67 + path_count: to_string(length(paths)) 68 + ) 69 + 70 + {:ok, %{uploaded: paths, failed: []}} 71 + 72 + {output, exit_code} -> 73 + Logger.error( 74 + msg: "Upload failed", 75 + backend: "niks3", 76 + exit_code: to_string(exit_code), 77 + output: String.slice(output, 0, 500) 78 + ) 79 + 80 + error_reason = parse_error(output, exit_code) 81 + {:error, error_reason} 82 + end 83 + end 84 + 85 + defp parse_error(output, exit_code) do 86 + cond do 87 + String.contains?(output, "401") or 88 + String.contains?(output, "unauthorized") or 89 + String.contains?(output, "Unauthorized") -> 90 + "authentication failed - check auth token" 91 + 92 + String.contains?(output, "connection refused") or 93 + String.contains?(output, "Connection refused") -> 94 + "connection refused - check server URL" 95 + 96 + String.contains?(output, "no such host") or 97 + String.contains?(output, "No such host") -> 98 + "server not found - check server URL" 99 + 100 + true -> 101 + first_line = 102 + output 103 + |> String.split("\n", parts: 2) 104 + |> List.first() 105 + |> String.slice(0, 200) 106 + 107 + {exit_code, first_line} 108 + end 109 + end 110 + end
+4
apps/sower_cli/lib/sower_cli/cache.ex
··· 14 14 iex> SowerCli.Cache.parse_url("ssh://user@host") 15 15 {:ok, {Nix.Cache.NixCopy, %{destination: "ssh://user@host"}}} 16 16 """ 17 + def parse_url("niks3://" <> _rest) do 18 + {:ok, {Nix.Cache.Niks3, %{}}} 19 + end 20 + 17 21 def parse_url("attic://" <> rest) do 18 22 {:ok, {Nix.Cache.Attic, %{cache: rest}}} 19 23 end
+1
flake.nix
··· 71 71 pkgs.oapi-codegen 72 72 73 73 pkgs.attic-client 74 + pkgs.niks3 74 75 pkgs.nushell 75 76 76 77 # dev tools