#+TITLE: Tcl 9.1 Static Build
#+AUTHOR: Aditya Siram
#+OPTIONS: ^:nil ;; let an underscore be an underscore, disable sub/superscripting
#+OPTIONS: timestamp:nil
* Introduction
:PROPERTIES:
:ID:       45a6494c-370a-43c4-87dc-ead93d624a79
:END:
This CMake recipe builds and installs [[https://www.tcl-lang.org/software/tcltk/9.0.html][Tcl/Tk 9]] from scratch on Windows and Linux. The Tcl and Tk interpreters ~tclsh~ and ~wish~ are built statically across platforms and can be installed in a local workspace with no other requirements. The goal is to
provide a convenient environment to build and even deploy Tcl and Tk GUI applications without having set up any dependencies or rely on package managers.

The [[https://www.tcl-lang.org/software/tcllib/][Tcl Standard Library]] is also installed along with the [[https://www.tcl-lang.org/software/tcllib/][parts accelerated by Critcl.]] This includes many useful libraries that are not part of the standard Tcl distribution like [[https://wiki.tcl-lang.org/page/Tcllib+JSON][encoding/decoding JSON.]]

In addition a static version of the Tcl interpreter is also built at _configure_ time. This allows using this recipe to also be used as part of a CMake super build in which subsequent steps have full access to Tcl which is much easier to work with than CMake. As an example, it is used in this recipe [[*Check Tcl Version][to check the version of the Tcl interpreter]] to avoid rebuilding it.
* Getting Started
** Linux and MacOS
Requirements:
- ~cmake~ > 3.14
- ~gcc~ (tested with ~gcc 15~)
- Autoconf
- make

Assuming this repo has been cloned to ~tcl-static-build~ this should download Tcl/Tk sources, build and then install them to ~~/MyWorkspace/bin/~:
#+begin_example
> cd tcl-static-build
> mkdir build
> cd build
> cmake .. -DCMAKE_INSTALL_PREFIX=~/MyWorkspace -DTCL_STATIC_BUILD_TK=On
> make
#+end_example

And checking the runtime dependencies ...:
#+begin_example
> ldd ~/MyWorkspace/bin/tclsh
        linux-vdso.so.1 (0x00007fb0c6af1000)
        libz.so.1 => /usr/lib/libz.so.1 (0x00007fb0c6a9f000)
        libm.so.6 => /usr/lib/libm.so.6 (0x00007fb0c66e2000)
        libtommath.so.1 => /usr/lib/libtommath.so.1 (0x00007fb0c6a87000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007fb0c64f1000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fb0c6af3000)
> ldd ~/MyWorkspace/bin/wish9.1
        linux-vdso.so.1 (0x00007ff057210000)
        libXft.so.2 => /usr/lib/libXft.so.2 (0x00007ff056de6000)
        libfontconfig.so.1 => /usr/lib/libfontconfig.so.1 (0x00007ff056d95000)
        libX11.so.6 => /usr/lib/libX11.so.6 (0x00007ff056c53000)
        ... ( a whole bunch of X11/Wayland runtime dependencies, standard for GUI applications )
#+end_example
** Windows
Requirements:
- [[https://cmake.org/download/][~cmake~]] > 3.14
- [[https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2026][Build Tools For Visual Studio]]

Please make sure to start the build in [[https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=visualstudio][Visual Studio Developer Command Prompt]], this ensures required tools like ~nmake~ and ~vcc~ available at the command prompt which is a lot easier than hunting down explicit paths.

Assuming this repo has been cloned to ~tcl-static-build~ this should download Tcl/Tk sources, build and install them to ~C:\Users\<your-username>\Tcl\bin~:
#+begin_example
> cd tcl-static-build
> mkdir build
> cd build
> cmake.exe .. -DCMAKE_INSTALL_PREFIX=C:\Users\<your-username>\Tcl -DTCL_STATIC_BUILD_TK=On
> MSBuild.exe ALL_BUILD.vcxproj
#+end_example

And checking the runtime dependencies ...:
#+begin_example
> cd C:\Users\<your-username>\Tcl\bin
> dumpbin.exe /DEPENDENTS tclsh.exe
Microsoft (R) COFF/PE Dumper Version 14.39.33521.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file tclsh91s.exe

File Type: EXECUTABLE IMAGE

  Image has the following dependencies:

    VCRUNTIME140.dll
    KERNEL32.dll
    ADVAPI32.dll
    NETAPI32.dll
    USER32.dll
    USERENV.dll
    WS2_32.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-locale-l1-1-0.dll

  Summary

        3000 .data
       13000 .pdata
       A2000 .rdata
        3000 .reloc
        F000 .rsrc
      1CD000 .text

> dumpbin.exe /DEPENDENTS wish91s.exe
Microsoft (R) COFF/PE Dumper Version 14.39.33521.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file wish91s.exe

File Type: EXECUTABLE IMAGE

  Image has the following dependencies:

    api-ms-win-crt-runtime-l1-1-0.dll
    WS2_32.dll
    OLEAUT32.dll
    COMDLG32.dll
    COMCTL32.dll
    IMM32.dll
    UxTheme.dll
    VCRUNTIME140.dll
    KERNEL32.dll
    ADVAPI32.dll
    NETAPI32.dll
    GDI32.dll
    USER32.dll
    USERENV.dll
    WINSPOOL.DRV
    SHELL32.dll
    ole32.dll
    OLEACC.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-utility-l1-1-0.dll
    api-ms-win-crt-locale-l1-1-0.dll

  Summary

        7000 .data
       20000 .pdata
       FB000 .rdata
        7000 .reloc
       23000 .rsrc
      2FB000 .text
#+end_example

* CMake Variables
|---------------------------------------------+-----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------|
| Variable                                    | Default Value                                             | Documentation                                                                                                   |
|---------------------------------------------+-----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------|
| ~TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK~ | On                                                        | Whether to build and install Tcl and Tk. This recipe can be used to build Tcl only at configure time            |
|                                             |                                                           | so it can be part of a super build which needs Tcl in some subsequent step without installing it at build time. |
| ~TCL_STATIC_BUILD_TCL_LOCAL_SOURCES~        | ""                                                        | Use a local copy of Tcl sources for offline builds                                                              |
| ~TCL_STATIC_BUILD_CRITCL_LOCAL_SOURCES~     | ""                                                        | Use a local copy of [[https://wiki.tcl-lang.org/page/Critcl][Critcl]] sources                                                                              |
| ~TCL_STATIC_BUILD_TCLLIB_LOCAL_SOURCES~     | ""                                                        | Use a local copy of [[https://www.tcl-lang.org/software/tcllib/][TclLib]], the Tcl standard library                                                            |
| ~TCL_STATIC_BUILD_TK_LOCAL_SOURCES~         | ""                                                        | Use a local copy of Tk sources for offline builds                                                               |
| ~TCL_STATIC_BUILD_TK~                       | Off                                                       | Optionally build Tk, only required for GUI applications                                                         |
| ~TCL_STATIC_BUILD_UNIX_TCLSH~               | Configure time location of the Tcl interpreter on Unix    | Path to the configure time location of ~tclsh~ for Unix and MacOs builds                                        |
| ~TCL_STATIC_BUILD_WIN_TCLSH~                | Configure time location of the Tcl interperter on Windows | Path to the configure time location of ~tclsh~ for Windows builds                                               |
|---------------------------------------------+-----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------|
* Implementation
** Preamble
#+begin_src cmake :tangle CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(tcl-static-build)
include(ExternalProject)
include(FetchContent)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/modules")
include(Utils)

if(NOT (MSVC OR UNIX))
  message(FATAL_ERROR "This build currenly works only with Microsoft Visual Studio and Linux. MacOS is planned.")
endif()
#+end_src

~TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK~ toggles whether to build and install Tcl/Tk, if the user has not set it assume we do.
#+begin_src cmake :tangle CMakeLists.txt
if(NOT DEFINED TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK)
  set(TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK On)
endif()
#+end_src
** Check for zip
In theory if ~zip~ is not available the Tcl/Tk sources ship with ~minizip~, a small drop in replacement, but that doesn't seem to work on Unix builds.
On most distros ~zip~ is available by default and if not (looking at you Arch) it is an easy enough dependency to add.
#+begin_src cmake :tangle CMakeLists.txt
if(APPLE OR UNIX)
  find_program(TCL_STATIC_BUILD_ZIP_EXECUTABLE NAMES zip)
  if(NOT TCL_STATIC_BUILD_ZIP_EXECUTABLE)
    message(FATAL_ERROR "Could not find 'zip' which is necessary for building with ZipFs support.")
  endif()
endif()
#+end_src
** Check for libtool
#+begin_src cmake :tangle CMakeLists.txt
if(APPLE OR UNIX)
  find_program(TCL_STATIC_BUILD_LIBTOOL libtool)
  if(NOT TCL_STATIC_BUILD_LIBTOOL)
    message(FATAL_ERROR "Could not find 'libtool' which is necessary to build static archives")
  endif()
endif()
#+end_src
** Set Tcl/Tk version
#+begin_src cmake :tangle CMakeLists.txt
set(TCL_STATIC_BUILD_VERSION "9.1")
set(TCL_STATIC_ZIP_VERSION 91a1)
#+end_src
** Set Tcl Standard Library Versions
#+begin_src cmake :tangle CMakeLists.txt
set(TCL_STATIC_BUILD_CRITCL_VERSION "3.3.1")
set(TCL_STATIC_BUILD_TCLLIB_VERSION "2.0")
#+end_src
** Set Tcl/Tk Source URLs
Check if the user has specified offline paths to the Tcl/Tk source otherwise get it from [[https://www.tcl-lang.org/software/tcltk/download.html][the official site]].
#+begin_src cmake :tangle CMakeLists.txt
if(TCL_STATIC_BUILD_TCL_LOCAL_SOURCES)
  to_absolute_path(${TCL_STATIC_BUILD_TCL_LOCAL_SOURCES} TCL_STATIC_BUILD_URL)
else()
  set(TCL_STATIC_BUILD_URL http://prdownloads.sourceforge.net/tcl/tcl${TCL_STATIC_ZIP_VERSION}-src.zip)
endif()
if(TCL_STATIC_BUILD_TK_LOCAL_SOURCES)
  to_absolute_path(${TCL_STATIC_BUILD_TK_LOCAL_SOURCES} TK_STATIC_BUILD_URL)
else()
  set(TK_STATIC_BUILD_URL http://prdownloads.sourceforge.net/tcl/tk${TCL_STATIC_ZIP_VERSION}-src.zip)
endif()
#+end_src
** Set Critcl Source URL
#+begin_src cmake :tangle CMakeLists.txt
if(TCL_STATIC_BUILD_CRITCL_LOCAL_SOURCES)
  to_absolute_path(${TCL_STATIC_BUILD_CRITCL_LOCAL_SOURCES} CRITCL_STATIC_BUILD_URL)
else()
  set(CRITCL_STATIC_BUILD_URL http://github.com/andreas-kupries/critcl/zipball/${TCL_STATIC_BUILD_CRITCL_VERSION})
endif()
#+end_src
** Set Tcllib Source URL
#+begin_src cmake :tangle CMakeLists.txt
if(TCL_STATIC_BUILD_TCLLIB_LOCAL_SOURCES)
  to_absolute_path(${TCL_STATIC_BUILD_TCLLIB_LOCAL_SOURCES} CRITCL_STATIC_BUILD_URL)
else()
  set(TCLLIB_STATIC_BUILD_URL https://core.tcl-lang.org/tcllib/uv/tcllib-${TCL_STATIC_BUILD_TCLLIB_VERSION}.zip)
endif()
#+end_src
** Make Tcl/Tk sources available at configure time
*** Warn if install location is not overridden
This build is meant to be used in an isolated workspace so if ~CMAKE_INSTALL_PREFIX~ is still pointing to the system wide install location let the user know.
#+begin_src cmake :tangle CMakeLists.txt
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
  if(APPLE OR UNIX)
    set(EXAMPLE_INSTALL_PREFIX "/home/user/MyWorkspace")
  else()
    set(EXAMPLE_INSTALL_PREFIX "C:\User\user\MyWorkspace")
  endif()
  message(WARNING "CMAKE_INSTALL_PREFIX is the default ${CMAKE_INSTALL_PREFIX}. It is recommended to override it to your local workspace, eg. 'cmake .. -DCMAKE_INSTALL_PREFIX=${EXAMPLE_INSTALL_PREFIX}'")
endif()
#+end_src
*** Download or Pull in Local Tcl/Tk sources
***** Declare Tcl sources
#+begin_src cmake :tangle CMakeLists.txt
set(TCL_SOURCES tcl${TCL_STATIC_BUILD_VERSION})
FetchContent_Declare(
  ${TCL_SOURCES}
  URL ${TCL_STATIC_BUILD_URL}
  DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
#+end_src
***** Declare Critcl sources
#+begin_src cmake :tangle CMakeLists.txt
set(CRITCL_SOURCES critcl${TCL_STATIC_BUILD_CRITCL_VERSION})
FetchContent_Declare(
  ${CRITCL_SOURCES}
  URL ${CRITCL_STATIC_BUILD_URL}
  DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
#+end_src
***** Declare Tcllib sources
#+begin_src cmake :tangle CMakeLists.txt
set(TCLLIB_SOURCES tcllib${TCL_STATIC_BUILD_TCLLIB_VERSION})
FetchContent_Declare(
  ${TCLLIB_SOURCES}
  URL ${TCLLIB_STATIC_BUILD_URL}
  DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
#+end_src
***** Declare Tk sources
#+begin_src cmake :tangle CMakeLists.txt
if(TCL_STATIC_BUILD_TK)
  set(TK_SOURCES tk${TCL_STATIC_BUILD_VERSION})
  FetchContent_Declare(
    ${TK_SOURCES}
    URL ${TK_STATIC_BUILD_URL}
    DOWNLOAD_EXTRACT_TIMESTAMP FALSE
  )
endif()
#+end_src
***** Download Tcl sources
#+begin_src cmake :tangle CMakeLists.txt
if(NOT ${TCL_SOURCES}_POPULATED)
  if(NOT TCL_STATIC_BUILD_TCL_LOCAL_SOURCES)
    message(STATUS "Downloading Tcl sources: ${TCL_STATIC_BUILD_URL} ...")
  else()
    message(STATUS "Using local Tcl sources: ${TCL_STATIC_BUILD_URL}")
  endif()
  FetchContent_MakeAvailable(${TCL_SOURCES})
  if(NOT TCL_STATIC_BUILD_TCL_LOCAL_SOURCES)
    message(STATUS "Completed download: ${${TCL_SOURCES}_SOURCE_DIR}")
  endif()
endif()
#+end_src
***** Download Critcl sources
#+begin_src cmake :tangle CMakeLists.txt
if(NOT ${CRITCL_SOURCES}_POPULATED)
  if(NOT TCL_STATIC_BUILD_CRITCL_LOCAL_SOURCES)
    message(STATUS "Downloading Critcl sources: ${CRITCL_STATIC_BUILD_URL} ...")
  else()
    message(STATUS "Using local Critcl sources: ${CRITCL_STATIC_BUILD_URL}")
  endif()
  FetchContent_MakeAvailable(${CRITCL_SOURCES})
  if(NOT TCL_STATIC_BUILD_TCL_LOCAL_SOURCES)
    message(STATUS "Completed download: ${${CRITCL_SOURCES}_SOURCE_DIR}")
  endif()
endif()
#+end_src
***** Download Tcllib sources
#+begin_src cmake :tangle CMakeLists.txt
if(NOT ${TCLLIB_SOURCES}_POPULATED)
  if(NOT TCL_STATIC_BUILD_TCLLIB_LOCAL_SOURCES)
    message(STATUS "Downloading Tcllib sources: ${TCLLIB_STATIC_BUILD_URL} ...")
  else()
    message(STATUS "Using local Tcllib sources: ${TCLLIB_STATIC_BUILD_URL}")
  endif()
  FetchContent_MakeAvailable(${TCLLIB_SOURCES})
  if(NOT TCL_STATIC_BUILD_TCL_LOCAL_SOURCES)
    message(STATUS "Completed download: ${${TCLLIB_SOURCES}_SOURCE_DIR}")
  endif()
endif()
#+end_src
***** Download Tk sources
#+begin_src cmake :tangle CMakeLists.txt
if(TCL_STATIC_BUILD_TK)
  if(NOT ${TK_SOURCES}_POPULATED)
    if(NOT TCL_STATIC_BUILD_TK_LOCAL_SOURCES)
      message(STATUS "Downloading Tk sources: ${TK_STATIC_BUILD_URL} ...")
    else()
      message(STATUS "Using local Tk sources: ${TK_STATIC_BUILD_URL}")
    endif()
    FetchContent_MakeAvailable(${TK_SOURCES})
    if(NOT TCL_STATIC_BUILD_TK_LOCAL_SOURCES)
      message(STATUS "Completed download: ${${TK_SOURCES}_SOURCE_DIR}")
    endif()
  endif()
endif()
#+end_src
** Kick off a Unix or Window build
#+begin_src cmake :tangle CMakeLists.txt
if(APPLE OR UNIX)
 include(LinuxMacosBuild)
else()
  include(Windows)
endif()
#+end_src
** Linux and MacOS
*** Set configure time path to tclsh
#+begin_src cmake :tangle modules/LinuxMacosBuild.cmake
set(TCL_STATIC_BUILD_TCLSH_EXE "tclsh9.1")
set(TCL_STATIC_BUILD_WISH_EXE "wish9.1")
normalize_path(TCL_STATIC_BUILD_UNIX_TCLSH ${FETCHCONTENT_BASE_DIR} "bin" ${TCL_STATIC_BUILD_TCLSH_EXE})
normalize_path(TCL_STATIC_BUILD_UNIX_INSTALLED_TCLSH ${CMAKE_INSTALL_PREFIX} "bin" ${TCL_STATIC_BUILD_TCLSH_EXE})
#+end_src
*** Build Tcl at configure time
#+begin_src cmake :tangle modules/LinuxMacosBuild.cmake
if(EXISTS ${TCL_STATIC_BUILD_UNIX_TCLSH})
  check_tcl_version_matches_required_version(TCL_SOURCES_TCLSH_MATCHES_REQUIRED ${TCL_STATIC_BUILD_UNIX_TCLSH})
  if(${TCL_SOURCES_TCLSH_MATCHES_REQUIRED})
    message(STATUS "The Tcl executable already exists: ${TCL_STATIC_BUILD_UNIX_TCLSH}")
  else()
    message(STATUS "Rebuilding Tcl since the existing executable is not the required version: ${TCL_STATIC_BUILD_VERSION}")
    build_tcl_unix_at_configure_time(${${TCL_SOURCES}_SOURCE_DIR} ${FETCHCONTENT_BASE_DIR})
  endif()
else()
  build_tcl_unix_at_configure_time(${${TCL_SOURCES}_SOURCE_DIR} ${FETCHCONTENT_BASE_DIR})
endif()
#+end_src
*** Add Build Time Tcl Target
#+begin_src cmake :tangle modules/LinuxMacosBuild.cmake
if(TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK)
  build_tcl_unix(${${TCL_SOURCES}_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX})
  symlink_tcl(${TCL_STATIC_BUILD_TCLSH_EXE})
  build_critcl(${TCL_STATIC_BUILD_UNIX_INSTALLED_TCLSH} ${${CRITCL_SOURCES}_SOURCE_DIR})
  build_tcllib(${TCL_STATIC_BUILD_UNIX_INSTALLED_TCLSH} ${${TCLLIB_SOURCES}_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX})
  if(TCL_STATIC_BUILD_TK)
    build_tk_unix(${${TK_SOURCES}_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX} ${CMAKE_INSTALL_PREFIX})
    symlink_tk(${TCL_STATIC_BUILD_WISH_EXE})
  endif()
endif()
#+end_src
** Windows
*** Set configure time path to tclsh
Note the "s" after ~tclsh91~ and ~wish91~, probably denotes a static build ...
#+begin_src cmake :tangle modules/Windows.cmake
set(TCL_STATIC_BUILD_TCLSH_EXE "tclsh91s.exe")
set(TCL_STATIC_BUILD_WISH_EXE "wish91s.exe")
normalize_path(TCL_STATIC_BUILD_WIN_TCLSH ${FETCHCONTENT_BASE_DIR} "bin" ${TCL_STATIC_BUILD_TCLSH_EXE})
normalize_path(TCL_STATIC_BUILD_WIN_INSTALLED_TCLSH ${CMAKE_INSTALL_PREFIX} "bin" ${TCL_STATIC_BUILD_TCLSH_EXE})
#+end_src
*** Build Tcl at configure time
#+begin_src cmake :tangle modules/Windows.cmake
if(EXISTS ${TCL_STATIC_BUILD_WIN_TCLSH})
  check_tcl_version_matches_required_version(TCL_SOURCES_TCLSH_MATCHES_REQUIRED ${TCL_STATIC_BUILD_WIN_TCLSH})
  if(${TCL_SOURCES_TCLSH_MATCHES_REQUIRED})
    message(STATUS "The Tcl executable already exists: ${TCL_STATIC_BUILD_WIN_TCLSH}")
  else()
    message(STATUS "Rebuilding Tcl since the existing executable is not the required version: ${TCL_STATIC_BUILD_VERSION}")
    build_tcl_win_at_configure_time(${${TCL_SOURCES}_SOURCE_DIR} ${FETCHCONTENT_BASE_DIR})
  endif()
else()
  build_tcl_win_at_configure_time(${${TCL_SOURCES}_SOURCE_DIR} ${FETCHCONTENT_BASE_DIR})
endif()
#+end_src
*** Add Build Time Tcl Target
#+begin_src cmake :tangle modules/Windows.cmake
if(TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK)
  build_tcl_win(${${TCL_SOURCES}_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX})
  symlink_tcl_win(${TCL_STATIC_BUILD_TCLSH_EXE})
  build_critcl(${TCL_STATIC_BUILD_WIN_INSTALLED_TCLSH} ${${CRITCL_SOURCES}_SOURCE_DIR})
  build_tcllib_win(${TCL_STATIC_BUILD_WIN_INSTALLED_TCLSH} ${${TCLLIB_SOURCES}_SOURCE_DIR})
  if(TCL_STATIC_BUILD_TK)
    build_tk_win(${${TK_SOURCES}_SOURCE_DIR} ${${TCL_SOURCES}_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX})
    symlink_tk_win(${TCL_STATIC_BUILD_WISH_EXE})
  endif()
endif()
#+end_src
** Utilities
*** Concatenate and normalize a list of paths
#+begin_src cmake :tangle modules/Utils.cmake
function(normalize_path output_path path)
  cmake_path(
    APPEND path ${ARGN}
    OUTPUT_VARIABLE UNNORMALIZED_PATH_LOCAL
  )
  cmake_path(
    NORMAL_PATH UNNORMALIZED_PATH_LOCAL
    OUTPUT_VARIABLE NORMALIZED_PATH_LOCAL
  )
  set(${output_path} ${NORMALIZED_PATH_LOCAL} PARENT_SCOPE)
endfunction()
#+end_src
*** Convert path to absolute path
#+begin_src cmake :tangle modules/Utils.cmake
function(to_absolute_path path out_variable)
  normalize_path(PATH_LOCAL_NORMALIZED ${path})
  file(REAL_PATH ${PATH_LOCAL_NORMALIZED} PATH_LOCAL BASE_DIRECTORY "${CMAKE_BINARY_DIR}")
  set(${out_variable} ${PATH_LOCAL} PARENT_SCOPE)
endfunction()
#+end_src
*** Check Tcl Version
If Tcl has already been built once before check that the version Tcl executable matches what we expect.
#+begin_src cmake :tangle modules/Utils.cmake
function(check_tcl_version_matches_required_version output_variable tclsh)
  normalize_path(CHECK_VERSION_TCL_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR} "modules" "check_version.tcl")
  execute_process(
    COMMAND ${tclsh} ${CHECK_VERSION_TCL_SCRIPT} "${TCL_STATIC_BUILD_VERSION}"
    RESULT_VARIABLE TCL_VERSION_CHECK_RESULT_LOCAL
  )
  if(TCL_VERSION_CHECK_RESULT_LOCAL EQUAL 0)
    set(${output_variable} Y PARENT_SCOPE)
  else()
    set(${output_variable} N PARENT_SCOPE)
  endif()
endfunction()
#+end_src
Check the required version against the major/minor/patch version of this Tcl interpreter but only the prefix, eg. ~9.1~ will match ~9.1a~
#+begin_src tcl :tangle modules/check_version.tcl
set required_version [lindex $argv 0]
if {[string first $required_version [info patchlevel]] eq 0} {
    exit 0
} else {
    exit 1
}
#+end_src
*** Build Tcl at configure time
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcl_unix_at_configure_time source_dir install_prefix)
  find_program(MAKE_EXE NAMES make REQUIRED)
  normalize_path(TCL_SOURCES_UNIX_DIRECTORY ${source_dir} "unix")
  normalize_path(TCL_SOURCES_UNIX_CONFIGURE ${TCL_SOURCES_UNIX_DIRECTORY} "configure")
  execute_process(
    COMMAND ${TCL_SOURCES_UNIX_CONFIGURE} --disable-shared --prefix=${install_prefix}
    WORKING_DIRECTORY ${TCL_SOURCES_UNIX_DIRECTORY}
    COMMAND_ECHO STDOUT
  )
  execute_process(
    COMMAND make install
    WORKING_DIRECTORY ${TCL_SOURCES_UNIX_DIRECTORY}
    COMMAND_ECHO STDOUT
  )
endfunction()
#+end_src

#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcl_win_at_configure_time source_dir install_prefix)
  find_program(MAKE_EXE NAMES nmake nmake.exe REQUIRED)
  normalize_path(TCL_SOURCES_WIN_DIRECTORY ${source_dir} "win")
  execute_process(
    COMMAND ${MAKE_EXE} /f makefile.vc release OPTS=static INSTALLDIR=${install_prefix}
    WORKING_DIRECTORY ${TCL_SOURCES_WIN_DIRECTORY}
    COMMAND_ECHO STDOUT
  )
  execute_process(
    COMMAND ${MAKE_EXE} /f makefile.vc install OPTS=static INSTALLDIR=${install_prefix}
    WORKING_DIRECTORY ${TCL_SOURCES_WIN_DIRECTORY}
    COMMAND_ECHO STDOUT
  )
endfunction()
#+end_src
*** Build Tcl
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcl_unix source_dir install_prefix)
  find_program(MAKE_EXE make REQUIRED)
  normalize_path(TCL_SOURCES_UNIX_DIRECTORY ${source_dir} "unix")
  normalize_path(TCL_SOURCES_UNIX_CONFIGURE ${TCL_SOURCES_UNIX_DIRECTORY} "configure")
  ExternalProject_Add(
    tcl
    SOURCE_DIR ${TCL_SOURCES_UNIX_DIRECTORY}
    CONFIGURE_COMMAND ${TCL_SOURCES_UNIX_CONFIGURE} --enable-64bit --disable-shared --prefix=${install_prefix}
    BUILD_COMMAND ${MAKE_EXE}
    INSTALL_COMMAND ${MAKE_EXE} install
  )
endfunction()
#+end_src
*** Build Tcl (Windows)
NOTE:
- ~CONFIGURE_COMMAND~ must be explicitly set to the empty string on Windows or ~MSBuild.exe~ will attempt to build ~tcl~ as a CMake project
- ~BUILD_IN_SOURCE~ tells ~MSBuild.exe~ to ~cd~ into the ~tcl~ Windows source directory, if it isn't set ~MSBuild.exe~ will attempt to build from the top level directory and will error on not being able find ~makefile.vc~
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcl_win source_dir install_prefix)
  find_program(MAKE_EXE NAMES nmake nmake.exe REQUIRED)
  normalize_path(TCL_SOURCES_WIN_DIRECTORY ${source_dir} "win")
  ExternalProject_Add(
    tcl
    SOURCE_DIR ${TCL_SOURCES_WIN_DIRECTORY}
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ${MAKE_EXE} /f makefile.vc release OPTS=static INSTALLDIR=${install_prefix}
    INSTALL_COMMAND ${MAKE_EXE} /f makefile.vc install OPTS=static INSTALLDIR=${install_prefix}
    BUILD_IN_SOURCE TRUE
  )
endfunction()
#+end_src
*** Symlink Tcl
#+begin_src cmake :tangle modules/Utils.cmake
function(symlink_tcl tclsh_exe)
  add_custom_command(
    TARGET tcl
    COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/bin/${tclsh_exe} ${CMAKE_INSTALL_PREFIX}/bin/tclsh
    POST_BUILD
  )
endfunction()
#+end_src
*** Symlink Tcl (Windows)
On Windows symlinking [[https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/create-symbolic-links#reference][unfortunately requires higher privileges]], as a placeholder we just copy the binary as a safe workaround until we come up with a better alternative.
#+begin_src cmake :tangle modules/Utils.cmake
function(symlink_tcl_win tclsh_exe)
  add_custom_command(
    TARGET tcl
    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_INSTALL_PREFIX}/bin/${tclsh_exe} ${CMAKE_INSTALL_PREFIX}/bin/tclsh.exe
    POST_BUILD
  )
endfunction()
#+end_src
*** Build Critcl
#+begin_src cmake :tangle modules/Utils.cmake
function(build_critcl tclsh source_dir)
  ExternalProject_Add(
    critcl
    SOURCE_DIR ${source_dir}
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ${CMAKE_COMMAND} -E env --modify PATH=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin ${tclsh} ${source_dir}/build.tcl install
    INSTALL_COMMAND ""
    BUILD_IN_SOURCE TRUE
    DEPENDS tcl
  )
endfunction()
#+end_src
*** Build Tcllib
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcllib tclsh source_dir install_prefix)
  find_program(MAKE_EXE make REQUIRED)
  normalize_path(TCLLIB_SOURCES_UNIX_CONFIGURE ${source_dir} "configure")
  ExternalProject_Add(
    tcllib
    SOURCE_DIR ${source_dir}
    CONFIGURE_COMMAND ${TCLLIB_SOURCES_UNIX_CONFIGURE} --with-tclsh=${tclsh} --prefix=${install_prefix}
    BUILD_COMMAND ${CMAKE_COMMAND} -E env --modify PATH=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin ${MAKE_EXE}
    INSTALL_COMMAND ${CMAKE_COMMAND} -E env --modify PATH=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin ${MAKE_EXE} install
    DEPENDS critcl
  )
endfunction()
#+end_src
*** Build TclLib (Windows)
On Windows, ~critcl~ is installed as script, ~critcl.tcl~ instead of an executable so we have to set the ~CRITCL~ environment variable.

The Windows build also doesn't come with a Makefile so we copy the command line flags for building and installing used by the Unix Makefile.
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tcllib_win tclsh source_dir)
  ExternalProject_Add(
    tcllib
    SOURCE_DIR ${source_dir}
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ${CMAKE_COMMAND} -E env --modify PATH=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin ${CMAKE_COMMAND} -E env --modify CRITCL=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin/critcl.tcl ${tclsh} sak.tcl critcl -includedir ${CMAKE_INSTALL_PREFIX}/include -libdir ${CMAKE_INSTALL_PREFIX}/lib -pkg tcllibc${TCL_STATIC_BUILD_TCLSH_VERSION}
    INSTALL_COMMAND ${CMAKE_COMMAND} -E env --modify PATH=path_list_prepend:${CMAKE_INSTALL_PREFIX}/bin ${tclsh} installer.tcl -no-html -no-examples -pkg-path ${CMAKE_INSTALL_PREFIX}/lib/tcllib${TCL_STATIC_BUILD_TCLLIB_VERSION} -app-path ${CMAKE_INSTALL_PREFIX}/bin -no-wait -no-gui
    BUILD_IN_SOURCE TRUE
    DEPENDS critcl
  )
endfunction()
#+end_src
*** Build Tk
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tk_unix source_dir tcl_install_prefix install_prefix)
  find_program(MAKE_EXE make REQUIRED)
  normalize_path(TK_SOURCES_UNIX_DIRECTORY ${source_dir} "unix")
  normalize_path(TK_SOURCES_UNIX_CONFIGURE ${TK_SOURCES_UNIX_DIRECTORY} "configure")
  normalize_path(TCL_LIB_PATH ${tcl_install_prefix} "lib")
  ExternalProject_Add(
    tk
    DEPENDS tcl
    SOURCE_DIR ${TK_SOURCES_UNIX_DIRECTORY}
    CONFIGURE_COMMAND ${TK_SOURCES_UNIX_CONFIGURE} --disable-shared --with-tcl=${TCL_LIB_PATH} --prefix=${install_prefix}
    BUILD_COMMAND ${MAKE_EXE}
    INSTALL_COMMAND ${MAKE_EXE} install
  )
endfunction()
#+end_src
*** Build Tk (Windows)
NOTE:
- ~CONFIGURE_COMMAND~ must be explicitly set to the empty string on Windows or ~MSBuild.exe~ will attempt to build ~tk~ as a CMake project
- ~BUILD_IN_SOURCE~ tells ~MSBuild.exe~ to ~cd~ into the ~tk~ Windows source directory, if it isn't set ~MSBuild.exe~ will attempt to build from the top level directory and will error on not being able find ~makefile.vc~
#+begin_src cmake :tangle modules/Utils.cmake
function(build_tk_win source_dir tcl_sources_dir install_prefix)
  find_program(MAKE_EXE NAMES nmake nmake.exe REQUIRED)
  normalize_path(TK_SOURCES_WIN_DIRECTORY ${source_dir} "win")
  ExternalProject_Add(
    tk
    DEPENDS tcl
    SOURCE_DIR ${TK_SOURCES_WIN_DIRECTORY}
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ${MAKE_EXE} /f makefile.vc release OPTS=static INSTALLDIR=${install_prefix} TCLDIR=${tcl_sources_dir}
    INSTALL_COMMAND ${MAKE_EXE} /f makefile.vc install OPTS=static INSTALLDIR=${install_prefix} TCLDIR=${tcl_sources_dir}
    BUILD_IN_SOURCE TRUE
  )
endfunction()
#+end_src
*** Symlink Tk
#+begin_src cmake :tangle modules/Utils.cmake
function(symlink_tk wish_exe)
  add_custom_command(
    TARGET tk
    COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/bin/${wish_exe} ${CMAKE_INSTALL_PREFIX}/bin/wish
    POST_BUILD
  )
endfunction()
#+end_src
*** Symlink Tk (Windows)
We run into the same issue symlinking Tk as [[*Symlink Tcl (Windows)][symlinking ~tclsh~]], just copy for now and figure it out later ...
 #+begin_src cmake :tangle modules/Utils.cmake
function(symlink_tk_win wish_exe)
  add_custom_command(
    TARGET tk
    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_INSTALL_PREFIX}/bin/${wish_exe} ${CMAKE_INSTALL_PREFIX}/bin/wish.exe
    POST_BUILD
  )
endfunction()
#+end_src
