diff --git a/src/BRCommands.ts b/src/BRCommands.ts index e5264d7..c116ada 100644 --- a/src/BRCommands.ts +++ b/src/BRCommands.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode'; -import * as BREnvironment from './Environment/BREnvironment'; +import { Environment } from './Environment/Environment'; import * as BRDialogs from './UI/BrDialogs'; @@ -32,7 +32,7 @@ function registerContributedCommands(context: vscode.ExtensionContext) { context.subscriptions.push(disposable); // Update configuration of installed AS versions from file system search disposable = vscode.commands.registerCommand('vscode-brautomationtools.updateAvailableAutomationStudioVersions', - BREnvironment.updateAvailableAutomationStudioVersions); + Environment.automationStudio.updateVersions); context.subscriptions.push(disposable); } diff --git a/src/BrAsBuildTaskProvider.ts b/src/BrAsBuildTaskProvider.ts index c66ade4..179fd2e 100644 --- a/src/BrAsBuildTaskProvider.ts +++ b/src/BrAsBuildTaskProvider.ts @@ -6,11 +6,11 @@ import * as vscode from 'vscode'; import * as childProcess from 'child_process'; import * as BrAsProjectWorkspace from './BRAsProjectWorkspace'; -import * as BrEnvironment from './Environment/BREnvironment'; import * as BrDialogs from './UI/BrDialogs'; import { logger } from './BrLog'; import { extensionConfiguration } from './BRConfiguration'; import { timeDiffString } from './Tools/Helpers'; +import { Environment } from './Environment/Environment'; /** @@ -297,7 +297,7 @@ class BrAsBuildTerminal implements vscode.Pseudoterminal { this.done(43); return; } - const buildExe = await BrEnvironment.getBrAsBuilExe(asProject.asVersion); + const buildExe = (await Environment.automationStudio.getVersion(asProject.asVersion))?.buildExe.exePath; if (!buildExe) { this.writeLine(`ERROR: BR.AS.Build.exe not found for AS Version: ${asProject.asVersion}`); this.done(44); diff --git a/src/BrAsTransferTaskProvider.ts b/src/BrAsTransferTaskProvider.ts index 9a6c73d..b7b7a4a 100644 --- a/src/BrAsTransferTaskProvider.ts +++ b/src/BrAsTransferTaskProvider.ts @@ -16,9 +16,7 @@ import * as uriTools from './Tools/UriTools'; import * as fileTools from './Tools/FileTools'; import * as Dialogs from './UI/Dialogs'; import * as BrAsProjectWorkspace from './BRAsProjectWorkspace'; -import * as BrEnvironment from './Environment/BREnvironment'; import * as BrDialogs from './UI/BrDialogs'; -import * as BrConfiguration from './BRConfiguration'; import { logger } from './BrLog'; import { Environment } from './Environment/Environment'; @@ -409,7 +407,7 @@ class BrPviTransferTerminal implements vscode.Pseudoterminal { } // Get PVITransfer.exe in highest version // TODO Maybe start process in PviTransferExe.ts - const pviTransferExe = (await Environment.getPviVersion())?.pviTransfer.executable; + const pviTransferExe = (await Environment.pvi.getVersion())?.pviTransfer.exePath; if (!pviTransferExe) { this.writeLine(`ERROR: No PVI version found`); this.done(70); diff --git a/src/Environment/AutomationStudioVersion.ts b/src/Environment/AutomationStudioVersion.ts new file mode 100644 index 0000000..598ba5f --- /dev/null +++ b/src/Environment/AutomationStudioVersion.ts @@ -0,0 +1,183 @@ +import * as vscode from 'vscode'; +import * as uriTools from '../Tools/UriTools'; +import * as semver from 'semver'; +import { logger } from '../BrLog'; +import { GccInstallation } from './GccInstallation'; +import { BrAsBuildExe } from './BrAsBuildExe'; + +/** + * Representation of an Automation Studio version + */ +export class AutomationStudioVersion { + + /** + * Gets all Automation Studio versions which are located in the installRoot. + * @param installRoot The root directory containing multiple Automation Studio installations. e.g. `C:\BrAutomation` + * @returns An array with all found versions + */ + public static async searchVersionsInDir(installRoot: vscode.Uri): Promise { + // Get matching subdirectories + const asDirRegExp = /^AS(\d)(\d+)$/; + const subDirs = await uriTools.listSubDirectories(installRoot, asDirRegExp); + // create AutomationStudioVersion from matching subdirectories + const versions: AutomationStudioVersion[] = []; + for (const dir of subDirs) { + const asVersion = await this.createFromDir(dir); + if (asVersion !== undefined) { + versions.push(asVersion); + } + } + // done + return versions; + } + + /** + * Creates an Automation Studio version from a specified root directory + * @param asRoot The root directory containing a single Automation Studio installation. e.g. `C:\BrAutomation\AS410` + * @returns The version which was parsed from the root URI + */ + public static async createFromDir(asRoot: vscode.Uri): Promise { + // Create and initialize object + try { + const asVersion = new AutomationStudioVersion(asRoot); + await asVersion.#initialize(); + logger.info(`Automation Studio Version V${asVersion.version.version} found in '${asVersion.rootPath.fsPath}'`); + return asVersion; + } catch (error) { + if (error instanceof Error) { + logger.error(`Failed to get Automation Studio in path '${asRoot.fsPath}': ${error.message}`); + } else { + logger.error(`Failed to get Automation Studio in path '${asRoot.fsPath}'`); + } + return undefined; + } + } + + /** Object is not ready to use after constructor due to async operations, + * #initialize() has to be called for the object to be ready to use! */ + private constructor(asRoot: vscode.Uri) { + this.#rootPath = asRoot; + // other properties rely on async and will be initialized in #initialize() + } + + /** Async operations to finalize object construction */ + async #initialize(): Promise { + this.#version = await parseAutomationStudioVersion(this.#rootPath); + // Find BR.AS.Build.exe + this.#buildExe = await searchAutomationStudioBuildExe(this.#rootPath); + if (!this.#buildExe) { + throw new Error('Cannot find BR.AS.Build.exe'); + } + // find gcc versions + const gccInstallRoot = vscode.Uri.joinPath(this.#rootPath, './AS/gnuinst'); + this.#gccInstallation = await GccInstallation.searchAutomationStudioGnuinst(gccInstallRoot); + // init done + this.#isInitialized = true; + } + #isInitialized = false; + + /** The root URI of the Automation Studio version */ + public get rootPath(): vscode.Uri { + if (!this.#isInitialized) { throw new Error(`Use of not initialized ${AutomationStudioVersion.name} object`); } + return this.#rootPath; + } + #rootPath: vscode.Uri; + + /** The Automation Studio version */ + public get version(): semver.SemVer { + if (!this.#isInitialized || !this.#version) { throw new Error(`Use of not initialized ${AutomationStudioVersion.name} object`); } + return this.#version; + } + #version: semver.SemVer | undefined; + + /** The Automation Studio project build tool */ + public get buildExe(): BrAsBuildExe { + if (!this.#isInitialized || !this.#buildExe) { throw new Error(`Use of not initialized ${AutomationStudioVersion.name} object`); } + return this.#buildExe; + } + #buildExe: BrAsBuildExe | undefined; + + /** gcc compiler versions available in this Automation Studio version */ + public get gccInstallation(): GccInstallation { + if (!this.#isInitialized || !this.#gccInstallation) { throw new Error(`Use of not initialized ${AutomationStudioVersion.name} object`); } + return this.#gccInstallation; + } + #gccInstallation: GccInstallation | undefined; + + /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ + public toJSON(): any { + return { + rootPath: this.rootPath.toString(true), + version: this.version.version, + buildExe: this.buildExe, + gccInstallation: this.gccInstallation, + }; + } +} + +/** + * Trys to parse the version if an Automation Studio installation. The info is gathered from info files and the rootPath name. + * @param asRoot Root path of the AS installation. e.g. `C:\BrAutomation\AS410` + * @returns The parsed version, or V0.0.0 if parsing failed + */ +async function parseAutomationStudioVersion(asRoot: vscode.Uri): Promise { + let version: semver.SemVer | undefined = undefined; + // Try to get version from ./BrSetup/VInfo/ProductInfo.ini + const prodInfoPath = uriTools.pathJoin(asRoot, 'BrSetup/VInfo/ProductInfo.ini'); + try { + const prodInfoDoc = await vscode.workspace.openTextDocument(prodInfoPath); + const prodInfoText = prodInfoDoc.getText(); + const versionRegex = /^Version=[\D]*([\d.]*)[\D]*$/gm; + const versionMatch = versionRegex.exec(prodInfoText); + if (versionMatch) { + version = semver.coerce(versionMatch[0]) ?? undefined; + } + } catch (error) { + // no reaction required + } + if (version) { + return version; + } else { + logger.warning(`Failed to get AS Version from '${prodInfoPath.toString(true)}'. Will try to parse from directory name`); + } + // Try parse version from root directory name if get from file failed + const dirName = uriTools.pathBasename(asRoot); + const asDirRegExp = /^AS(\d)(\d+)$/; + const match = asDirRegExp.exec(dirName); + if (match && match.length >= 3) { + version = semver.coerce(`${match![1]}.${match![2]}.0`) ?? undefined; + } + if (version) { + return version; + } else { + logger.warning(`Failed to parse AS Version from directory name '${asRoot.toString(true)}'. AS will be listed as V0.0.0`); + } + // set to V0.0.0 as backup, so AS is still available but with wrong version... + return new semver.SemVer('0.0.0'); +} + +/** + * Search for Br.As.Build.exe in the Automation Studio installation + * @param asRoot Root path of the AS installation. e.g. `C:\BrAutomation\AS410` + * @returns The first found Br.As.Build.exe, or undefined if no such was found. + */ +async function searchAutomationStudioBuildExe(asRoot: vscode.Uri): Promise { + // english + const buildExeUriEn = uriTools.pathJoin(asRoot, 'Bin-en/BR.AS.Build.exe'); + if (await uriTools.exists(buildExeUriEn)) { + return new BrAsBuildExe(buildExeUriEn); + } + // german + const buildExeUriDe = uriTools.pathJoin(asRoot, 'Bin-de/BR.AS.Build.exe'); + if (await uriTools.exists(buildExeUriDe)) { + return new BrAsBuildExe(buildExeUriDe); + } + // slower search if none was found yet + const searchPattern = new vscode.RelativePattern(asRoot, '**/BR.AS.Build.exe'); + const searchResult = await vscode.workspace.findFiles(searchPattern); + if (searchResult.length > 0) { + return new BrAsBuildExe(searchResult[0]); + } + // none was found + return undefined; +} \ No newline at end of file diff --git a/src/Environment/BREnvironment.ts b/src/Environment/BREnvironment.ts deleted file mode 100644 index 71210e1..0000000 --- a/src/Environment/BREnvironment.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Handling of the installation environment of B&R programs on the developer computer. - * @packageDocumentation - */ -//TODO get version information once on startup, only specify an array of base install paths in the configuration - -import * as vscode from 'vscode'; -import { logger } from '../BrLog'; -import * as uriTools from '../Tools/UriTools'; -import * as semver from 'semver'; -import { extensionConfiguration } from '../BRConfiguration'; - - -//#region exported interfaces - - -/** - * Base interface for version information - */ -export interface VersionInfo { - /** The version of the component */ - version: semver.SemVer; - /** The base folder URI of the component */ - baseUri: vscode.Uri; -} - - -/** - * B&R Automation Studio version and installation information - */ -export interface ASVersionInfo extends VersionInfo { - /** URI of the BR.AS.Build.exe file */ - brAsBuildExe: vscode.Uri; - /** Array containing all information of all available gcc versions within this AS version */ - gccVersions: AsGccVersionInfo[]; -} - -/** - * Information of gcc installation within B&R Automation Studio - */ -export interface AsGccVersionInfo extends VersionInfo { - /** Data for all supported target systems. Access by `targetSystemData[' ']` */ - targetSystemData: { - [targetSystem: string]: AsGccTargetSystemInfo - } -} - -/** - * Target system types in notation ' ' - */ -//TODO maybe use for AsGccVersionInfo.targetSystemData[targetSystem: TargetSystemType], but it gives errors -export type TargetSystemType = 'SG3 M68k' | 'SGC M68k' | 'SG4 Ia32' | 'SG4 Arm'; - - -/** - * Information of target systems within a gcc installation. - */ -export interface AsGccTargetSystemInfo { //TODO rename? was not fully clear to me after some time not working on this... - /** URI to gcc.exe for this target system */ - gccExe: vscode.Uri; -} - - -/** - * B&R Process Variable Interface (PVI) version and installation information - */ -export interface PviVersionInfo extends VersionInfo { - pviTransferExe: vscode.Uri; -} - - -//#endregion exported interfaces - -//#region exported functions - - -/** - * Gets all available Automation Studio versions in the configured installation paths. The versions are sorted, so the first entry contains the highest version. - */ -export async function getAvailableAutomationStudioVersions(): Promise { - return await _availableAutomationStudioVersions; -} - - -/** - * Updates the installed Automation Studio Version from the configured installation paths. - */ -export async function updateAvailableAutomationStudioVersions(): Promise { - //TODO return number like in BrAsProjectWorkspace - //TODO call when configuration value of baseInstallPaths changes - logger.debug('updateAvailableAutomationStudioVersions() -> start'); - _availableAutomationStudioVersions = findAvailableASVersions(); - const versionInfos = await _availableAutomationStudioVersions; - if (versionInfos.length === 0) { - const messageItems = ['Change baseInstallPaths']; - //TODO action for item - //TODO note that currently also intellisense is not available (no plctypes.h, ...) - vscode.window.showWarningMessage('No Automation Studio versions found. Build functionality will not be available.', ...messageItems); - logger.warning('No Automation Studio versions found. Build functionality will not be available.'); - return; - } - - logger.debug('updateAvailableAutomationStudioVersions() -> end', {numFoundVersions: versionInfos.length}); -} - - -/** - * Gets the version information for a specified AS version. - * @param versionRequest The AS version of the project as a string or semantic version object. - * @returns `undefined` if no fitting version was found. - */ -export async function getAsVersionInfo(versionRequest: semver.SemVer | string): Promise { - const semanticRequest = semver.coerce(versionRequest); - if (!semanticRequest) { - return undefined; - } - const fitBugfix = `${semanticRequest.major}.${semanticRequest.minor}.x`; - const asVersions = await getAvailableAutomationStudioVersions(); - return asVersions.find((v) => semver.satisfies(v.version, fitBugfix)); -} - - -/** - * Gets the BR.AS.Build.exe URI for a specified AS version. - * @param versionRequest The AS version of the project as a string or semantic version object. - * @returns `undefined` if no fitting version was found. - */ -export async function getBrAsBuilExe(versionRequest: semver.SemVer | string): Promise { - return (await getAsVersionInfo(versionRequest))?.brAsBuildExe; -} - - -/** - * Gets the target system information for the specified versions and target system type - * @param asVersion - */ -export async function getGccTargetSystemInfo(asVersion: semver.SemVer | string, gccVersion: semver.SemVer | string, targetSystem: TargetSystemType): Promise { - const asVersionInfo = await getAsVersionInfo(asVersion); - const gccVersionInfo = asVersionInfo?.gccVersions.find((v) => v.version.compare(gccVersion) === 0); - if (!gccVersionInfo) { - return undefined; - } - return gccVersionInfo.targetSystemData[targetSystem]; -} - - -//#endregion exported functions - - -//#region local variables - - -/** Array of all available AS versions. The array is sorted, so that the highest version is always the first array element */ -//TODO put functionality in a class to save state, or are local variables like this OK? -let _availableAutomationStudioVersions: Promise = findAvailableASVersions(); - - -//#endregion local variables - - -//#region local functions - - -/** - * Searches for AS installations within the configured installation paths. Search is not recursive! - */ -async function findAvailableASVersions(): Promise { - const baseInstallUris = extensionConfiguration.environment.automationStudioInstallPaths; - const versionInfos: ASVersionInfo[] = []; - for (const uri of baseInstallUris) { - versionInfos.push(...(await findAvailableASVersionsInUri(uri))); - } - // sort by version - versionInfos.sort((a, b) => semver.compare(b.version, a.version)); - return versionInfos; -} - - -/** - * Searches for AS installations within the given URI. Search is not recursive! - */ -async function findAvailableASVersionsInUri(uri: vscode.Uri): Promise { - logger.debug('findAvailableASVersionsInUri(uri)', {uri: uri.toString(true)}); - // filter subdirectories with regular expression for AS version - const subDirectories = await uriTools.listSubDirectoryNames(uri); - const asDirRegExp = new RegExp(/^AS(\d)(\d+)$/); - const matching = subDirectories.filter((d) => asDirRegExp.test(d)).map((d) => asDirRegExp.exec(d)); - // create version information from matching subdirectories - const versionInfos: ASVersionInfo[] = []; - for (const match of matching) { - // create full URI - const versionBaseUri = uriTools.pathJoin(uri, match![0]); - // create semantic version, handling for AS V3.X is different than V4.X - const major = parseInt(match![1]); - const minor = major === 3 ? 0 : parseInt(match![2]); - const bugfix = major === 3 ? parseInt(match![2]) : 0; - const version = semver.coerce(`${major}.${minor}.${bugfix}`); - if (!version) { - //TODO more user friendly message - logger.error('Cannot create semantic version from URI: ' + versionBaseUri.fsPath); - continue; - } - // get AS build executable - //TODO maybe language sensitive for Bin-en / Bin-de if both are available? - const asBuildExecutables: vscode.Uri[] = []; - const buildExeUriEn = uriTools.pathJoin(versionBaseUri, 'Bin-en/BR.AS.Build.exe'); - if (await uriTools.exists(buildExeUriEn)) { - asBuildExecutables.push(buildExeUriEn); - } - const buildExeUriDe = uriTools.pathJoin(versionBaseUri, 'Bin-de/BR.AS.Build.exe'); - if (await uriTools.exists(buildExeUriDe)) { - asBuildExecutables.push(buildExeUriDe); - } - if (asBuildExecutables.length === 0) { - // much slower backup solution if folder structure changes in future AS versions - asBuildExecutables.push(...await vscode.workspace.findFiles({base: versionBaseUri.fsPath, pattern: '**/BR.AS.Build.exe'})); - if (asBuildExecutables.length === 0) { - logger.warning(`Cannot find BR.AS.Build.exe in URI: ${versionBaseUri.fsPath}`); - continue; - } - } - logger.info(`AS Version V${version.version} found in ${versionBaseUri.fsPath}`); - const buildExecutable = asBuildExecutables[0]; - // create version information and push to array - const versionInfo: ASVersionInfo = { - version: version, - baseUri: versionBaseUri, - brAsBuildExe: buildExecutable, - gccVersions: [] - }; - // find gcc versions and push - await findAvailableGccVersions(versionInfo); - versionInfos.push(versionInfo); - } - return versionInfos; -} - - -/** - * Searches for gcc installations within asVersion.baseUri and pushes all found versions to asVersion.gccVersions - * @param asVersion AS version info for which gcc versions are searched. asVersion.gccVersions is modified by this function - */ -async function findAvailableGccVersions(asVersion: ASVersionInfo): Promise { - // remove all existing gcc version - asVersion.gccVersions.length = 0; - // filter gcc subdirectories with regular expression for version - const gccContainingUri = uriTools.pathJoin(asVersion.baseUri, 'AS/gnuinst'); - const gccSubDirs = await uriTools.listSubDirectoryNames(gccContainingUri); - const gccDirRegExp = new RegExp('^V(\\d+).(\\d+).(\\d+)$'); - const matching = gccSubDirs.filter((d) => gccDirRegExp.test(d)).map((d) => gccDirRegExp.exec(d)); - // create version information from matching subdirectories - for (const match of matching) { - const gccVersionUri = uriTools.pathJoin(gccContainingUri, match![0]); - const version = semver.coerce(match![0]); - if (!version) { - //TODO more user friendly message - logger.warning('Cannot create semantic version for URI: ' + gccVersionUri.fsPath); - continue; - } - //HACK from AS V >= 4.9 there is an additional subfolder '4.9' -> do it properly with regex, so future versions can be handled - const gccUriAs49 = uriTools.pathJoin(gccVersionUri, '4.9'); - const newGccPathScheme = await uriTools.exists(gccUriAs49); - const finalGccUri = newGccPathScheme ? gccUriAs49 : gccVersionUri; - // create gcc version object and push - const gccVersion: AsGccVersionInfo = - { - baseUri: finalGccUri, - version: version, - targetSystemData: {} - }; - findAvailableGccTargetSystems(gccVersion); - asVersion.gccVersions.push(gccVersion); - } - - asVersion.gccVersions.push(); - // sort - asVersion.gccVersions.sort((a, b) => semver.compare(a.version, b.version)); -} - - -/** - * Searches for gcc installations within gccVersion.baseUri and sets all found versions to gccVersion.targetSystemData - * @param gccVersion gcc version info for which target systems are searched. gccVersion.targetSystemData is modified by this function - */ -async function findAvailableGccTargetSystems(gccVersion: AsGccVersionInfo): Promise { - // clear existing data - for (const key in gccVersion.targetSystemData) { - delete gccVersion.targetSystemData[key]; - } - // setting of compiler exe and includes. Currently hard coded, because structures and folder names differ on each gcc version - //TODO find a more generic solution - const gccUri = gccVersion.baseUri; - // gcc V4.1.2 - if (gccVersion.version.compare('4.1.2') === 0) { - // SG4 Ia32 - gccVersion.targetSystemData['SG4 Ia32'] = { - //gccExe: uriTools.pathJoin(gccUri, 'bin/i386-elf-gcc.exe'), // i386 gcc V4.1.2 does not support query from C/C++ extension - gccExe: uriTools.pathJoin(gccUri, 'bin/arm-elf-gcc.exe') - }; - // SG4 Arm - gccVersion.targetSystemData['SG4 Arm'] = { - gccExe: uriTools.pathJoin(gccUri, 'bin/arm-elf-gcc.exe') - }; - } - // gcc V6.3.0 - if (gccVersion.version.compare('6.3.0') === 0) { - // SG4 Ia32 - gccVersion.targetSystemData['SG4 Ia32'] = { - gccExe: uriTools.pathJoin(gccUri, 'bin/i686-elf-gcc.exe') - }; - // SG4 Arm - gccVersion.targetSystemData['SG4 Arm'] = { - gccExe: uriTools.pathJoin(gccUri, 'bin/arm-eabi-gcc.exe') - }; - } -} - - -//#endregion local functions diff --git a/src/Environment/BrAsBuildExe.ts b/src/Environment/BrAsBuildExe.ts new file mode 100644 index 0000000..ab7b166 --- /dev/null +++ b/src/Environment/BrAsBuildExe.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode'; + +/** + * Representation of Automation Studio build exe (BR.AS.Build.exe) + */ +export class BrAsBuildExe { + //TODO maybe implement execution, args, ... directly in here + + public constructor(exePath: vscode.Uri) { + this.#exePath = exePath; + } + + /** The path to the BR.AS.Build.exe file */ + public get exePath() : vscode.Uri { + return this.#exePath; + } + #exePath: vscode.Uri; + + /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ + public toJSON(): any { + return { + exePath: this.exePath.toString(true), + }; + } +} \ No newline at end of file diff --git a/src/Environment/Environment.ts b/src/Environment/Environment.ts index 6ddc6c3..dd958ec 100644 --- a/src/Environment/Environment.ts +++ b/src/Environment/Environment.ts @@ -7,55 +7,127 @@ import * as vscode from 'vscode'; import { logger } from '../BrLog'; import * as semver from 'semver'; import { extensionConfiguration } from '../BRConfiguration'; -import { requestVersion } from './SemVerTools'; +import { requestVersion } from '../Tools/SemVer'; import { PviVersion } from './PviVersion'; +import { AutomationStudioVersion } from './AutomationStudioVersion'; export class Environment { - /** - * Get all available PVI versions - * @returns All available PVI versions - */ - static async getPviVersions(): Promise { - if (this.#pviVersions === undefined) { - this.#pviVersions = await this.updatePviVersions(); - } - return this.#pviVersions; - } - - /** - * Get a specific Pvi version object. If used in non strict mode, the highest available version will be returned. - * @param versionRequest The requested version which should be prefered. Can be set to `undefined` if any version is ok - * @param strict Only return a Pvi with same major.minor version. Defaults to `false` - * @returns A `Pvi` version which fullfills the request or `undefined` if no such version was found - */ - static async getPviVersion(requested?: semver.SemVer | string, strict = false): Promise { - const versions = await this.getPviVersions(); - return requestVersion(versions, requested, strict); - } - - /** - * Starts a new search for PVI versions in the configured directories and updates the internal list. - * @returns All available PVI versions after update - */ - static async updatePviVersions(): Promise { - logger.info('Start searching for PVI versions'); - // search for PVI installations in all configured directories - const result: PviVersion[] = []; - const configuredDirs = extensionConfiguration.environment.pviInstallPaths; - for (const configDir of configuredDirs) { - logger.info(`Searching for PVI versions in '${configDir.fsPath}'`); - const versionsInDir = await PviVersion.searchVersionsInDir(configDir); - result.push(...versionsInDir); - } - // done - logger.info(`Searching for PVI versions done, ${result.length} versions found`); - this.#pviVersions = result; - return this.#pviVersions; - } - static #pviVersions: PviVersion[] | undefined; - /** static only class */ private constructor() { } + + /** PVI (Process Variable Interface) environment */ + public static pvi = class { + + /** static only class */ + private constructor() { } + + /** + * Get all available PVI versions + * @returns All available PVI versions + */ + public static async getVersions(): Promise { + if (this.#versions === undefined) { + this.#versions = this.#searchVersions(); + } + return await this.#versions; + } + + /** + * Get a specific Pvi version object. If used in non strict mode, the highest available version will be returned. + * @param version The requested version which should be prefered. Can be set to `undefined` if any version is ok + * @param strict Only return a Pvi with same major.minor version. Defaults to `false` + * @returns A `Pvi` version which fullfills the request or `undefined` if no such version was found + */ + public static async getVersion(version?: semver.SemVer | string, strict = false): Promise { + const versions = await this.getVersions(); + return requestVersion(versions, version, strict); + } + + /** + * Starts a new search for PVI versions in the configured directories and updates the internal list. + * @returns All available PVI versions after update + */ + public static async updateVersions(): Promise { + this.#versions = this.#searchVersions(); + return await this.#versions; + } + static async #searchVersions(): Promise { + logger.info('Start searching for PVI versions'); + // search for PVI installations in all configured directories + const foundVersions: PviVersion[] = []; + const configuredDirs = extensionConfiguration.environment.pviInstallPaths; + for (const configDir of configuredDirs) { + logger.info(`Searching for PVI versions in '${configDir.fsPath}'`); + const versionsInDir = await PviVersion.searchVersionsInDir(configDir); + foundVersions.push(...versionsInDir); + } + // done + if (foundVersions.length > 0) { + logger.info(`Searching for PVI versions done, ${foundVersions.length} versions found`); + } else { + logger.warning(`No PVI versions found. Some functioanlity will not be available.`); + } + return foundVersions; + } + static #versions: Promise | undefined; + }; + + /** Automation Studio environment */ + public static automationStudio = class { + + /** static only class */ + private constructor() { } + + /** + * Get all available Automation Studio versions + * @returns All available Automation Studio versions + */ + public static async getVersions(): Promise { + if (this.#versions === undefined) { + this.#versions = this.#searchVersions(); + } + return await this.#versions; + } + + /** + * Get a specific Automation Studio version object. If used in non strict mode, the highest available version will be returned. + * @param version The requested version which should be prefered. Can be set to `undefined` if any version is ok + * @param strict Only return an Automation Studio with same major.minor version. Defaults to `false` + * @returns An Automation Studio version which fullfills the request or `undefined` if no such version was found + */ + public static async getVersion(version?: semver.SemVer | string, strict = false): Promise { + const versions = await this.getVersions(); + return requestVersion(versions, version, strict); + } + + /** + * Starts a new search for Automation Studio versions in the configured directories and updates the internal list. + * @returns All available Automation Studio versions after update + */ + public static async updateVersions(): Promise { + this.#versions = this.#searchVersions(); + return await this.#versions; + } + static async #searchVersions(): Promise { + logger.info('Start searching for Automation Studio versions'); + // search for Automation Studio installations in all configured directories + const foundVersions: AutomationStudioVersion[] = []; + const configuredDirs = extensionConfiguration.environment.automationStudioInstallPaths; + for (const configDir of configuredDirs) { + logger.info(`Searching for Automation Studio versions in '${configDir.fsPath}'`); + const versionsInDir = await AutomationStudioVersion.searchVersionsInDir(configDir); + foundVersions.push(...versionsInDir); + } + // done + if (foundVersions.length > 0) { + logger.info(`Searching for Automation Studio versions done, ${foundVersions.length} versions found`); + } else { + logger.warning(`No Automation Studio versions found. Some functioanlity will not be available.`); + } + return foundVersions; + } + static #versions: Promise | undefined; + }; } \ No newline at end of file diff --git a/src/Environment/GccExecutable.ts b/src/Environment/GccExecutable.ts new file mode 100644 index 0000000..8db77aa --- /dev/null +++ b/src/Environment/GccExecutable.ts @@ -0,0 +1,186 @@ +import * as vscode from 'vscode'; +import * as uriTools from '../Tools/UriTools'; +import * as semver from 'semver'; +import { logger } from '../BrLog'; +import { SystemGeneration, TargetArchitecture } from './CommonTypes'; +import { spawnSync } from 'child_process'; + +/** + * Representation of a gcc.exe with additional information + */ +export class GccExecutable { + + /** + * Compare two gcc executables for match query, to give best result in non strict mode + * @returns `0` if a == b; `1` if a is greater; `-1` if b is greater + */ + public static compareForQuery(a: GccExecutable, b: GccExecutable): number { + // compare with priority of comparison, version > system generation > architecture + // version + const versionCompare = semver.compare(a.version, b.version); + if (versionCompare !== 0) { + return versionCompare; + } + // system generation + const sgPriority: SystemGeneration[] = ['SGC', 'SG3', 'SG4', 'UNKNOWN']; + const sgValueA = sgPriority.indexOf(a.systemGeneration); + const sgValueB = sgPriority.indexOf(b.systemGeneration); + const sgCompare = sgValueA - sgValueB; + if (sgCompare !== 0) { + return sgCompare < 0 ? -1 : 1; + } + // Architecture priority based on age and usage + const archPriority: TargetArchitecture[] = ['M68K', 'Arm', 'IA32', 'UNKNOWN']; + const archValueA = archPriority.indexOf(a.architecture); + const archValueB = archPriority.indexOf(b.architecture); + const archCompare = archValueA - archValueB; + if (archCompare !== 0) { + return archCompare < 0 ? -1 : 1; + } + return 0; + } + + /** + * Creates a gcc.exe representation for a specfic target system type by parsing the provided + * data. + * @param exePath URI to the gcc.exe specific to this target system (e.g. '.../i386-elf-gcc.exe') + * @param gccVersion Version of gcc of the exe to prevent querying the gcc.exe. Can be set if version is known for better performance (~50-100ms per call) + */ + public constructor(exePath: vscode.Uri, gccVersion?: semver.SemVer) { + this.#exePath = exePath; + // assign version from contructor arg, query to gcc or V0.0.0 + let usedVersion = gccVersion; + if (!usedVersion) { + usedVersion = queryGccVersion(exePath); + } + if (!usedVersion) { + logger.warning(`gcc version for '${exePath.toString(true)}' could not be evaluated. Gcc will be listed as V0.0.0`); + usedVersion = new semver.SemVer('0.0.0'); + } + this.#version = usedVersion; + // Get target machine from gcc.exe prefix or query to gcc + const exeName = uriTools.pathBasename(exePath); + const machinePrefixIdx = exeName.lastIndexOf('-gcc.exe'); + let targetMachine = exeName.substring(0, machinePrefixIdx); + if (targetMachine.length === 0) { + targetMachine = queryGccMachine(exePath) ?? ''; + } + this.#targetMachine = targetMachine; + // use target machine to set system generation, architecture and include dir + [this.#systemGeneration, this.#architecture] = targetSystemLookup(this.#targetMachine); + if ((this.#systemGeneration === 'UNKNOWN') || (this.#architecture === 'UNKNOWN')) { + logger.warning(`B&R system generation and architecture could not be evaluated for gcc in ${exePath.fsPath}`); + } + const sysInclude = vscode.Uri.joinPath(exePath, '../../', this.#targetMachine, './include'); + this.#systemIncludes = [sysInclude]; + // decide if gcc can be queried by the C/C++ extension + //HACK This is based purely on trial and the expectation that all newer versions will support queries + if (this.#version.major < 4) { + this.#supportsQuery = false; + } else if ((this.#version.major === 4) && (this.#architecture === 'IA32')) { + this.#supportsQuery = false; + } else { + this.#supportsQuery = true; + } + } + + /** URI to the gcc.exe */ + public get exePath(): vscode.Uri { + return this.#exePath; + } + #exePath: vscode.Uri; + + /** gcc version */ + public get version() : semver.SemVer { + return this.#version; + } + #version: semver.SemVer; + + /** target machine string */ + public get targetMachine() : string { + return this.#targetMachine; + } + #targetMachine: string; + + /** The B&R system generation targeted by this gcc executable */ + public get systemGeneration(): SystemGeneration { + return this.#systemGeneration; + } + #systemGeneration: SystemGeneration; + + /** The CPU architecture targeted by this gcc executable */ + public get architecture(): TargetArchitecture { + return this.#architecture; + } + #architecture: TargetArchitecture; + + /** System include directories. Use only if `supportsQuery` is `false` */ + public get systemIncludes(): vscode.Uri[] { + return this.#systemIncludes; + } + #systemIncludes: vscode.Uri[]; + + /** Compiler supports to be queried for includes... by the C/C++ extension + * If queries are supported, the `systemIncludes` should be ignored, as the query is + * a more reliable source. + */ + public get supportsQuery(): boolean { + return this.#supportsQuery; + } + #supportsQuery: boolean; + + /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ + public toJSON(): any { + return { + exePath: this.exePath.toString(true), + version: this.version.version, + targetMachine: this.targetMachine, + systemGeneration: this.systemGeneration, + architecture: this.architecture, + systemIncludes: this.systemIncludes.map((uri) => uri.toString(true)), + supportsQuery: this.supportsQuery, + }; + } +} + +/** Lookup for target system generation and architecture from *-gcc.exe prefix */ +function targetSystemLookup(gccPrefix: string): [SystemGeneration, TargetArchitecture] { + // B&R gcc.exe are named according to the target system. For SG4 IA32 this is e.g. i386-elf-gcc.exe + switch (gccPrefix) { + case 'm68k-elf': + return ['SG3', 'M68K']; + + case 'i386-elf': + case 'i686-elf': + return ['SG4', 'IA32']; + + case 'arm-elf': + case 'arm-eabi': + return ['SG4', 'Arm']; + + default: + return ['UNKNOWN', 'UNKNOWN']; + } +} + +/** Calls `gcc.exe -dumpversion` to get the gcc version */ +function queryGccVersion(gccExe: vscode.Uri): semver.SemVer | undefined { + const callResult = spawnSync(gccExe.fsPath, ['-dumpversion']); + if (!callResult.error) { + const stdout = callResult.stdout.toString().trim(); + return semver.coerce(stdout) ?? undefined; + } else { + return undefined; + } +} + +/** Calls `gcc.exe -dumpmachine` to get the gcc target machine */ +function queryGccMachine(gccExe: vscode.Uri): string | undefined { + const callResult = spawnSync(gccExe.fsPath, ['-dumpmachine']); + if (!callResult.error) { + const stdout = callResult.stdout.toString().trim(); + return stdout; + } else { + return undefined; + } +} \ No newline at end of file diff --git a/src/Environment/GccInstallation.ts b/src/Environment/GccInstallation.ts new file mode 100644 index 0000000..bfa92db --- /dev/null +++ b/src/Environment/GccInstallation.ts @@ -0,0 +1,123 @@ +import * as vscode from 'vscode'; +import * as uriTools from '../Tools/UriTools'; +import * as semver from 'semver'; +import { logger } from '../BrLog'; +import { SystemGeneration, TargetArchitecture } from './CommonTypes'; +import { GccExecutable } from './GccExecutable'; + +/** + * Representation of a gcc installation + */ +export class GccInstallation { + + /** + * Gets all gcc executables which are located in the Automation Studio `gnuinst` directory + * @param root The Automation Studio `gnuinst` directory containing multiple gcc installations. e.g. `C:\BrAutomation\AS410\AS\gnuinst` + * @returns The installation with all found *gcc.exe + */ + public static async searchAutomationStudioGnuinst(root: vscode.Uri): Promise { + // create gcc installation from all matching directories + const gccDirRegExp = /^V(\d+).(\d+).(\d+)$/; + const rootUris = await uriTools.listSubDirectories(root, gccDirRegExp); + const installation = new GccInstallation(); + await installation.#initialize(...rootUris); + // done + logger.debug('GccInstallation.searchAutomationStudioGnuinst(root)', { root: root, result: installation }); + return installation; + } + + /** + * Creates a gcc version from a specified root directory + * @param root The root directory containing a single gcc installation. e.g. `C:\BrAutomation\AS410\AS\gnuinst\V6.3.0`, or `C:\msys64\mingw64` + * @returns The version which was parsed from the root URI + */ + public static async createFromDir(root: vscode.Uri): Promise { + // Create and initialize object + const installation = new GccInstallation(); + await installation.#initialize(root); + // done + logger.debug('GccInstallation.createFromDir(root)', { root: root, result: installation }); + return installation; + } + + /** Object is not ready to use after constructor due to async operations, + * #initialize() has to be called for the object to be ready to use! */ + private constructor() { } + + /** Async operations to finalize object construction */ + async #initialize(...roots: vscode.Uri[]): Promise { + for (const root of roots) { + // try parse version from directory name + const dirName = uriTools.pathBasename(root); + let version: semver.SemVer | undefined = undefined; + const dirNameIsVersion = /^V(\d+).(\d+).(\d+)$/.test(dirName); + if (dirNameIsVersion) { + version = semver.coerce(dirName) ?? undefined; + } + // find bin directory + const binDir = await uriTools.findDirectory(root, 2, 'bin'); + if (binDir === undefined) { + logger.warning(`Could not find directory 'bin' of gcc in '${root.toString(true)}'`); + continue; + } + // find *gcc.exe in bin directory + const gccExePaths = await uriTools.listSubFiles(binDir, /^([\w\-_]*)gcc.exe$/); + const gccExes = gccExePaths.map((exe) => new GccExecutable(exe, version)); + this.#executables.push(...gccExes); + } + // init done + this.#isInitialized = true; + } + #isInitialized = false; + + /** Targets available in this gcc version */ + public get executables(): GccExecutable[] { + if (!this.#isInitialized) { throw new Error(`Use of not initialized ${GccInstallation.name} object`); } + return this.#executables; + } + #executables: GccExecutable[] = []; + + /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ + public toJSON(): any { + return { + executables: this.executables, + }; + } + + /** + * Get a `GccTarget` which fullfills the requested requirements. + * @param version The requested gcc version + * @param systemGeneration The requested B&R system generation + * @param architecture The requested CPU architecture + * @param strict If `true`, only exact matches will be returned + * @returns A `GccTarget` which fullfills the requested requirements or `undefined` if no such was found. + */ + public getExecutable(version?: semver.SemVer | string, systemGeneration?: SystemGeneration, architecture?: TargetArchitecture, strict?: boolean): GccExecutable | undefined { + // directly return if no targets available + if (this.executables.length === 0) { + return undefined; + } + // prepare sorted array to select best target if there is no exact match. Best target will be index 0 (descending sort) + const sorted = [...this.executables].sort((a, b) => GccExecutable.compareForQuery(b, a)); + // get matches for version, system generation and architecture. If argument === undefined all is a match + const versionMatch = !version ? sorted : sorted.filter((el) => semver.eq(el.version, version)); + const sgMatch = !systemGeneration ? sorted : sorted.filter((el) => (el.systemGeneration === systemGeneration)); + const archMatch = !architecture ? sorted : sorted.filter((el) => (el.architecture === architecture)); + // get intersection of separate matches + const allMatch = versionMatch.filter((ele) => (sgMatch.includes(ele) && archMatch.includes(ele))); + // find best match: matchAll > versionMatch > sgMatch > archMatch > noMatch + if (allMatch.length > 0) { + return allMatch[0]; + } else if (strict) { + return undefined; + } else if (versionMatch.length > 0) { + return versionMatch[0]; + } else if (sgMatch.length > 0) { + return sgMatch[0]; + } else if (archMatch.length > 0) { + return archMatch[0]; + } else { + return sorted[0]; + } + } +} \ No newline at end of file diff --git a/src/Environment/GccTarget.ts b/src/Environment/GccTarget.ts deleted file mode 100644 index 2af46ef..0000000 --- a/src/Environment/GccTarget.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as vscode from 'vscode'; -import * as uriTools from '../Tools/UriTools'; -import * as semver from 'semver'; -import { logger } from '../BrLog'; -import { SystemGeneration, TargetArchitecture } from './CommonTypes'; - -/** - * Representation of a gcc.exe for a specific target system type with additional information - */ -export class GccTarget { - - /** - * Creates a gcc.exe representation for a specfic target system type by parsing the provided - * data. - * @param gccExe URI to the gcc.exe specific to this target system (e.g. '.../i386-elf-gcc.exe') - * @param gccVersion Version of gcc of the exe (only required for `supportsQuery` hack) - */ - constructor(gccExe: vscode.Uri, gccVersion: semver.SemVer) { - this.#executable = gccExe; - // get prefix to find system generation, architecture and includes - const targetPrefix = uriTools.pathBasename(gccExe, '-gcc.exe'); - [this.#systemGeneration, this.#architecture] = targetSystemLookup(targetPrefix); - if ((this.#systemGeneration === undefined) || (this.#architecture === undefined)) { - logger.warning(`B&R system generation and architecture could not be evaluated for gcc in ${gccExe.fsPath}`); - } - const sysInclude = vscode.Uri.joinPath(gccExe, '../../', targetPrefix, './include'); - this.#systemIncludes = [sysInclude]; - // decide if gcc can be queried by the C/C++ extension - //HACK This is based purely on trial and the expectation that all newer versions will support queries - if (gccVersion.version === '2.95.3') { - this.#supportsQuery = false; - } else if (gccVersion.version === '4.1.1') { - this.#supportsQuery = false; - } else if ((gccVersion.version === '4.1.2') && (this.#architecture === 'IA32')) { - this.#supportsQuery = false; - } else { - this.#supportsQuery = true; - } - } - - /** URI to the gcc.exe */ - public get executable(): vscode.Uri { - return this.#executable; - } - #executable: vscode.Uri; - - /** The B&R system generation targeted by this gcc executable */ - public get systemGeneration(): SystemGeneration { - return this.#systemGeneration; - } - #systemGeneration: SystemGeneration; - - /** The CPU architecture targeted by this gcc executable */ - public get architecture(): TargetArchitecture { - return this.#architecture; - } - #architecture: TargetArchitecture; - - /** System include directories. Use only if `supportsQuery` is `false` */ - public get systemIncludes(): vscode.Uri[] { - return this.#systemIncludes; - } - #systemIncludes: vscode.Uri[]; - - /** Compiler supports to be queried for includes... by the C/C++ extension - * If queries are supported, the `systemIncludes` should be ignored, as the query is - * a more reliable source. - */ - public get supportsQuery(): boolean { - return this.#supportsQuery; - } - #supportsQuery: boolean; - - /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ - public toJSON(): any { - return { - executable: this.executable.toString(true), - systemGeneration: this.systemGeneration, - architecture: this.architecture, - systemIncludes: this.systemIncludes.map((uri) => uri.toString(true)), - supportsQuery: this.supportsQuery, - }; - } - - /** Sort value which can be used to get 'highest' target system */ - public get sortValue(): number { - // System generation priority based on age -> newer is better - const sgPriority: SystemGeneration[] = ['SGC', 'SG3', 'SG4', 'UNKNOWN']; - const sgValue = sgPriority.indexOf(this.systemGeneration); - // Architecture priority based on age and usage - const archPriority: TargetArchitecture[] = ['M68K', 'Arm', 'IA32', 'UNKNOWN']; - const archValue = archPriority.indexOf(this.architecture); - // Total sort value gives higher priority to generation, as newer architectures support mostly the same functionality - const value = (10 * sgValue) + archValue; - return value; - } -} - -/** Lookup for target system generation and architecture from *-gcc.exe prefix */ -function targetSystemLookup(gccPrefix: string): [SystemGeneration, TargetArchitecture] { - // B&R gcc.exe are named according to the target system. For SG4 IA32 this is e.g. i386-elf-gcc.exe - switch (gccPrefix) { - case 'm68k-elf': - return ['SG3', 'M68K']; - - case 'i386-elf': - case 'i686-elf': - return ['SG4', 'IA32']; - - case 'arm-elf': - case 'arm-eabi': - return ['SG4', 'Arm']; - - default: - return ['UNKNOWN', 'UNKNOWN']; - } -} \ No newline at end of file diff --git a/src/Environment/GccVersion.ts b/src/Environment/GccVersion.ts deleted file mode 100644 index d314def..0000000 --- a/src/Environment/GccVersion.ts +++ /dev/null @@ -1,145 +0,0 @@ -import * as vscode from 'vscode'; -import * as uriTools from '../Tools/UriTools'; -import * as semver from 'semver'; -import { logger } from '../BrLog'; -import { SystemGeneration, TargetArchitecture } from './CommonTypes'; -import { GccTarget } from './GccTarget'; - -/** - * Representation of a gcc version - */ -export class GccVersion { - - /** - * Gets all gcc versions which are located in the rootUri. - * @param rootUri The root directory containing multiple gcc installations. e.g. `C:\BrAutomation\AS410\AS\gnuinst` - * @returns An array with all found versions - */ - public static async searchVersionsInDir(rootUri: vscode.Uri): Promise { - // Get matching subdirectory names - const subDirNames = await uriTools.listSubDirectoryNames(rootUri); - const gccDirRegExp = new RegExp('^V(\\d+).(\\d+).(\\d+)$'); - const matching = subDirNames.filter((d) => gccDirRegExp.test(d)).map((d) => gccDirRegExp.exec(d)); - // create GccVersion from matching subdirectories - const result: GccVersion[] = []; - for (const match of matching) { - const gccVersionUri = vscode.Uri.joinPath(rootUri, `./${match![0]}`); - const gccVersion = await this.createFromDir(gccVersionUri); - if (gccVersion !== undefined) { - result.push(gccVersion); - } - } - // done - return result; - } - - /** - * Creates a gcc version from a specified root directory - * @param rootUri The root directory containing a single gcc installation. e.g. `C:\BrAutomation\AS410\AS\gnuinst\V6.3.0` - * @returns The version which was parsed from the root URI - */ - public static async createFromDir(rootUri: vscode.Uri): Promise { - // Create and initialize object - try { - const gccVersion = new GccVersion(rootUri); - await gccVersion.#initialize(); - logger.info(`gcc Version V${gccVersion.version.version} found in '${gccVersion.rootUri.fsPath}'`); - return gccVersion; - } catch (error) { - if (error instanceof Error) { - logger.error(`Failed to get gcc in path '${rootUri.fsPath}': ${error.message}`); - } else { - logger.error(`Failed to get gcc in path '${rootUri.fsPath}'`); - } - return undefined; - } - } - - /** Object is not ready to use after constructor due to async operations, - * #initialize() has to be called for the object to be ready to use! */ - private constructor(rootUri: vscode.Uri) { - this.#rootUri = rootUri; - // parse version from directory name - const dirName = uriTools.pathBasename(this.#rootUri); - const version = semver.coerce(dirName); - if (!version) { - throw new Error('Cannot parse version from directory name'); - } - this.#version = version; - // other properties rely on async and will be initialized in #initialize() - } - - /** Async operations to finalize object construction */ - async #initialize(): Promise { - // find targets - const findGccExePattern = new vscode.RelativePattern(this.#rootUri, '**/bin/*-gcc.exe'); - const gccExes = await vscode.workspace.findFiles(findGccExePattern); - this.#targets = gccExes.map((exe) => new GccTarget(exe, this.#version)); - // init done - this.#isInitialized = true; - } - #isInitialized = false; - - /** The root URI of the gcc version */ - public get rootUri(): vscode.Uri { - if (!this.#isInitialized) { throw new Error(`Use of not initialized ${GccVersion.name} object`); } - return this.#rootUri; - } - #rootUri: vscode.Uri; - - /** The gcc version */ - public get version(): semver.SemVer { - if (!this.#isInitialized) { throw new Error(`Use of not initialized ${GccVersion.name} object`); } - return this.#version; - } - #version: semver.SemVer; - - /** Targets available in this gcc version */ - public get targets(): GccTarget[] { - if (this.#targets === undefined) { throw new Error(`Use of not initialized ${GccVersion.name} object`); } - return this.#targets; - } - #targets: GccTarget[] | undefined; - - /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ - public toJSON(): any { - return { - rootUri: this.rootUri.toString(true), - version: this.version.version, - targets: this.#targets, - }; - } - - /** - * Get a `GccTarget` which fullfills the requested requirements. - * @param systemGeneration The requested B&R system generation - * @param architecture The requested CPU architecture - * @param strict If `true`, only exact matches will be returned - * @returns A `GccTarget` which fullfills the requested requirements or `undefined` if no such was found. - */ - public getTarget(systemGeneration?: SystemGeneration, architecture?: TargetArchitecture, strict?: boolean): GccTarget |undefined { - // directly return if no targets available - if (this.targets.length === 0) { - return undefined; - } - // prepare sorted array to select best target if there is no exact match. Best target will be index 0 (descending sort) - const sorted = [...this.targets].sort((a, b) => (b.sortValue - a.sortValue)); - // get matches for system generation and architecture. If argument === undefined all is a match - const sgMatch = !systemGeneration ? sorted : sorted.filter((el) => (el.systemGeneration === systemGeneration)); - const archMatch = !architecture ? sorted : sorted.filter((el) => (el.architecture === architecture)); - // get intersection of separate matches - const allMatch = sgMatch.filter((sgEle) => archMatch.includes(sgEle)); - // find best match: matchAll > matchSg > matchArch > matchNone - if (allMatch.length > 0) { - return allMatch[0]; - } else if (strict) { - return undefined; - } else if (sgMatch.length > 0) { - return sgMatch[0]; - } else if (archMatch.length > 0) { - return archMatch[0]; - } else { - return sorted[0]; - } - } -} \ No newline at end of file diff --git a/src/Environment/PviTransferExe.ts b/src/Environment/PviTransferExe.ts index ed2cca5..f45dbbd 100644 --- a/src/Environment/PviTransferExe.ts +++ b/src/Environment/PviTransferExe.ts @@ -1,27 +1,29 @@ -/** - * Handling PVITransfer.exe - * @packageDocumentation -*/ - import * as vscode from 'vscode'; +/** + * Representation of PVITransfer.exe + */ export class PviTransferExe { - //TODO implement + //TODO maybe implement execution, args, ... directly in here - constructor(executable: vscode.Uri) { - this.#executable = executable; + /** + * Creates a PVITransfer.exe representation + * @param exePath URI to the PVITransfer.exe + */ + public constructor(exePath: vscode.Uri) { + this.#exePath = exePath; } /** The path to the PVITransfer.exe file */ - public get executable() : vscode.Uri { - return this.#executable; + public get exePath() : vscode.Uri { + return this.#exePath; } - #executable: vscode.Uri; + #exePath: vscode.Uri; /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ public toJSON(): any { return { - executable: this.executable.toString(true), + exePath: this.exePath.toString(true), }; } } \ No newline at end of file diff --git a/src/Environment/PviVersion.ts b/src/Environment/PviVersion.ts index 626b163..8dbdba9 100644 --- a/src/Environment/PviVersion.ts +++ b/src/Environment/PviVersion.ts @@ -11,40 +11,42 @@ export class PviVersion { /** * Gets all PVI versions which are located in the rootUri. - * @param rootUri The root directory containing multiple PVI installations. e.g. `C:\BrAutomation\PVI` + * @param installRoot The root directory containing multiple PVI installations. e.g. `C:\BrAutomation\PVI` * @returns An array with all found versions */ - public static async searchVersionsInDir(rootUri: vscode.Uri): Promise { - // create PviVersion from all subdirectories - const result: PviVersion[] = []; - const subDirs = await uriTools.listSubDirectories(rootUri); - for (const subDir of subDirs) { - const version = await this.createFromDir(subDir); - if (version) { - result.push(version); + public static async searchVersionsInDir(installRoot: vscode.Uri): Promise { + // Get matching subdirectories + const pviDirRegExp = /^V[\d]+\.[\d\.]+$/; + const subDirs = await uriTools.listSubDirectories(installRoot, pviDirRegExp); + // create PviVersion from from matching subdirectories + const versions: PviVersion[] = []; + for (const dir of subDirs) { + const pviVersion = await this.createFromDir(dir); + if (pviVersion) { + versions.push(pviVersion); } } //done - return result; + return versions; } /** * Creates a PVI version from a specified root directory - * @param rootUri The root directory containing a single PVI installation. e.g. `C:\BrAutomation\PVI\V4.10` + * @param pviRoot The root directory containing a single PVI installation. e.g. `C:\BrAutomation\PVI\V4.10` * @returns The version which was parsed from the root URI */ - public static async createFromDir(rootUri: vscode.Uri): Promise { + public static async createFromDir(pviRoot: vscode.Uri): Promise { // Create and initialize object try { - const pvi = new PviVersion(rootUri); + const pvi = new PviVersion(pviRoot); await pvi.#initialize(); - logger.info(`PVI Version V${pvi.version.version} found in '${pvi.rootUri.fsPath}'`); + logger.info(`PVI Version V${pvi.version.version} found in '${pvi.rootPath.fsPath}'`); return pvi; } catch (error) { if (error instanceof Error) { - logger.error(`Failed to get PVI in path '${rootUri.fsPath}': ${error.message}`); + logger.error(`Failed to get PVI in path '${pviRoot.fsPath}': ${error.message}`); } else { - logger.error(`Failed to get PVI in path '${rootUri.fsPath}'`); + logger.error(`Failed to get PVI in path '${pviRoot.fsPath}'`); } return undefined; } @@ -52,70 +54,96 @@ export class PviVersion { /** Object is not ready to use after constructor due to async operations, * #initialize() has to be called for the object to be ready to use! */ - private constructor(rootUri: vscode.Uri) { - this.#rootUri = rootUri; - // parse version from directory name - const dirName = uriTools.pathBasename(this.#rootUri); - const version = semver.coerce(dirName); - if (!version) { - throw new Error('Cannot parse version from directory name'); - } - this.#version = version; + private constructor(pviRoot: vscode.Uri) { + this.#rootPath = pviRoot; // other properties rely on async and will be initialized in #initialize() } /** Async operations to finalize object construction */ async #initialize() { - // find PVITransfer.exe candidates and select first - const pviTransferExecutables: vscode.Uri[] = []; - const transferExeAsInstall = vscode.Uri.joinPath(this.#rootUri, './PVI/Tools/PVITransfer/PVITransfer.exe'); // Standard installation path for AS installation - if (await uriTools.exists(transferExeAsInstall)) { - pviTransferExecutables.push(transferExeAsInstall); - } - const transferExeRucExport = vscode.Uri.joinPath(this.#rootUri, './PVITransfer.exe'); // Directly in directory (e.g. by RUC export) - if (await uriTools.exists(transferExeRucExport)) { - pviTransferExecutables.push(transferExeRucExport); - } - if (pviTransferExecutables.length === 0) { - // much slower backup solution which searches recursive - pviTransferExecutables.push(...await vscode.workspace.findFiles({ base: this.#rootUri.fsPath, pattern: '**/PVITransfer.exe' })); - if (pviTransferExecutables.length === 0) { - throw new Error('Cannot find PVITransfer.exe'); - } + this.#version = await parsePviVersion(this.#rootPath); + // Find PVITransfer.exe + this.#pviTransfer = await searchPviTransferExe(this.#rootPath); + if (!this.#pviTransfer) { + throw new Error('Cannot find PVITransfer.exe'); } - this.#pviTransfer = new PviTransferExe(pviTransferExecutables[0]); // init done this.#isInitialized = true; } #isInitialized = false; /** The root URI of the PVI version */ - public get rootUri(): vscode.Uri { + public get rootPath(): vscode.Uri { if (!this.#isInitialized) { throw new Error('Use of not initialized Pvi object'); } - return this.#rootUri; + return this.#rootPath; } - #rootUri: vscode.Uri; + #rootPath: vscode.Uri; /** The version of the PVI */ public get version(): semver.SemVer { - if (!this.#isInitialized) { throw new Error('Use of not initialized Pvi object'); } + if (!this.#isInitialized || !this.#version) { throw new Error('Use of not initialized Pvi object'); } return this.#version; } - #version: semver.SemVer; + #version: semver.SemVer | undefined; /** PVITransfer.exe of this PVI version */ public get pviTransfer(): PviTransferExe { - if (!this.#isInitialized) { throw new Error('Use of not initialized Pvi object'); } - return this.#pviTransfer!; + if (!this.#isInitialized || !this.#pviTransfer) { throw new Error('Use of not initialized Pvi object'); } + return this.#pviTransfer; } #pviTransfer: PviTransferExe | undefined; /** toJSON required as getter properties are not shown in JSON.stringify() otherwise */ public toJSON(): any { return { - rootUri: this.rootUri.toString(true), + rootPath: this.rootPath.toString(true), version: this.version.version, pviTransfer: this.#pviTransfer, }; } +} + +/** + * Trys to parse the version if a PVI installation. The info is gathered from the rootPath name. + * @param pviRoot Root path of the PVI installation. e.g. `C:\BrAutomation\PVI\V4.8` + * @returns The parsed version, or V0.0.0 if parsing failed + */ +async function parsePviVersion(pviRoot: vscode.Uri): Promise { + let version: semver.SemVer | undefined = undefined; + // Try parse version from root directory name + const dirName = uriTools.pathBasename(pviRoot); + version = semver.coerce(dirName) ?? undefined; + if (version) { + return version; + } else { + logger.warning(`Failed to parse PVI Version from directory name '${pviRoot.toString(true)}'. PVI will be listed as V0.0.0`); + } + // set to V0.0.0 as backup, so PVI is still available but with wrong version... + return new semver.SemVer('0.0.0'); +} + +/** + * Search for PVITransfer.exe in the PVI installation + * @param pviRoot Root path of the PVI installation. e.g. `C:\BrAutomation\PVI\V4.8` + * @returns The first found PVITransfer.exe, or `undefined` if no such was found. + */ +async function searchPviTransferExe(pviRoot: vscode.Uri): Promise { + // Standard installation path for AS or separate PVI installation + const transferExeAsInstall = vscode.Uri.joinPath(pviRoot, 'PVI/Tools/PVITransfer/PVITransfer.exe'); // Standard installation path for AS installation + if (await uriTools.exists(transferExeAsInstall)) { + return new PviTransferExe(transferExeAsInstall); + } + // Directly in root directory (e.g. by RUC export) + const transferExeRucExport = vscode.Uri.joinPath(pviRoot, 'PVITransfer.exe'); + if (await uriTools.exists(transferExeRucExport)) { + return new PviTransferExe(transferExeRucExport); + } + // slower search if none was found yet + const searchPattern = new vscode.RelativePattern(pviRoot, '**/PVITransfer.exe'); + const searchResult = await vscode.workspace.findFiles(searchPattern); + if (searchResult.length > 0) { + return new PviTransferExe(searchResult[0]); + } + // none was found + return undefined; } \ No newline at end of file diff --git a/src/ExternalApi/CppToolsApi.ts b/src/ExternalApi/CppToolsApi.ts index 47da0dd..f7934b0 100644 --- a/src/ExternalApi/CppToolsApi.ts +++ b/src/ExternalApi/CppToolsApi.ts @@ -6,10 +6,10 @@ import * as vscode from 'vscode'; import * as cppTools from 'vscode-cpptools'; import * as BRAsProjectWorkspace from '../BRAsProjectWorkspace'; -import * as BREnvironment from '../Environment/BREnvironment'; import { logger } from '../BrLog'; import * as Helpers from '../Tools/Helpers'; import * as uriTools from '../Tools/UriTools'; +import { Environment } from '../Environment/Environment'; /** @@ -51,7 +51,7 @@ class CppConfigurationProvider implements cppTools.CustomConfigurationProvider { } this.#cppApi.registerCustomConfigurationProvider(this); // Ready only parsing of workspace and environment (required for proper includes) - BREnvironment.getAvailableAutomationStudioVersions().then(() => this.didChangeCppToolsConfig()); + Environment.automationStudio.getVersions().then(() => this.didChangeCppToolsConfig()); BRAsProjectWorkspace.getWorkspaceProjects().then(() => this.didChangeCppToolsConfig()); this.#cppApi.notifyReady(this); return true; @@ -155,8 +155,9 @@ class CppConfigurationProvider implements cppTools.CustomConfigurationProvider { return undefined; } // get gcc data - const gccInfo = await BREnvironment.getGccTargetSystemInfo(asProjectInfo.asVersion, activeCfg.buildSettings.gccVersion, 'SG4 Ia32'); //TODO parameter targetSystem not hard coded (#11) - if (!gccInfo) { + const gccExe = (await Environment.automationStudio.getVersion(asProjectInfo.asVersion)) + ?.gccInstallation.getExecutable(activeCfg.buildSettings.gccVersion, 'SG4', 'Arm'); + if (!gccExe) { return undefined; } // get compiler arguments @@ -172,7 +173,7 @@ class CppConfigurationProvider implements cppTools.CustomConfigurationProvider { intelliSenseMode: undefined, standard: undefined, compilerArgs: compilerArgs, - compilerPath: gccInfo.gccExe.fsPath, + compilerPath: gccExe.exePath.fsPath, } }; return config; diff --git a/src/Tools/ApiTests.ts b/src/Tools/ApiTests.ts index 9e4cf03..332e4b1 100644 --- a/src/Tools/ApiTests.ts +++ b/src/Tools/ApiTests.ts @@ -10,15 +10,18 @@ import * as Helpers from './Helpers'; import * as uriTools from './UriTools'; import * as fileTools from './FileTools'; import * as Dialogs from '../UI/Dialogs'; -import * as BREnvironment from '../Environment/BREnvironment'; import * as BRAsProjectWorkspace from '../BRAsProjectWorkspace'; import * as BrAsProjectFiles from '../BrAsProjectFiles'; +import * as semver from 'semver'; import { logger } from '../BrLog'; import { extensionConfiguration } from '../BRConfiguration'; import { statusBar } from '../UI/StatusBar'; import { Environment } from '../Environment/Environment'; -import { GccVersion } from '../Environment/GccVersion'; +import { GccInstallation } from '../Environment/GccInstallation'; import { SystemGeneration, TargetArchitecture } from '../Environment/CommonTypes'; +import { AutomationStudioVersion } from '../Environment/AutomationStudioVersion'; +import { spawnSync } from 'child_process'; +import { spawnAsync } from './ChildProcess'; //import * as NAME from '../BRxxxxxx'; @@ -40,6 +43,9 @@ async function testCommand(arg1: any, arg2: any, context: vscode.ExtensionContex logger.showOutput(); logHeader('Test command start'); // select tests to execute + if (await Dialogs.yesNoDialog('Run tests for temporary stuff?')) { + await testTemp(context); + } if (await Dialogs.yesNoDialog('Run various tests?')) { await testVarious(arg1, arg2); } @@ -55,8 +61,8 @@ async function testCommand(arg1: any, arg2: any, context: vscode.ExtensionContex if (await Dialogs.yesNoDialog('Run tests for file system events?')) { await testFileSystemEvents(); } - if (await Dialogs.yesNoDialog('Run tests for BREnvironment?')) { - await testBREnvironment(); + if (await Dialogs.yesNoDialog('Run tests for AutomationStudioVersion?')) { + await testAutomationStudioVersion(context); } if (await Dialogs.yesNoDialog('Run tests for gcc?')) { await testGcc(context); @@ -86,6 +92,11 @@ async function testCommand(arg1: any, arg2: any, context: vscode.ExtensionContex logHeader('Test command end'); } +async function testTemp(context: vscode.ExtensionContext): Promise { + logHeader('Test temporary stuff start'); + logHeader('Test temporary stuff end'); +} + async function testVarious(arg1: any, arg2: any) { logHeader('Test various start'); // check command arguments @@ -266,37 +277,26 @@ async function testHelpers() { } -async function testBREnvironment() { - logHeader('Test BREnvironment start'); +async function testAutomationStudioVersion(context: vscode.ExtensionContext): Promise { + logHeader('Test AutomationStudioVersion start'); // Update AS versions - if (await Dialogs.yesNoDialog('Update AS versions?')) { - logger.info('BREnvironment.updateAvailableAutomationStudioVersions() start'); - const result = await BREnvironment.updateAvailableAutomationStudioVersions(); - logger.info('BREnvironment.updateAvailableAutomationStudioVersions() done', { result: result }); - } - // get AS version info - const asVersions = await BREnvironment.getAvailableAutomationStudioVersions(); - logger.info('BREnvironment.getAvailableAutomationStudioVersions()', { result: asVersions }); - // get BR.AS.Build.exe - const inputAsVersion = await vscode.window.showInputBox({prompt: 'Enter an AS version to find BR.AS.Build.exe'}); - if (inputAsVersion) { - const buildExe = await BREnvironment.getBrAsBuilExe(inputAsVersion); - logger.info('BREnvironment.getBrAsBuilExe(requested)', { requested: inputAsVersion, result: buildExe }); + const update = await Dialogs.yesNoDialog('Update AS versions?'); + if (update) { + const allVersions = await Environment.automationStudio.updateVersions(); + logger.info('AS versions found:', { versions: allVersions }); } - // get gcc target system info - const getTargetInfoAsVersion = '4.6.5'; - const getTargetInfoGccVersion = '4.1.2'; - const getTargetSystemType = 'SG4 Ia32'; - const targetSystemInfo = await BREnvironment.getGccTargetSystemInfo(getTargetInfoAsVersion, getTargetInfoGccVersion, getTargetSystemType); - logger.info('BREnvironment.getGccTargetSystemInfo(asVersion, gccVersion, targetSystem)', { - asVersion: getTargetInfoAsVersion, - gccVersion: getTargetInfoGccVersion, - targetSystem: getTargetSystemType, - result: targetSystemInfo - }); - - // end - logHeader('Test BREnvironment end'); + // Test queries for specific AS versions + const highestAs = await Environment.automationStudio.getVersion(); + logger.info('highest AS', { as: highestAs }); + const asV48 = await Environment.automationStudio.getVersion('4.8'); + logger.info('AS V4.8 not strict', { as: asV48 }); + const asV48Strict = await Environment.automationStudio.getVersion('4.8', true); + logger.info('AS V4.8 strict', { as: asV48Strict }); + const asV46 = await Environment.automationStudio.getVersion('4.6'); + logger.info('AS V4.6 not strict', { as: asV46 }); + const asV46Strict = await Environment.automationStudio.getVersion('4.6', true); + logger.info('AS V4.6 strict', { as: asV46Strict }); + logHeader('Test AutomationStudioVersion end'); } @@ -304,15 +304,15 @@ async function testGcc(context: vscode.ExtensionContext): Promise { logHeader('Test gcc start'); // get gcc versions for AS V4.10 const gccBase = vscode.Uri.file('C:\\BrAutomation\\AS410\\AS\\gnuinst'); - const gccVersions = await GccVersion.searchVersionsInDir(gccBase); + const gccInstall = await GccInstallation.searchAutomationStudioGnuinst(gccBase); logger.info('Gcc versions', { base: gccBase.fsPath, - versions: gccVersions, + gccInstall: gccInstall, }); - const gccVersionSelItems = gccVersions.map((gcc) => ({value: gcc, label: gcc.version.version})); // Get targets do { - const selectedGcc = await Dialogs.getQuickPickSingleValue(gccVersionSelItems, { title: 'Select gcc version' }); + const gccVersionStr = await vscode.window.showInputBox({title: 'Enter gcc version'}); + const gccVersion = semver.coerce(gccVersionStr) ?? undefined; const sysGen = await Dialogs.getQuickPickSingleValue([ { value: 'SGC', label: 'SGC' }, { value: 'SG3', label: 'SG3' }, @@ -326,32 +326,36 @@ async function testGcc(context: vscode.ExtensionContext): Promise { { value: 'UNKNOWN', label: 'UNKNOWN' }, ], { title: 'Select architecture' }); const strict = await Dialogs.yesNoDialog('Strict search?'); - const matchingTarget = selectedGcc?.getTarget(sysGen, arch, strict); - logger.info('Matching target:', { result: matchingTarget }); + const matchingExe = gccInstall.getExecutable(gccVersion, sysGen, arch, strict); + logger.info('Matching gcc exe:', { result: matchingExe }); } while (await Dialogs.yesNoDialog('Try again?')); + // Test mingw gcc + const mingwBase = vscode.Uri.file('C:\\msys64\\mingw64'); + const mingwGcc = await GccInstallation.createFromDir(mingwBase); + logger.info('mingw64 gcc', { gcc: mingwGcc });//TODO Gives 64.0.0... logHeader('Test gcc end'); } async function testPvi(context: vscode.ExtensionContext): Promise { logHeader('Test PVI start'); + // Update PVI + const update = await Dialogs.yesNoDialog('Update PVI versions?'); + if (update) { + const allVersions = await Environment.pvi.updateVersions(); + logger.info('PVI versions found:', { versions: allVersions }); + } // Test queries for specific PVI versions - const highestPvi = await Environment.getPviVersion(); + const highestPvi = await Environment.pvi.getVersion(); logger.info('highest PVI', { pvi: highestPvi }); - const pviV48 = await Environment.getPviVersion('4.8'); + const pviV48 = await Environment.pvi.getVersion('4.8'); logger.info('PVI V4.8 not strict', { pvi: pviV48 }); - const pviV48Strict = await Environment.getPviVersion('4.8', true); + const pviV48Strict = await Environment.pvi.getVersion('4.8', true); logger.info('PVI V4.8 strict', { pvi: pviV48Strict }); - const pviV46 = await Environment.getPviVersion('4.6'); + const pviV46 = await Environment.pvi.getVersion('4.6'); logger.info('PVI V4.6 not strict', { pvi: pviV46 }); - const pviV46Strict = await Environment.getPviVersion('4.6', true); + const pviV46Strict = await Environment.pvi.getVersion('4.6', true); logger.info('PVI V4.6 strict', { pvi: pviV46Strict }); - // Update PVI - const update = await Dialogs.yesNoDialog('Update PVI versions?'); - if (update) { - const allVersions = await Environment.updatePviVersions(); - logger.info('PVI versions found:', { versions: allVersions }); - } logHeader('Test PVI end'); } diff --git a/src/Tools/ChildProcess.ts b/src/Tools/ChildProcess.ts new file mode 100644 index 0000000..53e1ce5 --- /dev/null +++ b/src/Tools/ChildProcess.ts @@ -0,0 +1,61 @@ +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; + + +export interface ExecuteResult { + exitCode: number; + stdout: { + string: string, + chunks: any[], + stringChunks: string[], + }, + stderr: { + string: string, + chunks: any[], + stringChunks: string[], + } +} + + +/** + * Spawn process async. + * Can be useful to spawn multiple processes in parallel and afterwards await all (Promise.all()), or do other stuff during process execution. + * For single runs, the original spawnSync has slightly better performance. + * @param executable + * @param args + * @returns + */ +export async function spawnAsync(executable: vscode.Uri, ...args: string[]): Promise { + //https://stackoverflow.com/questions/56460290/read-everything-from-child-process-spawns-stderr-in-nodejs + //https://stackoverflow.com/a/58571306/6279206 + const child = childProcess.spawn(executable.fsPath, args); + const dataChunks = []; + //child.stdout.on('data', (chunk) => (dataChunks.push(chunk))); + for await (const chunk of child.stdout) { + dataChunks.push(chunk); + } + const errorChunks = []; + for await (const chunk of child.stderr) { + errorChunks.push(chunk); + } + const exitCode = await new Promise((resolve, reject) => { + child.on('close', (code) => { + resolve(code ?? 0); + }); + }); + const result: ExecuteResult = { + exitCode: exitCode, + stdout: { + chunks: dataChunks, + stringChunks: dataChunks.map((chunk) => String(chunk)), + string: dataChunks.join(''), + }, + stderr: { + chunks: errorChunks, + stringChunks: errorChunks.map((chunk) => String(chunk)), + string: errorChunks.join(''), + }, + }; + return result; + +} \ No newline at end of file diff --git a/src/Environment/SemVerTools.ts b/src/Tools/SemVer.ts similarity index 88% rename from src/Environment/SemVerTools.ts rename to src/Tools/SemVer.ts index b1440e4..140f982 100644 --- a/src/Environment/SemVerTools.ts +++ b/src/Tools/SemVer.ts @@ -1,5 +1,3 @@ -//TODO rename files to something... - import { coerce, compare, satisfies, SemVer } from 'semver'; import { logger } from '../BrLog'; @@ -8,7 +6,6 @@ export interface HasVersion { version: SemVer } - /** * Get a specific version object. If used in non strict mode, the highest available version will be returned. * @param versionRequest The requested version which should be prefered. Can be set to `undefined` if any version is ok @@ -42,8 +39,11 @@ export function requestVersion(source: T[], requested?: Se } } - -function highestVersion(source: T[]): T | undefined { +/** + * Get the object of a collection with the highest version + * @returns The object with the highest version in the collection, or `undefined` if the collection is empty + */ +export function highestVersion(source: T[]): T | undefined { // direct return if no versions available if (source.length <= 0) { return undefined; diff --git a/src/Tools/UriTools.ts b/src/Tools/UriTools.ts index 458c10b..065395d 100644 --- a/src/Tools/UriTools.ts +++ b/src/Tools/UriTools.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import {posix} from 'path'; // always use posix style path for vscode.Uri.path: https://github.com/microsoft/vscode-extension-samples/blob/master/fsconsumer-sample/README.md +import { isString } from './TypeGuards'; //#region implementations of path.posix for vscode.Uri // see also https://nodejs.org/docs/latest/api/path.html @@ -38,6 +39,8 @@ export function pathDirname(uri: vscode.Uri): vscode.Uri { */ export function pathJoin(baseUri: vscode.Uri, ...append: string[]): vscode.Uri { //TODO obsolete? -> vscode.Uri.joinPath + //TODO no! go back to uriTools.pathJoin for uniform code structure. vscode api still lacks others, e.g. relative, resolve... + // behaviour is same as vscode api const basePath = baseUri.path; const joinedPath = posix.join(basePath, ...append); const joinedUri = baseUri.with({path: joinedPath}); @@ -196,45 +199,79 @@ export async function isDirectory(uri: vscode.Uri): Promise { /** * Lists the names of all subdirectories within a base URI. * @param baseUri The base for the list + * @param filter An optional filter for the directory name. */ -export async function listSubDirectoryNames(baseUri: vscode.Uri): Promise { - return await listSubsOfType(baseUri, vscode.FileType.Directory); +export async function listSubDirectoryNames(baseUri: vscode.Uri, filter?: string | RegExp): Promise { + return await listSubsOfType(baseUri, vscode.FileType.Directory, filter); } /** * Lists the full URIs of all subdirectories within a base URI. * @param baseUri The base for the list. + * @param filter An optional filter for the directory name. */ -export async function listSubDirectories(baseUri: vscode.Uri) { - const dirNames = await listSubDirectoryNames(baseUri); +export async function listSubDirectories(baseUri: vscode.Uri, filter?: string | RegExp) { + const dirNames = await listSubDirectoryNames(baseUri, filter); return dirNames.map((name) => pathJoin(baseUri, name)); } +/** + * Lists the names of all the files within a base URI. + * @param baseUri The base for the list. + * @param filter An optional filter for the file name. + */ +export async function listSubFileNames(baseUri: vscode.Uri, filter?: string | RegExp): Promise { + return await listSubsOfType(baseUri, vscode.FileType.File, filter); +} + + /** * Lists the full URIs of all the files within a base URI. * @param baseUri The base for the list. + * @param filter An optional filter for the file name. */ -export async function listSubFiles(baseUri: vscode.Uri): Promise { - const fileNames = await listSubsOfType(baseUri, vscode.FileType.File); +export async function listSubFiles(baseUri: vscode.Uri, filter?: string | RegExp): Promise { + const fileNames = await listSubFileNames(baseUri, filter); return fileNames.map((name) => pathJoin(baseUri, name)); } +/** + * Lists the full URIs of all subdirectories within multiple base URIs as a flat array. + * This can be helpful e.g. for a breadth first tree search + * @param baseUris The base URIs for the list. + * @returns A flat array containing the URIs to the sub directories of all base URIs + */ +async function listAllSubDirectories(...baseUris: vscode.Uri[]): Promise { + const result: vscode.Uri[] = []; + for (const uri of baseUris) { + const subs = await listSubDirectories(uri); + result.push(...subs); + } + return result; +} + /** * Lists the names of all sub filesystem objects of a specified type within a base URI. * @param baseUri The base URI to search in. * @param fileType The file type to search for. + * @param filter An optional filter for the sub element name. */ -async function listSubsOfType(baseUri: vscode.Uri, fileType: vscode.FileType): Promise { +async function listSubsOfType(baseUri: vscode.Uri, fileType: vscode.FileType, filter?: string | RegExp): Promise { const subs = await vscode.workspace.fs.readDirectory(baseUri); const subsOfType = subs.filter((sub) => sub[1] === fileType); const subNames = subsOfType.map((sub) => sub[0]); - return subNames; + if (filter === undefined) { + return subNames; + } else if (isString(filter)) { + return subNames.filter((name) => name === filter); + } else { + return subNames.filter((name) => filter.test(name)); + } } - /** * Creates a `vscode.RelativePattern` which matches only the specified file URI. * @param uri URI to create the pattern from @@ -243,4 +280,38 @@ export function uriToSingleFilePattern(uri: vscode.Uri): vscode.RelativePattern const fileName = pathBasename(uri); const dirName = pathDirname(uri).fsPath; return {base: dirName, pattern: fileName}; +} + +/** + * Recursive search for a directory. A breadth first search is used and the first match will be returned. + * This function can have better performance than `vscode.workspace.findFiles()` in some cases. + * @param rootUri The starting point for the search + * @param filter The used filter for the search. Use string for an exact match, or a RegExp for a pattern match + * @param depth The maximum level of subdirectories in which is searched. Depth 0 means only direct children of `rootUri` will be searched. + * @returns The first found directory which matches the criteria, or undefined if no such was found + */ +export async function findDirectory(rootUri: vscode.Uri, depth: number, filter: string | RegExp): Promise { + // prepare result and current search level + let result: vscode.Uri | undefined = undefined; + let actDepthUris: vscode.Uri[] = [rootUri]; + // search loop + for (let actDepth = 0; actDepth <= depth; actDepth++) { + // get all subdirectories of the current depth + const subDirs = await listAllSubDirectories(...actDepthUris); + // find match + let match = undefined; + if (isString(filter)) { + match = subDirs.find((dir) => pathBasename(dir) === filter); + } else { + match = subDirs.find((dir) => filter.test(pathBasename(dir))); + } + // finish on first match + if (match !== undefined) { + result = match; + break; + } + // set new starting point for next iteration + actDepthUris = subDirs; + } + return result; } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index cabf8bd..dc8c6a5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,7 +14,6 @@ import {getWorkspaceProjects, registerProjectWorkspace} from './BRAsProjectWorks import { notifications } from './UI/Notifications'; import { extensionState } from './BrExtensionState'; import { extensionConfiguration } from './BRConfiguration'; -import { getAvailableAutomationStudioVersions } from './Environment/BREnvironment'; import { statusBar } from './UI/StatusBar'; import { Environment } from './Environment/Environment'; @@ -36,9 +35,9 @@ export async function activate(context: vscode.ExtensionContext) { registerBuildTaskProviders(context); registerTransferTaskProviders(context); // get promises for long running activation events and add to status bar - const waitAsVersion = getAvailableAutomationStudioVersions(); + const waitAsVersion = Environment.automationStudio.getVersions(); statusBar.addBusyItem(waitAsVersion, 'Searching for installed AS versions'); - const waitPviVersions = Environment.getPviVersions(); + const waitPviVersions = Environment.pvi.getVersions(); statusBar.addBusyItem(waitPviVersions, 'Searching for installed PVI versions'); const waitWorkspaceProjects = getWorkspaceProjects(); statusBar.addBusyItem(waitWorkspaceProjects, 'Parsing AS projects in workspace');