IJPL-222935 Fix Settings Sync writing WSL JDK table to wrong location

When Settings Sync is enabled, it acts as an exclusive `com.intellij.configurationStore.StreamProvider`, so all settings writes go through `SettingsSyncIdeMediatorImpl.write()`, which calls `getFileRelativeToRootConfig()` to resolve the final disk path.

In 2025.3 the per-environment workspace model separation feature stores WSL JDK settings under an environment-specific subdirectory, e.g. "WSL-Ubuntu/jdk.table.xml". `getFileRelativeToRootConfig()` classified any fileSpec containing "/" as a "schema" path and returned it unchanged, causing the file to be written to "{configRoot}/WSL-Ubuntu/jdk.table.xml" instead of "{configRoot}/options/WSL-Ubuntu/jdk.table.xml". Because the `StreamProvider` is exclusive, the correct "options/" path was never written, and JPS couldn't find the JDK table → "Cannot find JDK for module".

Fix: extend the condition in `getFileRelativeToRootConfig()` to recognise env-prefixed paths whose filename is one of the known options-directory files ("jdk.table.xml", "applicationLibraries.xml") and prepend "options/" for them, just like for plain single-component specs.


(cherry picked from commit 5b62fc11ad09b637fcdba001d97846f1a30753ae)

IJ-MR-195770

GitOrigin-RevId: 49f0e95c83f3a3d19a2c56f5dad5e2a06545cff6
This commit is contained in:
Alexander Koshevoy
2026-03-12 14:08:03 +00:00
committed by intellij-monorepo-bot
parent 151ec953ac
commit 27f189e47e
2 changed files with 30 additions and 1 deletions
@@ -190,6 +190,16 @@ fun getPerOsSettingsStorageFolderName(): String = when {
else -> if (SystemInfoRt.isUnix) "unix" else "other_os"
}
/**
* File names that are stored inside the options directory but may arrive with an environment-specific
* subdirectory prefix (e.g. "WSL-Ubuntu/jdk.table.xml"). Despite containing a path separator, they
* still need the "options/" prefix — unlike schema files which already carry their full relative path.
*
* Note: an identical set exists privately in OptionsDirectoryProcessor (intellij.java.compiler.impl),
* kept separate to avoid a cross-module dependency.
*/
private val ENVIRONMENT_SPECIFIC_OPTIONS_FILENAMES = setOf("jdk.table.xml", "applicationLibraries.xml")
/**
* Converts fileSpec passed to [StreamProvider]'s methods to a relative path from the root config directory.
*/
@@ -197,7 +207,9 @@ fun getPerOsSettingsStorageFolderName(): String = when {
fun getFileRelativeToRootConfig(fileSpecPassedToProvider: String): String =
// For PersistentStateComponents the fileSpec is passed without the 'options' folder, e.g. 'editor.xml' or 'mac/keymaps.xml'
// OTOH for schemas it is passed together with the containing folder, e.g. 'keymaps/my_keymap.xml'
if (!fileSpecPassedToProvider.contains("/") || fileSpecPassedToProvider.startsWith(getPerOsSettingsStorageFolderName() + "/")) {
if (!fileSpecPassedToProvider.contains("/")
|| fileSpecPassedToProvider.startsWith(getPerOsSettingsStorageFolderName() + "/")
|| ENVIRONMENT_SPECIFIC_OPTIONS_FILENAMES.any { fileSpecPassedToProvider.endsWith("/$it") }) {
"${PathManager.OPTIONS_DIRECTORY}/${fileSpecPassedToProvider}"
}
else fileSpecPassedToProvider
@@ -2,6 +2,7 @@ package com.intellij.settingsSync.core
import com.intellij.configurationStore.ComponentStoreImpl
import com.intellij.configurationStore.StateStorageManager
import com.intellij.configurationStore.getFileRelativeToRootConfig
import com.intellij.configurationStore.getStateSpec
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.idea.TestFor
@@ -236,6 +237,22 @@ class SettingsSyncIdeMediatorTest : BasePlatformTestCase() {
}
}
@TestFor(issues = ["IJPL-222935"])
@Test
fun `getFileRelativeToRootConfig prepends options for environment-specific file specs`() {
// Plain option files (no slash) → options/ prefix added
assertThat(getFileRelativeToRootConfig("jdk.table.xml")).isEqualTo("options/jdk.table.xml")
assertThat(getFileRelativeToRootConfig("applicationLibraries.xml")).isEqualTo("options/applicationLibraries.xml")
assertThat(getFileRelativeToRootConfig("editor.xml")).isEqualTo("options/editor.xml")
// Environment-specific subdirectory prefix → options/ prefix must be added too
assertThat(getFileRelativeToRootConfig("WSL-Ubuntu/jdk.table.xml")).isEqualTo("options/WSL-Ubuntu/jdk.table.xml")
assertThat(getFileRelativeToRootConfig("WSL-Ubuntu/applicationLibraries.xml")).isEqualTo("options/WSL-Ubuntu/applicationLibraries.xml")
// Schema paths (folder/file that is not an env-specific options file) → returned as-is
assertThat(getFileRelativeToRootConfig("keymaps/my_keymap.xml")).isEqualTo("keymaps/my_keymap.xml")
}
@State(
name = "SecondComponent",
storages = [Storage("second.xml")],