Repository: PyO3/maturin (language: python)

Issue:
Extend `--skip-auditwheel` to support check mode
              I think we can extend `--skip-auditwheel` to add some variants:

* `--skip-auditwheel`: no functional change, deprecate it in favor of `--auditwheel=skip`
* `--auditwheel=repair`: same as `--compability=XXX` w/o `--skip-auditwheel`
* `--auditwheel=skip`: new `--skip-auditwheel` equivlent option
* `--auditwheel=check`: fail the build if it requires copy DLLs or new GLIBC versions

_Originally posted by @messense in https://github.com/PyO3/maturin/issues/2038#issuecomment-2050791915_
            


Discussion / hints:
👍 I think this is an important feature. As I mentioned in #2038:

> IANAL but can't there also be legal implications to distributing a DLL vs linking to it?

I also want to opt-in to copying extra DLLs into my package, vs. having it creep in automatically via some dependency.

Fix patch:
diff --git a/Changelog.md b/Changelog.md
index 3c918413a..e4de1ed97 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -826,7 +826,7 @@ With this release, the name of this project changes from _pyo3-pack_ to _maturin
 
 ### Changed
 
- * The `--skip-auditwheel` flag has been deprecated in favor of `--manxlinux=[1|1-unchecked|2010|2010-unchecked|off]`.
+ * The `--skip-auditwheel` flag has been deprecated in favor of `--manylinux=[1|1-unchecked|2010|2010-unchecked|off]`.
  * Switched to rustls. This means the upload feature can be used from the docker container and builds of maturin itself are manylinux compliant when compiled with the musl target.
 
 ## [0.4.2] - 2018-12-15
diff --git a/guide/src/config.md b/guide/src/config.md
index 644061db2..a14975841 100644
--- a/guide/src/config.md
+++ b/guide/src/config.md
@@ -46,7 +46,9 @@ exclude = []
 bindings = "pyo3"
 # Control the platform tag on linux
 compatibility = "manylinux2014"
-# Don't check for manylinux compliance
+# auditwheel mode, possible values are repair, check and skip
+auditwheel = "repair"
+# Don't check for manylinux compliance, deprecated in favor of auditwheel = "audit"
 skip-auditwheel = false
 # Python source directory
 python-source = "src"
diff --git a/guide/src/distribution.md b/guide/src/distribution.md
index d6a52e786..ea3b1ee67 100644
--- a/guide/src/distribution.md
+++ b/guide/src/distribution.md
@@ -107,8 +107,13 @@ Options:
   -o, --out <OUT>
           The directory to store the built wheels in. Defaults to a new "wheels" directory in the project's target directory
 
-      --skip-auditwheel
-          Don't check for manylinux compliance
+      --auditwheel <AUDITWHEEL>
+          Audit wheel for manylinux compliance
+
+          Possible values:
+          - repair: Audit and repair wheel for manylinux compliance
+          - check:  Check wheel for manylinux compliance, but do not repair
+          - skip:   Don't check for manylinux compliance
 
       --zig
           For manylinux targets, use zig to ensure compliance for the chosen manylinux version
diff --git a/maturin.schema.json b/maturin.schema.json
index 28898eba3..0af4b8173 100644
--- a/maturin.schema.json
+++ b/maturin.schema.json
@@ -11,6 +11,17 @@
         "null"
       ]
     },
