#lang at-exp racket/base (require racket/format racket/list racket/system racket/path racket/file racket/runtime-path setup/getinfo setup/cross-system) (provide installer-exe) (define-runtime-path installer-dir "windows-installer") (define (get-exe-actions src-dir filename combine) (define f (build-path src-dir "lib" filename)) (for/list ([(k v) (if (file-exists? f) (call-with-input-file* f read) (hash))]) (combine k v))) (define (get-extreg src-dir) (apply append (get-exe-actions src-dir "extreg.rktd" (lambda (k v) (for/list ([v (in-list v)]) (append v (list k))))))) (define (get-startmenu src-dir) (get-exe-actions src-dir "startmenu.rktd" (lambda (k v) k))) (define (get-auto-launch src-dir) (define l (filter (lambda (p) (real? (cdr p))) (get-exe-actions src-dir "startmenu.rktd" cons))) (if (null? l) #f (path-replace-suffix (caar (sort l < #:key cdr)) #""))) (define (try-exe f) (and (file-exists? f) f)) (define (nsis-generate dest distname version winplatform makensis #:extension-registers [extregs null] #:start-menus [startmenus null] #:versionless [versionless? #t] #:simple? [simple? #f] #:auto-launch [auto-launch #f]) (define distdir (regexp-replace* #rx" " distname "-")) (define destfilename (file-name-from-path dest)) (define-values (version1 version2 version3 version4) (apply values (take (cdr (regexp-match #rx"^([0-9]*)[.]([0-9]*)[.]([0-9]*)[.]([0-9]*)" (string-append version ".0.0.0"))) 4))) (define got-files (make-hash)) (define (get-file s) (unless (hash-ref got-files s #f) (define dest (build-path "bundle" s)) (unless (file-exists? dest) (hash-set! got-files s #t) (copy-file (build-path installer-dir s) dest))) s) (define script @~a{ !include "MUI2.nsh" !include "WinVer.nsh" !include "nsDialogs.nsh" ;; ==================== Configuration !define RKTVersion "@|version|" !define RKTVersionLong "@|version1|.@|version2|.@|version3|.@|version4|" ;; Full name for the package, and a short name for installer texts !define RKTHumanName "@|distname| v@|version| (@|winplatform|)" !define RKTShortName "@|distname|" !define RKTStartName "@|distname|@(if versionless? "" @~a{ v@|version|})" !define RKTDirName "@|distdir|@(if versionless? "" @~a{-@|version|})" !define RKTRegName "@|distdir|-@|winplatform|-@|version|" !define RKTProgFiles "$PROGRAMFILES@(if (equal? winplatform "x86_64") "64" "")" @(if simple? @~a{!define SimpleInstaller} "") @(if auto-launch @~a{!define RKTLaunchProgram "@|auto-launch|"} "") Name "${RKTHumanName}" OutFile "@|destfilename|" BrandingText "${RKTHumanName}" BGGradient off SetCompressor /SOLID "LZMA" InstallDir "${RKTProgFiles}\${RKTDirName}" !ifndef SimpleInstaller InstallDirRegKey HKLM "Software\${RKTRegName}" "" !endif !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${RKTStartName}" !define MUI_ICON "@(get-file "installer.ico")" !define MUI_UNICON "@(get-file "uninstaller.ico")" !define MUI_HEADERIMAGE !define MUI_HEADERIMAGE_BITMAP "@(get-file "header.bmp")" !define MUI_HEADERIMAGE_BITMAP_RTL "@(get-file "header-r.bmp")" !define MUI_HEADERIMAGE_RIGHT !define MUI_WELCOMEFINISHPAGE_BITMAP "@(get-file "welcome.bmp")" !define MUI_UNWELCOMEFINISHPAGE_BITMAP "@(get-file "welcome.bmp")" !define MUI_WELCOMEPAGE_TITLE "${RKTHumanName} Setup" !define MUI_UNWELCOMEPAGE_TITLE "${RKTHumanName} Uninstall" !ifdef SimpleInstaller !define MUI_WELCOMEPAGE_TEXT "This is a simple installer for ${RKTShortName}.$\r$\n$\r$\nIt will only create the @|distname| folder. To uninstall, simply remove the folder.$\r$\n$\r$\n$_CLICK" !else !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of ${RKTShortName}.$\r$\n$\r$\nPlease close any running Racket applications so the installer can update the relevant system files.$\r$\n$\r$\n$_CLICK" !endif !define MUI_UNWELCOMEPAGE_TEXT "This wizard will guide you through the removal of ${RKTShortName}.$\r$\n$\r$\nBefore starting, make sure no Racket applications are running.$\r$\n$\r$\n$_CLICK" !define MUI_FINISHPAGE_TITLE "${RKTHumanName}" !ifdef SimpleInstaller !define MUI_FINISHPAGE_RUN !define MUI_FINISHPAGE_RUN_FUNCTION OpenInstDir Function OpenInstDir ExecShell "" "$INSTDIR" FunctionEnd !define MUI_FINISHPAGE_RUN_TEXT "Open the installation folder" @(if auto-launch @~a{ !else !define MUI_FINISHPAGE_RUN "$INSTDIR\${RKTLaunchProgram}.exe" !define MUI_FINISHPAGE_RUN_TEXT "Run ${RKTLaunchProgram}"} "") !endif !define MUI_FINISHPAGE_LINK "Visit the Racket web site" !define MUI_FINISHPAGE_LINK_LOCATION "https://racket-lang.org/" ; !define MUI_UNFINISHPAGE_NOAUTOCLOSE ; to allow users see what was erased !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM" !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${RKTRegName}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" ; Doesn't work on some non-xp machines ; !define MUI_INSTFILESPAGE_PROGRESSBAR colored VIProductVersion "${RKTVersionLong}" VIAddVersionKey "ProductName" "Racket" VIAddVersionKey "Comments" "This is the Racket language, see https://racket-lang.org/." VIAddVersionKey "CompanyName" "PLT Design Inc." VIAddVersionKey "LegalCopyright" "© PLT Design Inc." VIAddVersionKey "FileDescription" "Racket Installer" VIAddVersionKey "FileVersion" "${RKTVersion}" ;; ==================== Variables !ifndef SimpleInstaller Var MUI_TEMP Var STARTMENU_FOLDER !endif ;; ==================== Interface !define MUI_ABORTWARNING ; Install !insertmacro MUI_PAGE_WELCOME !define MUI_PAGE_CUSTOMFUNCTION_LEAVE myTestInstDir !insertmacro MUI_PAGE_DIRECTORY !ifndef SimpleInstaller !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER !endif !insertmacro MUI_PAGE_INSTFILES ; Uncheck and hide the "run" checkbox on vista, since it will run with ; elevated permissions (see also ../nsis-vista-note.txt) !define MUI_PAGE_CUSTOMFUNCTION_SHOW DisableRunCheckBoxIfOnVista !insertmacro MUI_PAGE_FINISH Function DisableRunCheckBoxIfOnVista ${If} ${AtLeastWinVista} ; use EnableWindow instead of ShowWindow to just disable it ShowWindow $mui.FinishPage.Run 0 ${NSD_Uncheck} $mui.FinishPage.Run ${EndIf} FunctionEnd !ifndef SimpleInstaller ; Uninstall !define MUI_WELCOMEPAGE_TITLE "${MUI_UNWELCOMEPAGE_TITLE}" !define MUI_WELCOMEPAGE_TEXT "${MUI_UNWELCOMEPAGE_TEXT}" ; !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES ; !insertmacro MUI_UNPAGE_FINISH !endif !ifndef SimpleInstaller !define MUI_CUSTOMFUNCTION_UNGUIINIT un.myGUIInit !endif !insertmacro MUI_LANGUAGE "English" !ifndef SimpleInstaller !define UNINSTEXE "$INSTDIR\Uninstall.exe" !endif ;; ==================== Installer !ifdef SimpleInstaller Function myTestInstDir IfFileExists "$INSTDIR\*.*" +1 inst_dir_exists MessageBox MB_YESNO "The directory '$INSTDIR' already exists, continue?" /SD IDYES IDYES inst_dir_exists Abort inst_dir_exists: FunctionEnd !else Function myTestInstDir ; The assumption is that users might have all kinds of ways to get a Racket ; tree, plus, they might have an old wise-based installation, so it is better ; to rely on files rather than test registry keys. Note: no version check. ; if any of these exist, then we assume it's an old installation IfFileExists "$INSTDIR\Racket.exe" racket_is_installed @(if auto-launch @~a{IfFileExists "$INSTDIR\${RKTLaunchProgram}.exe" racket_is_installed} "") IfFileExists "$INSTDIR\collects" racket_is_installed Goto racket_is_not_installed racket_is_installed: IfFileExists "${UNINSTEXE}" we_have_uninstall MessageBox MB_YESNO "It appears that there is an existing Racket installation in '$INSTDIR', but no Uninstaller was found.$\r$\nContinue anyway (not recommended)?" /SD IDYES IDYES maybe_remove_tree Abort we_have_uninstall: MessageBox MB_YESNO "It appears that there is an existing Racket installation in '$INSTDIR'.$\r$\nDo you want to uninstall it first (recommended)?" /SD IDNO IDNO maybe_remove_tree HideWindow ClearErrors ExecWait '"${UNINSTEXE}" _?=$INSTDIR' IfErrors uninstaller_problematic IfFileExists "$INSTDIR\Racket.exe" uninstaller_problematic BringToFront Goto racket_is_not_installed uninstaller_problematic: MessageBox MB_YESNO "Errors in uninstallation!$\r$\nDo you want to quit and sort things out now (highly recommended)?" /SD IDNO IDNO maybe_remove_tree Quit maybe_remove_tree: MessageBox MB_YESNO "Since you insist, do you want to simply remove the previous directory now?$\r$\n(It is really better if you sort this out manually.)" /SD IDYES IDNO racket_is_not_installed RMDir /r $INSTDIR racket_is_not_installed: FunctionEnd !endif Section "" SetShellVarContext all SetDetailsPrint both DetailPrint "Installing Racket..." SetDetailsPrint listonly SetOutPath "$INSTDIR" File /a /r "racket\*.*" !ifndef SimpleInstaller WriteUninstaller "${UNINSTEXE}" ; Create uninstaller !endif !ifndef SimpleInstaller SetDetailsPrint both DetailPrint "Creating Shortcuts..." SetDetailsPrint listonly !insertmacro MUI_STARTMENU_WRITE_BEGIN Application SetOutPath "$INSTDIR" ; Make installed links run in INSTDIR CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" @apply[~a #:separator "\n" (for/list ([exe-str (in-list startmenus)]) (define exe exe-str) (define lnk (path->string (path-replace-suffix exe-str #".lnk"))) @~a{ CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@|lnk|" "$INSTDIR\@|exe|"})] CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Racket.lnk" "$INSTDIR\Racket.exe" CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Racket Folder.lnk" "$INSTDIR" CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "${UNINSTEXE}" !insertmacro MUI_STARTMENU_WRITE_END SetDetailsPrint both DetailPrint "Setting Registry Keys..." SetDetailsPrint listonly WriteRegStr HKLM "Software\${RKTRegName}" "" "$INSTDIR" ; Save folder location @apply[~a #:separator "\n" (apply append (for/list ([extreg (in-list extregs)]) (define kind (list-ref extreg 1)) (define icon (list-ref extreg 3)) (define cmd (list-ref extreg 4)) (define exe-name (list-ref extreg 5)) (append (for/list ([ext (in-list (list-ref extreg 2))]) @~a{ WriteRegStr HKCR ".@|ext|" "" "@|kind|"}) (list @~a{ WriteRegStr HKCR "@|kind|" "" "@(list-ref extreg 0)"} @~a{ WriteRegStr HKCR "@|kind|\DefaultIcon" "" "$INSTDIR\lib\@|icon|"}) (if cmd (list @~a{ WriteRegStr HKCR "@|kind|\shell\open\command" "" '"$INSTDIR\@|exe-name|" @|cmd|'}) null))))] ; Example, in case we want some things like this in the future ; WriteRegStr HKCR "Racket.Document\shell\racket" "" "Run with Racket" ; WriteRegStr HKCR "Racket.Document\shell\racket\command" "" '"$INSTDIR\Racket.exe" "-r" "%1"' WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "UninstallString" '"${UNINSTEXE}"' WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "InstallLocation" "$INSTDIR" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayName" "${RKTHumanName}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayIcon" "$INSTDIR\DrRacket.exe,0" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayVersion" "${RKTVersion}" ; used to also have "VersionMajor" & "VersionMinor" but looks like it's not needed WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "HelpLink" "https://racket-lang.org/" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "URLInfoAbout" "https://racket-lang.org/" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "Publisher" "PLT Design Inc." WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "NoModify" "1" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "NoRepair" "1" !endif SetDetailsPrint both DetailPrint "Installation complete." SectionEnd ;; ==================== Uninstaller !ifndef SimpleInstaller Function un.myGUIInit ; if any of these exist, then we're fine IfFileExists "$INSTDIR\Racket.exe" racket_is_installed_un IfFileExists "$INSTDIR\lib\GRacket.exe" racket_is_installed_un @(if auto-launch @~a{IfFileExists "$INSTDIR\${RKTLaunchProgram}.exe" racket_is_installed_un} "") IfFileExists "$INSTDIR\collects" racket_is_installed_un MessageBox MB_YESNO "It does not appear that Racket is installed in '$INSTDIR'.$\r$\nContinue anyway (not recommended)?" /SD IDYES IDYES racket_is_installed_un Abort "Uninstall aborted by user" racket_is_installed_un: FunctionEnd Section "Uninstall" SetShellVarContext all SetDetailsPrint both DetailPrint "Removing the Racket installation..." SetDetailsPrint listonly Delete "$INSTDIR\*.exe" Delete "$INSTDIR\README*.*" RMDir /r "$INSTDIR\include" RMDir /r "$INSTDIR\collects" RMDir /r "$INSTDIR\lib" RMDir /r "$INSTDIR\share" RMDir /r "$INSTDIR\etc" RMDir /r "$INSTDIR\doc" ;; these exist in Racket-Full installations RMDir /r "$INSTDIR\man" ; RMDir /r "$INSTDIR\src" Delete "${UNINSTEXE}" RMDir "$INSTDIR" ;; if the directory is opened, it will take some time to remove Sleep 1000 IfErrors +1 uninstall_inst_dir_ok MessageBox MB_YESNO "The Racket installation at '$INSTDIR' was not completely removed.$\r$\nForce deletion?$\r$\n(Make sure no Racket applications are running.)" /SD IDYES IDNO uninstall_inst_dir_ok RMDir /r "$INSTDIR" IfErrors +1 uninstall_inst_dir_ok MessageBox MB_OK "Forced deletion did not work either, you will need to clean up '$INSTDIR' manually." /SD IDOK uninstall_inst_dir_ok: SetDetailsPrint both DetailPrint "Removing Shortcuts..." SetDetailsPrint listonly !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP Delete "$SMPROGRAMS\$MUI_TEMP\*.lnk" ;; Delete empty start menu parent diretories StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" startMenuDeleteLoop: RMDir $MUI_TEMP GetFullPathName $MUI_TEMP "$MUI_TEMP\.." IfErrors startMenuDeleteLoopDone StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop startMenuDeleteLoopDone: SetDetailsPrint both DetailPrint "Removing Registry Keys..." SetDetailsPrint listonly DeleteRegKey /ifempty HKLM "Software\${RKTRegName}\Start Menu Folder" DeleteRegKey /ifempty HKLM "Software\${RKTRegName}" @apply[~a #:separator "\n" (append (for*/list ([extreg (in-list extregs)] [ext (in-list (list-ref extreg 2))]) @~a{ DeleteRegKey HKCR ".@|ext|"}) (for/list ([extreg (in-list extregs)]) @~a{ DeleteRegKey HKCR ".@(list-ref extreg 1)"}))] DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" SetDetailsPrint both DetailPrint "Uninstallation complete." SectionEnd !endif }) (call-with-output-file* "bundle/installer.nsi" #:mode 'text #:exists 'truncate (lambda (o) (display script o) (newline o))) (parameterize ([current-directory "bundle"]) (define verbose (if (eq? 'windows (system-type)) "/V3" "-V3")) (system* makensis verbose "installer.nsi"))) (define (installer-exe human-name base-name versionless? dist-suffix readme osslsigncode-args) (define makensis (or (case (system-type) [(windows) (or (find-executable-path "makensis.exe") (try-exe "c:\\Program Files\\NSIS\\makensis.exe") (try-exe "c:\\Program Files (x86)\\NSIS\\makensis.exe"))] [else (find-executable-path "makensis")]) (error 'installer-exe "cannot find \"makensis.exe\""))) (define platform (let-values ([(base name dir?) (split-path (cross-system-library-subpath #f))]) (bytes->string/utf-8 (path-element->bytes name)))) (define exe-path (format "bundle/~a-~a-win32~a.exe" base-name platform dist-suffix)) (when readme (call-with-output-file* #:exists 'truncate (build-path "bundle" "racket" "README.txt") (lambda (o) (display (regexp-replace* #rx"\n" readme "\r\n") o)))) (nsis-generate exe-path human-name (version) platform makensis #:versionless versionless? #:extension-registers (get-extreg "bundle/racket") #:start-menus (get-startmenu "bundle/racket") #:auto-launch (get-auto-launch "bundle/racket")) (when osslsigncode-args (define unsigned-exe-path (let-values ([(base name dir?) (split-path exe-path)]) (build-path base "unsigned" name))) (make-directory* "bundle/unsigned") (rename-file-or-directory exe-path unsigned-exe-path #t) (unless (apply system* (or (find-executable-path (case (system-type) [(windows) "osslsigncode.exe"] [else "osslsigncode"])) (error "cannot find `osslsigncode`")) (append osslsigncode-args (list "-n" human-name "-t" "http://timestamp.verisign.com/scripts/timstamp.dll" "-in" unsigned-exe-path "-out" exe-path))) (error "signing failed"))) exe-path)