Skip to content

Conversation

@macCesar
Copy link
Contributor

@macCesar macCesar commented Jan 10, 2026

Summary

This PR fixes critical build failures that prevented Mac Catalyst apps from compiling and being distributed through the App Store. These issues have affected developers since at least SDK 12.x, requiring workarounds that disrupted normal development workflows.

Problems Addressed

1. Framework Search Path Issue

Symptom: Intermittent build failures with framework linking errors
Root cause: Relative framework search path "Frameworks" failed to resolve correctly in certain build scenarios
Solution: Use absolute path "$(PROJECT_DIR)/Frameworks" for reliable framework location

2. Missing TitaniumKit.framework Symlinks

Symptom: Compilation errors and App Store rejections
Build-time error:

<module-includes>:1:9: error: could not build module 'TitaniumKit'
@import TitaniumKit;
        ^
<unknown>:0: error: Unable to find module dependency: 'TitaniumKit'

App Store rejection:

ERROR ITMS-90206: "Invalid Bundle. The bundle at 'YourApp.app/Contents/Frameworks/
TitaniumKit.framework' contains disallowed file 'Versions'."

Asset validation failed: The bundle 'YourApp.app/Contents/Frameworks/
TitaniumKit.framework' does not exist.

Root cause: Mac Catalyst requires macOS framework structure with proper symlink hierarchy (Versions/Current -> A and root-level symlinks), but builds were producing iOS framework structure
Solution: Automatically create required symlinks at two critical points:

  • Before xcodebuild (fixes compilation errors)
  • After xcodebuild in final app bundle (fixes App Store validation)

3. Incremental Build Issues

Symptom: EEXIST: file already exists errors when rebuilding without ti clean
Root cause: Previous SDK versions created directories instead of symlinks, or symlinks became broken during incremental builds
Solution: Detect and remove directories, files, and broken symlinks before creating new ones

Changes

Modified Files

  • iphone/cli/commands/_build.js

Specific Changes

  1. Line 3524: Updated FRAMEWORK_SEARCH_PATHS from "Frameworks" to "$(PROJECT_DIR)/Frameworks"

  2. Lines 7116-7242: Added createTitaniumKitSymlinks() function

    • Guards execution to Mac Catalyst targets only (macos, dist-macappstore)
    • Creates proper macOS framework structure with symlinks
    • Handles both build directory and final app bundle locations
    • Intelligently handles incremental builds:
      • Detects directories created by previous SDK versions
      • Detects broken symlinks (symlinks pointing to non-existent targets)
      • Verifies existing symlinks point to correct targets
      • Removes incorrect/broken items before creating new ones
    • Uses lstatSync() instead of existsSync() to detect broken symlinks
    • Provides informative logging for debugging
  3. Lines 7101, 7104, 7433: Integrated symlink creation into build pipeline

    • Called before xcodebuild to fix compilation
    • Called after xcodebuild to fix App Store packaging

Impact

What This Fixes

  • ✅ Mac Catalyst apps now compile successfully
  • ✅ Incremental builds work without ti clean required
  • ✅ Broken symlinks from previous builds are automatically fixed
  • ✅ Directories created by SDK 13.0.1.GA are automatically converted to symlinks
  • ✅ App Store validation passes for Mac Catalyst apps
  • ✅ Enables distribution through Mac App Store
  • ✅ Applies to all Titanium apps targeting Mac Catalyst

What This Doesn't Affect

  • Standard iOS device/simulator builds continue to work identically
  • Android builds are completely unaffected
  • Changes only execute for Mac Catalyst targets due to explicit guards

Testing

Verified across multiple scenarios with different Titanium projects:

  • ✅ Fresh project builds successfully
  • ✅ Incremental builds work without ti clean
  • ✅ Development builds (Debug-maccatalyst) compile and run
  • ✅ Distribution builds (Release-maccatalyst) create valid xcarchive
  • ✅ App Store validation accepts framework structure
  • ✅ Broken symlinks from previous builds are automatically repaired
  • ✅ Standard iOS builds unaffected
  • ✅ Existing iOS projects continue working normally

