diff --git a/appveyor.yml b/appveyor.yml index 6dac200915790b6681baf0fae538d6e18e809434..57a174bdec8b45ec76c5a7789593d19af4c3a2e7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,14 +25,11 @@ install: - cmd: conda install nose coverage anaconda-client - cmd: pip install coveralls - - cmd: curl -o condaci.py https://raw.githubusercontent.com/menpo/condaci/v0.4.8/condaci.py - - cmd: python condaci.py setup - build: off test_script: - cmd: nosetests --with-coverage --cover-package=plio after_test: - # After test success, package and upload to Anaconda - - cmd: ~/miniconda/bin/python condaci.py build ./conda + # Afte test success, package and upload to Anaconda + - cmd: python ci_tools/condaci.py build ./conda diff --git a/ci_tools/condaci.py b/ci_tools/condaci.py new file mode 100644 index 0000000000000000000000000000000000000000..30ab480869ded58ef0b5bce76032daab0dacfe61 --- /dev/null +++ b/ci_tools/condaci.py @@ -0,0 +1,764 @@ +#!/usr/bin/env python +import subprocess +import os +import shutil +import os.path as p +from functools import partial +import platform as stdplatform +import uuid +import sys +from pprint import pprint + +# on windows we have to download a small secondary script that configures +# Python 3 64-bit extensions. Here we define the URL and the local path that +# we will use for this script. +import zipfile + +MAGIC_WIN_SCRIPT_URL = 'https://raw.githubusercontent.com/menpo/condaci/master/run_with_env.cmd' +MAGIC_WIN_SCRIPT_PATH = r'C:\run_with_env.cmd' +VS2008_PATCH_URL = 'https://raw.githubusercontent.com/menpo/condaci/master/vs2008_patch.zip' +VS2008_PATCH_PATH = r'C:\vs2008_patch.zip' +VS2008_PATCH_FOLDER_PATH = r'C:\vs2008_patch' + +VS2008_PATH = r'C:\Program Files (x86)\Microsoft Visual Studio 9.0' +VS2008_BIN_PATH = os.path.join(VS2008_PATH, 'VC', 'bin') +VS2010_PATH = r'C:\Program Files (x86)\Microsoft Visual Studio 10.0' +VS2010_BIN_PATH = os.path.join(VS2010_PATH, 'VC', 'bin') +VS2010_AMD64_VCVARS_CMD = r'CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64' + +# a random string we can use for the miniconda installer +# (to avoid name collisions) +RANDOM_UUID = uuid.uuid4() + +# -------------------------------- STATE ------------------------------------ # + +# Key globals that need to be set for the rest of the script. +PYTHON_VERSION = None +PYTHON_VERSION_NO_DOT = None +BINSTAR_USER = None +BINSTAR_KEY = None + + +def set_globals_from_environ(verbose=True): + global PYTHON_VERSION, BINSTAR_KEY, BINSTAR_USER, PYTHON_VERSION_NO_DOT + + PYTHON_VERSION = os.environ.get('PYTHON_VERSION') + BINSTAR_USER = os.environ.get('BINSTAR_USER') + BINSTAR_KEY = os.environ.get('BINSTAR_KEY') + + if verbose: + print('Environment variables extracted:') + print(' PYTHON_VERSION: {}'.format(PYTHON_VERSION)) + print(' BINSTAR_USER: {}'.format(BINSTAR_USER)) + print(' BINSTAR_KEY: {}'.format('*****' if BINSTAR_KEY is not None + else '-')) + + if PYTHON_VERSION is None: + raise ValueError('Fatal: PYTHON_VERSION is not set.') + if PYTHON_VERSION not in ['2.7', '3.4', '3.5']: + raise ValueError("Fatal: PYTHON_VERSION '{}' is invalid - must be " + "either '2.7', '3.4' or '3.5'".format(PYTHON_VERSION)) + + # Required when setting Python version in conda + PYTHON_VERSION_NO_DOT = PYTHON_VERSION.replace('.', '') + + +# ------------------------------ UTILITIES ---------------------------------- # + +# forward stderr to stdout +check = partial(subprocess.check_call, stderr=subprocess.STDOUT) + + +def execute(cmd, verbose=True, env_additions=None): + r""" Runs a command, printing the command and it's output to screen. + """ + env_for_p = os.environ.copy() + if env_additions is not None: + env_for_p.update(env_additions) + if verbose: + print('> {}'.format(' '.join(cmd))) + if env_additions is not None: + print('Additional environment variables: ' + '{}'.format(', '.join(['{}={}'.format(k, v) + for k, v in env_additions.items()]))) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, env=env_for_p) + sentinal = '' + if sys.version_info.major == 3: + sentinal = b'' + for line in iter(proc.stdout.readline, sentinal): + if verbose: + if sys.version_info.major == 3: + # convert bytes to string + line = line.decode("utf-8") + sys.stdout.write(line) + sys.stdout.flush() + output = proc.communicate()[0] + if proc.returncode == 0: + return + else: + e = subprocess.CalledProcessError(proc.returncode, cmd, output=output) + print(' -> {}'.format(e.output)) + raise e + + +def execute_sequence(*cmds, **kwargs): + r""" Execute a sequence of commands. If any fails, display an error. + """ + verbose = kwargs.get('verbose', True) + for cmd in cmds: + execute(cmd, verbose) + + +def extract_zip(zip_path, dest_dir): + r""" + Extract a zip file to a destination + """ + with zipfile.PyZipFile(str(zip_path)) as z: + z.extractall(path=str(dest_dir)) + + +def download_file(url, path_to_download): + try: + from urllib2 import urlopen + except ImportError: + from urllib.request import urlopen + f = urlopen(url) + with open(path_to_download, "wb") as fp: + fp.write(f.read()) + fp.close() + + +def dirs_containing_file(fname, root=os.curdir): + for path, dirs, files in os.walk(os.path.abspath(root)): + if fname in files: + yield path + + +def host_platform(): + return stdplatform.system() + + +def host_arch(): + arch = stdplatform.architecture()[0] + # need to be a little more sneaky to check the platform on Windows: + # http://stackoverflow.com/questions/2208828/detect-64bit-os-windows-in-python + if host_platform() == 'Windows': + if 'APPVEYOR' in os.environ: + av_platform = os.environ['PLATFORM'] + if av_platform == 'x86': + arch = '32bit' + elif av_platform == 'x64': + arch = '64bit' + else: + print('Was unable to interpret the platform "{}"'.format()) + return arch + + +# ------------------------ MINICONDA INTEGRATION ---------------------------- # + +def url_for_platform_version(platform, py_version, arch): + version = 'latest' + base_url = 'http://repo.continuum.io/miniconda/Miniconda' + platform_str = {'Linux': 'Linux', + 'Darwin': 'MacOSX', + 'Windows': 'Windows'} + arch_str = {'64bit': 'x86_64', + '32bit': 'x86'} + ext = {'Linux': '.sh', + 'Darwin': '.sh', + 'Windows': '.exe'} + + if py_version in ['3.4', '3.5']: + base_url += '3' + elif py_version != '2.7': + raise ValueError("Python version must be '2.7', '3.4' or '3.5'") + return '-'.join([base_url, version, + platform_str[platform], + arch_str[arch]]) + ext[platform] + + +def appveyor_miniconda_dir(): + if PYTHON_VERSION in ['3.4', '3.5']: + conda_dir = r'C:\Miniconda3' + elif PYTHON_VERSION == '2.7': + conda_dir = r'C:\Miniconda' + else: + raise ValueError("Python version must be '2.7', '3.4' or '3.5'") + + if host_arch() == '64bit': + conda_dir += '-x64' + + return conda_dir + + +def temp_installer_path(): + # we need a place to download the miniconda installer too. use a random + # string for the filename to avoid collisions, but choose the dir based + # on platform + return ('C:\{}.exe'.format(RANDOM_UUID) if host_platform() == 'Windows' + else p.expanduser('~/{}.sh'.format(RANDOM_UUID))) + + +def miniconda_dir(): + # the directory where miniconda will be installed too/is + if host_platform() == 'Windows': + path = appveyor_miniconda_dir() + else: # Unix + path = p.expanduser('~/miniconda') + if is_on_jenkins(): + # jenkins persists miniconda installs between builds, but we want a + # unique miniconda env for each executor + if not os.path.isdir(path): + os.mkdir(path) + exec_no = os.environ['EXECUTOR_NUMBER'] + j_path = os.path.join(path, exec_no) + if not os.path.isdir(j_path): + os.mkdir(j_path) + path = os.path.join(j_path, PYTHON_VERSION) + return path + + +# the script directory inside a miniconda install varies based on platform +def miniconda_script_dir_name(): + return 'Scripts' if host_platform() == 'Windows' else 'bin' + + +# handles to binaries from a miniconda install +exec_ext = '.exe' if host_platform() == 'Windows' else '' +miniconda_script_dir = lambda mc: p.join(mc, miniconda_script_dir_name()) +conda = lambda mc: p.join(miniconda_script_dir(mc), 'conda' + exec_ext) +binstar = lambda mc: p.join(miniconda_script_dir(mc), 'anaconda' + exec_ext) + + +def acquire_miniconda(url, path_to_download): + print('Downloading miniconda from {} to {}'.format(url, path_to_download)) + download_file(url, path_to_download) + + +def install_miniconda(path_to_installer, path_to_install): + print('Installing miniconda to {}'.format(path_to_install)) + if host_platform() == 'Windows': + execute([path_to_installer, '/S', '/D={}'.format(path_to_install)]) + else: + execute(['chmod', '+x', path_to_installer]) + execute([path_to_installer, '-b', '-p', path_to_install]) + + +def setup_miniconda(python_version, installation_path, binstar_user=None): + conda_cmd = conda(installation_path) + if os.path.exists(conda_cmd): + print('conda is already setup at {}'.format(installation_path)) + else: + print('No existing conda install detected at {}'.format(installation_path)) + url = url_for_platform_version(host_platform(), python_version, + host_arch()) + print('Setting up miniconda from URL {}'.format(url)) + print("(Installing to '{}')".format(installation_path)) + acquire_miniconda(url, temp_installer_path()) + install_miniconda(temp_installer_path(), installation_path) + # delete the installer now we are done + os.unlink(temp_installer_path()) + cmds = [[conda_cmd, 'update', '-q', '--yes', 'conda'], + [conda_cmd, 'install', '-q', '--yes', 'conda-build', 'jinja2', + 'anaconda-client']] + root_config = os.path.join(installation_path, '.condarc') + if os.path.exists(root_config): + print('existing root config at present at {} - removing'.format(root_config)) + os.unlink(root_config) + if binstar_user is not None: + print("(adding user channel '{}' for dependencies to root config)".format(binstar_user)) + cmds.append([conda_cmd, 'config', '--system', '--add', 'channels', binstar_user]) + else: + print("No user channels have been configured (all dependencies have to be " + "sourced from anaconda)") + execute_sequence(*cmds) + + +# ------------------------ CONDA BUILD INTEGRATION -------------------------- # + +def get_conda_build_path(mc_dir, recipe_dir): + path_bytes = subprocess.check_output([conda(mc_dir), 'build', + recipe_dir, '--output']) + return path_bytes.decode("utf-8").strip() + + +def conda_build_package_win(mc, path): + if 'BINSTAR_KEY' in os.environ: + print('found BINSTAR_KEY in environment on Windows - deleting to ' + 'stop vcvarsall from telling the world') + del os.environ['BINSTAR_KEY'] + os.environ['PYTHON_ARCH'] = host_arch()[:2] + os.environ['PYTHON_VERSION'] = PYTHON_VERSION + print('PYTHON_ARCH={} PYTHON_VERSION={}'.format(os.environ['PYTHON_ARCH'], + os.environ['PYTHON_VERSION'])) + execute(['cmd', '/E:ON', '/V:ON', '/C', MAGIC_WIN_SCRIPT_PATH, + conda(mc), 'build', '-q', path, + '--py={}'.format(PYTHON_VERSION_NO_DOT)]) + + +def windows_setup_compiler(): + arch = host_arch() + if PYTHON_VERSION == '2.7': + download_file(VS2008_PATCH_URL, VS2008_PATCH_PATH) + if not os.path.exists(VS2008_PATCH_FOLDER_PATH): + os.makedirs(VS2008_PATCH_FOLDER_PATH) + extract_zip(VS2008_PATCH_PATH, VS2008_PATCH_FOLDER_PATH) + + if arch == '64bit': + execute([os.path.join(VS2008_PATCH_FOLDER_PATH, 'setup_x64.bat')]) + + VS2008_AMD64_PATH = os.path.join(VS2008_BIN_PATH, 'amd64') + if not os.path.exists(VS2008_AMD64_PATH): + os.makedirs(VS2008_AMD64_PATH) + shutil.copyfile(os.path.join(VS2008_BIN_PATH, 'vcvars64.bat'), + os.path.join(VS2008_AMD64_PATH, 'vcvarsamd64.bat')) + elif arch == '32bit': + # For some reason these files seems to be missing on Appveyor + # execute([os.path.join(VS2008_PATCH_FOLDER_PATH, 'setup_x86.bat')]) + pass + else: + raise ValueError('Unexpected architecture {}'.format(arch)) + elif PYTHON_VERSION == '3.4' and arch == '64bit': + VS2010_AMD64_PATH = os.path.join(VS2010_BIN_PATH, 'amd64') + if not os.path.exists(VS2010_AMD64_PATH): + os.makedirs(VS2010_AMD64_PATH) + VS2010_AMD64_VCVARS_PATH = os.path.join(VS2010_AMD64_PATH, + 'vcvars64.bat') + with open(VS2010_AMD64_VCVARS_PATH, 'w') as f: + f.write(VS2010_AMD64_VCVARS_CMD) + + +def build_conda_package(mc, path, binstar_user=None): + print('Building package at path {}'.format(path)) + v = get_version(path) + print('Detected version: {}'.format(v)) + print('Setting CONDACI_VERSION environment variable to {}'.format(v)) + os.environ['CONDACI_VERSION'] = v + print('Setting CONDA_PY environment variable to {}'.format( + PYTHON_VERSION_NO_DOT)) + os.environ['CONDA_PY'] = PYTHON_VERSION_NO_DOT + + # we want to add the master channel when doing dev builds to source our + # other dev dependencies + if not (is_release_tag(v) or is_rc_tag(v)): + print('building a non-release non-RC build - adding master channel.') + if binstar_user is None: + print('warning - no binstar user provided - cannot add master channel') + else: + execute([conda(mc), 'config', '--system', '--add', 'channels', + binstar_user + '/channel/master']) + else: + print('building a RC or tag release - no master channel added.') + + if host_platform() == 'Windows': + # Before building the package, we may need to edit the environment a bit + # to handle the nightmare that is Visual Studio compilation + windows_setup_compiler() + conda_build_package_win(mc, path) + else: + execute([conda(mc), 'build', '-q', path, + '--py={}'.format(PYTHON_VERSION_NO_DOT)]) + + +# ------------------------- VERSIONING INTEGRATION -------------------------- # + +# versions that match up to master changes (anything after a '+') +same_version_different_build = lambda v1, v2: v2.startswith(v1.split('+')[0]) + + +def versions_from_versioneer(): + # Ideally, we will interrogate versioneer to find out the version of the + # project we are building. Note that we can't simply look at + # project.__version__ as we need the version string pre-build, so the + # package may not be importable. + for dir_ in dirs_containing_file('_version.py'): + sys.path.insert(0, dir_) + + try: + import _version + yield _version.get_versions()['version'] + except Exception as e: + print(e) + finally: + if '_version' in sys.modules: + sys.modules.pop('_version') + + sys.path.pop(0) + + +def version_from_meta_yaml(path): + meta_yaml_path = os.path.join(path, 'meta.yaml') + with open(meta_yaml_path, 'rt') as f: + s = f.read() + v = s.split('version:', 1)[1].split('\n', 1)[0].strip().strip("'").strip('"') + if '{{' in v: + raise ValueError('Trying to establish version from meta.yaml' + ' and it seems to be dynamic: {}'.format(v)) + return v + + +def get_version(path): + # search for versioneer versions in our subdirs + versions = list(versions_from_versioneer()) + + if len(versions) == 1: + version = versions[0] + print('Found unambiguous versioneer version: {}'.format(version)) + elif len(versions) > 1: + raise ValueError('Multiple versioneer _version.py files - cannot ' + 'resolve unambiguous version. ' + 'Versions found are: {}'.format(versions)) + else: + # this project doesn't seem to be versioneer controlled - maybe the + # version is hardcoded? Interrogate meta.yaml + version = version_from_meta_yaml(path) + return version + +# booleans about the state of the the PEP440 tags. +is_tag = lambda v: '+' not in v +is_dev_tag = lambda v: v.split('.')[-1].startswith('dev') +is_rc_tag = lambda v: 'rc' in v.split('+')[0] +is_release_tag = lambda v: is_tag(v) and not (is_rc_tag(v) or is_dev_tag(v)) + + +# -------------------------- BINSTAR INTEGRATION ---------------------------- # + + +class LetMeIn: + def __init__(self, key): + self.token = key + self.site = False + + +def login_to_binstar(): + from binstar_client.utils import get_binstar + return get_binstar() + + +def login_to_binstar_with_key(key): + from binstar_client.utils import get_binstar + return get_binstar(args=LetMeIn(key)) + + +class BinstarFile(object): + + def __init__(self, full_name): + self.full_name = full_name + + @property + def user(self): + return self.full_name.split('/')[0] + + @property + def name(self): + return self.full_name.split('/')[1] + + @property + def basename(self): + return '/'.join(self.full_name.split('/')[3:]) + + @property + def version(self): + return self.full_name.split('/')[2] + + @property + def platform(self): + return self.full_name.replace('\\', '/').split('/')[3] + + @property + def configuration(self): + return self.full_name.replace('\\', '/').split('/')[4].split('-')[2].split('.')[0] + + def __str__(self): + return self.full_name + + def __repr__(self): + return self.full_name + + def all_info(self): + s = [" user: {}".format(self.user), + " name: {}".format(self.name), + " basename: {}".format(self.basename), + " version: {}".format(self.version), + " platform: {}".format(self.platform), + "configuration: {}".format(self.configuration)] + return "\n".join(s) + + +configuration_from_binstar_filename = lambda fn: fn.split('-')[-1].split('.')[0] +name_from_binstar_filename = lambda fn: fn.split('-')[0] +version_from_binstar_filename = lambda fn: fn.split('-')[1] +platform_from_binstar_filepath = lambda fp: p.split(p.split(fp)[0])[-1] + + +def binstar_channels_for_user(b, user): + return b.list_channels(user).keys() + + +def binstar_files_on_channel(b, user, channel): + info = b.show_channel(channel, user) + return [BinstarFile(i['full_name']) for i in info['files']] + + +def binstar_remove_file(b, bfile): + b.remove_dist(bfile.user, bfile.name, bfile.version, bfile.basename) + + +def files_to_remove(b, user, channel, filepath): + platform_ = platform_from_binstar_filepath(filepath) + filename = p.split(filepath)[-1] + name = name_from_binstar_filename(filename) + version = version_from_binstar_filename(filename) + configuration = configuration_from_binstar_filename(filename) + # find all the files on this channel + all_files = binstar_files_on_channel(b, user, channel) + # other versions of this exact setup that are not tagged versions should + # be removed + print('Removing old releases matching:' + '\nname: {}\nconfiguration: {}\nplatform: {}' + '\nversion: {}'.format(name, configuration, platform_, version)) + print('candidate releases with same name are:') + pprint([f.all_info() for f in all_files if f.name == name]) + return [f for f in all_files if + f.name == name and + f.configuration == configuration and + f.platform == platform_ and + f.version != version and + not is_release_tag(f.version) and + same_version_different_build(version, f.version)] + + +def purge_old_binstar_files(b, user, channel, filepath): + to_remove = files_to_remove(b, user, channel, filepath) + print("Found {} releases to remove".format(len(to_remove))) + for old_file in to_remove: + print("Removing '{}'".format(old_file)) + binstar_remove_file(b, old_file) + + +def binstar_upload_unchecked(mc, key, user, channel, path): + print('Uploading from {} using {}'.format(path, binstar(mc))) + try: + # TODO - could this safely be co? then we would get the binstar error.. + check([binstar(mc), '-t', key, 'upload', + '--force', '-u', user, '-c', channel, path]) + except subprocess.CalledProcessError as e: + # mask the binstar key... + cmd = e.cmd + cmd[2] = 'BINSTAR_KEY' + # ...then raise the error + raise subprocess.CalledProcessError(e.returncode, cmd) + + +def binstar_upload_if_appropriate(mc, path, user, key): + if key is None: + print('No binstar key provided') + if user is None: + print('No binstar user provided') + if user is None or key is None: + print('-> Unable to upload to binstar') + return + print('Have a user ({}) and key - can upload if suitable'.format(user)) + + # decide if we should attempt an upload (if it's a PR we can't) + if resolve_can_upload_from_ci(): + print('Auto resolving channel based on release type and CI status') + channel = binstar_channel_from_ci(path) + print("Fit to upload to channel '{}'".format(channel)) + binstar_upload_and_purge(mc, key, user, channel, + get_conda_build_path(mc, path)) + else: + print("Cannot upload to binstar - must be a PR.") + + +def binstar_upload_and_purge(mc, key, user, channel, filepath): + if not os.path.exists(filepath): + raise ValueError('Built file {} does not exist. ' + 'UPLOAD FAILED.'.format(filepath)) + else: + print('Uploading to {}/{}'.format(user, channel)) + binstar_upload_unchecked(mc, key, user, channel, filepath) + b = login_to_binstar_with_key(key) + if channel != 'main': + print("Purging old releases from channel '{}'".format(channel)) + purge_old_binstar_files(b, user, channel, filepath) + else: + print("On main channel - no purging of releases will be done.") + + +# -------------- CONTINUOUS INTEGRATION-SPECIFIC FUNCTIONALITY -------------- # + +is_on_appveyor = lambda: 'APPVEYOR' in os.environ +is_on_travis = lambda: 'TRAVIS' in os.environ +is_on_jenkins = lambda: 'JENKINS_URL' in os.environ + +is_pr_from_travis = lambda: os.environ['TRAVIS_PULL_REQUEST'] != 'false' +is_pr_from_appveyor = lambda: 'APPVEYOR_PULL_REQUEST_NUMBER' in os.environ +is_pr_from_jenkins = lambda: 'ghprbSourceBranch' in os.environ + +branch_from_appveyor = lambda: os.environ['APPVEYOR_REPO_BRANCH'] + + +def branch_from_jenkins(): + branch = os.environ['GIT_BRANCH'] + print('Jenkins has set GIT_BRANCH={}'.format(branch)) + if branch.startswith('origin/tags/'): + print('WARNING - on jenkins and GIT_BRANCH starts with origin/tags/. ' + 'This suggests that we are building a tag.') + print('Jenkins obscures the branch in this scenario, so we assume that' + ' the branch is "master"') + return 'master' + elif branch.startswith('origin/'): + return branch.split('origin/', 1)[-1] + else: + raise ValueError('Error: jenkins branch name seems ' + 'suspicious: {}'.format(branch)) + + +def branch_from_travis(): + tag = os.environ['TRAVIS_TAG'] + branch = os.environ['TRAVIS_BRANCH'] + if tag == branch: + print('WARNING - on travis and TRAVIS_TAG == TRAVIS_BRANCH. This ' + 'suggests that we are building a tag.') + print('Travis obscures the branch in this scenario, so we assume that' + ' the branch is "master"') + return 'master' + else: + return branch + + +def is_pr_on_ci(): + if is_on_travis(): + return is_pr_from_travis() + elif is_on_appveyor(): + return is_pr_from_appveyor() + elif is_on_jenkins(): + return is_pr_from_jenkins() + else: + raise ValueError("Not on appveyor, travis or jenkins, so can't " + "resolve whether we are on a PR or not") + + +def branch_from_ci(): + if is_on_travis(): + return branch_from_travis() + elif is_on_appveyor(): + return branch_from_appveyor() + elif is_on_jenkins(): + return branch_from_jenkins() + else: + raise ValueError("We aren't on jenkins, " + "Appveyor or Travis so can't " + "decide on branch") + + +def resolve_can_upload_from_ci(): + # can upload as long as this isn't a PR + can_upload = not is_pr_on_ci() + print("Can we can upload (i.e. is this not a PR)? : {}".format(can_upload)) + return can_upload + + +def binstar_channel_from_ci(path): + v = get_version(path) + if is_release_tag(v): + # tagged releases always go to main + print("current head is a tagged release ({}), " + "uploading to 'main' channel".format(v)) + return 'main' + else: + print('current head is not a release - interrogating CI to decide on ' + 'channel to upload to (based on branch)') + return branch_from_ci() + + +# -------------------- [EXPERIMENTAL] PYPI INTEGRATION ---------------------- # + +# pypirc_path = p.join(p.expanduser('~'), '.pypirc') +# pypi_upload_allowed = (host_platform() == 'Linux' and +# host_arch() == '64bit' and +# sys.version_info.major == 2) +# +# pypi_template = """[distutils] +# index-servers = pypi +# +# [pypi] +# username:{} +# password:{}""" +# +# +# def pypi_setup_dotfile(username, password): +# with open(pypirc_path, 'wb') as f: +# f.write(pypi_template.format(username, password)) +# +# +# def upload_to_pypi_if_appropriate(mc, username, password): +# if username is None or password is None: +# print('Missing PyPI username or password, skipping upload') +# return +# v = get_version() +# if not is_release_tag(v): +# print('Not on a tagged release - not uploading to PyPI') +# return +# if not pypi_upload_allowed: +# print('Not on key node (Linux 64 Py2) - no PyPI upload') +# print('Setting up .pypirc file..') +# pypi_setup_dotfile(username, password) +# print("Uploading to PyPI user '{}'".format(username)) +# execute_sequence([python(mc), 'setup.py', 'sdist', 'upload']) + + +# --------------------------- ARGPARSE COMMANDS ----------------------------- # + +def miniconda_dir_cmd(_): + set_globals_from_environ(verbose=False) + print(miniconda_dir()) + + +def setup_cmd(_): + set_globals_from_environ() + mc = miniconda_dir() + setup_miniconda(PYTHON_VERSION, mc, binstar_user=BINSTAR_USER) + + +def build_cmd(args): + set_globals_from_environ() + mc = miniconda_dir() + conda_meta = args.meta_yaml_dir + + if host_platform() == 'Windows': + print('downloading magical Windows SDK configuration' + ' script to {}'.format(MAGIC_WIN_SCRIPT_PATH)) + download_file(MAGIC_WIN_SCRIPT_URL, MAGIC_WIN_SCRIPT_PATH) + + build_conda_package(mc, conda_meta, binstar_user=BINSTAR_USER) + print('successfully built conda package, proceeding to upload') + binstar_upload_if_appropriate(mc, conda_meta, BINSTAR_USER, BINSTAR_KEY) + # upload_to_pypi_if_appropriate(mc, args.pypiuser, args.pypipassword) + + +if __name__ == "__main__": + from argparse import ArgumentParser + pa = ArgumentParser( + description=r""" + Sets up miniconda, builds, and uploads to Binstar. + """) + subp = pa.add_subparsers() + + sp = subp.add_parser('setup', help='setup a miniconda environment') + sp.set_defaults(func=setup_cmd) + + bp = subp.add_parser('build', help='run a conda build') + bp.add_argument('meta_yaml_dir', + help="path to the dir containing the conda 'meta.yaml'" + "build script") + + mp = subp.add_parser('miniconda_dir', + help='path to the miniconda root directory') + mp.set_defaults(func=miniconda_dir_cmd) + + bp.set_defaults(func=build_cmd) + args = pa.parse_args() + args.func(args) diff --git a/ci_tools/upload_or_check_nonexistence.py b/ci_tools/upload_or_check_nonexistence.py deleted file mode 100644 index 9cedfddd950207f2cb38706ec7a430f915f8dad5..0000000000000000000000000000000000000000 --- a/ci_tools/upload_or_check_nonexistence.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -import argparse -import hashlib -import os -import subprocess -import sys - -from binstar_client.utils import get_binstar -import binstar_client.errors -import conda.config -from conda_build.metadata import MetaData -from conda_build.build import bldpkg_path - - -def built_distribution_already_exists(cli, meta, owner): - """ - Checks to see whether the built recipe (aka distribution) already - exists on the owner/user's binstar account. - """ - distro_name = '{}/{}.tar.bz2'.format(conda.config.subdir, meta.dist()) - fname = bldpkg_path(meta) - try: - dist_info = cli.distribution(owner, meta.name(), meta.version(), - distro_name) - except binstar_client.errors.NotFound: - dist_info = {} - - exists = bool(dist_info) - # Unfortunately, we cannot check the md5 quality of the built distribution, as - # this will depend on fstat information such as modification date (because - # distributions are tar files). Therefore we can only assume that the distribution - # just built, and the one on anaconda.org are the same. -# if exists: -# md5_on_binstar = dist_info.get('md5') -# with open(fname, 'rb') as fh: -# md5_of_build = hashlib.md5(fh.read()).hexdigest() -# -# if md5_on_binstar != md5_of_build: -# raise ValueError('This build ({}), and the build already on binstar ' -# '({}) are different.'.format(md5_of_build, md5_on_binstar)) - return exists - - -def upload(cli, meta, owner, channels): - try: - with open('binstar.token', 'w') as fh: - fh.write(cli.token) - subprocess.check_call(['anaconda', '--quiet', '-t', 'binstar.token', - 'upload', bldpkg_path(meta), - '--user={}'.format(owner), - '--channel={}'.format(channels)], - env=os.environ) - finally: - os.remove('binstar.token') - - -def distribution_exists_on_channel(binstar_cli, meta, owner, channel='main'): - """ - Determine whether a distribution exists on a specific channel. - Note from @pelson: As far as I can see, there is no easy way to do this on binstar. - """ - fname = '{}/{}.tar.bz2'.format(conda.config.subdir, meta.dist()) - distributions_on_channel = [dist['basename'] for dist in - binstar_cli.show_channel(owner=owner, channel=channel)['files']] - return fname in distributions_on_channel - - -def add_distribution_to_channel(binstar_cli, meta, owner, channel='main'): - """ - Add a(n already existing) distribution on binstar to another channel. - Note - the addition is done based on name and version - no build strings etc. - so if you have a foo-0.1-np18 and foo-0.1-np19 *both* will be added to the channel. - """ - package_fname = '{}/{}.tar.bz2'.format(conda.config.subdir, meta.dist()) - binstar_cli.add_channel(channel, owner, meta.name(), meta.version()) - - -def main(): - token = os.environ.get('BINSTAR_KEY') - - description = ('Upload or check consistency of a built version of a ' - 'conda recipe with binstar. Note: The existence of the ' - 'BINSTAR_KEY environment variable determines ' - 'whether the upload should actually take place.') - parser = argparse.ArgumentParser(description=description) - parser.add_argument('recipe_dir', help='the conda recipe directory') - parser.add_argument('owner', help='the binstar owner/user') - parser.add_argument('--channel', help='the binstar channel', default='main') - args = parser.parse_args() - recipe_dir, owner, channel = args.recipe_dir, args.owner, args.channel - - cli = get_binstar(argparse.Namespace(token=token, site=None)) - meta = MetaData(recipe_dir) - if meta.skip(): - print("No upload to take place - this configuration was skipped in build/skip.") - return - exists = built_distribution_already_exists(cli, meta, owner) - if token: - on_channel = distribution_exists_on_channel(cli, meta, owner, channel) - if not exists: - upload(cli, meta, owner, channel) - print('Uploaded {}'.format(bldpkg_path(meta))) - elif not on_channel: - print('Adding distribution {} to {}\'s {} channel' - ''.format(bldpkg_path(meta), owner, channel)) - add_distribution_to_channel(cli, meta, owner, channel) - else: - print('Distribution {} already \nexists on {}\'s {} channel.' - ''.format(bldpkg_path(meta), owner, channel)) - else: - print("No BINSTAR_KEY present, so no upload is taking place. " - "The distribution just built {} already available on {}'s " - "{} channel.".format('is' if exists else 'is not', - owner, channel)) - -if __name__ == '__main__': - main() \ No newline at end of file