#!/usr/bin/env jimsh # jimredo -- djb's build-only redo implementation on pure Jim Tcl # Copyright (C) 2026 Sergey Matveev # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . proc msg {colour text} {puts stderr $text} if {![info exists env(NO_COLOR)]} { set Colours [dict create \ Red "\033\[31m" Grn "\033\[32m" Yel "\033\[33m" Blu "\033\[34m" \ Mag "\033\[35m" Cya "\033\[36m" Wht "\033\[37m" Res "\033\[0m"] proc msg {colour text} { puts -nonewline stderr [dict get $::Colours $colour] puts stderr $text puts -nonewline stderr [dict get $::Colours Res] } } proc path-join {args} { set n [list] foreach e [file split [file join {*}$args]] { if {$e == "."} {continue} if {$e == ".."} { if {[llength $n] > 1} { set n [lrange $n 0 end-1] } } else { lappend n $e } } return [file join {*}$n] } proc path-split {pth} { set l [file split $pth] return [list [file join {*}[lrange $l 0 end-1]] [lindex $l end]] } proc find-do {tgtA {ifcreatesVar ""}} { if {[file exists $tgtA.do]} { return [list 0 $tgtA.do] } if {$ifcreatesVar != ""} { upvar $ifcreatesVar ifcreates lappend ifcreates $tgtA.do } lassign [path-split $tgtA] dir tgt set doOrig $tgt.do set ups 0 set dirAbsPrev "" while {1} { set up [join [lrepeat $ups ..] /] set exts [lrange [split $doOrig .] 1 end] while {[llength $exts] > 0} { set do "default.[join $exts .]" if {($ups > 0) || (($do != $doOrig) && ($do != $tgt))} { set fn [path-join $dir $up $do] if {[file exists $fn]} { return [list $ups $fn] } else { if {$ifcreatesVar != ""} {lappend ifcreates $fn} } } set exts [lrange $exts 1 end] } incr ups set dirAbs [path-join $dir $up .] if {($dirAbs == $::TopDir) || ($dirAbs == $dirAbsPrev) || [file exists $dirAbs/.redo/top]} {return [list 0 ""]} set $dirAbsPrev $dirAbs } } proc run {tgt} { lassign [find-do $tgt] ups do if {$do == ""} { msg Red "$tgt: no .do found" return false } lassign [path-split $tgt] tgtH tgtT set dirPrefix "" if {$ups != 0} { set dirPrefix [file join {*}[lrange [file split $tgtH] end-$($ups - 1) end]] } set cmd [list $do] if {![file executable $do]} { set cmd [list /bin/sh -e$::X $do] } lappend cmd [file join $dirPrefix $tgtT] set basename $tgtT set fns [lrange [split [file tail $do] .] 0 end-1] if {[lindex $fns 0] == "default"} { set l [string length [join [lrange $fns 1 end] .]] if {$l != 0} { set basename [string range $tgtT 0 end-$($l + 1)] } } lappend cmd [file join $dirPrefix $basename] unset basename fns set fn1 [file tempfile $tgtH/.redo.1.XXXXXX] set fn3 [file tempfile $tgtH/.redo.3.XXXXXX] file delete $fn3 lappend cmd $fn3 if {$::D} {msg Yel "$tgt: run: $cmd"} lappend cmd >$fn1 2>@stderr cd [file dirname $do] if {[catch {exec {*}$cmd}]} { file delete $fn1 $fn3 $tgt msg Red "$tgt: failed" return false } if {[file exists $fn3]} { file delete $fn1 file rename -force $fn3 $tgt } else { if {[file size $fn1] == 0} { file delete $fn1 $tgt } else { file rename -force $fn1 $tgt } } msg Grn "$tgt: done" return true } proc is-src {tgt} { if {[file exists $tgt]} { lassign [find-do $tgt] _ do if {$do == ""} { if {$::D} {msg Blu "$tgt: is src"} return true } } return false } proc is-built {tgt} { set fh [open $::env(REDO_BUILT)] set data [read $fh] close $fh foreach l [split $data \n] { if {$tgt == $l} { if {$::D} {msg Blu "$tgt: already built"} return true } } return false } if {[info exists env(REDO_TOP_DIR)]} {set TopDir $env(REDO_TOP_DIR)} {set TopDir /} set CWD [pwd] if {$argc == 0} {lappend argv all} switch [file tail $argv0] { redo-ifcreate {exit} redo-always {exit} redo-whichdo { set ifcreates [list] lassign [find-do [path-join $CWD [lindex $argv 0]] ifcreates] _ do foreach fn $ifcreates {puts $fn} if {$do == ""} {exit 1} puts $do exit } redo-ifchange {} redo {} default { puts stderr "unknown redo-* command" exit 1 } } if {![info exists env(REDO_BUILT)]} { set env(REDO_BUILT) [file join $CWD .redo.built] close [open $env(REDO_BUILT) w] } if {[info exists env(REDO_TRACE)]} {set X x} {set X ""} if {[info exists env(REDO_DEBUG)]} {set D true} {set D false} foreach tgt $argv { switch -- $tgt { -x {set X x} -xx {set X x ; set env(REDO_TRACE) 1} -d {set D true ; set env(REDO_DEBUG) 1} -h {puts {This is djb's minimalistic redo implementation, supporting: * neither dependency tracking (only full rebuild is done) * nor parallel builds * nor any kind of helpers or target's sane behaviour checkers * nor fsync-ing of targets, committing their results atomically It is intended to be used to fully build targets from the ground. Not for development or debugging, but for the ready tested software. It is advisable to use goredo implementation for those purposes. jimredo expects to be called through symlinked on it commands: * redo, redo-ifchange -- they are identical * redo-ifcreate, redo-always -- they do nothing * redo-whichdo -d option (or $REDO_DEBUG=1) for redo turns on debug output. -x option enables shell script target tracing. -xx (or $REDO_TRACE=1) forces all targets to be traced. jimredo respects goredo's $REDO_TOP_DIR environment variable and .../.redo/top file to limit top directory searching. It also respects $NO_COLOR to turn colouring off. redo command leaves the .redo.built plain text file in the current directory, where it keeps the list of built targets during the run.} } default { cd $CWD set tgt [path-join $CWD $tgt] if {[is-built $tgt]} {continue} if {[is-src $tgt]} {continue} if {![run $tgt]} {exit 1} set fh [open $env(REDO_BUILT) a] puts $fh $tgt close $fh } } } # vim: ft=tcl