Additional Context

The issues fixed by this PR affected any Titanium application attempting to:

  • Compile for Mac Catalyst (Target: macos)
  • Create distribution builds for Mac App Store (Target: dist-macappstore)

These were fundamental build system issues, not app-specific problems.

⚠️ Module Compatibility

Mac Catalyst builds require native modules to include an ios-arm64_x86_64-maccatalyst slice in their xcframework.

What this means:

  • If a module doesn't support Mac Catalyst, builds targeting Mac will fail
  • iOS device and simulator builds are completely unaffected
  • This is expected behavior - not all modules can or should support Mac

Example error when a module lacks Mac Catalyst support:

error: While building for Mac Catalyst, no library for this platform was found in
'module.xcframework'

For module developers:
To enable Mac Catalyst support in your module:

  1. Add mac: true to your module's manifest file
  2. Rebuild the module - the SDK will automatically build all three slices

Note: Some modules may have legitimate reasons to not support Mac Catalyst (iOS-only APIs, hardware dependencies, etc.).

Backward Compatibility

This change is fully backward compatible:

  • No breaking changes to existing APIs
  • No changes to tiapp.xml configuration
  • No impact on existing iOS/Android workflows
  • Mac Catalyst builds that previously required workarounds now work natively
  • Automatically migrates projects from SDK 13.0.1.GA directory structure to proper symlinks

Note: This fix enables Mac Catalyst as a viable deployment target for all Titanium developers, opening the Mac App Store distribution channel that was previously blocked by these issues.

macCesar and others added 4 commits January 10, 2026 13:42
This commit fixes 4 critical bugs that prevented Mac Catalyst builds
from succeeding and being distributed via Mac App Store:

Bug tidev#1: SWIFT_ACTIVE_COMPILATION_CONDITIONS (already fixed in template)
- Template already uses $(SWIFT_CONDITIONS) instead of $(GCC_DEFINITIONS)
- No changes needed

Bug tidev#2: Invalid FRAMEWORK_SEARCH_PATHS
- Changed relative path 'Frameworks' to absolute '$(PROJECT_DIR)/Frameworks'
- Fixes 'Unable to find module dependency: TitaniumKit' error
- Location: Line 3524

Bug tidev#3: Missing build-time symlinks in TitaniumKit.framework
- Created createTitaniumKitSymlinks() function to ensure proper macOS framework structure
- Creates required symlinks: Versions/Current, TitaniumKit, Headers, Resources, Modules
- Called before xcodebuild to fix build directory frameworks
- Locations: Lines 7114-7207, 7101, 7104

Bug tidev#4: Missing distribution symlinks in final app bundle
- Same createTitaniumKitSymlinks() function handles app bundle
- Called after xcodebuild completes to fix frameworks in .app/Contents/Frameworks
- Prevents App Store rejection due to missing/malformed TitaniumKit
- Location: Line 7433

Impact:
- Enables successful Mac Catalyst compilation (ti build -p ios -T macos)
- Enables App Store distribution (ti build -p ios -T macos --deploy-type production)
- Eliminates need for 'ti clean' before each build
- Restores fast incremental builds for Mac Catalyst

Tested with: TiDesigner app (iPad app with modules: ti.compression, dk.napp.social)

Related documentation: MAC_CATALYST_BUILD_FIXES.md in TiDesigner project
Document all Mac Catalyst build fixes in CHANGELOG:
- FRAMEWORK_SEARCH_PATHS absolute path fix
- TitaniumKit.framework symlink creation
- Clearly note Mac Catalyst-only impact (no effect on iOS/Android)

Related to commit 3e403c6

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
macCesar and others added 2 commits January 10, 2026 18:21
…ilds

Improve createTitaniumKitSymlinks() to handle incremental builds:
- Detect and remove directories created by previous SDK versions
- Detect and verify existing symlinks point to correct targets
- Remove incorrect symlinks/directories/files before creating new ones
- Enables builds without `ti clean` for projects with existing builds

