XDG library path support for OCaml via Eio capabilities
linux macos ocaml xdg

initial import of xdge

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