pyoxidizer 0.24.0

Package self-contained Python applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use {
    crate::{
        environment::{default_target_triple, PYOXIDIZER_VERSION},
        project_building, projectmgmt,
    },
    anyhow::{anyhow, Context, Result},
    clap::{value_parser, Arg, ArgAction, ArgMatches, Command},
    std::{
        collections::HashMap,
        path::{Path, PathBuf},
    },
};

const BUILD_ABOUT: &str = "\
Build a PyOxidizer project.

The PATH argument is a filesystem path to a directory containing an
existing PyOxidizer enabled project.

This command will invoke Rust's build system tool (Cargo) to build
the project.
";

const INIT_RUST_PROJECT_ABOUT: &str = "\
Create a new Rust project embedding Python.

The PATH argument is a filesystem path that should be created to hold the
new Rust project.

This command will call `cargo init PATH` and then install files and make
modifications required to embed a Python interpreter in that application.

The new project's binary will be configured to launch a Python REPL by
default.

Created projects inherit settings such as Python distribution URLs and
dependency crate versions and locations from the PyOxidizer executable
they were created with.

On success, instructions on potential next steps are printed.
";

const GENERATE_PYTHON_EMBEDDING_ARTIFACTS_ABOUT: &str = "\
Generate files useful for embedding Python in a [Rust] binary.

This low-level command can be used to write files that facilitate the
embedding of Python in a larger binary. It can be used to write:

* A custom libpython that can be linked into a binary.
* A configuration file for the PyO3 Rust crate telling it how to
  link against the aforementioned custom libpython.
* A Python packed resources file containing the entirety of the Python
  standard library.
* A Rust file defining a default `pyembed::OxidizedPythonInterpreterConfig`
  struct for configuring the embedded Python interpreter.
* tcl/tk support files (for tkinter module support).
* Microsoft Visual C++ Redistributable Runtime DLLs (Windows only).

This command essentially does what the `run-build-script` command does except
it doesn't require the presence of a PyOxidizer configuration file. Instead,
it uses an opinionated default configuration suitable for producing a set of
files suitable for common Python embedding scenarios. If the defaults are not
appropriate for your use case, you can always define a configuration file to
customize them and use `run-build-script` to produce similar output files.
";

const RUN_BUILD_SCRIPT_ABOUT: &str = "\
Runs a crate build script to generate Python artifacts.

When the Rust crate embedding Python is built, it needs to consume various
artifacts derived from processing the active PyOxidizer config file.
These files are typically generated when the crate's build script runs.

This command executes the functionality to derive various artifacts and
emits special lines that tell the Rust build system how to consume them.
";

const RESOURCES_SCAN_ABOUT: &str = "\
Scan a directory or file for Python resources.

This command invokes the logic used by various PyOxidizer functionality
walking a directory tree or parsing a file and categorizing seen files.

The directory walking functionality is used by
`oxidized_importer.find_resources_in_path()` and Starlark methods like
`PythonExecutable.pip_install()` and
`PythonExecutable.read_package_root()`.

The file parsing logic is used for parsing the contents of wheels.

This command can be used to debug failures with PyOxidizer's code
for converting files/directories into strongly typed objects. This
conversion is critical for properly packaging Python applications and
bugs can result in incorrect install layouts, missing resources, etc.
";

const VAR_HELP: &str = "\
Defines a single string key to set in the VARS global dict.

This argument can be used to inject variable content into the Starlark
execution context to influence evaluation.

<name> defines the key in the dict to set and <value> is its string
value.

For example, `--var my_var my_value` is functionally similar to the
Starlark expression `VARS[\"my_var\"] = \"my_value\"`.

If a Starlark variable is defined multiple times, an error occurs.
";

const ENV_VAR_HELP: &str = "\
Defines a single string key to set in the VARS global dict from an environment variable.

This is like --var except the value of the dict key comes from an
environment variable.

The <env> environment variable is read and becomes the value of the
<name> key in the VARS dict.

If the <env> environment variable is not set, the Starlark value will
be `None` instead of a `string`.

If a Starlark variable is defined multiple times, an error occurs.
";