This fixes:
- "Couldn't resolve framework symlink" error (readlink on directory)
- "EEXIST: file already exists" on incremental rebuilds
- Allows seamless migration from SDK 13.0.1.GA to 13.1.0

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Improve ensureSymlink() to handle broken symlinks correctly:
- Use lstatSync() instead of existsSync() to detect broken symlinks
- existsSync() follows symlinks and returns false for broken ones
- lstatSync() doesn't follow symlinks and detects them even if broken
- Verify symlink target exists before considering it valid
- Remove and recreate broken symlinks automatically

This fixes EEXIST errors in incremental builds when symlinks
point to targets that no longer exist.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Copy link
Collaborator

@hansemannn hansemannn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these changes look quite verbose, so I'm wondering if only parts of these changes are actually relevant for this precise fix.

Comment on lines 7119 to 7120
// Bug #3: Build-time symlinks in build/Frameworks directory
// Bug #4: Distribution symlinks in final app bundle
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these comments, it's looks odd as part of an overall build script. If necessary, add it to the comments

…back

- Remove verbose "Bug #X" comments, use descriptive explanations instead
- Simplify ensureSymlink function from 44 to 23 lines
- Remove redundant fs.existsSync check as suggested by reviewer
- Remove unused logger parameter from ensureSymlink

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@macCesar
Copy link
Contributor Author

@hansemannn Thanks for the detailed feedback! I've addressed your concerns in the latest commit:

Changes made:

  1. Removed "Bug #X" comments - Replaced with descriptive explanations that make sense in context

  2. Simplified ensureSymlink function - Reduced from 44 to 23 lines (~50% less code):

    • Removed redundant fs.existsSync check as you suggested
    • Consolidated the symlink validation logic
    • Removed verbose debug logging
    • Removed unused logger parameter
  3. Kept FRAMEWORK_SEARCH_PATHS - The $(PROJECT_DIR)/Frameworks path is necessary for Mac Catalyst. Without it, the linker fails with "Unable to find module dependency: TitaniumKit"

Testing performed:

  • ✅ Clean build (ti build -p ios -T macos)
  • ✅ Incremental build (detects existing symlinks correctly)
  • ✅ Distribution build (ti build -p ios -T dist-macappstore)
  • ✅ Framework structure verified with correct symlinks

Ready for your review when you have a chance!

Copy link
Collaborator

@hansemannn hansemannn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work, we can approve it now! :)

