Files
Iristyle a000ce8acc feat(ST2.UtilPackages): bump up all packages
- Refresh PackageCache with latest versions of everything
2013-09-16 22:35:46 -04:00

1025 lines
41 KiB
Python

import sys
import os
import re
import socket
import json
import time
import zipfile
import shutil
from fnmatch import fnmatch
import datetime
import tempfile
import locale
try:
# Python 3
from urllib.parse import urlencode, urlparse
import compileall
str_cls = str
except (ImportError):
# Python 2
from urllib import urlencode
from urlparse import urlparse
str_cls = unicode
import sublime
from .show_error import show_error
from .console_write import console_write
from .open_compat import open_compat, read_compat
from .unicode import unicode_from_os
from .clear_directory import clear_directory
from .cache import (clear_cache, set_cache, get_cache, merge_cache_under_settings,
merge_cache_over_settings, set_cache_under_settings, set_cache_over_settings)
from .versions import version_comparable, version_sort
from .downloaders.background_downloader import BackgroundDownloader
from .downloaders.downloader_exception import DownloaderException
from .providers.provider_exception import ProviderException
from .clients.client_exception import ClientException
from .download_manager import downloader
from .providers.channel_provider import ChannelProvider
from .upgraders.git_upgrader import GitUpgrader
from .upgraders.hg_upgrader import HgUpgrader
from .package_io import read_package_file
from .providers import CHANNEL_PROVIDERS, REPOSITORY_PROVIDERS
from . import __version__
class PackageManager():
"""
Allows downloading, creating, installing, upgrading, and deleting packages
Delegates metadata retrieval to the CHANNEL_PROVIDERS classes.
Uses VcsUpgrader-based classes for handling git and hg repositories in the
Packages folder. Downloader classes are utilized to fetch contents of URLs.
Also handles displaying package messaging, and sending usage information to
the usage server.
"""
def __init__(self):
# Here we manually copy the settings since sublime doesn't like
# code accessing settings from threads
self.settings = {}
settings = sublime.load_settings('Package Control.sublime-settings')
for setting in ['timeout', 'repositories', 'channels',
'package_name_map', 'dirs_to_ignore', 'files_to_ignore',
'package_destination', 'cache_length', 'auto_upgrade',
'files_to_ignore_binary', 'files_to_keep', 'dirs_to_keep',
'git_binary', 'git_update_command', 'hg_binary',
'hg_update_command', 'http_proxy', 'https_proxy',
'auto_upgrade_ignore', 'auto_upgrade_frequency',
'submit_usage', 'submit_url', 'renamed_packages',
'files_to_include', 'files_to_include_binary', 'certs',
'ignore_vcs_packages', 'proxy_username', 'proxy_password',
'debug', 'user_agent', 'http_cache', 'http_cache_length',
'install_prereleases', 'openssl_binary']:
if settings.get(setting) == None:
continue
self.settings[setting] = settings.get(setting)
# https_proxy will inherit from http_proxy unless it is set to a
# string value or false
no_https_proxy = self.settings.get('https_proxy') in ["", None]
if no_https_proxy and self.settings.get('http_proxy'):
self.settings['https_proxy'] = self.settings.get('http_proxy')
if self.settings.get('https_proxy') == False:
self.settings['https_proxy'] = ''
self.settings['platform'] = sublime.platform()
self.settings['version'] = sublime.version()
# Use the cache to see if settings have changed since the last
# time the package manager was created, and clearing any cached
# values if they have.
previous_settings = get_cache('filtered_settings', {})
# Reduce the settings down to exclude channel info since that will
# make the settings always different
filtered_settings = self.settings.copy()
for key in ['repositories', 'channels', 'package_name_map', 'cache']:
if key in filtered_settings:
del filtered_settings[key]
if filtered_settings != previous_settings and previous_settings != {}:
console_write(u'Settings change detected, clearing cache', True)
clear_cache()
set_cache('filtered_settings', filtered_settings)
def get_metadata(self, package):
"""
Returns the package metadata for an installed package
:return:
A dict with the keys:
version
url
description
or an empty dict on error
"""
try:
debug = self.settings.get('debug')
metadata_json = read_package_file(package, 'package-metadata.json', debug=debug)
if metadata_json:
return json.loads(metadata_json)
except (IOError, ValueError) as e:
pass
return {}
def list_repositories(self):
"""
Returns a master list of all repositories pulled from all sources
These repositories come from the channels specified in the
"channels" setting, plus any repositories listed in the
"repositories" setting.
:return:
A list of all available repositories
"""
cache_ttl = self.settings.get('cache_length')
repositories = self.settings.get('repositories')[:]
channels = self.settings.get('channels')
for channel in channels:
channel = channel.strip()
# Caches various info from channels for performance
cache_key = channel + '.repositories'
channel_repositories = get_cache(cache_key)
merge_cache_under_settings(self, 'package_name_map', channel)
merge_cache_under_settings(self, 'renamed_packages', channel)
merge_cache_under_settings(self, 'unavailable_packages', channel, list_=True)
# If any of the info was not retrieved from the cache, we need to
# grab the channel to get it
if channel_repositories == None:
for provider_class in CHANNEL_PROVIDERS:
if provider_class.match_url(channel):
provider = provider_class(channel, self.settings)
break
try:
channel_repositories = provider.get_repositories()
set_cache(cache_key, channel_repositories, cache_ttl)
for repo in channel_repositories:
repo_packages = provider.get_packages(repo)
packages_cache_key = repo + '.packages'
set_cache(packages_cache_key, repo_packages, cache_ttl)
# Have the local name map override the one from the channel
name_map = provider.get_name_map()
set_cache_under_settings(self, 'package_name_map', channel, name_map, cache_ttl)
renamed_packages = provider.get_renamed_packages()
set_cache_under_settings(self, 'renamed_packages', channel, renamed_packages, cache_ttl)
unavailable_packages = provider.get_unavailable_packages()
set_cache_under_settings(self, 'unavailable_packages', channel, unavailable_packages, cache_ttl, list_=True)
provider_certs = provider.get_certs()
certs = self.settings.get('certs', {}).copy()
certs.update(provider_certs)
# Save the master list of certs, used by downloaders/cert_provider.py
set_cache('*.certs', certs, cache_ttl)
except (DownloaderException, ClientException, ProviderException) as e:
console_write(e, True)
continue
repositories.extend(channel_repositories)
return [repo.strip() for repo in repositories]
def list_available_packages(self):
"""
Returns a master list of every available package from all sources
:return:
A dict in the format:
{
'Package Name': {
# Package details - see example-packages.json for format
},
...
}
"""
if self.settings.get('debug'):
console_write(u"Fetching list of available packages", True)
console_write(u" Platform: %s-%s" % (sublime.platform(),sublime.arch()))
console_write(u" Sublime Text Version: %s" % sublime.version())
console_write(u" Package Control Version: %s" % __version__)
cache_ttl = self.settings.get('cache_length')
repositories = self.list_repositories()
packages = {}
bg_downloaders = {}
active = []
repos_to_download = []
name_map = self.settings.get('package_name_map', {})
# Repositories are run in reverse order so that the ones first
# on the list will overwrite those last on the list
for repo in repositories[::-1]:
cache_key = repo + '.packages'
repository_packages = get_cache(cache_key)
if repository_packages != None:
packages.update(repository_packages)
else:
domain = urlparse(repo).hostname
if domain not in bg_downloaders:
bg_downloaders[domain] = BackgroundDownloader(
self.settings, REPOSITORY_PROVIDERS)
bg_downloaders[domain].add_url(repo)
repos_to_download.append(repo)
for bg_downloader in list(bg_downloaders.values()):
bg_downloader.start()
active.append(bg_downloader)
# Wait for all of the downloaders to finish
while active:
bg_downloader = active.pop()
bg_downloader.join()
# Grabs the results and stuff it all in the cache
for repo in repos_to_download:
domain = urlparse(repo).hostname
bg_downloader = bg_downloaders[domain]
provider = bg_downloader.get_provider(repo)
# Allow name mapping of packages for schema version < 2.0
repository_packages = {}
for name, info in provider.get_packages():
name = name_map.get(name, name)
info['name'] = name
repository_packages[name] = info
# Display errors we encountered while fetching package info
for url, exception in provider.get_failed_sources():
console_write(exception, True)
for name, exception in provider.get_broken_packages():
console_write(exception, True)
cache_key = repo + '.packages'
set_cache(cache_key, repository_packages, cache_ttl)
packages.update(repository_packages)
renamed_packages = provider.get_renamed_packages()
set_cache_under_settings(self, 'renamed_packages', repo, renamed_packages, cache_ttl)
unavailable_packages = provider.get_unavailable_packages()
set_cache_under_settings(self, 'unavailable_packages', repo, unavailable_packages, cache_ttl, list_=True)
return packages
def list_packages(self, unpacked_only=False):
"""
:param unpacked_only:
Only list packages that are not inside of .sublime-package files
:return: A list of all installed, non-default, package names
"""
package_names = os.listdir(sublime.packages_path())
package_names = [path for path in package_names if
os.path.isdir(os.path.join(sublime.packages_path(), path))]
if int(sublime.version()) > 3000 and unpacked_only == False:
package_files = os.listdir(sublime.installed_packages_path())
package_names += [f.replace('.sublime-package', '') for f in package_files if re.search('\.sublime-package$', f) != None]
# Ignore things to be deleted
ignored = ['User']
for package in package_names:
cleanup_file = os.path.join(sublime.packages_path(), package,
'package-control.cleanup')
if os.path.exists(cleanup_file):
ignored.append(package)
packages = list(set(package_names) - set(ignored) -
set(self.list_default_packages()))
packages = sorted(packages, key=lambda s: s.lower())
return packages
def list_all_packages(self):
""" :return: A list of all installed package names, including default packages"""
packages = self.list_default_packages() + self.list_packages()
packages = sorted(packages, key=lambda s: s.lower())
return packages
def list_default_packages(self):
""" :return: A list of all default package names"""
if int(sublime.version()) > 3000:
bundled_packages_path = os.path.join(os.path.dirname(sublime.executable_path()),
'Packages')
files = os.listdir(bundled_packages_path)
else:
files = os.listdir(os.path.join(os.path.dirname(
sublime.packages_path()), 'Pristine Packages'))
files = list(set(files) - set(os.listdir(
sublime.installed_packages_path())))
packages = [file.replace('.sublime-package', '') for file in files]
packages = sorted(packages, key=lambda s: s.lower())
return packages
def get_package_dir(self, package):
""":return: The full filesystem path to the package directory"""
return os.path.join(sublime.packages_path(), package)
def get_mapped_name(self, package):
""":return: The name of the package after passing through mapping rules"""
return self.settings.get('package_name_map', {}).get(package, package)
def create_package(self, package_name, package_destination,
binary_package=False):
"""
Creates a .sublime-package file from the running Packages directory
:param package_name:
The package to create a .sublime-package file for
:param package_destination:
The full filesystem path of the directory to save the new
.sublime-package file in.
:param binary_package:
If the created package should follow the binary package include/
exclude patterns from the settings. These normally include a setup
to exclude .py files and include .pyc files, but that can be
changed via settings.
:return: bool if the package file was successfully created
"""
package_dir = self.get_package_dir(package_name)
if not os.path.exists(package_dir):
show_error(u'The folder for the package name specified, %s, does not exist in %s' % (
package_name, sublime.packages_path()))
return False
package_filename = package_name + '.sublime-package'
package_path = os.path.join(package_destination,
package_filename)
if not os.path.exists(sublime.installed_packages_path()):
os.mkdir(sublime.installed_packages_path())
if os.path.exists(package_path):
os.remove(package_path)
try:
package_file = zipfile.ZipFile(package_path, "w",
compression=zipfile.ZIP_DEFLATED)
except (OSError, IOError) as e:
show_error(u'An error occurred creating the package file %s in %s.\n\n%s' % (
package_filename, package_destination, unicode_from_os(e)))
return False
if int(sublime.version()) >= 3000:
compileall.compile_dir(package_dir, quiet=True, legacy=True, optimize=2)
dirs_to_ignore = self.settings.get('dirs_to_ignore', [])
if not binary_package:
files_to_ignore = self.settings.get('files_to_ignore', [])
files_to_include = self.settings.get('files_to_include', [])
else:
files_to_ignore = self.settings.get('files_to_ignore_binary', [])
files_to_include = self.settings.get('files_to_include_binary', [])
slash = '\\' if os.name == 'nt' else '/'
trailing_package_dir = package_dir + slash if package_dir[-1] != slash else package_dir
package_dir_regex = re.compile('^' + re.escape(trailing_package_dir))
for root, dirs, files in os.walk(package_dir):
[dirs.remove(dir_) for dir_ in dirs if dir_ in dirs_to_ignore]
paths = dirs
paths.extend(files)
for path in paths:
full_path = os.path.join(root, path)
relative_path = re.sub(package_dir_regex, '', full_path)
ignore_matches = [fnmatch(relative_path, p) for p in files_to_ignore]
include_matches = [fnmatch(relative_path, p) for p in files_to_include]
if any(ignore_matches) and not any(include_matches):
continue
if os.path.isdir(full_path):
continue
package_file.write(full_path, relative_path)
package_file.close()
return True
def install_package(self, package_name):
"""
Downloads and installs (or upgrades) a package
Uses the self.list_available_packages() method to determine where to
retrieve the package file from.
The install process consists of:
1. Finding the package
2. Downloading the .sublime-package/.zip file
3. Extracting the package file
4. Showing install/upgrade messaging
5. Submitting usage info
6. Recording that the package is installed
:param package_name:
The package to download and install
:return: bool if the package was successfully installed
"""
packages = self.list_available_packages()
is_available = package_name in list(packages.keys())
is_unavailable = package_name in self.settings.get('unavailable_packages', [])
if is_unavailable and not is_available:
console_write(u'The package "%s" is not available on this platform.' % package_name, True)
return False
if not is_available:
show_error(u'The package specified, %s, is not available' % package_name)
return False
url = packages[package_name]['download']['url']
package_filename = package_name + '.sublime-package'
tmp_dir = tempfile.mkdtemp()
try:
# This is refers to the zipfile later on, so we define it here so we can
# close the zip file if set during the finally clause
package_zip = None
tmp_package_path = os.path.join(tmp_dir, package_filename)
unpacked_package_dir = self.get_package_dir(package_name)
package_path = os.path.join(sublime.installed_packages_path(),
package_filename)
pristine_package_path = os.path.join(os.path.dirname(
sublime.packages_path()), 'Pristine Packages', package_filename)
if os.path.exists(os.path.join(unpacked_package_dir, '.git')):
if self.settings.get('ignore_vcs_packages'):
show_error(u'Skipping git package %s since the setting ignore_vcs_packages is set to true' % package_name)
return False
return GitUpgrader(self.settings['git_binary'],
self.settings['git_update_command'], unpacked_package_dir,
self.settings['cache_length'], self.settings['debug']).run()
elif os.path.exists(os.path.join(unpacked_package_dir, '.hg')):
if self.settings.get('ignore_vcs_packages'):
show_error(u'Skipping hg package %s since the setting ignore_vcs_packages is set to true' % package_name)
return False
return HgUpgrader(self.settings['hg_binary'],
self.settings['hg_update_command'], unpacked_package_dir,
self.settings['cache_length'], self.settings['debug']).run()
old_version = self.get_metadata(package_name).get('version')
is_upgrade = old_version != None
# Download the sublime-package or zip file
try:
with downloader(url, self.settings) as manager:
package_bytes = manager.fetch(url, 'Error downloading package.')
except (DownloaderException) as e:
console_write(e, True)
show_error(u'Unable to download %s. Please view the console for more details.' % package_name)
return False
with open_compat(tmp_package_path, "wb") as package_file:
package_file.write(package_bytes)
# Try to open it as a zip file
try:
package_zip = zipfile.ZipFile(tmp_package_path, 'r')
except (zipfile.BadZipfile):
show_error(u'An error occurred while trying to unzip the package file for %s. Please try installing the package again.' % package_name)
return False
# Scan through the root level of the zip file to gather some info
root_level_paths = []
last_path = None
for path in package_zip.namelist():
try:
if not isinstance(path, str_cls):
path = path.decode('utf-8', 'strict')
except (UnicodeDecodeError):
console_write(u'One or more of the zip file entries in %s is not encoded using UTF-8, aborting' % package_name, True)
return False
last_path = path
if path.find('/') in [len(path) - 1, -1]:
root_level_paths.append(path)
# Make sure there are no paths that look like security vulnerabilities
if path[0] == '/' or path.find('../') != -1 or path.find('..\\') != -1:
show_error(u'The package specified, %s, contains files outside of the package dir and cannot be safely installed.' % package_name)
return False
if last_path and len(root_level_paths) == 0:
root_level_paths.append(last_path[0:last_path.find('/') + 1])
# If there is only a single directory at the top leve, the file
# is most likely a zip from BitBucket or GitHub and we need
# to skip the top-level dir when extracting
skip_root_dir = len(root_level_paths) == 1 and \
root_level_paths[0].endswith('/')
no_package_file_zip_path = '.no-sublime-package'
if skip_root_dir:
no_package_file_zip_path = root_level_paths[0] + no_package_file_zip_path
# If we should extract unpacked or as a .sublime-package file
unpack = True
# By default, ST3 prefers .sublime-package files since this allows
# overriding files in the Packages/{package_name}/ folder
if int(sublime.version()) >= 3000:
unpack = False
# If the package maintainer doesn't want a .sublime-package
try:
package_zip.getinfo(no_package_file_zip_path)
unpack = True
except (KeyError):
pass
# If we already have a package-metadata.json file in
# Packages/{package_name}/, the only way to successfully upgrade
# will be to unpack
unpacked_metadata_file = os.path.join(unpacked_package_dir,
'package-metadata.json')
if os.path.exists(unpacked_metadata_file):
unpack = True
# If we determined it should be unpacked, we extract directly
# into the Packages/{package_name}/ folder
if unpack:
self.backup_package_dir(package_name)
package_dir = unpacked_package_dir
# Otherwise we go into a temp dir since we will be creating a
# new .sublime-package file later
else:
tmp_working_dir = os.path.join(tmp_dir, 'working')
os.mkdir(tmp_working_dir)
package_dir = tmp_working_dir
package_metadata_file = os.path.join(package_dir,
'package-metadata.json')
if not os.path.exists(package_dir):
os.mkdir(package_dir)
os.chdir(package_dir)
# Here we don't use .extractall() since it was having issues on OS X
overwrite_failed = False
extracted_paths = []
for path in package_zip.namelist():
dest = path
try:
if not isinstance(dest, str_cls):
dest = dest.decode('utf-8', 'strict')
except (UnicodeDecodeError):
console_write(u'One or more of the zip file entries in %s is not encoded using UTF-8, aborting' % package_name, True)
return False
if os.name == 'nt':
regex = ':|\*|\?|"|<|>|\|'
if re.search(regex, dest) != None:
console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True)
continue
# If there was only a single directory in the package, we remove
# that folder name from the paths as we extract entries
if skip_root_dir:
dest = dest[len(root_level_paths[0]):]
if os.name == 'nt':
dest = dest.replace('/', '\\')
else:
dest = dest.replace('\\', '/')
dest = os.path.join(package_dir, dest)
def add_extracted_dirs(dir_):
while dir_ not in extracted_paths:
extracted_paths.append(dir_)
dir_ = os.path.dirname(dir_)
if dir_ == package_dir:
break
if path.endswith('/'):
if not os.path.exists(dest):
os.makedirs(dest)
add_extracted_dirs(dest)
else:
dest_dir = os.path.dirname(dest)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
add_extracted_dirs(dest_dir)
extracted_paths.append(dest)
try:
open_compat(dest, 'wb').write(package_zip.read(path))
except (IOError) as e:
message = unicode_from_os(e)
if re.search('[Ee]rrno 13', message):
overwrite_failed = True
break
console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True)
except (UnicodeDecodeError):
console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True)
package_zip.close()
package_zip = None
# If upgrading failed, queue the package to upgrade upon next start
if overwrite_failed:
reinstall_file = os.path.join(package_dir, 'package-control.reinstall')
open_compat(reinstall_file, 'w').close()
# Don't delete the metadata file, that way we have it
# when the reinstall happens, and the appropriate
# usage info can be sent back to the server
clear_directory(package_dir, [reinstall_file, package_metadata_file])
show_error(u'An error occurred while trying to upgrade %s. Please restart Sublime Text to finish the upgrade.' % package_name)
return False
# Here we clean out any files that were not just overwritten. It is ok
# if there is an error removing a file. The next time there is an
# upgrade, it should be cleaned out successfully then.
clear_directory(package_dir, extracted_paths)
self.print_messages(package_name, package_dir, is_upgrade, old_version)
with open_compat(package_metadata_file, 'w') as f:
metadata = {
"version": packages[package_name]['download']['version'],
"url": packages[package_name]['homepage'],
"description": packages[package_name]['description']
}
json.dump(metadata, f)
# Submit install and upgrade info
if is_upgrade:
params = {
'package': package_name,
'operation': 'upgrade',
'version': packages[package_name]['download']['version'],
'old_version': old_version
}
else:
params = {
'package': package_name,
'operation': 'install',
'version': packages[package_name]['download']['version']
}
self.record_usage(params)
# Record the install in the settings file so that you can move
# settings across computers and have the same packages installed
def save_package():
settings = sublime.load_settings('Package Control.sublime-settings')
installed_packages = settings.get('installed_packages', [])
if not installed_packages:
installed_packages = []
installed_packages.append(package_name)
installed_packages = list(set(installed_packages))
installed_packages = sorted(installed_packages,
key=lambda s: s.lower())
settings.set('installed_packages', installed_packages)
sublime.save_settings('Package Control.sublime-settings')
sublime.set_timeout(save_package, 1)
# If we didn't extract directly into the Packages/{package_name}/
# folder, we need to create a .sublime-package file and install it
if not unpack:
try:
# Remove the downloaded file since we are going to overwrite it
os.remove(tmp_package_path)
package_zip = zipfile.ZipFile(tmp_package_path, "w",
compression=zipfile.ZIP_DEFLATED)
except (OSError, IOError) as e:
show_error(u'An error occurred creating the package file %s in %s.\n\n%s' % (
package_filename, tmp_dir, unicode_from_os(e)))
return False
package_dir_regex = re.compile('^' + re.escape(package_dir))
for root, dirs, files in os.walk(package_dir):
paths = dirs
paths.extend(files)
for path in paths:
full_path = os.path.join(root, path)
relative_path = re.sub(package_dir_regex, '', full_path)
if os.path.isdir(full_path):
continue
package_zip.write(full_path, relative_path)
package_zip.close()
package_zip = None
if os.path.exists(package_path):
os.remove(package_path)
shutil.move(tmp_package_path, package_path)
# We have to remove the pristine package too or else Sublime Text 2
# will silently delete the package
if os.path.exists(pristine_package_path):
os.remove(pristine_package_path)
os.chdir(sublime.packages_path())
return True
finally:
# We need to make sure the zipfile is closed to
# help prevent permissions errors on Windows
if package_zip:
package_zip.close()
# Try to remove the tmp dir after a second to make sure
# a virus scanner is holding a reference to the zipfile
# after we close it.
def remove_tmp_dir():
try:
shutil.rmtree(tmp_dir)
except (PermissionError):
# If we can't remove the tmp dir, don't let an uncaught exception
# fall through and break the install process
pass
sublime.set_timeout(remove_tmp_dir, 1000)
def backup_package_dir(self, package_name):
"""
Does a full backup of the Packages/{package}/ dir to Backup/
:param package_name:
The name of the package to back up
:return:
If the backup succeeded
"""
package_dir = os.path.join(sublime.packages_path(), package_name)
if not os.path.exists(package_dir):
return True
try:
backup_dir = os.path.join(os.path.dirname(
sublime.packages_path()), 'Backup',
datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
package_backup_dir = os.path.join(backup_dir, package_name)
if os.path.exists(package_backup_dir):
console_write(u"FOLDER %s ALREADY EXISTS!" % package_backup_dir)
shutil.copytree(package_dir, package_backup_dir)
return True
except (OSError, IOError) as e:
show_error(u'An error occurred while trying to backup the package directory for %s.\n\n%s' % (
package_name, unicode_from_os(e)))
if os.path.exists(package_backup_dir):
shutil.rmtree(package_backup_dir)
return False
def print_messages(self, package, package_dir, is_upgrade, old_version):
"""
Prints out package install and upgrade messages
The functionality provided by this allows package maintainers to
show messages to the user when a package is installed, or when
certain version upgrade occur.
:param package:
The name of the package the message is for
:param package_dir:
The full filesystem path to the package directory
:param is_upgrade:
If the install was actually an upgrade
:param old_version:
The string version of the package before the upgrade occurred
"""
messages_file = os.path.join(package_dir, 'messages.json')
if not os.path.exists(messages_file):
return
messages_fp = open_compat(messages_file, 'r')
try:
message_info = json.loads(read_compat(messages_fp))
except (ValueError):
console_write(u'Error parsing messages.json for %s' % package, True)
return
messages_fp.close()
output = ''
if not is_upgrade and message_info.get('install'):
install_messages = os.path.join(package_dir,
message_info.get('install'))
message = '\n\n%s:\n%s\n\n ' % (package,
('-' * len(package)))
with open_compat(install_messages, 'r') as f:
message += read_compat(f).replace('\n', '\n ')
output += message + '\n'
elif is_upgrade and old_version:
upgrade_messages = list(set(message_info.keys()) -
set(['install']))
upgrade_messages = version_sort(upgrade_messages, reverse=True)
old_version_cmp = version_comparable(old_version)
for version in upgrade_messages:
if version_comparable(version) <= old_version_cmp:
break
if not output:
message = '\n\n%s:\n%s\n' % (package,
('-' * len(package)))
output += message
upgrade_message_path = os.path.join(package_dir,
message_info.get(version))
message = '\n '
with open_compat(upgrade_message_path, 'r') as f:
message += read_compat(f).replace('\n', '\n ')
output += message + '\n'
if not output:
return
def print_to_panel():
window = sublime.active_window()
views = window.views()
view = None
for _view in views:
if _view.name() == 'Package Control Messages':
view = _view
break
if not view:
view = window.new_file()
view.set_name('Package Control Messages')
view.set_scratch(True)
def write(string):
view.run_command('package_message', {'string': string})
if not view.size():
view.settings().set("word_wrap", True)
write('Package Control Messages\n' +
'========================')
write(output)
sublime.set_timeout(print_to_panel, 1)
def remove_package(self, package_name):
"""
Deletes a package
The deletion process consists of:
1. Deleting the directory (or marking it for deletion if deletion fails)
2. Submitting usage info
3. Removing the package from the list of installed packages
:param package_name:
The package to delete
:return: bool if the package was successfully deleted
"""
installed_packages = self.list_packages()
if package_name not in installed_packages:
show_error(u'The package specified, %s, is not installed' % package_name)
return False
os.chdir(sublime.packages_path())
# Give Sublime Text some time to ignore the package
time.sleep(1)
package_filename = package_name + '.sublime-package'
installed_package_path = os.path.join(sublime.installed_packages_path(),
package_filename)
pristine_package_path = os.path.join(os.path.dirname(
sublime.packages_path()), 'Pristine Packages', package_filename)
package_dir = self.get_package_dir(package_name)
version = self.get_metadata(package_name).get('version')
try:
if os.path.exists(installed_package_path):
os.remove(installed_package_path)
except (OSError, IOError) as e:
show_error(u'An error occurred while trying to remove the installed package file for %s.\n\n%s' % (
package_name, unicode_from_os(e)))
return False
try:
if os.path.exists(pristine_package_path):
os.remove(pristine_package_path)
except (OSError, IOError) as e:
show_error(u'An error occurred while trying to remove the pristine package file for %s.\n\n%s' % (
package_name, unicode_from_os(e)))
return False
# We don't delete the actual package dir immediately due to a bug
# in sublime_plugin.py
can_delete_dir = True
if not clear_directory(package_dir):
# If there is an error deleting now, we will mark it for
# cleanup the next time Sublime Text starts
open_compat(os.path.join(package_dir, 'package-control.cleanup'),
'w').close()
can_delete_dir = False
params = {
'package': package_name,
'operation': 'remove',
'version': version
}
self.record_usage(params)
# Remove the package from the installed packages list
def clear_package():
settings = sublime.load_settings('Package Control.sublime-settings')
installed_packages = settings.get('installed_packages', [])
if not installed_packages:
installed_packages = []
installed_packages.remove(package_name)
settings.set('installed_packages', installed_packages)
sublime.save_settings('Package Control.sublime-settings')
sublime.set_timeout(clear_package, 1)
if can_delete_dir and os.path.exists(package_dir):
os.rmdir(package_dir)
return True
def record_usage(self, params):
"""
Submits install, upgrade and delete actions to a usage server
The usage information is currently displayed on the Package Control
community package list at http://wbond.net/sublime_packages/community
:param params:
A dict of the information to submit
"""
if not self.settings.get('submit_usage'):
return
params['package_control_version'] = \
self.get_metadata('Package Control').get('version')
params['sublime_platform'] = self.settings.get('platform')
params['sublime_version'] = self.settings.get('version')
# For Python 2, we need to explicitly encoding the params
for param in params:
if isinstance(params[param], str_cls):
params[param] = params[param].encode('utf-8')
url = self.settings.get('submit_url') + '?' + urlencode(params)
try:
with downloader(url, self.settings) as manager:
result = manager.fetch(url, 'Error submitting usage information.')
except (DownloaderException) as e:
console_write(e, True)
return
try:
result = json.loads(result.decode('utf-8'))
if result['result'] != 'success':
raise ValueError()
except (ValueError):
console_write(u'Error submitting usage information for %s' % params['package'], True)