Skip to content

Commit a088ba9

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
fix: invalid custom board option handling in FQBN
Closes #1588 Signed-off-by: Akos Kitta <[email protected]>
1 parent 2a325a5 commit a088ba9

11 files changed

+586
-139
lines changed

Diff for: arduino-ide-extension/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"fast-json-stable-stringify": "^2.1.0",
7373
"fast-safe-stringify": "^2.1.1",
7474
"filename-reserved-regex": "^2.0.0",
75+
"fqbn": "^1.0.5",
7576
"glob": "^7.1.6",
7677
"google-protobuf": "^3.20.1",
7778
"hash.js": "^1.1.7",

Diff for: arduino-ide-extension/src/browser/boards/boards-data-store.ts

+75-32
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
1212
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
1313
import type { Mutable } from '@theia/core/lib/common/types';
1414
import { inject, injectable, named } from '@theia/core/shared/inversify';
15+
import { FQBN } from 'fqbn';
1516
import {
1617
BoardDetails,
1718
BoardsService,
@@ -20,6 +21,7 @@ import {
2021
Programmer,
2122
isBoardIdentifierChangeEvent,
2223
isProgrammer,
24+
sanitizeFqbn,
2325
} from '../../common/protocol';
2426
import { notEmpty } from '../../common/utils';
2527
import type {
@@ -29,6 +31,14 @@ import type {
2931
import { NotificationCenter } from '../notification-center';
3032
import { BoardsServiceProvider } from './boards-service-provider';
3133

34+
export interface SelectConfigOptionParams {
35+
readonly fqbn: string;
36+
readonly optionsToUpdate: readonly Readonly<{
37+
option: string;
38+
selectedValue: string;
39+
}>[];
40+
}
41+
3242
@injectable()
3343
export class BoardsDataStore
3444
implements
@@ -64,7 +74,12 @@ export class BoardsDataStore
6474
this.toDispose.pushAll([
6575
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
6676
if (isBoardIdentifierChangeEvent(event)) {
67-
this.updateSelectedBoardData(event.selectedBoard?.fqbn);
77+
this.updateSelectedBoardData(
78+
event.selectedBoard?.fqbn,
79+
// If the change event comes from toolbar and the FQBN contains custom board options, change the currently selected options
80+
// https://github.com/arduino/arduino-ide/issues/1588
81+
event.reason === 'toolbar'
82+
);
6883
}
6984
}),
7085
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
@@ -116,7 +131,7 @@ export class BoardsDataStore
116131
if (!fqbn) {
117132
return undefined;
118133
} else {
119-
const data = await this.getData(fqbn);
134+
const data = await this.getData(sanitizeFqbn(fqbn));
120135
if (data === BoardsDataStore.Data.EMPTY) {
121136
return undefined;
122137
}
@@ -125,9 +140,22 @@ export class BoardsDataStore
125140
}
126141

127142
private async updateSelectedBoardData(
128-
fqbn: string | undefined
143+
fqbn: string | undefined,
144+
updateConfigOptions = false
129145
): Promise<void> {
130146
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
147+
if (fqbn && updateConfigOptions) {
148+
const { options } = new FQBN(fqbn);
149+
if (options) {
150+
const optionsToUpdate = Object.entries(options).map(([key, value]) => ({
151+
option: key,
152+
selectedValue: value,
153+
}));
154+
const params = { fqbn, optionsToUpdate };
155+
await this.selectConfigOption(params);
156+
this._selectedBoardData = await this.getSelectedBoardData(fqbn); // reload the updated data
157+
}
158+
}
131159
}
132160

133161
onStop(): void {
@@ -168,7 +196,7 @@ export class BoardsDataStore
168196
return undefined;
169197
}
170198
const { configOptions } = await this.getData(fqbn);
171-
return ConfigOption.decorate(fqbn, configOptions);
199+
return new FQBN(fqbn).withConfigOptions(...configOptions).toString();
172200
}
173201

174202
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
@@ -201,48 +229,63 @@ export class BoardsDataStore
201229
fqbn: string;
202230
selectedProgrammer: Programmer;
203231
}): Promise<boolean> {
204-
const storedData = deepClone(await this.getData(fqbn));
232+
const sanitizedFQBN = sanitizeFqbn(fqbn);
233+
const storedData = deepClone(await this.getData(sanitizedFQBN));
205234
const { programmers } = storedData;
206235
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
207236
return false;
208237
}
209238

