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): with 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, packageRootDir, releaseInfo, forcedName): fileInfo = next(x for x in self._files if x.release == releaseInfo) assertIsNotNone(fileInfo) return self._extractor.extractUnityPackage(packageRootDir, 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) assertThat(self._sys.directoryExists(actualPath)) 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 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): with 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, packageRootDir, releaseInfo, forcedName): assertThat(releaseInfo.url) try: with self._log.heading("Downloading release from url '{0}'".format( releaseInfo.url)): 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( packageRootDir, tempFilePath, releaseInfo.name, forcedName) finally: if os.path.exists(tempFilePath): os.remove(tempFilePath)
class ProjectConfigChanger: _log = Inject('Logger') _sys = Inject('SystemHelper') _packageManager = Inject('PackageManager') _varMgr = Inject('VarManager') def _getProjectConfigPath(self, projectName): return self._varMgr.expandPath('[UnityProjectsDir]/{0}/{1}'.format( projectName, ProjectConfigFileName)) def _loadProjectConfig(self, projectName): configPath = self._getProjectConfigPath(projectName) yamlData = YamlSerializer.deserialize( self._sys.readFileAsText(configPath)) result = ProjectConfig() for pair in yamlData.__dict__.items(): result.__dict__[pair[0]] = pair[1] return result def _saveProjectConfig(self, projectName, projectConfig): configPath = self._getProjectConfigPath(projectName) self._sys.writeFileAsText(configPath, YamlSerializer.serialize(projectConfig)) def addPackage(self, projectName, packageName, addToAssetsFolder): with self._log.heading('Adding package {0} to project {1}'.format( packageName, projectName)): assertThat( packageName in self._packageManager.getAllPackageNames(), "Could not find the given package '{0}' in the UnityPackages folder", packageName) self._packageManager.setPathsForProjectPlatform( projectName, Platforms.Windows) projConfig = self._loadProjectConfig(projectName) assertThat( packageName not in projConfig.assetsFolder and packageName not in projConfig.pluginsFolder, "Given package '{0}' has already been added to project config", packageName) if addToAssetsFolder: projConfig.assetsFolder.append(packageName) else: projConfig.pluginsFolder.append(packageName) self._saveProjectConfig(projectName, projConfig) self._log.good("Added package '{0}' to file '{1}/{2}'", packageName, projectName, ProjectConfigFileName)
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 ScriptRunner: _log = Inject('Logger') def runWrapper(self, runner): startTime = datetime.now() succeeded = False try: runner() succeeded = True except KeyboardInterrupt as e: self._log.error('Operation aborted by user by hitting CTRL+C') except Exception as e: self._log.error(str(e)) # Only print stack trace if it's a build-script error if not isinstance(e, ProcessErrorCodeException) and not isinstance(e, ProcessTimeoutException): if MiscUtil.isRunningAsExe(): self._log.noise('\n' + traceback.format_exc()) else: self._log.debug('\n' + traceback.format_exc()) totalSeconds = (datetime.now()-startTime).total_seconds() totalSecondsStr = Util.formatTimeDelta(totalSeconds) if succeeded: self._log.good('Operation completed successfully. Took ' + totalSecondsStr + '.\n') else: self._log.info('Operation completed with errors. Took ' + totalSecondsStr + '.\n') return succeeded
class Test2: qux = Inject('Qux') def __init__(self): self.X = 0 def Run(self): print(self.qux.GetValue())
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 Foo1: foo2 = Inject('Foo2') def __init__(self, val): self.val = val def Start(self): print(self.foo2.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 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 Runner: _scriptRunner = Inject('ScriptRunner') _log = Inject('Logger') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _vsSolutionHelper = Inject('VisualStudioHelper') _prjVsSolutionHelper = Inject('ProjenyVisualStudioHelper') 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) if platform == None: solutionPath = None else: solutionPath = self._prjVsSolutionHelper.getCustomSolutionPath(project, platform) self._vsSolutionHelper.openFile( self._args.filePath, lineNo, solutionPath) 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] try: platformProjectDirName = dirs[1] platformDirName = platformProjectDirName[platformProjectDirName.rfind('-')+1:] platform = PlatformUtil.fromPlatformFolderName(platformDirName) except: platform = None return projectName, platform
class LogStreamFile: _varManager = Inject('VarManager') def __init__(self): self._fileStream = self._tryGetFileStream() def log(self, logType, message): if logType == LogType.HeadingStart: 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, 'a', encoding='utf-8', errors='ignore')
class ProjenyVisualStudioHelper: _vsHelper = Inject('VisualStudioHelper') _vsSolutionGenerator = Inject('VisualStudioSolutionGenerator') _log = Inject('Logger') _sys = Inject('SystemHelper') _packageManager = Inject('PackageManager') _unityHelper = Inject('UnityHelper') def updateCustomSolution(self, project, platform): self._vsSolutionGenerator.updateVisualStudioSolution(project, platform) def openCustomSolution(self, project, platform, filePath=None): self._vsHelper.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) with self._log.heading('Building {0}-{1}.sln'.format( project, platform)): self._vsHelper.buildVisualStudioProject(solutionPath, 'Debug') 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. """ with 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 ProjectConfig: """ Misc. helper functions related to windows junctions """ _log = Inject('Logger') def __init__(self): self.pluginsFolder = [] self.assetsFolder = [] self.customDirectories = {} self.solutionProjects = [] self.solutionFolders = [] self.packageFolders = [] self.targetPlatforms = [] self.projectSettingsPath = None def getAssetByName(self, name): for x in self.assetsFolder: if type(x) is dict: if x["name"] == name: return x else: if x == name: return x
class Runner: _log = Inject('Logger') _packageMgr = Inject('PackageManager') _schemaLoader = Inject('ProjectSchemaLoader') _projectConfigChanger = Inject('ProjectConfigChanger') _unityHelper = Inject('UnityHelper') _projVsHelper = Inject('ProjenyVisualStudioHelper') _releaseSourceManager = Inject('ReleaseSourceManager') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') def run(self, project, platform, requestId, param1, param2, param3): 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 self._param3 = param3 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 _outputAllPathVars(self): self._outputContent( YamlSerializer.serialize(self._varMgr.getAllParameters())) def _outputContent(self, value): self._log.noise(value) sys.stderr.write(value) def _runInternal(self): self._packageMgr.setPathsForProjectPlatform(self._project, self._platform) if self._requestId == 'updateLinks': isInit = self._packageMgr.isProjectPlatformInitialized( self._project, self._platform) self._packageMgr.updateProjectJunctions(self._project, self._platform) if not isInit: self._packageMgr.updateLinksForAllProjects() elif self._requestId == 'openUnity': self._packageMgr.checkProjectInitialized(self._project, self._platform) self._unityHelper.openUnity(self._project, self._platform) elif self._requestId == 'getPathVars': self._outputAllPathVars() elif self._requestId == 'updateCustomSolution': self._projVsHelper.updateCustomSolution(self._project, self._platform) elif self._requestId == 'openCustomSolution': self._projVsHelper.openCustomSolution(self._project, self._platform) elif self._requestId == 'listPackages': infos = self._packageMgr.getAllPackageFolderInfos(self._project) for folderInfo in infos: self._outputContent('---\n') self._outputContent( YamlSerializer.serialize(folderInfo) + '\n') elif self._requestId == 'listProjects': projectNames = self._packageMgr.getAllProjectNames() for projName in projectNames: self._outputContent(projName + '\n') elif self._requestId == 'addPackage': self._log.debug("Running addPackage") self._projectConfigChanger.addPackage(self._project, self._param1, True) elif self._requestId == 'setPackages': self._log.debug("Running setPackage") packages = ast.literal_eval(self._param1) if isinstance(packages, list): packages = [n.strip() for n in packages] self._projectConfigChanger.setPackagesForProject( self._project, packages) else: self._log.debug("failing setPackage") elif self._requestId == 'listReleases': for release in self._releaseSourceManager.lookupAllReleases(): self._outputContent('---\n') self._outputContent(YamlSerializer.serialize(release) + '\n') elif self._requestId == 'installRelease': releaseName = self._param1 packageRoot = self._param2 versionCode = self._param3 if versionCode == None or len(versionCode) == 0: versionCode = 0 self._log.info( "Installing release '{0}' into package dir '{1}' with version code '{2}'", releaseName, packageRoot, versionCode) self._releaseSourceManager.installReleaseById( releaseName, self._project, packageRoot, versionCode, True) elif self._requestId == 'createProject': newProjName = self._param1 duplicateSettings = (self._param2 == 'True') self._log.info("Creating new project '{0}'", newProjName) self._packageMgr.createProject( newProjName, self._project if duplicateSettings else None) elif self._requestId == 'listDependencies': packageName = self._param1 schema = self._schemaLoader.loadSchema(self._project, self._platform) self._outputContent( YamlSerializer.serialize([ schema.packages[x].dirPath for x in schema.packages[packageName].allDependencies ])) else: assertThat(False, "Invalid request id '{0}'", self._requestId)
class Logger: _streams = InjectMany('LogStream') _config = Inject('Config') ''' Simple log class to use with build scripts ''' def __init__(self): self._totalStartTime = None self._headingBlocks = [] self.goodPatterns = self._getPatterns('GoodPatterns') self.goodMaps = self._getPatternMaps('GoodPatternMaps') self.infoPatterns = self._getPatterns('InfoPatterns') self.infoMaps = self._getPatternMaps('InfoPatternMaps') self.errorPatterns = self._getPatterns('ErrorPatterns') self.errorMaps = self._getPatternMaps('ErrorPatternMaps') self.warningPatterns = self._getPatterns('WarningPatterns') self.warningMaps = self._getPatternMaps('WarningPatternMaps') self.warningPatternsIgnore = self._getPatterns('WarningPatternsIgnore') self.debugPatterns = self._getPatterns('DebugPatterns') self.debugMaps = self._getPatternMaps('DebugPatternMaps') @property def totalStartTime(self): return self._totalStartTime @property def hasHeading(self): return any(self._headingBlocks) def getCurrentNumHeadings(self): return len(self._headingBlocks) def heading(self, message, *args): if not self._totalStartTime: self._totalStartTime = datetime.now() # Need to format it now so that heading gets the args if len(args) > 0: message = message.format(*args) block = HeadingBlock(self, message) self._headingBlocks.append(block) return block def noise(self, message, *args): self._logInternal(message, LogType.Noise, *args) def debug(self, message, *args): self._logInternal(message, LogType.Debug, *args) def info(self, message, *args): self._logInternal(message, LogType.Info, *args) def error(self, message, *args): self._logInternal(message, LogType.Error, *args) def warn(self, message, *args): self._logInternal(message, LogType.Warn, *args) def good(self, message, *args): self._logInternal(message, LogType.Good, *args) def _logInternal(self, message, logType, *args): if len(args) > 0: message = message.format(*args) newLogType, newMessage = self.classifyMessage(logType, message) for stream in self._streams: stream.log(newLogType, newMessage) def _getPatternMaps(self, settingName): maps = self._config.tryGetDictionary({}, 'Log', settingName) result = [] for key, value in maps.items(): regex = re.compile(key) logMap = LogMap(regex, value) result.append(logMap) return result def _getPatterns(self, settingName): patternStrings = self._config.tryGetList([], 'Log', settingName) result = [] for pattern in patternStrings: result.append(re.compile('.*' + pattern + '.*')) return result def tryMatchPattern(self, message, maps, patterns): for logMap in maps: if logMap.regex.match(message): return logMap.regex.sub(logMap.sub, message) for pattern in patterns: match = pattern.match(message) if match: groups = match.groups() if len(groups) > 0: return groups[0] return message return None def classifyMessage(self, logType, message): if logType != LogType.Noise: # If it is explicitly logged as something by calling for eg. log.info, use info type return logType, message parsedMessage = self.tryMatchPattern(message, self.errorMaps, self.errorPatterns) if parsedMessage: return LogType.Error, parsedMessage if not any(p.match(message) for p in self.warningPatternsIgnore): parsedMessage = self.tryMatchPattern(message, self.warningMaps, self.warningPatterns) if parsedMessage: return LogType.Warn, parsedMessage parsedMessage = self.tryMatchPattern(message, self.goodMaps, self.goodPatterns) if parsedMessage: return LogType.Good, parsedMessage parsedMessage = self.tryMatchPattern(message, self.infoMaps, self.infoPatterns) if parsedMessage: return LogType.Info, parsedMessage parsedMessage = self.tryMatchPattern(message, self.debugMaps, self.debugPatterns) if parsedMessage: return LogType.Debug, parsedMessage return LogType.Noise, message
class LogStreamConsole: _log = Inject('Logger') _sys = Inject('SystemHelper') _varManager = Inject('VarManager') _config = Inject('Config') def __init__(self, verbose, veryVerbose): self._verbose = verbose self._veryVerbose = veryVerbose self.headingPatterns = self._getPatterns('HeadingPatterns') self.headingMaps = self._getPatternMaps('HeadingPatternMaps') self.goodPatterns = self._getPatterns('GoodPatterns') self.goodMaps = self._getPatternMaps('GoodPatternMaps') self.infoPatterns = self._getPatterns('InfoPatterns') self.infoMaps = self._getPatternMaps('InfoPatternMaps') self.errorPatterns = self._getPatterns('ErrorPatterns') self.errorMaps = self._getPatternMaps('ErrorPatternMaps') self.warningPatterns = self._getPatterns('WarningPatterns') self.warningMaps = self._getPatternMaps('WarningPatternMaps') self.warningPatternsIgnore = self._getPatterns('WarningPatternsIgnore') self.debugPatterns = self._getPatterns('DebugPatterns') self.debugMaps = self._getPatternMaps('DebugPatternMaps') self._useColors = self._config.tryGetBool(False, 'Console', 'UseColors') self._fileStream = None if self._config.tryGetBool(False, 'Console', 'OutputToFilteredLog'): self._fileStream = self._getFileStream() if self._useColors: self._initColors() def _initColors(self): self._defaultColors = ColorConsole.get_text_attr() self._defaultBg = self._defaultColors & 0x0070 self._defaultFg = self._defaultColors & 0x0007 def log(self, logType, message): logType, message = self.classifyMessage(logType, message) if logType is not None: if logType == LogType.HeadingFailed or logType == LogType.Error: self._output(logType, message, sys.stderr, self._useColors) else: self._output(logType, message, sys.stdout, self._useColors) if self._fileStream: self._output(logType, message, self._fileStream, False) def _getFileStream(self): primaryPath = self._varManager.expand('[LogFilteredPath]') if not primaryPath: raise Exception("Could not find path for log file") previousPath = None if self._varManager.hasKey('LogFilteredPreviousPath'): previousPath = self._varManager.expand('[LogFilteredPreviousPath]') # Keep one old build log if os.path.isfile(primaryPath) and previousPath: shutil.copy2(primaryPath, previousPath) return open(primaryPath, 'w', encoding='utf-8', errors='ignore') def _output(self, logType, message, stream, useColors): stream.write('\n') if self._log.hasHeading and logType != LogType.Heading and logType != LogType.HeadingSucceeded and logType != LogType.HeadingFailed: stream.write(' ') if not useColors or logType == LogType.Info: stream.write(message) stream.flush() else: ColorConsole.set_text_attr(self._getColorAttrs(logType)) stream.write(message) stream.flush() ColorConsole.set_text_attr(self._defaultColors) def _getColorAttrs(self, logType): if logType == LogType.Heading or logType == LogType.HeadingSucceeded: return ColorConsole.FOREGROUND_CYAN | self._defaultBg | ColorConsole.FOREGROUND_INTENSITY if logType == LogType.Good: return ColorConsole.FOREGROUND_GREEN | self._defaultBg | ColorConsole.FOREGROUND_INTENSITY if logType == LogType.Warn: return ColorConsole.FOREGROUND_YELLOW | self._defaultBg | ColorConsole.FOREGROUND_INTENSITY if logType == LogType.Error or logType == LogType.HeadingFailed: return ColorConsole.FOREGROUND_RED | self._defaultBg | ColorConsole.FOREGROUND_INTENSITY if logType == LogType.Debug: return ColorConsole.FOREGROUND_BLACK | self._defaultBg | ColorConsole.FOREGROUND_INTENSITY assertThat(False, 'Unrecognized log type "{0}"'.format(logType)) def _getPatternMaps(self, settingName): maps = self._config.tryGetDictionary({}, 'Console', settingName) result = [] for key, value in maps.items(): regex = re.compile(key) logMap = LogMap(regex, value) result.append(logMap) return result def _getPatterns(self, settingName): patternStrings = self._config.tryGetList([], 'Console', settingName) result = [] for pattern in patternStrings: result.append(re.compile('.*' + pattern + '.*')) return result def tryMatchPattern(self, message, maps, patterns): for logMap in maps: if logMap.regex.match(message): return logMap.regex.sub(logMap.sub, message) for pattern in patterns: match = pattern.match(message) if match: groups = match.groups() if len(groups) > 0: return groups[0] return message return None def classifyMessage(self, logType, message): if logType == LogType.Info or logType == LogType.Heading or logType == LogType.HeadingFailed or logType == LogType.HeadingSucceeded or logType == LogType.Good or logType == LogType.Warn or logType == LogType.Error: return logType, message parsedMessage = self.tryMatchPattern(message, self.errorMaps, self.errorPatterns) if parsedMessage: return LogType.Error, parsedMessage if not any(p.match(message) for p in self.warningPatternsIgnore): parsedMessage = self.tryMatchPattern(message, self.warningMaps, self.warningPatterns) if parsedMessage: return LogType.Warn, parsedMessage parsedMessage = self.tryMatchPattern(message, self.headingMaps, self.headingPatterns) if parsedMessage: return LogType.Heading, parsedMessage parsedMessage = self.tryMatchPattern(message, self.goodMaps, self.goodPatterns) if parsedMessage: return LogType.Good, parsedMessage parsedMessage = self.tryMatchPattern(message, self.infoMaps, self.infoPatterns) if parsedMessage: return LogType.Info, parsedMessage if self._verbose: parsedMessage = self.tryMatchPattern(message, self.debugMaps, self.debugPatterns) if parsedMessage: return LogType.Debug, parsedMessage if self._veryVerbose: return LogType.Debug, message return None, message
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): with self._log.heading( 'Clearing all generated files in Demo/UnityProjects folder'): self._packageMgr.clearAllProjectGeneratedFiles() self._sys.deleteDirectoryIfExists('[TempDir]') self._sys.copyDirectory('[ProjenyDir]/Demo', '[TempDir]') self._sys.removeFileIfExists('[TempDir]/.gitignore') self._sys.removeFileIfExists('[TempDir]/PrjLog.txt') with self._log.heading('Zipping up demo project'): self._zipHelper.createZipFile( '[TempDir]', '[DistDir]/ProjenySamples-v{0}.zip'.format(versionStr)) def _createInstaller(self, installerOutputPath): with 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]') with self._log.heading('Building exes'): self._sys.executeAndWait('[PythonDir]/BuildAllExes.bat') with 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') for fileName in self._sys.getAllFilesInDirectory( '[InstallerDir]/BinFiles'): self._sys.copyFile('[InstallerDir]/BinFiles/' + fileName, '[TempDir]/Bin/' + fileName) self._sys.removeByRegex('[TempDir]/Bin/UnityPlugin/Release/*.pdb') self._sys.deleteDirectoryIfExists( '[TempDir]/Bin/UnityPlugin/Debug')
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): with self._log.heading('Updating Visual Studio solution for project "{0}"'.format(projectName)): self._packageManager.setPathsForProjectPlatform(projectName, platform) self._packageManager.checkProjectInitialized(projectName, platform) schema = self._schemaLoader.loadSchema(projectName, platform) self._updateVisualStudioSolutionInternal( schema.packages.values(), schema.customFolderMap) 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}'. Try updating the unity generated solution, the assembly references might be out of date.".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) defines = self._getDefineConstantsElement(unityProjRoot) references = self._getUnityProjectReferencesItems(unityProjRoot) referencesEditor = self._getUnityProjectReferencesItems(unityProjEditorRoot) return UnityGeneratedProjInfo(defines, references, referencesEditor) def _updateVisualStudioSolutionInternal(self, allPackages, customFolderMap): # Necessary to avoid having ns0: prefixes everywhere on output ET.register_namespace('', 'http://schemas.microsoft.com/developer/msbuild/2003') unifyProjInfo = self._parseGeneratedUnityProject() projectMap = self._createProjectMap(allPackages) self._initDependenciesForAllProjects( allPackages, projectMap, unifyProjInfo) self._addFilesForAllProjects( projectMap, unifyProjInfo) self._writeCsProjFiles( projectMap, unifyProjInfo) self._createSolution(projectMap.values(), customFolderMap) def _createProjectMap(self, allPackages): projectMap = {} self._addStandardProjects(projectMap) self._addCustomProjects(allPackages, projectMap) return projectMap def _addStandardProjects(self, projectMap): projectMap[PluginsProjectName] = self._createStandardCsProjInfo( PluginsProjectName, '[PluginsDir]') projectMap[AssetsProjectName] = self._createStandardCsProjInfo( AssetsProjectName, '[ProjectAssetsDir]') projectMap[AssetsEditorProjectName] = self._createStandardCsProjInfo( AssetsEditorProjectName, '[ProjectAssetsDir]') projectMap[PluginsEditorProjectName] = self._createStandardCsProjInfo( PluginsEditorProjectName, '[PluginsDir]') def _addFilesForAllProjects( self, projectMap, unifyProjInfo): excludeDirs = [] for projInfo in projectMap.values(): if projInfo.packageInfo != None: packageDir = self._varMgr.expandPath(os.path.join(projInfo.packageInfo.outputDirVar, projInfo.packageInfo.name)) excludeDirs.append(packageDir) self._initFilesForStandardCsProjForDirectory( projectMap[PluginsEditorProjectName], excludeDirs, unifyProjInfo, True) self._initFilesForStandardCsProjForDirectory( projectMap[PluginsProjectName], excludeDirs, unifyProjInfo, False) excludeDirs.append(self._varMgr.expandPath('[PluginsDir]')) self._initFilesForStandardCsProjForDirectory( projectMap[AssetsProjectName], excludeDirs, unifyProjInfo, False) self._initFilesForStandardCsProjForDirectory( projectMap[AssetsEditorProjectName], excludeDirs, unifyProjInfo, True) def _writeCsProjFiles( self, projectMap, unifyProjInfo): for projInfo in projectMap.values(): if projInfo.projectType != ProjectType.Custom and projInfo.projectType != ProjectType.CustomEditor: continue if projInfo.projectType == ProjectType.CustomEditor: refItems = unifyProjInfo.referencesEditor else: refItems = unifyProjInfo.references self._writeCsProject(projInfo, projectMap, projInfo.files, refItems, unifyProjInfo.defines) self._writeStandardCsProjForDirectory( projectMap[PluginsEditorProjectName], projectMap, unifyProjInfo, True) self._writeStandardCsProjForDirectory( projectMap[PluginsProjectName], projectMap, unifyProjInfo, False) self._writeStandardCsProjForDirectory( projectMap[AssetsProjectName], projectMap, unifyProjInfo, False) self._writeStandardCsProjForDirectory( projectMap[AssetsEditorProjectName], projectMap, unifyProjInfo, True) def _initDependenciesForAllProjects( self, allPackages, projectMap, unifyProjInfo): for projInfo in projectMap.values(): if projInfo.projectType != ProjectType.Custom and projInfo.projectType != ProjectType.CustomEditor: continue assertThat(projInfo.packageInfo.createCustomVsProject) self._log.debug('Processing generated project "{0}"'.format(projInfo.name)) projInfo.dependencies = self._getProjectDependencies(projectMap, projInfo) pluginsProj = projectMap[PluginsProjectName] self._log.debug('Processing project "{0}"'.format(pluginsProj.name)) prebuiltProjectInfos = [x for x in projectMap.values() if x.projectType == ProjectType.Prebuilt] pluginsProj.dependencies = prebuiltProjectInfos pluginsEditorProj = projectMap[PluginsEditorProjectName] pluginsEditorProj.dependencies = [pluginsProj] + prebuiltProjectInfos for packageInfo in allPackages: if packageInfo.createCustomVsProject and packageInfo.isPluginDir: pluginsEditorProj.dependencies.append(projectMap[packageInfo.name]) scriptsProj = projectMap[AssetsProjectName] self._log.debug('Processing project "{0}"'.format(scriptsProj.name)) scriptsProj.dependencies = [pluginsProj] + prebuiltProjectInfos scriptsEditorProj = projectMap[AssetsEditorProjectName] scriptsEditorProj.dependencies = scriptsProj.dependencies + [scriptsProj, pluginsEditorProj] def _addCustomProjects( self, allPackages, allCustomProjects): for packageInfo in allPackages: if not packageInfo.createCustomVsProject: continue if packageInfo.assemblyProjectInfo == None: customProject = self._createGeneratedCsProjInfo(packageInfo, False) allCustomProjects[customProject.name] = customProject customEditorProject = self._createGeneratedCsProjInfo(packageInfo, True) allCustomProjects[customEditorProject.name] = customEditorProject else: projId = self._getCsProjIdFromFile(packageInfo.assemblyProjectInfo.root) customProject = CsProjInfo( projId, packageInfo.assemblyProjectInfo.path, packageInfo.name, [], False, packageInfo.assemblyProjectInfo.config, ProjectType.Prebuilt, packageInfo) allCustomProjects[customProject.name] = customProject def _getCsProjIdFromFile(self, projectRoot): projId = projectRoot.findall('./{0}PropertyGroup/{0}ProjectGuid'.format(NsPrefix))[0].text return re.match('^{(.*)}$', projId).groups()[0] 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 _createGeneratedCsProjInfo(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, True) isIgnored = (len(files) == 0 or (len(files) == 1 and os.path.basename(files[0]) == PackageConfigFileName)) return CsProjInfo( projId, outputPath, csProjectName, files, isIgnored, None, ProjectType.CustomEditor if isEditor else ProjectType.Custom, packageInfo) def _getProjectDependencies(self, projectMap, projInfo): packageInfo = projInfo.packageInfo assertIsNotNone(packageInfo) projDependencies = [] isEditor = projInfo.projectType == ProjectType.CustomEditor if isEditor: projDependencies.append(projectMap[PluginsProjectName]) projDependencies.append(projectMap[PluginsEditorProjectName]) projDependencies.append(projectMap[packageInfo.name]) if not packageInfo.isPluginDir: projDependencies.append(projectMap[AssetsProjectName]) projDependencies.append(projectMap[AssetsEditorProjectName]) else: projDependencies.append(projectMap[PluginsProjectName]) if not packageInfo.isPluginDir: projDependencies.append(projectMap[AssetsProjectName]) for dependName in packageInfo.allDependencies: assertThat(not dependName in projDependencies) if dependName in projectMap: dependProj = projectMap[dependName] projDependencies.append(dependProj) if isEditor: dependEditorName = dependName + EditorProjectNameSuffix if dependEditorName in projectMap: dependEditorProj = projectMap[dependEditorName] projDependencies.append(dependEditorProj) return projDependencies 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 = '' projectFolderMapsStr = '' folderIds = {} usedFolders = set() for folderName in customFolderMap: folderId = self._createProjectGuid() folderIds[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' if proj.configType != None: buildConfig = proj.configType else: buildConfig = '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: usedFolders.add(folderName) folderId = folderIds[folderName] if len(projectFolderMapsStr) != 0: projectFolderMapsStr += '\n' projectFolderMapsStr += \ '\t\t{{{0}}} = {{{1}}}' \ .format(proj.id, folderId) projectFolderStr = '' for folderName, folderId in folderIds.items(): if folderName in usedFolders: projectFolderStr += 'Project("{{{0}}}") = "{1}", "{2}", "{{{3}}}"\nEndProject\n' \ .format(SolutionFolderTypeGuid, folderName, folderName, 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, None, ProjectType.Standard, None) def _initFilesForStandardCsProjForDirectory( self, projInfo, excludeDirs, unityProjInfo, isEditor): outputDir = os.path.dirname(projInfo.absPath) projInfo.files = [] self._addCsFilesInDirectory(outputDir, excludeDirs, projInfo.files, isEditor, False) # If it only contains the project config file then ignore it if len([x for x in projInfo.files if not x.endswith('.yaml')]) == 0: projInfo.isIgnored = True def _writeStandardCsProjForDirectory( self, projInfo, projectMap, unityProjInfo, isEditor): if projInfo.isIgnored: return if isEditor: references = unityProjInfo.referencesEditor else: references = unityProjInfo.references self._writeCsProject( projInfo, projectMap, projInfo.files, references, unityProjInfo.defines) def _createProjectGuid(self): return str(uuid.uuid4()).upper() def _shouldReferenceBeCopyLocal(self, refName): return refName != 'System' and refName != 'System.Core' def _writeCsProject(self, projInfo, projectMap, files, refItems, defines): 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() prebuiltProjectInfos = [x for x in projectMap.values() if x.projectType == ProjectType.Prebuilt] # Add reference items given from unity project for refInfo in refItems: if any([x for x in prebuiltProjectInfos if x.name == refInfo.name]): 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 = self._config.tryGetString('', 'SolutionGeneration', 'RootNamespace') 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}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 ProjenyGeneratedDirectoryIgnorePattern.match(fullPath): # Never include the generated stuff return True return ProjenyDirectoryIgnorePattern.match(fullPath) def _addCsFilesInDirectory(self, dirPath, excludeDirs, files, isForEditor, includeYaml): 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, includeYaml) else: if itemName.endswith('.cs') or itemName.endswith('.txt') or (includeYaml and itemName.endswith('.yaml')): if not isForEditor or isInsideEditorFolder or itemName == PackageConfigFileName: files.append(fullPath)
class Runner: _scriptRunner = Inject('ScriptRunner') _log = Inject('Logger') _sys = Inject('SystemHelper') _varMgr = Inject('VarManager') _zipHelper = Inject('ZipHelper') _vsSolutionHelper = Inject('VisualStudioHelper') _unityHelper = Inject('UnityHelper') def run(self, args): self._args = args success = self._scriptRunner.runWrapper(self._runInternal) if not success: sys.exit(1) def _runInternal(self): self._varMgr.set('RootDir', RootDir) self._varMgr.set('BuildDir', BuildDir) self._varMgr.set('PythonDir', '[BuildDir]/python') self._varMgr.set('TempDir', '[BuildDir]/Temp') self._varMgr.set('AssetsDir', '[RootDir]/UnityProject/Assets') self._varMgr.set('ZenjectDir', '[AssetsDir]/Zenject') self._varMgr.set('DistDir', '[BuildDir]/Dist') self._varMgr.set('BinDir', '[RootDir]/AssemblyBuild/Bin') versionStr = self._sys.readFileAsText( '[ZenjectDir]/Version.txt').strip() self._log.info("Found version {0}", versionStr) self._populateDistDir(versionStr) if self._args.addTag: self._sys.executeAndReturnOutput( "git tag -a v{0} -m 'Version {0}'".format(versionStr)) self._log.info( "Incremented version to {0}! New tag was created as well", versionStr) else: self._log.info( "Incremented version to {0}! Dist directory contains the releases. NOTE: No tags were created however", versionStr) def _populateDistDir(self, versionStr): self._sys.deleteDirectoryIfExists('[DistDir]') self._sys.createDirectory('[DistDir]') self._sys.deleteDirectoryIfExists('[TempDir]') self._sys.createDirectory('[TempDir]') try: self._createCSharpPackage( True, '[DistDir]/Zenject-WithAsteroidsDemo@v{0}.unitypackage'.format( versionStr)) self._createCSharpPackage( False, '[DistDir]/Zenject@v{0}.unitypackage'.format(versionStr)) self._createDllPackage( '[DistDir]/Zenject-BinariesOnly@v{0}.unitypackage'.format( versionStr)) self._createNonUnityZip( '[DistDir]/Zenject-NonUnity@v{0}.zip'.format(versionStr)) finally: self._sys.deleteDirectory('[TempDir]') def _createNonUnityZip(self, zipPath): self._log.heading('Creating non unity zip') tempDir = '[TempDir]/NonUnity' self._sys.createDirectory(tempDir) self._sys.clearDirectoryContents(tempDir) binDir = '[BinDir]/Not Unity Release' self._sys.deleteDirectoryIfExists(binDir) self._sys.createDirectory(binDir) self._vsSolutionHelper.buildVisualStudioProject( '[RootDir]/AssemblyBuild/Zenject.sln', 'Not Unity Release') self._log.info('Copying Zenject dlls') self._sys.copyFile('{0}/Zenject.dll'.format(binDir), '{0}/Zenject.dll'.format(tempDir)) self._sys.copyFile('{0}/Zenject.Commands.dll'.format(binDir), '{0}/Zenject.Commands.dll'.format(tempDir)) self._zipHelper.createZipFile(tempDir, zipPath) def _createDllPackage(self, outputPath): self._log.heading('Creating {0}'.format(os.path.basename(outputPath))) self._varMgr.set('PackageTempDir', '[TempDir]/Packager') self._varMgr.set('ZenTempDir', '[PackageTempDir]/Assets/Zenject') self._sys.createDirectory('[PackageTempDir]') self._sys.createDirectory('[PackageTempDir]/ProjectSettings') try: self._log.info('Building zenject dlls') self._varMgr.set('ZenDllDir', '[BinDir]/Release') self._varMgr.set('ZenDllMetaDir', '[BuildDir]/BinaryMetas') self._sys.deleteDirectoryIfExists('[ZenDllDir]') self._sys.createDirectory('[ZenDllDir]') self._vsSolutionHelper.buildVisualStudioProject( '[RootDir]/AssemblyBuild/Zenject.sln', 'Release') self._log.info('Copying Zenject dlls') self._sys.copyFile('[ZenDllDir]/Zenject.dll', '[ZenTempDir]/Zenject.dll') self._sys.copyFile('[ZenDllMetaDir]/Zenject.dll.meta', '[ZenTempDir]/Zenject.dll.meta') self._sys.copyFile('[ZenDllDir]/Zenject-editor.dll', '[ZenTempDir]/Editor/Zenject-editor.dll') self._sys.copyFile('[ZenDllMetaDir]/Zenject-editor.dll.meta', '[ZenTempDir]/Editor/Zenject-editor.dll.meta') self._sys.copyFile('[ZenDllDir]/Zenject.Commands.dll', '[ZenTempDir]/Zenject.Commands.dll') self._sys.copyFile('[ZenDllMetaDir]/Zenject.Commands.dll.meta', '[ZenTempDir]/Zenject.Commands.dll.meta') self._sys.copyFile('[ZenjectDir]/Version.txt', '[ZenTempDir]/Version.txt') self._sys.copyFile('[ZenjectDir]/Version.txt.meta', '[ZenTempDir]/Version.txt.meta') self._sys.copyFile('[ZenjectDir]/LICENSE.txt', '[ZenTempDir]/LICENSE.txt') self._sys.copyFile('[ZenjectDir]/LICENSE.txt.meta', '[ZenTempDir]/LICENSE.txt.meta') self._sys.copyDirectory('[ZenjectDir]/Documentation', '[ZenTempDir]/Documentation') self._sys.copyFile('[ZenjectDir]/Documentation.meta', '[ZenTempDir]/Documentation.meta') self._createUnityPackage('[PackageTempDir]', outputPath) finally: self._sys.deleteDirectory('[PackageTempDir]') self._log.heading('Creating {0}'.format(os.path.basename(outputPath))) def _createCSharpPackage(self, includeSample, outputPath): self._log.heading('Creating {0}'.format(os.path.basename(outputPath))) self._varMgr.set('PackageTempDir', '[TempDir]/Packager') self._varMgr.set('ZenTempDir', '[PackageTempDir]/Assets/Zenject') self._sys.createDirectory('[PackageTempDir]') self._sys.createDirectory('[PackageTempDir]/ProjectSettings') try: self._log.info('Copying Zenject to temporary directory') self._sys.copyDirectory('[ZenjectDir]', '[ZenTempDir]') self._log.info('Cleaning up Zenject directory') self._zipHelper.createZipFile( '[ZenTempDir]/OptionalExtras/UnitTests', '[ZenTempDir]/OptionalExtras/UnitTests.zip') self._sys.deleteDirectory('[ZenTempDir]/OptionalExtras/UnitTests') self._sys.removeFile('[ZenTempDir]/OptionalExtras/UnitTests.meta') self._zipHelper.createZipFile( '[ZenTempDir]/OptionalExtras/IntegrationTests', '[ZenTempDir]/OptionalExtras/IntegrationTests.zip') self._sys.deleteDirectory( '[ZenTempDir]/OptionalExtras/IntegrationTests') self._sys.removeFile( '[ZenTempDir]/OptionalExtras/IntegrationTests.meta') self._zipHelper.createZipFile( '[ZenTempDir]/OptionalExtras/AutoMocking', '[ZenTempDir]/OptionalExtras/AutoMocking.zip') self._sys.deleteDirectory( '[ZenTempDir]/OptionalExtras/AutoMocking') self._sys.removeFile( '[ZenTempDir]/OptionalExtras/AutoMocking.meta') self._sys.removeFile('[ZenTempDir]/Source/Zenject.csproj') self._sys.removeFile('[ZenTempDir]/Source/Zenject.csproj.user') self._sys.removeFile( '[ZenTempDir]/OptionalExtras/CommandsAndSignals/Zenject.Commands.csproj' ) self._sys.removeFile( '[ZenTempDir]/OptionalExtras/CommandsAndSignals/Zenject.Commands.csproj.user' ) if not includeSample: self._sys.deleteDirectory( '[ZenTempDir]/OptionalExtras/SampleGame') self._createUnityPackage('[PackageTempDir]', outputPath) finally: self._sys.deleteDirectory('[PackageTempDir]') def _createUnityPackage(self, projectPath, outputPath): self._sys.copyFile( '[BuildDir]/UnityPackager/UnityPackageUtil.cs', '{0}/Assets/Editor/UnityPackageUtil.cs'.format(projectPath)) self._log.info('Running unity to create unity package') self._unityHelper.runEditorFunction( '[PackageTempDir]', 'Zenject.UnityPackageUtil.CreateUnityPackage') self._sys.move('{0}/Zenject.unitypackage'.format(projectPath), outputPath)
class ProjectSchemaLoader: _varMgr = Inject('VarManager') _log = Inject('Logger') _sys = Inject('SystemHelper') def loadSchema(self, name, platform): try: return self._loadSchemaInternal(name, platform) except Exception as e: raise Exception("Failed while processing config yaml for project '{0}' (platform '{1}'). Details: {2}".format(name, platform, str(e))) from e def loadProjectConfig(self, name): 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)) yamlConfig = Config(loadYamlFilesThatExist(schemaPath, schemaPathUser, schemaPathGlobal, schemaPathUserGlobal)) config = ProjectConfig() config.pluginsFolder = yamlConfig.tryGetList([], 'PluginsFolder') config.assetsFolder = yamlConfig.tryGetList([], 'AssetsFolder') config.solutionProjects = yamlConfig.tryGetList([], 'SolutionProjects') config.targetPlatforms = yamlConfig.tryGetList([Platforms.Windows], 'TargetPlatforms') config.solutionFolders = yamlConfig.tryGetOrderedDictionary(OrderedDict(), 'SolutionFolders') config.packageFolders = yamlConfig.getList('PackageFolders') config.projectSettingsPath = yamlConfig.getString('ProjectSettingsPath') # Remove duplicates config.assetsFolder = list(set(config.assetsFolder)) config.pluginsFolder = list(set(config.pluginsFolder)) for packageName in config.pluginsFolder: assertThat(not packageName in config.assetsFolder, "Found package '{0}' in both scripts and plugins. Must be in only one or the other".format(packageName)) return config def _loadSchemaInternal(self, name, platform): config = self.loadProjectConfig(name) # Search all the given packages and any new packages that are dependencies and create PackageInfo() objects for each packageMap = self._getAllPackageInfos(config, platform) self._addGroupedDependenciesAsExplicitDependencies(packageMap) self._ensurePrebuiltProjectsHaveNoScripts(packageMap) self._ensurePrebuiltProjectDependenciesArePrebuilt(packageMap) # We have all the package infos, but we don't know which packages depend on what so calculate that self._calculateDependencyListForEachPackage(packageMap) # For the pre-built assembly projects, if we add one of them to our solution, # then we need to add all the pre-built dependencies, since unlike generated projects # we can't make the prebuilt projects use the dll directly self._ensureVisiblePrebuiltProjectHaveVisibleDependencies(packageMap) self._printDependencyTree(packageMap) for customProj in config.solutionProjects: assertThat(customProj.startswith('/') or customProj in packageMap, 'Given project "{0}" in schema is not included in either "scripts" or "plugins"'.format(customProj)) self._log.debug('Found {0} packages in total for given schema'.format(len(packageMap))) # 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) for info in packageMap.values(): if info.forcePluginsDir and not info.isPluginDir: assertThat(False, "Package '{0}' must be in plugins directory".format(info.name)) self._ensureAllPackagesExist(packageMap) return ProjectSchema(name, packageMap, config.solutionFolders, config.projectSettingsPath, platform, config.targetPlatforms) def _shouldIncludeForPlatform(self, packageName, packageConfig, folderType, platform): if folderType == FolderTypes.AndroidProject or folderType == FolderTypes.AndroidLibraries: allowedPlatforms = [Platforms.Android] elif folderType == FolderTypes.Ios: allowedPlatforms = [Platforms.Ios] elif folderType == FolderTypes.WebGl: allowedPlatforms = [Platforms.WebGl] else: allowedPlatforms = packageConfig.tryGetList([], 'Platforms') if len(allowedPlatforms) == 0: return True if platform not in allowedPlatforms: self._log.debug("Skipped project '{0}' since it is not enabled for platform '{1}'".format(packageName, platform)) return False return True def _getFolderTypeFromString(self, value): value = value.lower() if not value or value == FolderTypes.Normal or len(value) == 0: return FolderTypes.Normal if value == FolderTypes.AndroidProject: return FolderTypes.AndroidProject if value == FolderTypes.AndroidLibraries: return FolderTypes.AndroidLibraries if value == FolderTypes.Ios: return FolderTypes.Ios if value == FolderTypes.WebGl: return FolderTypes.WebGl if value == FolderTypes.StreamingAssets: return FolderTypes.StreamingAssets assertThat(False, "Unrecognized folder type '{0}'".format(value)) return "" def _getAllPackageInfos(self, projectConfig, platform): configRefDesc = "'{0}' or '{1}'".format(ProjectConfigFileName, ProjectUserConfigFileName) allPackageRefs = [PackageReference(x, configRefDesc) for x in projectConfig.pluginsFolder + projectConfig.assetsFolder] packageMap = {} # Resolve all dependencies for each package # by default, put any dependencies that are not declared explicitly into the plugins folder for packageRef in allPackageRefs: packageName = packageRef.name packageDir = None for packageFolder in projectConfig.packageFolders: candidatePackageDir = os.path.join(packageFolder, packageName) if self._sys.directoryExists(candidatePackageDir): packageDir = self._varMgr.expandPath(candidatePackageDir) break assertIsNotNone(packageDir, "Could not find package '{0}' in any of the package directories! Referenced in {1}", packageName, packageRef.sourceDesc) configPath = os.path.join(packageDir, PackageConfigFileName) if os.path.exists(configPath): packageConfig = Config(loadYamlFilesThatExist(configPath)) else: packageConfig = Config([]) folderType = self._getFolderTypeFromString(packageConfig.tryGetString('', 'FolderType')) if not self._shouldIncludeForPlatform(packageName, packageConfig, folderType, platform): continue createCustomVsProject = self._shouldCreateVsProjectForName(packageName, projectConfig.solutionProjects) isPluginsDir = True if packageName in projectConfig.assetsFolder: assertThat(not packageName in projectConfig.pluginsFolder) isPluginsDir = False if packageConfig.tryGetBool(False, 'ForceAssetsDirectory'): isPluginsDir = False explicitDependencies = packageConfig.tryGetList([], 'Dependencies') forcePluginsDir = packageConfig.tryGetBool(False, 'ForcePluginsDirectory') assemblyProjInfo = self._tryGetAssemblyProjectInfo(packageConfig, packageName) sourceDesc = '"{0}"'.format(configPath) if assemblyProjInfo != None: for assemblyDependName in assemblyProjInfo.dependencies: if assemblyDependName not in [x.name for x in allPackageRefs]: allPackageRefs.append(PackageReference(assemblyDependName, sourceDesc)) explicitDependencies += assemblyProjInfo.dependencies groupedDependencies = packageConfig.tryGetList([], 'GroupWith') extraDependencies = packageConfig.tryGetList([], 'Extras') assertThat(not packageName in packageMap, "Found duplicate package with name '{0}'", packageName) packageMap[packageName] = PackageInfo( isPluginsDir, packageName, packageConfig, createCustomVsProject, explicitDependencies, forcePluginsDir, folderType, assemblyProjInfo, packageDir, groupedDependencies) for dependName in (explicitDependencies + groupedDependencies + extraDependencies): if dependName not in [x.name for x in allPackageRefs]: # Yes, python is ok with changing allPackageRefs even while iterating over it allPackageRefs.append(PackageReference(dependName, sourceDesc)) return packageMap def _tryGetAssemblyProjectInfo(self, packageConfig, packageName): assemblyProjectRelativePath = packageConfig.tryGetString(None, 'AssemblyProject', 'Path') if assemblyProjectRelativePath == None: return None projFullPath = self._varMgr.expand(assemblyProjectRelativePath) if not os.path.isabs(projFullPath): projFullPath = os.path.join(packageDir, assemblyProjectRelativePath) assertThat(self._sys.fileExists(projFullPath), "Expected to find file at '{0}'.", projFullPath) projAnalyzer = CsProjAnalyzer(projFullPath) assemblyName = projAnalyzer.getAssemblyName() assertThat(assemblyName == '$(MSBuildProjectName)' or assemblyName.lower() == packageName.lower(), 'Packages that represent assembly projects must have the same name as the assembly') assertIsEqual(self._sys.getFileNameWithoutExtension(projFullPath).lower(), packageName.lower(), 'Assembly projects must have the same name as their package') projConfig = packageConfig.tryGetString(None, 'AssemblyProject', 'Config') dependencies = projAnalyzer.getProjectReferences() return AssemblyProjectInfo( projFullPath, projAnalyzer.root, projConfig, dependencies) def getDependenciesFromCsProj(self, projectRoot): result = [] for projRef in projectRoot.findall('./{0}ItemGroup/{0}ProjectReference/{0}Name'.format(NsPrefix)): result.append(projRef.text) return result def _ensureAllPackagesExist(self, packageMap): for package in packageMap.values(): assertThat(self._sys.directoryExists(package.dirPath), "Could not find directory for package '{0}'", package.name) def _ensureVisiblePrebuiltProjectHaveVisibleDependencies(self, packageMap): for package in packageMap.values(): if package.assemblyProjectInfo != None and package.createCustomVsProject: self._makeAllPrebuiltDependenciesVisible(package, packageMap) def _makeAllPrebuiltDependenciesVisible(self, package, packageMap): for dependName in package.explicitDependencies: depend = packageMap[dependName] if not depend.createCustomVsProject: depend.createCustomVsProject = True self._makeAllPrebuiltDependenciesVisible(depend, packageMap) def _ensurePrebuiltProjectDependenciesArePrebuilt(self, packageMap): for packageInfo in packageMap.values(): assInfo = packageInfo.assemblyProjectInfo if assInfo == None: continue for dependName in assInfo.dependencies: depend = packageMap[dependName] assertThat(depend.assemblyProjectInfo != None, "Expected package '{0}' to have an assembly project defined, since another assembly project ({1}) depends on it", dependName, packageInfo.name) def _ensurePrebuiltProjectsHaveNoScripts(self, packageMap): for package in packageMap.values(): if package.assemblyProjectInfo != None: assertThat(not any(self._sys.findFilesByPattern(package.dirPath, '*.cs')), "Found C# scripts in assembly project '{0}'. This is not allowed - please move to a separate package.", package.name) 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.debug('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.debug('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 _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 _shouldCreateVsProjectForName(self, packageName, solutionProjects): if packageName in solutionProjects: return True # Allow regex's! for projPattern in solutionProjects: 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 _addGroupedDependenciesAsExplicitDependencies(self, packageMap): # There is a bug here where it won't handle grouped dependencies within grouped dependencies for info in packageMap.values(): extras = set() for explicitDependName in info.explicitDependencies: if explicitDependName not in packageMap: continue explicitDependInfo = packageMap[explicitDependName] for groupedDependName in explicitDependInfo.groupedDependencies: if info.name != groupedDependName: extras.add(groupedDependName) info.explicitDependencies += list(extras) def _calculateDependencyListForEachPackage(self, packageMap): self._log.debug('Processing dependency tree') inProgress = set() for info in packageMap.values(): self._calculateDependencyListForPackage(info, packageMap, inProgress) def _calculateDependencyListForPackage(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: # This can happen if a package depends on another package that is platform specific continue explicitDependInfo = packageMap[explicitDependName] if explicitDependInfo.allDependencies == None: self._calculateDependencyListForPackage(explicitDependInfo, packageMap, inProgress) for dependName in explicitDependInfo.allDependencies: allDependencies.add(dependName) packageInfo.allDependencies = list(allDependencies) inProgress.remove(packageInfo.name)
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) assertThat(not self.directoryExists(dirPath), 'Tried to create a directory that already exists') os.makedirs(dirPath) 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 move(self, fromPath, toPath): toPath = self._varManager.expand(toPath) fromPath = self._varManager.expand(fromPath) self.makeMissingDirectoriesInPath(toPath) shutil.move(fromPath, toPath) def IsDir(self, path): return os.path.isdir(self._varManager.expand(path)) def clearDirectoryContents(self, dirPath): dirPath = self._varManager.expand(dirPath) if not os.path.exists(dirPath): return 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 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 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", 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.UpdateMonodevelopProject')
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 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 Test2: title = Inject('Title', Assertions.IsInstanceOf(str)) def Run(self): print('title: {0}'.format(self.title))
class Test1: con = Inject('Console', Assertions.HasMethods('WriteLine')) def Run(self): self.con.WriteLine('lorem ipsum')
class VisualStudioHelper: _log = Inject('Logger') _config = Inject('Config') _varMgr = Inject('VarManager') _sys = Inject('SystemHelper') def openFile(self, filePath, lineNo, solutionPath): 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.openVisualStudioSolution(solutionPath, filePath) def openFileInExistingVisualStudioInstance(self, filePath, lineNo): try: vsPath = self._varMgr.expand('[VisualStudioIdePath]') if '2017' in vsPath or 'Visual Studio 15.0' in vsPath: dte = win32com.client.GetActiveObject("VisualStudio.DTE.15.0") elif 'Visual Studio 14.0' in vsPath: dte = win32com.client.GetActiveObject("VisualStudio.DTE.14.0") elif 'Visual Studio 12.0' in vsPath: dte = win32com.client.GetActiveObject("VisualStudio.DTE.12.0") else: assertThat(False, "Could not determine visual studio version") 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 self._varMgr.hasKey('VisualStudioIdePath'): assertThat( self._sys.fileExists('[VisualStudioIdePath]'), "Cannot find path to visual studio. Expected to find it at '{0}'" .format(self._varMgr.expand('[VisualStudioIdePath]'))) if solutionPath == None: self._sys.executeNoWait('"[VisualStudioIdePath]" {0}'.format( self._sys.canonicalizePath(filePath) if filePath else "")) else: solutionPath = self._sys.canonicalizePath(solutionPath) self._sys.executeNoWait( '"[VisualStudioIdePath]" {0} {1}'.format( solutionPath, self._sys.canonicalizePath(filePath) if filePath else "")) else: assertThat( filePath == None, "Path to visual studio has not been defined. Please set <VisualStudioIdePath> within one of your {0} files. See documentation for details.", ConfigFileName) self._sys.executeShellCommand(solutionPath, None, False) def buildVisualStudioProject(self, solutionPath, buildConfig): solutionPath = self._varMgr.expand(solutionPath) 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)
class Runner: _scriptRunner = Inject('ScriptRunner') _unityHelper = Inject('UnityHelper') _log = Inject('Logger') _sys = Inject('SystemHelper') _varManager = Inject('VarManager') def __init__(self): self._platform = Platforms.Windows def run(self, args): self._args = args success = self._scriptRunner.runWrapper(self._runInternal) if not success: sys.exit(1) def _runBuilds(self): if self._args.clearOutput: self._log.heading("Clearing output directory") self._sys.clearDirectoryContents('[OutputRootDir]') if self._args.buildType == 'all' or self._args.buildType == 'win35': self._log.heading("Building windows 3.5") self._platform = Platforms.Windows self._enableNet35() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'win46': self._log.heading("Building windows 4.6") self._platform = Platforms.Windows self._enableNet46() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'wsa35': self._log.heading("Building WindowsStoreApp 3.5 .net") self._platform = Platforms.WindowsStoreApp self._enableNet35() self._enableNetBackend() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'wsa46': self._log.heading("Building WindowsStoreApp 4.6 .net") self._platform = Platforms.WindowsStoreApp self._enableNet46() self._enableNetBackend() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'wsa46il2cpp': self._log.heading("Building WindowsStoreApp 4.6 il2cpp") self._platform = Platforms.WindowsStoreApp self._enableNet46() self._enableIl2cpp() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'wsa35il2cpp': self._log.heading("Building WindowsStoreApp 3.5 il2cpp") self._platform = Platforms.WindowsStoreApp self._enableNet35() self._enableIl2cpp() self._createBuild() if self._args.buildType == 'all' or self._args.buildType == 'webgl35': self._log.heading("Building WebGl 3.5") self._platform = Platforms.WebGl self._enableNet35() self._createBuild() self._sys.copyFile('[WebGlTemplate]', '[OutputRootDir]/WebGl/Net35/Web.config') if self._args.buildType == 'all' or self._args.buildType == 'webgl46': self._log.heading("Building WebGl 4.6") self._platform = Platforms.WebGl self._enableNet46() self._createBuild() self._sys.copyFile('[WebGlTemplate]', '[OutputRootDir]/WebGl/Net46/Web.config') # TODO #self._log.heading("Building Ios") #self._platform = Platforms.Ios #self._createBuild() #self._log.heading("Building Android") #self._platform = Platforms.Android #self._createBuild() def _runTests(self): self._runUnityTests('editmode') self._runUnityTests('playmode') def _runUnityTests(self, testPlatform): self._log.heading('Running unity {0} unit tests'.format(testPlatform)) resultPath = self._varManager.expandPath( '[TempDir]/UnityUnitTestsResults.xml').replace('\\', '/') self._sys.removeFileIfExists(resultPath) try: self._unityHelper.runEditorFunctionRaw( '[UnityProjectPath]', None, self._platform, '-runTests -batchmode -nographics -testResults "{0}" -testPlatform {1}' .format(resultPath, testPlatform)) except UnityReturnedErrorCodeException as e: if self._sys.fileExists(resultPath): # Print out the test error info outRoot = ET.parse(resultPath) for item in outRoot.findall('.//failure/..'): name = item.get('name') self._log.error("Unit test failed for '{0}'.", name) failure = item.find('./failure') self._log.error("Message: {0}", failure.find('./message').text.strip()) stackTrace = failure.find('./stack-trace') if stackTrace is not None: self._log.error("Stack Trace: {0}", stackTrace.text.strip()) raise outRoot = ET.parse(resultPath) total = outRoot.getroot().get('total') self._log.info("Processed {0} {1} tests without errors", total, testPlatform) def _runInternal(self): if self._args.runTests: self._runTests() if self._args.runBuilds: self._runBuilds() if self._args.openUnity: self._openUnity() def _createBuild(self): self._log.info("Creating build") self._runEditorFunction('BuildRelease') #self._runEditorFunction('BuildDebug') def _enableNet46(self): self._log.info("Changing runtime to .net 4.6") self._runEditorFunction('EnableNet46') def _enableNet35(self): self._log.info("Changing runtime to .net 3.5") self._runEditorFunction('EnableNet35') def _enableNetBackend(self): self._log.info("Changing backend to .net") self._runEditorFunction('EnableBackendNet') def _enableIl2cpp(self): self._log.info("Enabling il2cpp") self._runEditorFunction('EnableBackendIl2cpp') def _openUnity(self): self._unityHelper.openUnity('[UnityProjectPath]', self._platform) def _runEditorFunction(self, functionName): self._log.info("Calling SampleBuilder." + functionName) self._unityHelper.runEditorFunction( '[UnityProjectPath]', 'Zenject.Internal.SampleBuilder.' + functionName, self._platform)
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.noise(line.decode(sys.stdout.encoding).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, wait = True): 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) if not wait: return ResultType.Success (stdoutData, stderrData) = proc.communicate() output = stdoutData.decode(encoding=sys.stdout.encoding, errors='ignore').strip() errors = stderrData.decode(encoding=sys.stderr.encoding, errors='ignore').strip() if output: for line in output.split('\n'): self._log.noise(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