Browse Source

build outputs: punk::zip 0.2.0 into bootsupport + _vfscommon, thin-layout make.tcl sync

Regenerated by 'make.tcl modules' + 'bootsupport' + 'vfscommonupdate' for the G-124
change. make.tcl loads punk::zip from bootsupport, so the reader only reaches the
zipfs-less bake path once the snapshot carries it.

Also carries two version propagations pending from earlier work, not authored here:
punk 0.2.7 -> 0.2.8 and punk::platform 0.1.0 -> 0.2.0 (the latter was added to
include_modules.config in 5c1e6927 but never snapshotted). Their source buildversion
files are unchanged by this change-set.

The templates modpod is rebuilt because the layout make.tcl inside it changed. Note for
next time: 'make.tcl bootsupport' cannot replace a bootsupport modpod that the running
make.tcl has itself mounted - it failed twice with 'BOOTSUPPORT module update FAILED:
... templates-0.2.0.tm (invalid argument)' while a plain tclsh copied the identical file
without complaint. Copied by hand, then re-run to reconcile the punkcheck record.

Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
39c6e9a025
  1. 11
      src/bootsupport/modules/punk-0.2.8.tm
  2. BIN
      src/bootsupport/modules/punk/mix/templates-0.2.0.tm
  3. 83
      src/bootsupport/modules/punk/platform-0.2.0.tm
  4. 914
      src/bootsupport/modules/punk/zip-0.1.1.tm
  5. 1698
      src/bootsupport/modules/punk/zip-0.2.0.tm
  6. 25
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl
  7. 25
      src/project_layouts/vendor/punk/basic/src/make.tcl
  8. 25
      src/project_layouts/vendor/punk/project-0.1/src/make.tcl
  9. 11
      src/vfs/_vfscommon.vfs/modules/punk-0.2.8.tm
  10. BIN
      src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm
  11. 83
      src/vfs/_vfscommon.vfs/modules/punk/platform-0.2.0.tm
  12. 914
      src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm
  13. 1698
      src/vfs/_vfscommon.vfs/modules/punk/zip-0.2.0.tm

11
src/bootsupport/modules/punk-0.2.7.tm → src/bootsupport/modules/punk-0.2.8.tm

