Skip to main content

cxx_build/
out.rs

1use crate::bridge::fs;
2use crate::error::{Error, Result};
3use crate::paths;
4use std::path::{Component, Path, PathBuf};
5use std::{env, io};
6
7pub(crate) fn write(path: impl AsRef<Path>, content: &[u8]) -> Result<()> {
8    let path = path.as_ref();
9
10    let mut create_dir_error = None;
11    if fs::exists(path) {
12        if let Ok(existing) = fs::read(path)
13            && existing == content
14        {
15            // Avoid bumping modified time with unchanged contents.
16            return Ok(());
17        }
18        best_effort_remove(path);
19    } else {
20        let parent = path.parent().unwrap();
21        create_dir_error = fs::create_dir_all(parent).err();
22    }
23
24    match fs::write(path, content) {
25        // As long as write succeeded, ignore any create_dir_all error.
26        Ok(()) => Ok(()),
27        // If create_dir_all and write both failed, prefer the first error.
28        Err(err) => Err(Error::Fs(create_dir_error.unwrap_or(err))),
29    }
30}
31
32pub(crate) fn relative_symlink_file(
33    original: impl AsRef<Path>,
34    link: impl AsRef<Path>,
35) -> Result<()> {
36    let original = original.as_ref();
37    let link = link.as_ref();
38
39    let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
40    let relativized = best_effort_relativize_symlink(original, link);
41
42    symlink_file(&relativized, original, link, parent_directory_error)
43}
44
45pub(crate) fn absolute_symlink_file(
46    original: impl AsRef<Path>,
47    link: impl AsRef<Path>,
48) -> Result<()> {
49    let original = original.as_ref();
50    let link = link.as_ref();
51
52    let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
53
54    symlink_file(original, original, link, parent_directory_error)
55}
56
57pub(crate) fn relative_symlink_dir(
58    original: impl AsRef<Path>,
59    link: impl AsRef<Path>,
60) -> Result<()> {
61    let original = original.as_ref();
62    let link = link.as_ref();
63
64    let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
65    let relativized = best_effort_relativize_symlink(original, link);
66
67    symlink_dir(&relativized, link, parent_directory_error)
68}
69
70fn prepare_parent_directory_for_symlink(link: &Path) -> fs::Result<()> {
71    if fs::exists(link) {
72        best_effort_remove(link);
73        Ok(())
74    } else {
75        let parent = link.parent().unwrap();
76        fs::create_dir_all(parent)
77    }
78}
79
80fn symlink_file(
81    path_for_symlink: &Path,
82    path_for_copy: &Path,
83    link: &Path,
84    parent_directory_error: Option<fs::Error>,
85) -> Result<()> {
86    match paths::symlink_or_copy(path_for_symlink, path_for_copy, link) {
87        // As long as symlink_or_copy succeeded, ignore any create_dir_all error.
88        Ok(()) => Ok(()),
89        Err(err) => {
90            if err.kind() == io::ErrorKind::AlreadyExists {
91                // This is fine, a different simultaneous build script already
92                // created the same link or copy. The cxx_build target directory
93                // is laid out such that the same path never refers to two
94                // different targets during the same multi-crate build, so if
95                // some other build script already created the same path then we
96                // know it refers to the identical target that the current build
97                // script was trying to create.
98                Ok(())
99            } else {
100                // If create_dir_all and symlink_or_copy both failed, prefer the
101                // first error.
102                Err(Error::Fs(parent_directory_error.unwrap_or(err)))
103            }
104        }
105    }
106}
107
108fn symlink_dir(
109    path_for_symlink: &Path,
110    link: &Path,
111    parent_directory_error: Option<fs::Error>,
112) -> Result<()> {
113    match fs::symlink_dir(path_for_symlink, link) {
114        // As long as symlink_dir succeeded, ignore any create_dir_all error.
115        Ok(()) => Ok(()),
116        // If create_dir_all and symlink_dir both failed, prefer the first error.
117        Err(err) => Err(Error::Fs(parent_directory_error.unwrap_or(err))),
118    }
119}
120
121fn best_effort_remove(path: &Path) {
122    use std::fs;
123
124    if falsecfg!(windows) {
125        // On Windows, the correct choice of remove_file vs remove_dir needs to
126        // be used according to what the symlink *points to*. Trying to use
127        // remove_file to remove a symlink which points to a directory fails
128        // with "Access is denied".
129        if let Ok(metadata) = fs::metadata(path) {
130            if metadata.is_dir() {
131                let _ = fs::remove_dir_all(path);
132            } else {
133                let _ = fs::remove_file(path);
134            }
135        } else if fs::symlink_metadata(path).is_ok() {
136            // The symlink might exist but be dangling, in which case there is
137            // no standard way to determine what "kind" of symlink it is. Try
138            // deleting both ways.
139            if fs::remove_dir_all(path).is_err() {
140                let _ = fs::remove_file(path);
141            }
142        }
143    } else {
144        // On non-Windows, we check metadata not following symlinks. All
145        // symlinks are removed using remove_file.
146        if let Ok(metadata) = fs::symlink_metadata(path) {
147            if metadata.is_dir() {
148                let _ = fs::remove_dir_all(path);
149            } else {
150                let _ = fs::remove_file(path);
151            }
152        }
153    }
154}
155
156fn best_effort_relativize_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> PathBuf {
157    let original = original.as_ref();
158    let link = link.as_ref();
159
160    let Some(relative_path) = abstractly_relativize_symlink(original, link) else {
161        return original.to_path_buf();
162    };
163
164    // Sometimes "a/b/../c" refers to a different canonical location than "a/c".
165    // This can happen if 'b' is a symlink. The '..' canonicalizes to the parent
166    // directory of the symlink's target, not back to 'a'. In cxx-build's case
167    // someone could be using `--target-dir` with a location containing such
168    // symlinks.
169    if let Ok(original_canonical) = original.canonicalize()
170        && let Ok(relative_canonical) = link.parent().unwrap().join(&relative_path).canonicalize()
171        && original_canonical == relative_canonical
172    {
173        return relative_path;
174    }
175
176    original.to_path_buf()
177}
178
179fn abstractly_relativize_symlink(
180    original: impl AsRef<Path>,
181    link: impl AsRef<Path>,
182) -> Option<PathBuf> {
183    let original = original.as_ref();
184    let link = link.as_ref();
185
186    // Relativization only makes sense if there is a semantically meaningful
187    // base directory shared between the two paths.
188    //
189    // For example /Volumes/code/library/src/lib.rs
190    //         and /Volumes/code/library/target/path/to/something.a
191    // have a meaningful shared base of /Volumes/code/library. The target and
192    // source directory only likely ever get relocated as one unit.
193    //
194    // On the other hand, /Volumes/code/library/src/lib.rs
195    //                and /Volumes/shared_target
196    // do not, since upon moving library to a different location it should
197    // continue referring to the original location of that shared Cargo target
198    // directory.
199    let likely_no_semantic_prefix = env::var_os("CARGO_TARGET_DIR").is_some();
200
201    if likely_no_semantic_prefix
202        || original.is_relative()
203        || link.is_relative()
204        || path_contains_intermediate_components(original)
205        || path_contains_intermediate_components(link)
206    {
207        return None;
208    }
209
210    let (common_prefix, rest_of_original, rest_of_link) = split_after_common_prefix(original, link);
211
212    if common_prefix == Path::new("") {
213        return None;
214    }
215
216    let mut rest_of_link = rest_of_link.components();
217    rest_of_link
218        .next_back()
219        .expect("original can't be a subdirectory of link");
220
221    let mut path_to_common_prefix = PathBuf::new();
222    while rest_of_link.next_back().is_some() {
223        path_to_common_prefix.push(Component::ParentDir);
224    }
225
226    Some(path_to_common_prefix.join(rest_of_original))
227}
228
229fn path_contains_intermediate_components(path: impl AsRef<Path>) -> bool {
230    path.as_ref()
231        .components()
232        .any(|component| component == Component::ParentDir)
233}
234
235fn split_after_common_prefix<'first, 'second>(
236    first: &'first Path,
237    second: &'second Path,
238) -> (&'first Path, &'first Path, &'second Path) {
239    let entire_first = first;
240    let mut first = first.components();
241    let mut second = second.components();
242    loop {
243        let rest_of_first = first.as_path();
244        let rest_of_second = second.as_path();
245        match (first.next(), second.next()) {
246            (Some(first_component), Some(second_component))
247                if first_component == second_component => {}
248            _ => {
249                let mut common_prefix = entire_first;
250                for _ in rest_of_first.components().rev() {
251                    if let Some(parent) = common_prefix.parent() {
252                        common_prefix = parent;
253                    } else {
254                        common_prefix = Path::new("");
255                        break;
256                    }
257                }
258                return (common_prefix, rest_of_first, rest_of_second);
259            }
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use crate::out::abstractly_relativize_symlink;
267    use std::path::Path;
268
269    #[cfg(not(windows))]
270    #[test]
271    fn test_relativize_symlink_unix() {
272        assert_eq!(
273            abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/eggs").as_deref(),
274            Some(Path::new("../bar/baz")),
275        );
276        assert_eq!(
277            abstractly_relativize_symlink("/foo/bar/../baz", "/foo/spam/eggs"),
278            None,
279        );
280        assert_eq!(
281            abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/./eggs").as_deref(),
282            Some(Path::new("../bar/baz")),
283        );
284    }
285
286    #[cfg(windows)]
287    #[test]
288    fn test_relativize_symlink_windows() {
289        use std::path::PathBuf;
290
291        let windows_target = PathBuf::from_iter(["c:\\", "windows", "foo"]);
292        let windows_link = PathBuf::from_iter(["c:\\", "users", "link"]);
293        let windows_different_volume_link = PathBuf::from_iter(["d:\\", "users", "link"]);
294
295        assert_eq!(
296            abstractly_relativize_symlink(&windows_target, windows_link).as_deref(),
297            Some(Path::new("..\\windows\\foo")),
298        );
299        assert_eq!(
300            abstractly_relativize_symlink(&windows_target, windows_different_volume_link),
301            None,
302        );
303    }
304}