fn add_env_args(app: Command) -> Command {
    app.arg(
        Arg::new("vars")
            .long("var")
            .action(ArgAction::Append)
            .value_names(["name", "value"])
            .number_of_values(2)
            .help("Define a variable in Starlark environment")
            .long_help(VAR_HELP),
    )
    .arg(
        Arg::new("vars_env")
            .long("var-env")
            .action(ArgAction::Append)
            .value_names(["name", "env"])
            .number_of_values(2)
            .help("Define an environment variable in Starlark environment")
            .long_help(ENV_VAR_HELP),
    )
}

fn add_python_distribution_args(app: Command) -> Command {
    app.arg(
        Arg::new("target_triple")
            .long("target-triple")
            .action(ArgAction::Set)
            .help("Rust target triple being targeted")
            .default_value(default_target_triple()),
    )
    .arg(
        Arg::new("flavor")
            .long("flavor")
            .action(ArgAction::Set)
            .help("Python distribution flavor")
            .default_value("standalone"),
    )
    .arg(
        Arg::new("python_version")
            .long("python-version")
            .action(ArgAction::Set)
            .help("Python version (X.Y) to use"),
    )
}

fn starlark_vars(args: &ArgMatches) -> Result<HashMap<String, Option<String>>> {
    let mut res = HashMap::new();

    if let Some(mut vars) = args.get_many::<String>("vars") {
        while let (Some(name), Some(value)) = (vars.next(), vars.next()) {
            if res.contains_key(name) {
                return Err(anyhow!("Starlark variable {} already defined", name));
            }

            res.insert(name.to_string(), Some(value.to_string()));
        }
    }

    if let Some(mut vars) = args.get_many::<String>("vars_env") {
        while let (Some(name), Some(env)) = (vars.next(), vars.next()) {
            if res.contains_key(name) {
                return Err(anyhow!("Starlark variable {} already defined", name));
            }

            res.insert(name.to_string(), std::env::var(env).ok());
        }
    }

    Ok(res)
}

