pyoxidizerlib/py_packaging/
embedding.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/*! Functionality for embedding Python in a binary. */
6
7use {
8    crate::py_packaging::config::PyembedPythonInterpreterConfig,
9    anyhow::{anyhow, Context, Result},
10    pyo3_build_config::{
11        BuildFlags, InterpreterConfig as PyO3InterpreterConfig, PythonImplementation, PythonVersion,
12    },
13    python_packaging::{
14        licensing::{LicensedComponent, LicensedComponents},
15        resource_collection::CompiledResourcesCollection,
16    },
17    simple_file_manifest::{FileEntry, FileManifest},
18    std::path::{Path, PathBuf},
19};
20
21/// Describes extra behavior for a linker invocation.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum LinkingAnnotation {
24    /// Link an Apple framework library of the given name.
25    LinkFramework(String),
26
27    /// Link a library of the given name.
28    LinkLibrary(String),
29
30    /// Link a static library of the given name.
31    LinkLibraryStatic(String),
32
33    /// A search path for libraries.
34    Search(PathBuf),
35
36    /// A search path for native libraries.
37    SearchNative(PathBuf),
38
39    /// An extra argument to the linker.
40    Argument(String),
41}
42
43impl LinkingAnnotation {
44    /// Convert the instance to a `cargo:*` string representing this annotation.
45    pub fn to_cargo_annotation(&self) -> String {
46        match self {
47            Self::LinkFramework(framework) => {
48                format!("cargo:rustc-link-lib=framework={}", framework)
49            }
50            Self::LinkLibrary(lib) => format!("cargo:rustc-link-lib={}", lib),
51            Self::LinkLibraryStatic(lib) => format!("cargo:rustc-link-lib=static={}", lib),
52            Self::Search(path) => format!("cargo:rustc-link-search={}", path.display()),
53            Self::SearchNative(path) => {
54                format!("cargo:rustc-link-search=native={}", path.display())
55            }
56            Self::Argument(arg) => {
57                format!("cargo:rustc-link-arg={}", arg)
58            }
59        }
60    }
61}
62
63/// Resolver linking annotations for a given target triple.
64pub fn linking_annotations_for_target(target_triple: &str) -> Vec<LinkingAnnotation> {
65    // By default Rust will not export dynamic symbols from built executables. Python
66    // symbols need to be exported so external Python extension modules (which are
67    // shared libraries) can resolve them. This requires passing extra linker arguments
68    // to export the symbols.
69
70    // TODO we may not need to do this when dynamically linking libpython. But, we
71    // may need to do this because our binary provides extension modules whose symbols
72    // may need to be visible by the Python interpreter? We implemented this as
73    // unconditional to preserve backwards compatible behavior. But we should investigate
74    // whether this is really needed. If we revisit this, we also need to consider the
75    // emission of these flags by the pyembed crate's build script, which may override
76    // any behavior we set here.
77
78    if target_triple.contains("-linux-") {
79        vec![LinkingAnnotation::Argument("-Wl,-export-dynamic".into())]
80    } else if target_triple.contains("-apple-darwin") {
81        vec![LinkingAnnotation::Argument("-rdynamic".into())]
82    } else {
83        vec![]
84    }
85}
86
87/// Represents a linkable target defining a Python implementation.
88pub trait LinkablePython {
89    /// Write any files that need to exist to support linking.
90    ///
91    /// Files will be written to the directory specified.
92    fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()>;
93
94    /// Obtain linker annotations needed to link this libpython.
95    ///
96    /// `dest_dir` will be the directory where any files written by `write_files()` will
97    /// be located.
98    ///
99    /// `alias` denotes whether to alias the library name to `pythonXY`.
100    fn linking_annotations(
101        &self,
102        dest_dir: &Path,
103        alias: bool,
104        target_triple: &str,
105    ) -> Result<Vec<LinkingAnnotation>>;
106}
107
108/// Link against a shared library on the filesystem.
109#[derive(Clone, Debug)]
110pub struct LinkSharedLibraryPath {
111    /// Path to dynamic library to link.
112    pub library_path: PathBuf,
113
114    /// Additional linking annotations.
115    pub linking_annotations: Vec<LinkingAnnotation>,
116}
117
118impl LinkSharedLibraryPath {
119    /// Resolve the name of the library.
120    fn library_name(&self) -> Result<String> {
121        let filename = self
122            .library_path
123            .file_name()
124            .ok_or_else(|| anyhow!("unable to resolve shared library file name"))?
125            .to_string_lossy();
126
127        if filename.ends_with(".dll") {
128            Ok(filename.trim_end_matches(".dll").to_string())
129        } else if filename.ends_with(".dylib") {
130            Ok(filename
131                .trim_end_matches(".dylib")
132                .trim_start_matches("lib")
133                .to_string())
134        } else if filename.ends_with(".so") {
135            Ok(filename
136                .trim_end_matches(".so")
137                .trim_start_matches("lib")
138                .to_string())
139        } else {
140            Err(anyhow!(
141                "unhandled libpython shared library filename: {}",
142                filename
143            ))
144        }
145    }
146}
147
148impl LinkablePython for LinkSharedLibraryPath {
149    fn write_files(&self, _dest_dir: &Path, _target_triple: &str) -> Result<()> {
150        Ok(())
151    }
152
153    fn linking_annotations(
154        &self,
155        _dest_dir: &Path,
156        alias: bool,
157        target_triple: &str,
158    ) -> Result<Vec<LinkingAnnotation>> {
159        let lib_dir = self
160            .library_path
161            .parent()
162            .ok_or_else(|| anyhow!("could not derive parent directory of library path"))?;
163
164        let mut annotations = vec![
165            LinkingAnnotation::LinkLibrary(if alias {
166                format!("pythonXY:{}", self.library_name()?)
167            } else {
168                self.library_name()?
169            }),
170            LinkingAnnotation::SearchNative(lib_dir.to_path_buf()),
171        ];
172
173        annotations.extend(self.linking_annotations.iter().cloned());
174        annotations.extend(linking_annotations_for_target(target_triple));
175
176        Ok(annotations)
177    }
178}
179
180/// Link against a custom built static library with tracked library data.
181#[derive(Clone, Debug)]
182pub struct LinkStaticLibraryData {
183    /// libpython static library content.
184    pub library_data: Vec<u8>,
185
186    /// Additional linker directives to link this static library.
187    pub linking_annotations: Vec<LinkingAnnotation>,
188}
189
190impl LinkStaticLibraryData {
191    fn library_name(&self) -> &'static str {
192        "python3"
193    }
194
195    fn library_path(&self, dest_dir: impl AsRef<Path>, target_triple: &str) -> PathBuf {
196        dest_dir
197            .as_ref()
198            .join(if target_triple.contains("-windows-") {
199                format!("{}.lib", self.library_name())
200            } else {
201                format!("lib{}.a", self.library_name())
202            })
203    }
204}
205
206impl LinkablePython for LinkStaticLibraryData {
207    fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()> {
208        let lib_path = self.library_path(dest_dir, target_triple);
209
210        std::fs::write(&lib_path, &self.library_data)
211            .with_context(|| format!("writing {}", lib_path.display()))?;
212
213        Ok(())
214    }
215
216    fn linking_annotations(
217        &self,
218        dest_dir: &Path,
219        alias: bool,
220        target_triple: &str,
221    ) -> Result<Vec<LinkingAnnotation>> {
222        let mut annotations = vec![
223            LinkingAnnotation::LinkLibraryStatic(if alias {
224                format!("pythonXY:{}", self.library_name())
225            } else {
226                self.library_name().to_string()
227            }),
228            LinkingAnnotation::SearchNative(dest_dir.to_path_buf()),
229        ];
230
231        annotations.extend(self.linking_annotations.iter().cloned());
232        annotations.extend(linking_annotations_for_target(target_triple));
233
234        Ok(annotations)
235    }
236}
237
238/// Describes how to link a `libpython`.
239pub enum LibpythonLinkSettings {
240    /// Link against an existing shared library.
241    ExistingDynamic(LinkSharedLibraryPath),
242    /// Link against a custom static library.
243    StaticData(LinkStaticLibraryData),
244}
245
246impl LinkablePython for LibpythonLinkSettings {
247    fn write_files(&self, dest_dir: &Path, target_triple: &str) -> Result<()> {
248        match self {
249            Self::ExistingDynamic(l) => l.write_files(dest_dir, target_triple),
250            Self::StaticData(l) => l.write_files(dest_dir, target_triple),
251        }
252    }
253
254    fn linking_annotations(
255        &self,
256        dest_dir: &Path,
257        alias: bool,
258        target_triple: &str,
259    ) -> Result<Vec<LinkingAnnotation>> {
260        match self {
261            Self::ExistingDynamic(l) => l.linking_annotations(dest_dir, alias, target_triple),
262            Self::StaticData(l) => l.linking_annotations(dest_dir, alias, target_triple),
263        }
264    }
265}
266
267impl From<LinkSharedLibraryPath> for LibpythonLinkSettings {
268    fn from(l: LinkSharedLibraryPath) -> Self {
269        Self::ExistingDynamic(l)
270    }
271}
272
273impl From<LinkStaticLibraryData> for LibpythonLinkSettings {
274    fn from(l: LinkStaticLibraryData) -> Self {
275        Self::StaticData(l)
276    }
277}
278
279/// Filename of artifact containing the default PythonInterpreterConfig.
280pub const DEFAULT_PYTHON_CONFIG_FILENAME: &str = "default_python_config.rs";
281
282/// Holds context necessary to embed Python in a binary.
283pub struct EmbeddedPythonContext<'a> {
284    /// The configuration for the embedded interpreter.
285    pub config: PyembedPythonInterpreterConfig,
286
287    /// Information on how to link against Python.
288    pub link_settings: LibpythonLinkSettings,
289
290    /// Python resources that need to be serialized to a file.
291    pub pending_resources: Vec<(CompiledResourcesCollection<'a>, PathBuf)>,
292
293    /// Extra files to install next to produced binary.
294    pub extra_files: FileManifest,
295
296    /// Rust target triple for the host we are running on.
297    pub host_triple: String,
298
299    /// Rust target triple for the target we are building for.
300    pub target_triple: String,
301
302    /// Name of the Python implementation.
303    pub python_implementation: PythonImplementation,
304
305    /// Python interpreter version.
306    pub python_version: PythonVersion,
307
308    /// Path to a `python` executable that runs on the host/build machine.
309    pub python_exe_host: PathBuf,
310
311    /// Python build flags.
312    ///
313    /// To pass to PyO3.
314    pub python_build_flags: BuildFlags,
315
316    /// Name of file to write licensing information to.
317    pub licensing_filename: Option<String>,
318
319    /// Licensing metadata for components to be built/embedded.
320    pub licensing: LicensedComponents,
321}
322
323impl<'a> EmbeddedPythonContext<'a> {
324    /// Obtain the filesystem of the generated Rust source file containing the interpreter configuration.
325    pub fn interpreter_config_rs_path(&self, dest_dir: impl AsRef<Path>) -> PathBuf {
326        dest_dir.as_ref().join(DEFAULT_PYTHON_CONFIG_FILENAME)
327    }
328
329    /// Resolve the filesystem path to the PyO3 configuration file.
330    pub fn pyo3_config_path(&self, dest_dir: impl AsRef<Path>) -> PathBuf {
331        dest_dir.as_ref().join("pyo3-build-config-file.txt")
332    }
333
334    /// Resolve a [PyO3InterpreterConfig] for this instance.
335    pub fn pyo3_interpreter_config(
336        &self,
337        dest_dir: impl AsRef<Path>,
338    ) -> Result<PyO3InterpreterConfig> {
339        Ok(PyO3InterpreterConfig {
340            implementation: self.python_implementation,
341            version: self.python_version,
342            // Irrelevant since we control link settings below.
343            shared: matches!(
344                &self.link_settings,
345                LibpythonLinkSettings::ExistingDynamic(_)
346            ),
347            // pyembed requires the full Python API.
348            abi3: false,
349            // We define linking info via explicit build script lines.
350            lib_name: None,
351            lib_dir: None,
352            executable: Some(self.python_exe_host.to_string_lossy().to_string()),
353            // TODO set from Python distribution metadata.
354            pointer_width: Some(if self.target_triple.starts_with("i686-") {
355                32
356            } else {
357                64
358            }),
359            build_flags: BuildFlags(self.python_build_flags.0.clone()),
360            suppress_build_script_link_lines: true,
361            extra_build_script_lines: self
362                .link_settings
363                .linking_annotations(
364                    dest_dir.as_ref(),
365                    self.target_triple.contains("-windows-"),
366                    &self.target_triple,
367                )?
368                .iter()
369                .map(|la| la.to_cargo_annotation())
370                .collect::<Vec<_>>(),
371        })
372    }
373
374    /// Ensure packed resources files are written.
375    pub fn write_packed_resources(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
376        for (collection, path) in &self.pending_resources {
377            let dest_path = dest_dir.as_ref().join(path);
378
379            let mut writer = std::io::BufWriter::new(
380                std::fs::File::create(&dest_path)
381                    .with_context(|| format!("opening {} for writing", dest_path.display()))?,
382            );
383            collection
384                .write_packed_resources(&mut writer)
385                .context("writing packed resources")?;
386        }
387
388        Ok(())
389    }
390
391    /// Ensure files required by libpython are written.
392    pub fn write_libpython(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
393        self.link_settings
394            .write_files(dest_dir.as_ref(), &self.target_triple)
395    }
396
397    /// Write the file containing the default interpreter configuration Rust struct.
398    pub fn write_interpreter_config_rs(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
399        self.config
400            .write_default_python_config_rs(self.interpreter_config_rs_path(&dest_dir))?;
401
402        Ok(())
403    }
404
405    /// Write the PyO3 configuration file.
406    pub fn write_pyo3_config(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
407        let dest_dir = dest_dir.as_ref();
408
409        let mut fh = std::fs::File::create(self.pyo3_config_path(dest_dir))?;
410        self.pyo3_interpreter_config(dest_dir)?
411            .to_writer(&mut fh)
412            .map_err(|e| anyhow!("error writing PyO3 config file: {}", e))?;
413
414        Ok(())
415    }
416
417    /// Write an aggregated licensing document, if enabled.
418    pub fn write_licensing(&self, dest_dir: impl AsRef<Path>) -> Result<()> {
419        if let Some(filename) = &self.licensing_filename {
420            let text = self.licensing.aggregate_license_document(false)?;
421
422            std::fs::write(dest_dir.as_ref().join(filename), text.as_bytes())?;
423        }
424
425        Ok(())
426    }
427
428    /// Write out files needed to build a binary against our configuration.
429    pub fn write_files(&self, dest_dir: &Path) -> Result<()> {
430        self.write_packed_resources(dest_dir)
431            .context("write_packed_resources()")?;
432        self.write_libpython(dest_dir)
433            .context("write_libpython()")?;
434        self.write_interpreter_config_rs(dest_dir)
435            .context("write_interpreter_config_rs()")?;
436        self.write_pyo3_config(dest_dir)
437            .context("write_pyo3_config()")?;
438        self.write_licensing(dest_dir)
439            .context("write_licensing()")?;
440
441        Ok(())
442    }
443
444    /// Obtain licensing information for this instance.
445    pub fn licensing(&self) -> &LicensedComponents {
446        &self.licensing
447    }
448
449    /// Add a licensed component to the collection.
450    pub fn add_licensed_component(&mut self, component: LicensedComponent) -> Result<()> {
451        self.licensing.add_component(component);
452
453        self.synchronize_licensing()?;
454
455        Ok(())
456    }
457
458    /// Ensuring licensing state between registered licenses and an output licensing file is in sync.
459    pub fn synchronize_licensing(&mut self) -> Result<()> {
460        // Write a unified licensing file if told to do so.
461        if let Some(filename) = &self.licensing_filename {
462            self.extra_files.add_file_entry(
463                filename,
464                FileEntry::new_from_data(
465                    self.licensing.aggregate_license_document(false)?.as_bytes(),
466                    false,
467                ),
468            )?;
469        }
470
471        Ok(())
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_dynamic_library_name() -> Result<()> {
481        assert_eq!(
482            LinkSharedLibraryPath {
483                library_path: "libpython3.9.so".into(),
484                linking_annotations: vec![],
485            }
486            .library_name()?,
487            "python3.9"
488        );
489
490        assert_eq!(
491            LinkSharedLibraryPath {
492                library_path: "libpython3.9.dylib".into(),
493                linking_annotations: vec![],
494            }
495            .library_name()?,
496            "python3.9"
497        );
498
499        assert_eq!(
500            LinkSharedLibraryPath {
501                library_path: "python3.dll".into(),
502                linking_annotations: vec![],
503            }
504            .library_name()?,
505            "python3"
506        );
507
508        Ok(())
509    }
510}