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]:
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:
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:
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]:
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]