gix_path/env/
mod.rs

1use std::{
2    ffi::{OsStr, OsString},
3    path::{Path, PathBuf},
4};
5
6use bstr::{BString, ByteSlice};
7use std::sync::LazyLock;
8
9use crate::env::git::EXE_NAME;
10
11mod auxiliary;
12mod git;
13
14/// Return the location at which installation specific git configuration file can be found, or `None`
15/// if the binary could not be executed or its results could not be parsed.
16///
17/// ### Performance
18///
19/// This invokes the git binary which is slow on windows.
20pub fn installation_config() -> Option<&'static Path> {
21    git::install_config_path().and_then(|p| crate::try_from_byte_slice(p).ok())
22}
23
24/// Return the location at which git installation specific configuration files are located, or `None` if the binary
25/// could not be executed or its results could not be parsed.
26///
27/// ### Performance
28///
29/// This invokes the git binary which is slow on windows.
30pub fn installation_config_prefix() -> Option<&'static Path> {
31    installation_config().map(git::config_to_base_path)
32}
33
34/// Return the shell that Git would use, the shell to execute commands from.
35///
36/// On Windows, this is the full path to `sh.exe` bundled with Git for Windows if we can find it.
37/// If the bundled shell on Windows cannot be found, `sh.exe` is returned as the name of a shell,
38/// as it could possibly be found in `PATH`. On Unix it's `/bin/sh` as the POSIX-compatible shell.
39///
40/// Note that the returned path might not be a path on disk, if it is a fallback path or if the
41/// file was moved or deleted since the first time this function is called.
42pub fn shell() -> &'static OsStr {
43    static PATH: LazyLock<OsString> = LazyLock::new(|| {
44        if cfg!(windows) {
45            auxiliary::find_git_associated_windows_executable_with_fallback("sh")
46        } else {
47            "/bin/sh".into()
48        }
49    });
50    PATH.as_ref()
51}
52
53/// Return the name of the Git executable to invoke it.
54///
55/// If it's in the `PATH`, it will always be a short name.
56///
57/// Note that on Windows, we will find the executable in the `PATH` if it exists there, or search it
58/// in alternative locations which when found yields the full path to it.
59pub fn exe_invocation() -> &'static Path {
60    if cfg!(windows) {
61        /// The path to the Git executable as located in the `PATH` or in other locations that it's
62        /// known to be installed to. It's `None` if environment variables couldn't be read or if
63        /// no executable could be found.
64        static EXECUTABLE_PATH: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
65            std::env::split_paths(&std::env::var_os("PATH")?)
66                .chain(git::ALTERNATIVE_LOCATIONS.iter().map(Into::into))
67                .find_map(|prefix| {
68                    let full_path = prefix.join(EXE_NAME);
69                    full_path.is_file().then_some(full_path)
70                })
71                .map(|exe_path| {
72                    let is_in_alternate_location = git::ALTERNATIVE_LOCATIONS
73                        .iter()
74                        .any(|prefix| exe_path.strip_prefix(prefix).is_ok());
75                    if is_in_alternate_location {
76                        exe_path
77                    } else {
78                        EXE_NAME.into()
79                    }
80                })
81        });
82        EXECUTABLE_PATH.as_deref().unwrap_or(Path::new(git::EXE_NAME))
83    } else {
84        Path::new("git")
85    }
86}
87
88/// Returns the fully qualified path in the *xdg-home* directory (or equivalent in the home dir) to
89/// `file`, accessing `env_var(<name>)` to learn where these bases are.
90///
91/// Note that the `HOME` directory should ultimately come from [`home_dir()`] as it handles Windows
92/// correctly. The same can be achieved by using [`var()`] as `env_var`.
93pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option<OsString>) -> Option<PathBuf> {
94    env_var("XDG_CONFIG_HOME")
95        .map(|home| {
96            let mut p = PathBuf::from(home);
97            p.push("git");
98            p.push(file);
99            p
100        })
101        .or_else(|| {
102            env_var("HOME").map(|home| {
103                let mut p = PathBuf::from(home);
104                p.push(".config");
105                p.push("git");
106                p.push(file);
107                p
108            })
109        })
110}
111
112static GIT_CORE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
113    let mut cmd = std::process::Command::new(exe_invocation());
114
115    #[cfg(windows)]
116    {
117        use std::os::windows::process::CommandExt;
118        const CREATE_NO_WINDOW: u32 = 0x08000000;
119        cmd.creation_flags(CREATE_NO_WINDOW);
120    }
121    let output = cmd.arg("--exec-path").output().ok()?;
122
123    if !output.status.success() {
124        return None;
125    }
126
127    BString::new(output.stdout)
128        .strip_suffix(b"\n")?
129        .to_path()
130        .ok()?
131        .to_owned()
132        .into()
133});
134
135/// Return the directory obtained by calling `git --exec-path`.
136///
137/// Returns `None` if Git could not be found or if it returned an error.
138pub fn core_dir() -> Option<&'static Path> {
139    GIT_CORE_DIR.as_deref()
140}
141
142fn system_prefix_from_core_dir<F>(core_dir_func: F) -> Option<PathBuf>
143where
144    F: Fn() -> Option<&'static Path>,
145{
146    let path = core_dir_func()?;
147    let one_past_prefix = path.components().enumerate().find_map(|(idx, c)| {
148        matches!(c,std::path::Component::Normal(name) if name.to_str() == Some("libexec")).then_some(idx)
149    })?;
150    Some(path.components().take(one_past_prefix.checked_sub(1)?).collect())
151}
152
153fn system_prefix_from_exepath_var<F>(var_os_func: F) -> Option<PathBuf>
154where
155    F: Fn(&str) -> Option<OsString>,
156{
157    // Only attempt this optimization if the `EXEPATH` variable is set to an absolute path.
158    let root = var_os_func("EXEPATH").map(PathBuf::from).filter(|r| r.is_absolute())?;
159
160    let mut candidates = ["clangarm64", "mingw64", "mingw32"]
161        .iter()
162        .map(|component| root.join(component))
163        .filter(|candidate| candidate.is_dir());
164
165    let path = candidates.next()?;
166    match candidates.next() {
167        Some(_) => None, // Multiple plausible candidates, so don't use the `EXEPATH` optimization.
168        None => Some(path),
169    }
170}
171
172/// Returns the platform dependent system prefix or `None` if it cannot be found (right now only on Windows).
173///
174/// ### Performance
175///
176/// On Windows, the slowest part is the launch of the Git executable in the PATH. This is often
177/// avoided by inspecting the environment, when launched from inside a Git Bash MSYS2 shell.
178///
179/// ### When `None` is returned
180///
181/// This happens only Windows if the git binary can't be found at all for obtaining its executable
182/// path, or if the git binary wasn't built with a well-known directory structure or environment.
183pub fn system_prefix() -> Option<&'static Path> {
184    if cfg!(windows) {
185        static PREFIX: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
186            system_prefix_from_exepath_var(|key| std::env::var_os(key))
187                .or_else(|| system_prefix_from_core_dir(core_dir))
188        });
189        PREFIX.as_deref()
190    } else {
191        Path::new("/").into()
192    }
193}
194
195/// Returns `$HOME` or `None` if it cannot be found.
196#[cfg(target_family = "wasm")]
197pub fn home_dir() -> Option<PathBuf> {
198    std::env::var("HOME").map(PathBuf::from).ok()
199}
200
201/// Tries to obtain the home directory from `HOME` on all platforms, but falls back to
202/// [`home::home_dir()`] for more complex ways of obtaining a home directory, particularly useful
203/// on Windows.
204///
205/// The reason `HOME` is tried first is to allow Windows users to have a custom location for their
206/// linux-style home, as otherwise they would have to accumulate dot files in a directory these are
207/// inconvenient and perceived as clutter.
208#[cfg(not(target_family = "wasm"))]
209pub fn home_dir() -> Option<PathBuf> {
210    std::env::var_os("HOME").map(Into::into).or_else(home::home_dir)
211}
212
213/// Returns the contents of an environment variable of `name` with some special handling for
214/// certain environment variables (like `HOME`) for platform compatibility.
215pub fn var(name: &str) -> Option<OsString> {
216    if name == "HOME" {
217        home_dir().map(PathBuf::into_os_string)
218    } else {
219        std::env::var_os(name)
220    }
221}
222
223#[cfg(test)]
224mod tests;