class LocalFolderReleaseSource:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _extractor = Inject('UnityPackageExtractor')
    _packageAnalyzer = Inject('UnityPackageAnalyzer')

    def __init__(self, folderPath):
        self._folderPath = folderPath
        self._files = []

    @property
    def releases(self):
        return [x.release for x in self._files]

    def init(self):
        self._log.heading('Initializing release source for local folder')
        self._log.debug('Initializing release source for local folder "{0}"', self._folderPath)

        for path in self._sys.findFilesByPattern(self._folderPath, '*.unitypackage'):
            release = self._packageAnalyzer.getReleaseInfoFromUnityPackage(path)

            self._files.append(FileInfo(path, release))

        self._log.info("Found {0} released in folder '{1}'", len(self._files), self._folderPath)

    def getName(self):
        return "Local Folder ({0})".format(self._folderPath)

    # Should return the chosen name for the package
    # If forcedName is non-null then this should always be the value of forcedName
    def installRelease(self, releaseInfo, forcedName):
        fileInfo = next(x for x in self._files if x.release == releaseInfo)
        assertIsNotNone(fileInfo)

        return self._extractor.extractUnityPackage(fileInfo.path, releaseInfo.name, forcedName)
示例#2
0
class JunctionHelper:
    """
    Misc. helper functions related to windows junctions
    """
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')

    def __init__(self):
        pass

    def removeJunction(self, linkDir):
        linkDir = self._varMgr.expand(linkDir)
        if os.path.isdir(linkDir) and JunctionUtil.islink(linkDir):
            try:
                # Use rmdir not python unlink to ensure we don't delete the link source
                self._sys.executeShellCommand('rmdir "{0}"'.format(linkDir))
            except Exception as e:
                raise Exception(
                    'Failed while attempting to delete junction "{0}":\n{1}'.
                    format(linkDir, str(e))) from e

            return True

        return False

    def makeJunction(self, actualPath, linkPath):
        actualPath = self._varMgr.expandPath(actualPath)
        linkPath = self._varMgr.expandPath(linkPath)

        self._sys.makeMissingDirectoriesInPath(linkPath)

        self._log.debug(
            'Making junction with actual path ({0}) and new link path ({1})'.
            format(linkPath, actualPath))
        # Note: mklink is a shell command and can't be executed otherwise
        self._sys.executeShellCommand('mklink /J "{0}" "{1}"'.format(
            linkPath, actualPath))

    def removeJunctionsInDirectory(self, dirPath, recursive):
        fullDirPath = self._varMgr.expandPath(dirPath)

        if not os.path.exists(fullDirPath):
            return

        for name in os.listdir(fullDirPath):
            fullPath = os.path.join(fullDirPath, name)

            if not os.path.isdir(fullPath):
                continue

            if self.removeJunction(fullPath):
                if os.path.exists(fullPath + '.meta'):
                    os.remove(fullPath + '.meta')

                self._log.debug(
                    'Removed directory for package "{0}"'.format(name))
            else:
                if recursive:
                    self.removeJunctionsInDirectory(fullPath, True)
示例#3
0
class Runner:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')

    def run(self, args):
        self._args = args

        for filePath in self._sys.findFilesByPattern(self._args.directory,
                                                     '*.py'):
            self._sys.executeAndWait(
                'autoflake --in-place --remove-unused-variables "{0}"'.format(
                    filePath))
class RemoteServerReleaseSource:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _packageExtractor = Inject('UnityPackageExtractor')

    def __init__(self, manifestUrl):
        self._manifestUrl = manifestUrl
        self._releaseInfos = []

    @property
    def releases(self):
        return self._releaseInfos

    def init(self):
        self._log.heading("Initializing remote server release source")
        self._log.debug("Initializing remote server release source with URL '{0}'", self._manifestUrl)

        response = urllib.request.urlopen(self._manifestUrl)
        manifestData = response.read().decode('utf-8')

        self._log.debug("Got manifest with data: \n{0}".format(manifestData))

        self._manifest = YamlSerializer.deserialize(manifestData)

        for info in self._manifest.releases:
            info.url = urllib.parse.urljoin(self._manifestUrl, info.localPath)
            info.localPath = None
            self._releaseInfos.append(info)

    def getName(self):
        return "File Server"

    def installRelease(self, releaseInfo, forcedName):
        assertThat(releaseInfo.url)

        self._log.heading("Downloading release from url '{0}'".format(releaseInfo.url))

        try:
            with tempfile.NamedTemporaryFile(delete=False, suffix='.unitypackage') as tempFile:
                tempFilePath = tempFile.name

            self._log.debug("Downloading url to temporary file '{0}'".format(tempFilePath))
            urllib.request.urlretrieve(releaseInfo.url, tempFilePath)

            return self._packageExtractor.extractUnityPackage(tempFilePath, releaseInfo.name, forcedName)
        finally:
            if os.path.exists(tempFilePath):
                os.remove(tempFilePath)
class Test2:
    qux = Inject('Qux')

    def __init__(self):
        self.X = 0

    def Run(self):
        print(self.qux.GetValue())
示例#6
0
class Foo1:
    foo2 = Inject('Foo2')

    def __init__(self, val):
        self.val = val

    def Start(self):
        print(self.foo2.val)
示例#7
0
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))
示例#8
0
class CommonSettings:
    _config = Inject('Config')

    def __init__(self):
        self.maxProjectNameLength = self._config.getInt('MaxProjectNameLength')

    def getShortProjectName(self, val):
        return val[0:self.maxProjectNameLength]
示例#9
0
class Runner:
    _scriptRunner = Inject('ScriptRunner')
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _varMgr = Inject('VarManager')
    _vsSolutionHelper = Inject('VisualStudioHelper')

    def run(self, args):
        self._args = args
        success = self._scriptRunner.runWrapper(self._runInternal)

        if not success:
            sys.exit(1)

    def _runInternal(self):
        self._log.debug("Started OpenInVisualStudio with arguments: {0}".format(" ".join(sys.argv[1:])))

        project, platform = self._getProjectAndPlatformFromFilePath(self._args.filePath)

        self._log.debug("Determined Project = {0}, Platform = {1}", project, platform)

        lineNo = 1
        if self._args.lineNo:
            lineNo = int(self._args.lineNo)

        self._vsSolutionHelper.openFile(
            self._args.filePath, lineNo, project, platform)

    def _getProjectAndPlatformFromFilePath(self, filePath):
        unityProjectsDir = self._sys.canonicalizePath(self._varMgr.expand('[UnityProjectsDir]'))
        filePath = self._sys.canonicalizePath(filePath)

        if not filePath.startswith(unityProjectsDir):
            raise Exception("The given file path is not within the UnityProjects directory")

        relativePath = filePath[len(unityProjectsDir)+1:]
        dirs = relativePath.split(os.path.sep)

        projectName = dirs[0]

        platformProjectDirName = dirs[1]
        platformDirName = platformProjectDirName[platformProjectDirName.rfind('-')+1:]

        platform = PlatformUtil.fromPlatformFolderName(platformDirName)

        return projectName, platform
示例#10
0
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)))
示例#11
0
class LogStreamFile:
    _varManager = Inject('VarManager')

    def __init__(self):
        self._fileStream = self._tryGetFileStream()

    def log(self, logType, message):

        if logType == LogType.Heading:
            self._printSeperator()
            self._writeLine(message)
            self._printSeperator()
        else:
            self._writeLine(message)

    def dispose(self):
        if self._fileStream:
            self._fileStream.close()

    def _printSeperator(self):
        self._writeLine('--------------------------')

    def _writeLine(self, value):
        self._write('\n' + value)

    def _write(self, value):
        if self._fileStream:
            self._fileStream.write(value)
            self._fileStream.flush()

    def _tryGetFileStream(self):

        if not self._varManager.hasKey('LogPath'):
            return None

        primaryPath = self._varManager.expand('[LogPath]')

        previousPath = None
        if self._varManager.hasKey('LogPreviousPath'):
            previousPath = self._varManager.expand('[LogPreviousPath]')

        # Keep one old build log
        if os.path.isfile(primaryPath) and previousPath:
            shutil.copy2(primaryPath, previousPath)

        self._outputFilePath = primaryPath
        return open(primaryPath, 'w', encoding='utf-8', errors='ignore')
示例#12
0
class ScriptRunner:
    _log = Inject('Logger')

    def runWrapper(self, runner):
        startTime = datetime.now()

        succeeded = False

        try:
            runner()
            succeeded = True

        except KeyboardInterrupt as e:
            self._log.endHeading()
            self._log.error('Operation aborted by user by hitting CTRL+C')

        except Exception as e:
            self._log.endHeading()
            self._log.error(str(e))

            if self._log.currentHeading:
                self._log.error("Failed while executing '" +
                                self._log.currentHeading + "'")

            # Only print stack trace if it's a build-script error
            if not isinstance(e, ProcessErrorCodeException) and not isinstance(
                    e, ProcessTimeoutException):
                self._log.debug('\n' + traceback.format_exc())

        totalSeconds = (datetime.now() - startTime).total_seconds()
        totalSecondsStr = Util.formatTimeDelta(totalSeconds)

        if succeeded:
            self._log.finished('Operation completed successfully.  Took ' +
                               totalSecondsStr + '.\n')
        else:
            self._log.finished('Operation completed with errors.  Took ' +
                               totalSecondsStr + '.\n')

        return succeeded
示例#13
0
class UnityPackageExtractor:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')

    # Returns the chosen name for the directory
    # If forcedName is given then this value is always forcedName
    def extractUnityPackage(self, unityPackagePath, fallbackName, forcedName):

        fileName = os.path.basename(unityPackagePath)

        self._log.heading("Extracting '{0}'", fileName)
        self._log.debug("Extracting unity package at path '{0}'",
                        unityPackagePath)

        tempDir = tempfile.mkdtemp()
        self._log.info("Using temp directory '{0}'", tempDir)

        try:
            self._sys.createDirectory(os.path.join(tempDir, 'ProjectSettings'))
            self._sys.createDirectory(os.path.join(tempDir, 'Assets'))

            self._sys.executeAndWait(
                '"[UnityExePath]" -batchmode -nographics -quit -projectPath "{0}" -importPackage "{1}"'
                .format(tempDir, unityPackagePath))

            self._log.heading("Copying extracted results to output directory")

            assetsDir = os.path.join(tempDir, 'Assets')

            # If the unitypackage only contains a single directory, then extract that instead
            # To avoid ending up with PackageName/PackageName directories for everything
            dirToCopy = self._chooseDirToCopy(assetsDir)

            dirToCopyName = os.path.basename(dirToCopy)

            assertThat(not self._isSpecialFolderName(dirToCopyName))

            # If the extracted package contains a single directory, then by default use that directory as the name for the package
            # This is nice for packages that assume some directory structure (eg. UnityTestTools)
            # Also, some packages have titles that aren't as nice as directories.  For example, Unity Test Tools uses the directory name UnityTestTools
            # which is a bit nicer (though adds a bit of confusion since the release name doesn't match)
            # Note that for upgrading/downgrading, this doesn't matter because it uses the ID which is stored in the ProjenyInstall.yaml file
            if not forcedName and (dirToCopyName.lower() != 'assets'
                                   and dirToCopyName.lower() != 'plugins'):
                forcedName = dirToCopyName

            if forcedName:
                newPackageName = forcedName
            else:
                assertThat(fallbackName)
                newPackageName = fallbackName

            newPackageName = self._sys.convertToValidFileName(newPackageName)

            assertThat(not self._isSpecialFolderName(newPackageName))

            outDirPath = '[UnityPackagesDir]/{0}'.format(newPackageName)
            self._sys.copyDirectory(dirToCopy, outDirPath)

            return newPackageName
        finally:
            self._log.debug("Deleting temporary directory", tempDir)
            shutil.rmtree(tempDir)

    def _isSpecialFolderName(self, dirName):
        dirNameLower = dirName.lower()
        return dirNameLower == 'editor' or dirNameLower == 'streamingassets' or dirNameLower == 'webplayertemplates'

    def _chooseDirToCopy(self, startDir):
        rootNames = [
            x for x in self._sys.walkDir(startDir) if not x.endswith('.meta')
        ]

        assertThat(len(rootNames) > 0)

        if len(rootNames) > 1:
            return startDir

        rootName = rootNames[0]
        fullRootPath = os.path.join(startDir, rootName)

        if rootName.lower() == 'plugins':
            return self._chooseDirToCopy(fullRootPath)

        if rootName.lower() == 'editor':
            return startDir

        if os.path.isdir(fullRootPath):
            return fullRootPath

        return startDir
