diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja new file mode 100644 index 0000000000..465575e220 --- /dev/null +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -0,0 +1,194 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura's build system is released under the terms of the AGPLv3 or higher. + +!define APP_NAME "{{ app_name }} {{ version_major }}.{{ version_minor }}.{{ version_patch }}" +!define COMP_NAME "{{ company }}" +!define WEB_SITE "{{ web_site }}" +!define VERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.{{ version_build }}" +!define COPYRIGHT "Copyright (c) {{ year }} {{ company }}" +!define DESCRIPTION "Application" +!define LICENSE_TXT "{{ cura_license_file }}" +!define INSTALLER_NAME "{{ destination }}" +!define MAIN_APP_EXE "{{ main_app }}" +!define INSTALL_TYPE "SetShellVarContext current" +!define REG_ROOT "HKCU" +!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}" +!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" + +!define REG_START_MENU "Start Menu Folder" + +;Require administrator access +RequestExecutionLevel admin + +var SM_Folder + +###################################################################### + +VIProductVersion "${VERSION}" +VIAddVersionKey "ProductName" "${APP_NAME}" +VIAddVersionKey "CompanyName" "${COMP_NAME}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileDescription" "${DESCRIPTION}" +VIAddVersionKey "FileVersion" "${VERSION}" + +###################################################################### + +SetCompressor {{ compression_method }} +Name "${APP_NAME}" +Caption "${APP_NAME}" +OutFile "${INSTALLER_NAME}" +BrandingText "${APP_NAME}" +InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" "" +InstallDir "$PROGRAMFILES64\${APP_NAME}" + +###################################################################### + +!include "MUI2.nsh" +!include fileassoc.nsh + +!define MUI_ABORTWARNING +!define MUI_UNABORTWARNING + +!define MUI_ICON "{{ cura_icon }}" + +!define MUI_WELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}" + +!insertmacro MUI_PAGE_WELCOME + +!ifdef LICENSE_TXT +!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}" +!endif + +!insertmacro MUI_PAGE_DIRECTORY + +!ifdef REG_START_MENU +!define MUI_STARTMENUPAGE_NODISABLE +!define MUI_STARTMENUPAGE_DEFAULTFOLDER "{{ app_name }}" +!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" +!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" +!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" +!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder +!endif + +!insertmacro MUI_PAGE_INSTFILES + +# Set up explorer to run Cura instead of directly, so it's not executed elevated (with all negative consequences that brings for an unelevated user). +!define MUI_FINISHPAGE_RUN "$WINDIR\explorer.exe" +!define MUI_FINISHPAGE_RUN_PARAMETERS "$INSTDIR\${MAIN_APP_EXE}" +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM + +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_UNPAGE_FINISH + +!insertmacro MUI_LANGUAGE "English" + +###################################################################### + +Section -MainProgram +${INSTALL_TYPE} +SetOverwrite ifnewer +{% for out_path, files in mapped_out_paths.items() %}SetOutPath "{{ out_path }}"{% for file in files %} +File "{{ file[0] }}"{% endfor %} +{% endfor %}SectionEnd + +###################################################################### + +Section -Extension_Reg +!insertmacro APP_ASSOCIATE "stl" "Cura.model" "Standard Tessellation Language (STL) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\"" +!insertmacro APP_ASSOCIATE "3mf" "Cura.project" "3D Manufacturing Format (3MF) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\"" +SectionEnd + +Section -Icons_Reg +SetOutPath "$INSTDIR" +WriteUninstaller "$INSTDIR\uninstall.exe" + +!ifdef REG_START_MENU +!insertmacro MUI_STARTMENU_WRITE_BEGIN Application +CreateDirectory "$SMPROGRAMS\$SM_Folder" +CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" +CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" + +!ifdef WEB_SITE +WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}" +CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url" +!endif +!insertmacro MUI_STARTMENU_WRITE_END +!endif + +!ifndef REG_START_MENU +CreateDirectory "$SMPROGRAMS\{{ app_name }}" +CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" +CreateShortCut "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" + +!ifdef WEB_SITE +WriteIniStr "$INSTDIR\${APP_NAME} website.url" "InternetShortcut" "URL" "${WEB_SITE}" +CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME} Website.lnk" "$INSTDIR\${APP_NAME} website.url" +!endif +!endif + +WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}" +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe" +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}" +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" + +!ifdef WEB_SITE +WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}" +!endif +SectionEnd + +###################################################################### + +Section Uninstall +${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %} +Delete "{{ file[1] }}"{% endfor %}{% endfor %}{% for rem_dir in rmdir_paths %} +RmDir "{{ rem_dir }}"{% endfor %} + +# FIXME: dirty solution, but for some reason these directories aren't removed +RmDir "$INSTDIR\share\cura\resources\scripts" +RmDir "$INSTDIR\share\cura\resources" +RmDir "$INSTDIR\share\cura" +RmDir "$INSTDIR\share\uranium\resources\scripts" +RmDir "$INSTDIR\share\uranium\resources" +RmDir "$INSTDIR\share\uranium" +RmDir "$INSTDIR\share" + +Delete "$INSTDIR\uninstall.exe" +!ifdef WEB_SITE +Delete "$INSTDIR\${APP_NAME} website.url" +!endif + +RmDir "$INSTDIR" + +!ifdef REG_START_MENU +!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder +Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" +!ifdef WEB_SITE +Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME} Website.lnk" +!endif +RmDir "$SMPROGRAMS\$SM_Folder" +!endif + +!ifndef REG_START_MENU +Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" +!ifdef WEB_SITE +Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME} Website.lnk" +!endif +RmDir "$SMPROGRAMS\{{ app_name }}" +!endif + +!insertmacro APP_UNASSOCIATE "stl" "Cura.model" +!insertmacro APP_UNASSOCIATE "3mf" "Cura.project" + +DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" +DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" +SectionEnd + +###################################################################### diff --git a/packaging/NSIS/fileassoc.nsh b/packaging/NSIS/fileassoc.nsh new file mode 100644 index 0000000000..cb0fb2fe65 --- /dev/null +++ b/packaging/NSIS/fileassoc.nsh @@ -0,0 +1,134 @@ +; fileassoc.nsh +; File association helper macros +; Written by Saivert +; +; Improved by Nikku. +; +; Features automatic backup system and UPDATEFILEASSOC macro for +; shell change notification. +; +; |> How to use <| +; To associate a file with an application so you can double-click it in explorer, use +; the APP_ASSOCIATE macro like this: +; +; Example: +; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \ +; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\"" +; +; Never insert the APP_ASSOCIATE macro multiple times, it is only ment +; to associate an application with a single file and using the +; the "open" verb as default. To add more verbs (actions) to a file +; use the APP_ASSOCIATE_ADDVERB macro. +; +; Example: +; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \ +; "$INSTDIR\myapp.exe /edit $\"%1$\"" +; +; To have access to more options when registering the file association use the +; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the +; standard action (default verb). +; +; Note, that this script takes into account user versus global installs. +; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext. +; +; And finally: To remove the association from the registry use the APP_UNASSOCIATE +; macro. Here is another example just to wrap it up: +; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile" +; +; |> Note <| +; When defining your file class string always use the short form of your application title +; then a period (dot) and the type of file. This keeps the file class sort of unique. +; Examples: +; Winamp.Playlist +; NSIS.Script +; Photoshop.JPEGFile +; +; |> Tech info <| +; The registry key layout for a global file association is: +; +; HKEY_LOCAL_MACHINE\Software\Classes +; <".ext"> = +; = <"description"> +; shell +; = <"menu-item text"> +; command = <"command string"> +; +; +; The registry key layout for a per-user file association is: +; +; HKEY_CURRENT_USER\Software\Classes +; <".ext"> = +; = <"description"> +; shell +; = <"menu-item text"> +; command = <"command string"> +; + +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + StrCmp "${SHELLNEW}" "0" +2 + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" "" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}` +!macroend + + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT + ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" "" +!macroend + + +; !defines for use with SHChangeNotify +!ifdef SHCNE_ASSOCCHANGED +!undef SHCNE_ASSOCCHANGED +!endif +!define SHCNE_ASSOCCHANGED 0x08000000 +!ifdef SHCNF_FLUSH +!undef SHCNF_FLUSH +!endif +!define SHCNF_FLUSH 0x1000 + +!macro UPDATEFILEASSOC +; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we +; can update the shell. + System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" +!macroend \ No newline at end of file diff --git a/packaging/NSIS/nsis-configurator.py b/packaging/NSIS/nsis-configurator.py new file mode 100644 index 0000000000..c8986fb118 --- /dev/null +++ b/packaging/NSIS/nsis-configurator.py @@ -0,0 +1,78 @@ +import shutil +import sys +from datetime import datetime + +from pathlib import Path + +from jinja2 import Template + +if __name__ == "__main__": + """ + - dist_loc: Location of distribution folder, as output by pyinstaller + - nsi_jinja_loc: Jinja2 template to use + - app_name: Should be "Ultimaker Cura". + - main_app: Name of executable, e.g. Ultimaker-Cura.exe? + - version_major: Major version number of Semver (e.g. 5). + - version_minor: Minor version number of Semver (e.g. 0). + - version_patch: Patch version number of Semver (e.g. 0). + - version_build: A version number that gets manually incremented at each build. + - company: Publisher of the application. Should be "Ultimaker B.V." + - web_site: Website to find more information. Should be "https://ultimaker.com". + - cura_license_file: Path to a license file in Cura. Should point to packaging/cura_license.txt in this repository. + - compression_method: Compression algorithm to use to compress the data inside the executable. Should be ZLIB, ZBIP2 or LZMA. + - cura_banner_img: Path to an image shown on the left in the installer. Should point to packaging/cura_banner_nsis.bmp in this repository. + - icon_path: Path to the icon to use on the installer + - destination: Where to put the installer after it's generated. +` """ + for i, v in enumerate(sys.argv): + print(f"{i} = {v}") + dist_loc = Path(sys.argv[1]) + instdir = Path("$INSTDIR") + dist_paths = [p.relative_to(dist_loc) for p in sorted(dist_loc.rglob("*")) if p.is_file()] + mapped_out_paths = {} + for dist_path in dist_paths: + if "__pycache__" not in dist_path.parts: + out_path = instdir.joinpath(dist_path).parent + if out_path not in mapped_out_paths: + mapped_out_paths[out_path] = [(dist_loc.joinpath(dist_path), instdir.joinpath(dist_path))] + else: + mapped_out_paths[out_path].append((dist_loc.joinpath(dist_path), instdir.joinpath(dist_path))) + + rmdir_paths = set() + for rmdir_f in mapped_out_paths.values(): + for _, rmdir_p in rmdir_f: + for rmdir in rmdir_p.parents: + rmdir_paths.add(rmdir) + + rmdir_paths = sorted(list(rmdir_paths), reverse = True)[:-2] + + jinja_template_path = Path(sys.argv[2]) + with open(jinja_template_path, "r") as f: + template = Template(f.read()) + + nsis_content = template.render( + app_name = sys.argv[3], + main_app = sys.argv[4], + version_major = sys.argv[5], + version_minor = sys.argv[6], + version_patch = sys.argv[7], + version_build = sys.argv[8], + company = sys.argv[9], + web_site = sys.argv[10], + year = datetime.now().year, + cura_license_file = Path(sys.argv[11]), + compression_method = sys.argv[12], # ZLIB, BZIP2 or LZMA + cura_banner_img = Path(sys.argv[13]), + cura_icon = Path(sys.argv[14]), + mapped_out_paths = mapped_out_paths, + rmdir_paths = rmdir_paths, + destination = Path(sys.argv[15]) + ) + + with open(dist_loc.parent.joinpath(jinja_template_path.stem), "w") as f: + f.write(nsis_content) + + shutil.copy(Path(__file__).absolute().parent.joinpath("fileassoc.nsh"), dist_loc.parent.joinpath("fileassoc.nsh")) + icon_path = Path(sys.argv[14]) + shutil.copy(icon_path, dist_loc.joinpath(icon_path.name)) +