Source file umake.icn
############################################################################
#
#	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

This page produced by UniDoc on 2021/04/15 @ 23:59:43.