class LocalFolderReleaseSource: _log = Inject('Logger') _sys = Inject('SystemHelper') _extractor = Inject('UnityPackageExtractor') _packageAnalyzer = Inject('UnityPackageAnalyzer') def __init__(self, folderPath): self._folderPath = folderPath self._files = [] @property def releases(self): return [x.release for x in self._files] def init(self): self._log.heading('Initializing release source for local folder') self._log.debug('Initializing release source for local folder "{0}"', self._folderPath) for path in self._sys.findFilesByPattern(self._folderPath, '*.unitypackage'): release = self._packageAnalyzer.getReleaseInfoFromUnityPackage(path) self._files.append(FileInfo(path, release)) self._log.info("Found {0} released in folder '{1}'", len(self._files), self._folderPath) def getName(self): return "Local Folder ({0})".format(self._folderPath) # Should return the chosen name for the package # If forcedName is non-null then this should always be the value of forcedName def installRelease(self, releaseInfo, forcedName): fileInfo = next(x for x in self._files if x.release == releaseInfo) assertIsNotNone(fileInfo) return self._extractor.extractUnityPackage(fileInfo.path, releaseInfo.name, forcedName)
class JunctionHelper: """ Misc. helper functions related to windows junctions """ _varMgr = Inject('VarManager') _log = Inject('Logger') _sys = Inject('SystemHelper') def __init__(self): pass def removeJunction(self, linkDir): linkDir = self._varMgr.expand(linkDir) if os.path.isdir(linkDir) and JunctionUtil.islink(linkDir): try: # Use rmdir not python unlink to ensure we don't delete the link source self._sys.executeShellCommand('rmdir "{0}"'.format(linkDir)) except Exception as e: raise Exception( 'Failed while attempting to delete junction "{0}":\n{1}'. format(linkDir, str(e))) from e return True return False def makeJunction(self, actualPath, linkPath): actualPath = self._varMgr.expandPath(actualPath) linkPath = self._varMgr.expandPath(linkPath) self._sys.makeMissingDirectoriesInPath(linkPath) self._log.debug( 'Making junction with actual path ({0}) and new link path ({1})'. format(linkPath, actualPath)) # Note: mklink is a shell command and can't be executed otherwise self._sys.executeShellCommand('mklink /J "{0}" "{1}"'.format( linkPath, actualPath)) def removeJunctionsInDirectory(self, dirPath, recursive): fullDirPath = self._varMgr.expandPath(dirPath) if not os.path.exists(fullDirPath): return for name in os.listdir(fullDirPath): fullPath = os.path.join(fullDirPath, name) if not os.path.isdir(fullPath): continue if self.removeJunction(fullPath): if os.path.exists(fullPath + '.meta'): os.remove(fullPath + '.meta') self._log.debug( 'Removed directory for package "{0}"'.format(name)) else: if recursive: self.removeJunctionsInDirectory(fullPath, True)
class Runner: _log = Inject('Logger') _sys = Inject('SystemHelper') def run(self, args): self._args = args for filePath in self._sys.findFilesByPattern(self._args.directory, '*.py'): self._sys.executeAndWait( 'autoflake --in-place --remove-unused-variables "{0}"'.format( filePath))
class RemoteServerReleaseSource: _log = Inject('Logger') _sys = Inject('SystemHelper') _packageExtractor = Inject('UnityPackageExtractor') def __init__(self, manifestUrl): self._manifestUrl = manifestUrl self._releaseInfos = [] @property def releases(self): return self._releaseInfos def init(self): self._log.heading("Initializing remote server release source") self._log.debug("Initializing remote server release source with URL '{0}'", self._manifestUrl) response = urllib.request.urlopen(self._manifestUrl) manifestData = response.read().decode('utf-8') self._log.debug("Got manifest with data: \n{0}".format(manifestData)) self._manifest = YamlSerializer.deserialize(manifestData) for info in self._manifest.releases: info.url = urllib.parse.urljoin(self._manifestUrl, info.localPath) info.localPath = None self._releaseInfos.append(info) def getName(self): return "File Server" def installRelease(self, releaseInfo, forcedName): assertThat(releaseInfo.url) self._log.heading("Downloading release from url '{0}'".format(releaseInfo.url)) try: with tempfile.NamedTemporaryFile(delete=False, suffix='.unitypackage') as tempFile: tempFilePath = tempFile.name self._log.debug("Downloading url to temporary file '{0}'".format(tempFilePath)) urllib.request.urlretrieve(releaseInfo.url, tempFilePath) return self._packageExtractor.extractUnityPackage(tempFilePath, releaseInfo.name, forcedName) finally: if os.path.exists(tempFilePath): os.remove(tempFilePath)
class Test2: qux = Inject('Qux') def __init__(self): self.X = 0 def Run(self): print(self.qux.GetValue())
class Foo1: foo2 = Inject('Foo2') def __init__(self, val): self.val = val def Start(self): print(self.foo2.val)
class Foo2: foo1 = Inject('Foo1') def __init__(self, val): self.val = val def Start(self): print('foo2 {0}, foo1 {1}'.format(self.val, self.foo1.val))
class CommonSettings: _config = Inject('Config') def __init__(self): self.maxProjectNameLength = self._config.getInt('MaxProjectNameLength') def getShortProjectName(self, val): return val[0:self.maxProjectNameLength]
class Runner: _scriptRunner = Inject('ScriptRunner') _log = Inject('Logger') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _vsSolutionHelper = Inject('VisualStudioHelper') def run(self, args): self._args = args success = self._scriptRunner.runWrapper(self._runInternal) if not success: sys.exit(1) def _runInternal(self): self._log.debug("Started OpenInVisualStudio with arguments: {0}".format(" ".join(sys.argv[1:]))) project, platform = self._getProjectAndPlatformFromFilePath(self._args.filePath) self._log.debug("Determined Project = {0}, Platform = {1}", project, platform) lineNo = 1 if self._args.lineNo: lineNo = int(self._args.lineNo) self._vsSolutionHelper.openFile( self._args.filePath, lineNo, project, platform) def _getProjectAndPlatformFromFilePath(self, filePath): unityProjectsDir = self._sys.canonicalizePath(self._varMgr.expand('[UnityProjectsDir]')) filePath = self._sys.canonicalizePath(filePath) if not filePath.startswith(unityProjectsDir): raise Exception("The given file path is not within the UnityProjects directory") relativePath = filePath[len(unityProjectsDir)+1:] dirs = relativePath.split(os.path.sep) projectName = dirs[0] platformProjectDirName = dirs[1] platformDirName = platformProjectDirName[platformProjectDirName.rfind('-')+1:] platform = PlatformUtil.fromPlatformFolderName(platformDirName) return projectName, platform
class ZipHelper: _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _log = Inject('Logger') def createZipFile(self, dirPath, zipFilePath): assertThat(zipFilePath.endswith('.zip')) dirPath = self._varMgr.expandPath(dirPath) zipFilePath = self._varMgr.expandPath(zipFilePath) self._sys.makeMissingDirectoriesInPath(zipFilePath) self._sys.removeFileIfExists(zipFilePath) self._log.debug("Writing directory '{0}' to zip at '{1}'", dirPath, zipFilePath) self._writeDirectoryToZipFile(zipFilePath, dirPath) def _writeDirectoryToZipFile(self, zipFilePath, dirPath): with zipfile.ZipFile(zipFilePath, 'w', zipfile.ZIP_DEFLATED) as zipf: self._zipAddDir(zipf, dirPath, '') def _zipAddDir(self, zipf, dirPath, zipPathPrefix=None): dirPath = self._varMgr.expandPath(dirPath) assertThat(os.path.isdir(dirPath), 'Invalid directory given at "{0}"'.format(dirPath)) if zipPathPrefix is None: zipPathPrefix = os.path.basename(dirPath) for root, dirs, files in os.walk(dirPath): for file in files: filePath = os.path.join(root, file) zipf.write( filePath, os.path.join(zipPathPrefix, os.path.relpath(filePath, dirPath)))
class LogStreamFile: _varManager = Inject('VarManager') def __init__(self): self._fileStream = self._tryGetFileStream() def log(self, logType, message): if logType == LogType.Heading: self._printSeperator() self._writeLine(message) self._printSeperator() else: self._writeLine(message) def dispose(self): if self._fileStream: self._fileStream.close() def _printSeperator(self): self._writeLine('--------------------------') def _writeLine(self, value): self._write('\n' + value) def _write(self, value): if self._fileStream: self._fileStream.write(value) self._fileStream.flush() def _tryGetFileStream(self): if not self._varManager.hasKey('LogPath'): return None primaryPath = self._varManager.expand('[LogPath]') previousPath = None if self._varManager.hasKey('LogPreviousPath'): previousPath = self._varManager.expand('[LogPreviousPath]') # Keep one old build log if os.path.isfile(primaryPath) and previousPath: shutil.copy2(primaryPath, previousPath) self._outputFilePath = primaryPath return open(primaryPath, 'w', encoding='utf-8', errors='ignore')
class ScriptRunner: _log = Inject('Logger') def runWrapper(self, runner): startTime = datetime.now() succeeded = False try: runner() succeeded = True except KeyboardInterrupt as e: self._log.endHeading() self._log.error('Operation aborted by user by hitting CTRL+C') except Exception as e: self._log.endHeading() self._log.error(str(e)) if self._log.currentHeading: self._log.error("Failed while executing '" + self._log.currentHeading + "'") # Only print stack trace if it's a build-script error if not isinstance(e, ProcessErrorCodeException) and not isinstance( e, ProcessTimeoutException): self._log.debug('\n' + traceback.format_exc()) totalSeconds = (datetime.now() - startTime).total_seconds() totalSecondsStr = Util.formatTimeDelta(totalSeconds) if succeeded: self._log.finished('Operation completed successfully. Took ' + totalSecondsStr + '.\n') else: self._log.finished('Operation completed with errors. Took ' + totalSecondsStr + '.\n') return succeeded
class UnityPackageExtractor: _log = Inject('Logger') _sys = Inject('SystemHelper') # Returns the chosen name for the directory # If forcedName is given then this value is always forcedName def extractUnityPackage(self, unityPackagePath, fallbackName, forcedName): fileName = os.path.basename(unityPackagePath) self._log.heading("Extracting '{0}'", fileName) self._log.debug("Extracting unity package at path '{0}'", unityPackagePath) tempDir = tempfile.mkdtemp() self._log.info("Using temp directory '{0}'", tempDir) try: self._sys.createDirectory(os.path.join(tempDir, 'ProjectSettings')) self._sys.createDirectory(os.path.join(tempDir, 'Assets')) self._sys.executeAndWait( '"[UnityExePath]" -batchmode -nographics -quit -projectPath "{0}" -importPackage "{1}"' .format(tempDir, unityPackagePath)) self._log.heading("Copying extracted results to output directory") assetsDir = os.path.join(tempDir, 'Assets') # If the unitypackage only contains a single directory, then extract that instead # To avoid ending up with PackageName/PackageName directories for everything dirToCopy = self._chooseDirToCopy(assetsDir) dirToCopyName = os.path.basename(dirToCopy) assertThat(not self._isSpecialFolderName(dirToCopyName)) # If the extracted package contains a single directory, then by default use that directory as the name for the package # This is nice for packages that assume some directory structure (eg. UnityTestTools) # Also, some packages have titles that aren't as nice as directories. For example, Unity Test Tools uses the directory name UnityTestTools # which is a bit nicer (though adds a bit of confusion since the release name doesn't match) # Note that for upgrading/downgrading, this doesn't matter because it uses the ID which is stored in the ProjenyInstall.yaml file if not forcedName and (dirToCopyName.lower() != 'assets' and dirToCopyName.lower() != 'plugins'): forcedName = dirToCopyName if forcedName: newPackageName = forcedName else: assertThat(fallbackName) newPackageName = fallbackName newPackageName = self._sys.convertToValidFileName(newPackageName) assertThat(not self._isSpecialFolderName(newPackageName)) outDirPath = '[UnityPackagesDir]/{0}'.format(newPackageName) self._sys.copyDirectory(dirToCopy, outDirPath) return newPackageName finally: self._log.debug("Deleting temporary directory", tempDir) shutil.rmtree(tempDir) def _isSpecialFolderName(self, dirName): dirNameLower = dirName.lower() return dirNameLower == 'editor' or dirNameLower == 'streamingassets' or dirNameLower == 'webplayertemplates' def _chooseDirToCopy(self, startDir): rootNames = [ x for x in self._sys.walkDir(startDir) if not x.endswith('.meta') ] assertThat(len(rootNames) > 0) if len(rootNames) > 1: return startDir rootName = rootNames[0] fullRootPath = os.path.join(startDir, rootName) if rootName.lower() == 'plugins': return self._chooseDirToCopy(fullRootPath) if rootName.lower() == 'editor': return startDir if os.path.isdir(fullRootPath): return fullRootPath return startDir
class Runner: _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _log = Inject('Logger') _scriptRunner = Inject('ScriptRunner') _packageMgr = Inject('PackageManager') _zipHelper = Inject('ZipHelper') _vsSolutionHelper = Inject('VisualStudioHelper') def run(self, args): self._args = args success = self._scriptRunner.runWrapper(self._runInternal) if not success: sys.exit(1) def _copyDir(self, relativePath): self._sys.copyDirectory('[ProjenyDir]/' + relativePath, '[TempDir]/' + relativePath) def _copyFile(self, relativePath): self._sys.copyFile('[ProjenyDir]/' + relativePath, '[TempDir]/' + relativePath) def _runInternal(self): self._varMgr.add('PythonDir', PythonDir) self._varMgr.add('ProjenyDir', ProjenyDir) self._varMgr.add('SourceDir', '[ProjenyDir]/Source') self._varMgr.add('InstallerDir', '[ProjenyDir]/Installer') self._varMgr.add('TempDir', '[InstallerDir]/Build') self._varMgr.add('DistDir', '[InstallerDir]/Dist') self._sys.deleteAndReCreateDirectory('[DistDir]') self._sys.deleteAndReCreateDirectory('[TempDir]') try: self._updateBuildDirectory() versionStr = self._sys.readFileAsText( '[SourceDir]/Version.txt').strip() installerOutputPath = '[DistDir]/ProjenyInstaller-v{0}.exe'.format( versionStr) self._createInstaller(installerOutputPath) self._createSamplesZip(versionStr) if self._args.addTag: self._log.info('Adding git tag for version number') self._sys.executeAndWait( "git tag -a v{0} -m 'Version {0}'".format(versionStr)) if self._args.runInstallerAfter: self._sys.deleteDirectoryIfExists( 'C:/Program Files (x86)/Projeny') self._sys.executeNoWait(installerOutputPath) finally: self._sys.deleteDirectoryIfExists('[TempDir]') def _createSamplesZip(self, versionStr): self._log.heading( 'Clearing all generated files in Demo/UnityProjects folder') self._packageMgr.clearAllProjectGeneratedFiles(False) self._sys.deleteDirectoryIfExists('[TempDir]') self._sys.copyDirectory('[ProjenyDir]/Demo', '[TempDir]') self._sys.removeFileIfExists('[TempDir]/.gitignore') self._sys.removeFileIfExists('[TempDir]/PrjLog.txt') self._log.heading('Zipping up demo project') self._zipHelper.createZipFile( '[TempDir]', '[DistDir]/ProjenySamples-v{0}.zip'.format(versionStr)) def _createInstaller(self, installerOutputPath): self._log.heading('Creating installer exe') assertThat(self._sys.directoryExists(NsisPath)) self._sys.createDirectory('[DistDir]') self._sys.executeAndWait( '"{0}" "[InstallerDir]/CreateInstaller.nsi"'.format(NsisPath)) self._sys.renameFile('[DistDir]/ProjenyInstaller.exe', installerOutputPath) def _updateBuildDirectory(self): self._sys.deleteAndReCreateDirectory('[TempDir]') self._log.heading('Building exes') self._sys.executeAndWait('[PythonDir]/BuildAllExes.bat') self._log.heading('Building unity plugin dlls') self._vsSolutionHelper.buildVisualStudioProject( '[ProjenyDir]/UnityPlugin/Projeny.sln', 'Release') self._copyDir('UnityPlugin/Projeny/Assets') self._copyDir('Templates') self._copyFile(ConfigFileName) self._copyDir('Bin') self._sys.removeFile('[TempDir]/Bin/.gitignore') self._sys.removeByRegex('[TempDir]/Bin/UnityPlugin/Release/*.pdb') self._sys.deleteDirectoryIfExists('[TempDir]/Bin/UnityPlugin/Debug')
class ProcessRunner: _log = Inject('Logger') def execNoWait(self, vals, startDir): params = {} if startDir != None: params['cwd'] = startDir Popen(vals, **params) def waitForProcessOrTimeout(self, commandVals, seconds, startDir = None): params = {} params['stdout'] = subprocess.PIPE params['stderr'] = subprocess.STDOUT if startDir != None: params['cwd'] = startDir proc = Popen(commandVals, **params) # TODO - clean this up so there's only one thread, then # do the timeout logic on the main thread timeout = KillProcessThread(seconds, proc.pid) timeout.run() def enqueueOutput(out, queue): for line in iter(out.readline, b''): queue.put(line) out.close() # We use a queue here instead of just calling stdout.readline() on the main thread # so that we can catch the KeyboardInterrupt event, and force kill the process queue = Queue() thread = threading.Thread(target = enqueueOutput, args = (proc.stdout, queue)) thread.daemon = True # thread dies with the program thread.start() while True: try: try: line = queue.get_nowait() self._log.debug(line.decode('utf-8').rstrip()) except Empty: if not thread.isAlive(): break time.sleep(0.2) except KeyboardInterrupt as e: self._log.error("Detected KeyboardInterrupt - killing process...") timeout.forceKill() raise e resultCode = proc.wait() timeout.cancel() if timeout.timeOutOccurred: return ResultType.TimedOut if resultCode != 0: return ResultType.Error return ResultType.Success # Note that in this case we pass the command as a string # This is recommended by the python docs here when using shell = True # https://docs.python.org/2/library/subprocess.html#subprocess.Popen def execShellCommand(self, commandStr, startDir = None): params = {} params['stdout'] = subprocess.PIPE params['stderr'] = subprocess.PIPE params['shell'] = True if startDir != None: params['cwd'] = startDir # Would be nice to get back output in real time but I can't figure # out a way to do this # This method should only be used for a few command-prompt specific # commands anyway so not a big loss proc = Popen(commandStr, **params) (stdoutData, stderrData) = proc.communicate() output = stdoutData.decode('utf-8').strip() errors = stderrData.decode('utf-8').strip() if output: for line in output.split('\n'): self._log.debug(line) if errors: self._log.error('Error occurred during command "{0}":'.format(commandStr)) for line in errors.split('\n'): self._log.error(' ' + line) exitStatus = proc.returncode if exitStatus != 0: return ResultType.Error return ResultType.Success
class Test1: foo = Inject('Foo') bar = InjectOptional('Bar', 5)
class SystemHelper: '''Responsibilities: - Miscellaneous file-handling/path-related operations - Wrapper to execute arbitrary commands ''' _varManager = Inject('VarManager') _log = Inject('Logger') _processRunner = Inject('ProcessRunner') # Use an hour timeout def __init__(self, timeout=60 * 60): self._timeout = timeout def canonicalizePath(self, pathStr): # Make one standard representation of the given path # This will remove ..\ and also change to always use back slashes since this is what os.path.join etc. uses return self._varManager.expandPath(pathStr) def executeAndWait(self, commandStr, startDir=None): expandedStr = self._varManager.expand(commandStr) self._log.debug("Executing '%s'" % expandedStr) vals = self._splitCommandStr(expandedStr) if startDir != None: startDir = self._varManager.expand(startDir) result = self._processRunner.waitForProcessOrTimeout( vals, self._timeout, startDir) if result == ResultType.Error: raise ProcessErrorCodeException( 'Command returned with error code while executing: %s' % expandedStr) if result == ResultType.TimedOut: raise ProcessTimeoutException( 'Timed out while waiting for command: %s' % expandedStr) assertThat(result == ResultType.Success) def executeNoWait(self, commandStr, startDir=None): expandedStr = self._varManager.expand(commandStr) self._log.debug("Executing '{0}'".format(expandedStr)) vals = self._splitCommandStr(expandedStr) if startDir != None: startDir = self._varManager.expand(startDir) self._processRunner.execNoWait(vals, startDir) # This is only used to execute shell-specific commands like copy, mklink, etc. def executeShellCommand(self, commandStr, startDir=None): expandedStr = self._varManager.expand(commandStr) self._log.debug("Executing '%s'" % expandedStr) if startDir != None: startDir = self._varManager.expand(startDir) result = self._processRunner.execShellCommand(expandedStr, startDir) if result == ResultType.Error: raise ProcessErrorCodeException( 'Command returned with error code while executing: %s' % expandedStr) assertThat(result == ResultType.Success, "Expected success result but found '{0}'".format(result)) def _splitCommandStr(self, commandStr): # Hacky but necessary since shlex.split will otherwise remove our backslashes if platform.platform().startswith('Windows'): commandStr = commandStr.replace(os.sep, os.sep + os.sep) # Convert command to argument list to avoid issues with escape characters, etc. # Based on an answer here: http://stackoverflow.com/questions/12081970/python-using-quotes-in-the-subprocess-popen return shlex.split(commandStr) def executeAndReturnOutput(self, commandStr): self._log.debug("Executing '%s'" % commandStr) return subprocess.getoutput(self._splitCommandStr(commandStr)).strip() def walkDir(self, dirPath): dirPath = self._varManager.expand(dirPath) return os.listdir(dirPath) def getParentDirectoriesWithSelf(self, path): yield path for parentDir in self.getParentDirectories(path): yield parentDir def getParentDirectories(self, path): path = self._varManager.expand(path) lastParentDir = None parentDir = os.path.dirname(path) while parentDir and parentDir != lastParentDir: yield parentDir lastParentDir = parentDir parentDir = os.path.dirname(parentDir) def createDirectory(self, dirPath): dirPath = self._varManager.expand(dirPath) try: os.makedirs(dirPath) except: pass def convertToValidFileName(self, s): """Take a string and return a valid filename constructed from the string. Uses a whitelist approach: any characters not present in valid_chars are removed. Note: this method may still produce invalid filenames such as ``, `.` or `..` """ valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) filename = ''.join(c for c in s if c in valid_chars) return filename def makeMissingDirectoriesInPath(self, dirPath): dirPath = self._varManager.expand(dirPath) try: os.makedirs(os.path.dirname(dirPath)) except: pass def copyFile(self, fromPath, toPath): toPath = self._varManager.expand(toPath) fromPath = self._varManager.expand(fromPath) self.makeMissingDirectoriesInPath(toPath) shutil.copy2(fromPath, toPath) def IsDir(self, path): return os.path.isdir(self._varManager.expand(path)) def clearDirectoryContents(self, dirPath): dirPath = self._varManager.expand(dirPath) for fileName in os.listdir(dirPath): filePath = os.path.join(dirPath, fileName) if os.path.isfile(filePath): os.unlink(filePath) elif os.path.isdir(filePath): shutil.rmtree(filePath) def deleteDirectoryWaitIfNecessary(self, dirPath): dirPath = self._varManager.expand(dirPath) if not os.path.isdir(dirPath): # Already removed return attemptsLeft = 10 while True: try: shutil.rmtree(dirPath) except Exception as e: self._log.warn( 'Could not delete directory at "{0}". Waiting to try again...' .format(dirPath)) time.sleep(5) attemptsLeft -= 1 if attemptsLeft < 0: raise e continue break def deleteDirectory(self, dirPath): dirPath = self._varManager.expand(dirPath) shutil.rmtree(dirPath) def deleteAndReCreateDirectory(self, dirPath): self.deleteDirectoryIfExists(dirPath) self.createDirectory(dirPath) def deleteDirectoryIfExists(self, dirPath): dirPath = self._varManager.expand(dirPath) if os.path.exists(dirPath): shutil.rmtree(dirPath) return True return False def deleteEmptyDirectoriesUnder(self, dirPath): dirPath = self._varManager.expandPath(dirPath) if not os.path.isdir(dirPath): return 0 # Can't process long paths on windows if len(dirPath) >= 256: return 0 files = os.listdir(dirPath) numDirsDeleted = 0 for fileName in files: fullpath = os.path.join(dirPath, fileName) if os.path.isdir(fullpath): numDirsDeleted += self.deleteEmptyDirectoriesUnder(fullpath) files = os.listdir(dirPath) if len(files) == 0: self._log.debug("Removing empty folder '%s'" % dirPath) os.rmdir(dirPath) numDirsDeleted += 1 metaFilePath = dirPath + '/../' + os.path.basename( dirPath) + '.meta' if os.path.isfile(metaFilePath): self._log.debug("Removing meta file '%s'" % metaFilePath) os.remove(metaFilePath) return numDirsDeleted def fileExists(self, path): return os.path.isfile(self._varManager.expand(path)) def directoryExists(self, dirPath): return os.path.exists(self._varManager.expand(dirPath)) def copyDirectory(self, fromPath, toPath): fromPath = self._varManager.expand(fromPath) toPath = self._varManager.expand(toPath) self._log.debug("Copying directory '{0}' to '{1}'".format( fromPath, toPath)) shutil.copytree(fromPath, toPath) def readFileAsText(self, path): with self.openInputFile(path) as f: return f.read() def writeFileAsText(self, path, text): with self.openOutputFile(path) as f: f.write(text) def openOutputFile(self, path): path = self._varManager.expand(path) self.makeMissingDirectoriesInPath(path) return open(path, 'w', encoding='utf-8', errors='ignore') def openInputFile(self, path): return open(self._varManager.expand(path), 'r', encoding='utf-8', errors='ignore') def removeFile(self, fileName): os.remove(self._varManager.expand(fileName)) def removeFileIfExists(self, fileName): fullPath = self._varManager.expand(fileName) if os.path.isfile(fullPath): os.remove(fullPath) return True return False def findFilesByPattern(self, startDir, pattern): startDir = self._varManager.expand(startDir) for root, dirs, files in os.walk(startDir): for basename in files: if fnmatch.fnmatch(basename, pattern): filename = os.path.join(root, basename) yield filename def renameFile(self, currentName, newName): os.rename(self._varManager.expand(currentName), self._varManager.expand(newName)) def removeFileWaitIfNecessary(self, fileName): outputPath = self._varManager.expand(fileName) if not os.path.isfile(outputPath): # File already removed return while True: try: os.remove(outputPath) except OSError: self._log.warn( 'Could not delete file at "{0}". Waiting to try again...'. format(outputPath)) time.sleep(5) continue break def removeByRegex(self, regex): regex = self._varManager.expand(regex) count = 0 for filePath in glob(regex): os.unlink(filePath) count += 1 self._log.debug("Removed %s files matching '%s'" % (count, regex)) def makeMissingDirectoriesInPath(self, dirPath): dirPath = self._varManager.expand(dirPath) self._log.debug( "Making missing directories in path '{0}'".format(dirPath)) try: os.makedirs(os.path.dirname(dirPath)) except: pass
class Runner: _scriptRunner = Inject('ScriptRunner') _log = Inject('Logger') _sys = Inject('SystemHelper') _packageAnalyzer = Inject('UnityPackageAnalyzer') def __init__(self): self._manifest = None def run(self, args): self._args = args self._args.directory = self._sys.canonicalizePath(self._args.directory) self._scriptRunner.runWrapper(self._runInternal) def _runInternal(self): self._log.debug("Started ReleaseManifestUpdater with arguments: {0}".format(" ".join(sys.argv[1:]))) while True: releasePaths = self._getAllReleasePaths() self._log.info("Checking for changes...") if self._hasChanged(releasePaths): self._manifest = self._createManifest(releasePaths) self._saveManifest() self._log.info("Detected change to one or more releasePaths. Release manifest has been updated.") if self._args.pollInternal <= 0: break time.sleep(self._args.pollInternal) def _saveManifest(self): yamlStr = YamlSerializer.serialize(self._manifest) self._sys.writeFileAsText(os.path.join(self._args.directory, ReleaseManifestFileName), yamlStr) def _createManifest(self, releasePaths): manifest = ReleaseManifest() for path in releasePaths: path = self._sys.canonicalizePath(path) releaseInfo = self._packageAnalyzer.getReleaseInfoFromUnityPackage(path) assertThat(path.startswith(self._args.directory)) relativePath = path[len(self._args.directory)+1:] releaseInfo.localPath = relativePath manifest.releases.append(releaseInfo) return manifest def _hasChanged(self, releasePaths): if self._manifest == None: return True return False def _getAllReleasePaths(self): releasePath = [] for filePath in self._sys.findFilesByPattern(self._args.directory, '*.unitypackage'): releasePath.append(filePath) return releasePath
class Runner: _log = Inject('Logger') _packageMgr = Inject('PackageManager') _unityHelper = Inject('UnityHelper') _vsSolutionHelper = Inject('VisualStudioHelper') _releaseSourceManager = Inject('ReleaseSourceManager') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') def run(self, project, platform, requestId, param1, param2): self._log.debug("Started EditorApi with arguments: {0}".format( " ".join(sys.argv[1:]))) self._project = project self._platform = platform self._requestId = requestId self._param1 = param1 self._param2 = param2 succeeded = True # This is repeated in __main__ but this is better because # it will properly log detailed errors to the file log instead of to the console try: self._runInternal() except Exception as e: sys.stderr.write(str(e)) self._log.error(str(e)) if not MiscUtil.isRunningAsExe(): self._log.error('\n' + traceback.format_exc()) succeeded = False if not succeeded: sys.exit(1) def _runInternal(self): if self._requestId == 'updateLinks': self._packageMgr.updateProjectJunctions(self._project, self._platform) elif self._requestId == 'openUnity': self._packageMgr.checkProjectInitialized(self._project, self._platform) self._unityHelper.openUnity(self._project, self._platform) elif self._requestId == 'openPackagesFolder': os.startfile(self._varMgr.expandPath("[UnityPackagesDir]")) elif self._requestId == 'updateCustomSolution': self._vsSolutionHelper.updateCustomSolution( self._project, self._platform) elif self._requestId == 'openCustomSolution': self._vsSolutionHelper.openCustomSolution(self._project, self._platform) elif self._requestId == 'listPackages': infos = self._packageMgr.getAllPackageInfos() for packageInfo in infos: sys.stderr.write('---\n') sys.stderr.write(YamlSerializer.serialize(packageInfo) + '\n') elif self._requestId == 'listProjects': projectNames = self._packageMgr.getAllProjectNames() for projName in projectNames: sys.stderr.write(projName + '\n') elif self._requestId == 'listReleases': for release in self._releaseSourceManager.lookupAllReleases(): sys.stderr.write('---\n') sys.stderr.write(YamlSerializer.serialize(release) + '\n') elif self._requestId == 'deletePackage': self._log.info("Deleting package '{0}'", self._param1) self._packageMgr.deletePackage(self._param1) elif self._requestId == 'createPackage': self._log.info("Creating package '{0}'", self._param1) self._packageMgr.createPackage(self._param1) elif self._requestId == 'installRelease': self._log.info("Installing release '{0}' version code '{1}'", self._param1, self._param2) self._releaseSourceManager.installReleaseById( self._param1, self._param2, True) elif self._requestId == 'createProject': self._log.info("Creating new project '{0}'", self._project) self._packageMgr._createProject(self._project) else: assertThat(False, "Invalid request id '{0}'", self._requestId)
class VisualStudioSolutionGenerator: """ Handler for creating custom visual studio solutions based on ProjenyProject.yaml files """ _log = Inject('Logger') _packageManager = Inject('PackageManager') _schemaLoader = Inject('ProjectSchemaLoader') _unityHelper = Inject('UnityHelper') _config = Inject('Config') _varMgr = Inject('VarManager') _sys = Inject('SystemHelper') def updateVisualStudioSolution(self, projectName, platform): self._log.heading('Updating Visual Studio solution for project "{0}"'.format(projectName)) self._packageManager.setPathsForProject(projectName, platform) self._packageManager.checkProjectInitialized(projectName, platform) schema = self._schemaLoader.loadSchema(projectName, platform) self._updateProjects(schema) def _prettify(self, doc): return minidom.parseString(ET.tostring(doc)).toprettyxml(indent=" ") def _getDefineConstantsElement(self, root): return root.findall('.//{0}DefineConstants'.format(NsPrefix))[0].text def _getUnityProjectReferencesItems(self, root): items = [] refElems = root.findall('./{0}ItemGroup/{0}Reference'.format(NsPrefix)) for refElem in refElems: name = refElem.get('Include') children = refElem.getchildren() hintPath = None if len(children) > 0: hintPathElem = children[0] assertThat(hintPathElem.tag == '{0}HintPath'.format(NsPrefix)) hintPath = hintPathElem.text.replace('/', '\\') if hintPath: if not os.path.isabs(hintPath): hintPath = self._varMgr.expandPath('[ProjectPlatformRoot]/{0}'.format(hintPath)) assertThat(self._sys.fileExists(hintPath), "Expected to find file at '{0}'".format(hintPath)) items.append(RefInfo(name, hintPath)) return items def _chooseMostRecentFile(self, path1, path2): path1 = self._varMgr.expandPath(path1) path2 = self._varMgr.expandPath(path2) # If they both exist choose most recent if self._sys.fileExists(path1) and self._sys.fileExists(path2): modtime1 = os.path.getmtime(path1) modtime2 = os.path.getmtime(path2) if modtime1 > modtime2: return path1 return path2 if self._sys.fileExists(path1): return path1 if self._sys.fileExists(path2): return path2 return None def _parseGeneratedUnityProject(self): # Annoyingly, unity does generate the solution using different paths # depending on settings # If visual studio is set to external editor, it names it the first one # and otherwise it names it the second one # So check modification times for the case where the user changes this setting unityProjPath = self._chooseMostRecentFile( '[UnityGeneratedProjectPath]', '[UnityGeneratedProjectPath2]') unityEditorProjPath = self._chooseMostRecentFile( '[UnityGeneratedProjectEditorPath]', '[UnityGeneratedProjectEditorPath2]') assertThat(unityProjPath and self._sys.fileExists(unityProjPath) and unityEditorProjPath and self._sys.fileExists(unityEditorProjPath), \ 'Could not find unity-generated project when generating custom solution. This is necessary so the custom solution can add things like unity defines and DLL references within the unity project.') unityProjRoot = ET.parse(unityProjPath) unityProjEditorRoot = ET.parse(unityEditorProjPath) unityDefines = self._getDefineConstantsElement(unityProjRoot) unityRefItems = self._getUnityProjectReferencesItems(unityProjRoot) unityRefItemsEditor = self._getUnityProjectReferencesItems(unityProjEditorRoot) return unityDefines, unityRefItems, unityRefItemsEditor def _updateProjects(self, schema): # Necessary to avoid having ns0: prefixes everywhere on output ET.register_namespace('', 'http://schemas.microsoft.com/developer/msbuild/2003') unityDefines, unityRefItems, unityRefItemsEditor = self._parseGeneratedUnityProject() excludeDirs = [] includedProjects = [] allCustomProjects = {} pluginsProj = self._createStandardCsProjInfo('PluginsFolder', '[PluginsDir]') includedProjects.append(pluginsProj) scriptsEditorProj = self._createStandardCsProjInfo('AssetsFolder-Editor', '[ProjectAssetsDir]') includedProjects.append(scriptsEditorProj) scriptsProj = self._createStandardCsProjInfo('AssetsFolder', '[ProjectAssetsDir]') includedProjects.append(scriptsProj) pluginsEditorProj = self._createStandardCsProjInfo('PluginsFolder-Editor', '[PluginsDir]') includedProjects.append(pluginsEditorProj) allPackages = schema.packages.values() prebuiltProjectInfos = self._createPrebuiltProjInfo(schema.prebuiltProjects) customEditorProjects = [] customAssetsProjects = [] # Store lambdas to create the csproj projects so that the isignored flags are always up to date when they get written # Otherwise sometimes we will output csproj files that have a ProjectReference tag to a project that doesn't exist # This is really hacky but I am about to release the new version and it's not worth doing a proper refactor to get # this ordering right csProjWriters = [] # Need to populate allCustomProjects first so we can get references in _tryCreateCustomProject for packageInfo in allPackages: if packageInfo.createCustomVsProject: customProject = self._createCsProjInfo(packageInfo, False) allCustomProjects[customProject.name] = customProject customAssetsProjects.append(customProject) customEditorProject = self._createCsProjInfo(packageInfo, True) allCustomProjects[customEditorProject.name] = customEditorProject customEditorProjects.append(customEditorProject) for packageInfo in allPackages: if packageInfo.createCustomVsProject: self._log.debug('Processing project "{0}"'.format(packageInfo.name)) customProject = self._tryCreateCustomProject( \ False, allCustomProjects, packageInfo, unityRefItems, unityRefItemsEditor, unityDefines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjectInfos, csProjWriters) if customProject: includedProjects.append(customProject) customEditorProject = self._tryCreateCustomProject( \ True, allCustomProjects, packageInfo, unityRefItems, unityRefItemsEditor, unityDefines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjectInfos, csProjWriters) if customEditorProject: includedProjects.append(customEditorProject) self._log.debug('Processing project "{0}"'.format(pluginsProj.name)) pluginsProj.dependencies = prebuiltProjectInfos self._createStandardCsProjForDirectory(pluginsProj, excludeDirs, unityRefItems, unityDefines, False, prebuiltProjectInfos, csProjWriters) pluginsEditorProj.dependencies = [pluginsProj] + prebuiltProjectInfos for packageInfo in allPackages: if packageInfo.createCustomVsProject and packageInfo.isPluginDir: pluginsEditorProj.dependencies.append(allCustomProjects[packageInfo.name]) self._log.debug('Processing project "{0}"'.format(pluginsEditorProj.name)) self._createStandardCsProjForDirectory(pluginsEditorProj, excludeDirs, unityRefItemsEditor, unityDefines, True, prebuiltProjectInfos, csProjWriters) excludeDirs.append(self._varMgr.expandPath('[PluginsDir]')) self._log.debug('Processing project "{0}"'.format(scriptsProj.name)) scriptsProj.dependencies = [pluginsProj] + prebuiltProjectInfos self._createStandardCsProjForDirectory(scriptsProj, excludeDirs, unityRefItems, unityDefines, False, prebuiltProjectInfos, csProjWriters) scriptsEditorProj.dependencies = scriptsProj.dependencies + [scriptsProj, pluginsEditorProj] self._log.debug('Processing project "{0}"'.format(scriptsEditorProj.name)) self._createStandardCsProjForDirectory(scriptsEditorProj, excludeDirs, unityRefItemsEditor, unityDefines, True, prebuiltProjectInfos, csProjWriters) self._ensureNoMatchingPrebuiltCsProjNames(includedProjects, prebuiltProjectInfos) for prebuiltProj in prebuiltProjectInfos: includedProjects.append(prebuiltProj) for writer in csProjWriters: writer() self._createSolution(includedProjects, schema.customFolderMap) def _ensureNoMatchingPrebuiltCsProjNames(self, projects, prebuiltProjects): for proj in projects: for prebuiltProj in prebuiltProjects: assertThat(prebuiltProj.name != proj.name, 'Found prebuilt project and normal project both using the same name "{0}". This will not work in the same visual studio solution'.format(prebuiltProj.name)) def _createPrebuiltProjInfo(self, prebuiltProjects): processedList = [] for projAbsPath in prebuiltProjects: projId = self._getProjectIdFromFile(projAbsPath) projName = os.path.splitext(os.path.basename(projAbsPath))[0] processedList.append(CsProjInfo(projId, projAbsPath, projName, True, [], False)) return processedList def _getFolderName(self, packageName, customFolders): for item in customFolders.items(): folderName = item[0] pattern = item[1] if packageName == pattern or (pattern.startswith('/') and re.match(pattern[1:], packageName)): return folderName return None def _createCsProjInfo(self, packageInfo, isEditor): projId = self._createProjectGuid() outputDir = self._varMgr.expandPath(packageInfo.outputDirVar) csProjectName = packageInfo.name if isEditor: csProjectName += EditorProjectNameSuffix outputPath = os.path.join(outputDir, csProjectName + ".csproj") packageDir = os.path.join(outputDir, packageInfo.name) files = [] self._addCsFilesInDirectory(packageDir, [], files, isEditor) isIgnored = (len(files) == 0) return CsProjInfo(projId, outputPath, csProjectName, False, files, isIgnored) def _tryCreateCustomProject(self, isEditor, customCsProjects, packageInfo, unityRefItems, unityRefItemsEditor, defines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjects, csProjWriters): projName = packageInfo.name if isEditor: projName += EditorProjectNameSuffix csProjInfo = customCsProjects[projName] outputDir = os.path.dirname(csProjInfo.absPath) packageDir = os.path.join(outputDir, packageInfo.name) excludeDirs.append(packageDir) if csProjInfo.isIgnored: return None projDependencies = [] if isEditor: projDependencies.append(pluginsProj) projDependencies.append(pluginsEditorProj) projDependencies.append(customCsProjects[packageInfo.name]) if not packageInfo.isPluginDir: projDependencies.append(scriptsProj) projDependencies.append(scriptsEditorProj) else: projDependencies.append(pluginsProj) if not packageInfo.isPluginDir: projDependencies.append(scriptsProj) for dependName in packageInfo.allDependencies: assertThat(not dependName in projDependencies) if dependName in customCsProjects: dependProj = customCsProjects[dependName] if not dependProj.isIgnored: projDependencies.append(dependProj) if isEditor: dependEditorName = dependName + EditorProjectNameSuffix if dependEditorName in customCsProjects: dependEditorProj = customCsProjects[dependEditorName] if not dependEditorProj.isIgnored: projDependencies.append(dependEditorProj) if isEditor: refItems = unityRefItemsEditor else: refItems = unityRefItems csProjInfo.dependencies = list(projDependencies) for proj in prebuiltProjects: csProjInfo.dependencies.append(proj) csProjWriters.append(lambda: self._writeCsProject(csProjInfo, csProjInfo.files, refItems, defines, prebuiltProjects)) return csProjInfo def _getProjectIdFromFile(self, projPath): doc = ET.parse(projPath) root = doc.getroot() projId = root.findall('./{0}PropertyGroup/{0}ProjectGuid'.format(NsPrefix))[0].text return re.match('^{(.*)}$', projId).groups()[0] def _createSolution(self, projects, customFolderMap): with open(self._varMgr.expandPath('[CsSolutionTemplate]'), 'r', encoding='utf-8', errors='ignore') as inputFile: solutionStr = inputFile.read() outputPath = self._varMgr.expandPath('[SolutionPath]') outputDir = os.path.dirname(outputPath) projectList = '' postSolution = '' projectFolderStr = '' projectFolderMapsStr = '' folderIds = {} for folderName in customFolderMap: folderId = self._createProjectGuid() folderIds[folderName] = folderId projectFolderStr += 'Project("{{{0}}}") = "{1}", "{2}", "{{{3}}}"\nEndProject\n' \ .format(SolutionFolderTypeGuid, folderName, folderName, folderId) for proj in projects: assertThat(proj.name) assertThat(proj.id) assertThat(proj.absPath) if proj.isIgnored: continue projectList += 'Project("{{{0}}}") = "{1}", "{2}", "{{{3}}}"\n' \ .format(CsProjTypeGuid, proj.name, os.path.relpath(proj.absPath, outputDir), proj.id) if len(proj.dependencies) > 0: projectList += '\tProjectSection(ProjectDependencies) = postProject\n' for projDepend in proj.dependencies: if not projDepend.isIgnored: projectList += '\t\t{{{0}}} = {{{0}}}\n'.format(projDepend.id) projectList += '\tEndProjectSection\n' projectList += 'EndProject\n' if len(postSolution) != 0: postSolution += '\n' buildConfig = 'Unity Debug' if proj.isPrebuild else 'Debug' postSolution += \ '\t\t{{{0}}}.Debug|Any CPU.ActiveCfg = {1}|Any CPU\n\t\t{{{0}}}.Debug|Any CPU.Build.0 = {1}|Any CPU' \ .format(proj.id, buildConfig) folderName = self._getFolderName(proj.name, customFolderMap) if folderName: folderId = folderIds[folderName] if len(projectFolderMapsStr) != 0: projectFolderMapsStr += '\n' projectFolderMapsStr += \ '\t\t{{{0}}} = {{{1}}}' \ .format(proj.id, folderId) solutionStr = solutionStr.replace('[ProjectList]', projectList) if len(postSolution.strip()) > 0: solutionStr = solutionStr.replace('[PostSolution]', """ GlobalSection(ProjectConfigurationPlatforms) = postSolution {0} EndGlobalSection""".format(postSolution)) else: solutionStr = solutionStr.replace('[PostSolution]', '') solutionStr = solutionStr.replace('[ProjectFolders]', projectFolderStr) if len(projectFolderMapsStr) > 0: fullStr = '\tGlobalSection(NestedProjects) = preSolution\n{0}\n\tEndGlobalSection'.format(projectFolderMapsStr) solutionStr = solutionStr.replace('[ProjectFolderMaps]', fullStr) else: solutionStr = solutionStr.replace('[ProjectFolderMaps]', '') with open(outputPath, 'w', encoding='utf-8', errors='ignore') as outFile: outFile.write(solutionStr) self._log.debug('Saved new solution file at "{0}"'.format(outputPath)) def _createStandardCsProjInfo(self, projectName, outputDir): outputDir = self._varMgr.expandPath(outputDir) outputPath = os.path.join(outputDir, projectName + ".csproj") projId = self._createProjectGuid() return CsProjInfo(projId, outputPath, projectName, False, [], False) def _createStandardCsProjForDirectory(self, projInfo, excludeDirs, unityRefItems, defines, isEditor, prebuiltProjects, csProjWriters): outputDir = os.path.dirname(projInfo.absPath) files = [] self._addCsFilesInDirectory(outputDir, excludeDirs, files, isEditor) if len(files) == 0: projInfo.isIgnored = True return csProjWriters.append(lambda: self._writeCsProject(projInfo, files, unityRefItems, defines, prebuiltProjects)) def _createProjectGuid(self): return str(uuid.uuid4()).upper() def _shouldReferenceBeCopyLocal(self, refName): return refName != 'System' and refName != 'System.Core' def _writeCsProject(self, projInfo, files, refItems, defines, prebuiltProjects): outputDir = os.path.dirname(projInfo.absPath) doc = ET.parse(self._varMgr.expandPath('[CsProjectTemplate]')) root = doc.getroot() self._stripWhitespace(root) refsItemGroupElem = root.findall('./{0}ItemGroup[{0}Reference]'.format(NsPrefix))[0] refsItemGroupElem.clear() # Add reference items given from unity project for refInfo in refItems: if len([x for x in prebuiltProjects if x.name == refInfo.name]) > 0: #self._log.debug('Ignoring reference for prebuilt project "{0}"'.format(refInfo.name)) continue refElem = ET.SubElement(refsItemGroupElem, 'Reference') refElem.set('Include', refInfo.name) if refInfo.hintPath: refPath = refInfo.hintPath assertThat(os.path.isabs(refPath), "Invalid path '{0}'".format(refPath)) if refPath.startswith(outputDir): refPath = os.path.relpath(refPath, outputDir) hintPathElem = ET.SubElement(refElem, 'HintPath') hintPathElem.text = refPath ET.SubElement(refElem, 'Private').text = 'True' if self._shouldReferenceBeCopyLocal(refInfo.name) else 'False' # Add cs files 'compile' items filesItemGroupElem = root.findall('./{0}ItemGroup[{0}Compile]'.format(NsPrefix))[0] filesItemGroupElem.clear() for filePath in files: if filePath.endswith('.cs'): compileElem = ET.SubElement(filesItemGroupElem, 'Compile') else: compileElem = ET.SubElement(filesItemGroupElem, 'None') compileElem.set('Include', os.path.relpath(filePath, outputDir)) root.findall('./{0}PropertyGroup/{0}RootNamespace'.format(NsPrefix))[0] \ .text = projInfo.name root.findall('./{0}PropertyGroup/{0}ProjectGuid'.format(NsPrefix))[0] \ .text = '{' + projInfo.id + '}' root.findall('./{0}PropertyGroup/{0}OutputPath'.format(NsPrefix))[0] \ .text = os.path.relpath(self._varMgr.expandPath('[ProjectPlatformRoot]/Bin'), outputDir) root.findall('./{0}PropertyGroup/{0}AssemblyName'.format(NsPrefix))[0] \ .text = projInfo.name root.findall('./{0}PropertyGroup/{0}RootNamespace'.format(NsPrefix))[0] \ .text = "ModestTree" root.findall('./{0}PropertyGroup/{0}DefineConstants'.format(NsPrefix))[0] \ .text = defines tempFilesDir = os.path.relpath(self._varMgr.expandPath('[IntermediateFilesDir]'), outputDir) root.findall('./{0}PropertyGroup/{0}IntermediateOutputPath'.format(NsPrefix))[0] \ .text = tempFilesDir root.findall('./{0}PropertyGroup/{0}BaseIntermediateOutputPath'.format(NsPrefix))[0] \ .text = tempFilesDir # Add project references projectRefGroupElem = root.findall('./{0}ItemGroup[{0}ProjectReference]'.format(NsPrefix))[0] projectRefGroupElem.clear() for dependInfo in projInfo.dependencies: if dependInfo.isIgnored: continue projectRefElem = ET.SubElement(projectRefGroupElem, 'ProjectReference') projectRefElem.set('Include', os.path.relpath(dependInfo.absPath, outputDir)) ET.SubElement(projectRefElem, 'Project').text = '{' + dependInfo.id + '}' ET.SubElement(projectRefElem, 'Name').text = dependInfo.name self._sys.makeMissingDirectoriesInPath(projInfo.absPath) with open(projInfo.absPath, 'w', encoding='utf-8', errors='ignore') as outputFile: outputFile.write(self._prettify(root)) def _stripWhitespace(self, elem): for x in ET.ElementTree(elem).getiterator(): if x.text: x.text = x.text.strip() if x.tail: x.tail = x.tail.strip() def _prettify(self, doc): return minidom.parseString(ET.tostring(doc)).toprettyxml(indent=" ") def _shouldIgnoreCsProjFile(self, fullPath): if self._config.getBool('LinkToProjenyEditorDir'): return False return ProjenyDirectoryIgnorePattern.match(fullPath) def _addCsFilesInDirectory(self, dirPath, excludeDirs, files, isForEditor): isInsideEditorFolder = re.match(r'.*\\Editor($|\\).*', dirPath) if not isForEditor and isInsideEditorFolder: return if dirPath in excludeDirs: return #self._log.debug('Processing ' + dirPath) for excludeDir in excludeDirs: assertThat(not dirPath.startswith(excludeDir + "\\")) if not self._sys.directoryExists(dirPath): return for itemName in os.listdir(dirPath): fullPath = os.path.join(dirPath, itemName) if self._shouldIgnoreCsProjFile(fullPath): continue if os.path.isdir(fullPath): self._addCsFilesInDirectory(fullPath, excludeDirs, files, isForEditor) else: if re.match('.*\.(cs|txt)$', itemName): if not isForEditor or isInsideEditorFolder: files.append(fullPath)
class ReleaseSourceManager: _varMgr = Inject('VarManager') _log = Inject('Logger') _config = Inject('Config') _sys = Inject('SystemHelper') _packageManager = Inject('PackageManager') def __init__(self): self._hasInitialized = False self._releaseSources = [] def _lazyInit(self): if self._hasInitialized: return self._hasInitialized = True for regSettings in self._config.getList('ReleaseSources'): for pair in regSettings.items(): reg = self._createReleaseSource(pair[0], pair[1]) reg.init() self._releaseSources.append(reg) self._log.info( "Finished initializing Release Source Manager, found {0} releases in total", self._getTotalReleaseCount()) def _getTotalReleaseCount(self): total = 0 for reg in self._releaseSources: total += len(reg.releases) return total def _createReleaseSource(self, regType, settings): if regType == 'LocalFolder': folderPath = self._varMgr.expand(settings['Path']).replace( "\\", "/") return LocalFolderReleaseSource(folderPath) if regType == 'AssetStoreCache': return AssetStoreCacheReleaseSource() if regType == 'FileServer': return RemoteServerReleaseSource(settings['ManifestUrl']) assertThat(False, "Could not find release source with type '{0}'", regType) def listAllReleases(self): self._lazyInit() self._log.heading('Found {0} Releases', self._getTotalReleaseCount()) for release in self.lookupAllReleases(): self._log.info("{0} ({1}) ({2})", release.name, release.version, release.versionCode) def lookupAllReleases(self): self._lazyInit() result = [] for source in self._releaseSources: for release in source.releases: result.append(release) result.sort(key=lambda x: x.name.lower()) return result def _findReleaseInfoAndSourceByIdAndVersionCode(self, releaseId, releaseVersionCode): assertIsType(releaseVersionCode, int) for source in self._releaseSources: for release in source.releases: if release.id == releaseId and release.versionCode == releaseVersionCode: return (release, source) return (None, None) def _findReleaseInfoAndSourceByNameAndVersion(self, releaseName, releaseVersion): for source in self._releaseSources: for release in source.releases: if release.name == releaseName and release.version == releaseVersion: return (release, source) return (None, None) def installReleaseByName(self, releaseName, releaseVersion, suppressPrompts=False): assertThat(releaseName) assertThat(releaseVersion) self._lazyInit() self._log.heading( "Attempting to install release '{0}' (version '{1}')", releaseName, releaseVersion) assertThat( len(self._releaseSources) > 0, "Could not find any release sources to search for the given release" ) releaseInfo, releaseSource = self._findReleaseInfoAndSourceByNameAndVersion( releaseName, releaseVersion) assertThat( releaseInfo, "Failed to install release '{0}' (version {1}) - could not find it in any of the release sources.\nSources checked: \n {2}\nTry listing all available release with the -lr command" .format(releaseName, releaseVersion, "\n ".join([x.getName() for x in self._releaseSources]))) self._installReleaseInternal(releaseInfo, releaseSource, suppressPrompts) def installReleaseById(self, releaseId, releaseVersionCode, suppressPrompts=False): self._log.info( "Attempting to install release with ID '{0}' and version code '{1}'", releaseId, releaseVersionCode) try: releaseVersionCode = int(releaseVersionCode) except ValueError: assertThat( False, "Invalid version code '{0}' - must be convertable to an integer", releaseVersionCode) assertThat(releaseVersionCode, 'Invalid release version code supplied') assertThat(releaseId) self._lazyInit() assertThat( len(self._releaseSources) > 0, "Could not find any release sources to search for the given release" ) releaseInfo, releaseSource = self._findReleaseInfoAndSourceByIdAndVersionCode( releaseId, releaseVersionCode) assertThat( releaseInfo, "Failed to install release '{0}' - could not find it in any of the release sources.\nSources checked: \n {1}\nTry listing all available release with the -lr command" .format(releaseId, "\n ".join([x.getName() for x in self._releaseSources]))) self._installReleaseInternal(releaseInfo, releaseSource, suppressPrompts) def _installReleaseInternal(self, releaseInfo, releaseSource, suppressPrompts=False): self._log.heading("Installing release '{0}' (version {1})", releaseInfo.name, releaseInfo.version) installDirName = None for packageInfo in self._packageManager.getAllPackageInfos(): installInfo = packageInfo.installInfo if installInfo and installInfo.releaseInfo and installInfo.releaseInfo.id == releaseInfo.id: if installInfo.releaseInfo.versionCode == releaseInfo.versionCode: if not suppressPrompts: shouldContinue = MiscUtil.confirmChoice( "Release '{0}' (version {1}) is already installed. Would you like to re-install anyway? Note that this will overwrite any local changes you've made to it." .format(releaseInfo.name, releaseInfo.version)) assertThat(shouldContinue, 'User aborted') else: print( "\nFound release '{0}' already installed with version '{1}'" .format(releaseInfo.name, releaseInfo.version), end='') installDirection = 'UPGRADE' if releaseInfo.versionCode > installInfo.releaseInfo.versionCode else 'DOWNGRADE' if not suppressPrompts: shouldContinue = MiscUtil.confirmChoice( "Are you sure you want to {0} '{1}' from version '{2}' to version '{3}'? (y/n)" .format(installDirection, releaseInfo.name, installInfo.releaseInfo.version, releaseInfo.version)) assertThat(shouldContinue, 'User aborted') self._packageManager.deletePackage(packageInfo.name) # Retain original directory name in case it is referenced by other packages installDirName = packageInfo.name installDirName = releaseSource.installRelease(releaseInfo, installDirName) destDir = self._varMgr.expand( '[UnityPackagesDir]/{0}'.format(installDirName)) assertThat(self._sys.directoryExists(destDir), 'Expected dir "{0}" to exist', destDir) newInstallInfo = PackageInstallInfo() newInstallInfo.releaseInfo = releaseInfo newInstallInfo.installDate = datetime.utcnow() yamlStr = YamlSerializer.serialize(newInstallInfo) self._sys.writeFileAsText(os.path.join(destDir, InstallInfoFileName), yamlStr) self._log.info("Successfully installed '{0}' (version {1})", releaseInfo.name, releaseInfo.version)
class VarManager: _config = Inject('Config') ''' Stores a dictionary of keys to values to replace path variables with ''' def __init__(self, initialParams=None): self._params = initialParams if initialParams else {} self._params['StartCurrentDir'] = os.getcwd() self._params['ExecDir'] = MiscUtil.getExecDirectory().replace( '\\', '/') # We could just call self._config.getDictionary('PathVars') here but # then we wouldn't be able to use fallback (?) and override (!) characters in # our config self._regex = re.compile('^([^\[]*)(\[[^\]]*\])(.*)$') def hasKey(self, key): return key in self._params or self._config.tryGet('PathVars', key) != None def get(self, key): if key in self._params: return self._params[key] return self._config.getString('PathVars', key) def tryGet(self, key): if key in self._params: return self._params[key] return self._config.tryGetString(None, 'PathVars', key) def add(self, key, value): self._params[key] = value def set(self, key, value): self._params[key] = value def expandPath(self, text, extraVars=None): ''' Same as expand() except it cleans up the path to remove ../ ''' return os.path.realpath(self.expand(text, extraVars)) def expand(self, text, extraVars=None): if not extraVars: extraVars = {} allArgs = self._params.copy() allArgs.update(extraVars) while True: match = self._regex.match(text) if not match: break prefix = match.group(1) var = match.group(2) suffix = match.group(3) var = var[1:-1] if var in allArgs: replacement = allArgs[var] else: replacement = self.get(var) text = prefix + replacement + suffix if '[' in text: raise Exception( "Unable to find all keys in path '{0}'".format(text)) return text
class Test1: con = Inject('Console', Assertions.HasMethods('WriteLine')) def Run(self): self.con.WriteLine('lorem ipsum')
class Test2: title = Inject('Title', Assertions.IsInstanceOf(str)) def Run(self): print('title: {0}'.format(self.title))
class ProjectSchemaLoader: _varMgr = Inject('VarManager') _log = Inject('Logger') def loadSchema(self, name, platform): schemaPath = self._varMgr.expandPath( '[UnityProjectsDir]/{0}/{1}'.format(name, ProjectConfigFileName)) schemaPathUser = self._varMgr.expandPath( '[UnityProjectsDir]/{0}/{1}'.format(name, ProjectUserConfigFileName)) schemaPathGlobal = self._varMgr.expandPath( '[UnityProjectsDir]/{0}'.format(ProjectConfigFileName)) schemaPathUserGlobal = self._varMgr.expandPath( '[UnityProjectsDir]/{0}'.format(ProjectUserConfigFileName)) self._log.debug('Loading schema at path "{0}"'.format(schemaPath)) config = Config( loadYamlFilesThatExist(schemaPath, schemaPathUser, schemaPathGlobal, schemaPathUserGlobal)) pluginDependencies = config.tryGetList([], 'PluginsFolder') scriptsDependencies = config.tryGetList([], 'AssetsFolder') customProjects = config.tryGetList([], 'SolutionProjects') customFolders = config.tryGetDictionary({}, 'SolutionFolders') prebuiltProjects = config.tryGetList([], 'Prebuilt') # Remove duplicates scriptsDependencies = list(set(scriptsDependencies)) pluginDependencies = list(set(pluginDependencies)) for packageName in pluginDependencies: assertThat( not packageName in scriptsDependencies, "Found package '{0}' in both scripts and plugins. Must be in only one or the other" .format(packageName)) allDependencies = pluginDependencies + scriptsDependencies packageMap = {} # Resolve all dependencies for each package # by default, put any dependencies that are not declared explicitly into the plugins folder for packageName in allDependencies: configPath = self._varMgr.expandPath( '[UnityPackagesDir]/{0}/ProjenyPackage.yaml'.format( packageName)) if os.path.exists(configPath): packageConfig = Config(loadYamlFilesThatExist(configPath)) else: packageConfig = Config([]) createCustomVsProject = self._checkCustomProjectMatch( packageName, customProjects) isPluginsDir = packageName in pluginDependencies if isPluginsDir: assertThat(not packageName in scriptsDependencies) else: assertThat(packageName in scriptsDependencies) if packageConfig.tryGetBool(False, 'ForceAssetsDirectory'): isPluginsDir = False explicitDependencies = packageConfig.tryGetList([], 'Dependencies') forcePluginsDir = packageConfig.tryGetBool( False, 'ForcePluginsDirectory') assertThat(not packageName in packageMap) packageMap[packageName] = PackageInfo(isPluginsDir, packageName, packageConfig, createCustomVsProject, explicitDependencies, forcePluginsDir) for dependName in explicitDependencies: if not dependName in allDependencies: pluginDependencies.append(dependName) # Yes, python is ok with changing allDependencies even while iterating over it allDependencies.append(dependName) for dependName in packageConfig.tryGetList([], 'Extras'): if not dependName in allDependencies: if isPluginsDir: pluginDependencies.append(dependName) else: scriptsDependencies.append(dependName) # Yes, python is ok with changing allDependencies even while iterating over it allDependencies.append(dependName) self._removePlatformSpecificPackages(packageMap, platform) self._printDependencyTree(packageMap) self._fillOutDependencies(packageMap) for customProj in customProjects: assertThat( customProj.startswith('/') or customProj in allDependencies, 'Given project "{0}" in schema is not included in either "scripts" or "plugins"' .format(customProj)) self._addPrebuiltProjectsFromPackages(packageMap, prebuiltProjects) self._log.info('Found {0} packages in total for given schema'.format( len(allDependencies))) # In Unity, the plugins folder can not have any dependencies on anything in the scripts folder # So if dependencies exist then just automatically move those packages to the scripts folder self._ensurePluginPackagesDoNotHaveDependenciesInAssets(packageMap) self._ensurePackagesThatAreNotProjectsDoNotHaveProjectDependencies( packageMap) projectsDir = self._varMgr.expandPath('[UnityProjectsDir]') prebuiltProjectPaths = [ os.path.normpath(os.path.join(projectsDir, x)) for x in prebuiltProjects ] for info in packageMap.values(): if info.forcePluginsDir and not info.isPluginDir: assertThat( False, "Package '{0}' must be in plugins directory".format( info.name)) return ProjectSchema(name, packageMap, customFolders, prebuiltProjectPaths) def _ensurePackagesThatAreNotProjectsDoNotHaveProjectDependencies( self, packageMap): changedOne = True while changedOne: changedOne = False for info in packageMap.values(): if not info.createCustomVsProject and self._hasVsProjectDependency( info, packageMap): info.createCustomVsProject = True self._log.warn( 'Created visual studio project for {0} package even though it wasnt marked as one, because it has csproj dependencies' .format(info.name)) changedOne = True def _hasVsProjectDependency(self, info, packageMap): for dependName in info.allDependencies: if not dependName in packageMap: # For eg. a platform specific dependency continue dependInfo = packageMap[dependName] if dependInfo.createCustomVsProject: return True return False def _ensurePluginPackagesDoNotHaveDependenciesInAssets(self, packageMap): movedProject = True while movedProject: movedProject = False for info in packageMap.values(): if info.isPluginDir and self._hasAssetsDependency( info, packageMap): info.isPluginDir = False self._log.warn( 'Moved {0} package to scripts folder since it has dependencies there and therefore cannot be in plugins' .format(info.name)) movedProject = True def _hasAssetsDependency(self, info, packageMap): for dependName in info.allDependencies: if not dependName in packageMap: # For eg. a platform specific dependency continue dependInfo = packageMap[dependName] if not dependInfo.isPluginDir: return True return False def _addPrebuiltProjectsFromPackages(self, packageMap, prebuiltProjects): for info in packageMap.values(): prebuiltPaths = info.config.tryGetList([], 'Prebuilt') for path in prebuiltPaths: if path not in prebuiltProjects: prebuiltProjects.append(path) def _removePlatformSpecificPackages(self, packageMap, platform): for info in list(packageMap.values()): if info.folderType == FolderTypes.AndroidProject or info.folderType == FolderTypes.AndroidLibraries: platforms = [Platforms.Android] elif info.folderType == FolderTypes.Ios: platforms = [Platforms.Ios] elif info.folderType == FolderTypes.WebGl: platforms = [Platforms.WebGl] else: platforms = info.config.tryGetList([], 'Platforms') if len(platforms) == 0: continue if platform not in platforms: del packageMap[info.name] self._log.debug( 'Skipped project {0} since it is not enabled for platform {0}' .format(info.name, platform)) def _printDependencyTree(self, packageMap): packages = sorted(packageMap.values(), key=lambda p: (p.isPluginDir, -len(p.explicitDependencies))) done = {} for pack in packages: self._printDependency(pack, done, 1, packageMap) def _printDependency(self, package, done, indentCount, packageMap): done[package.name] = True indentInterval = ' ' indent = ((indentCount - 1) * (indentInterval + '.')) + indentInterval self._log.debug(indent + '|-' + package.name) for dependName in package.explicitDependencies: if dependName in packageMap: subPackage = packageMap[dependName] if subPackage.name in done: self._log.debug(indent + '.' + indentInterval + '|~' + subPackage.name) else: self._printDependency(subPackage, done, indentCount + 1, packageMap) def _checkCustomProjectMatch(self, packageName, customProjects): if packageName in customProjects: return True # Allow regex's! for projPattern in customProjects: if projPattern.startswith('/'): projPattern = projPattern[1:] try: if re.match(projPattern, packageName): return True except Exception as e: raise Exception( "Failed while parsing project regex '/{0}' from {1}/{2}. Details: {3}" .format(projPattern, self._varMgr.expand('ProjectName'), ProjectConfigFileName, str(e))) return False def _fillOutDependencies(self, packageMap): self._log.debug('Processing dependency tree') inProgress = set() for info in packageMap.values(): self._fillOutDependenciesForPackage(info, packageMap, inProgress) def _fillOutDependenciesForPackage(self, packageInfo, packageMap, inProgress): if packageInfo.name in inProgress: assertThat( False, "Found circular dependency when processing package {0}. Dependency list: {1}" .format( packageInfo.name, ' -> '.join([x for x in inProgress]) + '-> ' + packageInfo.name)) inProgress.add(packageInfo.name) allDependencies = set(packageInfo.explicitDependencies) for explicitDependName in packageInfo.explicitDependencies: if explicitDependName not in packageMap: # Might be stripped out based on platform or something so just ignore continue explicitDependInfo = packageMap[explicitDependName] if not explicitDependInfo.allDependencies: self._fillOutDependenciesForPackage(explicitDependInfo, packageMap, inProgress) for dependName in explicitDependInfo.allDependencies: allDependencies.add(dependName) packageInfo.allDependencies = list(allDependencies) inProgress.remove(packageInfo.name)
class PrjRunner: _scriptRunner = Inject('ScriptRunner') _config = Inject('Config') _packageMgr = Inject('PackageManager') _unityHelper = Inject('UnityHelper') _varMgr = Inject('VarManager') _log = Inject('Logger') _mainConfig = InjectOptional('MainConfigPath', None) _sys = Inject('SystemHelper') _vsSolutionHelper = Inject('VisualStudioHelper') _releaseSourceManager = Inject('ReleaseSourceManager') def run(self, args): self._args = args success = self._scriptRunner.runWrapper(self._runInternal) if not success: sys.exit(1) def _runPreBuild(self): if self._args.openDocumentation: self._openDocumentation() if self._args.clearProjectGeneratedFiles: self._packageMgr.clearProjectGeneratedFiles(self._project) if self._args.clearAllProjectGeneratedFiles: self._packageMgr.clearAllProjectGeneratedFiles() if self._args.deleteAllLinks: self._packageMgr.deleteAllLinks() if self._args.deletePackage: if not self._args.suppressPrompts: if not MiscUtil.confirmChoice("Are you sure you want to delete package '{0}'? (y/n) \nNote that this change is non-recoverable! (unless you are using source control) ".format(self._args.deletePackage)): assertThat(False, "User aborted operation") self._packageMgr.deletePackage(self._args.deletePackage) if self._args.deleteProject: if not self._args.suppressPrompts: if not MiscUtil.confirmChoice("Are you sure you want to delete project '{0}'? (y/n) \nNote that this will only delete your unity project settings and the {1} for this project. \nThe rest of the content for your project will remain in the UnityPackages folder ".format(self._args.deleteProject, ProjectConfigFileName)): assertThat(False, "User aborted operation") self._packageMgr.deleteProject(self._args.deleteProject) if self._args.installRelease: releaseName, releaseVersion = self._args.installRelease self._releaseSourceManager.installReleaseByName(releaseName, releaseVersion) if self._args.init: self._packageMgr.updateLinksForAllProjects() if self._args.updateLinks: self._packageMgr.updateProjectJunctions(self._project, self._platform) if self._args.updateUnitySolution: self._vsSolutionHelper.updateUnitySolution(self._project, self._platform) if self._args.updateCustomSolution: self._vsSolutionHelper.updateCustomSolution(self._project, self._platform) def _openDocumentation(self): webbrowser.open('https://github.com/modesttree/ModestUnityPackageManager') def _runBuild(self): if self._args.buildCustomSolution: self._vsSolutionHelper.buildCustomSolution(self._project, self._platform) def _runPostBuild(self): if self._args.listReleases: self._releaseSourceManager.listAllReleases() if self._args.listProjects: self._packageMgr.listAllProjects() if self._args.listPackages: self._packageMgr.listAllPackages() if self._args.openUnity: self._packageMgr.checkProjectInitialized(self._project, self._platform) self._unityHelper.openUnity(self._project, self._platform) if self._args.openCustomSolution: self._vsSolutionHelper.openCustomSolution(self._project, self._platform) if self._args.editProjectYaml: self._editProjectYaml() def _editProjectYaml(self): assertThat(self._project) schemaPath = self._varMgr.expandPath('[UnityProjectsDir]/{0}/{1}'.format(self._project, ProjectConfigFileName)) os.startfile(schemaPath) def _runInternal(self): self._log.debug("Started Prj with arguments: {0}".format(" ".join(sys.argv[1:]))) self.processArgs() self._validateArgs() if self._args.createConfig: self._createConfig() if self._args.createProject: self._packageMgr._createProject(self._args.project) if self._args.createPackage: self._packageMgr.createPackage(self._args.createPackage) self._runPreBuild() self._runBuild() self._runPostBuild() def _createConfig(self): self._log.heading('Initializing new projeny config') assertThat(not self._mainConfig, "Cannot initialize new projeny project, found existing config at '{0}'".format(self._mainConfig)) curDir = os.getcwd() configPath = os.path.join(curDir, ConfigFileName) assertThat(not os.path.isfile(configPath), "Found existing projeny config at '{0}'. Has the configuration already been created?", configPath) self._sys.createDirectory(os.path.join(curDir, 'UnityPackages')) self._sys.createDirectory(os.path.join(curDir, 'UnityProjects')) with self._sys.openOutputFile(configPath) as outFile: outFile.write( """ PathVars: UnityPackagesDir: '[ConfigDir]/UnityPackages' UnityProjectsDir: '[ConfigDir]/UnityProjects' LogPath: '[ConfigDir]/PrjLog.txt' """) def processArgs(self): self._project = self._args.project if not self._project: self._project = self._config.tryGetString(None, 'DefaultProject') if self._project and not self._packageMgr.projectExists(self._project) and not self._args.createProject: self._project = self._packageMgr.getProjectFromAlias(self._project) if not self._project and self._varMgr.hasKey('UnityProjectsDir'): allProjects = self._packageMgr.getAllProjectNames() # If there's only one project, then just always assume they are operating on that if len(allProjects) == 1: self._project = allProjects[0] self._platform = PlatformUtil.fromPlatformArgName(self._args.platform) def _validateArgs(self): requiresProject = self._args.updateLinks or self._args.updateUnitySolution \ or self._args.updateCustomSolution or self._args.buildCustomSolution \ or self._args.clearProjectGeneratedFiles or self._args.buildFull \ or self._args.openUnity or self._args.openCustomSolution \ or self._args.editProjectYaml or self._args.createProject if requiresProject and not self._project: assertThat(False, "Cannot execute the given arguments without a project specified, or a default project defined in the {0} file", ConfigFileName)
class UnityHelper: _log = Inject('Logger') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _commonSettings = Inject('CommonSettings') def __init__(self): pass def onUnityLog(self, logStr): self._log.debug(logStr) def runEditorFunction(self, projectName, platform, editorCommand, batchMode=True, quitAfter=True, extraExtraArgs=''): extraArgs = '' if quitAfter: extraArgs += ' -quit' if batchMode: extraArgs += ' -batchmode -nographics' extraArgs += ' ' + extraExtraArgs self.runEditorFunctionRaw(projectName, platform, editorCommand, extraArgs) def openUnity(self, projectName, platform): self._log.heading('Opening Unity') projectPath = self._sys.canonicalizePath( "[UnityProjectsDir]/{0}/{1}-{2}".format( projectName, self._commonSettings.getShortProjectName(projectName), PlatformUtil.toPlatformFolderName(platform))) self._sys.executeNoWait( '"[UnityExePath]" -buildTarget {0} -projectPath "{1}"'.format( self._getBuildTargetArg(platform), projectPath)) def _getBuildTargetArg(self, platform): if platform == Platforms.Windows: return 'win32' if platform == Platforms.WebPlayer: return 'web' if platform == Platforms.Android: return 'android' if platform == Platforms.WebGl: return 'WebGl' if platform == Platforms.OsX: return 'osx' if platform == Platforms.Linux: return 'linux' if platform == Platforms.Ios: return 'ios' assertThat(False) def runEditorFunctionRaw(self, projectName, platform, editorCommand, extraArgs): logPath = self._varMgr.expandPath(UnityLogFileLocation) logWatcher = LogWatcher(logPath, self.onUnityLog) logWatcher.start() os.environ['ModestTreeBuildConfigOverride'] = "FromBuildScript" assertThat(self._varMgr.hasKey('UnityExePath'), "Could not find path variable 'UnityExePath'") try: command = '"[UnityExePath]" -buildTarget {0} -projectPath "[UnityProjectsDir]/{1}/{2}-{3}"'.format( self._getBuildTargetArg(platform), projectName, self._commonSettings.getShortProjectName(projectName), PlatformUtil.toPlatformFolderName(platform)) if editorCommand: command += ' -executeMethod ' + editorCommand command += ' ' + extraArgs self._sys.executeAndWait(command) except ProcessErrorCodeException as e: raise UnityReturnedErrorCodeException( "Error while running Unity! Command returned with error code." ) except: raise UnityUnknownErrorException( "Unknown error occurred while running Unity!") finally: logWatcher.stop() while not logWatcher.isDone: time.sleep(0.1) os.environ['ModestTreeBuildConfigOverride'] = ""
class VisualStudioHelper: _log = Inject('Logger') _config = Inject('Config') _packageManager = Inject('PackageManager') _unityHelper = Inject('UnityHelper') _varMgr = Inject('VarManager') _sys = Inject('SystemHelper') _vsSolutionGenerator = Inject('VisualStudioSolutionGenerator') def openFile(self, filePath, lineNo, project, platform): if not lineNo or lineNo <= 0: lineNo = 1 if MiscUtil.doesProcessExist('^devenv\.exe$'): self.openFileInExistingVisualStudioInstance(filePath, lineNo) # This works too but doesn't allow going to a specific line #self._sys.executeNoWait('[VisualStudioCommandLinePath] /edit "{0}"'.format(filePath)) else: # Unfortunately, in this case we can't pass in the line number self.openCustomSolution(project, platform, filePath) def openFileInExistingVisualStudioInstance(self, filePath, lineNo): try: dte = win32com.client.GetActiveObject("VisualStudio.DTE.12.0") dte.MainWindow.Activate dte.ItemOperations.OpenFile(self._sys.canonicalizePath(filePath)) dte.ActiveDocument.Selection.MoveToLineAndOffset(lineNo, 1) except Exception as error: raise Exception("COM Error. This is often triggered when given a bad line number. Details: {0}".format(win32api.FormatMessage(error.excepinfo[5]))) def openVisualStudioSolution(self, solutionPath, filePath = None): if not self._varMgr.hasKey('VisualStudioIdePath'): assertThat(False, "Path to visual studio has not been defined. Please set <VisualStudioIdePath> within one of your {0} files. See documentation for details.", ConfigFileName) if self._sys.fileExists('[VisualStudioIdePath]'): self._sys.executeNoWait('"[VisualStudioIdePath]" {0} {1}'.format(self._sys.canonicalizePath(solutionPath), self._sys.canonicalizePath(filePath) if filePath else "")) else: assertThat(False, "Cannot find path to visual studio. Expected to find it at '{0}'".format(self._varMgr.expand('[VisualStudioIdePath]'))) def updateCustomSolution(self, project, platform): self._vsSolutionGenerator.updateVisualStudioSolution(project, platform) def openCustomSolution(self, project, platform, filePath = None): self.openVisualStudioSolution(self._getCustomSolutionPath(project, platform), filePath) def buildCustomSolution(self, project, platform): solutionPath = self._getCustomSolutionPath(project, platform) if not self._sys.fileExists(solutionPath): self._log.warn('Could not find generated custom solution. Generating now.') self._vsSolutionGenerator.updateVisualStudioSolution(project, platform) self._log.heading('Building {0}-{1}.sln'.format(project, platform)) self.buildVisualStudioProject(solutionPath, 'Debug') def buildVisualStudioProject(self, solutionPath, buildConfig): if self._config.getBool('Compilation', 'UseDevenv'): buildCommand = '"[VisualStudioCommandLinePath]" {0} /build "{1}"'.format(solutionPath, buildConfig) else: buildCommand = '"[MsBuildExePath]" /p:VisualStudioVersion=12.0' #if rebuild: #buildCommand += ' /t:Rebuild' buildCommand += ' /p:Configuration="{0}" "{1}"'.format(buildConfig, solutionPath) self._sys.executeAndWait(buildCommand) def _getCustomSolutionPath(self, project, platform): return '[UnityProjectsDir]/{0}/{0}-{1}.sln'.format(project, platform) def updateUnitySolution(self, projectName, platform): """ Simply runs unity and then generates the monodevelop solution file using an editor script This is used when generating the Visual Studio Solution to get DLL references and defines etc. """ self._log.heading('Updating unity generated solution for project {0} ({1})'.format(projectName, platform)) self._packageManager.checkProjectInitialized(projectName, platform) # This will generate the unity csproj files which we need to generate Modest3d.sln correctly # It's also necessary to run this first on clean checkouts to initialize unity properly self._unityHelper.runEditorFunction(projectName, platform, 'Projeny.ProjenyEditorUtil.ForceGenerateUnitySolution')
class UnityPackageAnalyzer: _log = Inject('Logger') _sys = Inject('SystemHelper') def getReleaseInfoFromUnityPackage(self, unityPackagePath): fileName = os.path.basename(unityPackagePath) #self._log.debug("Analyzing unity package '{0}'", fileName) assertThat(self._sys.fileExists(unityPackagePath)) info = ReleaseInfo() info.localPath = unityPackagePath headerInfo = self._tryGetAssetStoreInfoFromHeader(unityPackagePath) info.compressedSize = os.path.getsize(unityPackagePath) info.fileModificationDate = datetime.utcfromtimestamp( os.path.getmtime(unityPackagePath)) if headerInfo: info.name = headerInfo['title'] info.versionCode = int(headerInfo['version_id']) info.version = headerInfo['version'] info.id = headerInfo['id'] info.assetStoreInfo = self._getAssetStoreInfo(headerInfo) else: # If there are no headers, then we have to derive the info from the file name info.id, info.name, info.versionCode, info.version = self._getInfoFromFileName( fileName) return info def _getInfoFromFileName(self, fileName): parts = os.path.splitext(fileName) assertThat(parts[1].lower() == '.unitypackage') baseName = parts[0].strip() match = re.match('^(.*)@\s*(\d+\.?\d*)\s*$', baseName) if match: groups = match.groups() name = groups[0] versionStr = groups[1] numDecimals = len(re.match('^\d+\.(\d*)$', versionStr).groups()[0]) assertThat( numDecimals <= 7, 'Projeny only supports up to 7 decimal points in the version number!' ) # Need a flat int so we can do greater than/less than comparisons versionCode = int(10000000 * float(groups[1])) return (name, name, versionCode, versionStr) return (baseName, baseName, None, None) def _getAssetStoreInfo(self, allInfo): info = AssetStoreInfo() info.publisherId = allInfo['publisher']['id'] info.publisherLabel = allInfo['publisher']['label'] info.publishNotes = allInfo.get('publishnotes', '') info.categoryId = allInfo['category']['id'] info.categoryLabel = allInfo['category']['label'] info.uploadId = allInfo.get('upload_id', None) info.description = allInfo.get('description', '') pubDate = allInfo['pubdate'] info.publishDate = datetime.strptime(pubDate, "%d %b %Y") info.unityVersion = allInfo.get('unity_version', None) info.linkId = allInfo['link']['id'] info.linkType = allInfo['link']['type'] return info def _tryGetAssetStoreInfoFromHeader(self, unityPackagePath): with open(unityPackagePath, 'rb') as f: headerBytes = f.read(16) headerHexValues = binascii.hexlify(headerBytes) headerString = headerHexValues.decode('utf8') flag1 = headerString[0:4] unixTimeStamp = (headerBytes[7] << 24) + (headerBytes[6] << 16) + ( headerBytes[5] << 8) + headerBytes[4] datetime.utcfromtimestamp(unixTimeStamp) flag2 = headerString[6:8] flag3 = headerString[24:28] numBytes = int(headerString[22:24] + headerString[20:22], 16) numJsonBytes = int(headerString[30:32] + headerString[28:30], 16) assertThat(flag1 == "1f8b", "Invalid .unitypackage file") # These flags indicate that it is an asset store package if flag2 == "04" and flag3 == "4124": assertThat(numBytes == numJsonBytes + 4) infoBytes = f.read(numJsonBytes) infoStr = infoBytes.decode('utf8') return json.loads(infoStr) return None
class PackageManager: """ Main interface for Modest Package Manager """ _config = Inject('Config') _varMgr = Inject('VarManager') _log = Inject('Logger') _sys = Inject('SystemHelper') _unityHelper = Inject('UnityHelper') _junctionHelper = Inject('JunctionHelper') _projectInitHandlers = InjectMany('ProjectInitHandlers') _schemaLoader = Inject('ProjectSchemaLoader') _commonSettings = Inject('CommonSettings') _vsSolutionHelper = Inject('VisualStudioHelper') def __init__(self): self._packageInfos = None def projectExists(self, projectName): return self._sys.directoryExists('[UnityProjectsDir]/{0}'.format(projectName)) or self._sys.fileExists('[UnityProjectsDir]/{0}.ini'.format(projectName)) def listAllProjects(self): projectNames = self.getAllProjectNames() self._log.info("Found {0} Projects:".format(len(projectNames))) for proj in projectNames: alias = self.tryGetAliasFromFullName(proj) if alias: proj = "{0} ({1})".format(proj, alias) self._log.info(" " + proj) def listAllPackages(self): packagesNames = self.getAllPackageNames() self._log.info("Found {0} Packages:".format(len(packagesNames))) for packageName in packagesNames: self._log.info(" " + packageName) def _findSourceControl(self): for dirPath in self._sys.getParentDirectoriesWithSelf('[ConfigDir]'): if self._sys.directoryExists(os.path.join(dirPath, '.git')): return SourceControlTypes.Git if self._sys.directoryExists(os.path.join(dirPath, '.svn')): return SourceControlTypes.Subversion return None def _createProject(self, projName): self._log.heading('Initializing new project "{0}"', projName) projDirPath = self._varMgr.expand('[UnityProjectsDir]/{0}'.format(projName)) assertThat(not self._sys.directoryExists(projDirPath), "Cannot initialize new project '{0}', found existing project at '{1}'", projName, projDirPath) self._sys.createDirectory(projDirPath) with self._sys.openOutputFile(os.path.join(projDirPath, ProjectConfigFileName)) as outFile: outFile.write( """ #AssetsFolder: # Uncomment and Add package names here """) self.updateProjectJunctions(projName, Platforms.Windows) def getProjectFromAlias(self, alias): aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases') assertThat(alias in aliasMap.keys(), "Unrecognized project '{0}' and could not find an alias with that name either".format(alias)) return aliasMap[alias] def tryGetAliasFromFullName(self, name): aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases') for pair in aliasMap.items(): if pair[1] == name: return pair[0] return None def _validateDirForFolderType(self, packageInfo, sourceDir): if packageInfo.folderType == FolderTypes.AndroidProject: assertThat(os.path.exists(os.path.join(sourceDir, "project.properties")), "Project '{0}' is marked with foldertype AndroidProject and therefore must contain a project.properties file".format(packageInfo.name)) def updateProjectJunctions(self, projectName, platform): """ Initialize all the folder links for the given project """ self._log.heading('Updating package directories for project {0}'.format(projectName)) self.checkProjectInitialized(projectName, platform) self.setPathsForProject(projectName, platform) schema = self._schemaLoader.loadSchema(projectName, platform) self._updateDirLinksForSchema(schema) self._checkForVersionControlIgnore() self._log.good('Finished updating packages for project "{0}"'.format(schema.name)) def _checkForVersionControlIgnore(self): sourceControlType = self._findSourceControl() if sourceControlType == SourceControlTypes.Git: self._log.heading('Detected git repository. Making sure generated project folders are ignored by git...') if not self._sys.fileExists('[ProjectRootGitIgnoreTemplate]'): self._log.heading('Adding missing git ignore file to project root') self._sys.copyFile('[ProjectRootGitIgnoreTemplate]', os.path.join(projDirPath, '.gitignore')) elif sourceControlType == SourceControlTypes.Subversion: self._log.info('Detected subversion repository. Making sure generated project folders are ignored by SVN...') try: self._sys.executeAndWait('svn propset svn:ignore -F [ProjectRootSvnIgnoreTemplate] .', '[ProjectRoot]') except Exception as e: self._log.warn("Warning: Failed to add generated project directories to SVN ignore! This may be caused by 'svn' not being available on the command line. Details: " + str(e)) #else: #self._log.warn('Warning: Could not determine source control in use! An ignore file will not be added for your project.') def getAllPackageInfos(self): if not self._packageInfos: self._packageInfos = [] for name in self.getAllPackageNames(): path = self._varMgr.expandPath('[UnityPackagesDir]/{0}'.format(name)) installInfoFilePath = os.path.join(path, InstallInfoFileName) info = PackageInfo() info.name = name info.path = path if self._sys.fileExists(installInfoFilePath): installInfo = YamlSerializer.deserialize(self._sys.readFileAsText(installInfoFilePath)) info.installInfo = installInfo self._packageInfos.append(info) return self._packageInfos def deleteProject(self, projName): self._log.heading("Deleting project '{0}'", projName) assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars. Have you set up your {0} file?", ConfigFileName) fullPath = '[UnityProjectsDir]/{0}'.format(projName) assertThat(self._sys.directoryExists(fullPath), "Could not find project with name '{0}' - delete failed", projName) self.clearProjectGeneratedFiles(projName, True) self._sys.deleteDirectory(fullPath) def createPackage(self, packageName): assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars. Have you set up your {0} file?", ConfigFileName) self._log.heading('Creating new package "{0}"', packageName) newPath = '[UnityPackagesDir]/{0}'.format(packageName) assertThat(not self._sys.directoryExists(newPath), "Found existing package at path '{0}'", newPath) self._sys.createDirectory(newPath) # This can be nice when sorting packages by install date, but it adds noise to the directory # Seems nicer to just leave the directory empty for custom packages #newInstallInfo = PackageInstallInfo() #newInstallInfo.releaseInfo = None #newInstallInfo.installDate = datetime.utcnow() #yamlStr = YamlSerializer.serialize(newInstallInfo) #self._sys.writeFileAsText(os.path.join(newPath, InstallInfoFileName), yamlStr) def deletePackage(self, name): self._log.heading("Deleting package '{0}'", name) assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars. Have you set up your {0} file?", ConfigFileName) fullPath = '[UnityPackagesDir]/{0}'.format(name) assertThat(self._sys.directoryExists(fullPath), "Could not find package with name '{0}' - delete failed", name) self._sys.deleteDirectory(fullPath) def getAllPackageNames(self): assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars. Have you set up your {0} file?", ConfigFileName) results = [] for name in self._sys.walkDir('[UnityPackagesDir]'): if self._sys.IsDir('[UnityPackagesDir]/' + name): results.append(name) return results def getAllProjectNames(self): assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars. Have you set up your {0} file?", ConfigFileName) results = [] for name in self._sys.walkDir('[UnityProjectsDir]'): if self._sys.IsDir('[UnityProjectsDir]/' + name): results.append(name) return results # This will set up all the directory junctions for all projects for all platforms def updateLinksForAllProjects(self): for projectName in self.getAllProjectNames(): self._log.heading('Initializing project "{0}"'.format(projectName)) try: #for platform in Platforms.All: for platform in [Platforms.Windows]: self.updateProjectJunctions(projectName, platform) self._log.good('Successfully initialized project "{0}"'.format(projectName)) except Exception as e: self._log.warn('Failed to initialize project "{0}": {1}'.format(projectName, e)) def _createPlaceholderCsFile(self, path): with self._sys.openOutputFile(path) as outFile: outFile.write( """ // This file exists purely as a way to force unity to generate the MonoDevelop csproj files so that Projeny can read the settings from it """) def _createSwitchProjectMenuScript(self, currentProjName, outputPath): foundCurrent = False menuFile = """ using UnityEditor; using Projeny.Internal; namespace Projeny { public static class ProjenyChangeProjectMenu {""" projIndex = 1 for projName in self.getAllProjectNames(): menuFile += """ [MenuItem("Projeny/Change Project/{0}", false, 8)]""".format(projName) menuFile += """ public static void ChangeProject{0}()""".format(projIndex) menuFile += """ {""" menuFile += """ PrjHelper.ChangeProject("{0}");""".format(projName) menuFile += """ } """ if projName == currentProjName: assertThat(not foundCurrent) foundCurrent = True menuFile += """ [MenuItem("Projeny/Change Project/{0}", true, 8)]""".format(currentProjName) menuFile += """ public static bool ChangeProject{0}Validate()""".format(projIndex) menuFile += """ { return false; }""" projIndex += 1 menuFile += """ } } """ assertThat(foundCurrent) self._sys.writeFileAsText(outputPath, menuFile) def _updateDirLinksForSchema(self, schema): self._removePackageJunctions() self._sys.deleteDirectoryIfExists('[PluginsDir]/Projeny') if self._config.getBool('LinkToProjenyEditorDir') and not MiscUtil.isRunningAsExe(): self._junctionHelper.makeJunction('[ProjenyDir]/UnityPlugin/Projeny', '[PluginsDir]/Projeny/Editor/Source') else: dllOutPath = '[PluginsDir]/Projeny/Editor/Projeny.dll' self._sys.copyFile('[ProjenyUnityEditorDllPath]', dllOutPath) self._sys.copyFile('[ProjenyUnityEditorDllMetaFilePath]', dllOutPath + '.meta') self._sys.copyFile('[YamlDotNetDllPath]', '[PluginsDir]/Projeny/Editor/YamlDotNet.dll') assetsOutPath = '[PluginsDir]/Projeny/Editor/Assets' self._sys.copyDirectory('[ProjenyUnityEditorAssetsDirPath]', assetsOutPath) settingsFileOutPath = os.path.join(assetsOutPath, 'Resources/Projeny/PmSettings.asset') self._sys.writeFileAsText(settingsFileOutPath, self._sys.readFileAsText(settingsFileOutPath).replace( 'm_Script: {fileID: 11500000, guid: 01fe9b81f68762b438dd4eecbcfe2900, type: 3}', 'm_Script: {fileID: 1582608718, guid: b7b2ba04b543d234aa4225d91c60af2b, type: 3}')) self._createSwitchProjectMenuScript(schema.name, '[PluginsDir]/Projeny/Editor/ProjenyChangeProjectMenu.cs') self._createPlaceholderCsFile('[PluginsDir]/Projeny/Placeholder.cs') self._createPlaceholderCsFile('[PluginsDir]/Projeny/Editor/Placeholder.cs') for packageInfo in schema.packages.values(): self._log.debug('Processing package "{0}"'.format(packageInfo.name)) sourceDir = self._varMgr.expandPath('[UnityPackagesDir]/{0}'.format(packageInfo.name)) self._validateDirForFolderType(packageInfo, sourceDir) assertThat(os.path.exists(sourceDir), "Could not find package with name '{0}' while processing schema '{1}'. See build log for full object graph to see where it is referenced".format(packageInfo.name, schema.name)) outputPackageDir = self._varMgr.expandPath(packageInfo.outputDirVar) linkDir = os.path.join(outputPackageDir, packageInfo.name) assertThat(not os.path.exists(linkDir), "Did not expect this path to exist: '{0}'".format(linkDir)) self._junctionHelper.makeJunction(sourceDir, linkDir) def checkProjectInitialized(self, projectName, platform): self.setPathsForProject(projectName, platform) if self._sys.directoryExists('[ProjectPlatformRoot]'): return self._log.warn('Project "{0}" is not initialized for platform "{1}". Initializing now.'.format(projectName, platform)) self._initNewProjectForPlatform(projectName, platform) def setPathsForProject(self, projectName, platform): self._varMgr.set('ShortProjectName', self._commonSettings.getShortProjectName(projectName)) self._varMgr.set('ShortPlatform', PlatformUtil.toPlatformFolderName(platform)) self._varMgr.set('Platform', platform) self._varMgr.set('ProjectName', projectName) self._varMgr.set('ProjectRoot', '[UnityProjectsDir]/[ProjectName]') self._varMgr.set('ProjectPlatformRoot', '[ProjectRoot]/[ShortProjectName]-[ShortPlatform]') self._varMgr.set('ProjectAssetsDir', '[ProjectPlatformRoot]/Assets') self._varMgr.set('UnityGeneratedProjectEditorPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Editor.Plugins.csproj') self._varMgr.set('UnityGeneratedProjectPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Plugins.csproj') # For reasons I don't understand, the unity generated project is named with 'Assembly' on some machines and not other # Problem due to unity version but for now just allow either or self._varMgr.set('UnityGeneratedProjectEditorPath2', '[ProjectPlatformRoot]/Assembly-CSharp-Editor-firstpass.csproj') self._varMgr.set('UnityGeneratedProjectPath2', '[ProjectPlatformRoot]/Assembly-CSharp-firstpass.csproj') self._varMgr.set('PluginsDir', '[ProjectAssetsDir]/Plugins') self._varMgr.set('PluginsAndroidDir', '[PluginsDir]/Android') self._varMgr.set('PluginsAndroidLibraryDir', '[PluginsDir]/Android/libs') self._varMgr.set('PluginsIosLibraryDir', '[PluginsDir]/iOS') self._varMgr.set('PluginsWebGlLibraryDir', '[PluginsDir]/WebGL') self._varMgr.set('StreamingAssetsDir', '[ProjectAssetsDir]/StreamingAssets') self._varMgr.set('IntermediateFilesDir', '[ProjectPlatformRoot]/obj') self._varMgr.set('SolutionPath', '[ProjectRoot]/[ProjectName]-[Platform].sln') def deleteAllLinks(self): self._log.heading('Deleting all junctions for all projects') projectNames = [] projectsDir = self._varMgr.expandPath('[UnityProjectsDir]') for itemName in os.listdir(projectsDir): fullPath = os.path.join(projectsDir, itemName) if os.path.isdir(fullPath): projectNames.append(itemName) for projectName in projectNames: for platform in Platforms.All: self.setPathsForProject(projectName, platform) self._removeJunctionsForProjectPlatform() def _removePackageJunctions(self): self._junctionHelper.removeJunctionsInDirectory('[ProjectAssetsDir]', True) def _removeJunctionsForProjectPlatform(self): self._junctionHelper.removeJunction('[ProjectPlatformRoot]/ProjectSettings') self._removePackageJunctions() def clearAllProjectGeneratedFiles(self, addHeadings = True): for projName in self.getAllProjectNames(): self.clearProjectGeneratedFiles(projName, addHeadings) def clearProjectGeneratedFiles(self, projectName, addHeading = True): if addHeading: self._log.heading('Clearing generated files for project {0}'.format(projectName)) self._junctionHelper.removeJunctionsInDirectory('[UnityProjectsDir]/{0}'.format(projectName), True) for platform in Platforms.All: self.setPathsForProject(projectName, platform) if os.path.exists(self._varMgr.expandPath('[ProjectPlatformRoot]')): platformRootPath = self._varMgr.expand('[ProjectPlatformRoot]') try: shutil.rmtree(platformRootPath) except: self._log.warn('Unable to remove path {0}. Trying to kill adb.exe to see if that will help...'.format(platformRootPath)) MiscUtil.tryKillAdbExe(self._sys) try: shutil.rmtree(platformRootPath) except: self._log.error('Still unable to remove path {0}! A running process may have one of the files locked. Ensure you have closed down unity / visual studio / etc.'.format(platformRootPath)) raise self._log.debug('Removed project directory {0}'.format(platformRootPath)) self._log.good('Successfully deleted project {0} ({1})'.format(projectName, platform)) else: self._log.debug('Project {0} ({1}) already deleted'.format(projectName, platform)) # Remove the solution files and the suo files etc. self._sys.removeByRegex('[ProjectRoot]/[ProjectName]-[Platform].*') def _initNewProjectForPlatform(self, projectName, platform): self._log.heading('Initializing new project {0} ({1})'.format(projectName, platform)) schema = self._schemaLoader.loadSchema(projectName, platform) self.setPathsForProject(projectName, platform) if not self._sys.directoryExists('[ProjectRoot]/ProjectSettings'): self._sys.createDirectory('[ProjectRoot]/ProjectSettings') if self._sys.directoryExists('[ProjectPlatformRoot]'): raise Exception('Unable to create project "{0}". Directory already exists at path "{1}".'.format(projectName, self._varMgr.expandPath('[ProjectPlatformRoot]'))) try: self._sys.createDirectory('[ProjectPlatformRoot]') self._log.debug('Created directory "{0}"'.format(self._varMgr.expandPath('[ProjectPlatformRoot]'))) self._junctionHelper.makeJunction('[ProjectRoot]/ProjectSettings', '[ProjectPlatformRoot]/ProjectSettings') self._updateDirLinksForSchema(schema) for handler in self._projectInitHandlers: handler.onProjectInit(projectName, platform) except: self._log.endHeading() self._log.error("Failed to initialize project '{0}' for platform '{1}'.".format(schema.name, platform)) raise self._log.good('Finished creating new project "{0}" ({1})'.format(schema.name, platform))