1use {
8 super::{
9 binary::{LibpythonLinkMode, PythonBinaryBuilder},
10 config::{default_memory_allocator, PyembedPythonInterpreterConfig},
11 distribution::{
12 resolve_python_distribution_from_location, AppleSdkInfo, BinaryLibpythonLinkMode,
13 DistributionExtractLock, PythonDistribution, PythonDistributionLocation,
14 },
15 distutils::prepare_hacked_distutils,
16 standalone_builder::StandalonePythonExecutableBuilder,
17 },
18 crate::environment::{Environment, LINUX_TARGET_TRIPLES, MACOS_TARGET_TRIPLES},
19 anyhow::{anyhow, Context, Result},
20 duct::cmd,
21 log::{info, warn},
22 once_cell::sync::Lazy,
23 path_dedot::ParseDot,
24 python_packaging::{
25 bytecode::{BytecodeCompiler, PythonBytecodeCompiler},
26 filesystem_scanning::{find_python_resources, walk_tree_files},
27 interpreter::{PythonInterpreterConfig, PythonInterpreterProfile, TerminfoResolution},
28 licensing::{ComponentFlavor, LicenseFlavor, LicensedComponent},
29 location::ConcreteResourceLocation,
30 module_util::{is_package_from_path, PythonModuleSuffixes},
31 policy::PythonPackagingPolicy,
32 resource::{
33 LibraryDependency, PythonExtensionModule, PythonExtensionModuleVariants,
34 PythonModuleSource, PythonPackageResource, PythonResource,
35 },
36 },
37 serde::Deserialize,
38 simple_file_manifest::{FileData, FileEntry},
39 std::{
40 collections::{hash_map::RandomState, BTreeMap, HashMap},
41 io::{BufRead, BufReader, Read},
42 path::{Path, PathBuf},
43 sync::Arc,
44 },
45};
46
47const PYOXIDIZER_STATE_DIR: &str = "state/pyoxidizer";
49
50#[cfg(windows)]
51const PYTHON_EXE_BASENAME: &str = "python.exe";
52
53#[cfg(unix)]
54const PYTHON_EXE_BASENAME: &str = "python3";
55
56#[cfg(windows)]
57const PIP_EXE_BASENAME: &str = "pip3.exe";
58
59#[cfg(unix)]
60const PIP_EXE_BASENAME: &str = "pip3";
61
62pub static BROKEN_EXTENSIONS_LINUX: Lazy<Vec<String>> = Lazy::new(|| {
66 vec![
67 "_crypt".to_string(),
69 "nis".to_string(),
71 ]
72});
73
74pub static BROKEN_EXTENSIONS_MACOS: Lazy<Vec<String>> = Lazy::new(|| {
78 vec![
79 "curses".to_string(),
81 "_curses_panel".to_string(),
82 "readline".to_string(),
83 ]
84});
85
86pub static NO_BYTECODE_MODULES: Lazy<Vec<&'static str>> = Lazy::new(|| {
90 vec![
91 "lib2to3.tests.data.bom",
92 "lib2to3.tests.data.crlf",
93 "lib2to3.tests.data.different_encoding",
94 "lib2to3.tests.data.false_encoding",
95 "lib2to3.tests.data.py2_test_grammar",
96 "lib2to3.tests.data.py3_test_grammar",
97 "test.bad_coding",
98 "test.badsyntax_3131",
99 "test.badsyntax_future3",
100 "test.badsyntax_future4",
101 "test.badsyntax_future5",
102 "test.badsyntax_future6",
103 "test.badsyntax_future7",
104 "test.badsyntax_future8",
105 "test.badsyntax_future9",
106 "test.badsyntax_future10",
107 "test.badsyntax_pep3120",
108 ]
109});
110
111#[derive(Debug, Deserialize)]
112struct LinkEntry {
113 name: String,
114 path_static: Option<String>,
115 path_dynamic: Option<String>,
116 framework: Option<bool>,
117 system: Option<bool>,
118}
119
120impl LinkEntry {
121 fn to_library_dependency(&self, python_path: &Path) -> LibraryDependency {
123 LibraryDependency {
124 name: self.name.clone(),
125 static_library: self
126 .path_static
127 .clone()
128 .map(|p| FileData::Path(python_path.join(p))),
129 static_filename: self
130 .path_static
131 .as_ref()
132 .map(|f| PathBuf::from(PathBuf::from(f).file_name().unwrap())),
133 dynamic_library: self
134 .path_dynamic
135 .clone()
136 .map(|p| FileData::Path(python_path.join(p))),
137 dynamic_filename: self
138 .path_dynamic
139 .as_ref()
140 .map(|f| PathBuf::from(PathBuf::from(f).file_name().unwrap())),
141 framework: self.framework.unwrap_or(false),
142 system: self.system.unwrap_or(false),
143 }
144 }
145}
146
147#[allow(unused)]
148#[derive(Debug, Deserialize)]
149struct PythonBuildExtensionInfo {
150 in_core: bool,
151 init_fn: String,
152 licenses: Option<Vec<String>>,
153 license_paths: Option<Vec<String>>,
154 license_public_domain: Option<bool>,
155 links: Vec<LinkEntry>,
156 objs: Vec<String>,
157 required: bool,
158 static_lib: Option<String>,
159 shared_lib: Option<String>,
160 variant: String,
161}
162
163#[allow(unused)]
164#[derive(Debug, Deserialize)]
165struct PythonBuildCoreInfo {
166 objs: Vec<String>,
167 links: Vec<LinkEntry>,
168 shared_lib: Option<String>,
169 static_lib: Option<String>,
170}
171
172#[allow(unused)]
173#[derive(Debug, Deserialize)]
174struct PythonBuildInfo {
175 core: PythonBuildCoreInfo,
176 extensions: BTreeMap<String, Vec<PythonBuildExtensionInfo>>,
177 inittab_object: String,
178 inittab_source: String,
179 inittab_cflags: Vec<String>,
180 object_file_format: String,
181}
182
183#[allow(unused)]
184#[derive(Debug, Deserialize)]
185struct PythonJsonMain {
186 version: String,
187 target_triple: String,
188 optimizations: String,
189 python_tag: String,
190 python_abi_tag: Option<String>,
191 python_config_vars: HashMap<String, String>,
192 python_platform_tag: String,
193 python_implementation_cache_tag: String,
194 python_implementation_hex_version: u64,
195 python_implementation_name: String,
196 python_implementation_version: Vec<String>,
197 python_version: String,
198 python_major_minor_version: String,
199 python_paths: HashMap<String, String>,
200 python_paths_abstract: HashMap<String, String>,
201 python_exe: String,
202 python_stdlib_test_packages: Vec<String>,
203 python_suffixes: HashMap<String, Vec<String>>,
204 python_bytecode_magic_number: String,
205 python_symbol_visibility: String,
206 python_extension_module_loading: Vec<String>,
207 apple_sdk_canonical_name: Option<String>,
208 apple_sdk_platform: Option<String>,
209 apple_sdk_version: Option<String>,
210 apple_sdk_deployment_target: Option<String>,
211 libpython_link_mode: String,
212 crt_features: Vec<String>,
213 run_tests: String,
214 build_info: PythonBuildInfo,
215 licenses: Option<Vec<String>>,
216 license_path: Option<String>,
217 tcl_library_path: Option<String>,
218 tcl_library_paths: Option<Vec<String>>,
219}
220
221fn parse_python_json(path: &Path) -> Result<PythonJsonMain> {
222 if !path.exists() {
223 return Err(anyhow!("PYTHON.json does not exist; are you using an up-to-date Python distribution that conforms with our requirements?"));
224 }
225
226 let buf = std::fs::read(path)?;
227
228 let value: serde_json::Value = serde_json::from_slice(&buf)?;
229 let o = value
230 .as_object()
231 .ok_or_else(|| anyhow!("PYTHON.json does not parse to an object"))?;
232
233 match o.get("version") {
234 Some(version) => {
235 let version = version
236 .as_str()
237 .ok_or_else(|| anyhow!("unable to parse version as a string"))?;
238
239 if version != "7" {
240 return Err(anyhow!(
241 "expected version 7 standalone distribution; found version {}",
242 version
243 ));
244 }
245 }
246 None => return Err(anyhow!("version key not present in PYTHON.json")),
247 }
248
249 let v: PythonJsonMain = serde_json::from_slice(&buf)?;
250
251 Ok(v)
252}
253
254fn parse_python_json_from_distribution(dist_dir: &Path) -> Result<PythonJsonMain> {
255 let python_json_path = dist_dir.join("python").join("PYTHON.json");
256 parse_python_json(&python_json_path)
257}
258
259fn parse_python_major_minor_version(version: &str) -> String {
260 let mut at_least_minor_version = String::from(version);
261 if !version.contains('.') {
262 at_least_minor_version.push_str(".0");
263 }
264 at_least_minor_version
265 .split('.')
266 .take(2)
267 .collect::<Vec<_>>()
268 .join(".")
269}
270
271pub fn python_exe_path(dist_dir: &Path) -> Result<PathBuf> {
273 let pi = parse_python_json_from_distribution(dist_dir)?;
274
275 Ok(dist_dir.join("python").join(&pi.python_exe))
276}
277
278#[derive(Debug)]
279pub struct PythonPaths {
280 pub prefix: PathBuf,
281 pub bin_dir: PathBuf,
282 pub python_exe: PathBuf,
283 pub stdlib: PathBuf,
284 pub site_packages: PathBuf,
285 pub pyoxidizer_state_dir: PathBuf,
286}
287
288pub fn resolve_python_paths(base: &Path, python_version: &str) -> PythonPaths {
290 let prefix = base.to_path_buf();
291
292 let p = prefix.clone();
293
294 let windows_layout = p.join("Scripts").exists();
295
296 let bin_dir = if windows_layout {
297 p.join("Scripts")
298 } else {
299 p.join("bin")
300 };
301
302 let python_exe = if bin_dir.join(PYTHON_EXE_BASENAME).exists() {
303 bin_dir.join(PYTHON_EXE_BASENAME)
304 } else {
305 p.join(PYTHON_EXE_BASENAME)
306 };
307
308 let mut pyoxidizer_state_dir = p.clone();
309 pyoxidizer_state_dir.extend(PYOXIDIZER_STATE_DIR.split('/'));
310
311 let unix_lib_dir = p.join("lib").join(format!(
312 "python{}",
313 parse_python_major_minor_version(python_version)
314 ));
315
316 let stdlib = if unix_lib_dir.exists() {
317 unix_lib_dir
318 } else if windows_layout {
319 p.join("Lib")
320 } else {
321 unix_lib_dir
322 };
323
324 let site_packages = stdlib.join("site-packages");
325
326 PythonPaths {
327 prefix,
328 bin_dir,
329 python_exe,
330 stdlib,
331 site_packages,
332 pyoxidizer_state_dir,
333 }
334}
335
336pub fn invoke_python(python_paths: &PythonPaths, args: &[&str]) {
337 let site_packages_s = python_paths.site_packages.display().to_string();
338
339 if site_packages_s.starts_with("\\\\?\\") {
340 panic!("Unexpected Windows UNC path in site-packages path");
341 }
342
343 info!("setting PYTHONPATH {}", site_packages_s);
344
345 let mut envs: HashMap<String, String, RandomState> = std::env::vars().collect();
346 envs.insert("PYTHONPATH".to_string(), site_packages_s);
347
348 info!(
349 "running {} {}",
350 python_paths.python_exe.display(),
351 args.join(" ")
352 );
353
354 let command = cmd(&python_paths.python_exe, args)
355 .full_env(&envs)
356 .stderr_to_stdout()
357 .reader()
358 .unwrap_or_else(|_| {
359 panic!(
360 "failed to run {} {}",
361 python_paths.python_exe.display(),
362 args.join(" ")
363 )
364 });
365 {
366 let reader = BufReader::new(&command);
367 for line in reader.lines() {
368 match line {
369 Ok(line) => {
370 warn!("{}", line);
371 }
372 Err(err) => {
373 warn!("Error when reading output: {:?}", err);
374 }
375 }
376 }
377 }
378}
379
380#[derive(Clone, Debug, PartialEq, Eq)]
382pub enum StandaloneDistributionLinkMode {
383 Static,
384 Dynamic,
385}
386
387#[allow(unused)]
393#[derive(Clone, Debug)]
394pub struct StandaloneDistribution {
395 pub base_dir: PathBuf,
397
398 pub target_triple: String,
400
401 pub python_implementation: String,
403
404 pub python_tag: String,
406
407 pub python_abi_tag: Option<String>,
409
410 pub python_platform_tag: String,
412
413 pub version: String,
415
416 pub python_exe: PathBuf,
418
419 pub stdlib_path: PathBuf,
421
422 stdlib_test_packages: Vec<String>,
424
425 link_mode: StandaloneDistributionLinkMode,
427
428 pub python_symbol_visibility: String,
430
431 extension_module_loading: Vec<String>,
433
434 apple_sdk_info: Option<AppleSdkInfo>,
436
437 pub core_license: Option<LicensedComponent>,
439
440 pub licenses: Option<Vec<String>>,
446
447 pub license_path: Option<PathBuf>,
449
450 tcl_library_path: Option<PathBuf>,
452
453 tcl_library_paths: Option<Vec<String>>,
455
456 pub objs_core: BTreeMap<PathBuf, PathBuf>,
460
461 pub links_core: Vec<LibraryDependency>,
463
464 pub libpython_shared_library: Option<PathBuf>,
468
469 pub extension_modules: BTreeMap<String, PythonExtensionModuleVariants>,
471
472 pub frozen_c: Vec<u8>,
473
474 pub includes: BTreeMap<String, PathBuf>,
478
479 pub libraries: BTreeMap<String, PathBuf>,
484
485 pub py_modules: BTreeMap<String, PathBuf>,
486
487 pub resources: BTreeMap<String, BTreeMap<String, PathBuf>>,
492
493 pub venv_base: PathBuf,
495
496 pub inittab_object: PathBuf,
498
499 pub inittab_cflags: Vec<String>,
501
502 pub cache_tag: String,
506
507 module_suffixes: PythonModuleSuffixes,
509
510 pub crt_features: Vec<String>,
512
513 config_vars: HashMap<String, String>,
515}
516
517impl StandaloneDistribution {
518 pub fn from_location(
519 location: &PythonDistributionLocation,
520 distributions_dir: &Path,
521 ) -> Result<Self> {
522 let (archive_path, extract_path) =
523 resolve_python_distribution_from_location(location, distributions_dir)?;
524
525 Self::from_tar_zst_file(&archive_path, &extract_path)
526 }
527
528 pub fn from_tar_zst_file(path: &Path, extract_dir: &Path) -> Result<Self> {
532 let basename = path
533 .file_name()
534 .ok_or_else(|| anyhow!("unable to determine filename"))?
535 .to_string_lossy();
536
537 if !basename.ends_with(".tar.zst") {
538 return Err(anyhow!("unhandled distribution format: {}", path.display()));
539 }
540
541 let fh = std::fs::File::open(path)
542 .with_context(|| format!("unable to open {}", path.display()))?;
543
544 let reader = BufReader::new(fh);
545
546 Self::from_tar_zst(reader, extract_dir).context("reading tar.zst distribution data")
547 }
548
549 pub fn from_tar_zst<R: Read>(source: R, extract_dir: &Path) -> Result<Self> {
551 let dctx = zstd::stream::Decoder::new(source)?;
552
553 Self::from_tar(dctx, extract_dir).context("reading tar distribution data")
554 }
555
556 #[allow(clippy::unnecessary_unwrap)]
558 pub fn from_tar<R: Read>(source: R, extract_dir: &Path) -> Result<Self> {
559 let mut tf = tar::Archive::new(source);
560
561 {
562 let _lock = DistributionExtractLock::new(extract_dir)?;
563
564 let test_path = extract_dir.join("python").join("PYTHON.json");
567 if !test_path.exists() {
568 std::fs::create_dir_all(extract_dir)?;
569 let absolute_path = std::fs::canonicalize(extract_dir)?;
570
571 let mut symlinks = vec![];
572
573 for entry in tf.entries()? {
574 let mut entry =
575 entry.map_err(|e| anyhow!("failed to iterate over archive: {}", e))?;
576
577 entry.set_preserve_mtime(false);
584
585 let link_name = entry.link_name().unwrap_or(None);
589
590 if link_name.is_some() && cfg!(target_family = "windows") {
591 let mut dest = absolute_path.clone();
601 dest.extend(entry.path()?.components());
602 let dest = dest
603 .parse_dot()
604 .with_context(|| "dedotting symlinked source")?
605 .to_path_buf();
606
607 let mut source = dest
608 .parent()
609 .ok_or_else(|| anyhow!("unable to resolve parent"))?
610 .to_path_buf();
611 source.extend(link_name.unwrap().components());
612 let source = source
613 .parse_dot()
614 .with_context(|| "dedotting symlink destination")?
615 .to_path_buf();
616
617 if !source.starts_with(&absolute_path) {
618 return Err(anyhow!("malicious symlink detected in archive"));
619 }
620
621 symlinks.push((source, dest));
622 } else {
623 entry
624 .unpack_in(&absolute_path)
625 .with_context(|| "unable to extract tar member")?;
626 }
627 }
628
629 for (source, dest) in symlinks {
630 std::fs::copy(&source, &dest).with_context(|| {
631 format!(
632 "copying symlinked file {} -> {}",
633 source.display(),
634 dest.display(),
635 )
636 })?;
637 }
638
639 let walk = walkdir::WalkDir::new(&absolute_path);
644 for entry in walk.into_iter() {
645 let entry = entry?;
646
647 let metadata = entry.metadata()?;
648 let mut permissions = metadata.permissions();
649
650 if permissions.readonly() {
651 permissions.set_readonly(false);
652 std::fs::set_permissions(entry.path(), permissions).with_context(|| {
653 format!("unable to mark {} as writable", entry.path().display())
654 })?;
655 }
656 }
657 }
658 }
659
660 Self::from_directory(extract_dir)
661 }
662
663 #[allow(clippy::cognitive_complexity)]
665 pub fn from_directory(dist_dir: &Path) -> Result<Self> {
666 let mut objs_core: BTreeMap<PathBuf, PathBuf> = BTreeMap::new();
667 let mut links_core: Vec<LibraryDependency> = Vec::new();
668 let mut extension_modules: BTreeMap<String, PythonExtensionModuleVariants> =
669 BTreeMap::new();
670 let mut includes: BTreeMap<String, PathBuf> = BTreeMap::new();
671 let mut libraries = BTreeMap::new();
672 let frozen_c: Vec<u8> = Vec::new();
673 let mut py_modules: BTreeMap<String, PathBuf> = BTreeMap::new();
674 let mut resources: BTreeMap<String, BTreeMap<String, PathBuf>> = BTreeMap::new();
675
676 for entry in std::fs::read_dir(dist_dir)? {
677 let entry = entry?;
678
679 match entry.file_name().to_str() {
680 Some("python") => continue,
681 Some(value) => {
682 return Err(anyhow!(
683 "unexpected entry in distribution root directory: {}",
684 value
685 ))
686 }
687 _ => {
688 return Err(anyhow!(
689 "error listing root directory of Python distribution"
690 ))
691 }
692 };
693 }
694
695 let python_path = dist_dir.join("python");
696
697 for entry in std::fs::read_dir(&python_path)? {
698 let entry = entry?;
699
700 match entry.file_name().to_str() {
701 Some("build") => continue,
702 Some("install") => continue,
703 Some("lib") => continue,
704 Some("licenses") => continue,
705 Some("LICENSE.rst") => continue,
706 Some("PYTHON.json") => continue,
707 Some(value) => {
708 return Err(anyhow!("unexpected entry in python/ directory: {}", value))
709 }
710 _ => return Err(anyhow!("error listing python/ directory")),
711 };
712 }
713
714 let pi = parse_python_json_from_distribution(dist_dir)?;
715
716 let core_license = if let Some(ref python_license_path) = pi.license_path {
718 let license_path = python_path.join(python_license_path);
719 let license_text = std::fs::read_to_string(&license_path).with_context(|| {
720 format!("unable to read Python license {}", license_path.display())
721 })?;
722
723 let expression = pi.licenses.clone().unwrap().join(" OR ");
724
725 let mut component = LicensedComponent::new_spdx(
726 ComponentFlavor::PythonDistribution(pi.python_implementation_name.clone()),
727 &expression,
728 )?;
729 component.add_license_text(license_text);
730
731 Some(component)
732 } else {
733 None
734 };
735
736 for obj in &pi.build_info.core.objs {
738 let rel_path = PathBuf::from(obj);
739 let full_path = python_path.join(obj);
740
741 objs_core.insert(rel_path, full_path);
742 }
743
744 for entry in &pi.build_info.core.links {
745 let depends = entry.to_library_dependency(&python_path);
746
747 if let Some(p) = &depends.static_library {
748 if let Some(p) = p.backing_path() {
749 libraries.insert(depends.name.clone(), p.to_path_buf());
750 }
751 }
752
753 links_core.push(depends);
754 }
755
756 let module_suffixes = PythonModuleSuffixes {
757 source: pi
758 .python_suffixes
759 .get("source")
760 .ok_or_else(|| anyhow!("distribution does not define source suffixes"))?
761 .clone(),
762 bytecode: pi
763 .python_suffixes
764 .get("bytecode")
765 .ok_or_else(|| anyhow!("distribution does not define bytecode suffixes"))?
766 .clone(),
767 debug_bytecode: pi
768 .python_suffixes
769 .get("debug_bytecode")
770 .ok_or_else(|| anyhow!("distribution does not define debug bytecode suffixes"))?
771 .clone(),
772 optimized_bytecode: pi
773 .python_suffixes
774 .get("optimized_bytecode")
775 .ok_or_else(|| anyhow!("distribution does not define optimized bytecode suffixes"))?
776 .clone(),
777 extension: pi
778 .python_suffixes
779 .get("extension")
780 .ok_or_else(|| anyhow!("distribution does not define extension suffixes"))?
781 .clone(),
782 };
783
784 for (module, variants) in &pi.build_info.extensions {
786 let mut ems = PythonExtensionModuleVariants::default();
787
788 for entry in variants.iter() {
789 let extension_file_suffix = if let Some(p) = &entry.shared_lib {
790 if let Some(idx) = p.rfind('.') {
791 p[idx..].to_string()
792 } else {
793 "".to_string()
794 }
795 } else {
796 "".to_string()
797 };
798
799 let object_file_data = entry
800 .objs
801 .iter()
802 .map(|p| FileData::Path(python_path.join(p)))
803 .collect();
804 let mut links = Vec::new();
805
806 for link in &entry.links {
807 let depends = link.to_library_dependency(&python_path);
808
809 if let Some(p) = &depends.static_library {
810 if let Some(p) = p.backing_path() {
811 libraries.insert(depends.name.clone(), p.to_path_buf());
812 }
813 }
814
815 links.push(depends);
816 }
817
818 let component_flavor =
819 ComponentFlavor::PythonStandardLibraryExtensionModule(module.clone());
820
821 let mut license = if entry.license_public_domain.unwrap_or(false) {
822 LicensedComponent::new(component_flavor, LicenseFlavor::PublicDomain)
823 } else if let Some(licenses) = &entry.licenses {
824 let expression = licenses.join(" OR ");
825 LicensedComponent::new_spdx(component_flavor, &expression)?
826 } else if let Some(core) = &core_license {
827 LicensedComponent::new_spdx(
828 component_flavor,
829 core.spdx_expression()
830 .ok_or_else(|| anyhow!("could not resolve SPDX license for core"))?
831 .as_ref(),
832 )?
833 } else {
834 LicensedComponent::new(component_flavor, LicenseFlavor::None)
835 };
836
837 if let Some(license_paths) = &entry.license_paths {
838 for path in license_paths {
839 let path = python_path.join(path);
840 let text = std::fs::read_to_string(&path)
841 .with_context(|| format!("reading {}", path.display()))?;
842
843 license.add_license_text(text);
844 }
845 }
846
847 ems.push(PythonExtensionModule {
848 name: module.clone(),
849 init_fn: Some(entry.init_fn.clone()),
850 extension_file_suffix,
851 shared_library: entry
852 .shared_lib
853 .as_ref()
854 .map(|path| FileData::Path(python_path.join(path))),
855 object_file_data,
856 is_package: false,
857 link_libraries: links,
858 is_stdlib: true,
859 builtin_default: entry.in_core,
860 required: entry.required,
861 variant: Some(entry.variant.clone()),
862 license: Some(license),
863 });
864 }
865
866 extension_modules.insert(module.clone(), ems);
867 }
868
869 let include_path = if let Some(p) = pi.python_paths.get("include") {
870 python_path.join(p)
871 } else {
872 return Err(anyhow!("include path not defined in distribution"));
873 };
874
875 for entry in walk_tree_files(&include_path) {
876 let full_path = entry.path();
877 let rel_path = full_path
878 .strip_prefix(&include_path)
879 .expect("unable to strip prefix");
880 includes.insert(
881 String::from(rel_path.to_str().expect("path to string")),
882 full_path.to_path_buf(),
883 );
884 }
885
886 let stdlib_path = if let Some(p) = pi.python_paths.get("stdlib") {
887 python_path.join(p)
888 } else {
889 return Err(anyhow!("stdlib path not defined in distribution"));
890 };
891
892 for entry in find_python_resources(
893 &stdlib_path,
894 &pi.python_implementation_cache_tag,
895 &module_suffixes,
896 false,
897 true,
898 )? {
899 match entry? {
900 PythonResource::PackageResource(resource) => {
901 if !resources.contains_key(&resource.leaf_package) {
902 resources.insert(resource.leaf_package.clone(), BTreeMap::new());
903 }
904
905 resources.get_mut(&resource.leaf_package).unwrap().insert(
906 resource.relative_name.clone(),
907 match &resource.data {
908 FileData::Path(path) => path.to_path_buf(),
909 FileData::Memory(_) => {
910 return Err(anyhow!(
911 "should not have received in-memory resource data"
912 ))
913 }
914 },
915 );
916 }
917 PythonResource::ModuleSource(source) => match &source.source {
918 FileData::Path(path) => {
919 py_modules.insert(source.name.clone(), path.to_path_buf());
920 }
921 FileData::Memory(_) => {
922 return Err(anyhow!("should not have received in-memory source data"))
923 }
924 },
925
926 PythonResource::ModuleBytecodeRequest(_) => {}
927 PythonResource::ModuleBytecode(_) => {}
928 PythonResource::PackageDistributionResource(_) => {}
929 PythonResource::ExtensionModule(_) => {}
930 PythonResource::EggFile(_) => {}
931 PythonResource::PathExtension(_) => {}
932 PythonResource::File(_) => {}
933 };
934 }
935
936 let venv_base = dist_dir.parent().unwrap().join("hacked_base");
937
938 let (link_mode, libpython_shared_library) = if pi.libpython_link_mode == "static" {
939 (StandaloneDistributionLinkMode::Static, None)
940 } else if pi.libpython_link_mode == "shared" {
941 (
942 StandaloneDistributionLinkMode::Dynamic,
943 Some(python_path.join(pi.build_info.core.shared_lib.unwrap())),
944 )
945 } else {
946 return Err(anyhow!("unhandled link mode: {}", pi.libpython_link_mode));
947 };
948
949 let apple_sdk_info = if let Some(canonical_name) = pi.apple_sdk_canonical_name {
950 let platform = pi
951 .apple_sdk_platform
952 .ok_or_else(|| anyhow!("apple_sdk_platform not defined"))?;
953 let version = pi
954 .apple_sdk_version
955 .ok_or_else(|| anyhow!("apple_sdk_version not defined"))?;
956 let deployment_target = pi
957 .apple_sdk_deployment_target
958 .ok_or_else(|| anyhow!("apple_sdk_deployment_target not defined"))?;
959
960 Some(AppleSdkInfo {
961 canonical_name,
962 platform,
963 version,
964 deployment_target,
965 })
966 } else {
967 None
968 };
969
970 let inittab_object = python_path.join(pi.build_info.inittab_object);
971
972 Ok(Self {
973 base_dir: dist_dir.to_path_buf(),
974 target_triple: pi.target_triple,
975 python_implementation: pi.python_implementation_name,
976 python_tag: pi.python_tag,
977 python_abi_tag: pi.python_abi_tag,
978 python_platform_tag: pi.python_platform_tag,
979 version: pi.python_version.clone(),
980 python_exe: python_exe_path(dist_dir)?,
981 stdlib_path,
982 stdlib_test_packages: pi.python_stdlib_test_packages,
983 link_mode,
984 python_symbol_visibility: pi.python_symbol_visibility,
985 extension_module_loading: pi.python_extension_module_loading,
986 apple_sdk_info,
987 core_license,
988 licenses: pi.licenses.clone(),
989 license_path: pi.license_path.as_ref().map(PathBuf::from),
990 tcl_library_path: pi
991 .tcl_library_path
992 .as_ref()
993 .map(|path| dist_dir.join("python").join(path)),
994 tcl_library_paths: pi.tcl_library_paths.clone(),
995 extension_modules,
996 frozen_c,
997 includes,
998 links_core,
999 libraries,
1000 objs_core,
1001 libpython_shared_library,
1002 py_modules,
1003 resources,
1004 venv_base,
1005 inittab_object,
1006 inittab_cflags: pi.build_info.inittab_cflags,
1007 cache_tag: pi.python_implementation_cache_tag,
1008 module_suffixes,
1009 crt_features: pi.crt_features,
1010 config_vars: pi.python_config_vars,
1011 })
1012 }
1013
1014 pub fn libpython_link_support(&self) -> (bool, bool) {
1019 if self.target_triple.contains("pc-windows") {
1020 (
1030 self.libpython_shared_library.is_none(),
1031 self.libpython_shared_library.is_some(),
1032 )
1033 } else if self.target_triple.contains("linux-musl") {
1034 (true, false)
1036 } else {
1037 (true, true)
1039 }
1040 }
1041
1042 pub fn is_extension_module_file_loadable(&self) -> bool {
1044 self.extension_module_loading
1045 .contains(&"shared-library".to_string())
1046 }
1047}
1048
1049impl PythonDistribution for StandaloneDistribution {
1050 fn clone_trait(&self) -> Arc<dyn PythonDistribution> {
1051 Arc::new(self.clone())
1052 }
1053
1054 fn target_triple(&self) -> &str {
1055 &self.target_triple
1056 }
1057
1058 fn compatible_host_triples(&self) -> Vec<String> {
1059 let mut res = vec![self.target_triple.clone()];
1060
1061 res.extend(
1062 match self.target_triple() {
1063 "aarch64-unknown-linux-gnu" => vec![],
1064 "aarch64-unknown-linux-musl" => vec!["aarch64-unknown-linux-gnu"],
1066 "x86_64-unknown-linux-gnu" => vec![],
1067 "x86_64-unknown-linux-musl" => vec!["x86_64-unknown-linux-gnu"],
1069 "aarch64-apple-darwin" => vec![],
1070 "x86_64-apple-darwin" => vec![],
1071 "i686-pc-windows-gnu" => vec![
1073 "i686-pc-windows-msvc",
1074 "x86_64-pc-windows-gnu",
1075 "x86_64-pc-windows-msvc",
1076 ],
1077 "i686-pc-windows-msvc" => vec![
1079 "i686-pc-windows-gnu",
1080 "x86_64-pc-windows-gnu",
1081 "x86_64-pc-windows-msvc",
1082 ],
1083 "x86_64-pc-windows-gnu" => vec!["x86_64-pc-windows-msvc"],
1085 "x86_64-pc-windows-msvc" => vec!["x86_64-pc-windows-gnu"],
1086 _ => vec![],
1087 }
1088 .iter()
1089 .map(|x| x.to_string()),
1090 );
1091
1092 res
1093 }
1094
1095 fn python_exe_path(&self) -> &Path {
1096 &self.python_exe
1097 }
1098
1099 fn python_version(&self) -> &str {
1100 &self.version
1101 }
1102
1103 fn python_major_minor_version(&self) -> String {
1104 parse_python_major_minor_version(&self.version)
1105 }
1106
1107 fn python_implementation(&self) -> &str {
1108 &self.python_implementation
1109 }
1110
1111 fn python_implementation_short(&self) -> &str {
1112 match self.python_implementation.as_str() {
1114 "cpython" => "cp",
1115 "python" => "py",
1116 "pypy" => "pp",
1117 "ironpython" => "ip",
1118 "jython" => "jy",
1119 s => panic!("unsupported Python implementation: {}", s),
1120 }
1121 }
1122
1123 fn python_tag(&self) -> &str {
1124 &self.python_tag
1125 }
1126
1127 fn python_abi_tag(&self) -> Option<&str> {
1128 match &self.python_abi_tag {
1129 Some(tag) => {
1130 if tag.is_empty() {
1131 None
1132 } else {
1133 Some(tag)
1134 }
1135 }
1136 None => None,
1137 }
1138 }
1139
1140 fn python_platform_tag(&self) -> &str {
1141 &self.python_platform_tag
1142 }
1143
1144 fn python_platform_compatibility_tag(&self) -> &str {
1145 if !self.is_extension_module_file_loadable() {
1147 return "none";
1148 }
1149
1150 match self.python_platform_tag.as_str() {
1151 "linux-aarch64" => "manylinux2014_aarch64",
1152 "linux-x86_64" => "manylinux2014_x86_64",
1153 "linux-i686" => "manylinux2014_i686",
1154 "macosx-10.9-x86_64" => "macosx_10_9_x86_64",
1155 "macosx-11.0-arm64" => "macosx_11_0_arm64",
1156 "win-amd64" => "win_amd64",
1157 "win32" => "win32",
1158 p => panic!("unsupported Python platform: {}", p),
1159 }
1160 }
1161
1162 fn cache_tag(&self) -> &str {
1163 &self.cache_tag
1164 }
1165
1166 fn python_module_suffixes(&self) -> Result<PythonModuleSuffixes> {
1167 Ok(self.module_suffixes.clone())
1168 }
1169
1170 fn python_config_vars(&self) -> &HashMap<String, String> {
1171 &self.config_vars
1172 }
1173
1174 fn stdlib_test_packages(&self) -> Vec<String> {
1175 self.stdlib_test_packages.clone()
1176 }
1177
1178 fn apple_sdk_info(&self) -> Option<&AppleSdkInfo> {
1179 self.apple_sdk_info.as_ref()
1180 }
1181
1182 fn create_bytecode_compiler(
1183 &self,
1184 env: &Environment,
1185 ) -> Result<Box<dyn PythonBytecodeCompiler>> {
1186 let temp_dir = env.temporary_directory("pyoxidizer-bytecode-compiler")?;
1187
1188 Ok(Box::new(BytecodeCompiler::new(
1189 &self.python_exe,
1190 temp_dir.path(),
1191 )?))
1192 }
1193
1194 fn create_packaging_policy(&self) -> Result<PythonPackagingPolicy> {
1195 let mut policy = PythonPackagingPolicy::default();
1196
1197 if self.supports_in_memory_shared_library_loading() {
1200 policy.set_resources_location(ConcreteResourceLocation::InMemory);
1201 policy.set_resources_location_fallback(Some(ConcreteResourceLocation::RelativePath(
1202 "lib".to_string(),
1203 )));
1204 }
1205
1206 for triple in LINUX_TARGET_TRIPLES.iter() {
1207 for ext in BROKEN_EXTENSIONS_LINUX.iter() {
1208 policy.register_broken_extension(triple, ext);
1209 }
1210 }
1211
1212 for triple in MACOS_TARGET_TRIPLES.iter() {
1213 for ext in BROKEN_EXTENSIONS_MACOS.iter() {
1214 policy.register_broken_extension(triple, ext);
1215 }
1216 }
1217
1218 for name in NO_BYTECODE_MODULES.iter() {
1219 policy.register_no_bytecode_module(name);
1220 }
1221
1222 Ok(policy)
1223 }
1224
1225 fn create_python_interpreter_config(&self) -> Result<PyembedPythonInterpreterConfig> {
1226 let embedded_default = PyembedPythonInterpreterConfig::default();
1227
1228 Ok(PyembedPythonInterpreterConfig {
1229 config: PythonInterpreterConfig {
1230 profile: PythonInterpreterProfile::Isolated,
1231 ..embedded_default.config
1232 },
1233 allocator_backend: default_memory_allocator(self.target_triple()),
1234 allocator_raw: true,
1235 oxidized_importer: true,
1236 filesystem_importer: false,
1237 terminfo_resolution: TerminfoResolution::Dynamic,
1238 ..embedded_default
1239 })
1240 }
1241
1242 fn as_python_executable_builder(
1243 &self,
1244 host_triple: &str,
1245 target_triple: &str,
1246 name: &str,
1247 libpython_link_mode: BinaryLibpythonLinkMode,
1248 policy: &PythonPackagingPolicy,
1249 config: &PyembedPythonInterpreterConfig,
1250 host_distribution: Option<Arc<dyn PythonDistribution>>,
1251 ) -> Result<Box<dyn PythonBinaryBuilder>> {
1252 let target_distribution = Arc::new(self.clone());
1254 let host_distribution: Arc<dyn PythonDistribution> =
1255 host_distribution.unwrap_or_else(|| Arc::new(self.clone()));
1256
1257 let builder = StandalonePythonExecutableBuilder::from_distribution(
1258 host_distribution,
1259 target_distribution,
1260 host_triple.to_string(),
1261 target_triple.to_string(),
1262 name.to_string(),
1263 libpython_link_mode,
1264 policy.clone(),
1265 config.clone(),
1266 )?;
1267
1268 Ok(builder as Box<dyn PythonBinaryBuilder>)
1269 }
1270
1271 fn python_resources<'a>(&self) -> Vec<PythonResource<'a>> {
1272 let extension_modules = self
1273 .extension_modules
1274 .iter()
1275 .flat_map(|(_, exts)| exts.iter().map(|e| PythonResource::from(e.to_owned())));
1276
1277 let module_sources = self.py_modules.iter().map(|(name, path)| {
1278 PythonResource::from(PythonModuleSource {
1279 name: name.clone(),
1280 source: FileData::Path(path.clone()),
1281 is_package: is_package_from_path(path),
1282 cache_tag: self.cache_tag.clone(),
1283 is_stdlib: true,
1284 is_test: self.is_stdlib_test_package(name),
1285 })
1286 });
1287
1288 let resource_datas = self.resources.iter().flat_map(|(package, inner)| {
1289 inner.iter().map(move |(name, path)| {
1290 PythonResource::from(PythonPackageResource {
1291 leaf_package: package.clone(),
1292 relative_name: name.clone(),
1293 data: FileData::Path(path.clone()),
1294 is_stdlib: true,
1295 is_test: self.is_stdlib_test_package(package),
1296 })
1297 })
1298 });
1299
1300 extension_modules
1301 .chain(module_sources)
1302 .chain(resource_datas)
1303 .collect::<Vec<PythonResource<'a>>>()
1304 }
1305
1306 fn ensure_pip(&self) -> Result<PathBuf> {
1308 let dist_prefix = self.base_dir.join("python").join("install");
1309 let python_paths = resolve_python_paths(&dist_prefix, &self.version);
1310
1311 let pip_path = python_paths.bin_dir.join(PIP_EXE_BASENAME);
1312
1313 if !pip_path.exists() {
1314 warn!("{} doesnt exist", pip_path.display().to_string());
1315 invoke_python(&python_paths, &["-m", "ensurepip"]);
1316 }
1317
1318 Ok(pip_path)
1319 }
1320
1321 fn resolve_distutils(
1322 &self,
1323 libpython_link_mode: LibpythonLinkMode,
1324 dest_dir: &Path,
1325 extra_python_paths: &[&Path],
1326 ) -> Result<HashMap<String, String>> {
1327 let mut res = match libpython_link_mode {
1328 LibpythonLinkMode::Static => prepare_hacked_distutils(
1330 &self.stdlib_path.join("distutils"),
1331 dest_dir,
1332 extra_python_paths,
1333 ),
1334 LibpythonLinkMode::Dynamic => Ok(HashMap::new()),
1335 }?;
1336
1337 res.insert("SETUPTOOLS_USE_DISTUTILS".to_string(), "stdlib".to_string());
1352
1353 Ok(res)
1354 }
1355
1356 fn supports_in_memory_shared_library_loading(&self) -> bool {
1358 self.target_triple.contains("pc-windows")
1362 && self.python_symbol_visibility == "dllexport"
1363 && self
1364 .extension_module_loading
1365 .contains(&"shared-library".to_string())
1366 }
1367
1368 fn tcl_files(&self) -> Result<Vec<(PathBuf, FileEntry)>> {
1369 let mut res = vec![];
1370
1371 if let Some(root) = &self.tcl_library_path {
1372 if let Some(paths) = &self.tcl_library_paths {
1373 for subdir in paths {
1374 for entry in walkdir::WalkDir::new(root.join(subdir))
1375 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
1376 .into_iter()
1377 {
1378 let entry = entry?;
1379
1380 let path = entry.path();
1381
1382 if path.is_dir() {
1383 continue;
1384 }
1385
1386 let rel_path = path.strip_prefix(root)?;
1387
1388 res.push((rel_path.to_path_buf(), FileEntry::try_from(path)?));
1389 }
1390 }
1391 }
1392 }
1393
1394 Ok(res)
1395 }
1396
1397 fn tcl_library_path_directory(&self) -> Option<String> {
1398 Some("tcl8.6".to_string())
1400 }
1401}
1402
1403#[cfg(test)]
1404pub mod tests {
1405 use {
1406 super::*,
1407 crate::testutil::*,
1408 python_packaging::{
1409 bytecode::CompileMode, policy::ExtensionModuleFilter,
1410 resource::BytecodeOptimizationLevel,
1411 },
1412 std::collections::BTreeSet,
1413 };
1414
1415 #[test]
1416 fn test_stdlib_annotations() -> Result<()> {
1417 let distribution = get_default_distribution(None)?;
1418
1419 for resource in distribution.python_resources() {
1420 match resource {
1421 PythonResource::ModuleSource(module) => {
1422 assert!(module.is_stdlib);
1423
1424 if module.name.starts_with("test") {
1425 assert!(module.is_test);
1426 }
1427 }
1428 PythonResource::PackageResource(r) => {
1429 assert!(r.is_stdlib);
1430 if r.leaf_package.starts_with("test") {
1431 assert!(r.is_test);
1432 }
1433 }
1434 _ => (),
1435 }
1436 }
1437
1438 Ok(())
1439 }
1440
1441 #[test]
1442 fn test_tcl_files() -> Result<()> {
1443 for dist in get_all_standalone_distributions()? {
1444 let tcl_files = dist.tcl_files()?;
1445
1446 if dist.target_triple().contains("pc-windows")
1447 && !dist.is_extension_module_file_loadable()
1448 {
1449 assert!(tcl_files.is_empty());
1450 } else {
1451 assert!(!tcl_files.is_empty());
1452 }
1453 }
1454
1455 Ok(())
1456 }
1457
1458 #[test]
1459 fn test_extension_module_copyleft_filtering() -> Result<()> {
1460 for dist in get_all_standalone_distributions()? {
1461 let mut policy = dist.create_packaging_policy()?;
1462 policy.set_extension_module_filter(ExtensionModuleFilter::All);
1463
1464 let all_extensions = policy
1465 .resolve_python_extension_modules(
1466 dist.extension_modules.values(),
1467 &dist.target_triple,
1468 )?
1469 .into_iter()
1470 .map(|e| (e.name, e.variant))
1471 .collect::<BTreeSet<_>>();
1472
1473 policy.set_extension_module_filter(ExtensionModuleFilter::NoCopyleft);
1474
1475 let no_copyleft_extensions = policy
1476 .resolve_python_extension_modules(
1477 dist.extension_modules.values(),
1478 &dist.target_triple,
1479 )?
1480 .into_iter()
1481 .map(|e| (e.name, e.variant))
1482 .collect::<BTreeSet<_>>();
1483
1484 let dropped = all_extensions
1485 .difference(&no_copyleft_extensions)
1486 .cloned()
1487 .collect::<Vec<_>>();
1488
1489 let added = no_copyleft_extensions
1490 .difference(&all_extensions)
1491 .cloned()
1492 .collect::<Vec<_>>();
1493
1494 let (linux_dropped, linux_added) =
1496 if ["3.8", "3.9"].contains(&dist.python_major_minor_version().as_str()) {
1497 (
1498 vec![
1499 ("_gdbm".to_string(), Some("default".to_string())),
1500 ("readline".to_string(), Some("default".to_string())),
1501 ],
1502 vec![("readline".to_string(), Some("libedit".to_string()))],
1503 )
1504 } else {
1505 (vec![], vec![])
1506 };
1507
1508 let (wanted_dropped, wanted_added) = match (
1509 dist.python_major_minor_version().as_str(),
1510 dist.target_triple(),
1511 ) {
1512 (_, "aarch64-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1513 (_, "x86_64-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1514 (_, "x86_64_v2-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1515 (_, "x86_64_v3-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1516 (_, "x86_64-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1517 (_, "x86_64_v2-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1518 (_, "x86_64_v3-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1519 (_, "i686-pc-windows-msvc") => (vec![], vec![]),
1520 (_, "x86_64-pc-windows-msvc") => (vec![], vec![]),
1521 (_, "aarch64-apple-darwin") => (vec![], vec![]),
1522 (_, "x86_64-apple-darwin") => (vec![], vec![]),
1523 _ => (vec![], vec![]),
1524 };
1525
1526 assert_eq!(
1527 dropped,
1528 wanted_dropped,
1529 "dropped matches for {} {}",
1530 dist.python_major_minor_version(),
1531 dist.target_triple(),
1532 );
1533 assert_eq!(
1534 added,
1535 wanted_added,
1536 "added matches for {} {}",
1537 dist.python_major_minor_version(),
1538 dist.target_triple()
1539 );
1540 }
1541
1542 Ok(())
1543 }
1544
1545 #[test]
1546 fn compile_syntax_error() -> Result<()> {
1547 let env = get_env()?;
1548 let dist = get_default_distribution(None)?;
1549
1550 let temp_dir = env.temporary_directory("pyoxidizer-test")?;
1551
1552 let mut compiler = BytecodeCompiler::new(dist.python_exe_path(), temp_dir.path())?;
1553 let res = compiler.compile(
1554 b"invalid syntax",
1555 "foo.py",
1556 BytecodeOptimizationLevel::Zero,
1557 CompileMode::Bytecode,
1558 );
1559 assert!(res.is_err());
1560 let err = res.err().unwrap();
1561 assert!(err
1562 .to_string()
1563 .starts_with("compiling error: invalid syntax"));
1564
1565 temp_dir.close()?;
1566
1567 Ok(())
1568 }
1569
1570 #[test]
1571 fn apple_sdk_info() -> Result<()> {
1572 for dist in get_all_standalone_distributions()? {
1573 if dist.target_triple().contains("-apple-") {
1574 assert!(dist.apple_sdk_info().is_some());
1575 } else {
1576 assert!(dist.apple_sdk_info().is_none());
1577 }
1578 }
1579
1580 Ok(())
1581 }
1582
1583 #[test]
1584 fn test_parse_python_major_minor_version() {
1585 let version_expectations = [
1586 ("3.7.1", "3.7"),
1587 ("3.10.1", "3.10"),
1588 ("1.2.3.4.5", "1.2"),
1589 ("1", "1.0"),
1590 ];
1591 for (version, expected) in version_expectations {
1592 assert_eq!(parse_python_major_minor_version(version), expected);
1593 }
1594 }
1595
1596 #[test]
1597 fn test_resolve_python_paths_site_packages() -> Result<()> {
1598 let python_paths = resolve_python_paths(Path::new("/test/dir"), "3.10.4");
1599 assert_eq!(
1600 python_paths
1601 .site_packages
1602 .to_str()
1603 .unwrap()
1604 .replace('\\', "/"),
1605 "/test/dir/lib/python3.10/site-packages"
1606 );
1607 let python_paths = resolve_python_paths(Path::new("/test/dir"), "3.9.1");
1608 assert_eq!(
1609 python_paths
1610 .site_packages
1611 .to_str()
1612 .unwrap()
1613 .replace('\\', "/"),
1614 "/test/dir/lib/python3.9/site-packages"
1615 );
1616 Ok(())
1617 }
1618}