3838import inspect
3939import io
4040import os
41+ import pickle
4142import platform
4243import re
4344import shlex
@@ -2690,15 +2691,17 @@ def run_if( command, out, *prerequisites, caller=1):
26902691
26912692 Args:
26922693 command:
2693- The command to run. We write this into a file <out>.cmd so that we
2694- know to run a command if the command itself has changed.
2694+ The command to run. We write this and a hash of argv[0] into a file
2695+ <out>.cmd so that we know to run a command if the command itself
2696+ has changed.
26952697 out:
26962698 Path of the output file.
26972699
26982700 prerequisites:
26992701 List of prerequisite paths or true/false/None items. If an item
27002702 is None it is ignored, otherwise if an item is not a string we
2701- immediately return it cast to a bool.
2703+ immediately return it cast to a bool. We recurse into directories,
2704+ effectively using the newest file in the directory.
27022705
27032706 Returns:
27042707 True if we ran the command, otherwise None.
@@ -2757,25 +2760,75 @@ def run_if( command, out, *prerequisites, caller=1):
27572760
27582761 >>> run_if( f'touch {out}', out, prerequisite, caller=0)
27592762 pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
2763+
2764+ We detect changes to the contents of argv[0]:
2765+
2766+ Create a shell script and run it:
2767+
2768+ >>> _ = subprocess.run('rm run_if_test_argv0.* 1>/dev/null 2>/dev/null || true', shell=1)
2769+ >>> with open('run_if_test_argv0.sh', 'w') as f:
2770+ ... print('#! /bin/sh', file=f)
2771+ ... print('echo hello world > run_if_test_argv0.out', file=f)
2772+ >>> _ = subprocess.run(f'chmod u+x run_if_test_argv0.sh', shell=1)
2773+ >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0)
2774+ pipcl.py:run_if(): Running command because: File does not exist: 'run_if_test_argv0.out'
2775+ pipcl.py:run_if(): Running: ./run_if_test_argv0.sh
2776+ True
2777+
2778+ Running it a second time does nothing:
2779+
2780+ >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0)
2781+ pipcl.py:run_if(): Not running command because up to date: 'run_if_test_argv0.out'
2782+
2783+ Modify the script.
2784+
2785+ >>> with open('run_if_test_argv0.sh', 'a') as f:
2786+ ... print('\\ necho hello >> run_if_test_argv0.out', file=f)
2787+
2788+ And now it is run because the hash of argv[0] has changed:
2789+
2790+ >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0)
2791+ pipcl.py:run_if(): Running command because: arg0 hash has changed.
2792+ pipcl.py:run_if(): Running: ./run_if_test_argv0.sh
2793+ True
27602794 '''
27612795 doit = False
2796+
2797+ # Path of file containing pickle data for command and hash of command's
2798+ # first arg.
27622799 cmd_path = f'{ out } .cmd'
2800+
2801+ def hash_get (path ):
2802+ try :
2803+ with open (path , 'rb' ) as f :
2804+ return hashlib .md5 (f .read ()).hexdigest ()
2805+ except Exception as e :
2806+ #log(f'Failed to get hash of {path=}: {e}')
2807+ return None
2808+
2809+ command_args = shlex .split (command or '' )
2810+ command_arg0_path = fs_find_in_paths (command_args [0 ])
2811+ command_arg0_hash = hash_get (command_arg0_path )
2812+
2813+ cmd_args , cmd_arg0_hash = (None , None )
2814+ if os .path .isfile (cmd_path ):
2815+ with open (cmd_path , 'rb' ) as f :
2816+ try :
2817+ cmd_args , cmd_arg0_hash = pickle .load (f )
2818+ except Exception as e :
2819+ #log(f'pickle.load() failed with {cmd_path=}: {e}')
2820+ pass
27632821
27642822 if not doit :
2823+ # Set doit if outfile does not exist.
27652824 out_mtime = _fs_mtime ( out )
27662825 if out_mtime == 0 :
27672826 doit = f'File does not exist: { out !r} '
27682827
27692828 if not doit :
2770- if os .path .isfile ( cmd_path ):
2771- with open ( cmd_path ) as f :
2772- cmd = f .read ()
2773- else :
2774- cmd = None
2775- cmd_args = shlex .split (cmd or '' )
2776- command_args = shlex .split (command or '' )
2829+ # Set doit if command has changed.
27772830 if command_args != cmd_args :
2778- if cmd is None :
2831+ if cmd_args is None :
27792832 doit = 'No previous command stored'
27802833 else :
27812834 doit = f'Command has changed'
@@ -2791,8 +2844,8 @@ def run_if( command, out, *prerequisites, caller=1):
27912844 # shlex.split().
27922845 doit += ':\n '
27932846 lines = difflib .unified_diff (
2794- cmd . split () ,
2795- command . split () ,
2847+ cmd_args ,
2848+ command_args ,
27962849 lineterm = '' ,
27972850 )
27982851 # Skip initial lines.
@@ -2801,6 +2854,13 @@ def run_if( command, out, *prerequisites, caller=1):
28012854 for line in lines :
28022855 doit += f' { line } \n '
28032856
2857+ if not doit :
2858+ # Set doit if argv[0] hash has changed.
2859+ #print(f'{cmd_arg0_hash=} {command_arg0_hash=}', file=sys.stderr)
2860+ if command_arg0_hash != cmd_arg0_hash :
2861+ doit = f'arg0 hash has changed.'
2862+ #doit = f'arg0 hash has changed from {cmd_arg0_hash=} to {command_arg0_hash=}..'
2863+
28042864 if not doit :
28052865 # See whether any prerequisites are newer than target.
28062866 def _make_prerequisites (p ):
@@ -2845,8 +2905,9 @@ def _make_prerequisites(p):
28452905 run ( command , caller = caller + 1 )
28462906
28472907 # Write the command we ran, into `cmd_path`.
2848- with open ( cmd_path , 'w' ) as f :
2849- f .write ( command )
2908+
2909+ with open (cmd_path , 'wb' ) as f :
2910+ pickle .dump ((command_args , command_arg0_hash ), f )
28502911 return True
28512912 else :
28522913 log1 ( f'Not running command because up to date: { out !r} ' , caller = caller + 1 )
@@ -2857,6 +2918,35 @@ def _make_prerequisites(p):
28572918 )
28582919
28592920
2921+ def fs_find_in_paths ( name , paths = None , verbose = False ):
2922+ '''
2923+ Looks for `name` in paths and returns complete path. `paths` is list/tuple
2924+ or `os.pathsep`-separated string; if `None` we use `$PATH`. If `name`
2925+ contains `/`, we return `name` itself if it is a file or None, regardless
2926+ of <paths>.
2927+ '''
2928+ if '/' in name :
2929+ return name if os .path .isfile ( name ) else None
2930+ if paths is None :
2931+ paths = os .environ .get ( 'PATH' , '' )
2932+ if verbose :
2933+ log ('From os.environ["PATH"]: {paths=}' )
2934+ if isinstance ( paths , str ):
2935+ paths = paths .split ( os .pathsep )
2936+ if verbose :
2937+ log ('After split: {paths=}' )
2938+ for path in paths :
2939+ p = os .path .join ( path , name )
2940+ if verbose :
2941+ log ('Checking {p=}' )
2942+ if os .path .isfile ( p ):
2943+ if verbose :
2944+ log ('Returning because is file: {p!r}' )
2945+ return p
2946+ if verbose :
2947+ log ('Returning None because not found: {name!r}' )
2948+
2949+
28602950def _get_prerequisites (path ):
28612951 '''
28622952 Returns list of prerequisites from Makefile-style dependency file, e.g.
0 commit comments