pub fn run_cli() -> Result<()> {
    let mut env = crate::environment::Environment::new()?;

    let version = env.pyoxidizer_source.version_long();

    let app = Command::new("PyOxidizer")
        .version(PYOXIDIZER_VERSION)
        .long_version(version)
        .author("Gregory Szorc <gregory.szorc@gmail.com>")
        .long_about("Build and distribute Python applications")
        .arg_required_else_help(true)
        .arg(
            Arg::new("system_rust")
                .long("system-rust")
                .global(true)
                .action(ArgAction::SetTrue)
                .help("Use a system install of Rust instead of a self-managed Rust installation"),
        )
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .global(true)
                .action(ArgAction::Count)
                .help("Increase logging verbosity. Can be specified multiple times"),
        );

    let app = app.subcommand(
        Command::new("analyze").about("Analyze a built binary").arg(
            Arg::new("path")
                .action(ArgAction::Set)
                .value_parser(value_parser!(PathBuf))
                .required(true)
                .help("Path to executable to analyze"),
        ),
    );

    let app = app.subcommand(add_env_args(
        Command::new("build")
            .about("Build a PyOxidizer enabled project")
            .long_about(BUILD_ABOUT)
            .arg(
                Arg::new("target_triple")
                    .long("target-triple")
                    .action(ArgAction::Set)
                    .help("Rust target triple to build for"),
            )
            .arg(
                Arg::new("release")
                    .long("release")
                    .action(ArgAction::SetTrue)
                    .help("Build a release binary"),
            )
            .arg(
                Arg::new("path")
                    .long("path")
                    .action(ArgAction::Set)
                    .value_parser(value_parser!(PathBuf))
                    .default_value(".")
                    .value_name("PATH")
                    .help("Directory containing project to build"),
            )
            .arg(
                Arg::new("targets")
                    .value_name("TARGET")
                    .action(ArgAction::Append)
                    .num_args(0..)
                    .help("Target to resolve"),
            ),
    ));

    let app =
        app.subcommand(Command::new("cache-clear").about("Clear PyOxidizer's user-specific cache"));

    let app = app.subcommand(
        Command::new("find-resources")
            .about("Find resources in a file or directory")
            .long_about(RESOURCES_SCAN_ABOUT)
            .arg(
                Arg::new("distributions_dir")
                    .long("distributions-dir")
                    .action(ArgAction::Set)
                    .value_parser(value_parser!(PathBuf))
                    .value_name("PATH")
                    .help("Directory to extract downloaded Python distributions into"),
            )
            .arg(
                Arg::new("scan_distribution")
                    .long("scan-distribution")
                    .action(ArgAction::SetTrue)
                    .help("Scan the Python distribution instead of a path"),
            )
            .arg(
                Arg::new("target_triple")
                    .long("target-triple")
                    .action(ArgAction::Set)
                    .default_value(default_target_triple())
                    .help("Target triple of Python distribution to use"),
            )
            .arg(
                Arg::new("no_classify_files")
                    .long("no-classify-files")
                    .action(ArgAction::SetTrue)
                    .help("Whether to skip classifying files as typed resources"),
            )
            .arg(
                Arg::new("no_emit_files")
                    .long("no-emit-files")
                    .action(ArgAction::SetTrue)
                    .help("Whether to skip emitting File resources"),
            )
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .value_parser(value_parser!(PathBuf))
                    .value_name("PATH")
                    .required(true)
                    .help("Filesystem path to scan for resources. Must be a directory or Python wheel")
            ),
    );

    let app = app.subcommand(add_python_distribution_args(
        Command::new("generate-python-embedding-artifacts")
            .about("Generate files useful for embedding Python in a [Rust] binary")
            .long_about(GENERATE_PYTHON_EMBEDDING_ARTIFACTS_ABOUT)
            .arg(
                Arg::new("dest_path")
                    .action(ArgAction::Set)
                    .value_parser(value_parser!(PathBuf))
                    .value_name("DESTINATION_PATH")
                    .required(true)
                    .help("Output directory for written files"),
            ),
    ));

    let app = app.subcommand(
        Command::new("init-config-file")
            .about("Create a new PyOxidizer configuration file.")
            .arg(
                Arg::new("python-code")
                    .long("python-code")
                    .action(ArgAction::Set)
                    .help("Default Python code to execute in built executable"),
            )
            .arg(
                Arg::new("pip-install")
                    .long("pip-install")
                    .action(ArgAction::Append)
                    .help("Python package to install via `pip install`"),
            )
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .required(true)
                    .value_name("PATH")
                    .help("Directory where configuration file should be created"),
            ),
    );

    let app = app.subcommand(
        Command::new("init-rust-project")
            .about("Create a new Rust project embedding a Python interpreter")
            .long_about(INIT_RUST_PROJECT_ABOUT)
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .required(true)
                    .value_name("PATH")
                    .help("Path of project directory to create"),
            ),
    );

    let app = app.subcommand(
        Command::new("list-targets")
            .about("List targets available to resolve in a configuration file")
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .default_value(".")
                    .value_name("PATH")
                    .help("Path to project to evaluate"),
            ),
    );

    let app = app.subcommand(
        Command::new("python-distribution-extract")
            .about("Extract a Python distribution archive to a directory")
            .arg(
                Arg::new("download-default")
                    .long("download-default")
                    .action(ArgAction::SetTrue)
                    .help("Download and extract the default distribution for this platform"),
            )
            .arg(
                Arg::new("archive-path")
                    .long("archive-path")
                    .action(ArgAction::Set)
                    .value_name("DISTRIBUTION_PATH")
                    .help("Path to a Python distribution archive"),
            )
            .arg(
                Arg::new("dest_path")
                    .action(ArgAction::Set)
                    .required(true)
                    .value_name("DESTINATION_PATH")
                    .help("Path to directory where distribution should be extracted"),
            ),
    );

    let app = app.subcommand(
        Command::new("python-distribution-info")
            .about("Show information about a Python distribution archive")
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .required(true)
                    .value_name("PATH")
                    .help("Path to Python distribution archive to analyze"),
            ),
    );

    let app = app.subcommand(
        Command::new("python-distribution-licenses")
            .about("Show licenses for a given Python distribution")
            .arg(
                Arg::new("path")
                    .action(ArgAction::Set)
                    .required(true)
                    .value_name("PATH")
                    .help("Path to Python distribution to analyze"),
            ),
    );

    let app = app.subcommand(add_env_args(
        Command::new("run-build-script")
            .about("Run functionality that a build script would perform")
            .long_about(RUN_BUILD_SCRIPT_ABOUT)
            .arg(
                Arg::new("build-script-name")
                    .action(ArgAction::Set)
                    .required(true)
                    .help("Value to use for Rust build script"),
            )
            .arg(
                Arg::new("target")
                    .long("target")
                    .action(ArgAction::Set)
                    .help("The config file target to resolve"),
            ),
    ));

    let app = app.subcommand(add_env_args(
        Command::new("run")
            .about("Run a target in a PyOxidizer configuration file")
            .trailing_var_arg(true)
            .arg(
                Arg::new("target_triple")
                    .long("target-triple")
                    .action(ArgAction::Set)
                    .help("Rust target triple to build for"),
            )
            .arg(
                Arg::new("release")
                    .long("release")
                    .action(ArgAction::SetTrue)
                    .help("Run a release binary"),
            )
            .arg(
                Arg::new("path")
                    .long("path")
                    .action(ArgAction::Set)
                    .default_value(".")
                    .value_name("PATH")
                    .help("Directory containing project to build"),
            )
            .arg(
                Arg::new("target")
                    .long("target")
                    .action(ArgAction::Set)
                    .help("Build target to run"),
            )
            .arg(Arg::new("extra").action(ArgAction::Append).num_args(0..)),
    ));

    let app = app.subcommand(
        Command::new("rust-project-licensing")
            .about("Show licensing information for a Rust project")
            .arg(
                Arg::new("all_features")
                    .long("all-features")
                    .action(ArgAction::SetTrue)
                    .help("Activate all crate features during evaluation"),
            )
            .arg(
                Arg::new("target_triple")
                    .long("target-triple")
                    .action(ArgAction::Set)
                    .help("Rust target triple to simulate building for"),
            )
            .arg(
                Arg::new("unified_license")
                    .long("unified-license")
                    .action(ArgAction::SetTrue)
                    .help("Print a unified license document"),
            )
            .arg(
                Arg::new("project_path")
                    .action(ArgAction::Set)
                    .value_parser(value_parser!(PathBuf))
                    .required(true)
                    .help("The path to the Rust project to evaluate"),
            ),
    );

    let matches = app.get_matches();

    let verbose = matches.contains_id("verbose");

    let log_level = match matches.get_count("verbose") {
        0 => log::LevelFilter::Warn,
        1 => log::LevelFilter::Info,
        2 => log::LevelFilter::Debug,
        _ => log::LevelFilter::Trace,
    };

    let mut builder = env_logger::Builder::from_env(
        env_logger::Env::default().default_filter_or(log_level.as_str()),
    );

    builder
        .format_timestamp(None)
        .format_level(false)
        .format_target(false);

    builder.init();

    if matches.get_flag("system_rust") {
        env.unmanage_rust().context("unmanaging Rust")?;
    }

    let (command, args) = matches
        .subcommand()
        .ok_or_else(|| anyhow!("invalid sub-command"))?;

    match command {
        "analyze" => {
            let path = args.get_one::<PathBuf>("path").unwrap();

            tugger_binary_analysis::analyze_file(path.clone());

            Ok(())
        }

        "build" => {
            let starlark_vars = starlark_vars(args)?;
            let release = args.get_flag("release");
            let target_triple = args.get_one::<String>("target_triple");
            let path = args.get_one::<PathBuf>("path").unwrap();
            let resolve_targets = args
                .get_many::<String>("targets")
                .map(|x| x.cloned().collect::<Vec<_>>());

            projectmgmt::build(
                &env,
                path,
                target_triple.map(|x| x.as_str()),
                resolve_targets,
                starlark_vars,
                release,
                verbose,
            )
        }

        "cache-clear" => projectmgmt::cache_clear(&env),

        "find-resources" => {
            let path = args.get_one::<PathBuf>("path");
            let distributions_dir = args.get_one::<PathBuf>("distributions_dir");
            let scan_distribution = args.get_flag("scan_distribution");
            let target_triple = args.get_one::<String>("target_triple").unwrap();
            let classify_files = !args.get_flag("no_classify_files");
            let emit_files = !args.get_flag("no_emit_files");

            if path.is_none() && !scan_distribution {
                Err(anyhow!("must specify a path or --scan-distribution"))
            } else {
                projectmgmt::find_resources(
                    &env,
                    path.map(|x| x.as_path()),
                    distributions_dir.map(|x| x.as_path()),
                    scan_distribution,
                    target_triple,
                    classify_files,
                    emit_files,
                )
            }
        }

        "generate-python-embedding-artifacts" => {
            let target_triple = args
                .get_one::<String>("target_triple")
                .expect("target_triple should have default");
            let flavor = args
                .get_one::<String>("flavor")
                .expect("flavor should have default");
            let python_version = args.get_one::<String>("python_version");
            let dest_path = args
                .get_one::<PathBuf>("dest_path")
                .expect("dest_path should be required");

            projectmgmt::generate_python_embedding_artifacts(
                &env,
                target_triple,
                flavor,
                python_version.map(|x| x.as_str()),
                dest_path,
            )
        }

        "init-config-file" => {
            let code = args.get_one::<String>("python-code");
            let pip_install = args
                .get_many::<String>("pip-install")
                .unwrap_or_default()
                .map(|x| x.as_str())
                .collect::<Vec<_>>();
            let path = args.get_one::<String>("path").unwrap();
            let config_path = Path::new(path);

            projectmgmt::init_config_file(
                &env.pyoxidizer_source,
                config_path,
                code.map(|x| x.as_str()),
                &pip_install,
            )
        }

        "list-targets" => {
            let path = args.get_one::<String>("path").unwrap();

            projectmgmt::list_targets(&env, Path::new(path))
        }

        "init-rust-project" => {
            let path = args.get_one::<String>("path").unwrap();
            let project_path = Path::new(path);

            projectmgmt::init_rust_project(&env, project_path)
        }

        "python-distribution-extract" => {
            let download_default = args.get_flag("download-default");
            let archive_path = args.get_one::<String>("archive-path");
            let dest_path = args.get_one::<String>("dest_path").unwrap();

            if !download_default && archive_path.is_none() {
                Err(anyhow!("must specify --download-default or --archive-path"))
            } else if download_default && archive_path.is_some() {
                Err(anyhow!(
                    "must only specify one of --download-default or --archive-path"
                ))
            } else {
                projectmgmt::python_distribution_extract(
                    download_default,
                    archive_path.map(|x| x.as_str()),
                    dest_path,
                )
            }
        }

        "python-distribution-info" => {
            let dist_path = args.get_one::<String>("path").unwrap();

            projectmgmt::python_distribution_info(&env, dist_path)
        }

        "python-distribution-licenses" => {
            let path = args.get_one::<String>("path").unwrap();

            projectmgmt::python_distribution_licenses(&env, path)
        }

        "run-build-script" => {
            let starlark_vars = starlark_vars(args)?;
            let build_script = args.get_one::<String>("build-script-name").unwrap();
            let target = args.get_one::<String>("target");

            project_building::run_from_build(
                &env,
                build_script,
                target.map(|x| x.as_str()),
                starlark_vars,
            )
        }

        "run" => {
            let starlark_vars = starlark_vars(args)?;
            let target_triple = args.get_one::<String>("target_triple");
            let release = args.get_flag("release");
            let path = args.get_one::<String>("path").unwrap();
            let target = args.get_one::<String>("target");
            let extra = args
                .get_many::<String>("extra")
                .unwrap_or_default()
                .map(|x| x.as_str())
                .collect::<Vec<_>>();

            projectmgmt::run(
                &env,
                Path::new(path),
                target_triple.map(|x| x.as_str()),
                release,
                target.map(|x| x.as_str()),
                starlark_vars,
                &extra,
                verbose,
            )
        }

        "rust-project-licensing" => {
            let project_path = args
                .get_one::<PathBuf>("project_path")
                .expect("argument is required");
            let all_features = args.get_flag("all_features");
            let target_triple = args.get_one::<String>("target_triple");
            let unified_license = args.get_flag("unified_license");

            projectmgmt::rust_project_licensing(
                &env,
                project_path,
                all_features,
                target_triple.map(|x| x.as_str()),
                unified_license,
            )
        }

        _ => Err(anyhow!("invalid sub-command")),
    }
}