diff --git a/src/vfs/AGENTS.md b/src/vfs/AGENTS.md index faa893ea..276aa342 100644 --- a/src/vfs/AGENTS.md +++ b/src/vfs/AGENTS.md @@ -15,6 +15,9 @@ VFS (Virtual File System) folders define the runtime payloads that get wrapped i - `*.vfs` folders are build artifacts consumed by `tclsh src/make.tcl project`. - VFS content must be compatible with the target platform runtime. - `_vfscommon.vfs/` is an auto-generated merge of common libraries; manually editing it will be overwritten. +- `_config/punk_main.tcl` accepts an optional package_mode argument before the subcommand. Valid modes are `dev`, `os`, `internal`, and `src`, dash-delimited in priority order (e.g `src-internal`). `internal` is always appended if omitted. +- `src` mode loads unbuilt modules and libraries from the project's `src/` tree (`src/modules`, `src/lib`, `src/bootsupport`, `src/vendormodules`) instead of built output. It also runs an inline `#modpod` package-ifneeded scanner (using Tcl builtins only, since no punk modules are loaded at boot time) and sets `package prefer latest` so `999999.0a1.0` dev modules are preferred over stable bootsupport/vendored copies. +- `dev` mode loads built output from `/modules` and `/lib`; `src` mode loads unbuilt source from `/src/modules` etc. Use `src-dev-internal` to combine both with src priority. ## Work Guidance diff --git a/src/vfs/_config/punk_main.tcl b/src/vfs/_config/punk_main.tcl index 12950af0..9558ece2 100644 --- a/src/vfs/_config/punk_main.tcl +++ b/src/vfs/_config/punk_main.tcl @@ -273,8 +273,10 @@ apply { args { set tm_additions_internal [list] set tm_additions_dev [list] + set tm_additions_src [list] set auto_path_additions_internal [list] set auto_path_additions_dev [list] + set auto_path_additions_src [list] set lc_auto_path [string tolower $::auto_path] @@ -357,47 +359,50 @@ apply { args { # dev - refers to module and library paths relative to the project (executable path) # os - refers to modules and library paths gleaned from ::env (TCLLIBPATH and TCL__TM_PATH) # internal - refers to modules and libraries supplied from the mounted filesystem of a kit or zipfs based executable + # src - refers to unbuilt modules and libraries under the project's src/ tree (src/modules, src/lib, src/bootsupport, src/vendormodules) # ----------------------------------------------------------------------------------------------------------- # Note that unlike standard 'package unknown' punk::libunknown does not stop searching for packages when a .tm file is found that matches requirements, # The auto_path is still examined. (avoids quirks where higher versioned pkgIndex based package not always found) # ----------------------------------------------------------------------------------------------------------- - set all_package_modes [list dev os internal] + set all_package_modes [list dev os internal src] #package_mode is specified as a dash-delimited ordered value e.g dev-os #"internal" is the default and if not present is always added to the list #i.e "dev-os" is equivalent to "dev-os-internal" #"os" is equivalent to "os-internal" #"internal-os" and "internal" are left as is. - #The effective package_mode has 1 2 or 3 members. + #The effective package_mode has 1 2 3 or 4 members. # The only case where it has 1 member is if just "internal" is specified. - #This gives the number of permutations as how many ways to choose 3 items plus how many ways to choose 2 of the 3 items (one must be 'internal') plus the sole allowable way to choose 1 - #for a total of 11 possible final orderings. - #(16 possible values for package_mode argument when you include the short-forms "",os,dev,os-dev,dev-os which always have 'internal' appended) set test_package_mode [lindex $args 0] #puts stderr "main.tcl test_package_mode: '$test_package_mode'" - switch -exact -- $test_package_mode { - internal - - os-internal - dev-internal - internal-os - internal-dev - - os-dev-internal - os-internal-dev - dev-os-internal - dev-internal-os - internal-os-dev - internal-dev-os { - #fully specified ('internal' is present) - set package_modes [split $test_package_mode -] - set arglist [lrange $args 1 end] + #Token-by-token validation: instead of exhaustively listing all permutations, + #split the first arg on dash and validate each token against the known mode set. + #This scales to any number of modes without enumerating every permutation. + set package_modes "" + set arglist "" + if {$test_package_mode eq ""} { + #empty first arg consumed as equivalent of 'internal' + set package_modes internal + set arglist [lrange $args 1 end] + } else { + set tokens [split $test_package_mode -] + set valid 1 + foreach t $tokens { + if {$t ni $all_package_modes} { + set valid 0 + break + } } - os - dev - os-dev - dev-os { - #partially specified - 'internal' ommitted but implied at tail - set package_modes [list {*}[split $test_package_mode -] internal] + if {$valid && [llength $tokens] >= 1} { + set package_modes $tokens + if {"internal" ni $package_modes} { + lappend package_modes internal + } set arglist [lrange $args 1 end] - } - default { - #empty first arg - or some unrelated arg + } else { + #not a package_mode - treat as subcommand set package_modes internal - if {$test_package_mode eq ""} { - #consume the empty first arg as an equivalent of 'internal' - #don't consume any first arg that isn't recognised as a package_mode - set arglist [lrange $args 1 end] - } else { - set arglist $args - } + set arglist $args } } #assert: arglist has had any first arg that is a package_mode (including empty string) stripped. @@ -532,6 +537,54 @@ apply { args { } } + #src mode: discover the project's src/ tree and add unbuilt module paths. + #Unlike dev mode (which points at built output in /modules), + #src mode points at the unbuilt source in /src/modules etc. + if {"src" in $package_modes} { + #reuse the dev-mode project root discovery (exe in bin/ -> backtrack 1) + set src_project_root "" + set normexe_dir_for_src [file dirname $normexe] + if {[file tail $normexe_dir_for_src] eq "bin"} { + set src_project_root [file dirname $normexe_dir_for_src] + } else { + set src_project_root $normexe_dir_for_src + } + #also check symlink target + if {$src_project_root eq ""} { + set nameexe_dir_for_src [file dirname [file normalize [info nameofexecutable]]] + if {[file tail $nameexe_dir_for_src] eq "bin"} { + set src_project_root [file dirname $nameexe_dir_for_src] + } else { + set src_project_root $nameexe_dir_for_src + } + } + + if {$src_project_root ne "" && [file isdirectory [file join $src_project_root src]]} { + foreach p [list modules modules_tcl$tclmajorv] { + set modpath [file join $src_project_root src $p] + if {[file isdirectory $modpath] && $modpath ni $tm_additions_src} { + lappend tm_additions_src $modpath + } + } + #bootsupport modules (for boot-critical packages like punkcheck, punk::mix etc) + foreach p [list modules modules_tcl$tclmajorv] { + set modpath [file join $src_project_root src bootsupport $p] + if {[file isdirectory $modpath] && $modpath ni $tm_additions_src} { + lappend tm_additions_src $modpath + } + } + #vendormodules (vendored dependencies like voo) + foreach p [list vendormodules vendormodules_tcl$tclmajorv] { + set modpath [file join $src_project_root src $p] + if {[file isdirectory $modpath] && $modpath ni $tm_additions_src} { + lappend tm_additions_src $modpath + } + } + } else { + puts stderr "Warning - src mode: no src/ directory found relative to executable ($normexe_dir_for_src)" + } + } + @@ -595,6 +648,13 @@ apply { args { } } } + src { + foreach n $tm_additions_src { + if {$n ni $new_tm_path} { + lappend new_tm_path $n + } + } + } os { foreach n $external_tm_dirs { if {$n ni $new_tm_path} { @@ -698,6 +758,42 @@ apply { args { } } + + #src mode: add unbuilt source library paths from the project's src/ tree. + if {"src" in $package_modes && [info exists src_project_root] && $src_project_root ne ""} { + set platform [::punkboot::platform_generic] + set src_lib_base [file join $src_project_root src] + #src/lib and src/lib_tcl (editable library source) + foreach libsub [list lib_tcl$tclmajorv lib] { + set libfolder [file join $src_lib_base $libsub] + if {[file isdirectory $libfolder]} { + if {[string match lib_tcl* [file tail $libfolder]]} { + if {[file exists $libfolder/allplatforms]} { + lappend auto_path_additions_src $libfolder/allplatforms + } + if {[file exists $libfolder/$platform]} { + lappend auto_path_additions_src $libfolder/$platform + } + } else { + lappend auto_path_additions_src $libfolder + } + } + } + #src/bootsupport/lib (bootstrap libraries) + set src_bs_lib [file join $src_lib_base bootsupport lib] + if {[file isdirectory $src_bs_lib]} { + if {$src_bs_lib ni $auto_path_additions_src} { + lappend auto_path_additions_src $src_bs_lib + } + } + #src/bootsupport/lib/tcl/ (platform-specific bootstrap libraries) + set src_bs_lib_arch [file join $src_lib_base bootsupport lib tcl$tclmajorv $platform] + if {[file isdirectory $src_bs_lib_arch]} { + if {$src_bs_lib_arch ni $auto_path_additions_src} { + lappend auto_path_additions_src $src_bs_lib_arch + } + } + } # -- --- --- --- --- --- --- --- #split existing ::auto_path entries into internal & external set internal_ap_dirs [list] ;# @@ -743,6 +839,13 @@ apply { args { } } } + src { + foreach n $auto_path_additions_src { + if {$n ni $new_auto_path} { + lappend new_auto_path $n + } + } + } os { foreach n $external_ap_dirs { if {$n ni $new_auto_path} { @@ -900,6 +1003,84 @@ apply { args { #assert arglist has had 'dev|os|os-dev etc' first arg removed if it was present. + #-------------------------------------------------------- + #src mode: register #modpod modules from src/modules via package ifneeded + #and set package prefer latest so 999999.0a1.0 dev modules are preferred + #over stable bootsupport/vendored copies on unversioned package require. + #Uses only Tcl builtins (glob, file, string) since no punk modules are loaded yet. + #-------------------------------------------------------- + if {"src" in $package_modes && [info exists src_project_root] && $src_project_root ne ""} { + #package prefer latest is set here so 999999.0a1.0 dev modules are preferred + #over stable bootsupport/vendored copies on unversioned package require. + #This must be set after libunknown::init (which may reset the preference to stable) + #and before any package require calls in the subcommand handler below. + #We set it again just before subcommand dispatch to ensure it isn't overridden. + + #inline #modpod scanner — equivalent to punk::tcltestrun::tm_path_additional_ifneeded + #but using only Tcl builtins since punk::path isn't loaded at boot time. + set src_modules_dir [file join $src_project_root src modules] + set modpod_count 0 + if {[file isdirectory $src_modules_dir]} { + #recursive glob for #modpod-* directories (Tcl 8.6+ supports ** in glob) + set modpod_dirs [list] + foreach found [glob -nocomplain -type d -directory $src_modules_dir ** #modpod-*] { + #skip _build subdirectories + if {[string match "*_build*" $found]} { continue } + lappend modpod_dirs $found + } + foreach modpod_dir $modpod_dirs { + set tail [file tail $modpod_dir] + #directory name format: #modpod-- + #strip leading "#modpod-" (8 chars), then split on last "-" to separate modname from version + set rest [string range $tail 8 end] + set last_dash [string last "-" $rest] + if {$last_dash < 0} { continue } + set modname [string range $rest 0 [expr {$last_dash - 1}]] + set modver [string range $rest [expr {$last_dash + 1}] end] + set modpath [file join $modpod_dir "$modname-$modver.tm"] + #compute fully qualified module name from path relative to src/modules + #file relative isn't available in all Tcl builds at boot time, so compute manually + set reldir "" + set checkdir $modpod_dir + set base $src_modules_dir + #walk up from modpod_dir until we reach src/modules, collecting path components + while {$checkdir ne $base && $checkdir ne ""} { + set reldir [linsert $reldir 0 [file tail $checkdir]] + set checkdir [file dirname $checkdir] + } + if {$reldir eq ""} { + set fullmodname $modname + } else { + set fullmodname [join $reldir ::]::$modname + } + if {[file exists $modpath]} { + eval [list package ifneeded $fullmodname $modver [list source $modpath]] + incr modpod_count + } + } + if {$modpod_count > 0} { + puts stderr "src mode: registered $modpod_count #modpod module[expr {$modpod_count == 1 ? "" : "s"}] from $src_modules_dir" + } + } + } + #-------------------------------------------------------- + + + #src mode: set package prefer latest as late as possible (after libunknown::init + #which may reset it to stable) so 999999.0a1.0 dev modules are preferred over + #stable bootsupport/vendored copies on unversioned package require. + if {"src" in $package_modes} { + package prefer latest + # Force a scan of all tcl::tm::list paths by triggering package unknown. + # The VFS-bundled stable versions (e.g punk 0.1.1) are already registered + # from Tcl's init, so package require for those packages would never call + # package unknown — meaning 999999.0a1.0 dev versions in src/modules would + # never be discovered. This dummy require forces package unknown to scan + # all tm paths and register all ifneeded scripts, including the dev versions. + # After this, package prefer latest will select 999999.0a1.0 over 0.1.1. + catch {package require __src_mode_tm_scan__} + } + #tclsh,shellspy,punk,script,shell set subcommand [lindex $arglist 0] switch -- $subcommand {