############################################################################ # # File: umake.icn # # Subject: Unicon version of the "make" program. # # Author: Clinton Jeffery # # Date: August 8, 2013 # ############################################################################ # # PRELIMINARY. ALPHA TEST LEVEL. # # Reasons for the existence of this program: # # 1. link directly into IDE, do not launch an external process # 2. do not depend on end user to install a make.exe # 3. lingo-supremacist hubris. Write a shorter make in your language. $ifdef MAIN procedure main(argv) make(argv) end $endif global depgraph, macros, filechars, noexec, verbose procedure make(argv) macros := table("") i := 1 while i <= *argv do { if argv[i]=="-n" then { noexec := 1 delete(argv,i) next } else if argv[i]=="-f" then { delete(argv,i) # remove the -f makefile := argv[i] delete(argv,i) # remove the filename } else if argv[i]=="-v" then { verbose := 1 delete(argv,i) next } i +:= 1 } /makefile := "makefile" if *argv=0 then (depgraph := DependencyGraph(makefile)).make() else { depgraph := DependencyGraph(makefile) every i := 1 to *argv do { depgraph.make(argv[i]) } } end # # make is organized around a dependency graph. The nodes are labeled by targets. # class DependencyGraph(targets, initialtarget, marked) # Add a new target to the graph. Example call: # # ufiles := "" # every targ := !targets do { # if targ.target[-2:0]==".u" then ufiles ||:= targ[target] # } # dg.add_node("clean", [], ["rm "|| ufiles ||" uniclass.*"]) # method add_node(target, dependencies, buildrules) /dependencies := [] /buildrules := [] # if \targets[target] then { # # Target already exists. Under some circumstances this ought to # be an error/fail/warning, but by default we replace silently. # # write(&errout, "replacing ", image(target)) # } targets[target] := Rule(target, dependencies, buildrules) end method add_dependency(source,target) /targets := Rule(target) if not (source == !(targets[target].dependencies)) then put(targets[target].dependencies, source) end method mark(s) insert(marked, s) end method ismarked(s) return member(marked,s) end # produce a dependency graph node corresponding to a named file method node(s) return \ targets[s] if stat(s) then fail # no node for s, but it exists stop("no rule to make ", image(s)) end method make(targ) /targ := initialtarget if /targ then stop("no initial target? can't make.") if /(targets[targ]) then { # first, check for matching extensions every k := key(targets) do { if find("%", k) & (k ~=== "%") then { s := targ kk := k while s[1]===k[1] do { s[1] := ""; k[1] := "" } while s[-1]===k[-1] do { s[-1] := ""; k[-1] := "" } if k == "%" & (*s>0) then { # instantiate the generic rule if \verbose then write("instantiating generic rule for ", targ) targets[targ] := targets[kk].clone(s) return targets[targ].make() } } } # last, try a global if t := targets["%"] then { write("there exists a universal target") targets[targ] := targets["%"].clone(targ) return targets[targ].make() } stop("no target for ", image(targ)) } targets[targ].make() end method get_targets(target) local s # for version 0, just break on spaces target ? { while s := tab(find(" ")) do { suspend s; =" " } s := tab(0) suspend s } end initially(filename) filechars := &letters ++ './_%-' targets := table() marked := set() fstack := [] readahead := [] if f := open(\filename) then { while line := (pop(readahead) | unsplit(f) | (close(f) & (f:=pop(fstack)) & unsplit(f))) do { line := macroexpand(line) line ? { tab(many(' ')) if ="#" then { # skip comment # write("comment: ", image(line)) continue } else if pos(0) then { # skip empty lines continue } else if ="export" & tab(many(' \t')) & (envname := tab(many(filechars))) & =":=" & (val:=tab(0)) then { #what the heck, let's expand macro names val := macroexpand(val) if \verbose then write("setting ", envname, " to ", image(val)) # if Windows... do we need to map PATH to Path, or is PATH OK? # need to test. Mebbe PATH is OK. setenv(envname, val) } else if (target:=tab(many(filechars++' '))) & =":" then { # build a target or targets tab(many(' ')) deps := [] buildrules := [] while dependency := tab(many(filechars)) do { put(deps,dependency) tab(many(' ')) } # This is a "recipe line", so continuations should not be concatenated # i.e. use read(f) directly, rather than unsplit(f) while (line := read(f)) & (line[1]=="\t") do { put(buildrules, macroexpand(line[2:0])) line := &null } put(readahead, \line) every t := get_targets(target) do { r := Rule(target,deps,buildrules) targets[t] := r } if not find("%", target, 1) then /initialtarget := target } else if (macroname:=tab(many(filechars))) & (tab(many(' '))|"") & ="=" then { # build a macro macros[macroname] := macroexpand(tab(0)) if \verbose then write("macro ", macroname, " defined as ", macros[macroname]) } else if ="include" & tab(many(' \t')) & (inclname := tab(many(filechars))) then { push(fstack, f) if \verbose then write("including ", image(inclname)) f := open(inclname) } else write("??? ", image(line)) } } close(f) } else if \filename then stop("can't open ", image(filename)) else stop("usage: umake ... (reads makefile)") end class Rule(target, dependencies, buildrules) method subst(s, percent_sub, symbol : "%") s[find(symbol, s) +: *symbol] := percent_sub return s end method clone(percent_sub) newRule := Rule(target, copy(dependencies), copy(buildrules)) newRule.target := subst(newRule.target, percent_sub) every d := 1 to *dependencies do { newRule.dependencies[d] := subst(newRule.dependencies[d], percent_sub) } every b := 1 to *buildrules do { newRule.buildrules[b] := subst(newRule.buildrules[b], newRule.dependencies[1], "$<") } return newRule end method make() local s if depgraph.ismarked(target) then { if \verbose then write("umake ", target, " (already made)") } else { if \verbose then write("umaking ", target) depgraph.mark(target) } # first, recursively build dependencies every depgraph.node(!dependencies).make() # check timestamps of dependencies against my timestamp if (not stat(target)) | newer(!dependencies) then { exec() return } if \verbose then write("umake: `",target, "' is up to date") end # check me (target timestamp) against d and succeed if d is newer method newer(d) local mytimestamp, theirtimestamp if not (mytimestamp := stat(target).mtime) then return if not (theirtimestamp := stat(d).mtime) then stop(target, " depends on ", d, " but it didn't get built") if \verbose then write(" ", target,": ", image(mytimestamp), ", ", d, ": ", image(theirtimestamp)) if mytimestamp < theirtimestamp then return end method exec() local r every r := !buildrules do { if \noexec then write(r) else { if not (rv := system(r)) then stop("umake: system(",image(r),") failed") if rv ~=== 0 then stop("umake: ", rv, " exit by ", image(r)) } } end method print() writes("target ", image(target), " : ") every writes(" ", image(!dependencies)) write() write("built by") every write("\t", image(!buildrules)) end initially /dependencies := [] /buildrules := [] end # # This is (should be) a dependency graph with built-in knowledge of # .u and .icn dependencies necessary for Unicon programs. What about # subclasses and packages that introduce additional file dependencies? # class UniconProject : DependencyGraph() end procedure macroexpand(s) s ? { while tab(i := find("$")) & ="$(" & (mac := tab(many(filechars))) & =")" do { if not member(macros, mac) then { if val := getenv(mac|initcap(mac)|map(mac)) then macros[mac] := val else if \verbose then write("warning: empty macro ", mac) } s[i +: *mac+3] := macros[mac] &subject := s; &pos := i } } return s end procedure initcap(s) return map(s[1],&lcase,&ucase) || map(s[2:0]) end # Concatenate lines with a trailing backslash with the next line # and replace the "whitespace \ whitespace" at the join with a single space procedure unsplit(f) local line := read(f) | fail if line[-1] == "\\" then return trim(line, '\t \\') || " " || trim(\unsplit(f), ' \t', 1) else return line end