210-
const data = { ...storedData, selectedProgrammer };
211-
await this.setData({ fqbn, data });
212-
this.fireChanged({ fqbn, data });
239+
const change: BoardsDataStoreChange = {
240+
fqbn: sanitizedFQBN,
241+
data: { ...storedData, selectedProgrammer },
242+
};
243+
await this.setData(change);
244+
this.fireChanged(change);
213245
return true;
214246
}
215247

216-
async selectConfigOption({
217-
fqbn,
218-
option,
219-
selectedValue,
220-
}: {
221-
fqbn: string;
222-
option: string;
223-
selectedValue: string;
224-
}): Promise<boolean> {
225-
const data = deepClone(await this.getData(fqbn));
226-
const { configOptions } = data;
227-
const configOption = configOptions.find((c) => c.option === option);
228-
if (!configOption) {
248+
async selectConfigOption(params: SelectConfigOptionParams): Promise<boolean> {
249+
const { fqbn, optionsToUpdate } = params;
250+
if (!optionsToUpdate.length) {
229251
return false;
230252
}
231-
let updated = false;
232-
for (const value of configOption.values) {
233-
const mutable: Mutable<ConfigValue> = value;
234-
if (mutable.value === selectedValue) {
235-
mutable.selected = true;
236-
updated = true;
237-
} else {
238-
mutable.selected = false;
253+
254+
const sanitizedFQBN = sanitizeFqbn(fqbn);
255+
const mutableData = deepClone(await this.getData(sanitizedFQBN));
256+
let didChange = false;
257+
258+
for (const { option, selectedValue } of optionsToUpdate) {
259+
const { configOptions } = mutableData;
260+
const configOption = configOptions.find((c) => c.option === option);
261+
if (configOption) {
262+
const configOptionValueIndex = configOption.values.findIndex(
263+
(configOptionValue) => configOptionValue.value === selectedValue
264+
);
265+
if (configOptionValueIndex >= 0) {
266+
// unselect all
267+
configOption.values
268+
.map((value) => value as Mutable<ConfigValue>)
269+
.forEach((value) => (value.selected = false));
270+
const mutableConfigValue: Mutable<ConfigValue> =
271+
configOption.values[configOptionValueIndex];
272+
// make the new value `selected`
273+
mutableConfigValue.selected = true;
274+
didChange = true;
275+
}
239276
}
240277
}
241-
if (!updated) {
278+
279+
if (!didChange) {
242280
return false;
243281
}
244-
await this.setData({ fqbn, data });
245-
this.fireChanged({ fqbn, data });
282+
283+
const change: BoardsDataStoreChange = {
284+
fqbn: sanitizedFQBN,
285+
data: mutableData,
286+
};
287+
await this.setData(change);
288+
this.fireChanged(change);
246289
return true;
247290
}
248291

Diff for: arduino-ide-extension/src/browser/boards/boards-service-provider.ts

+48-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Emitter } from '@theia/core/lib/common/event';
1212
import { ILogger } from '@theia/core/lib/common/logger';
1313
import { MessageService } from '@theia/core/lib/common/message-service';
1414
import { nls } from '@theia/core/lib/common/nls';
15+
import { deepClone } from '@theia/core/lib/common/objects';
1516
import { Deferred } from '@theia/core/lib/common/promise-util';
1617
import type { Mutable } from '@theia/core/lib/common/types';
1718
import { inject, injectable, optional } from '@theia/core/shared/inversify';
@@ -21,31 +22,32 @@ import {
2122
} from '@theia/output/lib/browser/output-channel';
2223
import {
2324
BoardIdentifier,
24-
boardIdentifierEquals,
25+
BoardUserField,
26+
BoardWithPackage,
2527
BoardsConfig,
2628
BoardsConfigChangeEvent,
2729
BoardsPackage,
2830
BoardsService,
29-
BoardUserField,
30-
BoardWithPackage,
3131
DetectedPorts,
32+
Port,
33+
PortIdentifier,
34+
boardIdentifierEquals,
3235
emptyBoardsConfig,
3336
isBoardIdentifier,
3437
isBoardIdentifierChangeEvent,
3538
isPortIdentifier,
3639
isPortIdentifierChangeEvent,
37-
Port,
38-
PortIdentifier,
3940
portIdentifierEquals,
41+
sanitizeFqbn,
4042
serializePlatformIdentifier,
4143
} from '../../common/protocol';
4244
import {
4345
BoardList,
4446
BoardListHistory,
45-
createBoardList,
4647
EditBoardsConfigActionParams,
47-
isBoardListHistory,
4848
SelectBoardsConfigActionParams,
49+
createBoardList,
50+
isBoardListHistory,
4951
} from '../../common/protocol/board-list';
5052
import type { Defined } from '../../common/types';
5153
import type {
@@ -104,6 +106,21 @@ type BoardListHistoryUpdateResult =
104106
type BoardToSelect = BoardIdentifier | undefined | 'ignore-board';
105107
type PortToSelect = PortIdentifier | undefined | 'ignore-port';
106108

109+
function sanitizeBoardToSelectFQBN(board: BoardToSelect): BoardToSelect {
110+
if (isBoardIdentifier(board)) {
111+
return sanitizeBoardIdentifierFQBN(board);
112+
}
113+
return board;
114+
}
115+
function sanitizeBoardIdentifierFQBN(board: BoardIdentifier): BoardIdentifier {
116+
if (board.fqbn) {
117+
const copy: Mutable<BoardIdentifier> = deepClone(board);
118+
copy.fqbn = sanitizeFqbn(board.fqbn);
119+
return copy;
120+
}
121+
return board;
122+
}
123+
107124
interface UpdateBoardListHistoryParams {
108125
readonly portToSelect: PortToSelect;
109126
readonly boardToSelect: BoardToSelect;
@@ -136,6 +153,9 @@ export interface BoardListUIActions {
136153
}
137154
export type BoardListUI = BoardList & BoardListUIActions;
138155

156+
export type BoardsConfigChangeEventUI = BoardsConfigChangeEvent &
157+
Readonly<{ reason?: UpdateBoardsConfigReason }>;
158+
139159
@injectable()
140160
export class BoardListDumper implements Disposable {
141161
@inject(OutputChannelManager)
@@ -190,7 +210,7 @@ export class BoardsServiceProvider
190210
private _ready = new Deferred<void>();
191211

192212
private readonly boardsConfigDidChangeEmitter =
193-
new Emitter<BoardsConfigChangeEvent>();
213+
new Emitter<BoardsConfigChangeEventUI>();
194214
readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event;
195215

196216
private readonly boardListDidChangeEmitter = new Emitter<BoardListUI>();
@@ -353,7 +373,8 @@ export class BoardsServiceProvider
353373
portToSelect !== 'ignore-port' &&
354374
!portIdentifierEquals(portToSelect, previousSelectedPort);
355375
const boardDidChangeEvent = boardDidChange
356-
? { selectedBoard: boardToSelect, previousSelectedBoard }
376+
? // The change event must always contain any custom board options. Hence the board to select is not sanitized.
377+
{ selectedBoard: boardToSelect, previousSelectedBoard }
357378
: undefined;
358379
const portDidChangeEvent = portDidChange
359380
? { selectedPort: portToSelect, previousSelectedPort }
@@ -374,16 +395,31 @@ export class BoardsServiceProvider
374395
return false;
375396
}
376397

377-
this.maybeUpdateBoardListHistory({ portToSelect, boardToSelect });
378-
this.maybeUpdateBoardsData({ boardToSelect, reason });
398+
// unlike for the board change event, every persistent state must not contain custom board config options in the FQBN
399+
const sanitizedBoardToSelect = sanitizeBoardToSelectFQBN(boardToSelect);
400+
401+
this.maybeUpdateBoardListHistory({
402+
portToSelect,
403+
boardToSelect: sanitizedBoardToSelect,
404+
});
405+
this.maybeUpdateBoardsData({
406+
boardToSelect: sanitizedBoardToSelect,
407+
reason,
408+
});
379409

380410
if (isBoardIdentifierChangeEvent(event)) {
381-
this._boardsConfig.selectedBoard = event.selectedBoard;
411+
this._boardsConfig.selectedBoard = event.selectedBoard
412+
? sanitizeBoardIdentifierFQBN(event.selectedBoard)
413+
: event.selectedBoard;
382414
}
383415
if (isPortIdentifierChangeEvent(event)) {
384416
this._boardsConfig.selectedPort = event.selectedPort;
385417
}
386418

419+
if (reason) {
420+
event = Object.assign(event, { reason });
421+
}
422+
387423
this.boardsConfigDidChangeEmitter.fire(event);
388424
this.refreshBoardList();
389425
this.saveState();

Diff for: arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ export class BoardsDataMenuUpdater extends Contribution {
8787
execute: () =>
8888
this.boardsDataStore.selectConfigOption({
8989
fqbn,
90-
option,
91-
selectedValue: value.value,
90+
optionsToUpdate: [{ option, selectedValue: value.value }],
9291
}),
9392
isToggled: () => value.selected,
9493
};

Diff for: arduino-ide-extension/src/browser/contributions/ino-language.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
BoardIdentifier,
1010
BoardsService,
1111
ExecutableService,
12-
assertSanitizedFqbn,
1312
isBoardIdentifierChangeEvent,
1413
sanitizeFqbn,
1514
} from '../../common/protocol';
@@ -159,14 +158,11 @@ export class InoLanguage extends SketchContribution {
159158
this.notificationCenter.onDidReinitialize(() => forceRestart()),
160159
this.boardDataStore.onDidChange((event) => {
161160
if (this.languageServerFqbn) {
162-
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
163-
if (!sanitizeFqbn) {
164-
throw new Error(
165-
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
166-
);
167-
}
161+
const sanitizedFQBN = sanitizeFqbn(this.languageServerFqbn);
162+
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison.
163+
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328
168164
const matchingChange = event.changes.find(
169-
(change) => change.fqbn === sanitizedFqbn
165+
(change) => sanitizedFQBN === sanitizeFqbn(change.fqbn)
170166
);
171167
const { boardsConfig } = this.boardsServiceProvider;
172168
if (
@@ -228,7 +224,6 @@ export class InoLanguage extends SketchContribution {
228224
}
229225
return;
230226
}
231-
assertSanitizedFqbn(fqbn);
232227
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
233228
if (!fqbnWithConfig) {
234229
throw new Error(

Diff for: arduino-ide-extension/src/browser/contributions/upload-sketch.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Emitter } from '@theia/core/lib/common/event';
22
import { nls } from '@theia/core/lib/common/nls';
33
import { inject, injectable } from '@theia/core/shared/inversify';
4-
import { CoreService, sanitizeFqbn } from '../../common/protocol';
4+
import { FQBN } from 'fqbn';
5+
import { CoreService } from '../../common/protocol';
56
import { ArduinoMenus } from '../menu/arduino-menus';
67
import { CurrentSketch } from '../sketches-service-client-impl';
78
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -173,7 +174,11 @@ export class UploadSketch extends CoreServiceContribution {
173174
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
174175
await Promise.all([
175176
verifyOptions.fqbn, // already decorated FQBN
176-
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
177+
this.boardsDataStore.getData(
178+
verifyOptions.fqbn
179+
? new FQBN(verifyOptions.fqbn).toString(true)
180+
: undefined
181+
),
177182
this.preferences.get('arduino.upload.verify'),
178183
this.preferences.get('arduino.upload.verbose'),
179184
]);

0 commit comments

Comments
 (0)