Skip to content

Commit

Permalink
Install entrypoint scripts in develop command on Unix
Browse files Browse the repository at this point in the history
  • Loading branch information
messense committed Sep 21, 2021
1 parent 3aee10d commit 06f4812
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 48 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* auditwheel: add `libz.so.1` to whitelisted libraries in [#625](https://github.com/PyO3/maturin/pull/625)
* auditwheel: detect musl libc in [#629](https://github.com/PyO3/maturin/pull/629)
* Fixed Python 3.10 and later versions detection on Windows in [#630](https://github.com/PyO3/maturin/pull/630)
* Install entrypoint scripts in `maturin develop` command on Unix in [#633](https://github.com/PyO3/maturin/pull/633)

## [0.11.3] - 2021-08-25

Expand Down
3 changes: 1 addition & 2 deletions src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,7 @@ pub fn find_interpreter(
env::set_var("PYTHON_SYS_EXECUTABLE", &host_python.executable);

let sysconfig_path = find_sysconfigdata(cross_lib_dir.as_ref(), target)?;
let sysconfig_data =
parse_sysconfigdata(&host_python.executable, sysconfig_path)?;
let sysconfig_data = parse_sysconfigdata(host_python, sysconfig_path)?;
let major = sysconfig_data
.get("version_major")
.context("version_major is not defined")?
Expand Down
47 changes: 3 additions & 44 deletions src/cross_compile.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crate::target::get_host_target;
use crate::Target;
use crate::{PythonInterpreter, Target};
use anyhow::{bail, Result};
use fs_err::{self as fs, DirEntry};
use std::collections::HashMap;
use std::env;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

pub fn is_cross_compiling(target: &Target) -> Result<bool> {
let target_triple = target.target_triple();
Expand Down Expand Up @@ -45,7 +43,7 @@ pub fn is_cross_compiling(target: &Target) -> Result<bool> {
/// python executable and library. Here it is read and added to a script to extract only what is
/// necessary. This necessitates a python interpreter for the host machine to work.
pub fn parse_sysconfigdata(
interpreter: &Path,
interpreter: &PythonInterpreter,
config_path: impl AsRef<Path>,
) -> Result<HashMap<String, String>> {
let mut script = fs::read_to_string(config_path)?;
Expand All @@ -60,7 +58,7 @@ KEYS = [
for key in KEYS:
print(key, build_time_vars.get(key, ""))
"#;
let output = run_python_script(interpreter, &script)?;
let output = interpreter.run_script(&script)?;

Ok(parse_script_output(&output))
}
Expand All @@ -75,45 +73,6 @@ fn parse_script_output(output: &str) -> HashMap<String, String> {
.collect()
}

/// Run a python script using the specified interpreter binary.
fn run_python_script(interpreter: &Path, script: &str) -> Result<String> {
let out = Command::new(interpreter)
.env("PYTHONIOENCODING", "utf-8")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.as_mut()
.expect("piped stdin")
.write_all(script.as_bytes())?;
child.wait_with_output()
});

match out {
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
bail!(
"Could not find any interpreter at {}, \
are you sure you have Python installed on your PATH?",
interpreter.display()
);
} else {
bail!(
"Failed to run the Python interpreter at {}: {}",
interpreter.display(),
err
);
}
}
Ok(ok) if !ok.status.success() => bail!("Python script failed"),
Ok(ok) => Ok(String::from_utf8(ok.stdout)?),
}
}

fn starts_with(entry: &DirEntry, pat: &str) -> bool {
let name = entry.file_name();
name.to_string_lossy().starts_with(pat)
Expand Down
111 changes: 110 additions & 1 deletion src/develop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ use crate::compile::compile;
use crate::module_writer::{write_bindings_module, write_cffi_module, PathWriter};
use crate::PythonInterpreter;
use crate::Target;
use crate::{write_dist_info, BuildOptions};
use crate::{write_dist_info, BuildOptions, Metadata21};
use crate::{ModuleWriter, PlatformTag};
use anyhow::{anyhow, bail, format_err, Context, Result};
use fs_err as fs;
#[cfg(not(target_os = "windows"))]
use std::fs::OpenOptions;
use std::io::Write;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::process::Command;

Expand Down Expand Up @@ -191,5 +196,109 @@ pub fn develop(

writer.write_record(&build_context.metadata21)?;

write_entry_points(&interpreter, &build_context.metadata21)?;

Ok(())
}

/// https://packaging.python.org/specifications/entry-points/
///
/// entry points examples:
/// 1. `foomod:main`
/// 2. `foomod:main_bar [bar,baz]` where `bar` and `baz` are extra requires
fn parse_entry_point(entry: &str) -> Option<(&str, &str)> {
// remove extras since we don't care about them
let entry = entry
.split_once(' ')
.map(|(first, _)| first)
.unwrap_or(entry);
entry.split_once(':')
}

/// Build a shebang line. In the simple case (on Windows, or a shebang line
/// which is not too long or contains spaces) use a simple formulation for
/// the shebang. Otherwise, use /bin/sh as the executable, with a contrived
/// shebang which allows the script to run either under Python or sh, using
/// suitable quoting. Thanks to Harald Nordgren for his input.
/// See also: http://www.in-ulm.de/~mascheck/various/shebang/#length
/// https://hg.mozilla.org/mozilla-central/file/tip/mach
fn get_shebang(executable: &Path) -> String {
let executable = executable.display().to_string();
if cfg!(unix) {
let max_length = if cfg!(target_os = "macos") { 512 } else { 127 };
// Add 3 for '#!' prefix and newline suffix.
let shebang_length = executable.len() + 3;
if !executable.contains(' ') && shebang_length <= max_length {
return format!("#!{}\n", executable);
}
let mut shebang = "#!/bin/sh\n".to_string();
shebang.push_str(&format!("'''exec' {} \"$0\" \"$@\"\n' '''", executable));
shebang
} else {
format!("#!{}\n", executable)
}
}

fn write_entry_points(interpreter: &PythonInterpreter, metadata21: &Metadata21) -> Result<()> {
if cfg!(target_os = "windows") {
// FIXME: add Windows support
return Ok(());
}
let code = "import sysconfig; print(sysconfig.get_path('scripts'))";
let script_dir = interpreter.run_script(code)?;
let script_dir = Path::new(script_dir.trim());
// FIXME: On Windows shebang has to be used with Python launcher
let shebang = get_shebang(&interpreter.executable);
for (name, entry) in metadata21
.scripts
.iter()
.chain(metadata21.gui_scripts.iter())
{
let (module, func) =
parse_entry_point(entry).context("Invalid entry point specification")?;
let import_name = func.split_once('.').map(|(first, _)| first).unwrap_or(func);
let script = format!(
r#"# -*- coding: utf-8 -*-
import re
import sys
from {module} import {import_name}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit({func}())
"#,
module = module,
import_name = import_name,
func = func,
);
let script = shebang.clone() + &script;
// FIXME: on Windows scripts needs to have .exe extension
let script_path = script_dir.join(name);
// We only need to set the executable bit on unix
let mut file = {
#[cfg(not(target_os = "windows"))]
{
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o755)
.open(&script_path)
}
#[cfg(target_os = "windows")]
{
fs::File::create(&script_path)
}
}
.context(format!(
"Failed to create a file at {}",
script_path.display()
))?;

file.write_all(script.as_bytes()).context(format!(
"Failed to write to file at {}",
script_path.display()
))?;
}

Ok(())
}
41 changes: 40 additions & 1 deletion src/python_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::collections::HashSet;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Stdio};
use std::str;

/// This snippets will give us information about the python interpreter's
Expand Down Expand Up @@ -546,6 +546,45 @@ impl PythonInterpreter {

Ok(available_versions)
}

/// Run a python script using this Python interpreter.
pub fn run_script(&self, script: &str) -> Result<String> {
let out = Command::new(&self.executable)
.env("PYTHONIOENCODING", "utf-8")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.as_mut()
.expect("piped stdin")
.write_all(script.as_bytes())?;
child.wait_with_output()
});

match out {
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
bail!(
"Could not find any interpreter at {}, \
are you sure you have Python installed on your PATH?",
self.executable.display()
);
} else {
bail!(
"Failed to run the Python interpreter at {}: {}",
self.executable.display(),
err
);
}
}
Ok(ok) if !ok.status.success() => bail!("Python script failed"),
Ok(ok) => Ok(String::from_utf8(ok.stdout)?),
}
}
}

impl fmt::Display for PythonInterpreter {
Expand Down
5 changes: 5 additions & 0 deletions test-crates/pyo3-pure/check_installed/check_installed.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import os
import subprocess

import pyo3_pure

Expand All @@ -10,4 +11,8 @@
assert os.path.exists(os.path.join(install_path, "__init__.pyi"))
assert os.path.exists(os.path.join(install_path, "py.typed"))

# Check entrypoints (Unix only for now)
if os.name != "nt":
assert subprocess.run(["get_42"]).returncode == 42

print("SUCCESS")

0 comments on commit 06f4812

Please sign in to comment.