Skip to content

Commit 5a6d329

Browse files
pipcl.py: run_if(): also detect changes to argv[0].
We compare hashes of argv[0]. This will help if we change swig between runs - in this case argv[0] itself may be unchanged, but the contents of the file `which argv[0]` will differ.
1 parent d91f5db commit 5a6d329

File tree

1 file changed

+105
-15
lines changed

1 file changed

+105
-15
lines changed

pipcl.py

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import inspect
3939
import io
4040
import os
41+
import pickle
4142
import platform
4243
import re
4344
import 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+
28602950
def _get_prerequisites(path):
28612951
'''
28622952
Returns list of prerequisites from Makefile-style dependency file, e.g.

0 commit comments

Comments
 (0)