use {
crate::{
environment::Environment,
py_packaging::{distribution::AppleSdkInfo, embedding::LinkingAnnotation},
},
anyhow::{anyhow, Context, Result},
apple_sdk::AppleSdk,
duct::cmd,
log::warn,
python_packaging::libpython::LibPythonBuildContext,
simple_file_manifest::FileData,
std::{
collections::BTreeSet,
ffi::OsStr,
fs,
fs::create_dir_all,
hash::Hasher,
io::{BufRead, BufReader, Cursor},
path::{Path, PathBuf},
},
};
#[cfg(target_family = "unix")]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
fn osstr_to_bytes(s: &OsStr) -> Result<Vec<u8>> {
Ok(s.as_bytes().to_vec())
}
#[cfg(not(unix))]
fn osstr_to_bytes(s: &OsStr) -> Result<Vec<u8>> {
let utf8: &str = s
.to_str()
.ok_or_else(|| anyhow!("invalid UTF-8 filename"))?;
Ok(utf8.as_bytes().to_vec())
}
pub fn make_config_c<T>(extensions: &[(T, T)]) -> String
where
T: AsRef<str>,
{
let mut lines: Vec<String> = vec!["#include \"Python.h\"".to_string()];
for (_name, init_fn) in extensions {
if init_fn.as_ref() != "NULL" {
lines.push(format!("extern PyObject* {}(void);", init_fn.as_ref()));
}
}
lines.push(String::from("struct _inittab _PyImport_Inittab[] = {"));
for (name, init_fn) in extensions {
lines.push(format!("{{\"{}\", {}}},", name.as_ref(), init_fn.as_ref()));
}
lines.push(String::from("{0, 0}"));
lines.push(String::from("};"));
lines.join("\n")
}
fn create_ar_symbols_index(dest_dir: &Path, lib_data: &[u8]) -> Result<Vec<u8>> {
let lib_path = dest_dir.join("lib.a");
std::fs::write(&lib_path, lib_data).context("writing archive to temporary file")?;
warn!("invoking `ar s` to index archive symbols");
let command = cmd("ar", &["s".to_string(), lib_path.display().to_string()])
.stderr_to_stdout()
.unchecked()
.reader()?;
{
let reader = BufReader::new(&command);
for line in reader.lines() {
warn!("{}", line?);
}
}
let output = command
.try_wait()?
.ok_or_else(|| anyhow!("unable to wait on ar"))?;
if !output.status.success() {
return Err(anyhow!("failed to invoke `ar s`"));
}
Ok(std::fs::read(&lib_path)?)
}
fn ar_header(path: &Path) -> Result<ar::Header> {
let filename = path
.file_name()
.ok_or_else(|| anyhow!("could not determine file name"))?;
let identifier = osstr_to_bytes(filename)?;
let metadata = std::fs::metadata(path)?;
let mut header = ar::Header::from_metadata(identifier, &metadata);
header.set_uid(0);
header.set_gid(0);
header.set_mtime(0);
header.set_mode(0o644);
Ok(header)
}
fn assemble_archive_gnu(objects: &[PathBuf], temp_dir: &Path) -> Result<Vec<u8>> {
let buffer = Cursor::new(vec![]);
let identifiers = objects
.iter()
.map(|p| {
Ok(p.file_name()
.ok_or_else(|| anyhow!("object file name could not be determined"))?
.to_string_lossy()
.as_bytes()
.to_vec())
})
.collect::<Result<Vec<_>>>()?;
let mut builder = ar::GnuBuilder::new(buffer, identifiers);
for path in objects {
let header = ar_header(path)
.with_context(|| format!("resolving ar header for {}", path.display()))?;
let fh = std::fs::File::open(path)?;
builder.append(&header, fh)?;
}
let data = builder.into_inner()?.into_inner();
create_ar_symbols_index(temp_dir, &data)
}
fn assemble_archive_bsd(objects: &[PathBuf], temp_dir: &Path) -> Result<Vec<u8>> {
let buffer = Cursor::new(vec![]);
let mut builder = ar::Builder::new(buffer);
for path in objects {
let header = ar_header(path)
.with_context(|| format!("resolving ar header for {}", path.display()))?;
let fh = std::fs::File::open(path)?;
builder.append(&header, fh)?;
}
let data = builder.into_inner()?.into_inner();
create_ar_symbols_index(temp_dir, &data)
}
#[derive(Debug)]
pub struct LibpythonInfo {
pub libpython_data: Vec<u8>,
pub linking_annotations: Vec<LinkingAnnotation>,
}
#[allow(clippy::too_many_arguments)]
pub fn link_libpython(
env: &Environment,
context: &LibPythonBuildContext,
host_triple: &str,
target_triple: &str,
opt_level: &str,
apple_sdk_info: Option<&AppleSdkInfo>,
) -> Result<LibpythonInfo> {
let temp_dir = env.temporary_directory("pyoxidizer-libpython")?;
let config_c_dir = temp_dir.path().join("config_c");
std::fs::create_dir(&config_c_dir).context("creating config_c subdirectory")?;
let libpython_dir = temp_dir.path().join("libpython");
std::fs::create_dir(&libpython_dir).context("creating libpython subdirectory")?;
let mut linking_annotations = vec![];
let windows = crate::environment::WINDOWS_TARGET_TRIPLES.contains(&target_triple);
warn!(
"deriving custom config.c from {} extension modules",
context.init_functions.len()
);
let config_c_source = make_config_c(&context.init_functions.iter().collect::<Vec<_>>());
let config_c_path = config_c_dir.join("config.c");
let config_object_path = if config_c_path.has_root() {
let dirname = config_c_path
.parent()
.ok_or_else(|| anyhow!("could not determine parent directory"))?;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write(dirname.to_string_lossy().as_bytes());
config_c_dir.join(format!("{:016x}-{}", hasher.finish(), "config.o"))
} else {
config_c_dir.join("config.o")
};
fs::write(&config_c_path, config_c_source.as_bytes())?;
for (rel_path, location) in &context.includes {
let full = config_c_dir.join(rel_path);
create_dir_all(
full.parent()
.ok_or_else(|| anyhow!("unable to resolve parent directory"))?,
)?;
let data = location.resolve_content()?;
std::fs::write(&full, &data)?;
}
warn!("compiling custom config.c to object file");
let mut build = cc::Build::new();
if let Some(flags) = &context.inittab_cflags {
for flag in flags {
build.flag(flag);
}
}
if target_triple.contains("-apple-") {
let sdk_info = apple_sdk_info.ok_or_else(|| {
anyhow!("Apple SDK info should be defined when targeting Apple platforms")
})?;
let sdk = env
.resolve_apple_sdk(sdk_info)
.context("resolving Apple SDK to use")?;
build.flag("-isysroot");
build.flag(&format!("{}", sdk.path().display()));
}
build
.out_dir(&config_c_dir)
.host(host_triple)
.target(target_triple)
.opt_level_str(opt_level)
.file(&config_c_path)
.include(&config_c_dir)
.cargo_metadata(false)
.compile("irrelevant");
warn!("resolving inputs for custom Python library...");
let mut objects = BTreeSet::new();
objects.insert(config_object_path);
for (i, location) in context.object_files.iter().enumerate() {
match location {
FileData::Memory(data) => {
let out_path = libpython_dir.join(format!("libpython.{}.o", i));
fs::write(&out_path, data)?;
objects.insert(out_path);
}
FileData::Path(p) => {
objects.insert(p.clone());
}
}
}
for framework in &context.frameworks {
linking_annotations.push(LinkingAnnotation::LinkFramework(framework.to_string()));
}
for lib in &context.system_libraries {
linking_annotations.push(LinkingAnnotation::LinkLibrary(lib.to_string()));
}
for lib in &context.dynamic_libraries {
linking_annotations.push(LinkingAnnotation::LinkLibrary(lib.to_string()));
}
for lib in &context.static_libraries {
linking_annotations.push(LinkingAnnotation::LinkLibraryStatic(lib.to_string()));
}
if target_triple.ends_with("-apple-darwin") {
if let Some(path) = macos_clang_search_path()? {
linking_annotations.push(LinkingAnnotation::Search(path));
}
linking_annotations.push(LinkingAnnotation::LinkLibrary("clang_rt.osx".to_string()));
}
warn!("linking customized Python library...");
let objects = objects.into_iter().collect::<Vec<_>>();
let libpython_data = if target_triple.contains("-linux-") {
assemble_archive_gnu(&objects, &libpython_dir)?
} else if target_triple.contains("-apple-") {
assemble_archive_bsd(&objects, &libpython_dir)?
} else {
let mut build = cc::Build::new();
build.out_dir(&libpython_dir);
build.host(host_triple);
build.target(target_triple);
build.opt_level_str(opt_level);
build.cargo_metadata(false);
for object in objects {
build.object(object);
}
build.compile("python");
std::fs::read(libpython_dir.join(if windows { "python.lib" } else { "libpython.a" }))
.context("reading libpython")?
};
warn!("{} byte Python library created", libpython_data.len());
for path in &context.library_search_paths {
linking_annotations.push(LinkingAnnotation::SearchNative(path.clone()));
}
temp_dir.close().context("closing temporary directory")?;
Ok(LibpythonInfo {
libpython_data,
linking_annotations,
})
}
fn macos_clang_search_path() -> Result<Option<PathBuf>> {
let output = std::process::Command::new("clang")
.arg("--print-search-dirs")
.output()?;
if !output.status.success() {
return Ok(None);
}
for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains("libraries: =") {
let path = line
.split('=')
.nth(1)
.ok_or_else(|| anyhow!("could not parse libraries line"))?;
return Ok(Some(PathBuf::from(path).join("lib").join("darwin")));
}
}
Ok(None)
}