Skip to main content

Solving 'No such module' & iOS Build Errors in Kotlin Multiplatform

 There is a specific kind of frustration reserved for mobile developers transitioning to Kotlin Multiplatform (KMP). You have your shared logic compiling perfectly in Android Studio. Your unit tests are green. You open Xcode, hit Cmd + B, and are immediately greeted by a red error: "No such module 'shared'" or a cryptic failure in the script phase embedAndSignAppleFrameworkForXcode.

This is the "Hello World" of KMP configuration issues. It usually isn't a code problem; it is a toolchain orchestration problem.

This guide bridges the gap between Gradle and Xcode. We will dissect why the toolchains disconnect, how to verify your architecture targets, and how to permanently fix the "No such module" error using modern KMP best practices.

The Root Cause: The Gradle-Xcode Handshake

To fix the error, you must understand the build pipeline. Xcode does not know how to compile Kotlin. It relies on a "Run Script" build phase to delegate that work to Gradle.

When you press Build in Xcode, the following sequence attempts to happen:

  1. Xcode triggers a Run Script Phase.
  2. The Script invokes the Gradle Wrapper (./gradlew).
  3. Gradle runs the embedAndSignAppleFrameworkForXcode task. This task compiles the Kotlin code through the LLVM backend into an Apple Framework (.framework).
  4. Gradle places the Framework in a specific build directory.
  5. Xcode resumes compilation, looking for that framework in its "Framework Search Paths."

The "No such module" error occurs when step 4 fails (no framework generated) or step 5 fails (Xcode is looking in the wrong place). The embedAndSign error occurs when step 3 fails due to environment mismatches (usually Java versions or architecture types).

Solution 1: The "User Script Sandboxing" Trap

If you are using Xcode 14, 15, or newer, this is the most likely culprit. Apple introduced stricter build hardening which prevents build scripts from modifying files outside of specific directories.

Since Gradle needs to write to build directories that might be considered "external" to the strictly sandboxed process, the build fails silently or with permission errors, resulting in no framework generation.

The Fix

  1. Open your iOS project in Xcode.
  2. Select your Project (the blue icon at the top of the file navigator).
  3. Select the Build Settings tab.
  4. Filter for the term "Sandbox".
  5. Locate "User Script Sandboxing" (typically under Build Options).
  6. Set this value to No.

Why This Works

Disabling sandboxing allows the shell script invoked by Xcode to have full access to the file system. This grants the Gradle wrapper the necessary permissions to delete old artifacts and write the new binary framework into the derived data or project build folders.

Solution 2: The Java Home Mismatch

Xcode executes build scripts in a stripped-down shell environment. It does not inherit your .zshrc or .bash_profile. Consequently, it often cannot find the JDK required to run Gradle, or it defaults to a system Java version (like Java 8) that is incompatible with modern Android/Kotlin plugins.

The Modern Fix: .xcode.env

Do not hardcode paths in your Build Phase script. The modern KMP standard utilizes a dedicated environment file.

  1. Navigate to your iOS project root folder (where the .xcodeproj lives).
  2. Create or edit a file named .xcode.env.local (for local machine specific settings) or .xcode.env (for shared settings).
  3. Add your JAVA_HOME export explicitly.
# inside .xcode.env.local
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"

Note: The path above points to the JDK bundled with Android Studio, which is a safe bet for KMP compatibility. You can find your specific path by running echo $JAVA_HOME in your terminal.

Verifying the Build Phase Script

Ensure your Xcode "Run Script" phase is actually utilizing this setup. Open Build Phases > Run Script (it should be before "Compile Sources") and verify it looks like this:

cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode

Solution 3: Correcting Framework Search Paths

If Gradle builds successfully (you see BUILD SUCCESSFUL in the report navigator) but Xcode still claims "No such module", Xcode simply doesn't know where Gradle put the output.

The Fix

  1. In Xcode, navigate to Build Settings.
  2. Search for Framework Search Paths.
  3. Ensure the configuration handles the dynamic nature of the build.

Add the following path to the list:

$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)

Deep Dive

  • $(SRCROOT)/..: Moves up from the iOS folder to the root KMP folder.
  • shared/build/xcode-frameworks: The standard output directory for the CocoaPods or standard framework Gradle plugin.
  • $(CONFIGURATION): Resolves to Debug or Release.
  • $(SDK_NAME): Resolves to iphoneos (device) or iphonesimulator (simulator).

This dynamic path ensures that if you switch from a physical iPhone to a Simulator, Xcode looks in the folder where Gradle deposited the architecture-specific binary.

Solution 4: The Architecture Mismatch (M1/M2 vs Intel)

A common issue on Apple Silicon (M1/M2/M3) machines occurs when the Kotlin compiler generates valid binaries, but not for the architecture the simulator is requesting.

If you are running an iOS Simulator on Apple Silicon, you need the iosSimulatorArm64 target. If you only have iosX64 defined, the simulator (which runs on arm64 natively on M-series chips) will reject the framework.

The Fix

Open your shared module's build.gradle.kts and ensure you are defining your targets comprehensively:

// shared/build.gradle.kts
kotlin {
    // Target for physical iPhones
    iosArm64() 
    
    // Target for Simulators on Apple Silicon (M1/M2/M3)
    iosSimulatorArm64()
    
    // Target for Simulators on Intel Macs (or Rosetta)
    iosX64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        version = "1.0"
        ios.deploymentTarget = "16.0" // Ensure this matches your Xcode project
        
        framework {
            baseName = "shared"
            isStatic = true // Often resolves linking issues
        }
    }
    
    sourceSets {
        val commonMain by getting
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        
        // Create an intermediate source set to avoid duplicating code
        // for different iOS architectures
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
    }
}

Why This Matters

If iosSimulatorArm64 is missing, Gradle will not build the slice required for the modern iOS Simulator. Xcode will search the output folder, find a framework for iosX64, see that it doesn't match the simulator's CPU architecture, and throw the "No such module" error because it considers the framework invalid.

Advanced Debugging: The "Clean" Routine

If you have applied the fixes above and still face issues, you are likely suffering from a stale cache. Xcode's DerivedData is aggressive, and Gradle's caching is persistent.

Perform this exact sequence to force a full toolchain reset:

  1. Clean Gradle:
    ./gradlew clean
    
  2. Nuke Xcode Derived Data: Navigate to ~/Library/Developer/Xcode/DerivedData and delete the folder corresponding to your project.
  3. Clean Xcode Build: In Xcode, press Cmd + Shift + K.
  4. Rebuild: Press Cmd + B.

Conclusion

The "No such module" error is rarely a mystery; it is a mismatch in file paths, permissions, or CPU architectures. By ensuring User Script Sandboxing is off, explicitly defining your Java environment, and configuring your Kotlin Gradle targets for both Simulator and Device architectures, you create a robust bridge between the two ecosystems.

Once configured correctly, the KMP build process becomes transparent, allowing you to focus on sharing business logic rather than fighting the build tools.