cxx_build/
out.rs

1use crate::error::{Error, Result};
2use crate::gen::fs;
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            if existing == content {
14                // Avoid bumping modified time with unchanged contents.
15                return Ok(());
16            }
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 cfg!(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        if let Ok(relative_canonical) = link.parent().unwrap().join(&relative_path).canonicalize() {
171            if original_canonical == relative_canonical {
172                return relative_path;
173            }
174        }
175    }
176
177    original.to_path_buf()
178}
179
180fn abstractly_relativize_symlink(
181    original: impl AsRef<Path>,
182    link: impl AsRef<Path>,
183) -> Option<PathBuf> {
184    let original = original.as_ref();
185    let link = link.as_ref();
186
187    // Relativization only makes sense if there is a semantically meaningful
188    // base directory shared between the two paths.
189    //
190    // For example /Volumes/code/library/src/lib.rs
191    //         and /Volumes/code/library/target/path/to/something.a
192    // have a meaningful shared base of /Volumes/code/library. The target and
193    // source directory only likely ever get relocated as one unit.
194    //
195    // On the other hand, /Volumes/code/library/src/lib.rs
196    //                and /Volumes/shared_target
197    // do not, since upon moving library to a different location it should
198    // continue referring to the original location of that shared Cargo target
199    // directory.
200    let likely_no_semantic_prefix = env::var_os("CARGO_TARGET_DIR").is_some();
201
202    if likely_no_semantic_prefix
203        || original.is_relative()
204        || link.is_relative()
205        || path_contains_intermediate_components(original)
206        || path_contains_intermediate_components(link)
207    {
208        return None;
209    }
210
211    let (common_prefix, rest_of_original, rest_of_link) = split_after_common_prefix(original, link);
212
213    if common_prefix == Path::new("") {
214        return None;
215    }
216
217    let mut rest_of_link = rest_of_link.components();
218    rest_of_link
219        .next_back()
220        .expect("original can't be a subdirectory of link");
221
222    let mut path_to_common_prefix = PathBuf::new();
223    while rest_of_link.next_back().is_some() {
224        path_to_common_prefix.push(Component::ParentDir);
225    }
226
227    Some(path_to_common_prefix.join(rest_of_original))
228}
229
230fn path_contains_intermediate_components(path: impl AsRef<Path>) -> bool {
231    path.as_ref()
232        .components()
233        .any(|component| component == Component::ParentDir)
234}
235
236fn split_after_common_prefix<'first, 'second>(
237    first: &'first Path,
238    second: &'second Path,
239) -> (&'first Path, &'first Path, &'second Path) {
240    let entire_first = first;
241    let mut first = first.components();
242    let mut second = second.components();
243    loop {
244        let rest_of_first = first.as_path();
245        let rest_of_second = second.as_path();
246        match (first.next(), second.next()) {
247            (Some(first_component), Some(second_component))
248                if first_component == second_component => {}
249            _ => {
250                let mut common_prefix = entire_first;
251                for _ in rest_of_first.components().rev() {
252                    if let Some(parent) = common_prefix.parent() {
253                        common_prefix = parent;
254                    } else {
255                        common_prefix = Path::new("");
256                        break;
257                    }
258                }
259                return (common_prefix, rest_of_first, rest_of_second);
260            }
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::out::abstractly_relativize_symlink;
268    use std::path::Path;
269
270    #[cfg(not(windows))]
271    #[test]
272    fn test_relativize_symlink_unix() {
273        assert_eq!(
274            abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/eggs").as_deref(),
275            Some(Path::new("../bar/baz")),
276        );
277        assert_eq!(
278            abstractly_relativize_symlink("/foo/bar/../baz", "/foo/spam/eggs"),
279            None,
280        );
281        assert_eq!(
282            abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/./eggs").as_deref(),
283            Some(Path::new("../bar/baz")),
284        );
285    }
286
287    #[cfg(windows)]
288    #[test]
289    fn test_relativize_symlink_windows() {
290        use std::path::PathBuf;
291
292        let windows_target = PathBuf::from_iter(["c:\\", "windows", "foo"]);
293        let windows_link = PathBuf::from_iter(["c:\\", "users", "link"]);
294        let windows_different_volume_link = PathBuf::from_iter(["d:\\", "users", "link"]);
295
296        assert_eq!(
297            abstractly_relativize_symlink(&windows_target, windows_link).as_deref(),
298            Some(Path::new("..\\windows\\foo")),
299        );
300        assert_eq!(
301            abstractly_relativize_symlink(&windows_target, windows_different_volume_link),
302            None,
303        );
304    }
305}