pyoxidizerlib/py_packaging/
libpython.rs1use {
10 crate::{
11 environment::Environment,
12 py_packaging::{distribution::AppleSdkInfo, embedding::LinkingAnnotation},
13 },
14 anyhow::{anyhow, Context, Result},
15 apple_sdk::AppleSdk,
16 duct::cmd,
17 log::warn,
18 python_packaging::libpython::LibPythonBuildContext,
19 simple_file_manifest::FileData,
20 std::{
21 collections::BTreeSet,
22 ffi::OsStr,
23 fs,
24 fs::create_dir_all,
25 hash::Hasher,
26 io::{BufRead, BufReader, Cursor},
27 path::{Path, PathBuf},
28 },
29};
30
31#[cfg(target_family = "unix")]
32use std::os::unix::ffi::OsStrExt;
33
34#[cfg(unix)]
35fn osstr_to_bytes(s: &OsStr) -> Result<Vec<u8>> {
36 Ok(s.as_bytes().to_vec())
37}
38
39#[cfg(not(unix))]
40fn osstr_to_bytes(s: &OsStr) -> Result<Vec<u8>> {
41 let utf8: &str = s
42 .to_str()
43 .ok_or_else(|| anyhow!("invalid UTF-8 filename"))?;
44 Ok(utf8.as_bytes().to_vec())
45}
46
47pub fn make_config_c<T>(extensions: &[(T, T)]) -> String
49where
50 T: AsRef<str>,
51{
52 let mut lines: Vec<String> = vec!["#include \"Python.h\"".to_string()];
55
56 for (_name, init_fn) in extensions {
58 if init_fn.as_ref() != "NULL" {
59 lines.push(format!("extern PyObject* {}(void);", init_fn.as_ref()));
60 }
61 }
62
63 lines.push(String::from("struct _inittab _PyImport_Inittab[] = {"));
64
65 for (name, init_fn) in extensions {
66 lines.push(format!("{{\"{}\", {}}},", name.as_ref(), init_fn.as_ref()));
67 }
68
69 lines.push(String::from("{0, 0}"));
70 lines.push(String::from("};"));
71
72 lines.join("\n")
73}
74
75fn create_ar_symbols_index(dest_dir: &Path, lib_data: &[u8]) -> Result<Vec<u8>> {
77 let lib_path = dest_dir.join("lib.a");
78
79 std::fs::write(&lib_path, lib_data).context("writing archive to temporary file")?;
80
81 warn!("invoking `ar s` to index archive symbols");
82 let command = cmd("ar", &["s".to_string(), lib_path.display().to_string()])
83 .stderr_to_stdout()
84 .unchecked()
85 .reader()?;
86 {
87 let reader = BufReader::new(&command);
88 for line in reader.lines() {
89 warn!("{}", line?);
90 }
91 }
92 let output = command
93 .try_wait()?
94 .ok_or_else(|| anyhow!("unable to wait on ar"))?;
95
96 if !output.status.success() {
97 return Err(anyhow!("failed to invoke `ar s`"));
98 }
99
100 Ok(std::fs::read(&lib_path)?)
101}
102
103fn ar_header(path: &Path) -> Result<ar::Header> {
104 let filename = path
105 .file_name()
106 .ok_or_else(|| anyhow!("could not determine file name"))?;
107
108 let identifier = osstr_to_bytes(filename)?;
109
110 let metadata = std::fs::metadata(path)?;
111
112 let mut header = ar::Header::from_metadata(identifier, &metadata);
113
114 header.set_uid(0);
115 header.set_gid(0);
116 header.set_mtime(0);
117 header.set_mode(0o644);
118
119 Ok(header)
120}
121
122fn assemble_archive_gnu(objects: &[PathBuf], temp_dir: &Path) -> Result<Vec<u8>> {
123 let buffer = Cursor::new(vec![]);
124
125 let identifiers = objects
126 .iter()
127 .map(|p| {
128 Ok(p.file_name()
129 .ok_or_else(|| anyhow!("object file name could not be determined"))?
130 .to_string_lossy()
131 .as_bytes()
132 .to_vec())
133 })
134 .collect::<Result<Vec<_>>>()?;
135
136 let mut builder = ar::GnuBuilder::new(buffer, identifiers);
137
138 for path in objects {
139 let header = ar_header(path)
140 .with_context(|| format!("resolving ar header for {}", path.display()))?;
141 let fh = std::fs::File::open(path)?;
142
143 builder.append(&header, fh)?;
144 }
145
146 let data = builder.into_inner()?.into_inner();
147
148 create_ar_symbols_index(temp_dir, &data)
149}
150
151fn assemble_archive_bsd(objects: &[PathBuf], temp_dir: &Path) -> Result<Vec<u8>> {
152 let buffer = Cursor::new(vec![]);
153
154 let mut builder = ar::Builder::new(buffer);
155
156 for path in objects {
157 let header = ar_header(path)
158 .with_context(|| format!("resolving ar header for {}", path.display()))?;
159 let fh = std::fs::File::open(path)?;
160
161 builder.append(&header, fh)?;
162 }
163
164 let data = builder.into_inner()?.into_inner();
165
166 create_ar_symbols_index(temp_dir, &data)
167}
168
169#[derive(Debug)]
171pub struct LibpythonInfo {
172 pub libpython_data: Vec<u8>,
174
175 pub linking_annotations: Vec<LinkingAnnotation>,
177}
178
179#[allow(clippy::too_many_arguments)]
183pub fn link_libpython(
184 env: &Environment,
185 context: &LibPythonBuildContext,
186 host_triple: &str,
187 target_triple: &str,
188 opt_level: &str,
189 apple_sdk_info: Option<&AppleSdkInfo>,
190) -> Result<LibpythonInfo> {
191 let temp_dir = env.temporary_directory("pyoxidizer-libpython")?;
192
193 let config_c_dir = temp_dir.path().join("config_c");
194 std::fs::create_dir(&config_c_dir).context("creating config_c subdirectory")?;
195
196 let libpython_dir = temp_dir.path().join("libpython");
197 std::fs::create_dir(&libpython_dir).context("creating libpython subdirectory")?;
198
199 let mut linking_annotations = vec![];
200
201 let windows = crate::environment::WINDOWS_TARGET_TRIPLES.contains(&target_triple);
202
203 warn!(
208 "deriving custom config.c from {} extension modules",
209 context.init_functions.len()
210 );
211 let config_c_source = make_config_c(&context.init_functions.iter().collect::<Vec<_>>());
212 let config_c_path = config_c_dir.join("config.c");
213
214 let config_object_path = if config_c_path.has_root() {
216 let dirname = config_c_path
217 .parent()
218 .ok_or_else(|| anyhow!("could not determine parent directory"))?;
219 let mut hasher = std::collections::hash_map::DefaultHasher::new();
220 hasher.write(dirname.to_string_lossy().as_bytes());
221
222 config_c_dir.join(format!("{:016x}-{}", hasher.finish(), "config.o"))
223 } else {
224 config_c_dir.join("config.o")
225 };
226
227 fs::write(&config_c_path, config_c_source.as_bytes())?;
228
229 for (rel_path, location) in &context.includes {
231 let full = config_c_dir.join(rel_path);
232 create_dir_all(
233 full.parent()
234 .ok_or_else(|| anyhow!("unable to resolve parent directory"))?,
235 )?;
236 let data = location.resolve_content()?;
237 std::fs::write(&full, &data)?;
238 }
239
240 warn!("compiling custom config.c to object file");
241 let mut build = cc::Build::new();
242
243 if let Some(flags) = &context.inittab_cflags {
244 for flag in flags {
245 build.flag(flag);
246 }
247 }
248
249 if target_triple.contains("-apple-") {
254 let sdk_info = apple_sdk_info.ok_or_else(|| {
255 anyhow!("Apple SDK info should be defined when targeting Apple platforms")
256 })?;
257
258 let sdk = env
259 .resolve_apple_sdk(sdk_info)
260 .context("resolving Apple SDK to use")?;
261
262 build.flag("-isysroot");
263 build.flag(&format!("{}", sdk.path().display()));
264 }
265
266 build
267 .out_dir(&config_c_dir)
268 .host(host_triple)
269 .target(target_triple)
270 .opt_level_str(opt_level)
271 .file(&config_c_path)
272 .include(&config_c_dir)
273 .cargo_metadata(false)
274 .compile("irrelevant");
275
276 warn!("resolving inputs for custom Python library...");
277
278 let mut objects = BTreeSet::new();
279
280 objects.insert(config_object_path);
282
283 for (i, location) in context.object_files.iter().enumerate() {
284 match location {
285 FileData::Memory(data) => {
286 let out_path = libpython_dir.join(format!("libpython.{}.o", i));
287 fs::write(&out_path, data)?;
288 objects.insert(out_path);
289 }
290 FileData::Path(p) => {
291 objects.insert(p.clone());
292 }
293 }
294 }
295
296 for framework in &context.frameworks {
297 linking_annotations.push(LinkingAnnotation::LinkFramework(framework.to_string()));
298 }
299
300 for lib in &context.system_libraries {
301 linking_annotations.push(LinkingAnnotation::LinkLibrary(lib.to_string()));
302 }
303
304 for lib in &context.dynamic_libraries {
305 linking_annotations.push(LinkingAnnotation::LinkLibrary(lib.to_string()));
306 }
307
308 for lib in &context.static_libraries {
309 linking_annotations.push(LinkingAnnotation::LinkLibraryStatic(lib.to_string()));
310 }
311
312 if target_triple.ends_with("-apple-darwin") {
319 if let Some(path) = macos_clang_search_path()? {
320 linking_annotations.push(LinkingAnnotation::Search(path));
321 }
322
323 linking_annotations.push(LinkingAnnotation::LinkLibrary("clang_rt.osx".to_string()));
324 }
325
326 warn!("linking customized Python library...");
327
328 let objects = objects.into_iter().collect::<Vec<_>>();
329
330 let libpython_data = if target_triple.contains("-linux-") {
331 assemble_archive_gnu(&objects, &libpython_dir)?
332 } else if target_triple.contains("-apple-") {
333 assemble_archive_bsd(&objects, &libpython_dir)?
334 } else {
335 let mut build = cc::Build::new();
336 build.out_dir(&libpython_dir);
337 build.host(host_triple);
338 build.target(target_triple);
339 build.opt_level_str(opt_level);
340 build.cargo_metadata(false);
342
343 for object in objects {
344 build.object(object);
345 }
346
347 build.compile("python");
348
349 std::fs::read(libpython_dir.join(if windows { "python.lib" } else { "libpython.a" }))
350 .context("reading libpython")?
351 };
352
353 warn!("{} byte Python library created", libpython_data.len());
354
355 for path in &context.library_search_paths {
356 linking_annotations.push(LinkingAnnotation::SearchNative(path.clone()));
357 }
358
359 temp_dir.close().context("closing temporary directory")?;
360
361 Ok(LibpythonInfo {
362 libpython_data,
363 linking_annotations,
364 })
365}
366
367fn macos_clang_search_path() -> Result<Option<PathBuf>> {
369 let output = std::process::Command::new("clang")
370 .arg("--print-search-dirs")
371 .output()?;
372 if !output.status.success() {
373 return Ok(None);
374 }
375
376 for line in String::from_utf8_lossy(&output.stdout).lines() {
377 if line.contains("libraries: =") {
378 let path = line
379 .split('=')
380 .nth(1)
381 .ok_or_else(|| anyhow!("could not parse libraries line"))?;
382 return Ok(Some(PathBuf::from(path).join("lib").join("darwin")));
383 }
384 }
385
386 Ok(None)
387}