installer-exe.rkt (18844B)
1 #lang at-exp racket/base 2 (require racket/format 3 racket/list 4 racket/system 5 racket/path 6 racket/file 7 racket/runtime-path 8 setup/getinfo 9 setup/cross-system) 10 11 (provide installer-exe) 12 13 (define-runtime-path installer-dir "windows-installer") 14 15 (define (get-exe-actions src-dir filename combine) 16 (define f (build-path src-dir "lib" filename)) 17 (for/list ([(k v) (if (file-exists? f) 18 (call-with-input-file* f read) 19 (hash))]) 20 (combine k v))) 21 22 (define (get-extreg src-dir) 23 (apply 24 append 25 (get-exe-actions src-dir "extreg.rktd" 26 (lambda (k v) 27 (for/list ([v (in-list v)]) 28 (append v (list k))))))) 29 30 (define (get-startmenu src-dir) 31 (get-exe-actions src-dir "startmenu.rktd" 32 (lambda (k v) k))) 33 34 (define (get-auto-launch src-dir) 35 (define l 36 (filter (lambda (p) (real? (cdr p))) 37 (get-exe-actions src-dir "startmenu.rktd" 38 cons))) 39 (if (null? l) 40 #f 41 (path-replace-suffix (caar (sort l < #:key cdr)) #""))) 42 43 (define (try-exe f) 44 (and (file-exists? f) f)) 45 46 (define (nsis-generate dest distname version winplatform 47 makensis 48 #:extension-registers [extregs null] 49 #:start-menus [startmenus null] 50 #:versionless [versionless? #t] 51 #:simple? [simple? #f] 52 #:auto-launch [auto-launch #f]) 53 (define distdir (regexp-replace* #rx" " distname "-")) 54 (define destfilename (file-name-from-path dest)) 55 (define-values (version1 version2 version3 version4) 56 (apply 57 values 58 (take (cdr (regexp-match #rx"^([0-9]*)[.]([0-9]*)[.]([0-9]*)[.]([0-9]*)" 59 (string-append version ".0.0.0"))) 60 4))) 61 (define got-files (make-hash)) 62 (define (get-file s) 63 (unless (hash-ref got-files s #f) 64 (define dest (build-path "bundle" s)) 65 (unless (file-exists? dest) 66 (hash-set! got-files s #t) 67 (copy-file (build-path installer-dir s) dest))) 68 s) 69 (define script 70 @~a{ 71 !include "MUI2.nsh" 72 !include "WinVer.nsh" 73 !include "nsDialogs.nsh" 74 75 ;; ==================== Configuration 76 77 !define RKTVersion "@|version|" 78 !define RKTVersionLong "@|version1|.@|version2|.@|version3|.@|version4|" 79 ;; Full name for the package, and a short name for installer texts 80 !define RKTHumanName "@|distname| v@|version| (@|winplatform|)" 81 !define RKTShortName "@|distname|" 82 !define RKTStartName "@|distname|@(if versionless? "" @~a{ v@|version|})" 83 !define RKTDirName "@|distdir|@(if versionless? "" @~a{-@|version|})" 84 !define RKTRegName "@|distdir|-@|winplatform|-@|version|" 85 !define RKTProgFiles "$PROGRAMFILES@(if (equal? winplatform "x86_64") "64" "")" 86 @(if simple? @~a{!define SimpleInstaller} "") 87 @(if auto-launch @~a{!define RKTLaunchProgram "@|auto-launch|"} "") 88 89 Name "${RKTHumanName}" 90 OutFile "@|destfilename|" 91 92 BrandingText "${RKTHumanName}" 93 BGGradient off 94 95 SetCompressor /SOLID "LZMA" 96 97 InstallDir "${RKTProgFiles}\${RKTDirName}" 98 !ifndef SimpleInstaller 99 InstallDirRegKey HKLM "Software\${RKTRegName}" "" 100 !endif 101 !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${RKTStartName}" 102 !define MUI_ICON "@(get-file "installer.ico")" 103 !define MUI_UNICON "@(get-file "uninstaller.ico")" 104 !define MUI_HEADERIMAGE 105 !define MUI_HEADERIMAGE_BITMAP "@(get-file "header.bmp")" 106 !define MUI_HEADERIMAGE_BITMAP_RTL "@(get-file "header-r.bmp")" 107 !define MUI_HEADERIMAGE_RIGHT 108 109 !define MUI_WELCOMEFINISHPAGE_BITMAP "@(get-file "welcome.bmp")" 110 !define MUI_UNWELCOMEFINISHPAGE_BITMAP "@(get-file "welcome.bmp")" 111 112 !define MUI_WELCOMEPAGE_TITLE "${RKTHumanName} Setup" 113 !define MUI_UNWELCOMEPAGE_TITLE "${RKTHumanName} Uninstall" 114 !ifdef SimpleInstaller 115 !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" 116 !else 117 !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" 118 !endif 119 !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" 120 121 !define MUI_FINISHPAGE_TITLE "${RKTHumanName}" 122 !ifdef SimpleInstaller 123 !define MUI_FINISHPAGE_RUN 124 !define MUI_FINISHPAGE_RUN_FUNCTION OpenInstDir 125 Function OpenInstDir 126 ExecShell "" "$INSTDIR" 127 FunctionEnd 128 !define MUI_FINISHPAGE_RUN_TEXT "Open the installation folder" 129 @(if auto-launch 130 @~a{ 131 !else 132 !define MUI_FINISHPAGE_RUN "$INSTDIR\${RKTLaunchProgram}.exe" 133 !define MUI_FINISHPAGE_RUN_TEXT "Run ${RKTLaunchProgram}"} 134 "") 135 !endif 136 !define MUI_FINISHPAGE_LINK "Visit the Racket web site" 137 !define MUI_FINISHPAGE_LINK_LOCATION "https://racket-lang.org/" 138 139 ; !define MUI_UNFINISHPAGE_NOAUTOCLOSE ; to allow users see what was erased 140 141 !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM" 142 !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${RKTRegName}" 143 !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" 144 145 ; Doesn't work on some non-xp machines 146 ; !define MUI_INSTFILESPAGE_PROGRESSBAR colored 147 148 VIProductVersion "${RKTVersionLong}" 149 VIAddVersionKey "ProductName" "Racket" 150 VIAddVersionKey "Comments" "This is the Racket language, see https://racket-lang.org/." 151 VIAddVersionKey "CompanyName" "PLT Design Inc." 152 VIAddVersionKey "LegalCopyright" "© PLT Design Inc." 153 VIAddVersionKey "FileDescription" "Racket Installer" 154 VIAddVersionKey "FileVersion" "${RKTVersion}" 155 156 ;; ==================== Variables 157 158 !ifndef SimpleInstaller 159 Var MUI_TEMP 160 Var STARTMENU_FOLDER 161 !endif 162 163 ;; ==================== Interface 164 165 !define MUI_ABORTWARNING 166 167 ; Install 168 !insertmacro MUI_PAGE_WELCOME 169 !define MUI_PAGE_CUSTOMFUNCTION_LEAVE myTestInstDir 170 !insertmacro MUI_PAGE_DIRECTORY 171 !ifndef SimpleInstaller 172 !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER 173 !endif 174 !insertmacro MUI_PAGE_INSTFILES 175 176 ; Uncheck and hide the "run" checkbox on vista, since it will run with 177 ; elevated permissions (see also ../nsis-vista-note.txt) 178 !define MUI_PAGE_CUSTOMFUNCTION_SHOW DisableRunCheckBoxIfOnVista 179 !insertmacro MUI_PAGE_FINISH 180 Function DisableRunCheckBoxIfOnVista 181 ${If} ${AtLeastWinVista} 182 ; use EnableWindow instead of ShowWindow to just disable it 183 ShowWindow $mui.FinishPage.Run 0 184 ${NSD_Uncheck} $mui.FinishPage.Run 185 ${EndIf} 186 FunctionEnd 187 188 !ifndef SimpleInstaller 189 ; Uninstall 190 !define MUI_WELCOMEPAGE_TITLE "${MUI_UNWELCOMEPAGE_TITLE}" 191 !define MUI_WELCOMEPAGE_TEXT "${MUI_UNWELCOMEPAGE_TEXT}" 192 ; !insertmacro MUI_UNPAGE_WELCOME 193 !insertmacro MUI_UNPAGE_CONFIRM 194 !insertmacro MUI_UNPAGE_INSTFILES 195 ; !insertmacro MUI_UNPAGE_FINISH 196 !endif 197 198 !ifndef SimpleInstaller 199 !define MUI_CUSTOMFUNCTION_UNGUIINIT un.myGUIInit 200 !endif 201 202 !insertmacro MUI_LANGUAGE "English" 203 204 !ifndef SimpleInstaller 205 !define UNINSTEXE "$INSTDIR\Uninstall.exe" 206 !endif 207 208 ;; ==================== Installer 209 210 !ifdef SimpleInstaller 211 Function myTestInstDir 212 IfFileExists "$INSTDIR\*.*" +1 inst_dir_exists 213 MessageBox MB_YESNO "The directory '$INSTDIR' already exists, continue?" /SD IDYES IDYES inst_dir_exists 214 Abort 215 inst_dir_exists: 216 FunctionEnd 217 !else 218 Function myTestInstDir 219 ; The assumption is that users might have all kinds of ways to get a Racket 220 ; tree, plus, they might have an old wise-based installation, so it is better 221 ; to rely on files rather than test registry keys. Note: no version check. 222 ; if any of these exist, then we assume it's an old installation 223 IfFileExists "$INSTDIR\Racket.exe" racket_is_installed 224 @(if auto-launch @~a{IfFileExists "$INSTDIR\${RKTLaunchProgram}.exe" racket_is_installed} "") 225 IfFileExists "$INSTDIR\collects" racket_is_installed 226 Goto racket_is_not_installed 227 racket_is_installed: 228 IfFileExists "${UNINSTEXE}" we_have_uninstall 229 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 230 Abort 231 we_have_uninstall: 232 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 233 HideWindow 234 ClearErrors 235 ExecWait '"${UNINSTEXE}" _?=$INSTDIR' 236 IfErrors uninstaller_problematic 237 IfFileExists "$INSTDIR\Racket.exe" uninstaller_problematic 238 BringToFront 239 Goto racket_is_not_installed 240 uninstaller_problematic: 241 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 242 Quit 243 maybe_remove_tree: 244 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 245 RMDir /r $INSTDIR 246 racket_is_not_installed: 247 FunctionEnd 248 !endif 249 250 Section "" 251 SetShellVarContext all 252 253 SetDetailsPrint both 254 DetailPrint "Installing Racket..." 255 SetDetailsPrint listonly 256 SetOutPath "$INSTDIR" 257 File /a /r "racket\*.*" 258 !ifndef SimpleInstaller 259 WriteUninstaller "${UNINSTEXE}" ; Create uninstaller 260 !endif 261 262 !ifndef SimpleInstaller 263 SetDetailsPrint both 264 DetailPrint "Creating Shortcuts..." 265 SetDetailsPrint listonly 266 !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 267 SetOutPath "$INSTDIR" ; Make installed links run in INSTDIR 268 CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" 269 @apply[~a 270 #:separator "\n" 271 (for/list ([exe-str (in-list startmenus)]) 272 (define exe exe-str) 273 (define lnk (path->string (path-replace-suffix exe-str #".lnk"))) 274 @~a{ CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@|lnk|" "$INSTDIR\@|exe|"})] 275 CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Racket.lnk" "$INSTDIR\Racket.exe" 276 CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Racket Folder.lnk" "$INSTDIR" 277 CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "${UNINSTEXE}" 278 !insertmacro MUI_STARTMENU_WRITE_END 279 280 SetDetailsPrint both 281 DetailPrint "Setting Registry Keys..." 282 SetDetailsPrint listonly 283 WriteRegStr HKLM "Software\${RKTRegName}" "" "$INSTDIR" ; Save folder location 284 @apply[~a 285 #:separator "\n" 286 (apply 287 append 288 (for/list ([extreg (in-list extregs)]) 289 (define kind (list-ref extreg 1)) 290 (define icon (list-ref extreg 3)) 291 (define cmd (list-ref extreg 4)) 292 (define exe-name (list-ref extreg 5)) 293 (append 294 (for/list ([ext (in-list (list-ref extreg 2))]) 295 @~a{ WriteRegStr HKCR ".@|ext|" "" "@|kind|"}) 296 (list 297 @~a{ WriteRegStr HKCR "@|kind|" "" "@(list-ref extreg 0)"} 298 @~a{ WriteRegStr HKCR "@|kind|\DefaultIcon" "" "$INSTDIR\lib\@|icon|"}) 299 (if cmd 300 (list 301 @~a{ WriteRegStr HKCR "@|kind|\shell\open\command" "" '"$INSTDIR\@|exe-name|" @|cmd|'}) 302 null))))] 303 ; Example, in case we want some things like this in the future 304 ; WriteRegStr HKCR "Racket.Document\shell\racket" "" "Run with Racket" 305 ; WriteRegStr HKCR "Racket.Document\shell\racket\command" "" '"$INSTDIR\Racket.exe" "-r" "%1"' 306 307 WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "UninstallString" '"${UNINSTEXE}"' 308 WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "InstallLocation" "$INSTDIR" 309 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayName" "${RKTHumanName}" 310 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayIcon" "$INSTDIR\DrRacket.exe,0" 311 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "DisplayVersion" "${RKTVersion}" 312 ; used to also have "VersionMajor" & "VersionMinor" but looks like it's not needed 313 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "HelpLink" "https://racket-lang.org/" 314 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "URLInfoAbout" "https://racket-lang.org/" 315 WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "Publisher" "PLT Design Inc." 316 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "NoModify" "1" 317 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" "NoRepair" "1" 318 !endif 319 320 SetDetailsPrint both 321 DetailPrint "Installation complete." 322 SectionEnd 323 324 ;; ==================== Uninstaller 325 326 !ifndef SimpleInstaller 327 328 Function un.myGUIInit 329 ; if any of these exist, then we're fine 330 IfFileExists "$INSTDIR\Racket.exe" racket_is_installed_un 331 IfFileExists "$INSTDIR\lib\GRacket.exe" racket_is_installed_un 332 @(if auto-launch @~a{IfFileExists "$INSTDIR\${RKTLaunchProgram}.exe" racket_is_installed_un} "") 333 IfFileExists "$INSTDIR\collects" racket_is_installed_un 334 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 335 Abort "Uninstall aborted by user" 336 racket_is_installed_un: 337 FunctionEnd 338 339 Section "Uninstall" 340 SetShellVarContext all 341 342 SetDetailsPrint both 343 DetailPrint "Removing the Racket installation..." 344 SetDetailsPrint listonly 345 Delete "$INSTDIR\*.exe" 346 Delete "$INSTDIR\README*.*" 347 RMDir /r "$INSTDIR\include" 348 RMDir /r "$INSTDIR\collects" 349 RMDir /r "$INSTDIR\lib" 350 RMDir /r "$INSTDIR\share" 351 RMDir /r "$INSTDIR\etc" 352 RMDir /r "$INSTDIR\doc" 353 ;; these exist in Racket-Full installations 354 RMDir /r "$INSTDIR\man" 355 ; RMDir /r "$INSTDIR\src" 356 Delete "${UNINSTEXE}" 357 RMDir "$INSTDIR" 358 ;; if the directory is opened, it will take some time to remove 359 Sleep 1000 360 IfErrors +1 uninstall_inst_dir_ok 361 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 362 RMDir /r "$INSTDIR" 363 IfErrors +1 uninstall_inst_dir_ok 364 MessageBox MB_OK "Forced deletion did not work either, you will need to clean up '$INSTDIR' manually." /SD IDOK 365 uninstall_inst_dir_ok: 366 367 SetDetailsPrint both 368 DetailPrint "Removing Shortcuts..." 369 SetDetailsPrint listonly 370 !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP 371 Delete "$SMPROGRAMS\$MUI_TEMP\*.lnk" 372 ;; Delete empty start menu parent diretories 373 StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" 374 startMenuDeleteLoop: 375 RMDir $MUI_TEMP 376 GetFullPathName $MUI_TEMP "$MUI_TEMP\.." 377 IfErrors startMenuDeleteLoopDone 378 StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop 379 startMenuDeleteLoopDone: 380 381 SetDetailsPrint both 382 DetailPrint "Removing Registry Keys..." 383 SetDetailsPrint listonly 384 DeleteRegKey /ifempty HKLM "Software\${RKTRegName}\Start Menu Folder" 385 DeleteRegKey /ifempty HKLM "Software\${RKTRegName}" 386 @apply[~a 387 #:separator "\n" 388 (append 389 (for*/list ([extreg (in-list extregs)] 390 [ext (in-list (list-ref extreg 2))]) 391 @~a{ DeleteRegKey HKCR ".@|ext|"}) 392 (for/list ([extreg (in-list extregs)]) 393 @~a{ DeleteRegKey HKCR ".@(list-ref extreg 1)"}))] 394 DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${RKTRegName}" 395 396 SetDetailsPrint both 397 DetailPrint "Uninstallation complete." 398 SectionEnd 399 400 !endif 401 }) 402 (call-with-output-file* 403 "bundle/installer.nsi" 404 #:mode 'text 405 #:exists 'truncate 406 (lambda (o) 407 (display script o) 408 (newline o))) 409 (parameterize ([current-directory "bundle"]) 410 (define verbose (if (eq? 'windows (system-type)) 411 "/V3" 412 "-V3")) 413 (system* makensis verbose "installer.nsi"))) 414 415 (define (installer-exe human-name base-name versionless? dist-suffix readme osslsigncode-args) 416 (define makensis (or (case (system-type) 417 [(windows) 418 (or (find-executable-path "makensis.exe") 419 (try-exe "c:\\Program Files\\NSIS\\makensis.exe") 420 (try-exe "c:\\Program Files (x86)\\NSIS\\makensis.exe"))] 421 [else (find-executable-path "makensis")]) 422 (error 'installer-exe "cannot find \"makensis.exe\""))) 423 (define platform (let-values ([(base name dir?) (split-path (cross-system-library-subpath #f))]) 424 (bytes->string/utf-8 (path-element->bytes name)))) 425 (define exe-path (format "bundle/~a-~a-win32~a.exe" base-name platform dist-suffix)) 426 (when readme 427 (call-with-output-file* 428 #:exists 'truncate 429 (build-path "bundle" "racket" "README.txt") 430 (lambda (o) 431 (display (regexp-replace* #rx"\n" readme "\r\n") o)))) 432 (nsis-generate exe-path 433 human-name 434 (version) 435 platform 436 makensis 437 #:versionless versionless? 438 #:extension-registers (get-extreg "bundle/racket") 439 #:start-menus (get-startmenu "bundle/racket") 440 #:auto-launch (get-auto-launch "bundle/racket")) 441 (when osslsigncode-args 442 (define unsigned-exe-path (let-values ([(base name dir?) (split-path exe-path)]) 443 (build-path base "unsigned" name))) 444 (make-directory* "bundle/unsigned") 445 (rename-file-or-directory exe-path unsigned-exe-path #t) 446 (unless (apply system* 447 (or (find-executable-path (case (system-type) 448 [(windows) "osslsigncode.exe"] 449 [else "osslsigncode"])) 450 (error "cannot find `osslsigncode`")) 451 (append osslsigncode-args 452 (list "-n" human-name 453 "-t" "http://timestamp.verisign.com/scripts/timstamp.dll" 454 "-in" unsigned-exe-path "-out" exe-path))) 455 (error "signing failed"))) 456 exe-path)