Tcl/Tk application binaries through Go

2025-02-24 dbohdan: I have discovered modernc.org/tk9.0 . This remarkable Go package is an automatic translation of Tcl/Tk 9.0 from C to pure Go. Its primary goal is to allow Go developers to use Tk without C library dependencies. However, because the binaries include a full Tcl 9 interpreter, they can run arbitrary Tcl code. When you combine this with the package embed in Go's standard library, it gives you a means to:

  • Turn Tcl code into standalone executables
  • Easily cross-compile these executables for other systems

This is something I think Tclers should know about and could make use of.

Example

In the following example, we are going to turn a single Tcl source file into a binary. Before we start, we need to install a recent version of Go. The example has been tested with Go 1.22 on Ubuntu 24.04 (x86_64).

We are going to use the unaltered code of the Tiny Excel-like app in plain Tcl/Tk as main.tcl. Create a new directory called test and save that code as main.tcl in the directory.

Now we must create a Go wrapper for our program. Save the following code as main.go in the same directory as main.tcl.

package main

import (
        _ "embed"

        . "modernc.org/tk9.0"
        . "modernc.org/tk9.0/extensions/eval"
        _ "modernc.org/tk9.0/themes/azure"
)

//go:embed main.tcl
var tclMain string

func main() {
        InitializeExtension("eval")

        ActivateTheme("azure light")
        Eval(tclMain)
        App.Center().Wait()
}

The code uses the directive //go:embed to store the contents of main.tcl in the variable tclMain at build time. It imports the Azure Tk theme. modernc.org/tk9.0 currently provides this one custom theme in addition to standard themes like Clam. The theme looks like Microsoft's Fluent Design on all systems and implements dark mode.

With the wrapper in place, let's run the commands to initialize the Go module (project) and download the dependencies. (Run them in the directory with main.go and main.tcl.)

go mod init test
go mod tidy

This will create the files go.mod and go.sum. In a real project, you should commit both to source control.

Now we have all we need to build and run the program. Let's do it.

go build -trimpath
# POSIX shell.
./test
# PowerShell or cmd.exe on Windows.
.\test.exe

Once test starts, you should see something a lot like this screenshot (minus the formulas in the cells):

modernc.org-tk9.0-test.png

The flag -trimpath excludes our full build paths from the executable for reproducible builds and privacy. The final binary is 8.3 MiB when compiled with Go 1.22 on x86_64 Ubuntu 24.04.

We can also leverage Go's cross-compilation. It takes a single command to build the same program for Windows.

GOOS=windows go build -trimpath

The resulting AMD64 Windows executable is 8.9 MiB. Note that it displays a console window in addition to the GUI.

If you are on Windows, you can cross-compile for Linux:

# cmd.exe
set GOOS=linux
go build -trimpath

# PowerShell
$env:GOOS="linux"
go build -trimpath

Limitations and quirks

By default, Linux binaries aren't static and depend on libc. You can override the linker options to build a static binary. You may want to build static binaries on a Linux system with musl libc, not glibc.

Links for more information:

Discussion