diff --git a/ue4cli/CachedDataManager.py b/ue4cli/CachedDataManager.py index 34678a7..9b03753 100644 --- a/ue4cli/CachedDataManager.py +++ b/ue4cli/CachedDataManager.py @@ -1,7 +1,6 @@ from .ConfigurationManager import ConfigurationManager from .JsonDataManager import JsonDataManager -from .Utility import Utility -import os +import os, shutil class CachedDataManager(object): """ @@ -14,7 +13,7 @@ def clearCache(): Clears any cached data we have stored about specific engine versions """ if os.path.exists(CachedDataManager._cacheDir()) == True: - Utility.removeDir(CachedDataManager._cacheDir()) + shutil.rmtree(CachedDataManager._cacheDir()) @staticmethod def getCachedDataKey(engineVersionHash, key): diff --git a/ue4cli/JsonDataManager.py b/ue4cli/JsonDataManager.py index 049f762..53fbb77 100644 --- a/ue4cli/JsonDataManager.py +++ b/ue4cli/JsonDataManager.py @@ -1,7 +1,6 @@ from .UnrealManagerException import UnrealManagerException from .Utility import Utility -from .UtilityException import UtilityException -import json, os +import json, os, platform class JsonDataManager(object): """ @@ -13,19 +12,6 @@ def __init__(self, jsonFile): Creates a new JsonDataManager instance for the specified JSON file """ self.jsonFile = jsonFile - - def loads(self): - """ - Reads and loads owned jsonFile - """ - try: - path = self.jsonFile - file = Utility.readFile(path) - return json.loads(file) - except json.JSONDecodeError as e: - # FIXME: This is the only place outside of Utility class where we use UtilityException. - # Not worth to create new Exception class for only one single case, at least not now. - raise UtilityException(f'failed to load "{str(path)}" due to: ({type(e).__name__}) {str(e)}') from e def getKey(self, key): """ @@ -42,7 +28,10 @@ def getDictionary(self): Retrieves the entire data dictionary """ if os.path.exists(self.jsonFile): - return self.loads() + try: + return json.loads(Utility.readFile(self.jsonFile)) + except json.JSONDecodeError as err: + raise UnrealManagerException('malformed JSON configuration file "{}" ({})'.format(self.jsonFile, err)) else: return {} @@ -62,7 +51,7 @@ def setDictionary(self, data): # Create the directory containing the JSON file if it doesn't already exist jsonDir = os.path.dirname(self.jsonFile) if os.path.exists(jsonDir) == False: - Utility.makeDirs(jsonDir) + os.makedirs(jsonDir) # Store the dictionary Utility.writeFile(self.jsonFile, json.dumps(data)) diff --git a/ue4cli/UE4BuildInterrogator.py b/ue4cli/UE4BuildInterrogator.py index 098f056..12a42d8 100644 --- a/ue4cli/UE4BuildInterrogator.py +++ b/ue4cli/UE4BuildInterrogator.py @@ -2,15 +2,11 @@ from .UnrealManagerException import UnrealManagerException from .CachedDataManager import CachedDataManager from .Utility import Utility -from .UtilityException import UtilityException -from .JsonDataManager import JsonDataManager -import os, tempfile +import json, os, platform, shutil, tempfile class UE4BuildInterrogator(object): def __init__(self, engineRoot, engineVersion, engineVersionHash, runUBTFunc): - # WARN: os.path.realpath can potentially fail with OSError, - # but if it ever happens, this is most likely bug in our code self.engineRoot = os.path.realpath(engineRoot) self.engineSourceDir = 'Engine/Source/' self.engineVersion = engineVersion @@ -164,7 +160,7 @@ def _getThirdPartyLibs(self, platformIdentifier, configuration): sentinelBackup = sentinelFile + '.bak' renameSentinel = os.path.exists(sentinelFile) and os.environ.get('UE4CLI_SENTINEL_RENAME', '0') == '1' if renameSentinel == True: - Utility.moveFile(sentinelFile, sentinelBackup) + shutil.move(sentinelFile, sentinelBackup) # Invoke UnrealBuildTool in JSON export mode (make sure we specify gathering mode, since this is a prerequisite of JSON export) # (Ensure we always perform sentinel file cleanup even when errors occur) @@ -176,10 +172,10 @@ def _getThirdPartyLibs(self, platformIdentifier, configuration): self.runUBTFunc('UE4Editor', platformIdentifier, configuration, args) finally: if renameSentinel == True: - Utility.moveFile(sentinelBackup, sentinelFile) + shutil.move(sentinelBackup, sentinelFile) # Parse the JSON output - result = JsonDataManager(jsonFile).loads() + result = json.loads(Utility.readFile(jsonFile)) # Extract the list of third-party library modules # (Note that since UE4.21, modules no longer have a "Type" field, so we must @@ -192,7 +188,7 @@ def _getThirdPartyLibs(self, platformIdentifier, configuration): # Remove the temp directory try: - Utility.removeDir(tempDir) + shutil.rmtree(tempDir) except: pass diff --git a/ue4cli/UnrealManagerBase.py b/ue4cli/UnrealManagerBase.py index b66d994..5b71c53 100644 --- a/ue4cli/UnrealManagerBase.py +++ b/ue4cli/UnrealManagerBase.py @@ -5,9 +5,7 @@ from .CachedDataManager import CachedDataManager from .CMakeCustomFlags import CMakeCustomFlags from .Utility import Utility -from .JsonDataManager import JsonDataManager -from .UtilityException import UtilityException -import glob, hashlib, json, os, re +import glob, hashlib, json, os, re, shutil, sys class UnrealManagerBase(object): """ @@ -59,13 +57,13 @@ def setEngineRootOverride(self, rootDir): # Set the new root directory rootDir = os.path.abspath(rootDir) ConfigurationManager.setConfigKey('rootDirOverride', rootDir) - Utility.printStderr('Setting engine root path override:', str(rootDir)) + print('Set engine root path override: {}'.format(rootDir)) # Check that the specified directory is valid and warn the user if it is not try: self.getEngineVersion() except: - raise UnrealManagerException('the specified directory does not appear to contain a valid version of the Unreal Engine.') from None + print('Warning: the specified directory does not appear to contain a valid version of the Unreal Engine.') def clearEngineRootOverride(self): """ @@ -109,7 +107,7 @@ def getEngineVersion(self, outputFormat = 'full'): # Verify that the requested output format is valid if outputFormat not in formats: - raise UnrealManagerException(f'unreconised version output format "{str(outputFormat)}"') + raise Exception('unreconised version output format "{}"'.format(outputFormat)) return formats[outputFormat] @@ -193,7 +191,7 @@ def getDescriptor(self, dir): try: return self.getPluginDescriptor(dir) except: - raise UnrealManagerException(f'could not detect an Unreal project or plugin in the directory "{str(dir)}"') from None + raise UnrealManagerException('could not detect an Unreal project or plugin in the directory "{}"'.format(dir)) def isProject(self, descriptor): """ @@ -335,7 +333,8 @@ def generateProjectFiles(self, dir=os.getcwd(), args=[]): # If the project is a pure Blueprint project, then we cannot generate project files if os.path.exists(os.path.join(dir, 'Source')) == False: - raise UnrealManagerException('Pure Blueprint project, nothing to generate project files for.') + Utility.printStderr('Pure Blueprint project, nothing to generate project files for.') + return # Generate the project files genScript = self.getGenerateScript() @@ -352,8 +351,8 @@ def cleanDescriptor(self, dir=os.getcwd()): # Because performing a clean will also delete the engine build itself when using # a source build, we simply delete the `Binaries` and `Intermediate` directories - Utility.removeDir(os.path.join(dir, 'Binaries'), ignore_errors=True) - Utility.removeDir(os.path.join(dir, 'Intermediate'), ignore_errors=True) + shutil.rmtree(os.path.join(dir, 'Binaries'), ignore_errors=True) + shutil.rmtree(os.path.join(dir, 'Intermediate'), ignore_errors=True) # If we are cleaning a project, also clean any plugins if self.isProject(descriptor): @@ -372,12 +371,12 @@ def buildDescriptor(self, dir=os.getcwd(), configuration='Development', target=' # If the project or plugin is Blueprint-only, there is no C++ code to build if os.path.exists(os.path.join(dir, 'Source')) == False: - raise UnrealManagerException(f'Pure Blueprint {str(descriptorType)}, nothing to build.') + Utility.printStderr('Pure Blueprint {}, nothing to build.'.format(descriptorType)) + return # Verify that the specified build configuration is valid if configuration not in self.validBuildConfigurations(): - raise UnrealManagerException(f'invalid build configuration "{str(configuration)}"') - + raise UnrealManagerException('invalid build configuration "' + configuration + '"') # Verify that the specified build target is valid if target not in self.validBuildTargets(): raise UnrealManagerException('invalid build target "' + target + '"') @@ -433,7 +432,7 @@ def packageProject(self, dir=os.getcwd(), configuration='Shipping', extraArgs=[] # Verify that the specified build configuration is valid if configuration not in self.validBuildConfigurations(): - raise UnrealManagerException(f'invalid build configuration "{str(configuration)}"') + raise UnrealManagerException('invalid build configuration "' + configuration + '"') # Strip out the `-NoCompileEditor` flag if the user has specified it, since the Development version # of the Editor modules for the project are needed in order to run the commandlet that cooks content @@ -563,12 +562,10 @@ def listAutomationTests(self, projectFile): # Detect if the Editor terminated abnormally (i.e. not triggered by `automation quit`) # In Unreal Engine 4.27.0, the exit method changed from RequestExit to RequestExitWithStatus if 'PlatformMisc::RequestExit(' not in logOutput.stdout and 'PlatformMisc::RequestExitWithStatus(' not in logOutput.stdout: - Utility.printStderr("Warning: abnormal Editor termination detected!") - Utility.printStderr("printing stdout..") - print(logOutput.stdout) - Utility.printStderr("printing stderr..") - print(logOutput.stderr) - raise UnrealManagerException('failed to retrieve the list of automation tests!') + raise RuntimeError( + 'failed to retrieve the list of automation tests!' + + ' stdout was: "{}", stderr was: "{}"'.format(logOutput.stdout, logOutput.stderr) + ) return sorted(list(tests)) @@ -583,7 +580,7 @@ def automationTests(self, dir=os.getcwd(), args=[]): # Verify that at least one argument was supplied if len(args) == 0: - raise UnrealManagerException('at least one test name must be specified') + raise RuntimeError('at least one test name must be specified') # Gather any additional arguments to pass directly to the Editor extraArgs = [] @@ -594,12 +591,7 @@ def automationTests(self, dir=os.getcwd(), args=[]): # Build the project if it isn't already built Utility.printStderr('Ensuring project is built...') - try: - self.buildDescriptor(dir, suppressOutput=True) - except UnrealManagerException: - # FIXME: Ideally, we should NOT catch every UnrealManagerException here - # This is currently a limitation of our API that uses only one Exception class for multiple different cases - pass + self.buildDescriptor(dir, suppressOutput=True) # Determine which arguments we are passing to the automation test commandlet projectFile = self.getProjectDescriptor(dir) @@ -637,7 +629,7 @@ def automationTests(self, dir=os.getcwd(), args=[]): # Detect abnormal exit conditions (those not triggered by `automation quit`) # In Unreal Engine 4.27.0, the exit method changed from RequestExit to RequestExitWithStatus if 'PlatformMisc::RequestExit(' not in logOutput.stdout and 'PlatformMisc::RequestExitWithStatus(' not in logOutput.stdout: - raise UnrealManagerException('abnormal exit condition detected') + sys.exit(1) # Since UE4 doesn't consistently produce accurate exit codes across all platforms, we need to rely on # text-based heuristics to detect failed automation tests or errors related to not finding any tests to run @@ -650,12 +642,12 @@ def automationTests(self, dir=os.getcwd(), args=[]): ] for errorStr in errorStrings: if errorStr in logOutput.stdout: - raise UnrealManagerException('abnormal exit condition detected') + sys.exit(1) # If an explicit exit code was specified in the output text then identify it and propagate it - match = re.search('TEST COMPLETE\\. EXIT CODE: ([1-9]+)', logOutput.stdout + logOutput.stderr) + match = re.search('TEST COMPLETE\\. EXIT CODE: ([0-9]+)', logOutput.stdout + logOutput.stderr) if match is not None: - raise UnrealManagerException('abnormal exit condition detected') + sys.exit(int(match.group(1))) # "Protected" methods @@ -670,7 +662,7 @@ def _getEngineVersionDetails(self): Parses the JSON version details for the latest installed version of UE4 """ versionFile = os.path.join(self.getEngineRoot(), 'Engine', 'Build', 'Build.version') - return JsonDataManager(versionFile).loads() + return json.loads(Utility.readFile(versionFile)) def _getEngineVersionHash(self): """ diff --git a/ue4cli/UnrealManagerWindows.py b/ue4cli/UnrealManagerWindows.py index e7a2101..535c69b 100644 --- a/ue4cli/UnrealManagerWindows.py +++ b/ue4cli/UnrealManagerWindows.py @@ -42,7 +42,7 @@ def getGenerateScript(self): except: pass - raise UnrealManagerException('could not detect the location of GenerateProjectFiles.bat or UnrealVersionSelector.exe. This typically indicates that .uproject files are not correctly associated with UE4.') + raise UnrealManagerException('could not detect the location of GenerateProjectFiles.bat or UnrealVersionSelector.exe.\nThis typically indicates that .uproject files are not correctly associated with UE4.') def getRunUATScript(self): return self.getEngineRoot() + '\\Engine\\Build\\BatchFiles\\RunUAT.bat' @@ -52,7 +52,7 @@ def generateProjectFiles(self, dir=os.getcwd(), args=[]): # If we are using our custom batch file, use the appropriate arguments genScript = self.getGenerateScript() projectFile = self.getProjectDescriptor(dir) - Utility.printStderr('Using project file:', projectFile) + print(projectFile) if '.ue4\\GenerateProjectFiles.bat' in genScript: Utility.run([genScript, projectFile], raiseOnError=True) else: @@ -91,7 +91,7 @@ def _customBatchScriptDir(self): # If the script directory doesn't already exist, attempt to create it scriptDir = os.path.join(os.environ['HOMEDRIVE'] + os.environ['HOMEPATH'], '.ue4') try: - Utility.makeDirs(scriptDir) + os.makedirs(scriptDir) except: pass diff --git a/ue4cli/Utility.py b/ue4cli/Utility.py index d8989da..65e4e12 100644 --- a/ue4cli/Utility.py +++ b/ue4cli/Utility.py @@ -1,5 +1,4 @@ -from .UtilityException import UtilityException -import os, platform, shlex, subprocess, sys, shutil +import os, platform, shlex, subprocess, sys class CommandOutput(object): """ @@ -22,39 +21,23 @@ def printStderr(*args, **kwargs): Prints to stderr instead of stdout """ if os.environ.get('UE4CLI_QUIET', '0') != '1': - print('(ue4cli)', *args, file=sys.stderr, **kwargs) + print(*args, file=sys.stderr, **kwargs) @staticmethod def readFile(filename): """ Reads data from a file """ - try: - with open(filename, 'rb') as f: - return f.read().decode('utf-8') - except OSError as e: - raise UtilityException(f'failed to read file "{str(filename)}" due to: ({type(e).__name__}) {str(e)}') from e + with open(filename, 'rb') as f: + return f.read().decode('utf-8') @staticmethod def writeFile(filename, data): """ Writes data to a file """ - try: - with open(filename, 'wb') as f: - f.write(data.encode('utf-8')) - except OSError as e: - raise UtilityException(f'failed to write file "{str(filename)}" due to: ({type(e).__name__}) {str(e)}') from e - - @staticmethod - def moveFile(src, dst): - """ - Moves file from 'src' to 'dst' - """ - try: - shutil.move(src, dst) - except OSError as e: - raise UtilityException(f'failed to move file from "{str(src)}" to "{str(dst)}" due to: ({type(e).__name__}) {str(e)}') from e + with open(filename, 'wb') as f: + f.write(data.encode('utf-8')) @staticmethod def patchFile(filename, replacements): @@ -69,26 +52,6 @@ def patchFile(filename, replacements): Utility.writeFile(filename, patched) - @staticmethod - def removeDir(path, ignore_errors=False): - """ - Recursively remove directory tree - """ - try: - shutil.rmtree(path, ignore_errors) - except OSError as e: - raise UtilityException(f'failed to remove directory "{str(path)}" due to: ({type(e).__name__}) {str(e)}') from e - - @staticmethod - def makeDirs(name, mode=0o777, exist_ok=False): - """ - Makes directory - """ - try: - os.makedirs(name, mode, exist_ok) - except OSError as e: - raise UtilityException(f'failed to create directory "{str(name)}" due to: ({type(e).__name__}) {str(e)}') from e - @staticmethod def forwardSlashes(paths): """ @@ -160,12 +123,12 @@ def capture(command, input=None, cwd=None, shell=False, raiseOnError=False): # If the child process failed and we were asked to raise an exception, do so if raiseOnError == True and proc.returncode != 0: - Utility.printStderr("Warning: child process failure encountered!") - Utility.printStderr("printing stdout..") - print(stdout) - Utility.printStderr("printing stderr..") - print(stderr) - raise UtilityException(f'child process {str(command)} failed with exit code {str(proc.returncode)}') + raise Exception( + 'child process ' + str(command) + + ' failed with exit code ' + str(proc.returncode) + + '\nstdout: "' + stdout + '"' + + '\nstderr: "' + stderr + '"' + ) return CommandOutput(proc.returncode, stdout, stderr) @@ -180,7 +143,7 @@ def run(command, cwd=None, shell=False, raiseOnError=False): returncode = subprocess.call(command, cwd=cwd, shell=shell) if raiseOnError == True and returncode != 0: - raise UtilityException(f'child process {str(command)} failed with exit code {str(returncode)}') + raise Exception('child process ' + str(command) + ' failed with exit code ' + str(returncode)) return returncode @staticmethod @@ -189,4 +152,4 @@ def _printCommand(command): Prints a command if verbose output is enabled """ if os.environ.get('UE4CLI_VERBOSE', '0') == '1': - Utility.printStderr('EXECUTE COMMAND:', command) + Utility.printStderr('[UE4CLI] EXECUTE COMMAND:', command) diff --git a/ue4cli/UtilityException.py b/ue4cli/UtilityException.py deleted file mode 100644 index 53af630..0000000 --- a/ue4cli/UtilityException.py +++ /dev/null @@ -1,2 +0,0 @@ -class UtilityException(Exception): - pass diff --git a/ue4cli/cli.py b/ue4cli/cli.py index 8c6d44e..cb0e215 100644 --- a/ue4cli/cli.py +++ b/ue4cli/cli.py @@ -2,9 +2,7 @@ from .PluginManager import PluginManager from .UnrealManagerException import UnrealManagerException from .UnrealManagerFactory import UnrealManagerFactory -from .Utility import Utility -from .UtilityException import UtilityException -import os, sys, logging +import os, sys # Our list of supported commands SUPPORTED_COMMANDS = { @@ -203,10 +201,8 @@ def displayHelp(): print() def main(): - - logger = logging.getLogger(__name__) - try: + # Perform plugin detection and register our detected plugins plugins = PluginManager.getPlugins() for command in plugins: @@ -226,22 +222,8 @@ def main(): if command in SUPPORTED_COMMANDS: SUPPORTED_COMMANDS[command]['action'](manager, args) else: - # FIXME: This is the only place outside of UnrealManager... classes where we use UnrealManagerException. - # Not worth to create new Exception class for only one single case, at least not now. - raise UnrealManagerException(f'unrecognised command "{str(command)}"') from None - - except ( - UnrealManagerException, - UtilityException, - KeyboardInterrupt, - ) as e: - Utility.printStderr(f'Error: ({type(e).__name__}) {str(e)}') - sys.exit(1) - - except BaseException as e: - Utility.printStderr('Unhandled exception! Crashing...') - logging.basicConfig(level=logging.DEBUG) - logger.exception(e) - Utility.printStderr('ue4cli has crashed! Please, report it at: https://github.com/adamrehn/ue4cli/issues') + raise UnrealManagerException('unrecognised command "' + command + '"') + + except UnrealManagerException as e: + print('Error: ' + str(e)) sys.exit(1) -