@ -9002,7 +9002,14 @@ namespace eval punk {
runtime-artifact metadata target field. The raw Tcl
platform-package identifiers are shown for comparison
(punk::platform::normalize folds their version-dependent
aliases - amd64/aarch64/macos - into the canonical names)."
aliases - amd64/aarch64/macos - into the canonical names, and
folds the msys2 family - mingw64/mingw32/ucrt64/clang* - onto
the one 'msys' tag, since msys2's uname renders that token from
MSYSTEM rather than from the binary).
These are HOST names. What a name implies for build OUTPUT
(.exe suffixing, process tooling, which bin/runtime tier holds
a runtime) is a TARGET question src/make.tcl derives from the
name - a cygwin-family host targets win32-x86_64 by default."
@values -min 0 -max 0
}
proc platforms {context args} {
@ -9505,7 +9512,7 @@ punkcheck::cli set_alias punkcheck
package provide punk [namespace eval punk {
#FUNCTL
variable version
set version 0.2.7
set version 0.2.8
}]

BIN
src/bootsupport/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

83
src/vfs/_vfscommon.vfs/modules/punk/platform-0.1.0.tm → src/bootsupport/modules/punk/platform-0.2.0.tm

@ -7,7 +7,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::platform 0.1.0
# Application punk::platform 0.2.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -40,6 +40,29 @@ namespace eval punk::platform {
# the darwin 'arm64' machine name to 'arm'; Apple silicon is 64-bit only,
# so the arm token on macosx always means arm64. Elsewhere 'arm' is a
# genuine 32-bit token and is preserved.)
# - os mingw64/mingw32/ucrt64/clang64/clangarm64 -> msys (G-122; see the
# MSYSTEM-variance note below)
#
#MSYSTEM VARIANCE (cygwin-family hosts on windows). An msys2-runtime tclsh
#reports tcl_platform(platform) 'unix' with an os string msys2's uname
#renders from the MSYSTEM environment variable, so ONE binary canonizes
#differently per launching shell - verified 2026-07-26 with msys2's
#/usr/bin/tclsh8.6: MSYSTEM=MSYS -> MSYS_NT-10.0-26200 (msys-x86_64), and
#MSYSTEM=MINGW64, =UCRT64 or unset (msys2 defaults it) -> MINGW64_NT-10.0-26200
#(mingw64-x86_64). The MSYSTEM token names the shell that launched the
#process, not the binary's ABI: a genuinely MINGW-built tclsh is a native
#windows binary reporting os 'Windows NT' and canonizes as win32-*. So every
#msys2-runtime token folds to the one stable tag 'msys'. Real cygwin keeps
#its own tag 'cygwin' - a separate runtime dll and mount world, so a store
#tier serving one does not serve the other. Windows build numbers never
#reach a tag (platform::generic cuts the os at its first _ or -; normalize_os
#re-folds a raw uname-style string anyway).
#These are HOST canon names. What a tag implies for BUILD OUTPUT - .exe
#suffixing, which process tooling manages a running artifact, which
#bin/runtime store tier holds a runtime - is a TARGET question that
#src/make.tcl derives from the tag (G-122 host/target split): cygwin-family
#hosts default their build target to win32-x86_64, since the kits they bake
#are native windows binaries.
#The special name 'macosx' (no cpu suffix) is the RUNTIME-tier convention for
#universal (multi-arch) macOS binaries - punkbin and bin/runtime keep one
#universal folder; the per-arch macosx-x86_64/macosx-arm64 names serve the
@ -83,7 +106,8 @@ namespace eval punk::platform {
macosx-arm64 {status supported tiers {lib} buildsuite none notes "Apple silicon; would follow a macosx runtime arc"}
freebsd-x86_64 {status supported tiers {runtime lib} buildsuite candidate notes ""}
freebsd-arm64 {status supported tiers {runtime lib} buildsuite candidate notes "no artifacts yet"}
msys-x86_64 {status dormant tiers {lib} buildsuite none notes "msys/cygwin-built tclsh runtimes; utility under review"}
msys-x86_64 {status dormant tiers {runtime lib} buildsuite none notes "msys2-runtime tclsh on windows (posix personality, .exe files); host canon for every MSYSTEM shell - see normalize"}
cygwin-x86_64 {status recognized tiers {runtime lib} buildsuite none notes "cygwin-runtime tclsh on windows; separate dll/mount world from msys2, so a distinct tier"}
openbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
netbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
dragonflybsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
@ -124,6 +148,45 @@ namespace eval punk::platform {
return $platforms
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::platform::normalize_os
@cmd -name punk::platform::normalize_os\
-summary\
"Fold an os token to its canonical punkshell spelling."\
-help\
"Fold a single os token (the <os> half of an <os>-<cpu> platform
identifier) to its canonical punkshell spelling:
macos -> macosx
mingw32|mingw64|ucrt64|clang* -> msys
MSYS_NT-<build> -> msys
CYGWIN_NT-<build> -> cygwin
The msys2 family folds to one tag because msys2's uname renders
the os string from the MSYSTEM environment variable: the same
binary reports MSYS_NT or MINGW64_NT depending on which shell
launched it, while a genuinely MINGW-built (native windows)
tclsh reports 'Windows NT' and canonizes as win32. Raw
uname-style strings carrying a windows build number fold too -
a build number never belongs in a platform tag.
Unrecognized tokens pass through unchanged."
@values -min 1 -max 1
os -type string -help\
"os token, e.g the <os> half of platform::generic's result."
}]
}
proc normalize_os {os} {
switch -glob -- [string tolower $os] {
macos {return macosx}
msys - msys_nt-* {return msys}
mingw32 - mingw64 - mingw_nt-* - mingw32_nt-* - mingw64_nt-* {return msys}
ucrt64 - ucrt64_nt-* {return msys}
clang32 - clang64 - clangarm64 - clang32_nt-* - clang64_nt-* - clangarm64_nt-* {return msys}
cygwin_nt-* {return cygwin}
}
return $os
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@ -137,6 +200,9 @@ namespace eval punk::platform {
platform-dir name:
cpu amd64 -> x86_64, aarch64 -> arm64
os macos -> macosx (Tcl platform 1.1.x renamed modern macOS)
os mingw64/mingw32/ucrt64/clang* -> msys (one stable tag for
the msys2 runtime family - the reported name follows MSYSTEM,
not the binary; see punk::platform::normalize_os)
macosx cpu arm -> arm64 (platform::generic's arm* glob folds
the darwin arm64 machine name to arm; Apple silicon is
64-bit only - non-macosx 'arm' stays 32-bit arm)
@ -150,18 +216,13 @@ namespace eval punk::platform {
proc normalize {platform} {
set parts [split $platform -]
if {[llength $parts] < 2} {
#single-token names (e.g the universal 'macosx') - fold macos only
if {$platform eq "macos"} {
return macosx
}
return $platform
#single-token names (e.g the universal 'macosx') - fold the os token only
return [normalize_os $platform]
}
#os may itself contain a dash in principle - treat last token as cpu
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
switch -- $os {
macos {set os macosx}
}
set os [normalize_os $os]
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
@ -232,6 +293,6 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::platform [namespace eval punk::platform {
variable version
set version 0.1.0
set version 0.2.0
}]
return

914
src/bootsupport/modules/punk/zip-0.1.1.tm

@ -1,914 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024 JMN
# (C) 2009 Path Thoyts <patthyts@users.sourceforge.net>
#
# @@ Meta Begin
# Application punk::zip 0.1.1
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::zip 0 0.1.1]
#[copyright "2024"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
#[require punk::zip]
#[keywords module zip fileformat]
#[description]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::zip
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::zip
#[list_begin itemized]
package require Tcl 8.6-
package require punk::args
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {punk::args}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::zip {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
#variable xyz
#*** !doctools
#[subsection {Namespace punk::zip}]
#[para] Core API functions for punk::zip
#[list_begin definitions]
proc Path_a_atorbelow_b {path_a path_b} {
return [expr {[StripPath $path_b $path_a] ne $path_a}]
}
proc Path_a_at_b {path_a path_b} {
return [expr {[StripPath $path_a $path_b] eq "." }]
}
proc Path_strip_alreadynormalized_prefixdepth {path prefix} {
if {$prefix eq ""} {
return $path
}
set pathparts [file split $path]
set prefixparts [file split $prefix]
if {[llength $prefixparts] >= [llength $pathparts]} {
return ""
}
return [file join \
{*}[lrange \
$pathparts \
[llength $prefixparts] \
end]]
}
#StripPath - borrowed from tcllib fileutil
# ::fileutil::stripPath --
#
# If the specified path references/is a path in prefix (or prefix itself) it
# is made relative to prefix. Otherwise it is left unchanged.
# In the case of it being prefix itself the result is the string '.'.
#
# Arguments:
# prefix prefix to strip from the path.
# path path to modify
#
# Results:
# path The (possibly) modified path.
if {[string equal $::tcl_platform(platform) windows]} {
# Windows. While paths are stored with letter-case preserved al
# comparisons have to be done case-insensitive. For reference see
# SF Tcllib Bug 2499641.
proc StripPath {prefix path} {
# [file split] is used to generate a canonical form for both
# paths, for easy comparison, and also one which is easy to modify
# using list commands.
set prefix [file split $prefix]
set npath [file split $path]
if {[string equal -nocase $prefix $npath]} {
return "."
}
if {[string match -nocase "${prefix} *" $npath]} {
set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]]
}
return $path
}
} else {
proc StripPath {prefix path} {
# [file split] is used to generate a canonical form for both
# paths, for easy comparison, and also one which is easy to modify
# using list commands.
set prefix [file split $prefix]
set npath [file split $path]
if {[string equal $prefix $npath]} {
return "."
}
if {[string match "${prefix} *" $npath]} {
set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]]
}
return $path
}
}
proc Timet_to_dos {time_t} {
#*** !doctools
#[call [fun Timet_to_dos] [arg time_t]]
#[para] convert a unix timestamp into a DOS timestamp for ZIP times.
#[example {
# DOS timestamps are 32 bits split into bit regions as follows:
# 24 16 8 0
# +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
# |Y|Y|Y|Y|Y|Y|Y|m| |m|m|m|d|d|d|d|d| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s|
# +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
#}]
set s [clock format $time_t -format {%Y %m %e %k %M %S}]
scan $s {%d %d %d %d %d %d} year month day hour min sec
expr {(($year-1980) << 25) | ($month << 21) | ($day << 16)
| ($hour << 11) | ($min << 5) | ($sec >> 1)}
}
punk::args::define {
@id -id ::punk::zip::walk
@cmd -name punk::zip::walk -help\
"Walk the directory structure starting at base/<-subpath>
and return a list of the files and folders encountered.
Resulting paths are relative to base unless -resultrelative
is supplied.
Folder names will end with a trailing slash.
"
-resultrelative -optional 1 -help\
"Resulting paths are relative to this value.
Defaults to the value of base. If empty string
is given to -resultrelative the paths returned
are effectively absolute paths."
-emptydirs -default 0 -type boolean -help\
"Whether to include directory trees in the result which had no
matches for the given fileglobs.
Intermediate dirs are always returned if there is a match with
fileglobs further down even if -emptdirs is 0.
"
-excludes -default "" -help "list of glob expressions to match against files and exclude"
-subpath -default "" -help\
"May contain glob chars for folder elements"
#If we don't include --, the call walk <options> -- <base> <globs>.. will return nothing as 'base' will receive the --
-- -type none -optional 1
@values -min 1 -max -1
base
fileglobs -default {*} -multiple 1
}
proc walk {args} {
#*** !doctools
#[call [fun walk] [arg ?options?] [arg base]]
#[para] Walk a directory tree rooted at base
#[para] the -excludes list can be a set of glob expressions to match against files and avoid
#[para] e.g
#[example {
# punk::zip::walk -exclude {CVS/* *~.#*} library
#}]
#todo: -relative 0|1 flag?
set argd [punk::args::parse $args withid ::punk::zip::walk]
set base [dict get $argd values base]
set fileglobs [dict get $argd values fileglobs]
set subpath [dict get $argd opts -subpath]
set excludes [dict get $argd opts -excludes]
set emptydirs [dict get $argd opts -emptydirs]
set received [dict get $argd received]
set imatch [list]
foreach fg $fileglobs {
lappend imatch [file join $subpath $fg]
}
if {![dict exists $received -resultrelative]} {
set relto $base
set prefix ""
} else {
set relto [file normalize [dict get $argd opts -resultrelative]]
if {$relto ne ""} {
if {![Path_a_atorbelow_b $base $relto]} {
error "punk::zip::walk base must be at or below -resultrelative value (backtracking not currently supported)"
}
set prefix [Path_strip_alreadynormalized_prefixdepth $base $relto]
} else {
set prefix $base
}
}
set result {}
#set imatch [file join $subpath $match]
set files [glob -nocomplain -tails -types f -directory $base -- {*}$imatch]
foreach file $files {
set excluded 0
foreach glob $excludes {
if {[string match $glob $file]} {
set excluded 1
break
}
}
if {!$excluded} {lappend result [file join $prefix $file]}
}
foreach dir [glob -nocomplain -tails -types d -directory $base -- [file join $subpath *]] {
set submatches [walk -subpath $dir -emptydirs $emptydirs -excludes $excludes $base {*}$fileglobs]
set subdir_entries [list]
set thisdir_match [list]
set has_file 0
foreach sd $submatches {
set fullpath [file join $prefix $sd] ;#file join destroys trailing slash
if {[string index $sd end] eq "/"} {
lappend subdir_entries $fullpath/
} else {
set has_file 1
lappend subdir_entries $fullpath
}
}
if {$emptydirs} {
set thisdir_match [list "[file join $prefix $dir]/"]
} else {
if {$has_file} {
set thisdir_match [list "[file join $prefix $dir]/"]
} else {
set subdir_entries [list]
}
}
#NOTE: trailing slash required for entries to be recognised as 'file type' = "directory"
#This is true for 2024 Tcl9 mounted zipfs at least. zip utilities such as 7zip seem(icon correct) to recognize dirs with or without trailing slash
#Although there are attributes on some systems to specify if entry is a directory - it appears trailing slash should always be used for folder names.
set result [list {*}$result {*}$thisdir_match {*}$subdir_entries]
}
return $result
}
#if there is an external preamble - extract that. (if there is also an internal preamble - ignore and consider part of the archive-data)
#Otherwise extract an internal preamble.
#if neither -?
#review - reconsider auto-determination of internal vs external preamble
punk::args::define {
@id -id ::punk::zip::extract_preamble
@cmd -name punk::zip::extract_preamble -help\
"Split a zipfs based executable or library into its constituent
binary and zip parts.
Note that the binary preamble might be either 'within' the zip offsets,
or simply catenated prior to an unadjusted zip.
Some build processes may have 'adjusted' the zip offsets to make the zip cover the entire file
('file based' offset) whilst the more modern approach is to simply concatenate the binary and the zip
('archive based' offset). An archive-based offset is simpler and more reliably points to the proper
split location. It also allows 'zipfs info //zipfs:/app' to return the correct offset information.
Either way, extract_preamble can usually separate them, but in the unusual case that there is both an
external preamble and a preamble within the zip, only the external preamble will be split, with the
internal one remaining in the zip.
The inverse of this process would be to extract the .zip file created by this split to a folder,
e.g extracted_zip_folder (adjusting contents as required) and then to run:
zipfs mkimg newbinaryname.exe extracted_zip_folder <prefix> \"\" <extracted_preamble_or_alternative exe>
"
@values -min 2 -max 3
infile -type file -optional 0 -help\
"Name of existing tcl executable or shared lib with attached zipfs filesystem"
outfile_preamble -optional 0 -type file -help\
"Name of output file for binary preamble to be extracted to.
If this file already exists, an error will be raised"
outfile_zip -default "" -type file -help\
"Name of output file for zip data to be extracted to.
If this file already exists, an error will be raised"
}
proc extract_preamble {args} {
set argd [punk::args::parse $args withid ::punk::zip::extract_preamble]
lassign [dict values $argd] leaders opts values received
set infile [dict get $values infile]
set outfile_preamble [dict get $values outfile_preamble]
set outfile_zip [dict get $values outfile_zip]
set inzip [open $infile r]
fconfigure $inzip -encoding iso8859-1 -translation binary
if {[file exists $outfile_preamble]} {
error "outfile_preamble $outfile_preamble already exists - please remove first"
}
if {$outfile_zip ne ""} {
if {[file exists $outfile_zip] && [file size $outfile_zip]} {
error "outfile_zip $outfile_zip already exists - please remove first"
}
}
chan seek $inzip 0 end
set insize [tell $inzip] ;#faster (including seeks) than calling out to filesystem using file size - but should be equivalent
chan seek $inzip 0 start
#only scan last 64k - cover max signature size?? review
if {$insize < 65559} {
set tailsearch_start 0
} else {
set tailsearch_start [expr {$insize - 65559}]
}
chan seek $inzip $tailsearch_start start
set scan [read $inzip]
#EOCD - End Of Central Directory record
set start_of_end [string last "\x50\x4b\x05\x06" $scan]
puts stdout "==>start_of_end: $start_of_end"
if {$start_of_end == -1} {
#no zip eocdr - consider entire file to be the zip preamble
set baseoffset $insize
} else {
set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}]
chan seek $inzip $filerelative_eocd_posn
set cdir_record_plus [read $inzip] ;#can have trailing data
binary scan $cdir_record_plus issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
#rule out a false positive from within a nonzip (e.g plain exe)
#There exists for example a PK\5\6 in a plain tclsh, but it doesn't appear to be zip related.
#It doesn't seem to occur near the end - so perhaps not an issue - but we'll do some basic checks anyway
#we only support single disk - so we'll validate a bit more by requiring disknbr and ctrldirdisk to be zeros
#todo - just search for Pk\5\6\0\0\0\0 in the first place? //review
if {$eocd(disknbr) + $eocd(ctrldirdisk) != 0} {
#review - should keep searching?
#for now we assume not a zip
set baseoffset $insize
} else {
#use the central dir size to jump back tko start of central dir
#determine if diroffset is file or archive relative
set filerelative_cdir_start [expr {$filerelative_eocd_posn - $eocd(dirsize)}]
puts stdout "---> [read $inzip 4]"
if {$filerelative_cdir_start > $eocd(diroffset)} {
#'external preamble' easy case
# - ie 'archive' offset - (and one of the reasons I prefer archive-offset - it makes finding the 'prefix' easier
#though we are assuming zip offsets are not corrupted
set baseoffset [expr {$filerelative_cdir_start - $eocd(diroffset)}]
} else {
#'internal preamble' hard case
# - either no preamble - or offsets have been adjusted to be file relative.
#we could scan from top (ugly) - and with binary prefixes we could get false positives in the data that look like PK\3\4 headers
#we could either work out the format for all possible executables that could be appended (across all platforms) and understand where they end?
#or we just look for the topmost PK\3\4 header pointed to by a CDR record - and assume the CDR is complete
#step one - read all the CD records and find the highest pointed to local file record (which isn't necessarily the first - but should get us above most if not all of the zip data)
#we can't assume they're ordered in any particular way - so we in theory have to look at them all.
set baseoffset "unknown"
chan seek $inzip $filerelative_cdir_start start
#binary scan $cdir_record_plus issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
# eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
#load the whole central dir into cdir
#todo! loop through all cdr file headers - find highest offset?
#tclZipfs.c just looks at first file header in Central Directory
#looking at all entries would be more robust - but we won't work harder than tclZipfs.c for now //REVIEW
set cdirdata [read $inzip $eocd(dirsize)]
binary scan $cdirdata issssssiiisssssii cdir(signature) cdir(_vermadeby) cdir(_verneeded) cdir(gpbitflag) cdir(compmethod) cdir(lastmodifiedtime) cdir(lastmodifieddate)\
cdir(uncompressedcrc32) cdir(compressedsize) cdir(uncompressedsize) cdir(filenamelength) cdir(extrafieldlength) cdir(filecommentlength) cdir(disknbr)\
cdir(internalfileattributes) cdir(externalfileatributes) cdir(relativeoffset)
#since we're in this branch - we assume cdir(relativeoffset) is from the start of the file
chan seek $inzip $cdir(relativeoffset)
#let's at least check that we landed on a local file header..
set local_file_header_beginning [read $inzip 28]; #local_file_header without the file name and extra field
binary scan $local_file_header_beginning isssssiiiss lfh(signature) lfh(_verneeded) lfh(gpbitflag) lfh(compmethod) lfh(lastmodifiedtime) lfh(lastmodifieddate)\
lfh(uncompressedcrc32) lfh(compressedsize) lfh(uncompressedsize) lfh(filenamelength) lfh(extrafieldlength)
#dec2hex 67324752 = 4034B50 = PK\3\4
puts stdout "1st local file header sig: $lfh(signature)"
if {$lfh(signature) == 67324752} {
#looks like a local file header
#use our cdir(relativeoffset) as the start of the zip-data (//review - possible embedded password + end marker preceeding this)
set baseoffset $cdir(relativeoffset)
}
}
puts stdout "filerel_cdirstart: $filerelative_cdir_start recorded_offset: $eocd(diroffset)"
}
}
puts stdout "baseoffset: $baseoffset"
#expect CDFH PK\1\2
#above the CD - we expect a bunch of PK\3\4 records - (possibly not all of them pointed to by the CDR)
#above that we expect: *possibly* a stored password with trailing marker - then the prefixed exe/script
if {![string is integer -strict $baseoffset]} {
error "unable to determine zip baseoffset of file $infile"
}
if {$baseoffset < $insize} {
set pout [open $outfile_preamble w]
fconfigure $pout -encoding iso8859-1 -translation binary
chan seek $inzip 0 start
chan copy $inzip $pout -size $baseoffset
close $pout
if {$outfile_zip ne ""} {
#todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile
set zout [open $outfile_zip w]
fconfigure $zout -encoding iso8859-1 -translation binary
chan copy $inzip $zout
close $zout
}
close $inzip
} else {
#no valid (from our perspective) eocdr found - baseoffset has been set to insize
close $inzip
file copy $infile $outfile_preamble
if {$outfile_zip ne ""} {
#touch equiv?
set fd [open $outfile_zip w]
close $fd
}
}
}
punk::args::define {
@id -id ::punk::zip::Addentry
@cmd -name punk::zip::Addentry\
-summary\
"Add zip-entry for file at 'path'"\
-help\
"Add a single file at 'path' to open channel 'zipchan'
return a central directory file record"
@opts
-comment -default "" -help "An optional comment specific to the added file"
@values -min 3 -max 4
zipchan -help "open file descriptor with cursor at position appropriate for writing a local file header"
base -help "base path for entries"
path -type file -help "path of file to add"
zipdataoffset -default 0 -type integer -range {0 ""} -help "offset of start of zip-data - ie length of prefixing script/exe
Can be specified as zero even if a prefix exists - which would make offsets 'file relative' as opposed to 'archive relative'"
}
# Addentry - was Mkzipfile --
#
# FIX ME: should handle the current offset for non-seekable channels
#
proc Addentry {args} {
#*** !doctools
#[call [fun Addentry] [arg zipchan] [arg base] [arg path] [arg ?comment?]]
#[para] Add a single file to a zip archive
#[para] The zipchan channel should already be open and binary.
#[para] You can provide a -comment for the file.
#[para] The return value is the central directory record that will need to be used when finalizing the zip archive.
set argd [punk::args::parse $args withid ::punk::zip::Addentry]
set zipchan [dict get $argd values zipchan]
set base [dict get $argd values base]
set path [dict get $argd values path]
set zipdataoffset [dict get $argd values zipdataoffset]
set comment [dict get $argd opts -comment]
set fullpath [file join $base $path]
set mtime [Timet_to_dos [file mtime $fullpath]]
set utfpath [encoding convertto utf-8 $path]
set utfcomment [encoding convertto utf-8 $comment]
set flags [expr {(1<<11)}] ;# utf-8 comment and path
set method 0 ;# store 0, deflate 8
set attr 0 ;# text or binary (default binary)
set version 20 ;# minumum version req'd to extract
set extra ""
set crc 0
set size 0
set csize 0
set data ""
set seekable [expr {[tell $zipchan] != -1}]
if {[file isdirectory $fullpath]} {
set attrex 0x41ff0010 ;# 0o040777 (drwxrwxrwx)
#set attrex 0x40000010
} elseif {[file executable $fullpath]} {
set attrex 0x81ff0080 ;# 0o100777 (-rwxrwxrwx)
} else {
set attrex 0x81b60020 ;# 0o100666 (-rw-rw-rw-)
if {[file extension $fullpath] in {".tcl" ".txt" ".c"}} {
set attr 1 ;# text
}
}
if {[file isfile $fullpath]} {
set size [file size $fullpath]
if {!$seekable} {set flags [expr {$flags | (1 << 3)}]}
}
set channeloffset [tell $zipchan] ;#position in the channel - this may include prefixing exe/zip
set local [binary format a4sssiiiiss PK\03\04 \
$version $flags $method $mtime $crc $csize $size \
[string length $utfpath] [string length $extra]]
append local $utfpath $extra
puts -nonewline $zipchan $local
if {[file isfile $fullpath]} {
# If the file is under 2MB then zip in one chunk, otherwize we use
# streaming to avoid requiring excess memory. This helps to prevent
# storing re-compressed data that may be larger than the source when
# handling PNG or JPEG or nested ZIP files.
if {$size < 0x00200000} {
set fin [open $fullpath rb]
set data [read $fin]
set crc [zlib crc32 $data]
set cdata [zlib deflate $data]
if {[string length $cdata] < $size} {
set method 8
set data $cdata
}
close $fin
set csize [string length $data]
puts -nonewline $zipchan $data
} else {
set method 8
set fin [open $fullpath rb]
set zlib [zlib stream deflate]
while {![eof $fin]} {
set data [read $fin 4096]
set crc [zlib crc32 $data $crc]
$zlib put $data
if {[string length [set zdata [$zlib get]]]} {
incr csize [string length $zdata]
puts -nonewline $zipchan $zdata
}
}
close $fin
$zlib finalize
set zdata [$zlib get]
incr csize [string length $zdata]
puts -nonewline $zipchan $zdata
$zlib close
}
if {$seekable} {
# update the header if the output is seekable
set local [binary format a4sssiiii PK\03\04 \
$version $flags $method $mtime $crc $csize $size]
set current [tell $zipchan]
seek $zipchan $channeloffset
puts -nonewline $zipchan $local
seek $zipchan $current
} else {
# Write a data descriptor record
set ddesc [binary format a4iii PK\7\8 $crc $csize $size]
puts -nonewline $zipchan $ddesc
}
}
#PK\x01\x02 Cdentral directory file header
#set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3
set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems)
set hdr [binary format a4ssssiiiisssssii PK\01\02 $v1 \
$version $flags $method $mtime $crc $csize $size \
[string length $utfpath] [string length $extra]\
[string length $utfcomment] 0 $attr $attrex [expr {$channeloffset - $zipdataoffset}]] ;#zipdataoffset may be zero - either because it's a pure zip, or file-based offsets desired.
append hdr $utfpath $extra $utfcomment
return $hdr
}
#### REVIEW!!!
#JMN - review - this looks to be offset relative to start of file - (same as 2024 Tcl 'mkzip mkimg')
# we want to enable (optionally) offsets relative to start of archive for exe/script-prefixed zips.on windows (editability with 7z,peazip)
####
punk::args::define {
@id -id ::punk::zip::mkzip
@cmd -name punk::zip::mkzip\
-summary\
"Create a zip archive in 'filename'."\
-help\
"Create a zip archive in 'filename'"
@opts
-offsettype -default "archive" -choices {archive file}\
-help\
"zip offsets stored relative to start of entire file or relative to start of zip-archive
Only relevant if the created file has a script/runtime prefix."
-return -default "pretty" -choices {pretty list none}\
-help\
"mkzip can return a list of the files and folders added to the archive
the option -return pretty is the default and uses the punk::lib pdict/plist system
to return a formatted list for the terminal
"
-zipkit -default 0 -type none\
-help\
"whether to add mounting script
mutually exclusive with -runtime option
currently vfs::zip based - todo - autodetect zipfs/vfs with pref for zipfs"
-runtime -default ""\
-help\
"specify a prefix file
e.g punk::zip::mkzip -runtime unzipsfx.exe -directory subdir -base subdir output.zip
will create a self-extracting zip archive from the subdir/ folder.
Expects runtime with no existing vfs attached (review)"
-comment -default ""\
-help "An optional comment for the archive"
-directory -default ""\
-help "Scan for contents within this folder or current directory if not provided."
-base -default ""\
-help\
"The new zip archive will be rooted in this directory if provided
it must be a parent of -directory or the same path as -directory"
-exclude -default {CVS/* */CVS/* *~ ".#*" "*/.#*"}
-- -type none -help\
"End of options marker"
@values -min 1 -max -1
filename -type file -default ""\
-help "name of zipfile to create"
globs -default {*} -multiple 1\
-help\
"list of glob patterns to match.
Only directories with matching files will be included in the archive."
}
# zip::mkzip --
#
# eg: zip my.zip -directory Subdir -runtime unzipsfx.exe *.txt
#
proc mkzip {args} {
#todo - doctools - [arg ?globs...?] syntax?
#*** !doctools
#[call [fun mkzip]\
# [opt "[option -offsettype] [arg offsettype]"]\
# [opt "[option -return] [arg returntype]"]\
# [opt "[option -zipkit] [arg 0|1]"]\
# [opt "[option -runtime] [arg preamble_filename]"]\
# [opt "[option -comment] [arg zipfilecomment]"]\
# [opt "[option -directory] [arg dir_to_zip]"]\
# [opt "[option -base] [arg archive_root]"]\
# [opt "[option -exclude] [arg globlist]"]\
# [arg zipfilename]\
# [arg ?glob...?]]
#[para] Create a zip archive in 'zipfilename'
#[para] If a file already exists, an error will be raised.
#[para] Call 'punk::zip::mkzip' with no arguments for usage display.
set argd [punk::args::parse $args withid ::punk::zip::mkzip]
set filename [dict get $argd values filename]
if {$filename eq ""} {
error "mkzip filename cannot be empty string"
}
if {[regexp {[?*]} $filename]} {
#catch a likely error where filename is omitted and first glob pattern is misinterpreted as zipfile name
error "mkzip filename should not contain glob characters ? *"
}
if {[file exists $filename]} {
error "mkzip filename:$filename already exists"
}
dict for {k v} [dict get $argd opts] {
switch -- $k {
-comment {
dict set argd opts $k [encoding convertto utf-8 $v]
}
-directory - -base {
dict set argd opts $k [file normalize $v]
}
}
}
array set opts [dict get $argd opts]
if {$opts(-directory) ne ""} {
if {$opts(-base) ne ""} {
#-base and -directory have been normalized already
if {![Path_a_atorbelow_b $opts(-directory) $opts(-base)]} {
error "punk::zip::mkzip -base $opts(-base) must be above or the same as -directory $opts(-directory)"
}
set base $opts(-base)
set relpath [Path_strip_alreadynormalized_prefixdepth $opts(-directory) $opts(-base)]
} else {
set base $opts(-directory)
set relpath ""
}
#will pick up intermediary folders as paths (ending with trailing slash)
set paths [walk -exclude $opts(-exclude) -subpath $relpath -- $base {*}[dict get $argd values globs]]
set norm_filename [file normalize $filename]
set norm_dir [file normalize $opts(-directory)] ;#we only care if filename below -directory (which is where we start scanning)
if {[Path_a_atorbelow_b $norm_filename $norm_dir]} {
#check that we aren't adding the zipfile to itself
#REVIEW - now that we open zipfile after scanning - this isn't really a concern!
#keep for now in case we can add an -update or a -force facility (or in case we modify to add to zip as we scan for members?)
#In the case of -force - we may want to delay replacement of original until scan is done?
#try to avoid looping on all paths and performing (somewhat) expensive file normalizations on each
#1st step is to check the patterns and see if our zipfile is already excluded - in which case we need not check the paths
set self_globs_match 0
foreach g [dict get $argd values globs] {
if {[string match $g [file tail $filename]]} {
set self_globs_match 1
break
}
}
if {$self_globs_match} {
#still dangerous
set self_excluded 0
foreach e $opts(-exclude) {
if {[string match $e [file tail $filename]]} {
set self_excluded 1
break
}
}
if {!$self_excluded} {
#still dangerous - likely to be in resultset - check each path
#puts stderr "zip file $filename is below directory $opts(-directory)"
set self_is_matched 0
set i 0
foreach p $paths {
set norm_p [file normalize [file join $opts(-directory) $p]]
if {[Path_a_at_b $norm_filename $norm_p]} {
set self_is_matched 1
break
}
incr i
}
if {$self_is_matched} {
puts stderr "WARNING - zipfile being created '$filename' was matched. Excluding this file. Relocate the zip, or use -exclude patterns to avoid this message"
set paths [lremove $paths $i]
}
}
}
}
} else {
#NOTE that we don't add intermediate folders when creating an archive without using the -directory flag!
#ie - only the exact *files* matching the glob are stored.
set paths [list]
set dir [pwd]
if {$opts(-base) ne ""} {
if {![Path_a_atorbelow_b $dir $opts(-base)]} {
error "punk::zip::mkzip -base $opts(-base) must be above current directory"
}
set relpath [Path_strip_alreadynormalized_prefixdepth [file normalize $dir] [file normalize $opts(-base)]]
} else {
set relpath ""
}
set base $opts(-base)
set matches [glob -nocomplain -type f -- {*}[dict get $argd values globs]]
foreach m $matches {
if {$m eq $filename} {
#puts stderr "--> excluding $filename"
continue
}
set isok 1
foreach e [concat $opts(-exclude) $filename] {
if {[string match $e $m]} {
set isok 0
break
}
}
if {$isok} {
lappend paths [file join $relpath $m]
}
}
}
if {![llength $paths]} {
return ""
}
set zf [open $filename wb]
if {$opts(-runtime) ne ""} {
#todo - strip any existing vfs - option to merge contents.. only if zip attached?
set rt [open $opts(-runtime) rb]
fcopy $rt $zf
close $rt
} elseif {$opts(-zipkit)} {
#TODO - update to zipfs ?
#see modpod
set zkd "#!/usr/bin/env tclkit\n\# This is a zip-based Tcl Module\n"
append zkd "package require vfs::zip\n"
append zkd "vfs::zip::Mount \[info script\] \[info script\]\n"
append zkd "if {\[file exists \[file join \[info script\] main.tcl\]\]} {\n"
append zkd " source \[file join \[info script\] main.tcl\]\n"
append zkd "}\n"
append zkd \x1A
puts -nonewline $zf $zkd
}
#todo - subtract this from the endrec offset
if {$opts(-offsettype) eq "archive"} {
set dataStartOffset [tell $zf] ;#the overall file offset of the start of archive-data //JMN 2024
} else {
set dataStartOffset 0 ;#offsets relative to file - the (old) zipfs mkzip way :/
}
set count 0
set cd ""
set members [list]
foreach path $paths {
#puts $path
lappend members $path
append cd [Addentry $zf $base $path $dataStartOffset] ;#path already includes relpath
incr count
}
set cdoffset [tell $zf]
set endrec [binary format a4ssssiis PK\05\06 0 0 \
$count $count [string length $cd] [expr {$cdoffset - $dataStartOffset}]\
[string length $opts(-comment)]]
append endrec $opts(-comment)
puts -nonewline $zf $cd
puts -nonewline $zf $endrec
close $zf
set result ""
switch -exact -- $opts(-return) {
list {
set result $members
}
pretty {
if {[info commands showlist] ne ""} {
set result [plist -channel none members]
} else {
set result $members
}
}
none {
set result ""
}
}
return $result
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::zip ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::zip::lib {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
tcl::namespace::path [tcl::namespace::parent]
#*** !doctools
#[subsection {Namespace punk::zip::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::zip::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::zip [tcl::namespace::eval punk::zip {
variable pkg punk::zip
variable version
set version 0.1.1
}]
return
#*** !doctools
#[manpage_end]

1698
src/bootsupport/modules/punk/zip-0.2.0.tm

File diff suppressed because it is too large Load Diff

25
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -5937,20 +5937,22 @@ foreach vfstail $vfs_tails {
file copy -force $building_runtime $raw_runtime
}
} else {
#The driving tcl we are calling with doesn't have zipfs - can't mount
#The driving tcl we are calling with doesn't have zipfs - can't mount.
#punk::zip reads the attached archive with stock Tcl only (G-124),
#so this path needs no zipfs, no vfs::zip and no tcllib.
package require punk::zip
puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble"
set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip
puts stdout "tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - reading the runtime's attached archive with punk::zip"
set extractedzipfolder $buildfolder/extracted_$runtime_fullname
file delete $raw_runtime
file delete $extractedzipfile
file delete -force $extractedzipfolder
if {![catch {
punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile
package require zipfile::decode
zipfile::decode::open $extractedzipfile
set archiveinfo [zipfile::decode::archive]
zipfile::decode::unzip $archiveinfo $extractedzipfolder
#Split off the executable prefix - the kit assembly needs it as a
#file. The MEMBERS are then read from the ORIGINAL runtime at its
#derived base offset: a runtime whose offsets are file-relative
#splits into a .zip no plain zip reader accepts, and reading the
#whole file sidesteps that intermediate entirely.
punk::zip::extract_preamble $building_runtime $raw_runtime
punk::zip::unzip -return none -- $building_runtime $extractedzipfolder
} extracterr]} {
set extraction_done 1
set extract_kit_type $extract_kit_try
@ -5958,10 +5960,7 @@ foreach vfstail $vfs_tails {
merge_over $extractedzipfolder $targetvfs
} else {
#Do not swallow the reason: without the runtime's own
#zip contents the built kit has no tcl_library. Note
#zipfile::decode is a system tcllib package, not a
#vendored one - a tclsh without tcllib (e.g the msys2
#/usr/bin/tclsh8.6) cannot take this path at all.
#zip contents the built kit has no tcl_library.
puts stderr "zipfs-less extraction of $runtime_fullname FAILED: $extracterr"
}
}

25
src/project_layouts/vendor/punk/basic/src/make.tcl vendored

@ -5937,20 +5937,22 @@ foreach vfstail $vfs_tails {
file copy -force $building_runtime $raw_runtime
}
} else {
#The driving tcl we are calling with doesn't have zipfs - can't mount
#The driving tcl we are calling with doesn't have zipfs - can't mount.
#punk::zip reads the attached archive with stock Tcl only (G-124),
#so this path needs no zipfs, no vfs::zip and no tcllib.
package require punk::zip
puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble"
set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip
puts stdout "tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - reading the runtime's attached archive with punk::zip"
set extractedzipfolder $buildfolder/extracted_$runtime_fullname
file delete $raw_runtime
file delete $extractedzipfile
file delete -force $extractedzipfolder
if {![catch {
punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile
package require zipfile::decode
zipfile::decode::open $extractedzipfile
set archiveinfo [zipfile::decode::archive]
zipfile::decode::unzip $archiveinfo $extractedzipfolder
#Split off the executable prefix - the kit assembly needs it as a
#file. The MEMBERS are then read from the ORIGINAL runtime at its
#derived base offset: a runtime whose offsets are file-relative
#splits into a .zip no plain zip reader accepts, and reading the
#whole file sidesteps that intermediate entirely.
punk::zip::extract_preamble $building_runtime $raw_runtime
punk::zip::unzip -return none -- $building_runtime $extractedzipfolder
} extracterr]} {
set extraction_done 1
set extract_kit_type $extract_kit_try
@ -5958,10 +5960,7 @@ foreach vfstail $vfs_tails {
merge_over $extractedzipfolder $targetvfs
} else {
#Do not swallow the reason: without the runtime's own
#zip contents the built kit has no tcl_library. Note
#zipfile::decode is a system tcllib package, not a
#vendored one - a tclsh without tcllib (e.g the msys2
#/usr/bin/tclsh8.6) cannot take this path at all.
#zip contents the built kit has no tcl_library.
puts stderr "zipfs-less extraction of $runtime_fullname FAILED: $extracterr"
}
}

25
src/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -5937,20 +5937,22 @@ foreach vfstail $vfs_tails {
file copy -force $building_runtime $raw_runtime
}
} else {
#The driving tcl we are calling with doesn't have zipfs - can't mount
#The driving tcl we are calling with doesn't have zipfs - can't mount.
#punk::zip reads the attached archive with stock Tcl only (G-124),
#so this path needs no zipfs, no vfs::zip and no tcllib.
package require punk::zip
puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble"
set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip
puts stdout "tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - reading the runtime's attached archive with punk::zip"
set extractedzipfolder $buildfolder/extracted_$runtime_fullname
file delete $raw_runtime
file delete $extractedzipfile
file delete -force $extractedzipfolder
if {![catch {
punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile
package require zipfile::decode
zipfile::decode::open $extractedzipfile
set archiveinfo [zipfile::decode::archive]
zipfile::decode::unzip $archiveinfo $extractedzipfolder
#Split off the executable prefix - the kit assembly needs it as a
#file. The MEMBERS are then read from the ORIGINAL runtime at its
#derived base offset: a runtime whose offsets are file-relative
#splits into a .zip no plain zip reader accepts, and reading the
#whole file sidesteps that intermediate entirely.
punk::zip::extract_preamble $building_runtime $raw_runtime
punk::zip::unzip -return none -- $building_runtime $extractedzipfolder
} extracterr]} {
set extraction_done 1
set extract_kit_type $extract_kit_try
@ -5958,10 +5960,7 @@ foreach vfstail $vfs_tails {
merge_over $extractedzipfolder $targetvfs
} else {
#Do not swallow the reason: without the runtime's own
#zip contents the built kit has no tcl_library. Note
#zipfile::decode is a system tcllib package, not a
#vendored one - a tclsh without tcllib (e.g the msys2
#/usr/bin/tclsh8.6) cannot take this path at all.
#zip contents the built kit has no tcl_library.
puts stderr "zipfs-less extraction of $runtime_fullname FAILED: $extracterr"
}
}

11
src/vfs/_vfscommon.vfs/modules/punk-0.2.7.tm → src/vfs/_vfscommon.vfs/modules/punk-0.2.8.tm

@ -9002,7 +9002,14 @@ namespace eval punk {
runtime-artifact metadata target field. The raw Tcl
platform-package identifiers are shown for comparison
(punk::platform::normalize folds their version-dependent
aliases - amd64/aarch64/macos - into the canonical names)."
aliases - amd64/aarch64/macos - into the canonical names, and
folds the msys2 family - mingw64/mingw32/ucrt64/clang* - onto
the one 'msys' tag, since msys2's uname renders that token from
MSYSTEM rather than from the binary).
These are HOST names. What a name implies for build OUTPUT
(.exe suffixing, process tooling, which bin/runtime tier holds
a runtime) is a TARGET question src/make.tcl derives from the
name - a cygwin-family host targets win32-x86_64 by default."
@values -min 0 -max 0
}
proc platforms {context args} {
@ -9505,7 +9512,7 @@ punkcheck::cli set_alias punkcheck
package provide punk [namespace eval punk {
#FUNCTL
variable version
set version 0.2.7
set version 0.2.8
}]

BIN
src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

83
src/bootsupport/modules/punk/platform-0.1.0.tm → src/vfs/_vfscommon.vfs/modules/punk/platform-0.2.0.tm

@ -7,7 +7,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::platform 0.1.0
# Application punk::platform 0.2.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -40,6 +40,29 @@ namespace eval punk::platform {
# the darwin 'arm64' machine name to 'arm'; Apple silicon is 64-bit only,
# so the arm token on macosx always means arm64. Elsewhere 'arm' is a
# genuine 32-bit token and is preserved.)
# - os mingw64/mingw32/ucrt64/clang64/clangarm64 -> msys (G-122; see the
# MSYSTEM-variance note below)
#
#MSYSTEM VARIANCE (cygwin-family hosts on windows). An msys2-runtime tclsh
#reports tcl_platform(platform) 'unix' with an os string msys2's uname
#renders from the MSYSTEM environment variable, so ONE binary canonizes
#differently per launching shell - verified 2026-07-26 with msys2's
#/usr/bin/tclsh8.6: MSYSTEM=MSYS -> MSYS_NT-10.0-26200 (msys-x86_64), and
#MSYSTEM=MINGW64, =UCRT64 or unset (msys2 defaults it) -> MINGW64_NT-10.0-26200
#(mingw64-x86_64). The MSYSTEM token names the shell that launched the
#process, not the binary's ABI: a genuinely MINGW-built tclsh is a native
#windows binary reporting os 'Windows NT' and canonizes as win32-*. So every
#msys2-runtime token folds to the one stable tag 'msys'. Real cygwin keeps
#its own tag 'cygwin' - a separate runtime dll and mount world, so a store
#tier serving one does not serve the other. Windows build numbers never
#reach a tag (platform::generic cuts the os at its first _ or -; normalize_os
#re-folds a raw uname-style string anyway).
#These are HOST canon names. What a tag implies for BUILD OUTPUT - .exe
#suffixing, which process tooling manages a running artifact, which
#bin/runtime store tier holds a runtime - is a TARGET question that
#src/make.tcl derives from the tag (G-122 host/target split): cygwin-family
#hosts default their build target to win32-x86_64, since the kits they bake
#are native windows binaries.
#The special name 'macosx' (no cpu suffix) is the RUNTIME-tier convention for
#universal (multi-arch) macOS binaries - punkbin and bin/runtime keep one
#universal folder; the per-arch macosx-x86_64/macosx-arm64 names serve the
@ -83,7 +106,8 @@ namespace eval punk::platform {
macosx-arm64 {status supported tiers {lib} buildsuite none notes "Apple silicon; would follow a macosx runtime arc"}
freebsd-x86_64 {status supported tiers {runtime lib} buildsuite candidate notes ""}
freebsd-arm64 {status supported tiers {runtime lib} buildsuite candidate notes "no artifacts yet"}
msys-x86_64 {status dormant tiers {lib} buildsuite none notes "msys/cygwin-built tclsh runtimes; utility under review"}
msys-x86_64 {status dormant tiers {runtime lib} buildsuite none notes "msys2-runtime tclsh on windows (posix personality, .exe files); host canon for every MSYSTEM shell - see normalize"}
cygwin-x86_64 {status recognized tiers {runtime lib} buildsuite none notes "cygwin-runtime tclsh on windows; separate dll/mount world from msys2, so a distinct tier"}
openbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
netbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
dragonflybsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
@ -124,6 +148,45 @@ namespace eval punk::platform {
return $platforms
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::platform::normalize_os
@cmd -name punk::platform::normalize_os\
-summary\
"Fold an os token to its canonical punkshell spelling."\
-help\
"Fold a single os token (the <os> half of an <os>-<cpu> platform
identifier) to its canonical punkshell spelling:
macos -> macosx
mingw32|mingw64|ucrt64|clang* -> msys
MSYS_NT-<build> -> msys
CYGWIN_NT-<build> -> cygwin
The msys2 family folds to one tag because msys2's uname renders
the os string from the MSYSTEM environment variable: the same
binary reports MSYS_NT or MINGW64_NT depending on which shell
launched it, while a genuinely MINGW-built (native windows)
tclsh reports 'Windows NT' and canonizes as win32. Raw
uname-style strings carrying a windows build number fold too -
a build number never belongs in a platform tag.
Unrecognized tokens pass through unchanged."
@values -min 1 -max 1
os -type string -help\
"os token, e.g the <os> half of platform::generic's result."
}]
}
proc normalize_os {os} {
switch -glob -- [string tolower $os] {
macos {return macosx}
msys - msys_nt-* {return msys}
mingw32 - mingw64 - mingw_nt-* - mingw32_nt-* - mingw64_nt-* {return msys}
ucrt64 - ucrt64_nt-* {return msys}
clang32 - clang64 - clangarm64 - clang32_nt-* - clang64_nt-* - clangarm64_nt-* {return msys}
cygwin_nt-* {return cygwin}
}
return $os
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@ -137,6 +200,9 @@ namespace eval punk::platform {
platform-dir name:
cpu amd64 -> x86_64, aarch64 -> arm64
os macos -> macosx (Tcl platform 1.1.x renamed modern macOS)
os mingw64/mingw32/ucrt64/clang* -> msys (one stable tag for
the msys2 runtime family - the reported name follows MSYSTEM,
not the binary; see punk::platform::normalize_os)
macosx cpu arm -> arm64 (platform::generic's arm* glob folds
the darwin arm64 machine name to arm; Apple silicon is
64-bit only - non-macosx 'arm' stays 32-bit arm)
@ -150,18 +216,13 @@ namespace eval punk::platform {
proc normalize {platform} {
set parts [split $platform -]
if {[llength $parts] < 2} {
#single-token names (e.g the universal 'macosx') - fold macos only
if {$platform eq "macos"} {
return macosx
}
return $platform
#single-token names (e.g the universal 'macosx') - fold the os token only
return [normalize_os $platform]
}
#os may itself contain a dash in principle - treat last token as cpu
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
switch -- $os {
macos {set os macosx}
}
set os [normalize_os $os]
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
@ -232,6 +293,6 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::platform [namespace eval punk::platform {
variable version
set version 0.1.0
set version 0.2.0
}]
return

914
src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm

@ -1,914 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024 JMN
# (C) 2009 Path Thoyts <patthyts@users.sourceforge.net>
#
# @@ Meta Begin
# Application punk::zip 0.1.1
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::zip 0 0.1.1]
#[copyright "2024"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
#[require punk::zip]
#[keywords module zip fileformat]
#[description]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::zip
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::zip
#[list_begin itemized]
package require Tcl 8.6-
package require punk::args
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {punk::args}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::zip {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
#variable xyz
#*** !doctools
#[subsection {Namespace punk::zip}]
#[para] Core API functions for punk::zip
#[list_begin definitions]
proc Path_a_atorbelow_b {path_a path_b} {
return [expr {[StripPath $path_b $path_a] ne $path_a}]
}
proc Path_a_at_b {path_a path_b} {
return [expr {[StripPath $path_a $path_b] eq "." }]
}
proc Path_strip_alreadynormalized_prefixdepth {path prefix} {
if {$prefix eq ""} {
return $path
}
set pathparts [file split $path]
set prefixparts [file split $prefix]
if {[llength $prefixparts] >= [llength $pathparts]} {
return ""
}
return [file join \
{*}[lrange \
$pathparts \
[llength $prefixparts] \
end]]
}
#StripPath - borrowed from tcllib fileutil
# ::fileutil::stripPath --
#
# If the specified path references/is a path in prefix (or prefix itself) it
# is made relative to prefix. Otherwise it is left unchanged.
# In the case of it being prefix itself the result is the string '.'.
#
# Arguments:
# prefix prefix to strip from the path.
# path path to modify
#
# Results:
# path The (possibly) modified path.
if {[string equal $::tcl_platform(platform) windows]} {
# Windows. While paths are stored with letter-case preserved al
# comparisons have to be done case-insensitive. For reference see
# SF Tcllib Bug 2499641.
proc StripPath {prefix path} {
# [file split] is used to generate a canonical form for both
# paths, for easy comparison, and also one which is easy to modify
# using list commands.
set prefix [file split $prefix]
set npath [file split $path]
if {[string equal -nocase $prefix $npath]} {
return "."
}
if {[string match -nocase "${prefix} *" $npath]} {
set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]]
}
return $path
}
} else {
proc StripPath {prefix path} {
# [file split] is used to generate a canonical form for both
# paths, for easy comparison, and also one which is easy to modify
# using list commands.
set prefix [file split $prefix]
set npath [file split $path]
if {[string equal $prefix $npath]} {
return "."
}
if {[string match "${prefix} *" $npath]} {
set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]]
}
return $path
}
}
proc Timet_to_dos {time_t} {
#*** !doctools
#[call [fun Timet_to_dos] [arg time_t]]
#[para] convert a unix timestamp into a DOS timestamp for ZIP times.
#[example {
# DOS timestamps are 32 bits split into bit regions as follows:
# 24 16 8 0
# +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
# |Y|Y|Y|Y|Y|Y|Y|m| |m|m|m|d|d|d|d|d| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s|
# +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
#}]
set s [clock format $time_t -format {%Y %m %e %k %M %S}]
scan $s {%d %d %d %d %d %d} year month day hour min sec
expr {(($year-1980) << 25) | ($month << 21) | ($day << 16)
| ($hour << 11) | ($min << 5) | ($sec >> 1)}
}
punk::args::define {
@id -id ::punk::zip::walk
@cmd -name punk::zip::walk -help\
"Walk the directory structure starting at base/<-subpath>
and return a list of the files and folders encountered.
Resulting paths are relative to base unless -resultrelative
is supplied.
Folder names will end with a trailing slash.
"
-resultrelative -optional 1 -help\
"Resulting paths are relative to this value.
Defaults to the value of base. If empty string
is given to -resultrelative the paths returned
are effectively absolute paths."
-emptydirs -default 0 -type boolean -help\
"Whether to include directory trees in the result which had no
matches for the given fileglobs.
Intermediate dirs are always returned if there is a match with
fileglobs further down even if -emptdirs is 0.
"
-excludes -default "" -help "list of glob expressions to match against files and exclude"
-subpath -default "" -help\
"May contain glob chars for folder elements"
#If we don't include --, the call walk <options> -- <base> <globs>.. will return nothing as 'base' will receive the --
-- -type none -optional 1
@values -min 1 -max -1
base
fileglobs -default {*} -multiple 1
}
proc walk {args} {
#*** !doctools
#[call [fun walk] [arg ?options?] [arg base]]
#[para] Walk a directory tree rooted at base
#[para] the -excludes list can be a set of glob expressions to match against files and avoid
#[para] e.g
#[example {
# punk::zip::walk -exclude {CVS/* *~.#*} library
#}]
#todo: -relative 0|1 flag?
set argd [punk::args::parse $args withid ::punk::zip::walk]
set base [dict get $argd values base]
set fileglobs [dict get $argd values fileglobs]
set subpath [dict get $argd opts -subpath]
set excludes [dict get $argd opts -excludes]
set emptydirs [dict get $argd opts -emptydirs]
set received [dict get $argd received]
set imatch [list]
foreach fg $fileglobs {
lappend imatch [file join $subpath $fg]
}
if {![dict exists $received -resultrelative]} {
set relto $base
set prefix ""
} else {
set relto [file normalize [dict get $argd opts -resultrelative]]
if {$relto ne ""} {
if {![Path_a_atorbelow_b $base $relto]} {
error "punk::zip::walk base must be at or below -resultrelative value (backtracking not currently supported)"
}
set prefix [Path_strip_alreadynormalized_prefixdepth $base $relto]
} else {
set prefix $base
}
}
set result {}
#set imatch [file join $subpath $match]
set files [glob -nocomplain -tails -types f -directory $base -- {*}$imatch]
foreach file $files {
set excluded 0
foreach glob $excludes {
if {[string match $glob $file]} {
set excluded 1
break
}
}
if {!$excluded} {lappend result [file join $prefix $file]}
}
foreach dir [glob -nocomplain -tails -types d -directory $base -- [file join $subpath *]] {
set submatches [walk -subpath $dir -emptydirs $emptydirs -excludes $excludes $base {*}$fileglobs]
set subdir_entries [list]
set thisdir_match [list]
set has_file 0
foreach sd $submatches {
set fullpath [file join $prefix $sd] ;#file join destroys trailing slash
if {[string index $sd end] eq "/"} {
lappend subdir_entries $fullpath/
} else {
set has_file 1
lappend subdir_entries $fullpath
}
}
if {$emptydirs} {
set thisdir_match [list "[file join $prefix $dir]/"]
} else {
if {$has_file} {
set thisdir_match [list "[file join $prefix $dir]/"]
} else {
set subdir_entries [list]
}
}
#NOTE: trailing slash required for entries to be recognised as 'file type' = "directory"
#This is true for 2024 Tcl9 mounted zipfs at least. zip utilities such as 7zip seem(icon correct) to recognize dirs with or without trailing slash
#Although there are attributes on some systems to specify if entry is a directory - it appears trailing slash should always be used for folder names.
set result [list {*}$result {*}$thisdir_match {*}$subdir_entries]
}
return $result
}
#if there is an external preamble - extract that. (if there is also an internal preamble - ignore and consider part of the archive-data)
#Otherwise extract an internal preamble.
#if neither -?
#review - reconsider auto-determination of internal vs external preamble
punk::args::define {
@id -id ::punk::zip::extract_preamble
@cmd -name punk::zip::extract_preamble -help\
"Split a zipfs based executable or library into its constituent
binary and zip parts.
Note that the binary preamble might be either 'within' the zip offsets,
or simply catenated prior to an unadjusted zip.
Some build processes may have 'adjusted' the zip offsets to make the zip cover the entire file
('file based' offset) whilst the more modern approach is to simply concatenate the binary and the zip
('archive based' offset). An archive-based offset is simpler and more reliably points to the proper
split location. It also allows 'zipfs info //zipfs:/app' to return the correct offset information.
Either way, extract_preamble can usually separate them, but in the unusual case that there is both an
external preamble and a preamble within the zip, only the external preamble will be split, with the
internal one remaining in the zip.
The inverse of this process would be to extract the .zip file created by this split to a folder,
e.g extracted_zip_folder (adjusting contents as required) and then to run:
zipfs mkimg newbinaryname.exe extracted_zip_folder <prefix> \"\" <extracted_preamble_or_alternative exe>
"
@values -min 2 -max 3
infile -type file -optional 0 -help\
"Name of existing tcl executable or shared lib with attached zipfs filesystem"
outfile_preamble -optional 0 -type file -help\
"Name of output file for binary preamble to be extracted to.
If this file already exists, an error will be raised"
outfile_zip -default "" -type file -help\
"Name of output file for zip data to be extracted to.
If this file already exists, an error will be raised"
}
proc extract_preamble {args} {
set argd [punk::args::parse $args withid ::punk::zip::extract_preamble]
lassign [dict values $argd] leaders opts values received
set infile [dict get $values infile]
set outfile_preamble [dict get $values outfile_preamble]
set outfile_zip [dict get $values outfile_zip]
set inzip [open $infile r]
fconfigure $inzip -encoding iso8859-1 -translation binary
if {[file exists $outfile_preamble]} {
error "outfile_preamble $outfile_preamble already exists - please remove first"
}
if {$outfile_zip ne ""} {
if {[file exists $outfile_zip] && [file size $outfile_zip]} {
error "outfile_zip $outfile_zip already exists - please remove first"
}
}
chan seek $inzip 0 end
set insize [tell $inzip] ;#faster (including seeks) than calling out to filesystem using file size - but should be equivalent
chan seek $inzip 0 start
#only scan last 64k - cover max signature size?? review
if {$insize < 65559} {
set tailsearch_start 0
} else {
set tailsearch_start [expr {$insize - 65559}]
}
chan seek $inzip $tailsearch_start start
set scan [read $inzip]
#EOCD - End Of Central Directory record
set start_of_end [string last "\x50\x4b\x05\x06" $scan]
puts stdout "==>start_of_end: $start_of_end"
if {$start_of_end == -1} {
#no zip eocdr - consider entire file to be the zip preamble
set baseoffset $insize
} else {
set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}]
chan seek $inzip $filerelative_eocd_posn
set cdir_record_plus [read $inzip] ;#can have trailing data
binary scan $cdir_record_plus issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
#rule out a false positive from within a nonzip (e.g plain exe)
#There exists for example a PK\5\6 in a plain tclsh, but it doesn't appear to be zip related.
#It doesn't seem to occur near the end - so perhaps not an issue - but we'll do some basic checks anyway
#we only support single disk - so we'll validate a bit more by requiring disknbr and ctrldirdisk to be zeros
#todo - just search for Pk\5\6\0\0\0\0 in the first place? //review
if {$eocd(disknbr) + $eocd(ctrldirdisk) != 0} {
#review - should keep searching?
#for now we assume not a zip
set baseoffset $insize
} else {
#use the central dir size to jump back tko start of central dir
#determine if diroffset is file or archive relative
set filerelative_cdir_start [expr {$filerelative_eocd_posn - $eocd(dirsize)}]
puts stdout "---> [read $inzip 4]"
if {$filerelative_cdir_start > $eocd(diroffset)} {
#'external preamble' easy case
# - ie 'archive' offset - (and one of the reasons I prefer archive-offset - it makes finding the 'prefix' easier
#though we are assuming zip offsets are not corrupted
set baseoffset [expr {$filerelative_cdir_start - $eocd(diroffset)}]
} else {
#'internal preamble' hard case
# - either no preamble - or offsets have been adjusted to be file relative.
#we could scan from top (ugly) - and with binary prefixes we could get false positives in the data that look like PK\3\4 headers
#we could either work out the format for all possible executables that could be appended (across all platforms) and understand where they end?
#or we just look for the topmost PK\3\4 header pointed to by a CDR record - and assume the CDR is complete
#step one - read all the CD records and find the highest pointed to local file record (which isn't necessarily the first - but should get us above most if not all of the zip data)
#we can't assume they're ordered in any particular way - so we in theory have to look at them all.
set baseoffset "unknown"
chan seek $inzip $filerelative_cdir_start start
#binary scan $cdir_record_plus issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
# eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
#load the whole central dir into cdir
#todo! loop through all cdr file headers - find highest offset?
#tclZipfs.c just looks at first file header in Central Directory
#looking at all entries would be more robust - but we won't work harder than tclZipfs.c for now //REVIEW
set cdirdata [read $inzip $eocd(dirsize)]
binary scan $cdirdata issssssiiisssssii cdir(signature) cdir(_vermadeby) cdir(_verneeded) cdir(gpbitflag) cdir(compmethod) cdir(lastmodifiedtime) cdir(lastmodifieddate)\
cdir(uncompressedcrc32) cdir(compressedsize) cdir(uncompressedsize) cdir(filenamelength) cdir(extrafieldlength) cdir(filecommentlength) cdir(disknbr)\
cdir(internalfileattributes) cdir(externalfileatributes) cdir(relativeoffset)
#since we're in this branch - we assume cdir(relativeoffset) is from the start of the file
chan seek $inzip $cdir(relativeoffset)
#let's at least check that we landed on a local file header..
set local_file_header_beginning [read $inzip 28]; #local_file_header without the file name and extra field
binary scan $local_file_header_beginning isssssiiiss lfh(signature) lfh(_verneeded) lfh(gpbitflag) lfh(compmethod) lfh(lastmodifiedtime) lfh(lastmodifieddate)\
lfh(uncompressedcrc32) lfh(compressedsize) lfh(uncompressedsize) lfh(filenamelength) lfh(extrafieldlength)
#dec2hex 67324752 = 4034B50 = PK\3\4
puts stdout "1st local file header sig: $lfh(signature)"
if {$lfh(signature) == 67324752} {
#looks like a local file header
#use our cdir(relativeoffset) as the start of the zip-data (//review - possible embedded password + end marker preceeding this)
set baseoffset $cdir(relativeoffset)
}
}
puts stdout "filerel_cdirstart: $filerelative_cdir_start recorded_offset: $eocd(diroffset)"
}
}
puts stdout "baseoffset: $baseoffset"
#expect CDFH PK\1\2
#above the CD - we expect a bunch of PK\3\4 records - (possibly not all of them pointed to by the CDR)
#above that we expect: *possibly* a stored password with trailing marker - then the prefixed exe/script
if {![string is integer -strict $baseoffset]} {
error "unable to determine zip baseoffset of file $infile"
}
if {$baseoffset < $insize} {
set pout [open $outfile_preamble w]
fconfigure $pout -encoding iso8859-1 -translation binary
chan seek $inzip 0 start
chan copy $inzip $pout -size $baseoffset
close $pout
if {$outfile_zip ne ""} {
#todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile
set zout [open $outfile_zip w]
fconfigure $zout -encoding iso8859-1 -translation binary
chan copy $inzip $zout
close $zout
}
close $inzip
} else {
#no valid (from our perspective) eocdr found - baseoffset has been set to insize
close $inzip
file copy $infile $outfile_preamble
if {$outfile_zip ne ""} {
#touch equiv?
set fd [open $outfile_zip w]
close $fd
}
}
}
punk::args::define {
@id -id ::punk::zip::Addentry
@cmd -name punk::zip::Addentry\
-summary\
"Add zip-entry for file at 'path'"\
-help\
"Add a single file at 'path' to open channel 'zipchan'
return a central directory file record"
@opts
-comment -default "" -help "An optional comment specific to the added file"
@values -min 3 -max 4
zipchan -help "open file descriptor with cursor at position appropriate for writing a local file header"
base -help "base path for entries"
path -type file -help "path of file to add"
zipdataoffset -default 0 -type integer -range {0 ""} -help "offset of start of zip-data - ie length of prefixing script/exe
Can be specified as zero even if a prefix exists - which would make offsets 'file relative' as opposed to 'archive relative'"
}
# Addentry - was Mkzipfile --
#
# FIX ME: should handle the current offset for non-seekable channels
#
proc Addentry {args} {
#*** !doctools
#[call [fun Addentry] [arg zipchan] [arg base] [arg path] [arg ?comment?]]
#[para] Add a single file to a zip archive
#[para] The zipchan channel should already be open and binary.
#[para] You can provide a -comment for the file.
#[para] The return value is the central directory record that will need to be used when finalizing the zip archive.
set argd [punk::args::parse $args withid ::punk::zip::Addentry]
set zipchan [dict get $argd values zipchan]
set base [dict get $argd values base]
set path [dict get $argd values path]
set zipdataoffset [dict get $argd values zipdataoffset]
set comment [dict get $argd opts -comment]
set fullpath [file join $base $path]
set mtime [Timet_to_dos [file mtime $fullpath]]
set utfpath [encoding convertto utf-8 $path]
set utfcomment [encoding convertto utf-8 $comment]
set flags [expr {(1<<11)}] ;# utf-8 comment and path
set method 0 ;# store 0, deflate 8
set attr 0 ;# text or binary (default binary)
set version 20 ;# minumum version req'd to extract
set extra ""
set crc 0
set size 0
set csize 0
set data ""
set seekable [expr {[tell $zipchan] != -1}]
if {[file isdirectory $fullpath]} {
set attrex 0x41ff0010 ;# 0o040777 (drwxrwxrwx)
#set attrex 0x40000010
} elseif {[file executable $fullpath]} {
set attrex 0x81ff0080 ;# 0o100777 (-rwxrwxrwx)
} else {
set attrex 0x81b60020 ;# 0o100666 (-rw-rw-rw-)
if {[file extension $fullpath] in {".tcl" ".txt" ".c"}} {
set attr 1 ;# text
}
}
if {[file isfile $fullpath]} {
set size [file size $fullpath]
if {!$seekable} {set flags [expr {$flags | (1 << 3)}]}
}
set channeloffset [tell $zipchan] ;#position in the channel - this may include prefixing exe/zip
set local [binary format a4sssiiiiss PK\03\04 \
$version $flags $method $mtime $crc $csize $size \
[string length $utfpath] [string length $extra]]
append local $utfpath $extra
puts -nonewline $zipchan $local
if {[file isfile $fullpath]} {
# If the file is under 2MB then zip in one chunk, otherwize we use
# streaming to avoid requiring excess memory. This helps to prevent
# storing re-compressed data that may be larger than the source when
# handling PNG or JPEG or nested ZIP files.
if {$size < 0x00200000} {
set fin [open $fullpath rb]
set data [read $fin]
set crc [zlib crc32 $data]
set cdata [zlib deflate $data]
if {[string length $cdata] < $size} {
set method 8
set data $cdata
}
close $fin
set csize [string length $data]
puts -nonewline $zipchan $data
} else {
set method 8
set fin [open $fullpath rb]
set zlib [zlib stream deflate]
while {![eof $fin]} {
set data [read $fin 4096]
set crc [zlib crc32 $data $crc]
$zlib put $data
if {[string length [set zdata [$zlib get]]]} {
incr csize [string length $zdata]
puts -nonewline $zipchan $zdata
}
}
close $fin
$zlib finalize
set zdata [$zlib get]
incr csize [string length $zdata]
puts -nonewline $zipchan $zdata
$zlib close
}
if {$seekable} {
# update the header if the output is seekable
set local [binary format a4sssiiii PK\03\04 \
$version $flags $method $mtime $crc $csize $size]
set current [tell $zipchan]
seek $zipchan $channeloffset
puts -nonewline $zipchan $local
seek $zipchan $current
} else {
# Write a data descriptor record
set ddesc [binary format a4iii PK\7\8 $crc $csize $size]
puts -nonewline $zipchan $ddesc
}
}
#PK\x01\x02 Cdentral directory file header
#set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3
set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems)
set hdr [binary format a4ssssiiiisssssii PK\01\02 $v1 \
$version $flags $method $mtime $crc $csize $size \
[string length $utfpath] [string length $extra]\
[string length $utfcomment] 0 $attr $attrex [expr {$channeloffset - $zipdataoffset}]] ;#zipdataoffset may be zero - either because it's a pure zip, or file-based offsets desired.
append hdr $utfpath $extra $utfcomment
return $hdr
}
#### REVIEW!!!
#JMN - review - this looks to be offset relative to start of file - (same as 2024 Tcl 'mkzip mkimg')
# we want to enable (optionally) offsets relative to start of archive for exe/script-prefixed zips.on windows (editability with 7z,peazip)
####
punk::args::define {
@id -id ::punk::zip::mkzip
@cmd -name punk::zip::mkzip\
-summary\
"Create a zip archive in 'filename'."\
-help\
"Create a zip archive in 'filename'"
@opts
-offsettype -default "archive" -choices {archive file}\
-help\
"zip offsets stored relative to start of entire file or relative to start of zip-archive
Only relevant if the created file has a script/runtime prefix."
-return -default "pretty" -choices {pretty list none}\
-help\
"mkzip can return a list of the files and folders added to the archive
the option -return pretty is the default and uses the punk::lib pdict/plist system
to return a formatted list for the terminal
"
-zipkit -default 0 -type none\
-help\
"whether to add mounting script
mutually exclusive with -runtime option
currently vfs::zip based - todo - autodetect zipfs/vfs with pref for zipfs"
-runtime -default ""\
-help\
"specify a prefix file
e.g punk::zip::mkzip -runtime unzipsfx.exe -directory subdir -base subdir output.zip
will create a self-extracting zip archive from the subdir/ folder.
Expects runtime with no existing vfs attached (review)"
-comment -default ""\
-help "An optional comment for the archive"
-directory -default ""\
-help "Scan for contents within this folder or current directory if not provided."
-base -default ""\
-help\
"The new zip archive will be rooted in this directory if provided
it must be a parent of -directory or the same path as -directory"
-exclude -default {CVS/* */CVS/* *~ ".#*" "*/.#*"}
-- -type none -help\
"End of options marker"
@values -min 1 -max -1
filename -type file -default ""\
-help "name of zipfile to create"
globs -default {*} -multiple 1\
-help\
"list of glob patterns to match.
Only directories with matching files will be included in the archive."
}
# zip::mkzip --
#
# eg: zip my.zip -directory Subdir -runtime unzipsfx.exe *.txt
#
proc mkzip {args} {
#todo - doctools - [arg ?globs...?] syntax?
#*** !doctools
#[call [fun mkzip]\
# [opt "[option -offsettype] [arg offsettype]"]\
# [opt "[option -return] [arg returntype]"]\
# [opt "[option -zipkit] [arg 0|1]"]\
# [opt "[option -runtime] [arg preamble_filename]"]\
# [opt "[option -comment] [arg zipfilecomment]"]\
# [opt "[option -directory] [arg dir_to_zip]"]\
# [opt "[option -base] [arg archive_root]"]\
# [opt "[option -exclude] [arg globlist]"]\
# [arg zipfilename]\
# [arg ?glob...?]]
#[para] Create a zip archive in 'zipfilename'
#[para] If a file already exists, an error will be raised.
#[para] Call 'punk::zip::mkzip' with no arguments for usage display.
set argd [punk::args::parse $args withid ::punk::zip::mkzip]
set filename [dict get $argd values filename]
if {$filename eq ""} {
error "mkzip filename cannot be empty string"
}
if {[regexp {[?*]} $filename]} {
#catch a likely error where filename is omitted and first glob pattern is misinterpreted as zipfile name
error "mkzip filename should not contain glob characters ? *"
}
if {[file exists $filename]} {
error "mkzip filename:$filename already exists"
}
dict for {k v} [dict get $argd opts] {
switch -- $k {
-comment {
dict set argd opts $k [encoding convertto utf-8 $v]
}
-directory - -base {
dict set argd opts $k [file normalize $v]
}
}
}
array set opts [dict get $argd opts]
if {$opts(-directory) ne ""} {
if {$opts(-base) ne ""} {
#-base and -directory have been normalized already
if {![Path_a_atorbelow_b $opts(-directory) $opts(-base)]} {
error "punk::zip::mkzip -base $opts(-base) must be above or the same as -directory $opts(-directory)"
}
set base $opts(-base)
set relpath [Path_strip_alreadynormalized_prefixdepth $opts(-directory) $opts(-base)]
} else {
set base $opts(-directory)
set relpath ""
}
#will pick up intermediary folders as paths (ending with trailing slash)
set paths [walk -exclude $opts(-exclude) -subpath $relpath -- $base {*}[dict get $argd values globs]]
set norm_filename [file normalize $filename]
set norm_dir [file normalize $opts(-directory)] ;#we only care if filename below -directory (which is where we start scanning)
if {[Path_a_atorbelow_b $norm_filename $norm_dir]} {
#check that we aren't adding the zipfile to itself
#REVIEW - now that we open zipfile after scanning - this isn't really a concern!
#keep for now in case we can add an -update or a -force facility (or in case we modify to add to zip as we scan for members?)
#In the case of -force - we may want to delay replacement of original until scan is done?
#try to avoid looping on all paths and performing (somewhat) expensive file normalizations on each
#1st step is to check the patterns and see if our zipfile is already excluded - in which case we need not check the paths
set self_globs_match 0
foreach g [dict get $argd values globs] {
if {[string match $g [file tail $filename]]} {
set self_globs_match 1
break
}
}
if {$self_globs_match} {
#still dangerous
set self_excluded 0
foreach e $opts(-exclude) {
if {[string match $e [file tail $filename]]} {
set self_excluded 1
break
}
}
if {!$self_excluded} {
#still dangerous - likely to be in resultset - check each path
#puts stderr "zip file $filename is below directory $opts(-directory)"
set self_is_matched 0
set i 0
foreach p $paths {
set norm_p [file normalize [file join $opts(-directory) $p]]
if {[Path_a_at_b $norm_filename $norm_p]} {
set self_is_matched 1
break
}
incr i
}
if {$self_is_matched} {
puts stderr "WARNING - zipfile being created '$filename' was matched. Excluding this file. Relocate the zip, or use -exclude patterns to avoid this message"
set paths [lremove $paths $i]
}
}
}
}
} else {
#NOTE that we don't add intermediate folders when creating an archive without using the -directory flag!
#ie - only the exact *files* matching the glob are stored.
set paths [list]
set dir [pwd]
if {$opts(-base) ne ""} {
if {![Path_a_atorbelow_b $dir $opts(-base)]} {
error "punk::zip::mkzip -base $opts(-base) must be above current directory"
}
set relpath [Path_strip_alreadynormalized_prefixdepth [file normalize $dir] [file normalize $opts(-base)]]
} else {
set relpath ""
}
set base $opts(-base)
set matches [glob -nocomplain -type f -- {*}[dict get $argd values globs]]
foreach m $matches {
if {$m eq $filename} {
#puts stderr "--> excluding $filename"
continue
}
set isok 1
foreach e [concat $opts(-exclude) $filename] {
if {[string match $e $m]} {
set isok 0
break
}
}
if {$isok} {
lappend paths [file join $relpath $m]
}
}
}
if {![llength $paths]} {
return ""
}
set zf [open $filename wb]
if {$opts(-runtime) ne ""} {
#todo - strip any existing vfs - option to merge contents.. only if zip attached?
set rt [open $opts(-runtime) rb]
fcopy $rt $zf
close $rt
} elseif {$opts(-zipkit)} {
#TODO - update to zipfs ?
#see modpod
set zkd "#!/usr/bin/env tclkit\n\# This is a zip-based Tcl Module\n"
append zkd "package require vfs::zip\n"
append zkd "vfs::zip::Mount \[info script\] \[info script\]\n"
append zkd "if {\[file exists \[file join \[info script\] main.tcl\]\]} {\n"
append zkd " source \[file join \[info script\] main.tcl\]\n"
append zkd "}\n"
append zkd \x1A
puts -nonewline $zf $zkd
}
#todo - subtract this from the endrec offset
if {$opts(-offsettype) eq "archive"} {
set dataStartOffset [tell $zf] ;#the overall file offset of the start of archive-data //JMN 2024
} else {
set dataStartOffset 0 ;#offsets relative to file - the (old) zipfs mkzip way :/
}
set count 0
set cd ""
set members [list]
foreach path $paths {
#puts $path
lappend members $path
append cd [Addentry $zf $base $path $dataStartOffset] ;#path already includes relpath
incr count
}
set cdoffset [tell $zf]
set endrec [binary format a4ssssiis PK\05\06 0 0 \
$count $count [string length $cd] [expr {$cdoffset - $dataStartOffset}]\
[string length $opts(-comment)]]
append endrec $opts(-comment)
puts -nonewline $zf $cd
puts -nonewline $zf $endrec
close $zf
set result ""
switch -exact -- $opts(-return) {
list {
set result $members
}
pretty {
if {[info commands showlist] ne ""} {
set result [plist -channel none members]
} else {
set result $members
}
}
none {
set result ""
}
}
return $result
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::zip ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::zip::lib {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
tcl::namespace::path [tcl::namespace::parent]
#*** !doctools
#[subsection {Namespace punk::zip::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::zip::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::zip [tcl::namespace::eval punk::zip {
variable pkg punk::zip
variable version
set version 0.1.1
}]
return
#*** !doctools
#[manpage_end]

1698
src/vfs/_vfscommon.vfs/modules/punk/zip-0.2.0.tm

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save