Skip to content

fix: sketchbook container building #1814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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