maturin

maturin's implementation of the PEP 517 interface. Calls maturin through subprocess

Currently, the "return value" of the rust implementation is the last line of stdout

On windows, apparently pip's subprocess handling sets stdout to some windows encoding (e.g. cp1252 on my machine), even though the terminal supports utf8. Writing directly to the binary stdout buffer avoids encoding errors due to maturin's emojis.

  1#!/usr/bin/env python3
  2"""
  3maturin's implementation of the PEP 517 interface. Calls maturin through subprocess
  4
  5Currently, the "return value" of the rust implementation is the last line of stdout
  6
  7On windows, apparently pip's subprocess handling sets stdout to some windows encoding (e.g. cp1252 on my machine),
  8even though the terminal supports utf8. Writing directly to the binary stdout buffer avoids encoding errors due to
  9maturin's emojis.
 10"""
 11
 12from __future__ import annotations
 13
 14import os
 15import platform
 16import shlex
 17import shutil
 18import struct
 19import subprocess
 20import sys
 21from subprocess import SubprocessError
 22from typing import Any, Dict, Mapping, List, Optional
 23
 24
 25def get_config() -> Dict[str, str]:
 26    try:
 27        import tomllib
 28    except ModuleNotFoundError:
 29        import tomli as tomllib  # type: ignore
 30
 31    with open("pyproject.toml", "rb") as fp:
 32        pyproject_toml = tomllib.load(fp)
 33    return pyproject_toml.get("tool", {}).get("maturin", {})
 34
 35
 36def get_maturin_pep517_args(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
 37    build_args = None
 38    if config_settings:
 39        # TODO: Deprecate and remove build-args in favor of maturin.build-args in maturin 2.0
 40        build_args = config_settings.get("maturin.build-args", config_settings.get("build-args"))
 41    if build_args is None:
 42        env_args = os.getenv("MATURIN_PEP517_ARGS", "")
 43        args = shlex.split(env_args)
 44    elif isinstance(build_args, str):
 45        args = shlex.split(build_args)
 46    else:
 47        args = build_args
 48    return args
 49
 50
 51def _get_sys_executable() -> str:
 52    executable = sys.executable
 53    if os.getenv("MATURIN_PEP517_USE_BASE_PYTHON") in {"1", "true"}:
 54        # Use the base interpreter path when running inside a venv to avoid recompilation
 55        # when switching between venvs
 56        base_executable = getattr(sys, "_base_executable")
 57        if base_executable and os.path.exists(base_executable):
 58            executable = os.path.realpath(base_executable)
 59    return executable
 60
 61
 62def _additional_pep517_args() -> List[str]:
 63    # Support building for 32-bit Python on x64 Windows
 64    if platform.system().lower() == "windows" and platform.machine().lower() == "amd64":
 65        pointer_width = struct.calcsize("P") * 8
 66        if pointer_width == 32:
 67            return ["--target", "i686-pc-windows-msvc"]
 68    return []
 69
 70
 71def _get_env() -> Optional[Dict[str, str]]:
 72    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
 73        from puccinialin import setup_rust
 74
 75        print("Rust not found, installing into a temporary directory")
 76        extra_env = setup_rust()
 77        return {**os.environ, **extra_env}
 78    else:
 79        return None
 80
 81
 82# noinspection PyUnusedLocal
 83def _build_wheel(
 84    wheel_directory: str,
 85    config_settings: Optional[Mapping[str, Any]] = None,
 86    metadata_directory: Optional[str] = None,
 87    editable: bool = False,
 88) -> str:
 89    # PEP 517 specifies that only `sys.executable` points to the correct
 90    # python interpreter
 91    base_command = [
 92        "maturin",
 93        "pep517",
 94        "build-wheel",
 95        "-i",
 96        _get_sys_executable(),
 97    ]
 98    options = _additional_pep517_args()
 99    if editable:
100        options.append("--editable")
101
102    pep517_args = get_maturin_pep517_args(config_settings)
103    if pep517_args:
104        options.extend(pep517_args)
105
106    if "--compatibility" not in options and "--manylinux" not in options:
107        # default to off if not otherwise specified
108        options = ["--compatibility", "off", *options]
109
110    command = [*base_command, *options]
111
112    print("Running `{}`".format(" ".join(command)))
113    sys.stdout.flush()
114    result = subprocess.run(command, stdout=subprocess.PIPE, env=_get_env())
115    sys.stdout.buffer.write(result.stdout)
116    sys.stdout.flush()
117    if result.returncode != 0:
118        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
119        sys.exit(1)
120    output = result.stdout.decode(errors="replace")
121    wheel_path = output.strip().splitlines()[-1]
122    filename = os.path.basename(wheel_path)
123    shutil.copy2(wheel_path, os.path.join(wheel_directory, filename))
124    return filename
125
126
127# noinspection PyUnusedLocal
128def build_wheel(
129    wheel_directory: str,
130    config_settings: Optional[Mapping[str, Any]] = None,
131    metadata_directory: Optional[str] = None,
132) -> str:
133    return _build_wheel(wheel_directory, config_settings, metadata_directory)
134
135
136# noinspection PyUnusedLocal
137def build_sdist(sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
138    command = ["maturin", "pep517", "write-sdist", "--sdist-directory", sdist_directory]
139
140    print("Running `{}`".format(" ".join(command)))
141    sys.stdout.flush()
142    result = subprocess.run(command, stdout=subprocess.PIPE, env=_get_env())
143    sys.stdout.buffer.write(result.stdout)
144    sys.stdout.flush()
145    if result.returncode != 0:
146        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
147        sys.exit(1)
148    output = result.stdout.decode(errors="replace")
149    return output.strip().splitlines()[-1]
150
151
152# noinspection PyUnusedLocal
153def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
154    if get_config().get("bindings") == "cffi":
155        requirements = ["cffi"]
156    else:
157        requirements = []
158    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
159        requirements += ["puccinialin"]
160    return requirements
161
162
163# noinspection PyUnusedLocal
164def build_editable(
165    wheel_directory: str,
166    config_settings: Optional[Mapping[str, Any]] = None,
167    metadata_directory: Optional[str] = None,
168) -> str:
169    return _build_wheel(wheel_directory, config_settings, metadata_directory, editable=True)
170
171
172# Requirements to build an editable are the same as for a wheel
173get_requires_for_build_editable = get_requires_for_build_wheel
174
175
176# noinspection PyUnusedLocal
177def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
178    requirements = []
179    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
180        requirements += ["puccinialin"]
181    return requirements
182
183
184# noinspection PyUnusedLocal
185def prepare_metadata_for_build_wheel(
186    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
187) -> str:
188    print("Checking for Rust toolchain....")
189    is_cargo_installed = False
190    try:
191        output = subprocess.check_output(["cargo", "--version"], env=_get_env()).decode("utf-8", "ignore")
192        if "cargo" in output:
193            is_cargo_installed = True
194    except (FileNotFoundError, SubprocessError):
195        pass
196
197    if not is_cargo_installed:
198        sys.stderr.write(
199            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
200            "This package requires Rust and Cargo to compile extensions. Install it through\n"
201            "the system's package manager or via https://rustup.rs/\n\n"
202        )
203        sys.exit(1)
204
205    command = [
206        "maturin",
207        "pep517",
208        "write-dist-info",
209        "--metadata-directory",
210        metadata_directory,
211        # PEP 517 specifies that only `sys.executable` points to the correct
212        # python interpreter
213        "--interpreter",
214        _get_sys_executable(),
215    ]
216    command.extend(_additional_pep517_args())
217    pep517_args = get_maturin_pep517_args(config_settings)
218    if pep517_args:
219        command.extend(pep517_args)
220
221    print("Running `{}`".format(" ".join(command)))
222    try:
223        _output = subprocess.check_output(command, env=_get_env())
224    except subprocess.CalledProcessError as e:
225        sys.stderr.write(f"Error running maturin: {e}\n")
226        sys.exit(1)
227    sys.stdout.buffer.write(_output)
228    sys.stdout.flush()
229    output = _output.decode(errors="replace")
230    return output.strip().splitlines()[-1]
231
232
233# Metadata for editable are the same as for a wheel
234prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
def get_config() -> Dict[str, str]:
26def get_config() -> Dict[str, str]:
27    try:
28        import tomllib
29    except ModuleNotFoundError:
30        import tomli as tomllib  # type: ignore
31
32    with open("pyproject.toml", "rb") as fp:
33        pyproject_toml = tomllib.load(fp)
34    return pyproject_toml.get("tool", {}).get("maturin", {})
def get_maturin_pep517_args(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
37def get_maturin_pep517_args(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
38    build_args = None
39    if config_settings:
40        # TODO: Deprecate and remove build-args in favor of maturin.build-args in maturin 2.0
41        build_args = config_settings.get("maturin.build-args", config_settings.get("build-args"))
42    if build_args is None:
43        env_args = os.getenv("MATURIN_PEP517_ARGS", "")
44        args = shlex.split(env_args)
45    elif isinstance(build_args, str):
46        args = shlex.split(build_args)
47    else:
48        args = build_args
49    return args
def build_wheel( wheel_directory: str, config_settings: Optional[Mapping[str, Any]] = None, metadata_directory: Optional[str] = None) -> str:
129def build_wheel(
130    wheel_directory: str,
131    config_settings: Optional[Mapping[str, Any]] = None,
132    metadata_directory: Optional[str] = None,
133) -> str:
134    return _build_wheel(wheel_directory, config_settings, metadata_directory)
def build_sdist( sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
138def build_sdist(sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
139    command = ["maturin", "pep517", "write-sdist", "--sdist-directory", sdist_directory]
140
141    print("Running `{}`".format(" ".join(command)))
142    sys.stdout.flush()
143    result = subprocess.run(command, stdout=subprocess.PIPE, env=_get_env())
144    sys.stdout.buffer.write(result.stdout)
145    sys.stdout.flush()
146    if result.returncode != 0:
147        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
148        sys.exit(1)
149    output = result.stdout.decode(errors="replace")
150    return output.strip().splitlines()[-1]
def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
154def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
155    if get_config().get("bindings") == "cffi":
156        requirements = ["cffi"]
157    else:
158        requirements = []
159    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
160        requirements += ["puccinialin"]
161    return requirements
def build_editable( wheel_directory: str, config_settings: Optional[Mapping[str, Any]] = None, metadata_directory: Optional[str] = None) -> str:
165def build_editable(
166    wheel_directory: str,
167    config_settings: Optional[Mapping[str, Any]] = None,
168    metadata_directory: Optional[str] = None,
169) -> str:
170    return _build_wheel(wheel_directory, config_settings, metadata_directory, editable=True)
def get_requires_for_build_editable(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
154def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
155    if get_config().get("bindings") == "cffi":
156        requirements = ["cffi"]
157    else:
158        requirements = []
159    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
160        requirements += ["puccinialin"]
161    return requirements
def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
178def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
179    requirements = []
180    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
181        requirements += ["puccinialin"]
182    return requirements
def prepare_metadata_for_build_wheel( metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
186def prepare_metadata_for_build_wheel(
187    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
188) -> str:
189    print("Checking for Rust toolchain....")
190    is_cargo_installed = False
191    try:
192        output = subprocess.check_output(["cargo", "--version"], env=_get_env()).decode("utf-8", "ignore")
193        if "cargo" in output:
194            is_cargo_installed = True
195    except (FileNotFoundError, SubprocessError):
196        pass
197
198    if not is_cargo_installed:
199        sys.stderr.write(
200            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
201            "This package requires Rust and Cargo to compile extensions. Install it through\n"
202            "the system's package manager or via https://rustup.rs/\n\n"
203        )
204        sys.exit(1)
205
206    command = [
207        "maturin",
208        "pep517",
209        "write-dist-info",
210        "--metadata-directory",
211        metadata_directory,
212        # PEP 517 specifies that only `sys.executable` points to the correct
213        # python interpreter
214        "--interpreter",
215        _get_sys_executable(),
216    ]
217    command.extend(_additional_pep517_args())
218    pep517_args = get_maturin_pep517_args(config_settings)
219    if pep517_args:
220        command.extend(pep517_args)
221
222    print("Running `{}`".format(" ".join(command)))
223    try:
224        _output = subprocess.check_output(command, env=_get_env())
225    except subprocess.CalledProcessError as e:
226        sys.stderr.write(f"Error running maturin: {e}\n")
227        sys.exit(1)
228    sys.stdout.buffer.write(_output)
229    sys.stdout.flush()
230    output = _output.decode(errors="replace")
231    return output.strip().splitlines()[-1]
def prepare_metadata_for_build_editable( metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
186def prepare_metadata_for_build_wheel(
187    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
188) -> str:
189    print("Checking for Rust toolchain....")
190    is_cargo_installed = False
191    try:
192        output = subprocess.check_output(["cargo", "--version"], env=_get_env()).decode("utf-8", "ignore")
193        if "cargo" in output:
194            is_cargo_installed = True
195    except (FileNotFoundError, SubprocessError):
196        pass
197
198    if not is_cargo_installed:
199        sys.stderr.write(
200            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
201            "This package requires Rust and Cargo to compile extensions. Install it through\n"
202            "the system's package manager or via https://rustup.rs/\n\n"
203        )
204        sys.exit(1)
205
206    command = [
207        "maturin",
208        "pep517",
209        "write-dist-info",
210        "--metadata-directory",
211        metadata_directory,
212        # PEP 517 specifies that only `sys.executable` points to the correct
213        # python interpreter
214        "--interpreter",
215        _get_sys_executable(),
216    ]
217    command.extend(_additional_pep517_args())
218    pep517_args = get_maturin_pep517_args(config_settings)
219    if pep517_args:
220        command.extend(pep517_args)
221
222    print("Running `{}`".format(" ".join(command)))
223    try:
224        _output = subprocess.check_output(command, env=_get_env())
225    except subprocess.CalledProcessError as e:
226        sys.stderr.write(f"Error running maturin: {e}\n")
227        sys.exit(1)
228    sys.stdout.buffer.write(_output)
229    sys.stdout.flush()
230    output = _output.decode(errors="replace")
231    return output.strip().splitlines()[-1]