├── graphics ├── nodejs.bmp ├── nrhex24.bmp ├── node-red-icon-small.bmp └── sidebar │ ├── Node RED Side Graphic - BMP.bmp │ ├── Node RED Side Graphic - PNG.png │ └── Node RED Side Graphic.afphoto ├── documentation └── preview.png ├── icons └── node-red-icons.ico ├── README.md ├── setup.ini ├── iss ├── image_button.iss ├── reddetector.iss └── redactionpage.iss ├── .github └── workflows │ └── installer.yml ├── LICENSE ├── bat └── setup_loop.bat ├── .gitignore └── nodered.iss /graphics/nodejs.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/nodejs.bmp -------------------------------------------------------------------------------- /graphics/nrhex24.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/nrhex24.bmp -------------------------------------------------------------------------------- /documentation/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/documentation/preview.png -------------------------------------------------------------------------------- /icons/node-red-icons.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/icons/node-red-icons.ico -------------------------------------------------------------------------------- /graphics/node-red-icon-small.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/node-red-icon-small.bmp -------------------------------------------------------------------------------- /graphics/sidebar/Node RED Side Graphic - BMP.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/sidebar/Node RED Side Graphic - BMP.bmp -------------------------------------------------------------------------------- /graphics/sidebar/Node RED Side Graphic - PNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/sidebar/Node RED Side Graphic - PNG.png -------------------------------------------------------------------------------- /graphics/sidebar/Node RED Side Graphic.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-red/windows-installer/main/graphics/sidebar/Node RED Side Graphic.afphoto -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows Installer for Node-RED 2 | 3 | The Node-RED installer for Windows 4 | 5 | Current state: **Beta** stage -> Feature complete, operational. 6 | 7 | 8 | 9 | Side Graphics by [@marcus-j-davies](https://github.com/marcus-j-davies) 10 | -------------------------------------------------------------------------------- /setup.ini: -------------------------------------------------------------------------------- 1 | [installer] 2 | title=Node-RED 3 | description=Node-RED Installer for Windows 4 | version=0.9.5 5 | copyright=2023 Ralph Wetzel 6 | url=https://github.com/node-red/windows-installer 7 | 8 | [node] 9 | index=https://nodejs.org/download/release/index.json 10 | recommended=20 11 | license=https://raw.githubusercontent.com/nodejs/node/main/LICENSE 12 | download=https://nodejs.org/dist 13 | 14 | ; this is for fallback only! maintain on convenience! 15 | versions_fallback=18,20,22 16 | 17 | [red] 18 | license=https://raw.githubusercontent.com/node-red/node-red/master/LICENSE 19 | versions=3.0.1 20 | min=3 21 | provision=10 22 | 23 | [python] 24 | version=3.10.11 25 | win32=83a67e1c4f6f1472bf75dd9681491bf1 26 | amd64=a55e9c1e6421c84a4bd8b4be41492f51 27 | 28 | [vs] 29 | where=https://github.com/microsoft/vswhere/releases/download/3.1.1/vswhere.exe 30 | download=https://aka.ms/vs/17/release/vs_BuildTools.exe 31 | min=15 -------------------------------------------------------------------------------- /iss/image_button.iss: -------------------------------------------------------------------------------- 1 | [Code] 2 | 3 | // This is an adaptation of 4 | // https://stackoverflow.com/questions/35129746/how-to-create-an-image-button-in-inno-setup 5 | 6 | function ImageList_Add(ImageList: THandle; Image, Mask: THandle): Integer; 7 | external 'ImageList_Add@Comctl32.dll stdcall'; 8 | function ImageList_Create(CX, CY: Integer; Flags: UINT; 9 | Initial, Grow: Integer): THandle; 10 | external 'ImageList_Create@Comctl32.dll stdcall'; 11 | 12 | const 13 | IMAGE_BITMAP = 0; 14 | LR_LOADFROMFILE = $10; 15 | ILC_COLOR32 = $20; 16 | BCM_SETIMAGELIST = $1600 + $0002; 17 | BUTTON_IMAGELIST_ALIGN_CENTER = $04; 18 | 19 | type 20 | BUTTON_IMAGELIST = record 21 | himl: THandle; 22 | margin: TRect; 23 | uAlign: UINT; 24 | end; 25 | 26 | function SendSetImageListMessage( 27 | Wnd: THandle; Msg: Cardinal; WParam: Cardinal; 28 | var LParam: BUTTON_IMAGELIST): Cardinal; 29 | external 'SendMessageW@User32.dll stdcall'; 30 | -------------------------------------------------------------------------------- /.github/workflows/installer.yml: -------------------------------------------------------------------------------- 1 | name: Create Installer for Node-RED 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - name: Run Inno Compiler 18 | run: iscc "nodered.iss" 19 | 20 | - name: CodeSign the installer 21 | uses: dlemstra/code-sign-action@v1 22 | if: ${{ vars.SIGN_INSTALLER == 'TRUE' }} 23 | with: 24 | certificate: '${{ secrets.CODE_SIGNING_CERTIFICATE }}' 25 | password: '${{ secrets.CODE_SIGNING_CERTIFICATE_PASSWORD }}' 26 | folder: 'Output' 27 | recursive: false 28 | files: | 29 | Node-RED.Installer.exe 30 | 31 | - name: Upload files to a GitHub release 32 | uses: svenstaro/upload-release-action@2.5.0 33 | with: 34 | repo_token: ${{ secrets.GITHUB_TOKEN }} 35 | file: Output\Node-RED Installer.exe 36 | asset_name: "Node-Red Installer.exe" 37 | tag: ${{ github.ref }} 38 | overwrite: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ralph Wetzel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bat/setup_loop.bat: -------------------------------------------------------------------------------- 1 | :: Excellent Syntax Reference: 2 | :: https://ss64.com 3 | 4 | @echo off 5 | setlocal EnableDelayedExpansion 6 | 7 | :: %1: Filter criteria for the tasklist command 8 | IF %1.==. EXIT 9 | :: %2: tag to look for in the output of tasklist command 10 | IF %2.==. EXIT 11 | 12 | echo|set /p="Waiting for the Visual Studio Installer to finish its job." 13 | :: give the installer time to settle... 14 | TIMEOUT 2 > nul 15 | 16 | :check_for_installer_running 17 | 18 | SET _continue=false 19 | 20 | FOR /F "tokens=*" %%G IN ('tasklist /FI %1 /FO Table /NH') DO ( 21 | CALL :check_loop "%%G" %2 22 | if errorlevel 1 SET _continue=true 23 | ) 24 | 25 | IF !_continue!==true ( 26 | echo|set /p="." 27 | TIMEOUT 3 > nul 28 | GOTO :check_for_installer_running 29 | ) 30 | GOTO :EOF 31 | 32 | :check_loop 33 | setlocal 34 | SET _line=%1 35 | SET _check=%2 36 | 37 | :: Try to replace %2 in the incoming string (%1) 38 | :: Set an errorlevel in case the result is different == the substring search was successful! 39 | CALL SET _result=%%_line:%_check%=%% 40 | If /i %_result% neq %_line% ( EXIT /b 1) 41 | GOTO :EOF -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | IDP_1.5.1/.DS_Store 107 | .DS_Store 108 | 109 | Output/ 110 | archive/ -------------------------------------------------------------------------------- /iss/reddetector.iss: -------------------------------------------------------------------------------- 1 | [Code] 2 | 3 | procedure SetText(var page: TOutputMarqueeProgressWizardPage; text: string); 4 | begin 5 | page.SetText(text, ''); 6 | end; 7 | 8 | function red_list(working_dir: string): string; 9 | var 10 | res: array of string; 11 | rv: string; 12 | splitres: TStringList; 13 | 14 | begin 15 | 16 | Result := ''; 17 | if not DirExists(working_dir) then Exit; 18 | 19 | SetArrayLength(res, 0); 20 | if RunCMD('npm list node-red', working_dir, res) then begin 21 | if GetArrayLength(res) > 1 then begin 22 | rv := res[1]; 23 | // -- node-red@3.0.2 extraneous 24 | splitres := TStringList.Create 25 | if SplitString(rv, ' ', splitres) then begin 26 | if splitres.Count > 1 then begin 27 | rv := splitres[1]; 28 | // node-red@3.0.2 29 | if StringChangeEx(rv, 'node-red@', '', True) = 1 then 30 | Result := rv; 31 | end; 32 | end; 33 | end; 34 | end; 35 | 36 | end; 37 | 38 | 39 | procedure query_registry(key: string; index: integer); 40 | var 41 | data: string; 42 | 43 | begin 44 | 45 | // Read additional data from registry 46 | RegQueryStringValue(main.HKLM, key, 'Name', main.red.installs[index].name); 47 | RegQueryStringValue(main.HKLM, key, 'Icon', main.red.installs[index].registry.icon); 48 | RegQueryStringValue(main.HKLM, key, 'Autostart', main.red.installs[index].registry.autostart); 49 | 50 | if RegQueryStringValue(main.HKLM, key, 'Port', data) = True then 51 | main.red.installs[index].port := StrToIntDef(data, 0); 52 | 53 | main.red.installs[index].icon := Length(main.red.installs[index].registry.icon) > 0; 54 | main.red.installs[index].autostart := Length(main.red.installs[index].registry.autostart) > 0; 55 | 56 | end; 57 | 58 | 59 | function detect_global_red(var page: TOutputMarqueeProgressWizardPage): integer; 60 | var 61 | res: array of string; 62 | rv: string; 63 | i: integer; 64 | _path: string; 65 | _key: string; 66 | begin 67 | 68 | _key := AddBackslash('{#REDInstallationsRegRoot}') + '0000'; 69 | 70 | Result := 0; 71 | // current_version := ''; 72 | SetArrayLength(res, 0); 73 | page.SetText('Requesting local npm prefix definition...', ''); 74 | if RunCMD('npm config get prefix', '', res) then begin 75 | if GetArrayLength(res) > 0 then begin 76 | _path := res[0]; 77 | page.SetText('Looking for global Node-RED installation...',''); 78 | rv := red_list(_path); 79 | 80 | if Length(rv) > 0 then begin 81 | i:= GetArrayLength(main.red.installs); 82 | SetArrayLength(main.red.installs, i+1); 83 | with main.red.installs[i] do begin 84 | key := _key; 85 | kind := rikGlobal; 86 | version := rv; 87 | path := _path; 88 | id := TObject.Create; 89 | end; 90 | 91 | query_registry(_key, i); 92 | 93 | Result := 1; 94 | end; 95 | end; 96 | end; 97 | 98 | end; 99 | 100 | 101 | function detect_path_red(var page: TOutputMarqueeProgressWizardPage; path: string): integer; 102 | var 103 | rv: string; 104 | i, having: integer; 105 | 106 | _subkeys: array of string; 107 | 108 | regKey: string; 109 | 110 | _path: string; 111 | check: boolean; 112 | _key: string; 113 | 114 | begin 115 | 116 | Result := 0; 117 | 118 | regKey := '{#REDInstallationsRegRoot}' 119 | 120 | // check registry for node-RED key 121 | // enumerate registered installations 122 | // check for package.json 123 | // call npm list 124 | 125 | page.SetText('Querying registry for known Node-RED installations...',''); 126 | if RegKeyExists(main.HKLM, regKey) then begin 127 | if RegGetSubkeyNames(main.HKLM, regKey, _subkeys) then begin 128 | for i:=0 to GetArrayLength(_subkeys) - 1 do begin 129 | 130 | // '0000' reserved for global installation 131 | if StrToIntDef(_subkeys[i], -1) < 1 then continue; 132 | 133 | check := False; 134 | _key := regKey + '\' + _subkeys[i]; 135 | if RegQueryStringValue(main.HKLM, _key, 'Path', _path) = True then begin 136 | page.SetText('Verifying Node-RED installation path: ' + _path, ''); 137 | if DirExists(_path) then begin 138 | rv := red_list(_path); 139 | 140 | if Length(rv) > 0 then begin 141 | having:= GetArrayLength(main.red.installs); 142 | SetArrayLength(main.red.installs, having+1); 143 | with main.red.installs[having] do begin 144 | key := _key; 145 | kind := rikPath; 146 | version := rv; 147 | path := _path; 148 | id := TObject.Create; 149 | end; 150 | 151 | // Additional Data 152 | query_registry(_key, having); 153 | 154 | having:= GetArrayLength(main.red.installs); 155 | Result := Result + 1; 156 | check := True; 157 | end; 158 | end; 159 | end; 160 | 161 | // no Node-RED installation in path! 162 | if not check then 163 | RegDeleteKeyIncludingSubkeys(main.HKLM, regKey + '\' + _subkeys[i]) 164 | 165 | end; 166 | end; 167 | end; 168 | end; 169 | 170 | 171 | function detect_red_installations(var page: TOutputMarqueeProgressWizardPage): integer; 172 | var 173 | i, ii: integer; 174 | 175 | begin 176 | 177 | i:=0; 178 | i:= i + detect_global_red(page); 179 | i:= i + detect_path_red(page, ''); 180 | 181 | for ii:=0 to i - 1 do begin 182 | debug('found: ' + main.red.installs[ii].version); 183 | end; 184 | 185 | Result := i; 186 | 187 | end; 188 | 189 | -------------------------------------------------------------------------------- /iss/redactionpage.iss: -------------------------------------------------------------------------------- 1 | #include <.\image_button.iss> 2 | 3 | [code] 4 | 5 | var 6 | 7 | // _add: TNewButton; 8 | // _remove: TNewButton; 9 | //_note: TLabel; 10 | _clicked_line: integer; 11 | // _nr: TNewButton; 12 | 13 | 14 | 15 | // _actions: array of TREDInstallationAction; 16 | 17 | // list of all the actions we put into the checklistbox 18 | // the index of an action in this array **must** match the index of the item shown in the CheckListBox! 19 | // _action_items: array of TREDListItem; 20 | 21 | // procedure WizardFormResize(Sender: TObject); forward; 22 | 23 | // procedure OnMM(Sender: TObject; Shift: TShiftState; X, Y: Integer); forward; 24 | 25 | 26 | // ***** 27 | // ** Work around this BUG! 28 | // ** https://stackoverflow.com/questions/31167028/inno-setup-components-graphical-refresh-issue 29 | // ** 30 | // ** As soon as the FontStyle of an item is changed (in whatever! way) 31 | // ** this item will not be drawn correctly when it's selected & the CheckListBox 32 | // ** looses the focus. 33 | // ** 34 | // ** There seems to be no workaround than to move ItemIndex to an item with unmodified FontStyle! 35 | // ** 36 | // ** FocusMonitorProc will be called by SetTimer, invoked in MakeRedActionPage 37 | 38 | function SetTimer(hWnd: LongWord; nIDEvent, uElapse: LongWord; 39 | lpTimerFunc: LongWord): LongWord; external 'SetTimer@user32.dll stdcall'; 40 | 41 | var 42 | LastFocusedControl: TWinControl; 43 | 44 | procedure FocusMonitorProc(H: LongWord; Msg: LongWord; IdEvent: LongWord; Time: LongWord); 45 | 46 | var 47 | fs: TFontStyles; 48 | box: TNewCheckListBox; 49 | index: integer; 50 | begin 51 | 52 | // if WizardForm.CurPageID <> main.pages.red_action.ID then Exit; 53 | 54 | box := main.pages.red_action.CheckListBox; 55 | 56 | if not (WizardForm.ActiveControl = box) then begin 57 | if LastFocusedControl = box then begin 58 | 59 | index := box.ItemIndex; 60 | fs := box.ItemFontStyle[index]; 61 | if not (fs = []) then 62 | box.ItemIndex := index + 1; 63 | 64 | end; 65 | end; 66 | 67 | LastFocusedControl := WizardForm.ActiveControl; 68 | 69 | end; 70 | 71 | // When an installation setup dataset is created (either for an existing installation or representing a new one), 72 | // a new TObject is created as 'id' property. 73 | // This 'id' is used to link between entries in the CheckListBox & main.red.installs 74 | 75 | function GetREDInstallationIndex(link: TObject): integer; 76 | var 77 | i: integer; 78 | 79 | begin 80 | Result := -1; 81 | for i:=0 to GetArrayLength(main.red.installs) - 1 do begin 82 | if main.red.installs[i].id = link then begin 83 | Result := i; 84 | break; 85 | end; 86 | end; 87 | debug('GetRED: ' + IntToStr(Result)); 88 | end; 89 | 90 | function _add_installation(index: integer): Boolean; forward; 91 | function _add_new_installation(): Boolean; forward; 92 | procedure _make_headline(); forward; 93 | 94 | function _set_remove_button_state(index: integer): boolean; 95 | var 96 | _remove: TNewButton; 97 | // _state: boolean; 98 | i, ii: integer; 99 | 100 | begin 101 | _remove := TNewButton(main.pages.red_action.FindComponent('redaction_remove')); 102 | 103 | Result := true; 104 | if index < 0 then begin 105 | index := main.pages.red_action.CheckListBox.ItemIndex; 106 | end; 107 | 108 | if not (index < 0) then begin 109 | if index < GetArrayLength(main.red.items) then begin 110 | Result := (GetREDInstallationIndex(main.red.items[index].link) >= 0); 111 | end; 112 | end; 113 | 114 | // Check if there's more than 1 valid entry. 115 | ii:=0; 116 | for i:=0 to GetArrayLength(main.red.installs) -1 do begin 117 | if main.red.installs[i].kind <> rikVoid then begin 118 | index:=i; 119 | ii:=ii+1; 120 | end; 121 | end; 122 | 123 | // If so: In case it's only one entry: Enable only if this is not a rikNew installation! 124 | if ii = 1 then begin 125 | Result := (main.red.installs[index].kind <> rikNew) and Result; 126 | end else begin 127 | Result := (ii > 1) and Result; 128 | end; 129 | 130 | _remove.Enabled := Result; 131 | 132 | end; 133 | 134 | 135 | procedure _on_click(Sender: TObject); 136 | var 137 | index: integer; 138 | box: TNewCheckListBox; 139 | // _remove: TNewButton; 140 | k: sREDListItemKind; 141 | _inst_index: integer; 142 | 143 | begin 144 | box := TNewCheckListBox(Sender); 145 | index := box.ItemIndex; 146 | 147 | _clicked_line := index; 148 | 149 | // _remove := TNewButton(main.pages.red_action.FindComponent('redaction_remove')); 150 | // _remove.Enabled := (GetREDInstallationIndex(main.red.items[index].link) >= 0); 151 | _set_remove_button_state(index); 152 | 153 | k := main.red.items[index].kind; 154 | _inst_index := GetREDInstallationIndex(main.red.items[index].link); 155 | 156 | if k = rlikAction then begin 157 | main.red.installs[_inst_index].action := main.red.items[index].action; 158 | end; 159 | 160 | if k = rlikAutostart then begin 161 | main.red.installs[_inst_index].autostart := (box.State[index] = cbChecked); 162 | end; 163 | 164 | if k = rlikIcon then begin 165 | main.red.installs[_inst_index].icon := (box.State[index] = cbChecked); 166 | end; 167 | 168 | if k = rlikGlobal then begin 169 | if box.State[index] = cbChecked then begin 170 | main.red.installs[_inst_index].path := ''; 171 | end; 172 | end; 173 | 174 | if k = rlikPath then begin 175 | if box.State[index] = cbChecked then begin 176 | main.red.installs[_inst_index].path := main.red.items[index].action; 177 | end; 178 | end; 179 | 180 | end; 181 | 182 | function _run_label_input_box(_label: string): string; forward; 183 | function _run_port_input_box(port: integer): integer; forward; 184 | 185 | procedure _on_dblclick(Sender: TObject); 186 | var 187 | index: integer; 188 | box: TNewCheckListBox; 189 | 190 | _label: string; 191 | _new_label: string; 192 | 193 | _port: integer; 194 | _path: string; 195 | 196 | k: sREDListItemKind; 197 | _inst_index: integer; 198 | 199 | begin 200 | box := TNewCheckListBox(Sender); 201 | index := box.ItemIndex; 202 | 203 | k := main.red.items[index].kind; 204 | _inst_index := GetREDInstallationIndex(main.red.items[index].link); 205 | 206 | if _inst_index < 0 then Exit; 207 | 208 | if k = rlikLabel then begin 209 | 210 | _label := main.red.installs[_inst_index].name; 211 | if Length(_label) < 1 then 212 | _label := 'Node-RED'; 213 | 214 | _new_label := _run_label_input_box(_label); 215 | 216 | if _label <> _new_label then begin 217 | _label := _new_label; 218 | 219 | main.red.installs[_inst_index].name := _label; 220 | 221 | if Length(_label) < 1 then 222 | _label := 'Node-RED'; 223 | 224 | box.ItemCaption[index] := '#' + main.red.items[index].action + ': ' + _label; 225 | end; 226 | 227 | end; 228 | 229 | if k = rlikPort then begin 230 | 231 | // debug(IntToStr(li.target.installation.port)); 232 | 233 | _port := _run_port_input_box(main.red.installs[_inst_index].port); 234 | if _port <> main.red.installs[_inst_index].port then begin 235 | 236 | // need to use _action_item[] here, as li seems to be (only) a copy. 237 | main.red.installs[_inst_index].port := _port; 238 | 239 | if _port = 0 then 240 | _label := '(Default)' 241 | else 242 | _label := IntToStr(_port); 243 | 244 | box.ItemCaption[index] := 'Port for Autostart & Desktop Icon: ' + _label; 245 | 246 | end; 247 | 248 | end; 249 | 250 | if k = rlikPath then begin 251 | 252 | _path := main.red.items[index].action; 253 | 254 | if BrowseForFolder( 255 | 'Select the installation directory for this Node-RED installation.', 256 | _path, True) then begin 257 | 258 | { 259 | if DirExists(_path) then begin 260 | if (not isEmptyDir(_path)) then begin 261 | MsgBox('Selected directory: ' + #13#10 + #13#10 + ' ' + _path + #13#10 + #13#10 262 | 'This directory is not empty.' + #13#10 + 263 | 'Please choose another - empty - directory!', 264 | mbError, MB_OK); 265 | _path := ''; 266 | end; 267 | end; 268 | } 269 | 270 | if Length(_path) > 0 then begin 271 | main.red.items[index].action := _path; 272 | box.ItemCaption[index] := 'Custom path: ' + _path; 273 | main.red.installs[_inst_index].path := _path; 274 | end; 275 | 276 | end; 277 | end; 278 | 279 | end; 280 | 281 | procedure _on_add_red(Sender: TObject); 282 | var 283 | _label: string; 284 | 285 | i: integer; 286 | 287 | _path: string; 288 | _ppage: TOutputMarqueeProgressWizardPage; 289 | rv: string; 290 | _error: integer; 291 | 292 | begin 293 | // box := TNewCheckListBox(Sender); 294 | // index := box.ItemIndex; 295 | 296 | // li := _action_items[index]; 297 | // debug(li.target.installation.name); 298 | 299 | if not BrowseForFolder( 300 | 'Select the installation directory of an existing Node-RED installation.', 301 | _path, True) then Exit; 302 | 303 | rv := ''; 304 | if DirExists(_path) then begin 305 | _ppage := CreateOutputMarqueeProgressPage('Verifying installation path...', 'We check if there''s a Node-RED installation at the given path.'); 306 | _ppage.SetText('Verifying: ' + _path, ''); 307 | 308 | try 309 | // _ppage.SetProgress(_iProgress, {#ProgressMax}); 310 | _ppage.Animate(); 311 | _ppage.Show; 312 | rv := red_list(_path); 313 | finally 314 | _ppage.Hide(); 315 | end; 316 | end; 317 | 318 | if Length(rv) < 1 then begin 319 | MsgBox( 320 | 'No Node-RED installation found @ ' + _path, 321 | mbInformation, MB_OK); 322 | Exit; 323 | end; 324 | 325 | _error := 0; 326 | 327 | for i:= 0 to GetArrayLength(main.red.installs) - 1 do begin 328 | if main.red.installs[i].path = _path then begin 329 | _error := 1; 330 | break; 331 | end; 332 | end; 333 | 334 | // debugInt(i); 335 | 336 | if _error > 0 then begin 337 | 338 | _label := main.red.installs[i].name; 339 | if Length(_label) < 1 then 340 | _label := 'Node-RED'; 341 | 342 | MsgBox( 343 | 'We found a Node-RED installation @ ' + _path + #13#10 + #13#10 + 344 | 'This yet is already managed by #' + IntToStr(i) + ': ' + _label + '.' + #13#10 + #13#10 + 345 | 'Thus we did not add another installation setup.', 346 | mbError, MB_OK); 347 | 348 | Exit; 349 | 350 | end; 351 | 352 | i:= GetArrayLength(main.red.installs); 353 | SetArrayLength(main.red.installs, i+1); 354 | with main.red.installs[i] do begin 355 | kind := rikPath; 356 | version := rv; 357 | path := _path; 358 | id := TObject.Create; 359 | end; 360 | 361 | _add_installation(i); 362 | _make_headline(); 363 | 364 | end; 365 | 366 | procedure _make_headline(); 367 | var 368 | _length, i: integer; 369 | _found: integer; 370 | _new: integer; 371 | 372 | _label: string; 373 | 374 | k: sREDInstallationKind; 375 | 376 | begin 377 | 378 | _length := GetArrayLength(main.red.installs); 379 | 380 | for i:= 0 to GetArrayLength(main.red.installs) - 1 do begin 381 | k:= main.red.installs[i].kind 382 | if k = rikNew then begin 383 | _new := _new + 1 384 | end else if k <> rikVoid then begin 385 | _found := _found + 1; 386 | end; 387 | end; 388 | 389 | if _found > 0 then begin 390 | _label := IntToStr(_found) + ' current '; 391 | if _found > 1 then 392 | _label := _label + 'installations' 393 | else 394 | _label := _label + 'installation'; 395 | 396 | _label := _label + ' found' 397 | end; 398 | 399 | if _new > 0 then begin 400 | 401 | if Length(_label) > 0 then 402 | _label := _label + ' / '; 403 | 404 | _label := _label + IntToStr(_new) + ' new '; 405 | if _new > 1 then 406 | _label := _label + 'installations' 407 | else 408 | _label := _label + 'installation'; 409 | 410 | _label := _label + ' projected:' 411 | end; 412 | 413 | with main.pages.red_action.SubCaptionLabel do begin 414 | Caption := _label; 415 | Font.Style := [fsBold]; 416 | end; 417 | 418 | end; 419 | 420 | function _run_label_input_box(_label: string): string; 421 | var 422 | _form: TSetupForm; 423 | _edit: TNewEdit; 424 | _ok, _cancel: TNewButton; 425 | _width: integer; 426 | 427 | box: TNewCheckListBox; 428 | index: integer; 429 | fs: TFontStyles; 430 | 431 | begin 432 | 433 | _form := CreateCustomForm(); 434 | try 435 | with _form do begin 436 | ClientWidth := ScaleX(256); 437 | ClientHeight := ScaleY(128); 438 | Caption := 'Label this installation'; 439 | end; 440 | 441 | _edit := TNewEdit.Create(_form); 442 | with _edit do begin 443 | Parent := _form; 444 | Top := ScaleY(10); 445 | Left := ScaleX(10); 446 | Width := _form.ClientWidth - ScaleX(2 * 10); 447 | Height := ScaleY(23); 448 | Anchors := [akLeft, akTop, akRight]; 449 | Text := _label; 450 | end; 451 | 452 | _ok := TNewButton.Create(_form); 453 | with _ok do begin 454 | Parent := _form; 455 | Caption := 'OK'; 456 | Left := _form.ClientWidth - ScaleX(75 + 6 + 75 + 10); 457 | Top := _form.ClientHeight - ScaleY(23 + 10); 458 | Height := ScaleY(23); 459 | Anchors := [akRight, akBottom] 460 | ModalResult := mrOk; 461 | Default := True; 462 | end; 463 | 464 | _cancel := TNewButton.Create(_form); 465 | with _cancel do begin 466 | Parent := _form; 467 | Caption := 'Cancel'; 468 | Left := _form.ClientWidth - ScaleX(75 + 10); 469 | Top := _form.ClientHeight - ScaleY(23 + 10); 470 | Height := ScaleY(23); 471 | Anchors := [akRight, akBottom] 472 | ModalResult := mrCancel; 473 | Cancel := True; 474 | end; 475 | 476 | _width := _form.CalculateButtonWidth([_ok.Caption, _cancel.Caption]); 477 | _ok.Width := _width; 478 | _cancel.Width := _width; 479 | 480 | _form.ActiveControl := _edit; 481 | { Keep the form from sizing vertically since we don't have any controls which can size vertically } 482 | _form.KeepSizeY := True; 483 | { Center on WizardForm. Without this call it will still automatically center, but on the screen } 484 | _form.FlipSizeAndCenterIfNeeded(True, WizardForm, False); 485 | 486 | // BoldItem-not-drawn-when-focus-lost-bug ... workaround 487 | box := main.pages.red_action.CheckListBox; 488 | index := box.ItemIndex; 489 | 490 | fs := box.ItemFontStyle[index]; 491 | if not (fs = []) then 492 | box.ItemIndex := index + 1; 493 | 494 | if _form.ShowModal() = mrOk then 495 | Result := _edit.Text 496 | else 497 | Result := _label; 498 | 499 | box.ItemIndex := index; 500 | 501 | finally 502 | _form.Free(); 503 | end; 504 | end; 505 | 506 | procedure _port_input_box_radio_default_checked(Sender: TObject); 507 | var 508 | _form: TSetupForm; 509 | _radio: TNewRadioButton; 510 | _ok: TNewButton; 511 | 512 | begin 513 | _radio := TNewRadioButton(Sender); 514 | _form := TSetupForm(_radio.Parent); 515 | _ok := TNewButton(_form.FindComponent('_port_input_ok')); 516 | _ok.Enabled := True; 517 | end; 518 | 519 | 520 | procedure _port_input_box_port_on_change(Sender: TObject); 521 | var 522 | _form: TSetupForm; 523 | _edit: TNewEdit; 524 | _text: string; 525 | _port: integer; 526 | _ok: TNewButton; 527 | _radio: TNewRadioButton; 528 | 529 | begin 530 | _edit := TNewEdit(Sender); 531 | _text := _edit.Text; 532 | _form := TSetupForm(_edit.Parent); 533 | _ok := TNewButton(_form.FindComponent('_port_input_ok')); 534 | _radio := TNewRadioButton(_form.FindComponent('_port_input_radio_port')); 535 | 536 | _radio.Checked := True; 537 | 538 | _port := StrToIntDef(_edit.Text, -1); 539 | if ((_port < 0) or (_port > 65535)) then begin 540 | _edit.Font.Color := clRed; 541 | _ok.Enabled := False; 542 | end else begin 543 | _edit.Font.Color := clBlack; 544 | _ok.Enabled := True; 545 | end; 546 | end; 547 | 548 | function _run_port_input_box(port: integer): integer; 549 | var 550 | _form: TSetupForm; 551 | _edit: TNewEdit; 552 | _radio_default, _radio_port: TNewRadioButton; 553 | _ok, _cancel: TNewButton; 554 | _width: integer; 555 | _info: TNewStaticText; 556 | 557 | begin 558 | 559 | _form := CreateCustomForm(); 560 | try 561 | with _form do begin 562 | ClientWidth := ScaleX(256); 563 | ClientHeight := ScaleY(128); 564 | Caption := 'Port for Autostart & Desktop Icon'; 565 | end; 566 | 567 | _radio_default := TNewRadioButton.Create(_form) 568 | with _radio_default do begin 569 | Parent := _form; 570 | Top := ScaleY(8); 571 | Left := ScaleX(8); 572 | Width := _form.ClientWidth - ScaleX(2 * 8); 573 | Height := ScaleY(Height); 574 | Anchors := [akLeft, akTop, akRight]; 575 | Caption := '(Default) - as defined in ''settings.js''.' 576 | OnClick := @_port_input_box_radio_default_checked; 577 | end; 578 | 579 | _radio_port := TNewRadioButton.Create(_form) 580 | with _radio_port do begin 581 | Parent := _form; 582 | Top := _radio_default.Top + _radio_default.Height + ScaleY(8); 583 | Left := ScaleX(8); 584 | Width := ScaleX(80); 585 | Height := ScaleY(Height); 586 | Anchors := [akLeft, akTop, akRight]; 587 | Caption := 'Custom Port:' 588 | Name := '_port_input_radio_port' 589 | end; 590 | 591 | _edit := TNewEdit.Create(_form); 592 | with _edit do begin 593 | Parent := _form; 594 | Top := _radio_port.Top; 595 | Left := _radio_port.Left + _radio_port.Width + ScaleX(8); 596 | Width := _form.ClientWidth - Left - ScaleX(8); 597 | Height := ScaleY(Height); 598 | Anchors := [akLeft, akTop, akRight]; 599 | // Text := IntToStr(port); 600 | OnChange := @_port_input_box_port_on_change; 601 | end; 602 | 603 | _info := TNewStaticText.Create(_form); 604 | with _info do begin 605 | Parent := _form; 606 | Top := _radio_port.Top + _radio_port.Height + ScaleY(4); 607 | Left := _edit.Left; // ScaleX(8 + 16); 608 | Width := _edit.Width; // _form.ClientWidth - Left - ScaleX(8); 609 | Height := ScaleY(Height * 2); 610 | Anchors := [akLeft, akTop, akRight]; 611 | WordWrap := True; 612 | Font.Style := [fsBold]; 613 | Caption := 'Please ensure that this port is free to be used by Node-RED.'; 614 | end; 615 | 616 | _ok := TNewButton.Create(_form); 617 | with _ok do begin 618 | Parent := _form; 619 | Caption := 'OK'; 620 | Left := _form.ClientWidth - ScaleX(75 + 6 + 75 + 10); 621 | Top := _form.ClientHeight - ScaleY(23 + 10); 622 | Height := ScaleY(23); 623 | Anchors := [akRight, akBottom] 624 | ModalResult := mrOk; 625 | Default := True; 626 | Name := '_port_input_ok' 627 | end; 628 | 629 | _cancel := TNewButton.Create(_form); 630 | with _cancel do begin 631 | Parent := _form; 632 | Caption := 'Cancel'; 633 | Left := _form.ClientWidth - ScaleX(75 + 10); 634 | Top := _form.ClientHeight - ScaleY(23 + 10); 635 | Height := ScaleY(23); 636 | Anchors := [akRight, akBottom] 637 | ModalResult := mrCancel; 638 | Cancel := True; 639 | end; 640 | 641 | _width := _form.CalculateButtonWidth([_ok.Caption, _cancel.Caption]); 642 | _ok.Width := _width; 643 | _cancel.Width := _width; 644 | 645 | _form.ActiveControl := _edit; 646 | { Keep the form from sizing vertically since we don't have any controls which can size vertically } 647 | _form.KeepSizeY := True; 648 | { Center on WizardForm. Without this call it will still automatically center, but on the screen } 649 | _form.FlipSizeAndCenterIfNeeded(True, WizardForm, False); 650 | 651 | if port > 0 then begin 652 | _edit.Text := IntToStr(port); 653 | _radio_port.Checked := True; 654 | _form.ActiveControl := _radio_port; 655 | end else begin 656 | _edit.Text := '1880'; 657 | _radio_default.Checked := True; 658 | _form.ActiveControl := _radio_default; 659 | end; 660 | 661 | if _form.ShowModal() = mrOk then begin 662 | if _radio_default.Checked then 663 | Result := 0 664 | else 665 | Result := StrToInt(_edit.Text); 666 | end else 667 | Result := port; 668 | 669 | finally 670 | _form.Free(); 671 | end; 672 | end; 673 | 674 | procedure _on_add_new(Sender: TObject); 675 | begin 676 | _add_new_installation(); 677 | end; 678 | 679 | { 680 | procedure move_action_record(var from_record: TREDInstallationAction; var to_record: TREDInstallationAction); 681 | begin 682 | to_record := from_record; 683 | end; 684 | 685 | 686 | procedure move_action_item_record(var from_record: TREDListItem; var to_record: TREDListItem); 687 | begin 688 | to_record := from_record; 689 | end; 690 | } 691 | 692 | const 693 | LB_DELETESTRING = $182; 694 | 695 | procedure _on_click_remove(Sender: TObject); 696 | var 697 | index: integer; 698 | box: TNewCheckListBox; 699 | line: integer; 700 | action: string; 701 | 702 | fs: TFontStyles; 703 | 704 | i, ii: integer; 705 | _label: string; 706 | 707 | _id: TObject; 708 | _inst_index: integer; 709 | 710 | begin 711 | box := main.pages.red_action.CheckListBox; 712 | index := box.ItemIndex; 713 | 714 | { 715 | debugInt(index); 716 | 717 | _ai := _action_items[index].target.index; 718 | line := _action_items[index].target.line; 719 | 720 | debugInt(_ai); 721 | debugInt(line); 722 | } 723 | 724 | _id := main.red.items[index].link; 725 | _inst_index := GetREDInstallationIndex(_id); 726 | 727 | debug('_inst_index: ' + IntToStr(_inst_index)); 728 | 729 | line := -1; 730 | for i:=0 to GetArrayLength(main.red.items) - 1 do begin 731 | if box.ItemLevel[i] <> 0 then continue; 732 | 733 | if GetREDInstallationIndex(main.red.items[i].link) = _inst_index then begin 734 | line := i; 735 | break; 736 | end; 737 | end; 738 | 739 | if line < 0 then Exit; 740 | 741 | if not (main.red.installs[_inst_index].kind = rikNew) then begin 742 | 743 | repeat 744 | action := ''; 745 | if main.red.items[line].kind = rlikAction then begin 746 | action := main.red.items[line].action; 747 | end; 748 | line := line + 1 749 | until action = 'remove'; 750 | 751 | line := line - 1; 752 | box.CheckItem(line, coCheck); 753 | main.red.installs[_inst_index].action := 'remove'; 754 | 755 | // work around this BUG! 756 | // https://stackoverflow.com/questions/31167028/inno-setup-components-graphical-refresh-issue 757 | fs := box.ItemFontStyle[index]; 758 | if not (fs = []) then begin 759 | box.ItemIndex := index + 1; 760 | end; 761 | 762 | Exit; 763 | 764 | end; 765 | 766 | if GetArrayLength(main.red.installs) = 1 then begin 767 | MsgBox('Please cancel the installation if you do not want to continue!', mbError, IDOK); 768 | Exit; 769 | end; 770 | 771 | // LB_DELETESTRING := $182; 772 | 773 | // Remove all entries of this installation from the CheckListBox 774 | index := line; 775 | while GetArrayLength(main.red.items) > index do begin 776 | if main.red.items[index].link = _id then begin 777 | SendMessage(box.Handle, LB_DELETESTRING, line, 0); 778 | index := index + 1; 779 | end else begin 780 | break; 781 | end; 782 | end; 783 | 784 | debug('index: ' + IntToStr(index)); 785 | 786 | // index now points to the first item of the next installation! 787 | 788 | if line > 0 then begin 789 | // there's still an empty line... 790 | line := line - 1; 791 | SendMessage(box.Handle, LB_DELETESTRING, line, 0); 792 | end; 793 | 794 | // debug('_action_items: ' + IntToStr(GetArrayLength(_action_items))); 795 | 796 | // restructure _action_items 797 | ii:=0; 798 | for i:=0 to GetArrayLength(main.red.items) - 1 do begin 799 | debug('i/ii: ' + IntToStr(i) + '/' + IntToStr(ii)); 800 | if i < line then begin 801 | ii:=ii+1; 802 | continue; 803 | end else if i >= index then begin 804 | // move_action_item_record(_action_items[i], _action_items[ii]); 805 | main.red.items[ii] := main.red.items[i]; 806 | ii:=ii+1; 807 | end; 808 | end; 809 | 810 | SetArrayLength(main.red.items, ii); 811 | 812 | debug('main.red.items: ' + IntToStr(GetArrayLength(main.red.items))); 813 | 814 | // finally: care for main.red.installations 815 | { 816 | ii:=0; 817 | for i:=0 to GetArrayLength(main.red.actions) - 1 do begin 818 | debug('i/ii: ' + IntToStr(i) + '/' + IntToStr(ii)); 819 | if i < _ai then begin 820 | ii:=ii+1; 821 | continue; 822 | end else if i = _ai then begin 823 | continue; 824 | end else begin 825 | // move things up one step... 826 | move_action_record(main.red.actions[i], main.red.actions[ii]); 827 | ii:=ii+1; 828 | end; 829 | end; 830 | 831 | SetArrayLength(main.red.actions, ii); 832 | } 833 | main.red.installs[_inst_index].kind := rikVoid; 834 | main.red.installs[_inst_index].id := nil; 835 | 836 | _set_remove_button_state(-1); 837 | _make_headline(); 838 | Exit; 839 | 840 | // The next sequence used to change the labels of the listed installations 841 | // to get a contious count without gaps. 842 | // Kept here in case we think this creates a better user experience - sometimes in the future... 843 | 844 | ii:=0; 845 | for i:=0 to GetArrayLength(main.red.items) - 1 do begin 846 | if box.ItemLevel[i] = 0 then begin 847 | 848 | if main.red.items[i].kind = rlikNone then continue; 849 | 850 | index := GetREDInstallationIndex(main.red.items[i].link); 851 | 852 | if index > -1 then begin 853 | 854 | _label := main.red.installs[index].name; 855 | 856 | if Length(_label) < 1 then 857 | _label := 'Node-RED'; 858 | 859 | box.ItemCaption[i] := '#' + IntToStr(ii + 1) + ': ' + _label; 860 | main.red.items[i].action := IntToStr(ii + 1); 861 | 862 | ii:=ii+1; 863 | end; 864 | 865 | end; 866 | end; 867 | 868 | _make_headline(); 869 | 870 | end; 871 | 872 | function _create_red_action_layout(page: TInputOptionWizardPage): Boolean; 873 | var 874 | box: TNewCheckListBox; 875 | 876 | _add: TNewButton; 877 | _remove: TNewButton; 878 | 879 | _red: TNewButton; 880 | _icon: string; 881 | 882 | _bitmap: TBitmap; 883 | _imageList: THandle; 884 | _buttonImageList: BUTTON_IMAGELIST; 885 | 886 | _note: TLabel; 887 | 888 | begin 889 | 890 | // _page := main.pages.red_action; 891 | if page = nil then begin 892 | Result := False; 893 | Exit; 894 | end; 895 | 896 | Result := True; 897 | 898 | box := page.CheckListBox; 899 | 900 | with box do begin 901 | ShowLines := True; 902 | WantTabs := False; 903 | Flat := True; 904 | end; 905 | 906 | _add := TNewButton.Create(page); 907 | with _add do begin 908 | Name := 'redaction_add'; 909 | Parent := page.Surface; 910 | Caption := '+'; 911 | Left := page.SurfaceWidth - ScaleX(Height); 912 | Top := box.Top; 913 | Width := ScaleX(Height); 914 | Height := ScaleY(Height); 915 | Anchors := [akTop, akRight]; 916 | Hint := 'Add additional installation'; 917 | ShowHint := True; 918 | Font.Size := Font.Size + 2; 919 | OnClick := @_on_add_new; 920 | end; 921 | 922 | _remove := TNewButton.Create(page); 923 | with _remove do begin 924 | Name := 'redaction_remove'; 925 | Parent := page.Surface; 926 | Caption := '-'; 927 | Left := _add.Left; 928 | Top := _add.Top + _add.Height + ScaleY(8); 929 | Width := ScaleX(Height); 930 | Height := ScaleY(Height); 931 | Anchors := [akTop, akRight]; 932 | Hint := 'Remove selected installation'; 933 | ShowHint := True; 934 | Font.Size := Font.Size + 4; 935 | Enabled := False; 936 | OnClick := @_on_click_remove; 937 | end; 938 | 939 | _note := TLabel.Create(page) 940 | with _note do begin 941 | Name := 'redaction_note'; 942 | Parent := page.Surface; 943 | Left := box.Left; 944 | Top := page.SurfaceHeight - ScaleY(Height); 945 | Width := _add.Left - ScaleX(8); 946 | Height := ScaleY(Height); 947 | Anchors := [akLeft, akRight, akBottom]; 948 | Caption := '* DoubleClick to edit.'; 949 | Font.Color := clGray; 950 | end; 951 | 952 | _red := TNewButton.Create(page); 953 | with _red do begin 954 | Name := 'redaction_red'; 955 | Parent := page.Surface; 956 | Caption := ''; 957 | Left := _add.Left; 958 | Top := _remove.Top + _remove.Height + ScaleY(24); 959 | Width := ScaleX(Height); 960 | Height := ScaleY(Height); 961 | Anchors := [akTop, akRight]; 962 | Hint := 'Add existing Node-RED installation'; 963 | ShowHint := True; 964 | Enabled := main.red.npm; 965 | OnClick := @_on_add_red; 966 | end; 967 | 968 | // Manage the icon for the _red button 969 | _icon := 'node-red-small.bmp'; 970 | if not FileExists(ExpandConstant('{tmp}\' + _icon)) then ExtractTemporaryFile(_icon); 971 | 972 | _imageList := ImageList_Create(32, 32, ILC_COLOR32, 1, 1); 973 | 974 | _bitmap := TBitmap.Create(); 975 | _bitmap.LoadFromFile(ExpandConstant('{tmp}\' + _icon)); 976 | 977 | ImageList_Add(_imageList, _bitmap.Handle, 0); 978 | 979 | _buttonImageList.himl := _imageList; 980 | _buttonImageList.uAlign := BUTTON_IMAGELIST_ALIGN_CENTER; 981 | 982 | SendSetImageListMessage(_red.Handle, BCM_SETIMAGELIST, 0, _buttonImageList); 983 | 984 | box.Width := _add.Left - ScaleX(8); 985 | box.Height := _note.Top - box.Top - ScaleY(8); 986 | box.Anchors := [akLeft, akTop, akRight, akBottom]; 987 | 988 | box.OnClick := @_on_click; 989 | box.OnDblClick := @_on_dblclick; 990 | 991 | Result := True; 992 | end; 993 | 994 | procedure _add_action_item(kind: sREDListItemKind; action: string; id: TObject; check: integer); 995 | var 996 | _length: integer; 997 | 998 | begin 999 | 1000 | _length := GetArrayLength(main.red.items); 1001 | SetArrayLength(main.red.items, _length + 1); 1002 | 1003 | main.red.items[_length].kind := kind; 1004 | main.red.items[_length].action := action; 1005 | main.red.items[_length].link := id; 1006 | // check => not used! 1007 | 1008 | end; 1009 | 1010 | function _add_actions(box: TNewCheckListBox; var installation: rREDInstallation; level: byte; show_header: boolean): integer; 1011 | 1012 | var 1013 | rvl: integer; 1014 | i, ii: integer; 1015 | cv, rv, tag: string; 1016 | index: integer; 1017 | 1018 | _id: TObject; 1019 | begin 1020 | 1021 | _id := installation.id; 1022 | 1023 | if show_header then begin 1024 | index := box.AddGroup('Action', '', level, nil); 1025 | _add_action_item(rlikNone, '', _id, index); 1026 | box.ItemFontStyle[index] := [fsBold]; 1027 | level := level + 1; 1028 | end; 1029 | 1030 | cv := installation.version; 1031 | rvl := GetArrayLength(main.red.versions); 1032 | tag := ''; 1033 | 1034 | // only if there's an installation (providing a version number) 1035 | if Length(cv) > 0 then begin 1036 | 1037 | for i := 0 to rvl - 1 do begin 1038 | rv := main.red.versions[i].version; 1039 | if CompareVersions(cv, rv) = 0 then begin 1040 | tag := main.red.versions[i].tag; 1041 | break; 1042 | end; 1043 | end; 1044 | 1045 | index := box.AddRadioButton('Keep Node-RED v' + cv, ANSIUpperCase(tag), level, False, True, nil); 1046 | _add_action_item(rlikAction, '', _id, index); 1047 | 1048 | box.Checked[index] := True; 1049 | 1050 | // 'Change' group label only if there's an installation 1051 | index := box.AddRadioButton('Change', '', level, False, True, nil); 1052 | _add_action_item(rlikNone, '', _id, index); 1053 | 1054 | end else begin 1055 | // Compensate for the +1 in later lines! 1056 | level := level - 1; 1057 | end; 1058 | 1059 | for i := 0 to rvl - 1 do begin 1060 | 1061 | // From Hi to Lo 1062 | rv := main.red.versions[rvl - 1 - i].version; 1063 | tag := main.red.versions[rvl - 1 - i].tag; 1064 | 1065 | ii := CompareVersions(cv, rv); 1066 | 1067 | if ((ii > 0) or (Length(cv) < 1)) then begin 1068 | index := box.AddRadioButton('Install Node-RED v' + rv, ANSIUpperCase(tag), level+1, False, True, nil); 1069 | _add_action_item(rlikAction, rv, _id, index); 1070 | 1071 | if Length(cv) < 1 then begin 1072 | if ANSILowerCase(tag) = 'latest' then begin 1073 | box.Checked[index] := True; 1074 | main.red.installs[GetREDInstallationIndex(_id)].action := rv; 1075 | end; 1076 | end; 1077 | 1078 | continue; 1079 | end else if ii = 0 then begin 1080 | continue; 1081 | end else if ii < 0 then begin 1082 | index := box.AddRadioButton('Update to Node-RED v' + rv, ANSIUpperCase(tag), level+1, False, True, nil); 1083 | _add_action_item(rlikAction, rv, _id, index); 1084 | end; 1085 | 1086 | end; 1087 | 1088 | if Length(cv) > 0 then begin 1089 | index := box.AddRadioButton('Remove Node-RED v' + cv, '', level, False, True, nil); 1090 | _add_action_item(rlikAction, 'remove', _id, index); 1091 | end; 1092 | 1093 | Result := index; 1094 | 1095 | end; 1096 | 1097 | { 1098 | procedure _copy_tredinstallation_record(const source: TREDInstallation; var target: TREDInstallation); 1099 | begin 1100 | 1101 | // deep copy! 1102 | target.kind := source.kind; 1103 | target.path := source.path; 1104 | target.version := source.version; 1105 | target.port := source.port; 1106 | target.name := source.name; 1107 | 1108 | end; 1109 | } 1110 | 1111 | function _add_details(box: TNewCheckListBox; var installation: rREDInstallation; level: byte; show_header: boolean): integer; 1112 | var 1113 | index: integer; 1114 | 1115 | _id: TObject; 1116 | 1117 | begin 1118 | 1119 | _id := installation.id; 1120 | 1121 | if show_header then begin 1122 | index := box.AddGroup('Details', '', level, nil); 1123 | _add_action_item(rlikNone, '', _id, index); 1124 | 1125 | box.ItemFontStyle[index] := [fsBold]; 1126 | level := level + 1; 1127 | end; 1128 | 1129 | index := box.AddGroup('Version: ' + installation.version, '', level, nil); 1130 | _add_action_item(rlikNone, '', _id, index); 1131 | 1132 | if installation.kind = rikGlobal then begin 1133 | index := box.AddGroup('Global installation @ ' + installation.path, '', level, nil); 1134 | end else begin 1135 | index := box.AddGroup('Path: ' + installation.path, '', level, nil); 1136 | end; 1137 | _add_action_item(rlikNone, '', _id, index); 1138 | 1139 | Result := index; 1140 | end; 1141 | 1142 | function _add_config(box: TNewCheckListBox; var installation: rREDInstallation; level: byte; show_header: boolean): integer; 1143 | var 1144 | index: integer; 1145 | // _inst: TREDInstallation; 1146 | _label: string; 1147 | _port: integer; 1148 | _path: string; 1149 | 1150 | _id: TObject; 1151 | id: integer; 1152 | 1153 | begin 1154 | 1155 | _id := installation.id; 1156 | 1157 | if show_header then begin 1158 | index := box.AddGroup('Configuration', '', level, nil); 1159 | _add_action_item(rlikNone, '', _id, index); 1160 | 1161 | box.ItemFontStyle[index] := [fsBold]; 1162 | level := level + 1; 1163 | end; 1164 | 1165 | _label := installation.name; 1166 | if Length(_label) < 1 then 1167 | _label := 'Node-RED'; 1168 | 1169 | index := box.AddCheckBox('Create Desktop Icon', '', level, installation.icon, True, False, False, nil); 1170 | _add_action_item(rlikIcon, '', _id, index); 1171 | 1172 | index := box.AddCheckBox('Add to Autostart group', '', level, installation.autostart, True, False, False, nil); 1173 | _add_action_item(rlikAutostart, '', _id, index); 1174 | 1175 | if installation.kind = rikNew then begin 1176 | 1177 | _path := ExpandConstant('{commonpf32}'); 1178 | 1179 | // Design decision: 1180 | // If the selected directory is empty, we install into this directory 1181 | // If it is not, we'll create a subdirectory 'Node-RED', if necessary postfixed by '(x)' & install there 1182 | 1183 | index := box.AddGroup('Installation path', '', level, nil); 1184 | _add_action_item(rlikNone, '', _id, index); 1185 | 1186 | index := box.AddRadioButton('Install with ''npm -global'' flag.', '', level+1, False, True, nil); 1187 | _add_action_item(rlikGlobal, '', _id, index); 1188 | 1189 | id:= GetREDInstallationIndex(_id); 1190 | if id = 0 then begin 1191 | box.Checked[index] := True; 1192 | main.red.items[index].action := BoolToStr(True); 1193 | installation.path := ''; 1194 | // Optionally: Run through all _actions and check if there's already a global installation! 1195 | end; 1196 | 1197 | index := box.AddRadioButton('Custom path: ' + _path, '*', level+1, False, True, nil); 1198 | _add_action_item(rlikPath, _path, _id, index); 1199 | if id > 0 then begin 1200 | box.Checked[index] := True; 1201 | main.red.items[index].action := _path; 1202 | installation.path := _path; 1203 | end; 1204 | 1205 | end; 1206 | 1207 | _port := installation.port; 1208 | if _port > 0 then 1209 | _label := IntToStr(_port) 1210 | else 1211 | _label := '(Default)'; 1212 | _port := 0; 1213 | 1214 | index := box.AddGroup('Port for Autostart & Desktop Icon: ' + _label , '*', level, nil); 1215 | _add_action_item(rlikPort, '', _id, index); 1216 | 1217 | Result := index; 1218 | end; 1219 | 1220 | function _add_installation(index: integer): Boolean; 1221 | var 1222 | 1223 | box: TNewCheckListBox; 1224 | line: integer; 1225 | _empty: rREDInstallation; 1226 | _label: string; 1227 | 1228 | ii: integer; 1229 | k: sREDInstallationKind; 1230 | 1231 | begin 1232 | 1233 | if index < 0 then Exit; 1234 | if index >= GetArrayLength(main.red.installs) then Exit; 1235 | 1236 | k := main.red.installs[index].kind; 1237 | if k = rikVoid then Exit; 1238 | 1239 | box := main.pages.red_action.CheckListBox; 1240 | line := GetArrayLength(main.red.items); 1241 | 1242 | Result := True; 1243 | main.red.installs[index].id := TObject.Create; 1244 | 1245 | if line > 0 then begin 1246 | 1247 | // an empty REDInstallation 1248 | // ... to provide a mean to return rikVoid on an empty line 1249 | _empty.id := TObject.Create; 1250 | _empty.kind := rikVoid; 1251 | 1252 | line := box.AddGroup('', '', 0, nil); 1253 | _add_action_item(rlikNone, '', _empty.id, line); 1254 | 1255 | end; 1256 | 1257 | ii:= index + 1; 1258 | 1259 | // With those next lines, the number in the label of an installation 1260 | // used to be calculated only based on the non-rikVoid entries. 1261 | // This created a continous count. 1262 | // Commented as we think it's better when one installation keeps it's label number for the liefetime of an setup run. 1263 | // Left here in if we change our mind at some point in time... 1264 | 1265 | { 1266 | ii:=0; 1267 | for i:=0 to index do begin 1268 | if main.red.installs[i].kind <> rikVoid then begin 1269 | ii:=ii+1; 1270 | end; 1271 | end; 1272 | } 1273 | 1274 | _label := main.red.installs[index].name; 1275 | if Length(_label) < 1 then 1276 | _label := 'Node-RED'; 1277 | 1278 | line := box.AddGroup('#' + IntToStr(ii) + ': ' + _label, '*', 0, nil); 1279 | 1280 | _add_action_item(rlikLabel, IntToStr(ii), main.red.installs[index].id, line); 1281 | 1282 | box.ItemFontStyle[line] := [fsBold]; 1283 | 1284 | if k = rikNew then begin 1285 | line := box.AddGroup('New installation', '', 1, nil); 1286 | _add_action_item(rlikNone, '', main.red.installs[index].id, line); 1287 | end else begin 1288 | line := _add_details(box, main.red.installs[index], 1, False); 1289 | end; 1290 | line := _add_actions(box, main.red.installs[index], 1, True); 1291 | line := _add_config(box, main.red.installs[index], 1, True); 1292 | 1293 | _set_remove_button_state(-1); 1294 | 1295 | end; 1296 | 1297 | 1298 | function _fill_current_installations(page: TInputOptionWizardPage): Boolean; 1299 | var 1300 | i: integer; 1301 | _length: integer; 1302 | 1303 | begin 1304 | 1305 | Result := True; 1306 | _length := GetArrayLength(main.red.installs); 1307 | 1308 | for i:= 0 to _length - 1 do begin 1309 | Result := _add_installation(i) and Result; 1310 | end; 1311 | 1312 | _make_headline(); 1313 | 1314 | end; 1315 | 1316 | function _add_new_installation(): Boolean; 1317 | var 1318 | _length: integer; 1319 | 1320 | begin 1321 | 1322 | _length := GetArrayLength(main.red.installs); 1323 | SetArrayLength(main.red.installs, _length + 1); 1324 | 1325 | main.red.installs[_length].kind := rikNew; 1326 | Result := _add_installation(_length); 1327 | 1328 | _make_headline(); 1329 | 1330 | end; 1331 | 1332 | function MakeRedActionPage(page: TInputOptionWizardPage): Boolean; 1333 | begin 1334 | 1335 | Result := False; 1336 | 1337 | // this is for bookkeeping of the actions connected to the lines in the CheckListBox 1338 | SetArrayLength(main.red.items, 0); 1339 | 1340 | if not _create_red_action_layout(page) then Exit; 1341 | 1342 | if GetArrayLength(main.red.installs) > 0 then begin 1343 | _fill_current_installations(page); 1344 | end else begin 1345 | _add_new_installation(); 1346 | end; 1347 | 1348 | // Set up 50ms timer to monitor the focus 1349 | SetTimer(0, 0, 50, CreateCallback(@FocusMonitorProc)); 1350 | 1351 | Result := True; 1352 | end; 1353 | -------------------------------------------------------------------------------- /nodered.iss: -------------------------------------------------------------------------------- 1 | ; ***** 2 | ; * Windows Installer for Node-RED 3 | ; * Definition file for the Inno Setup compiler. 4 | ; * Copyright 2023 Ralph Wetzel 5 | ; * License MIT 6 | ; * https://www.github.com/ralphwetzel/node-red-windows-installer 7 | ; * 8 | 9 | 10 | ; ***** 11 | ; * PreProcessor setup: 12 | ; * We configure all constants via an INI file. 13 | ; * That's easier for maintenence rather than searching for things in the source code 14 | ; * 15 | 16 | #define INIFile RemoveBackslash(SourcePath) + "\setup.ini" 17 | 18 | #define VersionInfoURL ReadIni(INIFile, "installer", "url", "https://nodered.org") 19 | 20 | ; Node.js Default Version - that we propose to install if none is present 21 | #define NodeVersionRecommended ReadIni(INIFile, "node", "recommended") 22 | 23 | ; Node.js version index file - that we process to get the latest available version number 24 | #define NodeVersionIndex ReadIni(INIFile, "node", "index") 25 | 26 | ; comma-separated list of major versions numbers we offer for download 27 | ; ATT: this list is only used as fallback, in case of issues w/ the index file download 28 | #define NodeVersions ReadIni(INIFILE, "node", "versions_fallback", NodeVersionRecommended) 29 | 30 | ; URL to download node.js license from 31 | #define NodeLicenseURL ReadIni(INIFILE, "node", "license") 32 | #define NodeLicenseTmpFileName "node.license" 33 | 34 | ; Root URL to download node.js files from 35 | #define NodeDownloadURL ReadIni(INIFILE, "node", "download", 'https://nodejs.org/dist') 36 | 37 | ; URL to download Node-RED license from 38 | #define REDLicenseURL ReadIni(INIFILE, "red", "license") 39 | #define REDLicenseTmpFileName "red.license" 40 | 41 | ; By default, we offer (for Node-RED) to install the dist-tag versions as known to npm. 42 | ; Additional versions may be defined here; duplicates don't matter! 43 | #define REDAddVersions ReadIni(INIFile, "red", "versions", "") 44 | 45 | ; If npm is not installed, we cannot get the dist-tag versions. 46 | ; In that case, we try to get at least the 'latest' version from 47 | ; https://github.com/node-red/node-red/releases/latest 48 | #define REDLatestTmpFileName "red.releases" 49 | 50 | ; This is the lowest version number we offer for installation 51 | #define REDMinVersion ReadIni(INIFile, "red", "min", "1.0") 52 | 53 | ; Count of parallel installations we provision for to manage 54 | ; We need this explicitely as there's no way to add dynamically to the [Run] section 55 | #define REDProvisionCount ReadIni(INIFile, "red", "provision", "5") 56 | 57 | ; The root key for bookkeeping of the Node-RED installations we know of on this system 58 | #define REDInstallationsRegRoot 'SOFTWARE\Node-RED\installations' 59 | 60 | ; py shall become something like '3.7.6' 61 | #define py ReadIni(INIFile, "python", "version") 62 | ; for pth we extract the first two digits of py 63 | #define pth Copy(StringChange(py, '.', ''), 1, 2) 64 | ; md5 sum for the potential python installer files 65 | #define PyMD5x86 ReadIni(INIFile, "python", "win32") 66 | #define PyMD5x64 ReadIni(INIFile, "python", "amd64") 67 | 68 | ; the download link of the VS Studio Build Tools 69 | ; check as well: https://visualstudio.microsoft.com/downloads/ 70 | #define VSBuildToolsURL ReadIni(INIFile, "vs", "download", 'https://aka.ms/vs/17/release/vs_BuildTools.exe') 71 | ; 15 = 2015 72 | #define VSBuildToolsMinVersion ReadIni(INIFile, "vs", "min", '15') 73 | 74 | ; URL to download vswhere from 75 | #define VSWhereURL ReadIni(INIFILE, "vs", "where") 76 | 77 | #define MyAppName "Node-RED" 78 | #define MyAppVersion "> 3.0" 79 | #define MyAppPublisher "The Node-RED community" 80 | #define MyAppURL "https://nodered.org" 81 | ; #define MyAppExeName "MyProg.exe" 82 | 83 | [Setup] 84 | ; NOTE: The value of AppId uniquely identifies this application. 85 | ; Do not use the same AppId value in installers for other applications. 86 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 87 | AppId={{70D435D8-542E-4087-8E1C-D313404C7E9D} 88 | AppName={#MyAppName} 89 | AppVersion={#MyAppVersion} 90 | AppVerName={#MyAppName} 91 | AppPublisher={#MyAppPublisher} 92 | AppPublisherURL={#MyAppURL} 93 | AppSupportURL={#MyAppURL} 94 | AppUpdatesURL={#MyAppURL} 95 | DefaultDirName={autopf}\{#MyAppName} 96 | DefaultGroupName={#MyAppName} 97 | DisableProgramGroupPage=yes 98 | DisableDirPage=yes 99 | DisableWelcomePage=no 100 | OutputBaseFilename="Node-RED Installer" 101 | Compression=lzma 102 | SolidCompression=yes 103 | SetupLogging=yes 104 | PrivilegesRequired=admin 105 | VersionInfoCopyright={#ReadIni(INIFile, "installer", "copyright", "")} 106 | VersionInfoDescription={#ReadIni(INIFile, "installer", "description", "")} 107 | VersionInfoVersion={#ReadIni(INIFile, "installer", "version", "")} 108 | WizardStyle=modern 109 | WizardImageAlphaFormat=defined 110 | ; WizardImageBackColor=clWhite 111 | WizardImageStretch=True 112 | WizardImageFile="graphics\sidebar\Node RED Side Graphic - BMP.bmp" 113 | LicenseFile="LICENSE" 114 | SetupIconFile={#SourcePath}\icons\node-red-icons.ico 115 | ; PrivilegesRequiredOverridesAllowed=dialog 116 | 117 | ; Installer Code Signing Data 118 | ; To create a test certificate: https://stackoverflow.com/questions/84847/how-do-i-create-a-self-signed-certificate-for-code-signing-on-windows 119 | ; To create a Secure String: $xxx = ConvertTo-SecureString plain-text-string -asPlainText -force 120 | ; to convert to Base64: certutil -encode .\ssCertInfo.pfx .\ssCertInfo.base64.txt 121 | ; Action: https://github.com/dlemstra/code-sign-action 122 | 123 | 124 | [Languages] 125 | Name: "english"; MessagesFile: "compiler:Default.isl" 126 | 127 | [Tasks] 128 | ; Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 129 | ; Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 130 | 131 | [Files] 132 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 133 | Source: "graphics\nodejs.bmp"; DestDir: "{tmp}"; DestName: "nodejs.bmp"; Flags: dontcopy 134 | Source: "graphics\nrhex24.bmp"; DestDir: "{tmp}"; DestName: "node-red.bmp"; Flags: dontcopy 135 | Source: "graphics\node-red-icon-small.bmp"; DestDir: "{tmp}"; DestName: "node-red-small.bmp"; Flags: dontcopy 136 | ; Source: "tools\vswhere.exe"; DestDir: "{tmp}"; DestName: "vswhere.exe" 137 | Source: "bat\setup_loop.bat"; DestDir: "{tmp}" 138 | Source: "icons\node-red-icons.ico"; DestDir: "{app}"; DestName: "red.ico" 139 | 140 | [Icons] 141 | ; Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 142 | ; Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 143 | ; Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 144 | ; Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 145 | ; Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon 146 | 147 | [Messages] 148 | english.WelcomeLabel1=Welcome to the%n[name] Setup Wizard 149 | english.WelcomeLabel2=This will install Node-RED on your computer.%n%nInitially we check the version of Node.js installed is %1 or greater. We will try to install node %2 if none is found. Optionally you can choose to install node %3.%n%nIf necessary we will then remove the old core of Node-RED, before then installing the latest version. You can also optionally specify the version required.%n%nWe will finally try to run 'npm rebuild' to refresh any extra nodes you have installed that may have a native binary component. While this normally works ok, you need to check that it succeeds for your combination of installed nodes.%n%nIt is recommended that you close all other applications before continuing. 150 | WizardReady=Final verification 151 | ReadyLabel1=We just ran a final verification of your installation setup. 152 | FinishedHeadingLabel=Completing the%n[name] Setup Wizard 153 | TranslatorNote={#ReadIni(INIFile, "installer", "description", "")}%nVersion {#ReadIni(INIFile, "installer", "version", "")}%nCopyright © {#ReadIni(INIFile, "installer", "copyright", "")}%nSide graphic by Marcus J. Davies%n{#VersionInfoURL} 154 | AboutSetupMenuItem=&About... 155 | AboutSetupTitle=About {#ReadIni(INIFile, "installer", "description", "")} 156 | 157 | [CustomMessages] 158 | english.MSG_FAILED_FINISHED1=Setup failed to install Node-RED on your computer: 159 | english.MSG_FAILED_FINISHED2=Sorry for this inconvenience! 160 | 161 | [Run] 162 | 163 | ; main.error is the global Error flag 164 | ; Each step checks if this is set, and if (it is) returns False@Check! 165 | 166 | ; Check if Node.js should be uninstalled. 167 | ; Run the uninstall; GUID queried from Registry. 168 | ; Verify that no Node.js is known. 169 | Filename: "msiexec"; \ 170 | Parameters: "/norestart /quiet /uninstall {code:GetInstalledNodeGUID}"; \ 171 | Flags: runascurrentuser; \ 172 | StatusMsg: "Uninstalling Node.js v{code:GetNodeVersionMain|current}..."; \ 173 | Check: RunCheck('rcsNodeUninstall', ''); \ 174 | BeforeInstall: SetupRunConfig; \ 175 | AfterInstall: Confirm('csNoNode', ''); 176 | 177 | 178 | ; The next two run - in general - the same command. 179 | ; #1 Installs in silent mode 180 | ; #2 Shows the Node installer & hides our installer 181 | ; Only one of these will be executed - as switched by the Check function. 182 | ; We do not install the components addressed by flags 'NodeEtwSupport' & 'NodePerfCtrSupport' 183 | 184 | ; Check that Node.js should be installed 185 | ; Run the installer 186 | ; Verify that Node.js is ready on the system. 187 | Filename: "msiexec"; \ 188 | Parameters: "/i {tmp}\node.msi TARGETDIR=""C:\Program Files\nodejs\"" ADDLOCAL=""DocumentationShortcuts,EnvironmentPathNode,EnvironmentPathNpmModules,npm,NodeRuntime,EnvironmentPath"" /qn"; \ 189 | Flags: runascurrentuser; \ 190 | StatusMsg: "Installing Node.js v{code:GetNodeVersionMain|selected}..."; \ 191 | Check: RunCheck('rcsNodeInstall', 'silent'); \ 192 | BeforeInstall: SetupRunConfig; \ 193 | AfterInstall: Confirm('csNode', ExpandConstant('{code:GetNodeVersionMain|selected}')); 194 | 195 | Filename: "msiexec"; \ 196 | Parameters: "/i {tmp}\node.msi TARGETDIR=""C:\Program Files\nodejs\"" ADDLOCAL=""DocumentationShortcuts,EnvironmentPathNode,EnvironmentPathNpmModules,npm,NodeRuntime,EnvironmentPath"" "; \ 197 | Flags: runascurrentuser hidewizard; \ 198 | StatusMsg: "Installing Node.js v{code:GetNodeVersionMain|selected}..."; \ 199 | Check: RunCheck('rcsNodeInstall', 'show'); \ 200 | BeforeInstall: SetupRunConfig; \ 201 | AfterInstall: Confirm('csNode', ExpandConstant('{code:GetNodeVersionMain|selected}')); 202 | 203 | Filename: "{tmp}\python_installer.exe"; \ 204 | Parameters: "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_launcher=0"; \ 205 | Flags: runascurrentuser; \ 206 | StatusMsg: "Installing Python..."; \ 207 | Check: RunCheck('rcsPython', 'no'); \ 208 | BeforeInstall: SetupRunConfig; \ 209 | AfterInstall: Confirm('csPython', ''); 210 | 211 | Filename: "{tmp}\VSBT_installer.exe"; \ 212 | Parameters: "--norestart --quiet --includeRecommended --add Microsoft.VisualStudio.Worlkload.VCTools"; \ 213 | Flags: runascurrentuser; \ 214 | StatusMsg: "Installing VisualStudio BuildTools..."; \ 215 | Check: RunCheck('rcsVSBuildTools', ''); \ 216 | BeforeInstall: SetupRunConfig; 217 | ; AfterInstall: Confirm('csVSBuildTools', ''); 218 | 219 | ; This doesn't work when running the VS installer w/ --quiet 220 | ; Parameters: """WINDOWTITLE eq VISUAL STUDIO Installer"" setup.exe"; \ 221 | ; As 'setup.exe' is quite ambigous, we've checked in PrepareInstallation that there 222 | ; wasn't another process with this IMAGENAME running! 223 | 224 | Filename: "{tmp}\setup_loop.bat"; \ 225 | Parameters: """IMAGENAME eq setup.exe"" setup.exe"; \ 226 | Flags: runasoriginaluser shellexec waituntilterminated runhidden; \ 227 | StatusMsg: "Waiting for Visual Studio Installer to finish its job..."; \ 228 | Check: RunCheck('rcsVSBuildTools', ''); \ 229 | BeforeInstall: SetupRunConfig; \ 230 | AfterInstall: Confirm('csVSBuildTools', ''); 231 | 232 | ; There's a breaking change introduced - intentionally - in NPM9 (Node.js v18+) 233 | ; that dis-allows non-standard config settings 234 | ; Thus the following doesn't work (anymore). 235 | ; Not sure about the consequences... 236 | ; Reference: https://github.com/npm/cli/issues/5852 237 | 238 | ; Filename: "npm"; \ 239 | ; Parameters: "config set python ""{code:GetPythonPath}"""; \ 240 | ; WorkingDir: "{code:GetNodeDataReg|path}"; \ 241 | ; Flags: runasoriginaluser shellexec waituntilterminated runhidden; \ 242 | ; StatusMsg: "Configuring Python path for Node.js..."; \ 243 | ; Check: RunCheck('rcsPythonConfig', ''); \ 244 | ; BeforeInstall: SetupRunConfig; \ 245 | ; AfterInstall: Confirm('csPythonConfig', ''); 246 | 247 | #define i 248 | 249 | #sub RemoveRED 250 | Filename: "npm"; \ 251 | Parameters: "uninstall {code:GetREDActionGlobal|{#i}} node-red"; \ 252 | WorkingDir: "{code:GetREDActionPath|{#i}}"; \ 253 | Flags: runascurrentuser shellexec waituntilterminated runhidden; \ 254 | StatusMsg: "Removing {code:GetREDCurrentMsg|{#i}}..."; \ 255 | Check: RunCheck('rcsREDRemove', '{#i}'); \ 256 | BeforeInstall: SetupRunConfig; \ 257 | AfterInstall: REDRemove('{#i}'); 258 | #endsub 259 | 260 | #sub InstallRED 261 | Filename: "npm"; \ 262 | Parameters: "install {code:GetREDActionGlobal|{#i}} node-red@{code:GetREDActionAction|{#i}}"; \ 263 | WorkingDir: "{code:GetREDActionPath|{#i}}"; \ 264 | Flags: runascurrentuser shellexec waituntilterminated runhidden; \ 265 | StatusMsg: "Installing {code:GetREDActionMsg|{#i}}..."; \ 266 | Check: RunCheck('rcsREDInstall', '{#i}'); \ 267 | BeforeInstall: REDPrepare('{#i}'); \ 268 | AfterInstall: REDFinalize('{#i}'); 269 | #endsub 270 | 271 | #sub NPMRebuild 272 | Filename: "npm"; \ 273 | Parameters: "rebuild"; \ 274 | WorkingDir: "{code:GetREDActionPath|{#i}}"; \ 275 | Flags: runascurrentuser shellexec waituntilterminated runhidden; \ 276 | StatusMsg: "Rebuilding packages for {code:GetREDActionMsg|{#i}}..."; \ 277 | Check: RunCheck('rcsREDInstall', '{#i}'); 278 | #endsub 279 | 280 | #for {i = 0; i < Int(REDProvisionCount, 5); i++} RemoveRED 281 | #for {i = 0; i < Int(REDProvisionCount, 5); i++} InstallRED 282 | #for {i = 0; i < Int(REDProvisionCount, 5); i++} NPMRebuild 283 | 284 | #undef i 285 | 286 | 287 | [Code] 288 | 289 | // All data managed by this installer is pushed into one huge record called 'main'. 290 | // 'main' is of type rInstallerData. 291 | // Dedicated records in 'main' carry information regarding Node.js, RED, Python, ... 292 | 293 | type 294 | 295 | rNodeVersion = record 296 | key: string; 297 | sha: string; 298 | latest: string; 299 | msi: string; 300 | default: boolean; 301 | file: string; 302 | end; 303 | // TNodeVersionList = array of rNodeVersion; 304 | 305 | rREDVersion = record 306 | version: string; 307 | tag: string; 308 | end; 309 | 310 | TREDVersionArray = array of rREDVersion; 311 | 312 | rNodeData = record 313 | majors: array of integer; // Supported major versions as read from the INI file 314 | versions: array of rNodeVersion; 315 | default: integer; 316 | selected: string; 317 | run_silent: boolean; 318 | install_tools: boolean; 319 | current: string; 320 | options: TStringList; 321 | end; 322 | 323 | rREDCalcData = record 324 | path: string; // if defined, ensure empty directory & create package.json 325 | port: integer; 326 | end; 327 | 328 | rREDRegData = record 329 | name: string; 330 | path: string; 331 | version: string; 332 | icon: string; 333 | port: integer; 334 | autostart: string; 335 | end; 336 | 337 | sREDInstallationKind = (rikGlobal, rikPath, rikNew, rikVoid); 338 | 339 | rREDInstallation = record 340 | key: string; 341 | id: TObject; 342 | kind: sREDInstallationKind; 343 | name: string; 344 | // _line: integer; 345 | path: string; 346 | version: string; 347 | port: integer; 348 | action: string; 349 | autostart: boolean; 350 | icon: boolean; 351 | // final_path: string; 352 | calc: rREDCalcData; 353 | // add additional properties here! 354 | registry: rREDRegData; 355 | end; 356 | 357 | sREDListItemKind = (rlikNone, rlikAction, rlikPath, rlikGlobal, rlikPort, rlikLabel, rlikIcon, rlikAutostart); 358 | 359 | rREDListItem = record 360 | kind: sREDListItemKind; 361 | action: string; 362 | link: TObject; 363 | end; 364 | 365 | rRedData = record 366 | versions: array of rREDVersion; 367 | selected: string; 368 | current: string; 369 | installs: array of rREDInstallation; 370 | items: array of rREDListItem; 371 | npm: boolean; 372 | error: boolean; 373 | run: array of integer; // index sequence of 'installs' to be installed. This eliminates the installs of 'rikVoid'. Populated @ UpdateReadyMemo. 374 | end; 375 | 376 | rPageID = record 377 | node_license: TOutputMsgMemoWizardPage; 378 | node_version: TInputOptionWizardPage; 379 | red_license: TOutputMsgMemoWizardPage; 380 | red_version: TInputOptionWizardPage; 381 | red_action: TInputOptionWizardPage; 382 | download: TDownloadWizardPage; 383 | end; 384 | 385 | rInstallationError = record 386 | status: boolean; 387 | msg: string; 388 | end; 389 | 390 | rPythonData = record 391 | version: string; // proposed version (via .ini) 392 | npm: boolean; // is npm aware of a python version? 393 | installed: string; // installed version? 394 | path: string; 395 | end; 396 | 397 | rVSData = record 398 | version: string; 399 | end; 400 | 401 | rInstallerData = record 402 | node: rNodeData; 403 | red: rRedData; 404 | 405 | // As described in the documenttion, HKLM is going be set to HKEY_LOCAL_MACHINE_64 when running on 64bit systems 406 | // *AND* "the system's processor architecture is included in the value of the ArchitecturesInstallIn64BitMode [Setup] section directive" 407 | // ArchitecturesInstallIn64BitMode [Setup] section yet is blank by default. 408 | // => HKLM - by default - always == HKEY_LOCAL_MACHINE 409 | // => set is as required: HKEY_LOCAL_MACHINE or HKEY_LOCAL_MACHINE_64 410 | HKLM: Integer; 411 | bit: String; 412 | 413 | pages: rPageID; 414 | error: rInstallationError; // Global Installation Error Status 415 | python: rPythonData; 416 | vs: rVSData; 417 | 418 | end; 419 | 420 | sImageType = (imgNODE, imgRED, imgNONE); 421 | 422 | var 423 | 424 | // Two additional controls 425 | REDLicenseAcceptedRadio: TRadioButton; 426 | REDLicenseNotAcceptedRadio: TRadioButton; 427 | 428 | // As described in the documenttion, HKLM is going be set to HKEY_LOCAL_MACHINE_64 when running on 64bit systems 429 | // *AND* "the system's processor architecture is included in the value of the ArchitecturesInstallIn64BitMode [Setup] section directive" 430 | // ArchitecturesInstallIn64BitMode [Setup] section yet is blank by default. 431 | // => HKLM - by default - always == HKEY_LOCAL_MACHINE 432 | // => set it as required: HKEY_LOCAL_MACHINE or HKEY_LOCAL_MACHINE_64 433 | 434 | // The list of all node versions offered to install 435 | // nodeVersionSelectionOptions: TStringList; 436 | // the index the user choose to install 437 | // nodeVersionSelectionOptionsSelected: Integer; 438 | 439 | // cbHideNodeInstaller: TNewCheckBox; 440 | // cbInstallWindowsTools: TNewCheckBox; 441 | 442 | // The Node-RED version the user selected for installation 443 | // redVersionSelected: String; 444 | 445 | // This record holds all relevant data 446 | main: rInstallerData; 447 | 448 | // testPage: TInputOptionWizardPage; 449 | // testPage2: TInputOptionWizardPage; 450 | 451 | // ***** 452 | // * Forward definition of some functions "exported" by other files 453 | 454 | // nodedetector.iss 455 | function detect_red_installations(var page: TOutputMarqueeProgressWizardPage): integer; forward; 456 | function red_list(working_dir: string): string; forward; 457 | 458 | // redactionpage.iss 459 | function MakeRedActionPage(page: TInputOptionWizardPage): Boolean; forward; 460 | 461 | // * 462 | // ***** 463 | 464 | 465 | // ***** 466 | // * Support functions 467 | 468 | procedure debug(message: string); 469 | begin 470 | Log('[NRI] ' + message); 471 | end; 472 | 473 | {procedure debugInt(int: integer); 474 | begin 475 | debug(IntToStr(int)); 476 | end;} 477 | 478 | // pastebin.com/STcQLfKR 479 | Function SplitString(const Value: string; Delimiter: string; Strings: TStrings): Boolean; 480 | var 481 | S: string; 482 | begin 483 | S := Value; 484 | if StringChangeEx(S, Delimiter, #13#10, True) > 0 then begin 485 | Strings.text := S; 486 | Result := True; 487 | Exit; 488 | end; 489 | Result := False; 490 | end; 491 | 492 | function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; 493 | begin 494 | if Progress = ProgressMax then begin 495 | Log(Format('Successfully downloaded file to {tmp}: %s', [FileName])); 496 | end; 497 | Result := True; 498 | end; 499 | 500 | function RightStr(S: string; C: Char; I: Integer): string; 501 | begin 502 | Result := StringOfChar(C, I - Length(S)) + S; 503 | end; 504 | 505 | function SortAsInt(List: TStringList; Index1, Index2: Integer): Integer; 506 | var 507 | i1, i2: Integer; 508 | begin 509 | i1 := StrToInt(List[Index1]); 510 | i2 := StrToInt(List[Index2]); 511 | if i1 < i2 then 512 | Result := -1 513 | else if i1 > i2 then Result := 1 514 | else 515 | Result := 0; 516 | end; 517 | 518 | 519 | // TStringList in Inno Setup flavour unfortunately does not support CustomSort 520 | // Thus we have to detour via an Integer array sort by QuickSort 521 | procedure QuickSort(var A: array of Integer; iLo, iHi: Integer) ; 522 | var 523 | Lo, Hi, Pivot, T: Integer; 524 | begin 525 | Lo := iLo; 526 | Hi := iHi; 527 | Pivot := A[(Lo + Hi) div 2]; 528 | repeat 529 | while A[Lo] < Pivot do Inc(Lo) ; 530 | while A[Hi] > Pivot do Dec(Hi) ; 531 | if Lo <= Hi then 532 | begin 533 | T := A[Lo]; 534 | A[Lo] := A[Hi]; 535 | A[Hi] := T; 536 | Inc(Lo) ; 537 | Dec(Hi) ; 538 | end; 539 | until Lo > Hi; 540 | if Hi > iLo then QuickSort(A, iLo, Hi) ; 541 | if Lo < iHi then QuickSort(A, Lo, iHi) ; 542 | end; 543 | 544 | 545 | function Max(A, B: Integer): Integer; 546 | begin 547 | if A > B then 548 | Result := A 549 | else 550 | Result := B; 551 | end; 552 | 553 | function RGB2TColor(const R, G, B: Byte): Integer; 554 | begin 555 | Result := (Integer(R) or (Integer(G) shl 8) or (Integer(B) shl 16)); 556 | end; 557 | 558 | 559 | procedure TColor2RGB(const Color: TColor; var R, G, B: Byte); 560 | begin 561 | // convert hexa-decimal values to RGB 562 | R := Color and $FF; 563 | G := (Color shr 8) and $FF; 564 | B := (Color shr 16) and $FF; 565 | end; 566 | 567 | 568 | function CompareVersions( checkVersion, compareVersion: string): integer; 569 | var 570 | checkV, compV: TStringList; 571 | i, v1, v2, l1, l2: integer; 572 | msg: string; 573 | 574 | begin 575 | 576 | checkV := TStringList.Create; 577 | compV := TStringList.Create; 578 | 579 | SplitString(checkVersion + '.', '.', checkV); 580 | SplitString(compareVersion + '.', '.', compV); 581 | 582 | l1 := checkV.Count; 583 | l2 := compV.Count; 584 | 585 | for i:= 0 to Max(l1, l2) - 1 do begin 586 | 587 | if l1 > i then begin 588 | v1 := StrToIntDef(checkV[i], -1); 589 | end else begin 590 | v1 := 0; 591 | end; 592 | 593 | if l2 > i then begin 594 | v2 := StrToIntDef(compV[i], -1); 595 | end else begin 596 | v2 := 0; 597 | end; 598 | 599 | // Only if both versions have a non-number part 600 | // compare those as strings 601 | // If only one has a non-number (e.g. '-beta.2') ammendment, 602 | // this version will be 'smaller' than the one without 603 | if ((v1 < 0) and (v2 < 0)) then begin 604 | Result := CompareStr(checkV[i], compV[i]); 605 | break; 606 | end; 607 | 608 | if v1 > v2 then begin 609 | Result := 1; 610 | break; 611 | end else if v1 < v2 then begin 612 | Result := -1; 613 | break; 614 | end; 615 | 616 | Result := 0; 617 | end; 618 | 619 | msg := '??'; 620 | case Result of 621 | -1: msg := '<'; 622 | 0: msg := '='; 623 | 1: msg := '>'; 624 | end; 625 | // debug(checkVersion + ' ' + msg + ' ' + compareVersion); 626 | 627 | end; 628 | 629 | function GetVersion(version: String; index: Integer): Integer; 630 | var 631 | vv: TStringList; 632 | begin 633 | Result := -1; 634 | vv := TStringList.Create; 635 | 636 | if SplitString(version, '.', vv) then begin 637 | if vv.Count > index then begin 638 | Result := StrToIntDef(vv[index], 0); 639 | end; 640 | end; 641 | end; 642 | 643 | function GetVersionMajor(version: String): Integer; 644 | begin 645 | Result := GetVersion(version, 0); 646 | end; 647 | 648 | function GetVersionMinor(version: String): Integer; 649 | begin 650 | Result := GetVersion(version, 1); 651 | end; 652 | 653 | function GetVersionPatch(version: String): Integer; 654 | begin 655 | Result := GetVersion(version, 2); 656 | end; 657 | 658 | procedure set_image(img: sImageType); 659 | var 660 | image: string; 661 | begin 662 | 663 | case img of 664 | imgNone: begin 665 | WizardForm.WizardSmallBitmapImage.Visible := False; 666 | Exit; 667 | end; 668 | imgNODE: image := 'nodejs.bmp'; 669 | imgRED: image := 'node-red.bmp'; 670 | else 671 | Exit; 672 | end; 673 | 674 | if not FileExists(ExpandConstant('{tmp}\' + image)) then ExtractTemporaryFile(image); 675 | WizardForm.WizardSmallBitmapImage.Bitmap.LoadFromFile(ExpandConstant('{tmp}\' + image)); 676 | WizardForm.WizardSmallBitmapImage.Visible := True; 677 | 678 | end; 679 | 680 | 681 | function BoolToStr(bool: boolean): string; 682 | begin 683 | if bool then 684 | Result:='true' 685 | else 686 | Result :='false'; 687 | end; 688 | 689 | function StrToBool(str: string): boolean; 690 | begin 691 | Result := str = 'true' 692 | Result := (str = 'yes') or Result; 693 | end; 694 | 695 | 696 | function isEmptyDir(dirName: String): Boolean; 697 | var 698 | FindRec: TFindRec; 699 | FileCount: Integer; 700 | begin 701 | Result := False; 702 | if FindFirst(dirName+'\*', FindRec) then begin 703 | try 704 | repeat 705 | if (FindRec.Name <> '.') and (FindRec.Name <> '..') then begin 706 | FileCount := 1; 707 | break; 708 | end; 709 | until not FindNext(FindRec); 710 | finally 711 | FindClose(FindRec); 712 | if FileCount = 0 then Result := True; 713 | end; 714 | end; 715 | end; 716 | 717 | 718 | // ***** 719 | // * Additional page to acknowledge Node-RED License 720 | // * https://stackoverflow.com/questions/34592002/how-to-create-two-licensefile-pages-in-inno-setup 721 | procedure CheckREDLicenseAccepted(Sender: TObject); 722 | begin 723 | // Update Next button when user (un)accepts the license 724 | WizardForm.NextButton.Enabled := REDLicenseAcceptedRadio.Checked; 725 | end; 726 | 727 | function CloneLicenseRadioButton(Source: TRadioButton): TRadioButton; 728 | begin 729 | Result := TRadioButton.Create(WizardForm); 730 | Result.Parent := main.pages.red_license.Surface; 731 | Result.Caption := Source.Caption; 732 | Result.Left := Source.Left; 733 | Result.Top := Source.Top; 734 | Result.Width := Source.Width; 735 | Result.Height := Source.Height; 736 | Result.OnClick := @CheckREDLicenseAccepted; 737 | end; 738 | 739 | function CreateREDLicensePage(after: Integer): TOutputMsgMemoWizardPage; 740 | 741 | begin 742 | 743 | Result := 744 | CreateOutputMsgMemoPage( 745 | after, 'Node-RED ' + SetupMessage(msgWizardLicense), 746 | SetupMessage(msgLicenseLabel), 747 | SetupMessage(msgLicenseLabel3), ''); 748 | 749 | // Shrink memo box to make space for radio buttons 750 | Result.RichEditViewer.Height := WizardForm.LicenseMemo.Height; 751 | 752 | // Clone accept/do not accept radio buttons for the second license 753 | REDLicenseAcceptedRadio := 754 | CloneLicenseRadioButton(WizardForm.LicenseAcceptedRadio); 755 | REDLicenseNotAcceptedRadio := 756 | CloneLicenseRadioButton(WizardForm.LicenseNotAcceptedRadio); 757 | 758 | // Initially not accepted 759 | REDLicenseNotAcceptedRadio.Checked := True; 760 | 761 | end; 762 | 763 | // * 764 | // ***** 765 | 766 | // ***** 767 | // ** RunCMD 768 | // ** Execute a command with Windows cmd.exe 769 | 770 | function RunCMD(Command, WorkingDir: string; var ResultArray: TArrayOfString): Boolean; 771 | 772 | var 773 | bat, file, res: string; 774 | rc, i: integer; 775 | 776 | p1, p2: string; 777 | msg: string; 778 | begin 779 | 780 | msg := '$> ' + Command; 781 | if Length(WorkingDir) > 0 then msg := msg + ' @ ' + WorkingDir; 782 | debug(msg); 783 | 784 | Result := False; 785 | 786 | // %PATH% is incomplete in cmd when we just use Exec, 787 | // thus calling 'node' or 'npm' tells us "not found". 788 | // => Read the two registry keys thet both hold the PATH data 789 | // => to combine them and set PATH explicitely - for this cmd session! 790 | // (based on an idea I got from https://stackoverflow.com/a/32420542) 791 | p1:=''; 792 | RegQueryStringValue(main.HKLM, 'System\CurrentControlSet\Control\Session Manager\Environment', 'Path', p1); 793 | p2:=''; 794 | RegQueryStringValue(HKCU, 'Environment', 'Path', p2); 795 | 796 | res:= ExpandConstant('{tmp}\cmd_result.txt'); 797 | 798 | bat := ''; 799 | if (Length(p1) + Length(p2)) > 0 then 800 | bat := 'PATH=' + p1 + ';' + p2 + ';' + #13#10; 801 | bat := bat + command + ' > "' + res + '" 2>&1'; 802 | 803 | file := ExpandConstant('{tmp}\run_cmd.bat'); 804 | 805 | if SaveStringToFile(file, bat, False) then begin 806 | if Exec(file, '', WorkingDir, SW_HIDE, ewWaitUntilTerminated, rc) then begin 807 | if LoadStringsFromFile(res, ResultArray) then begin 808 | for i:=0 to GetArrayLength(ResultArray) -1 do begin 809 | debug('>> ' + ResultArray[i]); 810 | end; 811 | Result:=True; 812 | end; 813 | end else begin 814 | debug('>> ' + SysErrorMessage(rc)); 815 | end; 816 | end; 817 | end; 818 | 819 | 820 | 821 | // ***** 822 | // ** NodeVersionSelectionPage 823 | // ** 824 | 825 | var 826 | _nodeVersion_cbHideInstaller: TNewCheckBox; 827 | _nodeVersion_cbInstallWindowsTools: TNewCheckBox; 828 | 829 | procedure _nodeVersion_OnClickInstallBackground(Sender: TObject); 830 | var 831 | status: boolean; 832 | begin 833 | status := _nodeVersion_cbHideInstaller.Checked; 834 | _nodeVersion_cbInstallWindowsTools.Visible := status; 835 | main.node.run_silent := status 836 | end; 837 | 838 | procedure _nodeVersion_OnClickNodeVersion(Sender: TObject); 839 | var 840 | index: Integer; 841 | nv: String; 842 | 843 | begin 844 | 845 | index := main.pages.node_version.SelectedValueIndex; 846 | 847 | if index < main.node.options.Count then begin 848 | 849 | nv := main.node.options[index]; 850 | main.node.selected := nv; 851 | 852 | _nodeVersion_cbHideInstaller.Visible := (Length(nv) > 0); 853 | _nodeVersion_cbInstallWindowsTools.Visible := ((Length(nv) > 0) and _nodeVersion_cbHideInstaller.Checked); 854 | end; 855 | 856 | end; 857 | 858 | function PrepareNodeVersionSelectionPage(): Boolean; 859 | var 860 | 861 | sFlag: String; 862 | i, ii: Integer; 863 | 864 | _nvp: TInputOptionWizardPage; 865 | 866 | _possible: array of rNodeVersion; // the Node.js versions we know of 867 | _options: TStringList; // the Node.js versions we offer for download; 868 | // this may include '' for "Keep the current..." 869 | _current: string; 870 | 871 | begin 872 | 873 | Result := False; 874 | 875 | // debug('PrepareNodeVersionSelectionPage'); 876 | 877 | // Page to select a Node.js version 878 | _nvp := main.pages.node_version; 879 | if _nvp = nil then Exit; 880 | 881 | _nvp.SubCaptionLabel.Font.Style := [fsBold]; 882 | _nvp.CheckListBox.OnClickCheck := @_nodeVersion_OnClickNodeVersion; 883 | 884 | // Additional checkboxes: 885 | _nodeVersion_cbHideInstaller := TNewCheckBox.Create(_nvp); 886 | with _nodeVersion_cbHideInstaller do begin 887 | Parent := _nvp.Surface; 888 | Top := _nvp.CheckListBox.Top + _nvp.CheckListBox.Height + ScaleY(8); 889 | Height := ScaleY(_nodeVersion_cbHideInstaller.Height); 890 | Left := _nvp.CheckListBox.Left; 891 | Width := _nvp.CheckListBox.Width; 892 | Caption := 'Run Node.js installer in the background.'; 893 | Checked := True; 894 | OnClick := @_nodeVersion_OnClickInstallBackground; 895 | end; 896 | main.node.run_silent := True; 897 | 898 | _nodeVersion_cbInstallWindowsTools := TNewCheckBox.Create(_nvp); 899 | with _nodeVersion_cbInstallWindowsTools do begin 900 | Parent := _nvp.Surface; 901 | Top := _nodeVersion_cbHideInstaller.Top + _nodeVersion_cbHideInstaller.Height + ScaleY(8); 902 | Height := ScaleY(_nodeVersion_cbInstallWindowsTools.Height); 903 | Left := _nodeVersion_cbHideInstaller.Left; 904 | Width := _nodeVersion_cbHideInstaller.Width; 905 | Caption := 'Install Windows Tools for Native Node.js Modules - if necessary!'; 906 | Checked := True; 907 | Enabled := True; 908 | end; 909 | main.node.install_tools := True; 910 | 911 | // Read the current node.js version from the registry 912 | _current := ''; 913 | if RegKeyExists(main.HKLM, 'SOFTWARE\Node.js') then begin 914 | if RegQueryStringValue(main.HKLM, 'SOFTWARE\Node.js', 'Version', _current) = True then begin 915 | _nvp.SubCaptionLabel.Caption := 'Currently installed: Node.js ' + _current; 916 | debug('According Registry, Node.js v' + _current + ' is installed.'); 917 | end; 918 | end; 919 | 920 | main.node.current := _current; 921 | 922 | // This list holds the versions numbers we offer for installation 923 | _options := TStringList.Create; 924 | 925 | _possible := main.node.versions; 926 | 927 | if _current = '' then begin 928 | _nvp.SubCaptionLabel.Caption := 'Currently there''s no Node.js version installed!'; 929 | end else begin 930 | 931 | // Check if current version > Min version 932 | if CompareVersions(_current, _possible[0].key) > 0 then begin 933 | _nvp.Add('Keep CURRENT Node.js ' + _current); 934 | _options.Add(''); 935 | 936 | // Set initial value to "Keep" 937 | _nvp.Values[0] := True; 938 | end; 939 | end; 940 | 941 | for i := 0 to GetArrayLength(_possible) - 1 do begin 942 | 943 | _options.Add(_possible[i].latest); 944 | 945 | ii := CompareVersions(_current, _possible[i].latest); 946 | if ii > 0 then begin 947 | _nvp.Add('Change to v' + _possible[i].key + ' LTS Node.js version >> ' + _possible[i].latest + ' | NOT RECOMMENDED'); 948 | continue; 949 | end else if ii = 0 then begin 950 | _nvp.Add('Re-Install v' + _possible[i].key + ' LTS Node.js version: ' + _possible[i].latest); 951 | continue; 952 | end; 953 | 954 | sFlag := ''; 955 | if GetVersionMajor(_current) < GetVersionMajor(_possible[i].latest) then begin 956 | if GetVersionMajor(_possible[i].latest) = main.node.default then begin 957 | // debugInt(main.node.default); 958 | sFlag := ' | RECOMMENDED '; 959 | end; 960 | end; 961 | 962 | _nvp.Add('Update to latest v' + _possible[i].key + ' LTS Node.js version >> ' + _possible[i].latest + sFlag); 963 | 964 | if Length(sFlag) > 0 then begin 965 | // if no version installed: Default to recommended version 966 | if _current = '' then 967 | _nvp.Values[_options.Count - 1] := True; 968 | end; 969 | end; 970 | 971 | main.node.options := _options; 972 | _nodeVersion_OnClickNodeVersion(nil); 973 | 974 | Result := True; 975 | 976 | end; 977 | 978 | // ** END 979 | // ** NodeVersionSelectionPage 980 | // ***** 981 | 982 | // ***** 983 | // ** Data Preparation Page 984 | // ** (which isn't a true interactive page, but a Download & a Progress page inserted after wpWelcome) 985 | 986 | function _redversion_insert_version(version, tag: String; red_versions: TREDVersionArray): TREDVersionArray; 987 | var 988 | 989 | buffer: TREDVersionArray; 990 | ii, ibuffer, rvLength, cv: Integer; 991 | 992 | begin 993 | 994 | // Do not accept versions that are lower than #REDMinVersion 995 | // debug('cv: ' + version + ' / {#REDMinVersion}'); 996 | 997 | cv := CompareVersions(version, '{#REDMinVersion}'); 998 | 999 | // debug('cv: ' + version + ' / {#REDMinVersion}' + ' = ' + IntToStr(cv)); 1000 | 1001 | if cv < 0 then begin 1002 | Result := red_versions; 1003 | Exit; 1004 | end; 1005 | 1006 | if Length(version) < 1 then begin 1007 | Result := red_versions; 1008 | Exit; 1009 | end; 1010 | 1011 | rvLength := GetArrayLength(red_versions); 1012 | SetArrayLength(buffer, rvLength + 1); 1013 | 1014 | if rvLength < 1 then begin 1015 | buffer[0].version := version; 1016 | buffer[0].tag := tag; 1017 | Result := buffer; 1018 | Exit; 1019 | end; 1020 | 1021 | ibuffer:= 0; 1022 | for ii:= 0 to rvLength - 1 do begin 1023 | // debug(version + ' ? ' + red_versions[ii].version); 1024 | 1025 | if Length(version) > 0 then begin 1026 | 1027 | cv := CompareVersions(version, red_versions[ii].version); 1028 | 1029 | if cv = 0 then begin 1030 | // if the version is already present, 1031 | // don't overwrite 1032 | version := ''; 1033 | 1034 | end else if cv < 0 then begin 1035 | buffer[ibuffer].version := version; 1036 | buffer[ibuffer].tag := tag; 1037 | ibuffer:=ibuffer+1; 1038 | version := '' 1039 | 1040 | end; 1041 | end; 1042 | 1043 | buffer[ibuffer].version := red_versions[ii].version; 1044 | buffer[ibuffer].tag := red_versions[ii].tag; 1045 | ibuffer:=ibuffer+1; 1046 | 1047 | end; 1048 | 1049 | if Length(version) > 0 then begin 1050 | buffer[ibuffer].version := version; 1051 | buffer[ibuffer].tag := tag; 1052 | ibuffer:=ibuffer+1; 1053 | end; 1054 | 1055 | // truncate in case version was already present! 1056 | SetArrayLength(buffer, ibuffer); 1057 | Result := buffer; 1058 | 1059 | end; 1060 | 1061 | 1062 | function GetPythonPath(param: string): string; forward; 1063 | function GetVSBuildToolsVersion(): string; forward; 1064 | function GetNPMPythonConfig(): string; forward; 1065 | 1066 | function RunDataPrepPage(): Boolean; 1067 | 1068 | #define ProgressMax 10; 1069 | 1070 | var 1071 | _ppage: TOutputMarqueeProgressWizardPage; 1072 | 1073 | i, ii, having: integer; 1074 | nvv: array of integer; 1075 | 1076 | nv, tmp_file_name, line: string; 1077 | check: boolean; 1078 | 1079 | parts, version: TStringList; 1080 | _sha, _latest: string; 1081 | 1082 | file: array of string; 1083 | 1084 | majors: array of integer; 1085 | 1086 | // Node-RED 1087 | res: array of string; 1088 | rvv, splitres: TStringList; 1089 | tag, rv: string; 1090 | red_versions: TREDVersionArray; 1091 | // current_version: string; 1092 | 1093 | msg: string; 1094 | 1095 | begin 1096 | 1097 | _ppage := CreateOutputMarqueeProgressPage('Processing additional data...', 'We collect and analyze data to prepare the installation.'); 1098 | 1099 | try 1100 | // _ppage.SetProgress(_iProgress, {#ProgressMax}); 1101 | _ppage.Animate(); 1102 | _ppage.Show; 1103 | 1104 | // Get the Node versions, the Node msi download links & the SHA 1105 | // This serves as well to verify that an internet connection is present. 1106 | 1107 | nvv := main.node.majors; 1108 | 1109 | debug('Trying to fetch version data of latest Node.js releases:'); 1110 | 1111 | for i := 0 to GetArrayLength(nvv) - 1 do begin 1112 | 1113 | nv := IntToStr(nvv[i]); 1114 | // debug(nv); 1115 | // Download the SHA file 1116 | tmp_file_name := ExpandConstant('{tmp}') + '\node' + nv + '.sha'; 1117 | // check := idpDownloadFile('https://nodejs.org/dist/latest-v' + nv + '.x/SHASUMS256.txt', tmp_file_name); 1118 | 1119 | if not FileExists(tmp_file_name) then continue; 1120 | _ppage.SetText('Extracting data for Node.js v' + nv, ''); 1121 | 1122 | // Reset the data we expect to find 1123 | // sha := ''; 1124 | // latest := ''; 1125 | // msi := ''; 1126 | 1127 | _ppage.Animate(); 1128 | LoadStringsFromFile(tmp_file_name, file); 1129 | 1130 | for ii := 0 to GetArrayLength(file) - 1 do begin 1131 | 1132 | _ppage.Animate(); 1133 | 1134 | // Example line in file: 1135 | // 76102997f9084e1faa544693ad1daeef68394d46ae7e363ad8df1fa86896133f node-v12.22.12-x64.msi 1136 | line := file[ii]; 1137 | 1138 | // find the line with '-x64.msi' or '-x86.msi' at the end 1139 | if StringChangeEx(line, '-' + main.bit + '.msi', '', True) > 0 then begin 1140 | 1141 | parts := TStringList.Create; 1142 | 1143 | // line - when found - looks now like this: 1144 | // 76102997f9084e1faa544693ad1daeef68394d46ae7e363ad8df1fa86896133f node-v12.22.12 1145 | if SplitString(line, ' ', parts) = True then begin 1146 | 1147 | if parts.Count = 2 then begin 1148 | 1149 | _sha := parts[0]; 1150 | line := parts[1]; 1151 | 1152 | // assure that the rest starts with 'node-v' 1153 | if StringChangeEx(line, 'node-v', '', True) > 0 then begin 1154 | 1155 | version := TStringList.Create; 1156 | 1157 | // line is now like 12.22.12 1158 | // final check: 1159 | // => we should have now a version of three elements 1160 | // => at index 0 shall be the version number we started with 1161 | if SplitString(line, '.', version) = True then begin 1162 | if ((version.Count = 3) and (version[0] = nv)) then 1163 | _latest := line; 1164 | end; 1165 | end; 1166 | end; 1167 | end; 1168 | end; 1169 | end; 1170 | 1171 | if ((Length(_sha) > 0) and (Length(_latest) > 0)) then begin 1172 | 1173 | having := GetArrayLength(main.node.versions); 1174 | SetArrayLength(main.node.versions, having + 1); 1175 | 1176 | with main.node.versions[having] do begin 1177 | key := nv; 1178 | latest := _latest; 1179 | sha := _sha; 1180 | msi := 'node-v' + latest + '-' + main.bit + '.msi'; 1181 | file := tmp_file_name; 1182 | end; 1183 | 1184 | debug('Latest Node.js @ v' + nv + ': ' + main.node.versions[having].latest); 1185 | 1186 | // re-construct the - now confirmed - 'majors' array 1187 | having := GetArrayLength(majors); 1188 | SetArrayLength(majors, having + 1); 1189 | majors[having] := nvv[i]; 1190 | 1191 | end; 1192 | end; 1193 | 1194 | // By now, this data is confirmed! 1195 | main.node.majors := majors; 1196 | 1197 | having := GetArrayLength(main.node.versions); 1198 | if having > 0 then begin 1199 | Result := True; 1200 | 1201 | end else begin 1202 | 1203 | // Error and out! 1204 | TaskDialogMsgBox('Error', 1205 | 'Failed to get Node.js download data.' + #13#10 + 'Please ensure that you''re connected to the Internet.', 1206 | mbCriticalError, 1207 | MB_OK, [], 0); 1208 | 1209 | Result := False; 1210 | end; 1211 | 1212 | if not Result then Exit; 1213 | 1214 | // Node-RED version processing 1215 | // Att: The calls to npm are really slow! 1216 | 1217 | rvv := TStringList.Create; 1218 | 1219 | msg := 'Requesting Node-RED version info from npm'; 1220 | _ppage.SetText(msg + '...', ''); 1221 | debug(msg + ':'); 1222 | 1223 | RunCMD('npm dist-tag ls node-red', '', res); 1224 | 1225 | // debug('npm dist-tag: ' + IntToStr(GetArrayLength(res))); 1226 | 1227 | for i:= 0 to GetArrayLength(res) - 1 do begin 1228 | 1229 | _ppage.Animate(); 1230 | 1231 | line := res[i]; 1232 | 1233 | // v1-maintenance: 1.3.7 1234 | if SplitString(line, ': ', rvv) then begin 1235 | tag:= Trim(rvv[0]); 1236 | rv:= Trim(rvv[1]); 1237 | end else 1238 | continue; 1239 | 1240 | // debug(rv + ' | ' + tag); 1241 | red_versions := _redversion_insert_version(rv, tag, red_versions); 1242 | 1243 | end; 1244 | 1245 | // debug('red_versions: ' + IntToStr(GetArrayLength(red_versions))); 1246 | 1247 | if GetArrayLength(red_versions) > 0 then begin 1248 | main.red.npm := True; 1249 | end else begin 1250 | 1251 | // This either means Node-RED is EOL ... :( 1252 | // ... or npm is not installed! 1253 | // In this case we try to get at least the latest version info by querying the GitHub 'release/latest' page (already downloaded!) 1254 | 1255 | tmp_file_name := ExpandConstant('{tmp}\') + '{#REDLatestTmpFileName}'; 1256 | 1257 | // debug(tmp_file_name); 1258 | 1259 | if FileExists(tmp_file_name) then begin 1260 | 1261 | _ppage.Animate(); 1262 | _ppage.SetText('Trying to extract the LATEST Node-RED version tag...', ''); 1263 | 1264 | LoadStringsFromFile(tmp_file_name, file); 1265 | check:= False; 1266 | 1267 | for i := 0 to GetArrayLength(file) - 1 do begin 1268 | 1269 | _ppage.Animate(); 1270 | 1271 | line := file[i]; 1272 | // debug(line); 1273 | 1274 | parts := TStringList.Create; 1275 | 1276 | // We're looking for this fragment: 1277 | // ... ... 1278 | // This is VERY fragile ... !! 1279 | if SplitString(line, '', splitres) then begin 1289 | line := splitres[0]; 1290 | // debug(line); 1291 | red_versions := _redversion_insert_version(line, 'latest', red_versions); 1292 | check:=True; 1293 | break; 1294 | end; 1295 | end; 1296 | end; 1297 | 1298 | end; 1299 | 1300 | if check then break; 1301 | 1302 | end; 1303 | 1304 | end; 1305 | end; 1306 | 1307 | // Now merge the "Additional Versions" as defined in setup.ini 1308 | // debug('{#REDAddVersions}'); 1309 | 1310 | // Att: There's - with intention! - an addition ',' appended to REDAddVersions 1311 | // SplitString only returns true, if the delimieter was found at least once! 1312 | if SplitString('{#REDAddVersions},', ',', rvv) = True then begin 1313 | for i := 0 to rvv.Count - 1 do begin 1314 | rv := rvv.Strings[i]; 1315 | StringChangeEx(rv, ' ', '', True); 1316 | // debug(rv); 1317 | red_versions := _redversion_insert_version(rv, '', red_versions); 1318 | end; 1319 | end; 1320 | 1321 | for i:= 0 to GetArrayLength(red_versions) - 1 do begin 1322 | debug(red_versions[i].version + ' / ' + red_versions[i].tag); 1323 | end; 1324 | 1325 | detect_red_installations(_ppage); 1326 | main.red.versions := red_versions; 1327 | 1328 | _ppage.SetText('Preparing the Node.js version selection page...', ''); 1329 | Result := PrepareNodeVersionSelectionPage() and Result; 1330 | 1331 | // _ppage.SetText('Preparing the Node-RED version selection page...', ''); 1332 | // Result := PrepareREDVersionSelectionPage() and Result; 1333 | 1334 | _ppage.SetText('Preparing the Node-RED installation setup page...', ''); 1335 | Result := MakeRedActionPage(main.pages.red_action) and Result; 1336 | 1337 | _ppage.SetText('Verifying the Python environment...', ''); 1338 | main.python.version := '{#py}'; 1339 | 1340 | // Check if npm is already configured correctly 1341 | if Length(main.node.current) > 0 then begin 1342 | main.python.path := GetNPMPythonConfig(); 1343 | if Length(main.python.path) > 0 then begin 1344 | main.python.npm := True; 1345 | end; 1346 | end; 1347 | 1348 | // Check if we have at least a python installation 1349 | if Length(main.python.path) < 1 then 1350 | main.python.path := GetPythonPath(''); 1351 | 1352 | _ppage.SetText('Verifying the VisualStudio BuildTools setup...', ''); 1353 | // if not FileExists(ExpandConstant('{tmp}\vswhere.exe')) then ExtractTemporaryFile('vswhere.exe'); 1354 | 1355 | // Check if vswhere is able to find an installation 1356 | main.vs.version := GetVSBuildToolsVersion(); 1357 | 1358 | finally 1359 | _ppage.Hide(); 1360 | end; 1361 | 1362 | end; 1363 | 1364 | 1365 | // ** END 1366 | // ** Data Prepartion Page 1367 | // ***** 1368 | 1369 | // ***** 1370 | // ** Event Implementations 1371 | // ** 1372 | 1373 | function InitializeSetup(): boolean; 1374 | 1375 | var 1376 | i, ii, having: integer; 1377 | node_versions: TStringList; 1378 | nv: string; 1379 | nvv: array of integer; 1380 | default_version: string; 1381 | 1382 | json, file, line, part: string; 1383 | nodejsjson, parts, vv: TArrayOfString; 1384 | version, v: integer; 1385 | 1386 | begin 1387 | 1388 | // check the bit status of this platform 1389 | if IsWin64() = True then begin 1390 | debug('We are going to install the 64bit version of Node.js.'); 1391 | main.bit := 'x64'; 1392 | main.HKLM := HKEY_LOCAL_MACHINE_64; 1393 | end else begin 1394 | main.bit := 'x86'; 1395 | main.HKLM := HKEY_LOCAL_MACHINE; 1396 | end; 1397 | 1398 | default_version := '{#Trim(NodeVersionRecommended)}'; 1399 | main.node.default := -1; 1400 | 1401 | // Try to collect the highest available version number of node.js 1402 | // This serves as well to verify that an internet connection is present. 1403 | 1404 | version := 0; 1405 | json := '{#nodeVersionIndex}'; 1406 | 1407 | Result := True; 1408 | 1409 | try 1410 | DownloadTemporaryFile(json, 'nodejs.json', '', nil); 1411 | except 1412 | debug('Failed to download Node.js index file from "' + json + '":'); 1413 | debug(AddPeriod(GetExceptionMessage)); 1414 | 1415 | // Error and out! 1416 | TaskDialogMsgBox('Error', 1417 | 'Failed to get Node.js index file.' + #13#10 + 'Please ensure that you''re connected to the Internet.', 1418 | mbCriticalError, 1419 | MB_OK, [], 0); 1420 | 1421 | Result := False; 1422 | 1423 | end; 1424 | 1425 | if not Result then Exit; 1426 | 1427 | file := ExpandConstant('{tmp}\nodejs.json'); 1428 | if FileExists(file) then begin 1429 | 1430 | if LoadStringsFromFile(file, nodejsjson) then begin 1431 | 1432 | for i := 0 to GetArrayLength(nodejsjson) - 1 do begin 1433 | 1434 | // Example line in file: 1435 | // {"version":"v23.7.0","date":"2025-01-30", ... ,"security":false}, 1436 | 1437 | if Pos('"version":"', nodejsjson[i]) > 0 then begin 1438 | 1439 | // #1: eliminate "{}," & split remaining line into elements 1440 | line := Copy(nodejsjson[i], 2, Length(nodejsjson[i]) - 3); 1441 | parts := StringSplitEx(line, [','], '"', stAll); 1442 | 1443 | for ii := 0 to GetArrayLength(parts) - 1 do begin 1444 | 1445 | part := parts[ii]; 1446 | // #2: part of interest looks like this: 1447 | // "version":"v23.7.0" 1448 | if StringChangeEx(part, '"version":"v', '', True) > 0 then begin 1449 | 1450 | // #3: Remaining string looks like '23.7.0"' 1451 | // Split this & get first index 1452 | vv := StringSplitEx(part, ['.'], '"', stAll); 1453 | 1454 | // get the max version number 1455 | version := StrToIntDef(vv[0], 0); 1456 | 1457 | end; 1458 | 1459 | if version > 0 then break; 1460 | 1461 | end; 1462 | end; 1463 | 1464 | if version > 0 then break; 1465 | 1466 | end; 1467 | end; 1468 | 1469 | // Inno Style round ;) 1470 | version := (version / 2) * 2; 1471 | debug('Highest available LTS version seems to be v' + IntToStr(version) + '.'); 1472 | 1473 | end; 1474 | 1475 | if version > 0 then begin 1476 | 1477 | // the safe way! 1478 | for i:= 0 to 2 do begin 1479 | v := version - i*2; 1480 | if v > 0 then begin 1481 | having := GetArrayLength(nvv); 1482 | SetArrayLength(nvv, having + 1); 1483 | nvv[having] := v; 1484 | 1485 | if default_version = IntToStr(v) then 1486 | main.node.default := i; 1487 | 1488 | end; 1489 | end; 1490 | 1491 | end else begin 1492 | 1493 | // FALLBACK - that must be maintained!! 1494 | // Read the major Node.js versions - offered to install - from the INI 1495 | node_versions := TStringList.Create; 1496 | node_versions.Duplicates := dupIgnore 1497 | 1498 | // Verify that it's just a number (nothing else) 1499 | // Att: Additional ',' appended to #nodeVersions - to satisfy SplitString! 1500 | if SplitString('{#nodeVersions},', ',', node_versions) = True then begin 1501 | for i := 0 to node_versions.Count - 1 do begin 1502 | nv := node_versions.Strings[i]; 1503 | StringChangeEx(nv, ' ', '', True); 1504 | try 1505 | ii := StrToIntDef(nv, -1); 1506 | if ii > 0 then begin 1507 | having := GetArrayLength(nvv); 1508 | SetArrayLength(nvv, having + 1); 1509 | nvv[having] := ii; 1510 | 1511 | if nv = default_version then 1512 | main.node.default := ii; 1513 | 1514 | end; 1515 | except 1516 | // That's fine as well... 1517 | end; 1518 | end; 1519 | end; 1520 | 1521 | end; 1522 | 1523 | // Bail out - bail out! 1524 | if GetArrayLength(nvv) < 1 then begin 1525 | MsgBox('Looks like no information was given which' + #13#10 + 'versions of Node.js I should offer for installation.' + #13#10 + #13#10 + 'Stopping here...', 1526 | mbCriticalError, 1527 | MB_OK); 1528 | Result:=False; 1529 | Exit; 1530 | end; 1531 | 1532 | // check if default version is in list of to-be-offered versions. 1533 | // if not, add it! 1534 | if (main.node.default < 0) then begin 1535 | 1536 | v := StrToIntDef(default_version, 0); 1537 | if v > 0 then begin 1538 | having := GetArrayLength(nvv); 1539 | SetArrayLength(nvv, having + 1); 1540 | nvv[having] := v; 1541 | main.node.default := v; 1542 | 1543 | end else begin 1544 | 1545 | // If not a valid number, let default version become min version 1546 | QuickSort(nvv, Low(nvv), High(nvv)); 1547 | main.node.default := nvv[0] 1548 | 1549 | end; 1550 | 1551 | end else begin 1552 | // index to value 1553 | main.node.default := nvv[main.node.default]; 1554 | end; 1555 | 1556 | // Now sort it ascending 1557 | QuickSort(nvv, Low(nvv), High(nvv)); 1558 | 1559 | // Keep this for later use 1560 | main.node.majors := nvv; 1561 | 1562 | end; 1563 | 1564 | 1565 | procedure InitializeWizard(); 1566 | var 1567 | 1568 | i, having: Integer; 1569 | msg: String; 1570 | wf: TWizardForm; 1571 | 1572 | begin 1573 | 1574 | // wpWelcome 1575 | having := GetArrayLength(main.node.majors); 1576 | if having > 0 then begin 1577 | 1578 | // Compose the message of the supported node.js versions 1579 | msg := '' 1580 | if having > 1 then begin 1581 | for i := 0 to having - 2 do begin 1582 | if Length(msg) > 0 then begin 1583 | msg := msg + ', '; 1584 | end; 1585 | msg := msg + IntToStr(main.node.majors[i]); 1586 | end; 1587 | end; 1588 | 1589 | if Length(msg) > 0 then begin 1590 | msg := msg + ' or '; 1591 | end; 1592 | 1593 | msg := msg + IntToStr(main.node.majors[having - 1]) + ' LTS'; 1594 | 1595 | wf := GetWizardForm(); 1596 | 1597 | // https://github.com/jrsoftware/issrc/blob/main/Projects/MsgIDs.pas 1598 | wf.WelcomeLabel2.Caption := FMTMessage(SetupMessage(msgWelcomeLabel2), [IntToStr(main.node.majors[0]), IntToStr(main.node.default), msg]); 1599 | 1600 | end; 1601 | 1602 | end; 1603 | 1604 | function NextButtonClick(CurPageID: Integer): Boolean; 1605 | 1606 | var 1607 | // bit, nv: String; 1608 | check: Boolean; 1609 | i: Integer; 1610 | 1611 | // node_versions: TStringList; 1612 | // node_license_path, red_license_path: String; 1613 | // lbl: TNewStaticText; 1614 | 1615 | having: integer; 1616 | file, nv, msg: string; 1617 | 1618 | _dp: TDownloadWizardPage; 1619 | 1620 | red_image: string; 1621 | 1622 | begin 1623 | 1624 | Result := True; 1625 | red_image := 'node-red.bmp'; 1626 | 1627 | if CurPageID = wpWelcome then begin 1628 | end; 1629 | 1630 | if CurPageID = wpWelcome then begin 1631 | 1632 | check := FileExists(ExpandConstant('{tmp}\{#NodeLicenseTmpFileName}')); 1633 | check := FileExists(ExpandConstant('{tmp}\{#REDLicenseTmpFileName}')) and check; 1634 | check := FileExists(ExpandConstant('{tmp}\vswhere.exe')) and check; 1635 | 1636 | having := GetArrayLength(main.node.versions); 1637 | if having > 0 then begin 1638 | for i:=0 to having -1 do begin 1639 | file := main.node.versions[i].file; 1640 | if Length(file) > 0 then 1641 | check := FileExists(file) and check; 1642 | end; 1643 | end else 1644 | check := False; 1645 | 1646 | if not check then begin 1647 | 1648 | _dp := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress); 1649 | 1650 | set_image(imgRED); 1651 | 1652 | // Download Licenses 1653 | _dp.Clear; 1654 | 1655 | _dp.Add('{#NodeLicenseURL}', '{#NodeLicenseTmpFileName}', ''); 1656 | _dp.Add('{#REDLicenseURL}', '{#REDLicenseTmpFileName}', ''); 1657 | _dp.Add('{#VSWhereURL}', 'vswhere.exe', ''); 1658 | 1659 | // to run the backup option if npm is not present 1660 | _dp.Add('https://github.com/node-red/node-red/releases/latest', '{#REDLatestTmpFileName}', ''); 1661 | 1662 | // Download the SHA file for the defined Node.js versions 1663 | for i := 0 to GetArrayLength(main.node.majors) - 1 do begin 1664 | 1665 | nv := IntToStr(main.node.majors[i]); 1666 | 1667 | file := 'node' + nv + '.sha'; 1668 | _dp.Add('{#NodeDownloadURL}/latest-v' + nv + '.x/SHASUMS256.txt', file, ''); 1669 | 1670 | end; 1671 | 1672 | // Don't add additional files here! 1673 | // Move them before the SHASUMS256's !! 1674 | 1675 | _dp.Show; 1676 | try 1677 | try 1678 | _dp.Download; // This downloads the files to {tmp} 1679 | 1680 | // Result := True; 1681 | except 1682 | if _dp.AbortedByUser then begin 1683 | Log('Aborted by user.') 1684 | Result := False; 1685 | end else begin 1686 | 1687 | // Very likely the last file of the nodejs SHASUMS256's may not be found & 404 will be returned. 1688 | // This (404) is ok though. 1689 | msg := GetExceptionMessage(); 1690 | if StringChangeEx(msg, '404', '404', True) = 0 then begin 1691 | SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); 1692 | Result := False; 1693 | end; 1694 | end; 1695 | end; 1696 | finally 1697 | _dp.Hide; 1698 | end; 1699 | 1700 | if not Result then Exit; 1701 | 1702 | // Adjust caption for Node.js License page ( = Standard License page ) 1703 | msg := PageFromID(wpLicense).Caption; 1704 | PageFromID(wpLicense).Caption := 'Node.js ' + msg; 1705 | 1706 | // Additional page for the Node-RED license acceptance 1707 | main.pages.red_license := CreateREDLicensePage(wpLicense); 1708 | 1709 | // Inject the licenses 1710 | WizardForm.LicenseMemo.Lines.LoadFromFile(ExpandConstant('{tmp}\{#NodeLicenseTmpFileName}')); 1711 | main.pages.red_license.RichEditViewer.Lines.LoadFromFile(ExpandConstant('{tmp}\{#REDLicenseTmpFileName}')); 1712 | 1713 | // Need to do this here, as the Wizard seems to change the Top of the 1714 | // radio buttons on the License page after loading the license file. 1715 | REDLicenseAcceptedRadio.Top := WizardForm.LicenseAcceptedRadio.Top; 1716 | REDLicenseNotAcceptedRadio.Top := WizardForm.LicenseNotAcceptedRadio.Top; 1717 | 1718 | // Additional page to select to be installed Node.js version 1719 | main.pages.node_version := CreateInputOptionPage(wpLicense, 1720 | 'Node.js Installation', 'Select the Node.js version you like to install.', 1721 | '', 1722 | True, False); 1723 | 1724 | { // Additional page to select to be installed Node-RED version 1725 | main.pages.red_version := CreateInputOptionPage(main.pages.red_license.ID, 1726 | 'Node-RED Installation', 'Select the Node-RED version you like to install.', 1727 | '', 1728 | True, False); 1729 | } 1730 | 1731 | // Additional page to select to be installed Node-RED version 1732 | main.pages.red_action := CreateInputOptionPage(main.pages.red_license.ID, 1733 | 'Node-RED Installation', 'Configure the Node-RED installation setup.', 1734 | '', 1735 | False, True); 1736 | 1737 | // pages will be initialized @ RunDataPrepPage 1738 | Result := RunDataPrepPage(); 1739 | 1740 | debug('Result:= ' + BoolToStr(Result)); 1741 | 1742 | end; 1743 | end; 1744 | 1745 | if main.pages.node_version <> nil then begin 1746 | if CurPageID = main.pages.node_version.ID then begin 1747 | // run_silent & selected have a dedicated click event handler 1748 | main.node.install_tools := _nodeVersion_cbInstallWindowsTools.Checked; 1749 | end; 1750 | end; 1751 | 1752 | end; 1753 | 1754 | function BackButtonClick(CurPageID: Integer): Boolean; 1755 | begin 1756 | 1757 | Result:=True; 1758 | 1759 | if main.pages.node_version <> nil then begin 1760 | if CurPageID = main.pages.node_version.ID then begin 1761 | // run_silent & selected have a dedicated click event handler 1762 | main.node.install_tools := _nodeVersion_cbInstallWindowsTools.Checked; 1763 | end; 1764 | end; 1765 | end; 1766 | 1767 | procedure CurPageChanged(CurPageID: Integer); 1768 | var 1769 | 1770 | wf: TWizardForm; 1771 | 1772 | node_image, red_image: String; 1773 | 1774 | // idNodeVersionSelection: integer; 1775 | // idDownload: integer; 1776 | // idREDLicense: integer; 1777 | 1778 | begin 1779 | 1780 | wf := GetWizardForm(); 1781 | 1782 | if CurPageID = wpWelcome then begin 1783 | 1784 | // wf.WelcomeLabel2.Caption = 1785 | 1786 | 1787 | end; 1788 | 1789 | node_image := 'nodejs.bmp'; 1790 | red_image := 'node-red.bmp'; 1791 | 1792 | if FileExists(ExpandConstant('{tmp}\' + node_image)) = False then ExtractTemporaryFile(node_image); 1793 | if FileExists(ExpandConstant('{tmp}\' + red_image)) = False then ExtractTemporaryFile(red_image); 1794 | 1795 | // Update Next button when user gets to second license page 1796 | if main.pages.download <> nil then begin 1797 | if CurPageID = main.pages.download.ID then begin 1798 | // WizardForm.WizardSmallBitmapImage.Bitmap.LoadFromFile(ExpandConstant('{tmp}\' + red_image)); 1799 | WizardForm.WizardSmallBitmapImage.Visible := False; 1800 | end; 1801 | end; 1802 | 1803 | if CurPageID = wpLicense then begin 1804 | set_image(imgNODE); 1805 | end; 1806 | 1807 | if main.pages.node_version <> nil then begin 1808 | if CurPageID = main.pages.node_version.ID then begin 1809 | set_image(imgNODE); 1810 | end; 1811 | end; 1812 | 1813 | // Update Next button when user gets to second license page 1814 | if main.pages.red_license <> nil then begin 1815 | if CurPageID = main.pages.red_license.ID then begin 1816 | set_image(imgRED); 1817 | CheckREDLicenseAccepted(nil); 1818 | end; 1819 | end; 1820 | 1821 | if CurPageID = wpReady then begin 1822 | // Title & ReadyLabel1 changed via 'Messages' section. 1823 | wf.ReadyLabel.Caption := 'Click Install to continue with the installation, or click Back if you want to review or change any settings.'; 1824 | wf.ReadyMemo.Font.Name := 'Courier New'; 1825 | // wf.ReadyMemo.Font.Size := wf.ReadyMemo.Font.Size + 1; 1826 | wf.ReadyMemo.Font.Style := [fsBold]; 1827 | wf.ReadyMemo.ScrollBars := ssVertical; 1828 | 1829 | wf.NextButton.Enabled := not main.red.error; 1830 | 1831 | end; 1832 | 1833 | // Customize FinishedPage in case of error. 1834 | if CurPageID = wpFinished then begin 1835 | if main.error.status then begin 1836 | WizardForm.FinishedHeadingLabel.Caption := 'Node-RED Setup Error'; 1837 | WizardForm.FinishedLabel.Caption := ExpandConstant('{cm:MSG_FAILED_FINISHED1}') + #13#10#13#10 + '>> ' + main.error.msg + #13#10#13#10 + ExpandConstant('{cm:MSG_FAILED_FINISHED2}'); 1838 | WizardForm.FinishedLabel.AdjustHeight(); 1839 | 1840 | wizardform.YesRadio.Top := WizardForm.FinishedLabel.Top + WizardForm.FinishedLabel.Height + ScaleY(8); 1841 | end; 1842 | end; 1843 | 1844 | 1845 | end; 1846 | 1847 | function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; 1848 | var 1849 | m: string; 1850 | i, ii, l, f: integer; 1851 | tag: integer; 1852 | p, pp: string; 1853 | c, cc: boolean; 1854 | _global: boolean; 1855 | 1856 | _paths: TStringList; 1857 | _error: boolean; 1858 | 1859 | _kind: sREDInstallationKind; 1860 | 1861 | begin 1862 | 1863 | _paths:= TStringList.Create; 1864 | 1865 | // Never try to npm install into our App Installation Dir 1866 | p := ExpandConstant('{#SetupSetting('DefaultDirName')}'); 1867 | _paths.Add(p); 1868 | 1869 | _error := False; 1870 | 1871 | m:= '*****************' + NewLine; 1872 | m:= m + '*** Node.js ***' + NewLine; 1873 | m:= m + NewLine; 1874 | 1875 | if Length(main.node.current) > 0 then begin 1876 | m:= m + 'Currently installed: ' + main.node.current + NewLine; 1877 | if Length(main.node.selected) > 0 then begin 1878 | m:= m + 'Changing to: ' + main.node.selected + NewLine; 1879 | end; 1880 | end else begin 1881 | m:= m + 'Currently NOT installed.' + NewLine; 1882 | m:= m + 'Installing: ' + main.node.selected + NewLine; 1883 | end; 1884 | 1885 | // m:= m + NewLine; 1886 | 1887 | if Length(main.node.selected) > 0 then begin 1888 | if main.node.run_silent then begin 1889 | m:= m + '+ Node.js installer will run in the background.' + NewLine; 1890 | 1891 | if main.node.install_tools then 1892 | m:= m + '+ Additional Tools for Windows will be installed.' + NewLine; 1893 | 1894 | end; 1895 | end; 1896 | 1897 | m:= m + NewLine; 1898 | m:= m + NewLine; 1899 | m:= m + '******************' + NewLine; 1900 | m:= m + '*** Node-RED ***' + NewLine; 1901 | 1902 | SetArrayLength(main.red.run, GetArrayLength(main.red.installs)); 1903 | 1904 | l:=0; 1905 | for i:= 0 to GetArrayLength(main.red.installs) - 1 do begin 1906 | if main.red.installs[i].kind <> rikVoid then begin 1907 | main.red.run[l] := i; 1908 | l:= l + 1; 1909 | end; 1910 | end; 1911 | 1912 | SetArrayLength(main.red.run, l); 1913 | 1914 | _global:= False; 1915 | tag := 0; 1916 | 1917 | for i:=0 to GetArrayLength(main.red.installs) - 1 do begin 1918 | 1919 | _kind := main.red.installs[i].kind; 1920 | if _kind = rikVoid then continue; 1921 | 1922 | m:= m + NewLine; 1923 | 1924 | if l > 1 then begin 1925 | tag := tag + 1; 1926 | m:= m + '#' + IntToStr(tag) + ': ' + NewLine; 1927 | end; 1928 | 1929 | if Length(main.red.installs[i].version) > 0 then begin 1930 | m:= m + 'Currently installed: ' + main.red.installs[i].version + NewLine; 1931 | c:= True; 1932 | end else begin 1933 | m:= m + 'NEW installation.' + NewLine; 1934 | c:= False; 1935 | end; 1936 | 1937 | if main.red.installs[i].action = 'remove' then begin 1938 | m:= m + 'This installation will be REMOVED!' + NewLine; 1939 | end else if Length(main.red.installs[i].action) < 1 then begin 1940 | if ((_kind = rikGlobal) or (Length(main.red.installs[i].path) < 1)) then begin 1941 | m:= m + '+ Global installation.' + NewLine; 1942 | if not _global then begin 1943 | _global := True; 1944 | end else begin 1945 | m:= m + ' *** ERROR: Another global installation configured!' + NewLine; 1946 | _error := True; 1947 | end; 1948 | end; 1949 | end else begin 1950 | 1951 | if c then begin 1952 | m:= m + 'Changing to: ' + main.red.installs[i].action + NewLine; 1953 | end else begin 1954 | m:= m + 'Installing: ' + main.red.installs[i].action + NewLine; 1955 | end; 1956 | 1957 | if ((_kind = rikGlobal) or (Length(main.red.installs[i].path) < 1)) then begin 1958 | m:= m + '+ Global installation.' + NewLine; 1959 | if not _global then begin 1960 | _global := True; 1961 | end else begin 1962 | m:= m + ' *** ERROR: Another global installation configured!' + NewLine; 1963 | _error := True; 1964 | end; 1965 | end else begin 1966 | 1967 | pp := main.red.installs[i].path; 1968 | 1969 | if _kind = rikPath then begin 1970 | if DirExists(pp) and FileExists(pp + '\package.json') then begin 1971 | // main.red.installs[i].calc.path := pp; 1972 | end else begin 1973 | _kind := rikNew; 1974 | end; 1975 | end; 1976 | 1977 | if _kind = rikNew then begin 1978 | 1979 | ii:= -1; 1980 | p:= ''; 1981 | // lbl:= main.red.installs[i].name; 1982 | // pp:= main.red.installs[i].path; 1983 | debug('mri: ' + pp); 1984 | 1985 | repeat 1986 | 1987 | // First Check: 1988 | // Is another installation already using this path? 1989 | // c := False if YES! 1990 | f:=-1; 1991 | c:= (_paths.IndexOf(pp) < 0); 1992 | // if _paths.IndexOf(pp) > -1 then c := (f < 0); 1993 | 1994 | debug('c: ' + BoolToStr(c)); 1995 | 1996 | if c then begin 1997 | 1998 | // Second Check: 1999 | // We take it, if the directory doesn't exist. 2000 | // cc := True => Dir does NOT exist! 2001 | if not DirExists(pp) then begin 2002 | cc:=True; 2003 | break; 2004 | end; 2005 | 2006 | end; 2007 | 2008 | // Final Check 2009 | // Is this Dir empty? 2010 | c:= isEmptyDir(pp) and c; 2011 | 2012 | if not c then begin 2013 | 2014 | // build a new path! 2015 | ii:=ii+1; 2016 | p:= 'Node-RED'; 2017 | if ii > 0 then 2018 | p:= p + '(' + IntToStr(ii) + ')'; 2019 | 2020 | pp:= main.red.installs[i].path + '\' + p; 2021 | debug('pp: ' + pp); 2022 | end; 2023 | 2024 | until c; 2025 | 2026 | main.red.installs[i].calc.path:= pp; 2027 | 2028 | end; 2029 | 2030 | _paths.Add(pp); 2031 | 2032 | m:= m + '+ Installation path: ' + pp + NewLine; 2033 | if cc then 2034 | m:= m + ' + Directory will be created.' + NewLine; 2035 | 2036 | end; 2037 | end; 2038 | 2039 | if main.red.installs[i].autostart then begin 2040 | m:= m + '+ Creating entry in Autostart group.' + NewLine; 2041 | if main.red.installs[i].port > 0 then 2042 | p := IntToStr(main.red.installs[i].port) 2043 | else 2044 | p := '(Default)'; 2045 | 2046 | m:= m + ' + Port: ' + p + NewLine; 2047 | end; 2048 | 2049 | if main.red.installs[i].icon then begin 2050 | m:= m + '+ Creating Desktop icon.' + NewLine; 2051 | if main.red.installs[i].port > 0 then 2052 | p := IntToStr(main.red.installs[i].port) 2053 | else 2054 | p := '(Default)'; 2055 | 2056 | m:= m + ' + Port: ' + p + NewLine; 2057 | end; 2058 | 2059 | end; 2060 | 2061 | main.red.error := _error; 2062 | 2063 | { 2064 | // UI Test only! 2065 | m:= m + NewLine; 2066 | m:= m + NewLine; 2067 | 2068 | m:= m + '********************************************' + NewLine; 2069 | m:= m + '*** It is safe to press ''Install'' now. ***' + NewLine; 2070 | m:= m + '*** This version is for UI Test only. ***' + NewLine; 2071 | m:= m + '*** It does NOT install Node-RED! ***' + NewLine; 2072 | m:= m + '********************************************' + NewLine; 2073 | } 2074 | Result := m; 2075 | end; 2076 | 2077 | function PrepareToInstall(var NeedsRestart: Boolean): String; 2078 | var 2079 | i: integer; 2080 | 2081 | _dp: TDownloadWizardPage; 2082 | 2083 | _nv, _msi, _sha: string; 2084 | 2085 | res: TArrayOfString; 2086 | md5: string; 2087 | 2088 | begin 2089 | 2090 | _nv := main.node.selected; 2091 | main.error.status := False; 2092 | 2093 | if Length(_nv) < 1 then begin 2094 | Result := ''; 2095 | Exit; 2096 | end; 2097 | 2098 | Result := 'Failed to prepare the installation: ' + #13#10 + #13#10; 2099 | 2100 | // Downloading the requested version of node.js 2101 | _dp := CreateDownloadPage('Downloading the Node.js installer...', '', nil); 2102 | _dp.Clear; 2103 | 2104 | _sha := ''; 2105 | for i:=0 to GetArrayLength(main.node.versions) - 1 do begin 2106 | if main.node.versions[i].latest = _nv then begin 2107 | _sha := main.node.versions[i].sha; 2108 | _msi := main.node.versions[i].msi; 2109 | break; 2110 | end; 2111 | end; 2112 | 2113 | if Length(_msi) < 1 then begin 2114 | Result := Result + 'Node.js download file name is missing.'; 2115 | Exit; 2116 | end; 2117 | 2118 | _dp.Add('{#NodeDownloadURL}/v' + main.node.selected + '/' + _msi, 'node.msi', _sha); 2119 | 2120 | if main.node.install_tools then begin 2121 | if Length(main.python.path) < 1 then begin 2122 | // No python! Get python to install later. 2123 | if main.bit = 'x64' then begin 2124 | _dp.Add('https://www.python.org/ftp/python/{#py}/python-{#py}-amd64.exe', 'python_installer.exe', ''); 2125 | end else begin 2126 | _dp.Add('https://www.python.org/ftp/python/{#py}/python-{#py}-win32.exe', 'python_installer.exe', ''); 2127 | end; 2128 | end; 2129 | end; 2130 | 2131 | if main.node.install_tools then begin 2132 | if Length(main.vs.version) < 1 then begin 2133 | // Need to download the BUilsTools! 2134 | _dp.Add('{#VSBuildToolsURL}', 'VSBT_installer.exe', ''); 2135 | end; 2136 | end; 2137 | 2138 | _dp.Show; 2139 | try 2140 | try 2141 | _dp.Download; // This downloads the files to {tmp} 2142 | except 2143 | if _dp.AbortedByUser then begin 2144 | Result := Result + 'File download aborted.'; 2145 | Exit; 2146 | end else begin 2147 | Result := Result + AddPeriod(GetExceptionMessage); 2148 | Exit; 2149 | end; 2150 | end; 2151 | finally 2152 | _dp.Hide; 2153 | end; 2154 | 2155 | if main.node.install_tools then begin 2156 | if Length(main.python.path) < 1 then begin 2157 | // Verify MD5 for downloaded python installer. 2158 | try 2159 | md5 := GetMD5OfFile(ExpandConstant('{tmp}\python_installer.exe')); 2160 | except 2161 | Result := Result + 'Failed to calculate MD5 Sum of downloaded Installer for Python.'; 2162 | Exit; 2163 | end; 2164 | 2165 | if ((main.bit = 'x64') xor (md5 = '{#PyMD5x64}')) or ((main.bit = 'x86') xor (md5 = '{#PyMD5x86}')) then begin 2166 | DeleteFile(ExpandConstant('{tmp}\python_installer.exe')); 2167 | Result := Result + 'Invalid MD5 sum calculated for Installer for Python (' + main.bit + '): ' + md5; 2168 | Exit; 2169 | end; 2170 | end; 2171 | end; 2172 | 2173 | if (main.node.run_silent and main.node.install_tools) then begin 2174 | SetArrayLength(res, 0); 2175 | RunCMD('tasklist /FI "IMAGENAME eq setup.exe" /FO Table /NH', '', res); 2176 | if GetArrayLength(res) > 0 then begin 2177 | for i:=0 to GetArrayLength(res) - 1 do begin 2178 | // If another program w/ image name 'setup.exe' is running, 2179 | // we cannot detect if the Visual Studio BuildTools Installer has finished it's job. 2180 | if StringChangeEx(res[i], 'setup.exe', '', True) > 0 then begin 2181 | Result := Result + 'Another installer seems to be running.' + #13#10 + 'Cannot install Native Build Tools for Node.js in the background.' 2182 | Exit; 2183 | end; 2184 | end; 2185 | end; 2186 | end; 2187 | 2188 | Result := ''; 2189 | 2190 | end; 2191 | 2192 | 2193 | function GetPythonPath(param: string): string; 2194 | var 2195 | res: array of string; 2196 | parts: TStringList; 2197 | i: integer; 2198 | begin 2199 | // Check if we have a python installation 2200 | SetArrayLength(res, 0); 2201 | RunCMD('python -V', '', res); 2202 | if GetArrayLength(res) = 1 then begin 2203 | // Python 3.11.2 2204 | parts := TStringList.Create(); 2205 | if SplitString(res[0], ' ', parts) then begin 2206 | if parts[0] = 'Python' then begin 2207 | i := CompareVersions(parts[1], '3.10'); 2208 | if i >= 0 then begin 2209 | SetArrayLength(res, 0); 2210 | RunCMD('where python', '', res); 2211 | if GetArrayLength(res) > 0 then begin 2212 | if FileExists(res[0]) then begin 2213 | Result := res[0]; 2214 | end; 2215 | end; 2216 | end; 2217 | end; 2218 | end; 2219 | end; 2220 | end; 2221 | 2222 | 2223 | function GetVSBuildToolsVersion(): string; 2224 | var 2225 | res: array of string; 2226 | version: TStringList; 2227 | _btv: string; 2228 | begin 2229 | // Check if vswhere is able to find an installation 2230 | SetArrayLength(res, 0); 2231 | RunCMD('vswhere -products Microsoft.VisualStudio.Product.BuildTools -property installationVersion', ExpandConstant('{tmp}'), res); 2232 | if GetArrayLength(res) = 1 then begin 2233 | version := TStringList.Create(); 2234 | if SplitString(res[0], '.', version) then begin 2235 | if version.Count > 1 then begin 2236 | if StrToIntDef(version[0], 0) > StrToIntDef('{#VSBuildToolsMinVersion}', 15) then begin // 15 = 2015 2237 | _btv := res[0]; 2238 | SetArrayLength(res, 0); 2239 | RunCMD('vswhere -products Microsoft.VisualStudio.Product.BuildTools -property isComplete', ExpandConstant('{tmp}'), res); 2240 | if GetArrayLength(res) = 1 then begin 2241 | if res[0] = '1' then begin 2242 | SetArrayLength(res, 0); 2243 | RunCMD('vswhere -products Microsoft.VisualStudio.Product.BuildTools -property isLaunchable', ExpandConstant('{tmp}'), res); 2244 | if GetArrayLength(res) = 1 then begin 2245 | if res[0] = '1' then begin 2246 | Result := _btv; 2247 | debug('BuildTools found: ' + main.vs.version); 2248 | end; 2249 | end; 2250 | end; 2251 | end; 2252 | end; 2253 | end; 2254 | end; 2255 | end; 2256 | end; 2257 | 2258 | function GetNPMPythonConfig(): string; 2259 | var 2260 | res: array of string; 2261 | begin 2262 | // Check if npm is already configured correctly 2263 | SetArrayLength(res, 0); 2264 | RunCMD('npm config get python', '', res); 2265 | if GetArrayLength(res) = 1 then begin 2266 | if res[0] <> 'undefined' then begin 2267 | if FileExists(res[0]) then begin 2268 | Result := res[0]; 2269 | end; 2270 | end; 2271 | end; 2272 | end; 2273 | 2274 | 2275 | 2276 | // Used while installing the node.js 2277 | // https://stackoverflow.com/questions/34336466/inno-setup-how-to-manipulate-progress-bar-on-run-section 2278 | procedure SetMarqueeProgress(Marquee: Boolean); 2279 | begin 2280 | if Marquee then begin 2281 | WizardForm.ProgressGauge.Style := npbstMarquee; 2282 | end else begin 2283 | WizardForm.ProgressGauge.Style := npbstNormal; 2284 | end; 2285 | end; 2286 | 2287 | 2288 | // To be used for the lengthy status messages in the [Run] section 2289 | procedure SetupRunConfig(); 2290 | begin 2291 | SetMarqueeProgress(True); 2292 | 2293 | WizardForm.StatusLabel.WordWrap := True; 2294 | WizardForm.StatusLabel.AdjustHeight(); 2295 | end; 2296 | 2297 | function GetNodeVersionMain(Param: string): string; 2298 | begin 2299 | 2300 | debug('gnv: ' + Param); 2301 | 2302 | if Param = 'current' then 2303 | Result := main.node.current; 2304 | 2305 | if Param = 'selected' then 2306 | Result := main.node.selected; 2307 | 2308 | debug('gnv: ' + Result); 2309 | 2310 | end; 2311 | 2312 | function GetInstalledNodeGUID(nv: string): string; 2313 | var 2314 | _base: string; 2315 | _keys: TArrayOfString; 2316 | i: integer; 2317 | _name: string; 2318 | _nv: string; 2319 | 2320 | begin 2321 | _base := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'; 2322 | Result := ''; 2323 | 2324 | if Length(nv) < 1 then nv := main.node.current; 2325 | 2326 | if RegGetSubkeyNames(main.HKLM, _base, _keys) then begin 2327 | for i:= 0 to GetArrayLength(_keys) - 1 do begin 2328 | if RegQueryStringValue(main.HKLM, _base + '\' + _keys[i], 'DisplayName', _name) = True then begin 2329 | if _name = 'Node.js' then begin 2330 | if RegQueryStringValue(main.HKLM, _base + '\' + _keys[i], 'DisplayVersion', _nv) = True then begin 2331 | if _nv = nv then begin 2332 | Result := _keys[i]; 2333 | break; 2334 | end; 2335 | end; 2336 | end; 2337 | end; 2338 | end; 2339 | end; 2340 | 2341 | end; 2342 | 2343 | 2344 | { 2345 | // Check function to indicate if Node.js is currently installed 2346 | // mode = silent: Return true only when 'Run in Background' was selected 2347 | // mode = show; Return true only when 'Run in Background' was *NOT* selected 2348 | function CheckNodeInstall(mode: string): Boolean; 2349 | begin 2350 | if main.error.status then Exit; 2351 | // Result := (Length(main.node.selected) > 0) and (main.node.selected <> main.node.current); 2352 | // We offer to re-install the current version! 2353 | Result := Length(main.node.selected) > 0; 2354 | 2355 | if mode = 'silent' then 2356 | Result := Result and main.node.run_silent; 2357 | 2358 | if mode = 'show' then 2359 | Result := Result and (not main.node.run_silent); 2360 | 2361 | end; 2362 | } 2363 | 2364 | 2365 | 2366 | // Each parameter MUST be string, integer or boolean. 2367 | // No SET type allowed here :(( ! 2368 | function RunCheck(step, param: string): Boolean; 2369 | var 2370 | bool: boolean; 2371 | i: integer; 2372 | str: string; 2373 | _kind: sREDInstallationKind; 2374 | 2375 | begin 2376 | if main.error.status then Exit; 2377 | 2378 | if step = 'rcsNodeUninstall' then begin 2379 | // We uninstall only if we shall install as well. 2380 | if RunCheck('rcsNodeInstall', '') then 2381 | Result := Length(GetInstalledNodeGUID(main.node.current)) > 0; 2382 | end; 2383 | 2384 | if step = 'rcsNodeInstall' then begin 2385 | Result := Length(main.node.selected) > 0; 2386 | if param = 'silent' then 2387 | // Return true only when 'Run in Background' was selected 2388 | Result := Result and main.node.run_silent; 2389 | if param = 'show' then 2390 | // Return true only when 'Run in Background' was *NOT* selected 2391 | Result := Result and (not main.node.run_silent); 2392 | end; 2393 | 2394 | if step = 'rcsPython' then begin 2395 | if Length(main.node.selected) < 1 then Exit; 2396 | if not main.node.run_silent then Exit; 2397 | if not main.node.install_tools then Exit; 2398 | bool := StrToBool(param); 2399 | if bool then begin 2400 | Result := Length(main.python.path) > 0; 2401 | end else begin 2402 | Result := Length(main.python.path) = 0; 2403 | end; 2404 | end; 2405 | 2406 | if step = 'rcsVSBuildTools' then begin 2407 | if Length(main.node.selected) < 1 then Exit; 2408 | if not main.node.run_silent then Exit; 2409 | if not main.node.install_tools then Exit; 2410 | Result := Length(main.vs.version) = 0; 2411 | end; 2412 | 2413 | if step = 'rcsPythonConfig' then begin 2414 | if main.python.npm then Exit; 2415 | Result := RunCheck('rcsPython', 'yes'); 2416 | end; 2417 | 2418 | if step = 'rcsREDRemove' then begin 2419 | i := StrToIntDef(param, -1); 2420 | if i < 0 then Exit; 2421 | if not (i < GetArrayLength(main.red.run)) then Exit; 2422 | i := main.red.run[i]; 2423 | _kind := main.red.installs[i].kind; 2424 | if _kind = rikVoid then Exit; 2425 | if _kind = rikNew then Exit; 2426 | Result := Length(main.red.installs[i].action) > 0; 2427 | end; 2428 | 2429 | if step = 'rcsREDInstall' then begin 2430 | i := StrToIntDef(param, -1); 2431 | if i < 0 then Exit; 2432 | if not (i < GetArrayLength(main.red.run)) then Exit; 2433 | i := main.red.run[i]; 2434 | if main.red.installs[i].kind = rikVoid then Exit; 2435 | str := main.red.installs[i].action; 2436 | if LowerCase(str) = 'remove' then Exit; 2437 | Result := Length(str) > 0; 2438 | end; 2439 | 2440 | end; 2441 | 2442 | { 2443 | // Check function to indicate if Node.js is currently installed and shall be uninstalled 2444 | function CheckNodeUninstall(): Boolean; 2445 | begin 2446 | if main.error.status then Exit; 2447 | if CheckNodeInstall('') then 2448 | Result := Length(GetInstalledNodeGUID(main.node.current)) > 0; 2449 | end; 2450 | } 2451 | 2452 | function GetNodeVersionCall(msg: string): string; 2453 | var 2454 | _page: TOutputMarqueeProgressWizardPage; 2455 | res: array of string; 2456 | _nv: string; 2457 | 2458 | begin 2459 | 2460 | Result := ''; 2461 | _page := CreateOutputMarqueeProgressPage('Verifying Node.js installation', msg); 2462 | 2463 | try 2464 | _page.Show(); 2465 | 2466 | RunCMD('node --version', '', res); 2467 | if GetArrayLength(res) = 1 then begin 2468 | _nv := res[0]; 2469 | if StringChangeEx(_nv, 'v', '', True) = 1 then begin 2470 | Result := _nv; 2471 | end; 2472 | end; 2473 | 2474 | finally 2475 | _page.Hide(); 2476 | end; 2477 | end; 2478 | 2479 | { 2480 | function GetNodeVersionReg(): string; 2481 | var 2482 | _nv: string; 2483 | begin 2484 | Result := '' 2485 | if RegKeyExists(main.HKLM, 'SOFTWARE\Node.js') then begin 2486 | if RegQueryStringValue(main.HKLM, 'SOFTWARE\Node.js', 'Version', _nv) = True then begin 2487 | Result := _nv; 2488 | end; 2489 | end; 2490 | end; 2491 | } 2492 | 2493 | function GetNodeDataReg(param: string): string; 2494 | var 2495 | _data: string; 2496 | _key: string; 2497 | begin 2498 | Result := '' 2499 | 2500 | param := LowerCase(param); 2501 | if param = 'version' then begin 2502 | _key := 'Version' 2503 | end else if param = 'path' then begin 2504 | _key := 'InstallPath' 2505 | end else begin 2506 | Exit; 2507 | end; 2508 | 2509 | if RegKeyExists(main.HKLM, 'SOFTWARE\Node.js') then begin 2510 | if RegQueryStringValue(main.HKLM, 'SOFTWARE\Node.js', _key, _data) = True then begin 2511 | Result := _data; 2512 | end; 2513 | end; 2514 | end; 2515 | 2516 | 2517 | // Each parameter MUST be string, integer or boolean. 2518 | // No SET type allowed here :(( ! 2519 | procedure Confirm(step, param: string); 2520 | var 2521 | _nvC: string; 2522 | _nvR: string; 2523 | _pyp: string; 2524 | _vs: string; 2525 | 2526 | begin 2527 | 2528 | if step = 'csNoNode' then begin 2529 | _nvC := GetNodeVersionCall('Verifying that Node.js is not installed.'); 2530 | _nvR := GetNodeDataReg('version'); 2531 | 2532 | if (Length(_nvC) > 0) or (Length(_nvR) > 0) then begin 2533 | main.error.msg := 'Detected Node.js despite it should have been removed.'; 2534 | end; 2535 | end; 2536 | 2537 | if step = 'csNode' then begin 2538 | _nvC := GetNodeVersionCall('Verifying that Node.js is not installed.'); 2539 | _nvR := GetNodeDataReg('version'); 2540 | 2541 | if not ((param = _nvC) and (_nvC = _nvR)) then begin 2542 | main.error.msg := 'Failed to confirm that Node.js v' + param + ' is installed.' 2543 | end; 2544 | end; 2545 | 2546 | if step = 'csFile' then begin 2547 | if not FileExists(param) then begin 2548 | main.error.msg := 'File not found: ' + param; 2549 | end; 2550 | end; 2551 | 2552 | if step = 'csPython' then begin 2553 | _pyp := GetPythonPath(''); 2554 | if Length(_pyp) < 1 then begin 2555 | main.error.msg := 'Failed to confirm that Python is installed.' 2556 | end else begin 2557 | main.python.path := _pyp; 2558 | end; 2559 | end; 2560 | 2561 | if step = 'csVSBuildTools' then begin 2562 | _vs := GetVSBuildToolsVersion(); 2563 | if Length(_vs) < 1 then begin 2564 | main.error.msg := 'Failed to confirm that the VisualStudio BuildTools are installed.' 2565 | end else begin 2566 | main.vs.version := _vs; 2567 | end; 2568 | end; 2569 | 2570 | if step = 'csPythonConfig' then begin 2571 | _pyp := GetNPMPythonConfig(); 2572 | if Length(_pyp) < 1 then begin 2573 | main.error.msg := 'Failed to configure Node.js to know the Python path.' 2574 | end; 2575 | end; 2576 | 2577 | if Length(main.error.msg) > 0 then 2578 | main.error.status := True; 2579 | 2580 | end; 2581 | 2582 | { 2583 | procedure ConfirmNoNode(); 2584 | var 2585 | _nvC: string; 2586 | _nvR: string; 2587 | 2588 | begin 2589 | _nvC := GetNodeVersionCall('Verifying that Node.js is not installed.'); 2590 | _nvR := GetNodeVersionReg(); 2591 | 2592 | if (Length(_nvC) > 0) or (Length(_nvR) > 0) then begin 2593 | main.error.status := True; 2594 | main.error.msg := 'Detected Node.js despite it should have been removed.'; 2595 | end; 2596 | end; 2597 | 2598 | 2599 | procedure ConfirmNode(nv: string); 2600 | var 2601 | _nvC: string; 2602 | _nvR: string; 2603 | 2604 | begin 2605 | _nvC := GetNodeVersionCall('Looking for Node.js v' + nv); 2606 | _nvR := GetNodeVersionReg(); 2607 | 2608 | if not ((_nvC = nv) and (_nvC = _nvR)) then begin 2609 | main.error.status := True; 2610 | main.error.msg := 'Failed to confirm that Node.js v' + nv + ' is installed.' 2611 | end; 2612 | end; 2613 | } 2614 | 2615 | function GetNodeInstallSilent(param: string): string; 2616 | begin 2617 | if main.node.run_silent then Result := '/qn'; 2618 | end; 2619 | 2620 | function GetREDInstallsData(data: string; index: integer): string; 2621 | var 2622 | p: string; 2623 | 2624 | begin 2625 | 2626 | debug('GetREDInstallsData: ' + data + ' / ' + IntToStr(index)); 2627 | 2628 | if index < 0 then Exit; 2629 | if not (index < GetArrayLength(main.red.installs)) then Exit; 2630 | 2631 | if data = 'global' then begin 2632 | // Explicitely - for an existing installation 2633 | if main.red.installs[index].kind = rikGlobal then Result := '-g'; 2634 | // implicitey - for a new installation 2635 | if Length(GetREDInstallsData('path', index)) < 1 then Result := '-g'; 2636 | 2637 | end; 2638 | 2639 | if data = 'version' then Result := main.red.installs[index].version; 2640 | if data = 'action' then Result := main.red.installs[index].action; 2641 | 2642 | if data = 'path' then begin 2643 | // For new installations, the path was calculated. 2644 | p := main.red.installs[index].calc.path; 2645 | // In case this is an already existing installation 2646 | // a new path will only be calculated if there was an issue with the given one. 2647 | if main.red.installs[index].kind = rikPath then begin 2648 | // if no path was calculated, take the given! 2649 | if Length(p) < 1 then 2650 | p := main.red.installs[index].path; 2651 | end; 2652 | Result := p; 2653 | 2654 | debug('GetREDActionData: path / ' + p); 2655 | end; 2656 | 2657 | if data = 'action' then Result := main.red.installs[index].action; 2658 | 2659 | end; 2660 | 2661 | // This functoin translates between the main.red.run index & the main.red.installs index 2662 | function GetREDRunData(data: string; index: integer): string; 2663 | begin 2664 | if index < 0 then Exit; 2665 | if not (index < GetArrayLength(main.red.run)) then Exit; 2666 | Result := GetREDInstallsData(data, main.red.run[index]); 2667 | end; 2668 | 2669 | function GetREDActionGlobal(param: string): string; 2670 | begin 2671 | Result := GetREDRunData('global', StrToIntDef(param, -1)); 2672 | end; 2673 | 2674 | function GetREDActionVersion(param: string): string; 2675 | begin 2676 | Result := GetREDRunData('version', StrToIntDef(param, -1)); 2677 | end; 2678 | 2679 | function GetREDActionAction(param: string): string; 2680 | begin 2681 | Result := GetREDRunData('action', StrToIntDef(param, -1)); 2682 | end; 2683 | 2684 | function GetREDActionPath(param: string): string; 2685 | begin 2686 | Result := GetREDRunData('path', StrToIntDef(param, -1)); 2687 | end; 2688 | 2689 | function GetREDCurrentMsg(param: string): string; 2690 | var 2691 | _rv: string; 2692 | _path: string; 2693 | i: integer; 2694 | 2695 | begin 2696 | i := StrToIntDef(param, -1); 2697 | if i < 0 then begin 2698 | Result := 'GetREDCurrentMsg / ERROR: ' + param; 2699 | Exit; 2700 | end; 2701 | 2702 | _rv := GetREDRunData('version', i); 2703 | _path := GetREDRunData('path', i); 2704 | 2705 | if Length(_path) < 1 then begin 2706 | Result := 'globally installed Node-RED v' + _rv; 2707 | end else begin 2708 | Result := 'Node-RED v' + _rv + ' @ ' + _path; 2709 | end; 2710 | 2711 | end; 2712 | 2713 | function GetREDActionMsg(param: string): string; 2714 | var 2715 | _rv: string; 2716 | _path: string; 2717 | i: integer; 2718 | 2719 | begin 2720 | i := StrToIntDef(param, -1); 2721 | if i < 0 then begin 2722 | Result := 'GetREDActionMsg / ERROR: ' + param; 2723 | Exit; 2724 | end; 2725 | 2726 | _rv := GetREDRunData('action', i); 2727 | _path := GetREDRunData('path', i); 2728 | 2729 | if Length(_path) < 1 then begin 2730 | Result := 'globally installed Node-RED v' + _rv; 2731 | end else begin 2732 | Result := 'Node-RED v' + _rv + ' @ ' + _path; 2733 | end; 2734 | 2735 | end; 2736 | 2737 | 2738 | procedure REDPrepare(param: string); 2739 | var 2740 | _path: string; 2741 | _pkg: string; 2742 | // _json: TJsonParser; 2743 | // _json: string; 2744 | index: integer; 2745 | _kind: sREDInstallationKind; 2746 | 2747 | _key, k, _root: string; 2748 | i: integer; 2749 | error: boolean; 2750 | 2751 | begin 2752 | 2753 | // We have no option to cancel the installation (step) at this stage. 2754 | // Thus even if we know there's something wrong, we need to accept the incoming impact. 2755 | if main.error.status then Exit; 2756 | index := StrToIntDef(param, -1); 2757 | if index < 0 then Exit; 2758 | if not (index < GetArrayLength(main.red.run)) then Exit; 2759 | index := main.red.run[index]; 2760 | 2761 | _kind := main.red.installs[index].kind; 2762 | if _kind = rikVoid then Exit; 2763 | 2764 | _path := GetREDInstallsData('path', index); 2765 | // if Length(_path) < 1 then Exit; 2766 | 2767 | // Start with Bookkeeping in the registry! 2768 | // This ensures that - whatever happens - next time the installer runs we'll know where to look for a NR installation 2769 | _key := main.red.installs[index].key; 2770 | if Length(_key) < 1 then begin 2771 | 2772 | _root := AddBackslash('{#REDInstallationsRegRoot}'); 2773 | 2774 | if main.red.installs[index].kind = rikGlobal then begin 2775 | _key := _root + '0000'; 2776 | 2777 | end else begin 2778 | i := 0; 2779 | repeat 2780 | i := i + 1; 2781 | k := '0000' + IntToStr(i); 2782 | // Pascal Script strings begin with index '1'! 2783 | _key := _root + '\' + Copy(k, Length(k) - 3, 4); 2784 | until (RegKeyExists(main.HKLM, _key) xor (i < 10000)); // True if False!! 2785 | 2786 | // I'm aware that this will 'break' if 10.000+ keys are present! 2787 | main.red.installs[index].key := _key; 2788 | end; 2789 | 2790 | end; 2791 | 2792 | error := False; 2793 | if not RegWriteStringValue(main.HKLM, _key, 'Path', _path) then error := True; 2794 | 2795 | // Additional properties follow 2796 | if not RegWriteStringValue(main.HKLM, _key, 'Name', main.red.installs[index].name) then error := True; 2797 | if not RegWriteStringValue(main.HKLM, _key, 'Port', IntToStr(main.red.installs[index].port)) then error := True; 2798 | 2799 | if error then debug('Failed to create registry enties for Node-RED installation @ ' + _path); 2800 | 2801 | if Length(_path) > 0 then begin 2802 | 2803 | // Prepare the installation directory 2804 | if not DirExists(_path) then begin 2805 | debug('Creating Node-RED installation directory @ ' + _path); 2806 | if not CreateDir(_path) then Exit; // this will create troubles... 2807 | end; 2808 | 2809 | _pkg := _path + '\package.json'; 2810 | if not FileExists(_pkg) then begin 2811 | debug('Creating minimal package.json @ ' + _pkg); 2812 | SaveStringToFile(_pkg, '{}' + #13#10, False); // no need to check for success here... 2813 | end; 2814 | 2815 | end; 2816 | 2817 | SetupRunConfig(); 2818 | 2819 | end; 2820 | 2821 | 2822 | function CreateREDIcon(index: integer; path: string; folder: string): string; 2823 | var 2824 | i: integer; 2825 | _name: string; 2826 | _port: string; 2827 | _run: string; 2828 | 2829 | begin 2830 | i:=0; 2831 | repeat 2832 | _name := main.red.installs[index].name 2833 | if Length(_name) < 1 then _name := 'Node-RED'; 2834 | if i > 0 then begin 2835 | _name := _name + '(' + IntToStr(i) + ')'; 2836 | end; 2837 | _name := AddBackslash(folder) + _name + '.lnk'; 2838 | i:=i+1; 2839 | until FileExists(_name) xor True; 2840 | 2841 | if main.red.installs[index].port > 0 then 2842 | _port := ' --port ' + IntToStr(main.red.installs[index].port) + ' '; 2843 | 2844 | // non-global install needs path to node.exe 2845 | if Length(path) > 0 then begin 2846 | _run := '"' + AddBackslash(GetNodeDataReg('path')) + 'node.exe"'; 2847 | // _run := _run + ' node_modules\node-red\red.js'; 2848 | end; 2849 | 2850 | if Length(main.red.installs[index].name) > 0 then 2851 | _port := _port + ' --title "' + main.red.installs[index].name + '" '; 2852 | 2853 | if Length(path) < 1 then begin 2854 | 2855 | try 2856 | // we launch the global install with 'node-red' 2857 | Result := CreateShellLink(_name, 'Run Node-RED', 'node-red', _port, '', ExpandConstant('{app}\red.ico'), 0, SW_SHOW); 2858 | except 2859 | Result := ''; 2860 | end; 2861 | 2862 | end else begin 2863 | 2864 | _port := _port + ' --userDir . '; 2865 | 2866 | try 2867 | Result := CreateShellLink(_name, 'Run Node-RED', _run, ' node_modules\node-red\red.js ' + _port, path, ExpandConstant('{app}\red.ico'), 0, SW_SHOW); 2868 | except 2869 | Result := ''; 2870 | end; 2871 | 2872 | end; 2873 | end; 2874 | 2875 | 2876 | procedure REDFinalize(param: string); 2877 | var 2878 | _action: string; 2879 | _path: string; 2880 | p: string; 2881 | error: boolean; 2882 | _rv: string; 2883 | res: array of string; 2884 | index: integer; 2885 | _kind: sREDInstallationKind; 2886 | 2887 | _global: boolean; 2888 | _name: string; 2889 | 2890 | begin 2891 | 2892 | if main.error.status then Exit; 2893 | index := StrToIntDef(param, -1); 2894 | if index < 0 then Exit; 2895 | if not (index < GetArrayLength(main.red.run)) then Exit; 2896 | index := main.red.run[index]; 2897 | 2898 | _kind := main.red.installs[index].kind; 2899 | if _kind = rikVoid then Exit; 2900 | 2901 | _action := main.red.installs[index].action; 2902 | if Length(_action) < 1 then Exit; 2903 | if LowerCase(_action) = 'remove' then Exit; 2904 | 2905 | // First: Let's confirm that there's 2906 | // > a NR installation 2907 | // > with the correct version 2908 | // > @ the requested path (or globally!) 2909 | 2910 | _path := GetREDInstallsData('path', index); 2911 | 2912 | p := _path; 2913 | if Length(_path) < 1 then begin 2914 | 2915 | _global := True; 2916 | 2917 | // Get the details of the global installation 2918 | SetArrayLength(res, 0); 2919 | if RunCMD('npm config get prefix', '', res) then begin 2920 | if GetArrayLength(res) > 0 then p := res[0]; 2921 | end; 2922 | end; 2923 | 2924 | _rv := red_list(p); 2925 | main.error.status := True; 2926 | if Length(_rv) > 0 then begin 2927 | if main.red.installs[index].action = _rv then begin 2928 | main.error.status := False; 2929 | end; 2930 | end; 2931 | 2932 | if main.error.status then begin 2933 | main.error.msg := 'Failed to confirm that installation of ' + GetREDActionMsg(param) + ' was successful.'; 2934 | Exit; 2935 | end; 2936 | 2937 | // if Length(_path) < 1 then Exit; 2938 | 2939 | // Next: Create the Desktop / Autostart entries 2940 | error := false; 2941 | if main.red.installs[index].icon then begin 2942 | _name := CreateREDIcon(index, _path, ExpandConstant('{autodesktop}')); 2943 | if Length(_name) > 0 then begin 2944 | if RegWriteStringValue(main.HKLM, main.red.installs[index].key, 'Icon', _name) = False then begin 2945 | error := true; 2946 | end 2947 | end; 2948 | end; 2949 | 2950 | if main.red.installs[index].autostart then begin 2951 | _name := CreateREDIcon(index, _path, ExpandConstant('{autostartup}')); 2952 | if Length(_name) > 0 then begin 2953 | if RegWriteStringValue(main.HKLM, main.red.installs[index].key, 'Autostart', _name) = False then begin 2954 | error := true; 2955 | end 2956 | end; 2957 | end; 2958 | 2959 | if error then begin 2960 | main.error.status := true; 2961 | main.error.msg := 'Failed to create icon entries for Node-RED installation @ ' + _path; 2962 | end; 2963 | 2964 | end; 2965 | 2966 | 2967 | function RemoveDirEx(dirName: string): boolean; 2968 | var 2969 | FindRec: TFindRec; 2970 | _path: string; 2971 | _delete: boolean; 2972 | begin 2973 | 2974 | Result := true; 2975 | 2976 | if FindFirst(AddBackslash(dirName)+'*', FindRec) then begin 2977 | try 2978 | repeat 2979 | if (FindRec.Name <> '.') and (FindRec.Name <> '..') then begin 2980 | _path := AddBackslash(dirName) + FindRec.Name; 2981 | if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) > 0 then begin 2982 | Result := RemoveDirEx(_path) and Result; 2983 | end else begin 2984 | _delete := false; 2985 | case FindRec.Name of 2986 | 'package.json': _delete := true; 2987 | 'package-lock.json': _delete := true; 2988 | '.package-lock.json': _delete := true; 2989 | else 2990 | begin 2991 | debug('Found unexpected file ''' + FindRec.Name + ''' while removing Node-RED installation @ ' + RemoveBackslash(dirName) + '. Directory will be kept.'); 2992 | Result := false; 2993 | end; 2994 | end; 2995 | if _delete then begin 2996 | Result := DeleteFile(_path) and Result; 2997 | end; 2998 | end; 2999 | end; 3000 | until not FindNext(FindRec); 3001 | finally 3002 | FindClose(FindRec); 3003 | end; 3004 | end; 3005 | 3006 | if Result then begin 3007 | Result := RemoveDir(RemoveBackslash(dirName)); 3008 | end; 3009 | 3010 | end; 3011 | 3012 | 3013 | procedure REDRemove(param: string); 3014 | var 3015 | _path, p: string; 3016 | res: array of string; 3017 | _rv: string; 3018 | index: integer; 3019 | _kind: sREDInstallationKind; 3020 | _icon: string; 3021 | 3022 | begin 3023 | 3024 | if main.error.status then Exit; 3025 | index := StrToIntDef(param, -1); 3026 | if index < 0 then Exit; 3027 | if not (index < GetArrayLength(main.red.run)) then Exit; 3028 | index := main.red.run[index]; 3029 | 3030 | _kind := main.red.installs[index].kind; 3031 | if _kind = rikVoid then Exit; 3032 | 3033 | // Let's ensure that there is NO Node-RED installation at the given path (or globally)! 3034 | _path := GetREDInstallsData('path', index); 3035 | 3036 | p := _path; 3037 | if Length(_path) < 1 then begin 3038 | SetArrayLength(res, 0); 3039 | if RunCMD('npm config get prefix', '', res) then begin 3040 | if GetArrayLength(res) > 0 then p := res[0]; 3041 | end; 3042 | end; 3043 | 3044 | if Length(p) < 1 then begin 3045 | main.error.status := True; 3046 | main.error.msg := 'Failed to get path to check for presence of Node-RED installation.'; 3047 | Exit; 3048 | end; 3049 | 3050 | _rv := red_list(p); 3051 | if Length(_rv) > 0 then begin 3052 | main.error.status := True; 3053 | main.error.msg := 'Failed to confirm the removal of ' + GetREDCurrentMsg(param) + '.'; 3054 | end; 3055 | 3056 | // We do not remove the Registry entries here! 3057 | // Once the installer runs another time, it will detect any obsolete entries & remove those then. 3058 | // This allows us to keep the sequence (in the registry) for this run - without puzzling around. 3059 | 3060 | // Try to remove the Desktop Icon 3061 | _icon := main.red.installs[index].registry.icon; 3062 | if Length(_icon) > 0 then DeleteFile(_icon); 3063 | 3064 | // Try to remove the Autostart Icon 3065 | _icon := main.red.installs[index].registry.autostart; 3066 | if Length(_icon) > 0 then DeleteFile(_icon); 3067 | 3068 | // Now try to empty & remove the directory 3069 | if Length(_path) > 0 then RemoveDirEx(_path); 3070 | 3071 | end; 3072 | 3073 | 3074 | #include <.\iss\redactionpage.iss> 3075 | #include <.\iss\reddetector.iss> --------------------------------------------------------------------------------