1use {
10 super::{
11 binary::LibpythonLinkMode, distribution::PythonDistribution,
12 distutils::read_built_extensions, standalone_distribution::resolve_python_paths,
13 },
14 crate::environment::Environment,
15 anyhow::{anyhow, Context, Result},
16 duct::{cmd, ReaderHandle},
17 log::warn,
18 python_packaging::{
19 filesystem_scanning::find_python_resources, policy::PythonPackagingPolicy,
20 resource::PythonResource, wheel::WheelArchive,
21 },
22 std::{
23 collections::{hash_map::RandomState, HashMap},
24 hash::BuildHasher,
25 io::{BufRead, BufReader},
26 path::{Path, PathBuf},
27 },
28};
29
30fn log_command_output(handle: &ReaderHandle) {
31 let reader = BufReader::new(handle);
32 for line in reader.lines() {
33 match line {
34 Ok(line) => {
35 warn!("{}", line);
36 }
37 Err(err) => {
38 warn!("Error when reading output: {:?}", err);
39 }
40 }
41 }
42}
43
44pub fn find_resources<'a>(
46 dist: &dyn PythonDistribution,
47 policy: &PythonPackagingPolicy,
48 path: &Path,
49 state_dir: Option<PathBuf>,
50) -> Result<Vec<PythonResource<'a>>> {
51 let mut res = Vec::new();
52
53 let built_extensions = if let Some(p) = state_dir {
54 read_built_extensions(&p)?
55 .iter()
56 .map(|ext| (ext.name.clone(), ext.clone()))
57 .collect()
58 } else {
59 HashMap::new()
60 };
61
62 for r in find_python_resources(
63 path,
64 dist.cache_tag(),
65 &dist.python_module_suffixes()?,
66 policy.file_scanner_emit_files(),
67 policy.file_scanner_classify_files(),
68 )? {
69 let r = r?.to_memory()?;
70
71 match r {
72 PythonResource::ExtensionModule(e) => {
73 res.push(if let Some(built) = built_extensions.get(&e.name) {
75 PythonResource::from(built.to_memory()?)
76 } else {
77 PythonResource::ExtensionModule(e)
78 });
79 }
80 _ => {
81 res.push(r);
82 }
83 }
84 }
85
86 Ok(res)
87}
88
89pub fn pip_download<'a>(
101 env: &Environment,
102 host_dist: &dyn PythonDistribution,
103 taget_dist: &dyn PythonDistribution,
104 policy: &PythonPackagingPolicy,
105 verbose: bool,
106 args: &[String],
107) -> Result<Vec<PythonResource<'a>>> {
108 let temp_dir = env.temporary_directory("pyoxidizer-pip-download")?;
109
110 host_dist.ensure_pip()?;
111
112 let target_dir = temp_dir.path();
113
114 warn!("pip downloading to {}", target_dir.display());
115
116 let mut pip_args = vec![
117 "-m".to_string(),
118 "pip".to_string(),
119 "--disable-pip-version-check".to_string(),
120 ];
121
122 if verbose {
123 pip_args.push("--verbose".to_string());
124 }
125
126 pip_args.extend(vec![
127 "download".to_string(),
128 "--dest".to_string(),
130 format!("{}", target_dir.display()),
131 "--only-binary=:all:".to_string(),
133 format!(
135 "--platform={}",
136 taget_dist.python_platform_compatibility_tag()
137 ),
138 format!("--python-version={}", taget_dist.python_version()),
139 format!(
140 "--implementation={}",
141 taget_dist.python_implementation_short()
142 ),
143 ]);
144
145 if let Some(abi) = taget_dist.python_abi_tag() {
146 pip_args.push(format!("--abi={}", abi));
147 }
148
149 pip_args.extend(args.iter().cloned());
150
151 warn!("running python {:?}", pip_args);
152
153 let command = cmd(host_dist.python_exe_path(), &pip_args)
154 .stderr_to_stdout()
155 .unchecked()
156 .reader()?;
157
158 log_command_output(&command);
159
160 let output = command
161 .try_wait()?
162 .ok_or_else(|| anyhow!("unable to wait on command"))?;
163 if !output.status.success() {
164 return Err(anyhow!("error running pip"));
165 }
166
167 let mut files = std::fs::read_dir(target_dir)?
172 .map(|entry| Ok(entry?.path()))
173 .collect::<Result<Vec<_>>>()?;
174 files.sort();
175
176 let mut res = Vec::new();
178
179 for path in &files {
180 let wheel = WheelArchive::from_path(path)?;
181
182 res.extend(wheel.python_resources(
183 taget_dist.cache_tag(),
184 &taget_dist.python_module_suffixes()?,
185 policy.file_scanner_emit_files(),
186 policy.file_scanner_classify_files(),
187 )?);
188 }
189
190 temp_dir.close().context("closing temporary directory")?;
191
192 Ok(res)
193}
194
195pub fn pip_install<'a, S: BuildHasher>(
197 env: &Environment,
198 dist: &dyn PythonDistribution,
199 policy: &PythonPackagingPolicy,
200 libpython_link_mode: LibpythonLinkMode,
201 verbose: bool,
202 install_args: &[String],
203 extra_envs: &HashMap<String, String, S>,
204) -> Result<Vec<PythonResource<'a>>> {
205 let temp_dir = env.temporary_directory("pyoxidizer-pip-install")?;
206
207 dist.ensure_pip()?;
208
209 let mut env: HashMap<String, String, RandomState> = std::env::vars().collect();
210 for (k, v) in dist.resolve_distutils(libpython_link_mode, temp_dir.path(), &[])? {
211 env.insert(k, v);
212 }
213
214 for (key, value) in extra_envs.iter() {
215 env.insert(key.clone(), value.clone());
216 }
217
218 let target_dir = temp_dir.path().join("install");
219
220 warn!("pip installing to {}", target_dir.display());
221
222 let mut pip_args: Vec<String> = vec![
223 "-m".to_string(),
224 "pip".to_string(),
225 "--disable-pip-version-check".to_string(),
226 ];
227
228 if verbose {
229 pip_args.push("--verbose".to_string());
230 }
231
232 pip_args.extend(vec![
233 "install".to_string(),
234 "--target".to_string(),
235 format!("{}", target_dir.display()),
236 ]);
237
238 pip_args.extend(install_args.iter().cloned());
239
240 let command = cmd(dist.python_exe_path(), &pip_args)
241 .full_env(&env)
242 .stderr_to_stdout()
243 .unchecked()
244 .reader()?;
245
246 log_command_output(&command);
247
248 let output = command
249 .try_wait()?
250 .ok_or_else(|| anyhow!("unable to wait on command"))?;
251 if !output.status.success() {
252 return Err(anyhow!("error running pip"));
253 }
254
255 let state_dir = env.get("PYOXIDIZER_DISTUTILS_STATE_DIR").map(PathBuf::from);
256
257 let resources =
258 find_resources(dist, policy, &target_dir, state_dir).context("scanning for resources")?;
259
260 temp_dir.close().context("closing temporary directory")?;
261
262 Ok(resources)
263}
264
265pub fn read_virtualenv<'a>(
267 dist: &dyn PythonDistribution,
268 policy: &PythonPackagingPolicy,
269 path: &Path,
270) -> Result<Vec<PythonResource<'a>>> {
271 let python_paths = resolve_python_paths(path, &dist.python_major_minor_version());
272
273 find_resources(dist, policy, &python_paths.site_packages, None)
274}
275
276#[allow(clippy::too_many_arguments)]
278pub fn setup_py_install<'a, S: BuildHasher>(
279 env: &Environment,
280 dist: &dyn PythonDistribution,
281 policy: &PythonPackagingPolicy,
282 libpython_link_mode: LibpythonLinkMode,
283 package_path: &Path,
284 verbose: bool,
285 extra_envs: &HashMap<String, String, S>,
286 extra_global_arguments: &[String],
287) -> Result<Vec<PythonResource<'a>>> {
288 if !package_path.is_absolute() {
289 return Err(anyhow!(
290 "package_path must be absolute: got {:?}",
291 package_path.display()
292 ));
293 }
294
295 let temp_dir = env.temporary_directory("pyoxidizer-setup-py-install")?;
296
297 let target_dir_path = temp_dir.path().join("install");
298 let target_dir_s = target_dir_path.display().to_string();
299
300 let python_paths = resolve_python_paths(&target_dir_path, &dist.python_major_minor_version());
301
302 std::fs::create_dir_all(&python_paths.site_packages)?;
303
304 let mut envs: HashMap<String, String, RandomState> = std::env::vars().collect();
305 for (k, v) in dist.resolve_distutils(
306 libpython_link_mode,
307 temp_dir.path(),
308 &[&python_paths.site_packages, &python_paths.stdlib],
309 )? {
310 envs.insert(k, v);
311 }
312
313 for (key, value) in extra_envs {
314 envs.insert(key.clone(), value.clone());
315 }
316
317 warn!(
318 "python setup.py installing {} to {}",
319 package_path.display(),
320 target_dir_s
321 );
322
323 let mut args = vec!["setup.py"];
324
325 if verbose {
326 args.push("--verbose");
327 }
328
329 for arg in extra_global_arguments {
330 args.push(arg);
331 }
332
333 args.extend(["install", "--prefix", &target_dir_s, "--no-compile"]);
334
335 let command = cmd(dist.python_exe_path(), &args)
336 .dir(package_path)
337 .full_env(&envs)
338 .stderr_to_stdout()
339 .unchecked()
340 .reader()?;
341
342 log_command_output(&command);
343
344 let output = command
345 .try_wait()?
346 .ok_or_else(|| anyhow!("unable to wait on command"))?;
347 if !output.status.success() {
348 return Err(anyhow!("error running pip"));
349 }
350
351 let state_dir = envs
352 .get("PYOXIDIZER_DISTUTILS_STATE_DIR")
353 .map(PathBuf::from);
354 warn!(
355 "scanning {} for resources",
356 python_paths.site_packages.display()
357 );
358 let resources = find_resources(dist, policy, &python_paths.site_packages, state_dir)
359 .context("scanning for resources")?;
360
361 temp_dir.close().context("closing temporary directory")?;
362
363 Ok(resources)
364}
365
366#[cfg(test)]
367mod tests {
368 use {
369 super::*,
370 crate::testutil::*,
371 std::{collections::BTreeSet, ops::Deref},
372 };
373
374 #[test]
375 fn test_install_black() -> Result<()> {
376 let env = get_env()?;
377 let distribution = get_default_distribution(None)?;
378
379 let resources: Vec<PythonResource> = pip_install(
380 &env,
381 distribution.deref(),
382 &distribution.create_packaging_policy()?,
383 LibpythonLinkMode::Dynamic,
384 false,
385 &["black==19.10b0".to_string()],
386 &HashMap::new(),
387 )?;
388
389 assert!(resources.iter().any(|r| r.full_name() == "appdirs"));
390 assert!(resources.iter().any(|r| r.full_name() == "black"));
391
392 Ok(())
393 }
394
395 #[test]
396 #[cfg(windows)]
397 fn test_install_cffi() -> Result<()> {
398 let env = get_env()?;
399 let distribution = get_default_dynamic_distribution()?;
400 let policy = distribution.create_packaging_policy()?;
401
402 let resources: Vec<PythonResource> = pip_install(
403 &env,
404 distribution.deref(),
405 &policy,
406 LibpythonLinkMode::Dynamic,
407 false,
408 &["cffi==1.15.0".to_string()],
409 &HashMap::new(),
410 )?;
411
412 let ems = resources
413 .iter()
414 .filter(|r| matches!(r, PythonResource::ExtensionModule { .. }))
415 .collect::<Vec<&PythonResource>>();
416
417 assert_eq!(ems.len(), 1);
418 assert_eq!(ems[0].full_name(), "_cffi_backend");
419
420 Ok(())
421 }
422
423 #[test]
424 fn test_pip_download_zstandard() -> Result<()> {
425 let env = get_env()?;
426
427 for target_dist in get_all_standalone_distributions()? {
428 if target_dist.python_platform_compatibility_tag() == "none" {
429 continue;
430 }
431
432 let host_dist = get_host_distribution_from_target(&target_dist)?;
433
434 warn!(
435 "using distribution {}-{}-{}",
436 target_dist.python_implementation,
437 target_dist.python_platform_tag,
438 target_dist.version
439 );
440
441 let policy = target_dist.create_packaging_policy()?;
442
443 let resources = pip_download(
444 &env,
445 &*host_dist,
446 &*target_dist,
447 &policy,
448 false,
449 &["zstandard==0.16.0".to_string()],
450 )?;
451
452 assert!(!resources.is_empty());
453 let zstandard_resources = resources
454 .iter()
455 .filter(|r| r.is_in_packages(&["zstandard".to_string()]))
456 .collect::<Vec<_>>();
457 assert!(!zstandard_resources.is_empty());
458
459 let full_names = zstandard_resources
460 .iter()
461 .map(|r| r.full_name())
462 .collect::<BTreeSet<_>>();
463
464 let mut expected_names = [
465 "zstandard",
466 "zstandard.__init__.pyi",
467 "zstandard.backend_c",
468 "zstandard.backend_cffi",
469 "zstandard.py.typed",
470 "zstandard:LICENSE",
471 "zstandard:METADATA",
472 "zstandard:RECORD",
473 "zstandard:WHEEL",
474 "zstandard:top_level.txt",
475 ]
476 .iter()
477 .map(|x| x.to_string())
478 .collect::<BTreeSet<String>>();
479
480 let mut expected_extensions_count = 1;
481 let mut expected_first_extension_name = "zstandard.backend_c";
482
483 if matches!(
484 target_dist.target_triple.as_str(),
485 "i686-pc-windows-msvc" | "x86_64-pc-windows-msvc"
486 ) {
487 expected_names.insert("zstandard._cffi".to_string());
488 expected_extensions_count = 2;
489 expected_first_extension_name = "zstandard._cffi";
490 }
491
492 assert_eq!(
493 full_names, expected_names,
494 "target triple: {}",
495 target_dist.target_triple
496 );
497
498 let extensions = zstandard_resources
499 .iter()
500 .filter_map(|r| match r {
501 PythonResource::ExtensionModule(em) => Some(em),
502 _ => None,
503 })
504 .collect::<Vec<_>>();
505
506 assert_eq!(
507 extensions.len(),
508 expected_extensions_count,
509 "target triple: {}",
510 target_dist.target_triple
511 );
512 let em = extensions[0];
513 assert_eq!(em.name, expected_first_extension_name);
514 assert!(em.shared_library.is_some());
515 }
516
517 Ok(())
518 }
519
520 #[test]
521 fn test_pip_download_numpy() -> Result<()> {
522 let env = get_env()?;
523
524 for target_dist in get_all_standalone_distributions()? {
525 if target_dist.python_platform_compatibility_tag() == "none" {
526 continue;
527 }
528
529 let host_dist = get_host_distribution_from_target(&target_dist)?;
530
531 warn!(
532 "using distribution {}-{}-{}",
533 target_dist.python_implementation,
534 target_dist.python_platform_tag,
535 target_dist.version
536 );
537
538 let mut policy = target_dist.create_packaging_policy()?;
539 policy.set_file_scanner_emit_files(true);
540 policy.set_file_scanner_classify_files(true);
541
542 let res = pip_download(
543 &env,
544 &*host_dist,
545 &*target_dist,
546 &policy,
547 false,
548 &["numpy==1.22.1".to_string()],
549 );
550
551 if target_dist.python_major_minor_version() == "3.10"
553 && target_dist.python_platform_tag() == "win32"
554 {
555 assert!(res.is_err());
556 continue;
557 }
558
559 let resources = res?;
560
561 assert!(!resources.is_empty());
562
563 let extensions = resources
564 .iter()
565 .filter_map(|r| match r {
566 PythonResource::ExtensionModule(em) => Some(em),
567 _ => None,
568 })
569 .collect::<Vec<_>>();
570
571 assert!(!extensions.is_empty());
572
573 assert!(extensions
574 .iter()
575 .any(|em| em.name == "numpy.random._common"));
576 }
577
578 Ok(())
579 }
580}