pantsbuild / pants

The Pants Build System
https://www.pantsbuild.org
Apache License 2.0
3.27k stars 627 forks source link

Improved workflows for Torch (and Tensorflow?) #18965

Open tgolsson opened 1 year ago

tgolsson commented 1 year ago

This thread is intended to focus on actionable solutions for #18293, potentially multi-platform Pex locks, maybe with other components as well.

Is your feature request related to a problem? Please describe.

The big neural network libraries Tensorflow and Pytorch have significant hurdles in usage based on how they're versioned. In general, the basic wheels published to PyPi aren't usable by default, and any important usage requires custom indexes and compute-API-tagged builds. In the case of tensorflow these are differently named packages, and in the case of torch they are differentiated by local versions. These specific packages also differ in what platforms they support, and the local versions may not exist for all platforms.

Currently, the universal lock provided by Pex specifies a maximum set of {Linux, Mac} but makes no guarantee either platform will be supported. This compounds the complexity of version specification, as local versions shall be preferred. In practice, one has to jump through hoops to ensure a basic resolve works. For example:

# pants.toml
[python-repos]
indexes = [
    "https://pypi.org/simple/",
    "https://download.pytorch.org/whl/cpu/",
]
# BUILD
python_requirement(
    name="torch",
    requirements=["torch==1.11.0"],
    resolve="generic",
)

python_requirement(
    name="torch_cpu",
    requirements=["torch==1.11.0+cpu"],
    resolve="cpu-only",  # only works on Linux since there's no +cpu for Mac.
)

This seems like we'd end up with one lock that works on both Mac (CPU) and Linux (CPU + random CUDA), and one for Linux with only CPU support. However; due to the PEP440 requirements both locks end up picking +CPU, and having no install candidates for Mac. The proper requirement is to specify it like this:

# BUILD
python_requirement(
    name="torch",
    requirements=["torch==1.11.0,!=1.11.0+cpu"],
    resolve="generic",
)

This means that the more different compute variants you need the more specific the generic constraints have to be. If we allowed Pex to separate the versions between Mac and Linux (and Windows?) we would have an easier time ensuring something reasonable gets picked. However; even so it'd be required to exclude all available versions in each resolve as we'd not want a massive +cuda result in the generic for Linux.

Describe the solution you'd like

The thread on Slack discusses multi-platform locks as a potential solution, but I think that still runs afoul of some footguns with the local-version specifiers as one still needs to exclude them. It might therefore be necessary to combine them with other solutions such as per-resolve extra-indexes.

Additional context

https://pantsbuild.slack.com/archives/C046T6T9U/p1683187396031649

jsirois commented 1 year ago

As I pointed out to @tgolsson offline, a multi-platform lock example (Pants does not support these) for this case looks like so:

$ pex3 lock create \
   --index https://download.pytorch.org/whl/cpu/ \
   --platform macosx-10.9-x86_64-cp-39-cp39 \
   --platform linux-x86_64-cp-39-cp39 \
   "torch==1.11.0+cpu ; platform_system != 'Darwin'" \
   "torch===1.11.0 ; platform_system == 'Darwin'" \
   --pip-version latest \
   --resolver-version pip-2020-resolver \
   --indent 2 \
   -o lock.mp.json

In real life you'd almost certainly want to use a pair of --complete-platform - this was just for quick demo. NB: I use === to semi-ameliorate the local version abuse footguns of torch here.

The result is a lock file containing 2 locked resolves:

{
  "allow_builds": true,
  "allow_prereleases": false,
  "allow_wheels": true,
  "build_isolation": true,
  "constraints": [],
  "locked_resolves": [
    {
      "locked_requirements": [
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "50fd9bf85c578c871c28f1cb0ace9dfc6024401c7f399b174fb0f370899f4454",
              "url": "https://download.pytorch.org/whl/cpu/torch-1.11.0-cp39-none-macosx_10_9_x86_64.whl"
            }
          ],
          "project_name": "torch",
          "requires_dists": [
            "typing-extensions"
          ],
          "requires_python": ">=3.7.0",
          "version": "1.11.0"
        },
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4",
              "url": "https://files.pythonhosted.org/packages/31/25/5abcd82372d3d4a3932e1fa8c3dbf9efac10cc7c0d16e78467460571b404/typing_extensions-4.5.0-py3-none-any.whl"
            }
          ],
          "project_name": "typing-extensions",
          "requires_dists": [],
          "requires_python": ">=3.7",
          "version": "4.5.0"
        }
      ],
      "platform_tag": [
        "cp39",
        "cp39",
        "macosx_10_9_x86_64"
      ]
    },
    {
      "locked_requirements": [
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "544c13ef120531ec2f28a3c858c06e600d514a6dfe09b4dd6fd0262088dd2fa3",
              "url": "https://download.pytorch.org/whl/cpu/torch-1.11.0%2Bcpu-cp39-cp39-linux_x86_64.whl"
            }
          ],
          "project_name": "torch",
          "requires_dists": [
            "typing-extensions"
          ],
          "requires_python": ">=3.7.0",
          "version": "1.11.0+cpu"
        },
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4",
              "url": "https://files.pythonhosted.org/packages/31/25/5abcd82372d3d4a3932e1fa8c3dbf9efac10cc7c0d16e78467460571b404/typing_extensions-4.5.0-py3-none-any.whl"
            }
          ],
          "project_name": "typing-extensions",
          "requires_dists": [],
          "requires_python": ">=3.7",
          "version": "4.5.0"
        }
      ],
      "platform_tag": [
        "cp39",
        "cp39",
        "linux_x86_64"
      ]
    }
  ],
  "path_mappings": {},
  "pex_version": "2.1.136",
  "pip_version": "23.1.2",
  "prefer_older_binary": false,
  "requirements": [
    "torch==1.11.0+cpu; platform_system != \"Darwin\"",
    "torch===1.11.0; platform_system == \"Darwin\""
  ],
  "requires_python": [],
  "resolver_version": "pip-2020-resolver",
  "style": "strict",
  "target_systems": [],
  "transitive": true,
  "use_pep517": null
}

Pex has the smarts to pick the best fitting lock - if any - at use time.

tgolsson commented 1 year ago

@jsirois Thank you for the clarification! It even works when not requiring local versions.

$ pex3 lock create \
   --index https://download.pytorch.org/whl/cpu/ \
   --platform macosx-10.9-x86_64-cp-39-cp39 \
   --platform linux-x86_64-cp-39-cp39 \
   "torch>=1.11.0,<1.12" \
   --pip-version latest \
   --resolver-version pip-2020-resolver \
   --indent 2 \
   -o lock.mp.json

Which still gives the proper selection, doesn't require equality matching. With multiple index it'd require per-resolve indexes though, but I think it'd be nice. Having to use == is very unergonomic and a bit against the usage of a lockfile.

