Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,3 @@ theme.qrc

# Swift Package Manager Build Artifacts
.swiftpm_codeql_detected_source_root

# Relative derived data of Xcode workspace
.xcode

# Mac-Crafter build directory
.mac-crafter
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ Avoid creating source files that implement multiple types; instead, place each t

## macOS Specifics

The following details are important when working on the desktop client on macOS.
The following details are important and only relevant when working on the desktop client on macOS.

### Requirements

- Latest stable Xcode available is required to be installed in the development environment.
- The targeted macOS release (and all newer major releases) is specified in `./CMakeLists.txt`.

### Project Structure

Expand Down Expand Up @@ -83,4 +84,5 @@ The following details are important when working on the desktop client on macOS.
- Do not test for logging by subjects under test.
- If there are changes in the Swift package located in `./shell_integration/MacOSX/NextcloudFileProviderKit`, then verify it still builds and runs tests successfully by running `swift test` in that directory. In case of build errors, try to fix them.
- If there are changes in the directory located in `./shell_integration/MacOSX/NextcloudIntegration`, then verify it still builds and runs tests successfully by running `xcodebuild build -scheme desktopclient` in that directory. In case of build errors, try to fix them.
- If there are changes in `./src`, then verify the main product still builds successfully by running `xcodebuild build -target NextcloudDev` in the directory `./shell_integration/MacOSX/NextcloudIntegration`. In case of build errors, try to fix them.
- If there are changes in `./src`, then verify the main product still builds successfully by running `xcodebuild build -target NextcloudDev` in the directory `./shell_integration/MacOSX/NextcloudIntegration`. In case of build errors, try to fix them.
- Do not attempt in place modifications of the built app bundle at `/Applications/NextcloudDev.app` because it will break the valid signing and corrupt the app as a whole. A rebuild is necessary instead.
141 changes: 115 additions & 26 deletions admin/osx/mac-crafter/Sources/Commands/Build.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,39 +293,43 @@ struct Build: AsyncParsableCommand {
.appendingPathComponent("image-\(buildType)-master")
.appendingPathComponent("\(appName).app")

// When building in dev mode, copy the dSYM bundles for the app extensions from the
// xcodebuild SYMROOT into Contents/PlugIns/ of the product app bundle alongside their
// respective .appex bundles.
// When building in dev mode, copy dSYM bundles into the build directory
// so that Xcode/LLDB can resolve breakpoints via Spotlight UUID lookup.
//
// Background: KDE Craft's __internalPostInstallHandleSymbols() deliberately moves every
// .dSYM bundle out of the main image directory and into a separate -dbg image directory
// before packaging. This means dSYMs never reach the product app via the normal CMake
// install() path. Reading directly from the xcodebuild SYMROOT bypasses that filtering.
// Background: KDE Craft's __internalPostInstallHandleSymbols() moves
// every .dSYM bundle out of the main image directory into a separate
// -dbg image directory whose contents have epoch (1970) timestamps and a
// double-nested DWARF structure — both of which prevent Spotlight from
// indexing them. Copying and flattening the bundles into the build
// directory (which Spotlight does index) makes them discoverable.
//
// With the dSYMs inside the app bundle under /Applications, Spotlight indexes them and
// Xcode can find them automatically via UUID lookup when attaching to the extension process,
// which allows breakpoints in extension source files to be resolved correctly.
// Placing the dSYMs in the build directory keeps /Applications clean and
// lets the entire build tree be discarded in one step.

if dev {
let dSYM = clientBuildURL
.appendingPathComponent("image-\(buildType)-master-dbg")
.appendingPathComponent("\(appName).app.dSYM")

let binaryLocation = clientAppURL
.appendingPathComponent("Contents")
.appendingPathComponent("MacOS")
.appendingPathComponent("\(appName).app.dSYM")
let dSYMDestination = buildURL.appendingPathComponent("\(appName).app.dSYM")

Log.info("Copying main dSYM bundle at \"\(dSYM.path)\" into product app bundle \"\(binaryLocation.path)\" for debugging...")
Log.info("Copying main dSYM bundle to \"\(dSYMDestination.path)\"...")

if fm.fileExists(atPath: binaryLocation.path) {
Log.info("Removing already existing main dSYM bundle at \"\(binaryLocation.path)\"...")
try fm.removeItem(at: binaryLocation)
if fm.fileExists(atPath: dSYMDestination.path) {
Log.info("Removing already existing main dSYM bundle at \"\(dSYMDestination.path)\"...")
try fm.removeItem(at: dSYMDestination)
}

try fm.copyItem(at: dSYM, to: binaryLocation)
try fm.copyItem(at: dSYM, to: dSYMDestination)

Log.info("Copying extension dSYM bundles into product app bundle for debugging...")
// KDE Craft's __internalPostInstallHandleSymbols() packs each
// library's complete .dSYM bundle as a subdirectory under
// Contents/Resources/DWARF/ instead of placing the bare DWARF binary
// there. Flatten it back to the standard layout so that Spotlight
// can extract UUIDs and LLDB can resolve breakpoints.
try flattenDSYMBundle(at: dSYMDestination)

Log.info("Copying extension dSYM bundles to \"\(buildURL.path)\"...")

let shellIntegrationBuildDir = clientBuildURL
.appendingPathComponent("work")
Expand All @@ -334,20 +338,16 @@ struct Build: AsyncParsableCommand {
.appendingPathComponent("MacOSX")
.appendingPathComponent(buildType)

let plugInsDir = clientAppURL
.appendingPathComponent("Contents")
.appendingPathComponent("PlugIns")

guard fm.fileExists(atPath: shellIntegrationBuildDir.path) else {
Log.info("Shell integration build directory not found, skipping dSYM copy: \(shellIntegrationBuildDir.path)")
Log.info("Shell integration build directory not found, skipping extension dSYM copy: \(shellIntegrationBuildDir.path)")
return
}

let entries = try fm.contentsOfDirectory(at: shellIntegrationBuildDir, includingPropertiesForKeys: [.isDirectoryKey])
let dSYMBundles = entries.filter { $0.pathExtension.lowercased() == "dsym" }

for dSYM in dSYMBundles {
let destination = plugInsDir.appendingPathComponent(dSYM.lastPathComponent)
let destination = buildURL.appendingPathComponent(dSYM.lastPathComponent)

if fm.fileExists(atPath: destination.path) {
Log.info("Removing already existing extension dSYM bundle at \"\(destination.path)\"...")
Expand Down Expand Up @@ -434,4 +434,93 @@ struct Build: AsyncParsableCommand {
Log.info("Done!")
Log.info(stopwatch.report())
}

/// Flatten a dSYM bundle whose `Contents/Resources/DWARF/` entries are
/// nested dSYM bundles (directories) instead of bare Mach-O DWARF files.
///
/// KDE Craft's symbol handling can produce this layout:
/// ```
/// Foo.app.dSYM/Contents/Resources/DWARF/Foo/Contents/Resources/DWARF/Foo ← actual DWARF binary
/// Foo.app.dSYM/Contents/Resources/DWARF/libBar.dylib/Contents/…/libBar.dylib
/// ```
/// This function rewrites it to the standard layout expected by Spotlight
/// and LLDB:
/// ```
/// Foo.app.dSYM/Contents/Info.plist
/// Foo.app.dSYM/Contents/Resources/DWARF/Foo ← bare binary
/// Foo.app.dSYM/Contents/Resources/DWARF/libBar.dylib ← bare binary
/// ```
private func flattenDSYMBundle(at bundle: URL) throws {
let fm = FileManager.default

let dwarfDir = bundle
.appendingPathComponent("Contents")
.appendingPathComponent("Resources")
.appendingPathComponent("DWARF")

guard fm.fileExists(atPath: dwarfDir.path) else {
return
}

let entries = try fm.contentsOfDirectory(at: dwarfDir, includingPropertiesForKeys: [.isDirectoryKey])

// Determine the main binary name from the dSYM bundle name (e.g.
// "NextcloudDev.app.dSYM" → "NextcloudDev").
var dSYMName = bundle.deletingPathExtension().lastPathComponent // strip .dSYM

if dSYMName.hasSuffix(".app") {
dSYMName = String(dSYMName.dropLast(4)) // strip .app
}

var promotedInfoPlist = false

for entry in entries {
let values = try entry.resourceValues(forKeys: [.isDirectoryKey])
guard values.isDirectory == true else {
continue
}

// Look for the inner DWARF binary.
let innerDWARF = entry
.appendingPathComponent("Contents")
.appendingPathComponent("Resources")
.appendingPathComponent("DWARF")
.appendingPathComponent(entry.lastPathComponent)

guard fm.fileExists(atPath: innerDWARF.path) else {
Log.info("Skipping unexpected directory in DWARF/: \(entry.lastPathComponent)")
continue
}

// Promote the inner Info.plist to the top-level bundle for the
// main binary so that Spotlight can index the dSYM by UUID.
if !promotedInfoPlist && entry.lastPathComponent == dSYMName {
let innerPlist = entry
.appendingPathComponent("Contents")
.appendingPathComponent("Info.plist")

let outerPlist = bundle
.appendingPathComponent("Contents")
.appendingPathComponent("Info.plist")

if fm.fileExists(atPath: innerPlist.path) {
if fm.fileExists(atPath: outerPlist.path) {
try fm.removeItem(at: outerPlist)
}

try fm.copyItem(at: innerPlist, to: outerPlist)
promotedInfoPlist = true
Log.info("Promoted Info.plist from inner dSYM to top-level bundle.")
}
}

// Replace the nested directory with the bare DWARF binary.
let flatTarget = dwarfDir.appendingPathComponent(entry.lastPathComponent + ".tmp")
try fm.copyItem(at: innerDWARF, to: flatTarget)
try fm.removeItem(at: entry)
try fm.moveItem(at: flatTarget, to: entry)

Log.info("Flattened DWARF entry: \(entry.lastPathComponent)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ else
fi

swift run mac-crafter \
--build-path="$DESKTOP_CLIENT_PROJECT_ROOT/.mac-crafter" \
--build-path="$DESKTOP_CLIENT_PROJECT_ROOT/build" \
--product-path="/Applications" \
--build-type="Debug" \
--dev \
Expand Down
Loading