Skip to content

Commit 76d07ca

Browse files
committed
Speed up move files on windows
1 parent 217c8ac commit 76d07ca

11 files changed

Lines changed: 332 additions & 249 deletions

File tree

sdkmover/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
/target/
4+
5+
# Generated by Tauri
6+
# will have schema files for capabilities auto-completion
7+
/gen/schemas

sdkmover/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdkmover/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "sdkmover"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
name = "sdkmover"
8+
path = "src/lib.rs"
9+
10+
[[bin]]
11+
name = "sdkmoverbin"
12+
path = "src/main.rs"
13+
14+
[dependencies]

sdkmover/src/lib.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
use std::{
2+
collections::HashSet,
3+
fs::{self, read_link},
4+
io::ErrorKind,
5+
os::unix::fs::symlink,
6+
path::{Component, Path},
7+
};
8+
9+
#[derive(Debug, Clone)]
10+
struct SDKEntry {
11+
names: HashSet<String>,
12+
values: Vec<SDKEntry>,
13+
}
14+
15+
impl SDKEntry {
16+
// empty = wildcard
17+
fn new(names: HashSet<String>, values: Vec<SDKEntry>) -> Self {
18+
SDKEntry { names, values }
19+
}
20+
21+
fn from_name(name: &str, values: Vec<SDKEntry>) -> Self {
22+
let mut set = HashSet::new();
23+
set.insert(name.to_string());
24+
SDKEntry::new(set, values)
25+
}
26+
27+
fn matches<'a, I>(&self, path: I) -> bool
28+
where
29+
I: Iterator<Item = &'a str> + Clone,
30+
{
31+
let mut path_clone = path.clone();
32+
let first = path_clone.next();
33+
if first.is_none() {
34+
return true;
35+
}
36+
let first = first.unwrap();
37+
if !self.names.is_empty() && !self.names.contains(first) {
38+
return false;
39+
}
40+
if self.values.is_empty() {
41+
return true;
42+
}
43+
let after_name = path_clone;
44+
for value in &self.values {
45+
if value.matches(after_name.clone()) {
46+
return true;
47+
}
48+
}
49+
false
50+
}
51+
52+
fn e(name: Option<&str>, values: Vec<SDKEntry>) -> SDKEntry {
53+
if let Some(name) = name {
54+
let parts: Vec<&str> = name.split('/').collect();
55+
let mut entry = SDKEntry::from_name(parts.last().unwrap(), values);
56+
for part in parts.iter().rev().skip(1) {
57+
entry = SDKEntry::from_name(part, vec![entry]);
58+
}
59+
entry
60+
} else {
61+
SDKEntry::new(HashSet::new(), values)
62+
}
63+
}
64+
}
65+
66+
// Build the wanted tree
67+
fn wanted_sdk_entry() -> SDKEntry {
68+
SDKEntry::e(
69+
Some("Contents/Developer"),
70+
vec![
71+
SDKEntry::e(
72+
Some("Toolchains/XcodeDefault.xctoolchain/usr/lib"),
73+
vec![
74+
SDKEntry::e(Some("swift"), vec![]),
75+
SDKEntry::e(Some("swift_static"), vec![]),
76+
SDKEntry::e(Some("clang"), vec![]),
77+
],
78+
),
79+
SDKEntry::e(
80+
Some("Platforms"),
81+
["iPhoneOS", "MacOSX", "iPhoneSimulator"]
82+
.iter()
83+
.map(|plat| {
84+
SDKEntry::e(
85+
Some(&format!("{}.platform/Developer", plat)),
86+
vec![
87+
SDKEntry::e(Some("SDKs"), vec![]),
88+
SDKEntry::e(
89+
Some("Library"),
90+
vec![
91+
SDKEntry::e(Some("Frameworks"), vec![]),
92+
SDKEntry::e(Some("PrivateFrameworks"), vec![]),
93+
],
94+
),
95+
SDKEntry::e(Some("usr/lib"), vec![]),
96+
],
97+
)
98+
})
99+
.collect(),
100+
),
101+
],
102+
)
103+
}
104+
105+
fn is_wanted(path: &Path) -> bool {
106+
let mut components: Vec<String> = path
107+
.components()
108+
.filter_map(|c| match c {
109+
Component::Normal(os) => Some(os.to_string_lossy().to_string()),
110+
_ => None,
111+
})
112+
.collect();
113+
114+
if let Some(first) = components.first()
115+
&& first == "."
116+
{
117+
components.remove(0);
118+
}
119+
if let Some(first) = components.first()
120+
&& first.ends_with(".app")
121+
{
122+
components.remove(0);
123+
}
124+
125+
if !wanted_sdk_entry().matches(components.iter().map(|s| s.as_str())) {
126+
return false;
127+
}
128+
129+
if components.len() >= 10
130+
&& components[9] == "prebuilt-modules"
131+
&& components.starts_with(
132+
&[
133+
"Contents",
134+
"Developer",
135+
"Toolchains",
136+
"XcodeDefault.xctoolchain",
137+
"usr",
138+
"lib",
139+
"swift",
140+
]
141+
.iter()
142+
.map(|s| s.to_string())
143+
.collect::<Vec<_>>(),
144+
)
145+
{
146+
return false;
147+
}
148+
149+
true
150+
}
151+
152+
pub fn copy_developer(
153+
src: &Path,
154+
dst: &Path,
155+
rel: &Path,
156+
crosses_devices: bool,
157+
) -> Result<(), String> {
158+
let mut has_crossed_device = crosses_devices;
159+
for entry in fs::read_dir(src).map_err(|e| format!("Failed to read dir: {}", e))? {
160+
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
161+
let file_name = entry.file_name();
162+
let rel_path = rel.join(&file_name);
163+
if !is_wanted(&rel_path) {
164+
continue;
165+
}
166+
let src_path = entry.path();
167+
168+
let mut rel_components = rel_path.components();
169+
if let Some(c) = rel_components.next()
170+
&& c.as_os_str() != "Contents"
171+
{
172+
rel_components = rel_path.components();
173+
}
174+
if let Some(c) = rel_components.next()
175+
&& c.as_os_str() != "Developer"
176+
{
177+
rel_components = rel_path.components();
178+
}
179+
let dst_path = dst.join(rel_components.as_path());
180+
181+
let metadata = fs::symlink_metadata(&src_path)
182+
.map_err(|e| format!("Failed to get metadata: {}", e))?;
183+
184+
if metadata.file_type().is_symlink() {
185+
let target =
186+
read_link(&src_path).map_err(|e| format!("Failed to read symlink: {}", e))?;
187+
if let Some(parent) = dst_path.parent() {
188+
fs::create_dir_all(parent)
189+
.map_err(|e| format!("Failed to create parent dir: {}", e))?;
190+
}
191+
symlink(&target, &dst_path).map_err(|e| format!("Failed to create symlink: {}", e))?;
192+
} else if metadata.is_dir() {
193+
fs::create_dir_all(&dst_path).map_err(|e| format!("Failed to create dir: {}", e))?;
194+
copy_developer(&src_path, dst, &rel_path, has_crossed_device)?;
195+
} else if metadata.is_file() {
196+
if let Some(parent) = dst_path.parent() {
197+
fs::create_dir_all(parent)
198+
.map_err(|e| format!("Failed to create parent dir: {}", e))?;
199+
}
200+
if has_crossed_device {
201+
fs::copy(&src_path, &dst_path)
202+
.map_err(|e| format!("Failed to copy file across devices: {}", e))?;
203+
continue;
204+
}
205+
match fs::rename(&src_path, &dst_path) {
206+
Ok(_) => {}
207+
Err(e) => {
208+
if e.kind() == ErrorKind::CrossesDevices {
209+
has_crossed_device = true;
210+
fs::copy(&src_path, &dst_path)
211+
.map_err(|e2| format!("Failed to copy file across devices: {}", e2))?;
212+
} else {
213+
return Err(format!("Failed to move file: {}", e));
214+
}
215+
}
216+
}
217+
}
218+
}
219+
Ok(())
220+
}

