Skip to content

Inno Setup: Removing files installed by previous version

I’m using Inno Setup to package a Java application for Windows; the application tree is like this:

|   MyApp.jar
---lib
    |   dependency-A-1.2.3.jar
    |   dependency-B-2.3.4.jar
    |   dependency-Z-x.y.z.jar

I use Ant to prepare the whole tree (all the files and folders) beforehand, including the lib directory (using *.jar wildcard to copy the dependencies), then I simply call ISCC with:

[Files]
Source: "PreparedFolder*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs

Now, I need to cleanup the lib directory everytime the user upgrades the application because I want to remove any obsolete dependencies. I could add the following section to my .iss file:

[InstallDelete]
{app}lib*.jar

but I’m not feeling safe because if a user decides to install the application in an existing folder that contains a not-empty lib subfolder (rare but not impossible), there is a chance that some user files are deleted on upgrade.

Are there some best practices to avoid this kind of troubles? Do other installers take care of these headaches? Thanks.

Answer

You can uninstall the previous version before the installation:


If you cannot do a complete uninstallation, you would have to implement a partial uninstallation.

Ideal would be to reverse-engineer the uninstaller log (unins000.dat), extract only installations to the lib subfolder and process (undo) them. But as that is an undocumented binary file, it can be difficult to do.


If you maintain an explicit list of files to be installed in the [Files] section, like

[Files]
Source: "libdependency-A-1.2.3.jar"; Dest: "{app}lib"
Source: "libdependency-B-2.3.4.jar"; Dest: "{app}lib"

then whenever a dependency changes, move the previous version to the [InstallDelete] section:

[Files]
Source: "libdependency-A-1.3.0.jar"; Dest: "{app}"
Source: "libdependency-B-2.3.4.jar"; Dest: "{app}"

[InstallDelete]
{app}libdependency-A-1.2.3.jar

If you install the dependencies using a wildcard,

[Files]
Source: "lib*.jar"; Dest: "{app}lib"

and you cannot reverse-engineer the uninstaller log, you would have to replicate its functionality by your own means.

You can use a preprocessor to generate a file with installed dependencies. Install that file to the {app} folder and process the file before installation.

[Files]
Source: "MyApp.jar"; DestDir: "{app}"
Source: "lib*.jar"; DestDir: "{app}lib"

#define ProcessFile(Source, FindResult, FindHandle) 
    Local[0] = FindGetFileName(FindHandle), 
    Local[1] = Source + "\" + Local[0], 
    Local[2] = FindNext(FindHandle), 
    "'" + Local[0] + "'#13#10" + 
        (Local[2] ? ProcessFile(Source, Local[2], FindHandle) : "")

#define ProcessFolder(Source) 
    Local[0] = FindFirst(Source + "\*.jar", faAnyFile), 
    ProcessFile(Source, Local[0], Local[0])

#define DepedenciesToInstall ProcessFolder("lib")
#define DependenciesLog "{app}dependencies.log"

[UninstallDelete]
Type: files; Name: "{#DependenciesLog}"
[Code]

procedure CurStepChanged(CurStep: TSetupStep);
var
  AppPath, DependenciesLogPath: string;
  Dependencies: TArrayOfString;
  Count, I: Integer;
