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

initial import of xdge

+1
.gitignore
···
+
_build
+1
.ocamlformat
···
+
version=0.28.1
+44
.tangled/workflows/build.yml
···
+
when:
+
- event: ["push", "pull_request"]
+
branch: ["main"]
+
+
dependencies:
+
nixpkgs:
+
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
+
- gcc
+
- ocaml
+
+
steps:
+
- name: opam
+
command: |
+
opam init --disable-sandboxing -any
+
- name: switch
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only
+
- name: build
+
command: |
+
opam exec -- dune build --verbose
+
- name: test
+
command: |
+
opam exec -- dune runtest --verbose
+
- name: doc
+
command: |
+
opam install -y odoc
+
opam exec -- dune build @doc
+18
LICENSE.md
···
+
(*
+
* ISC License
+
*
+
* Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
*
+
* Permission to use, copy, modify, and distribute this software for any
+
* purpose with or without fee is hereby granted, provided that the above
+
* copyright notice and this permission notice appear in all copies.
+
*
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
*
+
*)
+30
dune-project
···
+
(lang dune 3.20)
+
+
(name xdge)
+
+
(generate_opam_files true)
+
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.sh/@anil.recoil.org/xdge")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports "https://tangled.sh/@anil.recoil.org/xgde/issues")
+
(maintenance_intent "(latest)")
+
+
(package
+
(name xdge)
+
(synopsis "XDG Base Directory Specification support for Eio")
+
(description
+
"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.")
+
(depends
+
(ocaml (>= 5.1.0))
+
(eio (>= 1.1))
+
(cmdliner (>= 1.2.0))
+
(fmt (>= 0.11.0))
+
(odoc :with-doc)
+
(eio_main :with-test)
+
(alcotest (and :with-test (>= 1.7.0)))))
+4
lib/dune
···
+
(library
+
(public_name xdge)
+
(name xdge)
+
(libraries eio eio_main xdg cmdliner fmt))
+657
lib/xdge.ml
···
+
type source = Default | Env of string | Cmdline
+
+
type t = {
+
app_name : string;
+
config_dir : Eio.Fs.dir_ty Eio.Path.t;
+
config_dir_source : source;
+
data_dir : Eio.Fs.dir_ty Eio.Path.t;
+
data_dir_source : source;
+
cache_dir : Eio.Fs.dir_ty Eio.Path.t;
+
cache_dir_source : source;
+
state_dir : Eio.Fs.dir_ty Eio.Path.t;
+
state_dir_source : source;
+
runtime_dir : Eio.Fs.dir_ty Eio.Path.t option;
+
runtime_dir_source : source;
+
config_dirs : Eio.Fs.dir_ty Eio.Path.t list;
+
data_dirs : Eio.Fs.dir_ty Eio.Path.t list;
+
}
+
+
let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
+
+
let validate_runtime_base_dir base_path =
+
(* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
+
try
+
let path_str = Eio.Path.native_exn base_path in
+
let stat = Eio.Path.stat ~follow:true base_path in
+
let current_perm = stat.perm land 0o777 in
+
if current_perm <> 0o700 then
+
failwith
+
(Printf.sprintf
+
"XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o \
+
(must be 0700)"
+
path_str current_perm);
+
(* Check ownership - directory should be owned by current user *)
+
let uid = Unix.getuid () in
+
if stat.uid <> Int64.of_int uid then
+
failwith
+
(Printf.sprintf
+
"XDG_RUNTIME_DIR base directory %s not owned by current user (uid \
+
%d, owner %Ld)"
+
path_str uid stat.uid)
+
(* TODO: Check that directory is on local filesystem (not networked).
+
This would require filesystem type detection which is OS-specific. *)
+
with exn ->
+
failwith
+
(Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s"
+
(Printexc.to_string exn))
+
+
let ensure_runtime_dir _fs app_runtime_path =
+
(* Base directory validation is done in resolve_runtime_dir,
+
so we just create the app subdirectory *)
+
ensure_dir app_runtime_path
+
+
let get_home_dir fs =
+
let home_str =
+
match Sys.getenv_opt "HOME" with
+
| Some home -> home
+
| None -> (
+
match Sys.os_type with
+
| "Win32" | "Cygwin" -> (
+
match Sys.getenv_opt "USERPROFILE" with
+
| Some profile -> profile
+
| None -> failwith "Cannot determine home directory")
+
| _ -> (
+
try Unix.((getpwuid (getuid ())).pw_dir)
+
with _ -> failwith "Cannot determine home directory"))
+
in
+
Eio.Path.(fs / home_str)
+
+
let make_env_var_name app_name suffix =
+
String.uppercase_ascii app_name ^ "_" ^ suffix
+
+
exception Invalid_xdg_path of string
+
+
let validate_absolute_path context path =
+
if Filename.is_relative path then
+
raise
+
(Invalid_xdg_path
+
(Printf.sprintf "%s must be an absolute path, got: %s" context path))
+
+
let resolve_path fs home_path base_path =
+
if Filename.is_relative base_path then Eio.Path.(home_path / base_path)
+
else Eio.Path.(fs / base_path)
+
+
(* Helper to resolve system directories (config_dirs or data_dirs) *)
+
let resolve_system_dirs fs home_path app_name override_suffix xdg_var
+
default_paths =
+
let override_var = make_env_var_name app_name override_suffix in
+
match Sys.getenv_opt override_var with
+
| Some dirs when dirs <> "" ->
+
String.split_on_char ':' dirs
+
|> List.filter (fun s -> s <> "")
+
|> List.filter_map (fun path ->
+
try
+
validate_absolute_path override_var path;
+
Some Eio.Path.(resolve_path fs home_path path / app_name)
+
with Invalid_xdg_path _ -> None)
+
| Some _ | None -> (
+
match Sys.getenv_opt xdg_var with
+
| Some dirs when dirs <> "" ->
+
String.split_on_char ':' dirs
+
|> List.filter (fun s -> s <> "")
+
|> List.filter_map (fun path ->
+
try
+
validate_absolute_path xdg_var path;
+
Some Eio.Path.(resolve_path fs home_path path / app_name)
+
with Invalid_xdg_path _ -> None)
+
| Some _ | None ->
+
List.map
+
(fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
+
default_paths)
+
+
(* Helper to resolve a user directory with override precedence *)
+
let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix =
+
let override_var = make_env_var_name app_name override_suffix in
+
match Sys.getenv_opt override_var with
+
| Some dir when dir <> "" ->
+
validate_absolute_path override_var dir;
+
(Eio.Path.(fs / dir / app_name), Env override_var)
+
| Some _ | None ->
+
let xdg_base = xdg_getter xdg_ctx in
+
let base_path = resolve_path fs home_path xdg_base in
+
(Eio.Path.(base_path / app_name), Default)
+
+
(* Helper to resolve runtime directory (special case since it can be None) *)
+
let resolve_runtime_dir fs home_path app_name xdg_ctx =
+
let override_var = make_env_var_name app_name "RUNTIME_DIR" in
+
match Sys.getenv_opt override_var with
+
| Some dir when dir <> "" ->
+
validate_absolute_path override_var dir;
+
(* Validate the base runtime directory has correct permissions *)
+
let base_runtime_dir = resolve_path fs home_path dir in
+
validate_runtime_base_dir base_runtime_dir;
+
(Some Eio.Path.(base_runtime_dir / app_name), Env override_var)
+
| Some _ | None ->
+
( (match Xdg.runtime_dir xdg_ctx with
+
| Some base ->
+
(* Validate the base runtime directory has correct permissions *)
+
let base_runtime_dir = resolve_path fs home_path base in
+
validate_runtime_base_dir base_runtime_dir;
+
Some Eio.Path.(base_runtime_dir / app_name)
+
| None -> None),
+
Default )
+
+
let validate_standard_xdg_vars () =
+
(* Validate standard XDG environment variables for absolute paths *)
+
let xdg_vars =
+
[
+
"XDG_CONFIG_HOME";
+
"XDG_DATA_HOME";
+
"XDG_CACHE_HOME";
+
"XDG_STATE_HOME";
+
"XDG_RUNTIME_DIR";
+
"XDG_CONFIG_DIRS";
+
"XDG_DATA_DIRS";
+
]
+
in
+
List.iter
+
(fun var ->
+
match Sys.getenv_opt var with
+
| Some value when value <> "" ->
+
if String.contains value ':' then
+
(* Colon-separated list - validate each part *)
+
String.split_on_char ':' value
+
|> List.filter (fun s -> s <> "")
+
|> List.iter (fun path -> validate_absolute_path var path)
+
else
+
(* Single path *)
+
validate_absolute_path var value
+
| _ -> ())
+
xdg_vars
+
+
let create fs app_name =
+
let fs = fs in
+
let home_path = get_home_dir fs in
+
(* First validate all standard XDG environment variables *)
+
validate_standard_xdg_vars ();
+
let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
+
(* User directories *)
+
let config_dir, config_dir_source =
+
resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR"
+
in
+
let data_dir, data_dir_source =
+
resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR"
+
in
+
let cache_dir, cache_dir_source =
+
resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR"
+
in
+
let state_dir, state_dir_source =
+
resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR"
+
in
+
(* Runtime directory *)
+
let runtime_dir, runtime_dir_source =
+
resolve_runtime_dir fs home_path app_name xdg_ctx
+
in
+
(* System directories *)
+
let config_dirs =
+
resolve_system_dirs fs home_path app_name "CONFIG_DIRS" "XDG_CONFIG_DIRS"
+
[ "/etc/xdg" ]
+
in
+
let data_dirs =
+
resolve_system_dirs fs home_path app_name "DATA_DIRS" "XDG_DATA_DIRS"
+
[ "/usr/local/share"; "/usr/share" ]
+
in
+
ensure_dir config_dir;
+
ensure_dir data_dir;
+
ensure_dir cache_dir;
+
ensure_dir state_dir;
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
+
{
+
app_name;
+
config_dir;
+
config_dir_source;
+
data_dir;
+
data_dir_source;
+
cache_dir;
+
cache_dir_source;
+
state_dir;
+
state_dir_source;
+
runtime_dir;
+
runtime_dir_source;
+
config_dirs;
+
data_dirs;
+
}
+
+
let app_name t = t.app_name
+
let config_dir t = t.config_dir
+
let data_dir t = t.data_dir
+
let cache_dir t = t.cache_dir
+
let state_dir t = t.state_dir
+
let runtime_dir t = t.runtime_dir
+
let config_dirs t = t.config_dirs
+
let data_dirs t = t.data_dirs
+
+
(* File search following XDG specification *)
+
let find_file_in_dirs dirs filename =
+
let rec search_dirs = function
+
| [] -> None
+
| dir :: remaining_dirs -> (
+
let file_path = Eio.Path.(dir / filename) in
+
try
+
(* Try to check if file exists and is readable *)
+
let _ = Eio.Path.stat ~follow:true file_path in
+
Some file_path
+
with _ ->
+
(* File is inaccessible (non-existent, permissions, etc.)
+
Skip and continue with next directory per XDG spec *)
+
search_dirs remaining_dirs)
+
in
+
search_dirs dirs
+
+
let find_config_file t filename =
+
(* Search user config dir first, then system config dirs *)
+
find_file_in_dirs (t.config_dir :: t.config_dirs) filename
+
+
let find_data_file t filename =
+
(* Search user data dir first, then system data dirs *)
+
find_file_in_dirs (t.data_dir :: t.data_dirs) filename
+
+
let pp ?(brief = false) ?(sources = false) ppf t =
+
let pp_source ppf = function
+
| Default -> Fmt.(styled `Faint string) ppf "default"
+
| Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
+
| Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
+
in
+
let pp_path_with_source ppf path source =
+
if sources then
+
Fmt.pf ppf "%a %a"
+
Fmt.(styled `Green Eio.Path.pp)
+
path
+
Fmt.(styled `Faint (brackets pp_source))
+
source
+
else Fmt.(styled `Green Eio.Path.pp) ppf path
+
in
+
let pp_path_opt_with_source ppf path_opt source =
+
match path_opt with
+
| None ->
+
if sources then
+
Fmt.pf ppf "%a %a"
+
Fmt.(styled `Red string)
+
"<none>"
+
Fmt.(styled `Faint (brackets pp_source))
+
source
+
else Fmt.(styled `Red string) ppf "<none>"
+
| Some path -> pp_path_with_source ppf path source
+
in
+
let pp_paths ppf paths =
+
Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths
+
in
+
if brief then
+
Fmt.pf ppf "%a config=%a data=%a>"
+
Fmt.(styled `Cyan string)
+
("<xdg:" ^ t.app_name)
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.config_dir, t.config_dir_source)
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.data_dir, t.data_dir_source)
+
else (
+
Fmt.pf ppf "@[<v>%a@,"
+
Fmt.(styled `Bold string)
+
("XDG directories for '" ^ t.app_name ^ "':");
+
Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:";
+
Fmt.pf ppf "%a %a@,"
+
Fmt.(styled `Cyan string)
+
"config:"
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.config_dir, t.config_dir_source);
+
Fmt.pf ppf "%a %a@,"
+
Fmt.(styled `Cyan string)
+
"data:"
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.data_dir, t.data_dir_source);
+
Fmt.pf ppf "%a %a@,"
+
Fmt.(styled `Cyan string)
+
"cache:"
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.cache_dir, t.cache_dir_source);
+
Fmt.pf ppf "%a %a@,"
+
Fmt.(styled `Cyan string)
+
"state:"
+
(fun ppf (path, source) -> pp_path_with_source ppf path source)
+
(t.state_dir, t.state_dir_source);
+
Fmt.pf ppf "%a %a@]@,"
+
Fmt.(styled `Cyan string)
+
"runtime:"
+
(fun ppf (path_opt, source) ->
+
pp_path_opt_with_source ppf path_opt source)
+
(t.runtime_dir, t.runtime_dir_source);
+
Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:";
+
Fmt.pf ppf "%a [@[<hov>%a@]]@,"
+
Fmt.(styled `Cyan string)
+
"config_dirs:" pp_paths t.config_dirs;
+
Fmt.pf ppf "%a [@[<hov>%a@]]@]@]"
+
Fmt.(styled `Cyan string)
+
"data_dirs:" pp_paths t.data_dirs)
+
+
module Cmd = struct
+
type xdg_t = t
+
type 'a with_source = { value : 'a option; source : source }
+
+
type t = {
+
config_dir : string with_source;
+
data_dir : string with_source;
+
cache_dir : string with_source;
+
state_dir : string with_source;
+
runtime_dir : string with_source;
+
}
+
+
type dir = [ `Config | `Cache | `Data | `State | `Runtime ]
+
+
let term app_name fs ?(dirs = [ `Config; `Data; `Cache; `State; `Runtime ]) ()
+
=
+
let open Cmdliner in
+
let app_upper = String.uppercase_ascii app_name in
+
let show_paths =
+
let doc = "Show only the resolved directory paths without formatting" in
+
Arg.(value & flag & info [ "show-paths" ] ~doc)
+
in
+
let has_dir d = List.mem d dirs in
+
let make_dir_arg ~enabled name env_suffix xdg_var default_path =
+
if not enabled then
+
(* Return a term that always gives the environment-only result *)
+
Term.(
+
const (fun () ->
+
let app_env = app_upper ^ "_" ^ env_suffix in
+
match Sys.getenv_opt app_env with
+
| Some v when v <> "" -> { value = Some v; source = Env app_env }
+
| Some _ | None -> (
+
match Sys.getenv_opt xdg_var with
+
| Some v -> { value = Some v; source = Env xdg_var }
+
| None -> { value = None; source = Default }))
+
$ const ())
+
else
+
let app_env = app_upper ^ "_" ^ env_suffix in
+
let doc =
+
match default_path with
+
| Some path ->
+
Printf.sprintf
+
"Override %s directory. Can also be set with %s or %s. \
+
Default: %s"
+
name app_env xdg_var path
+
| None ->
+
Printf.sprintf
+
"Override %s directory. Can also be set with %s or %s. No \
+
default value."
+
name app_env xdg_var
+
in
+
let arg =
+
Arg.(
+
value
+
& opt (some string) None
+
& info [ name ^ "-dir" ] ~docv:"DIR" ~doc)
+
in
+
Term.(
+
const (fun cmdline_val ->
+
match cmdline_val with
+
| Some v -> { value = Some v; source = Cmdline }
+
| None -> (
+
match Sys.getenv_opt app_env with
+
| Some v when v <> "" ->
+
{ value = Some v; source = Env app_env }
+
| Some _ | None -> (
+
match Sys.getenv_opt xdg_var with
+
| Some v -> { value = Some v; source = Env xdg_var }
+
| None -> { value = None; source = Default })))
+
$ arg)
+
in
+
let home_prefix = "\\$HOME" in
+
let config_dir =
+
make_dir_arg ~enabled:(has_dir `Config) "config" "CONFIG_DIR"
+
"XDG_CONFIG_HOME"
+
(Some (home_prefix ^ "/.config/" ^ app_name))
+
in
+
let data_dir =
+
make_dir_arg ~enabled:(has_dir `Data) "data" "DATA_DIR" "XDG_DATA_HOME"
+
(Some (home_prefix ^ "/.local/share/" ^ app_name))
+
in
+
let cache_dir =
+
make_dir_arg ~enabled:(has_dir `Cache) "cache" "CACHE_DIR"
+
"XDG_CACHE_HOME"
+
(Some (home_prefix ^ "/.cache/" ^ app_name))
+
in
+
let state_dir =
+
make_dir_arg ~enabled:(has_dir `State) "state" "STATE_DIR"
+
"XDG_STATE_HOME"
+
(Some (home_prefix ^ "/.local/state/" ^ app_name))
+
in
+
let runtime_dir =
+
make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR"
+
"XDG_RUNTIME_DIR" None
+
in
+
Term.(
+
const
+
(fun
+
show_paths_flag
+
config_dir_ws
+
data_dir_ws
+
cache_dir_ws
+
state_dir_ws
+
runtime_dir_ws
+
->
+
let config =
+
{
+
config_dir = config_dir_ws;
+
data_dir = data_dir_ws;
+
cache_dir = cache_dir_ws;
+
state_dir = state_dir_ws;
+
runtime_dir = runtime_dir_ws;
+
}
+
in
+
let home_path = get_home_dir fs in
+
(* First validate all standard XDG environment variables *)
+
validate_standard_xdg_vars ();
+
let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
+
(* Helper to resolve directory from config with source tracking *)
+
let resolve_from_config config_ws xdg_getter =
+
match config_ws.value with
+
| Some dir -> (resolve_path fs home_path dir, config_ws.source)
+
| None ->
+
let xdg_base = xdg_getter xdg_ctx in
+
let base_path = resolve_path fs home_path xdg_base in
+
(Eio.Path.(base_path / app_name), config_ws.source)
+
in
+
(* User directories *)
+
let config_dir, config_dir_source =
+
resolve_from_config config.config_dir Xdg.config_dir
+
in
+
let data_dir, data_dir_source =
+
resolve_from_config config.data_dir Xdg.data_dir
+
in
+
let cache_dir, cache_dir_source =
+
resolve_from_config config.cache_dir Xdg.cache_dir
+
in
+
let state_dir, state_dir_source =
+
resolve_from_config config.state_dir Xdg.state_dir
+
in
+
(* Runtime directory *)
+
let runtime_dir, runtime_dir_source =
+
match config.runtime_dir.value with
+
| Some dir ->
+
(Some (resolve_path fs home_path dir), config.runtime_dir.source)
+
| None ->
+
( Option.map
+
(fun base ->
+
let base_path = resolve_path fs home_path base in
+
Eio.Path.(base_path / app_name))
+
(Xdg.runtime_dir xdg_ctx),
+
config.runtime_dir.source )
+
in
+
(* System directories - reuse shared helper *)
+
let config_dirs =
+
resolve_system_dirs fs home_path app_name "CONFIG_DIRS"
+
"XDG_CONFIG_DIRS" [ "/etc/xdg" ]
+
in
+
let data_dirs =
+
resolve_system_dirs fs home_path app_name "DATA_DIRS"
+
"XDG_DATA_DIRS"
+
[ "/usr/local/share"; "/usr/share" ]
+
in
+
ensure_dir config_dir;
+
ensure_dir data_dir;
+
ensure_dir cache_dir;
+
ensure_dir state_dir;
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
+
let xdg =
+
{
+
app_name;
+
config_dir;
+
config_dir_source;
+
data_dir;
+
data_dir_source;
+
cache_dir;
+
cache_dir_source;
+
state_dir;
+
state_dir_source;
+
runtime_dir;
+
runtime_dir_source;
+
config_dirs;
+
data_dirs;
+
}
+
in
+
(* Handle --show-paths option *)
+
if show_paths_flag then (
+
let print_path name path =
+
match path with
+
| None -> Printf.printf "%s: <none>\n" name
+
| Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p)
+
in
+
let print_paths name paths =
+
match paths with
+
| [] -> Printf.printf "%s: []\n" name
+
| paths ->
+
let paths_str =
+
String.concat ":" (List.map Eio.Path.native_exn paths)
+
in
+
Printf.printf "%s: %s\n" name paths_str
+
in
+
print_path "config_dir" (Some config_dir);
+
print_path "data_dir" (Some data_dir);
+
print_path "cache_dir" (Some cache_dir);
+
print_path "state_dir" (Some state_dir);
+
print_path "runtime_dir" runtime_dir;
+
print_paths "config_dirs" config_dirs;
+
print_paths "data_dirs" data_dirs;
+
Stdlib.exit 0);
+
(xdg, config))
+
$ show_paths $ config_dir $ data_dir $ cache_dir $ state_dir $ runtime_dir)
+
+
let cache_term app_name =
+
let open Cmdliner in
+
let app_upper = String.uppercase_ascii app_name in
+
let app_env = app_upper ^ "_CACHE_DIR" in
+
let xdg_var = "XDG_CACHE_HOME" in
+
let home = Sys.getenv "HOME" in
+
let default_path = home ^ "/.cache/" ^ app_name in
+
+
let doc =
+
Printf.sprintf
+
"Override cache directory. Can also be set with %s or %s. Default: %s"
+
app_env xdg_var default_path
+
in
+
+
let arg =
+
Arg.(
+
value & opt string default_path
+
& info [ "cache-dir"; "c" ] ~docv:"DIR" ~doc)
+
in
+
+
Term.(
+
const (fun cmdline_val ->
+
(* Check command line first *)
+
if cmdline_val <> default_path then cmdline_val
+
else
+
(* Then check app-specific env var *)
+
match Sys.getenv_opt app_env with
+
| Some v when v <> "" -> v
+
| _ -> (
+
(* Then check XDG env var *)
+
match Sys.getenv_opt xdg_var with
+
| Some v when v <> "" -> v ^ "/" ^ app_name
+
| _ -> default_path))
+
$ arg)
+
+
let env_docs app_name =
+
let app_upper = String.uppercase_ascii app_name in
+
Printf.sprintf
+
{|
+
Configuration Precedence (follows standard Unix conventions):
+
1. Command-line flags (e.g., --config-dir) - highest priority
+
2. Application-specific environment variable (e.g., %s_CONFIG_DIR)
+
3. XDG standard environment variable (e.g., XDG_CONFIG_HOME)
+
4. Default path (e.g., ~/.config/%s) - lowest priority
+
+
This allows per-application overrides without affecting other XDG-compliant programs.
+
For example, setting %s_CONFIG_DIR only changes the config directory for %s,
+
while XDG_CONFIG_HOME affects all XDG-compliant applications.
+
+
Application-specific variables:
+
%s_CONFIG_DIR Override config directory for %s only
+
%s_DATA_DIR Override data directory for %s only
+
%s_CACHE_DIR Override cache directory for %s only
+
%s_STATE_DIR Override state directory for %s only
+
%s_RUNTIME_DIR Override runtime directory for %s only
+
+
XDG standard variables (shared by all XDG applications):
+
XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s)
+
XDG_DATA_HOME User data directory (default: ~/.local/share/%s)
+
XDG_CACHE_HOME User cache directory (default: ~/.cache/%s)
+
XDG_STATE_HOME User state directory (default: ~/.local/state/%s)
+
XDG_RUNTIME_DIR User runtime directory (no default)
+
XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s)
+
XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s)
+
|}
+
app_upper app_name app_upper app_name app_upper app_name app_upper
+
app_name app_upper app_name app_upper app_name app_upper app_name app_name
+
app_name app_name app_name app_name app_name app_name
+
+
let pp ppf config =
+
let pp_source ppf = function
+
| Default -> Fmt.(styled `Faint string) ppf "default"
+
| Env var ->
+
Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
+
| Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
+
in
+
let pp_with_source name ppf ws =
+
match ws.value with
+
| None when ws.source = Default -> ()
+
| None ->
+
Fmt.pf ppf "@,%a %a %a"
+
Fmt.(styled `Cyan string)
+
(name ^ ":")
+
Fmt.(styled `Red string)
+
"<unset>"
+
Fmt.(styled `Faint (brackets pp_source))
+
ws.source
+
| Some value ->
+
Fmt.pf ppf "@,%a %a %a"
+
Fmt.(styled `Cyan string)
+
(name ^ ":")
+
Fmt.(styled `Green string)
+
value
+
Fmt.(styled `Faint (brackets pp_source))
+
ws.source
+
in
+
Fmt.pf ppf "@[<v>%a%a%a%a%a%a@]"
+
Fmt.(styled `Bold string)
+
"XDG config:"
+
(pp_with_source "config_dir")
+
config.config_dir
+
(pp_with_source "data_dir")
+
config.data_dir
+
(pp_with_source "cache_dir")
+
config.cache_dir
+
(pp_with_source "state_dir")
+
config.state_dir
+
(pp_with_source "runtime_dir")
+
config.runtime_dir
+
end
+428
lib/xdge.mli
···
+
(** XDG Base Directory Specification support with Eio capabilities
+
+
This library provides an OCaml implementation of the XDG Base Directory
+
Specification with Eio filesystem integration. The XDG specification defines
+
standard locations for user-specific and system-wide application files,
+
helping to keep user home directories clean and organized.
+
+
The specification is available at:
+
{{:https://specifications.freedesktop.org/basedir-spec/latest/} XDG Base
+
Directory Specification}
+
+
{b Key Concepts:}
+
+
The XDG specification defines several types of directories:
+
- {b User directories}: Store user-specific files (config, data, cache,
+
state, runtime)
+
- {b System directories}: Store system-wide files shared across users
+
- {b Precedence}: User directories take precedence over system directories
+
- {b Application isolation}: Each application gets its own subdirectory
+
+
{b Environment Variable Precedence:}
+
+
This library follows a three-level precedence system:
+
+ Application-specific variables (e.g., [MYAPP_CONFIG_DIR]) - highest
+
priority
+
+ XDG standard variables (e.g., [XDG_CONFIG_HOME])
+
+ Default paths (e.g., [$HOME/.config]) - lowest priority
+
+
This allows fine-grained control over directory locations without affecting
+
other XDG-compliant applications.
+
+
{b Directory Creation:}
+
+
All directories are automatically created with appropriate permissions
+
(0o755) when accessed, except for runtime directories which require stricter
+
permissions as per the specification.
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/>
+
XDG Base Directory Specification *)
+
+
type t
+
(** The main XDG context type containing all directory paths for an application.
+
+
A value of type [t] represents the complete XDG directory structure for a
+
specific application, including both user-specific and system-wide
+
directories. All paths are resolved at creation time and are absolute paths
+
within the Eio filesystem. *)
+
+
(** {1 Exceptions} *)
+
+
exception Invalid_xdg_path of string
+
(** Exception raised when XDG environment variables contain invalid paths.
+
+
The XDG specification requires all paths in environment variables to be
+
absolute. This exception is raised when a relative path is found. *)
+
+
(** {1 Construction} *)
+
+
val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
+
(** [create fs app_name] creates an XDG context for the given application.
+
+
This function initializes the complete XDG directory structure for your
+
application, resolving all paths according to the environment variables and
+
creating directories as needed.
+
+
@param fs The Eio filesystem providing filesystem access
+
@param app_name The name of your application (used as subdirectory name)
+
+
{b Path Resolution:}
+
+
For each directory type, the following precedence is used:
+
+ Application-specific environment variable (e.g., [MYAPP_CONFIG_DIR])
+
+ XDG standard environment variable (e.g., [XDG_CONFIG_HOME])
+
+ Default path as specified in the XDG specification
+
+
{b Example:}
+
{[
+
let xdg = Xdge.create env#fs "myapp" in
+
let config = Xdge.config_dir xdg in
+
(* config is now <fs:$HOME/.config/myapp> or the overridden path *)
+
]}
+
+
All directories are created with permissions 0o755 if they don't exist,
+
except for runtime directories which are created with 0o700 permissions and
+
validated according to the XDG specification.
+
+
@raise Invalid_xdg_path if any environment variable contains a relative path
+
*)
+
+
(** {1 Accessors} *)
+
+
val app_name : t -> string
+
(** [app_name t] returns the application name used when creating this XDG
+
context.
+
+
This is the name that was passed to {!create} and is used as the
+
subdirectory name within each XDG base directory. *)
+
+
(** {1 Base Directories} *)
+
+
val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
+
(** [config_dir t] returns the path to user-specific configuration files.
+
+
{b Purpose:} Store user preferences, settings, and configuration files.
+
Configuration files should be human-readable when possible.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_CONFIG_DIR]: Application-specific override (highest priority)
+
- [XDG_CONFIG_HOME]: XDG standard variable
+
- Default: [$HOME/.config/{app_name}]
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_CONFIG_HOME specification *)
+
+
val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t
+
(** [data_dir t] returns the path to user-specific data files.
+
+
{b Purpose:} Store persistent application data that should be preserved
+
across application restarts and system reboots. This data is typically not
+
modified by users directly.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_DATA_DIR]: Application-specific override (highest priority)
+
- [XDG_DATA_HOME]: XDG standard variable
+
- Default: [$HOME/.local/share/{app_name}]
+
+
{b Example Files:}
+
- Application databases
+
- User-generated content (documents, projects)
+
- Downloaded resources
+
- Application plugins or extensions
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_DATA_HOME specification *)
+
+
val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t
+
(** [cache_dir t] returns the path to user-specific cache files.
+
+
{b Purpose:} Store non-essential cached data that can be regenerated if
+
deleted. The application should remain functional if this directory is
+
cleared, though performance may be temporarily impacted.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_CACHE_DIR]: Application-specific override (highest priority)
+
- [XDG_CACHE_HOME]: XDG standard variable
+
- Default: [$HOME/.cache/{app_name}]
+
+
{b Example Files:}
+
- Downloaded thumbnails and previews
+
- Compiled bytecode or object files
+
- Network response caches
+
- Temporary computation results
+
+
Users may clear cache directories to free disk space, so always check for
+
cache validity and be prepared to regenerate data.
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_CACHE_HOME specification *)
+
+
val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t
+
(** [state_dir t] returns the path to user-specific state files.
+
+
{b Purpose:} Store persistent state data that should be preserved between
+
application restarts but is not important enough to be user data. This
+
includes application state that can be regenerated but would impact the user
+
experience if lost.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_STATE_DIR]: Application-specific override (highest priority)
+
- [XDG_STATE_HOME]: XDG standard variable
+
- Default: [$HOME/.local/state/{app_name}]
+
+
{b Example Files:}
+
- Application history (recently used files, command history)
+
- Current application state (window positions, open tabs)
+
- Logs and journal files
+
- Undo/redo history
+
+
{b Comparison with other directories:}
+
- Unlike cache: State should persist between reboots
+
- Unlike data: State can be regenerated (though inconvenient)
+
- Unlike config: State changes frequently during normal use
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_STATE_HOME specification *)
+
+
val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option
+
(** [runtime_dir t] returns the path to user-specific runtime files.
+
+
{b Purpose:} Store runtime files such as sockets, named pipes, and process
+
IDs. These files are only valid for the duration of the user's login
+
session.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_RUNTIME_DIR]: Application-specific override (highest
+
priority)
+
- [XDG_RUNTIME_DIR]: XDG standard variable
+
- Default: None (returns [None] if not set)
+
+
{b Required Properties (per specification):}
+
- Owned by the user with access mode 0700
+
- Bound to the user login session lifetime
+
- Located on a local filesystem (not networked)
+
- Fully-featured by the OS (supporting proper locking, etc.)
+
+
{b Example Files:}
+
- Unix domain sockets
+
- Named pipes (FIFOs)
+
- Lock files
+
- Small process communication files
+
+
This may return [None] if no suitable runtime directory is available.
+
Applications should handle this gracefully, perhaps by falling back to
+
[/tmp] with appropriate security measures.
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_RUNTIME_DIR specification *)
+
+
(** {1 System Directories} *)
+
+
val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
+
(** [config_dirs t] returns search paths for system-wide configuration files.
+
+
{b Purpose:} Provide a search path for configuration files that are shared
+
between multiple users. Files in user-specific {!config_dir} take precedence
+
over these system directories.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_CONFIG_DIRS]: Application-specific override (highest
+
priority)
+
- [XDG_CONFIG_DIRS]: XDG standard variable (colon-separated list)
+
- Default: [[/etc/xdg/{app_name}]]
+
+
{b Search Order:} Directories are ordered by preference, with earlier
+
entries taking precedence over later ones. When looking for a configuration
+
file, search {!config_dir} first, then each directory in this list.
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_CONFIG_DIRS specification *)
+
+
val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
+
(** [data_dirs t] returns search paths for system-wide data files.
+
+
{b Purpose:} Provide a search path for data files that are shared between
+
multiple users. Files in user-specific {!data_dir} take precedence over
+
these system directories.
+
+
{b Environment Variables:}
+
- [${APP_NAME}_DATA_DIRS]: Application-specific override (highest priority)
+
- [XDG_DATA_DIRS]: XDG standard variable (colon-separated list)
+
- Default: [[/usr/local/share/{app_name}; /usr/share/{app_name}]]
+
+
{b Search Order:} Directories are ordered by preference, with earlier
+
entries taking precedence over later ones. When looking for a data file,
+
search {!data_dir} first, then each directory in this list.
+
+
{b Example Files:}
+
- Application icons and themes
+
- Desktop files
+
- Shared application resources
+
- Documentation files
+
- Default templates
+
+
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
+
XDG_DATA_DIRS specification *)
+
+
(** {1 File Search} *)
+
+
val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
+
(** [find_config_file t filename] searches for a configuration file following
+
XDG precedence.
+
+
This function searches for the given filename in the user configuration
+
directory first, then in system configuration directories in order of
+
preference. Files that are inaccessible (due to permissions, non-existence,
+
etc.) are silently skipped as per the XDG specification.
+
+
@param t The XDG context
+
@param filename The name of the file to search for
+
@return [Some path] if found, [None] if not found in any directory
+
+
{b Search Order:} 1. User config directory ({!config_dir}) 2. System config
+
directories ({!config_dirs}) in preference order *)
+
+
val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
+
(** [find_data_file t filename] searches for a data file following XDG
+
precedence.
+
+
This function searches for the given filename in the user data directory
+
first, then in system data directories in order of preference. Files that
+
are inaccessible (due to permissions, non-existence, etc.) are silently
+
skipped as per the XDG specification.
+
+
@param t The XDG context
+
@param filename The name of the file to search for
+
@return [Some path] if found, [None] if not found in any directory
+
+
{b Search Order:} 1. User data directory ({!data_dir}) 2. System data
+
directories ({!data_dirs}) in preference order *)
+
+
(** {1 Pretty Printing} *)
+
+
val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
+
(** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration.
+
+
@param brief If [true], prints a compact one-line summary (default: [false])
+
@param sources
+
If [true], shows the source of each directory value, indicating whether it
+
came from defaults, environment variables, or command line (default:
+
[false])
+
@param ppf The formatter to print to
+
@param t The XDG context to print
+
+
{b Output formats:}
+
- Normal: Multi-line detailed view of all directories
+
- Brief: Single line showing app name and key directories
+
- With sources: Adds annotations showing where each path came from *)
+
+
(** {1 Cmdliner Integration} *)
+
+
module Cmd : sig
+
(** The type of the outer XDG context *)
+
type xdg_t = t
+
(** Cmdliner integration for XDG directory configuration.
+
+
This module provides integration with the Cmdliner library, allowing XDG
+
directories to be configured via command-line arguments while respecting
+
the precedence of environment variables. *)
+
+
type t
+
(** Type of XDG configuration gathered from command-line and environment.
+
+
This contains all XDG directory paths along with their sources, as
+
determined by command-line arguments and environment variables. *)
+
+
type dir =
+
[ `Config (** User configuration files *)
+
| `Cache (** User-specific cached data *)
+
| `Data (** User-specific application data *)
+
| `State (** User-specific state data (logs, history, etc.) *)
+
| `Runtime (** User-specific runtime files (sockets, pipes, etc.) *) ]
+
(** XDG directory types for specifying which directories an application needs.
+
+
These allow applications to declare which XDG directories they use,
+
enabling runtime systems to only provide the requested directories. *)
+
+
val term :
+
string ->
+
Eio.Fs.dir_ty Eio.Path.t ->
+
?dirs:dir list ->
+
unit ->
+
(xdg_t * t) Cmdliner.Term.t
+
(** [term app_name fs ?dirs ()] creates a Cmdliner term for XDG directory
+
configuration.
+
+
This function generates a Cmdliner term that handles XDG directory
+
configuration through both command-line flags and environment variables,
+
and directly returns the XDG context. Only command-line flags for the
+
requested directories are generated.
+
+
@param app_name
+
The application name (used for environment variable prefixes)
+
@param fs The Eio filesystem to use for path resolution
+
@param dirs
+
List of directories to include flags for (default: all directories)
+
+
{b Generated Command-line Flags:} Only the flags for requested directories
+
are generated:
+
- [--config-dir DIR]: Override configuration directory (if [`Config] in
+
dirs)
+
- [--data-dir DIR]: Override data directory (if [`Data] in dirs)
+
- [--cache-dir DIR]: Override cache directory (if [`Cache] in dirs)
+
- [--state-dir DIR]: Override state directory (if [`State] in dirs)
+
- [--runtime-dir DIR]: Override runtime directory (if [`Runtime] in dirs)
+
+
{b Environment Variable Precedence:} For each directory type, the
+
following precedence applies:
+
+ Command-line flag (e.g., [--config-dir]) - if enabled
+
+ Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
+
+ XDG standard variable (e.g., [XDG_CONFIG_HOME])
+
+ Default value *)
+
+
val cache_term : string -> string Cmdliner.Term.t
+
(** [cache_term app_name] creates a Cmdliner term that provides just the cache
+
directory path as a string, respecting XDG precedence.
+
+
This is a convenience function for applications that only need cache
+
directory configuration. It returns the resolved cache directory path
+
directly as a string, suitable for use in other Cmdliner terms.
+
+
@param app_name
+
The application name (used for environment variable prefixes)
+
+
{b Generated Command-line Flag:}
+
- [--cache-dir DIR]: Override cache directory
+
+
{b Environment Variable Precedence:}
+
+ Command-line flag ([--cache-dir])
+
+ Application-specific variable (e.g., [MYAPP_CACHE_DIR])
+
+ XDG standard variable ([XDG_CACHE_HOME])
+
+ Default value ([$HOME/.cache/{app_name}]) *)
+
+
val env_docs : string -> string
+
(** [env_docs app_name] generates documentation for environment variables.
+
+
Returns a formatted string documenting all environment variables that
+
affect XDG directory configuration for the given application. This is
+
useful for generating man pages or help text.
+
+
@param app_name The application name
+
@return A formatted documentation string
+
+
{b Included Information:}
+
- Configuration precedence rules
+
- Application-specific environment variables
+
- XDG standard environment variables
+
- Default values for each directory type *)
+
+
val pp : Format.formatter -> t -> unit
+
(** [pp ppf config] pretty prints a Cmdliner configuration.
+
+
This function formats the configuration showing each directory path along
+
with its source, which is helpful for debugging configuration issues or
+
displaying the current configuration to users.
+
+
@param ppf The formatter to print to
+
@param config The configuration to print *)
+
end
+10
test/dune
···
+
(executable
+
(name xdg_example)
+
(libraries xdge eio_main cmdliner fmt))
+
+
(executable
+
(name test_paths)
+
(libraries xdge eio eio_main))
+
+
(cram
+
(deps xdg_example.exe test_paths.exe))
+105
test/test_paths.ml
···
+
let test_path_validation () =
+
Printf.printf "Testing XDG path validation...\n";
+
(* Test absolute path validation for environment variables *)
+
let test_relative_path_rejection env_var relative_path =
+
Printf.printf "Testing rejection of relative path in %s...\n" env_var;
+
Unix.putenv env_var relative_path;
+
try
+
Eio_main.run @@ fun env ->
+
let _ = Xdge.create env#fs "test_validation" in
+
Printf.printf "ERROR: Should have rejected relative path\n";
+
false
+
with
+
| Xdge.Invalid_xdg_path msg ->
+
Printf.printf "SUCCESS: Correctly rejected relative path: %s\n" msg;
+
true
+
| exn ->
+
Printf.printf "ERROR: Wrong exception: %s\n" (Printexc.to_string exn);
+
false
+
in
+
let old_config_home = Sys.getenv_opt "XDG_CONFIG_HOME" in
+
let old_data_dirs = Sys.getenv_opt "XDG_DATA_DIRS" in
+
let success1 =
+
test_relative_path_rejection "XDG_CONFIG_HOME" "relative/path"
+
in
+
let success2 =
+
test_relative_path_rejection "XDG_DATA_DIRS" "rel1:rel2:/abs/path"
+
in
+
(* Restore original env vars *)
+
(match old_config_home with
+
| Some v -> Unix.putenv "XDG_CONFIG_HOME" v
+
| None -> ( try Unix.putenv "XDG_CONFIG_HOME" "" with _ -> ()));
+
(match old_data_dirs with
+
| Some v -> Unix.putenv "XDG_DATA_DIRS" v
+
| None -> ( try Unix.putenv "XDG_DATA_DIRS" "" with _ -> ()));
+
success1 && success2
+
+
let test_file_search () =
+
Printf.printf "\nTesting XDG file search...\n";
+
Eio_main.run @@ fun env ->
+
let xdg = Xdge.create env#fs "search_test" in
+
(* Create test files *)
+
let config_file = Eio.Path.(Xdge.config_dir xdg / "test.conf") in
+
let data_file = Eio.Path.(Xdge.data_dir xdg / "test.dat") in
+
Eio.Path.save ~create:(`Or_truncate 0o644) config_file "config content";
+
Eio.Path.save ~create:(`Or_truncate 0o644) data_file "data content";
+
(* Test finding existing files *)
+
(match Xdge.find_config_file xdg "test.conf" with
+
| Some path ->
+
let content = Eio.Path.load path in
+
Printf.printf "Found config file: %s\n" (String.trim content)
+
| None -> Printf.printf "ERROR: Config file not found\n");
+
(match Xdge.find_data_file xdg "test.dat" with
+
| Some path ->
+
let content = Eio.Path.load path in
+
Printf.printf "Found data file: %s\n" (String.trim content)
+
| None -> Printf.printf "ERROR: Data file not found\n");
+
(* Test non-existent file *)
+
match Xdge.find_config_file xdg "nonexistent.conf" with
+
| Some _ -> Printf.printf "ERROR: Should not have found nonexistent file\n"
+
| None -> Printf.printf "Correctly handled nonexistent file\n"
+
+
let () =
+
(* Check if we should run validation tests *)
+
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--validate" then (
+
let validation_success = test_path_validation () in
+
test_file_search ();
+
if validation_success then
+
Printf.printf "\nAll path validation tests passed!\n"
+
else Printf.printf "\nSome validation tests failed!\n")
+
else
+
(* Run original simple functionality test *)
+
Eio_main.run @@ fun env ->
+
let xdg = Xdge.create env#fs "path_test" in
+
(* Test config subdirectory *)
+
let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in
+
let profile_file = Eio.Path.(profiles_path / "default.json") in
+
(try
+
let content = Eio.Path.load profile_file in
+
Printf.printf "config file content: %s" (String.trim content)
+
with exn ->
+
Printf.printf "config file error: %s" (Printexc.to_string exn));
+
(* Test data subdirectory *)
+
let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in
+
let db_file = Eio.Path.(db_path / "main.db") in
+
(try
+
let content = Eio.Path.load db_file in
+
Printf.printf "\ndata file content: %s" (String.trim content)
+
with exn ->
+
Printf.printf "\ndata file error: %s" (Printexc.to_string exn));
+
(* Test cache subdirectory *)
+
let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in
+
let cache_file = Eio.Path.(cache_path / "thumb1.png") in
+
(try
+
let content = Eio.Path.load cache_file in
+
Printf.printf "\ncache file content: %s" (String.trim content)
+
with exn ->
+
Printf.printf "\ncache file error: %s" (Printexc.to_string exn));
+
(* Test state subdirectory *)
+
let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in
+
let log_file = Eio.Path.(logs_path / "app.log") in
+
try
+
let content = Eio.Path.load log_file in
+
Printf.printf "\nstate file content: %s\n" (String.trim content)
+
with exn ->
+
Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn)
+379
test/xdg.t
···
+
Test with default directories:
+
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./test_home/.config/xdg_example> [default]
+
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
No command-line args or env vars are set, so all directories use defaults.
+
Config shows empty (no overrides), and directories show [default] source. User
+
directories follow XDG spec: ~/.config, ~/.local/share, ~/.cache,
+
~/.local/state. Runtime dir is <none> since XDG_RUNTIME_DIR has no default.
+
System dirs use XDG spec defaults: /etc/xdg for config, /usr/{local/,}share for
+
data.
+
+
Test with all command line arguments specified
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ ./xdg_example.exe \
+
> --config-dir ./test-config \
+
> --data-dir ./test-data \
+
> --cache-dir ./test-cache \
+
> --state-dir ./test-state \
+
> --runtime-dir ./test-runtime
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./test-config [cmdline]
+
data_dir: ./test-data [cmdline]
+
cache_dir: ./test-cache [cmdline]
+
state_dir: ./test-state [cmdline]
+
runtime_dir: ./test-runtime [cmdline]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./test-config> [cmdline]
+
data: <fs:./test_home/./test-data> [cmdline]
+
cache: <fs:./test_home/./test-cache> [cmdline]
+
state: <fs:./test_home/./test-state> [cmdline]
+
runtime: <fs:./test_home/./test-runtime> [cmdline]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
All user directories are overridden by command-line arguments, showing
+
[cmdline] as the source. The config section shows all overrides with their
+
values and [cmdline] sources. System directories remain at their defaults since
+
they cannot be overridden by user directories command-line options.
+
+
Test with environment variables (app-specific)
+
$ XDG_EXAMPLE_CONFIG_DIR=./env-config \
+
> XDG_EXAMPLE_DATA_DIR=./env-data \
+
> XDG_EXAMPLE_CACHE_DIR=./env-cache \
+
> XDG_EXAMPLE_STATE_DIR=./env-state \
+
> XDG_EXAMPLE_RUNTIME_DIR=./env-runtime \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./env-config [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data_dir: ./env-data [env(XDG_EXAMPLE_DATA_DIR)]
+
cache_dir: ./env-cache [env(XDG_EXAMPLE_CACHE_DIR)]
+
state_dir: ./env-state [env(XDG_EXAMPLE_STATE_DIR)]
+
runtime_dir: ./env-runtime [env(XDG_EXAMPLE_RUNTIME_DIR)]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./env-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data: <fs:./test_home/./env-data> [env(XDG_EXAMPLE_DATA_DIR)]
+
cache: <fs:./test_home/./env-cache> [env(XDG_EXAMPLE_CACHE_DIR)]
+
state: <fs:./test_home/./env-state> [env(XDG_EXAMPLE_STATE_DIR)]
+
runtime: <fs:./test_home/./env-runtime> [env(XDG_EXAMPLE_RUNTIME_DIR)]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
App-specific environment variables (XDG_EXAMPLE_*) override the defaults. The
+
source correctly shows [env(XDG_EXAMPLE_*)] for each variable. These
+
app-specific variables take precedence over XDG standard variables when both
+
are available, allowing per-application customization.
+
+
Test with standard XDG environment variables:
+
+
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
+
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
+
> XDG_CACHE_HOME=/tmp/xdge/xdg-cache \
+
> XDG_STATE_HOME=/tmp/xdge/xdg-state \
+
> XDG_RUNTIME_DIR=/tmp/xdge/xdg-runtime \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)]
+
data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)]
+
cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)]
+
state_dir: /tmp/xdge/xdg-state [env(XDG_STATE_HOME)]
+
runtime_dir: /tmp/xdge/xdg-runtime [env(XDG_RUNTIME_DIR)]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)]
+
data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)]
+
cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)]
+
state: <fs:/tmp/xdge/xdg-state> [env(XDG_STATE_HOME)]
+
runtime: <fs:/tmp/xdge/xdg-runtime> [env(XDG_RUNTIME_DIR)]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
Standard XDG environment variables (XDG_*_HOME, XDG_RUNTIME_DIR) override the
+
defaults. The source correctly shows [env(XDG_*)] for each variable. Note that
+
the user directories use the raw paths from env vars (not app-specific subdirs)
+
since XDG_CONFIG_HOME etc. are intended to be the base directories for the
+
user.
+
+
Test command line overrides environment variables:
+
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ XDG_EXAMPLE_CONFIG_DIR=./env-config \
+
> ./xdg_example.exe --config-dir ./cli-config
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./cli-config [cmdline]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./cli-config> [cmdline]
+
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
Command-line arguments have highest precedence, overriding environment
+
variables. Only config_dir is shown in the config section since it is the only
+
one explicitly set. The config_dir shows [cmdline] source while other
+
directories fall back to defaults, demonstrating the precedence hierarchy: of
+
cmdline then app env vars then XDG env vars then defaults.
+
+
Test mixed environment variable precedence (app-specific overrides XDG
+
standard):
+
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
+
> XDG_EXAMPLE_CONFIG_DIR=./app-config \
+
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
+
> XDG_EXAMPLE_DATA_DIR=./app-data \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data_dir: ./app-data [env(XDG_EXAMPLE_DATA_DIR)]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data: <fs:./test_home/./app-data> [env(XDG_EXAMPLE_DATA_DIR)]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
Demonstrates app-specific environment variables taking precedence over XDG
+
standard ones. Both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR are set, but the
+
app-specific one wins. Same for data directories. Cache, state, and runtime
+
fall back to defaults since no variables are set for them.
+
+
Test partial environment variable override:
+
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ XDG_EXAMPLE_CONFIG_DIR=./app-config \
+
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
+
> XDG_CACHE_HOME=/tmp/xdge/xdg-cache \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)]
+
cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
+
data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)]
+
cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
Shows mixed sources working together. Config uses app-specific env var (highest
+
priority among env vars), data and cache use XDG standard env vars (no
+
app-specific ones set), and state uses default (no env vars set). Each
+
directory gets its value from the highest-priority available source.
+
+
Test command line overrides mixed environment variables:
+
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
+
> XDG_EXAMPLE_CONFIG_DIR=./app-config \
+
> ./xdg_example.exe --config-dir ./cli-config
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: ./cli-config [cmdline]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./cli-config> [cmdline]
+
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
Command-line argument overrides both types of environment variables. Even
+
though both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR are set, the
+
--config-dir flag takes precedence and shows [cmdline] source. Other
+
directories fall back to defaults since no other command-line args are
+
provided.
+
+
Test empty environment variable handling:
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ XDG_EXAMPLE_CONFIG_DIR="" \
+
> XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)]
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)]
+
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/etc/xdg/xdg_example>]
+
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
+
+
When an app-specific env var is empty (""), it falls back to the XDG standard
+
variable. XDG_EXAMPLE_CONFIG_DIR="" is ignored, so XDG_CONFIG_HOME is used
+
instead, correctly showing [env(XDG_CONFIG_HOME)] as the source. This behavior
+
ensures that empty app-specific variables do not override useful XDG standard
+
settings.
+
+
Test system directory environment variables:
+
+
$ export HOME=./test_home
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ XDG_CONFIG_DIRS=/tmp/xdge/sys1:/tmp/xdge/sys2 \
+
> XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2 \
+
> ./xdg_example.exe
+
=== Cmdliner Config ===
+
XDG config:
+
+
=== XDG Directories ===
+
XDG directories for 'xdg_example':
+
User directories:
+
config: <fs:./test_home/./test_home/.config/xdg_example> [default]
+
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
+
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
+
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
+
runtime: <none> [default]
+
System directories:
+
config_dirs: [<fs:/tmp/xdge/sys1/xdg_example>;
+
<fs:/tmp/xdge/sys2/xdg_example>]
+
data_dirs: [<fs:/tmp/xdge/data1/xdg_example>;
+
<fs:/tmp/xdge/data2/xdg_example>]
+
+
XDG_CONFIG_DIRS and XDG_DATA_DIRS environment variables override the default
+
system directories. The colon-separated paths are parsed and the app name is
+
appended to each path. User directories remain at defaults since no user-level
+
overrides are provided. System directory env vars only affect the system
+
directories, not user directories.
+
+
Test _path functions do not create directories but can access files within them:
+
+
$ export HOME=/tmp/xdge/xdg_path_test
+
$ mkdir -p /tmp/xdge/xdg_path_test
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
Create config subdirectory manually and write a test file:
+
$ mkdir -p "/tmp/xdge/xdg_path_test/.config/path_test/profiles"
+
$ echo "test profile content" > "/tmp/xdge/xdg_path_test/.config/path_test/profiles/default.json"
+
Create data subdirectory manually and write a test file:
+
$ mkdir -p "/tmp/xdge/xdg_path_test/.local/share/path_test/databases"
+
$ echo "test database content" > "/tmp/xdge/xdg_path_test/.local/share/path_test/databases/main.db"
+
Create cache subdirectory manually and write a test file:
+
$ mkdir -p "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails"
+
$ echo "test cache content" > "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails/thumb1.png"
+
Create state subdirectory manually and write a test file:
+
$ mkdir -p "/tmp/xdge/xdg_path_test/.local/state/path_test/logs"
+
$ echo "test log content" > "/tmp/xdge/xdg_path_test/.local/state/path_test/logs/app.log"
+
+
Now test that we can read the files through the XDG _path functions:
+
$ ./test_paths.exe
+
config file content: test profile content
+
data file content: test database content
+
cache file content: test cache content
+
state file content: test log content
+
+
This test verifies that the _path functions return correct paths that can be used to access
+
files within XDG subdirectories, without the functions automatically creating those directories.
+
+
Test path resolution with --show-paths:
+
+
Test with a preset HOME to verify correct path resolution:
+
$ export HOME=./home_testuser
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ ./xdg_example.exe --show-paths
+
config_dir: ./home_testuser/./home_testuser/.config/xdg_example
+
data_dir: ./home_testuser/./home_testuser/.local/share/xdg_example
+
cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example
+
state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example
+
runtime_dir: <none>
+
config_dirs: /etc/xdg/xdg_example
+
data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example
+
+
Test with environment variables set:
+
$ export HOME=./home_testuser
+
$ export XDG_CONFIG_HOME=/tmp/xdge/config
+
$ export XDG_DATA_HOME=/tmp/xdge/data
+
$ export XDG_CACHE_HOME=/tmp/xdge/cache
+
$ export XDG_STATE_HOME=/tmp/xdge/state
+
$ export XDG_CONFIG_DIRS=/tmp/xdge/config1:/tmp/xdge/config2
+
$ export XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2
+
$ ./xdg_example.exe --show-paths
+
config_dir: /tmp/xdge/config
+
data_dir: /tmp/xdge/data
+
cache_dir: /tmp/xdge/cache
+
state_dir: /tmp/xdge/state
+
runtime_dir: <none>
+
config_dirs: /tmp/xdge/config1/xdg_example:/tmp/xdge/config2/xdg_example
+
data_dirs: /tmp/xdge/data1/xdg_example:/tmp/xdge/data2/xdg_example
+
+
Test with command-line overrides:
+
$ export HOME=./home_testuser
+
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
+
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
+
$ ./xdg_example.exe --show-paths --config-dir ./override/config --data-dir ./override/data
+
config_dir: ./home_testuser/./override/config
+
data_dir: ./home_testuser/./override/data
+
cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example
+
state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example
+
runtime_dir: <none>
+
config_dirs: /etc/xdg/xdg_example
+
data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example
+
+34
test/xdg_example.ml
···
+
let run (xdg, cfg) =
+
Fmt.pr "%a@.%a@.@.%a@.%a@."
+
Fmt.(styled `Bold string)
+
"=== Cmdliner Config ===" Xdge.Cmd.pp cfg
+
Fmt.(styled `Bold string)
+
"=== XDG Directories ==="
+
(Xdge.pp ~brief:false ~sources:true)
+
xdg
+
+
open Cmdliner
+
+
let () =
+
Fmt.set_style_renderer Fmt.stdout `Ansi_tty;
+
let app_name = "xdg_example" in
+
let doc =
+
"Example program demonstrating XDG directory selection with Cmdliner"
+
in
+
let man =
+
[
+
`S Manpage.s_description;
+
`P
+
"This example shows how to use the Xdge library with Cmdliner to \
+
handle XDG Base Directory Specification paths with command-line and \
+
environment variable overrides.";
+
`S Manpage.s_environment;
+
`P (Xdge.Cmd.env_docs app_name);
+
]
+
in
+
let info = Cmdliner.Cmd.info "xdg_example" ~version:"1.0" ~doc ~man in
+
Eio_main.run @@ fun env ->
+
let create_xdg_term = Xdge.Cmd.term app_name env#fs () in
+
let main_term = Term.(const run $ create_xdg_term) in
+
let cmd = Cmdliner.Cmd.v info main_term in
+
exit @@ Cmdliner.Cmd.eval cmd
+35
xdge.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "XDG Base Directory Specification support for Eio"
+
description:
+
"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."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.sh/@anil.recoil.org/xdge"
+
bug-reports: "https://tangled.sh/@anil.recoil.org/xgde/issues"
+
depends: [
+
"dune" {>= "3.20"}
+
"ocaml" {>= "5.1.0"}
+
"eio" {>= "1.1"}
+
"cmdliner" {>= "1.2.0"}
+
"fmt" {>= "0.11.0"}
+
"odoc" {with-doc}
+
"eio_main" {with-test}
+
"alcotest" {with-test & >= "1.7.0"}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]