pyoxidizerlib/py_packaging/
libpython.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*!
6Build a custom libraries containing Python.
7*/
8
9use {
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
47/// Produce the content of the config.c file containing built-in extensions.
48pub fn make_config_c<T>(extensions: &[(T, T)]) -> String
49where
50    T: AsRef<str>,
51{
52    // It is easier to construct the file from scratch than parse the template
53    // and insert things in the right places.
54    let mut lines: Vec<String> = vec!["#include \"Python.h\"".to_string()];
55
56    // Declare the initialization functions.
57    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
75/// The `ar` crate doesn't support emitting the symbols index. So call out to `ar s` ourselves.
76fn 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/// Represents a built libpython.
170#[derive(Debug)]
171pub struct LibpythonInfo {
172    /// Raw data constituting static libpython library.
173    pub libpython_data: Vec<u8>,
174
175    /// Describes annotations necessary to link this libpython.
176    pub linking_annotations: Vec<LinkingAnnotation>,
177}
178
179/// Create a static libpython from a Python distribution.
180///
181/// Returns a struct describing the generated libpython.
182#[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    // We derive a custom Modules/config.c from the set of extension modules.
204    // We need to do this because config.c defines the built-in extensions and
205    // their initialization functions and the file generated by the source
206    // distribution may not align with what we want.
207    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    // The output file name is dependent on whether the input file name is absolute.
215    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    // Gather all includes into the temporary directory.
230    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    // The cc crate will pick up the default Apple SDK by default. There could be a mismatch
250    // between it and what we want. For example, if we're building for aarch64 but the default
251    // SDK is a 10.15 SDK that doesn't support ARM. We attempt to mitigate this by resolving
252    // a compatible Apple SDK and pointing the compiler invocation at it via compiler flags.
253    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    // Link our custom config.c's object file.
281    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    // Python 3.9+ on macOS uses __builtin_available(), which requires
313    // ___isOSVersionAtLeast(), which is part of libclang_rt. However,
314    // libclang_rt isn't linked by default by Rust. So unless something else
315    // pulls it in, we'll get unresolved symbol errors when attempting to link
316    // the final binary. Our solution to this is to always annotate
317    // `clang_rt.<platform>` as a library dependency of our static libpython.
318    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        // We handle this ourselves.
341        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
367/// Attempt to resolve the linker search path for clang libraries.
368fn 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}