begin
  DependenciesLogPath := ExpandConstant('{#DependenciesLog}');

  if CurStep = ssInstall then
  begin
    // If dependencies log already exists, 
    // remove the previously installed dependencies
    if LoadStringsFromFile(DependenciesLogPath, Dependencies) then
    begin
      Count := GetArrayLength(Dependencies);
      Log(Format('Loaded %d dependencies, deleting...', [Count]));
      for I := 0 to Count - 1 do
        DeleteFile(ExpandConstant('{app}lib' + Dependencies[I]));
    end;
  end
    else
  if CurStep = ssPostInstall then
  begin
    // Now that the app folder already exists,
    // save dependencies log (to be processed by future upgrade)
    if SaveStringToFile(DependenciesLogPath, {#DepedenciesToInstall}, False) then
    begin
      Log('Created dependencies log');
    end
      else
    begin
      Log('Failed to create dependencies log');
    end;
  end;
end;

Another approach is to delete all files in the installation folder that is not installed by the latest installer.

The easiest solution is to delete all files in the installation folder before the installation.

You can use [InstallDelete] section for that. But if you have some folder/files with configuration in the installation folder, it won’t allow you to exclude them.

You can code that Pascal Scripting instead. See Inno Setup – Delete whole application folder except for data subdirectory. You can call the DelTreeExceptSavesDir function from my answer to the that question from CurStepChanged(ssInstall) event function:

procedure CurStepChanged(CurStep: TSetupStep);
begin
  if CurStep = ssInstall then
  begin
    DelTreeExceptSavesDir(WizardDirValue); 
  end;
end;

If you really want to delete only obsolete files, to avoid deleting and re-creating existing files, you can use preprocessor to generate a list of files to be installed for the Pascal Scripting and use that to delete only really obsolete files.

#pragma parseroption -p-

#define FileEntry(DestDir) 
    "  FilesNotToBeDeleted.Add('" + LowerCase(DestDir) + "');n"

#define ProcessFile(Source, Dest, FindResult, FindHandle) 
    FindResult 
        ? 
            Local[0] = FindGetFileName(FindHandle), 
            Local[1] = Source + "\" + Local[0], 
            Local[2] = Dest + "\" + Local[0], 
            (Local[0] != "." && Local[0] != ".." 
                ? FileEntry(Local[2]) + 
                  (DirExists(Local[1]) ? ProcessFolder(Local[1], Local[2]) : "") 
                : "") + 
            ProcessFile(Source, Dest, FindNext(FindHandle), FindHandle) 
        : 
            ""

#define ProcessFolder(Source, Dest) 
    Local[0] = FindFirst(Source + "\*", faAnyFile), 
    ProcessFile(Source, Dest, Local[0], Local[0])

#pragma parseroption -p+
[Code]

var
  FilesNotToBeDeleted: TStringList;

function InitializeSetup(): Boolean;
begin
  FilesNotToBeDeleted := TStringList.Create;
  FilesNotToBeDeleted.Add('data');
  {#Trim(ProcessFolder('buildexe.win-amd64-3.6', ''))}
  FilesNotToBeDeleted.Sorted := True;

  Result := True;
end;

procedure DeleteObsoleteFiles(Path: string; RelativePath: string);
var
  FindRec: TFindRec;
  FilePath: string;
  FileRelativePath: string;
begin
  if FindFirst(Path + '*', FindRec) then
  begin
    try
      repeat
        if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
        begin
          FilePath := Path + '' + FindRec.Name;
          FileRelativePath := RelativePath + '' + FindRec.Name;
          if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0 then
          begin
            DeleteObsoleteFiles(FilePath, FileRelativePath);
          end;

          if FilesNotToBeDeleted.IndexOf(Lowercase(FileRelativePath)) < 0 then
          begin
            if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY <> 0 then
            begin
              if RemoveDir(FilePath) then
              begin
                Log(Format('Deleted obsolete directory %s', [FilePath]));
              end
                else
              begin
                Log(Format('Failed to delete obsolete directory %s', [FilePath]));
              end;
            end
              else
            begin
              if DeleteFile(FilePath) then
              begin
                Log(Format('Deleted obsolete file %s', [FilePath]));
              end
                else
              begin
                Log(Format('Failed to delete obsolete file %s', [FilePath]));
              end;
            end;
          end;
        end;
      until not FindNext(FindRec);
    finally
      FindClose(FindRec);
    end;
  end
    else
  begin
    Log(Format('Failed to list %s', [Path]));
  end;
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
  if CurStep = ssInstall then
  begin
    Log('Looking for obsolete files...');
    DeleteObsoleteFiles(WizardDirValue, '');
  end;
end;