{
  "allow_builds": true,
  "allow_prereleases": false,
  "allow_wheels": true,
  "build_isolation": true,
  "constraints": [],
  "locked_resolves": [
    {
      "locked_requirements": [
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "50fd9bf85c578c871c28f1cb0ace9dfc6024401c7f399b174fb0f370899f4454",
              "url": "https://download.pytorch.org/whl/cpu/torch-1.11.0-cp39-none-macosx_10_9_x86_64.whl"
            }
          ],
          "project_name": "torch",
          "requires_dists": [
            "typing-extensions"
          ],
          "requires_python": ">=3.7.0",
          "version": "1.11.0"
        },
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4",
              "url": "https://files.pythonhosted.org/packages/31/25/5abcd82372d3d4a3932e1fa8c3dbf9efac10cc7c0d16e78467460571b404/typing_extensions-4.5.0-py3-none-any.whl"
            }
          ],
          "project_name": "typing-extensions",
          "requires_dists": [],
          "requires_python": ">=3.7",
          "version": "4.5.0"
        }
      ],
      "platform_tag": [
        "cp39",
        "cp39",
        "macosx_10_9_x86_64"
      ]
    },
    {
      "locked_requirements": [
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "544c13ef120531ec2f28a3c858c06e600d514a6dfe09b4dd6fd0262088dd2fa3",
              "url": "https://download.pytorch.org/whl/cpu/torch-1.11.0%2Bcpu-cp39-cp39-linux_x86_64.whl"
            }
          ],
          "project_name": "torch",
          "requires_dists": [
            "typing-extensions"
          ],
          "requires_python": ">=3.7.0",
          "version": "1.11.0+cpu"
        },
        {
          "artifacts": [
            {
              "algorithm": "sha256",
              "hash": "fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4",
              "url": "https://files.pythonhosted.org/packages/31/25/5abcd82372d3d4a3932e1fa8c3dbf9efac10cc7c0d16e78467460571b404/typing_extensions-4.5.0-py3-none-any.whl"
            }
          ],
          "project_name": "typing-extensions",
          "requires_dists": [],
          "requires_python": ">=3.7",
          "version": "4.5.0"
        }
      ],
      "platform_tag": [
        "cp39",
        "cp39",
        "linux_x86_64"
      ]
    }
  ],
  "path_mappings": {},
  "pex_version": "2.1.136",
  "pip_version": "23.1.2",
  "prefer_older_binary": false,
  "requirements": [
    "torch<1.12.0,>=1.11.0"
  ],
  "requires_python": [],
  "resolver_version": "pip-2020-resolver",
  "style": "strict",
  "target_systems": [],
  "transitive": true,
  "use_pep517": null
}

The question is if we can "shorthand" the --platform arguments as well? I'm thinking about onboarding/ease-of-adoption...

jsirois commented 1 year ago

The question is if we can "shorthand" the --platform arguments as well? I'm thinking about onboarding/ease-of-adoption...

Well, the problem is worse. As I mentioned, --platform is a toy example and --complete-platform is what should really be used. Those require actually going to each target machine / python pair and generating a complete platform JSON blob. Now Pants could maybe ship a catalog of common ones, but you'd still need to spell out at least 3 components to identify one: OS, Chip Arch, Python Version.

jsirois commented 1 year ago

The only way to do this without platforms is to allow building up a lock instead of creating it all at once. Basically lock on 1 machine, now lock on another, now combine. In that sort of procedure you never need to spell out the complete platform details since you're running the lock operation natively on the target platform. For that convenience though, you buy the thorny problem of combining the results.

jsirois commented 1 year ago

See "Rationale" item 2 for one idea how to handle combination of multiple locks. The idea expressed in this rejected PEP is to use a lock directory with many files instead of a single lock file with many locked resolves like PEX currently does it: https://discuss.python.org/t/pep-665-specifying-installation-requirements-for-python-projects/9911

tgolsson commented 1 year ago

@jsirois Gotcha. I was naively thinking that some information could be derived from the interpreter constraints, but that's only one axis (the others being ISA and OS). But as far as I can tell there's already support for platforms in Pants, so would adding a lockstyle: PexLockstyle enumeration for the pex_binary and piping that through be enough to replicate the "naive" variant?

jsirois commented 1 year ago

@tgolsson this is exactly what is needed to resolve for Python:

  1. The Python interpreter version (for Requires-Python matching).
  2. The Python interpreter's values for any markers that may be encountered in a resolve: https://peps.python.org/pep-0508/#environment-markers
  3. The ordered list of compatible wheel tags.

1 is easy. 2 and 3 are not. Here's what Pex does for 2 when you only give it --platform (an "abbreviated" platform): https://github.com/pantsbuild/pex/blob/8b8147cd23256e7c5a141e79f9ab27549283e74f/pex/pep_508.py#L32-L118

Now you seem to be grumbling about this "abbreviated" form! And this form is as "nice" as it gets, and totally insufficient and should actually be killed. I at least have the abbreviated platforms now causing resolves using them to raise whenever they encounter unset environment marker values (used to silently "succeed" with wrong resolve results).

The complete platform is really and truly the only way to go. You can use Pex's pex3 tool to create one. Here's one I use for doing foreign resolves for Windows 11 64 bit CPython 3.11:

{
  "marker_environment": {
    "implementation_name": "cpython",
    "implementation_version": "3.11.0",
    "os_name": "nt",
    "platform_machine": "AMD64",
    "platform_python_implementation": "CPython",
    "platform_release": "10",
    "platform_system": "Windows",
    "platform_version": "10.0.22621",
    "python_full_version": "3.11.0",
    "python_version": "3.11",
    "sys_platform": "win32"
  },
  "compatible_tags": [
    "cp311-cp311-win_amd64",
    "cp311-abi3-win_amd64",
    "cp311-none-win_amd64",
    "cp310-abi3-win_amd64",
    "cp39-abi3-win_amd64",
    "cp38-abi3-win_amd64",
    "cp37-abi3-win_amd64",
    "cp36-abi3-win_amd64",
    "cp35-abi3-win_amd64",
    "cp34-abi3-win_amd64",
    "cp33-abi3-win_amd64",
    "cp32-abi3-win_amd64",
    "py311-none-win_amd64",
    "py3-none-win_amd64",
    "py310-none-win_amd64",
    "py39-none-win_amd64",
    "py38-none-win_amd64",
    "py37-none-win_amd64",
    "py36-none-win_amd64",
    "py35-none-win_amd64",
    "py34-none-win_amd64",
    "py33-none-win_amd64",
    "py32-none-win_amd64",
    "py31-none-win_amd64",
    "py30-none-win_amd64",
    "py311-none-any",
    "py3-none-any",
    "py310-none-any",
    "py39-none-any",
    "py38-none-any",
    "py37-none-any",
    "py36-none-any",
    "py35-none-any",
    "py34-none-any",
    "py33-none-any",
    "py32-none-any",
    "py31-none-any",
    "py30-none-any"
  ]
}

N.B.: Windows has the smallest compatible tags lists. Linux tends towards ~700 tags for modern glibc machines.

The command to get that:

