use {
crate::py_packaging::config::PyembedPythonInterpreterConfig,
anyhow::{anyhow, Context, Result},
pyo3_build_config::{
BuildFlags, InterpreterConfig as PyO3InterpreterConfig, PythonImplementation, PythonVersion,
},
python_packaging::{
licensing::{LicensedComponent, LicensedComponents},
resource_collection::CompiledResourcesCollection,
},
simple_file_manifest::{FileEntry, FileManifest},
std::path::{Path, PathBuf},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LinkingAnnotation {
LinkFramework(String),
LinkLibrary(String),
LinkLibraryStatic(String),
Search(PathBuf),
SearchNative(PathBuf),
Argument(String),
}
impl LinkingAnnotation {
pub fn to_cargo_annotation(&self) -> String {
match self {
Self::LinkFramework(framework) => {
format!("cargo:rustc-link-lib=framework={}", framework)
}
Self::LinkLibrary(lib) => format!("cargo:rustc-link-lib={}", lib),
Self::LinkLibraryStatic(lib) => format!("cargo:rustc-link-lib=static={}", lib),
Self::Search(path) => format!("cargo:rustc-link-search={}", path.display()),
Self::SearchNative(path) => {
format!("cargo:rustc-link-search=native={}", path.display())
}
Self::Argument(arg) => {
format!("cargo:rustc-link-arg={}", arg)
}
}
}
}
pub fn linking_annotations_for_target(target_triple: &str) -> Vec<LinkingAnnotation> {
if target_triple.contains("-linux-") {
vec![LinkingAnnotation::Argument("-Wl,-export-dynamic".into())]
} else if target_triple.contains("-apple-darwin") {
vec![LinkingAnnotation::Argument("-rdynamic".into())]
} else {
vec![]
}
}
pub trait LinkablePython {
fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()>;
fn linking_annotations(
&self,
dest_dir: &Path,
alias: bool,
target_triple: &str,
) -> Result<Vec<LinkingAnnotation>>;
}
#[derive(Clone, Debug)]
pub struct LinkSharedLibraryPath {
pub library_path: PathBuf,
pub linking_annotations: Vec<LinkingAnnotation>,
}
impl LinkSharedLibraryPath {
fn library_name(&self) -> Result<String> {
let filename = self
.library_path
.file_name()
.ok_or_else(|| anyhow!("unable to resolve shared library file name"))?
.to_string_lossy();
if filename.ends_with(".dll") {
Ok(filename.trim_end_matches(".dll").to_string())
} else if filename.ends_with(".dylib") {
Ok(filename
.trim_end_matches(".dylib")
.trim_start_matches("lib")
.to_string())
} else if filename.ends_with(".so") {
Ok(filename
.trim_end_matches(".so")
.trim_start_matches("lib")
.to_string())
} else {
Err(anyhow!(
"unhandled libpython shared library filename: {}",
filename
))
}
}
}
impl LinkablePython for LinkSharedLibraryPath {
fn write_files(&self, _dest_dir: &Path, _target_triple: &str) -> Result<()> {
Ok(())
}
fn linking_annotations(
&self,
_dest_dir: &Path,
alias: bool,
target_triple: &str,
) -> Result<Vec<LinkingAnnotation>> {
let lib_dir = self
.library_path
.parent()
.ok_or_else(|| anyhow!("could not derive parent directory of library path"))?;
let mut annotations = vec![
LinkingAnnotation::LinkLibrary(if alias {
format!("pythonXY:{}", self.library_name()?)
} else {
self.library_name()?
}),
LinkingAnnotation::SearchNative(lib_dir.to_path_buf()),
];
annotations.extend(self.linking_annotations.iter().cloned());
annotations.extend(linking_annotations_for_target(target_triple));
Ok(annotations)
}
}
#[derive(Clone, Debug)]
pub struct LinkStaticLibraryData {
pub library_data: Vec<u8>,
pub linking_annotations: Vec<LinkingAnnotation>,
}
impl LinkStaticLibraryData {
fn library_name(&self) -> &'static str {
"python3"
}
fn library_path(&self, dest_dir: impl AsRef<Path>, target_triple: &str) -> PathBuf {
dest_dir
.as_ref()
.join(if target_triple.contains("-windows-") {
format!("{}.lib", self.library_name())
} else {
format!("lib{}.a", self.library_name())
})
}
}
impl LinkablePython for LinkStaticLibraryData {
fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()> {
let lib_path = self.library_path(dest_dir, target_triple);
std::fs::write(&lib_path, &self.library_data)
.with_context(|| format!("writing {}", lib_path.display()))?;
Ok(())
}
fn linking_annotations(
&self,
dest_dir: &Path,
alias: bool,
target_triple: &str,
) -> Result<Vec<LinkingAnnotation>> {
let mut annotations = vec![
LinkingAnnotation::LinkLibraryStatic(if alias {
format!("pythonXY:{}", self.library_name())
} else {
self.library_name().to_string()
}),
LinkingAnnotation::SearchNative(dest_dir.to_path_buf()),
];
annotations.extend(self.linking_annotations.iter().cloned());
annotations.extend(linking_annotations_for_target(target_triple));
Ok(annotations)
}
}
pub enum LibpythonLinkSettings {
ExistingDynamic(LinkSharedLibraryPath),
StaticData(LinkStaticLibraryData),
}
impl LinkablePython for LibpythonLinkSettings {
fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()> {
match self {
Self::ExistingDynamic(l) => l.write_files(dest_dir, target_triple),
Self::StaticData(l) => l.write_files(dest_dir, target_triple),
}
}
fn linking_annotations(
&self,
dest_dir: &Path,
alias: bool,
target_triple: &str,
) -> Result<Vec<LinkingAnnotation>> {
match self {
Self::ExistingDynamic(l) => l.linking_annotations(dest_dir, alias, target_triple),
Self::StaticData(l) => l.linking_annotations(dest_dir, alias, target_triple),
}
}
}
impl From<LinkSharedLibraryPath> for LibpythonLinkSettings {
fn from(l: LinkSharedLibraryPath) -> Self {
Self::ExistingDynamic(l)
}
}
impl From<LinkStaticLibraryData> for LibpythonLinkSettings {
fn from(l: LinkStaticLibraryData) -> Self {
Self::StaticData(l)
}
}
pub const DEFAULT_PYTHON_CONFIG_FILENAME: &str = "default_python_config.rs";
pub struct EmbeddedPythonContext<'a> {
pub config: PyembedPythonInterpreterConfig,
pub link_settings: LibpythonLinkSettings,
pub pending_resources: Vec<(CompiledResourcesCollection<'a>, PathBuf)>,
pub extra_files: FileManifest,
pub host_triple: String,
pub target_triple: String,
pub python_implementation: PythonImplementation,
pub python_version: PythonVersion,
pub python_exe_host: PathBuf,
pub python_build_flags: BuildFlags,
pub licensing_filename: Option<String>,
pub licensing: LicensedComponents,
}
impl<'a> EmbeddedPythonContext<'a> {
pub fn interpreter_config_rs_path(&self, dest_dir: impl AsRef<Path>) -> PathBuf {
dest_dir.as_ref().join(DEFAULT_PYTHON_CONFIG_FILENAME)
}
pub fn pyo3_config_path(&self, dest_dir: impl AsRef<Path>) -> PathBuf {
dest_dir.as_ref().join("pyo3-build-config-file.txt")
}
pub fn pyo3_interpreter_config(
&self,
dest_dir: impl AsRef<Path>,
) -> Result<PyO3InterpreterConfig> {
Ok(PyO3InterpreterConfig {
implementation: self.python_implementation,
version: self.python_version,
shared: matches!(
&self.link_settings,
LibpythonLinkSettings::ExistingDynamic(_)
),
abi3: false,
lib_name: None,
lib_dir: None,
executable: Some(self.python_exe_host.to_string_lossy().to_string()),
pointer_width: Some(if self.target_triple.starts_with("i686-") {
32
} else {
64
}),
build_flags: BuildFlags(self.python_build_flags.0.clone()),
suppress_build_script_link_lines: true,
extra_build_script_lines: self
.link_settings
.linking_annotations(
dest_dir.as_ref(),
self.target_triple.contains("-windows-"),
&self.target_triple,
)?
.iter()
.map(|la| la.to_cargo_annotation())
.collect::<Vec<_>>(),
})
}
pub fn write_packed_resources(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
for (collection, path) in &self.pending_resources {
let dest_path = dest_dir.as_ref().join(path);
let mut writer = std::io::BufWriter::new(
std::fs::File::create(&dest_path)
.with_context(|| format!("opening {} for writing", dest_path.display()))?,
);
collection
.write_packed_resources(&mut writer)
.context("writing packed resources")?;
}
Ok(())
}
pub fn write_libpython(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
self.link_settings
.write_files(dest_dir.as_ref(), &self.target_triple)
}
pub fn write_interpreter_config_rs(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
self.config
.write_default_python_config_rs(self.interpreter_config_rs_path(&dest_dir))?;
Ok(())
}
pub fn write_pyo3_config(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
let dest_dir = dest_dir.as_ref();
let mut fh = std::fs::File::create(self.pyo3_config_path(dest_dir))?;
self.pyo3_interpreter_config(dest_dir)?
.to_writer(&mut fh)
.map_err(|e| anyhow!("error writing PyO3 config file: {}", e))?;
Ok(())
}
pub fn write_licensing(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
if let Some(filename) = &self.licensing_filename {
let text = self.licensing.aggregate_license_document(false)?;
std::fs::write(dest_dir.as_ref().join(filename), text.as_bytes())?;
}
Ok(())
}
pub fn write_files(&self, dest_dir: &Path) -> Result<()> {
self.write_packed_resources(dest_dir)
.context("write_packed_resources()")?;
self.write_libpython(dest_dir)
.context("write_libpython()")?;
self.write_interpreter_config_rs(dest_dir)
.context("write_interpreter_config_rs()")?;
self.write_pyo3_config(dest_dir)
.context("write_pyo3_config()")?;
self.write_licensing(dest_dir)
.context("write_licensing()")?;
Ok(())
}
pub fn licensing(&self) -> &LicensedComponents {
&self.licensing
}
pub fn add_licensed_component(&mut self, component: LicensedComponent) -> Result<()> {
self.licensing.add_component(component);
self.synchronize_licensing()?;
Ok(())
}
pub fn synchronize_licensing(&mut self) -> Result<()> {
if let Some(filename) = &self.licensing_filename {
self.extra_files.add_file_entry(
filename,
FileEntry::new_from_data(
self.licensing.aggregate_license_document(false)?.as_bytes(),
false,
),
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dynamic_library_name() -> Result<()> {
assert_eq!(
LinkSharedLibraryPath {
library_path: "libpython3.9.so".into(),
linking_annotations: vec![],
}
.library_name()?,
"python3.9"
);
assert_eq!(
LinkSharedLibraryPath {
library_path: "libpython3.9.dylib".into(),
linking_annotations: vec![],
}
.library_name()?,
"python3.9"
);
assert_eq!(
LinkSharedLibraryPath {
library_path: "python3.dll".into(),
linking_annotations: vec![],
}
.library_name()?,
"python3"
);
Ok(())
}
}