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