pex3 interpreter inspect --python /usr/bin/python3.9 --markers --tags --indent 2
{
  "path": "/usr/bin/python3.9",
  "compatible_tags": [
    "cp39-cp39-manylinux_2_35_x86_64",
    "cp39-cp39-manylinux_2_34_x86_64",
    "cp39-cp39-manylinux_2_33_x86_64",
    "cp39-cp39-manylinux_2_32_x86_64",
    "cp39-cp39-manylinux_2_31_x86_64",
    "cp39-cp39-manylinux_2_30_x86_64",
    "cp39-cp39-manylinux_2_29_x86_64",
    "cp39-cp39-manylinux_2_28_x86_64",
    "cp39-cp39-manylinux_2_27_x86_64",
    "cp39-cp39-manylinux_2_26_x86_64",
    "cp39-cp39-manylinux_2_25_x86_64",
    "cp39-cp39-manylinux_2_24_x86_64",
    "cp39-cp39-manylinux_2_23_x86_64",
    "cp39-cp39-manylinux_2_22_x86_64",
    "cp39-cp39-manylinux_2_21_x86_64",
    "cp39-cp39-manylinux_2_20_x86_64",
    "cp39-cp39-manylinux_2_19_x86_64",
    "cp39-cp39-manylinux_2_18_x86_64",
    "cp39-cp39-manylinux_2_17_x86_64",
    "cp39-cp39-manylinux2014_x86_64",
    "cp39-cp39-manylinux_2_16_x86_64",
    "cp39-cp39-manylinux_2_15_x86_64",
    "cp39-cp39-manylinux_2_14_x86_64",
    "cp39-cp39-manylinux_2_13_x86_64",
    "cp39-cp39-manylinux_2_12_x86_64",
    "cp39-cp39-manylinux2010_x86_64",
    "cp39-cp39-manylinux_2_11_x86_64",
    "cp39-cp39-manylinux_2_10_x86_64",
    "cp39-cp39-manylinux_2_9_x86_64",
    "cp39-cp39-manylinux_2_8_x86_64",
    "cp39-cp39-manylinux_2_7_x86_64",
    "cp39-cp39-manylinux_2_6_x86_64",
    "cp39-cp39-manylinux_2_5_x86_64",
    "cp39-cp39-manylinux1_x86_64",
    "cp39-cp39-linux_x86_64",
    "cp39-abi3-manylinux_2_35_x86_64",
    "cp39-abi3-manylinux_2_34_x86_64",
    "cp39-abi3-manylinux_2_33_x86_64",
    "cp39-abi3-manylinux_2_32_x86_64",
    "cp39-abi3-manylinux_2_31_x86_64",
    "cp39-abi3-manylinux_2_30_x86_64",
    "cp39-abi3-manylinux_2_29_x86_64",
    "cp39-abi3-manylinux_2_28_x86_64",
    "cp39-abi3-manylinux_2_27_x86_64",
    "cp39-abi3-manylinux_2_26_x86_64",
    "cp39-abi3-manylinux_2_25_x86_64",
    "cp39-abi3-manylinux_2_24_x86_64",
    "cp39-abi3-manylinux_2_23_x86_64",
    "cp39-abi3-manylinux_2_22_x86_64",
    "cp39-abi3-manylinux_2_21_x86_64",
    "cp39-abi3-manylinux_2_20_x86_64",
    "cp39-abi3-manylinux_2_19_x86_64",
    "cp39-abi3-manylinux_2_18_x86_64",
    "cp39-abi3-manylinux_2_17_x86_64",
    "cp39-abi3-manylinux2014_x86_64",
    "cp39-abi3-manylinux_2_16_x86_64",
    "cp39-abi3-manylinux_2_15_x86_64",
    "cp39-abi3-manylinux_2_14_x86_64",
    "cp39-abi3-manylinux_2_13_x86_64",
    "cp39-abi3-manylinux_2_12_x86_64",
    "cp39-abi3-manylinux2010_x86_64",
    "cp39-abi3-manylinux_2_11_x86_64",
    "cp39-abi3-manylinux_2_10_x86_64",
    "cp39-abi3-manylinux_2_9_x86_64",
    "cp39-abi3-manylinux_2_8_x86_64",
    "cp39-abi3-manylinux_2_7_x86_64",
    "cp39-abi3-manylinux_2_6_x86_64",
    "cp39-abi3-manylinux_2_5_x86_64",
    "cp39-abi3-manylinux1_x86_64",
    "cp39-abi3-linux_x86_64",
    "cp39-none-manylinux_2_35_x86_64",
    "cp39-none-manylinux_2_34_x86_64",
    "cp39-none-manylinux_2_33_x86_64",
    "cp39-none-manylinux_2_32_x86_64",
    "cp39-none-manylinux_2_31_x86_64",
    "cp39-none-manylinux_2_30_x86_64",
    "cp39-none-manylinux_2_29_x86_64",
    "cp39-none-manylinux_2_28_x86_64",
    "cp39-none-manylinux_2_27_x86_64",
    "cp39-none-manylinux_2_26_x86_64",
    "cp39-none-manylinux_2_25_x86_64",
    "cp39-none-manylinux_2_24_x86_64",
    "cp39-none-manylinux_2_23_x86_64",
    "cp39-none-manylinux_2_22_x86_64",
    "cp39-none-manylinux_2_21_x86_64",
    "cp39-none-manylinux_2_20_x86_64",
    "cp39-none-manylinux_2_19_x86_64",
    "cp39-none-manylinux_2_18_x86_64",
    "cp39-none-manylinux_2_17_x86_64",
    "cp39-none-manylinux2014_x86_64",
    "cp39-none-manylinux_2_16_x86_64",
    "cp39-none-manylinux_2_15_x86_64",
    "cp39-none-manylinux_2_14_x86_64",
    "cp39-none-manylinux_2_13_x86_64",
    "cp39-none-manylinux_2_12_x86_64",
    "cp39-none-manylinux2010_x86_64",
    "cp39-none-manylinux_2_11_x86_64",
    "cp39-none-manylinux_2_10_x86_64",
    "cp39-none-manylinux_2_9_x86_64",
    "cp39-none-manylinux_2_8_x86_64",
    "cp39-none-manylinux_2_7_x86_64",
    "cp39-none-manylinux_2_6_x86_64",
    "cp39-none-manylinux_2_5_x86_64",
    "cp39-none-manylinux1_x86_64",
    "cp39-none-linux_x86_64",
    "cp38-abi3-manylinux_2_35_x86_64",
    "cp38-abi3-manylinux_2_34_x86_64",
    "cp38-abi3-manylinux_2_33_x86_64",
    "cp38-abi3-manylinux_2_32_x86_64",
    "cp38-abi3-manylinux_2_31_x86_64",
    "cp38-abi3-manylinux_2_30_x86_64",
    "cp38-abi3-manylinux_2_29_x86_64",
    "cp38-abi3-manylinux_2_28_x86_64",
    "cp38-abi3-manylinux_2_27_x86_64",
    "cp38-abi3-manylinux_2_26_x86_64",
    "cp38-abi3-manylinux_2_25_x86_64",
    "cp38-abi3-manylinux_2_24_x86_64",
    "cp38-abi3-manylinux_2_23_x86_64",
    "cp38-abi3-manylinux_2_22_x86_64",
    "cp38-abi3-manylinux_2_21_x86_64",
    "cp38-abi3-manylinux_2_20_x86_64",
    "cp38-abi3-manylinux_2_19_x86_64",
    "cp38-abi3-manylinux_2_18_x86_64",
    "cp38-abi3-manylinux_2_17_x86_64",
    "cp38-abi3-manylinux2014_x86_64",
    "cp38-abi3-manylinux_2_16_x86_64",
    "cp38-abi3-manylinux_2_15_x86_64",
    "cp38-abi3-manylinux_2_14_x86_64",
    "cp38-abi3-manylinux_2_13_x86_64",
    "cp38-abi3-manylinux_2_12_x86_64",
    "cp38-abi3-manylinux2010_x86_64",
    "cp38-abi3-manylinux_2_11_x86_64",
    "cp38-abi3-manylinux_2_10_x86_64",
    "cp38-abi3-manylinux_2_9_x86_64",
    "cp38-abi3-manylinux_2_8_x86_64",
    "cp38-abi3-manylinux_2_7_x86_64",
    "cp38-abi3-manylinux_2_6_x86_64",
    "cp38-abi3-manylinux_2_5_x86_64",
    "cp38-abi3-manylinux1_x86_64",
    "cp38-abi3-linux_x86_64",
    "cp37-abi3-manylinux_2_35_x86_64",
    "cp37-abi3-manylinux_2_34_x86_64",
    "cp37-abi3-manylinux_2_33_x86_64",
    "cp37-abi3-manylinux_2_32_x86_64",
    "cp37-abi3-manylinux_2_31_x86_64",
    "cp37-abi3-manylinux_2_30_x86_64",
    "cp37-abi3-manylinux_2_29_x86_64",
    "cp37-abi3-manylinux_2_28_x86_64",
    "cp37-abi3-manylinux_2_27_x86_64",
    "cp37-abi3-manylinux_2_26_x86_64",
    "cp37-abi3-manylinux_2_25_x86_64",
    "cp37-abi3-manylinux_2_24_x86_64",
    "cp37-abi3-manylinux_2_23_x86_64",
    "cp37-abi3-manylinux_2_22_x86_64",
    "cp37-abi3-manylinux_2_21_x86_64",
    "cp37-abi3-manylinux_2_20_x86_64",
    "cp37-abi3-manylinux_2_19_x86_64",
    "cp37-abi3-manylinux_2_18_x86_64",
    "cp37-abi3-manylinux_2_17_x86_64",
    "cp37-abi3-manylinux2014_x86_64",
    "cp37-abi3-manylinux_2_16_x86_64",
    "cp37-abi3-manylinux_2_15_x86_64",
    "cp37-abi3-manylinux_2_14_x86_64",
    "cp37-abi3-manylinux_2_13_x86_64",
    "cp37-abi3-manylinux_2_12_x86_64",
    "cp37-abi3-manylinux2010_x86_64",
    "cp37-abi3-manylinux_2_11_x86_64",
    "cp37-abi3-manylinux_2_10_x86_64",
    "cp37-abi3-manylinux_2_9_x86_64",
    "cp37-abi3-manylinux_2_8_x86_64",
    "cp37-abi3-manylinux_2_7_x86_64",
    "cp37-abi3-manylinux_2_6_x86_64",
    "cp37-abi3-manylinux_2_5_x86_64",
    "cp37-abi3-manylinux1_x86_64",
    "cp37-abi3-linux_x86_64",
    "cp36-abi3-manylinux_2_35_x86_64",
    "cp36-abi3-manylinux_2_34_x86_64",
    "cp36-abi3-manylinux_2_33_x86_64",
    "cp36-abi3-manylinux_2_32_x86_64",
    "cp36-abi3-manylinux_2_31_x86_64",
    "cp36-abi3-manylinux_2_30_x86_64",
    "cp36-abi3-manylinux_2_29_x86_64",
    "cp36-abi3-manylinux_2_28_x86_64",
    "cp36-abi3-manylinux_2_27_x86_64",
    "cp36-abi3-manylinux_2_26_x86_64",
    "cp36-abi3-manylinux_2_25_x86_64",
    "cp36-abi3-manylinux_2_24_x86_64",
    "cp36-abi3-manylinux_2_23_x86_64",
    "cp36-abi3-manylinux_2_22_x86_64",
    "cp36-abi3-manylinux_2_21_x86_64",
    "cp36-abi3-manylinux_2_20_x86_64",
    "cp36-abi3-manylinux_2_19_x86_64",
    "cp36-abi3-manylinux_2_18_x86_64",
    "cp36-abi3-manylinux_2_17_x86_64",
    "cp36-abi3-manylinux2014_x86_64",
    "cp36-abi3-manylinux_2_16_x86_64",
    "cp36-abi3-manylinux_2_15_x86_64",
    "cp36-abi3-manylinux_2_14_x86_64",
    "cp36-abi3-manylinux_2_13_x86_64",
    "cp36-abi3-manylinux_2_12_x86_64",
    "cp36-abi3-manylinux2010_x86_64",
    "cp36-abi3-manylinux_2_11_x86_64",
    "cp36-abi3-manylinux_2_10_x86_64",
    "cp36-abi3-manylinux_2_9_x86_64",
    "cp36-abi3-manylinux_2_8_x86_64",
    "cp36-abi3-manylinux_2_7_x86_64",
    "cp36-abi3-manylinux_2_6_x86_64",
    "cp36-abi3-manylinux_2_5_x86_64",
    "cp36-abi3-manylinux1_x86_64",
    "cp36-abi3-linux_x86_64",
    "cp35-abi3-manylinux_2_35_x86_64",
    "cp35-abi3-manylinux_2_34_x86_64",
    "cp35-abi3-manylinux_2_33_x86_64",
    "cp35-abi3-manylinux_2_32_x86_64",
    "cp35-abi3-manylinux_2_31_x86_64",
    "cp35-abi3-manylinux_2_30_x86_64",
    "cp35-abi3-manylinux_2_29_x86_64",
    "cp35-abi3-manylinux_2_28_x86_64",
    "cp35-abi3-manylinux_2_27_x86_64",
    "cp35-abi3-manylinux_2_26_x86_64",
    "cp35-abi3-manylinux_2_25_x86_64",
    "cp35-abi3-manylinux_2_24_x86_64",
    "cp35-abi3-manylinux_2_23_x86_64",
    "cp35-abi3-manylinux_2_22_x86_64",
    "cp35-abi3-manylinux_2_21_x86_64",
    "cp35-abi3-manylinux_2_20_x86_64",
    "cp35-abi3-manylinux_2_19_x86_64",
    "cp35-abi3-manylinux_2_18_x86_64",
    "cp35-abi3-manylinux_2_17_x86_64",
    "cp35-abi3-manylinux2014_x86_64",
    "cp35-abi3-manylinux_2_16_x86_64",
    "cp35-abi3-manylinux_2_15_x86_64",
    "cp35-abi3-manylinux_2_14_x86_64",
    "cp35-abi3-manylinux_2_13_x86_64",
    "cp35-abi3-manylinux_2_12_x86_64",
    "cp35-abi3-manylinux2010_x86_64",
    "cp35-abi3-manylinux_2_11_x86_64",
    "cp35-abi3-manylinux_2_10_x86_64",
    "cp35-abi3-manylinux_2_9_x86_64",
    "cp35-abi3-manylinux_2_8_x86_64",
    "cp35-abi3-manylinux_2_7_x86_64",
    "cp35-abi3-manylinux_2_6_x86_64",
    "cp35-abi3-manylinux_2_5_x86_64",
    "cp35-abi3-manylinux1_x86_64",
    "cp35-abi3-linux_x86_64",
    "cp34-abi3-manylinux_2_35_x86_64",
    "cp34-abi3-manylinux_2_34_x86_64",
    "cp34-abi3-manylinux_2_33_x86_64",
    "cp34-abi3-manylinux_2_32_x86_64",
    "cp34-abi3-manylinux_2_31_x86_64",
    "cp34-abi3-manylinux_2_30_x86_64",
    "cp34-abi3-manylinux_2_29_x86_64",
    "cp34-abi3-manylinux_2_28_x86_64",
    "cp34-abi3-manylinux_2_27_x86_64",
    "cp34-abi3-manylinux_2_26_x86_64",
    "cp34-abi3-manylinux_2_25_x86_64",
    "cp34-abi3-manylinux_2_24_x86_64",
    "cp34-abi3-manylinux_2_23_x86_64",
    "cp34-abi3-manylinux_2_22_x86_64",
    "cp34-abi3-manylinux_2_21_x86_64",
    "cp34-abi3-manylinux_2_20_x86_64",
    "cp34-abi3-manylinux_2_19_x86_64",
    "cp34-abi3-manylinux_2_18_x86_64",
    "cp34-abi3-manylinux_2_17_x86_64",
    "cp34-abi3-manylinux2014_x86_64",
    "cp34-abi3-manylinux_2_16_x86_64",
    "cp34-abi3-manylinux_2_15_x86_64",
    "cp34-abi3-manylinux_2_14_x86_64",
    "cp34-abi3-manylinux_2_13_x86_64",
    "cp34-abi3-manylinux_2_12_x86_64",
    "cp34-abi3-manylinux2010_x86_64",
    "cp34-abi3-manylinux_2_11_x86_64",
    "cp34-abi3-manylinux_2_10_x86_64",
    "cp34-abi3-manylinux_2_9_x86_64",
    "cp34-abi3-manylinux_2_8_x86_64",
    "cp34-abi3-manylinux_2_7_x86_64",
    "cp34-abi3-manylinux_2_6_x86_64",
    "cp34-abi3-manylinux_2_5_x86_64",
    "cp34-abi3-manylinux1_x86_64",
    "cp34-abi3-linux_x86_64",
    "cp33-abi3-manylinux_2_35_x86_64",
    "cp33-abi3-manylinux_2_34_x86_64",
    "cp33-abi3-manylinux_2_33_x86_64",
    "cp33-abi3-manylinux_2_32_x86_64",
    "cp33-abi3-manylinux_2_31_x86_64",
    "cp33-abi3-manylinux_2_30_x86_64",
    "cp33-abi3-manylinux_2_29_x86_64",
    "cp33-abi3-manylinux_2_28_x86_64",
    "cp33-abi3-manylinux_2_27_x86_64",
    "cp33-abi3-manylinux_2_26_x86_64",
    "cp33-abi3-manylinux_2_25_x86_64",
    "cp33-abi3-manylinux_2_24_x86_64",
    "cp33-abi3-manylinux_2_23_x86_64",
    "cp33-abi3-manylinux_2_22_x86_64",
    "cp33-abi3-manylinux_2_21_x86_64",
    "cp33-abi3-manylinux_2_20_x86_64",
    "cp33-abi3-manylinux_2_19_x86_64",
    "cp33-abi3-manylinux_2_18_x86_64",
    "cp33-abi3-manylinux_2_17_x86_64",
    "cp33-abi3-manylinux2014_x86_64",
    "cp33-abi3-manylinux_2_16_x86_64",
    "cp33-abi3-manylinux_2_15_x86_64",
    "cp33-abi3-manylinux_2_14_x86_64",
    "cp33-abi3-manylinux_2_13_x86_64",
    "cp33-abi3-manylinux_2_12_x86_64",
    "cp33-abi3-manylinux2010_x86_64",
    "cp33-abi3-manylinux_2_11_x86_64",
    "cp33-abi3-manylinux_2_10_x86_64",
    "cp33-abi3-manylinux_2_9_x86_64",
    "cp33-abi3-manylinux_2_8_x86_64",
    "cp33-abi3-manylinux_2_7_x86_64",
    "cp33-abi3-manylinux_2_6_x86_64",
    "cp33-abi3-manylinux_2_5_x86_64",
    "cp33-abi3-manylinux1_x86_64",
    "cp33-abi3-linux_x86_64",
    "cp32-abi3-manylinux_2_35_x86_64",
    "cp32-abi3-manylinux_2_34_x86_64",
    "cp32-abi3-manylinux_2_33_x86_64",
    "cp32-abi3-manylinux_2_32_x86_64",
    "cp32-abi3-manylinux_2_31_x86_64",
    "cp32-abi3-manylinux_2_30_x86_64",
    "cp32-abi3-manylinux_2_29_x86_64",
    "cp32-abi3-manylinux_2_28_x86_64",
    "cp32-abi3-manylinux_2_27_x86_64",
    "cp32-abi3-manylinux_2_26_x86_64",
    "cp32-abi3-manylinux_2_25_x86_64",
    "cp32-abi3-manylinux_2_24_x86_64",
    "cp32-abi3-manylinux_2_23_x86_64",
    "cp32-abi3-manylinux_2_22_x86_64",
    "cp32-abi3-manylinux_2_21_x86_64",
    "cp32-abi3-manylinux_2_20_x86_64",
    "cp32-abi3-manylinux_2_19_x86_64",
    "cp32-abi3-manylinux_2_18_x86_64",
    "cp32-abi3-manylinux_2_17_x86_64",
    "cp32-abi3-manylinux2014_x86_64",
    "cp32-abi3-manylinux_2_16_x86_64",
    "cp32-abi3-manylinux_2_15_x86_64",
    "cp32-abi3-manylinux_2_14_x86_64",
    "cp32-abi3-manylinux_2_13_x86_64",
    "cp32-abi3-manylinux_2_12_x86_64",
    "cp32-abi3-manylinux2010_x86_64",
    "cp32-abi3-manylinux_2_11_x86_64",
    "cp32-abi3-manylinux_2_10_x86_64",
    "cp32-abi3-manylinux_2_9_x86_64",
    "cp32-abi3-manylinux_2_8_x86_64",
    "cp32-abi3-manylinux_2_7_x86_64",
    "cp32-abi3-manylinux_2_6_x86_64",
    "cp32-abi3-manylinux_2_5_x86_64",
    "cp32-abi3-manylinux1_x86_64",
    "cp32-abi3-linux_x86_64",
    "py39-none-manylinux_2_35_x86_64",
    "py39-none-manylinux_2_34_x86_64",
    "py39-none-manylinux_2_33_x86_64",
    "py39-none-manylinux_2_32_x86_64",
    "py39-none-manylinux_2_31_x86_64",
    "py39-none-manylinux_2_30_x86_64",
    "py39-none-manylinux_2_29_x86_64",
    "py39-none-manylinux_2_28_x86_64",
    "py39-none-manylinux_2_27_x86_64",
    "py39-none-manylinux_2_26_x86_64",
    "py39-none-manylinux_2_25_x86_64",
    "py39-none-manylinux_2_24_x86_64",
    "py39-none-manylinux_2_23_x86_64",
    "py39-none-manylinux_2_22_x86_64",
    "py39-none-manylinux_2_21_x86_64",
    "py39-none-manylinux_2_20_x86_64",
    "py39-none-manylinux_2_19_x86_64",
    "py39-none-manylinux_2_18_x86_64",
    "py39-none-manylinux_2_17_x86_64",
    "py39-none-manylinux2014_x86_64",
    "py39-none-manylinux_2_16_x86_64",
    "py39-none-manylinux_2_15_x86_64",
    "py39-none-manylinux_2_14_x86_64",
    "py39-none-manylinux_2_13_x86_64",
    "py39-none-manylinux_2_12_x86_64",
    "py39-none-manylinux2010_x86_64",
    "py39-none-manylinux_2_11_x86_64",
    "py39-none-manylinux_2_10_x86_64",
    "py39-none-manylinux_2_9_x86_64",
    "py39-none-manylinux_2_8_x86_64",
    "py39-none-manylinux_2_7_x86_64",
    "py39-none-manylinux_2_6_x86_64",
    "py39-none-manylinux_2_5_x86_64",
    "py39-none-manylinux1_x86_64",
    "py39-none-linux_x86_64",
    "py3-none-manylinux_2_35_x86_64",
    "py3-none-manylinux_2_34_x86_64",
    "py3-none-manylinux_2_33_x86_64",
    "py3-none-manylinux_2_32_x86_64",
    "py3-none-manylinux_2_31_x86_64",
    "py3-none-manylinux_2_30_x86_64",
    "py3-none-manylinux_2_29_x86_64",
    "py3-none-manylinux_2_28_x86_64",
    "py3-none-manylinux_2_27_x86_64",
    "py3-none-manylinux_2_26_x86_64",
    "py3-none-manylinux_2_25_x86_64",
    "py3-none-manylinux_2_24_x86_64",
    "py3-none-manylinux_2_23_x86_64",
    "py3-none-manylinux_2_22_x86_64",
    "py3-none-manylinux_2_21_x86_64",
    "py3-none-manylinux_2_20_x86_64",
    "py3-none-manylinux_2_19_x86_64",
    "py3-none-manylinux_2_18_x86_64",
    "py3-none-manylinux_2_17_x86_64",
    "py3-none-manylinux2014_x86_64",
    "py3-none-manylinux_2_16_x86_64",
    "py3-none-manylinux_2_15_x86_64",
    "py3-none-manylinux_2_14_x86_64",
    "py3-none-manylinux_2_13_x86_64",
    "py3-none-manylinux_2_12_x86_64",
    "py3-none-manylinux2010_x86_64",
    "py3-none-manylinux_2_11_x86_64",
    "py3-none-manylinux_2_10_x86_64",
    "py3-none-manylinux_2_9_x86_64",
    "py3-none-manylinux_2_8_x86_64",
    "py3-none-manylinux_2_7_x86_64",
    "py3-none-manylinux_2_6_x86_64",
    "py3-none-manylinux_2_5_x86_64",
    "py3-none-manylinux1_x86_64",
    "py3-none-linux_x86_64",
    "py38-none-manylinux_2_35_x86_64",
    "py38-none-manylinux_2_34_x86_64",
    "py38-none-manylinux_2_33_x86_64",
    "py38-none-manylinux_2_32_x86_64",
    "py38-none-manylinux_2_31_x86_64",
    "py38-none-manylinux_2_30_x86_64",
    "py38-none-manylinux_2_29_x86_64",
    "py38-none-manylinux_2_28_x86_64",
    "py38-none-manylinux_2_27_x86_64",
    "py38-none-manylinux_2_26_x86_64",
    "py38-none-manylinux_2_25_x86_64",
    "py38-none-manylinux_2_24_x86_64",
    "py38-none-manylinux_2_23_x86_64",
    "py38-none-manylinux_2_22_x86_64",
    "py38-none-manylinux_2_21_x86_64",
    "py38-none-manylinux_2_20_x86_64",
    "py38-none-manylinux_2_19_x86_64",
    "py38-none-manylinux_2_18_x86_64",
    "py38-none-manylinux_2_17_x86_64",
    "py38-none-manylinux2014_x86_64",
    "py38-none-manylinux_2_16_x86_64",
    "py38-none-manylinux_2_15_x86_64",
    "py38-none-manylinux_2_14_x86_64",
    "py38-none-manylinux_2_13_x86_64",
    "py38-none-manylinux_2_12_x86_64",
    "py38-none-manylinux2010_x86_64",
    "py38-none-manylinux_2_11_x86_64",
    "py38-none-manylinux_2_10_x86_64",
    "py38-none-manylinux_2_9_x86_64",
    "py38-none-manylinux_2_8_x86_64",
    "py38-none-manylinux_2_7_x86_64",
    "py38-none-manylinux_2_6_x86_64",
    "py38-none-manylinux_2_5_x86_64",
    "py38-none-manylinux1_x86_64",
    "py38-none-linux_x86_64",
    "py37-none-manylinux_2_35_x86_64",
    "py37-none-manylinux_2_34_x86_64",
    "py37-none-manylinux_2_33_x86_64",
    "py37-none-manylinux_2_32_x86_64",
    "py37-none-manylinux_2_31_x86_64",
    "py37-none-manylinux_2_30_x86_64",
    "py37-none-manylinux_2_29_x86_64",
    "py37-none-manylinux_2_28_x86_64",
    "py37-none-manylinux_2_27_x86_64",
    "py37-none-manylinux_2_26_x86_64",
    "py37-none-manylinux_2_25_x86_64",
    "py37-none-manylinux_2_24_x86_64",
    "py37-none-manylinux_2_23_x86_64",
    "py37-none-manylinux_2_22_x86_64",
    "py37-none-manylinux_2_21_x86_64",
    "py37-none-manylinux_2_20_x86_64",
    "py37-none-manylinux_2_19_x86_64",
    "py37-none-manylinux_2_18_x86_64",
    "py37-none-manylinux_2_17_x86_64",
    "py37-none-manylinux2014_x86_64",
    "py37-none-manylinux_2_16_x86_64",
    "py37-none-manylinux_2_15_x86_64",
    "py37-none-manylinux_2_14_x86_64",
    "py37-none-manylinux_2_13_x86_64",
    "py37-none-manylinux_2_12_x86_64",
    "py37-none-manylinux2010_x86_64",
    "py37-none-manylinux_2_11_x86_64",
    "py37-none-manylinux_2_10_x86_64",
    "py37-none-manylinux_2_9_x86_64",
    "py37-none-manylinux_2_8_x86_64",
    "py37-none-manylinux_2_7_x86_64",
    "py37-none-manylinux_2_6_x86_64",
    "py37-none-manylinux_2_5_x86_64",
    "py37-none-manylinux1_x86_64",
    "py37-none-linux_x86_64",
    "py36-none-manylinux_2_35_x86_64",
    "py36-none-manylinux_2_34_x86_64",
    "py36-none-manylinux_2_33_x86_64",
    "py36-none-manylinux_2_32_x86_64",
    "py36-none-manylinux_2_31_x86_64",
    "py36-none-manylinux_2_30_x86_64",
    "py36-none-manylinux_2_29_x86_64",
    "py36-none-manylinux_2_28_x86_64",
    "py36-none-manylinux_2_27_x86_64",
    "py36-none-manylinux_2_26_x86_64",
    "py36-none-manylinux_2_25_x86_64",
    "py36-none-manylinux_2_24_x86_64",
    "py36-none-manylinux_2_23_x86_64",
    "py36-none-manylinux_2_22_x86_64",
    "py36-none-manylinux_2_21_x86_64",
    "py36-none-manylinux_2_20_x86_64",
    "py36-none-manylinux_2_19_x86_64",
    "py36-none-manylinux_2_18_x86_64",
    "py36-none-manylinux_2_17_x86_64",
    "py36-none-manylinux2014_x86_64",
    "py36-none-manylinux_2_16_x86_64",
    "py36-none-manylinux_2_15_x86_64",
    "py36-none-manylinux_2_14_x86_64",
    "py36-none-manylinux_2_13_x86_64",
    "py36-none-manylinux_2_12_x86_64",
    "py36-none-manylinux2010_x86_64",
    "py36-none-manylinux_2_11_x86_64",
    "py36-none-manylinux_2_10_x86_64",
    "py36-none-manylinux_2_9_x86_64",
    "py36-none-manylinux_2_8_x86_64",
    "py36-none-manylinux_2_7_x86_64",
    "py36-none-manylinux_2_6_x86_64",
    "py36-none-manylinux_2_5_x86_64",
    "py36-none-manylinux1_x86_64",
    "py36-none-linux_x86_64",
    "py35-none-manylinux_2_35_x86_64",
    "py35-none-manylinux_2_34_x86_64",
    "py35-none-manylinux_2_33_x86_64",
    "py35-none-manylinux_2_32_x86_64",
    "py35-none-manylinux_2_31_x86_64",
    "py35-none-manylinux_2_30_x86_64",
    "py35-none-manylinux_2_29_x86_64",
    "py35-none-manylinux_2_28_x86_64",
    "py35-none-manylinux_2_27_x86_64",
    "py35-none-manylinux_2_26_x86_64",
    "py35-none-manylinux_2_25_x86_64",
    "py35-none-manylinux_2_24_x86_64",
    "py35-none-manylinux_2_23_x86_64",
    "py35-none-manylinux_2_22_x86_64",
    "py35-none-manylinux_2_21_x86_64",
    "py35-none-manylinux_2_20_x86_64",
    "py35-none-manylinux_2_19_x86_64",
    "py35-none-manylinux_2_18_x86_64",
    "py35-none-manylinux_2_17_x86_64",
    "py35-none-manylinux2014_x86_64",
    "py35-none-manylinux_2_16_x86_64",
    "py35-none-manylinux_2_15_x86_64",
    "py35-none-manylinux_2_14_x86_64",
    "py35-none-manylinux_2_13_x86_64",
    "py35-none-manylinux_2_12_x86_64",
    "py35-none-manylinux2010_x86_64",
    "py35-none-manylinux_2_11_x86_64",
    "py35-none-manylinux_2_10_x86_64",
    "py35-none-manylinux_2_9_x86_64",
    "py35-none-manylinux_2_8_x86_64",
    "py35-none-manylinux_2_7_x86_64",
    "py35-none-manylinux_2_6_x86_64",
    "py35-none-manylinux_2_5_x86_64",
    "py35-none-manylinux1_x86_64",
    "py35-none-linux_x86_64",
    "py34-none-manylinux_2_35_x86_64",
    "py34-none-manylinux_2_34_x86_64",
    "py34-none-manylinux_2_33_x86_64",
    "py34-none-manylinux_2_32_x86_64",
    "py34-none-manylinux_2_31_x86_64",
    "py34-none-manylinux_2_30_x86_64",
    "py34-none-manylinux_2_29_x86_64",
    "py34-none-manylinux_2_28_x86_64",
    "py34-none-manylinux_2_27_x86_64",
    "py34-none-manylinux_2_26_x86_64",
    "py34-none-manylinux_2_25_x86_64",
    "py34-none-manylinux_2_24_x86_64",
    "py34-none-manylinux_2_23_x86_64",
    "py34-none-manylinux_2_22_x86_64",
    "py34-none-manylinux_2_21_x86_64",
    "py34-none-manylinux_2_20_x86_64",
    "py34-none-manylinux_2_19_x86_64",
    "py34-none-manylinux_2_18_x86_64",
    "py34-none-manylinux_2_17_x86_64",
    "py34-none-manylinux2014_x86_64",
    "py34-none-manylinux_2_16_x86_64",
    "py34-none-manylinux_2_15_x86_64",
    "py34-none-manylinux_2_14_x86_64",
    "py34-none-manylinux_2_13_x86_64",
    "py34-none-manylinux_2_12_x86_64",
    "py34-none-manylinux2010_x86_64",
    "py34-none-manylinux_2_11_x86_64",
    "py34-none-manylinux_2_10_x86_64",
    "py34-none-manylinux_2_9_x86_64",
    "py34-none-manylinux_2_8_x86_64",
    "py34-none-manylinux_2_7_x86_64",
    "py34-none-manylinux_2_6_x86_64",
    "py34-none-manylinux_2_5_x86_64",
    "py34-none-manylinux1_x86_64",
    "py34-none-linux_x86_64",
    "py33-none-manylinux_2_35_x86_64",
    "py33-none-manylinux_2_34_x86_64",
    "py33-none-manylinux_2_33_x86_64",
    "py33-none-manylinux_2_32_x86_64",
    "py33-none-manylinux_2_31_x86_64",
    "py33-none-manylinux_2_30_x86_64",
    "py33-none-manylinux_2_29_x86_64",
    "py33-none-manylinux_2_28_x86_64",
    "py33-none-manylinux_2_27_x86_64",
    "py33-none-manylinux_2_26_x86_64",
    "py33-none-manylinux_2_25_x86_64",
    "py33-none-manylinux_2_24_x86_64",
    "py33-none-manylinux_2_23_x86_64",
    "py33-none-manylinux_2_22_x86_64",
    "py33-none-manylinux_2_21_x86_64",
    "py33-none-manylinux_2_20_x86_64",
    "py33-none-manylinux_2_19_x86_64",
    "py33-none-manylinux_2_18_x86_64",
    "py33-none-manylinux_2_17_x86_64",
    "py33-none-manylinux2014_x86_64",
    "py33-none-manylinux_2_16_x86_64",
    "py33-none-manylinux_2_15_x86_64",
    "py33-none-manylinux_2_14_x86_64",
    "py33-none-manylinux_2_13_x86_64",
    "py33-none-manylinux_2_12_x86_64",
    "py33-none-manylinux2010_x86_64",
    "py33-none-manylinux_2_11_x86_64",
    "py33-none-manylinux_2_10_x86_64",
    "py33-none-manylinux_2_9_x86_64",
    "py33-none-manylinux_2_8_x86_64",
    "py33-none-manylinux_2_7_x86_64",
    "py33-none-manylinux_2_6_x86_64",
    "py33-none-manylinux_2_5_x86_64",
    "py33-none-manylinux1_x86_64",
    "py33-none-linux_x86_64",
    "py32-none-manylinux_2_35_x86_64",
    "py32-none-manylinux_2_34_x86_64",
    "py32-none-manylinux_2_33_x86_64",
    "py32-none-manylinux_2_32_x86_64",
    "py32-none-manylinux_2_31_x86_64",
    "py32-none-manylinux_2_30_x86_64",
    "py32-none-manylinux_2_29_x86_64",
    "py32-none-manylinux_2_28_x86_64",
    "py32-none-manylinux_2_27_x86_64",
    "py32-none-manylinux_2_26_x86_64",
    "py32-none-manylinux_2_25_x86_64",
    "py32-none-manylinux_2_24_x86_64",
    "py32-none-manylinux_2_23_x86_64",
    "py32-none-manylinux_2_22_x86_64",
    "py32-none-manylinux_2_21_x86_64",
    "py32-none-manylinux_2_20_x86_64",
    "py32-none-manylinux_2_19_x86_64",
    "py32-none-manylinux_2_18_x86_64",
    "py32-none-manylinux_2_17_x86_64",
    "py32-none-manylinux2014_x86_64",
    "py32-none-manylinux_2_16_x86_64",
    "py32-none-manylinux_2_15_x86_64",
    "py32-none-manylinux_2_14_x86_64",
    "py32-none-manylinux_2_13_x86_64",
    "py32-none-manylinux_2_12_x86_64",
    "py32-none-manylinux2010_x86_64",
    "py32-none-manylinux_2_11_x86_64",
    "py32-none-manylinux_2_10_x86_64",
    "py32-none-manylinux_2_9_x86_64",
    "py32-none-manylinux_2_8_x86_64",
    "py32-none-manylinux_2_7_x86_64",
    "py32-none-manylinux_2_6_x86_64",
    "py32-none-manylinux_2_5_x86_64",
    "py32-none-manylinux1_x86_64",
    "py32-none-linux_x86_64",
    "py31-none-manylinux_2_35_x86_64",
    "py31-none-manylinux_2_34_x86_64",
    "py31-none-manylinux_2_33_x86_64",
    "py31-none-manylinux_2_32_x86_64",
    "py31-none-manylinux_2_31_x86_64",
    "py31-none-manylinux_2_30_x86_64",
    "py31-none-manylinux_2_29_x86_64",
    "py31-none-manylinux_2_28_x86_64",
    "py31-none-manylinux_2_27_x86_64",
    "py31-none-manylinux_2_26_x86_64",
    "py31-none-manylinux_2_25_x86_64",
    "py31-none-manylinux_2_24_x86_64",
    "py31-none-manylinux_2_23_x86_64",
    "py31-none-manylinux_2_22_x86_64",
    "py31-none-manylinux_2_21_x86_64",
    "py31-none-manylinux_2_20_x86_64",
    "py31-none-manylinux_2_19_x86_64",
    "py31-none-manylinux_2_18_x86_64",
    "py31-none-manylinux_2_17_x86_64",
    "py31-none-manylinux2014_x86_64",
    "py31-none-manylinux_2_16_x86_64",
    "py31-none-manylinux_2_15_x86_64",
    "py31-none-manylinux_2_14_x86_64",
    "py31-none-manylinux_2_13_x86_64",
    "py31-none-manylinux_2_12_x86_64",
    "py31-none-manylinux2010_x86_64",
    "py31-none-manylinux_2_11_x86_64",
    "py31-none-manylinux_2_10_x86_64",
    "py31-none-manylinux_2_9_x86_64",
    "py31-none-manylinux_2_8_x86_64",
    "py31-none-manylinux_2_7_x86_64",
    "py31-none-manylinux_2_6_x86_64",
    "py31-none-manylinux_2_5_x86_64",
    "py31-none-manylinux1_x86_64",
    "py31-none-linux_x86_64",
    "py30-none-manylinux_2_35_x86_64",
    "py30-none-manylinux_2_34_x86_64",
    "py30-none-manylinux_2_33_x86_64",
    "py30-none-manylinux_2_32_x86_64",
    "py30-none-manylinux_2_31_x86_64",
    "py30-none-manylinux_2_30_x86_64",
    "py30-none-manylinux_2_29_x86_64",
    "py30-none-manylinux_2_28_x86_64",
    "py30-none-manylinux_2_27_x86_64",
    "py30-none-manylinux_2_26_x86_64",
    "py30-none-manylinux_2_25_x86_64",
    "py30-none-manylinux_2_24_x86_64",
    "py30-none-manylinux_2_23_x86_64",
    "py30-none-manylinux_2_22_x86_64",
    "py30-none-manylinux_2_21_x86_64",
    "py30-none-manylinux_2_20_x86_64",
    "py30-none-manylinux_2_19_x86_64",
    "py30-none-manylinux_2_18_x86_64",
    "py30-none-manylinux_2_17_x86_64",
    "py30-none-manylinux2014_x86_64",
    "py30-none-manylinux_2_16_x86_64",
    "py30-none-manylinux_2_15_x86_64",
    "py30-none-manylinux_2_14_x86_64",
    "py30-none-manylinux_2_13_x86_64",
    "py30-none-manylinux_2_12_x86_64",
    "py30-none-manylinux2010_x86_64",
    "py30-none-manylinux_2_11_x86_64",
    "py30-none-manylinux_2_10_x86_64",
    "py30-none-manylinux_2_9_x86_64",
    "py30-none-manylinux_2_8_x86_64",
    "py30-none-manylinux_2_7_x86_64",
    "py30-none-manylinux_2_6_x86_64",
    "py30-none-manylinux_2_5_x86_64",
    "py30-none-manylinux1_x86_64",
    "py30-none-linux_x86_64",
    "py39-none-any",
    "py3-none-any",
    "py38-none-any",
    "py37-none-any",
    "py36-none-any",
    "py35-none-any",
    "py34-none-any",
    "py33-none-any",
    "py32-none-any",
    "py31-none-any",
    "py30-none-any"
  ],
  "marker_environment": {
    "implementation_name": "cpython",
    "implementation_version": "3.9.16",
    "os_name": "posix",
    "platform_machine": "x86_64",
    "platform_python_implementation": "CPython",
    "platform_release": "5.15.90.1-microsoft-standard-WSL2",
    "platform_system": "Linux",
    "platform_version": "#1 SMP Fri Jan 27 02:56:13 UTC 2023",
    "python_full_version": "3.9.16",
    "python_version": "3.9",
    "sys_platform": "linux"
  }
}
jsirois commented 1 year ago

