1 //! The wheel format is (mostly) specified in PEP 427
2 
3 use crate::build_context::ProjectLayout;
4 use crate::PythonInterpreter;
5 use crate::Target;
6 use crate::{BridgeModel, Metadata21};
7 use anyhow::{anyhow, bail, Context, Result};
8 use flate2::write::GzEncoder;
9 use flate2::Compression;
10 use fs_err as fs;
11 use fs_err::File;
12 use sha2::{Digest, Sha256};
13 use std::collections::HashMap;
14 use std::ffi::OsStr;
15 #[cfg(not(target_os = "windows"))]
16 use std::fs::OpenOptions;
17 use std::io;
18 use std::io::{Read, Write};
19 #[cfg(not(target_os = "windows"))]
20 use std::os::unix::fs::OpenOptionsExt;
21 use std::path::{Path, PathBuf};
22 use std::process::{Command, Output};
23 use std::str;
24 use tempfile::{tempdir, TempDir};
25 use walkdir::WalkDir;
26 use zip::{self, ZipWriter};
27 
28 /// Allows writing the module to a wheel or add it directly to the virtualenv
29 pub trait ModuleWriter {
30     /// Adds a directory relative to the module base path
add_directory(&mut self, path: impl AsRef<Path>) -> Result<()>31     fn add_directory(&mut self, path: impl AsRef<Path>) -> Result<()>;
32 
33     /// Adds a file with bytes as content in target relative to the module base path
add_bytes(&mut self, target: impl AsRef<Path>, bytes: &[u8]) -> Result<()>34     fn add_bytes(&mut self, target: impl AsRef<Path>, bytes: &[u8]) -> Result<()> {
35         // 0o644 is the default from the zip crate
36         self.add_bytes_with_permissions(target, bytes, 0o644)
37     }
38 
39     /// Adds a file with bytes as content in target relative to the module base path while setting
40     /// the given unix permissions
add_bytes_with_permissions( &mut self, target: impl AsRef<Path>, bytes: &[u8], permissions: u32, ) -> Result<()>41     fn add_bytes_with_permissions(
42         &mut self,
43         target: impl AsRef<Path>,
44         bytes: &[u8],
45         permissions: u32,
46     ) -> Result<()>;
47 
48     /// Copies the source file the the target path relative to the module base path
add_file(&mut self, target: impl AsRef<Path>, source: impl AsRef<Path>) -> Result<()>49     fn add_file(&mut self, target: impl AsRef<Path>, source: impl AsRef<Path>) -> Result<()> {
50         let read_failed_context = format!("Failed to read {}", source.as_ref().display());
51         let mut file = File::open(source.as_ref()).context(read_failed_context.clone())?;
52         let mut buffer = Vec::new();
53         file.read_to_end(&mut buffer).context(read_failed_context)?;
54         self.add_bytes(&target, &buffer)
55             .context(format!("Failed to write to {}", target.as_ref().display()))?;
56         Ok(())
57     }
58 
59     /// Copies the source file the the target path relative to the module base path while setting
60     /// the given unix permissions
add_file_with_permissions( &mut self, target: impl AsRef<Path>, source: impl AsRef<Path>, permissions: u32, ) -> Result<()>61     fn add_file_with_permissions(
62         &mut self,
63         target: impl AsRef<Path>,
64         source: impl AsRef<Path>,
65         permissions: u32,
66     ) -> Result<()> {
67         let read_failed_context = format!("Failed to read {}", source.as_ref().display());
68         let mut file = File::open(source.as_ref()).context(read_failed_context.clone())?;
69         let mut buffer = Vec::new();
70         file.read_to_end(&mut buffer).context(read_failed_context)?;
71         self.add_bytes_with_permissions(target.as_ref(), &buffer, permissions)
72             .context(format!("Failed to write to {}", target.as_ref().display()))?;
73         Ok(())
74     }
75 }
76 
77 /// A [ModuleWriter] that adds the module somewhere in the filesystem, e.g. in a virtualenv
78 pub struct PathWriter {
79     base_path: PathBuf,
80     record: Vec<(String, String, usize)>,
81 }
82 
83 impl PathWriter {
84     /// Creates a [ModuleWriter] that adds the modul to the current virtualenv
venv(target: &Target, venv_dir: &Path, bridge: &BridgeModel) -> Result<Self>85     pub fn venv(target: &Target, venv_dir: &Path, bridge: &BridgeModel) -> Result<Self> {
86         let interpreter =
87             PythonInterpreter::check_executable(target.get_venv_python(&venv_dir), target, bridge)?
88                 .ok_or_else(|| {
89                     anyhow!("Expected `python` to be a python interpreter inside a virtualenv ಠ_ಠ")
90                 })?;
91 
92         let base_path = target.get_venv_site_package(venv_dir, &interpreter);
93 
94         Ok(PathWriter {
95             base_path,
96             record: Vec::new(),
97         })
98     }
99 
100     /// Writes the module to the given path
from_path(path: impl AsRef<Path>) -> Self101     pub fn from_path(path: impl AsRef<Path>) -> Self {
102         Self {
103             base_path: path.as_ref().to_path_buf(),
104             record: Vec::new(),
105         }
106     }
107 
108     /// Removes a directory relative to the base path if it exists.
109     ///
110     /// This is to clean up the contents of an older develop call
delete_dir(&self, relative: impl AsRef<Path>) -> Result<()>111     pub fn delete_dir(&self, relative: impl AsRef<Path>) -> Result<()> {
112         let absolute = self.base_path.join(relative);
113         if absolute.exists() {
114             fs::remove_dir_all(&absolute)
115                 .context(format!("Failed to remove {}", absolute.display()))?;
116         }
117 
118         Ok(())
119     }
120 
121     /// Writes the RECORD file after everything else has been written
write_record(self, metadata21: &Metadata21) -> Result<()>122     pub fn write_record(self, metadata21: &Metadata21) -> Result<()> {
123         let record_file = self
124             .base_path
125             .join(metadata21.get_dist_info_dir())
126             .join("RECORD");
127         let mut buffer = File::create(&record_file).context(format!(
128             "Failed to create a file at {}",
129             record_file.display()
130         ))?;
131 
132         for (filename, hash, len) in self.record {
133             buffer
134                 .write_all(format!("{},sha256={},{}\n", filename, hash, len).as_bytes())
135                 .context(format!(
136                     "Failed to write to file at {}",
137                     record_file.display()
138                 ))?;
139         }
140         // Write the record for the RECORD file itself
141         buffer
142             .write_all(format!("{},,\n", record_file.display()).as_bytes())
143             .context(format!(
144                 "Failed to write to file at {}",
145                 record_file.display()
146             ))?;
147 
148         Ok(())
149     }
150 }
151 
152 impl ModuleWriter for PathWriter {
add_directory(&mut self, path: impl AsRef<Path>) -> Result<()>153     fn add_directory(&mut self, path: impl AsRef<Path>) -> Result<()> {
154         fs::create_dir_all(self.base_path.join(path))?;
155         Ok(())
156     }
157 
add_bytes_with_permissions( &mut self, target: impl AsRef<Path>, bytes: &[u8], _permissions: u32, ) -> Result<()>158     fn add_bytes_with_permissions(
159         &mut self,
160         target: impl AsRef<Path>,
161         bytes: &[u8],
162         _permissions: u32,
163     ) -> Result<()> {
164         let path = self.base_path.join(&target);
165 
166         // We only need to set the executable bit on unix
167         let mut file = {
168             #[cfg(not(target_os = "windows"))]
169             {
170                 OpenOptions::new()
171                     .create(true)
172                     .write(true)
173                     .truncate(true)
174                     .mode(_permissions)
175                     .open(&path)
176             }
177             #[cfg(target_os = "windows")]
178             {
179                 File::create(&path)
180             }
181         }
182         .context(format!("Failed to create a file at {}", path.display()))?;
183 
184         file.write_all(bytes)
185             .context(format!("Failed to write to file at {}", path.display()))?;
186 
187         let hash = base64::encode_config(&Sha256::digest(bytes), base64::URL_SAFE_NO_PAD);
188         self.record.push((
189             target.as_ref().to_str().unwrap().to_owned(),
190             hash,
191             bytes.len(),
192         ));
193 
194         Ok(())
195     }
196 }
197 
198 /// A glorified zip builder, mostly useful for writing the record file of a wheel
199 pub struct WheelWriter {
200     zip: ZipWriter<File>,
201     record: Vec<(String, String, usize)>,
202     record_file: PathBuf,
203     wheel_path: PathBuf,
204 }
205 
206 impl ModuleWriter for WheelWriter {
add_directory(&mut self, _path: impl AsRef<Path>) -> Result<()>207     fn add_directory(&mut self, _path: impl AsRef<Path>) -> Result<()> {
208         Ok(()) // We don't need to create directories in zip archives
209     }
210 
add_bytes_with_permissions( &mut self, target: impl AsRef<Path>, bytes: &[u8], permissions: u32, ) -> Result<()>211     fn add_bytes_with_permissions(
212         &mut self,
213         target: impl AsRef<Path>,
214         bytes: &[u8],
215         permissions: u32,
216     ) -> Result<()> {
217         // The zip standard mandates using unix style paths
218         let target = target.as_ref().to_str().unwrap().replace("\\", "/");
219 
220         // Unlike users which can use the develop subcommand, the tests have to go through
221         // packing a zip which pip than has to unpack. This makes this 2-3 times faster
222         let compression_method = if cfg!(feature = "faster-tests") {
223             zip::CompressionMethod::Stored
224         } else {
225             zip::CompressionMethod::Deflated
226         };
227         let options = zip::write::FileOptions::default()
228             .unix_permissions(permissions)
229             .compression_method(compression_method);
230         self.zip.start_file(target.clone(), options)?;
231         self.zip.write_all(bytes)?;
232 
233         let hash = base64::encode_config(&Sha256::digest(bytes), base64::URL_SAFE_NO_PAD);
234         self.record.push((target, hash, bytes.len()));
235 
236         Ok(())
237     }
238 }
239 
240 impl WheelWriter {
241     /// Create a new wheel file which can be subsequently expanded
242     ///
243     /// Adds the .dist-info directory and the METADATA file in it
244     pub fn new(
245         tag: &str,
246         wheel_dir: &Path,
247         metadata21: &Metadata21,
248         tags: &[String],
249     ) -> Result<WheelWriter> {
250         let wheel_path = wheel_dir.join(format!(
251             "{}-{}-{}.whl",
252             metadata21.get_distribution_escaped(),
253             metadata21.get_version_escaped(),
254             tag
255         ));
256 
257         let file = File::create(&wheel_path)?;
258 
259         let mut builder = WheelWriter {
260             zip: ZipWriter::new(file),
261             record: Vec::new(),
262             record_file: metadata21.get_dist_info_dir().join("RECORD"),
263             wheel_path,
264         };
265 
266         write_dist_info(&mut builder, metadata21, tags)?;
267 
268         Ok(builder)
269     }
270 
271     /// Creates the record file and finishes the zip
272     pub fn finish(mut self) -> Result<PathBuf, io::Error> {
273         let compression_method = if cfg!(feature = "faster-tests") {
274             zip::CompressionMethod::Stored
275         } else {
276             zip::CompressionMethod::Deflated
277         };
278         let options = zip::write::FileOptions::default().compression_method(compression_method);
279         let record_filename = self.record_file.to_str().unwrap().replace("\\", "/");
280         self.zip.start_file(&record_filename, options)?;
281         for (filename, hash, len) in self.record {
282             self.zip
283                 .write_all(format!("{},sha256={},{}\n", filename, hash, len).as_bytes())?;
284         }
285         // Write the record for the RECORD file itself
286         self.zip
287             .write_all(format!("{},,\n", record_filename).as_bytes())?;
288 
289         self.zip.finish()?;
290         Ok(self.wheel_path)
291     }
292 }
293 
294 /// Creates a .tar.gz archive containing the source distribution
295 pub struct SDistWriter {
296     tar: tar::Builder<GzEncoder<File>>,
297     path: PathBuf,
298 }
299 
300 impl ModuleWriter for SDistWriter {
301     fn add_directory(&mut self, _path: impl AsRef<Path>) -> Result<()> {
302         Ok(())
303     }
304 
305     fn add_bytes_with_permissions(
306         &mut self,
307         target: impl AsRef<Path>,
308         bytes: &[u8],
309         permissions: u32,
310     ) -> Result<()> {
311         let mut header = tar::Header::new_gnu();
312         header.set_size(bytes.len() as u64);
313         header.set_mode(permissions);
314         header.set_cksum();
315         self.tar
316             .append_data(&mut header, &target, bytes)
317             .context(format!(
318                 "Failed to add {} bytes to sdist as {}",
319                 bytes.len(),
320                 target.as_ref().display()
321             ))?;
322         Ok(())
323     }
324 
325     fn add_file(&mut self, target: impl AsRef<Path>, source: impl AsRef<Path>) -> Result<()> {
326         if source.as_ref() == self.path {
327             bail!(
328             "Attempting to include the sdist output tarball {} into itself! Check 'cargo package --list' output.",
329             source.as_ref().display()
330             );
331         }
332 
333         self.tar
334             .append_path_with_name(&source, &target)
335             .context(format!(
336                 "Failed to add file from {} to sdist as {}",
337                 source.as_ref().display(),
338                 target.as_ref().display(),
339             ))?;
340         Ok(())
341     }
342 }
343 
344 impl SDistWriter {
345     /// Create a source distribution .tar.gz which can be subsequently expanded
346     pub fn new(wheel_dir: impl AsRef<Path>, metadata21: &Metadata21) -> Result<Self, io::Error> {
347         let path = wheel_dir.as_ref().join(format!(
348             "{}-{}.tar.gz",
349             &metadata21.get_distribution_escaped(),
350             &metadata21.get_version_escaped()
351         ));
352 
353         let tar_gz = File::create(&path)?;
354         let enc = GzEncoder::new(tar_gz, Compression::default());
355         let tar = tar::Builder::new(enc);
356 
357         Ok(Self { tar, path })
358     }
359 
360     /// Finished the .tar.gz archive
361     pub fn finish(mut self) -> Result<PathBuf, io::Error> {
362         self.tar.finish()?;
363         Ok(self.path)
364     }
365 }
366 
367 fn wheel_file(tags: &[String]) -> String {
368     let mut wheel_file = format!(
369         "Wheel-Version: 1.0
370 Generator: {name} ({version})
371 Root-Is-Purelib: false
372 ",
373         name = env!("CARGO_PKG_NAME"),
374         version = env!("CARGO_PKG_VERSION"),
375     );
376 
377     for tag in tags {
378         wheel_file += &format!("Tag: {}\n", tag);
379     }
380 
381     wheel_file
382 }
383 
384 /// https://packaging.python.org/specifications/entry-points/
385 fn entry_points_txt(
386     entry_type: &str,
387     entrypoints: &HashMap<String, String, impl std::hash::BuildHasher>,
388 ) -> String {
389     entrypoints
390         .iter()
391         .fold(format!("[{}]\n", entry_type), |text, (k, v)| {
392             text + k + "=" + v + "\n"
393         })
394 }
395 
396 /// Glue code that exposes `lib`.
397 fn cffi_init_file() -> &'static str {
398     r#"__all__ = ["lib", "ffi"]
399 
400 import os
401 from .ffi import ffi
402 
403 lib = ffi.dlopen(os.path.join(os.path.dirname(__file__), 'native.so'))
404 del os
405 "#
406 }
407 
408 /// Wraps some boilerplate around error handling when calling python
409 fn call_python<I, S>(python: &Path, args: I) -> Result<Output>
410 where
411     I: IntoIterator<Item = S>,
412     S: AsRef<OsStr>,
413 {
414     Command::new(&python)
415         .args(args)
416         .output()
417         .context(format!("Failed to run python at {:?}", &python))
418 }
419 
420 /// Checks if user has provided their own header at `target/header.h`, otherwise
421 /// we run cbindgen to generate one.
422 fn cffi_header(crate_dir: &Path, tempdir: &TempDir) -> Result<PathBuf> {
423     let maybe_header = crate_dir.join("target").join("header.h");
424 
425     if maybe_header.is_file() {
426         println!("�� Using the existing header at {}", maybe_header.display());
427         Ok(maybe_header)
428     } else {
429         if crate_dir.join("cbindgen.toml").is_file() {
430             println!(
431                 "�� Using the existing cbindgen.toml configuration. \n\
432                  �� Enforcing the following settings: \n   \
433                  - language = \"C\" \n   \
434                  - no_includes = true \n   \
435                  - no include_guard \t (directives are not yet supported) \n   \
436                  - no defines       \t (directives are not yet supported)"
437             );
438         }
439 
440         let mut config = cbindgen::Config::from_root_or_default(&crate_dir);
441         config.defines = HashMap::new();
442         config.include_guard = None;
443 
444         let bindings = cbindgen::Builder::new()
445             .with_config(config)
446             .with_crate(crate_dir)
447             .with_language(cbindgen::Language::C)
448             .with_no_includes()
449             .generate()
450             .context("Failed to run cbindgen")?;
451 
452         let header = tempdir.as_ref().join("header.h");
453         bindings.write_to_file(&header);
454         Ok(header)
455     }
456 }
457 
458 /// Returns the content of what will become ffi.py by invoking cbindgen and cffi
459 ///
460 /// Checks if user has provided their own header at `target/header.h`, otherwise
461 /// we run cbindgen to generate one. Installs cffi if it's missing and we're inside a virtualenv
462 ///
463 /// We're using the cffi recompiler, which reads the header, translates them into instructions
464 /// how to load the shared library without the header and then writes those instructions to a
465 /// file called `ffi.py`. This `ffi.py` will expose an object called `ffi`. This object is used
466 /// in `__init__.py` to load the shared library into a module called `lib`.
generate_cffi_declarations(crate_dir: &Path, python: &Path) -> Result<String>467 pub fn generate_cffi_declarations(crate_dir: &Path, python: &Path) -> Result<String> {
468     let tempdir = tempdir()?;
469     let header = cffi_header(crate_dir, &tempdir)?;
470 
471     let ffi_py = tempdir.as_ref().join("ffi.py");
472 
473     // Using raw strings is important because on windows there are path like
474     // `C:\Users\JohnDoe\AppData\Local\TEmpl\pip-wheel-asdf1234` where the \U
475     // would otherwise be a broken unicode escape sequence
476     let cffi_invocation = format!(
477         r#"
478 import cffi
479 from cffi import recompiler
480 
481 ffi = cffi.FFI()
482 with open(r"{header}") as header:
483     ffi.cdef(header.read())
484 recompiler.make_py_source(ffi, "ffi", r"{ffi_py}")
485 "#,
486         ffi_py = ffi_py.display(),
487         header = header.display(),
488     );
489 
490     let output = call_python(python, &["-c", &cffi_invocation])?;
491     let install_cffi = if !output.status.success() {
492         // First, check whether the error was cffi not being installed
493         let last_line = str::from_utf8(&output.stderr)?.lines().last().unwrap_or("");
494         if last_line == "ModuleNotFoundError: No module named 'cffi'" {
495             // Then check whether we're running in a virtualenv.
496             // We don't want to modify any global environment
497             // https://stackoverflow.com/a/42580137/3549270
498             let output = call_python(
499                 python,
500                 &["-c", "import sys\nprint(sys.base_prefix != sys.prefix)"],
501             )?;
502 
503             match str::from_utf8(&output.stdout)?.trim() {
504                 "True" => true,
505                 "False" => false,
506                 _ => {
507                     println!(
508                         "⚠  Failed to determine whether python at {:?} is running inside a virtualenv",
509                         &python
510                     );
511                     false
512                 }
513             }
514         } else {
515             false
516         }
517     } else {
518         false
519     };
520 
521     // If there was success or an error that was not missing cffi, return here
522     if !install_cffi {
523         return handle_cffi_call_result(python, tempdir, &ffi_py, &output);
524     }
525 
526     println!("⚠  cffi not found. Trying to install it");
527     // Call pip through python to don't do the wrong thing when python and pip
528     // are coming from different environments
529     let output = call_python(python, &["-m", "pip", "install", "cffi"])?;
530     if !output.status.success() {
531         bail!(
532             "Installing cffi with `{:?} -m pip install cffi` failed: {}\n--- Stdout:\n{}\n--- Stderr:\n{}\n---\nPlease install cffi yourself.",
533             &python,
534             output.status,
535             str::from_utf8(&output.stdout)?,
536             str::from_utf8(&output.stderr)?
537         );
538     }
539     println!("�� Installed cffi");
540 
541     // Try again
542     let output = call_python(python, &["-c", &cffi_invocation])?;
543     handle_cffi_call_result(python, tempdir, &ffi_py, &output)
544 }
545 
546 /// Extracted into a function because this is needed twice
handle_cffi_call_result( python: &Path, tempdir: TempDir, ffi_py: &Path, output: &Output, ) -> Result<String>547 fn handle_cffi_call_result(
548     python: &Path,
549     tempdir: TempDir,
550     ffi_py: &Path,
551     output: &Output,
552 ) -> Result<String> {
553     if !output.status.success() {
554         bail!(
555             "Failed to generate cffi declarations using {}: {}\n--- Stdout:\n{}\n--- Stderr:\n{}",
556             python.display(),
557             output.status,
558             str::from_utf8(&output.stdout)?,
559             str::from_utf8(&output.stderr)?,
560         );
561     } else {
562         // Don't swallow warnings
563         std::io::stderr().write_all(&output.stderr)?;
564 
565         let ffi_py_content = fs::read_to_string(&ffi_py)?;
566         tempdir.close()?;
567         Ok(ffi_py_content)
568     }
569 }
570 
571 /// Copies the shared library into the module, which is the only extra file needed with bindings
write_bindings_module( writer: &mut impl ModuleWriter, project_layout: &ProjectLayout, module_name: &str, artifact: &Path, python_interpreter: Option<&PythonInterpreter>, target: &Target, develop: bool, ) -> Result<()>572 pub fn write_bindings_module(
573     writer: &mut impl ModuleWriter,
574     project_layout: &ProjectLayout,
575     module_name: &str,
576     artifact: &Path,
577     python_interpreter: Option<&PythonInterpreter>,
578     target: &Target,
579     develop: bool,
580 ) -> Result<()> {
581     let ext_name = project_layout.extension_name();
582     let so_filename = match python_interpreter {
583         Some(python_interpreter) => python_interpreter.get_library_name(ext_name),
584         // abi3
585         None => {
586             if target.is_unix() {
587                 format!("{base}.abi3.so", base = ext_name)
588             } else {
589                 // Apparently there is no tag for abi3 on windows
590                 format!("{base}.pyd", base = ext_name)
591             }
592         }
593     };
594 
595     match project_layout {
596         ProjectLayout::Mixed {
597             ref python_module,
598             ref rust_module,
599             ..
600         } => {
601             write_python_part(writer, python_module, &module_name)
602                 .context("Failed to add the python module to the package")?;
603 
604             if develop {
605                 let target = rust_module.join(&so_filename);
606                 fs::copy(&artifact, &target).context(format!(
607                     "Failed to copy {} to {}",
608                     artifact.display(),
609                     target.display()
610                 ))?;
611             }
612 
613             let relative = rust_module.strip_prefix(python_module.parent().unwrap())?;
614             writer.add_file_with_permissions(relative.join(&so_filename), &artifact, 0o755)?;
615         }
616         ProjectLayout::PureRust {
617             ref rust_module, ..
618         } => {
619             let module = PathBuf::from(module_name);
620             writer.add_directory(&module)?;
621             // Reexport the shared library as if it were the top level module
622             writer.add_bytes(
623                 &module.join("__init__.py"),
624                 format!("from .{} import *\n", module_name).as_bytes(),
625             )?;
626             let type_stub = rust_module.join(format!("{}.pyi", module_name));
627             if type_stub.exists() {
628                 writer.add_bytes(
629                     &module.join("__init__.pyi"),
630                     &fs_err::read(type_stub).context("Failed to read type stub file")?,
631                 )?;
632                 writer.add_bytes(&module.join("py.typed"), b"")?;
633                 println!("�� Found type stub file at {}.pyi", module_name);
634             }
635             writer.add_file_with_permissions(&module.join(so_filename), &artifact, 0o755)?;
636         }
637     }
638 
639     Ok(())
640 }
641 
642 /// Creates the cffi module with the shared library, the cffi declarations and the cffi loader
write_cffi_module( writer: &mut impl ModuleWriter, project_layout: &ProjectLayout, crate_dir: &Path, module_name: &str, artifact: &Path, python: &Path, develop: bool, ) -> Result<()>643 pub fn write_cffi_module(
644     writer: &mut impl ModuleWriter,
645     project_layout: &ProjectLayout,
646     crate_dir: &Path,
647     module_name: &str,
648     artifact: &Path,
649     python: &Path,
650     develop: bool,
651 ) -> Result<()> {
652     let cffi_declarations = generate_cffi_declarations(crate_dir, python)?;
653 
654     let module;
655 
656     match project_layout {
657         ProjectLayout::Mixed {
658             ref python_module,
659             ref rust_module,
660             ref extension_name,
661         } => {
662             write_python_part(writer, python_module, &module_name)
663                 .context("Failed to add the python module to the package")?;
664 
665             if develop {
666                 let base_path = python_module.join(&module_name);
667                 fs::create_dir_all(&base_path)?;
668                 let target = base_path.join("native.so");
669                 fs::copy(&artifact, &target).context(format!(
670                     "Failed to copy {} to {}",
671                     artifact.display(),
672                     target.display()
673                 ))?;
674                 File::create(base_path.join("__init__.py"))?
675                     .write_all(cffi_init_file().as_bytes())?;
676                 File::create(base_path.join("ffi.py"))?.write_all(cffi_declarations.as_bytes())?;
677             }
678 
679             let relative = rust_module.strip_prefix(python_module.parent().unwrap())?;
680             module = relative.join(extension_name);
681             writer.add_directory(&module)?;
682         }
683         ProjectLayout::PureRust {
684             ref rust_module, ..
685         } => {
686             module = PathBuf::from(module_name);
687             writer.add_directory(&module)?;
688             let type_stub = rust_module.join(format!("{}.pyi", module_name));
689             if type_stub.exists() {
690                 writer.add_bytes(
691                     &module.join("__init__.pyi"),
692                     &fs_err::read(type_stub).context("Failed to read type stub file")?,
693                 )?;
694                 writer.add_bytes(&module.join("py.typed"), b"")?;
695                 println!("�� Found type stub file at {}.pyi", module_name);
696             }
697         }
698     };
699 
700     writer.add_bytes(&module.join("__init__.py"), cffi_init_file().as_bytes())?;
701     writer.add_bytes(&module.join("ffi.py"), cffi_declarations.as_bytes())?;
702     writer.add_file_with_permissions(&module.join("native.so"), &artifact, 0o755)?;
703 
704     Ok(())
705 }
706 
707 /// Adds a data directory with a scripts directory with the binary inside it
write_bin( writer: &mut impl ModuleWriter, artifact: &Path, metadata: &Metadata21, bin_name: &OsStr, ) -> Result<()>708 pub fn write_bin(
709     writer: &mut impl ModuleWriter,
710     artifact: &Path,
711     metadata: &Metadata21,
712     bin_name: &OsStr,
713 ) -> Result<()> {
714     let data_dir = PathBuf::from(format!(
715         "{}-{}.data",
716         &metadata.get_distribution_escaped(),
717         &metadata.version
718     ))
719     .join("scripts");
720 
721     writer.add_directory(&data_dir)?;
722 
723     // We can't use add_file since we need to mark the file as executable
724     writer.add_file_with_permissions(&data_dir.join(bin_name), artifact, 0o755)?;
725     Ok(())
726 }
727 
728 /// Adds the python part of a mixed project to the writer,
729 /// excluding older versions of the native library or generated cffi declarations
write_python_part( writer: &mut impl ModuleWriter, python_module: impl AsRef<Path>, module_name: impl AsRef<Path>, ) -> Result<()>730 pub fn write_python_part(
731     writer: &mut impl ModuleWriter,
732     python_module: impl AsRef<Path>,
733     module_name: impl AsRef<Path>,
734 ) -> Result<()> {
735     for absolute in WalkDir::new(&python_module) {
736         let absolute = absolute?.into_path();
737 
738         let relative = absolute.strip_prefix(python_module.as_ref().parent().unwrap())?;
739 
740         // Ignore the cffi folder from develop, if any
741         if relative.starts_with(module_name.as_ref().join(&module_name)) {
742             continue;
743         }
744 
745         if absolute.is_dir() {
746             writer.add_directory(relative)?;
747         } else {
748             // Ignore native libraries from develop, if any
749             if let Some(extension) = relative.extension() {
750                 if extension.to_string_lossy() == "so" {
751                     continue;
752                 }
753             }
754             writer
755                 .add_file(relative, &absolute)
756                 .context(format!("File to add file from {}", absolute.display()))?;
757         }
758     }
759 
760     Ok(())
761 }
762 
763 /// Creates the .dist-info directory and fills it with all metadata files except RECORD
write_dist_info( writer: &mut impl ModuleWriter, metadata21: &Metadata21, tags: &[String], ) -> Result<()>764 pub fn write_dist_info(
765     writer: &mut impl ModuleWriter,
766     metadata21: &Metadata21,
767     tags: &[String],
768 ) -> Result<()> {
769     let dist_info_dir = metadata21.get_dist_info_dir();
770 
771     writer.add_directory(&dist_info_dir)?;
772 
773     writer.add_bytes(
774         &dist_info_dir.join("METADATA"),
775         metadata21.to_file_contents().as_bytes(),
776     )?;
777 
778     writer.add_bytes(&dist_info_dir.join("WHEEL"), wheel_file(tags).as_bytes())?;
779 
780     let mut entry_points = String::new();
781     if !metadata21.scripts.is_empty() {
782         entry_points.push_str(&entry_points_txt("console_scripts", &metadata21.scripts));
783     }
784     if !metadata21.gui_scripts.is_empty() {
785         entry_points.push_str(&entry_points_txt("gui_scripts", &metadata21.gui_scripts));
786     }
787     for (entry_type, scripts) in &metadata21.entry_points {
788         entry_points.push_str(&entry_points_txt(entry_type, scripts));
789     }
790     if !entry_points.is_empty() {
791         writer.add_bytes(
792             &dist_info_dir.join("entry_points.txt"),
793             entry_points.as_bytes(),
794         )?;
795     }
796 
797     Ok(())
798 }
799