@@ -129,7 +129,10 @@ pub(crate) fn private_executable(app: &AppHandle) -> Result<std::path::PathBuf,
129129 . join ( "loopx" ) )
130130}
131131
132- pub ( crate ) fn install_private ( app : & AppHandle ) -> Result < String , String > {
132+ pub ( crate ) fn install_private (
133+ app : & AppHandle ,
134+ selected_executable : & str ,
135+ ) -> Result < String , String > {
133136 let metadata = identity ( app) ?;
134137 let archive = app
135138 . path ( )
@@ -142,11 +145,16 @@ pub(crate) fn install_private(app: &AppHandle) -> Result<String, String> {
142145 . parent ( )
143146 . and_then ( Path :: parent)
144147 . ok_or ( "runtime_selection_unavailable" ) ?;
145- install_snapshot ( & bytes, & metadata, root) ?;
148+ install_snapshot ( & bytes, & metadata, root, selected_executable ) ?;
146149 Ok ( executable. to_string_lossy ( ) . into_owned ( ) )
147150}
148151
149- fn install_snapshot ( bytes : & [ u8 ] , metadata : & Value , private_root : & Path ) -> Result < ( ) , String > {
152+ fn install_snapshot (
153+ bytes : & [ u8 ] ,
154+ metadata : & Value ,
155+ private_root : & Path ,
156+ selected_executable : & str ,
157+ ) -> Result < ( ) , String > {
150158 let digest: String = Sha256 :: digest ( bytes)
151159 . iter ( )
152160 . map ( |byte| format ! ( "{byte:02x}" ) )
@@ -178,14 +186,19 @@ fn install_snapshot(bytes: &[u8], metadata: &Value, private_root: &Path) -> Resu
178186 . env ( "LOOPX_SHELL_PROFILE" , root. join ( "shell-profile" ) )
179187 . env ( "LOOPX_MAN_ROOT" , root. join ( "man" ) )
180188 . env ( "LOOPX_MAN_DIR" , root. join ( "man/man1" ) ) ;
181- let selected_executable = root. join ( "bin/loopx" ) . to_string_lossy ( ) . into_owned ( ) ;
182- // Preserve the working interpreter of an existing managed snapshot.
183- if let Ok ( executable) = fs:: canonicalize ( & selected_executable) {
184- if let Some ( release) = executable. parent ( ) . and_then ( Path :: parent) {
185- if let Ok ( python) = fs:: read_to_string ( release. join ( ".loopx-python" ) ) {
186- command. env ( "LOOPX_PYTHON" , python. trim ( ) ) ;
187- }
188- }
189+ let private_executable = root. join ( "bin/loopx" ) . to_string_lossy ( ) . into_owned ( ) ;
190+ // Finder may see only the system Python even when the selected managed CLI
191+ // already has a working interpreter. Reuse its installer's record for a
192+ // first private install; an existing App-owned snapshot keeps precedence.
193+ let managed_python = |executable : & str | {
194+ let executable = fs:: canonicalize ( executable) . ok ( ) ?;
195+ let release = executable. parent ( ) ?. parent ( ) ?;
196+ fs:: read_to_string ( release. join ( ".loopx-python" ) ) . ok ( )
197+ } ;
198+ if let Some ( python) =
199+ managed_python ( & private_executable) . or_else ( || managed_python ( selected_executable) )
200+ {
201+ command. env ( "LOOPX_PYTHON" , python. trim ( ) ) ;
189202 }
190203 command
191204 . current_dir ( source. path ( ) )
@@ -231,9 +244,12 @@ fn install_snapshot(bytes: &[u8], metadata: &Value, private_root: &Path) -> Resu
231244 Ok ( Some ( status) ) => {
232245 return if status. success ( ) {
233246 let installed =
234- crate :: services:: runtime_identity_for_executable ( & selected_executable ) ;
247+ crate :: services:: runtime_identity_for_executable ( & private_executable ) ;
235248 if installed. as_ref ( ) . map ( |v| & v[ "source_revision" ] )
236249 == Some ( & metadata[ "source_revision" ] )
250+ && metadata. get ( "package_version" ) . is_none_or ( |version| {
251+ installed. as_ref ( ) . map ( |v| & v[ "package_version" ] ) == Some ( version)
252+ } )
237253 {
238254 Ok ( ( ) )
239255 } else {
@@ -304,13 +320,17 @@ mod tests {
304320 & fs:: read ( bundle. join ( "runtime-source.tar.gz" ) ) . unwrap ( ) ,
305321 & metadata,
306322 & root,
323+ & default,
307324 )
308325 . unwrap ( ) ;
309326 let observed = crate :: services:: runtime_identity_for_executable (
310327 root. join ( "bin/loopx" ) . to_str ( ) . unwrap ( ) ,
311328 )
312329 . unwrap ( ) ;
313330 assert_eq ! ( observed[ "source_revision" ] , metadata[ "source_revision" ] ) ;
331+ if let Some ( version) = metadata. get ( "package_version" ) {
332+ assert_eq ! ( & observed[ "package_version" ] , version) ;
333+ }
314334 let manifest: Value = serde_json:: from_slice (
315335 & fs:: read (
316336 fs:: canonicalize ( root. join ( "bin/loopx" ) )
@@ -328,6 +348,18 @@ mod tests {
328348 manifest[ "source" ] [ "git_ref" ] . is_null( ) && manifest[ "source" ] [ "git_dirty" ] . is_null( ) ,
329349 "a bundled archive has no Git checkout; host repository facts are not its provenance"
330350 ) ;
351+ let mut wrong_version = metadata. clone ( ) ;
352+ wrong_version[ "package_version" ] = json ! ( "0.0.0-mismatch" ) ;
353+ assert_eq ! (
354+ install_snapshot(
355+ & fs:: read( bundle. join( "runtime-source.tar.gz" ) ) . unwrap( ) ,
356+ & wrong_version,
357+ & root,
358+ & default ,
359+ )
360+ . unwrap_err( ) ,
361+ "runtime_identity_mismatch"
362+ ) ;
331363 assert_eq ! (
332364 fs:: read( default ) . unwrap( ) ,
333365 original,
0 commit comments