gix_path/env/
mod.rs

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