Skip to content

Commit

Permalink
fix: sketchbook container building
Browse files Browse the repository at this point in the history
Closes #1185

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
  • Loading branch information
Akos Kitta authored and kittaakos committed Jan 17, 2023
1 parent 40e7979 commit 692f29f
Show file tree
Hide file tree
Showing 19 changed files with 342 additions and 113 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ node_modules/
lib/
downloads/
build/
Examples/
arduino-ide-extension/Examples/
!electron/build/
src-gen/
webpack.config.js
Expand All @@ -21,3 +21,5 @@ scripts/themes/tokens
.env
# content trace files for electron
electron-app/traces
# any Arduino LS generated log files
inols*.log
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"files.exclude": {
"**/lib": false
},
"search.exclude": {
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
Expand Down
1 change: 1 addition & 0 deletions arduino-ide-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"dateformat": "^3.0.3",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
Expand Down
275 changes: 164 additions & 111 deletions arduino-ide-extension/src/node/sketches-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as glob from 'glob';
import * as crypto from 'crypto';
import * as PQueue from 'p-queue';
import { ncp } from 'ncp';
import { Mutable } from '@theia/core/lib/common/types';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileUri } from '@theia/core/lib/node/file-uri';
Expand Down Expand Up @@ -84,108 +85,15 @@ export class SketchesServiceImpl
this.logger.warn(`Could not derive sketchbook root from ${uri}.`);
return SketchContainer.create('');
}
const exists = await this.exists(root);
if (!exists) {
const rootExists = await exists(root);
if (!rootExists) {
this.logger.warn(`Sketchbook root ${root} does not exist.`);
return SketchContainer.create('');
}
const pathToAllSketchFiles = await new Promise<string[]>(
(resolve, reject) => {
glob(
'/!(libraries|hardware)/**/*.{ino,pde}',
{ root },
(error, results) => {
if (error) {
reject(error);
} else {
resolve(results);
}
}
);
}
const container = <Mutable<SketchContainer>>(
SketchContainer.create(uri ? path.basename(root) : 'Sketchbook')
);
// Sort by path length to filter out nested sketches, such as the `Nested_folder` inside the `Folder` sketch.
//
// `directories#user`
// |
// +--Folder
// |
// +--Folder.ino
// |
// +--Nested_folder
// |
// +--Nested_folder.ino
pathToAllSketchFiles.sort((left, right) => left.length - right.length);
const container = SketchContainer.create(
uri ? path.basename(root) : 'Sketchbook'
);
const getOrCreateChildContainer = (
parent: SketchContainer,
segments: string[]
) => {
if (segments.length === 1) {
throw new Error(
`Expected at least two segments relative path: ['ExampleSketchName', 'ExampleSketchName.{ino,pde}]. Was: ${segments}`
);
}
if (segments.length === 2) {
return parent;
}
const label = segments[0];
const existingSketch = parent.sketches.find(
(sketch) => sketch.name === label
);
if (existingSketch) {
// If the container has a sketch with the same label, it cannot have a child container.
// See above example about how to ignore nested sketches.
return undefined;
}
let child = parent.children.find((child) => child.label === label);
if (!child) {
child = SketchContainer.create(label);
parent.children.push(child);
}
return child;
};
for (const pathToSketchFile of pathToAllSketchFiles) {
const relative = path.relative(root, pathToSketchFile);
if (!relative) {
this.logger.warn(
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
);
continue;
}
const segments = relative.split(path.sep);
if (segments.length < 2) {
// folder name, and sketch name.
this.logger.warn(
`Expected at least one segment relative path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Segments were: ${segments}.`
);
continue;
}
// the folder name and the sketch name must match. For example, `Foo/foo.ino` is invalid.
// drop the folder name from the sketch name, if `.ino` or `.pde` remains, it's valid
const sketchName = segments[segments.length - 2];
const sketchFilename = segments[segments.length - 1];
const sketchFileExtension = segments[segments.length - 1].replace(
new RegExp(escapeRegExpCharacters(sketchName)),
''
);
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
this.logger.warn(
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
);
continue;
}
const child = getOrCreateChildContainer(container, segments);
if (child) {
child.sketches.push({
name: sketchName,
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
});
}
}
return container;
return discoverSketches(root, container, this.logger);
}