@hansemannn hansemannn merged commit 07d625d into tidev:main Jan 17, 2026
7 checks passed
@macCesar macCesar deleted the fix/mac-catalyst-builds branch January 21, 2026 19:48
hansemannn pushed a commit that referenced this pull request Jan 29, 2026
…ion issues (#14370)

* fix(ios): Fix Mac Catalyst build failures and App Store distribution

This commit fixes 4 critical bugs that prevented Mac Catalyst builds
from succeeding and being distributed via Mac App Store:

Bug #1: SWIFT_ACTIVE_COMPILATION_CONDITIONS (already fixed in template)
- Template already uses $(SWIFT_CONDITIONS) instead of $(GCC_DEFINITIONS)
- No changes needed

Bug #2: Invalid FRAMEWORK_SEARCH_PATHS
- Changed relative path 'Frameworks' to absolute '$(PROJECT_DIR)/Frameworks'
- Fixes 'Unable to find module dependency: TitaniumKit' error
- Location: Line 3524

Bug #3: Missing build-time symlinks in TitaniumKit.framework
- Created createTitaniumKitSymlinks() function to ensure proper macOS framework structure
- Creates required symlinks: Versions/Current, TitaniumKit, Headers, Resources, Modules
- Called before xcodebuild to fix build directory frameworks
- Locations: Lines 7114-7207, 7101, 7104

Bug #4: Missing distribution symlinks in final app bundle
- Same createTitaniumKitSymlinks() function handles app bundle
- Called after xcodebuild completes to fix frameworks in .app/Contents/Frameworks
- Prevents App Store rejection due to missing/malformed TitaniumKit
- Location: Line 7433

Impact:
- Enables successful Mac Catalyst compilation (ti build -p ios -T macos)
- Enables App Store distribution (ti build -p ios -T macos --deploy-type production)
- Eliminates need for 'ti clean' before each build
- Restores fast incremental builds for Mac Catalyst

Tested with: TiDesigner app (iPad app with modules: ti.compression, dk.napp.social)

Related documentation: MAC_CATALYST_BUILD_FIXES.md in TiDesigner project

* docs: Add Mac Catalyst fixes to CHANGELOG.md for v13.1.0

Document all Mac Catalyst build fixes in CHANGELOG:
- FRAMEWORK_SEARCH_PATHS absolute path fix
- TitaniumKit.framework symlink creation
- Clearly note Mac Catalyst-only impact (no effect on iOS/Android)

Related to commit 3e403c6

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* chore: remove changelog updates (generated during release)

* fix: address eslint for titanium kit symlinks

* fix(ios): Handle existing directories and symlinks in Mac Catalyst builds

Improve createTitaniumKitSymlinks() to handle incremental builds:
- Detect and remove directories created by previous SDK versions
- Detect and verify existing symlinks point to correct targets
- Remove incorrect symlinks/directories/files before creating new ones
- Enables builds without `ti clean` for projects with existing builds

This fixes:
- "Couldn't resolve framework symlink" error (readlink on directory)
- "EEXIST: file already exists" on incremental rebuilds
- Allows seamless migration from SDK 13.0.1.GA to 13.1.0

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* fix(ios): Detect and fix broken symlinks in Mac Catalyst builds

Improve ensureSymlink() to handle broken symlinks correctly:
- Use lstatSync() instead of existsSync() to detect broken symlinks
- existsSync() follows symlinks and returns false for broken ones
- lstatSync() doesn't follow symlinks and detects them even if broken
- Verify symlink target exists before considering it valid
- Remove and recreate broken symlinks automatically

This fixes EEXIST errors in incremental builds when symlinks
point to targets that no longer exist.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* refactor(ios): simplify Mac Catalyst symlink handling per review feedback

- Remove verbose "Bug #X" comments, use descriptive explanations instead
- Simplify ensureSymlink function from 44 to 23 lines
- Remove redundant fs.existsSync check as suggested by reviewer
- Remove unused logger parameter from ensureSymlink

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.5 <[email protected]>
@hansemannn
Copy link
Collaborator

@macCesar After merging all changes for 13.1.1 (incl. yours) to 13_1_X, the build fails due to a Mac Catalyst error:

https://github.com/tidev/titanium-sdk/actions/runs/21472015632/job/61846652200

Can you please review the issue? Note: The build was triggered by the tab-bar fixes, but only because that was the last cherry-picked commit before pushing it to the branch where the CI starts.

@hbugdoll
Copy link
Contributor

hbugdoll commented Jan 29, 2026

@hansemannn I don't think it was a Mac Catalyst error.
After macOS and Simulator xcarchives were created, the build process aborted at the beginning of the iOS step with:
error:iOS 26.0 is not installed.

Therefore error: Unable to find a destination matching the provided destination specifier: { ... platform:iOS } and only platform:macOS destitations were available.

Now with your update to Xcode 26.2, it seems to be working 👍
https://github.com/tidev/titanium-sdk/actions/runs/21473518471/job/61851657778

Edit: Ah, I saw your discussion on Slack too late (I only use it in the browser, not the app).

@macCesar
Copy link
Contributor Author

@hansemannn @hbugdoll Thanks for clarifying!

So the failure was NOT caused by my PR, is that correct?

Sorry, I'm not very familiar with this type of CI/workflows! Still learning 🙂

Glad to hear everything is working now with Xcode 26.2!

@hbugdoll
Copy link
Contributor

So the failure was NOT caused by my PR, is that correct?

Yep.

@hbugdoll
Copy link
Contributor

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.

4 participants