Skip to content

Commit 4cb5769

Browse files
authored
fix: fallback self-updating apps to whole-app symlinks (#18)
1 parent 4cbecae commit 4cb5769

1 file changed

Lines changed: 56 additions & 8 deletions

File tree

AppPorts/ContentView.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,40 @@ struct ContentView: View {
6161

6262
private let fileManager = FileManager.default
6363

64+
/// 某些会自更新的应用(如 VS Code / Cursor / Electron + Squirrel / Sparkle)
65+
/// 对 bundle 结构假设更强,使用 Contents 深层链接时更容易在更新后损坏入口。
66+
/// 这类应用回退到整个 .app 的传统符号链接兼容性更好。
67+
private func prefersWholeAppSymlink(for appURL: URL) -> Bool {
68+
let frameworkCandidates = [
69+
"Contents/Frameworks/Squirrel.framework",
70+
"Contents/Frameworks/Sparkle.framework"
71+
]
72+
73+
for relativePath in frameworkCandidates {
74+
if fileManager.fileExists(atPath: appURL.appendingPathComponent(relativePath).path) {
75+
return true
76+
}
77+
}
78+
79+
let nameCandidates = ["shipit", "autoupdate", "updater", "update"]
80+
let searchRoots = [
81+
appURL.appendingPathComponent("Contents/MacOS"),
82+
appURL.appendingPathComponent("Contents/Frameworks")
83+
]
84+
85+
for root in searchRoots {
86+
let items = (try? fileManager.contentsOfDirectory(at: root, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)) ?? []
87+
if items.contains(where: { item in
88+
let name = item.lastPathComponent.lowercased()
89+
return nameCandidates.contains(where: { name.contains($0) })
90+
}) {
91+
return true
92+
}
93+
}
94+
95+
return false
96+
}
97+
6498
// Monitors
6599
@State private var localMonitor: FolderMonitor?
66100
@State private var externalMonitor: FolderMonitor?
@@ -836,13 +870,20 @@ struct ContentView: View {
836870
// 检测是否为 iOS 应用(使用 WrappedBundle 结构)
837871
let wrappedBundleURL = destinationURL.appendingPathComponent("WrappedBundle")
838872
let isIOSApp = fileManager.fileExists(atPath: wrappedBundleURL.path)
873+
let shouldUseWholeAppSymlink = prefersWholeAppSymlink(for: destinationURL)
839874

840875
if isIOSApp {
841876
// iOS 应用:使用直接符号链接(整个 .app)
842877
// 注意:iOS 应用无法使用深度链接策略,Finder 中会显示箭头
843878
AppLogger.shared.log("迁移策略: iOS 应用 (直接符号链接)", level: "STRATEGY")
844879
try fileManager.createSymbolicLink(at: appToMove.path, withDestinationURL: destinationURL)
845880
AppLogger.shared.log("已创建符号链接: \(appToMove.path.path) -> \(destinationURL.path)")
881+
} else if shouldUseWholeAppSymlink {
882+
// 某些自更新应用(如 Squirrel / Sparkle)更依赖完整 bundle 结构。
883+
// 对它们使用传统整体符号链接,兼容性比 Contents 深链更好。
884+
AppLogger.shared.log("迁移策略: 自更新应用 (整体符号链接兼容模式)", level: "STRATEGY")
885+
try fileManager.createSymbolicLink(at: appToMove.path, withDestinationURL: destinationURL)
886+
AppLogger.shared.log("已创建符号链接: \(appToMove.path.path) -> \(destinationURL.path)")
846887
} else {
847888
// Mac 原生应用:使用 Contents 深度符号链接(隐藏 Finder 箭头)
848889
AppLogger.shared.log("迁移策略: Mac 原生应用 (Contents 深度链接)", level: "STRATEGY")
@@ -891,14 +932,21 @@ struct ContentView: View {
891932
}
892933
}
893934

894-
// 2. Create Deep Symlink Structure
895-
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: false, attributes: nil)
896-
897-
let localContentsURL = destinationURL.appendingPathComponent("Contents")
898-
let externalContentsURL = appToLink.path.appendingPathComponent("Contents")
899-
900-
try fileManager.createSymbolicLink(at: localContentsURL, withDestinationURL: externalContentsURL)
901-
935+
let shouldUseWholeAppSymlink = prefersWholeAppSymlink(for: appToLink.path)
936+
937+
if shouldUseWholeAppSymlink {
938+
AppLogger.shared.log("链接策略: 自更新应用 (整体符号链接兼容模式)", level: "STRATEGY")
939+
try fileManager.createSymbolicLink(at: destinationURL, withDestinationURL: appToLink.path)
940+
} else {
941+
// 2. Create Deep Symlink Structure
942+
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: false, attributes: nil)
943+
944+
let localContentsURL = destinationURL.appendingPathComponent("Contents")
945+
let externalContentsURL = appToLink.path.appendingPathComponent("Contents")
946+
947+
try fileManager.createSymbolicLink(at: localContentsURL, withDestinationURL: externalContentsURL)
948+
}
949+
902950
try? fileManager.setAttributes([.modificationDate: Date()], ofItemAtPath: destinationURL.path)
903951
}
904952

0 commit comments

Comments
 (0)