Author: Aditya Siram

# Tcl 9.1 Static Build

## Table of Contents

- 1. Introduction
- 2. Getting Started
  - 2.1. Linux and MacOS
  - 2.2. Windows
- 3. CMake Variables
- 4. Implementation
  - 4.1. Preamble
  - 4.2. Check for zip
  - 4.3. Check for libtool
  - 4.4. Set Tcl/Tk version
  - 4.5. Set Tcl Standard Library Versions
  - 4.6. Set Tcl/Tk Source URLs
  - 4.7. Set Critcl Source URL
  - 4.8. Set Tcllib Source URL
  - 4.9. Make Tcl/Tk sources available at configure time
    - 4.9.1. Warn if install location is not overridden
    - 4.9.2. Download or Pull in Local Tcl/Tk sources
  - 4.10. Kick off a Unix or Window build
  - 4.11. Linux and MacOS
    - 4.11.1. Set configure time path to tclsh
    - 4.11.2. Build Tcl at configure time
    - 4.11.3. Add Build Time Tcl Target
  - 4.12. Windows
    - 4.12.1. Set configure time path to tclsh
    - 4.12.2. Build Tcl at configure time
    - 4.12.3. Add Build Time Tcl Target
  - 4.13. Utilities
    - 4.13.1. Concatenate and normalize a list of paths
    - 4.13.2. Convert path to absolute path
    - 4.13.3. Check Tcl Version
    - 4.13.4. Build Tcl at configure time
    - 4.13.5. Build Tcl
    - 4.13.6. Build Tcl (Windows)
    - 4.13.7. Symlink Tcl
    - 4.13.8. Symlink Tcl (Windows)
    - 4.13.9. Build Critcl
    - 4.13.10. Build Tcllib
    - 4.13.11. Build TclLib (Windows)
    - 4.13.12. Build Tk
    - 4.13.13. Build Tk (Windows)
    - 4.13.14. Symlink Tk
    - 4.13.15. Symlink Tk (Windows)

## 1. Introduction

This CMake recipe builds and installs 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 Tcl Standard Library is also installed along with the parts accelerated by Critcl. This includes many useful libraries that are not part of the standard Tcl distribution like 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 to check the version of the Tcl interpreter to avoid rebuilding it.

## 2. Getting Started

### 2.1. 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/`:

    > cd tcl-static-build
    > mkdir build
    > cd build
    > cmake .. -DCMAKE_INSTALL_PREFIX=~/MyWorkspace -DTCL_STATIC_BUILD_TK=On
    > make

And checking the runtime dependencies …:

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

### 2.2. Windows

Requirements:

- `cmake` > 3.14
- Build Tools For Visual Studio

Please make sure to start the build in 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`:

    > 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

And checking the runtime dependencies …:

    > 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

## 3. 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 Critcl sources
`TCL_STATIC_BUILD_TCLLIB_LOCAL_SOURCES` | “” | Use a local copy of 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

## 4. Implementation

### 4.1. Preamble

    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()

`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.

    if(NOT DEFINED TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK)
      set(TCL_STATIC_BUILD_BUILD_AND_INSTALL_TCL_TK On)
    endif()

### 4.2. 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.

    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()

### 4.3. Check for libtool

    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()

### 4.4. Set Tcl/Tk version

    set(TCL_STATIC_BUILD_VERSION "9.1")
    set(TCL_STATIC_ZIP_VERSION 91a1)

### 4.5. Set Tcl Standard Library Versions

    set(TCL_STATIC_BUILD_CRITCL_VERSION "3.3.1")
    set(TCL_STATIC_BUILD_TCLLIB_VERSION "2.0")

### 4.6. Set Tcl/Tk Source URLs

Check if the user has specified offline paths to the Tcl/Tk source otherwise get it from the official site.

    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()

### 4.7. Set Critcl Source URL

    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()

### 4.8. Set Tcllib Source URL

    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()

### 4.9. Make Tcl/Tk sources available at configure time

#### 4.9.1. 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.

    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()

#### 4.9.2. Download or Pull in Local Tcl/Tk sources

1. Declare Tcl sources

       set(TCL_SOURCES tcl${TCL_STATIC_BUILD_VERSION})
       FetchContent_Declare(
         ${TCL_SOURCES}
         URL ${TCL_STATIC_BUILD_URL}
         DOWNLOAD_EXTRACT_TIMESTAMP FALSE
       )

2. Declare Critcl sources

       set(CRITCL_SOURCES critcl${TCL_STATIC_BUILD_CRITCL_VERSION})
       FetchContent_Declare(
         ${CRITCL_SOURCES}
         URL ${CRITCL_STATIC_BUILD_URL}
         DOWNLOAD_EXTRACT_TIMESTAMP FALSE
       )

3. Declare Tcllib sources

       set(TCLLIB_SOURCES tcllib${TCL_STATIC_BUILD_TCLLIB_VERSION})
       FetchContent_Declare(
         ${TCLLIB_SOURCES}
         URL ${TCLLIB_STATIC_BUILD_URL}
         DOWNLOAD_EXTRACT_TIMESTAMP FALSE
       )

4. Declare Tk sources

       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()

5. Download Tcl sources

       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()

6. Download Critcl sources

       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()

7. Download Tcllib sources

       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()

8. Download Tk sources

       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()

### 4.10. Kick off a Unix or Window build

    if(APPLE OR UNIX)
     include(LinuxMacosBuild)
    else()
      include(Windows)
    endif()

### 4.11. Linux and MacOS

#### 4.11.1. Set configure time path to tclsh

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

#### 4.11.2. Build Tcl at configure time

    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()

#### 4.11.3. Add Build Time Tcl Target

    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()

### 4.12. Windows

#### 4.12.1. Set configure time path to tclsh

Note the “s” after `tclsh91` and `wish91`, probably denotes a static build …

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

#### 4.12.2. Build Tcl at configure time

    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()

#### 4.12.3. Add Build Time Tcl Target

    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()

### 4.13. Utilities

#### 4.13.1. Concatenate and normalize a list of paths

    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()

#### 4.13.2. Convert path to absolute path

    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()

#### 4.13.3. Check Tcl Version

If Tcl has already been built once before check that the version Tcl executable matches what we expect.

    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()

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`

    set required_version [lindex $argv 0]
    if {[string first $required_version [info patchlevel]] eq 0} {
        exit 0
    } else {
        exit 1
    }

#### 4.13.4. Build Tcl at configure time

    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()

    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()

#### 4.13.5. Build Tcl

    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()

#### 4.13.6. 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`

    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()

#### 4.13.7. Symlink Tcl

    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()

#### 4.13.8. Symlink Tcl (Windows)

On Windows symlinking unfortunately requires higher privileges, as a placeholder we just copy the binary as a safe workaround until we come up with a better alternative.

    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()

#### 4.13.9. Build Critcl

    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()

#### 4.13.10. Build Tcllib

    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()

#### 4.13.11. 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.

    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()

#### 4.13.12. Build Tk

    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()

#### 4.13.13. 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`

    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()

#### 4.13.14. Symlink Tk

    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()

#### 4.13.15. Symlink Tk (Windows)

We run into the same issue symlinking Tk as symlinking `tclsh`, just copy for now and figure it out later …

    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()

Author: Aditya Siram