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/// Use [`shell_command()`] when constructing a command. On Windows, it passes `--posix` to the
41/// Git for Windows `bin/sh.exe` shim so its delegated `bash.exe` behaves like `sh`.
42///
43/// Note that the returned path might not be a path on disk, if it is a fallback path or if the
44/// file was moved or deleted since the first time this function is called.
45pub fn shell() -> &'static OsStr {
46 &shell_configuration().program
47}
48
49struct Shell {
50 program: OsString,
51 /// Whether `program` requires `--posix` to behave like `sh`.
52 needs_posix_mode: bool,
53}
54
55fn shell_configuration() -> &'static Shell {
56 static SHELL: LazyLock<Shell> = LazyLock::new(|| {
57 if cfg!(windows) {
58 let shell = auxiliary::find_git_associated_windows_executable_with_fallback("sh");
59 Shell {
60 program: shell.program,
61 needs_posix_mode: shell.is_shim,
62 }
63 } else {
64 Shell {
65 program: "/bin/sh".into(),
66 needs_posix_mode: false,
67 }
68 }
69 });
70 &SHELL
71}
72
73/// Return a command configured to run the shell that Git would use.
74///
75/// On Windows, when [`shell()`] selected the Git for Windows `sh.exe` shim, this also requests
76/// POSIX mode explicitly. The shim delegates to `bash.exe`, which otherwise behaves as Bash
77/// rather than as `sh`. Other shells, including a caller-provided shell, must not receive this
78/// Bash-specific option.
79pub fn shell_command() -> std::process::Command {
80 let shell = shell_configuration();
81 let mut command = std::process::Command::new(&shell.program);
82 if shell.needs_posix_mode {
83 command.arg("--posix");
84 }
85 command
86}
87
88/// Return the name of the Git executable to invoke it.
89///
90/// If it's in the `PATH`, it will always be a short name.
91///
92/// Note that on Windows, we will find the executable in the `PATH` if it exists there, or search it
93/// in alternative locations which when found yields the full path to it.
94pub fn exe_invocation() -> &'static Path {
95 if cfg!(windows) {
96 /// The path to the Git executable as located in the `PATH` or in other locations that it's
97 /// known to be installed to. It's `None` if environment variables couldn't be read or if
98 /// no executable could be found.
99 static EXECUTABLE_PATH: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
100 std::env::split_paths(&std::env::var_os("PATH")?)
101 .chain(git::ALTERNATIVE_LOCATIONS.iter().map(Into::into))
102 .find_map(|prefix| {
103 let full_path = prefix.join(EXE_NAME);
104 full_path.is_file().then_some(full_path)
105 })
106 .map(|exe_path| {
107 let is_in_alternate_location = git::ALTERNATIVE_LOCATIONS
108 .iter()
109 .any(|prefix| exe_path.strip_prefix(prefix).is_ok());
110 if is_in_alternate_location {
111 exe_path
112 } else {
113 EXE_NAME.into()
114 }
115 })
116 });
117 EXECUTABLE_PATH.as_deref().unwrap_or(Path::new(git::EXE_NAME))
118 } else {
119 Path::new("git")
120 }
121}
122
123/// Returns the fully qualified path in the *xdg-home* directory (or equivalent in the home dir) to
124/// `file`, accessing `env_var(<name>)` to learn where these bases are.
125///
126/// Note that the `HOME` directory should ultimately come from [`home_dir()`] as it handles Windows
127/// correctly. The same can be achieved by using [`var()`] as `env_var`.
128pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option<OsString>) -> Option<PathBuf> {
129 env_var("XDG_CONFIG_HOME")
130 .map(|home| {
131 let mut p = PathBuf::from(home);
132 p.push("git");
133 p.push(file);
134 p
135 })
136 .or_else(|| {
137 env_var("HOME").map(|home| {
138 let mut p = PathBuf::from(home);
139 p.push(".config");
140 p.push("git");
141 p.push(file);
142 p
143 })
144 })
145}
146
147static GIT_CORE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
148 let mut cmd = std::process::Command::new(exe_invocation());
149
150 #[cfg(windows)]
151 {
152 use std::os::windows::process::CommandExt;
153 const CREATE_NO_WINDOW: u32 = 0x08000000;
154 cmd.creation_flags(CREATE_NO_WINDOW);
155 }
156 let output = cmd.arg("--exec-path").output().ok()?;
157
158 if !output.status.success() {
159 return None;
160 }
161
162 BString::new(output.stdout)
163 .strip_suffix(b"\n")?
164 .to_path()
165 .ok()?
166 .to_owned()
167 .into()
168});
169
170/// Return the directory obtained by calling `git --exec-path`.
171///
172/// Returns `None` if Git could not be found or if it returned an error.
173pub fn core_dir() -> Option<&'static Path> {
174 GIT_CORE_DIR.as_deref()
175}
176
177/// Return the path at which the Git-provided program with bare `name` resides within the [`core_dir()`],
178/// or `None` if it doesn't exist there or if Git could not be found.
179///
180/// This is the location `git` itself uses to find the programs implementing its subcommands, and is
181/// useful to invoke programs like `git-upload-pack` that are shipped with Git but aren't necessarily
182/// present in `PATH`.
183///
184/// Use [`installation_program()`] to also search Git for Windows' wider installation for bundled
185/// programs such as `sh` and `vim`.
186///
187/// Note that installations differ in which programs they provide as separate executables - builds
188/// with `SKIP_DASHED_BUILT_INS`, like Git for Windows, omit programs for builtin subcommands, which
189/// can then still be run through `git` itself.
190pub fn core_dir_program(name: &str) -> Option<PathBuf> {
191 if !is_bare_program_name(name) {
192 return None;
193 }
194 let path = core_dir()?.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
195 path.is_file().then_some(path)
196}
197
198/// Return the path at which a program distributed with Git resides, or `None` if it cannot be found.
199///
200/// Unlike [`core_dir_program()`], which searches only [`core_dir()`], this also searches the `bin`
201/// and `usr/bin` directories of the Git for Windows installation on Windows. These contain programs
202/// such as `sh`, `vim`, and, in some versions, `vi`.
203///
204/// Only a bare program `name` without path separators is accepted.
205pub fn installation_program(name: &str) -> Option<PathBuf> {
206 if !is_bare_program_name(name) {
207 return None;
208 }
209
210 core_dir_program(name).or_else(|| {
211 if cfg!(windows) {
212 auxiliary::find_git_associated_windows_executable(name).map(|executable| executable.program.into())
213 } else {
214 None
215 }
216 })
217}
218
219fn is_bare_program_name(name: &str) -> bool {
220 let mut components = Path::new(name).components();
221 matches!(components.next(), Some(std::path::Component::Normal(_))) && components.next().is_none()
222}
223
224fn system_prefix_from_core_dir<F>(core_dir_func: F) -> Option<PathBuf>
225where
226 F: Fn() -> Option<&'static Path>,
227{
228 let path = core_dir_func()?;
229 let one_past_prefix = path.components().enumerate().find_map(|(idx, c)| {
230 matches!(c,std::path::Component::Normal(name) if name.to_str() == Some("libexec")).then_some(idx)
231 })?;
232 Some(path.components().take(one_past_prefix.checked_sub(1)?).collect())
233}
234
235fn system_prefix_from_exepath_var<F>(var_os_func: F) -> Option<PathBuf>
236where
237 F: Fn(&str) -> Option<OsString>,
238{
239 // Only attempt this optimization if the `EXEPATH` variable is set to an absolute path.
240 let root = var_os_func("EXEPATH").map(PathBuf::from).filter(|r| r.is_absolute())?;
241
242 let mut candidates = ["clangarm64", "mingw64", "mingw32"]
243 .iter()
244 .map(|component| root.join(component))
245 .filter(|candidate| candidate.is_dir());
246
247 let path = candidates.next()?;
248 match candidates.next() {
249 Some(_) => None, // Multiple plausible candidates, so don't use the `EXEPATH` optimization.
250 None => Some(path),
251 }
252}
253
254/// Returns the platform dependent system prefix or `None` if it cannot be found (right now only on Windows).
255///
256/// ### Performance
257///
258/// On Windows, the slowest part is the launch of the Git executable in the PATH. This is often
259/// avoided by inspecting the environment, when launched from inside a Git Bash MSYS2 shell.
260///
261/// ### When `None` is returned
262///
263/// This happens only Windows if the git binary can't be found at all for obtaining its executable
264/// path, or if the git binary wasn't built with a well-known directory structure or environment.
265pub fn system_prefix() -> Option<&'static Path> {
266 if cfg!(windows) {
267 static PREFIX: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
268 system_prefix_from_exepath_var(|key| std::env::var_os(key))
269 .or_else(|| system_prefix_from_core_dir(core_dir))
270 });
271 PREFIX.as_deref()
272 } else {
273 Path::new("/").into()
274 }
275}
276
277/// Returns `$HOME` or `None` if it cannot be found.
278#[cfg(target_family = "wasm")]
279pub fn home_dir() -> Option<PathBuf> {
280 std::env::var("HOME").map(PathBuf::from).ok()
281}
282
283/// Tries to obtain the home directory from `HOME` on all platforms, but falls back to
284/// [`std::env::home_dir()`] for more complex ways of obtaining a home directory, particularly useful
285/// on Windows.
286///
287/// The reason `HOME` is tried first is to allow Windows users to have a custom location for their
288/// linux-style home, as otherwise they would have to accumulate dot files in a directory these are
289/// inconvenient and perceived as clutter.
290#[cfg(not(target_family = "wasm"))]
291pub fn home_dir() -> Option<PathBuf> {
292 std::env::var_os("HOME").map(Into::into).or_else(std::env::home_dir)
293}
294
295/// Returns the contents of an environment variable of `name` with some special handling for
296/// certain environment variables (like `HOME`) for platform compatibility.
297pub fn var(name: &str) -> Option<OsString> {
298 if name == "HOME" {
299 home_dir().map(PathBuf::into_os_string)
300 } else {
301 std::env::var_os(name)
302 }
303}
304
305#[cfg(test)]
306mod tests;