diff --git a/Cargo.toml b/Cargo.toml index 02839551..8077a745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,10 +35,7 @@ assert_approx_eq = "1.1.0" trybuild = "1.0.23" [build-dependencies] -regex = { version = "1", default_features = false, features = ["std"] } version_check = "0.9.1" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" [features] default = [] diff --git a/build.rs b/build.rs index 6be47e7c..4459041d 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,13 @@ -use regex::Regex; -use serde::Deserialize; -use std::io::{self, BufRead, BufReader}; -use std::process::{Command, Stdio}; -use std::{collections::HashMap, convert::AsRef, env, fmt, fs::File, path::Path}; +use std::{ + collections::HashMap, + convert::AsRef, + env, fmt, + fs::File, + io::{self, BufRead, BufReader}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + str::FromStr, +}; use version_check::{Channel, Date, Version}; /// Specifies the minimum nightly version needed to compile pyo3. @@ -23,7 +28,7 @@ macro_rules! bail { } /// Information returned from python interpreter -#[derive(Deserialize, Debug)] +#[derive(Debug)] struct InterpreterConfig { version: PythonVersion, libdir: Option, @@ -31,17 +36,17 @@ struct InterpreterConfig { ld_version: String, /// Prefix used for determining the directory of libpython base_prefix: String, - executable: String, + executable: PathBuf, calcsize_pointer: Option, } -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum PythonInterpreterKind { CPython, PyPy, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] struct PythonVersion { major: u8, // minor == None means any minor version will do @@ -67,6 +72,17 @@ impl fmt::Display for PythonVersion { } } +impl FromStr for PythonInterpreterKind { + type Err = Box; + fn from_str(s: &str) -> Result { + match s { + "CPython" => Ok(Self::CPython), + "PyPy" => Ok(Self::PyPy), + _ => Err(format!("Invalid interpreter: {}", s).into()), + } + } +} + /// A list of python interpreter compile-time preprocessor defines that /// we will pick up and pass to rustc via --cfg=py_sys_config={varname}; /// this allows using them conditional cfg attributes in the .rs files, so @@ -101,22 +117,15 @@ static SYSCONFIG_VALUES: [&str; 1] = [ /// Attempts to parse the header at the given path, returning a map of definitions to their values. /// Each entry in the map directly corresponds to a `#define` in the given header. fn parse_header_defines(header_path: impl AsRef) -> Result> { - // This regex picks apart a C style, single line `#define` statement into an identifier and a - // value. e.g. for the line `#define Py_DEBUG 1`, this regex will capture `Py_DEBUG` into - // `ident` and `1` into `value`. - let define_regex = Regex::new(r"^\s*#define\s+(?P[a-zA-Z0-9_]+)\s+(?P.+)\s*$")?; - - let header_file = File::open(header_path.as_ref())?; - let header_reader = BufReader::new(&header_file); - + let header_reader = BufReader::new(File::open(header_path.as_ref())?); let mut definitions = HashMap::new(); - let tostr = |r: regex::Match<'_>| r.as_str().to_string(); for maybe_line in header_reader.lines() { - if let Some(captures) = define_regex.captures(&maybe_line?) { - match (captures.name("ident"), captures.name("value")) { - (Some(key), Some(val)) => definitions.insert(tostr(key), tostr(val)), - _ => None, - }; + let line = maybe_line?; + let mut i = line.trim().split_whitespace(); + if i.next() == Some("#define") { + if let (Some(key), Some(value), None) = (i.next(), i.next(), i.next()) { + definitions.insert(key.into(), value.into()); + } } } Ok(definitions) @@ -179,7 +188,7 @@ fn load_cross_compile_info() -> Result<(InterpreterConfig, HashMap Result<(InterpreterConfig, HashMap Result> { - // FIXME: We can do much better here using serde: - // import json, sysconfig; print(json.dumps({k:str(v) for k, v in sysconfig.get_config_vars().items()})) - +fn get_config_vars(python_path: &Path) -> Result> { let mut script = "import sysconfig; \ config = sysconfig.get_config_vars();" .to_owned(); @@ -228,7 +234,7 @@ fn get_config_vars(python_path: &str) -> Result> { } #[cfg(target_os = "windows")] -fn get_config_vars(_: &str) -> Result> { +fn get_config_vars(_: &Path) -> Result> { // sysconfig is missing all the flags on windows, so we can't actually // query the interpreter directly for its build flags. // @@ -274,7 +280,7 @@ fn cfg_line_for_var(key: &str, val: &str) -> Option { } /// Run a python script using the specified interpreter binary. -fn run_python_script(interpreter: &str, script: &str) -> Result { +fn run_python_script(interpreter: &Path, script: &str) -> Result { let out = Command::new(interpreter) .args(&["-c", script]) .stderr(Stdio::inherit()) @@ -286,12 +292,12 @@ fn run_python_script(interpreter: &str, script: &str) -> Result { bail!( "Could not find any interpreter at {}, \ are you sure you have Python installed on your PATH?", - interpreter + interpreter.display() ); } else { bail!( "Failed to run the Python interpreter at {}: {}", - interpreter, + interpreter.display(), err ); } @@ -395,33 +401,24 @@ fn get_rustc_link_lib(config: &InterpreterConfig) -> Result { /// /// If none of the above works, an error is returned fn find_interpreter_and_get_config() -> Result<(InterpreterConfig, HashMap)> { - if let Some(sys_executable) = env::var_os("PYTHON_SYS_EXECUTABLE") { - let interpreter_path = sys_executable - .to_str() - .ok_or("Unable to get PYTHON_SYS_EXECUTABLE value")?; - let interpreter_config = get_config_from_interpreter(interpreter_path)?; - - return Ok((interpreter_config, get_config_vars(interpreter_path)?)); + let python_interpreter = if let Some(exe) = env::var_os("PYTHON_SYS_EXECUTABLE") { + exe.into() + } else { + PathBuf::from( + ["python", "python3"] + .iter() + .find(|bin| { + if let Ok(out) = Command::new(bin).arg("--version").output() { + // begin with `Python 3.X.X :: additional info` + out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3") + } else { + false + } + }) + .ok_or("Python 3.x interpreter not found")?, + ) }; - let python_interpreter = ["python", "python3"] - .iter() - .find(|bin| { - if let Ok(out) = Command::new(bin).arg("--version").output() { - // begin with `Python 3.X.X :: additional info` - out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3") - } else { - false - } - }) - .ok_or("Python 3.x interpreter not found")?; - - // check default python - let interpreter_config = get_config_from_interpreter(&python_interpreter)?; - if interpreter_config.version.major == 3 { - return Ok((interpreter_config, get_config_vars(&python_interpreter)?)); - } - let interpreter_config = get_config_from_interpreter(&python_interpreter)?; if interpreter_config.version.major == 3 { return Ok((interpreter_config, get_config_vars(&python_interpreter)?)); @@ -431,7 +428,7 @@ fn find_interpreter_and_get_config() -> Result<(InterpreterConfig, HashMap Result { +fn get_config_from_interpreter(interpreter: &Path) -> Result { let script = r#" import json import platform @@ -446,23 +443,40 @@ try: except AttributeError: base_prefix = sys.exec_prefix -print(json.dumps({ - "version": { - "major": sys.version_info[0], - "minor": sys.version_info[1], - "implementation": platform.python_implementation() - }, - "libdir": sysconfig.get_config_var('LIBDIR'), - "ld_version": sysconfig.get_config_var('LDVERSION') or sysconfig.get_config_var('py_version_short'), - "base_prefix": base_prefix, - "shared": PYPY or bool(sysconfig.get_config_var('Py_ENABLE_SHARED')), - "executable": sys.executable, - "calcsize_pointer": struct.calcsize("P"), -})) +libdir = sysconfig.get_config_var('LIBDIR') + +print("version_major", sys.version_info[0]) +print("version_minor", sys.version_info[1]) +print("implementation", platform.python_implementation()) +if libdir is not None: + print("libdir", libdir) +print("ld_version", sysconfig.get_config_var('LDVERSION') or sysconfig.get_config_var('py_version_short')) +print("base_prefix", base_prefix) +print("shared", PYPY or bool(sysconfig.get_config_var('Py_ENABLE_SHARED'))) +print("executable", sys.executable) +print("calcsize_pointer", struct.calcsize("P")) "#; - let json = run_python_script(interpreter, script)?; - Ok(serde_json::from_str(&json) - .map_err(|e| format!("Failed to get InterPreterConfig: {}", e))?) + let output = run_python_script(interpreter, script)?; + let map: HashMap = output + .lines() + .filter_map(|line| { + let mut i = line.splitn(2, ' '); + Some((i.next()?.into(), i.next()?.into())) + }) + .collect(); + Ok(InterpreterConfig { + version: PythonVersion { + major: map["version_major"].parse()?, + minor: Some(map["version_minor"].parse()?), + implementation: map["implementation"].parse()?, + }, + libdir: map.get("libdir").cloned(), + shared: map["shared"] == "True", + ld_version: map["ld_version"].clone(), + base_prefix: map["base_prefix"].clone(), + executable: map["executable"].clone().into(), + calcsize_pointer: Some(map["calcsize_pointer"].parse()?), + }) } fn configure(interpreter_config: &InterpreterConfig) -> Result {