''' 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 . ''' 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()