######################################################################## # 2024 September 25 # # The author disclaims copyright to this source code. In place of # a legal notice, here is a blessing: # # * May you do good and not evil. # * May you find forgiveness for yourself and forgive others. # * May you share freely, never taking more than you give. # # # ----- @module proj.tcl ----- # @section Project-agnostic Helper APIs # # # Routines for Steve Bennett's autosetup which are common to trees # managed in and around the umbrella of the SQLite project. # # The intent is that these routines be relatively generic, independent # of a given project. # # For practical purposes, the copy of this file hosted in the SQLite # project is the "canonical" one: # # https://sqlite.org/src/file/autosetup/proj.tcl # # This file was initially derived from one used in the libfossil # project, authored by the same person who ported it here, and this is # noted here only as an indication that there are no licensing issues # despite this code having a handful of near-twins running around a # handful of third-party source trees. # # Design notes: # # - Symbols with _ separators are intended for internal use within # this file, and are not part of the API which auto.def files should # rely on. Symbols with - separators are public APIs. # # - By and large, autosetup prefers to update global state with the # results of feature checks, e.g. whether the compiler supports flag # --X. In this developer's opinion that (A) causes more confusion # than it solves[^1] and (B) adds an unnecessary layer of "voodoo" # between the autosetup user and its internals. This module, in # contrast, instead injects the results of its own tests into # well-defined variables and leaves the integration of those values # to the caller's discretion. # # [1]: As an example: testing for the -rpath flag, using # cc-check-flags, can break later checks which use # [cc-check-function-in-lib ...] because the resulting -rpath flag # implicitly becomes part of those tests. In the case of an rpath # test, downstream tests may not like the $prefix/lib path added by # the rpath test. To avoid such problems, we avoid (intentionally) # updating global state via feature tests. # # # $proj__Config is an internal-use-only array for storing whatever generic # internal stuff we need stored. # array set ::proj__Config { self-tests 1 } # # List of dot-in files to filter in the final stages of # configuration. Some configuration steps may append to this. Each # one in this list which exists will trigger the generation of a # file with that same name, minus the ".in", in the build directory # (which differ from the source dir in out-of-tree builds). # # See: proj-dot-ins-append and proj-dot-ins-process # set ::proj__Config(dot-in-files) [list] set ::proj__Config(isatty) [isatty? stdout] # # @proj-warn msg # # Emits a warning message to stderr. All args are appended with a # space between each. # proc proj-warn {args} { show-notices puts stderr [join [list "WARNING: \[[proj-scope 1]\]: " {*}$args] " "] } # Internal impl of [proj-fatal] and [proj-error]. It must be called # using tailcall. proc proj__faterr {failMode argv} { show-notices set lvl 1 while {"-up" eq [lindex $argv 0]} { set argv [lassign $argv -] incr lvl } if {$failMode} { puts stderr [join [list "FATAL: \[[proj-scope $lvl]]: " {*}$argv]] exit 1 } else { error [join [list "\[[proj-scope $lvl]]:" {*}$argv]] } } # # @proj-fatal ?-up...? msg... # # Emits an error message to stderr and exits with non-0. All args are # appended with a space between each. # # The calling scope's name is used in the error message. To instead # use the name of a call higher up in the stack, use -up once for each # additional level. # proc proj-fatal {args} { tailcall proj__faterr 1 $args } # # @proj-error ?-up...? msg... # # Works like proj-fatal but uses [error] intead of [exit]. # proc proj-error {args} { tailcall proj__faterr 0 $args } set ::proj__Config(verbose-assert) [get-env proj-assert-verbose 0] # # @proj-assert script ?message? # # Kind of like a C assert: if uplevel of [list expr $script] is false, # a fatal error is triggered. The error message, by default, includes # the body of the failed assertion, but if $msg is set then that is # used instead. # proc proj-assert {script {msg ""}} { if {1 eq $::proj__Config(verbose-assert)} { msg-result [proj-bold "asserting: $script"] } if {![uplevel 1 [list expr $script]]} { if {"" eq $msg} { set msg $script } proj-fatal "Assertion failed in \[[proj-scope 1]\]: $msg" } } # # @proj-bold str # # If this function believes that the current console might support # ANSI escape sequences then this returns $str wrapped in a sequence # to bold that text, else it returns $str as-is. # proc proj-bold {args} { if {$::autosetup(iswin) || !$::proj__Config(isatty)} { return [join $args] } return "\033\[1m${args}\033\[0m" } # # @proj-indented-notice ?-error? ?-notice? msg # # Takes a multi-line message and emits it with consistent indentation. # It does not perform any line-wrapping of its own. Which output # routine it uses depends on its flags, defaulting to msg-result. # For -error and -notice it uses user-notice. # # If the -notice flag it used then it emits using [user-notice], which # means its rendering will (A) go to stderr and (B) be delayed until # the next time autosetup goes to output a message. # # If the -error flag is provided then it renders the message # immediately to stderr and then exits. # # If neither -notice nor -error are used, the message will be sent to # stdout without delay. # proc proj-indented-notice {args} { set fErr "" set outFunc "msg-result" while {[llength $args] > 1} { switch -exact -- [lindex $args 0] { -error { set args [lassign $args fErr] set outFunc "user-notice" } -notice { set args [lassign $args -] set outFunc "user-notice" } default { break } } } set lines [split [join $args] \n] foreach line $lines { set line [string trimleft $line] if {"" eq $line} { $outFunc $line } else { $outFunc " $line" } } if {"" ne $fErr} { show-notices exit 1 } } # # @proj-is-cross-compiling # # Returns 1 if cross-compiling, else 0. # proc proj-is-cross-compiling {} { expr {[get-define host] ne [get-define build]} } # # @proj-strip-hash-comments value # # Expects to receive string input, which it splits on newlines, strips # out any lines which begin with any number of whitespace followed by # a '#', and returns a value containing the [append]ed results of each # remaining line with a \n between each. It does not strip out # comments which appear after the first non-whitespace character. # proc proj-strip-hash-comments {val} { set x {} foreach line [split $val \n] { if {![string match "#*" [string trimleft $line]]} { append x $line \n } } return $x } # # @proj-cflags-without-werror # # Fetches [define $var], strips out any -Werror entries, and returns # the new value. This is intended for temporarily stripping -Werror # from CFLAGS or CPPFLAGS within the scope of a [define-push] block. # proc proj-cflags-without-werror {{var CFLAGS}} { set rv {} foreach f [get-define $var ""] { switch -exact -- $f { -Werror {} default { lappend rv $f } } } join $rv " " } # # @proj-check-function-in-lib # # A proxy for cc-check-function-in-lib with the following differences: # # - Does not make any global changes to the LIBS define. # # - Strips out the -Werror flag from CFLAGS before running the test, # as these feature tests will often fail if -Werror is used. # # Returns the result of cc-check-function-in-lib (i.e. true or false). # The resulting linker flags are stored in the [define] named # lib_${function}. # proc proj-check-function-in-lib {function libs {otherlibs {}}} { set found 0 define-push {LIBS CFLAGS} { #puts "CFLAGS before=[get-define CFLAGS]" define CFLAGS [proj-cflags-without-werror] #puts "CFLAGS after =[get-define CFLAGS]" set found [cc-check-function-in-lib $function $libs $otherlibs] } return $found } # # @proj-search-for-header-dir ?-dirs LIST? ?-subdirs LIST? header # # Searches for $header in a combination of dirs and subdirs, specified # by the -dirs {LIST} and -subdirs {LIST} flags (each of which have # sane defaults). Returns either the first matching dir or an empty # string. The return value does not contain the filename part. # proc proj-search-for-header-dir {header args} { set subdirs {include} set dirs {/usr /usr/local /mingw} # Debatable: # if {![proj-is-cross-compiling]} { # lappend dirs [get-define prefix] # } while {[llength $args]} { switch -exact -- [lindex $args 0] { -dirs { set args [lassign $args - dirs] } -subdirs { set args [lassign $args - subdirs] } default { proj-error "Unhandled argument: $args" } } } foreach dir $dirs { foreach sub $subdirs { if {[file exists $dir/$sub/$header]} { return "$dir/$sub" } } } return "" } # # @proj-find-executable-path ?-v? binaryName # # Works similarly to autosetup's [find-executable-path $binName] but: # # - If the first arg is -v, it's verbose about searching, else it's quiet. # # Returns the full path to the result or an empty string. # proc proj-find-executable-path {args} { set binName $args set verbose 0 if {[lindex $args 0] eq "-v"} { set verbose 1 set args [lassign $args - binName] msg-checking "Looking for $binName ... " } set check [find-executable-path $binName] if {$verbose} { if {"" eq $check} { msg-result "not found" } else { msg-result $check } } return $check } # # @proj-bin-define binName ?defName? # # Uses [proj-find-executable-path $binName] to (verbosely) search for # a binary, sets a define (see below) to the result, and returns the # result (an empty string if not found). # # The define'd name is: If $defName is not empty, it is used as-is. If # $defName is empty then "BIN_X" is used, where X is the upper-case # form of $binName with any '-' characters replaced with '_'. # proc proj-bin-define {binName {defName {}}} { set check [proj-find-executable-path -v $binName] if {"" eq $defName} { set defName "BIN_[string toupper [string map {- _} $binName]]" } define $defName $check return $check } # # @proj-first-bin-of bin... # # Looks for the first binary found of the names passed to this # function. If a match is found, the full path to that binary is # returned, else "" is returned. # # Despite using cc-path-progs to do the search, this function clears # any define'd name that function stores for the result (because the # caller has no sensible way of knowing which result it was unless # they pass only a single argument). # proc proj-first-bin-of {args} { set rc "" foreach b $args { set u [string toupper $b] # Note that cc-path-progs defines $u to "false" if it finds no # match. if {[cc-path-progs $b]} { set rc [get-define $u] } undefine $u if {"" ne $rc} break } return $rc } # # @proj-opt-was-provided key # # Returns 1 if the user specifically provided the given configure flag # or if it was specifically set using proj-opt-set, else 0. This can # be used to distinguish between options which have a default value # and those which were explicitly provided by the user, even if the # latter is done in a way which uses the default value. # # For example, with a configure flag defined like: # # { foo-bar:=baz => {its help text} } # # This function will, when passed foo-bar, return 1 only if the user # passes --foo-bar to configure, even if that invocation would resolve # to the default value of baz. If the user does not explicitly pass in # --foo-bar (with or without a value) then this returns 0. # # Calling [proj-opt-set] is, for purposes of the above, equivalent to # explicitly passing in the flag. # # Note: unlike most functions which deal with configure --flags, this # one does not validate that $key refers to a pre-defined flag. i.e. # it accepts arbitrary keys, even those not defined via an [options] # call. [proj-opt-set] manipulates the internal list of flags, such # that new options set via that function will cause this function to # return true. (That's an unintended and unavoidable side-effect, not # specifically a feature which should be made use of.) # proc proj-opt-was-provided {key} { dict exists $::autosetup(optset) $key } # # @proj-opt-set flag ?val? # # Force-set autosetup option $flag to $val. The value can be fetched # later with [opt-val], [opt-bool], and friends. # # Returns $val. # proc proj-opt-set {flag {val 1}} { if {$flag ni $::autosetup(options)} { # We have to add this to autosetup(options) or else future calls # to [opt-bool $flag] will fail validation of $flag. lappend ::autosetup(options) $flag } dict set ::autosetup(optset) $flag $val return $val } # # @proj-opt-exists flag # # Returns 1 if the given flag has been defined as a legal configure # option, else returns 0. # proc proj-opt-exists {flag} { expr {$flag in $::autosetup(options)}; } # # @proj-val-truthy val # # Returns 1 if $val appears to be a truthy value, else returns # 0. Truthy values are any of {1 on true yes enabled} # proc proj-val-truthy {val} { expr {$val in {1 on true yes enabled}} } # # @proj-opt-truthy flag # # Returns 1 if [opt-val $flag] appears to be a truthy value or # [opt-bool $flag] is true. See proj-val-truthy. # proc proj-opt-truthy {flag} { if {[proj-val-truthy [opt-val $flag]]} { return 1 } set rc 0 catch { # opt-bool will throw if $flag is not a known boolean flag set rc [opt-bool $flag] } return $rc } # # @proj-if-opt-truthy boolFlag thenScript ?elseScript? # # If [proj-opt-truthy $flag] is true, eval $then, else eval $else. # proc proj-if-opt-truthy {boolFlag thenScript {elseScript {}}} { if {[proj-opt-truthy $boolFlag]} { uplevel 1 $thenScript } else { uplevel 1 $elseScript } } # # @proj-define-for-opt flag def ?msg? ?iftrue? ?iffalse? # # If [proj-opt-truthy $flag] then [define $def $iftrue] else [define # $def $iffalse]. If $msg is not empty, output [msg-checking $msg] and # a [msg-results ...] which corresponds to the result. Returns 1 if # the opt-truthy check passes, else 0. # proc proj-define-for-opt {flag def {msg ""} {iftrue 1} {iffalse 0}} { if {"" ne $msg} { msg-checking "$msg " } set rcMsg "" set rc 0 if {[proj-opt-truthy $flag]} { define $def $iftrue set rc 1 } else { define $def $iffalse } switch -- [proj-val-truthy [get-define $def]] { 0 { set rcMsg no } 1 { set rcMsg yes } } if {"" ne $msg} { msg-result $rcMsg } return $rc } # # @proj-opt-define-bool ?-v? optName defName ?descr? # # Checks [proj-opt-truthy $optName] and calls [define $defName X] # where X is 0 for false and 1 for true. $descr is an optional # [msg-checking] argument which defaults to $defName. Returns X. # # If args[0] is -v then the boolean semantics are inverted: if # the option is set, it gets define'd to 0, else 1. Returns the # define'd value. # proc proj-opt-define-bool {args} { set invert 0 if {[lindex $args 0] eq "-v"} { incr invert lassign $args - optName defName descr } else { lassign $args optName defName descr } if {"" eq $descr} { set descr $defName } #puts "optName=$optName defName=$defName descr=$descr" set rc 0 msg-checking "[join $descr] ... " set rc [proj-opt-truthy $optName] if {$invert} { set rc [expr {!$rc}] } msg-result $rc define $defName $rc return $rc } # # @proj-check-module-loader # # Check for module-loading APIs (libdl/libltdl)... # # Looks for libltdl or dlopen(), the latter either in -ldl or built in # to libc (as it is on some platforms). Returns 1 if found, else # 0. Either way, it `define`'s: # # - HAVE_LIBLTDL to 1 or 0 if libltdl is found/not found # - HAVE_LIBDL to 1 or 0 if dlopen() is found/not found # - LDFLAGS_MODULE_LOADER one of ("-lltdl", "-ldl", or ""), noting # that -ldl may legally be empty on some platforms even if # HAVE_LIBDL is true (indicating that dlopen() is available without # extra link flags). LDFLAGS_MODULE_LOADER also gets "-rdynamic" appended # to it because otherwise trying to open DLLs will result in undefined # symbol errors. # # Note that if it finds LIBLTDL it does not look for LIBDL, so will # report only that is has LIBLTDL. # proc proj-check-module-loader {} { msg-checking "Looking for module-loader APIs... " if {99 ne [get-define LDFLAGS_MODULE_LOADER 99]} { if {1 eq [get-define HAVE_LIBLTDL 0]} { msg-result "(cached) libltdl" return 1 } elseif {1 eq [get-define HAVE_LIBDL 0]} { msg-result "(cached) libdl" return 1 } # else: wha??? } set HAVE_LIBLTDL 0 set HAVE_LIBDL 0 set LDFLAGS_MODULE_LOADER "" set rc 0 puts "" ;# cosmetic kludge for cc-check-XXX if {[cc-check-includes ltdl.h] && [cc-check-function-in-lib lt_dlopen ltdl]} { set HAVE_LIBLTDL 1 set LDFLAGS_MODULE_LOADER "-lltdl -rdynamic" msg-result " - Got libltdl." set rc 1 } elseif {[cc-with {-includes dlfcn.h} { cctest -link 1 -declare "extern char* dlerror(void);" -code "dlerror();"}]} { msg-result " - This system can use dlopen() without -ldl." set HAVE_LIBDL 1 set LDFLAGS_MODULE_LOADER "" set rc 1 } elseif {[cc-check-includes dlfcn.h]} { set HAVE_LIBDL 1 set rc 1 if {[cc-check-function-in-lib dlopen dl]} { msg-result " - dlopen() needs libdl." set LDFLAGS_MODULE_LOADER "-ldl -rdynamic" } else { msg-result " - dlopen() not found in libdl. Assuming dlopen() is built-in." set LDFLAGS_MODULE_LOADER "-rdynamic" } } define HAVE_LIBLTDL $HAVE_LIBLTDL define HAVE_LIBDL $HAVE_LIBDL define LDFLAGS_MODULE_LOADER $LDFLAGS_MODULE_LOADER return $rc } # # @proj-no-check-module-loader # # Sets all flags which would be set by proj-check-module-loader to # empty/falsy values, as if those checks had failed to find a module # loader. Intended to be called in place of that function when # a module loader is explicitly not desired. # proc proj-no-check-module-loader {} { define HAVE_LIBDL 0 define HAVE_LIBLTDL 0 define LDFLAGS_MODULE_LOADER "" } # # @proj-file-content ?-trim? filename # # Opens the given file, reads all of its content, and returns it. If # the first arg is -trim, the contents of the file named by the second # argument are trimmed before returning them. # proc proj-file-content {args} { set trim 0 set fname $args if {"-trim" eq [lindex $args 0]} { set trim 1 lassign $args - fname } set fp [open $fname rb] set rc [read $fp] close $fp if {$trim} { return [string trim $rc] } return $rc } # # @proj-file-conent filename # # Returns the contents of the given file as an array of lines, with # the EOL stripped from each input line. # proc proj-file-content-list {fname} { set fp [open $fname rb] set rc {} while { [gets $fp line] >= 0 } { lappend rc $line } close $fp return $rc } # # @proj-file-write ?-ro? fname content # # Works like autosetup's [writefile] but explicitly uses binary mode # to avoid EOL translation on Windows. If $fname already exists, it is # overwritten, even if it's flagged as read-only. # proc proj-file-write {args} { if {"-ro" eq [lindex $args 0]} { lassign $args ro fname content } else { set ro "" lassign $args fname content } file delete -force -- $fname; # in case it's read-only set f [open $fname wb] puts -nonewline $f $content close $f if {"" ne $ro} { catch { exec chmod -w $fname #file attributes -w $fname; #jimtcl has no 'attributes' } } } # # @proj-check-compile-commands ?configFlag? # # Checks the compiler for compile_commands.json support. If passed an # argument it is assumed to be the name of an autosetup boolean config # which controls whether to run/skip this check. # # Returns 1 if supported, else 0, and defines HAVE_COMPILE_COMMANDS to # that value. Defines MAKE_COMPILATION_DB to "yes" if supported, "no" # if not. The use of MAKE_COMPILATION_DB is deprecated/discouraged: # HAVE_COMPILE_COMMANDS is preferred. # # ACHTUNG: this test has a long history of false positive results # because of compilers reacting differently to the -MJ flag. # proc proj-check-compile-commands {{configFlag {}}} { msg-checking "compile_commands.json support... " if {"" ne $configFlag && ![proj-opt-truthy $configFlag]} { msg-result "explicitly disabled" define HAVE_COMPILE_COMMANDS 0 define MAKE_COMPILATION_DB no return 0 } else { if {[cctest -lang c -cflags {/dev/null -MJ} -source {}]} { # This test reportedly incorrectly succeeds on one of # Martin G.'s older systems. drh also reports a false # positive on an unspecified older Mac system. msg-result "compiler supports compile_commands.json" define MAKE_COMPILATION_DB yes; # deprecated define HAVE_COMPILE_COMMANDS 1 return 1 } else { msg-result "compiler does not support compile_commands.json" define MAKE_COMPILATION_DB no define HAVE_COMPILE_COMMANDS 0 return 0 } } } # # @proj-touch filename # # Runs the 'touch' external command on one or more files, ignoring any # errors. # proc proj-touch {filename} { catch { exec touch {*}$filename } } # # @proj-make-from-dot-in ?-touch? infile ?outfile? # # Uses [make-template] to create makefile(-like) file(s) $outfile from # $infile but explicitly makes the output read-only, to avoid # inadvertent editing (who, me?). # # If $outfile is empty then: # # - If $infile is a 2-element list, it is assumed to be an in/out pair, # and $outfile is set from the 2nd entry in that list. Else... # # - $outfile is set to $infile stripped of its extension. # # If the first argument is -touch then the generated file is touched # to update its timestamp. This can be used as a workaround for # cases where (A) autosetup does not update the file because it was # not really modified and (B) the file *really* needs to be updated to # please the build process. # # Failures when running chmod or touch are silently ignored. # proc proj-make-from-dot-in {args} { set fIn "" set fOut "" set touch 0 if {[lindex $args 0] eq "-touch"} { set touch 1 lassign $args - fIn fOut } else { lassign $args fIn fOut } if {"" eq $fOut} { if {[llength $fIn]>1} { lassign $fIn fIn fOut } else { set fOut [file rootname $fIn] } } #puts "filenames=$filename" if {[file exists $fOut]} { catch { exec chmod u+w $fOut } } #puts "making template: $fIn ==> $fOut" #define-push {top_srcdir} { #puts "--- $fIn $fOut top_srcdir=[get-define top_srcdir]" make-template $fIn $fOut #puts "--- $fIn $fOut top_srcdir=[get-define top_srcdir]" # make-template modifies top_srcdir #} if {$touch} { proj-touch $fOut } catch { exec chmod -w $fOut #file attributes -w $f; #jimtcl has no 'attributes' } } # # @proj-check-profile-flag ?flagname? # # Checks for the boolean configure option named by $flagname. If set, # it checks if $CC seems to refer to gcc. If it does (or appears to) # then it defines CC_PROFILE_FLAG to "-pg" and returns 1, else it # defines CC_PROFILE_FLAG to "" and returns 0. # # Note that the resulting flag must be added to both CFLAGS and # LDFLAGS in order for binaries to be able to generate "gmon.out". In # order to avoid potential problems with escaping, space-containing # tokens, and interfering with autosetup's use of these vars, this # routine does not directly modify CFLAGS or LDFLAGS. # proc proj-check-profile-flag {{flagname profile}} { #puts "flagname=$flagname ?[proj-opt-truthy $flagname]?" if {[proj-opt-truthy $flagname]} { set CC [get-define CC] regsub {.*ccache *} $CC "" CC # ^^^ if CC="ccache gcc" then [exec] treats "ccache gcc" as a # single binary name and fails. So strip any leading ccache part # for this purpose. if { ![catch { exec $CC --version } msg]} { if {[string first gcc $CC] != -1} { define CC_PROFILE_FLAG "-pg" return 1 } } } define CC_PROFILE_FLAG "" return 0 } # # @proj-looks-like-windows ?key? # # Returns 1 if this appears to be a Windows environment (MinGw, # Cygwin, MSys), else returns 0. The optional argument is the name of # an autosetup define which contains platform name info, defaulting to # "host" (meaning, somewhat counterintuitively, the target system, not # the current host). The other legal value is "build" (the build # machine, i.e. the local host). If $key == "build" then some # additional checks may be performed which are not applicable when # $key == "host". # proc proj-looks-like-windows {{key host}} { global autosetup switch -glob -- [get-define $key] { *-*-ming* - *-*-cygwin - *-*-msys - *windows* { return 1 } } if {$key eq "build"} { # These apply only to the local OS, not a cross-compilation target, # as the above check potentially can. if {$::autosetup(iswin)} { return 1 } if {[find-an-executable cygpath] ne "" || $::tcl_platform(os) eq "Windows NT"} { return 1 } } return 0 } # # @proj-looks-like-mac ?key? # # Looks at either the 'host' (==compilation target platform) or # 'build' (==the being-built-on platform) define value and returns if # if that value seems to indicate that it represents a Mac platform, # else returns 0. # proc proj-looks-like-mac {{key host}} { switch -glob -- [get-define $key] { *apple* { return 1 } default { return 0 } } } # # @proj-exe-extension # # Checks autosetup's "host" and "build" defines to see if the build # host and target are Windows-esque (Cygwin, MinGW, MSys). If the # build environment is then BUILD_EXEEXT is [define]'d to ".exe", else # "". If the target, a.k.a. "host", is then TARGET_EXEEXT is # [define]'d to ".exe", else "". # proc proj-exe-extension {} { set rH "" set rB "" if {[proj-looks-like-windows host]} { set rH ".exe" } if {[proj-looks-like-windows build]} { set rB ".exe" } define BUILD_EXEEXT $rB define TARGET_EXEEXT $rH } # # @proj-dll-extension # # Works like proj-exe-extension except that it defines BUILD_DLLEXT # and TARGET_DLLEXT to one of (.so, ,dll, .dylib). # # Trivia: for .dylib files, the linker needs the -dynamiclib flag # instead of -shared. # proc proj-dll-extension {} { set inner {{key} { switch -glob -- [get-define $key] { *apple* { return ".dylib" } *-*-ming* - *-*-cygwin - *-*-msys { return ".dll" } default { return ".so" } } }} define BUILD_DLLEXT [apply $inner build] define TARGET_DLLEXT [apply $inner host] } # # @proj-lib-extension # # Static-library counterpart of proj-dll-extension. Defines # BUILD_LIBEXT and TARGET_LIBEXT to the conventional static library # extension for the being-built-on resp. the target platform. # proc proj-lib-extension {} { set inner {{key} { switch -glob -- [get-define $key] { *-*-ming* - *-*-cygwin - *-*-msys { return ".a" # ^^^ this was ".lib" until 2025-02-07. See # https://sqlite.org/forum/forumpost/02db2d4240 } default { return ".a" } } }} define BUILD_LIBEXT [apply $inner build] define TARGET_LIBEXT [apply $inner host] } # # @proj-file-extensions # # Calls all of the proj-*-extension functions. # proc proj-file-extensions {} { proj-exe-extension proj-dll-extension proj-lib-extension } # # @proj-affirm-files-exist ?-v? filename... # # Expects a list of file names. If any one of them does not exist in # the filesystem, it fails fatally with an informative message. # Returns the last file name it checks. If the first argument is -v # then it emits msg-checking/msg-result messages for each file. # proc proj-affirm-files-exist {args} { set rc "" set verbose 0 if {[lindex $args 0] eq "-v"} { set verbose 1 set args [lrange $args 1 end] } foreach f $args { if {$verbose} { msg-checking "Looking for $f ... " } if {![file exists $f]} { user-error "not found: $f" } if {$verbose} { msg-result "" } set rc $f } return rc } # # @proj-check-emsdk # # Emscripten is used for doing in-tree builds of web-based WASM stuff, # as opposed to WASI-based WASM or WASM binaries we import from other # places. This is only set up for Unix-style OSes and is untested # anywhere but Linux. Requires that the --with-emsdk flag be # registered with autosetup. # # It looks for the SDK in the location specified by --with-emsdk. # Values of "" or "auto" mean to check for the environment var EMSDK # (which gets set by the emsdk_env.sh script from the SDK) or that # same var passed to configure. # # If the given directory is found, it expects to find emsdk_env.sh in # that directory, as well as the emcc compiler somewhere under there. # # If the --with-emsdk[=DIR] flag is explicitly provided and the SDK is # not found then a fatal error is generated, otherwise failure to find # the SDK is not fatal. # # Defines the following: # # - HAVE_EMSDK = 0 or 1 (this function's return value) # - EMSDK_HOME = "" or top dir of the emsdk # - EMSDK_ENV_SH = "" or $EMSDK_HOME/emsdk_env.sh # - BIN_EMCC = "" or $EMSDK_HOME/upstream/emscripten/emcc # # Returns 1 if EMSDK_ENV_SH is found, else 0. If EMSDK_HOME is not empty # but BIN_EMCC is then emcc was not found in the EMSDK_HOME, in which # case we have to rely on the fact that sourcing $EMSDK_ENV_SH from a # shell will add emcc to the $PATH. # proc proj-check-emsdk {} { set emsdkHome [opt-val with-emsdk] define EMSDK_HOME "" define EMSDK_ENV_SH "" define BIN_EMCC "" set hadValue [llength $emsdkHome] msg-checking "Emscripten SDK? " if {$emsdkHome in {"" "auto"}} { # Check the environment. $EMSDK gets set by sourcing emsdk_env.sh. set emsdkHome [get-env EMSDK ""] } set rc 0 if {$emsdkHome ne ""} { define EMSDK_HOME $emsdkHome set emsdkEnv "$emsdkHome/emsdk_env.sh" if {[file exists $emsdkEnv]} { msg-result "$emsdkHome" define EMSDK_ENV_SH $emsdkEnv set rc 1 set emcc "$emsdkHome/upstream/emscripten/emcc" if {[file exists $emcc]} { define BIN_EMCC $emcc } } else { msg-result "emsdk_env.sh not found in $emsdkHome" } } else { msg-result "not found" } if {$hadValue && 0 == $rc} { # Fail if it was explicitly requested but not found proj-fatal "Cannot find the Emscripten SDK" } define HAVE_EMSDK $rc return $rc } # # @proj-cc-check-Wl-flag ?flag ?args?? # # Checks whether the given linker flag (and optional arguments) can be # passed from the compiler to the linker using one of these formats: # # - -Wl,flag[,arg1[,...argN]] # - -Wl,flag -Wl,arg1 ...-Wl,argN # # If so, that flag string is returned, else an empty string is # returned. # proc proj-cc-check-Wl-flag {args} { cc-with {-link 1} { # Try -Wl,flag,...args set fli "-Wl" foreach f $args { append fli ",$f" } if {[cc-check-flags $fli]} { return $fli } # Try -Wl,flag -Wl,arg1 ...-Wl,argN set fli "" foreach f $args { append fli "-Wl,$f " } if {[cc-check-flags $fli]} { return [string trim $fli] } return "" } } # # @proj-check-rpath # # Tries various approaches to handling the -rpath link-time # flag. Defines LDFLAGS_RPATH to that/those flag(s) or an empty # string. Returns 1 if it finds an option, else 0. # # By default, the rpath is set to $prefix/lib. However, if either of # --exec-prefix=... or --libdir=... are explicitly passed to # configure then [get-define libdir] is used (noting that it derives # from exec-prefix by default). # proc proj-check-rpath {} { if {[proj-opt-was-provided libdir] || [proj-opt-was-provided exec-prefix]} { set lp "[get-define libdir]" } else { set lp "[get-define prefix]/lib" } # If we _don't_ use cc-with {} here (to avoid updating the global # CFLAGS or LIBS or whatever it is that cc-check-flags updates) then # downstream tests may fail because the resulting rpath gets # implicitly injected into them. cc-with {-link 1} { if {[cc-check-flags "-rpath $lp"]} { define LDFLAGS_RPATH "-rpath $lp" } else { set wl [proj-cc-check-Wl-flag -rpath $lp] if {"" eq $wl} { set wl [proj-cc-check-Wl-flag -R$lp] } define LDFLAGS_RPATH $wl } } expr {"" ne [get-define LDFLAGS_RPATH]} } # # @proj-check-soname ?libname? # # Checks whether CC supports the -Wl,soname,lib... flag. If so, it # returns 1 and defines LDFLAGS_SONAME_PREFIX to the flag's prefix, to # which the client would need to append "libwhatever.N". If not, it # returns 0 and defines LDFLAGS_SONAME_PREFIX to an empty string. # # The libname argument is only for purposes of running the flag # compatibility test, and is not included in the resulting # LDFLAGS_SONAME_PREFIX. It is provided so that clients may # potentially avoid some end-user confusion by using their own lib's # name here (which shows up in the "checking..." output). # proc proj-check-soname {{libname "libfoo.so.0"}} { cc-with {-link 1} { if {[cc-check-flags "-Wl,-soname,${libname}"]} { define LDFLAGS_SONAME_PREFIX "-Wl,-soname," return 1 } else { define LDFLAGS_SONAME_PREFIX "" return 0 } } } # # @proj-check-fsanitize ?list-of-opts? # # Checks whether CC supports -fsanitize=X, where X is each entry of # the given list of flags. If any of those flags are supported, it # returns the string "-fsanitize=X..." where X... is a comma-separated # list of all flags from the original set which are supported. If none # of the given options are supported then it returns an empty string. # # Example: # # set f [proj-check-fsanitize {address bounds-check just-testing}] # # Will, on many systems, resolve to "-fsanitize=address,bounds-check", # but may also resolve to "-fsanitize=address". # proc proj-check-fsanitize {{opts {address bounds-strict}}} { set sup {} foreach opt $opts { # -nooutput is used because -fsanitize=hwaddress will otherwise # pass this test on x86_64, but then warn at build time that # "hwaddress is not supported for this target". cc-with {-nooutput 1} { if {[cc-check-flags "-fsanitize=$opt"]} { lappend sup $opt } } } if {[llength $sup] > 0} { return "-fsanitize=[join $sup ,]" } return "" } # # Internal helper for proj-dump-defs-json. Expects to be passed a # [define] name and the variadic $args which are passed to # proj-dump-defs-json. If it finds a pattern match for the given # $name in the various $args, it returns the type flag for that $name, # e.g. "-str" or "-bare", else returns an empty string. # proc proj-defs-type_ {name spec} { foreach {type patterns} $spec { foreach pattern $patterns { if {[string match $pattern $name]} { return $type } } } return "" } # # Internal helper for proj-defs-format_: returns a JSON-ish quoted # form of the given string-type values. It only performs the most # basic of escaping. The input must not contain any control # characters. # proc proj-quote-str_ {value} { return \"[string map [list \\ \\\\ \" \\\"] $value]\" } # # An internal impl detail of proj-dump-defs-json. Requires a data # type specifier, as used by make-config-header, and a value. Returns # the formatted value or the value $::proj__Config(defs-skip) if the caller # should skip emitting that value. # set ::proj__Config(defs-skip) "-proj-defs-format_ sentinel" proc proj-defs-format_ {type value} { switch -exact -- $type { -bare { # Just output the value unchanged } -none { set value $::proj__Config(defs-skip) } -str { set value [proj-quote-str_ $value] } -auto { # Automatically determine the type if {![string is integer -strict $value]} { set value [proj-quote-str_ $value] } } -array { set ar {} foreach v $value { set v [proj-defs-format_ -auto $v] if {$::proj__Config(defs-skip) ne $v} { lappend ar $v } } set value "\[ [join $ar {, }] \]" } "" { set value $::proj__Config(defs-skip) } default { proj-fatal "Unknown type in proj-dump-defs-json: $type" } } return $value } # # @proj-dump-defs-json outfile ...flags # # This function works almost identically to autosetup's # make-config-header but emits its output in JSON form. It is not a # fully-functional JSON emitter, and will emit broken JSON for # complicated outputs, but should be sufficient for purposes of # emitting most configure vars (numbers and simple strings). # # In addition to the formatting flags supported by make-config-header, # it also supports: # # -array {patterns...} # # Any defines matching the given patterns will be treated as a list of # values, each of which will be formatted as if it were in an -auto {...} # set, and the define will be emitted to JSON in the form: # # "ITS_NAME": [ "value1", ...valueN ] # # Achtung: if a given -array pattern contains values which themselves # contains spaces... # # define-append foo {"-DFOO=bar baz" -DBAR="baz barre"} # # will lead to: # # ["-DFOO=bar baz", "-DBAR=\"baz", "barre\""] # # Neither is especially satisfactory (and the second is useless), and # handling of such values is subject to change if any such values ever # _really_ need to be processed by our source trees. # proc proj-dump-defs-json {file args} { file mkdir [file dirname $file] set lines {} lappend args -bare {SIZEOF_* HAVE_DECL_*} -auto HAVE_* foreach n [lsort [dict keys [all-defines]]] { set type [proj-defs-type_ $n $args] set value [proj-defs-format_ $type [get-define $n]] if {$::proj__Config(defs-skip) ne $value} { lappend lines "\"$n\": ${value}" } } set buf {} lappend buf [join $lines ",\n"] write-if-changed $file $buf { msg-result "Created $file" } } # # @proj-xfer-option-aliases map # # Expects a list of pairs of configure flags which have been # registered with autosetup, in this form: # # { alias1 => canonical1 # aliasN => canonicalN ... } # # The names must not have their leading -- part and must be in the # form which autosetup will expect for passing to [opt-val NAME] and # friends. # # Comment lines are permitted in the input. # # For each pair of ALIAS and CANONICAL, if --ALIAS is provided but # --CANONICAL is not, the value of the former is copied to the # latter. If --ALIAS is not provided, this is a no-op. If both have # explicitly been provided a fatal usage error is triggered. # # Motivation: autosetup enables "hidden aliases" in [options] lists, # and elides the aliases from --help output but does no further # handling of them. For example, when --alias is a hidden alias of # --canonical and a user passes --alias=X, [opt-val canonical] returns # no value. i.e. the script must check both [opt-val alias] and # [opt-val canonical]. The intent here is that this function be # passed such mappings immediately after [options] is called, to carry # over any values from hidden aliases into their canonical names, such # that [opt-value canonical] will return X if --alias=X is passed to # configure. # # That said: autosetup's [opt-str] does support alias forms, but it # requires that the caller know all possible aliases. It's simpler, in # terms of options handling, if there's only a single canonical name # which each down-stream call of [opt-...] has to know. # proc proj-xfer-options-aliases {mapping} { foreach {hidden - canonical} [proj-strip-hash-comments $mapping] { if {[proj-opt-was-provided $hidden]} { if {[proj-opt-was-provided $canonical]} { proj-fatal "both --$canonical and its alias --$hidden were used. Use only one or the other." } else { proj-opt-set $canonical [opt-val $hidden] } } } } # # Arguable/debatable... # # When _not_ cross-compiling and CC_FOR_BUILD is _not_ explicitly # specified, force CC_FOR_BUILD to be the same as CC, so that: # # ./configure CC=clang # # will use CC_FOR_BUILD=clang, instead of cc, for building in-tree # tools. This is based off of an email discussion and is thought to # be likely to cause less confusion than seeing 'cc' invocations # when when the user passes CC=clang. # # Sidebar: if we do this before the cc package is installed, it gets # reverted by that package. Ergo, the cc package init will tell the # user "Build C compiler...cc" shortly before we tell them otherwise. # proc proj-redefine-cc-for-build {} { if {![proj-is-cross-compiling] && [get-define CC] ne [get-define CC_FOR_BUILD] && "nope" eq [get-env CC_FOR_BUILD "nope"]} { user-notice "Re-defining CC_FOR_BUILD to CC=[get-define CC]. To avoid this, explicitly pass CC_FOR_BUILD=..." define CC_FOR_BUILD [get-define CC] } } # # @proj-which-linenoise headerFile # # Attempts to determine whether the given linenoise header file is of # the "antirez" or "msteveb" flavor. It returns 2 for msteveb, else 1 # (it does not validate that the header otherwise contains the # linenoise API). # proc proj-which-linenoise {dotH} { set srcHeader [proj-file-content $dotH] if {[string match *userdata* $srcHeader]} { return 2 } else { return 1 } } # # @proj-remap-autoconf-dir-vars # # "Re-map" the autoconf-conventional --XYZdir flags into something # which is more easily overridable from a make invocation. # # Based off of notes in . # # Consider: # # $ ./configure --prefix=/foo # $ make install prefix=/blah # # In that make invocation, $(libdir) would, at make-time, normally be # hard-coded to /foo/lib, rather than /blah/lib. That happens because # autosetup exports conventional $prefix-based values for the numerous # autoconfig-compatible XYZdir vars at configure-time. What we would # normally want, however, is that --libdir derives from the make-time # $(prefix). The distinction between configure-time and make-time is # the significant factor there. # # This function attempts to reconcile those vars in such a way that # they will derive, at make-time, from $(prefix) in a conventional # manner unless they are explicitly overridden at configure-time, in # which case those overrides takes precedence. # # Each autoconf-relvant --XYZ flag which is explicitly passed to # configure is exported as-is, as are those which default to some # top-level system directory, e.g. /etc or /var. All which derive # from either $prefix or $exec_prefix are exported in the form of a # Makefile var reference, e.g. libdir=${exec_prefix}/lib. Ergo, if # --exec-prefix=FOO is passed to configure, libdir will still derive, # at make-time, from whatever exec_prefix is passed to make, and will # use FOO if exec_prefix is not overridden at make-time. Without this # post-processing, libdir would be cemented in as FOO/lib at # configure-time, so could be tedious to override properly via a make # invocation. # proc proj-remap-autoconf-dir-vars {} { set prefix [get-define prefix] set exec_prefix [get-define exec_prefix $prefix] # The following var derefs must be formulated such that they are # legal for use in (A) makefiles, (B) pkgconfig files, and (C) TCL's # [subst] command. i.e. they must use the form ${X}. foreach {flag makeVar makeDeref} { exec-prefix exec_prefix ${prefix} datadir datadir ${prefix}/share mandir mandir ${datadir}/man includedir includedir ${prefix}/include bindir bindir ${exec_prefix}/bin libdir libdir ${exec_prefix}/lib sbindir sbindir ${exec_prefix}/sbin sysconfdir sysconfdir /etc sharedstatedir sharedstatedir ${prefix}/com localstatedir localstatedir /var runstatedir runstatedir /run infodir infodir ${datadir}/info libexecdir libexecdir ${exec_prefix}/libexec } { if {[proj-opt-was-provided $flag]} { define $makeVar [join [opt-val $flag]] } else { define $makeVar [join $makeDeref] } # Maintenance reminder: the [join] call is to avoid {braces} # around the output when someone passes in, # e.g. --libdir=\${prefix}/foo/bar. Debian's SQLite package build # script does that. } } # # @proj-env-file flag ?default? # # If a file named .env-$flag exists, this function returns a # trimmed copy of its contents, else it returns $dflt. The intended # usage is that things like developer-specific CFLAGS preferences can # be stored in .env-CFLAGS. # proc proj-env-file {flag {dflt ""}} { set fn ".env-${flag}" if {[file readable $fn]} { return [proj-file-content -trim $fn] } return $dflt } # # @proj-get-env var ?default? # # Extracts the value of "environment" variable $var from the first of # the following places where it's defined: # # - Passed to configure as $var=... # - Exists as an environment variable # - A file named .env-$var (see [proj-env-file]) # # If none of those are set, $dflt is returned. # proc proj-get-env {var {dflt ""}} { get-env $var [proj-env-file $var $dflt] } # # @proj-scope ?lvl? # # Returns the name of the _calling_ proc from ($lvl + 1) levels up the # call stack (where the caller's level will be 1 up from _this_ # call). If $lvl would resolve to global scope "global scope" is # returned and if it would be negative then a string indicating such # is returned (as opposed to throwing an error). # proc proj-scope {{lvl 0}} { #uplevel [expr {$lvl + 1}] {lindex [info level 0] 0} set ilvl [info level] set offset [expr {$ilvl - $lvl - 1}] if { $offset < 0} { return "invalid scope ($offset)" } elseif { $offset == 0} { return "global scope" } else { return [lindex [info level $offset] 0] } } # # Deprecated name of [proj-scope]. # proc proj-current-scope {{lvl 0}} { puts stderr \ "Deprecated proj-current-scope called from [proj-scope 1]. Use proj-scope instead." proj-scope [incr lvl] } # # Converts parts of tclConfig.sh to autosetup [define]s. # # Expects to be passed the name of a value tclConfig.sh or an empty # string. It converts certain parts of that file's contents to # [define]s (see the code for the whole list). If $tclConfigSh is an # empty string then it [define]s the various vars as empty strings. # proc proj-tclConfig-sh-to-autosetup {tclConfigSh} { set shBody {} set tclVars { TCL_INCLUDE_SPEC TCL_LIBS TCL_LIB_SPEC TCL_STUB_LIB_SPEC TCL_EXEC_PREFIX TCL_PREFIX TCL_VERSION TCL_MAJOR_VERSION TCL_MINOR_VERSION TCL_PACKAGE_PATH TCL_PATCH_LEVEL TCL_SHLIB_SUFFIX } # Build a small shell script which proxies the $tclVars from # $tclConfigSh into autosetup code... lappend shBody "if test x = \"x${tclConfigSh}\"; then" foreach v $tclVars { lappend shBody "$v= ;" } lappend shBody "else . \"${tclConfigSh}\"; fi" foreach v $tclVars { lappend shBody "echo define $v {\$$v} ;" } lappend shBody "exit" set shBody [join $shBody "\n"] #puts "shBody=$shBody\n"; exit eval [exec echo $shBody | sh] } # # @proj-tweak-default-env-dirs # # This function is not useful before [use system] is called to set up # --prefix and friends. It should be called as soon after [use system] # as feasible. # # For certain target environments, if --prefix is _not_ passed in by # the user, set the prefix to an environment-specific default. For # such environments its does [define prefix ...] and [proj-opt-set # prefix ...], but it does not process vars derived from the prefix, # e.g. exec-prefix. To do so it is generally necessary to also call # proj-remap-autoconf-dir-vars late in the config process (immediately # before ".in" files are filtered). # # Similar modifications may be made for --mandir. # # Returns 1 if it modifies the environment, else 0. # proc proj-tweak-default-env-dirs {} { set rc 0 switch -glob -- [get-define host] { *-haiku { if {![proj-opt-was-provided prefix]} { set hdir /boot/home/config/non-packaged proj-opt-set prefix $hdir define prefix $hdir incr rc } if {![proj-opt-was-provided mandir]} { set hdir /boot/system/documentation/man proj-opt-set mandir $hdir define mandir $hdir incr rc } } } return $rc } # # @proj-dot-ins-append file ?fileOut ?postProcessScript?? # # Queues up an autosetup [make-template]-style file to be processed # at a later time using [proj-dot-ins-process]. # # $file is the input file. If $fileOut is empty then this function # derives $fileOut from $file, stripping both its directory and # extension parts. i.e. it defaults to writing the output to the # current directory (typically $::autosetup(builddir)). # # If $postProcessScript is not empty then, during # [proj-dot-ins-process], it will be eval'd immediately after # processing the file. In the context of that script, the vars # $dotInsIn and $dotInsOut will be set to the input and output file # names. This can be used, for example, to make the output file # executable or perform validation on its contents. # # See [proj-dot-ins-process], [proj-dot-ins-list] # proc proj-dot-ins-append {fileIn args} { set srcdir $::autosetup(srcdir) switch -exact -- [llength $args] { 0 { lappend fileIn [file rootname [file tail $fileIn]] "" } 1 { lappend fileIn [join $args] "" } 2 { lappend fileIn {*}$args } default { proj-fatal "Too many arguments: $fileIn $args" } } #puts "******* [proj-scope]: adding $fileIn" lappend ::proj__Config(dot-in-files) $fileIn } # # @proj-dot-ins-list # # Returns the current list of [proj-dot-ins-append]'d files, noting # that each entry is a 3-element list of (inputFileName, # outputFileName, postProcessScript). # proc proj-dot-ins-list {} { return $::proj__Config(dot-in-files) } # # @proj-dot-ins-process ?-touch? ?-validate? ?-clear? # # Each file which has previously been passed to [proj-dot-ins-append] # is processed, with its passing its in-file out-file names to # [proj-make-from-dot-in]. # # The intent is that a project accumulate any number of files to # filter and delay their actual filtering until the last stage of the # configure script, calling this function at that time. # # Optional flags: # # -touch: gets passed on to [proj-make-from-dot-in] # # -validate: after processing each file, before running the file's # associated script, if any, it runs the file through # proj-validate-no-unresolved-ats, erroring out if that does. # # -clear: after processing, empty the dot-ins list. This effectively # makes proj-dot-ins-append available for re-use. # proc proj-dot-ins-process {args} { proj-parse-simple-flags args flags { -touch "" {return "-touch"} -clear 0 {expr 1} -validate 0 {expr 1} } if {[llength $args] > 0} { error "Invalid argument to [proj-scope]: $args" } foreach f $::proj__Config(dot-in-files) { proj-assert {3==[llength $f]} \ "Expecting proj-dot-ins-list to be stored in 3-entry lists" lassign $f fIn fOut fScript #puts "DOING $fIn ==> $fOut" proj-make-from-dot-in {*}$flags(-touch) $fIn $fOut if {$flags(-validate)} { proj-validate-no-unresolved-ats $fOut } if {"" ne $fScript} { uplevel 1 [join [list set dotInsIn $fIn \; \ set dotInsOut $fOut \; \ eval \{${fScript}\} \; \ unset dotInsIn dotInsOut]] } } if {$flags(-clear)} { set ::proj__Config(dot-in-files) [list] } } # # @proj-validate-no-unresolved-ats filenames... # # For each filename given to it, it validates that the file has no # unresolved @VAR@ references. If it finds any, it produces an error # with location information. # # Exception: if a filename matches the pattern {*[Mm]ake*} AND a given # line begins with a # (not including leading whitespace) then that # line is ignored for purposes of this validation. The intent is that # @VAR@ inside of makefile comments should not (necessarily) cause # validation to fail, as it's sometimes convenient to comment out # sections during development of a configure script and its # corresponding makefile(s). # proc proj-validate-no-unresolved-ats {args} { foreach f $args { set lnno 1 set isMake [string match {*[Mm]ake*} $f] foreach line [proj-file-content-list $f] { if {!$isMake || ![string match "#*" [string trimleft $line]]} { if {[regexp {(@[A-Za-z0-9_]+@)} $line match]} { error "Unresolved reference to $match at line $lnno of $f" } } incr lnno } } } # # @proj-first-file-found tgtVar fileList # # Searches $fileList for an existing file. If one is found, its name # is assigned to tgtVar and 1 is returned, else tgtVar is set to "" # and 0 is returned. # proc proj-first-file-found {tgtVar fileList} { upvar $tgtVar tgt foreach f $fileList { if {[file exists $f]} { set tgt $f return 1 } } set tgt "" return 0 } # # Defines $defName to contain makefile recipe commands for re-running # the configure script with its current set of $::argv flags. This # can be used to automatically reconfigure. # proc proj-setup-autoreconfig {defName} { define $defName \ [join [list \ cd \"$::autosetup(builddir)\" \ && [get-define AUTOREMAKE "error - missing @AUTOREMAKE@"]]] } # # @prop-append-to defineName args... # # A proxy for Autosetup's [define-append]. Appends all non-empty $args # to [define-append $defineName]. # proc proj-define-append {defineName args} { foreach a $args { if {"" ne $a} { define-append $defineName {*}$a } } } # # @prod-define-amend ?-p|-prepend? ?-d|-define? defineName args... # # A proxy for Autosetup's [define-append]. # # Appends all non-empty $args to the define named by $defineName. If # one of (-p | -prepend) are used it instead prepends them, in their # given order, to $defineName. # # If -define is used then each argument is assumed to be a [define]'d # flag and [get-define X ""] is used to fetch it. # # Re. linker flags: typically, -lXYZ flags need to be in "reverse" # order, with each -lY resolving symbols for -lX's to its left. This # order is largely historical, and not relevant on all environments, # but it is technically correct and still relevant on some # environments. # # See: proj-append-to # proc proj-define-amend {args} { set defName "" set prepend 0 set isdefs 0 set xargs [list] foreach arg $args { switch -exact -- $arg { "" {} -p - -prepend { incr prepend } -d - -define { incr isdefs } default { if {"" eq $defName} { set defName $arg } else { lappend xargs $arg } } } } if {"" eq $defName} { proj-error "Missing defineName argument in call from [proj-scope 1]" } if {$isdefs} { set args $xargs set xargs [list] foreach arg $args { lappend xargs [get-define $arg ""] } set args $xargs } # puts "**** args=$args" # puts "**** xargs=$xargs" set args $xargs if {$prepend} { lappend args {*}[get-define $defName ""] define $defName [join $args]; # join to eliminate {} entries } else { proj-define-append $defName {*}$args } } # # @proj-define-to-cflag ?-list? ?-quote? ?-zero-undef? defineName... # # Treat each argument as the name of a [define] and renders it like a # CFLAGS value in one of the following forms: # # -D$name # -D$name=integer (strict integer matches only) # '-D$name=value' (without -quote) # '-D$name="value"' (with -quote) # # It treats integers as numbers and everything else as a quoted # string, noting that it does not handle strings which themselves # contain quotes. # # The -zero-undef flag causes no -D to be emitted for integer values # of 0. # # By default it returns the result as string of all -D... flags, # but if passed the -list flag it will return a list of the # individual CFLAGS. # proc proj-define-to-cflag {args} { set rv {} proj-parse-simple-flags args flags { -list 0 {expr 1} -quote 0 {expr 1} -zero-undef 0 {expr 1} } foreach d $args { set v [get-define $d ""] set li {} if {"" eq $d} { set v "-D${d}" } elseif {[string is integer -strict $v]} { if {!$flags(-zero-undef) || $v ne "0"} { set v "-D${d}=$v" } } elseif {$flags(-quote)} { set v "'-D${d}=\"$v\"'" } else { set v "'-D${d}=$v'" } lappend rv $v } expr {$flags(-list) ? $rv : [join $rv]} } if {0} { # Turns out that autosetup's [options-add] essentially does exactly # this... # A list of lists of Autosetup [options]-format --flags definitions. # Append to this using [proj-options-add] and use # [proj-options-combine] to merge them into a single list for passing # to [options]. # set ::proj__Config(extra-options) {} # @proj-options-add list # # Adds a list of options to the pending --flag processing. It must be # in the format used by Autosetup's [options] function. # # This will have no useful effect if called from after [options] # is called. # # Use [proj-options-combine] to get a combined list of all added # options. # # PS: when writing this i wasn't aware of autosetup's [options-add], # works quite similarly. Only the timing is different. proc proj-options-add {list} { lappend ::proj__Config(extra-options) $list } # @proj-options-combine list1 ?...listN? # # Expects each argument to be a list of options compatible with # autosetup's [options] function. This function concatenates the # contents of each list into a new top-level list, stripping the outer # list part of each argument, and returning that list # # If passed no arguments, it uses the list generated by calls to # [proj-options-add]. proc proj-options-combine {args} { set rv [list] if {0 == [llength $args]} { set args $::proj__Config(extra-options) } foreach e $args { lappend rv {*}$e } return $rv } }; # proj-options-* # Internal cache for use via proj-cache-*. array set proj__Cache {} # # @proj-cache-key arg {addLevel 0} # # Helper to generate cache keys for [proj-cache-*]. # # $addLevel should almost always be 0. # # Returns a cache key for the given argument: # # integer: relative call stack levels to get the scope name of for # use as a key. [proj-scope [expr {1 + $arg + addLevel}]] is # then used to generate the key. i.e. the default of 0 uses the # calling scope's name as the key. # # Anything else: returned as-is # proc proj-cache-key {arg {addLevel 0}} { if {[string is integer -strict $arg]} { return [proj-scope [expr {$arg + $addLevel + 1}]] } return $arg } # # @proj-cache-set ?-key KEY? ?-level 0? value # # Sets a feature-check cache entry with the given key. # # See proj-cache-key for -key's and -level's semantics, noting that # this function adds one to -level for purposes of that call. proc proj-cache-set {args} { proj-parse-simple-flags args flags { -key => 0 -level => 0 } lassign $args val set key [proj-cache-key $flags(-key) [expr {1 + $flags(-level)}]] #puts "** fcheck set $key = $val" set ::proj__Cache($key) $val } # # @proj-cache-remove ?key? ?addLevel? # # Removes an entry from the proj-cache. proc proj-cache-remove {{key 0} {addLevel 0}} { set key [proj-cache-key $key [expr {1 + $addLevel}]] set rv "" if {[info exists ::proj__Cache($key)]} { set rv $::proj__Cache($key) unset ::proj__Cache($key) } return $rv; } # # @proj-cache-check ?-key KEY? ?-level LEVEL? tgtVarName # # Checks for a feature-check cache entry with the given key. # # If the feature-check cache has a matching entry then this function # assigns its value to tgtVar and returns 1, else it assigns tgtVar to # "" and returns 0. # # See proj-cache-key for $key's and $addLevel's semantics, noting that # this function adds one to $addLevel for purposes of that call. proc proj-cache-check {args} { proj-parse-simple-flags args flags { -key => 0 -level => 0 } lassign $args tgtVar upvar $tgtVar tgt set rc 0 set key [proj-cache-key $flags(-key) [expr {1 + $flags(-level)}]] #puts "** fcheck get key=$key" if {[info exists ::proj__Cache($key)]} { set tgt $::proj__Cache($key) incr rc } else { set tgt "" } return $rc } # # @proj-coalesce ...args # # Returns the first argument which is not empty (eq ""), or an empty # string on no match. proc proj-coalesce {args} { foreach arg $args { if {"" ne $arg} { return $arg } } return "" } # # @proj-parse-simple-flags ... # # A helper to parse flags from proc argument lists. # # Expects a list of arguments to parse, an array name to store any # -flag values to, and a prototype object which declares the flags. # # The prototype must be a list in one of the following forms: # # -flag defaultValue {script} # # -flag => defaultValue # -----^--^ (with spaces there!) # # Repeated for each flag. # # The first form represents a basic flag with no associated # following argument. The second form extracts its value # from the following argument in $argvName. # # The first argument to this function is the name of a var holding the # args to parse. It will be overwritten, possibly with a smaller list. # # The second argument the name of an array variable to create in the # caller's scope. (Pneumonic: => points to the next argument.) # # For the first form of flag, $script is run in the caller's scope if # $argv contains -flag, and the result of that script is the new value # for $tgtArrayName(-flag). This function intercepts [return $val] # from $script. Any empty script will result in the flag having "" # assigned to it. # # The args list is only inspected until the first argument which is # not described by $prototype. i.e. the first "non-flag" (not counting # values consumed for flags defined like --flag=>default). # # If a "--" flag is encountered, no more arguments are inspected as # flags. If "--" is the first non-flag argument, the "--" flag is # removed from the results but all remaining arguments are passed # through. If "--" appears after the first non-flag, it is retained. # # This function assumes that each flag is unique, and using a flag # more than once behaves in a last-one-wins fashion. # # Any argvName entries not described in $prototype are not treated as # flags. # # Returns the number of flags it processed in $argvName. # # Example: # # set args [list -foo -bar {blah} 8 9 10 -theEnd] # proj-parse-simple-flags args flags { # -foo 0 {expr 1} # -bar => 0 # -no-baz 2 {return 0} # } # # After that $flags would contain {-foo 1 -bar {blah} -no-baz 2} # and $args would be {8 9 10 -theEnd}. # # Potential TODOs: consider using lappend instead of set so that any # given flag can be used more than once. Or add a syntax to indicate # that multiples are allowed. Also consider searching the whole # argv list, rather than stopping at the first non-flag # proc proj-parse-simple-flags {argvName tgtArrayName prototype} { upvar $argvName argv upvar $tgtArrayName tgt array set dflt {} array set scripts {} array set consuming {} set n [llength $prototype] # Figure out what our flags are... for {set i 0} {$i < $n} {incr i} { set k [lindex $prototype $i] #puts "**** #$i of $n k=$k" proj-assert {[string match -* $k]} \ "Invalid flag value: $k" set v "" set s "" switch -exact -- [lindex $prototype [expr {$i + 1}]] { => { incr i 2 if {$i >= $n} { proj-error "Missing argument for $k => flag" } set consuming($k) 1 set v [lindex $prototype $i] } default { set v [lindex $prototype [incr i]] set s [lindex $prototype [incr i]] set scripts($k) $s } } #puts "**** #$i of $n k=$k v=$v s=$s" set dflt($k) $v } # Now look for those flags in the source list array set tgt [array get dflt] unset dflt set rc 0 set rv {} set skipMode 0 set n [llength $argv] for {set i 0} {$i < $n} {incr i} { set arg [lindex $argv $i] if {$skipMode} { lappend rv $arg } elseif {"--" eq $arg} { incr skipMode } elseif {[info exists tgt($arg)]} { if {[info exists consuming($arg)]} { if {$i + 1 >= $n} { proj-assert 0 {Cannot happen - bounds already checked} } set tgt($arg) [lindex $argv [incr i]] } elseif {"" eq $scripts($arg)} { set tgt($arg) "" } else { #puts "**** running scripts($arg) $scripts($arg)" set code [catch {uplevel 1 $scripts($arg)} xrc xopt] #puts "**** tgt($arg)=$scripts($arg) code=$code rc=$rc" if {$code in {0 2}} { set tgt($arg) $xrc } else { return {*}$xopt $xrc } } incr rc } else { incr skipMode lappend rv $arg } } set argv $rv return $rc } if {$::proj__Config(self-tests)} { apply {{} { #proj-warn "Test code for proj-cache" proj-assert {![proj-cache-check -key here check]} proj-assert {"here" eq [proj-cache-key here]} proj-assert {"" eq $check} proj-cache-set -key here thevalue proj-assert {[proj-cache-check -key here check]} proj-assert {"thevalue" eq $check} proj-assert {![proj-cache-check check]} #puts "*** key = ([proj-cache-key 0])" proj-assert {"" eq $check} proj-cache-set abc proj-assert {[proj-cache-check check]} proj-assert {"abc" eq $check} #parray ::proj__Cache; proj-assert {"" ne [proj-cache-remove]} proj-assert {![proj-cache-check check]} proj-assert {"" eq [proj-cache-remove]} proj-assert {"" eq $check} }} }