Making SDKMAN Java Work with macOS: A One-Line Fix

If you've ever tried building a Kotlin Multiplatform project in Xcode with a SDKMAN-installed JDK, you've probably seen this frustrating error:

The operation couldn't be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.

Your Java works perfectly in the terminal. java -version shows exactly what you expect. But Xcode? It's completely blind to your installation.

The Problem

This issue surfaced when I was working on iOS apps with Kotlin, which relies on Gradle being called from Xcode Script Build Phases. The root cause is a fundamental mismatch in how SDKMAN and macOS handle Java installations.

SDKMAN installs JDKs to:

~/.sdkman/candidates/java/

But macOS looks for JDKs in:

/Library/Java/JavaVirtualMachines/

The key player here is /usr/libexec/java_home - a mostly undocumented macOS utility that tools like Xcode use to locate Java installations. It completely ignores JAVA_HOME environment variables and only looks for JDKs in the standard macOS location with a specific directory structure.

Credit goes to Ribesg who discovered and documented this solution on StackOverflow. Their answer has helped thousands of developers facing the same issue. I've automated their manual process into a one-line installer.

The Manual Solution (Thanks Ribesg!)

Ribesg figured out that /usr/libexec/java_home expects this specific structure:

jdk-root-folder/
  Contents/
    Info.plist
    Home/
      <actual JDK files>

The fix is to create a "fake" JDK structure that symlinks to your SDKMAN installation:

  1. Create /Library/Java/JavaVirtualMachines/sdkman-current/Contents/
  2. Symlink Contents/Home to ~/.sdkman/candidates/java/current
  3. Add an Info.plist with proper metadata

The clever part: by setting the version to 9999 in the plist, this JDK always becomes the default when multiple versions exist.

The One-Line Fix

Manual setup is error-prone and tedious. So I automated it:

curl -fsSL https://gist.githubusercontent.com/abd3lraouf/1db9bf863144802733bfd29bb5dada87/raw/install.sh | bash -s install

This script:

The best part? When you switch Java versions with sdk use java 17, the integration automatically follows because it points to SDKMAN's current symlink.

Verification

After running the script, verify it works:

/usr/libexec/java_home
# Output: /Library/Java/JavaVirtualMachines/sdkman-current/Contents/Home

/usr/libexec/java_home -V
# Shows your SDKMAN JDK in the list

Now Xcode build phases, VSCode Java extensions, and any tool relying on /usr/libexec/java_home will find your SDKMAN installation.

Under the Hood

The script creates this structure:

/Library/Java/JavaVirtualMachines/sdkman-current/
├── Contents/
│   ├── Home → ~/.sdkman/candidates/java/current
│   └── Info.plist

The Info.plist contains:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>sdkman.current</string>
    <key>CFBundleName</key>
    <string>SDKMAN Current JDK</string>
    <key>JavaVM</key>
    <dict>
        <key>JVMVersion</key>
        <string>9999</string>
    </dict>
</dict>
</plist>

Other Commands

The script supports more than just installation:

# Verify the setup is working
./install.sh verify

# Clean removal
./install.sh uninstall

# Show help
./install.sh help

Why This Matters

This isn't just about Xcode. Many tools rely on /usr/libexec/java_home:

If you're using SDKMAN (and you should be - it's the best way to manage Java versions), this one-line fix bridges the gap between SDKMAN's flexible version management and macOS's rigid expectations.

Resources

Stop fighting with Java installations. Run the one-liner and get back to building.