1use {
8 crate::{
9 environment::{canonicalize_path, default_target_triple, Environment, PyOxidizerSource},
10 licensing::{licenses_from_cargo_manifest, log_licensing_info},
11 project_building::find_pyoxidizer_config_file_env,
12 project_layout::{initialize_project, write_new_pyoxidizer_config_file},
13 py_packaging::{
14 distribution::{
15 default_distribution_location, resolve_distribution,
16 resolve_python_distribution_archive, BinaryLibpythonLinkMode, DistributionCache,
17 DistributionFlavor, PythonDistribution,
18 },
19 standalone_distribution::StandaloneDistribution,
20 },
21 python_distributions::PYTHON_DISTRIBUTIONS,
22 starlark::eval::EvaluationContextBuilder,
23 },
24 anyhow::{anyhow, Context, Result},
25 python_packaging::licensing::LicenseFlavor,
26 python_packaging::{
27 filesystem_scanning::find_python_resources,
28 interpreter::{MemoryAllocatorBackend, PythonInterpreterProfile},
29 resource::PythonResource,
30 wheel::WheelArchive,
31 },
32 simple_file_manifest::{FileData, FileManifest},
33 std::{
34 collections::HashMap,
35 fs::create_dir_all,
36 io::{Cursor, Read},
37 path::{Path, PathBuf},
38 },
39};
40
41pub fn default_target() -> Result<String> {
43 if cfg!(target_os = "linux") {
45 if cfg!(target_arch = "aarch64") {
46 Ok("aarch64-unknown-linux-gnu".to_string())
47 } else {
48 Ok("x86_64-unknown-linux-gnu".to_string())
49 }
50 } else if cfg!(target_os = "windows") {
51 Ok("x86_64-pc-windows-msvc".to_string())
52 } else if cfg!(target_os = "macos") {
53 if cfg!(target_arch = "aarch64") {
54 Ok("aarch64-apple-darwin".to_string())
55 } else {
56 Ok("x86_64-apple-darwin".to_string())
57 }
58 } else {
59 Err(anyhow!("unable to resolve target"))
60 }
61}
62
63pub fn resolve_target(target: Option<&str>) -> Result<String> {
64 if let Some(s) = target {
65 Ok(s.to_string())
66 } else {
67 default_target()
68 }
69}
70
71pub fn list_targets(env: &Environment, project_path: &Path) -> Result<()> {
72 let config_path = find_pyoxidizer_config_file_env(project_path).ok_or_else(|| {
73 anyhow!(
74 "unable to find PyOxidizder config file at {}",
75 project_path.display()
76 )
77 })?;
78
79 let target_triple = default_target()?;
80
81 let mut context = EvaluationContextBuilder::new(env, config_path.clone(), target_triple)
82 .resolve_targets(vec![])
83 .into_context()?;
84
85 context.evaluate_file(&config_path)?;
86
87 if context.default_target()?.is_none() {
88 println!("(no targets defined)");
89 return Ok(());
90 }
91
92 for target in context.target_names()? {
93 let prefix = if Some(target.clone()) == context.default_target()? {
94 "*"
95 } else {
96 ""
97 };
98 println!("{}{}", prefix, target);
99 }
100
101 Ok(())
102}
103
104#[allow(clippy::too_many_arguments)]
109pub fn build(
110 env: &Environment,
111 project_path: &Path,
112 target_triple: Option<&str>,
113 resolve_targets: Option<Vec<String>>,
114 extra_vars: HashMap<String, Option<String>>,
115 release: bool,
116 verbose: bool,
117) -> Result<()> {
118 let config_path = find_pyoxidizer_config_file_env(project_path).ok_or_else(|| {
119 anyhow!(
120 "unable to find PyOxidizer config file at {}",
121 project_path.display()
122 )
123 })?;
124 let target_triple = resolve_target(target_triple)?;
125
126 let mut context = EvaluationContextBuilder::new(env, config_path.clone(), target_triple)
127 .extra_vars(extra_vars)
128 .release(release)
129 .verbose(verbose)
130 .resolve_targets_optional(resolve_targets)
131 .into_context()?;
132
133 context.evaluate_file(&config_path)?;
134
135 for target in context.targets_to_resolve()? {
136 context.build_resolved_target(&target)?;
137 }
138
139 Ok(())
140}
141
142#[allow(clippy::too_many_arguments)]
143pub fn run(
144 env: &Environment,
145 project_path: &Path,
146 target_triple: Option<&str>,
147 release: bool,
148 target: Option<&str>,
149 extra_vars: HashMap<String, Option<String>>,
150 _extra_args: &[&str],
151 verbose: bool,
152) -> Result<()> {
153 let config_path = find_pyoxidizer_config_file_env(project_path).ok_or_else(|| {
154 anyhow!(
155 "unable to find PyOxidizer config file at {}",
156 project_path.display()
157 )
158 })?;
159 let target_triple = resolve_target(target_triple)?;
160
161 let mut context = EvaluationContextBuilder::new(env, config_path.clone(), target_triple)
162 .extra_vars(extra_vars)
163 .release(release)
164 .verbose(verbose)
165 .resolve_target_optional(target)
166 .into_context()?;
167
168 context.evaluate_file(&config_path)?;
169
170 context.run_target(target)
171}
172
173pub fn cache_clear(env: &Environment) -> Result<()> {
174 let cache_dir = env.cache_dir();
175
176 println!("removing {}", cache_dir.display());
177 remove_dir_all::remove_dir_all(cache_dir)?;
178
179 Ok(())
180}
181
182pub fn find_resources(
184 env: &Environment,
185 path: Option<&Path>,
186 distributions_dir: Option<&Path>,
187 scan_distribution: bool,
188 target_triple: &str,
189 classify_files: bool,
190 emit_files: bool,
191) -> Result<()> {
192 let distribution_location =
193 default_distribution_location(&DistributionFlavor::Standalone, target_triple, None)?;
194
195 let mut temp_dir = None;
196
197 let extract_path = if let Some(path) = distributions_dir {
198 path
199 } else {
200 temp_dir.replace(env.temporary_directory("python-distribution")?);
201 temp_dir.as_ref().unwrap().path()
202 };
203
204 let dist = resolve_distribution(&distribution_location, extract_path)?;
205
206 if scan_distribution {
207 println!("scanning distribution");
208 for resource in dist.python_resources() {
209 print_resource(&resource);
210 }
211 } else if let Some(path) = path {
212 if path.is_dir() {
213 println!("scanning directory {}", path.display());
214 for resource in find_python_resources(
215 path,
216 dist.cache_tag(),
217 &dist.python_module_suffixes()?,
218 emit_files,
219 classify_files,
220 )? {
221 print_resource(&resource?);
222 }
223 } else if path.is_file() {
224 if let Some(extension) = path.extension() {
225 if extension.to_string_lossy() == "whl" {
226 println!("parsing {} as a wheel archive", path.display());
227 let wheel = WheelArchive::from_path(path)?;
228
229 for resource in wheel.python_resources(
230 dist.cache_tag(),
231 &dist.python_module_suffixes()?,
232 emit_files,
233 classify_files,
234 )? {
235 print_resource(&resource)
236 }
237
238 return Ok(());
239 }
240 }
241
242 println!("do not know how to find resources in {}", path.display());
243 } else {
244 println!("do not know how to find resources in {}", path.display());
245 }
246 } else {
247 println!("do not know what to scan");
248 }
249
250 Ok(())
251}
252
253fn print_resource(r: &PythonResource) {
254 match r {
255 PythonResource::ModuleSource(m) => println!(
256 "PythonModuleSource {{ name: {}, is_package: {}, is_stdlib: {}, is_test: {} }}",
257 m.name, m.is_package, m.is_stdlib, m.is_test
258 ),
259 PythonResource::ModuleBytecode(m) => println!(
260 "PythonModuleBytecode {{ name: {}, is_package: {}, is_stdlib: {}, is_test: {}, bytecode_level: {} }}",
261 m.name, m.is_package, m.is_stdlib, m.is_test, i32::from(m.optimize_level)
262 ),
263 PythonResource::ModuleBytecodeRequest(_) => println!(
264 "PythonModuleBytecodeRequest {{ you should never see this }}"
265 ),
266 PythonResource::PackageResource(r) => println!(
267 "PythonPackageResource {{ package: {}, name: {}, is_stdlib: {}, is_test: {} }}", r.leaf_package, r.relative_name, r.is_stdlib, r.is_test
268 ),
269 PythonResource::PackageDistributionResource(r) => println!(
270 "PythonPackageDistributionResource {{ package: {}, version: {}, name: {} }}", r.package, r.version, r.name
271 ),
272 PythonResource::ExtensionModule(em) => {
273 println!(
274 "PythonExtensionModule {{"
275 );
276 println!(" name: {}", em.name);
277 println!(" is_builtin: {}", em.builtin_default);
278 println!(" has_shared_library: {}", em.shared_library.is_some());
279 println!(" has_object_files: {}", !em.object_file_data.is_empty());
280 println!(" link_libraries: {:?}", em.link_libraries);
281 println!("}}");
282 },
283 PythonResource::EggFile(e) => println!(
284 "PythonEggFile {{ path: {} }}", match &e.data {
285 FileData::Path(p) => p.display().to_string(),
286 FileData::Memory(_) => "memory".to_string(),
287 }
288 ),
289 PythonResource::PathExtension(_pe) => println!(
290 "PythonPathExtension",
291 ),
292 PythonResource::File(f) => println!(
293 "File {{ path: {}, is_executable: {} }}", f.path().display(), f.entry().is_executable()
294 ),
295 }
296}
297
298pub fn init_config_file(
300 source: &PyOxidizerSource,
301 project_dir: &Path,
302 code: Option<&str>,
303 pip_install: &[&str],
304) -> Result<()> {
305 if project_dir.exists() && !project_dir.is_dir() {
306 return Err(anyhow!(
307 "existing path must be a directory: {}",
308 project_dir.display()
309 ));
310 }
311
312 if !project_dir.exists() {
313 create_dir_all(project_dir)?;
314 }
315
316 let name = project_dir.iter().last().unwrap().to_str().unwrap();
317
318 write_new_pyoxidizer_config_file(source, project_dir, name, code, pip_install)?;
319
320 println!();
321 println!("A new PyOxidizer configuration file has been created.");
322 println!("This configuration file can be used by various `pyoxidizer`");
323 println!("commands");
324 println!();
325 println!("For example, to build and run the default Python application:");
326 println!();
327 println!(" $ cd {}", project_dir.display());
328 println!(" $ pyoxidizer run");
329 println!();
330 println!("The default configuration is to invoke a Python REPL. You can");
331 println!("edit the configuration file to change behavior.");
332
333 Ok(())
334}
335
336pub fn init_rust_project(env: &Environment, project_path: &Path) -> Result<()> {
338 let cargo_exe = env
339 .ensure_rust_toolchain(None)
340 .context("resolving Rust environment")?
341 .cargo_exe;
342
343 initialize_project(
344 &env.pyoxidizer_source,
345 project_path,
346 &cargo_exe,
347 None,
348 &[],
349 "console",
350 )?;
351 println!();
352 println!(
353 "A new Rust binary application has been created in {}",
354 project_path.display()
355 );
356 print!(
357 r#"
358This application can be built most easily by doing the following:
359
360 $ cd {project_path}
361 $ pyoxidizer run
362
363Note however that this will bypass all the Rust code in the project
364folder, and build the project as if you had only created a pyoxidizer.bzl
365file. Building from Rust is more involved, and requires multiple steps.
366Please see the "PyOxidizer Rust Projects" section of the manual for more
367information.
368
369The default configuration is to invoke a Python REPL. You can
370edit the various pyoxidizer.*.bzl config files or the main.rs
371file to change behavior. The application will need to be rebuilt
372for configuration changes to take effect.
373"#,
374 project_path = project_path.display()
375 );
376
377 Ok(())
378}
379
380pub fn python_distribution_extract(
381 download_default: bool,
382 archive_path: Option<&str>,
383 dest_path: &str,
384) -> Result<()> {
385 let dist_path = if let Some(path) = archive_path {
386 PathBuf::from(path)
387 } else if download_default {
388 let location = default_distribution_location(
389 &DistributionFlavor::Standalone,
390 default_target_triple(),
391 None,
392 )?;
393
394 resolve_python_distribution_archive(&location, Path::new(dest_path))?
395 } else {
396 return Err(anyhow!("do not know what distribution to operate on"));
397 };
398
399 let mut fh = std::fs::File::open(&dist_path)?;
400 let mut data = Vec::new();
401 fh.read_to_end(&mut data)?;
402 let cursor = Cursor::new(data);
403 let dctx = zstd::stream::Decoder::new(cursor)?;
404 let mut tf = tar::Archive::new(dctx);
405
406 println!("extracting archive to {}", dest_path);
407 tf.unpack(dest_path)?;
408
409 Ok(())
410}
411
412pub fn python_distribution_info(env: &Environment, dist_path: &str) -> Result<()> {
413 let fh = std::fs::File::open(Path::new(dist_path))?;
414 let reader = std::io::BufReader::new(fh);
415
416 let temp_dir = env.temporary_directory("python-distribution")?;
417 let temp_dir_path = temp_dir.path();
418
419 let dist = StandaloneDistribution::from_tar_zst(reader, temp_dir_path)?;
420
421 println!("High-Level Metadata");
422 println!("===================");
423 println!();
424 println!("Target triple: {}", dist.target_triple);
425 println!("Tag: {}", dist.python_tag);
426 println!("Platform tag: {}", dist.python_platform_tag);
427 println!("Version: {}", dist.version);
428 println!();
429
430 println!("Extension Modules");
431 println!("=================");
432 for (name, ems) in dist.extension_modules {
433 println!("{}", name);
434 println!("{}", "-".repeat(name.len()));
435 println!();
436
437 for em in ems.iter() {
438 println!("{}", em.variant.as_ref().unwrap());
439 println!("{}", "^".repeat(em.variant.as_ref().unwrap().len()));
440 println!();
441 println!("Required: {}", em.required);
442 println!("Built-in Default: {}", em.builtin_default);
443 if let Some(component) = &em.license {
444 println!(
445 "Licensing: {}",
446 match component.license() {
447 LicenseFlavor::Spdx(expression) => expression.to_string(),
448 LicenseFlavor::OtherExpression(expression) => expression.to_string(),
449 LicenseFlavor::PublicDomain => "public domain".to_string(),
450 LicenseFlavor::None => "none".to_string(),
451 LicenseFlavor::Unknown(terms) => terms.join(","),
452 }
453 );
454 }
455 if !em.link_libraries.is_empty() {
456 println!(
457 "Links: {}",
458 em.link_libraries
459 .iter()
460 .map(|l| l.name.clone())
461 .collect::<Vec<String>>()
462 .join(", ")
463 );
464 }
465
466 println!();
467 }
468 }
469
470 println!("Python Modules");
471 println!("==============");
472 println!();
473 for name in dist.py_modules.keys() {
474 println!("{}", name);
475 }
476 println!();
477
478 println!("Python Resources");
479 println!("================");
480 println!();
481
482 for (package, resources) in dist.resources {
483 for name in resources.keys() {
484 println!("[{}].{}", package, name);
485 }
486 }
487
488 Ok(())
489}
490
491pub fn python_distribution_licenses(env: &Environment, path: &str) -> Result<()> {
492 let fh = std::fs::File::open(Path::new(path))?;
493 let reader = std::io::BufReader::new(fh);
494
495 let temp_dir = env.temporary_directory("python-distribution")?;
496 let temp_dir_path = temp_dir.path();
497
498 let dist = StandaloneDistribution::from_tar_zst(reader, temp_dir_path)?;
499
500 println!(
501 "Python Distribution Licenses: {}",
502 match dist.licenses {
503 Some(licenses) => itertools::join(licenses, ", "),
504 None => "NO LICENSE FOUND".to_string(),
505 }
506 );
507 println!();
508 println!("Extension Libraries and License Requirements");
509 println!("============================================");
510 println!();
511
512 for (name, variants) in &dist.extension_modules {
513 for variant in variants.iter() {
514 if variant.link_libraries.is_empty() {
515 continue;
516 }
517
518 let name = if variant.variant.as_ref().unwrap() == "default" {
519 name.clone()
520 } else {
521 format!("{} ({})", name, variant.variant.as_ref().unwrap())
522 };
523
524 println!("{}", name);
525 println!("{}", "-".repeat(name.len()));
526 println!();
527
528 for link in &variant.link_libraries {
529 println!("Dependency: {}", &link.name);
530 println!(
531 "Link Type: {}",
532 if link.system {
533 "system"
534 } else if link.framework {
535 "framework"
536 } else {
537 "library"
538 }
539 );
540
541 println!();
542 }
543
544 if let Some(component) = &variant.license {
545 match component.license() {
546 LicenseFlavor::Spdx(expression) => {
547 println!("Licensing: Valid SPDX: {}", expression);
548 }
549 LicenseFlavor::OtherExpression(expression) => {
550 println!("Licensing: Invalid SPDX: {}", expression);
551 }
552 LicenseFlavor::PublicDomain => {
553 println!("Licensing: Public Domain");
554 }
555 LicenseFlavor::None => {
556 println!("Licensing: None defined");
557 }
558 LicenseFlavor::Unknown(terms) => {
559 println!("Licensing: {}", terms.join(", "));
560 }
561 }
562 } else {
563 println!("Licensing: UNKNOWN");
564 }
565
566 println!();
567 }
568 }
569
570 Ok(())
571}
572
573pub fn generate_python_embedding_artifacts(
575 env: &Environment,
576 target_triple: &str,
577 flavor: &str,
578 python_version: Option<&str>,
579 dest_path: &Path,
580) -> Result<()> {
581 let flavor = DistributionFlavor::try_from(flavor).map_err(|e| anyhow!("{}", e))?;
582
583 std::fs::create_dir_all(dest_path)
584 .with_context(|| format!("creating directory {}", dest_path.display()))?;
585
586 let dest_path = canonicalize_path(dest_path).context("canonicalizing destination directory")?;
587
588 let distribution_record = PYTHON_DISTRIBUTIONS
589 .find_distribution(target_triple, &flavor, python_version)
590 .ok_or_else(|| anyhow!("could not find Python distribution matching requirements"))?;
591
592 let distribution_cache = DistributionCache::new(Some(&env.python_distributions_dir()));
593
594 let dist = distribution_cache
595 .resolve_distribution(&distribution_record.location, None)
596 .context("resolving Python distribution")?;
597
598 let host_dist = distribution_cache
599 .host_distribution(Some(dist.python_major_minor_version().as_str()), None)
600 .context("resolving host distribution")?;
601
602 let policy = dist
603 .create_packaging_policy()
604 .context("creating packaging policy")?;
605
606 let mut interpreter_config = dist
607 .create_python_interpreter_config()
608 .context("creating Python interpreter config")?;
609
610 interpreter_config.config.profile = PythonInterpreterProfile::Python;
611 interpreter_config.allocator_backend = MemoryAllocatorBackend::Default;
612
613 let mut builder = dist.as_python_executable_builder(
614 default_target_triple(),
615 target_triple,
616 "python",
617 BinaryLibpythonLinkMode::Default,
618 &policy,
619 &interpreter_config,
620 Some(host_dist.clone_trait()),
621 )?;
622
623 builder.set_tcl_files_path(Some("tcl".to_string()));
624
625 builder
626 .add_distribution_resources(None)
627 .context("adding distribution resources")?;
628
629 let embedded_context = builder
630 .to_embedded_python_context(env, "1")
631 .context("resolving embedded context")?;
632
633 embedded_context
634 .write_files(&dest_path)
635 .context("writing embedded artifact files")?;
636
637 embedded_context
638 .extra_files
639 .materialize_files(&dest_path)
640 .context("writing extra files")?;
641
642 let mut m = FileManifest::default();
644 for resource in find_python_resources(
645 &dist.stdlib_path,
646 dist.cache_tag(),
647 &dist.python_module_suffixes()?,
648 true,
649 false,
650 )? {
651 if let PythonResource::File(file) = resource? {
652 m.add_file_entry(file.path(), file.entry())?;
653 } else {
654 panic!("find_python_resources() should only emit File variant");
655 }
656 }
657
658 m.materialize_files_with_replace(dest_path.join("stdlib"))
659 .context("writing standard library")?;
660
661 Ok(())
662}
663
664pub fn rust_project_licensing(
665 env: &Environment,
666 project_path: &Path,
667 all_features: bool,
668 target_triple: Option<&str>,
669 unified_license: bool,
670) -> Result<()> {
671 let manifest_path = project_path.join("Cargo.toml");
672
673 let toolchain = env
674 .ensure_rust_toolchain(None)
675 .context("resolving Rust toolchain")?;
676
677 let licensing = licenses_from_cargo_manifest(
678 &manifest_path,
679 all_features,
680 [],
681 target_triple,
682 &toolchain,
683 true,
684 )?;
685
686 if unified_license {
687 println!("{}", licensing.aggregate_license_document(true)?);
688 } else {
689 log_licensing_info(&licensing);
690 }
691
692 Ok(())
693}