├── 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>
--------------------------------------------------------------------------------