diff options
author | Joseph Hunkeler <jhunkeler@gmail.com> | 2018-12-04 11:08:16 -0500 |
---|---|---|
committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2018-12-04 11:08:16 -0500 |
commit | f6eb0f96fec2a124333381c6a80ccd4f27290a7d (patch) | |
tree | 1dc20385009169baac43a31212d9dfd5ed2b6dde | |
parent | 3767b4566c124f0b7ac15352b0614d8ef94f23d1 (diff) | |
download | wheeblast-f6eb0f96fec2a124333381c6a80ccd4f27290a7d.tar.gz |
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | LICENSE.txt | 16 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rwxr-xr-x | blast.py | 506 | ||||
-rwxr-xr-x | build-project.sh | 85 | ||||
-rwxr-xr-x | check_upstream.py | 72 | ||||
-rw-r--r-- | etc/config.sh | 57 | ||||
-rw-r--r-- | etc/dev-requirements.txt | 7 | ||||
-rw-r--r-- | etc/functions.sh | 14 | ||||
-rw-r--r-- | etc/pip.conf | 2 | ||||
-rwxr-xr-x | start.sh | 98 |
11 files changed, 369 insertions, 514 deletions
@@ -1,5 +1,10 @@ *.pyc *.swp __pycache__ +pypirc +cache/ +logs/ src/ upload/ +wheelhouse/ +disthouse/ diff --git a/LICENSE.txt b/LICENSE.txt index 3ed9055..7372a15 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -6,16 +6,16 @@ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE diff --git a/README.md b/README.md new file mode 100644 index 0000000..e40eea8 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# wheeblast + +## Requirements + +- Docker + +## Using + +1. Edit `etc/config.sh` +2. Run `start.sh` +3. Drink coffee + +Wheels are placed in the `wheelhouse` and source distribution archives end up in `disthouse`. Both directories are automatically created for you. + +In order to upload packages you will need to install `twine` and configure `~/.pypirc` (or local configuration file) to point to an index of your choice. + +## Encouragement + +This is a proof of concept. Use at your own risk. + +Good luck. diff --git a/blast.py b/blast.py deleted file mode 100755 index 2315b92..0000000 --- a/blast.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env python -import contextlib -import os -import yaml -import subprocess -import sys -from glob import glob -from packaging.specifiers import SpecifierSet -from packaging.version import Version, InvalidVersion - - -USER_CONFIG = os.path.expanduser('~/.blast/config.yaml') -USER_CONFIG_EXISTS = os.path.exists(USER_CONFIG) -VERSION_BAD_PATTERNS = ['release_', '_release', 'release-', '-release'] -SPECIFIERS = ['~', '!', '<', '>', '='] - - -@contextlib.contextmanager -def use_directory(path): - prev = os.path.abspath(os.curdir) - os.chdir(path) - try: - yield - finally: - os.chdir(prev) - - -class PyEnv: - def __init__(self, version, venv): - self.version = version - self.venv = venv - self.environ = None - - def _cmd(self, *args, **kwargs): - if not args: - raise ValueError('Expecting arguments to pass to pyenv. ' - 'Got nothing.') - - command = [] - proc = None - shell = False - stdout = None - stderr = None - env = self.environ - - if kwargs.get('redirect', False): - stdout = subprocess.PIPE - stderr = subprocess.PIPE - - if kwargs.get('override', False): - command = ['pyenv'] - - for arg in args: - command.append(arg) - - if kwargs.get('shell', False): - shell = True - command = ' '.join(command) # convert to string - - try: - proc = subprocess.run(command, env=env, shell=shell, - stdout=stdout, stderr=stderr, - check=True, encoding='utf-8') - except subprocess.CalledProcessError as cpe: - if cpe.returncode: - print('Command (exit {}): {}'.format(cpe.returncode, command)) - return cpe - - return proc - - def run(self, *args, **kwargs): - proc = self._cmd(*args, **kwargs) - return proc - - def create(self): - if os.environ.get('PYENV_ROOT'): - if not os.path.exists(os.path.join(os.environ['PYENV_ROOT'], - 'versions', self.version)): - proc_inst = self._cmd('install', '-s', - self.version, override=True) - if proc_inst.stderr: - if 'already' not in proc_inst.stderr: - print(proc_inst.stderr) - exit(1) - - if not os.path.exists(os.path.join(os.environ['PYENV_ROOT'], - 'versions', self.venv)): - self._cmd('virtualenv', self.version, self.venv, override=True) - else: - raise RuntimeError('PYENV_ROOT is not defined.') - exit(1) - - def activate(self): - env = {} - command = 'eval "$(pyenv init -)" ' \ - '&& eval "$(pyenv virtualenv-init -)" ' \ - '&& pyenv activate "{}" ' \ - '&& pyenv rehash && printenv'.format(self.venv) - - proc = self._cmd(command, - shell=True, redirect=True) - - for record in proc.stdout.splitlines(): - if '=' not in record: - continue - k, v = record.split('=', 1) - env[k] = v - - self.environ = env.copy() - - -class Git: - def __init__(self, uri, root='.'): - self.uri = uri - self.root = os.path.abspath(root) - - if not os.path.exists(self.root): - print('Making {}'.format(self.root)) - os.makedirs(self.root) - - self.base = os.path.basename(self.uri).replace('.git', '') - self.basepath = os.path.abspath(os.path.join(self.root, self.base)) - self._tags = [] - - def _cmd(self, *args, **kwargs): - if not args: - raise ValueError('Expecting arguments to pass to Git. ' - 'Got nothing.') - - command = ['git'] - proc = None - - for arg in args: - command.append(arg) - - try: - proc = subprocess.run(command, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=True, - encoding='utf-8') - except subprocess.CalledProcessError as cpe: - print('Command (exit {}): {}'.format(cpe.returncode, command)) - print(cpe.stderr.strip()) - print(cpe) - exit(1) - - return proc - - def _has_repo(self): - if not os.path.exists('.git'): - raise FileNotFoundError('No git repository present.') - return True - - def clone(self): - if os.path.exists(self.basepath): - return self.basepath - - with use_directory(self.root): - self._cmd('clone', self.uri) - - return self.basepath - - def fetch(self, *args, **kwargs): - self._has_repo() - self._cmd('fetch', *args, *kwargs) - - def checkout(self, ref, **kwargs): - self._has_repo() - self._cmd('checkout', ref, **kwargs) - - def reset(self, *args, **kwargs): - self._has_repo() - self._cmd('reset', *args, **kwargs) - - def clean(self, *args, **kwargs): - self._has_repo() - self._cmd('clean', *args, **kwargs) - - def describe(self, *args, **kwargs): - self._has_repo() - result = self._cmd('describe', *args, **kwargs) - return result.stdout.strip() - - def tag_nearest(self): - self._has_repo() - return self.describe('--tags', '--abbrev=0') - - @property - def tags(self): - self._has_repo() - tags = [x.strip() for x in self._cmd('tag', quiet=True).stdout.splitlines()] - - if len(self._tags) == len(tags): - return self._tags - - self._tags = [] # Clear tags - for tag in tags: - self._tags.append(tag) - - return self._tags - - -def setuptools_inject(): - with open('setup.py', 'r') as script: - orig = script.read().splitlines() - - orig.insert(0, 'import setuptools') - - with open('setup.py', 'w+') as script: - new = '\n'.join(orig) - script.write(new) - - -def normalize_tag(tag, bad_patterns): - # Normalize non-standard tagging conventions - if bad_patterns is not None: - for pattern in bad_patterns: - tag = tag.replace(pattern, '') - - return tag - - -def eval_specifier(spec, tag, bad_patterns=None): - # Determine if specifiers are present in spec string - have_specifier = False - for ch in spec: - if ch in SPECIFIERS: - have_specifier = True - - # When no specifier is present we need to prepend one - # to satisfy SpecifierSet's basic input requirements - if not have_specifier: - spec = '==' + spec - - spec = SpecifierSet(spec) - tag = normalize_tag(tag) - - try: - tag = Version(tag) - except InvalidVersion as e: - print("{}".format(e), file=sys.stderr) - tag = '' - - return tag in spec - - -def dist_exists(root, mode, bad_patterns, *hints): - ext = '' - if mode == 'sdist': - ext = '.tar.gz' - elif mode == 'bdist_wheel': - ext = '.whl' - else: - raise NotImplementedError('Mode "{}" is not implemented.'.format(mode)) - - pattern = normalize_tag('*'.join(hints), bad_patterns) + '*' + ext - paths = glob(os.path.join(root, pattern)) - - if paths: - return True - - return False - - -if __name__ == '__main__': - # import argparse - # from pprint import pprint - - # parser = argparse.ArgumentParser() - # parser.add_argument('-c', '--config', action='store', default=USER_CONFIG) - # parser.add_argument('repofile', action='store') - - config = yaml.load(""" - host: https://github.com - organization: spacetelescope - - global: - python: - - 3.5.5 - - 3.6.6 - - 3.7.0 - - requires: - - wheel - - #version_latest: true - - version_bad_patterns: - - 'release_' - - '_release' - - 'release-' - - '-release' - - 'v' - - 'V' - - projects: - acstools: - requires: - - relic - - asdf: - requires: null - - calcos: - requires: null - - costools: - requires: null - - crds: - requires: - - astropy - - numpy - - requests - setuptools_inject: true - - drizzle: - requires: null - - drizzlepac: - requires: - - astropy - - numpy - - fitsblender: - requires: null - - imexam: - requires: null - - nictools: - requires: null - - pysynphot: - requires: null - - reftools: - requires: null - - relic: - requires: null - - stistools: - requires: null - - stsci.convolve: - requires: null - - stsci.distutils: - requires: null - - stsci.image: - requires: null - - stsci.imagemanip: - requires: null - - stsci.imagestats: - requires: null - - stsci.ndimage: - requires: null - - stsci.numdisplay: - requires: null - - stsci.stimage: - requires: null - - stsci.skypac: - requires: null - - stsci.sphere: - requires: null - - stsci.tools: - requires: null - - stregion: - requires: null - - stwcs: - requires: null - - wfpc2tools: - requires: null - - wfc3tools: - requires: null - """) - - output_dir = os.path.abspath(os.path.join(os.curdir, 'upload')) - - if config.get('output_dir'): - output_dir = os.path.abspath(config['output_dir']) - - if not os.path.exists(output_dir): - os.mkdir(output_dir) - - # Begin aliveness checks before we get too far - check_keys = ['global', 'host', 'organization', 'projects'] - failed_keys = False - failed_requires = False - - # Check general configuration keys - for key in check_keys: - if not config.get(key): - print('Error: The `{}` key is required.'.format(key), file=sys.stderr) - failed_keys = True - - if failed_keys: - exit(1) - - # Check structure of project dictionaries - for project, info in config['projects'].items(): - if info is None and not info.get('requires'): - print('Error: {}: Missing `requires` list'.format(project), file=sys.stderr) - failed_requires = True - - if failed_requires: - exit(1) - - # Perform matrix tasks - for project, info in config['projects'].items(): - url = '/'.join([config['host'], config['organization'], project]) - repo = Git(url, root='src') - - print('Repository: {}'.format(url)) - print('Source directory: {}'.format(repo.basepath)) - try: - repo.clone() - except FileNotFoundError as e: - print('Skipping "{}" due to: {}'.format(project, e)) - continue - - with use_directory(repo.basepath): - repo.fetch('--all', '--tags') - for python_version in config['global']['python']: - tags = [] - venv_name = 'py{}'.format(python_version.replace('.', '')) - - print('=> Using Python {} (virtual: {})...'.format(python_version, venv_name)) - pyenv = PyEnv(python_version, venv_name) - pyenv.create() - pyenv.activate() - - # Upgrade PIP - pyenv.run('pip', 'install', '-q', '--upgrade', 'pip', redirect=True) - - # Generic setup - if config.get('global'): - if config['global'].get('requires'): - for pkg in config['global']['requires']: - pyenv.run('pip', 'install', '-q', pkg, redirect=True) - - if config['global'].get('version_latest'): - repo.reset('--hard') - repo.clean('-f', '-x', '-d') - repo.checkout('master') - tags = [repo.tag_nearest()] - else: - tags = repo.tags - - if info.get('requires'): - for pkg in info['requires']: - pyenv.run('pip', 'install', '-q', pkg) - - for tag in tags: - sanitize = VERSION_BAD_PATTERNS - if info.get('build_versions'): - spec = info['build_versions'] - - if config['global'].get('version_bad_patterns'): - sanitize += config['global']['version_bad_patterns'] - - if info.get('version_bad_patterns'): - sanitize += info['version_bad_patterns'] - - if not eval_specifier(spec, tag, sanitize): - continue - - print('==> Building {}::{}'.format(project, tag)) - - repo.reset('--hard') - repo.clean('-f', '-x', '-d') - repo.checkout(tag) - - if info.get('setuptools_inject', False): - print('===> Overriding distutils with setuptools') - setuptools_inject() - - for build_command, build_args in [('sdist', []), - ('bdist_wheel', [])]: - print('===> {}::{}: {}: '.format(project, tag, build_command), end='') - if not dist_exists(output_dir, build_command, sanitize, project, tag): - proc = pyenv.run('python', 'setup.py', - build_command, '-d', output_dir, - *build_args, redirect=True) - if proc.stderr and proc.returncode: - print("FAILED") - print('#{}'.format('!' * 78)) - print(proc.stderr) - print('#{}'.format('!' * 78)) - elif not proc.returncode: - print("SUCCESS") - else: - print("EXISTS") diff --git a/build-project.sh b/build-project.sh new file mode 100755 index 0000000..569a4a0 --- /dev/null +++ b/build-project.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Global config +project="$1" +root=/static +logdir="${root}/logs" +sysconfdir="${root}/etc" +staging="${root}/staging" +wheelhouse="${root}/wheelhouse" +path_orig="${PATH}" + +# Build config +source "${sysconfdir}/config.sh" +source "${sysconfdir}/functions.sh" + +if [[ -z $project ]]; then + exit 255 +fi + +# Configure user to talk to artifactory (two-way) +pushd "${HOME}" &>/dev/null + msg2 "Configuring pip and setuptools..." + mkdir -p $HOME/.pip + [[ -f ${sysconfdir}/pip.conf ]] && cp -a ${sysconfdir}/pip.conf ${HOME}/.pip + [[ -f ${sysconfdir}/pypirc ]] && cp -a ${sysconfdir}/pypirc .pypirc +popd &>/dev/null + +# Enter build directory +msg2 "Entering build directory" +cd /io + +mkdir -p ${staging}/${project} + +# Install forced dependencies +# Our packages are tend to be very inconsistent +for build_env in "${envs[@]}"; do + PYBIN=/opt/python/${build_env}/bin + export PATH="${PYBIN}:${path_orig}" + hash -r + pip install -q -r ${sysconfdir}/dev-requirements.txt 1>/dev/null +done + +# Iterate through Python environments +msg2 "Building packages" +for build_env in "${envs[@]}"; do + PYBIN=/opt/python/${build_env}/bin + export PATH="${PYBIN}:${path_orig}" + hash -r + + python_version=$(python -V 2>&1 | awk '{ print $2 }') + project_version=$(git describe --long --tags 2>/dev/null || msg Unknown) + BYTE_COMPILE=$(find . -type f -iname '*.c' -o -iname '*.f') + + # Setup logging + logroot=${logdir}/${python_version}/${project}/${project_version} + mkdir -p "${logroot}" + + for dist in bdist_wheel sdist; do + msg3 "[${python_version}][${dist}][${project}-${project_version}]" + + python setup.py ${dist} -d ${staging}/${project} \ + 1>${logroot}/${dist}.stdout \ + 2>${logroot}/${dist}.stderr + + if [[ $? != 0 ]] && [[ ${dist} == bdist_wheel ]]; then + # Ahhhhhhhrrrrrrrgggg + pip wheel -w ${staging}/${project} ${dist} \ + 1>${logroot}/${dist}.stdout \ + 2>${logroot}/${dist}.stderr + fi + + # On failure, write log to console + if [[ $? != 0 ]]; then + cat ${logroot}/${dist}.stderr + fi + done + + # When not compiling Python extensions don't continue on + # to the next interpreter version + if [[ ! ${BYTE_COMPILE} ]]; then + break + fi +done + +echo diff --git a/check_upstream.py b/check_upstream.py new file mode 100755 index 0000000..116c17f --- /dev/null +++ b/check_upstream.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +import argparse +import os +import re +import requests +import sys + + +SERVER_ROOT = 'https://bytesalad.stsci.edu/artifactory/api/pypi' +DEFAULT_REPO = 'datb-pypi' +EXTS = ['.tar.gz', '.whl'] +NAME_RE = re.compile(r'([0-9A-Za-z_\.]+)-(.*)\.(whl|tar\.gz)') +# Match: ^Package_Name_^ ^Ver^ ^Ext______^ + +def check_name(name): + if NAME_RE.match(name): + return True + return False + + +def make_status(status, s): + return f'[{status:^7s}]: {s}' + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--repo', default=DEFAULT_REPO, + help='Named pypi repository') + parser.add_argument('--verbose', action='store_true') + parser.add_argument('package') + args = parser.parse_args() + + repo = args.repo + package = os.path.basename(args.package) + package_local = os.path.abspath(args.package) + pkg_temp = package + + if not os.path.exists(package_local): + print(f'Local file does exist: {package_local}', file=sys.stderr) + exit(2) + + if not check_name(package): + print(f'Non-conformant Python package: "{package}"', file=sys.stderr) + exit(3) + + # Drop file extensions + for ext in EXTS: + pkg_temp = pkg_temp.replace(ext, '') + + # Extract name and version data + name, version = pkg_temp.split('-', 1) + + # "Sanitize" data so URLs function correctly + name = name.replace('_', '-') # '-' and '_' exist; only '-' is discoverable + version = version.split('-', 1)[0] # Drop components of wheel package names + + # Construct upstream URL + url = '/'.join([SERVER_ROOT, repo, + 'packages', name, + version, package]) + + # Check upstream headers + with requests.head(url) as header: + if not header.ok and 'X-Checksum-Md5' not in header.headers: + if args.verbose: + print(make_status('MISSING', url), file=sys.stderr) + print(os.path.abspath(args.package)) + exit(1) + + if args.verbose: + print(make_status('OK', url), file=sys.stderr) + exit(0) diff --git a/etc/config.sh b/etc/config.sh new file mode 100644 index 0000000..65daccd --- /dev/null +++ b/etc/config.sh @@ -0,0 +1,57 @@ +tag_limit=1 +tag_regex='^\d+\.\d+?\.?\d+$' +docker_image=quay.io/pypa/manylinux1_x86_64 +envs=( + cp37-cp37m + cp36-cp36m +) +host=https://github.com +org=spacetelescope +projects=( + acstools + asdf + astroimtools + calcos + costools + crds + cubeviz + d2to1 + drizzle + drizzlepac + fitsblender + gwcs + imexam + mirage + mosviz + nictools + pandokia + photutils + poppy + pysynphot + reftools + refstis + relic + specviz + spherical_geometry + stginga + stistools + stsci.convolve + stsci.distutils + stsci.image + stsci.imagemanip + stsci.imagestats + stsci.ndimage + stsci.numdisplay + stsci.stimage + stsci.skypac + stsci.tools + stregion + stsynphot + stwcs + synphot_refactor + verhawk + webbpsf + wfpc2tools + wfc3tools + wss_tools +) diff --git a/etc/dev-requirements.txt b/etc/dev-requirements.txt new file mode 100644 index 0000000..d1dceb9 --- /dev/null +++ b/etc/dev-requirements.txt @@ -0,0 +1,7 @@ +numpy +astropy +cython +relic +wheel +#d2to1 +#stsci.distutils diff --git a/etc/functions.sh b/etc/functions.sh new file mode 100644 index 0000000..da68c0c --- /dev/null +++ b/etc/functions.sh @@ -0,0 +1,14 @@ +function msg +{ + printf '=> %s\n' "$@" +} + +function msg2 +{ + printf '==> %s\n' "$@" +} + +function msg3 +{ + printf ' -> %s\n' "$@" +} diff --git a/etc/pip.conf b/etc/pip.conf new file mode 100644 index 0000000..03d80a8 --- /dev/null +++ b/etc/pip.conf @@ -0,0 +1,2 @@ +[global] +index-url = https://bytesalad.stsci.edu/artifactory/api/pypi/datb-pypi-virtual/simple diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..696f5af --- /dev/null +++ b/start.sh @@ -0,0 +1,98 @@ +#!/bin/bash +root="$(pwd)" +build=/io +static=/static +srcdir=${root}/src +cache_left=${root}/cache +cache_right=/root/.cache + +staging=staging +wheelhouse=wheelhouse +disthouse=disthouse + +source "$(dirname ${BASH_SOURCE[0]})/etc/config.sh" +source "$(dirname ${BASH_SOURCE[0]})/etc/functions.sh" + +export GREP_MODE='-G' +if [[ $(uname) == Linux ]]; then + export GREP_MODE='-P' +elif [[ $(uname) == Darwin ]]; then + export GREP_MODE='-E' +fi + +for project in "${projects[@]}" +do + url="${host}/${org}/${project}" + dest="${srcdir}/${project}" + + if [[ ! -d ${dest} ]]; then + msg "Retrieving source for: ${project}" + git clone --recursive "${url}" "${dest}" &>/dev/null + if [[ $? != 0 ]]; then + msg2 "Failed to clone: ${url}" + continue + fi + fi + + pushd "${dest}" &>/dev/null + tags=$(git tag | grep ${GREP_MODE} ${tag_regex} | tail -n ${tag_limit}) + echo "Tags: ${tags}" + + for tag in $tags + do + git fetch --all &>/dev/null + git reset --hard &>/dev/null + git clean -ffxd &>/dev/null + git checkout "${tag}" &>/dev/null + + msg "Initializing Docker image: ${docker_image}" + docker run --rm -i -t \ + -v "${cache_left}:${cache_right}" \ + -v "${root}:${static}" \ + -v "${dest}:${build}" \ + "${docker_image}" ${static}/build-project.sh "${project}" + done + popd &>/dev/null +done + +wheels_binary=$(find ${staging} -type f -name '*cp*-cp*m*.whl') +wheels_universal=$(find ${staging} -type f -name '*-any.whl') +dists=$(find ${staging} -type f -name '*.tar.gz') + + +mkdir -p ${wheelhouse} +mkdir -p ${disthouse} + +if [[ $wheels_binary ]]; then + msg2 "Exporting binary wheels..." + for whl in $wheels_binary; do + docker run --rm -i -t \ + -v "${root}:${static}" \ + "${docker_image}" \ + auditwheel repair \ + "${static}/${whl}" \ + -w ${static}/wheelhouse + if [[ $? == 0 ]]; then + rm -f "${whl}" + fi + done +fi + +set +e + +# "auditwheel" wastes time for univeral wheels +if [[ $wheels_universal ]]; then + msg2 "Exporting universal wheels..." + for whl in $wheels_universal; do + msg3 "$(basename ${whl})" + mv "${whl}" "${wheelhouse}" + done +fi + +if [[ $dists ]]; then + msg2 "Exporting source dists..." + for dist in ${dists}; do + msg3 "$(basename "${dist}")" + mv "${dist}" "${disthouse}" + done +fi |