示例#14
0
class Runner:
    _sys = Inject('SystemHelper')
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _scriptRunner = Inject('ScriptRunner')
    _packageMgr = Inject('PackageManager')
    _zipHelper = Inject('ZipHelper')
    _vsSolutionHelper = Inject('VisualStudioHelper')

    def run(self, args):
        self._args = args
        success = self._scriptRunner.runWrapper(self._runInternal)

        if not success:
            sys.exit(1)

    def _copyDir(self, relativePath):
        self._sys.copyDirectory('[ProjenyDir]/' + relativePath,
                                '[TempDir]/' + relativePath)

    def _copyFile(self, relativePath):
        self._sys.copyFile('[ProjenyDir]/' + relativePath,
                           '[TempDir]/' + relativePath)

    def _runInternal(self):
        self._varMgr.add('PythonDir', PythonDir)
        self._varMgr.add('ProjenyDir', ProjenyDir)
        self._varMgr.add('SourceDir', '[ProjenyDir]/Source')
        self._varMgr.add('InstallerDir', '[ProjenyDir]/Installer')
        self._varMgr.add('TempDir', '[InstallerDir]/Build')
        self._varMgr.add('DistDir', '[InstallerDir]/Dist')

        self._sys.deleteAndReCreateDirectory('[DistDir]')
        self._sys.deleteAndReCreateDirectory('[TempDir]')

        try:
            self._updateBuildDirectory()

            versionStr = self._sys.readFileAsText(
                '[SourceDir]/Version.txt').strip()
            installerOutputPath = '[DistDir]/ProjenyInstaller-v{0}.exe'.format(
                versionStr)

            self._createInstaller(installerOutputPath)

            self._createSamplesZip(versionStr)

            if self._args.addTag:
                self._log.info('Adding git tag for version number')
                self._sys.executeAndWait(
                    "git tag -a v{0} -m 'Version {0}'".format(versionStr))

            if self._args.runInstallerAfter:
                self._sys.deleteDirectoryIfExists(
                    'C:/Program Files (x86)/Projeny')
                self._sys.executeNoWait(installerOutputPath)
        finally:
            self._sys.deleteDirectoryIfExists('[TempDir]')

    def _createSamplesZip(self, versionStr):
        self._log.heading(
            'Clearing all generated files in Demo/UnityProjects folder')
        self._packageMgr.clearAllProjectGeneratedFiles(False)

        self._sys.deleteDirectoryIfExists('[TempDir]')

        self._sys.copyDirectory('[ProjenyDir]/Demo', '[TempDir]')

        self._sys.removeFileIfExists('[TempDir]/.gitignore')
        self._sys.removeFileIfExists('[TempDir]/PrjLog.txt')

        self._log.heading('Zipping up demo project')
        self._zipHelper.createZipFile(
            '[TempDir]',
            '[DistDir]/ProjenySamples-v{0}.zip'.format(versionStr))

    def _createInstaller(self, installerOutputPath):
        self._log.heading('Creating installer exe')
        assertThat(self._sys.directoryExists(NsisPath))

        self._sys.createDirectory('[DistDir]')
        self._sys.executeAndWait(
            '"{0}" "[InstallerDir]/CreateInstaller.nsi"'.format(NsisPath))

        self._sys.renameFile('[DistDir]/ProjenyInstaller.exe',
                             installerOutputPath)

    def _updateBuildDirectory(self):

        self._sys.deleteAndReCreateDirectory('[TempDir]')

        self._log.heading('Building exes')
        self._sys.executeAndWait('[PythonDir]/BuildAllExes.bat')

        self._log.heading('Building unity plugin dlls')
        self._vsSolutionHelper.buildVisualStudioProject(
            '[ProjenyDir]/UnityPlugin/Projeny.sln', 'Release')

        self._copyDir('UnityPlugin/Projeny/Assets')
        self._copyDir('Templates')
        self._copyFile(ConfigFileName)
        self._copyDir('Bin')

        self._sys.removeFile('[TempDir]/Bin/.gitignore')

        self._sys.removeByRegex('[TempDir]/Bin/UnityPlugin/Release/*.pdb')
        self._sys.deleteDirectoryIfExists('[TempDir]/Bin/UnityPlugin/Debug')
示例#15
0
class ProcessRunner:
    _log = Inject('Logger')

    def execNoWait(self, vals, startDir):
        params = {}

        if startDir != None:
            params['cwd'] = startDir

        Popen(vals, **params)

    def waitForProcessOrTimeout(self, commandVals, seconds, startDir = None):

        params = {}
        params['stdout'] = subprocess.PIPE
        params['stderr'] = subprocess.STDOUT

        if startDir != None:
            params['cwd'] = startDir

        proc = Popen(commandVals, **params)

        # TODO - clean this up so there's only one thread, then
        # do the timeout logic on the main thread
        timeout = KillProcessThread(seconds, proc.pid)
        timeout.run()

        def enqueueOutput(out, queue):
            for line in iter(out.readline, b''):
                queue.put(line)
            out.close()

        # We use a queue here instead of just calling stdout.readline() on the main thread
        # so that we can catch the KeyboardInterrupt event, and force kill the process
        queue = Queue()
        thread = threading.Thread(target = enqueueOutput, args = (proc.stdout, queue))
        thread.daemon = True # thread dies with the program
        thread.start()

        while True:
            try:
                try:
                    line = queue.get_nowait()
                    self._log.debug(line.decode('utf-8').rstrip())
                except Empty:
                    if not thread.isAlive():
                        break
                    time.sleep(0.2)
            except KeyboardInterrupt as e:
                self._log.error("Detected KeyboardInterrupt - killing process...")
                timeout.forceKill()
                raise e

        resultCode = proc.wait()

        timeout.cancel()

        if timeout.timeOutOccurred:
            return ResultType.TimedOut

        if resultCode != 0:
            return ResultType.Error

        return ResultType.Success

    # Note that in this case we pass the command as a string
    # This is recommended by the python docs here when using shell = True
    # https://docs.python.org/2/library/subprocess.html#subprocess.Popen
    def execShellCommand(self, commandStr, startDir = None):

        params = {}
        params['stdout'] = subprocess.PIPE
        params['stderr'] = subprocess.PIPE
        params['shell'] = True

        if startDir != None:
            params['cwd'] = startDir

        # Would be nice to get back output in real time but I can't figure
        # out a way to do this
        # This method should only be used for a few command-prompt specific
        # commands anyway so not a big loss
        proc = Popen(commandStr, **params)

        (stdoutData, stderrData) = proc.communicate()

        output = stdoutData.decode('utf-8').strip()
        errors = stderrData.decode('utf-8').strip()

        if output:
            for line in output.split('\n'):
                self._log.debug(line)

        if errors:
            self._log.error('Error occurred during command "{0}":'.format(commandStr))
            for line in errors.split('\n'):
                self._log.error('    ' + line)

        exitStatus = proc.returncode

        if exitStatus != 0:
            return ResultType.Error

        return ResultType.Success
示例#16
0
class Test1:
    foo = Inject('Foo')
    bar = InjectOptional('Bar', 5)