private async root(uri?: string | undefined): Promise<string | undefined> {
Expand Down Expand Up @@ -488,7 +396,7 @@ export class SketchesServiceImpl
this.sketchSuffixIndex++
)}`;
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
const sketchExists = await this.exists(
const sketchExists = await exists(
path.join(sketchbookPath, sketchNameCandidate)
);
if (!sketchExists) {
Expand Down Expand Up @@ -579,8 +487,8 @@ export class SketchesServiceImpl
{ destinationUri }: { destinationUri: string }
): Promise<string> {
const source = FileUri.fsPath(sketch.uri);
const exists = await this.exists(source);
if (!exists) {
const sketchExists = await exists(source);
if (!sketchExists) {
throw new Error(`Sketch does not exist: ${sketch}`);
}
// Nothing to do when source and destination are the same.
Expand Down Expand Up @@ -635,7 +543,7 @@ export class SketchesServiceImpl
const { client } = await this.coreClient;
const archivePath = FileUri.fsPath(destinationUri);
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
if (await this.exists(archivePath)) {
if (await exists(archivePath)) {
await fs.unlink(archivePath);
}
const req = new ArchiveSketchRequest();
Expand Down Expand Up @@ -680,15 +588,6 @@ export class SketchesServiceImpl
});
}

private async exists(pathLike: string): Promise<boolean> {
try {
await fs.access(pathLike, constants.R_OK);
return true;
} catch {
return false;
}
}

// Returns the default.ino from the settings or from default folder.
private async readSettings(): Promise<Record<string, unknown> | undefined> {
const configDirUri = await this.envVariableServer.getConfigDirUri();
Expand Down Expand Up @@ -837,3 +736,157 @@ function sketchIndexToLetters(num: number): string {
} while (pow > 0);
return out;
}

async function exists(pathLike: string): Promise<boolean> {
try {
await fs.access(pathLike, constants.R_OK);
return true;
} catch {
return false;
}
}

/**
* Recursively discovers sketches in the `root` folder give by the filesystem path.
* Missing `root` must be handled by callers. This function expects an accessible `root` directory.
*/
export async function discoverSketches(
root: string,
container: Mutable<SketchContainer>,
logger?: ILogger
): Promise<SketchContainer> {
const pathToAllSketchFiles = await globSketches(
'/!(libraries|hardware)/**/*.{ino,pde}',
root
);
// if no match try to glob the sketchbook as a sketch folder
if (!pathToAllSketchFiles.length) {
pathToAllSketchFiles.push(...(await globSketches('/*.{ino,pde}', root)));
}

// Sort by path length to filter out nested sketches, such as the `Nested_folder` inside the `Folder` sketch.
//
// `directories#user`
// |
// +--Folder
// |
// +--Folder.ino
// |
// +--Nested_folder
// |
// +--Nested_folder.ino
pathToAllSketchFiles.sort((left, right) => left.length - right.length);
const getOrCreateChildContainer = (
container: SketchContainer,
segments: string[]
): SketchContainer => {
// the sketchbook is a sketch folder
if (segments.length === 1) {
return container;
}
const segmentsCopy = segments.slice();
let currentContainer = container;
while (segmentsCopy.length > 2) {
const currentSegment = segmentsCopy.shift();
if (!currentSegment) {
throw new Error(
`'currentSegment' was not set when processing sketch container: ${JSON.stringify(
container
)}, original segments: ${JSON.stringify(
segments
)}, current container: ${JSON.stringify(
currentContainer
)}, current working segments: ${JSON.stringify(segmentsCopy)}`
);
}
let childContainer = currentContainer.children.find(
(childContainer) => childContainer.label === currentSegment
);
if (!childContainer) {
childContainer = SketchContainer.create(currentSegment);
currentContainer.children.push(childContainer);
}
currentContainer = childContainer;
}
if (segmentsCopy.length !== 2) {
throw new Error(
`Expected exactly two segments. A sketch folder name and the main sketch file name. For example, ['ExampleSketchName', 'ExampleSketchName.{ino,pde}]. Was: ${segmentsCopy}`
);
}
return currentContainer;
};

// If the container has a sketch with the same name, it cannot have a child container.
// See above example about how to ignore nested sketches.
const prune = (
container: Mutable<SketchContainer>
): Mutable<SketchContainer> => {
for (const sketch of container.sketches) {
const childContainerIndex = container.children.findIndex(
(childContainer) => childContainer.label === sketch.name
);
if (childContainerIndex >= 0) {
container.children.splice(childContainerIndex, 1);
}
}
container.children.forEach(prune);
return container;
};

for (const pathToSketchFile of pathToAllSketchFiles) {
const relative = path.relative(root, pathToSketchFile);
if (!relative) {
logger?.warn(
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
);
continue;
}
const segments = relative.split(path.sep);
let sketchName: string;
let sketchFilename: string;
if (!segments.length) {
// no segments.
logger?.warn(
`Expected at least one segment relative path ${relative} from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping.`
);
continue;
} else if (segments.length === 1) {
// The sketchbook root is a sketch folder
sketchName = path.basename(root);
sketchFilename = segments[0];
} else {
// the folder name and the sketch name must match. For example, `Foo/foo.ino` is invalid.
// drop the folder name from the sketch name, if `.ino` or `.pde` remains, it's valid
sketchName = segments[segments.length - 2];
sketchFilename = segments[segments.length - 1];
}
const sketchFileExtension = segments[segments.length - 1].replace(
new RegExp(escapeRegExpCharacters(sketchName)),
''
);
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
logger?.warn(
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
);
continue;
}
const child = getOrCreateChildContainer(container, segments);
child.sketches.push({
name: sketchName,
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
});
}
return prune(container);
}

async function globSketches(pattern: string, root: string): Promise<string[]> {
return new Promise<string[]>((resolve, reject) => {
glob(pattern, { root }, (error, results) => {
if (error) {
reject(error);
} else {
resolve(results);
}
});
});
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describe('reconcileSettings', () => {

expect(reconciledSettings).not.to.have.property('setting4');
});
it('should reset non-value fields to those defiend in the default settings', async () => {
it('should reset non-value fields to those defined in the default settings', async () => {
const newSettings: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
JSON.stringify(defaultSettings)
);
Expand Down
Loading

0 comments on commit 692f29f

Please sign in to comment.