Cloud Cloud

Iva Horn

Notes from a software engineer for iPhone, iPad and Mac apps.

Memoji

Xcode Breakpoints in the Nextcloud Desktop Client

February 23, 2026 • #Debugging #File Provider #KDE Craft #MacOS #Nextcloud #Xcode

When attaching Xcode to the file provider extension process of the Nextcloud desktop client, the debugger connected just fine — but every single breakpoint was displayed hollow and in dashes which means Xcode cannot associate the position with a line of code on disk. Tracking down the root cause turned into quite the adventure.

The Nextcloud desktop client ships a macOS File Provider extension written in Swift. To clarify the following quotes and snippets: its target and binary are named FileProviderExt. The app itself is not built through an Xcode project you open and press Run on — instead, a Swift CLI tool called mac-crafter in admin/osx/mac-crafter/ utilizes KDE Craft underneath, which in turn calls xcodebuild with a custom SYMROOT pointing into the Craft build directory. The Xcode scheme used for development wraps the entire mac-crafter invocation as an external build tool. That inevitable Jenga tower of build tooling sabotaged the journey of our debugging symbols.

The Binary Has No DWARF — And That Is Fine

First thing I checked: does the installed binary actually have debug information embedded in it?

dwarfdump --debug-info \
  /Applications/NextcloudDev.app/Contents/PlugIns/FileProviderExt.appex/Contents/MacOS/FileProviderExt

Output: .debug_info contents: — completely empty. A bit alarming at first glance, but this is actually perfectly normal. Xcode strips debug info from the binary and extracts it into a separate .dSYM bundle during the build. The binary stays lean; the symbols live elsewhere. The question became: where is the dSYM?

Found it, buried deep in the Craft build directory at:

.mac-crafter/macos-clang-arm64/build/nextcloud-client/work/build/shell_integration/MacOSX/Debug/FileProviderExt.appex.dSYM

A quick UUID check with dwarfdump --uuid confirmed it matched the installed binary — the symbols existed, Xcode just could not find them.

Spotlight Does Not Index Dot-Directories

This is the core of the problem! Xcode does not search for dSYM bundles by walking directories manually — it uses Spotlight, via a UUID lookup. The dSYM lives inside .mac-crafter/, a hidden directory (dot-prefix). Spotlight skips hidden directories by default, so the dSYM bundle was completely invisible to Xcode’s symbol search, regardless of whether the UUID matched.

a memoji

The dSYM Is Not in the App Bundle on Purpose

Reading the build log more carefully revealed two things: xcodebuild is invoked with DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT=NO, so dSYMs are intentionally not copied alongside the product by the outer Xcode build. On top of that, SYMROOT points into the Craft build directory rather than Xcode’s DerivedData, so DerivedData never sees them at all.

My first fix attempt was to add CMake install rules in shell_integration/MacOSX/CMakeLists.txt to copy the dSYMs into the app bundle:

install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Debug/FileProviderExt.appex.dSYM
        DESTINATION ${CMAKE_INSTALL_PREFIX}/Contents/PlugIns)

The staging log showed them being installed — but they never made it into the final app.

a memoji

KDE Craft Deliberately Moves dSYMs Away

Digging into KDE Craft’s Python source (craft-clone/bin/BuildSystem/BuildSystemBase.py) explained exactly why:

# __internalPostInstallHandleSymbols() -- runs after every cmake install
symbolsPattern = re.compile(r".*\.dSYM$", re.IGNORECASE)
for f in utils.filterDirectoryContent(self.imageDir(), symFilter, ...):
    dest = self.symbolsImageDir() / Path(f).relative_to(self.imageDir())
    utils.moveFile(f, dest)  # moves to image-Debug-master-dbg/

KDE Craft deliberately moves every dSYM bundle from the main image directory (image-Debug-master/) into a separate debug-symbols image (image-Debug-master-dbg/). It is a first-class feature designed for producing separate debug-symbol packages. The CMake install rules were therefore a dead end: they staged the dSYMs into the image directory, and Craft immediately moved them back out again before the app was assembled.

a memoji

The Fix: Copy dSYMs with mac-crafter

Since KDE Craft’s packaging pipeline actively works against putting dSYMs in the image, the right place to intervene is after KDE Craft is done — in the build command implementation of mac-crafter. By reading the dSYMs directly from the xcodebuild SYMROOT (bypassing the Craft image directory entirely), KDE Craft’s filtering becomes irrelevant.

A new section was added before the signing step, so the dSYMs are present inside the app bundle when it gets signed and then copied to its product path. After this change, the app bundle looks like this:

/Applications/NextcloudDev.app/Contents/PlugIns/
├── FileProviderExt.appex/
├── FileProviderExt.appex.dSYM/       ← now present
├── FileProviderUIExt.appex/
├── FileProviderUIExt.appex.dSYM/     ← now present
├── FinderSyncExt.appex/
└── FinderSyncExt.appex.dSYM/         ← now present

/Applications is Spotlight-indexed, so Xcode now finds the dSYMs automatically via UUID lookup when attaching to the extension process. Breakpoints resolve correctly.

a memoji

Useful Tools for This Investigation

A few tools that proved genuinely helpful throughout:

  • dwarfdump --debug-info <binary> — inspect embedded DWARF (empty output means symbols are in a dSYM)
  • dwarfdump --uuid <binary|dSYM> — verify the UUID match between a binary and its dSYM
  • mdfind "com_apple_xcode_dsym_uuids == <UUID>" — check whether Spotlight has indexed a given dSYM
  • lldbimage lookup -v -n <symbol> — inspect what source path LLDB resolves a symbol to
  • lldbsettings set target.source-map <old> <new> — remap source paths when they differ between build machine and local checkout