sdkmover/src/main.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use std::path::Path;
2+
3+
use sdkmover::copy_developer;
4+
5+
fn main() {
6+
let args: Vec<String> = std::env::args().collect();
7+
if args.len() != 3 {
8+
eprintln!("Usage: {} <source> <destination>", args[0]);
9+
std::process::exit(1);
10+
}
11+
let source = &args[1];
12+
let destination = &args[2];
13+
14+
if let Err(e) = copy_developer(
15+
Path::new(source),
16+
Path::new(destination),
17+
Path::new("Contents/Developer"),
18+
false,
19+
) {
20+
eprintln!("Error: {}", e);
21+
std::process::exit(1);
22+
}
23+
}

src-tauri/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ image = "0.25.6"
4848
rustls = { version = "0.23.31", default-features = false, features = ["ring"] }
4949
unxip-rs = "0.1.1"
5050

51+
[target.'cfg(unix)'.dependencies]
52+
sdkmover = { path = "../sdkmover" }
53+
5154
[features]
5255
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
5356
custom-protocol = ["tauri/custom-protocol"]

src-tauri/sdkmoverbin

501 KB
Binary file not shown.

src-tauri/src/builder/crossplatform.rs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,36 +53,6 @@ pub fn symlink(target: &str, link: &str) -> std::io::Result<()> {
5353
}
5454
}
5555

56-
pub fn read_link(path: &PathBuf) -> Result<PathBuf, String> {
57-
#[cfg(not(target_os = "windows"))]
58-
{
59-
return fs::read_link(path).map_err(|e| format!("Failed to read symlink: {}", e));
60-
}
61-
#[cfg(target_os = "windows")]
62-
{
63-
if !has_wsl() {
64-
return Err("WSL is not available".to_string());
65-
}
66-
let wsl_path = windows_to_wsl_path(&path.to_string_lossy().to_string())?;
67-
let output = Command::new("wsl")
68-
.arg("readlink")
69-
.arg(wsl_path)
70-
.creation_flags(CREATE_NO_WINDOW)
71-
.output()
72-
.expect("failed to execute process");
73-
if output.status.success() {
74-
let res = String::from_utf8_lossy(&output.stdout).trim().to_string();
75-
Ok(PathBuf::from(res))
76-
} else {
77-
Err(format!(
78-
"Failed to read symlink '{}': {}",
79-
path.display(),
80-
String::from_utf8_lossy(&output.stderr)
81-
))
82-
}
83-
}
84-
}
85-
8656
pub fn linux_env(key: &str) -> Result<String, String> {
8757
#[cfg(not(target_os = "windows"))]
8858
{

0 commit comments

Comments
 (0)