From a1b19e1d10896f3ac2ce0b97c2bf24e0c1c4520f Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Fri, 30 Mar 2018 19:47:52 -0400 Subject: CI integration --- .ci/appveyor.yml | 51 +++++++++++++++++++++++++++++ .ci/build-manylinux-wheels.sh | 29 +++++++++++++++++ .ci/package-version.py | 26 +++++++++++++++ .ci/pypi-check.py | 30 ++++++++++++++++++ .ci/requirements-win.txt | 1 + .ci/requirements.txt | 2 ++ .ci/s3-download-release.py | 74 +++++++++++++++++++++++++++++++++++++++++++ .ci/s3-upload.py | 62 ++++++++++++++++++++++++++++++++++++ .ci/travis-before-install.sh | 16 ++++++++++ .ci/travis-build-wheels.sh | 73 ++++++++++++++++++++++++++++++++++++++++++ .ci/travis-install.sh | 13 ++++++++ .ci/travis-release.sh | 55 ++++++++++++++++++++++++++++++++ .ci/travis-tests.sh | 16 ++++++++++ .travis.yml | 74 +++++++++++++++++++++++++++++++++++++++++++ LICENSE | 3 +- 15 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 .ci/appveyor.yml create mode 100755 .ci/build-manylinux-wheels.sh create mode 100755 .ci/package-version.py create mode 100755 .ci/pypi-check.py create mode 100644 .ci/requirements-win.txt create mode 100644 .ci/requirements.txt create mode 100755 .ci/s3-download-release.py create mode 100755 .ci/s3-upload.py create mode 100755 .ci/travis-before-install.sh create mode 100755 .ci/travis-build-wheels.sh create mode 100755 .ci/travis-install.sh create mode 100755 .ci/travis-release.sh create mode 100755 .ci/travis-tests.sh create mode 100644 .travis.yml diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml new file mode 100644 index 0000000..318b16b --- /dev/null +++ b/.ci/appveyor.yml @@ -0,0 +1,51 @@ +environment: + global: + S3_UPLOAD_USERNAME: oss-ci-bot + S3_UPLOAD_BUCKET: magicstack-oss-releases + S3_UPLOAD_ACCESSKEY: + secure: 1vmOqSXq5zDN8UdezZ3H4l0A9LUJiTr7Wuy9whCdffE= + S3_UPLOAD_SECRET: + secure: XudOvV6WtY9yRoqKahXMswFth8SF1UTnSXws4UBjeqzQUjOx2V2VRvIdpPfiqUKt + + matrix: + - PYTHON: "C:\\Python35\\python.exe" + - PYTHON: "C:\\Python35-x64\\python.exe" + - PYTHON: "C:\\Python36\\python.exe" + - PYTHON: "C:\\Python36-x64\\python.exe" + +branches: + # Avoid building PR branches. + only: + - master + - ci + - releases + +install: + - "%PYTHON% -m pip install --upgrade pip wheel setuptools" + - "%PYTHON% -m pip install --upgrade -r .ci/requirements-win.txt" + +build_script: + - "%PYTHON% setup.py build_ext --inplace" + +test_script: + - "%PYTHON% setup.py test" + +after_test: + - "%PYTHON% setup.py bdist_wheel" + +artifacts: + - path: dist\* + +deploy_script: + - ps: | + if ($env:appveyor_repo_branch -eq 'releases') { + $PACKAGE_VERSION = & "$env:PYTHON" ".ci/package-version.py" + $PYPI_VERSION = & "$env:PYTHON" ".ci/pypi-check.py" "immutables" + + if ($PACKAGE_VERSION -eq $PYPI_VERSION) { + Write-Error "immutables-$PACKAGE_VERSION is already published on PyPI" + exit 1 + } + + & "$env:PYTHON" ".ci/s3-upload.py" dist\*.whl + } diff --git a/.ci/build-manylinux-wheels.sh b/.ci/build-manylinux-wheels.sh new file mode 100755 index 0000000..6d78104 --- /dev/null +++ b/.ci/build-manylinux-wheels.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e -x + +# Compile wheels +PYTHON="/opt/python/${PYTHON_VERSION}/bin/python" +PIP="/opt/python/${PYTHON_VERSION}/bin/pip" +${PIP} install --upgrade pip wheel +${PIP} install --upgrade setuptools +${PIP} install -r /io/.ci/requirements.txt +make -C /io/ PYTHON="${PYTHON}" +${PIP} wheel /io/ -w /io/dist/ + +# Bundle external shared libraries into the wheels. +for whl in /io/dist/*.whl; do + auditwheel repair $whl -w /io/dist/ + rm /io/dist/*-linux_*.whl +done + +# Grab docker host, where Postgres should be running. +export PGHOST=$(ip route | awk '/default/ { print $3 }' | uniq) +export PGUSER="postgres" + +PYTHON="/opt/python/${PYTHON_VERSION}/bin/python" +PIP="/opt/python/${PYTHON_VERSION}/bin/pip" +${PIP} install ${PYMODULE} --no-index -f file:///io/dist +rm -rf /io/tests/__pycache__ +make -C /io/ PYTHON="${PYTHON}" testinstalled +rm -rf /io/tests/__pycache__ diff --git a/.ci/package-version.py b/.ci/package-version.py new file mode 100755 index 0000000..bbf2ff4 --- /dev/null +++ b/.ci/package-version.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + + +import os.path +import sys + + +def main(): + version_file = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + 'immutables', '__init__.py') + + with open(version_file, 'r') as f: + for line in f: + if line.startswith('__version__ ='): + _, _, version = line.partition('=') + print(version.strip(" \n'\"")) + return 0 + + print('could not find package version in immutables/__init__.py', + file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.ci/pypi-check.py b/.ci/pypi-check.py new file mode 100755 index 0000000..1b9c11c --- /dev/null +++ b/.ci/pypi-check.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + + +import argparse +import sys +import xmlrpc.client + + +def main(): + parser = argparse.ArgumentParser(description='PyPI package checker') + parser.add_argument('package_name', metavar='PACKAGE-NAME') + + parser.add_argument( + '--pypi-index-url', + help=('PyPI index URL.'), + default='https://pypi.python.org/pypi') + + args = parser.parse_args() + + pypi = xmlrpc.client.ServerProxy(args.pypi_index_url) + releases = pypi.package_releases(args.package_name) + + if releases: + print(next(iter(sorted(releases, reverse=True)))) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.ci/requirements-win.txt b/.ci/requirements-win.txt new file mode 100644 index 0000000..791da6b --- /dev/null +++ b/.ci/requirements-win.txt @@ -0,0 +1 @@ +tinys3 diff --git a/.ci/requirements.txt b/.ci/requirements.txt new file mode 100644 index 0000000..403ef59 --- /dev/null +++ b/.ci/requirements.txt @@ -0,0 +1,2 @@ +tinys3 +twine diff --git a/.ci/s3-download-release.py b/.ci/s3-download-release.py new file mode 100755 index 0000000..223f7f1 --- /dev/null +++ b/.ci/s3-download-release.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + + +import argparse +import os +import os.path +import sys +import urllib.request + +import tinys3 + + +def main(): + parser = argparse.ArgumentParser(description='S3 File Uploader') + parser.add_argument( + '--s3-bucket', + help=('S3 bucket name (defaults to $S3_UPLOAD_BUCKET)'), + default=os.environ.get('S3_UPLOAD_BUCKET')) + parser.add_argument( + '--s3-region', + help=('S3 region (defaults to $S3_UPLOAD_REGION)'), + default=os.environ.get('S3_UPLOAD_REGION')) + parser.add_argument( + '--s3-username', + help=('S3 username (defaults to $S3_UPLOAD_USERNAME)'), + default=os.environ.get('S3_UPLOAD_USERNAME')) + parser.add_argument( + '--s3-key', + help=('S3 access key (defaults to $S3_UPLOAD_ACCESSKEY)'), + default=os.environ.get('S3_UPLOAD_ACCESSKEY')) + parser.add_argument( + '--s3-secret', + help=('S3 secret (defaults to $S3_UPLOAD_SECRET)'), + default=os.environ.get('S3_UPLOAD_SECRET')) + parser.add_argument( + '--destdir', + help='Destination directory.') + parser.add_argument( + 'package', metavar='PACKAGE', + help='Package name and version to download.') + + args = parser.parse_args() + + if args.s3_region: + endpoint = 's3-{}.amazonaws.com'.format(args.s3_region.lower()) + else: + endpoint = 's3.amazonaws.com' + + conn = tinys3.Connection( + access_key=args.s3_key, + secret_key=args.s3_secret, + default_bucket=args.s3_bucket, + tls=True, + endpoint=endpoint, + ) + + files = [] + + for entry in conn.list(args.package): + files.append(entry['key']) + + destdir = args.destdir or os.getpwd() + + for file in files: + print('Downloading {}...'.format(file)) + url = 'https://{}/{}/{}'.format(endpoint, args.s3_bucket, file) + target = os.path.join(destdir, file) + urllib.request.urlretrieve(url, target) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.ci/s3-upload.py b/.ci/s3-upload.py new file mode 100755 index 0000000..92479af --- /dev/null +++ b/.ci/s3-upload.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + + +import argparse +import glob +import os +import os.path +import sys + +import tinys3 + + +def main(): + parser = argparse.ArgumentParser(description='S3 File Uploader') + parser.add_argument( + '--s3-bucket', + help=('S3 bucket name (defaults to $S3_UPLOAD_BUCKET)'), + default=os.environ.get('S3_UPLOAD_BUCKET')) + parser.add_argument( + '--s3-region', + help=('S3 region (defaults to $S3_UPLOAD_REGION)'), + default=os.environ.get('S3_UPLOAD_REGION')) + parser.add_argument( + '--s3-username', + help=('S3 username (defaults to $S3_UPLOAD_USERNAME)'), + default=os.environ.get('S3_UPLOAD_USERNAME')) + parser.add_argument( + '--s3-key', + help=('S3 access key (defaults to $S3_UPLOAD_ACCESSKEY)'), + default=os.environ.get('S3_UPLOAD_ACCESSKEY')) + parser.add_argument( + '--s3-secret', + help=('S3 secret (defaults to $S3_UPLOAD_SECRET)'), + default=os.environ.get('S3_UPLOAD_SECRET')) + parser.add_argument( + 'files', nargs='+', metavar='FILE', help='Files to upload') + + args = parser.parse_args() + + if args.s3_region: + endpoint = 's3-{}.amazonaws.com'.format(args.s3_region.lower()) + else: + endpoint = 's3.amazonaws.com' + + conn = tinys3.Connection( + access_key=args.s3_key, + secret_key=args.s3_secret, + default_bucket=args.s3_bucket, + tls=True, + endpoint=endpoint, + ) + + for pattern in args.files: + for fn in glob.iglob(pattern): + with open(fn, 'rb') as f: + conn.upload(os.path.basename(fn), f) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.ci/travis-before-install.sh b/.ci/travis-before-install.sh new file mode 100755 index 0000000..fdc9202 --- /dev/null +++ b/.ci/travis-before-install.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e -x + +if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv + PYENV_ROOT="$HOME/.pyenv" + PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + + if ! (pyenv versions | grep "${PYTHON_VERSION}$"); then + pyenv install ${PYTHON_VERSION} + fi + pyenv global ${PYTHON_VERSION} + pyenv rehash +fi diff --git a/.ci/travis-build-wheels.sh b/.ci/travis-build-wheels.sh new file mode 100755 index 0000000..92771bd --- /dev/null +++ b/.ci/travis-build-wheels.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -e -x + + +if [[ "${TRAVIS_BRANCH}" != "releases" || "${BUILD}" != *wheels* ]]; then + # Not a release + exit 0 +fi + + +if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + PYENV_ROOT="$HOME/.pyenv" + PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" +fi + +PACKAGE_VERSION=$(python ".ci/package-version.py") +PYPI_VERSION=$(python ".ci/pypi-check.py" "${PYMODULE}") + +if [ "${PACKAGE_VERSION}" == "${PYPI_VERSION}" ]; then + echo "${PYMODULE}-${PACKAGE_VERSION} is already published on PyPI" + exit 1 +fi + + +pushd $(dirname $0) > /dev/null +_root=$(dirname $(pwd -P)) +popd > /dev/null + + +_upload_wheels() { + python "${_root}/.ci/s3-upload.py" "${_root}/dist"/*.whl + sudo rm -rf "${_root}/dist"/*.whl +} + + +if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + for pyver in ${RELEASE_PYTHON_VERSIONS}; do + ML_PYTHON_VERSION=$(python3 -c \ + "print('cp{maj}{min}-cp{maj}{min}m'.format( \ + maj='${pyver}'.split('.')[0], \ + min='${pyver}'.split('.')[1]))") + + for arch in x86_64 i686; do + ML_IMAGE="quay.io/pypa/manylinux1_${arch}" + docker pull "${ML_IMAGE}" + docker run --rm \ + -v "${_root}":/io \ + -e "PYMODULE=${PYMODULE}" \ + -e "PYTHON_VERSION=${ML_PYTHON_VERSION}" \ + "${ML_IMAGE}" /io/.ci/build-manylinux-wheels.sh + + _upload_wheels + done + done + +elif [ "${TRAVIS_OS_NAME}" == "osx" ]; then + export PGINSTALLATION="/usr/local/opt/postgresql@${PGVERSION}/bin" + + make clean && make -C "${_root}" + pip wheel "${_root}" -w "${_root}/dist/" + + pip install ${PYMODULE} --no-index -f "file:///${_root}/dist" + pushd / >/dev/null + make -C "${_root}" testinstalled + popd >/dev/null + + _upload_wheels + +else + echo "Cannot build on ${TRAVIS_OS_NAME}." +fi diff --git a/.ci/travis-install.sh b/.ci/travis-install.sh new file mode 100755 index 0000000..e9715ee --- /dev/null +++ b/.ci/travis-install.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e -x + +if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + PYENV_ROOT="$HOME/.pyenv" + PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" +fi + +pip install --upgrade pip wheel +pip install --upgrade setuptools +pip install --upgrade -r .ci/requirements.txt diff --git a/.ci/travis-release.sh b/.ci/travis-release.sh new file mode 100755 index 0000000..60d1c62 --- /dev/null +++ b/.ci/travis-release.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e -x + + +if [ -z "${TRAVIS_TAG}" ]; then + # Not a release + exit 0 +fi + + +PACKAGE_VERSION=$(python ".ci/package-version.py") +PYPI_VERSION=$(python ".ci/pypi-check.py" "${PYMODULE}") + +if [ "${PACKAGE_VERSION}" == "${PYPI_VERSION}" ]; then + echo "${PYMODULE}-${PACKAGE_VERSION} is already published on PyPI" + exit 0 +fi + +# Check if all expected wheels have been built and uploaded. +release_platforms=( + "macosx_10_??_x86_64" + "manylinux1_i686" + "manylinux1_x86_64" + "win32" + "win_amd64" +) + +P="${PYMODULE}-${PACKAGE_VERSION}" +expected_wheels=() + +for pyver in ${RELEASE_PYTHON_VERSIONS}; do + pyver="${pyver//./}" + abitag="cp${pyver}-cp${pyver}m" + for plat in "${release_platforms[@]}"; do + expected_wheels+=("${P}-${abitag}-${plat}.whl") + done +done + +rm -rf dist/*.whl dist/*.tar.* +python setup.py sdist +python ".ci/s3-download-release.py" --destdir=dist/ "${P}" + +_file_exists() { [[ -f $1 ]]; } + +for distfile in "${expected_wheels[@]}"; do + if ! _file_exists dist/${distfile}; then + echo "Expected wheel ${distfile} not found." + exit 1 + fi +done + +python -m twine upload dist/*.whl dist/*.tar.* + +.ci/travis-publish-docs.sh diff --git a/.ci/travis-tests.sh b/.ci/travis-tests.sh new file mode 100755 index 0000000..bf8f5ad --- /dev/null +++ b/.ci/travis-tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e -x + +if [[ "${BUILD}" != *tests* ]]; then + echo "Skipping tests." + exit 0 +fi + +if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + PYENV_ROOT="$HOME/.pyenv" + PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" +fi + +python setup.py test diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ebafe4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,74 @@ +language: generic + +env: + global: + - PYMODULE=immutables + - RELEASE_PYTHON_VERSIONS="3.5 3.6" + + - S3_UPLOAD_USERNAME=oss-ci-bot + - S3_UPLOAD_BUCKET=magicstack-oss-releases + # S3_UPLOAD_ACCESSKEY: + - secure: "iU37gukuyeaYM69StkR/aUTNgolblBdw2is034evvrm/SG0bKyzVVSrcK/dts9jolkCxJi+01VfpzxIBu2PF11QnCN1exUILb+XfmR+dVxUnNY2M1qqjILHvQ92rFJ9f2TlbYa2AlwgKynZlY4+edVSACSWwD/+TbWGAQEp0WInalA8ohljir+EPueXaYyC8mmH55cNQIa5WdDA2Vpg5ahRDdhVyD2J+/fLg78syLV7FGlnpXtASo9XiQKmRpPyHIT23yQB444kVh9xcjvuiB3aUBP5bGC2H4unElGYhCvfQvb1GoWvDqyvfzZvTOjlHqnG4AvIPoSCgEu/9cu8Cm/9OxWtqtWy7dECM8ZUIlOi3oPcvwUYDpNYAdATbTr1T6FRCBEp2eOi3sKoeE+nUDgQaN4r+ple4BKYnjrsSllXhI5W8ZqDNoUSsoGu+z6GFn6Dszrj6jbq8JHV4mZT9RCfR1y6inXWYGmaNRlwzm8wPHTav2RbW2O6bbwkkATWwYpyRB2FRlwMX6BB06druZWNOzx09RS8pTHnqcKOXW2mENNMgrA03OJUEV30UG/ncLZELYTpBARSJwymxjmmTK7vEI/HfxHkPrKcLLPPn2GoWym7mF2Lkh+jp81FkCGYrLTquyKPaoeUsofYukWMbGwE99ePL5dLocVDqTzatAoU=" + # S3_UPLOAD_SECRET: + - secure: "uCcM67fmT3ODhaHrUKhuid/DJZzy9aMtaVdRCkAkjaF/X0Dgj1Dq9bV/PPLn+HjVIRiloHKK4fk6eMLmnI/PWPnTD7hVYse2ECLEeTxZGtoBmTdBPzdf1+aHpl18o8xbN/XfO02SVk6pTHDSjZIwR8ZeiZ2Q75aY462BW9WBgV0JOL9xK5yJd3TjODsDJpwcK0P4TMwi1j2qKIpXMUJaZkyUPafZIykil2CbcePd2Y5AUfDN2BAqaJZqM9vVCeRRs7ECzCamBPsj2WUmXqs621IH3Ml/sSONCzeQoUlgUsG2a7b+Jic92sVsFHyLVqG56G5urIAsXm+Jc/8Ly/dTk1M3ER/OdvsB0z21mhQfaVHwROixPk6HPCbvTl3PITEauaU+wLwCIduiEbb6fcpoB11n3oRzgiLY5e4+QDA86LBNySDhBE8WIq1VKphgTp7ojgM/mHJg4VBZX3m+89JruUOLi49VPx1cK/CiWEBj3gWHZMNDL9agS5N/fwl6UnD5DAklTZtqlA5M2FZ8/aPN8/FgW4jTEgBBU87Ko2rTvVRmKZeCVEkIBS2lYsRDTG3ZmlyJuh2AGGReUzCh524pNAsonIF2ydCOzLv4DlTZSthOwbdnX0EMBRYuPEa436dgkVUUVP6ds5859IPZeXcN6JKJWPWQkzFWFwzoK9ttQLc=" + + - TWINE_USERNAME: magicstack-ci + # TWINE_PASSWORD: + - secure: "jyc9xHK3VjGPxvBZKx8Mcf5nfVvfIyGn6b4atcrmwVdJsV1bBLdKoAjUX3RGjNGyAHpNYOEKOdNfeZs+Wziwg5NK7ucC5qybaBK3MOTEOInCzaO0QJpcxThaHBQkkDxVtn8Qu1Gk3S/hXcXWjT2UEYJvQ84diaXn/XYRxfzOYTZX8eUroAWOMnUCYxlPxGzXTAtmuQSiJkL7P7veZTsWsGCOHtCpdAx7dgGb113CD8QheeUoZlH9Ml6jd3fGFteYmuFp7cR6fa3VYVzxp5BFsdEJqSI4VqDvBOpUoLkbpRRKMjosHKtphfi0PAzbkJw6UdKcrqQ/Ca4nGmWk0PIf3LTsJrv44p4ZTPVI8b3lihXMm72QUE28e11yu9SIZRe0hMgmvWlivXEJw3C3YT1N5w+JM3Y5dIWp/YLoiRXVkIzNJQMN3YeWvKEFf/xO1AD2BO3jjU9oBZfKQpxCJ58gPsQrRt6qM3Y6zYuF8s4B+llpwM/ex2xnNwrTbNkp4ARyXyCujX+ixhjiBLtElfGoHPP1jOaIkJhGje9DxaptddfFBDLAdq0/3Q+LHOmwdQcH5+libUy3HnyP7jf51kjjWE3XEJGSchHI2ewEAn9UZRH8h0UNRXutBzUVvKgC6K1lUvqzEreKVxvrYe6zgbZc/DiUvLgIzJBiJgP9rdZYpDQ=" + +branches: + # Avoid building PR branches. + only: + - master + - ci + - releases + - /^v\d+(\.\d+)*$/ + +matrix: + fast_finish: + true + + include: + - os: linux + dist: trusty + sudo: false + language: python + python: "3.5" + env: BUILD=tests + + - os: linux + dist: trusty + sudo: false + language: python + python: "3.6" + env: BUILD=tests,wheels,release + + - os: osx + env: BUILD=tests,wheels PYTHON_VERSION=3.5.4 PIP_USER=1 + + - os: osx + osx_image: xcode8.3 + env: BUILD=tests,wheels PYTHON_VERSION=3.6.4 PIP_USER=1 + + allow_failures: + - python: "3.7-dev" + +cache: + pip + +before_install: + - .ci/travis-before-install.sh + +install: + - .ci/travis-install.sh + +script: + - .ci/travis-tests.sh + - .ci/travis-build-wheels.sh + +deploy: + provider: script + script: .ci/travis-release.sh + on: + tags: true + condition: '"${BUILD}" == *release*' diff --git a/LICENSE b/LICENSE index bc9b4ed..5ce98d7 100644 --- a/LICENSE +++ b/LICENSE @@ -188,8 +188,7 @@ Copyright (C) 2017-present MagicStack Inc. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (C) 2016-present the asyncpg authors and contributors - + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -- cgit v1.2.3