Single file applications in Tcl 9

Below is a short script illustrating wrapping a Tcl application into a single file executable.

  • The example uses wish on Windows. Other platforms would be very similar.
  • For non-GUI apps, use a tclsh and omit copying of tk_library.
  • The example uses the running program as the template for the target executable. This must be a statically linked wish or tclsh. Alternatively, for example using tclsh to build a wish-based application, or to use a non-static shell to run the script, pass the (static) template executable as an additional argument to zipfs mkimg. You can find x86 and x64 static tclsh/wish executables in the magicsplat download site.
  • The example generates a main.tcl at the top of the VFS. Normally, you would copy your application's main.tcl there. This is responsible for all initialization including setting up auto_path appropriately so packages copied into the zip are found by package require.
  • Note one difference from Tcl 8.6 tclkits. Unlike 8.6 tclkits, 9.0 "zipkits" still set up search paths using TCLLIBPATH etc. If you do not want this, you need to explicitly modify auto_path, tm paths etc. in your main.tcl

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

  • the app's source is located in /home/me/projects/releases/myAPP/src/0.1 directory having subdirectories like data , docs, packages (libs), src etc.
  • the main script of the app is src/myAPP.tcl in /home/me/projects/releases/myAPP/src/0.1
  • the resulting single-file binary is myAPP_0.1 in /home/me/projects/releases/myAPP/bin

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

Single file from scratch on MS-Windows

HaO 2025-07-29: I try to build a starkit process using tcl 9 build-in methods.

Here is the log-book:

Build static tcl and tk

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.

  • Create a new folder, say c:\build
  • Extract tcl and tk sources as subfolder of this folder, so we get c:\build\tcl9.0.1 and c:\build\tk9.0.1. The advantage of this layout is, that Tk will find the Tcl folder without TCL_DIR option. In addition, htmlhelp build target only works with this layout.
  • The folder tcl9.0.1\pkgs can be removed. It contains the bundled packages. But a static build of those is not usable for our purpose. We need a normal dll build. See chapter below.
  • Start VS2022 x64 native prompt
  • cd c:\build\tcl9.0.1\win
  • nmake -f Makefile.vc OPTS=static
  • nmake -f Makefile.vc install OPTS=static INSTALLDIR=c:\build\tcl901static
  • cd c:\build\tk9.0.1\win
  • nmake -f Makefile.vc OPTS=static
  • nmake -f Makefile.vc install OPTS=static INSTALLDIR=c:\build\tcl901static

From this build, we need the following files:

  • The excutable (tclsh) c:\build\tcl901static\bin\tclsh90s.exe or (wish) c:\build\tcl901static\bin\wish90s.exe
  • You need the tcl and tk library folder trees. They are included in the attached zip of the executables. But You also have them as zip files in c:\build\tcl901static\lib\libtcl9.0.1.zip and c:\build\tcl901static\lib\libtk9.0.1.zip

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.

wrap script

Now lets build-up a test environment. We again start with a fresh folder "c:\wrap".

  • mkdir c:\wrap
  • mkdir c:\wrap\content
  • copy the content of c:\build\tcl901static\lib\libtcl9.0.1.zip and c:\build\tcl901static\lib\libtk9.0.1.zip to this folder. Then, we have the subfolders c:\wrap\content\tcl_library and c:\wrap\content\tk_library. Those are not identical to a "noembed" build, as packages are not transformed to tcl modules and are all placed in subfolders of tcl_library etc.
  • copy c:\build\tcl901static\bin\wish90s.exe c:\wrap\.
  • place a main.tcl file to c:\wrap\content as follows:
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

  • Parameter 1: "content.exe": output filename
  • Parameter 2: "content": The folder name of the directory tree to wrap
  • Parameter 3: "content": The prefix of the folder tree to remove. In the attached zip, the main.tcl will be on root level.
  • Parameter 4: "": An optional password for the zip folder
  • Parameter 5: "wish90s.exe": The executable where the current attached zip (if any) is replaced by the given zip

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.

Binary packages

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 }
        }
    }
}

Bundled packages

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]

Custom logo

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.

Signing the executable

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