diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in
index 78958249e..e9d9dc3bc 100644
--- a/packaging/macos/Info.plist.in
+++ b/packaging/macos/Info.plist.in
@@ -42,6 +42,10 @@
NSRequiresAquaSystemAppearance
+
+ NSMicrophoneUsageDescription
+ FilmCraft uses the microphone to record voice-over onto the timeline.
NSHumanReadableCopyright
Copyright © the FilmCraft authors. MIT OR Apache-2.0.
diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist
index 79fa2237b..74540670f 100644
--- a/packaging/macos/entitlements.plist
+++ b/packaging/macos/entitlements.plist
@@ -1,9 +1,13 @@
-
+
+ com.apple.security.device.audio-input
+
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 631678d84..648f217b0 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -504,3 +504,24 @@ fn main() -> ExitCode {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use std::path::Path;
+
+ fn macos_packaging(file: &str) -> String {
+ let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../packaging/macos").join(file);
+ std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))
+ }
+
+ /// Voice-over recording needs both halves on macOS: the usage string (else the permission
+ /// prompt never appears) and the hardened-runtime audio-input entitlement (else the input is
+ /// blocked). Regression test for #659.
+ #[test]
+ fn macos_bundle_may_use_the_microphone() {
+ let plist = macos_packaging("Info.plist.in");
+ assert!(plist.contains("NSMicrophoneUsageDescription"));
+ let entitlements = macos_packaging("entitlements.plist");
+ assert!(entitlements.contains("com.apple.security.device.audio-input\n "));
+ }
+}