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
 24try:
 25    import tomllib
 26except ModuleNotFoundError:
 27    import tomli as tomllib  # type: ignore
 28
 29
 30def get_config() -> Dict[str, str]:
 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 = config_settings.get("build-args") if config_settings else None
 38    if build_args is None:
 39        env_args = os.getenv("MATURIN_PEP517_ARGS", "")
 40        args = shlex.split(env_args)
 41    elif isinstance(build_args, str):
 42        args = shlex.split(build_args)
 43    else:
 44        args = build_args
 45    return args
 46
 47
 48def _get_sys_executable() -> str:
 49    executable = sys.executable
 50    if os.getenv("MATURIN_PEP517_USE_BASE_PYTHON") in {"1", "true"}:
 51        # Use the base interpreter path when running inside a venv to avoid recompilation
 52        # when switching between venvs
 53        base_executable = getattr(sys, "_base_executable")
 54        if base_executable and os.path.exists(base_executable):
 55            executable = os.path.realpath(base_executable)
 56    return executable
 57
 58
 59def _additional_pep517_args() -> List[str]:
 60    # Support building for 32-bit Python on x64 Windows
 61    if platform.system().lower() == "windows" and platform.machine().lower() == "amd64":
 62        pointer_width = struct.calcsize("P") * 8
 63        if pointer_width == 32:
 64            return ["--target", "i686-pc-windows-msvc"]
 65    return []
 66
 67
 68# noinspection PyUnusedLocal
 69def _build_wheel(
 70    wheel_directory: str,
 71    config_settings: Optional[Mapping[str, Any]] = None,
 72    metadata_directory: Optional[str] = None,
 73    editable: bool = False,
 74) -> str:
 75    # PEP 517 specifies that only `sys.executable` points to the correct
 76    # python interpreter
 77    base_command = [
 78        "maturin",
 79        "pep517",
 80        "build-wheel",
 81        "-i",
 82        _get_sys_executable(),
 83    ]
 84    options = _additional_pep517_args()
 85    if editable:
 86        options.append("--editable")
 87
 88    pep517_args = get_maturin_pep517_args(config_settings)
 89    if pep517_args:
 90        options.extend(pep517_args)
 91
 92    if "--compatibility" not in options and "--manylinux" not in options:
 93        # default to off if not otherwise specified
 94        options = ["--compatibility", "off", *options]
 95
 96    command = [*base_command, *options]
 97
 98    print("Running `{}`".format(" ".join(command)))
 99    sys.stdout.flush()
