#!/usr/bin/tclsh # Copyright (C) 2001-2023 Artifex Software, Inc. # All Rights Reserved. # # This software is provided AS-IS with no warranty, either express or # implied. # # This software is distributed under license and may not be copied, # modified or distributed except as expressly authorized under the terms # of the license contained in the file LICENSE in this distribution. # # Refer to licensing information at http://www.artifex.com or contact # Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco, # CA 94129, USA, for further information. # # gsmake.tcl - Tcl tools for Aladdin's products. Eventually we hope to: # Generate automatically: # For compiling and linking: # most of lib.mak, int.mak, and devs.mak # (_h defs, most .$(OBJ) rules, most .dev rules) # most of devs.mak # For fonts: # cfonts.mak # ccfonts option # The tools in this file can currently: # Check the makefiles for consistency with the #include lists # in .c/.cpp and .h files. # Check the makefiles for consistency with the devices defined # in .c/.cpp files. # Define a set of dependencies that we know about and don't consider # "unknown". set KNOWN_DEPS(cc.tr) 1 set KNOWN_DEPS(echogs) 1 # ---------------- Environment-specific procedures ---------------- # # Return a list of the exported and imported symbols of an object module. proc obj_symbols {objfile} { set export {} set import {} foreach line [split [exec nm -gp $objfile] "\n"] { if {[regexp {([A-Z]) ([^ ]+)$} [string trim $line] skip type sym]} { if {$type == "U"} { lappend import $sym } else { lappend export $sym } } } return [list $export $import] } # ---------------- Reading makefiles ---------------- # # The following procedures parse makefiles by reading them into an array # with the following components: # * files - a list of all the makefiles read in # * names - a list of all the defined names, in appearance order, # in the form macro= or target: # * pos:M= - the file and position of the definition of macro M # * pos:T: - the file and position of the rule for T # defn:M - definition of macro M # deps:T - dependencies for target T # body:T - rule body for T, a list of lines # File names in members marked with a * are normalized (see normalize_fname # below, as are target names (T); note that file names in dependencies # (value of deps:T) are not. # Global variables used: CWD # Initialize the tables. proc makefile_init {mfarray} { catch {uplevel 1 [list unset $mfarray]} upvar $mfarray mf set mf(files) "" set mf(names) "" } # Set CWD to the current working directory name (as a list). proc setcwd {} { global CWD set CWD [split [exec pwd] /] if {[lindex $CWD 0] == ""} { set CWD [lrange $CWD 1 end] } } # Normalize a file name by removing all occurrences of ./, # all occurrences of /../, # and any occurrences of ..// that are vacuous relative to CWD. proc normalize_fname {fname} { global CWD set name $fname # Remove a trailing / regsub {/$} $name "" name # Remove occurrences of ./ while {[regsub {^\./} $name "" name]} {} while {[regsub {/\./} $name / name]} {} while {[regsub {/\.$} $name "" name]} {} if {$name == ""} {return /} # Remove occurrences of /../ while {[regsub {(^|/)([^./]|.[^./])[^/]*/../} $name {\1} name]} {} # Now if any ../ are left, they are # the first thing in the file name. if {[regexp {^((../)+)(.*)$} $name skip up skip2 rest] && $rest != "" && $rest != ".."} { set count [expr {[string length $up] / 3}] if {$count <= [llength $CWD]} { set tail [lrange $CWD [expr {[llength $CWD] - $count}] end] while {$count > 0 && [regsub "^[lindex $tail 0]/" $rest "" rest]} { set up [string range $up 3 end] set tail [lrange $tail 1 end] incr count -1 } set name "$up$rest" } } if {$name == ""} {return .} return $name } # Find all the macro references in a string (macro, dependencies, or # rule body). proc macro_refs {line} { regsub -all {[^$]*(\$\(([^)]+)\)|\$|$)} $line {\2 } refs return [string trim $refs] } # Expand macro definitions in a string. # nosub decides whether a macro should (not) be expanded. # defer says what non-expanded macros should become. proc always_subst {var} { return 0 } proc macro_expand {mfarray str {nosub always_subst} {defer {$(\1)}}} { upvar $mfarray mf set in $str set out "" while {[regexp {^([^$]*)\$\(([^)]+)\)(.*)$} $in skip first var rest]} { if {[uplevel 1 [concat $nosub $var]] || ![info exists mf(pos:$var=)]} { regsub {^(.*)$} $var $defer var append out "$first$var" } else { append out "$first[macro_expand mf $mf(defn:$var) $nosub $defer]" } set in $rest } return "$out$in" } # Check the references to macros in a definition or rule line. proc check_refs {mfarray line ref} { upvar $mfarray mf foreach var [macro_refs $line] { if ![info exists mf(defn:$var)] { puts "Warning: $ref refers to undefined macro $var" set mf(defn:$var) "" } } } # Read a line from a makefile, recognizing a trailing \ for continuation. # source is an array with keys {file, lnum}. # Return -1 or the original value of source(lnum). proc linegets {sourcevar linevar} { upvar $sourcevar source $linevar line set infile $source(file) if {[gets $infile line] < 0} {return -1} set lnum $source(lnum) incr source(lnum) while {[regsub {\\$} $line {} line]} { gets $infile l append line $l incr source(lnum) } return $lnum } # Read a makefile, adding to the tables. proc read_makefile {mfarray inname} { global CWD upvar $mfarray mf setcwd set inname [normalize_fname $inname] set infile [open $inname] lappend mf(files) $inname set source(file) $infile set source(lnum) 1 while {[set pos [linegets source line]] >= 0} { if [regexp {^([A-Za-z_$][^=:]*)([=:])(.*)$} $line skip lhs eq rhs] { define$eq mf $lhs $rhs $inname:$pos source } elseif {[regsub {^(!|)include([ ]+)} $line {} file]} { regsub -all {"} $file {} file set file [macro_expand mf $file {string match {"}}] read_makefile mf $file } } close $infile } # Define a list (macro). proc define= {mfarray lhs rhs pos sourcevar} { upvar $mfarray mf set var [string trim [macro_expand mf $lhs]] if [info exists mf(pos:$var=)] { puts "Warning: $pos: macro $var redefined" puts " $mf(pos:$var=): previous definition" } set mf(pos:$var=) $pos set mf(defn:$var) $rhs check_refs mf $rhs "$pos: Macro $var" lappend mf(names) $var= } # Define a rule. proc define: {mfarray lhs rhs pos sourcevar} { upvar $mfarray mf $sourcevar source set targets "" foreach target [macro_expand mf $lhs] { lappend targets [normalize_fname $target] } set lines "" while {[set lnum [linegets source line]] >= 0 && $line != ""} { if ![regexp {^#} $line] { regsub {[0-9]+$} $pos $lnum lpos check_refs mf $line "$lpos: Rule for $targets" lappend lines $line } } foreach target $targets { set mf(pos:$target:) $pos set mf(deps:$target) $rhs set mf(body:$target) $lines lappend mf(names) $target: } } # ---------------- Reading source code ---------------- # # Scan a list of .c, .cpp, or .h files and extract references that conform # to a particular syntax. We use egrep to find the lines containing # the references, and regexp to extract the referent. proc set_references {refarray files grepexp rexp} { catch {uplevel 1 [list unset $refarray]} upvar $refarray refs switch [llength $files] { 0 {return} 1 { ;# force grep to output file name close [open _.nul w] lappend files _.nul } } foreach f $files { append refs($f) {} ;# ensure existence } set cmd [list exec -keepnewline grep -E $grepexp] append cmd " $files >_.tmp" if {![catch $cmd]} { set in [open _.tmp] set re {^([^:]*):} append re $rexp while {[gets $in line] > 0} { regexp $re $line skip f i lappend refs($f) $i } close $in } } # Scan a list of .c, .cpp, or .h files and extract the "include" lists. # Set the array incarray to the (sorted) lists. proc set_includes {incarray files} { upvar $incarray incs set gre {^#[ ]*include[ ]+\"} set re {#[\ \ ]*include[\ \ ]+"([^"]*)"} set_references incs $files $gre $re foreach f [array names incs] { set incs($f) [lsort $incs($f)] } } # Scan a list of .c or .cpp files and extract any devices they define. # Set the array devarray to the lists. proc set_devices {devarray files} { upvar $devarray devs set gre {gs_[0-9a-zA-Z]+_device.=} set re {.*gs_([0-9a-zA-Z]+)_device.=} set_references devs $files $gre $re } # ---------------- Checking makefiles ---------------- # # Expand a dependency list by substituting the values of all macro # references except _h macros. proc expand_deps {deps mfarray} { upvar $mfarray mf return [macro_expand mf $deps {regexp {_h$}}] } # Check the definition of one .h file. proc check_h {file incarray mfarray} { global KNOWN_DEPS upvar $incarray incs $mfarray mf set base [file tail $file] regsub {\.} $base {_} file_h if ![info exists mf(defn:$file_h)] { puts "$file exists, $file_h not defined" } else { set here { puts "In definition of $file_h at $mf(pos:$file_h=):" set here "" } foreach i $incs($file) { set inc($i) 1 } foreach d [expand_deps $mf(defn:$file_h) mf] { if [regexp {^\$\((.*)_h\)$} $d skip b] { set def($b.h) 1 } else { set d [normalize_fname $d] if {$d == $base || $d == $file} { } elseif {[regexp {\.h$} $d]} { set def($d) 1 } elseif {![info exists KNOWN_DEPS([file tail $d])]} { eval $here puts " Unknown element $d" } } } foreach i [array names inc] { if ![info exists def($i)] { eval $here puts " $base includes $i, missing from definition" } } foreach d [array names def] { if ![info exists inc($d)] { eval $here puts " Definition references $d, not included by $base" } } } } # Check the definition of one .c or .cpp file. proc check_c {file incarray mfarray} { global KNOWN_DEPS upvar $incarray incs $mfarray mf set base [file tail $file] regsub {\.(c|cpp)$} $file {.$(OBJ)} file_obj set file_obj [macro_expand mf $file_obj] if ![info exists mf(deps:$file_obj)] { # Maybe the object files are in another directory. set tail [file tail $file_obj] set known [concat [array names mf deps:$tail]\ [array names mf deps:*/$tail]] switch [llength $known] { 0 { puts "No rule for $file_obj" return } 1 { regsub {^deps:} [lindex $known 0] {} file_obj } default { puts "Ambiguous matches for $file_obj: $known" return } } } set here { puts "In rule for $file_obj at $mf(pos:$file_obj:):" set here "" } foreach i $incs($file) { set inc($i) 1 } foreach d [expand_deps $mf(deps:$file_obj) mf] { if [regexp {^\$\((.*)_h\)$} $d skip b] { set def($b.h) 1 } else { set d [normalize_fname $d] if {$d == $base || $d == $file} { } elseif {[regexp {\.h$} $d]} { set def($d) 1 } elseif {![info exists KNOWN_DEPS([file tail $d])]} { eval $here puts " Unknown element $d" } } } foreach i [array names inc] { if ![info exists def($i)] { eval $here puts " $base includes $i, missing from dependencies" } } foreach d [array names def] { if ![info exists inc($d)] { eval $here puts " Dependencies include $d, not included by $base" } } } # Check whether a given pattern occurs in a dependency tree. proc dep_search {target pattern mfarray} { upvar $mfarray mf set target [normalize_fname $target] set deps [expand_deps $mf(deps:$target) mf] if {[lsearch -glob $deps $pattern] >= 0} { return 1 } foreach d $deps { if {[regexp {(.*)\.dev$} $d]} { if {[dep_search $d $pattern mf]} { return 1 } } } } # Check that makefiles agree with device definitions in a .c/.cpp file. proc check_c_devs {file mfarray devsarray} { upvar $mfarray mf $devsarray devs foreach d $devs($file) { set mfnames [array names mf "pos:*\[/\\\]$d.dev:"] switch [llength $mfnames] { 0 { puts "No rule for $d.dev, defined in $file" } 1 { regexp {^pos:(.*):$} [lindex $mfnames 0] skip dev set base [file rootname [file tail $file]] if {![dep_search $dev "*\[/\\\]$base.*" mf]} { puts "$base missing from dependencies of $dev" } } default { puts "Multiple rules for $d.dev, defined in $file" } } } } # ---------------- Test code ---------------- # proc init_files {} { global FILES set FILES(h) {} set FILES(c) {} set FILES(cpp) {} } proc add_files {{dir .}} { global FILES if {$dir == "."} { set pre "" } else { set pre $dir/ } set total "" foreach extn {h c cpp} { lappend total\ [llength [set FILES($extn) [concat $FILES($extn)\ [lsort [glob -nocomplain ${pre}*.$extn]]]]] } return $total } proc all_files {} { global FILES set all {} foreach extn {h c cpp} {set all [concat $all $FILES($extn)]} return $all } proc get_includes {} { global INCS puts [time {set_includes INCS [all_files]}] } proc get_gs_devices {} { global DEVS puts [time {set_devices DEVS [glob ./src/gdev*.c]}] } proc check_headers {} { global FILES INCS MF foreach h $FILES(h) { check_h $h INCS MF } } proc check_code {} { global FILES INCS MF foreach c [concat $FILES(c) $FILES(cpp)] { check_c $c INCS MF } } proc check_devices {} { global DEVS MF foreach c [array names DEVS] { check_c_devs $c MF DEVS } } proc top_makefiles {dir} { foreach f [glob $dir/*.mak] { if {[regexp {lib.mak$} $f]} {continue} set mak($f) 1 } foreach f [array names mak] { set maybe_top 0 if {![catch {set lines [exec egrep {^(!|)include } $f]}]} { foreach line [split $lines "\n"] { if {[regsub {^(!|)include([ ]+)} $line {} file]} { set maybe_top 1 regsub -all {^"|"$} $file {} file regsub {^\$\([A-Z]+\)([/\\]|)} $file {} file catch {unset mak($dir/$file)} } } } if {!$maybe_top} { catch {unset mak($f)} } } return [array names mak] } proc check_makefile {args} { global MF if {$args == ""} {set args {makefile}} init_files makefile_init MF foreach f $args { while {![catch {set f [file readlink $f]}]} {} puts "Reading makefile $f" set dir [file dirname $f] if {![info exists dirs($dir)]} { set dirs($dir) 1 puts "Scanning source directory $dir" puts "[add_files $dir] files" } read_makefile MF $f } get_includes #get_gs_devices check_headers check_code #check_devices } if {$argv == [list "check"]} { eval check_makefile [lreplace $argv 0 0] }