Skip to content

fix: use whole-app symlinks for self-updating apps - #18

Merged
wzh4869 merged 1 commit into
wzh4869:developfrom
2han9wen71an:fix/self-updating-app-compat
Mar 7, 2026
Merged

fix: use whole-app symlinks for self-updating apps#18
wzh4869 merged 1 commit into
wzh4869:developfrom
2han9wen71an:fix/self-updating-app-compat

Conversation

@2han9wen71an

Copy link
Copy Markdown
Contributor

Summary

AppPorts currently migrates native macOS apps by leaving a local .app shell in /Applications and symlinking only its Contents directory to the external app bundle.

That works well for many apps, but it is fragile for self-updating apps such as VS Code / Cursor / other Electron apps using Squirrel, and for apps using Sparkle.

Those updaters tend to assume the bundle at /Applications/Foo.app is a complete bundle. With a fake shell + Contents symlink, updates can break the portal or silently de-migrate the app.

This PR adds a compatibility fallback:

  • detect common self-update frameworks / helper names (Squirrel.framework, Sparkle.framework, ShipIt, AutoUpdate, etc.)
  • for those apps, use a whole-app symlink instead of the Contents deep-link strategy
  • preserve the existing deep-link behavior for normal native apps

Why this helps

A whole-app symlink is less pretty in Finder, but it preserves bundle-level expectations better for apps that mutate or replace themselves during update.

This is especially relevant for VS Code-like apps, where auto-update is otherwise prone to breaking the AppPorts portal.

Validation

  • swiftc -typecheck passed against the project sources using the macOS SDK after stripping #Preview blocks into a temp copy (the current machine only has Command Line Tools, so xcodebuild is unavailable here)
  • existing code paths already support whole-app symlinks for iOS apps and legacy links, so this reuses an existing compatibility path rather than introducing a brand new storage model

Scope

This PR intentionally keeps the change small:

  • no persistent metadata/database changes
  • no UI changes yet
  • just choose a safer linking strategy for apps that are likely to self-update

If you like this direction, a follow-up could expose the selected strategy in the UI or let users choose between Contents mode and compatibility mode manually.

@wzh4869

wzh4869 commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for your contribution! Your work actually helped identify and fix an important issue that we previously didn't fully understand.

The first commit has been merged #17. However, has already been merged into main and the codebase has changed, so the second commit now has conflicts.

If you'd like, feel free to update it based on the latest main branch and I’ll be happy to review it again.
💗

@2han9wen71an

Copy link
Copy Markdown
Contributor Author

Follow-up after real-world repro on my machine:

  • Environment: AppPorts 1.4.0, macOS 15.5
  • App affected: Cursor.app
  • Current production behavior still used the Contents deep-link strategy:
    /Applications/Cursor.app/Contents -> /Volumes/Data/appDisks/Cursor.app/Contents
  • After the app updated, the backing directory under /Volumes/Data/appDisks/Cursor.app/Contents disappeared, while the shell app in /Applications/Cursor.app remained, leaving a broken app bundle that could no longer launch.

I verified that restoring Cursor as a normal local app fixes the issue immediately, and the problem is consistent with self-updating apps being incompatible with the Contents deep-link approach.

I have also refreshed this PR branch against the latest develop so it is up to date again.

Given this repro, I still think the whole-app symlink fallback for self-updating apps is worth merging.

@2han9wen71an

Copy link
Copy Markdown
Contributor Author

Thanks again for the earlier feedback. I checked PR #18 again against the latest develop, and the current single commit appears to apply cleanly now.

From my verification, I don’t think #17 and #18 conflict logically:

  • 功能增加-应用数据迁移 #17 mainly adds protection for externally migrated apps (locking the external .app to reduce the chance of it being removed during app update flows).
  • fix: use whole-app symlinks for self-updating apps #18 addresses a different layer: for self-updating apps, it falls back from the Contents deep-link strategy to a whole-app symlink strategy, which is more compatible with apps like Cursor / VS Code / Sparkle / Squirrel-based apps.

So my understanding is:

These two changes look complementary rather than mutually exclusive.

I also reproduced the real issue locally with Cursor, and the breakage is consistent with self-updating apps being incompatible with the Contents deep-link approach. Because of that, I still think #18 is worth merging even after #17.

If helpful, I’m happy to further adjust the patch to match your preferred style.

@sulimu2

sulimu2 commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

Thanks again for the earlier feedback. I checked PR #18 again against the latest develop, and the current single commit appears to apply cleanly now.

From my verification, I don’t think #17 and #18 conflict logically:

  • 功能增加-应用数据迁移 #17 mainly adds protection for externally migrated apps (locking the external .app to reduce the chance of it being removed during app update flows).
  • fix: use whole-app symlinks for self-updating apps #18 addresses a different layer: for self-updating apps, it falls back from the Contents deep-link strategy to a whole-app symlink strategy, which is more compatible with apps like Cursor / VS Code / Sparkle / Squirrel-based apps.

So my understanding is:

These two changes look complementary rather than mutually exclusive.

I also reproduced the real issue locally with Cursor, and the breakage is consistent with self-updating apps being incompatible with the Contents deep-link approach. Because of that, I still think #18 is worth merging even after #17.

If helpful, I’m happy to further adjust the patch to match your preferred style.

I also encountered the issue where self-updating applications stored on an external hard drive disappeared after updating. I attempted to resolve it but may not have succeeded, so in my commit I recommended moving the application back to the internal drive before updating. It's great to hear that you have a solution to this problem.

@wzh4869
wzh4869 merged commit 4cb5769 into wzh4869:develop Mar 7, 2026
2 checks passed
@wzh4869

wzh4869 commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Thank you so much for this contribution!

This is a critical fix that significantly improves AppPorts' compatibility with self-updating apps.

A quick note on the merge: Since my local develop branch was slightly out of date compared to main (which already had the 1.5.0 localization and migration features), I have manually synchronized the branches and resolved the conflicts.

Status:

Your fix is now successfully merged into the main branch.
Both main and develop branches are now fully up to date and include your changes.
This fix will be a key part of the v1.5.1 release.
Thanks again for helping make AppPorts better!

再次感谢您之前的反馈。我根据最新的 develop 分支重新检查了 PR #18 ,目前这个单独的提交现在似乎可以干净地应用了。

根据我的验证,我认为 #17#18 在逻辑上并不冲突:

  • 功能增加-应用数据迁移 #17 主要是为外部迁移的应用添加了保护(锁定外部的 .app 文件,以减少其在应用更新流程中被移除的可能性)。
  • fix: use whole-app symlinks for self-updating apps #18 解决的是另一个层面的问题:对于能够自我更新的应用,它从 Contents 深层链接策略回退到整个应用的符号链接策略,这能更好地兼容像 Cursor / VS Code / Sparkle / 基于 Squirrel 的应用等。

所以我的理解是:

这两项变更看起来是互补而非互斥的。

我还在本地用 Cursor 复现了实际问题,崩溃现象与自更新应用不兼容 Contents 深层链接方案的情况一致。因此,我仍然认为即使合并了 #17#18 也值得合并。

如果对您有帮助,我很乐意进一步调整补丁以符合您偏好的风格。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants