Archiving Catalyst project that embeds macOS tool

Hi!

I have a Catalyst app that embeds command line utility. So the project has two targets:

  1. Catalyst target, this target depends on #2 and embeds it into its bundle.
  2. macOS target, the command line tool.

Both targets have package dependency to the same package.

I used this to embed the CMD tool.

Everything builds, runs and works fine until I try to archive the project. Archiving stops early with such error:

error: Multiple commands produce '/Users/kse2/Library/Developer/Xcode/DerivedData/PkgTest-clngkndczxoprpdlwefqqiqlryjt/Build/Intermediates.noindex/ArchiveIntermediates/PkgTest/IntermediateBuildFilesPath/UninstalledProducts/macosx/MacrosForSwift.o'

note: Target 'MacrosForSwift' (project 'MacrosForSwift') has a command with output '/Users/kse2/Library/Developer/Xcode/DerivedData/PkgTest-clngkndczxoprpdlwefqqiqlryjt/Build/Intermediates.noindex/ArchiveIntermediates/PkgTest/IntermediateBuildFilesPath/UninstalledProducts/macosx/MacrosForSwift.o'

note: Target 'MacrosForSwift' (project 'MacrosForSwift') has a command with output '/Users/kse2/Library/Developer/Xcode/DerivedData/PkgTest-clngkndczxoprpdlwefqqiqlryjt/Build/Intermediates.noindex/ArchiveIntermediates/PkgTest/IntermediateBuildFilesPath/UninstalledProducts/macosx/MacrosForSwift.o'

What I have tried to fix archiving:

  1. Changing PkgTestCMD target to be on Catalyst/iOS instead of macOS. This works but I'm not sure how to properly run CMD tool with iOS SDK (if this correct at all): I need the main thread to be 'unblocked' and be active while background tasks exist.
  2. Adding an aux framework that act as container for the package. Doesn't work.
  3. Splitting targets into different projects, making workspace and cross-project reference. Doesn't work.

My understanding is that archiving attempts to produce .o files for both Catalyst and macOS simultaneously, due to PkgTest being Catalyst and PkgTestCMD being macOS targets.

How to archive such a project? Is there a way to separate archiving of CMD and main app? Maybe separate .o files into different directories.

Simple building and running the project works, why archiving doesn't want to?

I have a test project for the issue: PkgTest. main branch is the source project, DepFramework contains attempt #2, Workspace—#3.

Thank you!

So splitting targets into different projects is a way to go, but do not make cross-project references and add target dependency. The idea is to build the subproject fully separately.

Claude helped with implementation, so I asked it to write the rest of the post.

The Solution

Two scripts, triggered at different build stages:

1. Scheme Pre-Action — BuildPkgTestCMD.sh

Builds the CLI project via a separate xcodebuild invocation before the main build starts.

2. Run Script Build Phase — CopyPkgTestCMD.sh

Copies the built binary into the app bundle after the Resources phase.

Both scripts check EFFECTIVE_PLATFORM_NAME and skip on non-Catalyst builds (e.g. iOS).

Gotchas We Hit

Build settings leaking into the nested xcodebuild call.

Scheme pre-actions inherit all build settings from the parent target as environment variables. This means the nested xcodebuild silently picks up Catalyst platform, signing, and arch settings. Fix: wrap the call with env -i, passing through only selected variables:

env -i \
    PATH="$PATH" \
    HOME="$HOME" \
    TMPDIR="${TMPDIR:-/tmp}" \
    xcodebuild \
        -project "$PKGTESTCMD_PROJECT" \
        -target PkgTestCMD \
        -configuration "${CONFIGURATION}" \
        SYMROOT="${BUILD_DIR}/PkgTestCMD" \
        -quiet

Script sandboxing blocks reading the script file itself.

With ENABLE_USER_SCRIPT_SANDBOXING = YES (the default in recent Xcode), the Run Script phase can’t read its own .sh file unless it’s declared in inputPaths. Add the script path as the first input:

inputPaths:
  $(SRCROOT)/PkgTest/CopyPkgTestCMD.sh
  $(BUILD_DIR)/PkgTestCMD/$(CONFIGURATION)/PkgTestCMD

Archive fails with sandbox file-write-create deny.

During archive, the .app inside BUILT_PRODUCTS_DIR is actually a symlink to INSTALL_DIR/Applications/... The sandbox builds its write-allow list from declared outputPaths but doesn’t resolve symlinks — so the write to the real path gets denied. Fix: declare both paths in outputPaths:

outputPaths:
  $(BUILT_PRODUCTS_DIR)/$(EXECUTABLE_FOLDER_PATH)/PkgTestCMD
  $(DSTROOT)$(INSTALL_PATH)/$(EXECUTABLE_FOLDER_PATH)/PkgTestCMD

The first covers debug/run builds, the second covers the resolved archive path.

Result

Debug builds, iOS builds, and archives all work correctly. The CLI binary ends up at PkgTest.app/Contents/MacOS/PkgTestCMD alongside the main executable.

Archiving Catalyst project that embeds macOS tool
 
 
Q