@@ -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