From 1d6db661796b2b764e845b7cf6b9f8399037c668 Mon Sep 17 00:00:00 2001 From: Jannik Alber Date: Tue, 25 Oct 2022 22:13:02 +0200 Subject: CPack: Add Inno Setup generator --- .../ci/configure_windows_vs2022_x64_ninja.cmake | 1 + Help/cpack_gen/innosetup.rst | 420 +++++++ Help/manual/cpack-generators.7.rst | 1 + Help/release/dev/cpack-innosetup.rst | 12 + Modules/CPack.cmake | 24 +- Modules/Internal/CPack/ISComponents.pas | 88 ++ Modules/Internal/CPack/ISScript.template.in | 34 + Source/CMakeLists.txt | 1 + Source/CPack/cmCPackGeneratorFactory.cxx | 5 + Source/CPack/cmCPackInnoSetupGenerator.cxx | 1159 ++++++++++++++++++++ Source/CPack/cmCPackInnoSetupGenerator.h | 116 ++ Tests/CMakeLists.txt | 24 + Tests/CPackInnoSetupGenerator/CMakeLists.txt | 55 + Tests/CPackInnoSetupGenerator/Code.pas | 4 + .../RunCPackVerifyResult.cmake | 136 +++ Tests/CPackInnoSetupGenerator/main.c | 7 + Tests/CPackInnoSetupGenerator/my_bitmap.bmp | Bin 0 -> 9294 bytes Tests/CPackInnoSetupGenerator/my_file.txt | 1 + 18 files changed, 2081 insertions(+), 7 deletions(-) create mode 100644 Help/cpack_gen/innosetup.rst create mode 100644 Help/release/dev/cpack-innosetup.rst create mode 100644 Modules/Internal/CPack/ISComponents.pas create mode 100644 Modules/Internal/CPack/ISScript.template.in create mode 100644 Source/CPack/cmCPackInnoSetupGenerator.cxx create mode 100644 Source/CPack/cmCPackInnoSetupGenerator.h create mode 100644 Tests/CPackInnoSetupGenerator/CMakeLists.txt create mode 100644 Tests/CPackInnoSetupGenerator/Code.pas create mode 100644 Tests/CPackInnoSetupGenerator/RunCPackVerifyResult.cmake create mode 100644 Tests/CPackInnoSetupGenerator/main.c create mode 100644 Tests/CPackInnoSetupGenerator/my_bitmap.bmp create mode 100644 Tests/CPackInnoSetupGenerator/my_file.txt diff --git a/.gitlab/ci/configure_windows_vs2022_x64_ninja.cmake b/.gitlab/ci/configure_windows_vs2022_x64_ninja.cmake index 5bf0be8..54abf72 100644 --- a/.gitlab/ci/configure_windows_vs2022_x64_ninja.cmake +++ b/.gitlab/ci/configure_windows_vs2022_x64_ninja.cmake @@ -1,4 +1,5 @@ if (NOT "$ENV{CMAKE_CI_NIGHTLY}" STREQUAL "") + set(CMake_TEST_CPACK_INNOSETUP "ON" CACHE STRING "") set(CMake_TEST_ISPC "ON" CACHE STRING "") endif() set(CMake_TEST_TLS_VERIFY_URL "https://gitlab.kitware.com" CACHE STRING "") diff --git a/Help/cpack_gen/innosetup.rst b/Help/cpack_gen/innosetup.rst new file mode 100644 index 0000000..f48e7f5 --- /dev/null +++ b/Help/cpack_gen/innosetup.rst @@ -0,0 +1,420 @@ +CPack Inno Setup Generator +-------------------------- + +.. versionadded:: 3.27 + +Inno Setup is a free installer for Windows programs by Jordan Russell and +Martijn Laan (https://jrsoftware.org/isinfo.php). + +This documentation explains Inno Setup generator specific options. + +The generator provides a lot of options like components. Unfortunately, not +all features (e.g. component dependencies) are currently supported by +Inno Setup and they're ignored by the generator for now. + +CPack requires Inno Setup 6 or greater and only works on Windows. + +Variables specific to CPack Inno Setup generator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can use the following variables to change the behavior of the CPack +``INNOSETUP`` generator: + + +General +""""""" + +None of the following variables is required to be set for the Inno Setup +generator to work. If a variable is marked as mandatory below but not set, +its default value is taken. + +The variables can also contain Inno Setup constants like ``{app}``. Please +refer to the documentation of Inno Setup for more information. + +If you're asked to provide the path to any file, you can always give an +absolute path or in most cases the relative path from the top-level directory +where all files being installed by an :command:`install` instruction reside. + +CPack tries to escape quotes and other special characters for you. However, +using special characters could cause problems. + +The following variable simplifies the usage of Inno Setup in CMake: + +.. variable:: CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT + + Inno Setup only uses ``yes`` or ``no`` as boolean formats meanwhile CMake + uses a lot of alternative formats like ``ON`` or ``OFF``. Having this option + turned on enables an automatic conversion. + + Consider the following example: + + .. code-block:: cmake + + set(CMAKE_INNOSETUP_SETUP_AllowNoIcons OFF) + + If this option is turned on, the following line will be created in the output + script: ``AllowNoIcons=no``. + Else, the following erroneous line will be created: ``AllowNoIcons=OFF`` + + The conversion is enabled in every Inno Setup specific variable. + + :Mandatory: Yes + :Default: ``ON`` + + +Setup Specific Variables +"""""""""""""""""""""""" + +.. variable:: CPACK_INNOSETUP_ARCHITECTURE + + One of ``x86``, ``x64``, ``arm64`` or ``ia64``. This variable specifies the + target architecture of the installer. This also affects the Program Files + folder or registry keys being used. + + CPack tries to determine the correct value with a try compile (see + :variable:`CMAKE_SIZEOF_VOID_P`), but this option can be manually specified + too (especially when using ``ia64`` or cross-platform compilation). + + :Mandatory: Yes + :Default: Either ``x86`` or ``x64`` depending on the results of the try-compile + +.. variable:: CPACK_INNOSETUP_INSTALL_ROOT + + If you don't want the installer to create the installation directory under + Program Files, you've to specify the installation root here. + + The full directory of the installation will be: + ``${CPACK_INNOSETUP_INSTALL_ROOT}/${CPACK_PACKAGE_INSTALL_DIRECTORY}``. + + :Mandatory: Yes + :Default: ``{autopf}`` + +.. variable:: CPACK_INNOSETUP_ALLOW_CUSTOM_DIRECTORY + + If turned on, the installer allows the user to change the installation + directory providing an extra wizard page. + + :Mandatory: Yes + :Default: ``ON`` + +.. variable:: CPACK_INNOSETUP_PROGRAM_MENU_FOLDER + + The initial name of the start menu folder being created. + + If this variable is set to ``.``, then no separate folder is created, + application shortcuts will appear in the top-level start menu folder. + + :Mandatory: Yes + :Default: The value of :variable:`CPACK_PACKAGE_NAME` + +.. variable:: CPACK_INNOSETUP_LANGUAGES + + A :ref:`semicolon-separated list ` of languages you want + Inno Setup to include. + + Currently available: ``armenian``, ``brazilianPortuguese``, ``bulgarian``, + ``catalan``, ``corsican``, ``czech``, ``danish``, ``dutch``, ``english``, + ``finnish``, ``french``, ``german``, ``hebrew``, ``icelandic``, ``italian``, + ``japanese``, ``norwegian``, ``polish``, ``portuguese``, ``russian``, + ``slovak``, ``slovenian``, ``spanish``, ``turkish`` and ``ukrainian``. + This list might differ depending on the version of Inno Setup. + + :Mandatory: Yes + :Default: ``english`` + +.. variable:: CPACK_INNOSETUP_IGNORE_LICENSE_PAGE + + If you don't specify a license file using + :variable:`CPACK_RESOURCE_FILE_LICENSE`, CPack uses a file for demonstration + purposes. If you want the installer to ignore license files at all, you can + enable this option. + + :Mandatory: Yes + :Default: ``OFF`` + +.. variable:: CPACK_INNOSETUP_IGNORE_README_PAGE + + If you don't specify a readme file using + :variable:`CPACK_RESOURCE_FILE_README`, CPack uses a file for demonstration + purposes. If you want the installer to ignore readme files at all, you can + enable this option. Make sure the option is disabled when using + a custom readme file. + + :Mandatory: Yes + :Default: ``ON`` + +.. variable:: CPACK_INNOSETUP_PASSWORD + + Enables password protection and file encryption with the given password. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_USE_MODERN_WIZARD + + Enables the modern look and feel provided by Inno Setup. If this option is + turned off, the classic style is used instead. Images and icon files are + also affected. + + :Mandatory: Yes + :Default: ``OFF`` because of compatibility reasons + +.. variable:: CPACK_INNOSETUP_ICON_FILE + + The path to a custom installer ``.ico`` file. + + Use :variable:`CPACK_PACKAGE_ICON` to customize the bitmap file being shown + in the wizard. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_SETUP_ + + This group allows adapting any of the ``[Setup]`` section directives provided + by Inno Setup where ``directive`` is its name. + + Here are some examples: + + .. code-block:: cmake + + set(CPACK_INNOSETUP_SETUP_WizardSmallImageFile "my_bitmap.bmp") + set(CPACK_INNOSETUP_SETUP_AllowNoIcons OFF) # This requires CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT to be on + + All of these variables have higher priority than the others. + Consider the following example: + + .. code-block:: cmake + + set(CPACK_INNOSETUP_SETUP_Password "admin") + set(CPACK_INNOSETUP_PASSWORD "secret") + + The password will be ``admin`` at the end because ``CPACK_INNOSETUP_PASSWORD`` + has less priority than ``CPACK_INNOSETUP_SETUP_Password``. + + :Mandatory: No + + +File Specific Variables +""""""""""""""""""""""" + +Although all files being installed by an :command:`install` instruction are +automatically processed and added to the installer, there are some variables +to customize the installation process. + +Before using executables (only ``.exe`` or ``.com``) in shortcuts +(e.g. :variable:`CPACK_CREATE_DESKTOP_LINKS`) or ``[Run]`` entries, you've to +add the raw file name (without path and extension) to +:variable:`CPACK_PACKAGE_EXECUTABLES` and create a start menu shortcut +for them. + +If you have two files with the same raw name (e.g. ``a/executable.exe`` and +``b/executable.com``), an entry in the section is created twice. This will +result in undefined behavior and is not recommended. + +.. variable:: CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS + + This variable should contain a + :ref:`semicolon-separated list ` of pairs ``path``, + ``instruction`` and can be used to customize the install command being + automatically created for each file or directory. + + CPack creates the following Inno Setup instruction for every file... + + .. code-block:: + + Source: "absolute\path\to\my_file.txt"; DestDir: "{app}"; Flags: ignoreversion + + ...and the following line for every directory: + + .. code-block:: + + Name: "{app}\my_folder" + + You might want to change the destination directory or the flags of + ``my_file.txt``. Since we can also provide a relative path, the line you'd + like to have, is the following: + + .. code-block:: + + Source: "my_file.txt"; DestDir: "{userdocs}"; Flags: ignoreversion uninsneveruninstall + + You would do this by using ``my_file.txt`` as ``path`` and + ``Source: "my_file.txt"; DestDir: "{userdocs}"; Flags: ignoreversion uninsneveruninstall`` + as ``instruction``. + + You've to take care of the `escaping problem `_. + So the CMake command would be: + + .. code-block:: cmake + + set(CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS "my_file.txt;Source: \\\"my_file.txt\\\"\\; DestDir: \\\"{userdocs}\\\"\\; Flags: ignoreversion uninsneveruninstall") + + To improve readability, you should go around the escaping problem by using + :variable:`CPACK_VERBATIM_VARIABLES` or by placing the instruction into a + separate CPack project config file. + + If you customize the install instruction of a specific file, you lose the + connection to its component. To go around, manually add + ``Components: ``. You also need to add its shortcuts and ``[Run]`` + entries by yourself in a custom section, since the executable won't be found + anymore by :variable:`CPACK_PACKAGE_EXECUTABLES`. + + Here's another example (Note: You've to go around the escaping problem for + the example to work): + + .. code-block:: cmake + + set(CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS + "component1/my_folder" "Name: \"{userdocs}\\my_folder\"\; Components: component1" + "component2/my_folder2/my_file.txt" "Source: \"component2\\my_folder2\\my_file.txt\"\; DestDir: \"{app}\\my_folder2\\my_file.txt\"\; Flags: ignoreversion uninsneveruninstall\; Components: component2") + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_MENU_LINKS + + This variable should contain a + :ref:`semicolon-separated list ` of pairs ``link``, + ``link name`` and can be used to add shortcuts into the start menu folder + beside those of the executables (see :variable:`CPACK_PACKAGE_EXECUTABLES`). + While ``link name`` is the label, ``link`` can be a URL or a path relative to + the installation directory. + + Here's an example: + + .. code-block:: cmake + + set(CPACK_INNOSETUP_MENU_LINKS + "doc/cmake-@CMake_VERSION_MAJOR@.@CMake_VERSION_MINOR@/cmake.html" + "CMake Help" "https://cmake.org" "CMake Web Site") + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_CREATE_UNINSTALL_LINK + + If this option is turned on, a shortcut to the application's uninstaller is + automatically added to the start menu folder. + + :Mandatory: Yes + :Default: ``OFF`` + +.. variable:: CPACK_INNOSETUP_RUN_EXECUTABLES + + A :ref:`semicolon-separated list ` of executables being + specified in :variable:`CPACK_PACKAGE_EXECUTABLES` which the user can run + when the installer finishes. + + They're internally added to the ``[Run]`` section. + + :Mandatory: No + + +Components Specific Variables +""""""""""""""""""""""""""""" + +The generator supports components and also downloaded components. However, +there are some features of components that aren't supported yet (especially +component dependencies). These variables are ignored for now. + +CPack will change a component's name in Inno Setup if it has a parent group +for technical reasons. Consider using ``group\component`` as component name in +Inno Setup scripts if you have the component ``component`` and its parent +group ``group``. + +Here are some additional variables for components: + +.. variable:: CPACK_INNOSETUP__INSTALL_DIRECTORY + + If you don't want the component ``compName`` to be installed under ``{app}``, + you've to specify its installation directory here. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_VERIFY_DOWNLOADS + + This option only affects downloaded components. + + If this option is turned on, the hashes of the downloaded archives are + calculated during compile and + download time. The installer will only proceed if they match. + + :Mandatory: Yes + :Default: ``ON`` + + +Compilation and Scripting Specific Variables +"""""""""""""""""""""""""""""""""""""""""""" + +.. variable:: CPACK_INNOSETUP_EXECUTABLE + + The filename of the Inno Setup Script Compiler command. + + :Mandatory: Yes + :Default: ``ISCC`` + +.. variable:: CPACK_INNOSETUP_EXECUTABLE_ARGUMENTS + + A :ref:`semicolon-separated list ` of extra + command-line options for the Inno Setup Script Compiler command. + + For example: ``/Qp;/Smysigntool=$p`` + + Take care of the `escaping problem `_. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_DEFINE_ + + This group allows to add custom define directives as command-line options to + the Inno Setup Preprocessor command. Each entry emulates a + ``#define public `` directive. Its macro is accessible from anywhere + (``public``), so it can also be used in extra script files. + + Macro names must not contain any special characters. Refer to the Inno Setup + Preprocessor documentation for the detailed rules. + + Consider the following example: + + .. code-block:: cmake + + # The following line emulates: #define public MyMacro "Hello, World!" + set(CPACK_INNOSETUP_DEFINE_MyMacro "Hello, World!") + + At this point, you can use ``MyMacro`` anywhere. For example in the following + extra script: + + .. code-block:: + + AppComments={#emit "'My Macro' has the value: " + MyMacro} + + Take care of the `escaping problem `_. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_EXTRA_SCRIPTS + + A :ref:`semicolon-separated list ` of paths to + additional ``.iss`` script files to be processed. + + They're internally included at the top of the output script file using a + ``#include`` directive. + + You can add any section in your file to extend the installer (e.g. adding + additional tasks or registry keys). Prefer using + :variable:`CPACK_INNOSETUP_SETUP_` when extending the + ``[Setup]`` section. + + :Mandatory: No + +.. variable:: CPACK_INNOSETUP_CODE_FILES + + A :ref:`semicolon-separated list ` of paths to + additional Pascal files to be processed. + + This variable is actually the same as + :variable:`CPACK_INNOSETUP_EXTRA_SCRIPTS`, except you don't have to + add ``[Code]`` at the top of your file. Never change the current section in + a code file. This will result in undefined behavior! Treat them as normal + Pascal scripts instead. + + Code files are included at the very bottom of the output script. + + :Mandatory: No diff --git a/Help/manual/cpack-generators.7.rst b/Help/manual/cpack-generators.7.rst index ade9149..abb291b 100644 --- a/Help/manual/cpack-generators.7.rst +++ b/Help/manual/cpack-generators.7.rst @@ -20,6 +20,7 @@ Generators /cpack_gen/dmg /cpack_gen/external /cpack_gen/freebsd + /cpack_gen/innosetup /cpack_gen/ifw /cpack_gen/nsis /cpack_gen/nuget diff --git a/Help/release/dev/cpack-innosetup.rst b/Help/release/dev/cpack-innosetup.rst new file mode 100644 index 0000000..a9f8e8e --- /dev/null +++ b/Help/release/dev/cpack-innosetup.rst @@ -0,0 +1,12 @@ +cpack-innosetup +--------------- + +* The :cpack_gen:`CPack Inno Setup Generator` was added to package using + Inno Setup. + + The new generator adds: + + * A lot of options to customize the Inno Setup installer (e.g. custom + installation rules) + * Start menu and desktop shortcuts + * Components (and also downloaded components) diff --git a/Modules/CPack.cmake b/Modules/CPack.cmake index f9cf33f..ff1cb7e 100644 --- a/Modules/CPack.cmake +++ b/Modules/CPack.cmake @@ -262,8 +262,8 @@ installers. The most commonly-used variables are: Lists each of the executables and associated text label to be used to create Start Menu shortcuts. For example, setting this to the list ``ccmake;CMake`` will create a shortcut named "CMake" that will execute the - installed executable :program:`ccmake`. Not all CPack generators use it (at least - NSIS, and WIX do). + installed executable :program:`ccmake`. Not all CPack generators use it (at least + NSIS, Inno Setup and WIX do). .. variable:: CPACK_STRIP_FILES @@ -738,14 +738,16 @@ if(NOT CPACK_GENERATOR) ) endif() else() - option(CPACK_BINARY_7Z "Enable to build 7-Zip packages" OFF) - option(CPACK_BINARY_NSIS "Enable to build NSIS packages" ON) - option(CPACK_BINARY_NUGET "Enable to build NuGet packages" OFF) - option(CPACK_BINARY_WIX "Enable to build WiX packages" OFF) - option(CPACK_BINARY_ZIP "Enable to build ZIP packages" OFF) + option(CPACK_BINARY_7Z "Enable to build 7-Zip packages" OFF) + option(CPACK_BINARY_NSIS "Enable to build NSIS packages" ON) + option(CPACK_BINARY_INNOSETUP "Enable to build Inno Setup packages" OFF) + option(CPACK_BINARY_NUGET "Enable to build NuGet packages" OFF) + option(CPACK_BINARY_WIX "Enable to build WiX packages" OFF) + option(CPACK_BINARY_ZIP "Enable to build ZIP packages" OFF) mark_as_advanced( CPACK_BINARY_7Z CPACK_BINARY_NSIS + CPACK_BINARY_INNOSETUP CPACK_BINARY_NUGET CPACK_BINARY_WIX CPACK_BINARY_ZIP @@ -762,6 +764,7 @@ if(NOT CPACK_GENERATOR) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_FREEBSD FREEBSD) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_IFW IFW) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_NSIS NSIS) + cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_INNOSETUP INNOSETUP) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_NUGET NuGet) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_PRODUCTBUILD productbuild) cpack_optional_append(CPACK_GENERATOR CPACK_BINARY_RPM RPM) @@ -869,6 +872,13 @@ if(NOT DEFINED CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE unset(_CPack_CMP0133) endif() +# Inno Setup specific variables +if(CMAKE_SIZEOF_VOID_P EQUAL 4) + _cpack_set_default(CPACK_INNOSETUP_ARCHITECTURE "x86") +elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) + _cpack_set_default(CPACK_INNOSETUP_ARCHITECTURE "x64") +endif() + # WiX specific variables _cpack_set_default(CPACK_WIX_SIZEOF_VOID_P "${CMAKE_SIZEOF_VOID_P}") diff --git a/Modules/Internal/CPack/ISComponents.pas b/Modules/Internal/CPack/ISComponents.pas new file mode 100644 index 0000000..8b5c8b4 --- /dev/null +++ b/Modules/Internal/CPack/ISComponents.pas @@ -0,0 +1,88 @@ +{ Distributed under the OSI-approved BSD 3-Clause License. See accompanying +file Copyright.txt or https://cmake.org/licensing for details. } + +function CPackGetCustomInstallationMessage(Param: String): String; +begin + Result := SetupMessage(msgCustomInstallation); +end; + +{ Downloaded components } +#ifdef CPackDownloadCount +const + NO_PROGRESS_BOX = 4; + RESPOND_YES_TO_ALL = 16; +var + CPackDownloadPage: TDownloadWizardPage; + CPackShell: Variant; + + +procedure CPackInitializeWizard(); +begin + CPackDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); + CPackShell := CreateOleObject('Shell.Application'); +end; + + +function CPackNextButtonClick(CurPageID: Integer): Boolean; +begin + if CurPageID = wpReady then + begin + CPackDownloadPage.Clear; + CPackDownloadPage.Show; + +#sub AddDownload + if WizardIsComponentSelected('{#CPackDownloadComponents[i]}') then + #emit "CPackDownloadPage.Add('" + CPackDownloadUrls[i] + "', '" + CPackDownloadArchives[i] + ".zip', '" + CPackDownloadHashes[i] + "');" +#endsub +#define i +#for {i = 0; i < CPackDownloadCount; i++} AddDownload +#undef i + + try + try + CPackDownloadPage.Download; + Result := True; + except + if not CPackDownloadPage.AbortedByUser then + SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); + + Result := False; + end; + finally + CPackDownloadPage.Hide; + end; + end else + Result := True; +end; + +procedure CPackExtractFile(ArchiveName, FileName: String); +var + ZipFileName: String; + ZipFile: Variant; + Item: Variant; + TargetFolderName: String; + TargetFolder: Variant; +begin + TargetFolderName := RemoveBackslashUnlessRoot(ExpandConstant('{tmp}\' + ArchiveName + '\' + ExtractFileDir(FileName))); + ZipFileName := ExpandConstant('{tmp}\' + ArchiveName + '.zip'); + + if not DirExists(TargetFolderName) then + if not ForceDirectories(TargetFolderName) then + RaiseException(Format('Target path "%s" cannot be created', [TargetFolderName])); + + ZipFile := CPackShell.NameSpace(ZipFileName); + if VarIsClear(ZipFile) then + RaiseException(Format('Cannot open ZIP file "%s" or does not exist', [ZipFileName])); + + Item := ZipFile.ParseName(FileName); + if VarIsClear(Item) then + RaiseException(Format('Cannot find "%s" in "%s" ZIP file', [FileName, ZipFileName])); + + TargetFolder := CPackShell.NameSpace(TargetFolderName); + if VarIsClear(TargetFolder) then + RaiseException(Format('Target path "%s" does not exist', [TargetFolderName])); + + TargetFolder.CopyHere(Item, NO_PROGRESS_BOX or RESPOND_YES_TO_ALL); +end; + +#endif diff --git a/Modules/Internal/CPack/ISScript.template.in b/Modules/Internal/CPack/ISScript.template.in new file mode 100644 index 0000000..1171058 --- /dev/null +++ b/Modules/Internal/CPack/ISScript.template.in @@ -0,0 +1,34 @@ +; Script generated by the CPack Inno Setup generator. +; All changes made in this file will be lost when CPack is run again. + +@CPACK_INNOSETUP_INCLUDES_INTERNAL@ + +[Setup] +@CPACK_INNOSETUP_SETUP_INTERNAL@ + +[Languages] +@CPACK_INNOSETUP_LANGUAGES_INTERNAL@ + +[Dirs] +@CPACK_INNOSETUP_DIRS_INTERNAL@ + +[Files] +@CPACK_INNOSETUP_FILES_INTERNAL@ + +[Types] +@CPACK_INNOSETUP_TYPES_INTERNAL@ + +[Components] +@CPACK_INNOSETUP_COMPONENTS_INTERNAL@ + +[Tasks] +@CPACK_INNOSETUP_TASKS_INTERNAL@ + +[Icons] +@CPACK_INNOSETUP_ICONS_INTERNAL@ + +[Run] +@CPACK_INNOSETUP_RUN_INTERNAL@ + +[Code] +@CPACK_INNOSETUP_CODE_INTERNAL@ diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 4a7d9bc..2354f3d 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -1018,6 +1018,7 @@ add_library( CPack/cmCPackGeneratorFactory.cxx CPack/cmCPackGenerator.cxx CPack/cmCPackLog.cxx + CPack/cmCPackInnoSetupGenerator.cxx CPack/cmCPackNSISGenerator.cxx CPack/cmCPackNuGetGenerator.cxx CPack/cmCPackSTGZGenerator.cxx diff --git a/Source/CPack/cmCPackGeneratorFactory.cxx b/Source/CPack/cmCPackGeneratorFactory.cxx index efb94b9..6ca48bf 100644 --- a/Source/CPack/cmCPackGeneratorFactory.cxx +++ b/Source/CPack/cmCPackGeneratorFactory.cxx @@ -13,6 +13,7 @@ #include "cmCPackDebGenerator.h" #include "cmCPackExternalGenerator.h" #include "cmCPackGenerator.h" +#include "cmCPackInnoSetupGenerator.h" #include "cmCPackLog.h" #include "cmCPackNSISGenerator.h" #include "cmCPackNuGetGenerator.h" @@ -60,6 +61,10 @@ cmCPackGeneratorFactory::cmCPackGeneratorFactory() this->RegisterGenerator("STGZ", "Self extracting Tar GZip compression", cmCPackSTGZGenerator::CreateGenerator); } + if (cmCPackInnoSetupGenerator::CanGenerate()) { + this->RegisterGenerator("INNOSETUP", "Inno Setup packages", + cmCPackInnoSetupGenerator::CreateGenerator); + } if (cmCPackNSISGenerator::CanGenerate()) { this->RegisterGenerator("NSIS", "Null Soft Installer", cmCPackNSISGenerator::CreateGenerator); diff --git a/Source/CPack/cmCPackInnoSetupGenerator.cxx b/Source/CPack/cmCPackInnoSetupGenerator.cxx new file mode 100644 index 0000000..d8825d4 --- /dev/null +++ b/Source/CPack/cmCPackInnoSetupGenerator.cxx @@ -0,0 +1,1159 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying +file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmCPackInnoSetupGenerator.h" + +#include +#include +#include +#include +#include +#include + +#include "cmsys/RegularExpression.hxx" + +#include "cmCPackComponentGroup.h" +#include "cmCPackLog.h" +#include "cmDuration.h" +#include "cmGeneratedFileStream.h" +#include "cmList.h" +#include "cmStringAlgorithms.h" +#include "cmSystemTools.h" + +cmCPackInnoSetupGenerator::cmCPackInnoSetupGenerator() = default; +cmCPackInnoSetupGenerator::~cmCPackInnoSetupGenerator() = default; + +bool cmCPackInnoSetupGenerator::CanGenerate() +{ + // Inno Setup is only available for Windows +#ifdef _WIN32 + return true; +#else + return false; +#endif +} + +int cmCPackInnoSetupGenerator::InitializeInternal() +{ + if (cmIsOn(GetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY"))) { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "Inno Setup Generator cannot work with " + "CPACK_INCLUDE_TOPLEVEL_DIRECTORY set. " + "This option will be reset to 0 (for this generator only)." + << std::endl); + SetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", nullptr); + } + + std::vector path; + +#ifdef _WIN32 + path.push_back("C:\\Program Files (x86)\\Inno Setup 5"); + path.push_back("C:\\Program Files (x86)\\Inno Setup 6"); +#endif + + SetOptionIfNotSet("CPACK_INNOSETUP_EXECUTABLE", "ISCC"); + const std::string& isccPath = cmSystemTools::FindProgram( + GetOption("CPACK_INNOSETUP_EXECUTABLE"), path, false); + + if (isccPath.empty()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Cannot find Inno Setup compiler ISCC: " + "likely it is not installed, or not in your PATH" + << std::endl); + + return 0; + } + + const std::string isccCmd = cmStrCat(QuotePath(isccPath), "/?"); + cmCPackLogger(cmCPackLog::LOG_VERBOSE, + "Test Inno Setup version: " << isccCmd << std::endl); + std::string output; + cmSystemTools::RunSingleCommand(isccCmd, &output, &output, nullptr, nullptr, + this->GeneratorVerbose, cmDuration::zero()); + cmsys::RegularExpression vRex("Inno Setup ([0-9]+)"); + if (!vRex.find(output)) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Problem checking Inno Setup version with command: " + << isccCmd << std::endl + << "Have you downloaded Inno Setup from " + "https://jrsoftware.org/isinfo.php?" + << std::endl); + return 0; + } + + const int isccVersion = atoi(vRex.match(1).c_str()); + const int minIsccVersion = 6; + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "Inno Setup Version: " << isccVersion << std::endl); + + if (isccVersion < minIsccVersion) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPack requires Inno Setup Version 6 or greater. " + "Inno Setup found on the system was: " + << isccVersion << std::endl); + return 0; + } + + SetOption("CPACK_INSTALLER_PROGRAM", isccPath); + + return this->Superclass::InitializeInternal(); +} + +int cmCPackInnoSetupGenerator::PackageFiles() +{ + // Includes + if (IsSet("CPACK_INNOSETUP_EXTRA_SCRIPTS")) { + const cmList extraScripts(GetOption("CPACK_INNOSETUP_EXTRA_SCRIPTS")); + + for (const std::string& i : extraScripts) { + includeDirectives.push_back(cmStrCat( + "#include ", QuotePath(cmSystemTools::CollapseFullPath(i, toplevel)))); + } + } + + // [Languages] section + SetOptionIfNotSet("CPACK_INNOSETUP_LANGUAGES", "english"); + const cmList languages(GetOption("CPACK_INNOSETUP_LANGUAGES")); + for (std::string i : languages) { + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = Quote(i); + + if (cmSystemTools::LowerCase(i) == "english") { + params["MessagesFile"] = "\"compiler:Default.isl\""; + } else { + i[0] = static_cast(std::toupper(i[0])); + params["MessagesFile"] = cmStrCat("\"compiler:Languages\\", i, ".isl\""); + } + + languageInstructions.push_back(ISKeyValueLine(params)); + } + + if (!Components.empty() && !ProcessComponents()) { + return false; + } + + if (!(ProcessSetupSection() && ProcessFiles())) { + return false; + } + + // [Code] section + if (IsSet("CPACK_INNOSETUP_CODE_FILES")) { + const cmList codeFiles(GetOption("CPACK_INNOSETUP_CODE_FILES")); + + for (const std::string& i : codeFiles) { + codeIncludes.push_back(cmStrCat( + "#include ", QuotePath(cmSystemTools::CollapseFullPath(i, toplevel)))); + } + } + + return ConfigureISScript() && Compile(); +} + +bool cmCPackInnoSetupGenerator::ProcessSetupSection() +{ + if (!RequireOption("CPACK_PACKAGE_INSTALL_REGISTRY_KEY")) { + return false; + } + setupDirectives["AppId"] = GetOption("CPACK_PACKAGE_INSTALL_REGISTRY_KEY"); + + if (!RequireOption("CPACK_PACKAGE_NAME")) { + return false; + } + setupDirectives["AppName"] = GetOption("CPACK_PACKAGE_NAME"); + setupDirectives["UninstallDisplayName"] = GetOption("CPACK_PACKAGE_NAME"); + + if (!RequireOption("CPACK_PACKAGE_VERSION")) { + return false; + } + setupDirectives["AppVersion"] = GetOption("CPACK_PACKAGE_VERSION"); + + if (!RequireOption("CPACK_PACKAGE_VENDOR")) { + return false; + } + setupDirectives["AppPublisher"] = GetOption("CPACK_PACKAGE_VENDOR"); + + if (IsSet("CPACK_PACKAGE_HOMEPAGE_URL")) { + setupDirectives["AppPublisherURL"] = + GetOption("CPACK_PACKAGE_HOMEPAGE_URL"); + setupDirectives["AppSupportURL"] = GetOption("CPACK_PACKAGE_HOMEPAGE_URL"); + setupDirectives["AppUpdatesURL"] = GetOption("CPACK_PACKAGE_HOMEPAGE_URL"); + } + + SetOptionIfNotSet("CPACK_INNOSETUP_IGNORE_LICENSE_PAGE", "OFF"); + if (IsSet("CPACK_RESOURCE_FILE_LICENSE") && + !GetOption("CPACK_INNOSETUP_IGNORE_LICENSE_PAGE").IsOn()) { + setupDirectives["LicenseFile"] = cmSystemTools::ConvertToWindowsOutputPath( + GetOption("CPACK_RESOURCE_FILE_LICENSE")); + } + + SetOptionIfNotSet("CPACK_INNOSETUP_IGNORE_README_PAGE", "ON"); + if (IsSet("CPACK_RESOURCE_FILE_README") && + !GetOption("CPACK_INNOSETUP_IGNORE_README_PAGE").IsOn()) { + setupDirectives["InfoBeforeFile"] = + cmSystemTools::ConvertToWindowsOutputPath( + GetOption("CPACK_RESOURCE_FILE_README")); + } + + SetOptionIfNotSet("CPACK_INNOSETUP_USE_MODERN_WIZARD", "OFF"); + if (GetOption("CPACK_INNOSETUP_USE_MODERN_WIZARD").IsOn()) { + setupDirectives["WizardStyle"] = "modern"; + } else { + setupDirectives["WizardStyle"] = "classic"; + setupDirectives["WizardSmallImageFile"] = + "compiler:WizClassicSmallImage.bmp"; + setupDirectives["WizardImageFile"] = "compiler:WizClassicImage.bmp"; + setupDirectives["SetupIconFile"] = "compiler:SetupClassicIcon.ico"; + } + + if (IsSet("CPACK_INNOSETUP_ICON_FILE")) { + setupDirectives["SetupIconFile"] = + cmSystemTools::ConvertToWindowsOutputPath( + GetOption("CPACK_INNOSETUP_ICON_FILE")); + } + + if (IsSet("CPACK_PACKAGE_ICON")) { + setupDirectives["WizardSmallImageFile"] = + cmSystemTools::ConvertToWindowsOutputPath( + GetOption("CPACK_PACKAGE_ICON")); + } + + if (!RequireOption("CPACK_PACKAGE_INSTALL_DIRECTORY")) { + return false; + } + SetOptionIfNotSet("CPACK_INNOSETUP_INSTALL_ROOT", "{autopf}"); + setupDirectives["DefaultDirName"] = + cmSystemTools::ConvertToWindowsOutputPath( + cmStrCat(GetOption("CPACK_INNOSETUP_INSTALL_ROOT"), '\\', + GetOption("CPACK_PACKAGE_INSTALL_DIRECTORY"))); + + SetOptionIfNotSet("CPACK_INNOSETUP_ALLOW_CUSTOM_DIRECTORY", "ON"); + if (GetOption("CPACK_INNOSETUP_ALLOW_CUSTOM_DIRECTORY").IsOff()) { + setupDirectives["DisableDirPage"] = "yes"; + } + + SetOptionIfNotSet("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER", + GetOption("CPACK_PACKAGE_NAME")); + if (GetOption("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER") == ".") { + setupDirectives["DisableProgramGroupPage"] = "yes"; + toplevelProgramFolder = true; + } else { + setupDirectives["DefaultGroupName"] = + GetOption("CPACK_INNOSETUP_PROGRAM_MENU_FOLDER"); + toplevelProgramFolder = false; + } + + if (IsSet("CPACK_INNOSETUP_PASSWORD")) { + setupDirectives["Password"] = GetOption("CPACK_INNOSETUP_PASSWORD"); + setupDirectives["Encryption"] = "yes"; + } + + /* + * These directives can only be modified using the + * CPACK_INNOSETUP_SETUP_ variables + */ + setupDirectives["ShowLanguageDialog"] = "auto"; + setupDirectives["AllowNoIcons"] = "yes"; + setupDirectives["Compression"] = "lzma"; + setupDirectives["SolidCompression"] = "yes"; + + // Output file and directory + if (!RequireOption("CPACK_PACKAGE_FILE_NAME")) { + return false; + } + setupDirectives["OutputBaseFilename"] = GetOption("CPACK_PACKAGE_FILE_NAME"); + + if (!RequireOption("CPACK_TOPLEVEL_DIRECTORY")) { + return false; + } + setupDirectives["OutputDir"] = cmSystemTools::ConvertToWindowsOutputPath( + GetOption("CPACK_TOPLEVEL_DIRECTORY")); + + setupDirectives["SourceDir"] = + cmSystemTools::ConvertToWindowsOutputPath(toplevel); + + // Target architecture + if (!RequireOption("CPACK_INNOSETUP_ARCHITECTURE")) { + return false; + } + + const std::string& architecture = GetOption("CPACK_INNOSETUP_ARCHITECTURE"); + if (architecture != "x86" && architecture != "x64" && + architecture != "arm64" && architecture != "ia64") { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_INNOSETUP_ARCHITECTURE must be either x86, x64, " + "arm64 or ia64" + << std::endl); + return false; + } + + // The following directives must not be set to target x86 + if (architecture != "x86") { + setupDirectives["ArchitecturesAllowed"] = architecture; + setupDirectives["ArchitecturesInstallIn64BitMode"] = architecture; + } + + /* + * Handle custom directives (they have higher priority than other variables, + * so they have to be processed after all other variables) + */ + for (const std::string& i : GetOptions()) { + if (cmHasPrefix(i, "CPACK_INNOSETUP_SETUP_")) { + const std::string& directive = + i.substr(cmStrLen("CPACK_INNOSETUP_SETUP_")); + setupDirectives[directive] = GetOption(i); + } + } + + return true; +} + +bool cmCPackInnoSetupGenerator::ProcessFiles() +{ + std::map customFileInstructions; + if (IsSet("CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS")) { + const cmList instructions( + GetOption("CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS")); + if (instructions.size() % 2 != 0) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS should " + "contain pairs of and " + << std::endl); + return false; + } + + for (auto it = instructions.begin(); it != instructions.end(); ++it) { + const std::string& key = + QuotePath(cmSystemTools::CollapseFullPath(*it, toplevel)); + customFileInstructions[key] = *(++it); + } + } + + const std::string& iconsPrefix = + toplevelProgramFolder ? "{autoprograms}\\" : "{group}\\"; + + std::map icons; + if (IsSet("CPACK_PACKAGE_EXECUTABLES")) { + const cmList executables(GetOption("CPACK_PACKAGE_EXECUTABLES")); + if (executables.size() % 2 != 0) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_PACKAGE_EXECUTABLES should should contain pairs of " + " and " + << std::endl); + return false; + } + + for (auto it = executables.begin(); it != executables.end(); ++it) { + const std::string& key = *it; + icons[key] = *(++it); + } + } + + std::vector desktopIcons; + if (IsSet("CPACK_CREATE_DESKTOP_LINKS")) { + cmExpandList(GetOption("CPACK_CREATE_DESKTOP_LINKS"), desktopIcons); + } + + std::vector runExecutables; + if (IsSet("CPACK_INNOSETUP_RUN_EXECUTABLES")) { + cmExpandList(GetOption("CPACK_INNOSETUP_RUN_EXECUTABLES"), runExecutables); + } + + for (const std::string& i : files) { + cmCPackInnoSetupKeyValuePairs params; + + std::string toplevelDirectory; + std::string outputDir; + cmCPackComponent* component = nullptr; + std::string componentParam; + if (!Components.empty()) { + const std::string& fileName = cmSystemTools::RelativePath(toplevel, i); + const std::string::size_type pos = fileName.find('/'); + + // Use the custom component install directory if we have one + if (pos != std::string::npos) { + const std::string& componentName = fileName.substr(0, pos); + component = &Components[componentName]; + + toplevelDirectory = + cmSystemTools::CollapseFullPath(componentName, toplevel); + outputDir = CustomComponentInstallDirectory(component); + componentParam = + CreateRecursiveComponentPath(component->Group, component->Name); + + if (component->IsHidden && component->IsDisabledByDefault) { + continue; + } + + if (component->IsHidden) { + componentParam.clear(); + } + } else { + // Don't install component directories + continue; + } + } else { + toplevelDirectory = toplevel; + outputDir = "{app}"; + } + + if (!componentParam.empty()) { + params["Components"] = componentParam; + } + + if (cmSystemTools::FileIsDirectory(i)) { + // Custom instructions replace the automatic generated instructions + if (customFileInstructions.count(QuotePath(i))) { + dirInstructions.push_back(customFileInstructions[QuotePath(i)]); + } else { + std::string destDir = cmSystemTools::ConvertToWindowsOutputPath( + cmStrCat(outputDir, '\\', + cmSystemTools::RelativePath(toplevelDirectory, i))); + cmStripSuffixIfExists(destDir, '\\'); + + params["Name"] = QuotePath(destDir); + + dirInstructions.push_back(ISKeyValueLine(params)); + } + } else { + // Custom instructions replace the automatic generated instructions + if (customFileInstructions.count(QuotePath(i))) { + fileInstructions.push_back(customFileInstructions[QuotePath(i)]); + } else { + std::string destDir = cmSystemTools::ConvertToWindowsOutputPath( + cmStrCat(outputDir, '\\', + cmSystemTools::GetParentDirectory( + cmSystemTools::RelativePath(toplevelDirectory, i)))); + cmStripSuffixIfExists(destDir, '\\'); + + params["DestDir"] = QuotePath(destDir); + + if (component != nullptr && component->IsDownloaded) { + const std::string& archiveName = + cmSystemTools::GetFilenameWithoutLastExtension( + component->ArchiveFile); + const std::string& relativePath = + cmSystemTools::RelativePath(toplevelDirectory, i); + + params["Source"] = + QuotePath(cmStrCat("{tmp}\\", archiveName, '\\', relativePath)); + params["ExternalSize"] = + std::to_string(cmSystemTools::FileLength(i)); + params["Flags"] = "external ignoreversion"; + params["BeforeInstall"] = + cmStrCat("CPackExtractFile('", archiveName, "', '", + cmRemoveQuotes(cmSystemTools::ConvertToWindowsOutputPath( + relativePath)), + "')"); + } else { + params["Source"] = QuotePath(i); + params["Flags"] = "ignoreversion"; + } + + fileInstructions.push_back(ISKeyValueLine(params)); + + // Icon + const std::string& name = + cmSystemTools::GetFilenameWithoutLastExtension(i); + const std::string& extension = + cmSystemTools::GetFilenameLastExtension(i); + if ((extension == ".exe" || extension == ".com") && // only .exe, .com + icons.count(name)) { + cmCPackInnoSetupKeyValuePairs iconParams; + + iconParams["Name"] = QuotePath(cmStrCat(iconsPrefix, icons[name])); + iconParams["Filename"] = + QuotePath(cmStrCat(destDir, '\\', name, extension)); + + if (!componentParam.empty()) { + iconParams["Components"] = componentParam; + } + + iconInstructions.push_back(ISKeyValueLine(iconParams)); + + // Desktop icon + if (std::find(desktopIcons.begin(), desktopIcons.end(), name) != + desktopIcons.end()) { + iconParams["Name"] = + QuotePath(cmStrCat("{autodesktop}\\", icons[name])); + iconParams["Tasks"] = "desktopicon"; + + if (!componentParam.empty() && + std::find(desktopIconComponents.begin(), + desktopIconComponents.end(), + componentParam) == desktopIconComponents.end()) { + desktopIconComponents.push_back(componentParam); + } + iconInstructions.push_back(ISKeyValueLine(iconParams)); + } + + // [Run] section + if (std::find(runExecutables.begin(), runExecutables.end(), name) != + runExecutables.end()) { + cmCPackInnoSetupKeyValuePairs runParams; + + runParams["Filename"] = iconParams["Filename"]; + runParams["Description"] = cmStrCat( + "\"{cm:LaunchProgram,", PrepareForConstant(icons[name]), "}\""); + runParams["Flags"] = "nowait postinstall skipifsilent"; + + if (!componentParam.empty()) { + runParams["Components"] = componentParam; + } + + runInstructions.push_back(ISKeyValueLine(runParams)); + } + } + } + } + } + + // Additional icons + static cmsys::RegularExpression urlRegex( + "^(mailto:|(ftps?|https?|news)://).*$"); + + if (IsSet("CPACK_INNOSETUP_MENU_LINKS")) { + const cmList menuIcons(GetOption("CPACK_INNOSETUP_MENU_LINKS")); + if (menuIcons.size() % 2 != 0) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_INNOSETUP_MENU_LINKS should " + "contain pairs of and " + << std::endl); + return false; + } + + for (auto it = menuIcons.begin(); it != menuIcons.end(); ++it) { + const std::string& target = *it; + const std::string& label = *(++it); + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = QuotePath(cmStrCat(iconsPrefix, label)); + if (urlRegex.find(target)) { + params["Filename"] = Quote(target); + } else { + std::string dir = "{app}"; + std::string componentName; + for (const auto& i : Components) { + if (cmSystemTools::FileExists(cmSystemTools::CollapseFullPath( + cmStrCat(i.second.Name, '\\', target), toplevel))) { + dir = CustomComponentInstallDirectory(&i.second); + componentName = + CreateRecursiveComponentPath(i.second.Group, i.second.Name); + + if (i.second.IsHidden && i.second.IsDisabledByDefault) { + goto continueOuterLoop; + } else if (i.second.IsHidden) { + componentName.clear(); + } + + break; + } + } + + params["Filename"] = QuotePath(cmStrCat(dir, '\\', target)); + + if (!componentName.empty()) { + params["Components"] = componentName; + } + } + + iconInstructions.push_back(ISKeyValueLine(params)); + continueOuterLoop:; + } + } + + SetOptionIfNotSet("CPACK_INNOSETUP_CREATE_UNINSTALL_LINK", "OFF"); + if (GetOption("CPACK_INNOSETUP_CREATE_UNINSTALL_LINK").IsOn()) { + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = QuotePath( + cmStrCat(iconsPrefix, "{cm:UninstallProgram,", + PrepareForConstant(GetOption("CPACK_PACKAGE_NAME")), '}')); + params["Filename"] = "\"{uninstallexe}\""; + + iconInstructions.push_back(ISKeyValueLine(params)); + } + + return true; +} + +bool cmCPackInnoSetupGenerator::ProcessComponents() +{ + codeIncludes.push_back("{ The following lines are required by CPack because " + "this script uses components }"); + + // Installation types + bool noTypes = true; + std::vector types(InstallationTypes.size()); + for (auto& i : InstallationTypes) { + noTypes = false; + types[i.second.Index - 1] = &i.second; + } + + std::vector allTypes; // For required components + for (cmCPackInstallationType* i : types) { + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = Quote(i->Name); + params["Description"] = Quote(i->DisplayName); + + allTypes.push_back(i->Name); + typeInstructions.push_back(ISKeyValueLine(params)); + } + + if (!noTypes) { + // Inno Setup requires the "custom" type + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = "\"custom\""; + params["Description"] = "\"{code:CPackGetCustomInstallationMessage}\""; + params["Flags"] = "iscustom"; + + allTypes.push_back("custom"); + typeInstructions.push_back(ISKeyValueLine(params)); + } + + // Components + std::vector downloadedComponents; + std::stack groups; + for (auto& i : Components) { + cmCPackInnoSetupKeyValuePairs params; + cmCPackComponent* component = &i.second; + + if (component->IsHidden) { + continue; + } + + CreateRecursiveComponentGroups(component->Group); + + params["Name"] = + Quote(CreateRecursiveComponentPath(component->Group, component->Name)); + params["Description"] = Quote(component->DisplayName); + + if (component->IsRequired) { + params["Types"] = cmJoin(allTypes, " "); + params["Flags"] = "fixed"; + } else if (!component->InstallationTypes.empty()) { + std::vector installationTypes; + + for (cmCPackInstallationType* j : component->InstallationTypes) { + installationTypes.push_back(j->Name); + } + + params["Types"] = cmJoin(installationTypes, " "); + } + + componentInstructions.push_back(ISKeyValueLine(params)); + + if (component->IsDownloaded) { + downloadedComponents.push_back(component); + + if (component->ArchiveFile.empty()) { + // Compute the name of the archive. + if (!RequireOption("CPACK_TEMPORARY_DIRECTORY")) { + return false; + } + + std::string packagesDir = + cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), ".dummy"); + component->ArchiveFile = + cmStrCat(cmSystemTools::GetFilenameWithoutLastExtension(packagesDir), + '-', component->Name, ".zip"); + } else if (!cmHasSuffix(component->ArchiveFile, ".zip")) { + component->ArchiveFile = cmStrCat(component->ArchiveFile, ".zip"); + } + } + } + + // Downloaded components + if (!downloadedComponents.empty()) { + // Create the directory for the upload area + cmValue userUploadDirectory = GetOption("CPACK_UPLOAD_DIRECTORY"); + std::string uploadDirectory; + if (cmNonempty(userUploadDirectory)) { + uploadDirectory = *userUploadDirectory; + } else { + if (!RequireOption("CPACK_PACKAGE_DIRECTORY")) { + return false; + } + + uploadDirectory = + cmStrCat(GetOption("CPACK_PACKAGE_DIRECTORY"), "/CPackUploads"); + } + + if (!cmSystemTools::FileExists(uploadDirectory)) { + if (!cmSystemTools::MakeDirectory(uploadDirectory)) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Unable to create Inno Setup upload directory " + << uploadDirectory << std::endl); + return false; + } + } + + if (!RequireOption("CPACK_DOWNLOAD_SITE")) { + return false; + } + + SetOptionIfNotSet("CPACK_INNOSETUP_VERIFY_DOWNLOADS", "ON"); + const bool verifyDownloads = + GetOption("CPACK_INNOSETUP_VERIFY_DOWNLOADS").IsOn(); + + const std::string& urlPrefix = + cmHasSuffix(GetOption("CPACK_DOWNLOAD_SITE").GetCStr(), '/') + ? GetOption("CPACK_DOWNLOAD_SITE") + : cmStrCat(GetOption("CPACK_DOWNLOAD_SITE"), '/'); + + std::vector archiveUrls; + std::vector archiveFiles; + std::vector archiveHashes; + std::vector archiveComponents; + for (cmCPackComponent* i : downloadedComponents) { + std::string hash; + if (!BuildDownloadedComponentArchive( + i, uploadDirectory, (verifyDownloads ? &hash : nullptr))) { + return false; + } + + archiveUrls.push_back(Quote(cmStrCat(urlPrefix, i->ArchiveFile))); + archiveFiles.push_back( + Quote(cmSystemTools::GetFilenameWithoutLastExtension(i->ArchiveFile))); + archiveHashes.push_back(Quote(hash)); + archiveComponents.push_back( + Quote(CreateRecursiveComponentPath(i->Group, i->Name))); + } + + SetOption("CPACK_INNOSETUP_DOWNLOAD_COUNT_INTERNAL", + std::to_string(archiveFiles.size())); + SetOption("CPACK_INNOSETUP_DOWNLOAD_URLS_INTERNAL", + cmJoin(archiveUrls, ", ")); + SetOption("CPACK_INNOSETUP_DOWNLOAD_ARCHIVES_INTERNAL", + cmJoin(archiveFiles, ", ")); + SetOption("CPACK_INNOSETUP_DOWNLOAD_HASHES_INTERNAL", + cmJoin(archiveHashes, ", ")); + SetOption("CPACK_INNOSETUP_DOWNLOAD_COMPONENTS_INTERNAL", + cmJoin(archiveComponents, ", ")); + + static const std::string& downloadLines = + "#define protected CPackDownloadCount " + "@CPACK_INNOSETUP_DOWNLOAD_COUNT_INTERNAL@\n" + "#dim protected CPackDownloadUrls[CPackDownloadCount] " + "{@CPACK_INNOSETUP_DOWNLOAD_URLS_INTERNAL@}\n" + "#dim protected CPackDownloadArchives[CPackDownloadCount] " + "{@CPACK_INNOSETUP_DOWNLOAD_ARCHIVES_INTERNAL@}\n" + "#dim protected CPackDownloadHashes[CPackDownloadCount] " + "{@CPACK_INNOSETUP_DOWNLOAD_HASHES_INTERNAL@}\n" + "#dim protected CPackDownloadComponents[CPackDownloadCount] " + "{@CPACK_INNOSETUP_DOWNLOAD_COMPONENTS_INTERNAL@}"; + + std::string output; + if (!ConfigureString(downloadLines, output)) { + return false; + } + codeIncludes.push_back(output); + } + + // Add the required script + const std::string& componentsScriptTemplate = + FindTemplate("ISComponents.pas"); + if (componentsScriptTemplate.empty()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Could not find additional Inno Setup script file." + << std::endl); + return false; + } + + codeIncludes.push_back("#include " + QuotePath(componentsScriptTemplate) + + "\n"); + + return true; +} + +bool cmCPackInnoSetupGenerator::ConfigureISScript() +{ + const std::string& isScriptTemplate = FindTemplate("ISScript.template.in"); + const std::string& isScriptFile = + cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISScript.iss"); + + if (isScriptTemplate.empty()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Could not find Inno Setup installer template file." + << std::endl); + return false; + } + + // Create internal variables + std::vector setupSection; + for (const auto& i : setupDirectives) { + setupSection.push_back(cmStrCat(i.first, '=', TranslateBool(i.second))); + } + + // Also create comments if the sections are empty + const std::string& defaultMessage = + "; CPack didn't find any entries for this section"; + + if (IsSet("CPACK_CREATE_DESKTOP_LINKS") && + !GetOption("CPACK_CREATE_DESKTOP_LINKS").Get()->empty()) { + cmCPackInnoSetupKeyValuePairs tasks; + tasks["Name"] = "\"desktopicon\""; + tasks["Description"] = "\"{cm:CreateDesktopIcon}\""; + tasks["GroupDescription"] = "\"{cm:AdditionalIcons}\""; + tasks["Flags"] = "unchecked"; + + if (!desktopIconComponents.empty()) { + tasks["Components"] = cmJoin(desktopIconComponents, " "); + } + + SetOption("CPACK_INNOSETUP_TASKS_INTERNAL", ISKeyValueLine(tasks)); + } else { + SetOption("CPACK_INNOSETUP_TASKS_INTERNAL", defaultMessage); + } + + SetOption("CPACK_INNOSETUP_INCLUDES_INTERNAL", + includeDirectives.empty() ? "; No extra script files specified" + : cmJoin(includeDirectives, "\n")); + SetOption("CPACK_INNOSETUP_SETUP_INTERNAL", + setupSection.empty() ? defaultMessage + : cmJoin(setupSection, "\n")); + SetOption("CPACK_INNOSETUP_LANGUAGES_INTERNAL", + languageInstructions.empty() ? defaultMessage + : cmJoin(languageInstructions, "\n")); + SetOption("CPACK_INNOSETUP_DIRS_INTERNAL", + dirInstructions.empty() ? defaultMessage + : cmJoin(dirInstructions, "\n")); + SetOption("CPACK_INNOSETUP_FILES_INTERNAL", + fileInstructions.empty() ? defaultMessage + : cmJoin(fileInstructions, "\n")); + SetOption("CPACK_INNOSETUP_TYPES_INTERNAL", + typeInstructions.empty() ? defaultMessage + : cmJoin(typeInstructions, "\n")); + SetOption("CPACK_INNOSETUP_COMPONENTS_INTERNAL", + componentInstructions.empty() + ? defaultMessage + : cmJoin(componentInstructions, "\n")); + SetOption("CPACK_INNOSETUP_ICONS_INTERNAL", + iconInstructions.empty() ? defaultMessage + : cmJoin(iconInstructions, "\n")); + SetOption("CPACK_INNOSETUP_RUN_INTERNAL", + runInstructions.empty() ? defaultMessage + : cmJoin(runInstructions, "\n")); + SetOption("CPACK_INNOSETUP_CODE_INTERNAL", + codeIncludes.empty() ? "{ No extra code files specified }" + : cmJoin(codeIncludes, "\n")); + + cmCPackLogger(cmCPackLog::LOG_VERBOSE, + "Configure file: " << isScriptTemplate << " to " + << isScriptFile << std::endl); + + return ConfigureFile(isScriptTemplate, isScriptFile); +} + +bool cmCPackInnoSetupGenerator::Compile() +{ + const std::string& isScriptFile = + cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISScript.iss"); + const std::string& isccLogFile = + cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/ISCCOutput.log"); + + std::vector isccArgs; + + // Custom defines + for (const std::string& i : GetOptions()) { + if (cmHasPrefix(i, "CPACK_INNOSETUP_DEFINE_")) { + const std::string& name = i.substr(cmStrLen("CPACK_INNOSETUP_DEFINE_")); + isccArgs.push_back( + cmStrCat("\"/D", name, '=', TranslateBool(GetOption(i)), '"')); + } + } + + if (IsSet("CPACK_INNOSETUP_EXECUTABLE_ARGUMENTS")) { + const cmList args(GetOption("CPACK_INNOSETUP_EXECUTABLE_ARGUMENTS")); + + isccArgs.insert(isccArgs.end(), args.begin(), args.end()); + } + + const std::string& isccCmd = + cmStrCat(QuotePath(GetOption("CPACK_INSTALLER_PROGRAM")), ' ', + cmJoin(isccArgs, " "), ' ', QuotePath(isScriptFile)); + + cmCPackLogger(cmCPackLog::LOG_VERBOSE, "Execute: " << isccCmd << std::endl); + + std::string output; + int retVal = 1; + const bool res = cmSystemTools::RunSingleCommand( + isccCmd, &output, &output, &retVal, nullptr, this->GeneratorVerbose, + cmDuration::zero()); + + if (!res || retVal) { + cmGeneratedFileStream ofs(isccLogFile); + ofs << "# Run command: " << isccCmd << std::endl + << "# Output:" << std::endl + << output << std::endl; + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Problem running ISCC. Please check " + << isccLogFile << " for errors." << std::endl); + return false; + } + + return true; +} + +bool cmCPackInnoSetupGenerator::BuildDownloadedComponentArchive( + cmCPackComponent* component, const std::string& uploadDirectory, + std::string* hash) +{ + // Remove the old archive, if one exists + const std::string& archiveFile = + uploadDirectory + '/' + component->ArchiveFile; + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "- Building downloaded component archive: " << archiveFile + << std::endl); + if (cmSystemTools::FileExists(archiveFile, true)) { + if (!cmSystemTools::RemoveFile(archiveFile)) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Unable to remove archive file " << archiveFile + << std::endl); + return false; + } + } + + // Find a ZIP program + if (!IsSet("ZIP_EXECUTABLE")) { + ReadListFile("Internal/CPack/CPackZIP.cmake"); + + if (!IsSet("ZIP_EXECUTABLE")) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Unable to find ZIP program" << std::endl); + return false; + } + } + + if (!RequireOption("CPACK_TOPLEVEL_DIRECTORY") || + !RequireOption("CPACK_TEMPORARY_DIRECTORY") || + !RequireOption("CPACK_ZIP_NEED_QUOTES") || + !RequireOption("CPACK_ZIP_COMMAND")) { + return false; + } + + // The directory where this component's files reside + const std::string& dirName = + cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), '/', component->Name); + + // Build the list of files to go into this archive + const std::string& zipListFileName = + cmStrCat(GetOption("CPACK_TEMPORARY_DIRECTORY"), "/winZip.filelist"); + const bool needQuotesInFile = cmIsOn(GetOption("CPACK_ZIP_NEED_QUOTES")); + { // the scope is needed for cmGeneratedFileStream + cmGeneratedFileStream out(zipListFileName); + for (const std::string& i : component->Files) { + out << (needQuotesInFile ? Quote(i) : i) << std::endl; + } + } + + // Build the archive in the upload area + std::string cmd = GetOption("CPACK_ZIP_COMMAND"); + cmsys::SystemTools::ReplaceString(cmd, "", archiveFile.c_str()); + cmsys::SystemTools::ReplaceString(cmd, "", + zipListFileName.c_str()); + std::string output; + int retVal = -1; + const bool res = cmSystemTools::RunSingleCommand( + cmd, &output, &output, &retVal, dirName.c_str(), this->GeneratorVerbose, + cmDuration::zero()); + if (!res || retVal) { + std::string tmpFile = + cmStrCat(GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/CompressZip.log"); + cmGeneratedFileStream ofs(tmpFile); + ofs << "# Run command: " << cmd << std::endl + << "# Output:" << std::endl + << output << std::endl; + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Problem running zip command: " << cmd << std::endl + << "Please check " << tmpFile + << " for errors" + << std::endl); + return false; + } + + // Try to get the SHA256 hash of the archive file + if (hash == nullptr) { + return true; + } + +#ifdef _WIN32 + const std::string& hashCmd = + cmStrCat("certutil -hashfile ", QuotePath(archiveFile), " SHA256"); + + std::string hashOutput; + int hashRetVal = -1; + const bool hashRes = cmSystemTools::RunSingleCommand( + hashCmd, &hashOutput, &hashOutput, &hashRetVal, nullptr, + this->GeneratorVerbose, cmDuration::zero()); + if (!hashRes || hashRetVal) { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "Problem running certutil command: " << hashCmd + << std::endl); + } + *hash = cmTrimWhitespace(cmTokenize(hashOutput, "\n").at(1)); + + if (hash->length() != 64) { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "Problem parsing certutil output of command: " << hashCmd + << std::endl); + hash->clear(); + } +#endif + + return true; +} + +cmValue cmCPackInnoSetupGenerator::RequireOption(const std::string& key) +{ + cmValue value = GetOption(key); + + if (!value) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Required variable " << key << " not set" << std::endl); + } + + return value; +} + +std::string cmCPackInnoSetupGenerator::CustomComponentInstallDirectory( + const cmCPackComponent* component) +{ + cmValue outputDir = GetOption( + cmStrCat("CPACK_INNOSETUP_", component->Name, "_INSTALL_DIRECTORY")); + if (outputDir) { + std::string destDir = cmSystemTools::ConvertToWindowsOutputPath(outputDir); + cmStripSuffixIfExists(destDir, '\\'); + + /* + * Add a dir instruction for the custom directory + * (only once and not for Inno Setup constants ending with '}') + */ + static std::vector customDirectories; + if (!cmHasSuffix(destDir, '}') && + std::find(customDirectories.begin(), customDirectories.end(), + component->Name) == customDirectories.end()) { + cmCPackInnoSetupKeyValuePairs params; + params["Name"] = QuotePath(destDir); + params["Components"] = + CreateRecursiveComponentPath(component->Group, component->Name); + + dirInstructions.push_back(ISKeyValueLine(params)); + customDirectories.push_back(component->Name); + } + return destDir; + } + + return "{app}"; +} + +std::string cmCPackInnoSetupGenerator::TranslateBool(const std::string& value) +{ + if (value.empty()) { + return value; + } + + SetOptionIfNotSet("CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT", "ON"); + if (GetOption("CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT").IsOn()) { + if (cmIsOn(value)) { + return "yes"; + } + if (cmIsOff(value)) { + return "no"; + } + } + + return value; +} + +std::string cmCPackInnoSetupGenerator::ISKeyValueLine( + const cmCPackInnoSetupKeyValuePairs& params) +{ + /* + * To simplify readability of the generated code, the keys are sorted. + * Unknown keys are ignored to avoid errors during compilation. + */ + static const char* const availableKeys[] = { + "Source", "DestDir", "Name", "Filename", + "Description", "GroupDescription", "MessagesFile", "Types", + "ExternalSize", "BeforeInstall", "Flags", "Components", + "Tasks" + }; + + std::vector keys; + for (const char* i : availableKeys) { + if (params.count(i)) { + keys.push_back(cmStrCat(i, ": ", params.at(i))); + } + } + + return cmJoin(keys, "; "); +} + +std::string cmCPackInnoSetupGenerator::CreateRecursiveComponentPath( + cmCPackComponentGroup* group, const std::string& path) +{ + if (group == nullptr) { + return path; + } + + const std::string& newPath = + path.empty() ? group->Name : cmStrCat(group->Name, '\\', path); + return CreateRecursiveComponentPath(group->ParentGroup, newPath); +} + +void cmCPackInnoSetupGenerator::CreateRecursiveComponentGroups( + cmCPackComponentGroup* group) +{ + if (group == nullptr) { + return; + } + + CreateRecursiveComponentGroups(group->ParentGroup); + + static std::vector processedGroups; + if (std::find(processedGroups.begin(), processedGroups.end(), group) == + processedGroups.end()) { + processedGroups.push_back(group); + + cmCPackInnoSetupKeyValuePairs params; + + params["Name"] = Quote(CreateRecursiveComponentPath(group)); + params["Description"] = Quote(group->DisplayName); + + componentInstructions.push_back(ISKeyValueLine(params)); + } +} + +std::string cmCPackInnoSetupGenerator::Quote(const std::string& string) +{ + if (cmHasPrefix(string, '"') && cmHasSuffix(string, '"')) { + return Quote(string.substr(1, string.length() - 2)); + } + + // Double quote syntax + std::string nString = string; + cmSystemTools::ReplaceString(nString, "\"", "\"\""); + return cmStrCat('"', nString, '"'); +} + +std::string cmCPackInnoSetupGenerator::QuotePath(const std::string& path) +{ + return Quote(cmSystemTools::ConvertToWindowsOutputPath(path)); +} + +std::string cmCPackInnoSetupGenerator::PrepareForConstant( + const std::string& string) +{ + std::string nString = string; + + cmSystemTools::ReplaceString(nString, "%", "%25"); // First replacement! + cmSystemTools::ReplaceString(nString, "\"", "%22"); + cmSystemTools::ReplaceString(nString, ",", "%2c"); + cmSystemTools::ReplaceString(nString, "|", "%7c"); + cmSystemTools::ReplaceString(nString, "}", "%7d"); + + return nString; +} diff --git a/Source/CPack/cmCPackInnoSetupGenerator.h b/Source/CPack/cmCPackInnoSetupGenerator.h new file mode 100644 index 0000000..342f227 --- /dev/null +++ b/Source/CPack/cmCPackInnoSetupGenerator.h @@ -0,0 +1,116 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying +file Copyright.txt or https://cmake.org/licensing for details. */ + +#pragma once + +#include +#include +#include + +#include "cmCPackGenerator.h" +#include "cmValue.h" + +using cmCPackInnoSetupKeyValuePairs = std::map; + +class cmCPackComponentGroup; +class cmCPackComponent; + +/** \class cmCPackInnoSetupGenerator + * \brief A generator for Inno Setup + * + * https://jrsoftware.org/isinfo.php + */ +class cmCPackInnoSetupGenerator : public cmCPackGenerator +{ +public: + cmCPackTypeMacro(cmCPackInnoSetupGenerator, cmCPackGenerator); + + /** + * Construct generator + */ + cmCPackInnoSetupGenerator(); + ~cmCPackInnoSetupGenerator() override; + + static bool CanGenerate(); + +protected: + int InitializeInternal() override; + int PackageFiles() override; + + inline const char* GetOutputExtension() override { return ".exe"; } + + inline cmCPackGenerator::CPackSetDestdirSupport SupportsSetDestdir() + const override + { + return cmCPackGenerator::SETDESTDIR_UNSUPPORTED; + } + + inline bool SupportsAbsoluteDestination() const override { return false; } + inline bool SupportsComponentInstallation() const override { return true; } + +private: + bool ProcessSetupSection(); + bool ProcessFiles(); + bool ProcessComponents(); + + bool ConfigureISScript(); + bool Compile(); + + bool BuildDownloadedComponentArchive(cmCPackComponent* component, + const std::string& uploadDirectory, + std::string* hash); + + /** + * Returns the option's value or an empty string if the option isn't set. + */ + cmValue RequireOption(const std::string& key); + + std::string CustomComponentInstallDirectory( + const cmCPackComponent* component); + + /** + * Translates boolean expressions into "yes" or "no", as required in + * Inno Setup (only if "CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT" is on). + */ + std::string TranslateBool(const std::string& value); + + /** + * Creates a typical line of key and value pairs using the given map. + * + * (e.g.: Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; + * GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked) + */ + std::string ISKeyValueLine(const cmCPackInnoSetupKeyValuePairs& params); + + std::string CreateRecursiveComponentPath(cmCPackComponentGroup* group, + const std::string& path = ""); + + void CreateRecursiveComponentGroups(cmCPackComponentGroup* group); + + /** + * These functions add quotes if the given value hasn't already quotes. + * Paths are converted into the format used by Windows before. + */ + std::string Quote(const std::string& string); + std::string QuotePath(const std::string& path); + + /** + * This function replaces the following 5 characters with their %-encoding: + * '|' '}' ',' '%' '"' + * Required for Inno Setup constants like {cm:...} + */ + std::string PrepareForConstant(const std::string& string); + + std::vector includeDirectives; + cmCPackInnoSetupKeyValuePairs setupDirectives; + bool toplevelProgramFolder; + std::vector languageInstructions; + std::vector fileInstructions; + std::vector dirInstructions; + std::vector typeInstructions; + std::vector componentInstructions; + std::vector iconInstructions; + std::vector desktopIconComponents; + std::vector runInstructions; + std::vector codeIncludes; +}; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index e92d1c1..e3b5ec4 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -986,6 +986,30 @@ if(BUILD_TESTING) endif() endif() + # On Windows run the CPackInnoSetupGenerator test + if(WIN32 AND CMake_TEST_CPACK_INNOSETUP) + add_test(CPackInnoSetupGenerator ${CMAKE_CTEST_COMMAND} + -C \${CTEST_CONFIGURATION_TYPE} + --build-and-test + "${CMake_SOURCE_DIR}/Tests/CPackInnoSetupGenerator" + "${CMake_BINARY_DIR}/Tests/CPackInnoSetupGenerator" + ${build_generator_args} + --build-project CPackInnoSetupGenerator + --build-options + --test-command ${CMAKE_CMAKE_COMMAND} + "-DCPackInnoSetupGenerator_BINARY_DIR:PATH=${CMake_BINARY_DIR}/Tests/CPackInnoSetupGenerator" + "-Dconfig=\${CTEST_CONFIGURATION_TYPE}" + -P "${CMake_SOURCE_DIR}/Tests/CPackInnoSetupGenerator/RunCPackVerifyResult.cmake") + + set_property(TEST CPackInnoSetupGenerator PROPERTY + ATTACHED_FILES_ON_FAIL + "${CMake_BINARY_DIR}/Tests/CPackInnoSetupGenerator/_CPack_Packages/win32/INNOSETUP/ISCCOutput.log") + + set_property(TEST CPackInnoSetupGenerator PROPERTY + ATTACHED_FILES + "${CMake_BINARY_DIR}/Tests/CPackInnoSetupGenerator/_CPack_Packages/win32/INNOSETUP/ISScript.iss") + endif() + # On Windows run the CPackNSISGenerator test # if the nsis is available if(WIN32 AND NSIS_MAKENSIS_EXECUTABLE) diff --git a/Tests/CPackInnoSetupGenerator/CMakeLists.txt b/Tests/CPackInnoSetupGenerator/CMakeLists.txt new file mode 100644 index 0000000..bca0ad6 --- /dev/null +++ b/Tests/CPackInnoSetupGenerator/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.13) + +project(CPackInnoSetupGenerator VERSION 42.0 HOMEPAGE_URL "https://www.example.com") + +add_executable(hello main.c) +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/empty) + +install(TARGETS hello DESTINATION / COMPONENT application) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/empty DESTINATION / COMPONENT extras) +install(FILES my_bitmap.bmp DESTINATION awesome COMPONENT extras) +install(FILES my_file.txt DESTINATION / COMPONENT hidden_component) +install(FILES my_file.txt DESTINATION / COMPONENT hidden_component2) + +set(CPACK_GENERATOR "INNOSETUP") + +set(CPACK_PACKAGE_NAME "Hello, World!") # Test constant escape (like {cm:...}, see code documentation) +set(CPACK_PACKAGE_VENDOR "Sheldon Cooper") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "hello_world") +set(CPACK_PACKAGE_INSTALL_REGISTRY_KEY "hello_world") +set(CPACK_PACKAGE_FILE_NAME "hello_world_setup") +set(CPACK_SYSTEM_NAME "win32") +set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/my_bitmap.bmp") +set(CPACK_VERBATIM_VARIABLES ON) +set(CPACK_PACKAGE_EXECUTABLES "hello" "Hello, World!") +set(CPACK_CREATE_DESKTOP_LINKS hello) + +set(CPACK_INNOSETUP_INSTALL_ROOT "{autopf}\\Sheldon Cooper") +set(CPACK_INNOSETUP_PROGRAM_MENU_FOLDER ".") +set(CPACK_INNOSETUP_IGNORE_LICENSE_PAGE ON) +set(CPACK_INNOSETUP_IGNORE_README_PAGE OFF) # Test if only readme page is shown +set(CPACK_INNOSETUP_SETUP_AppComments ON) # Test if CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT works +set(CPACK_INNOSETUP_CUSTOM_INSTALL_INSTRUCTIONS "extras/empty" + "Name: \"{userdocs}\\empty\"\; Check: ReturnTrue\; Components: accessories\\extras") +set(CPACK_INNOSETUP_MENU_LINKS "https://www.example.com" "Web" + "my_file.txt" "Text") +set(CPACK_INNOSETUP_RUN_EXECUTABLES hello) +set(CPACK_INNOSETUP_CREATE_UNINSTALL_LINK ON) +# Test if this macro is available in the code file below containing the check function +set(CPACK_INNOSETUP_DEFINE_PascalMacro "end;") +set(CPACK_INNOSETUP_CODE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/Code.pas") +set(CPACK_INNOSETUP_EXECUTABLE "ISCC.exe") + +include(CPackComponent) + +cpack_add_install_type(basic DISPLAY_NAME "Basic installation") +cpack_add_install_type(full DISPLAY_NAME "\"Large\" installation") # Test double quote syntax +cpack_add_component_group(accessories DISPLAY_NAME "Accessories") + +cpack_add_component(application DISPLAY_NAME "Application" INSTALL_TYPES basic full REQUIRED) +cpack_add_component(extras DISPLAY_NAME "Additional components" INSTALL_TYPES full GROUP accessories) +cpack_add_component(hidden_component HIDDEN) +cpack_add_component(hidden_component2 HIDDEN DISABLED) +set(CPACK_INNOSETUP_extras_INSTALL_DIRECTORY "{userdocs}") + +include(CPack) diff --git a/Tests/CPackInnoSetupGenerator/Code.pas b/Tests/CPackInnoSetupGenerator/Code.pas new file mode 100644 index 0000000..d96d82f --- /dev/null +++ b/Tests/CPackInnoSetupGenerator/Code.pas @@ -0,0 +1,4 @@ +function ReturnTrue(): Boolean; +begin + Result := true; +{#PascalMacro} diff --git a/Tests/CPackInnoSetupGenerator/RunCPackVerifyResult.cmake b/Tests/CPackInnoSetupGenerator/RunCPackVerifyResult.cmake new file mode 100644 index 0000000..72a26ee --- /dev/null +++ b/Tests/CPackInnoSetupGenerator/RunCPackVerifyResult.cmake @@ -0,0 +1,136 @@ +message(STATUS "=============================================================") +message(STATUS "CTEST_FULL_OUTPUT (Avoid ctest truncation of output)") +message(STATUS "") + +if(NOT CPackInnoSetupGenerator_BINARY_DIR) + message(FATAL_ERROR "CPackInnoSetupGenerator_BINARY_DIR not set") +endif() + +message(STATUS "CMAKE_COMMAND: ${CMAKE_COMMAND}") +message(STATUS "CMAKE_CPACK_COMMAND: ${CMAKE_CPACK_COMMAND}") +message(STATUS "CPackInnoSetupGenerator_BINARY_DIR: ${CPackInnoSetupGenerator_BINARY_DIR}") + +if(config) + set(_C_config -C ${config}) +endif() + +execute_process(COMMAND "${CMAKE_CPACK_COMMAND}" + ${_C_config} + RESULT_VARIABLE CPack_result + OUTPUT_VARIABLE CPack_output + ERROR_VARIABLE CPack_output + WORKING_DIRECTORY "${CPackInnoSetupGenerator_BINARY_DIR}") + +if(CPack_result) + message(FATAL_ERROR "CPack execution went wrong!, Output: ${CPack_output}") +else () + message(STATUS "Output: ${CPack_output}") +endif() + +file(GLOB project_file "${CPackInnoSetupGenerator_BINARY_DIR}/_CPack_Packages/win32/INNOSETUP/ISScript.iss") +file(GLOB installer_file "${CPackInnoSetupGenerator_BINARY_DIR}/_CPack_Packages/win32/INNOSETUP/hello_world_setup.exe") + +message(STATUS "Project file: '${project_file}'") +message(STATUS "Installer file: '${installer_file}'") + +if(NOT project_file) + message(FATAL_ERROR "Project file does not exist") +endif() + +if(NOT installer_file) + message(FATAL_ERROR "Installer file does not exist") +endif() + +# Test if the correct registry key is set +file(STRINGS "${project_file}" results REGEX "^AppId=hello_world$") +if(results STREQUAL "") + message(FATAL_ERROR "CPACK_PACKAGE_INSTALL_REGISTRY_KEY doesn't match AppId") +endif() + +# Test if only readme page is shown +file(STRINGS "${project_file}" results REGEX "^LicenseFile=") +file(STRINGS "${project_file}" results2 REGEX "^InfoBeforeFile=") +if(NOT results STREQUAL "" OR results2 STREQUAL "") + message(FATAL_ERROR "Erroneous output with license and readme files") +endif() + +# Test if classic style is used by default +file(STRINGS "${project_file}" results REGEX "compiler:SetupClassicIcon\\.ico") +file(STRINGS "${project_file}" results2 REGEX "compiler:WizClassicImage\\.bmp") +if(results STREQUAL "" OR results2 STREQUAL "") + message(FATAL_ERROR "Images of classic style not used") +endif() + +# Test if the top-level start menu folder is used +file(STRINGS "${project_file}" results REGEX "{autoprograms}") +file(STRINGS "${project_file}" results2 REGEX "{group}") +if(results STREQUAL "" OR NOT results2 STREQUAL "") + message(FATAL_ERROR "Top-level start menu folder not used") +endif() + +# Test CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT +file(STRINGS "${project_file}" results REGEX "^AppComments=yes$") +if(results STREQUAL "") + message(FATAL_ERROR "CPACK_INNOSETUP_USE_CMAKE_BOOL_FORMAT doesn't convert booleans") +endif() + +# Test the custom installation rule +file(STRINGS "${project_file}" results REGEX "^Name: \"{userdocs}\\\\empty\"; Check: ReturnTrue; Components: accessories\\\\extras$") +if(results STREQUAL "") + message(FATAL_ERROR "Custom installation rule not found or incomplete") +endif() + +# Test if an uninstall shortcut has been created +file(STRINGS "${project_file}" results REGEX "{uninstallexe}") +if(results STREQUAL "") + message(FATAL_ERROR "No uninstall shortcut created") +endif() + +# Test CPACK_INNOSETUP__INSTALL_DIRECTORY +file(STRINGS "${project_file}" results REGEX "{app}.*Components: accessories\\\\extras") +if(NOT results STREQUAL "") + message(FATAL_ERROR "Component not in custom install directory") +endif() + +# Test if component names are nested correctly +file(STRINGS "${project_file}" results REGEX "Components:.* extras") +if(NOT results STREQUAL "") + message(FATAL_ERROR "Component names must contain their parent groups according to the documentation") +endif() + +# Test if custom installation type exists +file(STRINGS "${project_file}" results REGEX "Flags: .*iscustom") +if(results STREQUAL "") + message(FATAL_ERROR "Custom installation type doesn't exist") +endif() + +# Test if hidden components are processed but not displayed +file(STRINGS "${project_file}" results REGEX "Source:.+hidden_component\\\\my_file\\.txt") +file(STRINGS "${project_file}" results2 REGEX "Name: \"hidden_component\"") +if(results STREQUAL "" OR NOT results2 STREQUAL "") + message(FATAL_ERROR "Hidden component displayed or one of its files ignored") +endif() + +# Test if disabled and hidden components are ignored at all +file(STRINGS "${project_file}" results REGEX "Source:.+hidden_component2\\\\my_file\\.txt") +if(NOT results STREQUAL "") + message(FATAL_ERROR "Disabled and hidden component not ignored") +endif() + +# Test if required components ignore their installation types +file(STRINGS "${project_file}" results REGEX "Types: (basic|full|custom|basic full|full basic|basic custom|full custom); Flags: fixed") +if(NOT results STREQUAL "") + message(FATAL_ERROR "Required components don't ignore their installation types") +endif() + +# Test constant escape (should contain Hello%2c World!) +file(STRINGS "${project_file}" results REGEX "Hello%2c World!") +if(results STREQUAL "") + message(FATAL_ERROR "The comma character isn't escaped to %2c") +endif() + +# Test double quote syntax +file(STRINGS "${project_file}" results REGEX "\"\"Large\"\"") +if(results STREQUAL "") + message(FATAL_ERROR "The quote character isn't escaped correctly") +endif() diff --git a/Tests/CPackInnoSetupGenerator/main.c b/Tests/CPackInnoSetupGenerator/main.c new file mode 100644 index 0000000..413899c --- /dev/null +++ b/Tests/CPackInnoSetupGenerator/main.c @@ -0,0 +1,7 @@ +#include + +int main() +{ + printf("Hello, World!\n"); + return 42; +} diff --git a/Tests/CPackInnoSetupGenerator/my_bitmap.bmp b/Tests/CPackInnoSetupGenerator/my_bitmap.bmp new file mode 100644 index 0000000..d0e562f Binary files /dev/null and b/Tests/CPackInnoSetupGenerator/my_bitmap.bmp differ diff --git a/Tests/CPackInnoSetupGenerator/my_file.txt b/Tests/CPackInnoSetupGenerator/my_file.txt new file mode 100644 index 0000000..8ab686e --- /dev/null +++ b/Tests/CPackInnoSetupGenerator/my_file.txt @@ -0,0 +1 @@ +Hello, World! -- cgit v0.12