├── .coveragerc ├── .github └── FUNDING.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── conftest.py ├── pip_custom_platform ├── __init__.py ├── _main.py ├── default_platform.py ├── main.py └── pymonkey.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── testing ├── __init__.py ├── project_with_c │ ├── project_with_c.c │ └── setup.py ├── pure_py_project │ ├── pure_python_project.py │ ├── setup.cfg │ └── setup.py ├── uses_pip │ ├── setup.cfg │ ├── setup.py │ └── uses_pip.py └── util.py ├── tests ├── __init__.py ├── default_platform_test.py ├── main_test.py └── testing │ └── project_with_c_test.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = . 4 | parallel = True 5 | omit = 6 | .tox/* 7 | /usr/* 8 | setup.py 9 | 10 | [report] 11 | exclude_lines = 12 | # Have to re-enable the standard pragma 13 | \#\s*pragma: no cover 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | ^\s*raise AssertionError\b 17 | ^\s*raise NotImplementedError\b 18 | ^\s*return NotImplemented\b 19 | ^\s*raise$ 20 | 21 | # Don't complain if non-runnable code isn't run: 22 | ^if __name__ == ['"]__main__['"]:$ 23 | 24 | [html] 25 | directory = coverage-html 26 | 27 | # vim:ft=dosini 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: asottile 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.iml 3 | *.py[co] 4 | .*.sw[a-z] 5 | .coverage 6 | .coverage.* 7 | .idea 8 | .project 9 | .pydevproject 10 | .tox 11 | .venv.touch 12 | /venv* 13 | coverage-html 14 | dist 15 | /.pytest_cache 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-docstring-first 8 | - id: check-yaml 9 | - id: debug-statements 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 3.9.2 14 | hooks: 15 | - id: flake8 16 | - repo: https://github.com/pre-commit/mirrors-autopep8 17 | rev: v1.5.7 18 | hooks: 19 | - id: autopep8 20 | - repo: https://github.com/asottile/reorder_python_imports 21 | rev: v2.5.0 22 | hooks: 23 | - id: reorder-python-imports 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | this is deprecated without replacement. perhaps encourage pip to implement 4 | [pypa/pip#5453] 5 | 6 | [pypa/pip#5453]: https://github.com/pypa/pip/issues/5453 7 | 8 | ___ 9 | 10 | [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.pip-custom-platform?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=60&branchName=master) 11 | [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/60/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=60&branchName=master) 12 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/asottile/pip-custom-platform/master.svg)](https://results.pre-commit.ci/latest/github/asottile/pip-custom-platform/master) 13 | 14 | pip-custom-platform 15 | =================== 16 | 17 | [pip][pip]+[wheel][wheel] wrapper which allows you to choose a custom platform 18 | name for building, downloading, and installing wheels. 19 | 20 | This package assumes you're running your own PyPI server and would like 21 | support for wheels on named platforms that would otherwise be considered 22 | equivalent by the wheel infrastructure (for example not all linux_x86_64 are 23 | created equal). 24 | 25 | ## Default platform names 26 | 27 | By default, pip-custom-platform guesses a platform name for you based on the 28 | `distro` module for Linux, and uses the default platform name on Windows, OS 29 | X, or other systems. Some examples: 30 | 31 | | Platform | Default Platform Name | 32 | |-------------------------|----------------------------| 33 | | Ubuntu Trusty (14.04) | linux_ubuntu_14_04_x86_64 | 34 | | Debian Jessie (8) | linux_debian_8_x86_64 | 35 | | CentOS 7 | linux_centos_7_x86_64 | 36 | | Fedora 22 | linux_fedora_22_x86_64 | 37 | | Red Hat 7 | linux_rhel_7_x86_64 | 38 | | openSUSE 13.2 | linux_opensuse_13_x86_64 | 39 | 40 | You can choose your own platform name by passing `--platform my_platform` on 41 | the command line. 42 | 43 | ## Installation 44 | 45 | `pip install pip-custom-platform` 46 | 47 | ## Usage 48 | 49 | ### Building wheels 50 | 51 | `pip-custom-platform wheel --platform my-platform my-package` 52 | 53 | ### Downloading distributions 54 | 55 | `pip-custom-platform install --platform my-platform --download . my-package` 56 | 57 | (or with sufficiently new pip) 58 | 59 | `pip-custom-platform download --platform my-platform --dest . my-package` 60 | 61 | ### Installing packages 62 | 63 | `pip-custom-platform install --platform my-platform my-package` 64 | 65 | 66 | [pip]: https://github.com/pypa/pip 67 | [wheel]: https://bitbucket.org/pypa/wheel 68 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: [master, test-me-*] 4 | tags: 5 | include: ['*'] 6 | 7 | resources: 8 | repositories: 9 | - repository: asottile 10 | type: github 11 | endpoint: github 12 | name: asottile/azure-pipeline-templates 13 | ref: refs/tags/v2.1.0 14 | 15 | jobs: 16 | - template: job--python-tox.yml@asottile 17 | parameters: 18 | toxenvs: [pypy, pypy3, py27, py36, py37, py38] # , latest_pip] 19 | os: linux 20 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | # The Docker tests are ridiculously slow (~1 hour), take a bunch of disk 3 | # space, and are flaky by nature. So let's not run them by default (we have 4 | # mocks as well as the Docker integration tests). 5 | parser.addoption( 6 | '--docker', 7 | action='store_true', 8 | default=False, 9 | help='Run Docker tests (very slow, takes lots of disk space)', 10 | ) 11 | -------------------------------------------------------------------------------- /pip_custom_platform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile-archive/pip-custom-platform/345e5efcdfe0856388982045343f6b724dae9697/pip_custom_platform/__init__.py -------------------------------------------------------------------------------- /pip_custom_platform/_main.py: -------------------------------------------------------------------------------- 1 | """Build packages to a wheel with a custom platform name""" 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import argparse 7 | import contextlib 8 | import distutils.util 9 | import os 10 | import shutil 11 | import sys 12 | import tempfile 13 | 14 | try: # pragma: no cover (pip>=19.3) 15 | from pip._internal.commands import commands_dict 16 | 17 | def get_summaries(): 18 | return ((k, v.summary) for k, v in commands_dict.items()) 19 | except ImportError: # pragma: no cover (pip<19.3) 20 | try: # pragma: no cover (pip>=10) 21 | from pip._internal.commands import get_summaries 22 | except ImportError: # pragma: no cover (pip<10) 23 | from pip.commands import get_summaries 24 | 25 | 26 | @contextlib.contextmanager 27 | def tmpdir(): 28 | tempdir = tempfile.mkdtemp() 29 | try: 30 | yield tempdir 31 | finally: 32 | shutil.rmtree(tempdir) 33 | 34 | 35 | def mkdirp(path): 36 | try: 37 | os.makedirs(path) 38 | except OSError: 39 | if not os.path.isdir(path): 40 | raise 41 | 42 | 43 | def _wheel(wheel_dir, pip_main, pip_args): 44 | mkdirp(wheel_dir) 45 | # Wheels will always be build with the default platform (due to pip 46 | # subprocessing to build the wheel). 47 | # Do this in a tempdir in case there are already wheels in the output 48 | # directory 49 | with tmpdir() as tempdir: 50 | ret = pip_main(['wheel', '--wheel-dir', tempdir] + pip_args) 51 | if ret: 52 | return ret 53 | 54 | # Then rename any of the platform-specific wheels created 55 | for wheel_filename in os.listdir(tempdir): 56 | if not wheel_filename.endswith('-any.whl'): 57 | before, _ = wheel_filename.rsplit('-', 1) 58 | new_wheel_filename = '{}-{}.whl'.format( 59 | before, distutils.util.get_platform(), 60 | ) 61 | else: 62 | new_wheel_filename = wheel_filename 63 | dst = os.path.join(wheel_dir, new_wheel_filename) 64 | 65 | # And copy to the output directory 66 | shutil.copy(os.path.join(tempdir, wheel_filename), dst) 67 | 68 | 69 | def _show_platform_name(): 70 | print(distutils.util.get_platform()) 71 | return 0 72 | 73 | 74 | def get_main(pip_main): 75 | def main(argv=None): 76 | argv = argv if argv is not None else sys.argv[1:] 77 | 78 | def _add_platform_param(parser): 79 | parser.add_argument( 80 | '--platform', help=( 81 | 'Custom platform name. The default is auto-detected -- ' 82 | 'Use `pip-custom-platform show-platform-name` to show.' 83 | ), 84 | ) 85 | 86 | parser = argparse.ArgumentParser( 87 | prog='pip-custom-platform', 88 | description=( 89 | 'pip+wheel wrapper which allows you to choose a custom ' 90 | 'platform name for building, downloading, and installing ' 91 | 'wheels.\n\n' 92 | 'Any unparsed command arguments will be passed on to pip\n' 93 | ), 94 | ) 95 | subparsers = parser.add_subparsers(dest='command') 96 | subparsers.required = True 97 | 98 | for cmd, summary in get_summaries(): 99 | subparser = subparsers.add_parser(cmd, help=summary) 100 | if cmd in ('install', 'download', 'wheel'): 101 | _add_platform_param(subparser) 102 | if cmd == 'wheel': 103 | subparser.add_argument( 104 | '-w', '--wheel-dir', default='./wheelhouse', 105 | help='Build wheels into this directory', 106 | ) 107 | 108 | platform_name = subparsers.add_parser( 109 | 'show-platform-name', help='Show the default platform name', 110 | ) 111 | _add_platform_param(platform_name) 112 | 113 | args, rest = parser.parse_known_args(argv) 114 | if args.command == 'wheel': 115 | return _wheel(args.wheel_dir, pip_main, rest) 116 | elif args.command == 'show-platform-name': 117 | return _show_platform_name() 118 | else: 119 | return pip_main([args.command] + rest) 120 | return main 121 | -------------------------------------------------------------------------------- /pip_custom_platform/default_platform.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | import platform 5 | import re 6 | 7 | import distro 8 | 9 | 10 | def _sanitize_platform(platform_name): 11 | """Platform names must only be alphanumeric with underscores""" 12 | return re.sub('[^a-z0-9_]', '_', platform_name.lower()) 13 | 14 | 15 | def _default_platform_name(distutils_util_get_platform): 16 | """Guess a sane default platform name. 17 | 18 | On OS X and Windows, just uses the default platform name. On Linux, uses 19 | information from the `platform` module to try to make something reasonable. 20 | """ 21 | def grab_version(string, num): 22 | """Grab the `num` most significant components of a version string. 23 | 24 | >>> grab_version('12.04.1', 2) 25 | '12.04' 26 | >>> grab_version('8.2', 1) 27 | '8' 28 | """ 29 | return '.'.join(string.split('.')[:num]) 30 | 31 | if platform.system() == 'Linux': 32 | dist, version = distro.id(), distro.version() 33 | dist = re.sub('linux$', '', dist.lower()).strip() 34 | 35 | # Try to determine a good "release" name. This is highly dependent on 36 | # distribution and what guarantees they provide between versions. 37 | release = None 38 | 39 | if dist in {'debian', 'rhel', 'centos', 'fedora', 'opensuse'}: 40 | release = grab_version(version, 1) # one version component 41 | elif dist in {'ubuntu', 'amzn', 'alpine'}: 42 | release = grab_version(version, 2) # two version components 43 | 44 | if release: 45 | return 'linux_{dist}_{release}_{arch}'.format( 46 | dist=_sanitize_platform(dist), 47 | release=_sanitize_platform(release), 48 | arch=_sanitize_platform(platform.machine()), 49 | ) 50 | 51 | # For Windows, OS X, or Linux distributions we couldn't identify, just fall 52 | # back to whatever pip normally uses. 53 | return _sanitize_platform(distutils_util_get_platform()) 54 | 55 | 56 | def get_platform_func(args, distutils_util_get_platform): 57 | if args.platform: 58 | platform_name = _sanitize_platform(args.platform) 59 | else: 60 | platform_name = _default_platform_name(distutils_util_get_platform) 61 | return lambda: platform_name 62 | -------------------------------------------------------------------------------- /pip_custom_platform/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | from pymonkey import make_entry_point 5 | 6 | main = make_entry_point(('pip-custom-platform',), 'pip') 7 | 8 | if __name__ == '__main__': 9 | exit(main()) 10 | -------------------------------------------------------------------------------- /pip_custom_platform/pymonkey.py: -------------------------------------------------------------------------------- 1 | def pymonkey_argparse(argv): 2 | # We want to parse --platform out as early as possible so we can do patches 3 | # based on it 4 | import argparse 5 | parser = argparse.ArgumentParser(add_help=False) 6 | parser.add_argument('--platform') 7 | return parser.parse_known_args(argv) 8 | 9 | 10 | def pymonkey_patch(mod, args): 11 | if mod.__name__ == 'distutils.util': 12 | from pip_custom_platform.default_platform import get_platform_func 13 | mod.get_platform = get_platform_func(args, mod.get_platform) 14 | elif mod.__name__ in ('pip.pep425tags', 'pip._internal.pep425tags'): 15 | from pip_custom_platform.default_platform import get_platform_func 16 | mod.get_platform = get_platform_func(args, mod.get_platform) 17 | mod.supported_tags = mod.get_supported() 18 | mod.supported_tags_noarch = mod.get_supported(noarch=True) 19 | elif mod.__name__ in ('pip', 'pip._internal') and hasattr(mod, 'main'): 20 | from pip_custom_platform._main import get_main 21 | mod.main = get_main(mod.main) 22 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -e testing/uses_pip 3 | coverage 4 | mock 5 | # pip-custom-platform currently does not work with pip>=19.3 6 | pip<19.3 7 | pytest 8 | # pypy's wheel tag changed in pip 20 / wheel 34 9 | wheel<0.34 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pip_custom_platform 3 | version = 0.5.0 4 | description = pip + wheel wrapper which allows you to choose a custom platform name for building, downloading, and installing wheels. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/asottile/pip-custom-platform 8 | author = Anthony Sottile 9 | author_email = asottile@umich.edu 10 | license = MIT 11 | license_file = LICENSE 12 | classifiers = 13 | License :: OSI Approved :: MIT License 14 | Programming Language :: Python :: 2 15 | Programming Language :: Python :: 2.7 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.4 18 | Programming Language :: Python :: 3.5 19 | Programming Language :: Python :: 3.6 20 | Programming Language :: Python :: 3.7 21 | Programming Language :: Python :: Implementation :: CPython 22 | Programming Language :: Python :: Implementation :: PyPy 23 | 24 | [options] 25 | packages = find: 26 | install_requires = 27 | distro>=1.2.0 28 | pip 29 | wheel 30 | pymonkey>=0.2.2 31 | python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* 32 | 33 | [options.entry_points] 34 | console_scripts = 35 | pip-custom-platform = pip_custom_platform.main:main 36 | pymonkey = 37 | pip-custom-platform = pip_custom_platform.pymonkey 38 | pymonkey.argparse = 39 | pip-custom-platform = pip_custom_platform.pymonkey 40 | 41 | [options.packages.find] 42 | exclude = 43 | tests* 44 | testing* 45 | 46 | [bdist_wheel] 47 | universal = true 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup() 3 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile-archive/pip-custom-platform/345e5efcdfe0856388982045343f6b724dae9697/testing/__init__.py -------------------------------------------------------------------------------- /testing/project_with_c/project_with_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static PyObject* _hello_world(PyObject* self) { 4 | return PyUnicode_FromString("hello world"); 5 | } 6 | 7 | static struct PyMethodDef methods[] = { 8 | {"hello_world", (PyCFunction)_hello_world, METH_NOARGS}, 9 | {NULL, NULL} 10 | }; 11 | 12 | #if PY_MAJOR_VERSION >= 3 13 | static struct PyModuleDef module = { 14 | PyModuleDef_HEAD_INIT, 15 | "project_with_c", 16 | NULL, 17 | -1, 18 | methods 19 | }; 20 | 21 | PyMODINIT_FUNC PyInit_project_with_c(void) { 22 | return PyModule_Create(&module); 23 | } 24 | #else 25 | PyMODINIT_FUNC initproject_with_c(void) { 26 | Py_InitModule3("project_with_c", methods, NULL); 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /testing/project_with_c/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import Extension 2 | from setuptools import setup 3 | 4 | 5 | setup( 6 | name='project_with_c', 7 | version='0.1.0', 8 | ext_modules=[Extension('project_with_c', ['project_with_c.c'])], 9 | ) 10 | -------------------------------------------------------------------------------- /testing/pure_py_project/pure_python_project.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile-archive/pip-custom-platform/345e5efcdfe0856388982045343f6b724dae9697/testing/pure_py_project/pure_python_project.py -------------------------------------------------------------------------------- /testing/pure_py_project/setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = True 3 | -------------------------------------------------------------------------------- /testing/pure_py_project/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='pure_python_project', 6 | version='0.1.0', 7 | py_modules=['pure_python_project'], 8 | ) 9 | -------------------------------------------------------------------------------- /testing/uses_pip/setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = True 3 | -------------------------------------------------------------------------------- /testing/uses_pip/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='uses_pip', 6 | version='0.1.0', 7 | py_modules=['uses_pip'], 8 | entry_points={'console_scripts': ['uses-pip = uses_pip:main']}, 9 | ) 10 | -------------------------------------------------------------------------------- /testing/uses_pip/uses_pip.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | try: # pragma: no cover (pip>=10) 5 | from pip._internal import main as pip_main 6 | except ImportError: # pragma: no cover (pip<10) 7 | from pip import main as pip_main 8 | 9 | 10 | def main(): 11 | findlinks, download_dest, pkg, pkgname = sys.argv[1:] 12 | assert not pip_main(['wheel', pkg, '--wheel-dir', findlinks]) 13 | os.environ.pop('PIP_REQ_TRACKER', None) # not reentrant 14 | assert not pip_main([ 15 | 'download', 16 | '--dest', download_dest, 17 | '--find-links', 'file://{}'.format(findlinks), 18 | '--no-index', 19 | pkgname, 20 | ]) 21 | -------------------------------------------------------------------------------- /testing/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | from wheel.pep425tags import get_abbr_impl 5 | from wheel.pep425tags import get_abi_tag 6 | from wheel.pep425tags import get_impl_ver 7 | 8 | 9 | def expected_wheel_name(fmt): 10 | return fmt.format(get_abbr_impl() + get_impl_ver(), get_abi_tag()) 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile-archive/pip-custom-platform/345e5efcdfe0856388982045343f6b724dae9697/tests/__init__.py -------------------------------------------------------------------------------- /tests/default_platform_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import unicode_literals 3 | 4 | import collections 5 | import distutils.util 6 | import functools 7 | import os.path 8 | import subprocess 9 | 10 | import mock 11 | import pytest 12 | 13 | from pip_custom_platform.default_platform import _default_platform_name 14 | from pip_custom_platform.default_platform import _sanitize_platform 15 | 16 | 17 | default_platform_name = functools.partial( 18 | _default_platform_name, distutils.util.get_platform, 19 | ) 20 | 21 | 22 | SETUP_DEBIAN = ( 23 | 'apt-get update -qq', 24 | 'DEBIAN_FRONTEND=noninteractive apt-get install -qq -y ' + 25 | ' --no-install-recommends python python-pip >&2' 26 | ) 27 | SETUP_NO_PIP_PACKAGE = ( 28 | # for systems with no pip system package (or RHEL which wants $$$$) 29 | 'curl https://asottile.github.io/get-virtualenv.py | python - venv >&2', 30 | '. venv/bin/activate', 31 | ) 32 | 33 | SystemTestCase = collections.namedtuple('SystemTestCase', ( 34 | 'docker_image', 35 | 'mock_id', 36 | 'mock_version', 37 | 'expected_platform_name', 38 | 'setup_script', 39 | )) 40 | 41 | SYSTEM_TESTCASES = [ 42 | SystemTestCase( 43 | docker_image='ubuntu:trusty', 44 | mock_id='ubuntu', 45 | mock_version='14.04', 46 | expected_platform_name='linux_ubuntu_14_04_x86_64', 47 | setup_script=SETUP_DEBIAN, 48 | ), 49 | SystemTestCase( 50 | docker_image='debian:jessie', 51 | mock_id='debian', 52 | mock_version='8', 53 | expected_platform_name='linux_debian_8_x86_64', 54 | setup_script=SETUP_DEBIAN, 55 | ), 56 | SystemTestCase( 57 | docker_image='centos:centos7', 58 | mock_id='centos', 59 | mock_version='7', 60 | expected_platform_name='linux_centos_7_x86_64', 61 | setup_script=SETUP_NO_PIP_PACKAGE, 62 | ), 63 | SystemTestCase( 64 | docker_image='fedora:22', 65 | mock_id='fedora', 66 | mock_version='22', 67 | expected_platform_name='linux_fedora_22_x86_64', 68 | setup_script=('yum install -y python-pip >&2',), 69 | ), 70 | SystemTestCase( 71 | docker_image='amazonlinux:2016.09', 72 | mock_id='amzn', 73 | mock_version='2016.09', 74 | expected_platform_name='linux_amzn_2016_09_x86_64', 75 | setup_script=('yum install -y python27-pip >&2',), 76 | ), 77 | SystemTestCase( 78 | docker_image='richxsl/rhel7', 79 | mock_id='rhel', 80 | mock_version='7.0', 81 | expected_platform_name='linux_rhel_7_x86_64', 82 | setup_script=SETUP_NO_PIP_PACKAGE, 83 | ), 84 | SystemTestCase( 85 | docker_image='opensuse:13.2', 86 | mock_id='opensuse', 87 | mock_version='13.2', 88 | expected_platform_name='linux_opensuse_13_x86_64', 89 | setup_script=( 90 | 'zypper --non-interactive install python python-pip >&2', 91 | ) 92 | ), 93 | SystemTestCase( 94 | docker_image='base/archlinux', 95 | mock_id='arch', 96 | mock_version='', 97 | expected_platform_name='linux_x86_64', 98 | setup_script=( 99 | 'pacman -Syy >&2', 100 | 'pacman -S --noconfirm python python-pip >&2', 101 | ) 102 | ), 103 | SystemTestCase( 104 | docker_image='alpine:3.11', 105 | mock_id='alpine', 106 | mock_version='3.11.5', 107 | expected_platform_name='linux_alpine_3_11_x86_64', 108 | setup_script=( 109 | 'apk add --no-cache py-pip', 110 | ) 111 | ), 112 | ] 113 | 114 | 115 | @pytest.mark.parametrize('case', SYSTEM_TESTCASES) 116 | @mock.patch('pip_custom_platform.default_platform.platform') 117 | @mock.patch('pip_custom_platform.default_platform.distro') 118 | def test_platform_linux(mock_distro, mock_platform, case): 119 | mock_distro.id.return_value = case.mock_id 120 | mock_distro.version.return_value = case.mock_version 121 | mock_platform.system.return_value = 'Linux' 122 | mock_platform.machine.return_value = 'x86_64' 123 | assert default_platform_name() == case.expected_platform_name 124 | 125 | 126 | @mock.patch('pip_custom_platform.default_platform.platform') 127 | def test_platform_notlinux(mock_platform): 128 | mock_platform.system.return_value = "it's a unix system!" 129 | ret = default_platform_name() 130 | assert ret == _sanitize_platform(distutils.util.get_platform()) 131 | 132 | 133 | PLATFORM_SCRIPT = '''\ 134 | from distutils.util import get_platform 135 | from pip_custom_platform.default_platform import _default_platform_name 136 | print(_default_platform_name(get_platform)) 137 | ''' 138 | 139 | 140 | @pytest.mark.skipif( 141 | 'not config.getvalue("docker")', 142 | reason="Requires --docker", 143 | ) 144 | class TestDistributionNameDockerIntegration(object): # pragma: no cover 145 | """Launch Docker containers to test platform strings. 146 | 147 | These tests are slow and take up a lot of disk space. They are only run if 148 | `--docker` is passed to pytest on the command line. 149 | """ 150 | 151 | @pytest.mark.parametrize('case', SYSTEM_TESTCASES) 152 | def test_platform_name(self, case): 153 | """Ensure the default_platform_name() output matches what we expect.""" 154 | commands = case.setup_script + ( 155 | 'python -m pip install /mnt >&2', 156 | 'python -c "{}"'.format(PLATFORM_SCRIPT) 157 | ) 158 | stdout = run_in_docker(case.docker_image, commands) 159 | assert stdout.strip() == case.expected_platform_name 160 | 161 | @pytest.mark.parametrize('case', SYSTEM_TESTCASES) 162 | def test_mock_is_accurate(self, case): 163 | """Ensure our mocks for distro are accurate.""" 164 | commands = case.setup_script + ( 165 | 'python -m pip install distro >&2', 166 | 'python -c "' 167 | 'import distro\n' 168 | 'print(distro.id())\n' 169 | 'print(distro.version())\n' 170 | '"', 171 | ) 172 | stdout = run_in_docker(case.docker_image, commands) 173 | assert stdout == '\n'.join((case.mock_id, case.mock_version, '')) 174 | 175 | 176 | def run_in_docker(image, commands): # pragma: no cover 177 | """Launches the Docker image and executes the commands, returning 178 | stdout and stderr. 179 | 180 | :param image: a Docker image and tag (e.g. 'debian:jessie') 181 | :param commands: list of commands to paste to the shell 182 | """ 183 | repo_dir = os.path.abspath(os.path.join(__file__, '../../')) 184 | mount_option = '{}:/mnt:ro'.format(repo_dir) 185 | 186 | cmd = ('docker', 'run', '-v', mount_option, '-i', image, 'sh') 187 | proc = subprocess.Popen( 188 | cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 189 | ) 190 | 191 | lines = '\n'.join(commands) 192 | return proc.communicate(lines.encode('utf-8'))[0].decode('utf-8') 193 | -------------------------------------------------------------------------------- /tests/main_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | import subprocess 6 | import sys 7 | 8 | import pytest 9 | 10 | from testing.util import expected_wheel_name 11 | 12 | 13 | def call_coverage(*cmd): 14 | subprocess.check_call((sys.executable, '-m', 'coverage', 'run') + cmd) 15 | 16 | 17 | def call(*cmd): 18 | call_coverage('-m', 'pip_custom_platform.main', *cmd) 19 | 20 | 21 | def wheel(plat, wheeldir, pkg, *args): 22 | call('wheel', '--platform', plat, '--wheel-dir', wheeldir, pkg, *args) 23 | 24 | 25 | def test_useful_message_with_no_args(capfd): 26 | """We should print a useful message when called with no arguments.""" 27 | with pytest.raises(subprocess.CalledProcessError): 28 | call() 29 | _, err = capfd.readouterr() 30 | assert 'usage: pip-custom-platform' in err 31 | 32 | 33 | def test_pure_python_package(tmpdir): 34 | wheeldir = tmpdir.join('wheelhouse').strpath 35 | wheel('plat', wheeldir, 'testing/pure_py_project') 36 | assert os.listdir(wheeldir) == [ 37 | 'pure_python_project-0.1.0-py2.py3-none-any.whl', 38 | ] 39 | 40 | 41 | def test_project_with_c(tmpdir): 42 | wheeldir = tmpdir.join('wheelhouse').strpath 43 | wheel('plat', wheeldir, 'testing/project_with_c') 44 | assert os.listdir(wheeldir) == [ 45 | expected_wheel_name('project_with_c-0.1.0-{}-{}-plat.whl'), 46 | ] 47 | 48 | 49 | def test_multiple_platforms(tmpdir): 50 | wheeldir = tmpdir.join('wheelhouse').strpath 51 | wheel('plat1', wheeldir, 'testing/project_with_c') 52 | wheel('plat2', wheeldir, 'testing/project_with_c') 53 | assert set(os.listdir(wheeldir)) == { 54 | expected_wheel_name('project_with_c-0.1.0-{}-{}-plat1.whl'), 55 | expected_wheel_name('project_with_c-0.1.0-{}-{}-plat2.whl'), 56 | } 57 | 58 | 59 | def test_platform_with_dashes(tmpdir): 60 | wheeldir = tmpdir.join('wheelhouse').strpath 61 | wheel('with-dashes', wheeldir, 'testing/project_with_c') 62 | assert os.listdir(wheeldir) == [ 63 | expected_wheel_name('project_with_c-0.1.0-{}-{}-with_dashes.whl'), 64 | ] 65 | 66 | 67 | def test_wheel_can_fail(tmpdir): 68 | with pytest.raises(subprocess.CalledProcessError): 69 | wheel('plat1', tmpdir.strpath, 'asdf', '--no-index') 70 | 71 | 72 | def test_download_smoke(tmpdir): 73 | findlinks_dir = tmpdir.join('findlinks_dir').strpath 74 | download_dest = tmpdir.join('downloads').mkdir().strpath 75 | 76 | # Build a wheel that we'll install 77 | wheel('plat1', findlinks_dir, 'testing/project_with_c') 78 | 79 | call( 80 | 'download', 81 | '--platform', 'plat1', 82 | '--dest', download_dest, 83 | '--find-links', 'file://{}'.format(findlinks_dir), 84 | '--no-index', 85 | 'project_with_c', 86 | ) 87 | 88 | assert os.listdir(download_dest) == [ 89 | expected_wheel_name('project_with_c-0.1.0-{}-{}-plat1.whl'), 90 | ] 91 | 92 | 93 | def test_default_platform(tmpdir): 94 | findlinks_dir = tmpdir.join('findlinks_dir').strpath 95 | download_dest = tmpdir.join('downloads').mkdir().strpath 96 | 97 | call('wheel', '--wheel-dir', findlinks_dir, 'testing/project_with_c') 98 | 99 | call( 100 | 'download', 101 | '--dest', download_dest, 102 | '--find-links', 'file://{}'.format(findlinks_dir), 103 | '--no-index', 104 | 'project_with_c', 105 | ) 106 | 107 | # We don't _really_ know what the default platform is on this system, so 108 | # just assert that we get something 109 | assert os.listdir(download_dest) 110 | 111 | 112 | def test_download_falls_back_to_sdist(tmpdir): 113 | findlinks_dir = tmpdir.join('findlinks_dir').strpath 114 | download_dest = tmpdir.join('downloads').mkdir().strpath 115 | 116 | # Build an sdist 117 | subprocess.check_call( 118 | (sys.executable, 'setup.py', 'sdist', '--dist-dir', findlinks_dir), 119 | cwd='testing/project_with_c', 120 | ) 121 | 122 | call( 123 | 'download', 124 | '--platform', 'plat1', 125 | '--dest', download_dest, 126 | '--find-links', 'file://{}'.format(findlinks_dir), 127 | '--no-index', 128 | 'project_with_c', 129 | ) 130 | 131 | assert os.listdir(download_dest) == ['project_with_c-0.1.0.tar.gz'] 132 | 133 | 134 | def test_download_wrong_plat_falls_back_to_sdist(tmpdir): 135 | findlinks_dir = tmpdir.join('findlinks_dir').strpath 136 | download_dest = tmpdir.join('downloads').mkdir().strpath 137 | 138 | # Build an sdist 139 | subprocess.check_call( 140 | (sys.executable, 'setup.py', 'sdist', '--dist-dir', findlinks_dir), 141 | cwd='testing/project_with_c', 142 | ) 143 | 144 | # Also build a wheel 145 | wheel('plat2', findlinks_dir, 'testing/project_with_c') 146 | 147 | call( 148 | 'download', 149 | '--platform', 'plat1', 150 | '--dest', download_dest, 151 | '--find-links', 'file://{}'.format(findlinks_dir), 152 | '--no-index', 153 | 'project_with_c', 154 | ) 155 | 156 | assert os.listdir(download_dest) == ['project_with_c-0.1.0.tar.gz'] 157 | 158 | 159 | def test_show_platform_name_custom_platform(capfd): 160 | call('show-platform-name', '--platform', 'herp-derp') 161 | assert capfd.readouterr() == ('herp_derp\n', '') 162 | 163 | 164 | def test_show_platform_name_default(capfd): 165 | call('show-platform-name') 166 | out, err = capfd.readouterr() 167 | assert out 168 | assert err == '' 169 | 170 | 171 | def test_pymonkey_patch(tmpdir): 172 | findlinks_dir = tmpdir.join('findlinks_dir').strpath 173 | download_dest = tmpdir.join('downloads').mkdir().strpath 174 | 175 | call_coverage( 176 | '-m', 'pymonkey', 'pip-custom-platform', '--', 'uses-pip', 177 | '--platform', 'plat1', 178 | findlinks_dir, download_dest, 179 | 'testing/project_with_c', 'project-with-c', 180 | ) 181 | assert os.listdir(download_dest) == [ 182 | expected_wheel_name('project_with_c-0.1.0-{}-{}-plat1.whl'), 183 | ] 184 | 185 | 186 | def test_ok_with_unknown_pip_commands(capfd): 187 | call('help') 188 | out, err = capfd.readouterr() 189 | assert out 190 | assert err == '' 191 | -------------------------------------------------------------------------------- /tests/testing/project_with_c_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | import subprocess 5 | 6 | 7 | def test_project_with_c(tmpdir): 8 | """Make sure it is an installable C extension and produces the expected 9 | output. 10 | """ 11 | venv = tmpdir.join('venv') 12 | subprocess.check_call(('virtualenv', venv.strpath)) 13 | subprocess.check_call(( 14 | 'sh', '-c', 15 | '. {}/bin/activate && pip install testing/project_with_c'.format( 16 | venv.strpath, 17 | ), 18 | )) 19 | proc = subprocess.Popen( 20 | ( 21 | 'sh', '-c', 22 | '. {}/bin/activate && ' 23 | 'python -c "' 24 | 'import project_with_c\n' 25 | 'print(project_with_c.hello_world())"'.format( 26 | venv.strpath, 27 | ) 28 | ), 29 | stdout=subprocess.PIPE, 30 | ) 31 | out, _ = proc.communicate() 32 | assert proc.returncode == 0 33 | assert out == b'hello world\n' 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py36,py37,pypy,pypy3,pre-commit,latest_pip 3 | 4 | [testenv] 5 | deps = -rrequirements-dev.txt 6 | commands = 7 | coverage erase 8 | coverage run -m pytest {posargs:tests} 9 | coverage combine 10 | coverage report --fail-under 100 11 | 12 | [testenv:latest_pip] 13 | commands = 14 | pip install git+git://github.com/pypa/pip 15 | {[testenv]commands} 16 | 17 | [testenv:pre-commit] 18 | skip_install = true 19 | deps = pre-commit 20 | commands = pre-commit run --all-files --show-diff-on-failure 21 | 22 | [pep8] 23 | ignore = E265,E501,W504 24 | --------------------------------------------------------------------------------