示例#17
0
class SystemHelper:
    '''Responsibilities:
        - Miscellaneous file-handling/path-related operations
        - Wrapper to execute arbitrary commands
    '''
    _varManager = Inject('VarManager')
    _log = Inject('Logger')
    _processRunner = Inject('ProcessRunner')

    # Use an hour timeout
    def __init__(self, timeout=60 * 60):
        self._timeout = timeout

    def canonicalizePath(self, pathStr):
        # Make one standard representation of the given path
        # This will remove ..\ and also change to always use back slashes since this is what os.path.join etc. uses
        return self._varManager.expandPath(pathStr)

    def executeAndWait(self, commandStr, startDir=None):
        expandedStr = self._varManager.expand(commandStr)

        self._log.debug("Executing '%s'" % expandedStr)

        vals = self._splitCommandStr(expandedStr)

        if startDir != None:
            startDir = self._varManager.expand(startDir)

        result = self._processRunner.waitForProcessOrTimeout(
            vals, self._timeout, startDir)

        if result == ResultType.Error:
            raise ProcessErrorCodeException(
                'Command returned with error code while executing: %s' %
                expandedStr)

        if result == ResultType.TimedOut:
            raise ProcessTimeoutException(
                'Timed out while waiting for command: %s' % expandedStr)

        assertThat(result == ResultType.Success)

    def executeNoWait(self, commandStr, startDir=None):
        expandedStr = self._varManager.expand(commandStr)

        self._log.debug("Executing '{0}'".format(expandedStr))

        vals = self._splitCommandStr(expandedStr)

        if startDir != None:
            startDir = self._varManager.expand(startDir)

        self._processRunner.execNoWait(vals, startDir)

    # This is only used to execute shell-specific commands like copy, mklink, etc.
    def executeShellCommand(self, commandStr, startDir=None):
        expandedStr = self._varManager.expand(commandStr)

        self._log.debug("Executing '%s'" % expandedStr)

        if startDir != None:
            startDir = self._varManager.expand(startDir)

        result = self._processRunner.execShellCommand(expandedStr, startDir)

        if result == ResultType.Error:
            raise ProcessErrorCodeException(
                'Command returned with error code while executing: %s' %
                expandedStr)

        assertThat(result == ResultType.Success,
                   "Expected success result but found '{0}'".format(result))

    def _splitCommandStr(self, commandStr):
        # Hacky but necessary since shlex.split will otherwise remove our backslashes
        if platform.platform().startswith('Windows'):
            commandStr = commandStr.replace(os.sep, os.sep + os.sep)

        # Convert command to argument list to avoid issues with escape characters, etc.
        # Based on an answer here: http://stackoverflow.com/questions/12081970/python-using-quotes-in-the-subprocess-popen
        return shlex.split(commandStr)

    def executeAndReturnOutput(self, commandStr):
        self._log.debug("Executing '%s'" % commandStr)
        return subprocess.getoutput(self._splitCommandStr(commandStr)).strip()

    def walkDir(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        return os.listdir(dirPath)

    def getParentDirectoriesWithSelf(self, path):
        yield path

        for parentDir in self.getParentDirectories(path):
            yield parentDir

    def getParentDirectories(self, path):
        path = self._varManager.expand(path)

        lastParentDir = None
        parentDir = os.path.dirname(path)

        while parentDir and parentDir != lastParentDir:
            yield parentDir

            lastParentDir = parentDir
            parentDir = os.path.dirname(parentDir)

    def createDirectory(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        try:
            os.makedirs(dirPath)
        except:
            pass

    def convertToValidFileName(self, s):
        """Take a string and return a valid filename constructed from the string.
    Uses a whitelist approach: any characters not present in valid_chars are
    removed.

    Note: this method may still produce invalid filenames such as ``, `.` or `..`
    """
        valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
        filename = ''.join(c for c in s if c in valid_chars)
        return filename

    def makeMissingDirectoriesInPath(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        try:
            os.makedirs(os.path.dirname(dirPath))
        except:
            pass

    def copyFile(self, fromPath, toPath):
        toPath = self._varManager.expand(toPath)
        fromPath = self._varManager.expand(fromPath)

        self.makeMissingDirectoriesInPath(toPath)
        shutil.copy2(fromPath, toPath)

    def IsDir(self, path):
        return os.path.isdir(self._varManager.expand(path))

    def clearDirectoryContents(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        for fileName in os.listdir(dirPath):
            filePath = os.path.join(dirPath, fileName)
            if os.path.isfile(filePath):
                os.unlink(filePath)
            elif os.path.isdir(filePath):
                shutil.rmtree(filePath)

    def deleteDirectoryWaitIfNecessary(self, dirPath):
        dirPath = self._varManager.expand(dirPath)

        if not os.path.isdir(dirPath):
            # Already removed
            return

        attemptsLeft = 10

        while True:
            try:
                shutil.rmtree(dirPath)
            except Exception as e:
                self._log.warn(
                    'Could not delete directory at "{0}".  Waiting to try again...'
                    .format(dirPath))
                time.sleep(5)
                attemptsLeft -= 1

                if attemptsLeft < 0:
                    raise e
                continue
            break

    def deleteDirectory(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        shutil.rmtree(dirPath)

    def deleteAndReCreateDirectory(self, dirPath):
        self.deleteDirectoryIfExists(dirPath)
        self.createDirectory(dirPath)

    def deleteDirectoryIfExists(self, dirPath):
        dirPath = self._varManager.expand(dirPath)

        if os.path.exists(dirPath):
            shutil.rmtree(dirPath)
            return True

        return False

    def deleteEmptyDirectoriesUnder(self, dirPath):
        dirPath = self._varManager.expandPath(dirPath)

        if not os.path.isdir(dirPath):
            return 0

        # Can't process long paths on windows
        if len(dirPath) >= 256:
            return 0

        files = os.listdir(dirPath)

        numDirsDeleted = 0

        for fileName in files:
            fullpath = os.path.join(dirPath, fileName)

            if os.path.isdir(fullpath):
                numDirsDeleted += self.deleteEmptyDirectoriesUnder(fullpath)

        files = os.listdir(dirPath)

        if len(files) == 0:
            self._log.debug("Removing empty folder '%s'" % dirPath)
            os.rmdir(dirPath)
            numDirsDeleted += 1

            metaFilePath = dirPath + '/../' + os.path.basename(
                dirPath) + '.meta'

            if os.path.isfile(metaFilePath):
                self._log.debug("Removing meta file '%s'" % metaFilePath)
                os.remove(metaFilePath)

        return numDirsDeleted

    def fileExists(self, path):
        return os.path.isfile(self._varManager.expand(path))

    def directoryExists(self, dirPath):
        return os.path.exists(self._varManager.expand(dirPath))

    def copyDirectory(self, fromPath, toPath):
        fromPath = self._varManager.expand(fromPath)
        toPath = self._varManager.expand(toPath)

        self._log.debug("Copying directory '{0}' to '{1}'".format(
            fromPath, toPath))

        shutil.copytree(fromPath, toPath)

    def readFileAsText(self, path):
        with self.openInputFile(path) as f:
            return f.read()

    def writeFileAsText(self, path, text):
        with self.openOutputFile(path) as f:
            f.write(text)

    def openOutputFile(self, path):
        path = self._varManager.expand(path)
        self.makeMissingDirectoriesInPath(path)
        return open(path, 'w', encoding='utf-8', errors='ignore')

    def openInputFile(self, path):
        return open(self._varManager.expand(path),
                    'r',
                    encoding='utf-8',
                    errors='ignore')

    def removeFile(self, fileName):
        os.remove(self._varManager.expand(fileName))

    def removeFileIfExists(self, fileName):
        fullPath = self._varManager.expand(fileName)

        if os.path.isfile(fullPath):
            os.remove(fullPath)
            return True

        return False

    def findFilesByPattern(self, startDir, pattern):
        startDir = self._varManager.expand(startDir)

        for root, dirs, files in os.walk(startDir):
            for basename in files:
                if fnmatch.fnmatch(basename, pattern):
                    filename = os.path.join(root, basename)
                    yield filename

    def renameFile(self, currentName, newName):
        os.rename(self._varManager.expand(currentName),
                  self._varManager.expand(newName))

    def removeFileWaitIfNecessary(self, fileName):
        outputPath = self._varManager.expand(fileName)

        if not os.path.isfile(outputPath):
            # File already removed
            return

        while True:
            try:
                os.remove(outputPath)
            except OSError:
                self._log.warn(
                    'Could not delete file at "{0}".  Waiting to try again...'.
                    format(outputPath))
                time.sleep(5)
                continue
            break

    def removeByRegex(self, regex):
        regex = self._varManager.expand(regex)
        count = 0

        for filePath in glob(regex):
            os.unlink(filePath)
            count += 1

        self._log.debug("Removed %s files matching '%s'" % (count, regex))

    def makeMissingDirectoriesInPath(self, dirPath):
        dirPath = self._varManager.expand(dirPath)
        self._log.debug(
            "Making missing directories in path '{0}'".format(dirPath))
        try:
            os.makedirs(os.path.dirname(dirPath))
        except:
            pass
class Runner:
    _scriptRunner = Inject('ScriptRunner')
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _packageAnalyzer = Inject('UnityPackageAnalyzer')

    def __init__(self):
        self._manifest = None

    def run(self, args):
        self._args = args

        self._args.directory = self._sys.canonicalizePath(self._args.directory)
        self._scriptRunner.runWrapper(self._runInternal)

    def _runInternal(self):
        self._log.debug("Started ReleaseManifestUpdater with arguments: {0}".format(" ".join(sys.argv[1:])))

        while True:
            releasePaths = self._getAllReleasePaths()

            self._log.info("Checking for changes...")

            if self._hasChanged(releasePaths):
                self._manifest = self._createManifest(releasePaths)
                self._saveManifest()

                self._log.info("Detected change to one or more releasePaths. Release manifest has been updated.")

            if self._args.pollInternal <= 0:
                break

            time.sleep(self._args.pollInternal)

    def _saveManifest(self):
        yamlStr = YamlSerializer.serialize(self._manifest)
        self._sys.writeFileAsText(os.path.join(self._args.directory, ReleaseManifestFileName), yamlStr)

    def _createManifest(self, releasePaths):
        manifest = ReleaseManifest()
        for path in releasePaths:
            path = self._sys.canonicalizePath(path)

            releaseInfo = self._packageAnalyzer.getReleaseInfoFromUnityPackage(path)

            assertThat(path.startswith(self._args.directory))
            relativePath = path[len(self._args.directory)+1:]
            releaseInfo.localPath = relativePath

            manifest.releases.append(releaseInfo)
        return manifest

    def _hasChanged(self, releasePaths):
        if self._manifest == None:
            return True

        return False

    def _getAllReleasePaths(self):
        releasePath = []
        for filePath in self._sys.findFilesByPattern(self._args.directory, '*.unitypackage'):
            releasePath.append(filePath)
        return releasePath
示例#19
0
class Runner:
    _log = Inject('Logger')
    _packageMgr = Inject('PackageManager')
    _unityHelper = Inject('UnityHelper')
    _vsSolutionHelper = Inject('VisualStudioHelper')
    _releaseSourceManager = Inject('ReleaseSourceManager')
    _sys = Inject('SystemHelper')
    _varMgr = Inject('VarManager')

    def run(self, project, platform, requestId, param1, param2):
        self._log.debug("Started EditorApi with arguments: {0}".format(
            " ".join(sys.argv[1:])))

        self._project = project
        self._platform = platform
        self._requestId = requestId
        self._param1 = param1
        self._param2 = param2

        succeeded = True

        # This is repeated in __main__ but this is better because
        # it will properly log detailed errors to the file log instead of to the console
        try:
            self._runInternal()
        except Exception as e:
            sys.stderr.write(str(e))
            self._log.error(str(e))

            if not MiscUtil.isRunningAsExe():
                self._log.error('\n' + traceback.format_exc())

            succeeded = False

        if not succeeded:
            sys.exit(1)

    def _runInternal(self):
        if self._requestId == 'updateLinks':
            self._packageMgr.updateProjectJunctions(self._project,
                                                    self._platform)

        elif self._requestId == 'openUnity':
            self._packageMgr.checkProjectInitialized(self._project,
                                                     self._platform)
            self._unityHelper.openUnity(self._project, self._platform)

        elif self._requestId == 'openPackagesFolder':
            os.startfile(self._varMgr.expandPath("[UnityPackagesDir]"))

        elif self._requestId == 'updateCustomSolution':
            self._vsSolutionHelper.updateCustomSolution(
                self._project, self._platform)

        elif self._requestId == 'openCustomSolution':
            self._vsSolutionHelper.openCustomSolution(self._project,
                                                      self._platform)

        elif self._requestId == 'listPackages':
            infos = self._packageMgr.getAllPackageInfos()
            for packageInfo in infos:
                sys.stderr.write('---\n')
                sys.stderr.write(YamlSerializer.serialize(packageInfo) + '\n')

        elif self._requestId == 'listProjects':
            projectNames = self._packageMgr.getAllProjectNames()
            for projName in projectNames:
                sys.stderr.write(projName + '\n')

        elif self._requestId == 'listReleases':
            for release in self._releaseSourceManager.lookupAllReleases():
                sys.stderr.write('---\n')
                sys.stderr.write(YamlSerializer.serialize(release) + '\n')

        elif self._requestId == 'deletePackage':
            self._log.info("Deleting package '{0}'", self._param1)
            self._packageMgr.deletePackage(self._param1)

        elif self._requestId == 'createPackage':
            self._log.info("Creating package '{0}'", self._param1)
            self._packageMgr.createPackage(self._param1)

        elif self._requestId == 'installRelease':
            self._log.info("Installing release '{0}' version code '{1}'",
                           self._param1, self._param2)
            self._releaseSourceManager.installReleaseById(
                self._param1, self._param2, True)

        elif self._requestId == 'createProject':
            self._log.info("Creating new project '{0}'", self._project)
            self._packageMgr._createProject(self._project)

        else:
            assertThat(False, "Invalid request id '{0}'", self._requestId)
示例#20
0
class VisualStudioSolutionGenerator:
    """
    Handler for creating custom visual studio solutions based on ProjenyProject.yaml files
    """
    _log = Inject('Logger')
    _packageManager = Inject('PackageManager')
    _schemaLoader = Inject('ProjectSchemaLoader')
    _unityHelper = Inject('UnityHelper')
    _config = Inject('Config')
    _varMgr = Inject('VarManager')
    _sys = Inject('SystemHelper')

    def updateVisualStudioSolution(self, projectName, platform):
        self._log.heading('Updating Visual Studio solution for project "{0}"'.format(projectName))

        self._packageManager.setPathsForProject(projectName, platform)
        self._packageManager.checkProjectInitialized(projectName, platform)

        schema = self._schemaLoader.loadSchema(projectName, platform)
        self._updateProjects(schema)

    def _prettify(self, doc):
        return minidom.parseString(ET.tostring(doc)).toprettyxml(indent="    ")

    def _getDefineConstantsElement(self, root):
        return root.findall('.//{0}DefineConstants'.format(NsPrefix))[0].text

    def _getUnityProjectReferencesItems(self, root):
        items = []
        refElems = root.findall('./{0}ItemGroup/{0}Reference'.format(NsPrefix))

        for refElem in refElems:
            name = refElem.get('Include')
            children = refElem.getchildren()

            hintPath = None

            if len(children) > 0:
                hintPathElem = children[0]
                assertThat(hintPathElem.tag == '{0}HintPath'.format(NsPrefix))

                hintPath = hintPathElem.text.replace('/', '\\')

            if hintPath:
                if not os.path.isabs(hintPath):
                    hintPath = self._varMgr.expandPath('[ProjectPlatformRoot]/{0}'.format(hintPath))

                assertThat(self._sys.fileExists(hintPath), "Expected to find file at '{0}'".format(hintPath))

            items.append(RefInfo(name, hintPath))

        return items

    def _chooseMostRecentFile(self, path1, path2):
        path1 = self._varMgr.expandPath(path1)
        path2 = self._varMgr.expandPath(path2)

        # If they both exist choose most recent
        if self._sys.fileExists(path1) and self._sys.fileExists(path2):
            modtime1 = os.path.getmtime(path1)
            modtime2 = os.path.getmtime(path2)

            if modtime1 > modtime2:
                return path1

            return path2

        if self._sys.fileExists(path1):
            return path1

        if self._sys.fileExists(path2):
            return path2

        return None

    def _parseGeneratedUnityProject(self):

        # Annoyingly, unity does generate the solution using different paths
        # depending on settings
        # If visual studio is set to external editor, it names it the first one
        # and otherwise it names it the second one
        # So check modification times for the case where the user changes this setting
        unityProjPath = self._chooseMostRecentFile(
            '[UnityGeneratedProjectPath]', '[UnityGeneratedProjectPath2]')

        unityEditorProjPath = self._chooseMostRecentFile(
            '[UnityGeneratedProjectEditorPath]', '[UnityGeneratedProjectEditorPath2]')

        assertThat(unityProjPath and self._sys.fileExists(unityProjPath) and unityEditorProjPath and self._sys.fileExists(unityEditorProjPath), \
            'Could not find unity-generated project when generating custom solution.  This is necessary so the custom solution can add things like unity defines and DLL references within the unity project.')

        unityProjRoot = ET.parse(unityProjPath)
        unityProjEditorRoot = ET.parse(unityEditorProjPath)

        unityDefines = self._getDefineConstantsElement(unityProjRoot)

        unityRefItems = self._getUnityProjectReferencesItems(unityProjRoot)
        unityRefItemsEditor = self._getUnityProjectReferencesItems(unityProjEditorRoot)

        return unityDefines, unityRefItems, unityRefItemsEditor

    def _updateProjects(self, schema):

        # Necessary to avoid having ns0: prefixes everywhere on output
        ET.register_namespace('', 'http://schemas.microsoft.com/developer/msbuild/2003')

        unityDefines, unityRefItems, unityRefItemsEditor = self._parseGeneratedUnityProject()

        excludeDirs = []
        includedProjects = []
        allCustomProjects = {}

        pluginsProj = self._createStandardCsProjInfo('PluginsFolder', '[PluginsDir]')
        includedProjects.append(pluginsProj)

        scriptsEditorProj = self._createStandardCsProjInfo('AssetsFolder-Editor', '[ProjectAssetsDir]')
        includedProjects.append(scriptsEditorProj)

        scriptsProj = self._createStandardCsProjInfo('AssetsFolder', '[ProjectAssetsDir]')
        includedProjects.append(scriptsProj)

        pluginsEditorProj = self._createStandardCsProjInfo('PluginsFolder-Editor', '[PluginsDir]')
        includedProjects.append(pluginsEditorProj)

        allPackages = schema.packages.values()

        prebuiltProjectInfos = self._createPrebuiltProjInfo(schema.prebuiltProjects)

        customEditorProjects = []
        customAssetsProjects = []

        # Store lambdas to create the csproj projects so that the isignored flags are always up to date when they get written
        # Otherwise sometimes we will output csproj files that have a ProjectReference tag to a project that doesn't exist
        # This is really hacky but I am about to release the new version and it's not worth doing a proper refactor to get
        # this ordering right
        csProjWriters = []

        # Need to populate allCustomProjects first so we can get references in _tryCreateCustomProject
        for packageInfo in allPackages:
            if packageInfo.createCustomVsProject:
                customProject = self._createCsProjInfo(packageInfo, False)
                allCustomProjects[customProject.name] = customProject
                customAssetsProjects.append(customProject)

                customEditorProject = self._createCsProjInfo(packageInfo, True)
                allCustomProjects[customEditorProject.name] = customEditorProject
                customEditorProjects.append(customEditorProject)

        for packageInfo in allPackages:
            if packageInfo.createCustomVsProject:
                self._log.debug('Processing project "{0}"'.format(packageInfo.name))

                customProject = self._tryCreateCustomProject( \
                     False, allCustomProjects, packageInfo, unityRefItems, unityRefItemsEditor, unityDefines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjectInfos, csProjWriters)

                if customProject:
                    includedProjects.append(customProject)

                customEditorProject = self._tryCreateCustomProject( \
                     True, allCustomProjects, packageInfo, unityRefItems, unityRefItemsEditor, unityDefines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjectInfos, csProjWriters)

                if customEditorProject:
                    includedProjects.append(customEditorProject)

        self._log.debug('Processing project "{0}"'.format(pluginsProj.name))

        pluginsProj.dependencies = prebuiltProjectInfos
        self._createStandardCsProjForDirectory(pluginsProj, excludeDirs, unityRefItems, unityDefines, False, prebuiltProjectInfos, csProjWriters)

        pluginsEditorProj.dependencies = [pluginsProj] + prebuiltProjectInfos

        for packageInfo in allPackages:
            if packageInfo.createCustomVsProject and packageInfo.isPluginDir:
                pluginsEditorProj.dependencies.append(allCustomProjects[packageInfo.name])

        self._log.debug('Processing project "{0}"'.format(pluginsEditorProj.name))
        self._createStandardCsProjForDirectory(pluginsEditorProj, excludeDirs, unityRefItemsEditor, unityDefines, True, prebuiltProjectInfos, csProjWriters)

        excludeDirs.append(self._varMgr.expandPath('[PluginsDir]'))

        self._log.debug('Processing project "{0}"'.format(scriptsProj.name))
        scriptsProj.dependencies = [pluginsProj] + prebuiltProjectInfos
        self._createStandardCsProjForDirectory(scriptsProj, excludeDirs, unityRefItems, unityDefines, False, prebuiltProjectInfos, csProjWriters)

        scriptsEditorProj.dependencies = scriptsProj.dependencies + [scriptsProj, pluginsEditorProj]

        self._log.debug('Processing project "{0}"'.format(scriptsEditorProj.name))
        self._createStandardCsProjForDirectory(scriptsEditorProj, excludeDirs, unityRefItemsEditor, unityDefines, True, prebuiltProjectInfos, csProjWriters)

        self._ensureNoMatchingPrebuiltCsProjNames(includedProjects, prebuiltProjectInfos)

        for prebuiltProj in prebuiltProjectInfos:
            includedProjects.append(prebuiltProj)

        for writer in csProjWriters:
            writer()

        self._createSolution(includedProjects, schema.customFolderMap)

    def _ensureNoMatchingPrebuiltCsProjNames(self, projects, prebuiltProjects):
        for proj in projects:
            for prebuiltProj in prebuiltProjects:
                assertThat(prebuiltProj.name != proj.name, 'Found prebuilt project and normal project both using the same name "{0}".  This will not work in the same visual studio solution'.format(prebuiltProj.name))

    def _createPrebuiltProjInfo(self, prebuiltProjects):

        processedList = []

        for projAbsPath in prebuiltProjects:
            projId = self._getProjectIdFromFile(projAbsPath)
            projName = os.path.splitext(os.path.basename(projAbsPath))[0]

            processedList.append(CsProjInfo(projId, projAbsPath, projName, True, [], False))

        return processedList

    def _getFolderName(self, packageName, customFolders):

        for item in customFolders.items():
            folderName = item[0]
            pattern = item[1]
            if packageName == pattern or (pattern.startswith('/') and re.match(pattern[1:], packageName)):
                return folderName

        return None

    def _createCsProjInfo(self, packageInfo, isEditor):

        projId = self._createProjectGuid()
        outputDir = self._varMgr.expandPath(packageInfo.outputDirVar)

        csProjectName = packageInfo.name

        if isEditor:
            csProjectName += EditorProjectNameSuffix

        outputPath = os.path.join(outputDir, csProjectName + ".csproj")

        packageDir = os.path.join(outputDir, packageInfo.name)

        files = []
        self._addCsFilesInDirectory(packageDir, [], files, isEditor)

        isIgnored = (len(files) == 0)

        return CsProjInfo(projId, outputPath, csProjectName, False, files, isIgnored)

    def _tryCreateCustomProject(self, isEditor, customCsProjects, packageInfo, unityRefItems, unityRefItemsEditor, defines, excludeDirs, scriptsProj, pluginsProj, scriptsEditorProj, pluginsEditorProj, prebuiltProjects, csProjWriters):

        projName = packageInfo.name

        if isEditor:
            projName += EditorProjectNameSuffix

        csProjInfo = customCsProjects[projName]

        outputDir = os.path.dirname(csProjInfo.absPath)
        packageDir = os.path.join(outputDir, packageInfo.name)
        excludeDirs.append(packageDir)

        if csProjInfo.isIgnored:
            return None

        projDependencies = []

        if isEditor:
            projDependencies.append(pluginsProj)
            projDependencies.append(pluginsEditorProj)

            projDependencies.append(customCsProjects[packageInfo.name])

            if not packageInfo.isPluginDir:
                projDependencies.append(scriptsProj)
                projDependencies.append(scriptsEditorProj)
        else:
            projDependencies.append(pluginsProj)

            if not packageInfo.isPluginDir:
                projDependencies.append(scriptsProj)

        for dependName in packageInfo.allDependencies:
            assertThat(not dependName in projDependencies)

            if dependName in customCsProjects:
                dependProj = customCsProjects[dependName]

                if not dependProj.isIgnored:
                    projDependencies.append(dependProj)

            if isEditor:
                dependEditorName = dependName + EditorProjectNameSuffix

                if dependEditorName in customCsProjects:
                    dependEditorProj = customCsProjects[dependEditorName]

                    if not dependEditorProj.isIgnored:
                        projDependencies.append(dependEditorProj)

        if isEditor:
            refItems = unityRefItemsEditor
        else:
            refItems = unityRefItems

        csProjInfo.dependencies = list(projDependencies)

        for proj in prebuiltProjects:
            csProjInfo.dependencies.append(proj)

        csProjWriters.append(lambda: self._writeCsProject(csProjInfo, csProjInfo.files, refItems, defines, prebuiltProjects))

        return csProjInfo

    def _getProjectIdFromFile(self, projPath):

        doc = ET.parse(projPath)
        root = doc.getroot()

        projId = root.findall('./{0}PropertyGroup/{0}ProjectGuid'.format(NsPrefix))[0].text
        return re.match('^{(.*)}$', projId).groups()[0]

    def _createSolution(self, projects, customFolderMap):

        with open(self._varMgr.expandPath('[CsSolutionTemplate]'), 'r', encoding='utf-8', errors='ignore') as inputFile:
            solutionStr = inputFile.read()

        outputPath = self._varMgr.expandPath('[SolutionPath]')
        outputDir = os.path.dirname(outputPath)

        projectList = ''
        postSolution = ''
        projectFolderStr = ''
        projectFolderMapsStr = ''

        folderIds = {}

        for folderName in customFolderMap:
            folderId = self._createProjectGuid()
            folderIds[folderName] = folderId
            projectFolderStr += 'Project("{{{0}}}") = "{1}", "{2}", "{{{3}}}"\nEndProject\n' \
                .format(SolutionFolderTypeGuid, folderName, folderName, folderId)

        for proj in projects:
            assertThat(proj.name)
            assertThat(proj.id)
            assertThat(proj.absPath)

            if proj.isIgnored:
                continue

            projectList += 'Project("{{{0}}}") = "{1}", "{2}", "{{{3}}}"\n' \
                .format(CsProjTypeGuid, proj.name, os.path.relpath(proj.absPath, outputDir), proj.id)

            if len(proj.dependencies) > 0:
                projectList += '\tProjectSection(ProjectDependencies) = postProject\n'
                for projDepend in proj.dependencies:
                    if not projDepend.isIgnored:
                        projectList += '\t\t{{{0}}} = {{{0}}}\n'.format(projDepend.id)
                projectList += '\tEndProjectSection\n'

            projectList += 'EndProject\n'

            if len(postSolution) != 0:
                postSolution += '\n'

            buildConfig = 'Unity Debug' if proj.isPrebuild else 'Debug'

            postSolution += \
                '\t\t{{{0}}}.Debug|Any CPU.ActiveCfg = {1}|Any CPU\n\t\t{{{0}}}.Debug|Any CPU.Build.0 = {1}|Any CPU' \
                .format(proj.id, buildConfig)

            folderName = self._getFolderName(proj.name, customFolderMap)

            if folderName:
                folderId = folderIds[folderName]

                if len(projectFolderMapsStr) != 0:
                    projectFolderMapsStr += '\n'

                projectFolderMapsStr += \
                    '\t\t{{{0}}} = {{{1}}}' \
                    .format(proj.id, folderId)

        solutionStr = solutionStr.replace('[ProjectList]', projectList)

        if len(postSolution.strip()) > 0:
            solutionStr = solutionStr.replace('[PostSolution]', """
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0}
    EndGlobalSection""".format(postSolution))
        else:
            solutionStr = solutionStr.replace('[PostSolution]', '')

        solutionStr = solutionStr.replace('[ProjectFolders]', projectFolderStr)

        if len(projectFolderMapsStr) > 0:
            fullStr = '\tGlobalSection(NestedProjects) = preSolution\n{0}\n\tEndGlobalSection'.format(projectFolderMapsStr)
            solutionStr = solutionStr.replace('[ProjectFolderMaps]', fullStr)
        else:
            solutionStr = solutionStr.replace('[ProjectFolderMaps]', '')

        with open(outputPath, 'w', encoding='utf-8', errors='ignore') as outFile:
            outFile.write(solutionStr)

        self._log.debug('Saved new solution file at "{0}"'.format(outputPath))

    def _createStandardCsProjInfo(self, projectName, outputDir):

        outputDir = self._varMgr.expandPath(outputDir)
        outputPath = os.path.join(outputDir, projectName + ".csproj")

        projId = self._createProjectGuid()

        return CsProjInfo(projId, outputPath, projectName, False, [], False)

    def _createStandardCsProjForDirectory(self, projInfo, excludeDirs, unityRefItems, defines, isEditor, prebuiltProjects, csProjWriters):

        outputDir = os.path.dirname(projInfo.absPath)

        files = []
        self._addCsFilesInDirectory(outputDir, excludeDirs, files, isEditor)

        if len(files) == 0:
            projInfo.isIgnored = True
            return

        csProjWriters.append(lambda: self._writeCsProject(projInfo, files, unityRefItems, defines, prebuiltProjects))

    def _createProjectGuid(self):
        return str(uuid.uuid4()).upper()

    def _shouldReferenceBeCopyLocal(self, refName):
        return refName != 'System' and refName != 'System.Core'

    def _writeCsProject(self, projInfo, files, refItems, defines, prebuiltProjects):

        outputDir = os.path.dirname(projInfo.absPath)

        doc = ET.parse(self._varMgr.expandPath('[CsProjectTemplate]'))

        root = doc.getroot()
        self._stripWhitespace(root)

        refsItemGroupElem = root.findall('./{0}ItemGroup[{0}Reference]'.format(NsPrefix))[0]
        refsItemGroupElem.clear()

        # Add reference items given from unity project
        for refInfo in refItems:

            if len([x for x in prebuiltProjects if x.name == refInfo.name]) > 0:
                #self._log.debug('Ignoring reference for prebuilt project "{0}"'.format(refInfo.name))
                continue

            refElem = ET.SubElement(refsItemGroupElem, 'Reference')
            refElem.set('Include', refInfo.name)

            if refInfo.hintPath:
                refPath = refInfo.hintPath
                assertThat(os.path.isabs(refPath), "Invalid path '{0}'".format(refPath))

                if refPath.startswith(outputDir):
                    refPath = os.path.relpath(refPath, outputDir)

                hintPathElem = ET.SubElement(refElem, 'HintPath')
                hintPathElem.text = refPath

            ET.SubElement(refElem, 'Private').text = 'True' if self._shouldReferenceBeCopyLocal(refInfo.name) else 'False'

        # Add cs files 'compile' items
        filesItemGroupElem = root.findall('./{0}ItemGroup[{0}Compile]'.format(NsPrefix))[0]
        filesItemGroupElem.clear()

        for filePath in files:
            if filePath.endswith('.cs'):
                compileElem = ET.SubElement(filesItemGroupElem, 'Compile')
            else:
                compileElem = ET.SubElement(filesItemGroupElem, 'None')

            compileElem.set('Include', os.path.relpath(filePath, outputDir))

        root.findall('./{0}PropertyGroup/{0}RootNamespace'.format(NsPrefix))[0] \
            .text = projInfo.name

        root.findall('./{0}PropertyGroup/{0}ProjectGuid'.format(NsPrefix))[0] \
            .text = '{' + projInfo.id + '}'

        root.findall('./{0}PropertyGroup/{0}OutputPath'.format(NsPrefix))[0] \
            .text = os.path.relpath(self._varMgr.expandPath('[ProjectPlatformRoot]/Bin'), outputDir)

        root.findall('./{0}PropertyGroup/{0}AssemblyName'.format(NsPrefix))[0] \
            .text = projInfo.name

        root.findall('./{0}PropertyGroup/{0}RootNamespace'.format(NsPrefix))[0] \
            .text = "ModestTree"

        root.findall('./{0}PropertyGroup/{0}DefineConstants'.format(NsPrefix))[0] \
            .text = defines

        tempFilesDir = os.path.relpath(self._varMgr.expandPath('[IntermediateFilesDir]'), outputDir)

        root.findall('./{0}PropertyGroup/{0}IntermediateOutputPath'.format(NsPrefix))[0] \
            .text = tempFilesDir

        root.findall('./{0}PropertyGroup/{0}BaseIntermediateOutputPath'.format(NsPrefix))[0] \
            .text = tempFilesDir

        # Add project references
        projectRefGroupElem = root.findall('./{0}ItemGroup[{0}ProjectReference]'.format(NsPrefix))[0]
        projectRefGroupElem.clear()

        for dependInfo in projInfo.dependencies:
            if dependInfo.isIgnored:
                continue

            projectRefElem = ET.SubElement(projectRefGroupElem, 'ProjectReference')
            projectRefElem.set('Include', os.path.relpath(dependInfo.absPath, outputDir))

            ET.SubElement(projectRefElem, 'Project').text = '{' + dependInfo.id + '}'
            ET.SubElement(projectRefElem, 'Name').text = dependInfo.name

        self._sys.makeMissingDirectoriesInPath(projInfo.absPath)

        with open(projInfo.absPath, 'w', encoding='utf-8', errors='ignore') as outputFile:
            outputFile.write(self._prettify(root))

    def _stripWhitespace(self, elem):
        for x in ET.ElementTree(elem).getiterator():
            if x.text: x.text = x.text.strip()
            if x.tail: x.tail = x.tail.strip()

    def _prettify(self, doc):
        return minidom.parseString(ET.tostring(doc)).toprettyxml(indent="    ")

    def _shouldIgnoreCsProjFile(self, fullPath):
        if self._config.getBool('LinkToProjenyEditorDir'):
            return False

        return ProjenyDirectoryIgnorePattern.match(fullPath)

    def _addCsFilesInDirectory(self, dirPath, excludeDirs, files, isForEditor):
        isInsideEditorFolder = re.match(r'.*\\Editor($|\\).*', dirPath)

        if not isForEditor and isInsideEditorFolder:
            return

        if dirPath in excludeDirs:
            return

        #self._log.debug('Processing ' + dirPath)

        for excludeDir in excludeDirs:
            assertThat(not dirPath.startswith(excludeDir + "\\"))

        if not self._sys.directoryExists(dirPath):
            return

        for itemName in os.listdir(dirPath):
            fullPath = os.path.join(dirPath, itemName)

            if self._shouldIgnoreCsProjFile(fullPath):
                continue

            if os.path.isdir(fullPath):
                self._addCsFilesInDirectory(fullPath, excludeDirs, files, isForEditor)
            else:
                if re.match('.*\.(cs|txt)$', itemName):
                    if not isForEditor or isInsideEditorFolder:
                        files.append(fullPath)
示例#21
0
class ReleaseSourceManager:
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _config = Inject('Config')
    _sys = Inject('SystemHelper')
    _packageManager = Inject('PackageManager')

    def __init__(self):
        self._hasInitialized = False
        self._releaseSources = []

    def _lazyInit(self):
        if self._hasInitialized:
            return

        self._hasInitialized = True
        for regSettings in self._config.getList('ReleaseSources'):
            for pair in regSettings.items():
                reg = self._createReleaseSource(pair[0], pair[1])
                reg.init()
                self._releaseSources.append(reg)

        self._log.info(
            "Finished initializing Release Source Manager, found {0} releases in total",
            self._getTotalReleaseCount())

    def _getTotalReleaseCount(self):
        total = 0
        for reg in self._releaseSources:
            total += len(reg.releases)
        return total

    def _createReleaseSource(self, regType, settings):
        if regType == 'LocalFolder':
            folderPath = self._varMgr.expand(settings['Path']).replace(
                "\\", "/")
            return LocalFolderReleaseSource(folderPath)

        if regType == 'AssetStoreCache':
            return AssetStoreCacheReleaseSource()

        if regType == 'FileServer':
            return RemoteServerReleaseSource(settings['ManifestUrl'])

        assertThat(False, "Could not find release source with type '{0}'",
                   regType)

    def listAllReleases(self):
        self._lazyInit()
        self._log.heading('Found {0} Releases', self._getTotalReleaseCount())

        for release in self.lookupAllReleases():
            self._log.info("{0} ({1}) ({2})", release.name, release.version,
                           release.versionCode)

    def lookupAllReleases(self):
        self._lazyInit()

        result = []
        for source in self._releaseSources:
            for release in source.releases:
                result.append(release)
        result.sort(key=lambda x: x.name.lower())
        return result

    def _findReleaseInfoAndSourceByIdAndVersionCode(self, releaseId,
                                                    releaseVersionCode):
        assertIsType(releaseVersionCode, int)
        for source in self._releaseSources:
            for release in source.releases:
                if release.id == releaseId and release.versionCode == releaseVersionCode:
                    return (release, source)
        return (None, None)

    def _findReleaseInfoAndSourceByNameAndVersion(self, releaseName,
                                                  releaseVersion):
        for source in self._releaseSources:
            for release in source.releases:
                if release.name == releaseName and release.version == releaseVersion:
                    return (release, source)
        return (None, None)

    def installReleaseByName(self,
                             releaseName,
                             releaseVersion,
                             suppressPrompts=False):
        assertThat(releaseName)
        assertThat(releaseVersion)

        self._lazyInit()
        self._log.heading(
            "Attempting to install release '{0}' (version '{1}')", releaseName,
            releaseVersion)

        assertThat(
            len(self._releaseSources) > 0,
            "Could not find any release sources to search for the given release"
        )

        releaseInfo, releaseSource = self._findReleaseInfoAndSourceByNameAndVersion(
            releaseName, releaseVersion)

        assertThat(
            releaseInfo,
            "Failed to install release '{0}' (version {1}) - could not find it in any of the release sources.\nSources checked: \n  {2}\nTry listing all available release with the -lr command"
            .format(releaseName, releaseVersion,
                    "\n  ".join([x.getName() for x in self._releaseSources])))

        self._installReleaseInternal(releaseInfo, releaseSource,
                                     suppressPrompts)

    def installReleaseById(self,
                           releaseId,
                           releaseVersionCode,
                           suppressPrompts=False):

        self._log.info(
            "Attempting to install release with ID '{0}' and version code '{1}'",
            releaseId, releaseVersionCode)

        try:
            releaseVersionCode = int(releaseVersionCode)
        except ValueError:
            assertThat(
                False,
                "Invalid version code '{0}' - must be convertable to an integer",
                releaseVersionCode)

        assertThat(releaseVersionCode, 'Invalid release version code supplied')
        assertThat(releaseId)

        self._lazyInit()

        assertThat(
            len(self._releaseSources) > 0,
            "Could not find any release sources to search for the given release"
        )

        releaseInfo, releaseSource = self._findReleaseInfoAndSourceByIdAndVersionCode(
            releaseId, releaseVersionCode)

        assertThat(
            releaseInfo,
            "Failed to install release '{0}' - could not find it in any of the release sources.\nSources checked: \n  {1}\nTry listing all available release with the -lr command"
            .format(releaseId,
                    "\n  ".join([x.getName() for x in self._releaseSources])))

        self._installReleaseInternal(releaseInfo, releaseSource,
                                     suppressPrompts)

    def _installReleaseInternal(self,
                                releaseInfo,
                                releaseSource,
                                suppressPrompts=False):

        self._log.heading("Installing release '{0}' (version {1})",
                          releaseInfo.name, releaseInfo.version)

        installDirName = None

        for packageInfo in self._packageManager.getAllPackageInfos():
            installInfo = packageInfo.installInfo

            if installInfo and installInfo.releaseInfo and installInfo.releaseInfo.id == releaseInfo.id:
                if installInfo.releaseInfo.versionCode == releaseInfo.versionCode:
                    if not suppressPrompts:
                        shouldContinue = MiscUtil.confirmChoice(
                            "Release '{0}' (version {1}) is already installed.  Would you like to re-install anyway?  Note that this will overwrite any local changes you've made to it."
                            .format(releaseInfo.name, releaseInfo.version))

                        assertThat(shouldContinue, 'User aborted')
                else:
                    print(
                        "\nFound release '{0}' already installed with version '{1}'"
                        .format(releaseInfo.name, releaseInfo.version),
                        end='')

                    installDirection = 'UPGRADE' if releaseInfo.versionCode > installInfo.releaseInfo.versionCode else 'DOWNGRADE'

                    if not suppressPrompts:
                        shouldContinue = MiscUtil.confirmChoice(
                            "Are you sure you want to {0} '{1}' from version '{2}' to version '{3}'? (y/n)"
                            .format(installDirection, releaseInfo.name,
                                    installInfo.releaseInfo.version,
                                    releaseInfo.version))
                        assertThat(shouldContinue, 'User aborted')

                self._packageManager.deletePackage(packageInfo.name)
                # Retain original directory name in case it is referenced by other packages
                installDirName = packageInfo.name

        installDirName = releaseSource.installRelease(releaseInfo,
                                                      installDirName)

        destDir = self._varMgr.expand(
            '[UnityPackagesDir]/{0}'.format(installDirName))

        assertThat(self._sys.directoryExists(destDir),
                   'Expected dir "{0}" to exist', destDir)

        newInstallInfo = PackageInstallInfo()
        newInstallInfo.releaseInfo = releaseInfo
        newInstallInfo.installDate = datetime.utcnow()

        yamlStr = YamlSerializer.serialize(newInstallInfo)
        self._sys.writeFileAsText(os.path.join(destDir, InstallInfoFileName),
                                  yamlStr)

        self._log.info("Successfully installed '{0}' (version {1})",
                       releaseInfo.name, releaseInfo.version)
示例#22
0
class VarManager:
    _config = Inject('Config')
    '''
    Stores a dictionary of keys to values to replace path variables with
    '''
    def __init__(self, initialParams=None):
        self._params = initialParams if initialParams else {}
        self._params['StartCurrentDir'] = os.getcwd()
        self._params['ExecDir'] = MiscUtil.getExecDirectory().replace(
            '\\', '/')

        # We could just call self._config.getDictionary('PathVars') here but
        # then we wouldn't be able to use fallback (?) and override (!) characters in
        # our config

        self._regex = re.compile('^([^\[]*)(\[[^\]]*\])(.*)$')

    def hasKey(self, key):
        return key in self._params or self._config.tryGet('PathVars',
                                                          key) != None

    def get(self, key):
        if key in self._params:
            return self._params[key]

        return self._config.getString('PathVars', key)

    def tryGet(self, key):
        if key in self._params:
            return self._params[key]

        return self._config.tryGetString(None, 'PathVars', key)

    def add(self, key, value):
        self._params[key] = value

    def set(self, key, value):
        self._params[key] = value

    def expandPath(self, text, extraVars=None):
        ''' Same as expand() except it cleans up the path to remove ../ '''
        return os.path.realpath(self.expand(text, extraVars))

    def expand(self, text, extraVars=None):

        if not extraVars:
            extraVars = {}

        allArgs = self._params.copy()
        allArgs.update(extraVars)

        while True:
            match = self._regex.match(text)

            if not match:
                break

            prefix = match.group(1)
            var = match.group(2)
            suffix = match.group(3)

            var = var[1:-1]

            if var in allArgs:
                replacement = allArgs[var]
            else:
                replacement = self.get(var)

            text = prefix + replacement + suffix

        if '[' in text:
            raise Exception(
                "Unable to find all keys in path '{0}'".format(text))

        return text
class Test1:
    con = Inject('Console', Assertions.HasMethods('WriteLine'))

    def Run(self):
        self.con.WriteLine('lorem ipsum')
class Test2:
    title = Inject('Title', Assertions.IsInstanceOf(str))

    def Run(self):
        print('title: {0}'.format(self.title))
示例#25
0
class ProjectSchemaLoader:
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')

    def loadSchema(self, name, platform):
        schemaPath = self._varMgr.expandPath(
            '[UnityProjectsDir]/{0}/{1}'.format(name, ProjectConfigFileName))
        schemaPathUser = self._varMgr.expandPath(
            '[UnityProjectsDir]/{0}/{1}'.format(name,
                                                ProjectUserConfigFileName))
        schemaPathGlobal = self._varMgr.expandPath(
            '[UnityProjectsDir]/{0}'.format(ProjectConfigFileName))
        schemaPathUserGlobal = self._varMgr.expandPath(
            '[UnityProjectsDir]/{0}'.format(ProjectUserConfigFileName))

        self._log.debug('Loading schema at path "{0}"'.format(schemaPath))
        config = Config(
            loadYamlFilesThatExist(schemaPath, schemaPathUser,
                                   schemaPathGlobal, schemaPathUserGlobal))

        pluginDependencies = config.tryGetList([], 'PluginsFolder')
        scriptsDependencies = config.tryGetList([], 'AssetsFolder')
        customProjects = config.tryGetList([], 'SolutionProjects')
        customFolders = config.tryGetDictionary({}, 'SolutionFolders')
        prebuiltProjects = config.tryGetList([], 'Prebuilt')

        # Remove duplicates
        scriptsDependencies = list(set(scriptsDependencies))
        pluginDependencies = list(set(pluginDependencies))

        for packageName in pluginDependencies:
            assertThat(
                not packageName in scriptsDependencies,
                "Found package '{0}' in both scripts and plugins.  Must be in only one or the other"
                .format(packageName))

        allDependencies = pluginDependencies + scriptsDependencies

        packageMap = {}

        # Resolve all dependencies for each package
        # by default, put any dependencies that are not declared explicitly into the plugins folder
        for packageName in allDependencies:

            configPath = self._varMgr.expandPath(
                '[UnityPackagesDir]/{0}/ProjenyPackage.yaml'.format(
                    packageName))

            if os.path.exists(configPath):
                packageConfig = Config(loadYamlFilesThatExist(configPath))
            else:
                packageConfig = Config([])

            createCustomVsProject = self._checkCustomProjectMatch(
                packageName, customProjects)

            isPluginsDir = packageName in pluginDependencies

            if isPluginsDir:
                assertThat(not packageName in scriptsDependencies)
            else:
                assertThat(packageName in scriptsDependencies)

            if packageConfig.tryGetBool(False, 'ForceAssetsDirectory'):
                isPluginsDir = False

            explicitDependencies = packageConfig.tryGetList([], 'Dependencies')

            forcePluginsDir = packageConfig.tryGetBool(
                False, 'ForcePluginsDirectory')

            assertThat(not packageName in packageMap)
            packageMap[packageName] = PackageInfo(isPluginsDir, packageName,
                                                  packageConfig,
                                                  createCustomVsProject,
                                                  explicitDependencies,
                                                  forcePluginsDir)

            for dependName in explicitDependencies:
                if not dependName in allDependencies:
                    pluginDependencies.append(dependName)
                    # Yes, python is ok with changing allDependencies even while iterating over it
                    allDependencies.append(dependName)

            for dependName in packageConfig.tryGetList([], 'Extras'):
                if not dependName in allDependencies:
                    if isPluginsDir:
                        pluginDependencies.append(dependName)
                    else:
                        scriptsDependencies.append(dependName)
                    # Yes, python is ok with changing allDependencies even while iterating over it
                    allDependencies.append(dependName)

        self._removePlatformSpecificPackages(packageMap, platform)

        self._printDependencyTree(packageMap)

        self._fillOutDependencies(packageMap)

        for customProj in customProjects:
            assertThat(
                customProj.startswith('/') or customProj in allDependencies,
                'Given project "{0}" in schema is not included in either "scripts" or "plugins"'
                .format(customProj))

        self._addPrebuiltProjectsFromPackages(packageMap, prebuiltProjects)

        self._log.info('Found {0} packages in total for given schema'.format(
            len(allDependencies)))

        # In Unity, the plugins folder can not have any dependencies on anything in the scripts folder
        # So if dependencies exist then just automatically move those packages to the scripts folder
        self._ensurePluginPackagesDoNotHaveDependenciesInAssets(packageMap)

        self._ensurePackagesThatAreNotProjectsDoNotHaveProjectDependencies(
            packageMap)

        projectsDir = self._varMgr.expandPath('[UnityProjectsDir]')
        prebuiltProjectPaths = [
            os.path.normpath(os.path.join(projectsDir, x))
            for x in prebuiltProjects
        ]

        for info in packageMap.values():
            if info.forcePluginsDir and not info.isPluginDir:
                assertThat(
                    False, "Package '{0}' must be in plugins directory".format(
                        info.name))

        return ProjectSchema(name, packageMap, customFolders,
                             prebuiltProjectPaths)

    def _ensurePackagesThatAreNotProjectsDoNotHaveProjectDependencies(
            self, packageMap):
        changedOne = True

        while changedOne:
            changedOne = False

            for info in packageMap.values():
                if not info.createCustomVsProject and self._hasVsProjectDependency(
                        info, packageMap):
                    info.createCustomVsProject = True
                    self._log.warn(
                        'Created visual studio project for {0} package even though it wasnt marked as one, because it has csproj dependencies'
                        .format(info.name))
                    changedOne = True

    def _hasVsProjectDependency(self, info, packageMap):
        for dependName in info.allDependencies:
            if not dependName in packageMap:
                # For eg. a platform specific dependency
                continue

            dependInfo = packageMap[dependName]

            if dependInfo.createCustomVsProject:
                return True

        return False

    def _ensurePluginPackagesDoNotHaveDependenciesInAssets(self, packageMap):
        movedProject = True

        while movedProject:
            movedProject = False

            for info in packageMap.values():
                if info.isPluginDir and self._hasAssetsDependency(
                        info, packageMap):
                    info.isPluginDir = False
                    self._log.warn(
                        'Moved {0} package to scripts folder since it has dependencies there and therefore cannot be in plugins'
                        .format(info.name))
                    movedProject = True

    def _hasAssetsDependency(self, info, packageMap):
        for dependName in info.allDependencies:
            if not dependName in packageMap:
                # For eg. a platform specific dependency
                continue

            dependInfo = packageMap[dependName]

            if not dependInfo.isPluginDir:
                return True

        return False

    def _addPrebuiltProjectsFromPackages(self, packageMap, prebuiltProjects):
        for info in packageMap.values():
            prebuiltPaths = info.config.tryGetList([], 'Prebuilt')

            for path in prebuiltPaths:
                if path not in prebuiltProjects:
                    prebuiltProjects.append(path)

    def _removePlatformSpecificPackages(self, packageMap, platform):

        for info in list(packageMap.values()):

            if info.folderType == FolderTypes.AndroidProject or info.folderType == FolderTypes.AndroidLibraries:
                platforms = [Platforms.Android]
            elif info.folderType == FolderTypes.Ios:
                platforms = [Platforms.Ios]
            elif info.folderType == FolderTypes.WebGl:
                platforms = [Platforms.WebGl]
            else:
                platforms = info.config.tryGetList([], 'Platforms')

                if len(platforms) == 0:
                    continue

            if platform not in platforms:
                del packageMap[info.name]
                self._log.debug(
                    'Skipped project {0} since it is not enabled for platform {0}'
                    .format(info.name, platform))

    def _printDependencyTree(self, packageMap):
        packages = sorted(packageMap.values(),
                          key=lambda p:
                          (p.isPluginDir, -len(p.explicitDependencies)))

        done = {}

        for pack in packages:
            self._printDependency(pack, done, 1, packageMap)

    def _printDependency(self, package, done, indentCount, packageMap):
        done[package.name] = True

        indentInterval = '    '

        indent = ((indentCount - 1) * (indentInterval + '.')) + indentInterval
        self._log.debug(indent + '|-' + package.name)

        for dependName in package.explicitDependencies:
            if dependName in packageMap:
                subPackage = packageMap[dependName]

                if subPackage.name in done:
                    self._log.debug(indent + '.' + indentInterval + '|~' +
                                    subPackage.name)
                else:
                    self._printDependency(subPackage, done, indentCount + 1,
                                          packageMap)

    def _checkCustomProjectMatch(self, packageName, customProjects):
        if packageName in customProjects:
            return True

        # Allow regex's!
        for projPattern in customProjects:
            if projPattern.startswith('/'):
                projPattern = projPattern[1:]
                try:
                    if re.match(projPattern, packageName):
                        return True
                except Exception as e:
                    raise Exception(
                        "Failed while parsing project regex '/{0}' from {1}/{2}.  Details: {3}"
                        .format(projPattern,
                                self._varMgr.expand('ProjectName'),
                                ProjectConfigFileName, str(e)))

        return False

    def _fillOutDependencies(self, packageMap):

        self._log.debug('Processing dependency tree')

        inProgress = set()
        for info in packageMap.values():
            self._fillOutDependenciesForPackage(info, packageMap, inProgress)

    def _fillOutDependenciesForPackage(self, packageInfo, packageMap,
                                       inProgress):

        if packageInfo.name in inProgress:
            assertThat(
                False,
                "Found circular dependency when processing package {0}.  Dependency list: {1}"
                .format(
                    packageInfo.name, ' -> '.join([x for x in inProgress]) +
                    '-> ' + packageInfo.name))

        inProgress.add(packageInfo.name)
        allDependencies = set(packageInfo.explicitDependencies)

        for explicitDependName in packageInfo.explicitDependencies:
            if explicitDependName not in packageMap:
                # Might be stripped out based on platform or something so just ignore
                continue

            explicitDependInfo = packageMap[explicitDependName]

            if not explicitDependInfo.allDependencies:
                self._fillOutDependenciesForPackage(explicitDependInfo,
                                                    packageMap, inProgress)

            for dependName in explicitDependInfo.allDependencies:
                allDependencies.add(dependName)

        packageInfo.allDependencies = list(allDependencies)
        inProgress.remove(packageInfo.name)
示例#26
0
class PrjRunner:
    _scriptRunner = Inject('ScriptRunner')
    _config = Inject('Config')
    _packageMgr = Inject('PackageManager')
    _unityHelper = Inject('UnityHelper')
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _mainConfig = InjectOptional('MainConfigPath', None)
    _sys = Inject('SystemHelper')
    _vsSolutionHelper = Inject('VisualStudioHelper')
    _releaseSourceManager = Inject('ReleaseSourceManager')

    def run(self, args):
        self._args = args
        success = self._scriptRunner.runWrapper(self._runInternal)

        if not success:
            sys.exit(1)

    def _runPreBuild(self):
        if self._args.openDocumentation:
            self._openDocumentation()

        if self._args.clearProjectGeneratedFiles:
            self._packageMgr.clearProjectGeneratedFiles(self._project)

        if self._args.clearAllProjectGeneratedFiles:
            self._packageMgr.clearAllProjectGeneratedFiles()

        if self._args.deleteAllLinks:
            self._packageMgr.deleteAllLinks()

        if self._args.deletePackage:
            if not self._args.suppressPrompts:
                if not MiscUtil.confirmChoice("Are you sure you want to delete package '{0}'? (y/n)  \nNote that this change is non-recoverable!  (unless you are using source control)  ".format(self._args.deletePackage)):
                    assertThat(False, "User aborted operation")

            self._packageMgr.deletePackage(self._args.deletePackage)

        if self._args.deleteProject:
            if not self._args.suppressPrompts:
                if not MiscUtil.confirmChoice("Are you sure you want to delete project '{0}'? (y/n)  \nNote that this will only delete your unity project settings and the {1} for this project.  \nThe rest of the content for your project will remain in the UnityPackages folder  ".format(self._args.deleteProject, ProjectConfigFileName)):
                    assertThat(False, "User aborted operation")
            self._packageMgr.deleteProject(self._args.deleteProject)

        if self._args.installRelease:
            releaseName, releaseVersion = self._args.installRelease
            self._releaseSourceManager.installReleaseByName(releaseName, releaseVersion)

        if self._args.init:
            self._packageMgr.updateLinksForAllProjects()

        if self._args.updateLinks:
            self._packageMgr.updateProjectJunctions(self._project, self._platform)

        if self._args.updateUnitySolution:
            self._vsSolutionHelper.updateUnitySolution(self._project, self._platform)

        if self._args.updateCustomSolution:
            self._vsSolutionHelper.updateCustomSolution(self._project, self._platform)

    def _openDocumentation(self):
        webbrowser.open('https://github.com/modesttree/ModestUnityPackageManager')

    def _runBuild(self):
        if self._args.buildCustomSolution:
            self._vsSolutionHelper.buildCustomSolution(self._project, self._platform)

    def _runPostBuild(self):

        if self._args.listReleases:
            self._releaseSourceManager.listAllReleases()

        if self._args.listProjects:
            self._packageMgr.listAllProjects()

        if self._args.listPackages:
            self._packageMgr.listAllPackages()

        if self._args.openUnity:
            self._packageMgr.checkProjectInitialized(self._project, self._platform)
            self._unityHelper.openUnity(self._project, self._platform)

        if self._args.openCustomSolution:
            self._vsSolutionHelper.openCustomSolution(self._project, self._platform)

        if self._args.editProjectYaml:
            self._editProjectYaml()

    def _editProjectYaml(self):
        assertThat(self._project)
        schemaPath = self._varMgr.expandPath('[UnityProjectsDir]/{0}/{1}'.format(self._project, ProjectConfigFileName))
        os.startfile(schemaPath)

    def _runInternal(self):
        self._log.debug("Started Prj with arguments: {0}".format(" ".join(sys.argv[1:])))

        self.processArgs()
        self._validateArgs()

        if self._args.createConfig:
            self._createConfig()

        if self._args.createProject:
            self._packageMgr._createProject(self._args.project)

        if self._args.createPackage:
            self._packageMgr.createPackage(self._args.createPackage)

        self._runPreBuild()
        self._runBuild()
        self._runPostBuild()

    def _createConfig(self):
        self._log.heading('Initializing new projeny config')

        assertThat(not self._mainConfig,
           "Cannot initialize new projeny project, found existing config at '{0}'".format(self._mainConfig))

        curDir = os.getcwd()
        configPath = os.path.join(curDir, ConfigFileName)

        assertThat(not os.path.isfile(configPath), "Found existing projeny config at '{0}'.  Has the configuration already been created?", configPath)

        self._sys.createDirectory(os.path.join(curDir, 'UnityPackages'))
        self._sys.createDirectory(os.path.join(curDir, 'UnityProjects'))

        with self._sys.openOutputFile(configPath) as outFile:
            outFile.write(
"""
PathVars:
    UnityPackagesDir: '[ConfigDir]/UnityPackages'
    UnityProjectsDir: '[ConfigDir]/UnityProjects'
    LogPath: '[ConfigDir]/PrjLog.txt'
""")

    def processArgs(self):

        self._project = self._args.project

        if not self._project:
            self._project = self._config.tryGetString(None, 'DefaultProject')

        if self._project and not self._packageMgr.projectExists(self._project) and not self._args.createProject:
            self._project = self._packageMgr.getProjectFromAlias(self._project)

        if not self._project and self._varMgr.hasKey('UnityProjectsDir'):
            allProjects = self._packageMgr.getAllProjectNames()

            # If there's only one project, then just always assume they are operating on that
            if len(allProjects) == 1:
                self._project = allProjects[0]

        self._platform = PlatformUtil.fromPlatformArgName(self._args.platform)

    def _validateArgs(self):
        requiresProject = self._args.updateLinks or self._args.updateUnitySolution \
           or self._args.updateCustomSolution or self._args.buildCustomSolution \
           or self._args.clearProjectGeneratedFiles or self._args.buildFull \
           or self._args.openUnity or self._args.openCustomSolution \
           or self._args.editProjectYaml or self._args.createProject

        if requiresProject and not self._project:
            assertThat(False, "Cannot execute the given arguments without a project specified, or a default project defined in the {0} file", ConfigFileName)
示例#27
0
class UnityHelper:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _varMgr = Inject('VarManager')
    _commonSettings = Inject('CommonSettings')

    def __init__(self):
        pass

    def onUnityLog(self, logStr):
        self._log.debug(logStr)

    def runEditorFunction(self,
                          projectName,
                          platform,
                          editorCommand,
                          batchMode=True,
                          quitAfter=True,
                          extraExtraArgs=''):
        extraArgs = ''

        if quitAfter:
            extraArgs += ' -quit'

        if batchMode:
            extraArgs += ' -batchmode -nographics'

        extraArgs += ' ' + extraExtraArgs

        self.runEditorFunctionRaw(projectName, platform, editorCommand,
                                  extraArgs)

    def openUnity(self, projectName, platform):
        self._log.heading('Opening Unity')
        projectPath = self._sys.canonicalizePath(
            "[UnityProjectsDir]/{0}/{1}-{2}".format(
                projectName,
                self._commonSettings.getShortProjectName(projectName),
                PlatformUtil.toPlatformFolderName(platform)))
        self._sys.executeNoWait(
            '"[UnityExePath]" -buildTarget {0} -projectPath "{1}"'.format(
                self._getBuildTargetArg(platform), projectPath))

    def _getBuildTargetArg(self, platform):

        if platform == Platforms.Windows:
            return 'win32'

        if platform == Platforms.WebPlayer:
            return 'web'

        if platform == Platforms.Android:
            return 'android'

        if platform == Platforms.WebGl:
            return 'WebGl'

        if platform == Platforms.OsX:
            return 'osx'

        if platform == Platforms.Linux:
            return 'linux'

        if platform == Platforms.Ios:
            return 'ios'

        assertThat(False)

    def runEditorFunctionRaw(self, projectName, platform, editorCommand,
                             extraArgs):

        logPath = self._varMgr.expandPath(UnityLogFileLocation)

        logWatcher = LogWatcher(logPath, self.onUnityLog)
        logWatcher.start()

        os.environ['ModestTreeBuildConfigOverride'] = "FromBuildScript"

        assertThat(self._varMgr.hasKey('UnityExePath'),
                   "Could not find path variable 'UnityExePath'")

        try:
            command = '"[UnityExePath]" -buildTarget {0} -projectPath "[UnityProjectsDir]/{1}/{2}-{3}"'.format(
                self._getBuildTargetArg(platform), projectName,
                self._commonSettings.getShortProjectName(projectName),
                PlatformUtil.toPlatformFolderName(platform))

            if editorCommand:
                command += ' -executeMethod ' + editorCommand

            command += ' ' + extraArgs

            self._sys.executeAndWait(command)
        except ProcessErrorCodeException as e:
            raise UnityReturnedErrorCodeException(
                "Error while running Unity!  Command returned with error code."
            )

        except:
            raise UnityUnknownErrorException(
                "Unknown error occurred while running Unity!")
        finally:
            logWatcher.stop()

            while not logWatcher.isDone:
                time.sleep(0.1)

            os.environ['ModestTreeBuildConfigOverride'] = ""
示例#28
0
class VisualStudioHelper:
    _log = Inject('Logger')
    _config = Inject('Config')
    _packageManager = Inject('PackageManager')
    _unityHelper = Inject('UnityHelper')
    _varMgr = Inject('VarManager')
    _sys = Inject('SystemHelper')
    _vsSolutionGenerator = Inject('VisualStudioSolutionGenerator')

    def openFile(self, filePath, lineNo, project, platform):
        if not lineNo or lineNo <= 0:
            lineNo = 1

        if MiscUtil.doesProcessExist('^devenv\.exe$'):
            self.openFileInExistingVisualStudioInstance(filePath, lineNo)

            # This works too but doesn't allow going to a specific line
            #self._sys.executeNoWait('[VisualStudioCommandLinePath] /edit "{0}"'.format(filePath))
        else:
            # Unfortunately, in this case we can't pass in the line number
            self.openCustomSolution(project, platform, filePath)

    def openFileInExistingVisualStudioInstance(self, filePath, lineNo):
        try:
            dte = win32com.client.GetActiveObject("VisualStudio.DTE.12.0")

            dte.MainWindow.Activate
            dte.ItemOperations.OpenFile(self._sys.canonicalizePath(filePath))
            dte.ActiveDocument.Selection.MoveToLineAndOffset(lineNo, 1)
        except Exception as error:
            raise Exception("COM Error.  This is often triggered when given a bad line number. Details: {0}".format(win32api.FormatMessage(error.excepinfo[5])))

    def openVisualStudioSolution(self, solutionPath, filePath = None):
        if not self._varMgr.hasKey('VisualStudioIdePath'):
            assertThat(False, "Path to visual studio has not been defined.  Please set <VisualStudioIdePath> within one of your {0} files.  See documentation for details.", ConfigFileName)

        if self._sys.fileExists('[VisualStudioIdePath]'):
            self._sys.executeNoWait('"[VisualStudioIdePath]" {0} {1}'.format(self._sys.canonicalizePath(solutionPath), self._sys.canonicalizePath(filePath) if filePath else ""))
        else:
            assertThat(False, "Cannot find path to visual studio.  Expected to find it at '{0}'".format(self._varMgr.expand('[VisualStudioIdePath]')))

    def updateCustomSolution(self, project, platform):
        self._vsSolutionGenerator.updateVisualStudioSolution(project, platform)

    def openCustomSolution(self, project, platform, filePath = None):
        self.openVisualStudioSolution(self._getCustomSolutionPath(project, platform), filePath)

    def buildCustomSolution(self, project, platform):
        solutionPath = self._getCustomSolutionPath(project, platform)

        if not self._sys.fileExists(solutionPath):
            self._log.warn('Could not find generated custom solution.  Generating now.')
            self._vsSolutionGenerator.updateVisualStudioSolution(project, platform)

        self._log.heading('Building {0}-{1}.sln'.format(project, platform))
        self.buildVisualStudioProject(solutionPath, 'Debug')

    def buildVisualStudioProject(self, solutionPath, buildConfig):
        if self._config.getBool('Compilation', 'UseDevenv'):
            buildCommand = '"[VisualStudioCommandLinePath]" {0} /build "{1}"'.format(solutionPath, buildConfig)
        else:
            buildCommand = '"[MsBuildExePath]" /p:VisualStudioVersion=12.0'
            #if rebuild:
                #buildCommand += ' /t:Rebuild'
            buildCommand += ' /p:Configuration="{0}" "{1}"'.format(buildConfig, solutionPath)

        self._sys.executeAndWait(buildCommand)

    def _getCustomSolutionPath(self, project, platform):
        return '[UnityProjectsDir]/{0}/{0}-{1}.sln'.format(project, platform)

    def updateUnitySolution(self, projectName, platform):
        """
        Simply runs unity and then generates the monodevelop solution file using an editor script
        This is used when generating the Visual Studio Solution to get DLL references and defines etc.
        """
        self._log.heading('Updating unity generated solution for project {0} ({1})'.format(projectName, platform))

        self._packageManager.checkProjectInitialized(projectName, platform)

        # This will generate the unity csproj files which we need to generate Modest3d.sln correctly
        # It's also necessary to run this first on clean checkouts to initialize unity properly
        self._unityHelper.runEditorFunction(projectName, platform, 'Projeny.ProjenyEditorUtil.ForceGenerateUnitySolution')
示例#29
0
class UnityPackageAnalyzer:
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')

    def getReleaseInfoFromUnityPackage(self, unityPackagePath):
        fileName = os.path.basename(unityPackagePath)

        #self._log.debug("Analyzing unity package '{0}'", fileName)

        assertThat(self._sys.fileExists(unityPackagePath))

        info = ReleaseInfo()
        info.localPath = unityPackagePath

        headerInfo = self._tryGetAssetStoreInfoFromHeader(unityPackagePath)

        info.compressedSize = os.path.getsize(unityPackagePath)
        info.fileModificationDate = datetime.utcfromtimestamp(
            os.path.getmtime(unityPackagePath))

        if headerInfo:
            info.name = headerInfo['title']
            info.versionCode = int(headerInfo['version_id'])
            info.version = headerInfo['version']
            info.id = headerInfo['id']
            info.assetStoreInfo = self._getAssetStoreInfo(headerInfo)
        else:
            # If there are no headers, then we have to derive the info from the file name
            info.id, info.name, info.versionCode, info.version = self._getInfoFromFileName(
                fileName)

        return info

    def _getInfoFromFileName(self, fileName):
        parts = os.path.splitext(fileName)
        assertThat(parts[1].lower() == '.unitypackage')
        baseName = parts[0].strip()

        match = re.match('^(.*)@\s*(\d+\.?\d*)\s*$', baseName)

        if match:
            groups = match.groups()

            name = groups[0]
            versionStr = groups[1]

            numDecimals = len(re.match('^\d+\.(\d*)$', versionStr).groups()[0])

            assertThat(
                numDecimals <= 7,
                'Projeny only supports up to 7 decimal points in the version number!'
            )

            # Need a flat int so we can do greater than/less than comparisons
            versionCode = int(10000000 * float(groups[1]))

            return (name, name, versionCode, versionStr)

        return (baseName, baseName, None, None)

    def _getAssetStoreInfo(self, allInfo):
        info = AssetStoreInfo()
        info.publisherId = allInfo['publisher']['id']
        info.publisherLabel = allInfo['publisher']['label']
        info.publishNotes = allInfo.get('publishnotes', '')
        info.categoryId = allInfo['category']['id']
        info.categoryLabel = allInfo['category']['label']
        info.uploadId = allInfo.get('upload_id', None)
        info.description = allInfo.get('description', '')

        pubDate = allInfo['pubdate']
        info.publishDate = datetime.strptime(pubDate, "%d %b %Y")

        info.unityVersion = allInfo.get('unity_version', None)
        info.linkId = allInfo['link']['id']
        info.linkType = allInfo['link']['type']
        return info

    def _tryGetAssetStoreInfoFromHeader(self, unityPackagePath):

        with open(unityPackagePath, 'rb') as f:
            headerBytes = f.read(16)
            headerHexValues = binascii.hexlify(headerBytes)

            headerString = headerHexValues.decode('utf8')

            flag1 = headerString[0:4]

            unixTimeStamp = (headerBytes[7] << 24) + (headerBytes[6] << 16) + (
                headerBytes[5] << 8) + headerBytes[4]
            datetime.utcfromtimestamp(unixTimeStamp)

            flag2 = headerString[6:8]
            flag3 = headerString[24:28]

            numBytes = int(headerString[22:24] + headerString[20:22], 16)
            numJsonBytes = int(headerString[30:32] + headerString[28:30], 16)

            assertThat(flag1 == "1f8b", "Invalid .unitypackage file")

            # These flags indicate that it is an asset store package
            if flag2 == "04" and flag3 == "4124":
                assertThat(numBytes == numJsonBytes + 4)

                infoBytes = f.read(numJsonBytes)
                infoStr = infoBytes.decode('utf8')

                return json.loads(infoStr)

        return None
示例#30
0
class PackageManager:
    """
    Main interface for Modest Package Manager
    """
    _config = Inject('Config')
    _varMgr = Inject('VarManager')
    _log = Inject('Logger')
    _sys = Inject('SystemHelper')
    _unityHelper = Inject('UnityHelper')
    _junctionHelper = Inject('JunctionHelper')
    _projectInitHandlers = InjectMany('ProjectInitHandlers')
    _schemaLoader = Inject('ProjectSchemaLoader')
    _commonSettings = Inject('CommonSettings')
    _vsSolutionHelper = Inject('VisualStudioHelper')

    def __init__(self):
        self._packageInfos = None

    def projectExists(self, projectName):
        return self._sys.directoryExists('[UnityProjectsDir]/{0}'.format(projectName)) or self._sys.fileExists('[UnityProjectsDir]/{0}.ini'.format(projectName))

    def listAllProjects(self):
        projectNames = self.getAllProjectNames()
        self._log.info("Found {0} Projects:".format(len(projectNames)))
        for proj in projectNames:
            alias = self.tryGetAliasFromFullName(proj)
            if alias:
                proj = "{0} ({1})".format(proj, alias)
            self._log.info("  " + proj)

    def listAllPackages(self):
        packagesNames = self.getAllPackageNames()
        self._log.info("Found {0} Packages:".format(len(packagesNames)))
        for packageName in packagesNames:
            self._log.info("  " + packageName)

    def _findSourceControl(self):
        for dirPath in self._sys.getParentDirectoriesWithSelf('[ConfigDir]'):
            if self._sys.directoryExists(os.path.join(dirPath, '.git')):
                return SourceControlTypes.Git

            if self._sys.directoryExists(os.path.join(dirPath, '.svn')):
                return SourceControlTypes.Subversion

        return None

    def _createProject(self, projName):
        self._log.heading('Initializing new project "{0}"', projName)

        projDirPath = self._varMgr.expand('[UnityProjectsDir]/{0}'.format(projName))
        assertThat(not self._sys.directoryExists(projDirPath), "Cannot initialize new project '{0}', found existing project at '{1}'", projName, projDirPath)

        self._sys.createDirectory(projDirPath)

        with self._sys.openOutputFile(os.path.join(projDirPath, ProjectConfigFileName)) as outFile:
            outFile.write(
"""
#AssetsFolder:
    # Uncomment and Add package names here
""")

        self.updateProjectJunctions(projName, Platforms.Windows)

    def getProjectFromAlias(self, alias):
        aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases')

        assertThat(alias in aliasMap.keys(), "Unrecognized project '{0}' and could not find an alias with that name either".format(alias))
        return aliasMap[alias]

    def tryGetAliasFromFullName(self, name):
        aliasMap = self._config.tryGetDictionary({}, 'ProjectAliases')

        for pair in aliasMap.items():
            if pair[1] == name:
                return pair[0]

        return None

    def _validateDirForFolderType(self, packageInfo, sourceDir):
        if packageInfo.folderType == FolderTypes.AndroidProject:
            assertThat(os.path.exists(os.path.join(sourceDir, "project.properties")), "Project '{0}' is marked with foldertype AndroidProject and therefore must contain a project.properties file".format(packageInfo.name))

    def updateProjectJunctions(self, projectName, platform):
        """
        Initialize all the folder links for the given project
        """

        self._log.heading('Updating package directories for project {0}'.format(projectName))

        self.checkProjectInitialized(projectName, platform)

        self.setPathsForProject(projectName, platform)
        schema = self._schemaLoader.loadSchema(projectName, platform)
        self._updateDirLinksForSchema(schema)

        self._checkForVersionControlIgnore()

        self._log.good('Finished updating packages for project "{0}"'.format(schema.name))

    def _checkForVersionControlIgnore(self):
        sourceControlType = self._findSourceControl()

        if sourceControlType == SourceControlTypes.Git:
            self._log.heading('Detected git repository.  Making sure generated project folders are ignored by git...')
            if not self._sys.fileExists('[ProjectRootGitIgnoreTemplate]'):
                self._log.heading('Adding missing git ignore file to project root')
                self._sys.copyFile('[ProjectRootGitIgnoreTemplate]', os.path.join(projDirPath, '.gitignore'))
        elif sourceControlType == SourceControlTypes.Subversion:
            self._log.info('Detected subversion repository. Making sure generated project folders are ignored by SVN...')
            try:
                self._sys.executeAndWait('svn propset svn:ignore -F [ProjectRootSvnIgnoreTemplate] .', '[ProjectRoot]')
            except Exception as e:
                self._log.warn("Warning: Failed to add generated project directories to SVN ignore!  This may be caused by 'svn' not being available on the command line.  Details: " + str(e))
        #else:
            #self._log.warn('Warning: Could not determine source control in use!  An ignore file will not be added for your project.')

    def getAllPackageInfos(self):
        if not self._packageInfos:
            self._packageInfos = []

            for name in self.getAllPackageNames():
                path = self._varMgr.expandPath('[UnityPackagesDir]/{0}'.format(name))

                installInfoFilePath = os.path.join(path, InstallInfoFileName)

                info = PackageInfo()
                info.name = name
                info.path = path

                if self._sys.fileExists(installInfoFilePath):
                    installInfo = YamlSerializer.deserialize(self._sys.readFileAsText(installInfoFilePath))
                    info.installInfo = installInfo

                self._packageInfos.append(info)

        return self._packageInfos

    def deleteProject(self, projName):
        self._log.heading("Deleting project '{0}'", projName)
        assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        fullPath = '[UnityProjectsDir]/{0}'.format(projName)

        assertThat(self._sys.directoryExists(fullPath), "Could not find project with name '{0}' - delete failed", projName)

        self.clearProjectGeneratedFiles(projName, True)
        self._sys.deleteDirectory(fullPath)

    def createPackage(self, packageName):
        assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        self._log.heading('Creating new package "{0}"', packageName)
        newPath = '[UnityPackagesDir]/{0}'.format(packageName)

        assertThat(not self._sys.directoryExists(newPath), "Found existing package at path '{0}'", newPath)
        self._sys.createDirectory(newPath)

        # This can be nice when sorting packages by install date, but it adds noise to the directory
        # Seems nicer to just leave the directory empty for custom packages

        #newInstallInfo = PackageInstallInfo()
        #newInstallInfo.releaseInfo = None
        #newInstallInfo.installDate = datetime.utcnow()

        #yamlStr = YamlSerializer.serialize(newInstallInfo)
        #self._sys.writeFileAsText(os.path.join(newPath, InstallInfoFileName), yamlStr)

    def deletePackage(self, name):
        self._log.heading("Deleting package '{0}'", name)
        assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        fullPath = '[UnityPackagesDir]/{0}'.format(name)

        assertThat(self._sys.directoryExists(fullPath), "Could not find package with name '{0}' - delete failed", name)

        self._sys.deleteDirectory(fullPath)

    def getAllPackageNames(self):
        assertThat(self._varMgr.hasKey('UnityPackagesDir'), "Could not find 'UnityPackagesDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        results = []
        for name in self._sys.walkDir('[UnityPackagesDir]'):
            if self._sys.IsDir('[UnityPackagesDir]/' + name):
                results.append(name)
        return results

    def getAllProjectNames(self):
        assertThat(self._varMgr.hasKey('UnityProjectsDir'), "Could not find 'UnityProjectsDir' in PathVars.  Have you set up your {0} file?", ConfigFileName)

        results = []
        for name in self._sys.walkDir('[UnityProjectsDir]'):
            if self._sys.IsDir('[UnityProjectsDir]/' + name):
                results.append(name)
        return results

    # This will set up all the directory junctions for all projects for all platforms
    def updateLinksForAllProjects(self):
        for projectName in self.getAllProjectNames():
            self._log.heading('Initializing project "{0}"'.format(projectName))

            try:
                #for platform in Platforms.All:
                for platform in [Platforms.Windows]:
                    self.updateProjectJunctions(projectName, platform)

                self._log.good('Successfully initialized project "{0}"'.format(projectName))
            except Exception as e:
                self._log.warn('Failed to initialize project "{0}": {1}'.format(projectName, e))

    def _createPlaceholderCsFile(self, path):
        with self._sys.openOutputFile(path) as outFile:
            outFile.write(
"""
    // This file exists purely as a way to force unity to generate the MonoDevelop csproj files so that Projeny can read the settings from it
""")

    def _createSwitchProjectMenuScript(self, currentProjName, outputPath):

        foundCurrent = False
        menuFile = """
using UnityEditor;
using Projeny.Internal;

namespace Projeny
{
    public static class ProjenyChangeProjectMenu
    {"""
        projIndex = 1
        for projName in self.getAllProjectNames():
            menuFile += """
        [MenuItem("Projeny/Change Project/{0}", false, 8)]""".format(projName)

            menuFile += """
        public static void ChangeProject{0}()""".format(projIndex)

            menuFile += """
        {"""

            menuFile += """
            PrjHelper.ChangeProject("{0}");""".format(projName)

            menuFile += """
        }
"""
            if projName == currentProjName:
                assertThat(not foundCurrent)
                foundCurrent = True
                menuFile += """
        [MenuItem("Projeny/Change Project/{0}", true, 8)]""".format(currentProjName)
                menuFile += """
        public static bool ChangeProject{0}Validate()""".format(projIndex)
                menuFile += """
        {
            return false;
        }"""

            projIndex += 1

        menuFile += """
    }
}
"""
        assertThat(foundCurrent)
        self._sys.writeFileAsText(outputPath, menuFile)

    def _updateDirLinksForSchema(self, schema):
        self._removePackageJunctions()

        self._sys.deleteDirectoryIfExists('[PluginsDir]/Projeny')

        if self._config.getBool('LinkToProjenyEditorDir') and not MiscUtil.isRunningAsExe():
            self._junctionHelper.makeJunction('[ProjenyDir]/UnityPlugin/Projeny', '[PluginsDir]/Projeny/Editor/Source')
        else:
            dllOutPath = '[PluginsDir]/Projeny/Editor/Projeny.dll'
            self._sys.copyFile('[ProjenyUnityEditorDllPath]', dllOutPath)
            self._sys.copyFile('[ProjenyUnityEditorDllMetaFilePath]', dllOutPath + '.meta')

            self._sys.copyFile('[YamlDotNetDllPath]', '[PluginsDir]/Projeny/Editor/YamlDotNet.dll')

            assetsOutPath = '[PluginsDir]/Projeny/Editor/Assets'
            self._sys.copyDirectory('[ProjenyUnityEditorAssetsDirPath]', assetsOutPath)
            settingsFileOutPath = os.path.join(assetsOutPath, 'Resources/Projeny/PmSettings.asset')
            self._sys.writeFileAsText(settingsFileOutPath, self._sys.readFileAsText(settingsFileOutPath).replace(
                'm_Script: {fileID: 11500000, guid: 01fe9b81f68762b438dd4eecbcfe2900, type: 3}',
                'm_Script: {fileID: 1582608718, guid: b7b2ba04b543d234aa4225d91c60af2b, type: 3}'))

        self._createSwitchProjectMenuScript(schema.name, '[PluginsDir]/Projeny/Editor/ProjenyChangeProjectMenu.cs')

        self._createPlaceholderCsFile('[PluginsDir]/Projeny/Placeholder.cs')
        self._createPlaceholderCsFile('[PluginsDir]/Projeny/Editor/Placeholder.cs')

        for packageInfo in schema.packages.values():

            self._log.debug('Processing package "{0}"'.format(packageInfo.name))

            sourceDir = self._varMgr.expandPath('[UnityPackagesDir]/{0}'.format(packageInfo.name))

            self._validateDirForFolderType(packageInfo, sourceDir)

            assertThat(os.path.exists(sourceDir),
               "Could not find package with name '{0}' while processing schema '{1}'.  See build log for full object graph to see where it is referenced".format(packageInfo.name, schema.name))

            outputPackageDir = self._varMgr.expandPath(packageInfo.outputDirVar)

            linkDir = os.path.join(outputPackageDir, packageInfo.name)

            assertThat(not os.path.exists(linkDir), "Did not expect this path to exist: '{0}'".format(linkDir))

            self._junctionHelper.makeJunction(sourceDir, linkDir)

    def checkProjectInitialized(self, projectName, platform):
        self.setPathsForProject(projectName, platform)

        if self._sys.directoryExists('[ProjectPlatformRoot]'):
            return

        self._log.warn('Project "{0}" is not initialized for platform "{1}".  Initializing now.'.format(projectName, platform))
        self._initNewProjectForPlatform(projectName, platform)

    def setPathsForProject(self, projectName, platform):

        self._varMgr.set('ShortProjectName', self._commonSettings.getShortProjectName(projectName))
        self._varMgr.set('ShortPlatform', PlatformUtil.toPlatformFolderName(platform))

        self._varMgr.set('Platform', platform)
        self._varMgr.set('ProjectName', projectName)

        self._varMgr.set('ProjectRoot', '[UnityProjectsDir]/[ProjectName]')
        self._varMgr.set('ProjectPlatformRoot', '[ProjectRoot]/[ShortProjectName]-[ShortPlatform]')
        self._varMgr.set('ProjectAssetsDir', '[ProjectPlatformRoot]/Assets')

        self._varMgr.set('UnityGeneratedProjectEditorPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Editor.Plugins.csproj')
        self._varMgr.set('UnityGeneratedProjectPath', '[ProjectPlatformRoot]/[ShortProjectName]-[ShortPlatform].CSharp.Plugins.csproj')

        # For reasons I don't understand, the unity generated project is named with 'Assembly' on some machines and not other
        # Problem due to unity version but for now just allow either or
        self._varMgr.set('UnityGeneratedProjectEditorPath2', '[ProjectPlatformRoot]/Assembly-CSharp-Editor-firstpass.csproj')
        self._varMgr.set('UnityGeneratedProjectPath2', '[ProjectPlatformRoot]/Assembly-CSharp-firstpass.csproj')

        self._varMgr.set('PluginsDir', '[ProjectAssetsDir]/Plugins')
        self._varMgr.set('PluginsAndroidDir', '[PluginsDir]/Android')
        self._varMgr.set('PluginsAndroidLibraryDir', '[PluginsDir]/Android/libs')
        self._varMgr.set('PluginsIosLibraryDir', '[PluginsDir]/iOS')
        self._varMgr.set('PluginsWebGlLibraryDir', '[PluginsDir]/WebGL')

        self._varMgr.set('StreamingAssetsDir', '[ProjectAssetsDir]/StreamingAssets')

        self._varMgr.set('IntermediateFilesDir', '[ProjectPlatformRoot]/obj')

        self._varMgr.set('SolutionPath', '[ProjectRoot]/[ProjectName]-[Platform].sln')

    def deleteAllLinks(self):

        self._log.heading('Deleting all junctions for all projects')

        projectNames = []

        projectsDir = self._varMgr.expandPath('[UnityProjectsDir]')

        for itemName in os.listdir(projectsDir):
            fullPath = os.path.join(projectsDir, itemName)
            if os.path.isdir(fullPath):
                projectNames.append(itemName)

        for projectName in projectNames:
            for platform in Platforms.All:
                self.setPathsForProject(projectName, platform)
                self._removeJunctionsForProjectPlatform()

    def _removePackageJunctions(self):
        self._junctionHelper.removeJunctionsInDirectory('[ProjectAssetsDir]', True)

    def _removeJunctionsForProjectPlatform(self):
        self._junctionHelper.removeJunction('[ProjectPlatformRoot]/ProjectSettings')
        self._removePackageJunctions()

    def clearAllProjectGeneratedFiles(self, addHeadings = True):
        for projName in self.getAllProjectNames():
            self.clearProjectGeneratedFiles(projName, addHeadings)

    def clearProjectGeneratedFiles(self, projectName, addHeading = True):

        if addHeading:
            self._log.heading('Clearing generated files for project {0}'.format(projectName))

        self._junctionHelper.removeJunctionsInDirectory('[UnityProjectsDir]/{0}'.format(projectName), True)

        for platform in Platforms.All:
            self.setPathsForProject(projectName, platform)

            if os.path.exists(self._varMgr.expandPath('[ProjectPlatformRoot]')):
                platformRootPath = self._varMgr.expand('[ProjectPlatformRoot]')

                try:
                    shutil.rmtree(platformRootPath)
                except:
                    self._log.warn('Unable to remove path {0}.  Trying to kill adb.exe to see if that will help...'.format(platformRootPath))
                    MiscUtil.tryKillAdbExe(self._sys)

                    try:
                        shutil.rmtree(platformRootPath)
                    except:
                        self._log.error('Still unable to remove path {0}!  A running process may have one of the files locked.  Ensure you have closed down unity / visual studio / etc.'.format(platformRootPath))
                        raise

                self._log.debug('Removed project directory {0}'.format(platformRootPath))
                self._log.good('Successfully deleted project {0} ({1})'.format(projectName, platform))
            else:
                self._log.debug('Project {0} ({1}) already deleted'.format(projectName, platform))

            # Remove the solution files and the suo files etc.
            self._sys.removeByRegex('[ProjectRoot]/[ProjectName]-[Platform].*')

    def _initNewProjectForPlatform(self, projectName, platform):

        self._log.heading('Initializing new project {0} ({1})'.format(projectName, platform))

        schema = self._schemaLoader.loadSchema(projectName, platform)

        self.setPathsForProject(projectName, platform)

        if not self._sys.directoryExists('[ProjectRoot]/ProjectSettings'):
            self._sys.createDirectory('[ProjectRoot]/ProjectSettings')

        if self._sys.directoryExists('[ProjectPlatformRoot]'):
            raise Exception('Unable to create project "{0}". Directory already exists at path "{1}".'.format(projectName, self._varMgr.expandPath('[ProjectPlatformRoot]')))

        try:
            self._sys.createDirectory('[ProjectPlatformRoot]')

            self._log.debug('Created directory "{0}"'.format(self._varMgr.expandPath('[ProjectPlatformRoot]')))

            self._junctionHelper.makeJunction('[ProjectRoot]/ProjectSettings', '[ProjectPlatformRoot]/ProjectSettings')

            self._updateDirLinksForSchema(schema)

            for handler in self._projectInitHandlers:
                handler.onProjectInit(projectName, platform)

        except:
            self._log.endHeading()
            self._log.error("Failed to initialize project '{0}' for platform '{1}'.".format(schema.name, platform))
            raise

        self._log.good('Finished creating new project "{0}" ({1})'.format(schema.name, platform))