100    result = subprocess.run(command, stdout=subprocess.PIPE)
101    sys.stdout.buffer.write(result.stdout)
102    sys.stdout.flush()
103    if result.returncode != 0:
104        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
105        sys.exit(1)
106    output = result.stdout.decode(errors="replace")
107    wheel_path = output.strip().splitlines()[-1]
108    filename = os.path.basename(wheel_path)
109    shutil.copy2(wheel_path, os.path.join(wheel_directory, filename))
110    return filename
111
112
113# noinspection PyUnusedLocal
114def build_wheel(
115    wheel_directory: str,
116    config_settings: Optional[Mapping[str, Any]] = None,
117    metadata_directory: Optional[str] = None,
118) -> str:
119    return _build_wheel(wheel_directory, config_settings, metadata_directory)
120
121
122# noinspection PyUnusedLocal
123def build_sdist(sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
124    command = ["maturin", "pep517", "write-sdist", "--sdist-directory", sdist_directory]
125
126    print("Running `{}`".format(" ".join(command)))
127    sys.stdout.flush()
128    result = subprocess.run(command, stdout=subprocess.PIPE)
129    sys.stdout.buffer.write(result.stdout)
130    sys.stdout.flush()
131    if result.returncode != 0:
132        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
133        sys.exit(1)
134    output = result.stdout.decode(errors="replace")
135    return output.strip().splitlines()[-1]
136
137
138# noinspection PyUnusedLocal
139def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
140    if get_config().get("bindings") == "cffi":
141        return ["cffi"]
142    else:
143        return []
144
145
146# noinspection PyUnusedLocal
147def build_editable(
148    wheel_directory: str,
149    config_settings: Optional[Mapping[str, Any]] = None,
150    metadata_directory: Optional[str] = None,
151) -> str:
152    return _build_wheel(wheel_directory, config_settings, metadata_directory, editable=True)
153
154
155# Requirements to build an editable are the same as for a wheel
156get_requires_for_build_editable = get_requires_for_build_wheel
157
158
159# noinspection PyUnusedLocal
160def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
161    return []
162
163
164# noinspection PyUnusedLocal
165def prepare_metadata_for_build_wheel(
166    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
167) -> str:
168    print("Checking for Rust toolchain....")
169    is_cargo_installed = False
170    try:
171        output = subprocess.check_output(["cargo", "--version"]).decode("utf-8", "ignore")
172        if "cargo" in output:
173            is_cargo_installed = True
174    except (FileNotFoundError, SubprocessError):
175        pass
176
177    if not is_cargo_installed:
178        sys.stderr.write(
179            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
180            "This package requires Rust and Cargo to compile extensions. Install it through\n"
181            "the system's package manager or via https://rustup.rs/\n\n"
182        )
183        sys.exit(1)
184
185    command = [
186        "maturin",
187        "pep517",
188        "write-dist-info",
189        "--metadata-directory",
190        metadata_directory,
191        # PEP 517 specifies that only `sys.executable` points to the correct
192        # python interpreter
193        "--interpreter",
194        _get_sys_executable(),
195    ]
196    command.extend(_additional_pep517_args())
197    pep517_args = get_maturin_pep517_args(config_settings)
198    if pep517_args:
199        command.extend(pep517_args)
200
201    print("Running `{}`".format(" ".join(command)))
202    try:
203        _output = subprocess.check_output(command)
204    except subprocess.CalledProcessError as e:
205        sys.stderr.write(f"Error running maturin: {e}\n")
206        sys.exit(1)
207    sys.stdout.buffer.write(_output)
208    sys.stdout.flush()
209    output = _output.decode(errors="replace")
210    return output.strip().splitlines()[-1]
211
212
213# Metadata for editable are the same as for a wheel
214prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel
def get_config() -> Dict[str, str]:
31def get_config() -> Dict[str, str]:
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 = config_settings.get("build-args") if config_settings else None
39    if build_args is None:
40        env_args = os.getenv("MATURIN_PEP517_ARGS", "")
41        args = shlex.split(env_args)
42    elif isinstance(build_args, str):
43        args = shlex.split(build_args)
44    else:
45        args = build_args
46    return args
def build_wheel( wheel_directory: str, config_settings: Optional[Mapping[str, Any]] = None, metadata_directory: Optional[str] = None) -> str:
115def build_wheel(
116    wheel_directory: str,
117    config_settings: Optional[Mapping[str, Any]] = None,
118    metadata_directory: Optional[str] = None,
119) -> str:
120    return _build_wheel(wheel_directory, config_settings, metadata_directory)
def build_sdist( sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
124def build_sdist(sdist_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
125    command = ["maturin", "pep517", "write-sdist", "--sdist-directory", sdist_directory]
126
127    print("Running `{}`".format(" ".join(command)))
128    sys.stdout.flush()
129    result = subprocess.run(command, stdout=subprocess.PIPE)
130    sys.stdout.buffer.write(result.stdout)
131    sys.stdout.flush()
132    if result.returncode != 0:
133        sys.stderr.write(f"Error: command {command} returned non-zero exit status {result.returncode}\n")
134        sys.exit(1)
135    output = result.stdout.decode(errors="replace")
136    return output.strip().splitlines()[-1]
def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
140def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
141    if get_config().get("bindings") == "cffi":
142        return ["cffi"]
143    else:
144        return []
def build_editable( wheel_directory: str, config_settings: Optional[Mapping[str, Any]] = None, metadata_directory: Optional[str] = None) -> str:
148def build_editable(
149    wheel_directory: str,
150    config_settings: Optional[Mapping[str, Any]] = None,
151    metadata_directory: Optional[str] = None,
152) -> str:
153    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]:
140def get_requires_for_build_wheel(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
141    if get_config().get("bindings") == "cffi":
142        return ["cffi"]
143    else:
144        return []
def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
161def get_requires_for_build_sdist(config_settings: Optional[Mapping[str, Any]] = None) -> List[str]:
162    return []
def prepare_metadata_for_build_wheel( metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
166def prepare_metadata_for_build_wheel(
167    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
168) -> str:
169    print("Checking for Rust toolchain....")
170    is_cargo_installed = False
171    try:
172        output = subprocess.check_output(["cargo", "--version"]).decode("utf-8", "ignore")
173        if "cargo" in output:
174            is_cargo_installed = True
175    except (FileNotFoundError, SubprocessError):
176        pass
177
178    if not is_cargo_installed:
179        sys.stderr.write(
180            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
181            "This package requires Rust and Cargo to compile extensions. Install it through\n"
182            "the system's package manager or via https://rustup.rs/\n\n"
183        )
184        sys.exit(1)
185
186    command = [
187        "maturin",
188        "pep517",
189        "write-dist-info",
190        "--metadata-directory",
191        metadata_directory,
192        # PEP 517 specifies that only `sys.executable` points to the correct
193        # python interpreter
194        "--interpreter",
195        _get_sys_executable(),
196    ]
197    command.extend(_additional_pep517_args())
198    pep517_args = get_maturin_pep517_args(config_settings)
199    if pep517_args:
200        command.extend(pep517_args)
201
202    print("Running `{}`".format(" ".join(command)))
203    try:
204        _output = subprocess.check_output(command)
205    except subprocess.CalledProcessError as e:
206        sys.stderr.write(f"Error running maturin: {e}\n")
207        sys.exit(1)
208    sys.stdout.buffer.write(_output)
209    sys.stdout.flush()
210    output = _output.decode(errors="replace")
211    return output.strip().splitlines()[-1]
def prepare_metadata_for_build_editable( metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None) -> str:
166def prepare_metadata_for_build_wheel(
167    metadata_directory: str, config_settings: Optional[Mapping[str, Any]] = None
168) -> str:
169    print("Checking for Rust toolchain....")
170    is_cargo_installed = False
171    try:
172        output = subprocess.check_output(["cargo", "--version"]).decode("utf-8", "ignore")
173        if "cargo" in output:
174            is_cargo_installed = True
175    except (FileNotFoundError, SubprocessError):
176        pass
177
178    if not is_cargo_installed:
179        sys.stderr.write(
180            "\nCargo, the Rust package manager, is not installed or is not on PATH.\n"
181            "This package requires Rust and Cargo to compile extensions. Install it through\n"
182            "the system's package manager or via https://rustup.rs/\n\n"
183        )
184        sys.exit(1)
185
186    command = [
187        "maturin",
188        "pep517",
189        "write-dist-info",
190        "--metadata-directory",
191        metadata_directory,
192        # PEP 517 specifies that only `sys.executable` points to the correct
193        # python interpreter
194        "--interpreter",
195        _get_sys_executable(),
196    ]
197    command.extend(_additional_pep517_args())
198    pep517_args = get_maturin_pep517_args(config_settings)
199    if pep517_args:
200        command.extend(pep517_args)
201
202    print("Running `{}`".format(" ".join(command)))
203    try:
204        _output = subprocess.check_output(command)
205    except subprocess.CalledProcessError as e:
206        sys.stderr.write(f"Error running maturin: {e}\n")
207        sys.exit(1)
208    sys.stdout.buffer.write(_output)
209    sys.stdout.flush()
210    output = _output.decode(errors="replace")
211    return output.strip().splitlines()[-1]