#! /usr/bin/python3 # Last edited on 2026-02-09 10:19:25 by stolfi import sys, os, re; from sys import stderr as err from process_funcs import bash from error_funcs import arg_error, prog_error # To be included in other python3 programs. # Functions to simlate the Gnu {make} program. def make_targets(targets, pre, mak, tit): # Takes a list of file names {targets} and three dictionaires # {pre}, {max}, {tit} # such that for each filename {target} # # {pre[target]} is a list of names of other /input/ files that {target} is derived from. # {mak[target]} is a list of commands that make {target} from those input files. # {tit[target]} is a string that is printed to {stderr} before building {target}. # # All target files are assumed to go into the subfolder "out". The {target} # key should NOT have the "out/" prefix, but any input files that are in the "out" # subfolder MUST have the "out/" prefix. # # The strings in {mak[target]} are concatenated with "\n" appended at the end of # each. If a command must be continued in several lines, that last char of each # line, except the last one, should be a backslash. # err.write("begin {make_targets} ...\n") debug = False bash("mkdir -p out") if debug: print_build_list(targets, pre, "given") targets = expand_and_sort_targets(targets, pre) if debug: print_build_list(targets, pre, "sorted") for target in targets: target_file = f"out/{target}" if older_than(target_file, pre[target]): if os.path.exists(target_file): bash(f"rm {target_file}") err.write(f"=== {tit[target]} ===\n") err.write(f"!! {mak[target] = }\n") cmd = "\n".join(mak[target]) + "\n" bash(cmd) if not os.path.exists(target_file): prog_error(f"creation of {target_file} failed") err.write("end {make_targets}.\n") return # ---------------------------------------------------------------------- def print_build_list(targets, pre, which): err.write("!! --- %s dependency list -----------------------\n" % which) T = set(targets) | set (pre.keys()) for t1 in T: if not t1 in pre: err(f"no dependencies given for target '{t1}'\n") sys.exit(1) for t0 in pre[t1]: err.write("!! < %s\n" % t0) err.write("!! > %s\n" % t1) err.write("\n") err.write("!! ---------------------------------------------------------------\n") def expand_and_sort_targets(tgs, pre): # Takes a list {tgs} of files to be built, and a dict {pre} that # maps each target to a list of prerequisite /input/ files, returns a # list {srt} containing the given targets {tgs} plus and all the other targets # that need to be built to recreate them from scratch, in the proper # build order (that is, such that all the inputs of every target come # before the target). if len(tgs) <= 1: return tgs.copy() # Build complete potential target list, its dependency graph, and # prerequisite counts: pos, npre = build_dependency_graph(tgs, pre) ready = set() # Targets with no pending prerequisites. for t1 in npre.keys(): if npre[t1] == 0: ready.add(t1) # Output targets as their presequisites are satisfied: srt = [] # As targets are appended here, they are removed from {pos.keys()} while len(ready) > 0: t0 = ready.pop() assert t0 in npre and t0 in pos assert npre[t0] == 0 srt.append(t0) for t1 in pos[t0]: assert t1 in npre and t1 in pos assert npre[t1] > 0 npre[t1] -= 1 if npre[t1] == 0: ready.add(t1) del pos[t0] del npre[t0] # Check for completion: if len(pos) > 0: t0 = tuple(pos.keys())[0] err.write(f"circular dependency involving '{t0}'\n") sys.exit(1) return srt # ---------------------------------------------------------------------- def build_dependency_graph(tgs, pre): # The {pre} parameter should be a dict that maps the names of certain # files -- the /derived files/ -- to the names of other files -- their # /input files/. # # More precisely, for each derived file name {t1}, {pre[t1]} should be # a list with the names of the files that are directly used in the # building of file {t1}. # # Given a list {tgs} with some DERIVED file names, this function identifies # a set {T} of file names that includes {tgs} plus all the DERIVED # file names that MAY have to be rebuilt in order to bring the files # {tgs} up to date. Then it returns a reduced graph {pos} that # specifies the dependency graph among the files in {T} only, in the # opposite direction. Namely, for each {t0} in {T}, {pos[t0]} is the set # of all strings {t1} of {T} (only) such that {t0} is an input of {t1}. # # The files named in {tgs} and as the keys of {pre} # are presumed to be in the "out" subfolder, but those strings should NOT have # the "out/" prefix. Whereas the inputs listed in {pre[t1]} that are # in the "out" folder MUST start with "out/". The # elements of {pos[t1]} should be assumed to be in the "out" folder too, but # will NOT have the "out/" prefix. # # The function also returns a dict {npre} such that, for each {t1} in {T}, # {npre[t1]} is the number of DERIVED files from {T} that are # direct inputs of {t1}. # The set {T} is implicitly stored as the set of keys of {pos} and {npre}. debug = False pos = dict() npre = dict() queue = [] # Queue of targets to process. # Put the files from {tgs} into the queue, initialize their {npre} for ta in tgs: if not ta in pre: err.write(f"there is no recipe for target '{ta}'\n") sys.exit(1) if not ta in pos: queue.append(ta) pos[ta] = set() npre[ta] = 0 iq = 0 # Index of next target in queue to process. while iq < len(queue): # At this point the targets that need processing are # {queue[iq:]}, but more may be appended as we examine those. # The targets that have been processed already # or are still in the queue are in {pos.keys()}. # Processing a target {t1} means enumerating each input {t0} # of {t1} in {pre}, adding the pair {t0->t1} to {pos}, # incrementing {npre[t1]}, and enqueuing {t0} if it is new. t1 = queue[iq]; iq += 1 assert t1 in pos, f"** '{t1}' in queue but not in {{pos}}" assert t1 in npre, f"** '{t1}' in queue but not in {{npre}}" if debug: err.write(f"++ may need to build {t1}\n") for ot0 in pre[t1]: if debug: err.write(f"++ it needs {ot0}") if re.match(r"out/", ot0): t0 = ot0[4:] # Now derived file {t0} must be built before {t1}: if not t0 in pos: # Target {t0} has not been examined or enqueued yet: if debug: err.write(" (new)") queue.append(t0) pos[t0] = set() npre[t0] = 0 pos[t0].add(t1) npre[t1] += 1 if debug: err.write("\n") return pos, npre # ---------------------------------------------------------------------- def older_than(target_file, input_files): # Returns true if {target_file} does not exist, or if # it exists but its mod-date is older than that of some of the # files in the list {input_files}. if not os.path.exists(target_file): return True target_mtime = os.path.getmtime(target_file) for input_file in input_files: input_mtime = os.path.getmtime(input_file) if input_mtime > target_mtime: return True return False # ----------------------------------------------------------------------