As far as using the existing platforms (and complete_platforms) support in Pants, yes - that's the data needed to use pex3 lock create --style strict [--platform X]* [--complete-platform Y]* - which is what I think you mean by lockstyle: PexLockstyle.

In Pex parlance it's --style {strict|sources|universal}.

The strict (default) and sources styles allow resolving for one or more foreign platforms (abbreviated or complete) as well as any number of local interpreters. The resulting lock file will have a lock per each of those targets, custom for that target. The sources just always adds an sdist to the lock when there is an sdist available in addition to the most compatible wheel; so each locked project can have 1-2 artifacts locked. For strict, exactly 1 artifact (the most compatible one) is locked per project.

The universal style only works via interpreter constraints and optional --target-system filters and produces only 1 lock. You can pass platforms (abbreviated or complete again), but these are only used to validate the lock will work for those platforms after the universal lock is generated. They do not steer the resolve that builds the universal lock.

tgolsson commented 1 year ago

Alright, trying to keep up here!

<1000' lines of markers> So these lists are what you meant by maybe ship a catalog of common ones above, right? Which seems to me like a good solution, or maybe there's one maintained that could be pulled down somewhere. I've hit this before writing a GCS-cached downloader for Poetry, and maintaining that indeed seems a pain. In that case I could at least use the local Python, but as you note that doesn't work here.

