Skip to content

Commit 69f2b2a

Browse files
authored
Make configure-on-open resilient to delayed CMake availability (#4880)
* Fix false 'Bad CMake executable' error when vendor extension is still installing CMake When a vendor extension (e.g., STM32 for VS Code, ESP-IDF) is still downloading or installing CMake at the time CMake Tools activates, the automatic configure-on-open would immediately show a misleading 'Bad CMake executable' error popup. This change checks cmake availability before the initial configure attempt. When cmake is not found, it polls with exponential backoff (2s, 4s, 8s, 16s) using a lightweight getCMakeExecutable() probe that does not enter the driver strand or trigger error popups. If cmake becomes available during the retry window, a single configure runs. If not, the standard error popup fires once after retries exhaust. Also fixes reloadCMakeDriver() silently doing nothing when the CMake driver was never created (null). The method now always disposes the old driver if present and (re)creates a new one when cmake is available, or resets to null otherwise. This ensures that changing cmake.cmakePath in settings takes effect even if the initial cmake lookup failed. * Wait for vendor-managed CMake before failing configure-on-open Add `cmake.vendorIntegrators`, a string array that defaults to `["ST", "Espressif", "NXP", "Nordic"]`. During automatic configure-on-open, if CMake is missing but one of those vendor extensions is installed, do not fail right away. Some of those extensions install or expose CMake asynchronously, so we now poll for it with exponential backoff at 2s, 4s, 8s, and 16s before surfacing the error. For users without one of those extensions, nothing changes: they still see the error immediately. Also fix `reloadCMakeDriver()`. If the CMake driver was never created, the method used to silently do nothing. It now creates or recreates the driver when `cmake.cmakePath` changes, even if the initial lookup failed. * rename to be more comprehensive --------- Co-authored-by: Hannia Valera <[email protected]>
1 parent 8336fc6 commit 69f2b2a

9 files changed

Lines changed: 418 additions & 11 deletions

File tree

docs/cmake-settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Options that support substitution, in the table below, allow variable references
2828
| `cmake.configureEnvironment` | An object containing `key:value` pairs of environment variables, which will be passed to CMake only when configuring.| `null` (no environment variable pairs) | yes |
2929
| `cmake.configureOnEdit` | Automatically configure CMake project directories when the path in the `cmake.sourceDirectory` setting is updated or when `CMakeLists.txt` or `*.cmake` files are saved. | `true` | no |
3030
| `cmake.configureOnOpen` | Automatically configure CMake project directories when they are opened. | `true` | no |
31+
| `cmake.cmakeProviderExtensions` | List of VS Code extension IDs that provide or install their own CMake binary. When CMake is not found during automatic configure-on-open and one of these extensions is installed, CMake Tools will briefly poll for CMake availability instead of showing an immediate error. Set to an empty array to disable this behavior. | `["stmicroelectronics.stm32-vscode-extension", "espressif.esp-idf-extension", "NXPSemiconductors.mcuxpresso", "nordic-semiconductor.nrf-connect"]` | no |
3132
| `cmake.configureSettings` | An object containing `key:value` pairs, which will be passed to CMake when configuring. The same as passing `-DVAR_NAME=ON` via `cmake.configureArgs`. NOTE: Semicolons (`;`) in string values are automatically escaped to prevent CMake from interpreting them as list separators. If you want to pass a CMake list, use array notation instead, e.g. `"MY_LIST": [ "a", "b" ]`. | `{}` (no values) | yes |
3233
| `cmake.copyCompileCommands`| If not `null`, copies the `compile_commands.json` file generated by CMake to the path specified by this setting whenever CMake successfully configures. | `null` (do not copy the file) | yes |
3334
| `cmake.postConfigureTask`| If not `null`, the task with this name is executed whenever CMake successfully configures. | `null` (do not run any task) | yes |

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3126,6 +3126,20 @@
31263126
"description": "%cmake-tools.configuration.cmake.configureOnEdit.description%",
31273127
"scope": "resource"
31283128
},
3129+
"cmake.cmakeProviderExtensions": {
3130+
"type": "array",
3131+
"items": {
3132+
"type": "string"
3133+
},
3134+
"default": [
3135+
"stmicroelectronics.stm32-vscode-extension",
3136+
"espressif.esp-idf-extension",
3137+
"NXPSemiconductors.mcuxpresso",
3138+
"nordic-semiconductor.nrf-connect"
3139+
],
3140+
"description": "%cmake-tools.configuration.cmake.cmakeProviderExtensions.description%",
3141+
"scope": "resource"
3142+
},
31293143
"cmake.deleteBuildDirOnCleanConfigure": {
31303144
"type": "boolean",
31313145
"default": false,

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@
275275
"cmake-tools.configuration.cmake.postConfigureTask.description": "If set, this named task will be executed after a successful CMake configure.",
276276
"cmake-tools.configuration.cmake.configureOnOpen.description": "Automatically configure CMake project directories when they are opened.",
277277
"cmake-tools.configuration.cmake.configureOnEdit.description": "Automatically configure CMake project directories when cmake.sourceDirectory or CMakeLists.txt content are saved.",
278+
"cmake-tools.configuration.cmake.cmakeProviderExtensions.description": "List of VS Code extension IDs that provide or install their own CMake binary. When CMake is not found during automatic configure-on-open and one of these extensions is installed, CMake Tools will briefly poll for CMake availability instead of showing an immediate error. Set to an empty array to disable this behavior.",
278279
"cmake-tools.configuration.cmake.deleteBuildDirOnCleanConfigure.description": "Delete the entire build directory when a clean configure is invoked.",
279280
"cmake-tools.configuration.cmake.setBuildTypeOnMultiConfig.description": "Set CMAKE_BUILD_TYPE also on multi config generators.",
280281
"cmake-tools.configuration.cmake.skipConfigureIfCachePresent.description": "Skip over the configure process if cache is present.",

src/cmakeProject.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,14 +1265,18 @@ export class CMakeProject {
12651265
private async reloadCMakeDriver() {
12661266
try {
12671267
const drv = await this.cmakeDriver;
1268-
if (drv) {
1269-
log.debug(localize('reloading.driver', 'Reloading CMake driver'));
1270-
await drv?.asyncDispose();
1271-
return this.cmakeDriver = this.startNewCMakeDriver(await this.getCMakeExecutable());
1272-
}
1268+
log.debug(localize('reloading.driver', 'Reloading CMake driver'));
1269+
await drv?.asyncDispose();
12731270
} catch {
1274-
return this.cmakeDriver = this.startNewCMakeDriver(await this.getCMakeExecutable());
1271+
// Driver was in a bad state — proceed to create a new one.
1272+
}
1273+
const cmake = await this.getCMakeExecutable();
1274+
if (cmake.isPresent) {
1275+
return this.cmakeDriver = this.startNewCMakeDriver(cmake);
12751276
}
1277+
// CMake is still not available — reset to null so getCMakeDriverInstance()
1278+
// can surface the error when the user next triggers a command.
1279+
return this.cmakeDriver = Promise.resolve(null);
12761280
}
12771281

12781282
/**

src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export interface ExtensionConfigurationSettings {
227227
loadCompileCommands: boolean;
228228
configureOnOpen: boolean;
229229
configureOnEdit: boolean;
230+
cmakeProviderExtensions: string[];
230231
deleteBuildDirOnCleanConfigure: boolean;
231232
skipConfigureIfCachePresent: boolean | null;
232233
useCMakeServer: boolean;
@@ -504,6 +505,9 @@ export class ConfigurationReader implements vscode.Disposable {
504505
get configureOnEdit() {
505506
return this.configData.configureOnEdit;
506507
}
508+
get cmakeProviderExtensions(): string[] {
509+
return this.configData.cmakeProviderExtensions;
510+
}
507511
get deleteBuildDirOnCleanConfigure() {
508512
return this.configData.deleteBuildDirOnCleanConfigure;
509513
}
@@ -738,6 +742,7 @@ export class ConfigurationReader implements vscode.Disposable {
738742
loadCompileCommands: new vscode.EventEmitter<boolean>(),
739743
configureOnOpen: new vscode.EventEmitter<boolean>(),
740744
configureOnEdit: new vscode.EventEmitter<boolean>(),
745+
cmakeProviderExtensions: new vscode.EventEmitter<string[]>(),
741746
deleteBuildDirOnCleanConfigure: new vscode.EventEmitter<boolean>(),
742747
skipConfigureIfCachePresent: new vscode.EventEmitter<boolean | null>(),
743748
useCMakeServer: new vscode.EventEmitter<boolean>(),

src/extension.ts

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as api from 'vscode-cmake-tools';
1313
import { CMakeCache } from '@cmt/cache';
1414
import { CMakeProject, ConfigureType, ConfigureTrigger, DiagnosticsConfiguration, DiagnosticsSettings } from '@cmt/cmakeProject';
1515
import { ConfigurationReader, getSettingsChangePromise, TouchBarConfig } from '@cmt/config';
16+
import { CMakeDriver, CMakePreconditionProblems, ConfigureResult, ConfigureResultType } from '@cmt/drivers/cmakeDriver';
1617
import { CppConfigurationProvider, DiagnosticsCpptools } from '@cmt/cpptools';
1718
import { ProjectController, AfterAcknowledgeFolderType} from '@cmt/projectController';
1819

@@ -37,7 +38,6 @@ import { ProgressHandle, DummyDisposable, reportProgress, runCommand } from '@cm
3738
import { DEFAULT_VARIANTS } from '@cmt/kits/variant';
3839
import { expandString, KitContextVars } from '@cmt/expand';
3940
import paths from '@cmt/paths';
40-
import { CMakeDriver, CMakePreconditionProblems } from './drivers/cmakeDriver';
4141
import { platform } from 'os';
4242
import { CMakeToolsApiImpl } from '@cmt/api';
4343
import { DirectoryContext } from '@cmt/workspace';
@@ -65,6 +65,37 @@ export const hideLaunchCommandKey = 'cmake:hideLaunchCommand';
6565
export const hideDebugCommandKey = 'cmake:hideDebugCommand';
6666
export const hideBuildCommandKey = 'cmake:hideBuildCommand';
6767

68+
/**
69+
* Friendly display names for known vendor extensions. Used to show a nicer
70+
* progress message (e.g., "Waiting for STM32 for VS Code..." instead of
71+
* "Waiting for stmicroelectronics.stm32-vscode-extension...").
72+
* Extensions not in this map fall back to their raw ID.
73+
*/
74+
const vendorExtensionLabels: ReadonlyMap<string, string> = new Map([
75+
['stmicroelectronics.stm32-vscode-extension', 'STM32 for VS Code'],
76+
['espressif.esp-idf-extension', 'ESP-IDF'],
77+
['NXPSemiconductors.mcuxpresso', 'MCUXpresso'],
78+
['nordic-semiconductor.nrf-connect', 'nRF Connect']
79+
]);
80+
81+
/**
82+
* Check if any extension ID from the given list is currently installed.
83+
* Returns the friendly display name of the first match, or undefined if none.
84+
*/
85+
function getInstalledVendorHint(vendorIds: string[]): string | undefined {
86+
for (const id of vendorIds) {
87+
if (vscode.extensions.getExtension(id)) {
88+
return vendorExtensionLabels.get(id) ?? id;
89+
}
90+
}
91+
return undefined;
92+
}
93+
94+
/** Maximum number of retries when CMake is not found during configure-on-open. */
95+
const cmakeNotFoundMaxRetries = 4;
96+
/** Delay sequence (in ms) between retries — exponential backoff. */
97+
const cmakeNotFoundRetryDelaysMs: readonly number[] = [2000, 4000, 8000, 16000];
98+
6899
/**
69100
* The global extension manager. There is only one of these, even if multiple
70101
* backends.
@@ -702,12 +733,12 @@ export class ExtensionManager implements vscode.Disposable {
702733
await telemetry.deactivate();
703734
}
704735

705-
async configureExtensionInternal(trigger: ConfigureTrigger, project: CMakeProject): Promise<void> {
736+
async configureExtensionInternal(trigger: ConfigureTrigger, project: CMakeProject): Promise<ConfigureResult> {
706737
if (trigger !== ConfigureTrigger.configureWithCache && !await this.ensureActiveConfigurePresetOrKit(project)) {
707-
return;
738+
return { exitCode: -1, resultType: ConfigureResultType.Other };
708739
}
709740

710-
await project.configureInternal(trigger, [], ConfigureType.Normal);
741+
return project.configureInternal(trigger, [], ConfigureType.Normal);
711742
}
712743

713744
async postWorkspaceOpen(project?: CMakeProject) {
@@ -738,7 +769,26 @@ export class ExtensionManager implements vscode.Disposable {
738769
// We've opened a new workspace folder, and the user wants us to
739770
// configure it now.
740771
log.debug(localize('configuring.workspace.on.open', 'Configuring workspace on open {0}', project.folderPath));
741-
await this.configureExtensionInternal(ConfigureTrigger.configureOnOpen, project);
772+
773+
// Check cmake availability first. If cmake is not present AND
774+
// a vendor extension from the cmake.cmakeProviderExtensions setting is
775+
// installed, poll for cmake to appear (the vendor may still be
776+
// installing it). Otherwise, proceed to configure immediately —
777+
// the standard "Bad CMake executable" error will surface if needed.
778+
const cmake = await project.getCMakeExecutable();
779+
if (cmake.isPresent) {
780+
await this.configureExtensionInternal(ConfigureTrigger.configureOnOpen, project);
781+
} else {
782+
const vendorIds = project.workspaceContext.config.cmakeProviderExtensions;
783+
const vendorHint = getInstalledVendorHint(vendorIds);
784+
if (vendorHint) {
785+
await this.waitForCmakeAndConfigure(project, vendorHint);
786+
} else {
787+
// No vendor extension installed — cmake is genuinely missing.
788+
// Run configure to surface the error immediately.
789+
await this.configureExtensionInternal(ConfigureTrigger.configureOnOpen, project);
790+
}
791+
}
742792
} else {
743793
const configureButtonMessage = localize('configure.now.button', 'Configure Now');
744794
let result: string | undefined;
@@ -759,6 +809,70 @@ export class ExtensionManager implements vscode.Disposable {
759809
}
760810
}
761811

812+
/**
813+
* Wait for CMake to become available, then configure.
814+
*
815+
* When cmake is not found during the automatic configureOnOpen flow and a
816+
* vendor extension from the `cmake.cmakeProviderExtensions` setting is installed,
817+
* this method polls for cmake presence with exponential backoff. This handles
818+
* the case where a vendor extension (e.g., STMicroelectronics, Espressif) is
819+
* still downloading or installing cmake when CMake Tools activates.
820+
*
821+
* A window progress indicator is shown while retrying.
822+
*
823+
* Only the lightweight getCMakeExecutable() probe runs during the retry
824+
* window — no configure pipeline, no error popups, no driver strand held.
825+
* When cmake is finally found, a single configureExtensionInternal() call
826+
* runs the full configure.
827+
*
828+
* @param vendorHint Display name of the detected vendor extension, used in
829+
* the progress message.
830+
*/
831+
private async waitForCmakeAndConfigure(project: CMakeProject, vendorHint: string): Promise<void> {
832+
const progressTitle = localize('cmake.retry.vendor.title', 'Waiting for {0} to set up CMake...', vendorHint);
833+
834+
log.info(localize('cmake.not.found.retrying', 'CMake not found during configure-on-open. Waiting for {0} to finish setup...', vendorHint));
835+
836+
const found = await vscode.window.withProgress(
837+
{
838+
location: vscode.ProgressLocation.Window,
839+
title: progressTitle
840+
},
841+
async (): Promise<boolean> => {
842+
for (let attempt = 0; attempt < cmakeNotFoundMaxRetries; attempt++) {
843+
const delayMs = cmakeNotFoundRetryDelaysMs[
844+
Math.min(attempt, cmakeNotFoundRetryDelaysMs.length - 1)
845+
];
846+
await new Promise<void>(resolve => setTimeout(resolve, delayMs));
847+
848+
// Lightweight probe: just check if cmake is available now.
849+
// This does NOT enter the driverStrand, does NOT show popups,
850+
// and does NOT run the configure pipeline.
851+
const cmake = await project.getCMakeExecutable();
852+
if (cmake.isPresent) {
853+
log.info(localize('cmake.retry.success', 'CMake found after {0}s (attempt {1}/{2})',
854+
cmakeNotFoundRetryDelaysMs.slice(0, attempt + 1).reduce((a, b) => a + b, 0) / 1000,
855+
attempt + 1, cmakeNotFoundMaxRetries));
856+
return true;
857+
}
858+
859+
log.debug(localize('cmake.retry.not.yet', 'CMake not yet available (attempt {0}/{1})',
860+
attempt + 1, cmakeNotFoundMaxRetries));
861+
}
862+
return false;
863+
}
864+
);
865+
866+
if (found) {
867+
log.info(localize('cmake.retry.configuring', 'CMake is now available — configuring project'));
868+
} else {
869+
// All retries exhausted. Run configure anyway to surface the
870+
// "Bad CMake executable" error popup so the user knows what's wrong.
871+
log.warning(localize('cmake.retry.exhausted', 'CMake not found after {0} retries', cmakeNotFoundMaxRetries));
872+
}
873+
await this.configureExtensionInternal(ConfigureTrigger.configureOnOpen, project);
874+
}
875+
762876
private async onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined): Promise<void> {
763877
if (this.workspaceConfig.autoSelectActiveFolder && this.projectController.hasMultipleProjects && vscode.workspace.workspaceFolders) {
764878
let folder: vscode.WorkspaceFolder | undefined;

0 commit comments

Comments
 (0)