Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Install entrypoint scripts in develop command on Unix #633

Merged
merged 1 commit into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")