XDG library path support for OCaml via Eio capabilities
0
fork

Configure Feed

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

Squashed 'xdge/' content from commit 4bce4a7b git-subtree-split: 4bce4a7b2b6ecf984a9880276fe5420e3de08730

+1897
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version=0.28.1
+49
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + 30 + steps: 31 + - name: opam 32 + command: | 33 + opam init --disable-sandboxing -any 34 + - name: switch 35 + command: | 36 + opam install . --confirm-level=unsafe-yes --deps-only 37 + - name: build 38 + command: | 39 + opam exec -- dune build 40 + - name: switch-test 41 + command: | 42 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 43 + - name: test 44 + command: | 45 + opam exec -- dune runtest --verbose 46 + - name: doc 47 + command: | 48 + opam install -y odoc 49 + opam exec -- dune build @doc
+10
CHANGES.md
··· 1 + v1.1.0 (dev) 2 + ------------ 3 + 4 + - Remove dependency on `eio_main` for library (@avsm). 5 + Thanks to @Alizter for the workaround in https://github.com/ocaml/dune/issues/12821). 6 + 7 + v1.0.0 (2025-11-29) 8 + ------------------- 9 + 10 + - Initial public release (@avsm)
+18
LICENSE.md
··· 1 + (* 2 + * ISC License 3 + * 4 + * Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 5 + * 6 + * Permission to use, copy, modify, and distribute this software for any 7 + * purpose with or without fee is hereby granted, provided that the above 8 + * copyright notice and this permission notice appear in all copies. 9 + * 10 + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 + * 18 + *)
+75
README.md
··· 1 + # xdge - XDG Base Directory Specification for Eio 2 + 3 + This library implements the [XDG Base Directory 4 + Specification](https://specifications.freedesktop.org/basedir-spec/latest/) for 5 + OCaml applications using the [Eio](https://github.com/ocaml-multicore/eio) 6 + effects-based I/O library. 7 + 8 + ## What is XDG? 9 + 10 + The XDG Base Directory Specification defines standard locations for user-specific files on Unix-like systems, keeping home directories clean and organized: 11 + 12 + - Config (`~/.config/app`): User preferences and settings 13 + - Data (`~/.local/share/app`): Persistent application data 14 + - Cache (`~/.cache/app`): Non-essential cached data (safe to delete) 15 + - State (`~/.local/state/app`): Logs, history, and runtime state 16 + - Runtime (`$XDG_RUNTIME_DIR/app`): Sockets, pipes, and session-bound files 17 + 18 + The specification also defines system-wide search paths (`/etc/xdg`, 19 + `/usr/share`) and a precedence system using environment variables 20 + (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, and so on). 21 + 22 + ## Why Eio? 23 + 24 + Eio uses a **capability-based** approach to I/O where filesystem access must be 25 + explicitly passed to functions. This design aligns naturally with XDG directory 26 + management. For example: 27 + 28 + ```ocaml 29 + (* Filesystem access is an explicit capability *) 30 + let xdg = Xdge.create env#fs "myapp" 31 + ``` 32 + 33 + The capability model provides the benefit that code that needs filesystem 34 + access must receive the `fs` capability, with no hidden global state or ambient 35 + authority. The `Eio.Path.t` type returned by xdge encapsulates both the 36 + filesystem capability and the path, preventing path traversal outside the 37 + granted capability. Applications can restrict filesystem access by passing a 38 + sandboxed `fs` capability, and xdge respects those boundaries. 39 + 40 + ## Usage 41 + 42 + ```ocaml 43 + Eio_main.run @@ fun env -> 44 + let xdg = Xdge.create env#fs "myapp" in 45 + 46 + (* Access XDG directories as Eio paths *) 47 + let config = Xdge.config_dir xdg in 48 + let data = Xdge.data_dir xdg in 49 + 50 + (* Search for files following XDG precedence *) 51 + match Xdge.find_config_file xdg "settings.json" with 52 + | Some path -> (* use path *) 53 + | None -> (* use defaults *) 54 + ``` 55 + 56 + For CLI applications, xdge provides Cmdliner terms that handle environment 57 + variable precedence and command-line overrides: 58 + 59 + ```ocaml 60 + let () = 61 + Eio_main.run @@ fun env -> 62 + let term = Xdge.Cmd.term "myapp" env#fs () in 63 + (* Generates --config-dir, --data-dir, etc. flags *) 64 + (* Respects MYAPP_CONFIG_DIR > XDG_CONFIG_HOME > default *) 65 + ``` 66 + 67 + ## Installation 68 + 69 + ``` 70 + opam install xdge 71 + ``` 72 + 73 + ## License 74 + 75 + ISC
+4
dune
··· 1 + (alias 2 + (name default) 3 + (deps 4 + (alias_rec lib/all)))
+31
dune-project
··· 1 + (lang dune 3.20) 2 + 3 + (name xdge) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + (authors "Anil Madhavapeddy") 9 + (homepage "https://tangled.sh/@anil.recoil.org/xdge") 10 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 11 + (bug_reports "https://tangled.sh/@anil.recoil.org/xdge/issues") 12 + (maintenance_intent "(latest)") 13 + 14 + (package 15 + (name xdge) 16 + (synopsis "XDG Base Directory Specification support for Eio") 17 + (description 18 + "This library implements the XDG Base Directory Specification \ 19 + with Eio capabilities to provide safe access to configuration, \ 20 + data, cache, state, and runtime directories. The library exposes \ 21 + Cmdliner terms that allow for proper environment variable overrides \ 22 + and command-line flags.") 23 + (depends 24 + (ocaml (>= 5.1.0)) 25 + (eio (>= 1.1)) 26 + (cmdliner (>= 1.2.0)) 27 + fmt 28 + xdg 29 + (eio_main :with-test) 30 + (odoc :with-doc) 31 + (alcotest (and :with-test (>= 1.7.0)))))
+4
lib/dune
··· 1 + (library 2 + (public_name xdge) 3 + (name xdge) 4 + (libraries eio xdg unix cmdliner fmt))
+668
lib/xdge.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type source = Default | Env of string | Cmdline 7 + 8 + type t = { 9 + app_name : string; 10 + config_dir : Eio.Fs.dir_ty Eio.Path.t; 11 + config_dir_source : source; 12 + data_dir : Eio.Fs.dir_ty Eio.Path.t; 13 + data_dir_source : source; 14 + cache_dir : Eio.Fs.dir_ty Eio.Path.t; 15 + cache_dir_source : source; 16 + state_dir : Eio.Fs.dir_ty Eio.Path.t; 17 + state_dir_source : source; 18 + runtime_dir : Eio.Fs.dir_ty Eio.Path.t option; 19 + runtime_dir_source : source; 20 + config_dirs : Eio.Fs.dir_ty Eio.Path.t list; 21 + data_dirs : Eio.Fs.dir_ty Eio.Path.t list; 22 + } 23 + 24 + let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path 25 + 26 + let validate_runtime_base_dir base_path = 27 + (* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *) 28 + try 29 + let path_str = Eio.Path.native_exn base_path in 30 + let stat = Eio.Path.stat ~follow:true base_path in 31 + let current_perm = stat.perm land 0o777 in 32 + if current_perm <> 0o700 then 33 + failwith 34 + (Printf.sprintf 35 + "XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o \ 36 + (must be 0700)" 37 + path_str current_perm); 38 + (* Check ownership - directory should be owned by current user *) 39 + let uid = Unix.getuid () in 40 + if stat.uid <> Int64.of_int uid then 41 + failwith 42 + (Printf.sprintf 43 + "XDG_RUNTIME_DIR base directory %s not owned by current user (uid \ 44 + %d, owner %Ld)" 45 + path_str uid stat.uid) 46 + (* TODO: Check that directory is on local filesystem (not networked). 47 + This would require filesystem type detection which is OS-specific. *) 48 + with exn -> 49 + failwith 50 + (Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s" 51 + (Printexc.to_string exn)) 52 + 53 + let ensure_runtime_dir _fs app_runtime_path = 54 + (* Base directory validation is done in resolve_runtime_dir, 55 + so we just create the app subdirectory *) 56 + ensure_dir app_runtime_path 57 + 58 + let get_home_dir fs = 59 + let home_str = 60 + match Sys.getenv_opt "HOME" with 61 + | Some home -> home 62 + | None -> ( 63 + match Sys.os_type with 64 + | "Win32" | "Cygwin" -> ( 65 + match Sys.getenv_opt "USERPROFILE" with 66 + | Some profile -> profile 67 + | None -> failwith "Cannot determine home directory") 68 + | _ -> ( 69 + try Unix.((getpwuid (getuid ())).pw_dir) 70 + with _ -> failwith "Cannot determine home directory")) 71 + in 72 + Eio.Path.(fs / home_str) 73 + 74 + let make_env_var_name app_name suffix = 75 + String.uppercase_ascii app_name ^ "_" ^ suffix 76 + 77 + exception Invalid_xdg_path of string 78 + 79 + let validate_absolute_path context path = 80 + if Filename.is_relative path then 81 + raise 82 + (Invalid_xdg_path 83 + (Printf.sprintf "%s must be an absolute path, got: %s" context path)) 84 + 85 + let resolve_path fs home_path base_path = 86 + if Filename.is_relative base_path then Eio.Path.(home_path / base_path) 87 + else Eio.Path.(fs / base_path) 88 + 89 + (* Helper to resolve system directories (config_dirs or data_dirs) *) 90 + let resolve_system_dirs fs home_path app_name override_suffix xdg_var 91 + default_paths = 92 + let override_var = make_env_var_name app_name override_suffix in 93 + match Sys.getenv_opt override_var with 94 + | Some dirs when dirs <> "" -> 95 + String.split_on_char ':' dirs 96 + |> List.filter (fun s -> s <> "") 97 + |> List.filter_map (fun path -> 98 + try 99 + validate_absolute_path override_var path; 100 + Some Eio.Path.(resolve_path fs home_path path / app_name) 101 + with Invalid_xdg_path _ -> None) 102 + | Some _ | None -> ( 103 + match Sys.getenv_opt xdg_var with 104 + | Some dirs when dirs <> "" -> 105 + String.split_on_char ':' dirs 106 + |> List.filter (fun s -> s <> "") 107 + |> List.filter_map (fun path -> 108 + try 109 + validate_absolute_path xdg_var path; 110 + Some Eio.Path.(resolve_path fs home_path path / app_name) 111 + with Invalid_xdg_path _ -> None) 112 + | Some _ | None -> 113 + List.map 114 + (fun path -> Eio.Path.(resolve_path fs home_path path / app_name)) 115 + default_paths) 116 + 117 + (* Helper to resolve a user directory with override precedence *) 118 + let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix = 119 + let override_var = make_env_var_name app_name override_suffix in 120 + match Sys.getenv_opt override_var with 121 + | Some dir when dir <> "" -> 122 + validate_absolute_path override_var dir; 123 + (Eio.Path.(fs / dir / app_name), Env override_var) 124 + | Some _ | None -> 125 + let xdg_base = xdg_getter xdg_ctx in 126 + let base_path = resolve_path fs home_path xdg_base in 127 + (Eio.Path.(base_path / app_name), Default) 128 + 129 + (* Helper to resolve runtime directory (special case since it can be None) *) 130 + let resolve_runtime_dir fs home_path app_name xdg_ctx = 131 + let override_var = make_env_var_name app_name "RUNTIME_DIR" in 132 + match Sys.getenv_opt override_var with 133 + | Some dir when dir <> "" -> 134 + validate_absolute_path override_var dir; 135 + (* Validate the base runtime directory has correct permissions *) 136 + let base_runtime_dir = resolve_path fs home_path dir in 137 + validate_runtime_base_dir base_runtime_dir; 138 + (Some Eio.Path.(base_runtime_dir / app_name), Env override_var) 139 + | Some _ | None -> 140 + ( (match Xdg.runtime_dir xdg_ctx with 141 + | Some base -> 142 + (* Validate the base runtime directory has correct permissions *) 143 + let base_runtime_dir = resolve_path fs home_path base in 144 + validate_runtime_base_dir base_runtime_dir; 145 + Some Eio.Path.(base_runtime_dir / app_name) 146 + | None -> None), 147 + Default ) 148 + 149 + let validate_standard_xdg_vars () = 150 + (* Validate standard XDG environment variables for absolute paths *) 151 + let xdg_vars = 152 + [ 153 + "XDG_CONFIG_HOME"; 154 + "XDG_DATA_HOME"; 155 + "XDG_CACHE_HOME"; 156 + "XDG_STATE_HOME"; 157 + "XDG_RUNTIME_DIR"; 158 + "XDG_CONFIG_DIRS"; 159 + "XDG_DATA_DIRS"; 160 + ] 161 + in 162 + List.iter 163 + (fun var -> 164 + match Sys.getenv_opt var with 165 + | Some value when value <> "" -> 166 + if String.contains value ':' then 167 + (* Colon-separated list - validate each part *) 168 + String.split_on_char ':' value 169 + |> List.filter (fun s -> s <> "") 170 + |> List.iter (fun path -> validate_absolute_path var path) 171 + else 172 + (* Single path *) 173 + validate_absolute_path var value 174 + | _ -> ()) 175 + xdg_vars 176 + 177 + let create fs app_name = 178 + let fs = fs in 179 + let home_path = get_home_dir fs in 180 + (* First validate all standard XDG environment variables *) 181 + validate_standard_xdg_vars (); 182 + let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in 183 + (* User directories *) 184 + let config_dir, config_dir_source = 185 + resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR" 186 + in 187 + let data_dir, data_dir_source = 188 + resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR" 189 + in 190 + let cache_dir, cache_dir_source = 191 + resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR" 192 + in 193 + let state_dir, state_dir_source = 194 + resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR" 195 + in 196 + (* Runtime directory *) 197 + let runtime_dir, runtime_dir_source = 198 + resolve_runtime_dir fs home_path app_name xdg_ctx 199 + in 200 + (* System directories *) 201 + let config_dirs = 202 + resolve_system_dirs fs home_path app_name "CONFIG_DIRS" "XDG_CONFIG_DIRS" 203 + [ "/etc/xdg" ] 204 + in 205 + let data_dirs = 206 + resolve_system_dirs fs home_path app_name "DATA_DIRS" "XDG_DATA_DIRS" 207 + [ "/usr/local/share"; "/usr/share" ] 208 + in 209 + ensure_dir config_dir; 210 + ensure_dir data_dir; 211 + ensure_dir cache_dir; 212 + ensure_dir state_dir; 213 + Option.iter (ensure_runtime_dir fs) runtime_dir; 214 + { 215 + app_name; 216 + config_dir; 217 + config_dir_source; 218 + data_dir; 219 + data_dir_source; 220 + cache_dir; 221 + cache_dir_source; 222 + state_dir; 223 + state_dir_source; 224 + runtime_dir; 225 + runtime_dir_source; 226 + config_dirs; 227 + data_dirs; 228 + } 229 + 230 + let app_name t = t.app_name 231 + let config_dir t = t.config_dir 232 + let data_dir t = t.data_dir 233 + let cache_dir t = t.cache_dir 234 + let state_dir t = t.state_dir 235 + let runtime_dir t = t.runtime_dir 236 + let config_dirs t = t.config_dirs 237 + let data_dirs t = t.data_dirs 238 + 239 + (* Check if an Eio exception indicates a missing file/directory *) 240 + let is_not_found_error = function 241 + | Eio.Io (Eio.Fs.E (Eio.Fs.Not_found _), _) -> true 242 + | Eio.Io (Eio.Fs.E (Eio.Fs.Permission_denied _), _) -> true 243 + | _ -> false 244 + 245 + (* File search following XDG specification *) 246 + let find_file_in_dirs dirs filename = 247 + let rec search_dirs = function 248 + | [] -> None 249 + | dir :: remaining_dirs -> ( 250 + let file_path = Eio.Path.(dir / filename) in 251 + try 252 + (* Try to check if file exists and is readable *) 253 + let _ = Eio.Path.stat ~follow:true file_path in 254 + Some file_path 255 + with exn when is_not_found_error exn -> 256 + (* File is inaccessible (non-existent, permissions, etc.) 257 + Skip and continue with next directory per XDG spec *) 258 + search_dirs remaining_dirs) 259 + in 260 + search_dirs dirs 261 + 262 + let find_config_file t filename = 263 + (* Search user config dir first, then system config dirs *) 264 + find_file_in_dirs (t.config_dir :: t.config_dirs) filename 265 + 266 + let find_data_file t filename = 267 + (* Search user data dir first, then system data dirs *) 268 + find_file_in_dirs (t.data_dir :: t.data_dirs) filename 269 + 270 + let pp ?(brief = false) ?(sources = false) ppf t = 271 + let pp_source ppf = function 272 + | Default -> Fmt.(styled `Faint string) ppf "default" 273 + | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 274 + | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 275 + in 276 + let pp_path_with_source ppf path source = 277 + if sources then 278 + Fmt.pf ppf "%a %a" 279 + Fmt.(styled `Green Eio.Path.pp) 280 + path 281 + Fmt.(styled `Faint (brackets pp_source)) 282 + source 283 + else Fmt.(styled `Green Eio.Path.pp) ppf path 284 + in 285 + let pp_path_opt_with_source ppf path_opt source = 286 + match path_opt with 287 + | None -> 288 + if sources then 289 + Fmt.pf ppf "%a %a" 290 + Fmt.(styled `Red string) 291 + "<none>" 292 + Fmt.(styled `Faint (brackets pp_source)) 293 + source 294 + else Fmt.(styled `Red string) ppf "<none>" 295 + | Some path -> pp_path_with_source ppf path source 296 + in 297 + let pp_paths ppf paths = 298 + Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths 299 + in 300 + if brief then 301 + Fmt.pf ppf "%a config=%a data=%a>" 302 + Fmt.(styled `Cyan string) 303 + ("<xdg:" ^ t.app_name) 304 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 305 + (t.config_dir, t.config_dir_source) 306 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 307 + (t.data_dir, t.data_dir_source) 308 + else ( 309 + Fmt.pf ppf "@[<v>%a@," 310 + Fmt.(styled `Bold string) 311 + ("XDG directories for '" ^ t.app_name ^ "':"); 312 + Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:"; 313 + Fmt.pf ppf "%a %a@," 314 + Fmt.(styled `Cyan string) 315 + "config:" 316 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 317 + (t.config_dir, t.config_dir_source); 318 + Fmt.pf ppf "%a %a@," 319 + Fmt.(styled `Cyan string) 320 + "data:" 321 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 322 + (t.data_dir, t.data_dir_source); 323 + Fmt.pf ppf "%a %a@," 324 + Fmt.(styled `Cyan string) 325 + "cache:" 326 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 327 + (t.cache_dir, t.cache_dir_source); 328 + Fmt.pf ppf "%a %a@," 329 + Fmt.(styled `Cyan string) 330 + "state:" 331 + (fun ppf (path, source) -> pp_path_with_source ppf path source) 332 + (t.state_dir, t.state_dir_source); 333 + Fmt.pf ppf "%a %a@]@," 334 + Fmt.(styled `Cyan string) 335 + "runtime:" 336 + (fun ppf (path_opt, source) -> 337 + pp_path_opt_with_source ppf path_opt source) 338 + (t.runtime_dir, t.runtime_dir_source); 339 + Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:"; 340 + Fmt.pf ppf "%a [@[<hov>%a@]]@," 341 + Fmt.(styled `Cyan string) 342 + "config_dirs:" pp_paths t.config_dirs; 343 + Fmt.pf ppf "%a [@[<hov>%a@]]@]@]" 344 + Fmt.(styled `Cyan string) 345 + "data_dirs:" pp_paths t.data_dirs) 346 + 347 + module Cmd = struct 348 + type xdg_t = t 349 + type 'a with_source = { value : 'a option; source : source } 350 + 351 + type t = { 352 + config_dir : string with_source; 353 + data_dir : string with_source; 354 + cache_dir : string with_source; 355 + state_dir : string with_source; 356 + runtime_dir : string with_source; 357 + } 358 + 359 + type dir = [ `Config | `Cache | `Data | `State | `Runtime ] 360 + 361 + let term app_name fs ?(dirs = [ `Config; `Data; `Cache; `State; `Runtime ]) () 362 + = 363 + let open Cmdliner in 364 + let app_upper = String.uppercase_ascii app_name in 365 + let show_paths = 366 + let doc = "Show only the resolved directory paths without formatting" in 367 + Arg.(value & flag & info [ "show-paths" ] ~doc) 368 + in 369 + let has_dir d = List.mem d dirs in 370 + let make_dir_arg ~enabled name env_suffix xdg_var default_path = 371 + if not enabled then 372 + (* Return a term that always gives the environment-only result *) 373 + Term.( 374 + const (fun () -> 375 + let app_env = app_upper ^ "_" ^ env_suffix in 376 + match Sys.getenv_opt app_env with 377 + | Some v when v <> "" -> { value = Some v; source = Env app_env } 378 + | Some _ | None -> ( 379 + match Sys.getenv_opt xdg_var with 380 + | Some v -> { value = Some v; source = Env xdg_var } 381 + | None -> { value = None; source = Default })) 382 + $ const ()) 383 + else 384 + let app_env = app_upper ^ "_" ^ env_suffix in 385 + let doc = 386 + match default_path with 387 + | Some path -> 388 + Printf.sprintf 389 + "Override %s directory. Can also be set with %s or %s. \ 390 + Default: %s" 391 + name app_env xdg_var path 392 + | None -> 393 + Printf.sprintf 394 + "Override %s directory. Can also be set with %s or %s. No \ 395 + default value." 396 + name app_env xdg_var 397 + in 398 + let arg = 399 + Arg.( 400 + value 401 + & opt (some string) None 402 + & info [ name ^ "-dir" ] ~docv:"DIR" ~doc) 403 + in 404 + Term.( 405 + const (fun cmdline_val -> 406 + match cmdline_val with 407 + | Some v -> { value = Some v; source = Cmdline } 408 + | None -> ( 409 + match Sys.getenv_opt app_env with 410 + | Some v when v <> "" -> 411 + { value = Some v; source = Env app_env } 412 + | Some _ | None -> ( 413 + match Sys.getenv_opt xdg_var with 414 + | Some v -> { value = Some v; source = Env xdg_var } 415 + | None -> { value = None; source = Default }))) 416 + $ arg) 417 + in 418 + let home_prefix = "\\$HOME" in 419 + let config_dir = 420 + make_dir_arg ~enabled:(has_dir `Config) "config" "CONFIG_DIR" 421 + "XDG_CONFIG_HOME" 422 + (Some (home_prefix ^ "/.config/" ^ app_name)) 423 + in 424 + let data_dir = 425 + make_dir_arg ~enabled:(has_dir `Data) "data" "DATA_DIR" "XDG_DATA_HOME" 426 + (Some (home_prefix ^ "/.local/share/" ^ app_name)) 427 + in 428 + let cache_dir = 429 + make_dir_arg ~enabled:(has_dir `Cache) "cache" "CACHE_DIR" 430 + "XDG_CACHE_HOME" 431 + (Some (home_prefix ^ "/.cache/" ^ app_name)) 432 + in 433 + let state_dir = 434 + make_dir_arg ~enabled:(has_dir `State) "state" "STATE_DIR" 435 + "XDG_STATE_HOME" 436 + (Some (home_prefix ^ "/.local/state/" ^ app_name)) 437 + in 438 + let runtime_dir = 439 + make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" 440 + "XDG_RUNTIME_DIR" None 441 + in 442 + Term.( 443 + const 444 + (fun 445 + show_paths_flag 446 + config_dir_ws 447 + data_dir_ws 448 + cache_dir_ws 449 + state_dir_ws 450 + runtime_dir_ws 451 + -> 452 + let config = 453 + { 454 + config_dir = config_dir_ws; 455 + data_dir = data_dir_ws; 456 + cache_dir = cache_dir_ws; 457 + state_dir = state_dir_ws; 458 + runtime_dir = runtime_dir_ws; 459 + } 460 + in 461 + let home_path = get_home_dir fs in 462 + (* First validate all standard XDG environment variables *) 463 + validate_standard_xdg_vars (); 464 + let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in 465 + (* Helper to resolve directory from config with source tracking *) 466 + let resolve_from_config config_ws xdg_getter = 467 + match config_ws.value with 468 + | Some dir -> (resolve_path fs home_path dir, config_ws.source) 469 + | None -> 470 + let xdg_base = xdg_getter xdg_ctx in 471 + let base_path = resolve_path fs home_path xdg_base in 472 + (Eio.Path.(base_path / app_name), config_ws.source) 473 + in 474 + (* User directories *) 475 + let config_dir, config_dir_source = 476 + resolve_from_config config.config_dir Xdg.config_dir 477 + in 478 + let data_dir, data_dir_source = 479 + resolve_from_config config.data_dir Xdg.data_dir 480 + in 481 + let cache_dir, cache_dir_source = 482 + resolve_from_config config.cache_dir Xdg.cache_dir 483 + in 484 + let state_dir, state_dir_source = 485 + resolve_from_config config.state_dir Xdg.state_dir 486 + in 487 + (* Runtime directory *) 488 + let runtime_dir, runtime_dir_source = 489 + match config.runtime_dir.value with 490 + | Some dir -> 491 + (Some (resolve_path fs home_path dir), config.runtime_dir.source) 492 + | None -> 493 + ( Option.map 494 + (fun base -> 495 + let base_path = resolve_path fs home_path base in 496 + Eio.Path.(base_path / app_name)) 497 + (Xdg.runtime_dir xdg_ctx), 498 + config.runtime_dir.source ) 499 + in 500 + (* System directories - reuse shared helper *) 501 + let config_dirs = 502 + resolve_system_dirs fs home_path app_name "CONFIG_DIRS" 503 + "XDG_CONFIG_DIRS" [ "/etc/xdg" ] 504 + in 505 + let data_dirs = 506 + resolve_system_dirs fs home_path app_name "DATA_DIRS" 507 + "XDG_DATA_DIRS" 508 + [ "/usr/local/share"; "/usr/share" ] 509 + in 510 + ensure_dir config_dir; 511 + ensure_dir data_dir; 512 + ensure_dir cache_dir; 513 + ensure_dir state_dir; 514 + Option.iter (ensure_runtime_dir fs) runtime_dir; 515 + let xdg = 516 + { 517 + app_name; 518 + config_dir; 519 + config_dir_source; 520 + data_dir; 521 + data_dir_source; 522 + cache_dir; 523 + cache_dir_source; 524 + state_dir; 525 + state_dir_source; 526 + runtime_dir; 527 + runtime_dir_source; 528 + config_dirs; 529 + data_dirs; 530 + } 531 + in 532 + (* Handle --show-paths option *) 533 + if show_paths_flag then ( 534 + let print_path name path = 535 + match path with 536 + | None -> Printf.printf "%s: <none>\n" name 537 + | Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p) 538 + in 539 + let print_paths name paths = 540 + match paths with 541 + | [] -> Printf.printf "%s: []\n" name 542 + | paths -> 543 + let paths_str = 544 + String.concat ":" (List.map Eio.Path.native_exn paths) 545 + in 546 + Printf.printf "%s: %s\n" name paths_str 547 + in 548 + print_path "config_dir" (Some config_dir); 549 + print_path "data_dir" (Some data_dir); 550 + print_path "cache_dir" (Some cache_dir); 551 + print_path "state_dir" (Some state_dir); 552 + print_path "runtime_dir" runtime_dir; 553 + print_paths "config_dirs" config_dirs; 554 + print_paths "data_dirs" data_dirs; 555 + Stdlib.exit 0); 556 + (xdg, config)) 557 + $ show_paths $ config_dir $ data_dir $ cache_dir $ state_dir $ runtime_dir) 558 + 559 + let cache_term app_name = 560 + let open Cmdliner in 561 + let app_upper = String.uppercase_ascii app_name in 562 + let app_env = app_upper ^ "_CACHE_DIR" in 563 + let xdg_var = "XDG_CACHE_HOME" in 564 + let home = Sys.getenv "HOME" in 565 + let default_path = home ^ "/.cache/" ^ app_name in 566 + 567 + let doc = 568 + Printf.sprintf 569 + "Override cache directory. Can also be set with %s or %s. Default: %s" 570 + app_env xdg_var default_path 571 + in 572 + 573 + let arg = 574 + Arg.( 575 + value & opt string default_path 576 + & info [ "cache-dir"; "c" ] ~docv:"DIR" ~doc) 577 + in 578 + 579 + Term.( 580 + const (fun cmdline_val -> 581 + (* Check command line first *) 582 + if cmdline_val <> default_path then cmdline_val 583 + else 584 + (* Then check app-specific env var *) 585 + match Sys.getenv_opt app_env with 586 + | Some v when v <> "" -> v 587 + | _ -> ( 588 + (* Then check XDG env var *) 589 + match Sys.getenv_opt xdg_var with 590 + | Some v when v <> "" -> v ^ "/" ^ app_name 591 + | _ -> default_path)) 592 + $ arg) 593 + 594 + let env_docs app_name = 595 + let app_upper = String.uppercase_ascii app_name in 596 + Printf.sprintf 597 + {| 598 + Configuration Precedence (follows standard Unix conventions): 599 + 1. Command-line flags (e.g., --config-dir) - highest priority 600 + 2. Application-specific environment variable (e.g., %s_CONFIG_DIR) 601 + 3. XDG standard environment variable (e.g., XDG_CONFIG_HOME) 602 + 4. Default path (e.g., ~/.config/%s) - lowest priority 603 + 604 + This allows per-application overrides without affecting other XDG-compliant programs. 605 + For example, setting %s_CONFIG_DIR only changes the config directory for %s, 606 + while XDG_CONFIG_HOME affects all XDG-compliant applications. 607 + 608 + Application-specific variables: 609 + %s_CONFIG_DIR Override config directory for %s only 610 + %s_DATA_DIR Override data directory for %s only 611 + %s_CACHE_DIR Override cache directory for %s only 612 + %s_STATE_DIR Override state directory for %s only 613 + %s_RUNTIME_DIR Override runtime directory for %s only 614 + 615 + XDG standard variables (shared by all XDG applications): 616 + XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s) 617 + XDG_DATA_HOME User data directory (default: ~/.local/share/%s) 618 + XDG_CACHE_HOME User cache directory (default: ~/.cache/%s) 619 + XDG_STATE_HOME User state directory (default: ~/.local/state/%s) 620 + XDG_RUNTIME_DIR User runtime directory (no default) 621 + XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s) 622 + XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s) 623 + |} 624 + app_upper app_name app_upper app_name app_upper app_name app_upper 625 + app_name app_upper app_name app_upper app_name app_upper app_name app_name 626 + app_name app_name app_name app_name app_name app_name 627 + 628 + let pp ppf config = 629 + let pp_source ppf = function 630 + | Default -> Fmt.(styled `Faint string) ppf "default" 631 + | Env var -> 632 + Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 633 + | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 634 + in 635 + let pp_with_source name ppf ws = 636 + match ws.value with 637 + | None when ws.source = Default -> () 638 + | None -> 639 + Fmt.pf ppf "@,%a %a %a" 640 + Fmt.(styled `Cyan string) 641 + (name ^ ":") 642 + Fmt.(styled `Red string) 643 + "<unset>" 644 + Fmt.(styled `Faint (brackets pp_source)) 645 + ws.source 646 + | Some value -> 647 + Fmt.pf ppf "@,%a %a %a" 648 + Fmt.(styled `Cyan string) 649 + (name ^ ":") 650 + Fmt.(styled `Green string) 651 + value 652 + Fmt.(styled `Faint (brackets pp_source)) 653 + ws.source 654 + in 655 + Fmt.pf ppf "@[<v>%a%a%a%a%a%a@]" 656 + Fmt.(styled `Bold string) 657 + "XDG config:" 658 + (pp_with_source "config_dir") 659 + config.config_dir 660 + (pp_with_source "data_dir") 661 + config.data_dir 662 + (pp_with_source "cache_dir") 663 + config.cache_dir 664 + (pp_with_source "state_dir") 665 + config.state_dir 666 + (pp_with_source "runtime_dir") 667 + config.runtime_dir 668 + end
+440
lib/xdge.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** XDG Base Directory Specification support with Eio capabilities 7 + 8 + This library provides an OCaml implementation of the XDG Base Directory 9 + Specification with Eio filesystem integration. The XDG specification defines 10 + standard locations for user-specific and system-wide application files, 11 + helping to keep user home directories clean and organized. 12 + 13 + The specification is available at: 14 + {{:https://specifications.freedesktop.org/basedir-spec/latest/} XDG Base 15 + Directory Specification} 16 + 17 + {b Key Concepts:} 18 + 19 + The XDG specification defines several types of directories: 20 + - {b User directories}: Store user-specific files (config, data, cache, 21 + state, runtime) 22 + - {b System directories}: Store system-wide files shared across users 23 + - {b Precedence}: User directories take precedence over system directories 24 + - {b Application isolation}: Each application gets its own subdirectory 25 + 26 + {b Environment Variable Precedence:} 27 + 28 + This library follows a three-level precedence system: 29 + + Application-specific variables (e.g., [MYAPP_CONFIG_DIR]) - highest 30 + priority 31 + + XDG standard variables (e.g., [XDG_CONFIG_HOME]) 32 + + Default paths (e.g., [$HOME/.config]) - lowest priority 33 + 34 + This allows fine-grained control over directory locations without affecting 35 + other XDG-compliant applications. 36 + 37 + {b Directory Creation:} 38 + 39 + All directories are automatically created with appropriate permissions 40 + (0o755) when accessed, except for runtime directories which require stricter 41 + permissions as per the specification. 42 + 43 + @see <https://specifications.freedesktop.org/basedir-spec/latest/> 44 + XDG Base Directory Specification 45 + 46 + {2 Related Libraries} 47 + 48 + This library is used by: 49 + 50 + - [Requests] - HTTP client that uses Xdge for cookie persistence paths 51 + - [Cookeio_jar] - Cookie jar with XDG-compliant storage *) 52 + 53 + type t 54 + (** The main XDG context type containing all directory paths for an application. 55 + 56 + A value of type [t] represents the complete XDG directory structure for a 57 + specific application, including both user-specific and system-wide 58 + directories. All paths are resolved at creation time and are absolute paths 59 + within the Eio filesystem. *) 60 + 61 + (** {1 Exceptions} *) 62 + 63 + exception Invalid_xdg_path of string 64 + (** Exception raised when XDG environment variables contain invalid paths. 65 + 66 + The XDG specification requires all paths in environment variables to be 67 + absolute. This exception is raised when a relative path is found. *) 68 + 69 + (** {1 Construction} *) 70 + 71 + val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t 72 + (** [create fs app_name] creates an XDG context for the given application. 73 + 74 + This function initializes the complete XDG directory structure for your 75 + application, resolving all paths according to the environment variables and 76 + creating directories as needed. 77 + 78 + @param fs The Eio filesystem providing filesystem access 79 + @param app_name The name of your application (used as subdirectory name) 80 + 81 + {b Path Resolution:} 82 + 83 + For each directory type, the following precedence is used: 84 + + Application-specific environment variable (e.g., [MYAPP_CONFIG_DIR]) 85 + + XDG standard environment variable (e.g., [XDG_CONFIG_HOME]) 86 + + Default path as specified in the XDG specification 87 + 88 + {b Example:} 89 + {[ 90 + let xdg = Xdge.create env#fs "myapp" in 91 + let config = Xdge.config_dir xdg in 92 + (* config is now <fs:$HOME/.config/myapp> or the overridden path *) 93 + ]} 94 + 95 + All directories are created with permissions 0o755 if they don't exist, 96 + except for runtime directories which are created with 0o700 permissions and 97 + validated according to the XDG specification. 98 + 99 + @raise Invalid_xdg_path if any environment variable contains a relative path 100 + *) 101 + 102 + (** {1 Accessors} *) 103 + 104 + val app_name : t -> string 105 + (** [app_name t] returns the application name used when creating this XDG 106 + context. 107 + 108 + This is the name that was passed to {!create} and is used as the 109 + subdirectory name within each XDG base directory. *) 110 + 111 + (** {1 Base Directories} *) 112 + 113 + val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t 114 + (** [config_dir t] returns the path to user-specific configuration files. 115 + 116 + {b Purpose:} Store user preferences, settings, and configuration files. 117 + Configuration files should be human-readable when possible. 118 + 119 + {b Environment Variables:} 120 + - [${APP_NAME}_CONFIG_DIR]: Application-specific override (highest priority) 121 + - [XDG_CONFIG_HOME]: XDG standard variable 122 + - Default: [$HOME/.config/{app_name}] 123 + 124 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 125 + XDG_CONFIG_HOME specification *) 126 + 127 + val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t 128 + (** [data_dir t] returns the path to user-specific data files. 129 + 130 + {b Purpose:} Store persistent application data that should be preserved 131 + across application restarts and system reboots. This data is typically not 132 + modified by users directly. 133 + 134 + {b Environment Variables:} 135 + - [${APP_NAME}_DATA_DIR]: Application-specific override (highest priority) 136 + - [XDG_DATA_HOME]: XDG standard variable 137 + - Default: [$HOME/.local/share/{app_name}] 138 + 139 + {b Example Files:} 140 + - Application databases 141 + - User-generated content (documents, projects) 142 + - Downloaded resources 143 + - Application plugins or extensions 144 + 145 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 146 + XDG_DATA_HOME specification *) 147 + 148 + val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t 149 + (** [cache_dir t] returns the path to user-specific cache files. 150 + 151 + {b Purpose:} Store non-essential cached data that can be regenerated if 152 + deleted. The application should remain functional if this directory is 153 + cleared, though performance may be temporarily impacted. 154 + 155 + {b Environment Variables:} 156 + - [${APP_NAME}_CACHE_DIR]: Application-specific override (highest priority) 157 + - [XDG_CACHE_HOME]: XDG standard variable 158 + - Default: [$HOME/.cache/{app_name}] 159 + 160 + {b Example Files:} 161 + - Downloaded thumbnails and previews 162 + - Compiled bytecode or object files 163 + - Network response caches 164 + - Temporary computation results 165 + 166 + Users may clear cache directories to free disk space, so always check for 167 + cache validity and be prepared to regenerate data. 168 + 169 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 170 + XDG_CACHE_HOME specification *) 171 + 172 + val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t 173 + (** [state_dir t] returns the path to user-specific state files. 174 + 175 + {b Purpose:} Store persistent state data that should be preserved between 176 + application restarts but is not important enough to be user data. This 177 + includes application state that can be regenerated but would impact the user 178 + experience if lost. 179 + 180 + {b Environment Variables:} 181 + - [${APP_NAME}_STATE_DIR]: Application-specific override (highest priority) 182 + - [XDG_STATE_HOME]: XDG standard variable 183 + - Default: [$HOME/.local/state/{app_name}] 184 + 185 + {b Example Files:} 186 + - Application history (recently used files, command history) 187 + - Current application state (window positions, open tabs) 188 + - Logs and journal files 189 + - Undo/redo history 190 + 191 + {b Comparison with other directories:} 192 + - Unlike cache: State should persist between reboots 193 + - Unlike data: State can be regenerated (though inconvenient) 194 + - Unlike config: State changes frequently during normal use 195 + 196 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 197 + XDG_STATE_HOME specification *) 198 + 199 + val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option 200 + (** [runtime_dir t] returns the path to user-specific runtime files. 201 + 202 + {b Purpose:} Store runtime files such as sockets, named pipes, and process 203 + IDs. These files are only valid for the duration of the user's login 204 + session. 205 + 206 + {b Environment Variables:} 207 + - [${APP_NAME}_RUNTIME_DIR]: Application-specific override (highest 208 + priority) 209 + - [XDG_RUNTIME_DIR]: XDG standard variable 210 + - Default: None (returns [None] if not set) 211 + 212 + {b Required Properties (per specification):} 213 + - Owned by the user with access mode 0700 214 + - Bound to the user login session lifetime 215 + - Located on a local filesystem (not networked) 216 + - Fully-featured by the OS (supporting proper locking, etc.) 217 + 218 + {b Example Files:} 219 + - Unix domain sockets 220 + - Named pipes (FIFOs) 221 + - Lock files 222 + - Small process communication files 223 + 224 + This may return [None] if no suitable runtime directory is available. 225 + Applications should handle this gracefully, perhaps by falling back to 226 + [/tmp] with appropriate security measures. 227 + 228 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 229 + XDG_RUNTIME_DIR specification *) 230 + 231 + (** {1 System Directories} *) 232 + 233 + val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list 234 + (** [config_dirs t] returns search paths for system-wide configuration files. 235 + 236 + {b Purpose:} Provide a search path for configuration files that are shared 237 + between multiple users. Files in user-specific {!config_dir} take precedence 238 + over these system directories. 239 + 240 + {b Environment Variables:} 241 + - [${APP_NAME}_CONFIG_DIRS]: Application-specific override (highest 242 + priority) 243 + - [XDG_CONFIG_DIRS]: XDG standard variable (colon-separated list) 244 + - Default: [[/etc/xdg/{app_name}]] 245 + 246 + {b Search Order:} Directories are ordered by preference, with earlier 247 + entries taking precedence over later ones. When looking for a configuration 248 + file, search {!config_dir} first, then each directory in this list. 249 + 250 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 251 + XDG_CONFIG_DIRS specification *) 252 + 253 + val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list 254 + (** [data_dirs t] returns search paths for system-wide data files. 255 + 256 + {b Purpose:} Provide a search path for data files that are shared between 257 + multiple users. Files in user-specific {!data_dir} take precedence over 258 + these system directories. 259 + 260 + {b Environment Variables:} 261 + - [${APP_NAME}_DATA_DIRS]: Application-specific override (highest priority) 262 + - [XDG_DATA_DIRS]: XDG standard variable (colon-separated list) 263 + - Default: [[/usr/local/share/{app_name}; /usr/share/{app_name}]] 264 + 265 + {b Search Order:} Directories are ordered by preference, with earlier 266 + entries taking precedence over later ones. When looking for a data file, 267 + search {!data_dir} first, then each directory in this list. 268 + 269 + {b Example Files:} 270 + - Application icons and themes 271 + - Desktop files 272 + - Shared application resources 273 + - Documentation files 274 + - Default templates 275 + 276 + @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 277 + XDG_DATA_DIRS specification *) 278 + 279 + (** {1 File Search} *) 280 + 281 + val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 282 + (** [find_config_file t filename] searches for a configuration file following 283 + XDG precedence. 284 + 285 + This function searches for the given filename in the user configuration 286 + directory first, then in system configuration directories in order of 287 + preference. Files that are inaccessible (due to permissions, non-existence, 288 + etc.) are silently skipped as per the XDG specification. 289 + 290 + @param t The XDG context 291 + @param filename The name of the file to search for 292 + @return [Some path] if found, [None] if not found in any directory 293 + 294 + {b Search Order:} 1. User config directory ({!config_dir}) 2. System config 295 + directories ({!config_dirs}) in preference order *) 296 + 297 + val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 298 + (** [find_data_file t filename] searches for a data file following XDG 299 + precedence. 300 + 301 + This function searches for the given filename in the user data directory 302 + first, then in system data directories in order of preference. Files that 303 + are inaccessible (due to permissions, non-existence, etc.) are silently 304 + skipped as per the XDG specification. 305 + 306 + @param t The XDG context 307 + @param filename The name of the file to search for 308 + @return [Some path] if found, [None] if not found in any directory 309 + 310 + {b Search Order:} 1. User data directory ({!data_dir}) 2. System data 311 + directories ({!data_dirs}) in preference order *) 312 + 313 + (** {1 Pretty Printing} *) 314 + 315 + val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit 316 + (** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration. 317 + 318 + @param brief If [true], prints a compact one-line summary (default: [false]) 319 + @param sources 320 + If [true], shows the source of each directory value, indicating whether it 321 + came from defaults, environment variables, or command line (default: 322 + [false]) 323 + @param ppf The formatter to print to 324 + @param t The XDG context to print 325 + 326 + {b Output formats:} 327 + - Normal: Multi-line detailed view of all directories 328 + - Brief: Single line showing app name and key directories 329 + - With sources: Adds annotations showing where each path came from *) 330 + 331 + (** {1 Cmdliner Integration} *) 332 + 333 + module Cmd : sig 334 + (** The type of the outer XDG context *) 335 + type xdg_t = t 336 + (** Cmdliner integration for XDG directory configuration. 337 + 338 + This module provides integration with the Cmdliner library, allowing XDG 339 + directories to be configured via command-line arguments while respecting 340 + the precedence of environment variables. *) 341 + 342 + type t 343 + (** Type of XDG configuration gathered from command-line and environment. 344 + 345 + This contains all XDG directory paths along with their sources, as 346 + determined by command-line arguments and environment variables. *) 347 + 348 + type dir = 349 + [ `Config (** User configuration files *) 350 + | `Cache (** User-specific cached data *) 351 + | `Data (** User-specific application data *) 352 + | `State (** User-specific state data (logs, history, etc.) *) 353 + | `Runtime (** User-specific runtime files (sockets, pipes, etc.) *) ] 354 + (** XDG directory types for specifying which directories an application needs. 355 + 356 + These allow applications to declare which XDG directories they use, 357 + enabling runtime systems to only provide the requested directories. *) 358 + 359 + val term : 360 + string -> 361 + Eio.Fs.dir_ty Eio.Path.t -> 362 + ?dirs:dir list -> 363 + unit -> 364 + (xdg_t * t) Cmdliner.Term.t 365 + (** [term app_name fs ?dirs ()] creates a Cmdliner term for XDG directory 366 + configuration. 367 + 368 + This function generates a Cmdliner term that handles XDG directory 369 + configuration through both command-line flags and environment variables, 370 + and directly returns the XDG context. Only command-line flags for the 371 + requested directories are generated. 372 + 373 + @param app_name 374 + The application name (used for environment variable prefixes) 375 + @param fs The Eio filesystem to use for path resolution 376 + @param dirs 377 + List of directories to include flags for (default: all directories) 378 + 379 + {b Generated Command-line Flags:} Only the flags for requested directories 380 + are generated: 381 + - [--config-dir DIR]: Override configuration directory (if [`Config] in 382 + dirs) 383 + - [--data-dir DIR]: Override data directory (if [`Data] in dirs) 384 + - [--cache-dir DIR]: Override cache directory (if [`Cache] in dirs) 385 + - [--state-dir DIR]: Override state directory (if [`State] in dirs) 386 + - [--runtime-dir DIR]: Override runtime directory (if [`Runtime] in dirs) 387 + 388 + {b Environment Variable Precedence:} For each directory type, the 389 + following precedence applies: 390 + + Command-line flag (e.g., [--config-dir]) - if enabled 391 + + Application-specific variable (e.g., [MYAPP_CONFIG_DIR]) 392 + + XDG standard variable (e.g., [XDG_CONFIG_HOME]) 393 + + Default value *) 394 + 395 + val cache_term : string -> string Cmdliner.Term.t 396 + (** [cache_term app_name] creates a Cmdliner term that provides just the cache 397 + directory path as a string, respecting XDG precedence. 398 + 399 + This is a convenience function for applications that only need cache 400 + directory configuration. It returns the resolved cache directory path 401 + directly as a string, suitable for use in other Cmdliner terms. 402 + 403 + @param app_name 404 + The application name (used for environment variable prefixes) 405 + 406 + {b Generated Command-line Flag:} 407 + - [--cache-dir DIR]: Override cache directory 408 + 409 + {b Environment Variable Precedence:} 410 + + Command-line flag ([--cache-dir]) 411 + + Application-specific variable (e.g., [MYAPP_CACHE_DIR]) 412 + + XDG standard variable ([XDG_CACHE_HOME]) 413 + + Default value ([$HOME/.cache/{app_name}]) *) 414 + 415 + val env_docs : string -> string 416 + (** [env_docs app_name] generates documentation for environment variables. 417 + 418 + Returns a formatted string documenting all environment variables that 419 + affect XDG directory configuration for the given application. This is 420 + useful for generating man pages or help text. 421 + 422 + @param app_name The application name 423 + @return A formatted documentation string 424 + 425 + {b Included Information:} 426 + - Configuration precedence rules 427 + - Application-specific environment variables 428 + - XDG standard environment variables 429 + - Default values for each directory type *) 430 + 431 + val pp : Format.formatter -> t -> unit 432 + (** [pp ppf config] pretty prints a Cmdliner configuration. 433 + 434 + This function formats the configuration showing each directory path along 435 + with its source, which is helpful for debugging configuration issues or 436 + displaying the current configuration to users. 437 + 438 + @param ppf The formatter to print to 439 + @param config The configuration to print *) 440 + end
+10
test/dune
··· 1 + (executable 2 + (name xdg_example) 3 + (libraries xdge eio_main cmdliner fmt)) 4 + 5 + (executable 6 + (name test_paths) 7 + (libraries xdge eio eio_main)) 8 + 9 + (cram 10 + (deps xdg_example.exe test_paths.exe))
+110
test/test_paths.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let test_path_validation () = 7 + Printf.printf "Testing XDG path validation...\n"; 8 + (* Test absolute path validation for environment variables *) 9 + let test_relative_path_rejection env_var relative_path = 10 + Printf.printf "Testing rejection of relative path in %s...\n" env_var; 11 + Unix.putenv env_var relative_path; 12 + try 13 + Eio_main.run @@ fun env -> 14 + let _ = Xdge.create env#fs "test_validation" in 15 + Printf.printf "ERROR: Should have rejected relative path\n"; 16 + false 17 + with 18 + | Xdge.Invalid_xdg_path msg -> 19 + Printf.printf "SUCCESS: Correctly rejected relative path: %s\n" msg; 20 + true 21 + | exn -> 22 + Printf.printf "ERROR: Wrong exception: %s\n" (Printexc.to_string exn); 23 + false 24 + in 25 + let old_config_home = Sys.getenv_opt "XDG_CONFIG_HOME" in 26 + let old_data_dirs = Sys.getenv_opt "XDG_DATA_DIRS" in 27 + let success1 = 28 + test_relative_path_rejection "XDG_CONFIG_HOME" "relative/path" 29 + in 30 + let success2 = 31 + test_relative_path_rejection "XDG_DATA_DIRS" "rel1:rel2:/abs/path" 32 + in 33 + (* Restore original env vars *) 34 + (match old_config_home with 35 + | Some v -> Unix.putenv "XDG_CONFIG_HOME" v 36 + | None -> ( try Unix.putenv "XDG_CONFIG_HOME" "" with _ -> ())); 37 + (match old_data_dirs with 38 + | Some v -> Unix.putenv "XDG_DATA_DIRS" v 39 + | None -> ( try Unix.putenv "XDG_DATA_DIRS" "" with _ -> ())); 40 + success1 && success2 41 + 42 + let test_file_search () = 43 + Printf.printf "\nTesting XDG file search...\n"; 44 + Eio_main.run @@ fun env -> 45 + let xdg = Xdge.create env#fs "search_test" in 46 + (* Create test files *) 47 + let config_file = Eio.Path.(Xdge.config_dir xdg / "test.conf") in 48 + let data_file = Eio.Path.(Xdge.data_dir xdg / "test.dat") in 49 + Eio.Path.save ~create:(`Or_truncate 0o644) config_file "config content"; 50 + Eio.Path.save ~create:(`Or_truncate 0o644) data_file "data content"; 51 + (* Test finding existing files *) 52 + (match Xdge.find_config_file xdg "test.conf" with 53 + | Some path -> 54 + let content = Eio.Path.load path in 55 + Printf.printf "Found config file: %s\n" (String.trim content) 56 + | None -> Printf.printf "ERROR: Config file not found\n"); 57 + (match Xdge.find_data_file xdg "test.dat" with 58 + | Some path -> 59 + let content = Eio.Path.load path in 60 + Printf.printf "Found data file: %s\n" (String.trim content) 61 + | None -> Printf.printf "ERROR: Data file not found\n"); 62 + (* Test non-existent file *) 63 + match Xdge.find_config_file xdg "nonexistent.conf" with 64 + | Some _ -> Printf.printf "ERROR: Should not have found nonexistent file\n" 65 + | None -> Printf.printf "Correctly handled nonexistent file\n" 66 + 67 + let () = 68 + (* Check if we should run validation tests *) 69 + if Array.length Sys.argv > 1 && Sys.argv.(1) = "--validate" then ( 70 + let validation_success = test_path_validation () in 71 + test_file_search (); 72 + if validation_success then 73 + Printf.printf "\nAll path validation tests passed!\n" 74 + else Printf.printf "\nSome validation tests failed!\n") 75 + else 76 + (* Run original simple functionality test *) 77 + Eio_main.run @@ fun env -> 78 + let xdg = Xdge.create env#fs "path_test" in 79 + (* Test config subdirectory *) 80 + let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in 81 + let profile_file = Eio.Path.(profiles_path / "default.json") in 82 + (try 83 + let content = Eio.Path.load profile_file in 84 + Printf.printf "config file content: %s" (String.trim content) 85 + with exn -> 86 + Printf.printf "config file error: %s" (Printexc.to_string exn)); 87 + (* Test data subdirectory *) 88 + let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in 89 + let db_file = Eio.Path.(db_path / "main.db") in 90 + (try 91 + let content = Eio.Path.load db_file in 92 + Printf.printf "\ndata file content: %s" (String.trim content) 93 + with exn -> 94 + Printf.printf "\ndata file error: %s" (Printexc.to_string exn)); 95 + (* Test cache subdirectory *) 96 + let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in 97 + let cache_file = Eio.Path.(cache_path / "thumb1.png") in 98 + (try 99 + let content = Eio.Path.load cache_file in 100 + Printf.printf "\ncache file content: %s" (String.trim content) 101 + with exn -> 102 + Printf.printf "\ncache file error: %s" (Printexc.to_string exn)); 103 + (* Test state subdirectory *) 104 + let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in 105 + let log_file = Eio.Path.(logs_path / "app.log") in 106 + try 107 + let content = Eio.Path.load log_file in 108 + Printf.printf "\nstate file content: %s\n" (String.trim content) 109 + with exn -> 110 + Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn)
+4
test/test_paths.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*)
+379
test/xdg.t
··· 1 + Test with default directories: 2 + 3 + $ export HOME=./test_home 4 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 5 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 6 + $ ./xdg_example.exe 7 + === Cmdliner Config === 8 + XDG config: 9 + 10 + === XDG Directories === 11 + XDG directories for 'xdg_example': 12 + User directories: 13 + config: <fs:./test_home/./test_home/.config/xdg_example> [default] 14 + data: <fs:./test_home/./test_home/.local/share/xdg_example> [default] 15 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 16 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 17 + runtime: <none> [default] 18 + System directories: 19 + config_dirs: [<fs:/etc/xdg/xdg_example>] 20 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 21 + 22 + No command-line args or env vars are set, so all directories use defaults. 23 + Config shows empty (no overrides), and directories show [default] source. User 24 + directories follow XDG spec: ~/.config, ~/.local/share, ~/.cache, 25 + ~/.local/state. Runtime dir is <none> since XDG_RUNTIME_DIR has no default. 26 + System dirs use XDG spec defaults: /etc/xdg for config, /usr/{local/,}share for 27 + data. 28 + 29 + Test with all command line arguments specified 30 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 31 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 32 + $ ./xdg_example.exe \ 33 + > --config-dir ./test-config \ 34 + > --data-dir ./test-data \ 35 + > --cache-dir ./test-cache \ 36 + > --state-dir ./test-state \ 37 + > --runtime-dir ./test-runtime 38 + === Cmdliner Config === 39 + XDG config: 40 + config_dir: ./test-config [cmdline] 41 + data_dir: ./test-data [cmdline] 42 + cache_dir: ./test-cache [cmdline] 43 + state_dir: ./test-state [cmdline] 44 + runtime_dir: ./test-runtime [cmdline] 45 + 46 + === XDG Directories === 47 + XDG directories for 'xdg_example': 48 + User directories: 49 + config: <fs:./test_home/./test-config> [cmdline] 50 + data: <fs:./test_home/./test-data> [cmdline] 51 + cache: <fs:./test_home/./test-cache> [cmdline] 52 + state: <fs:./test_home/./test-state> [cmdline] 53 + runtime: <fs:./test_home/./test-runtime> [cmdline] 54 + System directories: 55 + config_dirs: [<fs:/etc/xdg/xdg_example>] 56 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 57 + 58 + All user directories are overridden by command-line arguments, showing 59 + [cmdline] as the source. The config section shows all overrides with their 60 + values and [cmdline] sources. System directories remain at their defaults since 61 + they cannot be overridden by user directories command-line options. 62 + 63 + Test with environment variables (app-specific) 64 + $ XDG_EXAMPLE_CONFIG_DIR=./env-config \ 65 + > XDG_EXAMPLE_DATA_DIR=./env-data \ 66 + > XDG_EXAMPLE_CACHE_DIR=./env-cache \ 67 + > XDG_EXAMPLE_STATE_DIR=./env-state \ 68 + > XDG_EXAMPLE_RUNTIME_DIR=./env-runtime \ 69 + > ./xdg_example.exe 70 + === Cmdliner Config === 71 + XDG config: 72 + config_dir: ./env-config [env(XDG_EXAMPLE_CONFIG_DIR)] 73 + data_dir: ./env-data [env(XDG_EXAMPLE_DATA_DIR)] 74 + cache_dir: ./env-cache [env(XDG_EXAMPLE_CACHE_DIR)] 75 + state_dir: ./env-state [env(XDG_EXAMPLE_STATE_DIR)] 76 + runtime_dir: ./env-runtime [env(XDG_EXAMPLE_RUNTIME_DIR)] 77 + 78 + === XDG Directories === 79 + XDG directories for 'xdg_example': 80 + User directories: 81 + config: <fs:./test_home/./env-config> [env(XDG_EXAMPLE_CONFIG_DIR)] 82 + data: <fs:./test_home/./env-data> [env(XDG_EXAMPLE_DATA_DIR)] 83 + cache: <fs:./test_home/./env-cache> [env(XDG_EXAMPLE_CACHE_DIR)] 84 + state: <fs:./test_home/./env-state> [env(XDG_EXAMPLE_STATE_DIR)] 85 + runtime: <fs:./test_home/./env-runtime> [env(XDG_EXAMPLE_RUNTIME_DIR)] 86 + System directories: 87 + config_dirs: [<fs:/etc/xdg/xdg_example>] 88 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 89 + 90 + App-specific environment variables (XDG_EXAMPLE_*) override the defaults. The 91 + source correctly shows [env(XDG_EXAMPLE_*)] for each variable. These 92 + app-specific variables take precedence over XDG standard variables when both 93 + are available, allowing per-application customization. 94 + 95 + Test with standard XDG environment variables: 96 + 97 + $ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \ 98 + > XDG_DATA_HOME=/tmp/xdge/xdg-data \ 99 + > XDG_CACHE_HOME=/tmp/xdge/xdg-cache \ 100 + > XDG_STATE_HOME=/tmp/xdge/xdg-state \ 101 + > XDG_RUNTIME_DIR=/tmp/xdge/xdg-runtime \ 102 + > ./xdg_example.exe 103 + === Cmdliner Config === 104 + XDG config: 105 + config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)] 106 + data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)] 107 + cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)] 108 + state_dir: /tmp/xdge/xdg-state [env(XDG_STATE_HOME)] 109 + runtime_dir: /tmp/xdge/xdg-runtime [env(XDG_RUNTIME_DIR)] 110 + 111 + === XDG Directories === 112 + XDG directories for 'xdg_example': 113 + User directories: 114 + config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)] 115 + data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)] 116 + cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)] 117 + state: <fs:/tmp/xdge/xdg-state> [env(XDG_STATE_HOME)] 118 + runtime: <fs:/tmp/xdge/xdg-runtime> [env(XDG_RUNTIME_DIR)] 119 + System directories: 120 + config_dirs: [<fs:/etc/xdg/xdg_example>] 121 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 122 + 123 + Standard XDG environment variables (XDG_*_HOME, XDG_RUNTIME_DIR) override the 124 + defaults. The source correctly shows [env(XDG_*)] for each variable. Note that 125 + the user directories use the raw paths from env vars (not app-specific subdirs) 126 + since XDG_CONFIG_HOME etc. are intended to be the base directories for the 127 + user. 128 + 129 + Test command line overrides environment variables: 130 + 131 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 132 + $ XDG_EXAMPLE_CONFIG_DIR=./env-config \ 133 + > ./xdg_example.exe --config-dir ./cli-config 134 + === Cmdliner Config === 135 + XDG config: 136 + config_dir: ./cli-config [cmdline] 137 + 138 + === XDG Directories === 139 + XDG directories for 'xdg_example': 140 + User directories: 141 + config: <fs:./test_home/./cli-config> [cmdline] 142 + data: <fs:./test_home/./test_home/.local/share/xdg_example> [default] 143 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 144 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 145 + runtime: <none> [default] 146 + System directories: 147 + config_dirs: [<fs:/etc/xdg/xdg_example>] 148 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 149 + 150 + Command-line arguments have highest precedence, overriding environment 151 + variables. Only config_dir is shown in the config section since it is the only 152 + one explicitly set. The config_dir shows [cmdline] source while other 153 + directories fall back to defaults, demonstrating the precedence hierarchy: of 154 + cmdline then app env vars then XDG env vars then defaults. 155 + 156 + Test mixed environment variable precedence (app-specific overrides XDG 157 + standard): 158 + 159 + $ export HOME=./test_home 160 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 161 + $ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \ 162 + > XDG_EXAMPLE_CONFIG_DIR=./app-config \ 163 + > XDG_DATA_HOME=/tmp/xdge/xdg-data \ 164 + > XDG_EXAMPLE_DATA_DIR=./app-data \ 165 + > ./xdg_example.exe 166 + === Cmdliner Config === 167 + XDG config: 168 + config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)] 169 + data_dir: ./app-data [env(XDG_EXAMPLE_DATA_DIR)] 170 + 171 + === XDG Directories === 172 + XDG directories for 'xdg_example': 173 + User directories: 174 + config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)] 175 + data: <fs:./test_home/./app-data> [env(XDG_EXAMPLE_DATA_DIR)] 176 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 177 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 178 + runtime: <none> [default] 179 + System directories: 180 + config_dirs: [<fs:/etc/xdg/xdg_example>] 181 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 182 + 183 + Demonstrates app-specific environment variables taking precedence over XDG 184 + standard ones. Both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR are set, but the 185 + app-specific one wins. Same for data directories. Cache, state, and runtime 186 + fall back to defaults since no variables are set for them. 187 + 188 + Test partial environment variable override: 189 + 190 + $ export HOME=./test_home 191 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 192 + $ XDG_EXAMPLE_CONFIG_DIR=./app-config \ 193 + > XDG_DATA_HOME=/tmp/xdge/xdg-data \ 194 + > XDG_CACHE_HOME=/tmp/xdge/xdg-cache \ 195 + > ./xdg_example.exe 196 + === Cmdliner Config === 197 + XDG config: 198 + config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)] 199 + data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)] 200 + cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)] 201 + 202 + === XDG Directories === 203 + XDG directories for 'xdg_example': 204 + User directories: 205 + config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)] 206 + data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)] 207 + cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)] 208 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 209 + runtime: <none> [default] 210 + System directories: 211 + config_dirs: [<fs:/etc/xdg/xdg_example>] 212 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 213 + 214 + Shows mixed sources working together. Config uses app-specific env var (highest 215 + priority among env vars), data and cache use XDG standard env vars (no 216 + app-specific ones set), and state uses default (no env vars set). Each 217 + directory gets its value from the highest-priority available source. 218 + 219 + Test command line overrides mixed environment variables: 220 + 221 + $ export HOME=./test_home 222 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 223 + $ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \ 224 + > XDG_EXAMPLE_CONFIG_DIR=./app-config \ 225 + > ./xdg_example.exe --config-dir ./cli-config 226 + === Cmdliner Config === 227 + XDG config: 228 + config_dir: ./cli-config [cmdline] 229 + 230 + === XDG Directories === 231 + XDG directories for 'xdg_example': 232 + User directories: 233 + config: <fs:./test_home/./cli-config> [cmdline] 234 + data: <fs:./test_home/./test_home/.local/share/xdg_example> [default] 235 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 236 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 237 + runtime: <none> [default] 238 + System directories: 239 + config_dirs: [<fs:/etc/xdg/xdg_example>] 240 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 241 + 242 + Command-line argument overrides both types of environment variables. Even 243 + though both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR are set, the 244 + --config-dir flag takes precedence and shows [cmdline] source. Other 245 + directories fall back to defaults since no other command-line args are 246 + provided. 247 + 248 + Test empty environment variable handling: 249 + $ export HOME=./test_home 250 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 251 + $ XDG_EXAMPLE_CONFIG_DIR="" \ 252 + > XDG_CONFIG_HOME=/tmp/xdge/xdg-config \ 253 + > ./xdg_example.exe 254 + === Cmdliner Config === 255 + XDG config: 256 + config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)] 257 + 258 + === XDG Directories === 259 + XDG directories for 'xdg_example': 260 + User directories: 261 + config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)] 262 + data: <fs:./test_home/./test_home/.local/share/xdg_example> [default] 263 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 264 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 265 + runtime: <none> [default] 266 + System directories: 267 + config_dirs: [<fs:/etc/xdg/xdg_example>] 268 + data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>] 269 + 270 + When an app-specific env var is empty (""), it falls back to the XDG standard 271 + variable. XDG_EXAMPLE_CONFIG_DIR="" is ignored, so XDG_CONFIG_HOME is used 272 + instead, correctly showing [env(XDG_CONFIG_HOME)] as the source. This behavior 273 + ensures that empty app-specific variables do not override useful XDG standard 274 + settings. 275 + 276 + Test system directory environment variables: 277 + 278 + $ export HOME=./test_home 279 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 280 + $ XDG_CONFIG_DIRS=/tmp/xdge/sys1:/tmp/xdge/sys2 \ 281 + > XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2 \ 282 + > ./xdg_example.exe 283 + === Cmdliner Config === 284 + XDG config: 285 + 286 + === XDG Directories === 287 + XDG directories for 'xdg_example': 288 + User directories: 289 + config: <fs:./test_home/./test_home/.config/xdg_example> [default] 290 + data: <fs:./test_home/./test_home/.local/share/xdg_example> [default] 291 + cache: <fs:./test_home/./test_home/.cache/xdg_example> [default] 292 + state: <fs:./test_home/./test_home/.local/state/xdg_example> [default] 293 + runtime: <none> [default] 294 + System directories: 295 + config_dirs: [<fs:/tmp/xdge/sys1/xdg_example>; 296 + <fs:/tmp/xdge/sys2/xdg_example>] 297 + data_dirs: [<fs:/tmp/xdge/data1/xdg_example>; 298 + <fs:/tmp/xdge/data2/xdg_example>] 299 + 300 + XDG_CONFIG_DIRS and XDG_DATA_DIRS environment variables override the default 301 + system directories. The colon-separated paths are parsed and the app name is 302 + appended to each path. User directories remain at defaults since no user-level 303 + overrides are provided. System directory env vars only affect the system 304 + directories, not user directories. 305 + 306 + Test _path functions do not create directories but can access files within them: 307 + 308 + $ export HOME=/tmp/xdge/xdg_path_test 309 + $ mkdir -p /tmp/xdge/xdg_path_test 310 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 311 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 312 + Create config subdirectory manually and write a test file: 313 + $ mkdir -p "/tmp/xdge/xdg_path_test/.config/path_test/profiles" 314 + $ echo "test profile content" > "/tmp/xdge/xdg_path_test/.config/path_test/profiles/default.json" 315 + Create data subdirectory manually and write a test file: 316 + $ mkdir -p "/tmp/xdge/xdg_path_test/.local/share/path_test/databases" 317 + $ echo "test database content" > "/tmp/xdge/xdg_path_test/.local/share/path_test/databases/main.db" 318 + Create cache subdirectory manually and write a test file: 319 + $ mkdir -p "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails" 320 + $ echo "test cache content" > "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails/thumb1.png" 321 + Create state subdirectory manually and write a test file: 322 + $ mkdir -p "/tmp/xdge/xdg_path_test/.local/state/path_test/logs" 323 + $ echo "test log content" > "/tmp/xdge/xdg_path_test/.local/state/path_test/logs/app.log" 324 + 325 + Now test that we can read the files through the XDG _path functions: 326 + $ ./test_paths.exe 327 + config file content: test profile content 328 + data file content: test database content 329 + cache file content: test cache content 330 + state file content: test log content 331 + 332 + This test verifies that the _path functions return correct paths that can be used to access 333 + files within XDG subdirectories, without the functions automatically creating those directories. 334 + 335 + Test path resolution with --show-paths: 336 + 337 + Test with a preset HOME to verify correct path resolution: 338 + $ export HOME=./home_testuser 339 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 340 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 341 + $ ./xdg_example.exe --show-paths 342 + config_dir: ./home_testuser/./home_testuser/.config/xdg_example 343 + data_dir: ./home_testuser/./home_testuser/.local/share/xdg_example 344 + cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example 345 + state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example 346 + runtime_dir: <none> 347 + config_dirs: /etc/xdg/xdg_example 348 + data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example 349 + 350 + Test with environment variables set: 351 + $ export HOME=./home_testuser 352 + $ export XDG_CONFIG_HOME=/tmp/xdge/config 353 + $ export XDG_DATA_HOME=/tmp/xdge/data 354 + $ export XDG_CACHE_HOME=/tmp/xdge/cache 355 + $ export XDG_STATE_HOME=/tmp/xdge/state 356 + $ export XDG_CONFIG_DIRS=/tmp/xdge/config1:/tmp/xdge/config2 357 + $ export XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2 358 + $ ./xdg_example.exe --show-paths 359 + config_dir: /tmp/xdge/config 360 + data_dir: /tmp/xdge/data 361 + cache_dir: /tmp/xdge/cache 362 + state_dir: /tmp/xdge/state 363 + runtime_dir: <none> 364 + config_dirs: /tmp/xdge/config1/xdg_example:/tmp/xdge/config2/xdg_example 365 + data_dirs: /tmp/xdge/data1/xdg_example:/tmp/xdge/data2/xdg_example 366 + 367 + Test with command-line overrides: 368 + $ export HOME=./home_testuser 369 + $ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR 370 + $ unset XDG_CONFIG_DIRS XDG_DATA_DIRS 371 + $ ./xdg_example.exe --show-paths --config-dir ./override/config --data-dir ./override/data 372 + config_dir: ./home_testuser/./override/config 373 + data_dir: ./home_testuser/./override/data 374 + cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example 375 + state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example 376 + runtime_dir: <none> 377 + config_dirs: /etc/xdg/xdg_example 378 + data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example 379 +
+39
test/xdg_example.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let run (xdg, cfg) = 7 + Fmt.pr "%a@.%a@.@.%a@.%a@." 8 + Fmt.(styled `Bold string) 9 + "=== Cmdliner Config ===" Xdge.Cmd.pp cfg 10 + Fmt.(styled `Bold string) 11 + "=== XDG Directories ===" 12 + (Xdge.pp ~brief:false ~sources:true) 13 + xdg 14 + 15 + open Cmdliner 16 + 17 + let () = 18 + Fmt.set_style_renderer Fmt.stdout `Ansi_tty; 19 + let app_name = "xdg_example" in 20 + let doc = 21 + "Example program demonstrating XDG directory selection with Cmdliner" 22 + in 23 + let man = 24 + [ 25 + `S Manpage.s_description; 26 + `P 27 + "This example shows how to use the Xdge library with Cmdliner to \ 28 + handle XDG Base Directory Specification paths with command-line and \ 29 + environment variable overrides."; 30 + `S Manpage.s_environment; 31 + `P (Xdge.Cmd.env_docs app_name); 32 + ] 33 + in 34 + let info = Cmdliner.Cmd.info "xdg_example" ~version:"1.0" ~doc ~man in 35 + Eio_main.run @@ fun env -> 36 + let create_xdg_term = Xdge.Cmd.term app_name env#fs () in 37 + let main_term = Term.(const run $ create_xdg_term) in 38 + let cmd = Cmdliner.Cmd.v info main_term in 39 + exit @@ Cmdliner.Cmd.eval cmd
+4
test/xdg_example.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*)
+28
xdge.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "XDG Base Directory Specification support for Eio" 4 + description: 5 + "This library implements the XDG Base Directory Specification with Eio capabilities to provide safe access to configuration, data, cache, state, and runtime directories. The library exposes Cmdliner terms that allow for proper environment variable overrides and command-line flags." 6 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 + authors: ["Anil Madhavapeddy"] 8 + license: "ISC" 9 + homepage: "https://tangled.sh/@anil.recoil.org/xdge" 10 + bug-reports: "https://tangled.sh/@anil.recoil.org/xdge/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.1.0"} 14 + "eio" {>= "1.1"} 15 + "cmdliner" {>= "1.2.0"} 16 + "fmt" 17 + "xdg" 18 + "eio_main" {with-test} 19 + "odoc" {with-doc} 20 + "alcotest" {with-test & >= "1.7.0"} 21 + ] 22 + x-maintenance-intent: ["(latest)"] 23 + build: [ 24 + [ "dune" "subst" ] {dev} 25 + [ "dune" "build" "-p" name "-j" jobs "@install" ] 26 + [ "dune" "build" "-p" name "-j" jobs "runtest" ] {with-test & opam-version >= "2.2"} 27 + [ "dune" "build" "-p" name "-j" jobs "@doc" ] {with-doc} 28 + ]
+6
xdge.opam.template
··· 1 + build: [ 2 + [ "dune" "subst" ] {dev} 3 + [ "dune" "build" "-p" name "-j" jobs "@install" ] 4 + [ "dune" "build" "-p" name "-j" jobs "runtest" ] {with-test & opam-version >= "2.2"} 5 + [ "dune" "build" "-p" name "-j" jobs "@doc" ] {with-doc} 6 + ]