![]() |
version 0.1 |
It began with an elaborate idea of a dependency analyzer, boiled down to something much simpler, and turned into Make that weekend.
--Stuart Feldman
See, my laptop currently has only one small functioning partition,
so I can't install the whole developer tools suite onto it.
I was working on some Java code on the laptop.
The project includes a Makefile
, but I had to
compile by hand on the laptop.
I noticed the basic Mac OS X install includes gcc
,
javac
, jar
, and java
,
but not make
(nor ant
nor SCons
).
That's when it occurred to me:
"Gee, I don't do anything really...complicated with make
..."
So I spent nine hours on Saturday writing the gist of
make
in Python.
At that point myke
did all the basic
stuff I use make
for in my own work.
Sunday I spent another nine hours futzing.
The flip side of diminishing returns is that the first 2% of a project
gives you an inflated opinion of yourself.
Yet another make-maker.
I've since found that although gnumake
is compiled from about 70 times as many
source lines and bytes as myke
, its compiled binary is only
about 300K bytes, and it does run when copied to my laptop
(no missing or incompatible libraries). So myke
has been a
diversion from what I was doing on Friday.
I modeled myke
on make
's behavior,
rather than documentation or any opinions of my own.
It was nice having so few design questions or decisions to make.
One mystery is the difference between "nothing to be done for
'all'," and "target 'all' is up-to-date." The latter seems to happen when
there were rules to run, but make
didn't echo any of them to the
terminal. You can trigger this by creating a rule with a line
beginning with a tab but otherwise blank. myke
doesn't
go for that.
I liked working on a program called "myke
." For instance there are
"raise MykeException" statements, and comments like "myke
counts on it."
myke is crazy like a fox, but not quite the character.myke uses McGuffin-based parsing.myke was inside of make ,
looking for a way out.myke searches for significant whitespace. |
![]() |
Here's the code to download and "inline" (take two they're small):
#!/usr/bin/env python """\ myke -- The 2% of make that does 50% of what I need. (The other 50% being to build other people's code.) This file: 375 lines, 11K bytes. The 33 C files in Gnu make 3.00's source: 30,000 lines, 790K bytes Steve Witham, "ess" "double-you" at remove-this tiac dot net http://www.tiac.net/~sw/2010/03/Myke """ progname = "myke" Makefile = "Makefile" def usage(): print >>stderr, '''\ Usage: %s [-s|--silent] [-n|--dry-run] [target] Makefile is always "%s".''' % ( progname, Makefile ) exit( 1 ) from sys import argv, stdout, stderr, exc_info, exit from os import getenv, stat, stat_float_times, system be_silent = False dry_run = False whitespace = [ ' ', '\t', '\\\n', '\n' ] name_end = whitespace + [ c for c in "$(){}=:" ] # no whitespace in target or variable names def myke( target=None ): dependencies = {} # { target: list_of_subtargets... } scripts = {} # { target: list_of_commands... } target = read_Makefile( dependencies, scripts, target ) times = {} stat_float_times() # Tells stat() to return floats if it can. check_dependencies( target, dependencies, scripts, times ) if times[ target ] == "MAKE": build( target, dependencies, scripts, times ) if dry_run and be_silent: return 1 # needs to be built else: warning( "Nothing to be done for "+repr(target)+"." ) return 0 def read_Makefile( dependencies, scripts, target=None ): current_command_target = None vardict = VarDict() try: M = open( Makefile, 'r' ) line = "" lineno = 1 while True: lineno += line.count( '\n' ) line = read_continuation_lines( M ) if line == "": break line = substitute_vars( line, lineno, vardict ) p = skip_whitespace( line, 0 ) if p == len( line ) or line[p] == '#': # blank line or comment: continue if line[0] == '\t': # command line: if current_command_target: scripts[ current_command_target ].append( line[ 1: ] ) # (Newlines are removed after printing before running.) continue else: parse_err( line, lineno, 1, "Command isn't directly under a target. Stop." ) token = get_token( line, 0, name_end ) p += len( token ) if token and line[ p ] == '=': # setting a variable: p += len( '=' ) vardict[ token ] = " ".join( split_at_whitespace( line[p:] ) ) current_command_target = None continue elif token and line[p] == ':': # target: dependencies p += len( ':' ) if token in dependencies: parse_err( line, lineno, 0, "Extra rule for target "+repr(token)+". Stop." ) dependencies[ token ] = split_at_whitespace( line[ p : ] ) current_command_target = token if target == None: target = token # first target in the file scripts[ token ] = [] continue else: parse_err( line, lineno, 0, "Can't parse. Stop." ) finally: M.close() return target def read_continuation_lines( f ): """ Read lines until EOF or line not ending in backslash. single string returned including all backslashes and newlines. """ whole_line = "" while True: line = f.readline() if line == "": return whole_line if line[ -1 ] != '\n': # Last line with no newline: line += '\n' # myke counts on it. whole_line += line if len(line) < 2 or line[ -2 ] != '\\': return whole_line def check_dependencies( target, dependencies, scripts, times, parent=None ): if target in times: return try: my_time = stat( target ).st_mtime if not target in dependencies: # i.e., no rule for target, times[ target ] = my_time return except OSError: # Assuming this means the file or directory doesn't exist. my_time = None if not target in dependencies: message = "*** No rule to make target " + repr(target) if parent: message += ", needed by " + repr(parent) runtime_err( message + ". Stop.", 1 ) times[ target ] = "CHECKING" latest_dep = None for dep in dependencies[ target ]: if times.get( dep ) == "CHECKING": warning( "Circular " +dep+ " <- " +dep+ " dependency dropped." ) continue check_dependencies( dep, dependencies, scripts, times, parent=target ) latest_dep = latest_of( latest_dep, times[ dep ] ) if my_time == None: if scripts[ target ]: times[ target ] = "MAKE" else: times[ target ] = latest_dep else: if my_time == latest_of( my_time, latest_dep ): times[ target ] = my_time else: times[ target ] = "MAKE" def latest_of( a, b ): if a == "MAKE" or b == "MAKE": return "MAKE" return max( a, b ) def build( target, dependencies, scripts, times ): if times[ target ] != "MAKE": return times[ target ] = "MAKING" for dep in dependencies[ target ]: build( dep, dependencies, scripts, times ) for command in scripts[ target ]: if command[0] == '@': # one-line silent treatment command = command[1:] # (remove the '@') elif not be_silent: print command.rstrip() if not dry_run: whites = [ '\t', '\\\n', '\n' ] command = " ".join( split_at_whitespace( command, whites ) ) retcode = system( command ) if retcode != 0: err, sig = retcode / 256, retcode % 256 runtime_err( "*** [" + target + "] Error " + str(err), err ) times[ target ] = "MADE" def substitute_vars( line, lineno, vardict ): """ Myke doesn't do recursive substitution. """ # $$ ${blah} $(blah) or $v # where v is any char not in the "name_end" set. parens = { "(": ")", "{": "}" } p = 0 result = "" while p < len( line ): q = line[p:].find( '$' ) if q == -1: break result += line[ p : p + q ] p += ( q + len( '$' ) ) if line[p] == '$': # $$ stands for $ p += len("$") result += "$" continue elif line[p] in parens: closer = parens[ line[p] ] p += len( line[p] ) varname = get_token( line, p, name_end ) p += len( varname ) # p points at mcguffin. if varname == "" or line[p] != closer: parse_err( line, lineno, p, "Bad long variable reference." ) p += len(closer) elif mcguffin_len( line[p:], name_end ) == None: # one legal name char varname = line[p] p += len(varname) else: parse_err( line, lineno, p, "Bad variable reference." ) result += vardict[ varname ] return result + line[ p: ] class VarDict: """ Variable dictionary just for myke. if d = VarDict() then d[key] = value sets myke variable; d[key] gets from myke variable, else os.environ, else ""; print d prints the internal dictionary only. """ def __init__( self ): self.d = {} def __setitem__( self, varname, value ): self.d[ varname ] = value def __getitem__( self, varname, default="" ): if varname in self.d: return self.d[ varname ] else: return getenv( varname, default ) def __repr__( self ): return repr( self.d ) class MykeException( Exception ): pass def parse_err( line, lineno, p, complaint ): """ Complain about line # lineno, without echoing the line, but count newlines before position p in line number. p should point at or before newline of the offending line. """ lineno += line[ 0 : p ].count( '\n' ) message = Makefile + ":" + str(lineno)+","+str(p)+": *** " + complaint raise MykeException( message, 1 ) def warning( complaint ): if not be_silent: print >>stderr, progname + ":", complaint def runtime_err( complaint, errcode ): raise MykeException( progname + ": " + complaint, errcode ) def skip_whitespace( line, p, mcguffins=whitespace ): while True: q = mcguffin_len( line[ p: ], mcguffins ) if q == None: return p p += q def split_at_whitespace( string, mcguffins=whitespace ): result = [] p = 0 while p < len( string ): chunk = get_token( string, p, mcguffins ) p += len( chunk ) p += mcguffin_len( string[ p : ], mcguffins ) if chunk != "": result.append( chunk ) return result def get_token( line, p, mcguffins=whitespace ): q = find_any( line[ p: ], mcguffins ) if q == -1: # In this program it's strange not to find a newline... return line[ p: ] else: return line[ p: p+q ] def mcguffin_len( string, mcguffins=whitespace ): """ Length of the mcguffin found by find_any() or get_token(). """ for substr in mcguffins: if string.startswith( substr ): return len( substr ) return None def find_any( string, mcguffins=whitespace ): """ find first instance of... any chars in mcguffins if it's a string any strings in mcguffins if it's a list. """ first = len( string ) for substr in mcguffins: p = string.find( substr ) if p >= 0 and p < first: first = p if first == len( string ): return -1 else: return first if __name__ == "__main__": if len(argv) == 0: args = [] else: progname = argv[0] args = argv[ 1: ] while len(args) > 0 and args[0].startswith( '-' ): if args[0] in [ "-s", "--silent" ]: be_silent = True elif args[0] in [ "-n", "--dry-run" ]: dry_run = True else: usage() args = args[ 1: ] if len(args) > 1: usage() try: retcode = myke( *args ) except MykeException: msg, retcode = exc_info()[1].args print >>stderr, msg exit( retcode )
--Steve Witham
Up to my home page.