#!/usr/bin/env python
#
# Simplifies installation of PySVN, working through platform and other
# compatibility differences.
#
# By default, this will install a wheel for the latest version of PySVN.
from __future__ import print_function, unicode_literals
import argparse
import atexit
import getpass
import glob
import os
import platform
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
from subprocess import CalledProcessError
try:
# Python 3
from urllib.error import URLError
from urllib.request import urlopen, urlretrieve
except ImportError:
# Python 2
from urllib import urlretrieve
from urllib2 import URLError, urlopen
try:
import pip
except ImportError:
sys.stderr.write('Install pip for Python %s.%s and try again.\n'
% sys.version_info[:2])
sys.exit(1)
try:
import wheel
except ImportError:
sys.stderr.write('Install wheel for Python %s.%s and try again.\n'
% sys.version_info[:2])
sys.exit(1)
INDEX_URL = 'https://sourceforge.net/projects/pysvn/rss?path=/pysvn&limit=10'
DOWNLOAD_URL_MASK = (
'https://sourceforge.net/projects/pysvn/files/pysvn/V%(version)s/'
'pysvn-%(version)s.tar.gz/download')
VERSION_RE = \
re.compile(br'.*/files/pysvn/V(?P[0-9\.-]+)/.*')
cwd = None
temp_path = None
_debug_mode = (os.environ.get('DEBUG_PYSVN_INSTALLER') == '1')
def destroy_temp():
shutil.rmtree(temp_path)
def debug(msg):
if _debug_mode:
sys.stderr.write(msg)
def get_pysvn_version():
try:
data = urlopen(INDEX_URL).read()
except URLError as e:
sys.stderr.write('Unable to fetch PySVN downloads RSS feed: %s\n' % e)
sys.stderr.write('Tried to load feed from %s\n' % INDEX_URL)
sys.exit(1)
m = VERSION_RE.search(data)
if not m:
sys.stderr.write('Unable to find latest PySVN version in RSS feed.\n')
sys.stderr.write('Please report to support@beanbaginc.com.\n')
sys.exit(1)
return m.groups('version')[0].decode('utf-8')
def fetch_pysvn(pysvn_version):
url = DOWNLOAD_URL_MASK % {
'version': pysvn_version,
}
debug('PySVN URL: %s\n' % url)
tarball_path = os.path.join(temp_path, 'pysvn.tar.gz')
try:
urlretrieve(url, filename=tarball_path)
except URLError as e:
sys.stderr.write('Unable to fetch PySVN %s: %s\n' % (pysvn_version, e))
sys.stderr.write('Please report to support@beanbaginc.com.\n')
sys.exit(1)
return tarball_path
def extract_pysvn(tarball_path):
with tarfile.open(tarball_path) as tar:
tar.extractall(temp_path)
try:
return glob.glob(os.path.join(temp_path, 'pysvn-*'))[0]
except IndexError:
sys.stderr.write('Unable to find pysvn-* directory in tarball.\n')
sys.stderr.write('Please report to support@beanbaginc.com.\n')
sys.exit(1)
def get_brew_prefix(package):
try:
path = (
subprocess.check_output(['brew', '--prefix', package])
.strip()
.decode('utf-8')
)
debug('%s was found in brew: %s\n' % (package, path))
return path
except CalledProcessError:
debug('%s was not found in brew\n' % package)
return None
def get_linux_arch():
"""Return the current multiarch architecture for Linux.
Returns:
str:
The architecture, or ``None`` if it can't be determined.
"""
try:
return (
subprocess.check_output(['gcc', '-dumpmachine'])
.strip()
.decode('utf-8')
)
except CalledProcessError:
return None
def get_linux_arch_dirs():
"""Return a list of Linux multiarch library directories.
These will be based off the current architecture. Only paths that exist
will be returned.
Returns:
list of str:
The list of multiarch directories.
"""
linux_arch = get_linux_arch()
if not linux_arch:
return []
return [
path
for path in [
'/usr/lib/%s' % linux_arch,
'/usr/local/lib/%s' % linux_arch,
]
if os.path.exists(path)
]
def build_pysvn(src_path, install=True):
system = platform.system()
debug('System = %s\n' % system)
debug('Machine = %s\n' % platform.machine())
debug('Processor = %s\n' % platform.processor())
os.chdir(src_path)
# Locate the PyCXX Import version, so we can force its usage during
# setup.py.
import_path = os.path.join(src_path, 'Import')
try:
pycxx_dirname = glob.glob(os.path.join(import_path, 'pycxx*'))[0]
except IndexError:
sys.stderr.write('PySVN seems to be missing an Import/pycxx* '
'directory\n')
sys.stderr.write('Please report to support@beanbaginc.com.\n')
sys.exit(1)
pycxx_path = os.path.join(import_path, pycxx_dirname)
# We need to patch setup.py to specify the --pycxx-dir parameter.
setup_py_path = os.path.join(src_path, 'setup.py')
with open(setup_py_path, 'r') as fp:
setup_py = fp.read()
config_token = 'setup.py configure'
if config_token not in setup_py:
sys.stderr.write("PySVN's setup.py can no longer be patched.\n")
sys.stderr.write('Please report to support@beanbaginc.com.\n')
sys.exit(1)
config_args = ['--pycxx-dir="%s"' % pycxx_path]
extra_apr_include_paths = []
extra_apr_lib_paths = []
extra_apu_include_paths = []
extra_svn_bin_paths = []
extra_svn_include_paths = []
extra_svn_lib_paths = []
apr_config_path = None
apu_config_path = None
libsvn_client_filename = None
libapr_filename = None
if system == 'Darwin':
debug('Enabling macOS framework support\n')
libapr_filename = 'libapr-1.dylib'
libsvn_client_filename = 'libsvn_client-1.a'
config_args.append('--link-python-framework-via-dynamic-lookup')
# We want to include a few additional places to look for headers
# and libraries. We'll start by seeing if Homebrew has some
# information, and we'll then proceed to including the XCode versions.
brew_svn_path = get_brew_prefix('subversion')
brew_apr_path = get_brew_prefix('apr')
brew_apr_util_path = get_brew_prefix('apr-util')
if brew_apr_path:
apr_config_path = os.path.join(brew_apr_path, 'bin',
'apr-1-config')
if brew_apr_util_path:
apu_config_path = os.path.join(brew_apr_util_path, 'bin',
'apu-1-config')
if brew_svn_path and os.path.exists(brew_svn_path):
# If SVN is installed from brew, we'll want to use those paths.
extra_svn_bin_paths.append(os.path.join(brew_svn_path, 'bin'))
extra_svn_include_paths.append(os.path.join(brew_svn_path,
'include',
'subversion-1'))
extra_svn_lib_paths.append(os.path.join(brew_svn_path, 'lib'))
# XCode bundle both APU directories under the same path.
xcode_apr_path = (
'/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk'
'/usr/include/apr-1')
extra_apr_include_paths.append(xcode_apr_path)
extra_apu_include_paths.append(xcode_apr_path)
elif system == 'Linux':
libapr_filename = 'libapr-1.so'
libsvn_client_filename = 'libsvn_client-1.so'
if getpass.getuser() == 'bitnami':
debug('Installing for Bitnami\n')
apr_config_path = '/opt/bitnami/apache/bin/apr-1-config'
apu_config_path = '/opt/bitnami/apache/bin/apu-1-config'
extra_svn_bin_paths.append('/opt/bitnami/subversion/bin')
extra_svn_lib_paths.append('/opt/bitnami/subversion/lib')
extra_svn_include_paths.append(
'/opt/bitnami/subversion/include/subversion-1')
else:
linux_arch_dirs = get_linux_arch_dirs()
debug('Linux arch directories: %r\n' % linux_arch_dirs)
extra_apr_lib_paths += linux_arch_dirs
extra_svn_lib_paths += linux_arch_dirs
# Scan for any directories based on the apu-1-config/apr-1-config tools.
# We'll prefer these paths over any others.
if apr_config_path and os.path.exists(apr_config_path):
extra_apr_include_paths.insert(
0,
subprocess.check_output([apr_config_path, '--includedir'])
.decode('utf-8').strip())
apr_prefix = (
subprocess.check_output([apr_config_path, '--prefix'])
.decode('utf-8').strip()
)
extra_apr_lib_paths.append(os.path.join(apr_prefix, 'lib'))
if apu_config_path and os.path.exists(apu_config_path):
extra_apu_include_paths.insert(
0,
subprocess.check_output([apu_config_path, '--includedir'])
.decode('utf-8').strip())
debug('Extra APR include paths: %r\n' % (extra_apr_include_paths,))
debug('Extra APR lib paths: %r\n' % (extra_apr_lib_paths,))
debug('Extra APU include paths: %r\n' % (extra_apu_include_paths,))
debug('Extra SVN bin paths: %r\n' % (extra_svn_bin_paths,))
debug('Extra SVN include paths: %r\n' % (extra_svn_include_paths,))
debug('Extra SVN lib paths: %r\n' % (extra_svn_lib_paths,))
for path in extra_apr_include_paths:
if os.path.exists(os.path.join(path, 'apr.h')):
config_args.append('--apr-inc-dir="%s"' % path)
break
if libapr_filename:
for path in extra_apr_lib_paths:
if os.path.exists(os.path.join(path, libapr_filename)):
config_args.append('--apr-lib-dir="%s"' % path)
break
for path in extra_apu_include_paths:
if os.path.exists(os.path.join(path, 'apu.h')):
config_args.append('--apu-inc-dir="%s"' % path)
break
for path in extra_svn_bin_paths:
if os.path.exists(os.path.join(path, 'svn')):
config_args.append('--svn-bin-dir="%s"' % path)
break
for path in extra_svn_include_paths:
if os.path.exists(os.path.join(path, 'svn_client.h')):
config_args.append('--svn-inc-dir="%s"' % path)
break
if libsvn_client_filename:
for path in extra_svn_lib_paths:
if os.path.exists(os.path.join(path, libsvn_client_filename)):
config_args.append('--svn-lib-dir="%s"' % path)
break
debug('Using configuration arguments: %r\n' % (config_args,))
setup_py = setup_py.replace(config_token,
'%s %s' % (config_token,
' '.join(config_args)))
with open(setup_py_path, 'w') as fp:
fp.write(setup_py)
if install:
cmd_args = ['-m', 'pip', 'install', src_path]
else:
cmd_args = ['setup.py', 'bdist_wheel', '--dist-dir', cwd]
result = subprocess.call([sys.executable] + cmd_args)
debug('Exit code = %s\n' % result)
return result
def main():
global cwd
global temp_path
parser = argparse.ArgumentParser()
parser.add_argument('--pysvn-version',
default=os.environ.get('PYSVN_INSTALLER_VERSION'),
help='A specific version of PySVN to install.')
parser.add_argument('--file',
default=os.environ.get('PYSVN_INSTALLER_SRC_FILE'),
help='A specific PySVN source tarball to install.')
parser.add_argument('--build-only',
action='store_true',
default=os.environ.get('PYSVN_INSTALLER_BUILD_ONLY',
False),
help="Build a wheel, but don't install it. The "
"wheel will be stored in the current directory.")
args = parser.parse_args()
cwd = os.getcwd()
temp_path = tempfile.mkdtemp(suffix='.pysvn-install')
atexit.register(destroy_temp)
if args.file:
tarball_path = args.file
if not os.path.exists(tarball_path):
sys.stderr.write('The provided PySVN tarball does not exist.\n')
sys.exit(1)
else:
if args.pysvn_version:
pysvn_version = args.pysvn_version
else:
print('Looking up latest PySVN version...')
pysvn_version = get_pysvn_version()
if pysvn_version == '1.9.13':
pysvn_version = '1.9.12'
debug('PySVN %s\n' % pysvn_version)
print('Downloading PySVN %s...' % pysvn_version)
tarball_path = fetch_pysvn(pysvn_version)
print('Building PySVN...')
src_path = extract_pysvn(tarball_path)
retcode = build_pysvn(src_path, install=not args.build_only)
if retcode == 0:
print()
if args.build_only:
print('PySVN is built. The wheel is in the current directory.')
else:
print('PySVN is installed.')
else:
sys.stderr.write('\n')
sys.stderr.write('PySVN failed to install. You might be missing some '
'dependencies.\n')
system = platform.system()
if system == 'Darwin':
sys.stderr.write('On macOS, run:\n')
sys.stderr.write('\n')
sys.stderr.write(' $ xcode-select --install\n')
sys.stderr.write(' $ brew install subversion\n')
sys.stderr.write('\n')
sys.stderr.write('Note that you will need to install Homebrew '
'from https://brew.sh/\n')
elif system == 'Linux':
if sys.version_info[0] == 3:
pkg_prefix = 'python3'
else:
pkg_prefix = 'python'
sys.stderr.write('On Linux, you will need Python development '
'headers and\n')
sys.stderr.write('Subversion development libraries.\n')
sys.stderr.write('\n')
sys.stderr.write('For Ubuntu:\n')
sys.stderr.write('\n')
sys.stderr.write(' $ sudo apt-get install %s-dev\n'
% pkg_prefix)
sys.stderr.write(' $ sudo apt-get build-dep %s-svn\n'
% pkg_prefix)
sys.stderr.write('\n')
sys.stderr.write('For RHEL/CentOS:\n')
sys.stderr.write('\n')
sys.stderr.write(' $ sudo yum install %s-devel '
'subversion-devel\n'
% pkg_prefix)
sys.exit(1)
if __name__ == '__main__':
main()