diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst
index 9ebb6e3f78b..45a13ec8521 100644
--- a/docs/html/cli/pip_install.rst
+++ b/docs/html/cli/pip_install.rst
@@ -315,8 +315,9 @@ Per-requirement Overrides
-------------------------
Since version 7.0 pip supports controlling the command line options given to
-``setup.py`` via requirements files. This disables the use of wheels (cached or
-otherwise) for that package, as ``setup.py`` does not exist for wheels.
+``setup.py`` via requirements files. Unless the ``always-install-via-wheel``
+feature is enabled, this disables the use of wheels (cached or otherwise),
+as ``setup.py`` does not exist for wheels.
The ``--global-option`` and ``--install-option`` options are used to pass
options to ``setup.py``. For example:
@@ -334,6 +335,10 @@ script as:
python setup.py --no-user-cfg install --prefix='/usr/local' --no-compile
+When the ``always-install-via-wheel`` feature is enabled, ``--install-option``
+is ignored, and ``--global-option`` is passed to the ``setup.py bdist_wheel``
+command that is used to create a wheel that will be installed.
+
Note that the only way of giving more than one option to ``setup.py``
is through multiple ``--global-option`` and ``--install-option``
options, as shown in the example above. The value of each option is
diff --git a/news/9778.feature.rst b/news/9778.feature.rst
new file mode 100644
index 00000000000..1958efe32be
--- /dev/null
+++ b/news/9778.feature.rst
@@ -0,0 +1,6 @@
+Add an ``always-install-via-wheel`` feature flag. When enabled, ``--no-binary``
+no longer implies using the legacy ``setup.py install`` path, and instead
+a wheel is built locally before installing. ``--build-option`` and
+``--global-option`` are now passed to ``setup.py bdist_wheel`` in ``pip
+install``, as it was already done in ``pip wheel``. ``--install-option`` is
+ignored (because there is no ``setup.py install`` involved).
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index f71c0b02011..29bc713971e 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -501,12 +501,15 @@ def no_binary():
callback=_handle_no_binary,
type="str",
default=format_control,
- help="Do not use binary packages. Can be supplied multiple times, and "
+ help="Do not download binary packages nor use the wheel cache. "
+ "Can be supplied multiple times, and "
'each time adds to the existing value. Accepts either ":all:" to '
'disable all binary packages, ":none:" to empty the set (notice '
"the colons), or one or more package names with commas between "
"them (no colons). Note that some packages are tricky to compile "
- "and may fail to install when this option is used on them.",
+ "and may fail to install when this option is used on them. "
+ "Additionally, this option forces the use of the legacy 'setup.py install' "
+ "path, unless the 'always-install-via-wheel' feature is enabled.",
)
@@ -821,7 +824,10 @@ def _handle_no_use_pep517(option, opt, value, parser):
'command (use like --install-option="--install-scripts=/usr/local/'
'bin"). Use multiple --install-option options to pass multiple '
"options to setup.py install. If you are using an option with a "
- "directory path, be sure to use absolute path.",
+ "directory path, be sure to use absolute path. "
+ "This option implies '--no-binary :all:'. "
+ "It is ignored by the 'pip install' command when the "
+ "'always-install-via-wheel' feature is enabled.",
) # type: Callable[..., Option]
build_options = partial(
@@ -830,7 +836,10 @@ def _handle_no_use_pep517(option, opt, value, parser):
dest="build_options",
metavar="options",
action="append",
- help="Extra arguments to be supplied to 'setup.py bdist_wheel'.",
+ help="Extra arguments to be supplied to 'setup.py bdist_wheel'. "
+ "This option implies '--no-binary :all:'."
+ "It is ignored by the 'pip install' command unless the "
+ "'always-install-via-wheel' feature is enabled.",
) # type: Callable[..., Option]
global_options = partial(
@@ -840,7 +849,8 @@ def _handle_no_use_pep517(option, opt, value, parser):
action="append",
metavar="options",
help="Extra global options to be supplied to the setup.py "
- "call before the install or bdist_wheel command.",
+ "call before the install or bdist_wheel command. "
+ "This option implies '--no-binary :all:'.",
) # type: Callable[..., Option]
no_clean = partial(
@@ -965,7 +975,7 @@ def check_list_path_option(options):
metavar="feature",
action="append",
default=[],
- choices=["2020-resolver", "fast-deps", "in-tree-build"],
+ choices=["2020-resolver", "fast-deps", "in-tree-build", "always-install-via-wheel"],
help="Enable new functionality, that may be backward incompatible.",
) # type: Callable[..., Option]
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index dc637d87635..20d0d86d948 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -45,10 +45,12 @@
logger = logging.getLogger(__name__)
-def get_check_binary_allowed(format_control):
- # type: (FormatControl) -> BinaryAllowedPredicate
+def get_check_binary_allowed(format_control, features_enabled):
+ # type: (FormatControl, List[str]) -> BinaryAllowedPredicate
def check_binary_allowed(req):
# type: (InstallRequirement) -> bool
+ if "always-install-via-wheel" in features_enabled:
+ return True
canonical_name = canonicalize_name(req.name or "")
allowed_formats = format_control.get_allowed_formats(canonical_name)
return "binary" in allowed_formats
@@ -174,6 +176,7 @@ def add_options(self):
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.install_options())
+ self.cmd_opts.add_option(cmdoptions.build_options())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
@@ -329,7 +332,7 @@ def run(self, options, args):
)
check_binary_allowed = get_check_binary_allowed(
- finder.format_control
+ finder.format_control, options.features_enabled
)
reqs_to_build = [
@@ -343,8 +346,8 @@ def run(self, options, args):
reqs_to_build,
wheel_cache=wheel_cache,
verify=True,
- build_options=[],
- global_options=[],
+ build_options=options.build_options or [],
+ global_options=options.global_options or [],
)
# If we're using PEP 517, we cannot do a direct install
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index 4b2f6d5d4df..f1cfee34ecf 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -75,7 +75,7 @@ def _should_build(
if not check_binary_allowed(req):
logger.info(
- "Skipping wheel build for %s, due to binaries "
+ "Using legacy 'setup.py install' for %s, due to binaries "
"being disabled for it.", req.name,
)
return False
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 2742e873e33..b96bc63e047 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -724,6 +724,17 @@ def test_install_global_option(script):
assert not result.files_created
+def test_install_global_option_does_not_disable_wheel_building(
+ script, data, with_wheel
+):
+ res = script.pip(
+ 'install', '--no-index', '--global-option=-v', '-f', data.find_links,
+ '--use-feature=always-install-via-wheel',
+ 'upper', expect_stderr=True)
+ assert "Successfully installed upper-2.0" in str(res), str(res)
+ assert "Building wheel for upper" in str(res), str(res)
+
+
def test_install_with_hacked_egg_info(script, data):
"""
test installing a package which defines its own egg_info class
@@ -1439,6 +1450,17 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel):
assert "Running setup.py install for upper" in str(res), str(res)
+def test_install_no_binary_does_not_disable_building_wheels(
+ script, data, with_wheel
+):
+ res = script.pip(
+ 'install', '--no-index', '--no-binary=upper', '-f', data.find_links,
+ '--use-feature=always-install-via-wheel',
+ 'upper', expect_stderr=True)
+ assert "Successfully installed upper-2.0" in str(res), str(res)
+ assert "Building wheel for upper" in str(res), str(res)
+
+
@pytest.mark.network
def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel):
to_install = data.packages.joinpath('pep517_setup_and_pyproject')