Search code examples
javajarinstallationinno-setupupgrade

Removing files installed by previous version in Inno Setup


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.

Is there any best practice to avoid this kind of troubles? Do other installers take care of these headaches? Thanks.


Solution

  • 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: "lib\dependency-A-1.2.3.jar"; Dest: "{app}\lib"
    Source: "lib\dependency-B-2.3.4.jar"; Dest: "{app}\lib"
    

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

    [Files]
    Source: "lib\dependency-A-1.3.0.jar"; Dest: "{app}"
    Source: "lib\dependency-B-2.3.4.jar"; Dest: "{app}"
    
    [InstallDelete]
    {app}\lib\dependency-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 Delete whole application folder except for "data" subdirectory in Inno Setup. 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('build\exe.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;