Maintain/Build/PlistEdit.rs
1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/PlistEdit.rs
3//=============================================================================//
4// Module: PlistEdit
5//
6// Brief Description: Implements Info.plist file editing for macOS bundle
7// configuration.
8//
9// RESPONSIBILITIES:
10// ================
11//
12// Primary:
13// - Inject LSEnvironment dictionary into the Info.plist template used by Tauri
14// - Ensure the bundled .app receives dev-control environment variables when
15// launched by LaunchServices (double-click / Finder / open)
16//
17// Secondary:
18// - Provide detailed logging of changes made
19// - Return whether any modifications occurred
20//
21// ARCHITECTURAL ROLE:
22// ===================
23//
24// Position:
25// - Infrastructure/File manipulation layer
26// - Apple Property List editing functionality
27//
28// Dependencies (What this module requires):
29// - External crates: std (fs, log), plist, log::info
30// - Internal modules: Error::BuildError
31// - Traits implemented: None
32//
33// Dependents (What depends on this module):
34// - Build orchestration functions (Process)
35//
36// IMPLEMENTATION DETAILS:
37// =======================
38//
39// Design Patterns:
40// - Builder pattern (via plist serde deserialization)
41// - Functional pattern
42//
43// Performance Considerations:
44// - Complexity: O(n) - parsing and writing based on file size
45// - Memory usage patterns: In-memory plist tree
46// - Hot path optimizations: None needed
47//
48// Thread Safety:
49// - Thread-safe: No (not designed for concurrent access to files)
50// - Synchronization mechanisms used: None
51// - Interior mutability considerations: None
52//
53// Error Handling:
54// - Error types returned: BuildError (Plist, Io types)
55// - Recovery strategies: Propagate error up; Guard restores original file
56//
57// WHY THE plist CRATE:
58// ====================
59//
60// The plist crate provides a proper parse-modify-serialize pipeline, the same
61// pattern JsonEdit uses via serde_json. The output is canonically formatted
62// XML with consistent indentation and sorted keys, so the file written to
63// disk is deterministic -- identical content produces identical bytes.
64// This avoids the string-manipulation pitfalls of the prior implementation
65// (nested-dict counting, whitespace drift, duplicate LSEnvironment blocks).
66//
67//=============================================================================//
68// IMPLEMENTATION
69//=============================================================================//
70use std::{collections::BTreeMap, fs, path::Path};
71
72use log::{debug, info};
73use plist::{Dictionary, Value, XmlWriteOptions};
74
75use crate::Build::Error::Error as BuildError;
76
77/// Injects or replaces the LSEnvironment dictionary in a macOS Info.plist.
78///
79/// Tauri uses the Info.plist in the project directory as a template during
80/// bundling. When the bundled .app is launched by LaunchServices (double-click
81/// in Finder, `open`, or Spotlight), any keys under LSEnvironment are injected
82/// into the process environment before the executable starts. This is the
83/// mechanism that makes the dev-control variables (Trace, Record, etc.)
84/// available without requiring a wrapper script.
85///
86/// # Parameters
87///
88/// * `File` - Path to the Info.plist template file
89/// * `EnvVars` - Map of environment variable names to their values. These
90/// become key-value pairs in the LSEnvironment dictionary.
91///
92/// # Returns
93///
94/// Returns a `Result<bool>` indicating:
95/// - `Ok(true)` - The file was modified and saved
96/// - `Ok(false)` - No changes were needed (LSEnvironment already matches)
97/// - `Err(BuildError)` - An error occurred during modification
98///
99/// # Errors
100///
101/// * `BuildError::Io` - If the file cannot be read or written
102/// * `BuildError::Plist` - If the plist cannot be parsed or serialized
103///
104/// # Behavior
105///
106/// - Parses the plist into an in-memory tree.
107/// - Inserts or replaces the LSEnvironment dictionary with the given env vars.
108/// - Serialises back to XML with tab indentation.
109/// - Only writes the file if the content changed.
110///
111/// # Example
112///
113/// ```no_run
114/// use crate::Maintain::Source::Build::PlistEdit;
115/// let mut env = std::collections::BTreeMap::new();
116/// env.insert("Trace".to_string(), "all".to_string());
117/// env.insert("Record".to_string(), "1".to_string());
118/// let modified = PlistEdit(&path, &env)?;
119/// ```
120pub fn PlistEdit(File:&Path, EnvVars:&BTreeMap<String, String>) -> Result<bool, BuildError> {
121 debug!(target: "Build::Plist", "Attempting to modify plist: {}", File.display());
122
123 let Data = fs::read(File)?;
124
125 let mut Root:Value = plist::from_bytes(&Data)?;
126
127 let Dict = Root.as_dictionary_mut().ok_or_else(|| {
128 BuildError::Io(std::io::Error::new(
129 std::io::ErrorKind::InvalidData,
130 "Root plist is not a dictionary",
131 ))
132 })?;
133
134 // Build the new LSEnvironment dictionary as a plist Value.
135 let EnvDict = Value::Dictionary(build_env_dict(EnvVars));
136
137 // Check whether the existing value already matches exactly.
138 if let Some(Existing) = Dict.get("LSEnvironment") {
139 if *Existing == EnvDict {
140 debug!(target: "Build::Plist", "LSEnvironment already up-to-date in {}", File.display());
141
142 return Ok(false);
143 }
144 }
145
146 // Insert or replace.
147 Dict.insert("LSEnvironment".to_string(), EnvDict);
148
149 let Written = write_plist(File, &Root)?;
150
151 if Written {
152 info!(target: "Build::Plist", "Updated LSEnvironment in {}", File.display());
153 }
154
155 Ok(Written)
156}
157
158/// Constructs a plist Dictionary from environment variable key-value pairs.
159fn build_env_dict(EnvVars:&BTreeMap<String, String>) -> Dictionary {
160 let mut Dict = Dictionary::new();
161
162 for (Key, Value) in EnvVars {
163 Dict.insert(Key.clone(), Value::String(Value.clone()));
164 }
165
166 Dict
167}
168
169/// Serialises an in-memory plist tree to XML and writes it to `File`.
170///
171/// Returns `true` if the file content changed, `false` if the serialisation
172/// happens to match what is already on disk (e.g. no-op after a prior write).
173fn write_plist(File:&Path, Root:&Value) -> Result<bool, BuildError> {
174 // Use XmlWriteOptions with tab indentation to match the hand-written style.
175 let Options = XmlWriteOptions::default().indent(b'\t', 1);
176
177 let mut Buffer = Vec::new();
178
179 // Serialise with the formatter.
180 plist::to_writer_xml_with_options(&mut Buffer, &Root, &Options)?;
181
182 // Ensure a trailing newline (plist::XmlWriteOptions does not add one).
183 if !Buffer.ends_with(b"\n") {
184 Buffer.push(b'\n');
185 }
186
187 // Read the existing file and compare bytes before writing.
188 let Existing = fs::read(File).ok();
189
190 if Existing.as_ref() == Some(&Buffer) {
191 return Ok(false);
192 }
193
194 fs::write(File, &Buffer)?;
195
196 Ok(true)
197}