diff options
| author | Stijn Kuipers <stijnkuipers@gmail.com> | 2023-06-29 16:26:07 +0200 |
|---|---|---|
| committer | Stijn Kuipers <stijnkuipers@gmail.com> | 2023-06-29 16:26:07 +0200 |
| commit | fb5a321dd7c2848128b04b306f3e1e59c87a3f70 (patch) | |
| tree | a8ef6273f9f331ebb1971a9baf20a8c897955612 /sw/ideScripts | |
| parent | bae7568fd4dd0676b370be8548c7ec95d6521ba1 (diff) | |
| download | plinky-fb5a321dd7c2848128b04b306f3e1e59c87a3f70.tar.gz | |
Initial Filedump
Tadaaa!!
Diffstat (limited to 'sw/ideScripts')
| -rwxr-xr-x | sw/ideScripts/importKeilProject.py | 670 | ||||
| -rwxr-xr-x | sw/ideScripts/templateStrings.py | 204 | ||||
| -rwxr-xr-x | sw/ideScripts/update.py | 103 | ||||
| -rwxr-xr-x | sw/ideScripts/updateBuildData.py | 371 | ||||
| -rwxr-xr-x | sw/ideScripts/updateLaunchConfig.py | 198 | ||||
| -rwxr-xr-x | sw/ideScripts/updateMakefile.py | 522 | ||||
| -rwxr-xr-x | sw/ideScripts/updatePaths.py | 273 | ||||
| -rwxr-xr-x | sw/ideScripts/updateTasks.py | 564 | ||||
| -rwxr-xr-x | sw/ideScripts/updateWorkspaceFile.py | 105 | ||||
| -rwxr-xr-x | sw/ideScripts/updateWorkspaceSources.py | 222 | ||||
| -rwxr-xr-x | sw/ideScripts/utilities.py | 614 |
11 files changed, 3846 insertions, 0 deletions
diff --git a/sw/ideScripts/importKeilProject.py b/sw/ideScripts/importKeilProject.py new file mode 100755 index 0000000..38dcdaa --- /dev/null +++ b/sw/ideScripts/importKeilProject.py @@ -0,0 +1,670 @@ +''' +This file gets data from Keil project and creates: + - base Makefile which can be used with VS Code STM32 IDE ideScripts + - VS Code workspace +''' +import copy +import json +import os +import shutil +import subprocess +import sys +from xml.dom import minidom + +import templateStrings as tmpStr +import updateMakefile as mkf +import utilities as utils +from updateMakefile import MakefileStrings as mkfStr + +__version__ = '1.0' + + +class Paths(): + def __init__(self): + self.rootFolder = None # path where ideScripts folder is placed + + self.cubeMxExe = None # path to STM32CubeMX executable + self.tmpCubeMxFolder = None # path to temporary folder, there CubeMx performs its magic + self.tmpCubeMxScript = None # path to temporary script file for CubeMx + self.tmpMakefile = None # tempory Makefile that is later modified and copied to ideScripts root folder + self.outputMakefile = None # final clean Makefile that is later used by ideScripts + + self.keilProjectFolder = None # path to Keil project file directory + self.keilProject = None # path to Keil project file + + +class KeilProjectData: + def __init__(self): + self.projName = None + self.cpuName = None + self.stmExactCpuName = None + self.svdFile = None + + self.cDefines = [] + self.asmDefines = [] + + self.cIncludes = [] # relative paths + self.asmIncludes = [] + + self.allSources = [] + self.cSources = [] + self.asmSources = [] + + self.cCompilerSettings = [] + self.asmCompilerSettings = [] + self.linkerSettings = [] + + +def getCubeMxExePath(): + ''' + Get absolute path to STM32CubeMX.exe either by windows default associated program or user input. + ''' + cubeMxPath = utils.findExecutablePath('ioc', raiseException=False) + if cubeMxPath is not None: + if os.path.exists(cubeMxPath): + cubeMxPath = utils.pathWithForwardSlashes(cubeMxPath) + print("STM32CubeMX.exe path automatically updated.") + return cubeMxPath + else: + while cubeMxPath is None: + cubeMxPath = utils.getUserPath('STM32CubeMX.exe') + if os.path.exists(cubeMxPath): + cubeMxPath = utils.pathWithForwardSlashes(cubeMxPath) + return cubeMxPath + else: + cubeMxPath = None + + +def getKeilProjectPath(paths: Paths): + ''' + Try to find Keil *.uvprojx file. If found, this file is used as project file. + If not found, throw error. + If multiple files found, user is asked to enter specific file path. + + Return files absolute paths: *.uvprojx + ''' + KEIL_PROJECT_FILE_EXTENSION = '.uvprojx' + + # Get the list of all files in directory tree at given path + allFiles = utils.getAllFilesInFolderTree(paths.rootFolder) + keilProjectFiles = [] + for theFile in allFiles: + if theFile.find(KEIL_PROJECT_FILE_EXTENSION) != -1: + keilProjectFiles.append(theFile) + + if len(keilProjectFiles) == 0: + errorMsg = "Unable to find any Keil project files ending with " + KEIL_PROJECT_FILE_EXTENSION + ". " + errorMsg += "Is folder structure correct?\n\t" + errorMsg += "Searched files in folder tree: " + paths.rootFolder + raise Exception(errorMsg) + + elif len(keilProjectFiles) == 1: + # only one keil project file available, take this one + print("Keil project file found:", keilProjectFiles[0]) + return keilProjectFiles[0] + + else: + print("More than one Keil project files available. Select the right one.") + keilProjectPath = None + while keilProjectPath is None: + keilProjectPath = utils.getUserPath('Keil project (.uvprojx)') + if os.path.exists(keilProjectPath): + break + else: + keilProjectPath = None + + print("Keil project path updated.") + return keilProjectPath + + +def getKeilProjectData(paths: Paths) -> KeilProjectData: + ''' + Read Keil project file and return filled KeilProjectData class. + + Some blocks are placed in try...except statements - error is thrown if xml field does not contain any items. + ''' + projData = KeilProjectData() + + _, fileName = os.path.split(paths.keilProject) + projData.projName, _ = os.path.splitext(fileName) + + projFileData = minidom.parse(paths.keilProject) + projData.cpuName = projFileData.getElementsByTagName('Device')[0].firstChild.data + + svdFile = projFileData.getElementsByTagName('SFDFile')[0].firstChild.data + _, projData.svdFile = os.path.split(svdFile) + + # c stuff + _cads = projFileData.getElementsByTagName('Cads')[0] + try: # c defines + cDefines = _cads.getElementsByTagName('Define')[0].firstChild.data + projData.cDefines = utils.stringToList(cDefines, ',') + except Exception as err: + print("WARNING: unable to get C Defines: error or no items") + try: # c include folders + cIncludes = _cads.getElementsByTagName('IncludePath')[0].firstChild.data + cIncludesList = utils.stringToList(cIncludes, ';') + projData.cIncludes = _fixRelativePaths(paths, cIncludesList) + except Exception as err: + print("WARNING: unable to get C Includes (folders): error or no items") + try: # c miscelaneous controls + cMiscControls = _cads.getElementsByTagName('MiscControls')[0].firstChild.data + projData.cCompilerSettings = utils.stringToList(cMiscControls, ',') + except Exception as err: + print("WARNING: unable to get C Miscelaneous settings: error or no items") + + # asm stuff + _aads = projFileData.getElementsByTagName('Aads')[0] + try: # asm defines + asmDefines = _aads.getElementsByTagName('Define')[0].firstChild.data + projData.asmDefines = utils.stringToList(asmDefines, ',') + except Exception as err: + print("WARNING: unable to get Asm Defines: error or no items") + try: # asm include folders + asmIncludes = _aads.getElementsByTagName('IncludePath')[0].firstChild.data + asmIncludes = utils.stringToList(asmIncludes, ';') + projData.asmIncludes = _fixRelativePaths(paths, asmIncludes) + except Exception as err: + print("WARNING: unable to get Asm Includes (folders): error or no items") + try: # asm miscelaneous controls + asmMiscControls = _aads.getElementsByTagName('MiscControls')[0].firstChild.data + projData.asmCompilerSettings = utils.stringToList(asmMiscControls, ',') + except Exception as err: + print("WARNING: unable to get Asm Miscelaneous settings: error or no items") + + # get linker misc controls + _lads = projFileData.getElementsByTagName('Cads')[0] + try: # asm miscelaneous controls + linkerMiscControls = _lads.getElementsByTagName('MiscControls')[0].firstChild.data + projData.linkerSettings = utils.stringToList(linkerMiscControls, ',') + except Exception as err: + print("WARNING: unable to get Linker Miscelaneous settings: error or no items") + + # get all source files. Add only '.c' and '.s' files. Throw error on exception, this data is mandatory. + files = projFileData.getElementsByTagName('FilePath') + cSourceFiles = [] + asmSourceFiles = [] + for fileData in files: + filePathList = _fixRelativePaths(paths, [fileData.firstChild.data]) + if len(filePathList) == 1: + filePath = filePathList[0] + projData.allSources.append(filePath) + + _, extension = os.path.splitext(filePath) + if extension == '.c': + cSourceFiles.append(filePath) + elif extension == '.s': + asmSourceFiles.append(filePath) + else: + msg = "WARNING: this file is not '.c' or '.s'. Not added to project (user must handle this manually).\n" + msg += "\t" + filePath + print(msg) + else: + # missing file reported in _fixRelativePaths + msg = "WARNING: seems like none or more than one file is specified. This is not a valid Keil project syntax: " + msg += str(filePathList) + print(msg) + + projData.cSources = cSourceFiles + print("\nC source files added:\n\t" + '\n\t'.join(cSourceFiles)) + projData.asmSources = asmSourceFiles + print("\nAsm source files added:\n\t" + '\n\t'.join(asmSourceFiles) + '\n') + + return projData + + +def _fixRelativePaths(paths: Paths, relativePaths: list): + ''' + Correct relative paths according to the folder structure as it is expected. + Relative paths in Keil project file are relative to the keil file path, + while we need paths relative to root folder where 'ideScripts' is. + + Return list of a VALID relative paths paths. + ''' + keilProjectAbsPath = os.path.normpath(os.path.join(paths.rootFolder, paths.keilProject)) + + allPaths = [] + for relativePath in relativePaths: + if os.path.isabs(relativePath): + relativePath = os.path.normpath(relativePath) + relativePath = utils.pathWithForwardSlashes(relativePath) + allPaths.append(relativePath) + continue + + absolutePath = os.path.normpath(os.path.join(paths.keilProjectFolder, relativePath)) + if os.path.exists(absolutePath): + # path is valid, build correct relative path + try: + newRelativePath = os.path.relpath(absolutePath, paths.rootFolder) + newRelativePath = utils.pathWithForwardSlashes(newRelativePath) + allPaths.append(newRelativePath) + except: + absolutePath = utils.pathWithForwardSlashes(absolutePath) + allPaths.append(absolutePath) + else: + print("WARNING: unable to find file/folder:", absolutePath) + print("\tBuilt from relative path:", relativePath) + + return allPaths + + +def _getAbsolutePaths(relativePaths): + ''' + Get list of relative paths and try to build absolute paths. + If any path does not exist, print warning message. + Return list of valid absolute paths. + ''' + absolutePaths = [] + for relativePath in relativePaths: + relativePath = relativePath.strip() + relativePath = os.path.normpath(os.path.join(paths.keilProjectFolder, relativePath)) + if os.path.exists(relativePath): + relativePath = utils.pathWithForwardSlashes(relativePath) + absolutePaths.append(relativePath) + else: + print("WARNING: unable to find file/folder:", relativePath) + + return absolutePaths + + +def createMakefileTemplate(paths: Paths, keilProjData: KeilProjectData): + ''' + Create Makefile template with CubeMX. + ''' + # create script that CubeMX executes + paths.tmpCubeMxFolder = os.path.join(paths.rootFolder, tmpStr.cubeMxTmpFolderName) + paths.tmpCubeMxFolder = utils.pathWithForwardSlashes(paths.tmpCubeMxFolder) + if not os.path.exists(paths.tmpCubeMxFolder): + try: + os.mkdir(paths.tmpCubeMxFolder) + except Exception as err: + errorMsg = "Unable to create existing temporary folder:\n" + str(err) + print(errorMsg) + + # even if any error occured, try to create files anyway + _createCubeMxTmpScript(paths, keilProjData) + + # run CubeMX as subprocess with this script as a parameter + cmd = ['java', '-jar', paths.cubeMxExe, '-s', paths.tmpCubeMxScript] + if _checkCubeMxFirmwarePackage(paths, keilProjData): + cmd.append('-q') # no-gui mode + print("\tSTM32CubeMX GUI set to non-visible mode.") + else: + print("\tSTM32CubeMX GUI set to visible because of repository warning.") + + try: + print("Generating template Makefile with STM32CubeMX...") + proc = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + if proc.returncode == 0: + print("\tSTM32CubeMX project generated.") + else: + errorMsg = "CubeMx returned non-zero exit code. Something went wrong:\n" + errorMsg += str(proc.stderr) + '\n' + errorMsg += str(proc.stdout) + + utils.printAndQuit(errorMsg) + except Exception as err: + errorMsg = "Exception error while creating template Makefile with STM32CubeMX:\n" + str(err) + utils.printAndQuit(errorMsg) + + # get makefile path + allGeneratedFiles = utils.getAllFilesInFolderTree(paths.tmpCubeMxFolder) + for theFile in allGeneratedFiles: + _, fileName = os.path.split(theFile) + if fileName == 'Makefile': + paths.tmpMakefile = theFile + print("\tMakefile found: " + paths.tmpMakefile) + + _copyStartupFile(paths, keilProjData) + return + else: + errorMsg = "Unable to find template Makefile generated by STM32CubeMX. Was project really generated?" + utils.printAndQuit(errorMsg) + + +def _copyStartupFile(paths: Paths, keilProjData: KeilProjectData): + ''' + Get '*.s' startup file in the same folder as CubeMX template Makefile file and + copy it into the same location as current startup file is. + ''' + # find CubeMX temporary generated startup file + filesInMakefileDir = os.listdir(os.path.dirname(paths.tmpMakefile)) + for theFile in filesInMakefileDir: + name, ext = os.path.splitext(theFile) + if ext == '.s': + startupFile = os.path.join(os.path.dirname(paths.tmpMakefile), theFile) + newStartupFilePath = os.path.join(paths.rootFolder, theFile) + try: + shutil.copy(startupFile, newStartupFilePath) + print("Default STM32CubeMX startup file copied to:", newStartupFilePath) + + relativeStartupFilePath = os.path.relpath(newStartupFilePath, paths.rootFolder) + relativeStartupFilePath = utils.pathWithForwardSlashes(relativeStartupFilePath) + break + except Exception as err: + pass + #print("Seems like default STM32CubeMX startup file already exist:", newStartupFilePath) + + # find startup file in current keil project data and replace it with this one + if len(keilProjData.asmSources) == 1: + # no problem only one '*.s' file, assume this is the startup file + originalStartupFile = keilProjData.asmSources[0] + keilProjData.asmSources = [relativeStartupFilePath] + + msg = "Default " + originalStartupFile + " source was replaced with CubeMX one: " + relativeStartupFilePath + print(msg) + return + + else: + # more than one assembler file found, try to find file with 'startup' string or throw error + possibleStartupFiles = [] + for startupFileListIndex, asmFile in enumerate(keilProjData.asmSources): + _, fileName = os.path.split(asmFile) + if fileName.lower().find('startup') != -1: + possibleStartupFiles.append((asmFile, startupFileListIndex)) # asm file, file index in list + + if len(possibleStartupFiles) == 1: + # OK, only one file with startup string + originalStartupFile = keilProjData.asmSources[possibleStartupFiles[0][1]] + keilProjData.asmSources[possibleStartupFiles[0][1]] = relativeStartupFilePath + + msg = "WARNING: Multiple '*.s' files found. " + msg += originalStartupFile + " source file was replaced with CubeMX one: " + relativeStartupFilePath + print(msg) + + else: + errorMsg = "Multiple '*.s' source files listed. Can't determine startup file (searched with 'startup' string)." + errorMsg += "\n\tAsm files: " + str(keilProjData.asmSources) + utils.printAndQuit(errorMsg) + + +def cleanTempMakefile(paths: Paths): + ''' + Clean default generated Makefile data (sources, includes, names, ...). + ''' + makefile = mkf.Makefile() + + try: + with open(paths.tmpMakefile, 'r') as makefileHandler: + data = makefileHandler.readlines() + + # do not change project name intentionally + # data = makefile.searchAndCleanData(data, makefile.mkfStr.projectName) + + data = makefile.searchAndCleanData(data, makefile.mkfStr.cSources) + data = makefile.searchAndCleanData(data, makefile.mkfStr.asmSources) + + data = makefile.searchAndCleanData(data, makefile.mkfStr.cDefines) + data = makefile.searchAndCleanData(data, makefile.mkfStr.asmDefines) + + data = makefile.searchAndCleanData(data, makefile.mkfStr.cIncludes) + data = makefile.searchAndCleanData(data, makefile.mkfStr.asmIncludes) + + data = makefile.searchAndCleanData(data, makefile.mkfStr.cIncludes) + + print("Makefile template prepared.") + return data + + except Exception as err: + errorMsg = "Exception during Makefile template preparation:\n" + str(err) + utils.printAndQuit(errorMsg) + + +def createNewMakefile(paths: Paths, keilProjData: KeilProjectData, newMakefileData): + ''' + Fill and write new makefile with data from Keil project. + ''' + makefile = mkf.Makefile() + try: + # sources + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.cSources, keilProjData.cSources) + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.asmSources, keilProjData.asmSources) + + # includes + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.cIncludes, keilProjData.cIncludes, preappend='-I') + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.asmIncludes, keilProjData.asmIncludes, preappend='-I') + + # defines + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.cDefines, keilProjData.cDefines, preappend='-D') + data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.asmDefines, keilProjData.asmDefines, preappend='-D') + + # compiler flags + # TODO should import? + # data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.cFlags, keilProjData.cCompilerSettings) + # data = makefile.searchAndAppend(newMakefileData, makefile.mkfStr.asmFlags, keilProjData.asmCompilerSettings) + if keilProjData.cCompilerSettings: + print("WARNING: C compiler settings not imported (user must handle manualy):", str(keilProjData.cCompilerSettings)) + if keilProjData.asmCompilerSettings: + print("WARNING: Asm compiler settings not imported (user must handle manualy):", str(keilProjData.asmCompilerSettings)) + if keilProjData.linkerSettings: + print("WARNING: Linker settings not imported (user must handle manualy):", str(keilProjData.linkerSettings)) + + with open(paths.outputMakefile, 'w+') as newMakefileHandler: + newMakefileHandler.writelines(data) + + print("Makefile created in: " + paths.outputMakefile) + + except Exception as err: + errorMsg = "Exception during creating new Makefile:\n" + str(err) + utils.printAndQuit(errorMsg) + + +def _getCPUName(paths: Paths, keilProjData: KeilProjectData): + ''' + Try to get correct CPU family name from Keil project device tag. + + STM32 CPU name, passed to CubeMX is not the same as Keil device name. + CubeMX device firmware pack must be installed so CubeMX is able to generate template Makefile. + ''' + cubeMxMcuFolderPath = os.path.join(os.path.dirname(paths.cubeMxExe), 'db', 'mcu') + allFamiliesFilePath = os.path.join(cubeMxMcuFolderPath, 'families.xml') + + allFiles = os.listdir(cubeMxMcuFolderPath) + for theFile in allFiles: + theFilePath = os.path.join(cubeMxMcuFolderPath, theFile) + if os.path.isfile(theFilePath): + if theFile.find(keilProjData.cpuName) != -1: + fileName, ext = os.path.splitext(theFile) + return fileName + + errorMsg = "Unable to find matching STM32 CPU name for Keil project device: " + keilProjData.cpuName + utils.printAndQuit(errorMsg) + + stm32McuData = minidom.parse(allFamiliesFilePath) + + # build possible device family name search strings. Search order is important + allMcuData = stm32McuData.getElementsByTagName('Mcu') + minimumSearchStringLenght = len('STM32xx') + numOfStrippedCharacters = len(keilProjData.cpuName) - minimumSearchStringLenght + possibleDeviceSearchString = [] + possibleDeviceSearchString.append(keilProjData.cpuName) + for charIndexFromBack in range(-1, -numOfStrippedCharacters-1, -1): + possibleDeviceSearchString.append(keilProjData.cpuName[:charIndexFromBack]) + + # find possible mcu ref names + allPossibleMcu = [None] * len(allMcuData) + subFamilyMcuData = None + for thisDeviceSearchString in possibleDeviceSearchString: + thisSearchStringPossibleMcu = [] + for mcuData in allMcuData: + thisMcuName = mcuData.attributes._attrs['RPN'].value + if thisMcuName.find(thisDeviceSearchString) != -1: + thisSearchStringPossibleMcu.append(thisMcuName) + + if thisSearchStringPossibleMcu: + if len(thisSearchStringPossibleMcu) <= len(allPossibleMcu): + allPossibleMcu = copy.copy(thisSearchStringPossibleMcu) + break + + if not allPossibleMcu: + errorMsg = "Unable to find any (even partly) matching device name:" + keilProjData.cpuName + utils.printAndQuit(errorMsg) + allPossibleMcu = list(set(allPossibleMcu)) # remove cuplicates + + # all possible MCUs are listed, ask user to select correct one + if len(allPossibleMcu) == 1: + keilProjData.stmExactCpuName = allPossibleMcu[0] + return allPossibleMcu[0] + else: + msg = "\n\n??? Please select exact CPU..." + for mcuIndex, mcu in enumerate(allPossibleMcu): + msg += '\n\t' + str(mcuIndex) + ': ' + mcu + limits = list(range(0, len(allPossibleMcu))) + askMsg = "Type number (0 - " + str(len(allPossibleMcu)) + ") and press Enter:" + print(msg + '\n' + askMsg) + + while(True): + userAnswer = input() + try: + userNumber = int(userAnswer) + except: + print(askMsg) + continue + if userNumber not in limits: + print(askMsg) + else: + print("--> " + allPossibleMcu[userNumber] + " selected.") + keilProjData.stmExactCpuName = allPossibleMcu[userNumber] + return allPossibleMcu[userNumber] + + +def _checkCubeMxFirmwarePackage(paths: Paths, keilProjData: KeilProjectData): + ''' + Check if this cpu family firmware package can be found inside CubeMX local repository. + Returns True if found, False otherwise. + ''' + errorMsg = '' + try: + # get all files inside local repository + appDataFolder = os.path.expandvars(os.environ['APPDATA']) + stm32CubeRepositoryFolder = os.path.join(appDataFolder, '..', '..', 'STM32Cube', 'Repository') + stm32CubeRepositoryFolder = os.path.normpath(stm32CubeRepositoryFolder) + + # get start of package name + cpuFamilyName = keilProjData.cpuName[len('STM32'):len('STM32xx')] + fwPackageName = 'STM32Cube_FW_' + cpuFamilyName + + # search if any folder name contains fwPackageName + for item in os.listdir(stm32CubeRepositoryFolder): + if os.path.isdir(os.path.join(stm32CubeRepositoryFolder, item)): + if item.find(fwPackageName) != -1: + print("Seems like STM32CubeMX " + fwPackageName + "* package is installed.") + return True + + except Exception as err: + errorMsg = "\nException:\n" + str(err) + + msg = "WARNING: unable to check if STM32Cube " + keilProjData.cpuName + " firmware package is installed." + msg += errorMsg + print(msg) + return False + + +def _createCubeMxTmpScript(paths: Paths, keilProjData: KeilProjectData): + ''' + Create tempory script for CubeMX Makefile generation. + Raises exception on error. + ''' + paths.tmpCubeMxScript = os.path.join(paths.tmpCubeMxFolder, tmpStr.cubeMxTmpFileName) + paths.tmpCubeMxScript = utils.pathWithForwardSlashes(paths.tmpCubeMxScript) + + dataToWrite = "// Temporary script for generating Base Makefile with STM32CubeMX.\n" + dataToWrite += "load " + _getCPUName(paths, keilProjData) + "\n" + dataToWrite += "project name " + keilProjData.projName + "\n" + dataToWrite += "project toolchain Makefile\n" + dataToWrite += "project path \"" + paths.tmpCubeMxFolder + "\"\n" + dataToWrite += "project generate\n" + dataToWrite += "exit" + + with open(paths.tmpCubeMxScript, 'w+') as scriptHandler: + scriptHandler.write(dataToWrite) + + print("Temporary STM32CubeMX script created.") + + +def deleteTemporaryFiles(paths: Paths): + ''' + Delete (clean) CubeMX temporary files. + ''' + try: + shutil.rmtree(paths.tmpCubeMxFolder) + print("STM32CubeMX temporary files deleted.") + except Exception as err: + errorMsg = "Exception while deleting STM32CubeMX temporary files:\n" + str(err) + raise Exception(err) + + +def _separateAbsoluteAndRelativePaths(pathsListToSeparate: list): + ''' + This function splits pathsListToSeparate to relative and absolute paths. + Returns two lists: absolutePaths, relativePaths + ''' + absPaths = [] + relPaths = [] + for path in pathsListToSeparate: + if os.path.isabs(path): + absPaths.append(path) + else: + relPaths.append(path) + + return absPaths, relPaths + + +def createVSCodeWorkspace(paths: Paths, keilProjData: KeilProjectData): + ''' + Create VS Code workspace so user can easily run 'update.py' from ideScripts. + ''' + # add non-relative source folders to VS Code workspace folders. + allPaths = [] + # TODO are c and asm includes folders needed in Code workspace? + # allPaths.extend(keilProjData.cIncludes) + # allPaths.extend(keilProjData.asmIncludes) + cSourcesFolders = [os.path.dirname(source) for source in keilProjData.cSources] + allPaths.extend(list(set(cSourcesFolders))) + asmSourcesFolders = [os.path.dirname(source) for source in keilProjData.asmSources] + allPaths.extend(list(set(asmSourcesFolders))) + absPaths, relPaths = _separateAbsoluteAndRelativePaths(allPaths) + + dataToWrite = """ + { + "folders": [ + { + "path": "." + } + """ + for absPath in absPaths: + addToFoldersStr = ",{ \"path\": \"" + absPath + "\"}" + dataToWrite += addToFoldersStr + dataToWrite += "]" + dataToWrite += ",\"settings\": { }" + dataToWrite += "}" + data = json.loads(dataToWrite) + data = json.dumps(data, indent=4, sort_keys=False) + + codeWorkspaceFileName = keilProjData.projName + '.code-workspace' + codeWorkspaceFilePath = os.path.join(paths.rootFolder, codeWorkspaceFileName) + with open(codeWorkspaceFilePath, 'w+') as fileHandler: + fileHandler.write(data) + + print("VS Code workspace file created:", codeWorkspaceFilePath) + + +if __name__ == "__main__": + paths = Paths() + thisFileAbsPath = os.path.abspath(sys.argv[0]) + paths.rootFolder = os.path.dirname(os.path.dirname(thisFileAbsPath)) + paths.rootFolder = utils.pathWithForwardSlashes(paths.rootFolder) + + paths.cubeMxExe = getCubeMxExePath() + paths.keilProject = getKeilProjectPath(paths) + paths.keilProjectFolder = utils.pathWithForwardSlashes(os.path.dirname(paths.keilProject)) + paths.outputMakefile = utils.pathWithForwardSlashes(os.path.join(paths.rootFolder, 'Makefile')) + + keilProjData = getKeilProjectData(paths) + + createMakefileTemplate(paths, keilProjData) + cleanMakefileData = cleanTempMakefile(paths) + createNewMakefile(paths, keilProjData, cleanMakefileData) + deleteTemporaryFiles(paths) + + createVSCodeWorkspace(paths, keilProjData) diff --git a/sw/ideScripts/templateStrings.py b/sw/ideScripts/templateStrings.py new file mode 100755 index 0000000..623ab1c --- /dev/null +++ b/sw/ideScripts/templateStrings.py @@ -0,0 +1,204 @@ +""" +Template scripts for generating workspace files: + - c_cpp_properties.json + - tasks.json + - makefile strings/functions + - buildData.json +""" +import os + +launchName_Debug = "Cortex debug" +launchName_Python = "Debug current Python file" + +taskName_build = "Build project" +taskName_compile = "Compile current file" +taskName_clean = "Delete build folder" + +taskName_CPU_buildDownloadRun = "CPU: Build, Download and run" +taskName_CPU_downloadRun = "CPU: Download and run" +taskName_CPU_resetRun = "CPU: Reset and run" +taskName_CPU_halt = "CPU: Halt" +taskName_CPU_run = "CPU: Run" + +taskName_Python = "Run Python file" +taskName_OpenCubeMX = "Open CubeMX project" +taskName_updateWorkspace = "Update workspace" + +######################################################################################################### +c_cpp_template = """{ + "env" : { + "____________________USER_FIELDS_CAN_BE_MODIFIED____________________": "", + "user_cSources": [], + "user_asmSources": [], + "user_ldSources": [], + "user_cIncludes": [], + "user_asmIncludes": [], + "user_ldIncludes": [], + "user_cDefines": [], + "user_asmDefines": [], + "user_cFlags": [], + "user_asmFlags": [], + "user_ldFlags": [], + + "____________________DO_NOT_MODIFY_FIELDS_BELOW____________________": "", + "cubemx_sourceFiles": [], + "cubemx_includes": [], + "cubemx_defines": [], + "gccExePath": "", + "gccIncludePath": "" + }, + "configurations": [ + { + "name": "devTestBoard name of the project?", + "intelliSenseMode": "msvc-x64", + "includePath": [ + "${workspaceFolder}", + "${cubemx_includes}", + "${gccIncludePath}", + "${user_cIncludes}", + "${user_asmIncludes}", + "${user_ldIncludes}" + ], + "browse": { + "path": [ + "${workspaceFolder}", + "${cubemx_includes}", + "${gccIncludePath}", + "${user_cIncludes}", + "${user_asmIncludes}", + "${user_ldIncludes}" + ], + "limitSymbolsToIncludedHeaders": true + }, + "defines": [ + "${cubemx_defines}", + "${user_cDefines}", + "${user_asmDefines}" + ], + "forcedInclude": [ + ], + "compilerPath": "${gccExePath}", + "cStandard": "c11", + "cppStandard": "c++17" + } + ], + "version": 4 +} +""" +######################################################################################################### +versionString = "Version ***" +lastRunString = "Last run: ***" + +######################################################################################################### +makefileHeader = ('#' * 100) + "\n" +makefileHeader += "# Makefile generated by updateMakefile.py\n" +makefileHeader += "# " + versionString + " \n" +makefileHeader += "# " + lastRunString + " \n" +makefileHeader += ('#' * 100) + "\n" + +######################################################################################################### +printMakefileVariableFunction = "print-%:" +printMakefileDefaultString = "VARIABLE=" +printMakefileVariable = "#######################################\n" +printMakefileVariable += "# Print makefile variables\n" +printMakefileVariable += "#######################################\n" +printMakefileVariable += printMakefileVariableFunction + "\n" +printMakefileVariable += "\t@echo " + printMakefileDefaultString + "$($*)\n" + +######################################################################################################### +cleanFunctionNameSearchString = "clean:" +cleanBuildDirFunctionName = "clean-build-dir" +cleanBuildDirFunction = "#######################################\n" +cleanBuildDirFunction += "# Clean build directory content \n" +cleanBuildDirFunction += "#######################################\n" +cleanBuildDirFunction += cleanBuildDirFunctionName + ":\n" +cleanBuildDirFunction += "\t@echo Build folder: '$(BUILD_DIR)' clean request (files with spaces and folders will not be removed):\n" +cleanBuildDirFunction += "\t@$(foreach file, $(wildcard $(BUILD_DIR)/*), rm -f $(file))\n" +cleanBuildDirFunction += "\t@echo OK.\n" + +######################################################################################################### +taskTemplate = """{ + "label": "Update workspace", + "type": "shell", + "command": "python", + "args": [ + "${workspaceFolder}\\\\test.py" + ], + "group": "none", + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": { + } + } +""" + +tasksFileTemplate = """{ + "version": "2.0.0", + "tasks": [""" +tasksFileTemplate += """ + ] +} +""" + +######################################################################################################### +# buildData.json has template with all keys listed, since it is needed for sorting purposes. There +# might be a better way to handle sorting (TODO) +buildDataTemplate = """{ + "ABOUT1": "This file holds combined user and CubeMX generated Makefile workspace dependecies.", + "ABOUT2": "User should not edit this fields, instead it should edit 'c_cpp_properties.json'", + "ABOUT3": "This file is regenerated on 'Update workspace' task.", + "VERSION": "", + "LAST_RUN": "", + "cSources": [], + "asmSources": [], + "ldSources": [], + "cIncludes": [], + "asmIncludes": [], + "ldIncludes": [], + "cDefines": [], + "asmDefines": [], + "cFlags" : [], + "asmFlags" : [], + "ldFlags" : [], + "buildDir": "", + "targetExecutablePath": "", + "cubeMxProjectPath": "", + "openOcdConfig": [], + "stm32SvdPath": "", + "ABOUT4": "---- Paths below are fetched from user-specific 'toolsPaths.json'. ----", + "gccExePath": "", + "gccInludePath": "", + "buildToolsPath": "", + "pythonExec": "", + "openOcdPath": "", + "openOcdInterfacePath": "" +} +""" + +######################################################################################################### +toolsPathsTemplate = """{ + "ABOUT1": "This file store common tools paths, shared by all VS Code ideScripts-based projects.", + "ABOUT2": "Delete/correct this file if paths/folder structure change on system.", + "VERSION": "", + "LAST_RUN": "" +} +""" + +######################################################################################################### +launchFileTemplate = """{ + "version": "0.2.0", + "configurations": [ + ] +} +""" + +######################################################################################################### +cubeMxTmpFolderName = '_tmpCubeMx' +cubeMxTmpFileName = 'tmpCubeMx.txt' + +######################################################################################################### +defaultVsCodeSettingsFolder_WIN = os.path.expandvars("%APPDATA%/Code/User/") +defaultVsCodeSettingsFolder_UNIX = os.path.expandvars("$HOME/.config/Code/User/") +defaultVsCodeSettingsFolder_OSX = os.path.expandvars("$HOME/Library/Application Support/Code/User/") diff --git a/sw/ideScripts/update.py b/sw/ideScripts/update.py new file mode 100755 index 0000000..5af3760 --- /dev/null +++ b/sw/ideScripts/update.py @@ -0,0 +1,103 @@ +''' +This script runs all other updateXxx.py scripts. +It should be called once CubeMX project was generated/re-generated or user settings were modified. + +- add 'print-variable' capabilities to Makefile +- update/generate 'c_cpp_properties.json' +- update/generate 'buildData.json' and 'toolsPaths.json' +- update/generate 'tasks.json' +- update/generate 'launch.json' +''' +import sys +import time +import traceback + +import updateWorkspaceFile as workspaceFile +import updateLaunchConfig as launch +import updateTasks as tasks +import updateBuildData as build +import updateMakefile as mkf +import updateWorkspaceSources as wks +import updatePaths as pth +import utilities as utils + +__version__ = utils.__version__ + +if sys.version_info[0] < 3: + raise Exception("Python 3 or later is required") + +######################################################################################################################## +if __name__ == "__main__": + startTime = time.time() + print("Update started.\n") + status = 'OK' + errorMsg = '' + try: + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + bData = build.BuildData() + cP = wks.CProperties() + makefile = mkf.Makefile() + tasks = tasks.Tasks() + launch = launch.LaunchConfigurations() + wksFile = workspaceFile.UpdateWorkspaceFile() + + # Makefile must exist + makefile.checkMakefileFile() # no point in continuing if Makefile does not exist + makefile.restoreOriginalMakefile() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + # data from original makefile + makeExePath = buildData[bData.bStr.buildToolsPath] + gccExePath = buildData[bData.bStr.gccExePath] + makefileData = makefile.getMakefileData(makeExePath, gccExePath) + + # create/update 'c_cpp_properties.json' + cP.checkCPropertiesFile() + cPropertiesData = cP.getCPropertiesData() + cPropertiesData = cP.addMakefileDataToCPropertiesFile(cPropertiesData, makefileData) + cPropertiesData = cP.addBuildDataToCPropertiesFile(cPropertiesData, buildData) + cPropertiesData = cP.addCustomDataToCPropertiesFile(cPropertiesData, makefileData, buildData) + cP.overwriteCPropertiesFile(cPropertiesData) + + # update Makefile + makefile.createNewMakefile() + makefileData = makefile.getMakefileData(makeExePath, gccExePath) # get data from new Makefile + + # update buildData.json + buildData = bData.addMakefileDataToBuildDataFile(buildData, makefileData) + buildData = bData.addCubeMxProjectPathToBuildData(buildData) + bData.overwriteBuildDataFile(buildData) + + # create build folder + buildFolderName = makefileData[mkf.MakefileStrings.buildDir] + utils.createBuildFolder(buildFolderName) + + # update tasks + tasks.checkTasksFile() + tasksData = tasks.getTasksData() + tasksData = tasks.addAllTasks(tasksData) + tasks.overwriteTasksFile(tasksData) + + # update launch configurations + launch.checkLaunchFile() + launchData = launch.getLaunchData() + launchData = launch.addAllLaunchConfigurations(launchData) + launch.overwriteLaunchFile(launchData) + + # update workspace file with "cortex-debug" specifics + wksFile.checkWorkspaceFile() + wksData = wksFile.getWorkspaceFileData() + wksData = wksFile.addBuildDataToWorkspaceFile(wksData, buildData) + wksFile.overwriteWorkspaceFile(wksData) + + except Exception as err: + status = "ERROR" + errorMsg = "Unexpected error occured during 'Update' procedure. Exception:\n" + traceback.format_exc() + + overallTime = int(time.time() - startTime) + msg = "\n" + status + " (" + str(overallTime) + " seconds).\n" + errorMsg + print(msg) diff --git a/sw/ideScripts/updateBuildData.py b/sw/ideScripts/updateBuildData.py new file mode 100755 index 0000000..3e84356 --- /dev/null +++ b/sw/ideScripts/updateBuildData.py @@ -0,0 +1,371 @@ +''' +Update/generate 'buildData.json' file in '.vscode' subfolder from new Makefile. +This file also handles 'toolsPaths.json' file. +New Makefile is not updated by this script - it is updated with 'updateMakefile.py' or 'updateWorkspaceSources.py' +''' +import os +import json +import datetime + +import utilities as utils +import templateStrings as tmpStr + +import updatePaths as pth +import updateMakefile as mkf +import updateWorkspaceSources as wks + +__version__ = utils.__version__ + + +class BuildDataStrings(): + # project sources, includes, defines, .... + cSources = 'cSources' + asmSources = 'asmSources' + ldSources = 'ldSources' + + cIncludes = 'cIncludes' + asmIncludes = 'asmIncludes' + ldIncludes = 'ldIncludes' + + cDefines = 'cDefines' + asmDefines = 'asmDefines' + + cFlags = 'cFlags' + asmFlags = 'asmFlags' + ldFlags = 'ldFlags' + + buildDirPath = 'buildDir' + + # build/interface tools paths, configuration files + gccInludePath = 'gccInludePath' # GCC standard libraries root folder path + gccExePath = 'gccExePath' # path to 'gcc.exe' + + buildToolsPath = 'buildToolsPath' # path to 'make.exe' + targetExecutablePath = 'targetExecutablePath' # path to downloadable '*.elf' file + + pythonExec = 'pythonExec' + + openOcdPath = 'openOcdPath' # path to 'openocd.exe' + openOcdInterfacePath = "openOcdInterfacePath" # path to OpenOCD interface cofniguration file (currently 'stlink.cfg') + + openOcdConfig = 'openOcdConfig' # path to target '*.cfg' file + stm32SvdPath = 'stm32SvdPath' # path to target '*.svd' file + + cubeMxProjectPath = 'cubeMxProjectPath' + + # list of paths that are automatically built (default, system or once their 'parent' paths are valid) + derivedPaths = [ + pythonExec, + gccInludePath + ] + + # list of target-specific configuration paths that must exist in 'buildData.json' + targetConfigurationPaths = [ + openOcdConfig, + stm32SvdPath + ] + + # list of paths that can be cached in 'toolsPaths.json' + toolsPaths = [ + gccExePath, + buildToolsPath, + pythonExec, + openOcdPath, + openOcdInterfacePath + ] + + +class BuildData(): + def __init__(self): + self.mkfStr = mkf.MakefileStrings() + self.cPStr = wks.CPropertiesStrings() + self.bStr = BuildDataStrings() + + def prepareBuildData(self, request=False): + ''' + This function is used in all 'update*.py' scripts and makes sure, that 'toolsPaths.json' and 'buildData.json' with a + valid tools/target cofniguration paths exist. Invalid paths are updated (requested from the user). + Returns available, valid build data. + + Note: tools paths listed in 'BuildDataStrings.toolsPaths' are stored in system local 'toolsPaths.json' file, and are + copied (overwritten) to 'buildData.json' on first 'Update' task run. This makes it possible for multiple code contributors. + ''' + paths = pth.UpdatePaths() + + self.checkBuildDataFile() + buildData = self.getBuildData() + + if self.checkToolsPathFile(): # a valid toolsPaths.json exists + toolsPathsData = self.getToolsPathsData() + + else: + # no valid data from 'toolsPaths.json' file + # try to get data from current 'buildData.json' - backward compatibility for paths that already exist in 'buildData.json' + toolsPathsData = json.loads(tmpStr.toolsPathsTemplate) + for path in self.bStr.toolsPaths: + if path in buildData: + if utils.pathExists(buildData[path]): + toolsPathsData[path] = buildData[path] + + # update/overwrite tools paths file. Don't mind if paths are already valid. + toolsPathsData = paths.verifyToolsPaths(toolsPathsData, request) + self.createUserToolsFile(toolsPathsData) + + buildData = self.addToolsPathsToBuildData(buildData, toolsPathsData) + + templateBuildData = json.loads(tmpStr.buildDataTemplate) + buildData = utils.mergeCurrentDataWithTemplate(buildData, templateBuildData) + + buildData = paths.verifyTargetConfigurationPaths(buildData, request) + buildData = paths.copyTargetConfigurationFiles(buildData) + + return buildData + + def checkToolsPathFile(self): + ''' + Returns True if 'toolsPaths.json' file exists and is a valid JSON file. + If it is not a valid JSON, delete it and return False. + ''' + if utils.pathExists(utils.toolsPaths): + # file exists, check if it loads OK + try: + with open(utils.toolsPaths, 'r') as toolsFileHandler: + json.load(toolsFileHandler) + print("Valid 'toolsPaths.json' file found.") + return True + + except Exception as err: + errorMsg = "Invalid 'toolsPaths.json' file. Error:\n" + str(err) + print(errorMsg) + + try: + os.remove(utils.toolsPaths) + msg = "\tDeleted. New 'toolsPaths.json' will be created on first workspace update." + print(msg) + except Exception as err: + errorMsg = "Error deleting 'toolsPaths.json'. Error:\n" + str(err) + utils.printAndQuit(errorMsg) + + # else: toolsPaths.json does not exist + return False + + def checkBuildDataFile(self): + ''' + This function makes sure 'buildData.json' is available. + If existing 'buildData.json' file is a valid JSON, it returns immediately. + If it is not a valid JSON file OR it does not exist, new 'buildData.json' file is created from template. + + Note: There is no backup file for buildData.json, since it is always regenerated on Update task. + ''' + if utils.pathExists(utils.buildDataPath): + # file exists, check if it loads OK + try: + with open(utils.buildDataPath, 'r') as buildDataFileHandler: + json.load(buildDataFileHandler) + print("Valid 'buildData.json' file found.") + + return + + except Exception as err: + errorMsg = "Invalid 'buildData.json' file. Error:\n" + str(err) + print(errorMsg) + + try: + os.remove(utils.buildDataPath) + msg = "\tDeleted. New 'buildData.json' will be created on first workspace update." + print(msg) + except Exception as err: + errorMsg = "Error deleting 'buildData.json'. Error:\n" + str(err) + utils.printAndQuit(errorMsg) + + # else: buildData.json does not exist + self.createBuildDataFile() + + def createUserToolsFile(self, toolsPaths): + ''' + Create 'toolsPaths.json' file with current tools paths. + This pats are absolute and not project-specific. + ''' + data = json.loads(tmpStr.toolsPathsTemplate) + try: + data["VERSION"] = __version__ + data["LAST_RUN"] = str(datetime.datetime.now()) + + for path in self.bStr.toolsPaths: + data[path] = toolsPaths[path] + + data = json.dumps(data, indent=4, sort_keys=False) + with open(utils.toolsPaths, 'w+') as toolsPathsFile: + toolsPathsFile.write(data) + print("'toolsPaths.json' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting 'toolsPaths.json' file:\n" + errorMsg += str(err) + print("WARNING:", errorMsg) + + def createBuildDataFile(self): + ''' + Create fresh 'buildData.json' file. + ''' + try: + data = json.loads(tmpStr.buildDataTemplate) + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + + with open(utils.buildDataPath, 'w+') as buildDataFile: + buildDataFile.truncate() + buildDataFile.write(dataToWrite) + + print("New template 'buildData.json' file created.") + except Exception as err: + errorMsg = "Exception error creating new 'buildData.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def getToolsPathsData(self): + ''' + Get data from current 'toolsPaths.json' file. + File existance is previoulsy checked in 'checkToolsPathFile()'. + ''' + with open(utils.toolsPaths, 'r') as toolsPathsFile: + data = json.load(toolsPathsFile) + + return data + + def getBuildData(self): + ''' + Get data from current 'buildData.json' file. + File existance is previoulsy checked in 'checkBuildDataFile()'. + ''' + with open(utils.buildDataPath, 'r') as buildDataFile: + data = json.load(buildDataFile) + + return data + + def addToolsPathsToBuildData(self, buildData, toolsPaths): + ''' + Get tools paths from 'toolsPaths.json' and add it to buildData + Returns new data. + ''' + allToolsPaths = [] + allToolsPaths.extend(self.bStr.toolsPaths) + allToolsPaths.extend(self.bStr.derivedPaths) + for path in allToolsPaths: + try: + buildData[path] = toolsPaths[path] + except Exception as err: + errorMsg = "Missing '" + path + "' key in tools paths data:\n" + str(toolsPaths) + print("Warning:", errorMsg) + + return buildData + + def addMakefileDataToBuildDataFile(self, buildData, makefileData): + ''' + This function fills buildData.json file with data from 'Makefile'. + Returns new data. + ''' + # sources + cSources = makefileData[self.mkfStr.cSources] + buildData[self.bStr.cSources] = cSources + + asmSources = makefileData[self.mkfStr.asmSources] + buildData[self.bStr.ldSources] = asmSources + + ldSources = makefileData[self.mkfStr.ldSources] + buildData[self.bStr.ldSources] = ldSources + + # includes + cIncludes = makefileData[self.mkfStr.cIncludes] + buildData[self.bStr.cIncludes] = cIncludes + + asmIncludes = makefileData[self.mkfStr.asmIncludes] + buildData[self.bStr.asmIncludes] = asmIncludes + + ldIncludes = makefileData[self.mkfStr.ldIncludes] + buildData[self.bStr.ldIncludes] = ldIncludes + + # defines + cDefines = makefileData[self.mkfStr.cDefines] + buildData[self.bStr.cDefines] = cDefines + + asmDefines = makefileData[self.mkfStr.asmDefines] + buildData[self.bStr.asmDefines] = asmDefines + + # compiler flags and paths + cFlags = makefileData[self.mkfStr.cFlags] + buildData[self.bStr.cFlags] = cFlags + + asmFlags = makefileData[self.mkfStr.asmFlags] + buildData[self.bStr.asmFlags] = asmFlags + + ldFlags = makefileData[self.mkfStr.ldFlags] + buildData[self.bStr.ldFlags] = ldFlags + + # build folder must be always inside workspace folder + buildDirPath = makefileData[self.mkfStr.buildDir] + buildData[self.bStr.buildDirPath] = buildDirPath + + # Target executable '.elf' file + projectName = makefileData[self.mkfStr.projectName] + targetExecutablePath = utils.getBuildElfFilePath(buildDirPath, projectName) + buildData[self.bStr.targetExecutablePath] = targetExecutablePath + + return buildData + + def addCubeMxProjectPathToBuildData(self, buildData): + ''' + If utils.cubeMxProjectFilePath is not None, add/update 'cubeMxProjectPath' field to 'buildData.json'. + ''' + if utils.cubeMxProjectFilePath is not None: + buildData[self.bStr.cubeMxProjectPath] = utils.cubeMxProjectFilePath + else: + buildData.pop(self.bStr.cubeMxProjectPath) + return buildData + + def overwriteBuildDataFile(self, data): + ''' + Overwrite existing 'buildData.json' file with new data. + ''' + try: + with open(utils.buildDataPath, 'r+') as buildDataFile: + data["VERSION"] = __version__ + data["LAST_RUN"] = str(datetime.datetime.now()) + + buildDataFile.seek(0) + buildDataFile.truncate() + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + buildDataFile.write(dataToWrite) + + print("'buildData.json' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting 'buildData.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + makefile = mkf.Makefile() + bData = BuildData() + + # Makefile must exist - # point in continuing if Makefile does not exist + makefile.checkMakefileFile() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + # data from current Makefile + makeExePath = buildData[bData.bStr.buildToolsPath] + gccExePath = buildData[bData.bStr.gccExePath] + makefileData = makefile.getMakefileData(makeExePath, gccExePath) + + # try to add CubeMX project file path + buildData = bData.addCubeMxProjectPathToBuildData(buildData) + + buildData = bData.addMakefileDataToBuildDataFile(buildData, makefileData) + + bData.overwriteBuildDataFile(buildData) diff --git a/sw/ideScripts/updateLaunchConfig.py b/sw/ideScripts/updateLaunchConfig.py new file mode 100755 index 0000000..5930c02 --- /dev/null +++ b/sw/ideScripts/updateLaunchConfig.py @@ -0,0 +1,198 @@ +''' +Update/generate 'launch.json' file in .vscode subfolder. +''' +import os +import json + +import utilities as utils +import templateStrings as tmpStr + +import updatePaths as pth +import updateBuildData as build + +__version__ = utils.__version__ + + +class LaunchConfigurations(): + def __init__(self): + self.bStr = build.BuildDataStrings() + + def checkLaunchFile(self): + ''' + Check if 'launch.json' file exists. If it does, check if it is a valid JSON file. + If it doesn't exist, create new according to template. + ''' + if utils.pathExists(utils.launchPath): + # file exists, check if it loads OK + try: + with open(utils.launchPath, 'r') as launchFile: + json.load(launchFile) + + print("Existing 'launch.json' file found.") + return + + except Exception as err: + errorMsg = "Invalid 'launch.json' file. Creating backup and new one.\n" + errorMsg += "Possible cause: invalid json format or comments (not supported by this scripts). Error:\n" + errorMsg += str(err) + print(errorMsg) + + utils.copyAndRename(utils.launchPath, utils.launchBackupPath) + + self.createLaunchFile() + + else: # 'launch.json' file does not exist jet, create it according to template string + self.createLaunchFile() + + def createLaunchFile(self): + ''' + Create fresh 'launch.json' file. + ''' + try: + with open(utils.launchPath, 'w') as launchFile: + data = json.loads(tmpStr.launchFileTemplate) + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + + launchFile.seek(0) + launchFile.truncate() + launchFile.write(dataToWrite) + + print("New 'launch.json' file created.") + + except Exception as err: + errorMsg = "Exception error creating new 'launch.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def getLaunchData(self): + ''' + Get data from current 'launch.json' file. + File existance is previoulsy checked in 'checkLaunchFile()'. + ''' + with open(utils.launchPath, 'r') as launchFile: + data = json.load(launchFile) + + return data + + def overwriteLaunchFile(self, data): + ''' + Overwrite existing 'launch.json' file with new data. + ''' + try: + with open(utils.launchPath, 'r+') as launchFile: + launchFile.seek(0) + launchFile.truncate() + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + launchFile.write(dataToWrite) + + print("'launch.json' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting 'launch.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def addOrReplaceLaunchConfiguration(self, data, launchData): + ''' + Check wether launch with this "name" already exists. If it doesn't, create new launch configuration, overwrite otherwise. + ''' + thisConfigurationName = launchData["name"] + + configurationExist = False + listOfConfigurations = data["configurations"] + for configurationIndex, config in enumerate(listOfConfigurations): + if config["name"] == thisConfigurationName: + # launch with this name already exist, replace it's content + data["configurations"][configurationIndex] = launchData + configurationExist = True + + if not configurationExist: + data["configurations"].append(launchData) + + return data + + def addAllLaunchConfigurations(self, launchData): + ''' + Merge and return all combined launch configuration data. + ''' + launchCfg = self.getDebugLaunchConfig() + launchData = self.addOrReplaceLaunchConfiguration(launchData, launchCfg) + + launchCfg = self.getRunPythonLaunchConfig() + launchData = self.addOrReplaceLaunchConfiguration(launchData, launchCfg) + + # TODO USER: User can add other launch configurations here + # - copy any of getXLaunchConfig() functions below, edit + # - add this function here as other launch configurations above + + return launchData + + ######################################################################################################################## + + ######################################################################################################################## + def getDebugLaunchConfig(self): + ''' + Create/repair 'Cortex debug' launch configuration. + ''' + configurationData = """ + { + "name": "will be replaced with templateStrings string", + "type": "cortex-debug", + "request": "launch", + "servertype": "openocd", + "cwd": "${workspaceFolder}", + "executable": "will be replaced with path from buildData.json", + "svdFile": "will be replaced with path from buildData.json", + "configFiles": ["will be replaced with path from buildData.json"], + "preLaunchTask": "will be replaced with templateStrings string" + } + """ + jsonConfigurationData = json.loads(configurationData) + + buildData = build.BuildData().getBuildData() + + jsonConfigurationData["name"] = tmpStr.launchName_Debug + jsonConfigurationData["executable"] = buildData[self.bStr.targetExecutablePath] + jsonConfigurationData["svdFile"] = buildData[self.bStr.stm32SvdPath] + jsonConfigurationData["configFiles"] = [buildData[self.bStr.openOcdInterfacePath]] + jsonConfigurationData["configFiles"].extend(buildData[self.bStr.openOcdConfig]) + jsonConfigurationData["preLaunchTask"] = tmpStr.taskName_build + + return jsonConfigurationData + + def getRunPythonLaunchConfig(self): + ''' + Create 'Debug current Python file' launch configuration. + ''' + configurationData = """ + { + "name": "Debug current Python file", + "type": "python", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${file}", + "console": "integratedTerminal" + } + """ + jsonConfigurationData = json.loads(configurationData) + + return jsonConfigurationData + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + bData = build.BuildData() + launch = LaunchConfigurations() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + # create taks file + launch.checkLaunchFile() + launchData = launch.getLaunchData() + launchData = launch.addAllLaunchConfigurations(launchData) + + launch.overwriteLaunchFile(launchData) diff --git a/sw/ideScripts/updateMakefile.py b/sw/ideScripts/updateMakefile.py new file mode 100755 index 0000000..86fbed1 --- /dev/null +++ b/sw/ideScripts/updateMakefile.py @@ -0,0 +1,522 @@ +''' +Generate (replace existing) Makefile file in workspace folder wtih data from +original Makefile and 'c_cpp_properties.json'. +''' + +import os +import datetime +from subprocess import Popen, PIPE + +import utilities as utils +import templateStrings as tmpStr + +import updatePaths as pth +import updateWorkspaceSources as wks +import updateBuildData as build + +__version__ = utils.__version__ + + +class MakefileStrings(): + projectName = 'TARGET' + buildDir = 'BUILD_DIR' + + cSources = 'C_SOURCES' + asmSources = 'ASM_SOURCES' + ldSources = 'LIBS' + cDefines = 'C_DEFS' + asmDefines = 'AS_DEFS' + cIncludes = 'C_INCLUDES' + asmIncludes = 'AS_INCLUDES' + ldIncludes = 'LIBDIR' + cFlags = 'CFLAGS' + asmFlags = 'ASFLAGS' + ldFlags = 'LDFLAGS' + + +class Makefile(): + def __init__(self): + self.mkfStr = MakefileStrings() + self.cPStr = wks.CPropertiesStrings() + + def checkMakefileFile(self): + ''' + Check if 'Makefile' file exists. If it doesn't, report as error. + ''' + if not utils.pathExists(utils.makefilePath): + errorMsg = "Makefile does not exist! Did CubeMX generated Makefile?\n" + errorMsg += "File name must be 'Makefile'." + utils.printAndQuit(errorMsg) + + def restoreOriginalMakefile(self): + ''' + Check wether current 'Makefile' has print capabilities. If it has, this means it was already altered by this script. + If it was, replace it with backup copy: 'Makefile.backup'. + If it does not have print capabilities, it is assumed 'Makefile' was regenerated with CubeMX + tool - print function is added and backup file is overwritten with this new 'Makefile'. + + At the end, fresh 'Makefile' with print function should be available. + ''' + if utils.pathExists(utils.makefilePath): + # Makefile exists, check if it is original (no print capabilities) + if self.hasPrintCapabilities(utils.makefilePath): + # Makefile exists, already modified + if utils.pathExists(utils.makefileBackupPath): + # can original file be restored from backup file? + if self.hasPrintCapabilities(utils.makefileBackupPath): + errorMsg = "Both, 'Makefile' and 'Makefile.backup' exists, but they are both modified!\n" + errorMsg += "Did you manually delete, replace or modify any of Makefiles?\n" + errorMsg += "-> Delete all Makefiles and regenerate with CubeMX." + utils.printAndQuit(errorMsg) + else: + # original will be restored from backup file + print("Existing 'Makefile' file will be restored from 'Makefile.backup'.") + utils.copyAndRename(utils.makefileBackupPath, utils.makefilePath) + else: + errorMsg = "'Makefile.backup' does not exist, while 'Makefile' was already modified!\n" + errorMsg += "Did you manually delete, replace or modify any of Makefiles?\n" + errorMsg += "-> Delete all Makefiles and regenerate with CubeMX." + utils.printAndQuit(errorMsg) + else: + print("Existing 'Makefile' file found (original).") + utils.copyAndRename(utils.makefilePath, utils.makefileBackupPath) + elif utils.pathExists(utils.makefileBackupPath): + # Makefile does not exist, but Makefile.backup does + if self.hasPrintCapabilities(utils.makefileBackupPath): + errorMsg = "'Makefile.backup' exists, but is already modified!\n" + errorMsg += "Did you manually delete, replace or modify any of Makefiles?\n" + errorMsg += "-> Delete all Makefiles and regenerate with CubeMX." + utils.printAndQuit(errorMsg) + else: + # original will be restored from backup file + print("'Makefile' file will be restored from 'Makefile.backup'.") + utils.copyAndRename(utils.makefileBackupPath, utils.makefilePath) + else: + errorMsg = "No Makefiles available, unable to proceed!\n" + errorMsg += "-> Regenerate with CubeMX." + utils.printAndQuit(errorMsg) + + self.addMakefileCustomFunctions(pathToMakefile=utils.makefilePath) + + def getMakefileData(self, makeExePath, gccExePath): + ''' + Get Makefile data. + Returns data in dictionary. + ''' + dataDictionaryList = {} + + # project name + projectName = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.projectName)[0] + dataDictionaryList[self.mkfStr.projectName] = projectName + + # dir name + buildDirName = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.buildDir)[0] + dataDictionaryList[self.mkfStr.buildDir] = buildDirName + + # source files + cSourcesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.cSources) + dataDictionaryList[self.mkfStr.cSources] = cSourcesList + + asmSourcesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.asmSources) + dataDictionaryList[self.mkfStr.asmSources] = asmSourcesList + + ldSourcesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.ldSources) + # ldSourcesList = utils.stripStartOfString(ldSourcesList, '-l') # more readable without stripping + dataDictionaryList[self.mkfStr.ldSources] = ldSourcesList + + # defines + asmDefinesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.asmDefines) + asmDefinesList = utils.stripStartOfString(asmDefinesList, '-D') + dataDictionaryList[self.mkfStr.asmDefines] = asmDefinesList + + cDefinesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.cDefines) + cDefinesList = utils.stripStartOfString(cDefinesList, '-D') + dataDictionaryList[self.mkfStr.cDefines] = cDefinesList + + # source & include directories + asmIncludesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.asmIncludes) + asmIncludesList = utils.stripStartOfString(asmIncludesList, '-I') + dataDictionaryList[self.mkfStr.asmIncludes] = asmIncludesList + + cIncludesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.cIncludes) + cIncludesList = utils.stripStartOfString(cIncludesList, '-I') + dataDictionaryList[self.mkfStr.cIncludes] = cIncludesList + + ldIncludesList = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.ldIncludes) + ldIncludesList = utils.stripStartOfString(ldIncludesList, '-L') + dataDictionaryList[self.mkfStr.ldIncludes] = ldIncludesList + + # flags + cFlags = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.cFlags) + dataDictionaryList[self.mkfStr.cFlags] = cFlags + + asmFlags = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.asmFlags) + dataDictionaryList[self.mkfStr.asmFlags] = asmFlags + + ldFlags = self.getMakefileVariable(makeExePath, gccExePath, self.mkfStr.ldFlags) + dataDictionaryList[self.mkfStr.ldFlags] = ldFlags + + return dataDictionaryList + + def parseMakefileData(self, data, startString): + ''' + Fetch and unparse data from existing Makefile (generated by CubeMX) starting with 'startString'. + ''' + endOfLineChars = "\\" + startString = startString + ' = ' + NOT_FOUND = -1 + + items = [] + # find start and end of defines and + for lineIndex, line in enumerate(data): + line = line.rstrip('\n') # strip string of '\n' + + startCharacter = line.find(startString) + if startCharacter != NOT_FOUND: # search for start string + + # check if one-liner + if line.find(endOfLineChars) == NOT_FOUND: + line = line[len(startString):] + if len(line) != 0: # check for 'SOMETHING = ' (empty line after '=') + # not an empty line after '=' + items.append(line) # strip string of start and and characters + return items + + else: # multiline item in Makefile + for line2 in data[lineIndex+1:]: + line2 = line2.rstrip('\n') + if line2.find(endOfLineChars) != NOT_FOUND: + line2 = line2.rstrip('\\') # strip of '\' + line2 = line2.rstrip(' ') # strip of ' ' + items.append(line2) + else: + line2 = line2.rstrip('\\') # strip of '\' + line2 = line2.rstrip(' ') # strip of ' ' + items.append(line2) + return items + + errorMsg = "String item '" + str(startString) + "' not found!\n" + errorMsg += "Invalid/changed Makefile or this script is outdated (change in CubeMX Makefile syntax?)." + utils.printAndQuit(errorMsg) + + def createNewMakefile(self): + ''' + Merge existing Makefile data and user fields from existing 'c_cpp_properties.json.' + ''' + cP = wks.CProperties() + cPropertiesData = cP.getCPropertiesData() + + with open(utils.makefilePath, 'r') as makefile: + data = makefile.readlines() + + # sources + cSources = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_cSources) + data = self.searchAndAppend(data, self.mkfStr.cSources, cSources) + + asmSources = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_asmSources) + data = self.searchAndAppend(data, self.mkfStr.asmSources, asmSources) + + ldSources = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_ldSources) + data = self.searchAndAppend(data, self.mkfStr.ldSources, ldSources, preappend='-l:') + + # includes + cIncludes = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_cIncludes) + data = self.searchAndAppend(data, self.mkfStr.cIncludes, cIncludes, preappend='-I') + + asmIncludes = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_asmIncludes) + data = self.searchAndAppend(data, self.mkfStr.asmIncludes, asmIncludes, preappend='-I') + + ldIncludes = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_ldIncludes) + data = self.searchAndAppend(data, self.mkfStr.ldIncludes, ldIncludes, preappend='-L') + + # defines + cDefines = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_cDefines) + data = self.searchAndAppend(data, self.mkfStr.cDefines, cDefines, preappend='-D') + + asmDefines = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_asmDefines) + data = self.searchAndAppend(data, self.mkfStr.asmDefines, asmDefines, preappend='-D') + + # compiler flags + cFlags = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_cFlags) + data = self.searchAndAppend(data, self.mkfStr.cFlags, cFlags) + + asmFlags = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_asmFlags) + data = self.searchAndAppend(data, self.mkfStr.asmFlags, asmFlags) + + ldFlags = cP.getCPropertiesKeyData(cPropertiesData, self.cPStr.user_ldFlags) + data = self.searchAndAppend(data, self.mkfStr.ldFlags, ldFlags) + + data = self.replaceMakefileHeader(data) + + try: + with open(utils.makefilePath, 'w') as makefile: + for line in data: + makefile.write(line) + print("New Makefile data succesfully written.") + + except Exception as err: + errorMsg = "Exception error writing new data to Makefile:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def searchAndAppend(self, data, searchString, appendData, preappend=None): + ''' + Search for string in 'data' list and append 'appendData' according to Makefile syntax. + if 'preappend' is defined, each item of 'appendData' is preappended with this string. + ''' + NOT_FOUND = -1 + + if preappend is not None: + appendData = utils.preappendString(appendData, preappend) + + for lineIndex, line in enumerate(data): + line = line.rstrip('\n') # strip string of '\n' + + if line.find(searchString) != NOT_FOUND: # search for start string + if line[0] == '#': # this is a comment + continue + + if line.find("\\") == NOT_FOUND: + # one-liner, no '\' sign at the end of the line + if isinstance(appendData, list): # if this is list + if appendData: # and it is not empty + if len(appendData) == 1: # this list has only one item, add it without '\' + if line[-1] != ' ': # avoid double spaces + line += " " + data[lineIndex] = line + appendData[0] + "\n" + + else: + # this is list with multiple items, '\' will be needed + line += " \\\n" + data[lineIndex] = line + + for itemIndex, item in enumerate(appendData): + stringToInsert = item + if item != appendData[-1]: # for last item do not append "\" + stringToInsert += "\\" + stringToInsert += "\n" # new line must always be added + data.insert(lineIndex + itemIndex + 1, stringToInsert) + + return data + + else: # appendData is string (not list) + if appendData != '': + if data[lineIndex][-1] != ' ': # avoid double spaces + data[lineIndex] += " " + data[lineIndex] += appendData + "\n" + + return data + else: # already a multi-liner, append at the beginning, but in new line + if isinstance(appendData, list): + for itemIndex, item in enumerate(appendData): + stringToInsert = item + " \\\n" + data.insert(lineIndex + itemIndex + 1, stringToInsert) + else: # appendData is string (not list) + data[lineIndex] += item + " \\\n" + + return data + + errorMsg = "String item " + str(searchString) + " not found!" + utils.printAndQuit(errorMsg) + + def searchAndCleanData(self, data, searchString): + ''' + Search for string in 'data' list and clear all belonging data according to Makefile syntax. + ''' + NOT_FOUND = -1 + + for lineIndex, line in enumerate(data): + line = line.rstrip('\n') # strip string of '\n' + + if line.find(searchString) != NOT_FOUND: # search for start string + if line[0] == '#': # this is a comment + continue + if line.find("\\") == NOT_FOUND: + # keep searchString and equaliy sign, append '\n' + equalitySignCharIndex = line.find('=') + data[lineIndex] = data[lineIndex][: equalitySignCharIndex+1] + ' \n' + return data + + else: # multi-liner, get last line index and delete this lines + lastLineIndex = lineIndex + 1 + while data[lastLineIndex].rstrip('\n') != '': + lastLineIndex = lastLineIndex + 1 + if lastLineIndex >= len(data): + errorMsg = "Unable to find end of multi-line Makefile item (" + searchString + "). " + errorMsg += "Was Makefile manually modified?" + utils.printAndQuit(errorMsg) + # delete this lines + delLineIndex = lineIndex + 1 + constLineIndex = lineIndex + 1 # this line will be deleted until an empty line is present + while delLineIndex != lastLineIndex: + del data[constLineIndex] + delLineIndex = delLineIndex + 1 + # keep searchString and equaliy sign, append '\n' + equalitySignCharIndex = line.find('=') + data[lineIndex] = line[: equalitySignCharIndex+1] + ' \n' + return data + + errorMsg = "String item " + str(searchString) + " not found!" + utils.printAndQuit(errorMsg) + + ######################################################################################################################## + + def getMakefileVariable(self, makeExePath, gccExePath, variableName): + ''' + Open subproces, call make print-variableName and catch stout. + Syntax with absolute paths: + "path to make.exe with spaces" GCC_PATH="path to gccsomething.exe with spaces" print-VARIABLE + + With + ''' + # change directory to the same folder as Makefile + cwd = os.getcwd() + os.chdir(utils.workspacePath) + + printStatement = "print-" + str(variableName) + gccExeFolderPath = os.path.dirname(gccExePath) + # gccPath = "\"\"GCC_PATH=" + gccExeFolderPath + gccPath = "GCC_PATH=\"" + gccExeFolderPath + "\"" + arguments = [makeExePath, gccPath, printStatement] + + proc = Popen(arguments, stdout=PIPE) + returnString = str((proc.communicate()[0]).decode('UTF-8')) + returnString = returnString.rstrip('\n') + returnString = returnString.rstrip('\r') + + os.chdir(cwd) # change directory back to where it was + + if returnString.find("make: *** No rule to make target") != -1: + errorMsg = "Can't retrieve " + variableName + " value from makefile." + utils.printAndQuit(errorMsg) + + # remove "VARIABLE=" string start. This string must be present, or 'Echo is off.' is displayed for empy variables. + if returnString.find(tmpStr.printMakefileDefaultString) != -1: + returnString = returnString.replace(tmpStr.printMakefileDefaultString, '') + + returnStringList = returnString.split(' ') # split string to list and remove empty items + returnStringListCopy = [] + for itemIndex, item in enumerate(returnStringList): + # handle strings where print statement (print-variableName) is present, like '-MF"print-VARIABLE"' + quotedPrintStatement = "\"" + printStatement + "\"" + if item.find(quotedPrintStatement) != -1: + item = item.replace(quotedPrintStatement, '') + elif item.find(printStatement) != -1: + item = item.replace(printStatement, '') + + # handle empty items + if item not in ['', ' ']: + returnStringListCopy.append(item) + + return returnStringListCopy + + def replaceMakefileHeader(self, data): + ''' + Change header, to distinguish between original and new Makefile. + ''' + # first find last line before '# target', that must not be changed + lastLine = None + for lineIndex, line in enumerate(data): + twoLinesAhead = data[lineIndex + 2] # first line is ######... and second should be '# target' + twoLinesAhead = twoLinesAhead.rstrip('\n') # strip string of '\n' + if twoLinesAhead.find("# target") != -1: # search for start string + lastLine = lineIndex + break + if lastLine is None: + print('') # previously there was no new line + errorMsg = "Makefile '# target' string missing.\n" + errorMsg += "Invalid/changed Makefile or this script is outdated (change in CubeMX Makefile syntax?)." + utils.printAndQuit(errorMsg) + + else: # '# target' line found + # delete current header + lineIndex = 0 + while lineIndex != lastLine: + lineIndex = lineIndex + 1 + del data[0] + + # add new header + for line in reversed(tmpStr.makefileHeader.splitlines()): + if line.find(tmpStr.versionString) != -1: + line = line.replace('***', __version__) + if line.find(tmpStr.lastRunString) != -1: + timestamp = datetime.datetime.now() + line = line.replace('***', str(timestamp)) + + line = line + "\n" + data.insert(0, line) + + return data + + def hasPrintCapabilities(self, pathToMakefile): + ''' + Check wether current Makefile has 'print-variable' function. + Returns True or False. + ''' + with open(pathToMakefile, 'r+') as makefile: + data = makefile.readlines() + + # Try to find existing print function + for line in reversed(data): + line = line.rstrip('\n') # strip string of '\n' + if line.find(tmpStr.printMakefileVariableFunction) != -1: + # existing print function found! + return True + + return False + + def addMakefileCustomFunctions(self, pathToMakefile): + ''' + Add all functions to makefile: + - print-variable + - clean-build-dir + + This function is called only if current Makefile does not have 'print-variable' capabilities. + ''' + with open(pathToMakefile, 'r+') as makefile: + makefileDataLines = makefile.readlines() + + makefileDataLines = self.addPrintVariableFunction(makefileDataLines) + + makefile.seek(0) + makefile.truncate() + for line in makefileDataLines: + makefile.write(line) + + def addPrintVariableFunction(self, makefileDataLines): + ''' + Add print Makefile variable capabilities to Makefile + ''' + makefileDataLines.append("\n\n") + for line in tmpStr.printMakefileVariable.splitlines(): + line = line + "\n" + makefileDataLines.append(line) + + print("Makefile 'print-variable' function added.") + return makefileDataLines + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + bData = build.BuildData() + cP = wks.CProperties() + makefile = Makefile() + + # Makefile must exist + makefile.checkMakefileFile() # no point in continuing if Makefile does not exist + + buildData = bData.prepareBuildData() + + makefile.restoreOriginalMakefile() + makeExePath = buildData[bData.bStr.buildToolsPath] + gccExePath = buildData[bData.bStr.gccExePath] + makefileData = makefile.getMakefileData(makeExePath, gccExePath) + + buildData = bData.addMakefileDataToBuildDataFile(buildData, makefileData) + + # get data from 'c_cpp_properties.json' and create new Makefile + cP.checkCPropertiesFile() + makefile.createNewMakefile() # reads 'c_cpp_properties.json' internally diff --git a/sw/ideScripts/updatePaths.py b/sw/ideScripts/updatePaths.py new file mode 100755 index 0000000..7599f39 --- /dev/null +++ b/sw/ideScripts/updatePaths.py @@ -0,0 +1,273 @@ +''' +This script can be run to update paths to gcc, openOCD and other tools/files/folders. +Script verify and add data to 'buildData.json' file. +''' +import os +import shutil + +import utilities as utils +import updateBuildData as build +import updateTasks as tasks +import updateLaunchConfig as launch +import updateWorkspaceFile as workspaceFile + +__version__ = utils.__version__ + + +class UpdatePaths(): + def __init__(self): + self.bStr = build.BuildDataStrings() + + # list of paths with explanatory names and (optionally) default path + # keys must match with 'self.bStr.toolsPaths' list + self.pathsDescriptionsData = { + self.bStr.gccExePath: { + "name": "arm-none-eabi-gcc executable (arm-none-eabi-gcc.exe)", + "defaultPath": "arm-none-eabi-gcc"}, + self.bStr.buildToolsPath: { + "name": "make executable (make.exe)", + "defaultPath": "make"}, + self.bStr.openOcdPath: { + "name": "OpenOCD executable (openocd.exe)", + "defaultPath": "openocd"}, + self.bStr.openOcdInterfacePath: { + "name": "OpenOCD ST Link interface path ('stlink.cfg')", + "defaultPath": "./scripts/interface/stlink.cfg"}, + self.bStr.stm32SvdPath: { + "name": "STM target '*.svd' file (.../Keil*/CMSIS/SVD/STM32F0x1.svd)", + "defaultPath": None} + } + + def verifyToolsPaths(self, toolsPaths, request=False): + ''' + This function checks if paths in 'toolsPaths.json' are a valid paths. + If any path is not valid/missing, user is asked for update via updatePath(). + If 'request' is set to True, user is asked to update path even if it is a valid path. + + Returns updated valid tools paths. + ''' + for pathName in self.bStr.toolsPaths: + try: + mustBeUpdated = False + if pathName in toolsPaths: + # 'toolsPaths.json' keys are not lists. Always a plain path (string) + if not utils.pathExists(toolsPaths[pathName]): + mustBeUpdated = True + # path not valid, check if command + if utils.commandExists(toolsPaths[pathName]): + mustBeUpdated = False + + if mustBeUpdated: + if toolsPaths[pathName] != '': + # avoid reporting invalid file path, if there is an empty string + msg = "\n\nInvalid path detected in '" + pathName + "' key." + print(msg) + else: + if request: + msg = "\n\nValid path(s) for " + pathName + " detected: '" + toolsPaths[pathName] + "'." + msg += "\n\tUpdate? [y/n]: " + if utils.getYesNoAnswer(msg): + mustBeUpdated = True + + else: # this key is missing in toolsPaths.json! + mustBeUpdated = True + + if mustBeUpdated: + if pathName in self.bStr.derivedPaths: + continue + + elif pathName == self.bStr.openOcdConfig: + # get openOcdConfig - special handler + toolsPaths[pathName] = utils.getOpenOcdConfig(toolsPaths[self.bStr.openOcdPath]) + + elif pathName in self.pathsDescriptionsData: + name = self.pathsDescriptionsData[pathName]['name'] + defaultPath = self.pathsDescriptionsData[pathName]['defaultPath'] + toolsPaths[pathName] = self.updatePath(name, defaultPath) + + else: + toolsPaths[pathName] = self.updatePath(pathName, None) + + except Exception as err: + toolsPaths[pathName] = self.updatePath(pathName, None) + + for pathName in self.bStr.derivedPaths: + if pathName == self.bStr.pythonExec: + toolsPaths[self.bStr.pythonExec] = utils.getPython3Executable() + + elif pathName == self.bStr.gccInludePath: + toolsPaths[self.bStr.gccInludePath] = utils.getGccIncludePath(toolsPaths[self.bStr.gccExePath]) + + else: + errorMsg = "ideScripts design error: pathName '" + pathName + "' is in 'self.bStr.derivedPaths' list, " + errorMsg += "but no 'get()' handler is specified." + utils.printAndQuit(errorMsg) + + return toolsPaths + + def verifyTargetConfigurationPaths(self, buildData, request=False): + ''' + This function checks if 'buildData.json' contains targetConfiguration paths. + If any path is not valid/missing, user is asked for update via updatePath(). + If 'request' is set to True, user is asked to update path even if it is a valid path. + + Returns buildData with a valid, updated tools paths. + ''' + for pathName in self.bStr.targetConfigurationPaths: + mustBeUpdated = False + + if pathName in self.bStr.derivedPaths: + # derived paths, build later + continue + + if pathName not in buildData: + mustBeUpdated = True + + else: + if isinstance(buildData[pathName], list): + if not buildData[pathName]: + mustBeUpdated = True + else: + for path in buildData[pathName]: + if not utils.pathExists(path): + mustBeUpdated = True + break + + else: # not a list, a single path expected + if not utils.pathExists(buildData[pathName]): + mustBeUpdated = True + # path not valid, check if command + if utils.commandExists(buildData[pathName]): + mustBeUpdated = False + + if mustBeUpdated: + notify = True + # avoid reporting invalid file path, if there is an empty string/list + if isinstance(buildData[pathName], list): + if not buildData[pathName]: + notify = False + else: + if buildData[pathName] == '': + notify = False + + if notify: + msg = "\n\nInvalid path detected in 'buildData.json' '" + pathName + "' key." + print(msg) + else: + if request: + msg = "\n\nValid path(s) for " + pathName + " detected: '" + str(buildData[pathName]) + "'." + msg += "\n\tUpdate? [y/n]: " + if utils.getYesNoAnswer(msg): + mustBeUpdated = True + + if mustBeUpdated: + if pathName == self.bStr.openOcdConfig: + # get openOcdConfig - special handler + buildData[pathName] = utils.getOpenOcdConfig(buildData[self.bStr.openOcdPath]) + + elif pathName in self.bStr.derivedPaths: + name = self.bStr.derivedPaths[pathName]['name'] + defaultPath = self.bStr.derivedPaths[pathName]['defaultPath'] + buildData[pathName] = self.updatePath(name, defaultPath) + + else: + buildData[pathName] = self.updatePath(pathName, None) + + return buildData + + def copyTargetConfigurationFiles(self, buildData): + ''' + This function checks if paths to target configuration files listed in 'BuildDataStrings.targetConfigurationPaths' + are available, stored inside this workspace '.vscode' subfolder. Once this files are copied, paths are updated and + new buildData is returned. + + Paths are previously checked/updated in 'verifyTargetConfigurationPaths()' + ''' + for pathName in self.bStr.targetConfigurationPaths: + currentPaths = buildData[pathName] + + if isinstance(currentPaths, list): + isList = True + else: + isList = False + currentPaths = [currentPaths] + + newPaths = [] + for currentPath in currentPaths: + fileName = utils.getFileName(currentPath, withExtension=True) + fileInVsCodeFolder = os.path.join(utils.vsCodeFolderPath, fileName) + + if not utils.pathExists(fileInVsCodeFolder): + # file does not exist in '.vscode' folder + try: + newPath = shutil.copy(currentPath, utils.vsCodeFolderPath) + except Exception as err: + errorMsg = "Unable to copy file '" + fileName + "' to '.vscode' folder. Exception:\n" + str(err) + utils.printAndQuit(errorMsg) + + newPath = os.path.relpath(fileInVsCodeFolder) + newPath = utils.pathWithForwardSlashes(newPath) + newPaths.append(newPath) + + if isList: + buildData[pathName] = newPaths + else: + buildData[pathName] = newPaths[0] + + return buildData + + def updatePath(self, pathName, default): + ''' + This function is called when a path is detected as invalid or the user requests to update paths. + ''' + pathDefault = None + + # check if default is a path + if utils.pathExists(default): + pathDefault = default + + # not a path command, check if it's a command + elif utils.commandExists(default): + pathDefault = shutil.which(default) + + if pathDefault is not None: + msg = "\n\tDefault path to '" + pathName + "' detected at '" + pathDefault + "'\n\tUse this path? [y/n]: " + if utils.getYesNoAnswer(msg): + return pathDefault + + # default not detected or user wants custom path/command + newPath = utils.getUserPath(pathName) + return newPath + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = UpdatePaths() + bData = build.BuildData() + tasks = tasks.Tasks() + launch = launch.LaunchConfigurations() + wksFile = workspaceFile.UpdateWorkspaceFile() + + # update build data + buildData = bData.prepareBuildData(request=True) + bData.overwriteBuildDataFile(buildData) + + # update tasks + tasks.checkTasksFile() + tasksData = tasks.getTasksData() + tasksData = tasks.addAllTasks(tasksData) + tasks.overwriteTasksFile(tasksData) + + # update launch configurations + launch.checkLaunchFile() + launchData = launch.getLaunchData() + launchData = launch.addAllLaunchConfigurations(launchData) + launch.overwriteLaunchFile(launchData) + + # update workspace file with "cortex-debug" specifics + wksFile.checkWorkspaceFile() + wksData = wksFile.getWorkspaceFileData() + wksData = wksFile.addBuildDataToWorkspaceFile(wksData, buildData) + wksFile.overwriteWorkspaceFile(wksData) diff --git a/sw/ideScripts/updateTasks.py b/sw/ideScripts/updateTasks.py new file mode 100755 index 0000000..16e29e0 --- /dev/null +++ b/sw/ideScripts/updateTasks.py @@ -0,0 +1,564 @@ +''' +Update/generate 'tasks.json' file in .vscode subfolder. + +'tasks.json' fields description: +https://code.visualstudio.com/docs/editor/tasks +''' +import os +import json + +import utilities as utils +import templateStrings as tmpStr + +import updatePaths as pth +import updateWorkspaceSources as wks +import updateMakefile as mkf +import updateBuildData as build + +__version__ = utils.__version__ + + +class Tasks(): + def __init__(self): + self.cPStr = wks.CPropertiesStrings() + self.mkfStr = mkf.MakefileStrings() + self.bStr = build.BuildDataStrings() + + def checkTasksFile(self): + ''' + Check if 'tasks.json' file exists. If it does, check if it is a valid JSON file. + If it doesn't exist, create new according to template. + ''' + if utils.pathExists(utils.tasksPath): + # file exists, check if it loads OK + try: + with open(utils.tasksPath, 'r') as tasksFile: + json.load(tasksFile) + + print("Existing 'tasks.json' file found.") + return + + except Exception as err: + errorMsg = "Invalid 'tasks.json' file. Creating backup and new one.\n" + errorMsg += "Possible cause: invalid json format or comments (not supported by this scripts). Error:\n" + errorMsg += str(err) + print(errorMsg) + + utils.copyAndRename(utils.tasksPath, utils.tasksBackupPath) + + self.createTasksFile() + + else: # 'tasks.json' file does not exist jet, create it according to template string + self.createTasksFile() + + def createTasksFile(self): + ''' + Create fresh 'tasks.json' file. + ''' + try: + with open(utils.tasksPath, 'w') as tasksFile: + data = json.loads(tmpStr.tasksFileTemplate) + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + + tasksFile.seek(0) + tasksFile.truncate() + tasksFile.write(dataToWrite) + + print("New 'tasks.json' file created.") + + except Exception as err: + errorMsg = "Exception error creating new 'tasks.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def getTasksData(self): + ''' + Get data from current 'tasks.json' file. + File existance is previoulsy checked in 'checkTasksFile()'. + ''' + with open(utils.tasksPath, 'r') as tasksFile: + data = json.load(tasksFile) + + return data + + def overwriteTasksFile(self, data): + ''' + Overwrite existing 'tasks.json' file with new data. + ''' + try: + with open(utils.tasksPath, 'r+') as tasksFile: + tasksFile.seek(0) + tasksFile.truncate() + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + tasksFile.write(dataToWrite) + + print("'tasks.json' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting 'tasks.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def addOrReplaceTask(self, data, taskData): + ''' + Check wether tasks with this "label" already exists. If it doesn't, create new task, overwrite otherwise. + ''' + thisTaskName = taskData["label"] + + taskExist = False + listOfTasks = data["tasks"] + for taskIndex, task in enumerate(listOfTasks): + if task["label"] == thisTaskName: + # task with this name already exist, replace it's content + data["tasks"][taskIndex] = taskData + taskExist = True + + if not taskExist: + data["tasks"].append(taskData) + + return data + + def addAllTasks(self, tasksData): + ''' + Merge and return all combined tasks data. + ''' + + # building and compiling project tasks + task = self.getBuildTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getCompileTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getDeleteBuildFolderTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + # debugging and target control tasts + task = self.getBuildDownloadAndRunTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getDownloadAndRunTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getResetAndRunTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getHaltTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getRunTask() + tasksData = self.addOrReplaceTask(tasksData, task) + + # update IDE workspace tasks + task = self.getRunCurrentPythonFileTask() # common "run python file" task + tasksData = self.addOrReplaceTask(tasksData, task) + + if utils.cubeMxProjectFilePath is not None: + task = self.getOpenCubeMXTask() # open CubeMX project + tasksData = self.addOrReplaceTask(tasksData, task) + + task = self.getUpdateTask() # update all files for VS Code so it can be used as IDE + tasksData = self.addOrReplaceTask(tasksData, task) + + # TODO USER: User can add other custom tasks here + # - copy any of getXTask() functions below, edit + # - add this function here as other tasks above + + return tasksData + + ######################################################################################################################## + # Build, compile and clean tasks + ######################################################################################################################## + + def getBuildTask(self): + ''' + Add build task (execute 'make' command). Also the VS Code default 'build' task. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "group": { + "kind": "build", + "isDefault": true + }, + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "problemMatcher": { + "pattern": { + "regexp": "^(.*):(\\\\d+):(\\\\d+):\\\\s+(warning|error):\\\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + }, + "presentation": { + "focus": true + } + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_build + jsonTaskData["command"] = buildData[self.bStr.buildToolsPath] + + gccFolderPath = os.path.dirname(buildData[self.bStr.gccExePath]) + gccFolderPath = utils.pathWithForwardSlashes(gccFolderPath) + jsonTaskData["args"] = ["GCC_PATH=" + gccFolderPath] # specify compiler path to make command + + numOfCores = os.cpu_count() + parallelJobsNumber = int(numOfCores * 1.5) # https://stackoverflow.com/questions/15289250/make-j4-or-j8/15295032 + parallelJobsStr = "-j" + str(parallelJobsNumber) + jsonTaskData["args"].append(parallelJobsStr) # set 'make' parallel job execution + + return jsonTaskData + + def getCompileTask(self): + ''' + Create compile current file task (execute gcc compile command). + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "will be replaced with GCC path below", + "args": ["will be replaced with path from buildData.json"], + "problemMatcher": { + "pattern": { + "regexp": "^(.*):(\\\\d+):(\\\\d+):\\\\s+(warning|error):\\\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + }, + "presentation": { + "focus": true + } + } + """ + jsonTaskData = json.loads(taskData) + + # get compiler C flags, defines, includes, ... from 'buildData.json' + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_compile + + # defines + cDefines = buildData[self.bStr.cDefines] + cDefines = utils.preappendString(cDefines, '-D') + + # includes + cIncludes = buildData[self.bStr.cIncludes] + cIncludes = utils.preappendString(cIncludes, '-I') + + # build directory + buildDir = buildData[self.bStr.buildDirPath] + + # c flags + cFlags = buildData[self.bStr.cFlags] + for flagIndex, flag in enumerate(cFlags): + if flag == "-MF": + newFlagString = "-MF'" + buildDir + "/${fileBasenameNoExtension}.d'" + cFlags[flagIndex] = newFlagString + continue + + # output file + outputFilePath = "'" + buildDir + "/${fileBasenameNoExtension}.o'" + outputFile = ["-o"] + outputFile.append(outputFilePath) + + # compile file string + fileString = "'${relativeFile}'" + fileString = [fileString] + + jsonTaskData["command"] = buildData[self.bStr.gccExePath] + jsonTaskData["args"] = ["-c"] # only compile switch + jsonTaskData["args"].extend(cDefines) + jsonTaskData["args"].extend(cIncludes) + jsonTaskData["args"].extend(cFlags) + jsonTaskData["args"].extend(fileString) + jsonTaskData["args"].extend(outputFile) + + return jsonTaskData + + def getDeleteBuildFolderTask(self): + ''' + Create delete task (execute 'make clean' command). + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["clean"], + "problemMatcher": [], + "presentation": { + "focus": false + } + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_clean + jsonTaskData["command"] = buildData[self.bStr.buildToolsPath] + + return jsonTaskData + + ######################################################################################################################## + # Debugging and target control tasks + ######################################################################################################################## + def getBuildDownloadAndRunTask(self): + ''' + Create Build + Download and run task. Use 'dependsOn' feature to avoid doubling code. + Note: If multiple 'dependOn' tasks are defined, these tasks are launched simultaneously, + not chained one after another. + ''' + jsonTaskData = self.getDownloadAndRunTask() + + jsonTaskData["label"] = tmpStr.taskName_CPU_buildDownloadRun + jsonTaskData["dependsOn"] = tmpStr.taskName_build + + return jsonTaskData + + def getDownloadAndRunTask(self): + ''' + Create Download and run task. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "problemMatcher": [] + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_CPU_downloadRun + jsonTaskData["command"] = buildData[self.bStr.openOcdPath] + jsonTaskData["args"] = [] + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(buildData[self.bStr.openOcdInterfacePath]) + for arg in buildData[self.bStr.openOcdConfig]: + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(arg) + + # -c program filename [verify] [reset] [exit] [offset] ([] are optional arguments) + # Note: due problems with VS Code OpenOCD Tasks in case of workspace path containing spaces, target executable is passed + # as relative path. + workspacePath = utils.workspacePath + jsonTaskData["args"].append("-c") + programString = "program " + buildData[self.bStr.targetExecutablePath] + " verify reset exit" + jsonTaskData["args"].append(programString) + + return jsonTaskData + + def getResetAndRunTask(self): + ''' + Create CPU: Reset and run task. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "problemMatcher": [] + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_CPU_resetRun + jsonTaskData["command"] = buildData[self.bStr.openOcdPath] + jsonTaskData["args"] = [] + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(buildData[self.bStr.openOcdInterfacePath]) + for arg in buildData[self.bStr.openOcdConfig]: + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(arg) + jsonTaskData["args"].append("-c init") # init must be executed before other commands! + jsonTaskData["args"].append("-c reset") + jsonTaskData["args"].append("-c exit") + + return jsonTaskData + + def getHaltTask(self): + ''' + Create Halt/stop task. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "problemMatcher": [] + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_CPU_halt + jsonTaskData["command"] = buildData[self.bStr.openOcdPath] + jsonTaskData["args"] = [] + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(buildData[self.bStr.openOcdInterfacePath]) + for arg in buildData[self.bStr.openOcdConfig]: + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(arg) + + jsonTaskData["args"].append("-c init") # init must be executed before other commands! + jsonTaskData["args"].append("-c halt") + jsonTaskData["args"].append("-c exit") + + return jsonTaskData + + def getRunTask(self): + ''' + Create Run task. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "problemMatcher": [] + } + """ + jsonTaskData = json.loads(taskData) + + buildData = build.BuildData().getBuildData() + jsonTaskData["label"] = tmpStr.taskName_CPU_run + jsonTaskData["command"] = buildData[self.bStr.openOcdPath] + jsonTaskData["args"] = [] + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(buildData[self.bStr.openOcdInterfacePath]) + for arg in buildData[self.bStr.openOcdConfig]: + jsonTaskData["args"].append("-f") + jsonTaskData["args"].append(arg) + + jsonTaskData["args"].append("-c init") # init must be executed before other commands! + jsonTaskData["args"].append("-c resume") + jsonTaskData["args"].append("-c exit") + + return jsonTaskData + + ######################################################################################################################## + # Other tasks + ######################################################################################################################## + def getRunCurrentPythonFileTask(self): + ''' + Create Run Python file task, which runs current active Python file. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": [ + "${file}" + ], + "presentation": { + "focus": true + }, + "problemMatcher": [] + } + """ + buildData = build.BuildData().getBuildData() + jsonTaskData = json.loads(taskData) + jsonTaskData["label"] = tmpStr.taskName_Python + jsonTaskData["command"] = buildData[self.bStr.pythonExec] + + return jsonTaskData + + def getOpenCubeMXTask(self): + ''' + Create Open CubeMX project task. Starts with default program. + + Method of starting CubeMX differs across systems: + - WIN: use standard 'start' cmd command to start default program for '.ioc' files + - LINUX: does not associate itself with files by default. + Use a program like "Main Menu" for GNOME to add CubeMX to the applications list, + and then it can be selected as the default program for .ioc files. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": ["specified below"], + "presentation": { + "focus": false + }, + "problemMatcher": [] + } + """ + osIs = utils.detectOs() + if osIs == "unix": + openCubeCommand = "xdg-open" + elif osIs == "osx": + openCubeCommand = "/Applications/STMicroelectronics/STM32CubeMX.app/Contents/MacOs/STM32CubeMX" + else: + openCubeCommand = "start" + + jsonTaskData = json.loads(taskData) + jsonTaskData["label"] = tmpStr.taskName_OpenCubeMX + jsonTaskData["command"] = openCubeCommand + jsonTaskData["args"] = [utils.cubeMxProjectFilePath] # opens with default program + + return jsonTaskData + + def getUpdateTask(self): + ''' + Create Update workspace task, which runs update.py script. + ''' + taskData = """ + { + "label": "will be replaced with templateStrings string", + "type": "shell", + "command": "specified below", + "args": [ + "${workspaceFolder}/ideScripts/update.py" + ], + "presentation": { + "focus": true + }, + "problemMatcher": [] + } + """ + buildData = build.BuildData().getBuildData() + jsonTaskData = json.loads(taskData) + jsonTaskData["label"] = tmpStr.taskName_updateWorkspace + jsonTaskData["command"] = buildData[self.bStr.pythonExec] + + return jsonTaskData + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + bData = build.BuildData() + cP = wks.CProperties() + makefile = mkf.Makefile() + tasks = Tasks() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + # create taks file + tasks.checkTasksFile() + tasksData = tasks.getTasksData() + tasksData = tasks.addAllTasks(tasksData) + + tasks.overwriteTasksFile(tasksData) diff --git a/sw/ideScripts/updateWorkspaceFile.py b/sw/ideScripts/updateWorkspaceFile.py new file mode 100755 index 0000000..80a96f2 --- /dev/null +++ b/sw/ideScripts/updateWorkspaceFile.py @@ -0,0 +1,105 @@ +''' +Update existing VS Code workspace file with debug paths in "settings": + - "cortex-debug.armToolchainPath" + - "cortex-debug.openocdPath" +''' +import os +import json + +import utilities as utils +import updatePaths as pth +import updateBuildData as build + +__version__ = utils.__version__ + + +class UpdateWorkspaceFile(): + def __init__(self): + self.bStr = build.BuildDataStrings() + + def checkWorkspaceFile(self): + ''' + Check if workspace '*.code-workspace' file exists. If it does, check if it is a valid JSON file. + If it doesn't exist report error and quit. + ''' + workspaceFiles = utils.getCodeWorkspaces() + if len(workspaceFiles) == 1: + _, fileName = os.path.split(workspaceFiles[0]) + workspaceFileName, _ = os.path.splitext(fileName) + if utils.pathExists(utils.workspaceFilePath): + # file exists, check if it loads OK + try: + with open(utils.workspaceFilePath, 'r') as workspaceFile: + workspaceFileData = json.load(workspaceFile) + + print("Existing " + fileName + " file found.") + + except Exception as err: + errorMsg = "Invalid " + fileName + " file.\n" + errorMsg += "Possible cause: invalid json format or comments (not supported by this scripts). Error:\n" + errorMsg += str(err) + print(errorMsg) + + # else: verified in 'utils.verifyFolderStructure()' + + def getWorkspaceFileData(self): + ''' + Get data from current '*.code-workspace' file. + File existance is previoulsy checked in 'checkWorkspaceFile()'. + ''' + with open(utils.workspaceFilePath, 'r') as workspaceFile: + data = json.load(workspaceFile) + + return data + + def addBuildDataToWorkspaceFile(self, workspaceData, buildData): + ''' + This function ads "cortex-debug.*" items to workspace file, if they don't exist yet. + Returns new data. + ''' + armToolchainPath = os.path.dirname(buildData[self.bStr.gccExePath]) + armToolchainPath = utils.pathWithForwardSlashes(armToolchainPath) + + if 'settings' not in workspaceData: + workspaceData["settings"] = {} + + workspaceData["settings"]["cortex-debug.armToolchainPath"] = armToolchainPath + workspaceData["settings"]["cortex-debug.openocdPath"] = buildData[self.bStr.openOcdPath] + + return workspaceData + + def overwriteWorkspaceFile(self, data): + ''' + Overwrite existing '*.code-workspace' file with new data. + ''' + try: + with open(utils.workspaceFilePath, 'r+') as workspaceFile: + workspaceFile.seek(0) + workspaceFile.truncate() + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + workspaceFile.write(dataToWrite) + + print("'*.code-workspace' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting '*.code-workspace' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + bData = build.BuildData() + wksFile = UpdateWorkspaceFile() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + wksFile.checkWorkspaceFile() + wksData = wksFile.getWorkspaceFileData() + wksData = wksFile.addBuildDataToWorkspaceFile(wksData, buildData) + + wksFile.overwriteWorkspaceFile(wksData) diff --git a/sw/ideScripts/updateWorkspaceSources.py b/sw/ideScripts/updateWorkspaceSources.py new file mode 100755 index 0000000..894a393 --- /dev/null +++ b/sw/ideScripts/updateWorkspaceSources.py @@ -0,0 +1,222 @@ +''' +Update/generate 'c_cpp_properties.json' file in .vscode subfolder. + +See details in "README_DETAILS.md'. + +'c_cpp_properties.json' fields description: +https://github.com/Microsoft/vscode-cpptools/blob/master/Documentation/LanguageServer/c_cpp_properties.json.md +''' +import json + +import utilities as utils +import templateStrings as tmpStr + +import updatePaths as pth +import updateMakefile as mkf +import updateBuildData as build + +__version__ = utils.__version__ + + +class CPropertiesStrings(): + user_cSources = 'user_cSources' + user_asmSources = 'user_asmSources' + user_ldSources = 'user_ldSources' + + user_cIncludes = 'user_cIncludes' + user_asmIncludes = 'user_asmIncludes' + user_ldIncludes = 'user_ldIncludes' + + user_cDefines = 'user_cDefines' + user_asmDefines = 'user_asmDefines' + + user_cFlags = 'user_cFlags' + user_asmFlags = 'user_asmFlags' + user_ldFlags = 'user_ldFlags' + + cubemx_sourceFiles = 'cubemx_sourceFiles' + cubemx_includes = 'cubemx_includes' + cubemx_defines = 'cubemx_defines' + gccExePath = 'gccExePath' + gccIncludePath = 'gccIncludePath' + + +class CProperties(): + def __init__(self): + self.cPStr = CPropertiesStrings() + self.mkfStr = mkf.MakefileStrings() + self.bStr = build.BuildDataStrings() + + def checkCPropertiesFile(self): + ''' + Check if 'c_cpp_properties.json' file exists. If it does, check if it is a valid JSON file. + If it doesn't exist, create new according to template. + ''' + if utils.pathExists(utils.cPropertiesPath): + # file exists, check if it loads OK + try: + with open(utils.cPropertiesPath, 'r') as cPropertiesFile: + currentData = json.load(cPropertiesFile) + # this is a valid json file + print("Existing 'c_cpp_properties.json' file found.") + + # merge current 'c_cpp_properties.json' with its template + templateData = json.loads(tmpStr.c_cpp_template) + dataToWrite = utils.mergeCurrentDataWithTemplate(currentData, templateData) + dataToWrite = json.dumps(dataToWrite, indent=4, sort_keys=False) + with open(utils.cPropertiesPath, 'w') as cPropertiesFile: + cPropertiesFile.write(dataToWrite) + print("\tKeys updated according to the template.") + return + + except Exception as err: + errorMsg = "Invalid 'c_cpp_properties.json' file. Creating backup and new one.\n" + errorMsg += "Possible cause: invalid json format or comments (not supported by this scripts). Error:\n" + errorMsg += str(err) + print(errorMsg) + + utils.copyAndRename(utils.cPropertiesPath, utils.cPropertiesBackupPath) + + self.createCPropertiesFile() + + else: # 'c_cpp_properties.json' file does not exist jet, create it according to template string + self.createCPropertiesFile() + + def createCPropertiesFile(self): + ''' + Create fresh 'c_cpp_properties.json' file. + ''' + try: + with open(utils.cPropertiesPath, 'w') as cPropertiesFile: + data = json.loads(tmpStr.c_cpp_template) + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + + cPropertiesFile.seek(0) + cPropertiesFile.truncate() + cPropertiesFile.write(dataToWrite) + + print("New 'c_cpp_properties.json' file created.") + + except Exception as err: + errorMsg = "Exception error creating new 'c_cpp_properties.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def getCPropertiesData(self): + ''' + Get data from current 'c_cpp_properties.json' file. + File existance is previoulsy checked in 'checkCPropertiesFile()'. + ''' + with open(utils.cPropertiesPath, 'r') as cPropertiesFile: + data = json.load(cPropertiesFile) + + return data + + def getCPropertiesKeyData(self, cPropertiesData, keyName): + ''' + Try to get data of keyName field from 'c_cpp_properties.json' file. + Return list of data or empty list. + ''' + try: + cPropEnvData = cPropertiesData['env'] + return cPropEnvData[keyName] + except Exception as err: + errorMsg = "Unable to get '" + str(keyName) + "' data from 'c_cpp_properties.json' file." + print("WARNING:", errorMsg) + return [] + + def addMakefileDataToCPropertiesFile(self, cPropertiesData, makefileData): + ''' + Add data from Makefile to 'cubemx_...' fields in 'c_cpp_properties.json' file. + Returns new data to be written to 'c_cpp_properties.json' file. + ''' + # source files + sourceFiles = makefileData[self.mkfStr.cSources] + sourceFiles += makefileData[self.mkfStr.asmSources] + cPropertiesData["env"][self.cPStr.cubemx_sourceFiles] = sourceFiles + + # includes + includes = makefileData[self.mkfStr.cIncludes] + # includes += makefileData[self.mkfStr.asmIncludes] # TODO Should assembler includes be included here? + cPropertiesData["env"][self.cPStr.cubemx_includes] = includes + + # defines + defines = makefileData[self.mkfStr.cDefines] + # defines += makefileData[self.mkfStr.asmDefines] # TODO Should assembler defines be included here? + cPropertiesData["env"][self.cPStr.cubemx_defines] = defines + + return cPropertiesData + + def addBuildDataToCPropertiesFile(self, cPropertiesData, buildData): + ''' + Add data from buildData to tools fields in 'c_cpp_properties.json' file. + Returns new data to be written to 'c_cpp_properties.json' file. + ''' + # gcc + cPropertiesData["env"][self.cPStr.gccExePath] = buildData[self.bStr.gccExePath] + cPropertiesData["env"][self.cPStr.gccIncludePath] = buildData[self.bStr.gccInludePath] + + return cPropertiesData + + def overwriteCPropertiesFile(self, data): + ''' + Overwrite existing 'c_cpp_properties.json' file with new data. + ''' + try: + with open(utils.cPropertiesPath, 'r+') as cPropertiesFile: + cPropertiesFile.seek(0) + cPropertiesFile.truncate() + dataToWrite = json.dumps(data, indent=4, sort_keys=False) + cPropertiesFile.write(dataToWrite) + + print("'c_cpp_properties.json' file updated!") + + except Exception as err: + errorMsg = "Exception error overwriting 'c_cpp_properties.json' file:\n" + errorMsg += str(err) + utils.printAndQuit(errorMsg) + + def addCustomDataToCPropertiesFile(self, cProperties, makefileData, buildData): + ''' + TODO USER Add custom data to 'c_cpp_properties.json' file. + ''' + cProperties["configurations"][0]["name"] = utils.getWorkspaceName() + + # TODO USER can add other specific here + # Note: be careful not to override other parameters that are added from 'Makefile' and 'buildData.json' + + return cProperties + + +######################################################################################################################## +if __name__ == "__main__": + utils.verifyFolderStructure() + + paths = pth.UpdatePaths() + cP = CProperties() + makefile = mkf.Makefile() + bData = build.BuildData() + + # Makefile must exist + makefile.checkMakefileFile() # no point in continuing if Makefile does not exist + makefile.restoreOriginalMakefile() + + # build data (update tools paths if neccessary) + buildData = bData.prepareBuildData() + + # data from original makefile + makeExePath = buildData[bData.bStr.buildToolsPath] + gccExePath = buildData[bData.bStr.gccExePath] + makefileData = makefile.getMakefileData(makeExePath, gccExePath) + + # create 'c_cpp_properties.json' file + cP.checkCPropertiesFile() + cPropertiesData = cP.getCPropertiesData() + cPropertiesData = cP.addBuildDataToCPropertiesFile(cPropertiesData, buildData) + cPropertiesData = cP.addMakefileDataToCPropertiesFile(cPropertiesData, makefileData) + cPropertiesData = cP.addCustomDataToCPropertiesFile(cPropertiesData, makefileData, buildData) + cP.overwriteCPropertiesFile(cPropertiesData) + + # create build folder if it does not exist jet + buildFolderName = makefileData[mkf.MakefileStrings.buildDir] + utils.createBuildFolder(buildFolderName) diff --git a/sw/ideScripts/utilities.py b/sw/ideScripts/utilities.py new file mode 100755 index 0000000..aca7e02 --- /dev/null +++ b/sw/ideScripts/utilities.py @@ -0,0 +1,614 @@ +''' +Common utilities for 'update*.py' scripts. + +This script can be called standalone to verify if folder structure is correct and to print out all workspace +paths. +''' + +import os +import shutil +import subprocess +import sys +import traceback +import platform + +import templateStrings as tmpStr + +__version__ = '1.7' # this is inherited by all 'update*.py' scripts + +######################################################################################################################## +# Global utilities and paths +######################################################################################################################## + +workspacePath = None # absolute path to workspace folder +workspaceFilePath = None # absolute file path to '*.code-workspace' file +cubeMxProjectFilePath = None # absolute path to *.ioc STM32CubeMX workspace file +ideScriptsPath = None # absolute path to 'ideScripts' folder +vsCodeFolderPath = None # absolute path to workspace '.vscode' folder + +makefilePath = None +makefileBackupPath = None +cPropertiesPath = None +cPropertiesBackupPath = None +buildDataPath = None +toolsPaths = None # absolute path to toolsPaths.json with common user settings +tasksPath = None +tasksBackupPath = None +launchPath = None +launchBackupPath = None + + +def printAndQuit(msg): + ''' + Unrecoverable error, print and quit with system + ''' + msg = "\n**** ERROR (unrecoverable) ****\n" + str(msg) + print(msg) + + if sys.exc_info()[2]: # was exception raised? + print("\nTraceback:") + traceback.print_exc() + sys.exit(1) + + +def pathExists(path): + ''' + Checks if a path exists. + ''' + if path is not None: + return os.path.exists(path) + else: + return False + + +def commandExists(command): + ''' + Checks if a command exists. + ''' + if command is not None: + if shutil.which(command): + return True + + return False + + +def getFileName(path, withExtension=False, exception=True): + ''' + Returns file name of a given 'path', with or without extension. + If given path is not a file, exception is raised if 'exception' is set to True. Otherwise, None is returned. + ''' + if os.path.isfile(path): + _, fileNameExt = os.path.split(path) + if withExtension: + return fileNameExt + else: + fileName, _ = os.path.splitext(fileNameExt) + return fileName + else: + if exception: + errorMsg = "Cannot get a file name - given path is not a file:\n\t" + path + raise Exception(errorMsg) + else: + return None + + +def detectOs(): + ''' + This function detects the operating system that python is running in. We use this for OS specific operations + ''' + if platform.system() == "Darwin": + osIs = "osx" + elif os.name == "nt": + osIs = "windows" + elif os.name == "java": + osIs = "java" + elif os.name == "posix": + release = platform.release() # get system release + release = release.lower() + if release.endswith("microsoft"): # Detect windows subsystem for linux (wsl) + osIs = "wsl" + else: + osIs = "unix" + return osIs + + +def copyAndRename(filePath, newPath): + ''' + Copy file from 'filePath' to a new 'newPath'. + ''' + if not pathExists(filePath): + errorMsg = "Can't copy non-existing file: " + str(filePath) + printAndQuit(errorMsg) + + shutil.copyfile(filePath, newPath) + newFileName = getFileName(newPath) + msg = "Copy of file (new name: " + newFileName + "): " + str(filePath) + print(msg) + + +def verifyFolderStructure(): + ''' + Verify if folder structure is correct. + 'ideScript' folder must be placed in the root of the project, where: + - exactly one '*.code-workspace' file must exist (this is also Workspace name) + - '.vscode' folder is present (it is created if it doesn't exist jet) + + If this requirements are met, all paths are built - but not checked (they are checked in their respective .py files). + - build, launch, tasks, cpp properties files + - Makefile + - STM32CubeMX '.ioc' + - backup file paths + ''' + global workspacePath + global workspaceFilePath + global cubeMxProjectFilePath + global ideScriptsPath + global vsCodeFolderPath + + global makefilePath + global makefileBackupPath + global cPropertiesPath + global cPropertiesBackupPath + global buildDataPath + global toolsPaths + global tasksPath + global tasksBackupPath + global launchPath + global launchBackupPath + + thisFolderPath = os.path.dirname(sys.argv[0]) + workspacePath = pathWithForwardSlashes(os.path.dirname(thisFolderPath)) + ideScriptsPath = pathWithForwardSlashes(os.path.join(workspacePath, 'ideScripts')) + + codeWorkspaces = getCodeWorkspaces() + if len(codeWorkspaces) == 1: + # '*.code-workspace' file found + workspaceFilePath = codeWorkspaces[0] # file existance is previously checked in getCodeWorkspaces() + else: + errorMsg = "Invalid folder/file structure:\n" + errorMsg += "Exactly one VS Code workspace ('*.code-workspace') file must exist " + errorMsg += "in the root folder where 'ideScripts' folder is placed.\n" + errorMsg += "Expecting one '*.code-workspace' file in: " + workspacePath + printAndQuit(errorMsg) + + vscodeFolder = pathWithForwardSlashes(os.path.join(workspacePath, ".vscode")) + if not pathExists(vscodeFolder): + try: + os.mkdir(vscodeFolder) + print("'.vscode' folder created.") + except Exception as err: + errorMsg = "Exception error creating '.vscode' subfolder:\n" + str(err) + printAndQuit(errorMsg) + else: + print("Existing '.vscode' folder used.") + vsCodeFolderPath = vscodeFolder + + # 'ideScripts' folder found in the same folder as '*.code-workspace' file. Structure seems OK. + cPropertiesPath = os.path.join(workspacePath, '.vscode', 'c_cpp_properties.json') + cPropertiesPath = pathWithForwardSlashes(cPropertiesPath) + cPropertiesBackupPath = cPropertiesPath + ".backup" + + makefilePath = os.path.join(workspacePath, 'Makefile') + makefilePath = pathWithForwardSlashes(makefilePath) + makefileBackupPath = makefilePath + ".backup" + + buildDataPath = os.path.join(workspacePath, '.vscode', 'buildData.json') + buildDataPath = pathWithForwardSlashes(buildDataPath) + # does not have backup file, always regenerated + + osIs = detectOs() + if osIs == "windows": + vsCodeSettingsFolderPath = tmpStr.defaultVsCodeSettingsFolder_WIN + elif osIs == "unix": + vsCodeSettingsFolderPath = tmpStr.defaultVsCodeSettingsFolder_UNIX + elif osIs == "osx": + vsCodeSettingsFolderPath = tmpStr.defaultVsCodeSettingsFolder_OSX + toolsPaths = os.path.join(vsCodeSettingsFolderPath, 'toolsPaths.json') + toolsPaths = pathWithForwardSlashes(toolsPaths) + + tasksPath = os.path.join(workspacePath, '.vscode', 'tasks.json') + tasksPath = pathWithForwardSlashes(tasksPath) + tasksBackupPath = tasksPath + ".backup" + + launchPath = os.path.join(workspacePath, '.vscode', 'launch.json') + launchPath = pathWithForwardSlashes(launchPath) + launchBackupPath = launchPath + ".backup" + + cubeMxFiles = getCubeMXProjectFiles() + if len(cubeMxFiles) == 1: + cubeMxProjectFilePath = cubeMxFiles[0] + print("One STM32CubeMX file found: " + cubeMxProjectFilePath) + else: # more iocFiles: + cubeMxProjectFilePath = None + print("WARNING: None or more than one STM32CubeMX files found. None or one expected.") + + +def printWorkspacePaths(): + print("\nWorkspace root folder:", workspacePath) + print("VS Code workspace file:", workspaceFilePath) + print("CubeMX project file:", cubeMxProjectFilePath) + print("'ideScripts' folder:", ideScriptsPath) + + print("\n'Makefile':", makefilePath) + print("'Makefile.backup':", makefileBackupPath) + + print("\n'c_cpp_properties.json':", cPropertiesPath) + print("'c_cpp_properties.json.backup':", cPropertiesBackupPath) + print("\n'tasks.json':", tasksPath) + print("'tasks.json.backup':", tasksBackupPath) + print("\n'launch.json':", launchPath) + print("'launch.json.backup':", launchBackupPath) + + print("\n'buildData.json':", buildDataPath) + print("'toolsPaths.json':", toolsPaths) + print() + + +def getCubeMXProjectFiles(): + ''' + Returns list of all STM32CubeMX '.ioc' files in root directory. + Since only root directory is searched, all files (paths) are relative to root dir. + ''' + iocFiles = [] + for theFile in os.listdir(workspacePath): + if theFile.endswith('.ioc'): + iocFiles.append(theFile) + + return iocFiles + + +def createBuildFolder(folderName='build'): + ''' + Create (if not already created) build folder with specified name where objects are stored when 'make' is executed. + ''' + buildFolderPath = os.path.join(workspacePath, folderName) + buildFolderPath = pathWithForwardSlashes(buildFolderPath) + if not pathExists(buildFolderPath): + os.mkdir(buildFolderPath) + print("Build folder created: " + buildFolderPath) + else: + print("Build folder already exist: '" + buildFolderPath + "'") + + +def getCodeWorkspaces(): + ''' + Search workspacePath for files that ends with '.code-workspace' (VS Code workspaces). + Returns list of all available VS Code workspace paths. + + Only root directory is searched. + ''' + codeFiles = [] + + for theFile in os.listdir(workspacePath): + if theFile.endswith(".code-workspace"): + theFilePath = os.path.join(workspacePath, theFile) + codeFiles.append(pathWithForwardSlashes(theFilePath)) + + return codeFiles + + +def getWorkspaceName(): + ''' + Return name (without extension) for this project '.code-workspace' file. + + Return first available file name without extension. + ''' + return getFileName(workspaceFilePath) + + +def stripStartOfString(dataList, stringToStrip): + newData = [] + + for data in dataList: + if data.find(stringToStrip) != -1: + item = data[len(stringToStrip):] + newData.append(item) + else: + newData.append(data) + + return newData + + +def preappendString(data, stringToAppend): + if type(data) is list: + for itemIndex, item in enumerate(data): + data[itemIndex] = stringToAppend + item + else: + data = stringToAppend + data + + return data + + +def stringToList(string, separator): + ''' + Get list of unparsed string items into list. Strip any redundant spaces. + ''' + allItems = [] + items = string.split(separator) + for item in items: + item = item.strip() + allItems.append(item) + + return allItems + + +def mergeCurrentDataWithTemplate(currentData, templateData): + ''' + Merge all fields from both, currentData and templateData and return merged dict. + This is needed for backward compatibility and adding missing default fields. + ''' + def recursiveClone(template, data): + for key, value in data.items(): + if key not in template: + template[key] = {} # create a dict in case it must be copied recursively + + if isinstance(value, dict): + template[key] = recursiveClone(template[key], value) + else: + template[key] = value + return template + + mergedData = recursiveClone(templateData, currentData) + + return mergedData + + +def getYesNoAnswer(msg): + ''' + Asks the user a generic yes/no question. + Returns True for yes, False for no + ''' + while(True): + resp = input(msg).lower() + if resp == 'y': + return True + elif resp == 'n': + return False + else: + continue + + +def getUserPath(pathName): + ''' + Get path or command from user (by entering path in terminal window). + Repeated as long as user does not enter a valid path or command to file/folder/executable. + ''' + while True: + msg = "\n\tEnter path or command for '" + pathName + "':\n\tPaste here and press Enter: " + path = input(msg) + path = pathWithoutQuotes(path) + path = pathWithForwardSlashes(path) + + if pathExists(path): + break + elif commandExists(path): + break + else: + print("\tPath/command not valid: ", path) + + return path + + +def pathWithoutQuotes(path): + path = path.replace('\"', '') # remove " " + path = path.replace('\'', '') # remove ' ' + path = path.strip() # remove any redundant spaces + + return path + + +def pathWithForwardSlashes(path): + path = os.path.normpath(path) + path = path.replace("\\", "/") + return path + + +def getGccIncludePath(gccExePath): + ''' + Get path to '...\include' folder from 'gccExePath', where standard libs and headers. Needed for VS Code Intellisense. + + If ARM GCC folder structure remains the same as official, the executable is located in \bin folder. + Other headers can be found in '\lib\gcc\arm-none-eabi\***\include' folder, which is found by searching for + <stdint.h>. + ''' + gccExeFolderPath = os.path.dirname(gccExePath) + gccFolderPath = os.path.dirname(gccExeFolderPath) + searchPath = os.path.join(gccFolderPath, "lib", "gcc", "arm-none-eabi") + + fileName = "stdint.h" + filePath = findFileInFolderTree(searchPath, fileName) + if filePath is None: + errorMsg = "Unable to find " + fileName + " file on path: " + searchPath + errorMsg += "\nOfficial GCC folder structure must remain intact!" + printAndQuit(errorMsg) + + folderPath = os.path.dirname(filePath) + return folderPath + + +def getPython3Executable(): + ''' + Uses detectOs() to determine the correct python command to use for python related tasks + ''' + osIs = detectOs() + + if osIs == "unix" or osIs == "wsl" or osIs=="osx": # detected unix based system + pythonExec = "python3" + else: # windows or other system + pythonExec = "python" + + if not commandExists(pythonExec): + msg = "\n\tPython version 3 or later installation not detected, please install or enter custom path/command below." + print(msg) + pythonExec = getUserPath(pythonExec) + + return pythonExec + + +def getOpenOcdInterface(openOcdPath): + ''' + Try to get OpenOCD interface file (TODO: currently hard-coded 'stlink.cfg') from 'openocd.exe' (openOcdPath) path. + If such path can't be found ask user for update. + Returns absolute path to 'stlink.cfg' file. + ''' + openOcdExeFolderPath = os.path.dirname(openOcdPath) # ../bin + openOcdRootPath = os.path.dirname(openOcdExeFolderPath) # ../ + # interfaceFolderPath = os.path.join(openOcdRootPath, 'scripts', 'interface') # only on windwos, linux has different structure + + # get openOcdInterfacePath from + # TODO here of once anything other than stlink will be supported + fileName = "stlink.cfg" + openOcdInterfacePath = findFileInFolderTree(openOcdRootPath, fileName) + if openOcdInterfacePath is None: + openOcdInterfacePath = getUserPath("stlink.cfg interface") + + return openOcdInterfacePath + + +def getOpenOcdConfig(openOcdInterfacePath): + ''' + Get openOCD configuration files from user, eg. 'interface/stlink.cfg, target/stm32f0x.cfg' + Paths can be passed in absolute or relative form, separated by comma. Optionally enclosed in " or '. + Returns the list of absolute paths to these config files. + ''' + openOcdScriptsPath = os.path.dirname(os.path.dirname(openOcdInterfacePath)) + + while(True): + msg = "\n\tEnter path(s) to OpenOCD configuration file(s):\n\t\t" + msg += "Example: 'target/stm32f0x.cfg'. Absolute or relative to OpenOCD /scripts/ folder.\n\t\t" + msg += "If more than one file is needed, separate with comma.\n\t\t" + msg += "Paste here and press Enter: " + configFilesStr = input(msg) + + allConfigFiles = [] + configFiles = configFilesStr.split(',') + for theFile in configFiles: + # ex.: " C:/asd/foo bar/fail.cfg " , ' C:/asd/bar foo/fail.cfg' , + theFile = theFile.strip() + theFile = theFile.strip('\'') + theFile = theFile.strip('\"') + theFile = theFile.strip() + theFile = pathWithForwardSlashes(theFile) + + if pathExists(theFile): # file is an absolute path + allConfigFiles.append(theFile) + else: + # arg is a relative path. Must be relative to OpenOCD 'scripts' folder + theFileAbs = os.path.join(openOcdScriptsPath, theFile) + theFileAbs = pathWithForwardSlashes(theFileAbs) + if pathExists(theFileAbs): + allConfigFiles.append(theFileAbs) + else: + msg = "\tConfiguration invalid (file not found): \'" + theFileAbs + "\'" + print(msg) + break + else: + break # break loop if config detected successfully + continue # continue if unsuccessful + + return allConfigFiles + + +def getStm32SvdFile(stm32SvdPath): + ''' # TODO HERE - deprecated? no use cases? + Get stm32SvdFile from user, eg. 'STM32F042x.svd' + Validates that file exists + ''' + while True: + msg = "\n\tEnter SVD File name (eg: 'STM32F042x.svd'), or 'ls' to list available SVD files.\n\tSVD file name: " + fileName = input(msg) + + if fileName == "ls": + print(os.listdir(stm32SvdPath)) + continue + + stm32SvdFilePath = os.path.join(stm32SvdPath, fileName) + stm32SvdFilePath = pathWithForwardSlashes(stm32SvdFilePath) + + if pathExists(stm32SvdFilePath): + break + else: + print("\tSVD File '" + fileName + "' not found") + continue + + return fileName + + +def getBuildElfFilePath(buildDirPath, projectName): + ''' + Returns .elf file path. + ''' + elfFile = projectName + ".elf" + buildFileName = os.path.join(buildDirPath, elfFile) + buildFileName = pathWithForwardSlashes(buildFileName) + + return buildFileName + + +def getAllFilesInFolderTree(pathToFolder): + ''' + Get the list of all files in directory tree at given path + ''' + allFiles = [] + if os.path.exists(pathToFolder): + for (dirPath, dirNames, fileNames) in os.walk(pathToFolder): + for theFile in fileNames: + filePath = os.path.join(dirPath, theFile) + filePath = pathWithForwardSlashes(filePath) + allFiles.append(filePath) + + return allFiles + + +def findFileInFolderTree(searchPath, fileName): + ''' + Find a file in a folder or subfolders, and return absolute path to the file. + Returns None if unsuccessful. + ''' + + for root, dirs, files in os.walk(searchPath, topdown=False): + if fileName in files: + filePath = os.path.join(root, fileName) + filePath = pathWithForwardSlashes(filePath) + return filePath + + return None + + +def findExecutablePath(extension, raiseException=False): + ''' + Find default associated path of a given file extension, for example 'pdf'. + ''' + arguments = "for /f \"delims== tokens=2\" %a in (\'assoc " + arguments += "." + extension + arguments += "\') do @ftype %a" + + errorMsg = "Unable to get associated program for ." + extension + "." + try: + proc = subprocess.run(arguments, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + if proc.returncode == 0: + returnString = str(proc.stdout) + path = returnString.split('=')[1] + path = path.split('\"')[0] + path = path.strip() + path = os.path.normpath(path) + if os.path.exists(path): + return path + else: + print(errorMsg) + + except Exception as err: + errorMsg += "Exception:\n" + str(err) + + if raiseException: + raise Exception(errorMsg) + else: + return None + + +######################################################################################################################## +if __name__ == "__main__": + print("Workspace generation script version: " + __version__) + verifyFolderStructure() + print("This workspace name:", getWorkspaceName()) + printWorkspacePaths() |
