Repository: jazzband/pip-tools (Python)

Issue:
Output --no-binary and --only-binary options to preserve pip behavior
<!-- Describe the issue briefly here. -->

#### Environment Versions

1. OS Type: Linux
1. Python version: 3.12.3
1. pip version: 24.0
1. pip-tools version: 7.4.1

#### Steps to replicate

1. Compile using `--pip-args='--only-binary=:all: --no-binary=library'
2. See that compiled requirements list the `--no-binary=library` option first, `--only-binary=:all:` option second.
3. When attempting to install from these requirements the `--no-binary` is wiped out by the `--only-binary=:all:`

#### Expected result

The resulting requirements contain `--no-binary` and `--only-binary` options that have the same behavior as input options.

#### Actual result

Requirements don't have the same behavior as input options.

This improvement matters because using --no-binary/--only-binary is the best way for users to control the amount of potential code execution that happens during the installation process. I have a local fix that I plan on creating a PR.



Discussion: Are you talking about the comment headers in the resulting constraint files?
I'm talking about a [file like this one](https://github.com/python/release-tools/blob/master/requirements.txt), I discovered this while using `--only-binary=:all:` for python/release-tools:

```
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
#    pip-compile --generate-hashes --output-file=requirements.txt --pip-args='--only-binary=:all: --no-binary=grapheme,grpclib' requirements.in
#
--no-binary grapheme
--no-binary grpclib
--only-binary :all:

... (all the requirements)
```

Note the order of `--pip-args` and that the combination of `--no-binary` and `--only-binary` in the resulting `requirements.txt` don't produce the same behavior in pip.
@webknjaz I've created a PR with a fix that also illustrates the problem: https://github.com/jazzband/pip-tools/pull/2082
FWIW I'm of a opinion that the output file should be used as a constraint, not a requirement.
As in, `pip install -r requirements.in -c requirements.txt`. And so such pip options should really go into the input file..



Fix (patch):
diff --git a/piptools/writer.py b/piptools/writer.py
index bcb70f02f..da603f1f7 100644
--- a/piptools/writer.py
+++ b/piptools/writer.py
@@ -148,9 +148,27 @@ def write_trusted_hosts(self) -> Iterator[str]:
                 yield f"--trusted-host {trusted_host}"
 
     def write_format_controls(self) -> Iterator[str]:
-        for nb in dedup(sorted(self.format_control.no_binary)):
+        # The ordering of output needs to preserve the behavior of pip's
+        # FormatControl.get_allowed_formats(). The behavior is the following:
+        #
+        #   * Parsing of CLI options happens first to last.
+        #   * --only-binary takes precedence over --no-binary
+        #   * Package names take precedence over :all:
+        #   * We'll never see :all: in both due to mutual exclusion.
+        #
+        # So in summary, we want to emit :all: first and then package names later.
+        no_binary = self.format_control.no_binary.copy()
+        only_binary = self.format_control.only_binary.copy()
+
+        if ":all:" in no_binary:
+            yield "--no-binary :all:"
+            no_binary.remove(":all:")
+        if ":all:" in only_binary:
+            yield "--only-binary :all:"
+            only_binary.remove(":all:")
+        for nb in dedup(sorted(no_binary)):
             yield f"--no-binary {nb}"
-        for ob in dedup(sorted(self.format_control.only_binary)):
+        for ob in dedup(sorted(only_binary)):
             yield f"--only-binary {ob}"
 
     def write_find_links(self) -> Iterator[str]: