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