#!/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]
}