예제 #1
0
class CreepClient(Client, cmd.Cmd):
    """Creep Mod Package Manager Client"""

    # Version of this client
    VERSION = '0.1.2'

    # Absolute path where this client is installed
    installdir = ''

    # Absolute path to the dotfile for this client
    appdir = ''

    # Absolute path to the minecraft dir
    minecraftdir = ''

    # Version of minecraft to target for mods
    minecraft_target = '1.7.10'

    def __init__(self, **kwargs):
        """Constructor"""
        cmd.Cmd.__init__(self)
        Client.__init__(self, **kwargs)

        self.updateVersionWithGitDescribe()
        self.updatePaths()
        self.load_target()

        self.createRepository()

    def do_version(self, args):
        """Display creep version"""
        print self.colortext("Creep v{}".format(self.VERSION), self.terminal.C_GREEN)
        self.display_target()

    def do_target(self, args):
        """Set the targeted minecraft version
Usage: creep target <minecraft_version>

Examples:
  creep target 1.7.10
     Set the version of minecraft for mods
"""
        if len(args) > 0:
            # TODO: Validate against ~/.minecraft/launcher_profiles.json for valid versions to use
            self.minecraft_target = args

        self.display_target()
        self.repository.set_minecraft_target(self.minecraft_target)
        self.save_options()

    def display_target(self):
        print self.colortext("Targetting minecraft version {}".format(self.minecraft_target), self.terminal.C_GREEN)

    def load_target(self):
        # TODO: More robust user options file handling. It should be its own object to load options
        options_path = self.appdir + os.sep + 'options.json'
        if os.path.isfile(options_path):
            options = json.load(open(options_path))
            self.minecraft_target = options['minecraft_target']

    def save_options(self):
        options_path = self.appdir + os.sep + 'options.json'
        with open(options_path, 'w') as outfile:
            json.dump({'minecraft_target': self.minecraft_target}, outfile)

    def do_list(self, args):
        """List packages (mods)
Usage: creep list [installed]

Examples:
  creep list
     List available packages in repository

  creep list installed
     List installed packages
"""
        if args == 'installed':
            installdir = self.minecraftdir + os.sep + 'mods' + os.sep + self.minecraft_target
            files = []
            try:
                files = os.listdir(installdir)
            except OSError:
                pass

            if not files:
                print self.colortext("Looking in {}".format(installdir), self.terminal.C_YELLOW)
                print "No mods installed"
                return False

            packages = []
            unknownfiles = []
            for name in files:
                package = self.repository.fetch_package_byfilename(name)
                if not package:
                    unknownfiles.append(name)
                else:
                    packages.append(package)

            packages.sort(key=attrgetter('name'))

            print self.colortext("Installed mods (in {}):".format(installdir), self.terminal.C_YELLOW)
            for package in packages:
                self.print_package(package)
            for name in unknownfiles:
                print self.colortext(name, self.terminal.C_RED)
        else:
            self.display_packages()

    def display_packages(self):
        """Display list of packages available"""
        for package in self.repository.unique_packages:
            self.print_package(package)

    def print_package(self, package):
        message = '{name} {version} - {description} [{mcversion}]'.format(
            name=package.name,
            version=self.colortext('(' + package.version + ')', self.terminal.C_YELLOW),
            description=self.colortext(package.description, self.terminal.C_MAGENTA),
            mcversion=self.colortext(package.get_minecraft_version(), self.terminal.C_YELLOW)
        )

        if package.type == 'collection':
            message = message + self.colortext(' [collection]', self.terminal.C_CYAN)

        print message

    def print_package_details(self, package):
        print package.name
        print '--------'
        print "Version: " + package.version
        print "Description: " + package.description
        print "Package Type: " + package.type
        print "Keywords: " + package.keywords
        print "Homepage: " + package.homepage
        print "Dependencies: "
        for key,value in package.require.iteritems():
            print " - " + key + ": " + value

    def do_search(self, args):
        """Search for a package (mod)
Usage: creep search <packagename>

Example: creep search nei
"""
        if args == '':
            return False

        packages = self.repository.search(args)
        for package in packages:
            self.print_package(package)

    def do_info(self, args):
        """Display details for a specific package (mod)
Usage: creep info <packagename>

Example: creep info slimeknights/tconstruct
"""
        if len(args) == 0:
            print self.colortext("Missing argument", self.terminal.C_RED)
            return 1

        package = self.repository.fetch_package(args)
        if not package:
            print self.colortext("Unknown package '{}'".format(args), self.terminal.C_RED)
            return 1

        self.print_package_details(package)

    def do_install(self, args):
        """Install a package (mod)
Usage: creep install <packagename>
       creep install -l <listfilename>

Example: creep install thecricket/chisel2
         creep install -l mymodlist.txt

<listfilename> is a list of packages to install consisting of one package name per line
"""
        args = shlex.split(args)

        if len(args) == 0:
            print self.colortext("Missing argument", self.terminal.C_RED)
            return 1

        if args[0] == '-l':
            # Handle install from listfile
            try:
                listfile = args[1]
            except IndexError:
                print self.colortext("Missing argument for listfile", self.terminal.C_RED)
                return 1
            self.install_from_listfile(listfile)
        else:
            # Handle install individual package
            self.install_package(args[0])

    def install_package(self, args):
        package = self.repository.fetch_package(args)
        if not package:
            print self.colortext("Unknown package '{}'".format(args), self.terminal.C_RED)
            return 1

        print self.colortext("Installing package {}".format(package), self.terminal.C_BLUE)

        # Install any required packages
        # Warning: does not handle versions or circular dependencies
        for dependency in package.require:
            if dependency == 'minecraft' or dependency == 'forge':
                continue
            print self.colortext("Installing dependency '{}'".format(dependency), self.terminal.C_CYAN)
            self.do_install(dependency)

        if package.type == 'collection':
            # Collection only has dependencies
            print self.colortext("Installed collection '{0}'".format(package.name), self.terminal.C_GREEN)
        else:
            cachedir = self.appdir + os.sep + 'cache' + os.sep + package.installdir

            if not os.path.isdir(cachedir):
                os.mkdir(cachedir)

            if not os.path.isfile(cachedir + os.sep + package.get_local_filename()):
                print self.colortext("Downloading mod '{0}' from {1}".format(package.name, package.get_download_location()), self.terminal.C_YELLOW)
                downloadResult = package.download(cachedir)

                if not downloadResult:
                    print "Download failed."
                    return False

            # Most of the time this is the '~/.minecraft/mods' dir, but some mods have an alternate location for artifacts
            savedir = self.minecraftdir + os.sep + package.installdir

            # Tack on the minecraft version to the savedir
            if package.installdir == 'mods':
                savedir = savedir + os.sep + package.require['minecraft']

            if not os.path.isdir(savedir):
                print self.colortext("Creating directory '{0}'".format(savedir))
                os.mkdir(savedir)

            if package.installstrategy:
                self.install_with_strategy(package.installstrategy, package, cachedir, savedir)

            shutil.copyfile(cachedir + os.sep + package.get_local_filename(), savedir + os.sep + package.get_local_filename())

            print self.colortext("Installed mod '{0}' in '{1}'".format(package.name, savedir + os.sep + package.get_local_filename()), self.terminal.C_GREEN)

    def install_from_listfile(self, listfile):
        print "Reading packages from file '{}'...".format(listfile)

        if not os.path.isfile(listfile):
            print self.colortext("File '{}' not found".format(listfile), self.terminal.C_RED)
            return 1

        # Read file and attempt to parse each line as a package name
        with open(listfile) as fp:
            for line in fp:
                args = line.split()
                self.install_package(args[0])

    def install_with_strategy(self, installstrategy, package, cachedir, savedir):

        print "Installing with strategy: " + installstrategy

        if ';' in installstrategy:
            strategies = installstrategy.split(';')
        else:
            strategies = [installstrategy]

        # set up a temppath where we will work
        tmppath = tempfile.gettempdir() + os.sep + package.name.replace('/', '_')
        if os.path.exists(tmppath):
            shutil.rmtree(tmppath)
        os.mkdir(tmppath)

        for strategy in strategies:
            args = shlex.split(strategy)
            if args[0] == 'unzip':
                print 'Unzipping archive: ' + cachedir + os.sep + package.get_local_filename()
                self.unzip(cachedir + os.sep + package.get_local_filename(), tmppath)
            elif args[0] == 'move':
                print 'Moving files: ' + args[1]
                path = args[1]
                if path[-2:] == '/*':
                    path = path.replace('/*', '')
                    distutils.dir_util.copy_tree(tmppath + os.sep + path, savedir)
                else:
                    shutil.copytree(tmppath + os.sep + path, savedir)

    def do_uninstall(self, args):
        """Uninstall a package (mod)
Usage: creep uninstall <packagename>

Example: creep uninstall thecricket/chisel2
"""
        package = self.repository.fetch_package(args)
        if not package:
            print 'Unknown package {}'.format(args)
            return 1

        if package.installdir == 'mods':
            savedir = self.minecraftdir + os.sep + package.installdir + os.sep + self.minecraft_target
        else:
            savedir = self.minecraftdir + os.sep + package.installdir

        os.remove(savedir + os.sep + package.get_local_filename())
        print "Removed mod '{0}' from '{1}'".format(package.name, savedir)

    def do_purge(self, args):
        """Purge all installed packages (mods)
Usage: creep purge
"""
        installdir = self.minecraftdir + os.sep + 'mods' + os.sep + self.minecraft_target
        print "Purging all installed mods in {}...".format(installdir)
        self.delete_path(installdir)
        print "Done."

    def do_refresh(self, args):
        """Force an refresh of the package repository"""

        self.repository.clear_cache()
        self.repository.populate()
        print self.colortext("Repository updated to version {} ({}).".format(self.repository.version_hash, self.repository.version_date), self.terminal.C_GREEN)
        print "Count: {} packages.".format(self.repository.count_packages())

    def delete_path(self, rootdir):
        files = os.listdir(rootdir)
        for f in files:
            if os.path.isdir(rootdir + os.sep + f):
                self.delete_path(rootdir + os.sep + f)
                os.rmdir(rootdir + os.sep + f)
            else:
                print self.colortext('Removing file {}'.format(f), self.terminal.C_RED)
                try:
                    os.remove(rootdir + os.sep + f)
                except OSError:
                    print 'opa'
                    continue

    def createRepository(self):
        self.repository = Repository(self.appdir)
        self.repository.set_minecraft_target(self.minecraft_target)

        # Check if local packages repository exists and load it too
        if os.path.isfile(self.appdir + os.sep + 'local-packages.json'):
            self.repository.populate('', False)
            self.repository.populate(self.appdir + os.sep + 'local-packages.json')
        else:
            self.repository.populate('', True)

    def updatePaths(self):
        self.installdir = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))

        self.appdir = self.getHomePath('.creep')
        if not os.path.isdir(self.appdir):
            os.mkdir(self.appdir)

        if not os.path.isdir(self.appdir + os.sep + 'cache'):
            os.mkdir(self.appdir + os.sep + 'cache')

        if sys.platform[:3] == 'win':
            self.minecraftdir = self.getHomePath('AppData\\Roaming\\.minecraft')
        elif sys.platform == 'darwin':
            self.minecraftdir = self.getHomePath('Library/Application Support/minecraft')
        elif sys.platform == 'cygwin':
            self.minecraftdir = os.getenv('APPDATA') + os.sep + '.minecraft'
        else:
            self.minecraftdir = self.getHomePath('.minecraft')

        if not os.path.isdir(self.minecraftdir):
            print "Minecraft dir not found ({})".format(self.minecraftdir)
            print "Is Minecraft installed?"
            sys.exit(2)

        if not os.path.isdir(self.minecraftdir + os.sep + 'mods'):
            os.mkdir(self.minecraftdir + os.sep + 'mods')

    def getHomePath(self, path=''):
        """Get the home path for this user from the OS"""
        home = os.getenv('HOME')
        if home == None:
            home = os.getenv('USERPROFILE')

        return home + os.sep + path

    def updateVersionWithGitDescribe(self):
        """Update the version of this client to reflect any local changes in git"""

        appdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))

        try:
            self.VERSION = subprocess.check_output(['git', '-C', appdir, 'describe'], stderr=subprocess.STDOUT).strip()
        except OSError:
            pass
        except subprocess.CalledProcessError:
            # Oh well, we tried, just use the VERSION as it was
            pass

    def colortext(self, text, forecolor=None, backcolor=None, isbold=None):

        if forecolor is None and backcolor is None and isbold is None:
            return text

        if isbold:
            boldstart = self.terminal.bold()
            boldend = self.terminal.sgr0()
        else:
            boldstart = ''
            boldend = ''

        return '{bold}{start}{text}{end}{unbold}'.format(
            bold=boldstart,
            start=self.colorstart(forecolor, backcolor),
            text=text,
            end=self.colorend(),
            unbold=boldend
        )

    def colorstart(self, forecolor=None, backcolor=None):
        coloring = ''
        if forecolor is not None:
            coloring += self.terminal.setaf(forecolor)
        if backcolor is not None:
            coloring += self.terminal.setab(backcolor)

        return coloring

    def colorend(self):
        return self.terminal.op()

    def unzip(self, source_filename, dest_dir):
        with zipfile.ZipFile(source_filename) as zf:
            zf.extractall(dest_dir)