Some of you might have already been bitten by this one: After installing a new Delphi version you end up with a garbled PATH variable, simply because the installer tries to extend this variable over some limit. I was not able to find enough reliable information to name this limit, but it might be something about 2000 characters, but this might as well depend on the underlying operating system.
During the setup of a new system with lots of older Delphi versions I decided to write a simple program that uses environment variables to compress the PATH variable so that hopefully it fits when installing the next Delphi version.
The process is plain simple: Check each entry in the PATH variable whether it starts with a given string and replace this with a short environment variable. When finished add those new variables to the environment and write back the shortened PATH variable.
For simplicity I decided to use a single file for this command line utility:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
program CompressPath; {$APPTYPE CONSOLE} {$R *.res} uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, System.IOUtils, System.Win.Registry; type TPathCompressor = class private FOrgLength: Integer; FPath: TStringList; FPathDelphi: string; FPathMyDocs: string; FPathProgramFiles: string; FPathProgramFilesX86: string; FPathSharedDocs: string; FRegistry: TRegistry; FShortcuts: TStringList; FVariables: TStringList; protected procedure AddShortCut(const Key, Value: string); procedure AddDelphiShortCut(const Key, RelExePath, RelDocPath: string); overload; procedure AddOldDelphiShortCut(const Key, RelExePath, RelDocPath: string); overload; function Compress(const Value: string): string; function GetExistingPath(const Prefix, RelPath: string): string; overload; function GetExistingPath(const Prefix: array of string; const RelPath: string): string; overload; procedure InitShortCuts; function ReadRegistry(const AName: string): string; function StorePath: Integer; procedure StoreVariables; procedure WriteRegistry(const Name, Value: string); property OrgLength: Integer read FOrgLength; property Path: TStringList read FPath; property PathDelphi: string read FPathDelphi; property PathMyDocs: string read FPathMyDocs; property PathProgramFiles: string read FPathProgramFiles; property PathProgramFilesX86: string read FPathProgramFilesX86; property PathSharedDocs: string read FPathSharedDocs; property Registry: TRegistry read FRegistry; property Shortcuts: TStringList read FShortcuts; property Variables: TStringList read FVariables; public constructor Create; destructor Destroy; override; procedure Execute(out OldLength, NewLength: Integer); function LoadPath(UserEnvironment: Boolean): Boolean; procedure LoadShortCuts(const AFileName: string); procedure StoreShortCuts(const AFileName: string); procedure NotifyChanges; end; constructor TPathCompressor.Create; begin inherited Create; FShortcuts := TStringList.Create; FVariables := TStringList.Create; FVariables.Sorted := true; FVariables.Duplicates := dupIgnore; FPath := TStringList.Create; FPath.Delimiter := ';'; FPath.StrictDelimiter := true; {$IFDEF DEBUG} { It is quite unlikely that we have write permission to HKEY_LOCAL_MACHINE when we are running from inside the IDE. In addition it does not make much sense to run in DEBUG mode outside of the IDE. Hence this restriction. There are other places where we check for DEBUG mode. In case you get unexpected results, please have a look for those. } FRegistry := TRegistry.Create(KEY_READ); {$ELSE} FRegistry := TRegistry.Create(); {$ENDIF} FPathMyDocs := TPath.GetDocumentsPath; FPathSharedDocs := TPath.GetSharedDocumentsPath; FPathProgramFilesX86 := GetEnvironmentVariable('ProgramFiles(x86)'); if FPathProgramFilesX86 = '' then begin FPathProgramFiles := GetEnvironmentVariable('ProgramFiles'); FPathDelphi := FPathProgramFiles; end else begin FPathProgramFiles := GetEnvironmentVariable('ProgramW6432'); FPathDelphi := FPathProgramFilesX86; end; InitShortCuts; end; destructor TPathCompressor.Destroy; begin FRegistry.Free; FPath.Free; FVariables.Free; FShortcuts.Free; inherited Destroy; end; procedure TPathCompressor.AddShortCut(const Key, Value: string); begin { Shortcuts are only usefull when they contain some data. } if Value > '' then begin Shortcuts.Values[Key] := Value; end; end; procedure TPathCompressor.AddDelphiShortCut(const Key, RelExePath, RelDocPath: string); begin AddShortCut(Key, GetExistingPath(PathDelphi, RelExePath)); { Depending on the type of installation (current user or all users), the BPLs are located in different documents folders. Here we first check inside the personal folder and if that does not exist, we take the public documents folder. } AddShortCut(Key + 'BPL', GetExistingPath([PathMyDocs, PathSharedDocs], RelDocPath)); end; procedure TPathCompressor.AddOldDelphiShortCut(const Key, RelExePath, RelDocPath: string); begin AddShortCut(Key, GetExistingPath(PathDelphi, RelExePath)); { In older Delphi versions the BPL folder resides below the Delphi folder. } AddShortCut(Key + 'BPL', GetExistingPath(PathDelphi, RelDocPath)); end; function TPathCompressor.Compress(const Value: string): string; var S: string; N: Integer; L: Integer; I: Integer; begin { Find the Shortcut that will replace the longest string from the beginning of Value. Ideally it will replace the whole string. } N := -1; L := 0; for I := 0 to Shortcuts.Count - 1 do begin S := Shortcuts.ValueFromIndex[I]; if Value.StartsWith(S, true) and (L < S.Length) then begin N := I; L := S.Length; end; end; if L > 0 then begin { Replace the Shortcut and remember to add a similar variable. } result := Value.Remove(0, L).Insert(0, '%' + Shortcuts.Names[N] + '%'); Variables.Append(Shortcuts[N]); end else begin { Nothing to compress. } result := Value; end; end; procedure TPathCompressor.Execute(out OldLength, NewLength: Integer); { Returns the old and new length of the PATH variable. } var I: Integer; begin OldLength := OrgLength; for I := 0 to Path.Count - 1 do begin Path[I] := Compress(Path[I]); end; StoreVariables; NewLength := StorePath; end; function TPathCompressor.GetExistingPath(const Prefix: array of string; const RelPath: string): string; var S: string; begin for S in Prefix do begin Result := S + RelPath; if TDirectory.Exists(Result) then Exit; end; Result := ''; end; function TPathCompressor.GetExistingPath(const Prefix, RelPath: string): string; begin Result := GetExistingPath([Prefix], RelPath); end; procedure TPathCompressor.InitShortCuts; begin { Program Files } AddShortCut('PF', PathProgramFiles); AddShortCut('PF86', PathProgramFilesX86); { SQL Server } AddShortCut('SQL', GetExistingPath(PathProgramFiles, '\Microsoft SQL Server')); AddShortCut('SQL86', GetExistingPath(PathProgramFilesX86, '\Microsoft SQL Server')); { TODO : Add folders for missing Delphi versions} { Delphi 7 } AddOldDelphiShortCut('D7', '\Borland\Delphi7', '\Borland\Delphi7\Projects\Bpl'); { Delphi D2007+ } AddDelphiShortCut('D2007', '\CodeGear\RAD Studio\5.0', '\RAD Studio\5.0\Bpl'); AddDelphiShortCut('D2009', '\CodeGear\RAD Studio\6.0', '\RAD Studio\6.0\Bpl'); AddDelphiShortCut('D2010', '\Embarcadero\RAD Studio\7.0', '\RAD Studio\7.0\Bpl'); AddDelphiShortCut('XE', '\Embarcadero\RAD Studio\8.0', '\RAD Studio\8.0\Bpl'); AddDelphiShortCut('XE2', '\Embarcadero\RAD Studio\9.0', '\RAD Studio\9.0\Bpl'); AddDelphiShortCut('XE3', '\Embarcadero\RAD Studio\10.0', '\RAD Studio\10.0\Bpl'); AddDelphiShortCut('XE4', '\Embarcadero\RAD Studio\11.0', '\RAD Studio\11.0\Bpl'); AddDelphiShortCut('XE5', '\Embarcadero\RAD Studio\12.0', '\RAD Studio\12.0\Bpl'); AddDelphiShortCut('XE6', '\Embarcadero\Studio\14.0', '\Embarcadero\Studio\14.0\Bpl'); AddDelphiShortCut('XE7', '\Embarcadero\Studio\15.0', '\Embarcadero\Studio\15.0\Bpl'); end; function TPathCompressor.LoadPath(UserEnvironment: Boolean): Boolean; const cKeyHKLM = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; cKeyHKCU = 'Environment'; var S: string; RegKey: string; begin Path.Clear; if UserEnvironment then begin Registry.RootKey := HKEY_CURRENT_USER; RegKey := cKeyHKCU; end else begin Registry.RootKey := HKEY_LOCAL_MACHINE; RegKey := cKeyHKLM; end; result := Registry.OpenKey(RegKey, false); if result then begin S := ReadRegistry('Path'); Path.DelimitedText := S; FOrgLength := S.Length; end; end; procedure TPathCompressor.LoadShortCuts(const AFileName: string); begin Shortcuts.LoadFromFile(AFileName); end; procedure TPathCompressor.StoreShortCuts(const AFileName: string); begin Shortcuts.SaveToFile(AFileName); end; procedure TPathCompressor.NotifyChanges; { Sending a WM_SETTINGCHANGE message to all top level windows. Otherwise the new environment variables will only be visible after logoff/logon. } begin {$IFDEF DEBUG} Exit; {$ENDIF} SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, NativeInt(PChar('Environment')), SMTO_ABORTIFHUNG, 5000, nil); end; function TPathCompressor.ReadRegistry(const AName: string): string; begin result := Registry.ReadString(AName); end; function TPathCompressor.StorePath: Integer; var S: string; begin S := Path.DelimitedText; WriteRegistry('Path', S); result := S.Length; end; procedure TPathCompressor.StoreVariables; var I: Integer; begin Variables.Sort; for I := 0 to Variables.Count - 1 do begin WriteRegistry(Variables.Names[I], Variables.ValueFromIndex[I]); end; end; procedure TPathCompressor.WriteRegistry(const Name, Value: string); begin {$IFDEF DEBUG} { In DEBUG mode only show the changes that would be made to the registry. } Writeln(Name, '=', Value); Exit; {$ENDIF} if Value.Contains('%') then Registry.WriteExpandString(Name, Value) else Registry.WriteString(Name, Value); end; procedure ShowHelp; var exeName: string; begin exeName := TPath.GetFileNameWithoutExtension(ParamStr(0)); Writeln('Compresses the system and user PATH variable using environment variables.'); Writeln; Writeln(exeName, ' [/F importfile] [/X exportfile]'); Writeln; Writeln(' /F importfile'.PadRight(20), 'load shortcuts from <importfile>'); Writeln(' /X exportfile'.PadRight(20), 'store shortcuts into <exportfile>'); Writeln; Writeln('You must have Administrator rights to change the system PATH variable!'); Writeln; end; procedure Main; var instance: TPathCompressor; lNew: Integer; lOld: Integer; shortCutFileName: string; begin if FindCmdLineSwitch('?') then begin ShowHelp; Exit; end; instance := TPathCompressor.Create; try if FindCmdLineSwitch('f', shortCutFileName) then begin instance.LoadShortCuts(shortCutFileName); end; if FindCmdLineSwitch('x', shortCutFileName) then begin instance.StoreShortCuts(shortCutFileName); end; Write('HKEY_LOCAL_MACHINE: '); if instance.LoadPath(false) then begin instance.Execute(lOld, lNew); Writeln(lOld, ' ==> ', lNew); end else begin Writeln('Could not load path. Try running as administrator.'); end; Write('HKEY_CURRENT_USER: '); if instance.LoadPath(true) then begin instance.Execute(lOld, lNew); Writeln(lOld, ' ==> ', lNew); end else begin Writeln('Could not load path. Try running as administrator.'); end; Write('notifying...'); instance.NotifyChanges; Writeln; finally instance.Free; end; end; begin try Main; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; {$IFDEF DEBUG} { When started from within the IDE we want to see the output. } Write('Press <Enter> to continue...'); Readln; {$ENDIF} end. |
In most cases it should be sufficient to just run the utility as Administrator – otherwise you might not have enough rights to change the registry key in HKEY_LOCAL_SYSTEM. The logic inside InitShortCuts does quite a good job to adapt for different Delphi installations and operating systems. Feel free to add more settings for the missing Delphi versions and leave a comment with your extensions.
The mechanism is not restricted to Delphi pathes and can easily be expanded for other programs. Method InitShortCuts has an example for SQL server and Program Files in general.
If your needs are a little bit more special you can prepare a specific shortcut file that can be loaded via the /F command line parameter. For an example of such a file simply specify the /X command line parameter together with a file name to get the default settings exported.
This program is written with Delphi XE7 and probably uses some language features that may not be available in previous Delphi versions. Nevertheless, it should be not that hard for you to backport those constructs.
Some kind of LZ compression… but for path names!
This was the idea here : http://stackoverflow.com/a/4405155/458259
But using external environment variables make it pretty obfuscated and difficult to guess.
I wonder if using the “short name” of the paths won’t be enough?
See http://msdn.microsoft.com/en-us/library/windows/desktop/aa364989
or http://stackoverflow.com/a/20362922/458259 for a simple .bat which does that.
It should be still readable, easily modifiable, and probably efficient.
One first compression is to remove all of the non-existent directories in my path that had accumulated over time. The .bat of the “short name” SO answer allows this, by the way.
Delphi should stop to mess with the system path, and use the AppPath registry key instead. Delphi developers should do as well.
But asking Delphi developers to learn anything introduced from Windows 2000 onwards is like asking them to learn how to build a lunar rocket…
http://msdn.microsoft.com/en-us/library/windows/desktop/ee872121(v=vs.85).aspx
The AppPath registry key is not used by CreateProcess, only ShellExecute etc, so it isn’t 100% reliable.
If you do use CreateProcess(), you can setup the desired environment yourself. Anyway when Delphi itself is run, it’s usually done via the shell APIs – unless you have your own launcher which uses directly CreateProcess()…
Messing with system-wide settings is a very bad practice. Your application is not the only one installed on a system, and you don’t know what gets installed before and after, leading to potential unwanted behaviours and even security risks if I can make my applications and DLLs be run instead of yours, epsecially since most installer not only modify the system path, but also prepends their paths to the system ones.
“Modernizing” your application means also to write well-behaved applications that respect the *actual* OS rules, and don’t keep using outdated practices used when everything was much simpler and less unsafe.
It’s not only a Delphi fault, there’s a lot of application requiring you to clean the path every time you install them.
Pointless to have these dirs in your path. I remove them all.
Simply remove those useless paths (Unless you really need the command line compiler). If you need additional paths (in delphi for example the good old bpls’s you can extend the (internal) path via delphi itself (options – environment variables – select path – add override (extend it)) I add my own library path to it.
Delphi isn’t the only IDE with problems like this. Visual Studio will fail to even load it the path is too long:
http://stackoverflow.com/questions/12750397/exception-has-been-thrown-by-a-target-of-invocation-when-starting-up-visual-st/12750664#comment38431214_12750664
If you do want command line tools then there’s no point having multiple versions mention on the path. Only one is relevant. Visual Studio gets this right. No mods to path there. Embarcadero are way behind the curve here.
I run into Path-Problems since XE5 and i used this trick, exclude redundant stuff into others variables, but with XE7 I t seems I hit a second barrier, the length of my Path variable is down to 1052 Bytes, but I run constantly into trouble. If I try to open an Project under XE6, which is using Zeos-lib, I get an Runtime Error 236 and then XE6 shuts down (!), Under XE4 I still can open the project but, I can’t run it in the IDE, can’t load SQLite dlls. Making the sqilte entry, the first think in the path variable, let me run it me under XE4 again, but XE6 still crashes with an Runtime Error 236 – by trying to open this project.
What option do I have now? Shot the computer?
XP and 2003 has a max environment block size of 32k. This limitation was lifted since Vista (and that was another good reason to upgrade…)
Now it’s the size of a single environment variable to be limited to 32k – but that also mean that applications should be able to manage them, some older applications may not.
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682653(v=vs.85).aspx