As far as using the existing platforms (and complete_platforms) support in Pants, yes - that's the data needed to use pex3 lock create --style strict [--platform X] [--complete-platform Y] - which is what I think you mean by lockstyle: PexLockstyle.

In Pex parlance it's --style {strict|sources|universal}.

Yepp indeed! It looks like both platforms and complete_platforms already exist, so being able to set lockstyle is the minimal diff to get closer to a "proper" workflow. Maybe it should be called style, it just feels so generic. I like my bikeshed fwiw. (I just realized: the configuration I've looked at is for the pex_binary but not the lockfile. How does that even work? Does it? Or would we need to add a more config elsewhere?

The strict (default) and sources styles allow resolving for one or more foreign platforms (abbreviated or complete) as well as any number of local interpreters. The resulting lock file will have a lock per each of those targets, custom for that target. The sources just always adds an sdist to the lock when there is an sdist available in addition to the most compatible wheel; so each locked project can have 1-2 artifacts locked. For strict, exactly 1 artifact (the most compatible one) is locked per project.

The universal style only works via interpreter constraints and optional --target-system filters and produces only 1 lock. You can pass platforms (abbreviated or complete again), but these are only used to validate the lock will work for those platforms after the universal lock is generated. They do not steer the resolve that builds the universal lock.

Makes sense. I assume when you say exactly 1, that's per-complete-platform I assume?


I think there's a bit of a conflict between what I expect from a tool like Pants in terms of abstraction level of workflows - high gain for low effort - and pex maybe focusing more on explicitness and correctness. And that's not an easy thing to resolve. FWIW from my perspective, I think there's a fundamental disconnect between what I'm configuring ("use torch") and the result ("only works on Linux") when I add an extra index elsewhere.

Whatever solution we go for; I think having the option to use "non-exact" version constraints is also an important part, as in my "torch>=1.11.0,<1.12" lock. And then getting something that picks the right variant. But that would require per-resolve indexes, which I think also makes sense as it precludes specifying the +version. (Or per-requirement-indexes?). I think using ==,!=,=== shouldn't be required if we can do per-platform resolves, but that requires disambiguation elsewhere. I'm not super-certain about this requirement, but it'd be nice. Open to better ideas or thoughts, you're much more familiar with the python specs here.

I added the pants.toml and BUILD files at the top for what currently doesn't work. Let's flip that around. I'd expect that given a configuration like:

# pants.toml
[python-repos]
indexes = [
    "https://pypi.org/simple/",
    "https://download.pytorch.org/whl/cpu/",
]
# BUILD
python_requirement(
    name="torch",
    requirements=["torch>=1.11.0,<1.12"],
    resolve="cpu",
)

This would lock +cpu for Linux and a generic version for Mac. The question is then how to avoid configuration complexity explosion if our pants.toml looks like this:

# pants.toml
[python-repos]
indexes = [
    "https://pypi.org/simple/",
    "https://download.pytorch.org/whl/cpu/",
    "https://download.pytorch.org/whl/cu115/",
]

Because I think at a minimum it'd have to look like:

# BUILD
python_requirement(
    name="torch",
    requirements=["torch>=1.11.0,<1.12,!=1.11.*+cu115"],
    resolve="cpu",
)
python_requirement(
    name="torch",
    requirements=["torch>=1.11.0,<1.12,!=1.11.*+cpu"],
    resolve="gpu",
)

Alternatively;

# BUILD
python_requirement(
    name="torch",
    requirements=[
        'torch==1.11.*+cpu ; platform_system != "Darwin"'
        'torch==1.11.* ; platform_system == "Darwin"'
    ],
    resolve="cpu",
)
python_requirement(
    name="torch",
    requirements=[
        'torch==1.11.*+cu115 ; platform_system != "Darwin"'
        'torch==1.11.* ; platform_system == "Darwin"'
    ],
    resolve="gpu",
)

But I think that's even worse (though less exponential). I'm also not sure if star-versions like that work with local versions.