+    "auditwheel": {
+      "description": "Audit wheel mode",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/AuditWheelMode"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
     "bindings": {
       "description": "Bindings type",
       "type": [
@@ -194,6 +205,32 @@
     }
   },
   "definitions": {
+    "AuditWheelMode": {
+      "description": "Auditwheel mode",
+      "oneOf": [
+        {
+          "description": "Audit and repair wheel for manylinux compliance",
+          "type": "string",
+          "enum": [
+            "repair"
+          ]
+        },
+        {
+          "description": "Check wheel for manylinux compliance, but do not repair",
+          "type": "string",
+          "enum": [
+            "check"
+          ]
+        },
+        {
+          "description": "Don't check for manylinux compliance",
+          "type": "string",
+          "enum": [
+            "skip"
+          ]
+        }
+      ]
+    },
     "CargoTarget": {
       "description": "Cargo compile target",
       "type": "object",
diff --git a/src/auditwheel/audit.rs b/src/auditwheel/audit.rs
index 6f2c6e8a3..0256eef4f 100644
--- a/src/auditwheel/audit.rs
+++ b/src/auditwheel/audit.rs
@@ -9,10 +9,11 @@ use goblin::elf::{sym::STT_FUNC, Elf};
 use lddtree::Library;
 use once_cell::sync::Lazy;
 use regex::Regex;
+use serde::{Deserialize, Serialize};
 use std::collections::{HashMap, HashSet};
-use std::io;
 use std::io::Read;
 use std::path::{Path, PathBuf};
+use std::{fmt, io};
 use thiserror::Error;
 
 static IS_LIBPYTHON: Lazy<Regex> =
@@ -54,13 +55,37 @@ pub enum AuditWheelError {
     #[error("Your library is not {0} compliant because it has unsupported architecture: {1}")]
     UnsupportedArchitecture(Policy, String),
     /// This platform tag isn't defined by auditwheel yet
-    #[error("{0} compatibility policy is not defined by auditwheel yet, pass `--skip-auditwheel` to proceed anyway")]
+    #[error("{0} compatibility policy is not defined by auditwheel yet, pass `--auditwheel=skip` to proceed anyway")]
     UndefinedPolicy(String),
     /// Failed to analyze external shared library dependencies of the wheel
     #[error("Failed to analyze external shared library dependencies of the wheel")]
     DependencyAnalysisError(#[source] lddtree::Error),
 }
 
+/// Auditwheel mode
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+#[serde(rename_all = "lowercase")]
+pub enum AuditWheelMode {
+    /// Audit and repair wheel for manylinux compliance
+    #[default]
+    Repair,
+    /// Check wheel for manylinux compliance, but do not repair
+    Check,
+    /// Don't check for manylinux compliance
+    Skip,
+}
+
+impl fmt::Display for AuditWheelMode {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            AuditWheelMode::Repair => write!(f, "repair"),
+            AuditWheelMode::Check => write!(f, "check"),
+            AuditWheelMode::Skip => write!(f, "skip"),
+        }
+    }
+}
+
 #[derive(Clone, Debug)]
 pub struct VersionedLibrary {
     /// library name
diff --git a/src/build_context.rs b/src/build_context.rs
index 91faf4f0d..2e498537d 100644
--- a/src/build_context.rs
+++ b/src/build_context.rs
@@ -1,4 +1,4 @@
-use crate::auditwheel::{get_policy_and_libs, patchelf, relpath};
+use crate::auditwheel::{get_policy_and_libs, patchelf, relpath, AuditWheelMode};
 use crate::auditwheel::{PlatformTag, Policy};
 use crate::build_options::CargoOptions;
 use crate::compile::{warn_missing_py_init, CompileTarget};
@@ -172,8 +172,8 @@ pub struct BuildContext {
     pub release: bool,
     /// Strip the library for minimum file size
     pub strip: bool,
-    /// Skip checking the linked libraries for manylinux/musllinux compliance
-    pub skip_auditwheel: bool,
+    /// Checking the linked libraries for manylinux/musllinux compliance
+    pub auditwheel: AuditWheelMode,
     /// When compiling for manylinux, use zig as linker to ensure glibc version compliance
     #[cfg(feature = "zig")]
     pub zig: bool,
@@ -285,7 +285,7 @@ impl BuildContext {
         platform_tag: &[PlatformTag],
         python_interpreter: Option<&PythonInterpreter>,
     ) -> Result<(Policy, Vec<Library>)> {
-        if self.skip_auditwheel {
+        if matches!(self.auditwheel, AuditWheelMode::Skip) {
             return Ok((Policy::default(), Vec::new()));
         }
 
@@ -369,6 +369,18 @@ impl BuildContext {
             return Ok(());
         }
 
+        if matches!(self.auditwheel, AuditWheelMode::Check) {
+            eprintln!("🖨️ Your library is not manylinux/musllinux compliant because it requires copying the following libraries:");
+            for lib in ext_libs.iter().flatten() {
+                if let Some(path) = lib.realpath.as_ref() {
+                    eprintln!("    {} => {}", lib.name, path.display())
+                } else {
+                    eprintln!("    {} => not found", lib.name)
+                };
+            }
+            bail!("Can not repair the wheel because `--auditwheel=check` is specified, re-run with `--auditwheel=repair` to copy the libraries.");
+        }
+
         patchelf::verify_patchelf()?;
 
         // Put external libs to ${module_name}.libs directory
@@ -841,7 +853,7 @@ impl BuildContext {
             let _ = warn_missing_py_init(&artifact.path, extension_name);
         }
 
-        if self.editable || self.skip_auditwheel {
+        if self.editable || matches!(self.auditwheel, AuditWheelMode::Skip) {
             return Ok(artifact);
         }
         // auditwheel repair will edit the file, so we need to copy it to avoid errors in reruns
diff --git a/src/build_options.rs b/src/build_options.rs
index 2587498f8..6df3dacbd 100644
--- a/src/build_options.rs
+++ b/src/build_options.rs
@@ -1,4 +1,4 @@
-use crate::auditwheel::PlatformTag;
+use crate::auditwheel::{AuditWheelMode, PlatformTag};
 use crate::build_context::BridgeModel;
 use crate::compile::{CompileTarget, LIB_CRATE_TYPES};
 use crate::cross_compile::{find_sysconfigdata, parse_sysconfigdata};
@@ -185,8 +185,12 @@ pub struct BuildOptions {
     #[arg(short, long)]
     pub out: Option<PathBuf>,
 
+    /// Audit wheel for manylinux compliance
+    #[arg(long, conflicts_with = "skip_auditwheel")]
+    pub auditwheel: Option<AuditWheelMode>,
+
     /// Don't check for manylinux compliance
-    #[arg(long = "skip-auditwheel")]
+    #[arg(long, hide = true)]
     pub skip_auditwheel: bool,
 
     /// For manylinux targets, use zig to ensure compliance for the chosen manylinux version
@@ -611,6 +615,14 @@ impl BuildOptions {
         let strip = pyproject.map(|x| x.strip()).unwrap_or_default() || strip;
         let skip_auditwheel =
             pyproject.map(|x| x.skip_auditwheel()).unwrap_or_default() || self.skip_auditwheel;
+        let auditwheel = self
+            .auditwheel
+            .or_else(|| pyproject.and_then(|x| x.auditwheel()))
+            .unwrap_or(if skip_auditwheel {
+                AuditWheelMode::Skip
+            } else {
+                AuditWheelMode::Repair
+            });
         let platform_tags = if self.platform_tag.is_empty() {
             #[cfg(feature = "zig")]
             let use_zig = self.zig;
@@ -699,7 +711,7 @@ impl BuildOptions {
             out: wheel_dir,
             release,
             strip,
-            skip_auditwheel,
+            auditwheel,
             #[cfg(feature = "zig")]
             zig: self.zig,
             platform_tag: platform_tags,
diff --git a/src/develop.rs b/src/develop.rs
index 49fdfe1e8..6884d56f6 100644
--- a/src/develop.rs
+++ b/src/develop.rs
@@ -1,3 +1,4 @@
+use crate::auditwheel::AuditWheelMode;
 use crate::build_options::CargoOptions;
 use crate::target::detect_arch_from_python;
 use crate::BuildContext;
@@ -321,6 +322,7 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> {
         find_interpreter: false,
         bindings,
         out: Some(wheel_dir.path().to_path_buf()),
+        auditwheel: Some(AuditWheelMode::Skip),
         skip_auditwheel: false,
         #[cfg(feature = "zig")]
         zig: false,
diff --git a/src/pyproject_toml.rs b/src/pyproject_toml.rs
index 95b8d74af..8728a27f4 100644
--- a/src/pyproject_toml.rs
+++ b/src/pyproject_toml.rs
@@ -1,5 +1,6 @@
 //! A pyproject.toml as specified in PEP 517
 
+use crate::auditwheel::AuditWheelMode;
 use crate::PlatformTag;
 use anyhow::{Context, Result};
 use fs_err as fs;
@@ -142,6 +143,8 @@ pub struct ToolMaturin {
     /// Platform compatibility
     #[serde(alias = "manylinux")]
     pub compatibility: Option<PlatformTag>,
+    /// Audit wheel mode
+    pub auditwheel: Option<AuditWheelMode>,
     /// Skip audit wheel
     #[serde(default)]
     pub skip_auditwheel: bool,
@@ -254,6 +257,13 @@ impl PyProjectToml {
         self.maturin()?.compatibility
     }
 
+    /// Returns the value of `[tool.maturin.auditwheel]` in pyproject.toml
+    pub fn auditwheel(&self) -> Option<AuditWheelMode> {
+        self.maturin()
+            .map(|maturin| maturin.auditwheel)
+            .unwrap_or_default()
+    }
+
     /// Returns the value of `[tool.maturin.skip-auditwheel]` in pyproject.toml
     pub fn skip_auditwheel(&self) -> bool {
         self.maturin()


Test patch:
diff --git a/tests/cmd/build.stdout b/tests/cmd/build.stdout
index 9651fe736..358c8a1bb 100644
--- a/tests/cmd/build.stdout
+++ b/tests/cmd/build.stdout
@@ -43,8 +43,13 @@ Options:
           The directory to store the built wheels in. Defaults to a new "wheels" directory in the
           project's target directory
 
-      --skip-auditwheel
-          Don't check for manylinux compliance
+      --auditwheel <AUDITWHEEL>
+          Audit wheel for manylinux compliance
+
+          Possible values:
+          - repair: Audit and repair wheel for manylinux compliance
+          - check:  Check wheel for manylinux compliance, but do not repair
+          - skip:   Don't check for manylinux compliance
 
       --zig
           For manylinux targets, use zig to ensure compliance for the chosen manylinux version
diff --git a/tests/cmd/publish.stdout b/tests/cmd/publish.stdout
index c6c9dba83..5ffeb10aa 100644
--- a/tests/cmd/publish.stdout
+++ b/tests/cmd/publish.stdout
@@ -89,8 +89,13 @@ Options:
           The directory to store the built wheels in. Defaults to a new "wheels" directory in the
           project's target directory
 
-      --skip-auditwheel
-          Don't check for manylinux compliance
+      --auditwheel <AUDITWHEEL>
+          Audit wheel for manylinux compliance
+
+          Possible values:
+          - repair: Audit and repair wheel for manylinux compliance
+          - check:  Check wheel for manylinux compliance, but do not repair
+          - skip:   Don't check for manylinux compliance
 
       --zig
           For manylinux targets, use zig to ensure compliance for the chosen manylinux version
diff --git a/tests/common/errors.rs b/tests/common/errors.rs
index e27945bf2..0c9cfabef 100644
--- a/tests/common/errors.rs
+++ b/tests/common/errors.rs
@@ -123,7 +123,7 @@ pub fn invalid_manylinux_does_not_panic() -> Result<()> {
             .source()
             .ok_or_else(|| format_err!("{}", err))?
             .to_string();
-        assert_eq!(err_string, "manylinux_2_99 compatibility policy is not defined by auditwheel yet, pass `--skip-auditwheel` to proceed anyway");
+        assert_eq!(err_string, "manylinux_2_99 compatibility policy is not defined by auditwheel yet, pass `--auditwheel=skip` to proceed anyway");
     } else {
         bail!("Should have errored");
     }