2323import subprocess
2424import sys
2525import sysconfig
26+ import time
2627import urllib .request
2728from importlib import resources
2829from typing import Any , Dict , List , Optional
@@ -311,6 +312,9 @@ def _read_requirements(requirements_path: str) -> list[str]:
311312 return result
312313
313314
315+ ANDROID_PACKAGE_ID : str = "com.pythonnative.android_template"
316+ HOT_RELOAD_DEV_ROOT : str = "pythonnative_dev"
317+
314318ANDROID_LOGCAT_FILTERS : list [str ] = [
315319 "python.stdout:V" ,
316320 "python.stderr:V" ,
@@ -366,6 +370,125 @@ def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
366370 proc .kill ()
367371
368372
373+ def _hot_reload_manifest_payload (
374+ changed_files : List [str ],
375+ project_dir : str ,
376+ * ,
377+ version : Optional [str ] = None ,
378+ ) -> Dict [str , Any ]:
379+ """Build the reload manifest consumed by the running app."""
380+ from pythonnative .hot_reload import ModuleReloader
381+
382+ rel_files = sorted (os .path .relpath (path , project_dir ) for path in changed_files )
383+ return {
384+ "version" : version or str (time .time_ns ()),
385+ "files" : rel_files ,
386+ "modules" : ModuleReloader .modules_from_files (rel_files ),
387+ }
388+
389+
390+ def _write_hot_reload_manifest (changed_files : List [str ], project_dir : str , build_dir : str ) -> str :
391+ """Write a local hot-reload manifest and return its path."""
392+ manifest_dir = os .path .join (build_dir , "hot_reload" )
393+ os .makedirs (manifest_dir , exist_ok = True )
394+ manifest_path = os .path .join (manifest_dir , "reload.json" )
395+ with open (manifest_path , "w" , encoding = "utf-8" ) as f :
396+ json .dump (_hot_reload_manifest_payload (changed_files , project_dir ), f )
397+ return manifest_path
398+
399+
400+ def _android_hot_reload_dest (rel_path : str ) -> str :
401+ """Return a `run-as` relative destination for an app source file."""
402+ return os .path .join ("files" , HOT_RELOAD_DEV_ROOT , rel_path )
403+
404+
405+ def _push_android_hot_reload_file (local_path : str , rel_path : str ) -> bool :
406+ """Push one file into the Android app's writable hot-reload overlay."""
407+ tmp_path = f"/data/local/tmp/pythonnative-hot-reload-{ os .getpid ()} -{ os .path .basename (local_path )} "
408+ dest_path = _android_hot_reload_dest (rel_path )
409+ dest_dir = os .path .dirname (dest_path )
410+ push = subprocess .run (["adb" , "push" , local_path , tmp_path ], check = False , capture_output = True )
411+ if push .returncode != 0 :
412+ return False
413+ subprocess .run (
414+ ["adb" , "shell" , "run-as" , ANDROID_PACKAGE_ID , "mkdir" , "-p" , dest_dir ],
415+ check = False ,
416+ capture_output = True ,
417+ )
418+ copy = subprocess .run (
419+ ["adb" , "shell" , "run-as" , ANDROID_PACKAGE_ID , "cp" , tmp_path , dest_path ],
420+ check = False ,
421+ capture_output = True ,
422+ )
423+ subprocess .run (["adb" , "shell" , "rm" , "-f" , tmp_path ], check = False , capture_output = True )
424+ return copy .returncode == 0
425+
426+
427+ def _ios_data_container () -> Optional [str ]:
428+ """Return the booted simulator's app data container, if available."""
429+ try :
430+ result = subprocess .run (
431+ ["xcrun" , "simctl" , "get_app_container" , "booted" , IOS_BUNDLE_ID , "data" ],
432+ check = False ,
433+ capture_output = True ,
434+ text = True ,
435+ )
436+ except FileNotFoundError :
437+ return None
438+ if result .returncode != 0 :
439+ return None
440+ container = result .stdout .strip ()
441+ return container or None
442+
443+
444+ def _push_ios_hot_reload_file (local_path : str , rel_path : str ) -> bool :
445+ """Copy one file into the booted iOS Simulator's hot-reload overlay."""
446+ container = _ios_data_container ()
447+ if container is None :
448+ return False
449+ dest_path = os .path .join (container , "Documents" , HOT_RELOAD_DEV_ROOT , rel_path )
450+ os .makedirs (os .path .dirname (dest_path ), exist_ok = True )
451+ shutil .copy2 (local_path , dest_path )
452+ return True
453+
454+
455+ def _clear_android_hot_reload_overlay () -> bool :
456+ """Remove stale Android hot-reload files before launching."""
457+ result = subprocess .run (
458+ ["adb" , "shell" , "run-as" , ANDROID_PACKAGE_ID , "rm" , "-rf" , f"files/{ HOT_RELOAD_DEV_ROOT } " ],
459+ check = False ,
460+ capture_output = True ,
461+ )
462+ return result .returncode == 0
463+
464+
465+ def _clear_ios_hot_reload_overlay () -> bool :
466+ """Remove stale iOS Simulator hot-reload files before launching."""
467+ container = _ios_data_container ()
468+ if container is None :
469+ return False
470+ shutil .rmtree (os .path .join (container , "Documents" , HOT_RELOAD_DEV_ROOT ), ignore_errors = True )
471+ return True
472+
473+
474+ def _clear_hot_reload_overlay (platform : str ) -> bool :
475+ """Remove stale hot-reload overlay files for `platform`."""
476+ if platform == "android" :
477+ return _clear_android_hot_reload_overlay ()
478+ if platform == "ios" :
479+ return _clear_ios_hot_reload_overlay ()
480+ return False
481+
482+
483+ def _push_hot_reload_file (platform : str , local_path : str , rel_path : str ) -> bool :
484+ """Push a changed source file to the running app."""
485+ if platform == "android" :
486+ return _push_android_hot_reload_file (local_path , rel_path )
487+ if platform == "ios" :
488+ return _push_ios_hot_reload_file (local_path , rel_path )
489+ return False
490+
491+
369492def run_project (args : argparse .Namespace ) -> None :
370493 """Build and run the project on the requested platform.
371494
@@ -491,6 +614,8 @@ def run_project(args: argparse.Namespace) -> None:
491614 pass
492615 subprocess .run (["./gradlew" , "installDebug" ], check = True , env = env )
493616
617+ _clear_hot_reload_overlay (platform )
618+
494619 # Run the Android app
495620 # Assumes that the package name of your app is "com.example.myapp" and the main activity is "MainActivity"
496621 # Replace "com.example.myapp" and ".MainActivity" with your actual package name and main activity
@@ -501,7 +626,7 @@ def run_project(args: argparse.Namespace) -> None:
501626 "am" ,
502627 "start" ,
503628 "-n" ,
504- "com.pythonnative.android_template /.MainActivity" ,
629+ f" { ANDROID_PACKAGE_ID } /.MainActivity" ,
505630 ],
506631 check = True ,
507632 )
@@ -792,6 +917,7 @@ def run_project(args: argparse.Namespace) -> None:
792917 subprocess .run (["xcrun" , "simctl" , "boot" , udid ], check = False , capture_output = True )
793918 # Install
794919 subprocess .run (["xcrun" , "simctl" , "install" , udid , app_path ], check = False )
920+ _clear_hot_reload_overlay (platform )
795921 if show_logs and not hot_reload :
796922 # Attach the app's stdout/stderr to this terminal so Python
797923 # print() calls and exceptions are visible. SIMCTL_CHILD_*
@@ -850,19 +976,25 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs:
850976 build_dir: Absolute path to the staged build directory.
851977 show_logs: Whether to stream device logs in parallel.
852978 """
853- from .hot_reload import FileWatcher
979+ from .. hot_reload import FileWatcher
854980
855981 app_dir = os .path .join (project_dir , "app" )
856982
857983 def on_change (changed_files : List [str ]) -> None :
984+ pushed : List [str ] = []
858985 for fpath in changed_files :
859986 rel = os .path .relpath (fpath , project_dir )
860987 print (f"[hot-reload] Changed: { rel } " )
861- if platform == "android" :
862- dest = f"/data/data/com.pythonnative.android_template/files/{ rel } "
863- subprocess .run (["adb" , "push" , fpath , dest ], check = False , capture_output = True )
864- elif platform == "ios" :
865- pass # simctl file push would go here
988+ if _push_hot_reload_file (platform , fpath , rel ):
989+ pushed .append (fpath )
990+ else :
991+ print (f"[hot-reload] Failed to push { rel } " )
992+ if pushed :
993+ manifest = _write_hot_reload_manifest (pushed , project_dir , build_dir )
994+ if _push_hot_reload_file (platform , manifest , "reload.json" ):
995+ print (f"[hot-reload] Signaled reload for { len (pushed )} file(s)." )
996+ else :
997+ print ("[hot-reload] Failed to signal reload; app will not refresh automatically." )
866998
867999 print ("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop." )
8681000 watcher = FileWatcher (app_dir , on_change , interval = 1.0 )
0 commit comments