Below is a short script illustrating wrapping a Tcl application into a single file executable.
Run the script below as wish90s scriptname...
# Make sure we are running a static wish set exe_path [zipfs mount //zipfs:/app] if {$exe_path eq ""} { pack [ttk::label .e -text "Please use a static wish to make single-file exes!"] pack [ttk::button .b -text Exit -command exit] return } pack [ttk::label .l -text "Building twapi single file exe. Please wait ..."] tk::PlaceWindow . update file delete -force twapi.vfs file mkdir twapi.vfs file copy $tcl_library [file join twapi.vfs tcl_library] file copy $tk_library [file join twapi.vfs tk_library] file mkdir [file join twapi.vfs lib] file copy C:/Tcl/magic/lib/twapi5.0.2 [file join twapi.vfs lib twapi5.0.2] writeFile [file join twapi.vfs main.tcl] { lappend auto_path //zipfs:/app/lib package require twapi console show puts "Twapi [twapi::get_version]" } zipfs mkimg twapi.exe twapi.vfs twapi.vfs "" $exe_path .l configure -text "Done." pack [ttk::button .b -text Exit -command exit]
aplsimple - 2025-04-06
My two cents of comments on this masterpiece. Yes, it works on other platforms, e.g. I've successfully checked it in Linux, see the script below. The script is run as wish90s scriptname.
The script supposes that
Still, there is a problem with Linux's libc.
Namely: if you build your single-file app in Linux with the newest libc, it can fail on platforms with older libc versions, error messages would be like this ./myAPP: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.35' not found (required by ./myAPP).
I.e., your Linux single-file app would not be truly static when based on the default non-static wish9.0 binary. It would fit only to your version of Linux or newer.
The solution could be simple: search and find truly static wish9.0 binary fit for your platform.
And as Ashok notes the importance of having large base of Tcl extensions, I would also note the importance of having large base of wish9.0 single-file builds.
# application's name & version set appName myAPP set appVersion 0.1 # source directory set inpDir /home/me/projects/releases/$appName/src/$appVersion # application's main script set mainScript src/myAPP.tcl # binary directory set outDir /home/me/projects/releases/$appName/bin/$appVersion # application's binary name set outFile myAPP_$appVersion set outFile [file join [file dirname [file dirname $outDir]] $outFile] #_______________________ # Make sure we are running a static wish set exe_path [zipfs mount //zipfs:/app] if {$exe_path eq ""} { pack [ttk::label .e -text "\n Please use a static wish to make single-file exes! "] pack [ttk::button .b -text Exit -command exit] -pady 8 return } pack [ttk::label .l -text "\n Building single file exe. Please wait ... \n"] tk::PlaceWindow . update #_______________________ file delete -force $outDir file mkdir $outDir file copy $tcl_library [file join $outDir tcl_library] file copy $tk_library [file join $outDir tk_library] file mkdir [file join $outDir lib] file copy $inpDir [file join $outDir lib $appName] cd [file dirname $outDir] writeFile [file join $outDir main.tcl] " lappend auto_path //zipfs:/app/lib source //zipfs:/app/lib/$appName/$mainScript " zipfs mkimg $outFile $appName $appName "" $exe_path #_______________________ .l configure -text "\n Done." pack [ttk::button .b1 -text Exit -command exit] \ -side left -padx 8 -pady 8 pack [ttk::button .b2 -text $outFile -command "exec {$outFile} & ; exit"] \ -side left -padx 8 -pady 8 focus .b1
HaO 2025-07-29: I try to build a starkit process using tcl 9 build-in methods.
Here is the log-book:
The tcl/tk zip may be appended to it. The later used "zipfs mkimg" will remove a zip appended to the exe before appending the new one.
From this build, we need the following files:
You don't need anything else. The libtommath.dll and zlib.dll in the bin folder are not required. I don't know, why they are there.
Now lets build-up a test environment. We again start with a fresh folder "c:\wrap".
console show puts "wrapped run"
Now start an arbitrary tcl9 interpreter (including the static ones just build) and do the wrapping by:
cd {c:\wrap} zipfs mkimg content.exe content content "" wish90s.exe
The result is a content.exe executable with the small main.tcl as payload.
Here is an explanation of the zipfs mkimg command parameters
CAUTION Contrary to starkits, any folder starting with "." (in my case ".svn") are also wrapped. I suppose, also other files (for example ending on "~") were filtered by sdk.kit, but are not filtered here.
Other binary packages should by available as dynamic builds. As with starkits, they may be included into the wrapped folder and will be extracted. Remark, that a dynamic build is required for those, not a static build. Static packages must be linked and not wrapped.
On MS-Windows, a wrapped dll is copied out to the temporary folder. This copy is left there and each program start creates another copy. This was already the case with starkits. My own solution is to delete all TCL temporary folders on program exit. The temporary folder of the current run may not be deleted, as the files are still locked. But the folders of previous runs may be deleted. This results in having only one leftover folder.
My code snippet is as follows:
foreach name { TMP TEMP } { if { [info exists ::env($name)] } { foreach dir [glob -nocomplain -types d -directory $::env($name)\ "TCL\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]"]\ { catch { file delete -force -- $dir } } } }
I have build the bundled packages with a normal build, not static build. The tcl distribution is copied to: c:\builddynamic\tcl9.0.1
cd c:\builddynamic\tcl9.0.1\win nmake -f Makefile.vc nmake -f Makefile.vc INSTALL INSTALLDIR=c:\builddynamic\tcl901
I need tdbc for odbc and sqlite. I put them into the lib subfolder of my wrapped application. This location was "natural" in starkit times, as the tcl distribution was there. I don't think, it is a good choice, but anyway, here are the steps:
cd c:\wrap\content mkdir lib cd lib copy c:\builddynamic\tcl901\lib\sqlite3.49.1 . copy c:\builddynamic\tcl901\lib\tdbc1.1.10 . copy c:\builddynamic\tcl901\lib\tdbcodbc1.1.10 . copy c:\builddynamic\tcl901\lib\tcl9 .
Now add this folder to the auto_path in "c:\wrap\content\main.tcl" :
lappend auto_path [file join [file dirname [info script]] lib]
Special care may be taken about the sqlite tdbc tcl mdoule file in:
c:\wrap\content\lib\tcl9\9.0\tdbc\sqlite3-1.1.11.tm
The parent folder should be added into the module path by adding this to "c:\wrap\content\main.tcl":
tcl::tm::path add [file join [file dirname [info script]] lib/tcl9/9.0]
Starkits were able to customize the logo and the resource strings at wrap time. This is not possible any more. One may use reshacker. I decided to modify the tk source distribtion with the relevant files wish.ico and wish.rc in:
c:\build\tk9.0.1\win\rc
Of cause, this must be done, before the static build is done.
If wrapped, the executable may be signed. It still works. Here is my signing command with the certificate of my company:
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\signtool.exe" sign /n "Certificate name" /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td SHA256 /fd SHA256 c:\wrap\content.exe