Talk:Uninstall only installed files: Difference between revisions
No edit summary |
m (Reverted edits by 91.236.74.139 to last version by Campg2003) |
||
(5 intermediate revisions by 5 users not shown) | |||
Line 36: | Line 36: | ||
# templates for the output | # templates for the output | ||
inst_dir_tpl = ' SetOutPath $INSTDIR%s' | inst_dir_tpl = ' SetOutPath "$INSTDIR%s"' | ||
inst_file_tpl = ' File ${FILES_SOURCE_PATH}%s' | inst_file_tpl = ' File "${FILES_SOURCE_PATH}%s"' | ||
uninst_file_tpl = ' Delete $INSTDIR%s' | uninst_file_tpl = ' Delete "$INSTDIR%s"' | ||
uninst_dir_tpl = ' RMDir $INSTDIR%s' | uninst_dir_tpl = ' RMDir "$INSTDIR%s"' | ||
# check args | # check args | ||
Line 107: | Line 107: | ||
print >> ih, inst_dir_tpl % my_dir | print >> ih, inst_dir_tpl % my_dir | ||
for f in my_files: | for f in my_files: | ||
print >> ih, inst_file_tpl % my_dir+os.sep+f | print >> ih, inst_file_tpl % (my_dir+os.sep+f) | ||
counter_files += 1 | counter_files += 1 | ||
print >> ih, " " | print >> ih, " " | ||
Line 120: | Line 120: | ||
for (my_files, my_dir) in stack_of_visited: | for (my_files, my_dir) in stack_of_visited: | ||
for f in my_files: | for f in my_files: | ||
print >> uh, uninst_file_tpl % my_dir+os.sep+f | print >> uh, uninst_file_tpl % (my_dir+os.sep+f) | ||
print >> uh, uninst_dir_tpl % my_dir | print >> uh, uninst_dir_tpl % my_dir | ||
print >> uh, " " | print >> uh, " " | ||
Line 168: | Line 168: | ||
</highlight-nsis> | </highlight-nsis> | ||
---- | |||
There is actually another solution, but I don't really have time to do a formal write-up right now. Essentially, however, the idea is to dump the console log to a file, and parse that. This means you can still use all the regular File functions without calling a special function, and without needing to know the source files in advance. | |||
In a nutshell, parse through the console log dump looking for "Output folder: " and "Extract: ". For each output folder, push the folder location on the stack. For each file extraction, just delete the file. Once done deleting files, create a loop that pops the stack and delete the folder, halting once the popped value is no longer an output folder (and push it back on the stack). | |||
There are still some snags, of course, such as having to make sure that all folders are reported, and when doing so manually that you do not report them in inverse hierarchical sequence, but that's not that big of a problem. | |||
The question is, really, whether the existing method described in this wiki page is really so bad... yes, you have to know the files in advance, so anytime there's a file change you have to adjust the installer as well - but it does also prevent files from accidentally getting included, and you always know exactly what files are in the installer and where they are getting extracted to. | |||
---- | |||
Here's VB script that does essentially the same thing, so it runs fine on any windows machine (no need for python). It recursively generates a list of files and directories to add and remove. I've only semi-tested it on my installation, so take the script with a grain of salt... | |||
<highlight-vb> | |||
sub dorecurse(dpath, spath) | |||
if fs.folderexists(spath) then | |||
out1.writeline "SetOutPath """ & dpath & """" | |||
set d = fs.getfolder(spath) | |||
for each f in d.files | |||
str = "File """ & f.Path & """" | |||
out1.writeline str | |||
str = "Delete """ & dpath & "\" & f.Name & """" | |||
out2.writeline str | |||
next | |||
for each d2 in d.subfolders | |||
str = "CreateDirectory """ & dpath & "\" & d2.Name & """" | |||
out1.writeline str | |||
dorecurse dpath & "\" & d2.Name, spath & "\" & d2.Name | |||
str = "RMDir """ & dpath & "\" & d2.Name & """" | |||
out2.writeline str | |||
next | |||
end if | |||
end sub | |||
set fs = createobject("Scripting.FileSystemObject") | |||
instdir = wscript.arguments(0) | |||
rootdir = wscript.arguments(1) | |||
filename1 = wscript.arguments(2) | |||
filename2 = wscript.arguments(3) | |||
set out1 = fs.createtextfile(filename1, true) | |||
set out2 = fs.createtextfile(filename2, true) | |||
dorecurse instdir, rootdir | |||
out1.close | |||
out2.close | |||
</highlight-vb> | |||
Your nsis script looks something like this: | |||
<highlight-nsis> | |||
Section "Main (required)" | |||
SectionIn RO | |||
!system 'cscript /nologo listfiles.vbs "$INSTDIR" "C:\somedir" "list.txt" "unlist.txt"' | |||
!include list.txt | |||
!system 'del list.txt' | |||
WriteUninstaller "uninstall.exe" | |||
SectionEnd | |||
Section "Uninstall" | |||
!include "unlist.txt" | |||
!system 'del unlist.txt' | |||
SectionEnd | |||
</highlight-nsis> | |||
== Modified script, date/size stamp, multiple install logs == | |||
This is a modification of the script on this page as of 8/4/2011. It doesn't contain the INI file macros, although they could easily be added. I haven't looked at the modification to add rmdir to see if it makes sense or not. I replaced sections with macros because it fit into the project I was writing at the time. It also supports the ability to write multiple install logs, and to warn about files that have been changed since they were installed. I also added documentation and an test script. | |||
===Modifications=== | |||
* In UninstallLog changed SetOutPath so that it doesn't log the path if it already exists. | |||
* In WriteRegDWORD changed WriteRegStr to WriteRegDWORD. | |||
* Converted uninstallLog to check file date and size of selected files and offer to not uninstall if files have been changed. | |||
* Made uninstall code into a function, moved to end of header. | |||
* Moved close and delete of log file to right after it has been read. This allows the INSTDIR to be removed. | |||
* Made section -openlogfile into macro UNINSTLOG_OPENINSTALL. | |||
* Added macro UNINSTLOG_CLOSEINSTALL to close log file, don't think it was done in original code. | |||
* Added !ifndef INSTALLLOGINCLUDED around header file contents. | |||
* In the uninstall section registry key removal code changed UNINSTALLPATH to REG_UNINSTALL_PATH. Added !ifdefs so that if this or REG_APP_PATH are not defined, code for deleting the respective registry paths is not executed. | |||
* Made open install code into a function with macro that calls it. | |||
* Changed all macros so that they don't try to write if the log file is closed. You can now disable logging by not calling INSTALLOPEN. | |||
* Added define UNINSTLOGDEBUG. | |||
* Added variable $UninstLogAlwaysLog to log files even if they already exist, also Macro AddItemAlways. | |||
===Documentation=== | |||
(for version 0.0.2 dated 7/27/2012) | |||
This header file supports the ability to uninstall only the installed files. | |||
It expects the following defines: | |||
REG_ROOT, REG_APP_PATH, and REG_UNINSTALL_PATH. If either of the latter two are undefined these registry keys won't be deleted even if they appear in the log file. | |||
====To use:==== | |||
* !include this file at the top of your scrip. | |||
(Note: it gets an error if it is before "SetCompressor /SOLID LZMA": "Error: can't change compressor after data already got compressed or header already changed!".) | |||
* Define REG_ROOT, and REG_APP_PATH and/or REG_UNINSTALL_PATH if you want to use ${WriteRegStr} or ${WriteRegDWORD}. | |||
Start and end each install section like this: | |||
<highlight-nsis> | |||
section "Install section" | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
;Install section code | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
sectionend | |||
</highlight-nsis> | |||
When the log is closed the commands will be executed but not logged. | |||
If you want the $INSTDIR to be removed automatically, you need to place | |||
${AddItemAlways} "$INSTDIR" | |||
after you initially open the log. This is because the $INSTDIR is already created at the time INSTLOG_OPENINSTALL is called, so it won't automatically be logged. UNINSTLOG_OPENINSTALL does not automatically log it so that you don't add a directory automatically if you close and reopen a new install block. (Automatically writing $INSTDIR on open makes a messy log but it might be okay since there are still files in the directory.) If you run the installer again to add additional files, it will again write the $INSTDIR to the log. | |||
In your uninstall section do: | |||
<highlight-nsis> | |||
!insertmacro UNINSTLOG_UNINSTALL | |||
</highlight-nsis> | |||
Note that this will push one entry on the stack for every entry in the log. | |||
The following commands are provided, most of them simple forms of the similar NSIS commands: | |||
* ${AddItem} Path -- adds a file or directory when the provided commands won't do the job. Does not add the item if it exists, so you need to call this before the command that creates it. | |||
* ${AddItemAlways} -- Like AddItem but adds item even if it exists. | |||
* ${File} Path FileName -- path is path on source machine, must be empty or end with backslash. | |||
* ${CreateShortcut} FilePath FilePointer Parameters Icon IconIndex -- create shortcut FilePath that links to FilePointer. | |||
* ${CopyFiles} source Dest - use full paths. | |||
* ${Rename} Source Dest - use full paths. | |||
* ${CreateDirectory} Path | |||
* ${SetOutPath} Path | |||
* ${WriteUninstaller} Path -- should use a fully-qualified path to make the logged value right. | |||
* ${WriteRegStr} Root path Key Value-- path is the subkey, must be the value of either ${rEG_APP_PATH} or ${REG_UNINSTALL_PATH), which must be defined prior to use. | |||
* ${WriteRegDWORD} -- see ${WriteRegStr} | |||
* ${AddItemDated} -- same as AddItem but includes the date/time and size of the installed file so that the uninstaller can detect if files are modified. | |||
* ${FileDated} -- same as ItemDated for the ${File} command. | |||
Uses the following additional defines: | |||
* UninstLog -- name of the log file, defaults to uninstall.log. You can define this just before calling UNINSTLOG_OPENINSTALL and UNINSTLOG_UNINSTALL to change the name of the log file. You could even create separate logs. You would then need to do more than one UNINSTLOG_UNINSTALL for each log file. The name will stay defined until you change it. | |||
* UNINSTLOGDSEP -- separator used to separate file path and date-size for ${FileDated} and $ItemDated} macros (|) | |||
* INSTLOGDEBUG -- Activates debugging messages if defined. | |||
and variables: | |||
* $UninstLog -- handle of log file, empty if log closed. When the log is closed commands are executed but not logged. | |||
* $UninstLogAlwaysLog -- Normally a file is not logged if it exists on the target system. You can cause files to always be logged even if they exist by setting the variable $UninstLogAlwaysLog to nonempty: | |||
<highlight-nsis> | |||
section "something" | |||
;... | |||
StrCpy $UninstLogAlwaysLog 1 | |||
${File} "" "MyFile.txt" ; will be logged even if it exists. | |||
${FileDated} "" "AnotherFile" ;so will this one. | |||
StrCpy $UninstLogAlwaysLog "" ; turn it off | |||
${File} "" "filexxx" ; won't be logged if it exists. | |||
;... | |||
sectionend | |||
</highlight-nsis> | |||
Note that this header uses strfunc.nsh function ${UnStrTok}. If you also include strfunc.nsh and use ${UnstrTok} after this file you can check to see if it has been initialized by: | |||
<highlight-nsis> | |||
!ifndef UnStrTokINCLUDED | |||
${UnStrTok} | |||
!endif | |||
</highlight-nsis> | |||
(This was discovered by inspection in strfunc.nsh v1.09.) | |||
====A note about wildcards==== | |||
Although you can use wildcards in ${File}, ${Rename}, and ${CopyFiles}, it could be dangerous and is not recommended unless you are sure you know what you are doing. Suppose you do ${File} "*.txt" where the source directory has a few text files, but the directory into which they are installed already contains text files. The value written to the log will be "*.txt", and this will cause the uninstaller to uninstall all of the .txt files in the folder, including the ones that were already installed. You can't use wildcards at all with ${FileDated}, although there is no check for this. (I need a way to run FindFirst/FindNext on the source system to do this. See above for an alternative.) | |||
The project for which I modified this header file uses ${AddItem}, ${AddItemAlways}, ${AddItemDated}, ${File}, ${FileDated}, ${CreateDirectory}, ${CreateShortcut}, ${SetOutPath}, and ${WriteUninstaller}. I have retained the other commands but I have not tested them much. | |||
====Bugs/enhancements==== | |||
* Issue with ${AddItemDated}: If ${AddItemDated} follows the command that creates its file then it won't log unless UninstLogAlwaysLog is on. If it precedes the command, the date-size stamp will be empty because it doesn't exist yet. It was not modified to always log regardless of the value of $UninstLogAlwaysLog to make it clear that it is set. | |||
* Issue with ${WriteReg...} macros: For these macros to work REG_APP_PATH and REG_UNINSTALL_PATH must be defined, and these are the only registry paths that can be written. This means that you must define these and use them in the path of any ${WriteReg...} calls to make them be deleted. If there were an installed file with the same name as these values that was installed but had been deleted before the uninstall, it could cause these keys to be deleted from the registry unintentionally. (I presume that's why the uninstaller code checks for these values before removing them-- otherwise any file in the log that doesn't exist on the system when uninstalled would be interpreted as a registry key to delete.) | |||
* To do: make all macros, variables, and !defines defined by this header start with "UninstLog" to avoid name collisions with other header files. | |||
===The script=== | |||
<highlight-nsis> | |||
;uninstlog.nsh | |||
/* | |||
Adapted by GaryC from code from http://nsis.sourceforge.net/Uninstall_only_installed_files by Afrow UK with modifications by others, taken 8/3/11. | |||
Version 0.0.2 | |||
Last modified 7/27/2012 | |||
Modifications: | |||
7/27/12 by GaryC: Added display of time stamps in modified file message. | |||
Moved documentation to another file. | |||
Removed example script (provided by testuninstlog.nsh). | |||
7/25/12 by GaryC: Updated documentation. | |||
Added version number. | |||
Macro AddItemAlways added sometime earlier, probably before or around 8/15/11. | |||
8/15/11 by GaryC: Added note about AddItem needing to be called before the command it applies to. | |||
8/15/11 by GaryC: | |||
Added file existence checks in more macros. Added check of $UninstLogAlwaysLog to macros that write files. | |||
Added note about not working before SetCompressor /SOLID LZMA. | |||
8/15/11 Initial modifications from WIKI code by GaryC: | |||
In UninstallLog changed SetOutPath so that it doesn't log the path if it already exists. | |||
In WriteRegDWORD changed WriteRegStr to WriteRegDWORD. | |||
Converted uninstallLog to check file date and size of selected files and offer to not uninstall if files have been changed. | |||
Made uninstall code into a function, moved to end of header. | |||
Moved close and delete of log file to right after it has been read. This allows the INSTDIR to be removed. | |||
Made section -openlogfile into macro UNINSTLOG_OPENINSTALL. | |||
Added macro UNINSTLOG_CLOSEINSTALL to close log file, don't think it was done in original code. | |||
Added !ifndef INSTALLLOGINCLUDED around header file contents. | |||
In the uninstall section registry key removal code changed UNINSTALLPATH to REG_UNINSTALL_PATH. Added !ifdefs so that if this or REG_APP_PATH are not defined, code for deleting the respective registry paths is not executed. | |||
Made open install code into a function with macro that calls it. | |||
Changed all macros so that they don't try to write if the log file is closed. You can now disable logging by not calling INSTALLOPEN. | |||
Added initialization call for ${UnStrTok}. Added define UNINSTLOGDEBUG. | |||
Commented out section to open uninstall log. | |||
Added variable $UninstLogAlwaysLog to log files even if they already exist. | |||
Added documentation and example script. | |||
*/ | |||
!ifndef UNINSTALLLOGINCLUDED | |||
!define UNINSTALLLOGINCLUDED | |||
!define UNINSTLOGDEBUG | |||
!include "strfunc.nsh" | |||
!include "filefunc.nsh" | |||
;-------------------------------- | |||
; Configure UnInstall log to only remove what is installed | |||
;-------------------------------- | |||
;The symbol that separates the date-size stamp from the file name. | |||
!define UNINSTLOGDSEP | | |||
Var UninstLog ; handle of log file | |||
Var UninstLogAlwaysLog ;If nonempty, FileDated logs the file even if it exists. | |||
;Ex: | |||
;StrCpy $UninstLogAlwaysLog 1 | |||
;${FileDated} "" "something" | |||
;StrCpy $UninstLogAlwaysLog "" ;turn it back off. | |||
;Uninstall log file missing. | |||
LangString UninstLogMissing ${LANG_ENGLISH} "${UninstLog} not found!$\r$\nUninstallation cannot proceed!" | |||
LangString UninstLogModified ${LANG_ENGLISH} "File $R0 has been modified since it was installed. Do you want to delete it?$\r$\nOriginal: $R3$\r$\nCurrent: $R4" | |||
LangString UninstLogShowDateSize ${LANG_ENGLISH} "$1 UTC $2 bytes" | |||
;We need to make sure these functions haven't already been initialized outside this header. Not documented, found by inspection. | |||
;!ifndef StrTokINCLUDED | |||
;${StrTok} | |||
;!endif | |||
!ifndef UnStrTokINCLUDED | |||
${UnStrTok} | |||
!endif | |||
;AddItem macro-- Writes an item to the log, for times when you need options the macros don't support. | |||
!macro AddItem Path | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${Path}" +3 ;if it exists we don't log it. | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${Path}$\r$\n" | |||
!macroend | |||
;AddItemAlways - Like AddItem but turns on $UninstLogAlwaysLog and restores it afterwards. | |||
!macro AddItemAlways Path | |||
push $UninstLogAlwaysLog | |||
StrCpy $UninstLogAlwaysLog "1" | |||
!insertmacro AddItem ${Path} | |||
pop $UninstLogAlwaysLog | |||
!macroend | |||
;AddItemDated macro. like AddItem but allows you to add date and size information to the entry so the uninstaller can tell if the file has been modified. | |||
;Writes path with date-size appended. path must exist. | |||
!macro AddItemDated Path | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${Path}" +9 ;if it exists we don't log it. | |||
StrCmp $UninstLog "" +8 ;bail if uninstall log closed | |||
push $0 | |||
push $1 | |||
strCpy $0 ${Path} | |||
call UninstLogMakeDateSize | |||
FileWrite $UninstLog "${Path}${UNINSTLOGDSEP}$1$\r$\n" | |||
pop $1 | |||
pop $0 | |||
!macroend | |||
;Consider ItemDated2 macro that would receive path Date (string containing YYYYMMDDhhmmss) size (string containing number of bytes in decimal). | |||
;File macro | |||
;Filepath is path on machine generating installer, must be empty or terminated with backslash. | |||
;Use regular file command and AddItem macro for anything more exhotic. | |||
!macro File FilePath FileName | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "$OUTDIR\${FileName}" +3 | |||
StrCmp $UninstLog "" +2 | |||
;detailprint "File: checking existence of $OUTDIR\${FileName}, $$UninstLog=$UninstLog" ; debug | |||
;IfFileExists "$OUTDIR\${FileName}" +4 ; debug | |||
;StrCmp $UninstLog "" +3 ; debug | |||
;detailprint "File: logging $OUTDIR\${FileName} to $UninstLog" | |||
FileWrite $UninstLog "$OUTDIR\${FileName}$\r$\n" | |||
;detailprint "File: executing File for ${FilePath}${FileName}" | |||
File "${FilePath}${FileName}" | |||
!macroend | |||
;FileDated macro | |||
;If $UninstLogAlwaysLog is nonempty, this will log the entry even if it exists on the target machine, which means it will be removed when uninstalled. Otherwise it will not be logged if it exists. | |||
!macro FileDated FilePath FileName | |||
push $0 | |||
push $1 | |||
push $2 | |||
StrCpy $2 "" | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "$OUTDIR\${FileName}" +3 ;if it exists we don't log it. | |||
strCmp $UninstLog "" +2 ; if log file not opened don't log | |||
strCpy $2 1 ;set flag to log file | |||
File "${FilePath}${FileName}" | |||
StrCmp $2 "" +4 ;skip logging | |||
StrCpy $0 "$OUTDIR\${FileName}" ;file on target system is here | |||
call UninstLogMakeDateSize | |||
;Write something like Outdir\filename|201108041600005234 | |||
FileWrite $UninstLog "$0${UNINSTLOGDSEP}$1$\r$\n" | |||
pop $2 | |||
pop $1 | |||
pop $0 | |||
!macroend | |||
;$0 - (in) file path (if it is a path it is so on the source system) | |||
;$1 - (out) date-size yyyymmddhhmmsssize. | |||
; We use a macro so we can get an install and uninstall version. Prefix is either "" or "un." | |||
!macro UninstLogInsertMakeDateSize prefix | |||
function ${prefix}UninstLogMakeDateSize | |||
push $R0 | |||
push $R1 | |||
push $R2 | |||
push $R3 | |||
push $R4 | |||
push $R5 | |||
push $R6 | |||
push $R7 | |||
push $R8 | |||
${GetTime} "$0" "MS" $R0 $R1 $R2 $R3 $R4 $R5 $R6 | |||
; Get file size. | |||
FileOpen $R8 "$0" r | |||
FileSeek $R8 0 END $R7 | |||
FileClose $R8 | |||
;return something like 201108041600005234 in $1 | |||
StrCpy $1 "$R2$R1$R0$R4$R5$R6$R7" | |||
pop $R8 | |||
pop $R7 | |||
pop $R6 | |||
pop $R5 | |||
pop $R4 | |||
pop $R3 | |||
pop $R2 | |||
pop $R1 | |||
pop $R0 | |||
functionend | |||
!macroend | |||
!insertmacro UninstLogInsertMakeDateSize "" | |||
!insertmacro UninstLogInsertMakeDateSize "un." | |||
; Produce a string containing display of a stamp returned by UninstLogMakeDateSize. | |||
; Input and output values are on the stack. | |||
function un.UninstLogShowDateSize | |||
exch $0 | |||
push $1 | |||
push $2 | |||
strcpy $1 $0 14 ; copy the time part | |||
strcpy $2 $0 "" 14 ; copy the size (everything after the time) | |||
strcpy $0 "$(UninstLogShowDateSize)" | |||
pop $2 | |||
pop $1 | |||
exch $0 | |||
functionend | |||
;CreateShortcut macro | |||
!macro CreateShortcut FilePath FilePointer Parameters Icon IconIndex | |||
!ifdef UNINSTLOGDEBUG ; debug | |||
StrCpy $0 "doesn't" | |||
IfFileExists "${FilePath}" 0 +2 | |||
StrCpy $0 "does" | |||
DetailPrint 'CreateShortcut: Checking existence of ${FilePath} which $0 exist.' ; debug | |||
!endif ; debug | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${FilePath}" +3 ;if it exists we don't log it. | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${FilePath}$\r$\n" | |||
CreateShortcut "${FilePath}" "${FilePointer}" "${Parameters}" "${Icon}" "${IconIndex}" | |||
!macroend | |||
;Copy files macro | |||
!macro CopyFiles SourcePath DestPath | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${DestPath}" +3 | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${DestPath}$\r$\n" | |||
CopyFiles "${SourcePath}" "${DestPath}" | |||
!macroend | |||
;Rename macro | |||
!macro Rename SourcePath DestPath | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${DestPath}" +3 | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${DestPath}$\r$\n" | |||
Rename "${SourcePath}" "${DestPath}" | |||
!macroend | |||
;CreateDirectory macro | |||
!macro CreateDirectory Path | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${Path}\*.*" +3 ;if it exists we don't log it. | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${Path}$\r$\n" | |||
CreateDirectory "${Path}" | |||
!macroend | |||
/* | |||
;SetOutPath macro | |||
; WARNING: If Path already exists the uninstaller will delete it.--GaryC | |||
!macro SetOutPath Path | |||
SetOutPath "${Path}" | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${Path}$\r$\n" | |||
!macroend | |||
*/ | |||
;SetOutPath macro | |||
;Modified to not log Path if it already exists.--GaryC | |||
;If you use this macro, the path you specify will be removed by the uninstaller if it does not already exist! | |||
!macro SetOutPath Path | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${Path}\*.*" +3 | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${Path}$\r$\n" | |||
SetOutPath "${Path}" | |||
!macroend | |||
;WriteUninstaller macro | |||
!macro WriteUninstaller Path | |||
StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. | |||
IfFileExists "${Path}" +3 ;if it exists we don't log it. | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${Path}$\r$\n" | |||
WriteUninstaller "${Path}" | |||
!macroend | |||
;WriteRegStr macro | |||
!macro WriteRegStr RegRoot UnInstallPath Key Value | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${RegRoot} ${UnInstallPath}$\r$\n" | |||
WriteRegStr "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" | |||
!macroend | |||
;WriteRegDWORD macro | |||
;WARNING: This writes spaces between items while WriteRegStr does not.--GaryC | |||
!macro WriteRegDWORD RegRoot UnInstallPath Key Value | |||
StrCmp $UninstLog "" +2 | |||
FileWrite $UninstLog "${RegRoot} ${UnInstallPath}$\r$\n" | |||
;WriteRegStr "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" | |||
WriteRegDWord "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" | |||
!macroend | |||
;Defines for commands | |||
;AddItem macro | |||
!define AddItem "!insertmacro AddItem" | |||
;AddItemAlways macro | |||
!define AddItemAlways "!insertmacro AddItemAlways" | |||
;AddItemDated macro | |||
!define AddItemDated "!insertmacro AddItemDated" | |||
;File macro | |||
!define File "!insertmacro File" | |||
;FileDated macro | |||
!define FileDated "!insertmacro FileDated" | |||
;CreateShortcut macro | |||
!define CreateShortcut "!insertmacro CreateShortcut" | |||
;Copy files macro | |||
!define CopyFiles "!insertmacro CopyFiles" | |||
;Rename macro | |||
!define Rename "!insertmacro Rename" | |||
;CreateDirectory macro | |||
!define CreateDirectory "!insertmacro CreateDirectory" | |||
;SetOutPath macro | |||
!define SetOutPath "!insertmacro SetOutPath" | |||
;WriteUninstaller macro | |||
!define WriteUninstaller "!insertmacro WriteUninstaller" | |||
;WriteRegStr macro | |||
!define WriteRegStr "!insertmacro WriteRegStr" | |||
;WriteRegDWORD macro | |||
!define WriteRegDWORD "!insertmacro WriteRegDWORD" | |||
;Need to invoke before items are logged. | |||
!macro UNINSTLOG_OPENINSTALL | |||
;Set the name of the uninstall log | |||
!ifndef UninstLog | |||
;Default value if not defined outside. | |||
!define UninstLog "uninstall.log" | |||
!endif | |||
!ifdef UNINSTLOGDEBUG | |||
!echo "Opening ${UninstLog} at line ${__LINE__}" | |||
!endif | |||
push $0 | |||
StrCpy $0 "${UninstLog}" | |||
call __UninstLogOpenInstall | |||
pop $0 | |||
!macroend | |||
; $0 -- path/filename of uninstall log. | |||
function __UninstLogOpenInstall | |||
push !1 | |||
StrCpy $1 "" ;should we log $INSTDIR? | |||
IfFileExists $INSTDIR +2 | |||
StrCpy $1 1 ;Doesn't exist, log it. | |||
CreateDirectory "$INSTDIR" | |||
IfFileExists "$INSTDIR\$0" LogAppend | |||
!ifdef UNINSTLOGDEBUG | |||
detailprint "Opening $0" | |||
!endif | |||
FileOpen $UninstLog "$INSTDIR\$0" w | |||
GoTo Opened | |||
LogAppend: | |||
!ifdef UNINSTLOGDEBUG | |||
detailprint "Opening $0 for append" | |||
!endif | |||
SetFileAttributes "$INSTDIR\$0" NORMAL | |||
FileOpen $UninstLog "$INSTDIR\$0" a | |||
FileSeek $UninstLog 0 END | |||
Opened: | |||
IntCmp $1 0 End | |||
${AddItem} "$INSTDIR" | |||
End: | |||
pop $1 | |||
functionend ; __UninstLogOpenInstall | |||
;Need to invoke at end of installation. | |||
!macro UNINSTLOG_CLOSEINSTALL | |||
FileClose $UninstLog | |||
StrCpy $UninstLog "" | |||
!ifdef UNINSTLOGDEBUG | |||
!echo "Closing install log at line ${__LINE__}" | |||
detailprint "Closing install log at line ${__LINE__}" | |||
!endif | |||
!macroend | |||
; $0 -- name of uninstall log file. | |||
function un.UninstLogUninstall | |||
;Can't uninstall if uninstall log is missing! | |||
IfFileExists "$INSTDIR\$0" +3 | |||
MessageBox MB_OK|MB_ICONSTOP "$(UninstLogMissing)" | |||
Abort | |||
Push $1 | |||
Push $R0 | |||
Push $R1 | |||
Push $R2 | |||
push $R3 | |||
push $R4 | |||
SetFileAttributes "$INSTDIR\$0" NORMAL | |||
FileOpen $UninstLog "$INSTDIR\$0" r | |||
;Set $OUTDIR to something we aren't going to remove so we can delete $INSTDIR. This works because all of the paths in the log are absolute. | |||
SetOutPath $PROGRAMFILES | |||
;Read in the uninstall log and put it on the stack. | |||
StrCpy $R1 -1 ; line count | |||
GetLineCount: | |||
ClearErrors | |||
FileRead $UninstLog $R0 | |||
IntOp $R1 $R1 + 1 | |||
StrCpy $R0 $R0 -2 ; remove $|R$\N | |||
Push $R0 | |||
IfErrors 0 GetLineCount | |||
FileClose $UninstLog | |||
Delete "$INSTDIR\$0" | |||
Pop $R0 | |||
!ifdef UNINSTLOGDEBUG | |||
DetailPrint "Read $R1 log entries" ; debug | |||
!endif | |||
LoopRead: | |||
StrCmp $R1 0 LoopDone | |||
Pop $R0 ; log entry | |||
IfFileExists "$R0\*.*" 0 NotDir | |||
!ifdef UNINSTLOGDEBUG ; debug | |||
DetailPrint "Attempting to remove directory $R0" ; debug | |||
!endif ; debug | |||
RMDir $R0 #is dir | |||
!ifdef UNINSTLOGDEBUG ; debug | |||
IfErrors 0 +2 ; debug | |||
DetailPrint "Error after trying to remove directory $0" ; debug | |||
!endif ; debug | |||
Goto LoopNext | |||
NotDir: | |||
${UnStrTok} $R2 $R0 ${UNINSTLOGDSEP} 1 0 ; date/size, 2nd token | |||
${UnStrTok} $R0 "$R0" ${UNINSTLOGDSEP} 0 0 ;remove date/size from path. | |||
!ifdef UNINSTLOGDEBUG | |||
DetailPrint "After separating time stamp, time stamp=$R2, file=$R0" ; debug | |||
!endif | |||
StrCmp $R2 "" NoDateSize ;Skip call if no timestamp | |||
push $0 | |||
StrCpy $0 $R0 | |||
Call un.UninstLogMakeDateSize | |||
pop $0 | |||
;$1 contains date + size from file, $R2 is same from log entry. | |||
NoDateSize: | |||
IfFileExists $R0 0 NotFile | |||
StrCmp $R2 "" NoDateSize2 ;If this log entry had no date-size, skip compare | |||
!ifdef UNINSTLOGDEBUG | |||
DetailPrint "UninstLog: file $0 has time stamp $1, entry stamp is $R2" ; debug | |||
!endif | |||
StrCmp $R2 $1 DateSizeMatch | |||
push $R2 ; log entry stamp | |||
call un.UninstLogShowDateSize | |||
pop $R3 ; display of log entry stamp | |||
push $1 ; current stamp | |||
call un.UninstLogShowDateSize | |||
pop $R4 ; current file stamp | |||
MessageBox MB_YESNO $(UninstLogModified) IDNO NoDelete | |||
pop $2 | |||
pop $1 | |||
DateSizeMatch: | |||
NoDateSize2: | |||
Delete $R0 #is file | |||
NoDelete: | |||
Goto LoopNext | |||
NotFile: | |||
!ifdef REG_APP_PATH | |||
StrCmp $R0 "${REG_ROOT} ${REG_APP_PATH}" 0 NotRegAppPath | |||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" #is Reg Element | |||
Goto LoopNext | |||
NotRegAppPath: | |||
!endif ; REG_APP_PATH | |||
!ifdef REG_UNINSTALL_PATH | |||
StrCmp $R0 "${REG_ROOT} ${REG_UNINSTALL_PATH}" 0 +2 | |||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" #is Reg Element | |||
!endif ; REG_UNINSTALL_PATH | |||
LoopNext: | |||
IntOp $R1 $R1 - 1 | |||
Goto LoopRead | |||
LoopDone: | |||
pop $R4 | |||
pop $R3 | |||
Pop $R2 | |||
Pop $R1 | |||
Pop $R0 | |||
Pop $1 | |||
;Remove registry keys | |||
;DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" | |||
;DeleteRegKey ${REG_ROOT} "${REG_UNINSTALL_PATH}" | |||
functionend | |||
!macro UNINSTLOG_UNINSTALL | |||
!ifndef UninstLog | |||
;Default value if not defined outside. | |||
!define UninstLog "uninstall.log" | |||
!endif | |||
push $0 | |||
StrCpy $0 "${UninstLog}" | |||
call un.UninstLogUninstall | |||
pop $0 | |||
!macroend | |||
;-- end header file addition | |||
!endif ; UNINSTALLLOGINCLUDED | |||
;--- end uninstalllog.nsh code | |||
</highlight-nsis> | |||
===Test Script/Example=== | |||
<highlight-nsis> | |||
/* | |||
7/24/12 Added section to test ${File} with wildcards. | |||
*/ | |||
!include "uninstlog.nsh" | |||
!define REG_ROOT "HKLM" | |||
!define REG_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\testuninstlog" | |||
!define SHORTCUTNAME "Uninstall testuninstlog" | |||
installdir "$PROGRAMFILES\testuninstlog" | |||
OutFile "testuninstlog.exe" | |||
Name "Test Uninstlog.nsh" | |||
RequestExecutionLevel user | |||
ShowInstDetails show | |||
ShowUninstDetails show | |||
ComponentText "This demonstrates the uninstlog.nsh header file which provides logging of installed files. File uninstlog.nsh has a date stamp. The shortcut ${SHORTCUTNAME} on the desktop will uninstall this test." | |||
page components | |||
page Directory | |||
page InstFiles | |||
UninstPage uninstConfirm | |||
UninstPage instfiles | |||
section "-install" | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
${AddItemAlways} "$INSTDIR" | |||
${SetOutPath} "$INSTDIR" | |||
${FileDated} "" "uninstlog.nsh" | |||
${File} "${NSISDIR}\Include\" "strfunc.nsh" | |||
${CreateShortcut} "$DESKTOP\${SHORTCUTNAME}.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 | |||
${WriteUninstaller} "$INSTDIR\uninstall.exe" | |||
${WriteRegStr} ${rEG_ROOT} "${REG_UNINSTALL_PaTH}" "UninstallString" "$INSTDIR\uninstall.exe" | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
sectionend | |||
section /O "Optional stuff, copy and rename" | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
${CreateDirectory} "$INSTDIR\optional" ;will be logged | |||
${SetOutPath} "$INSTDIR\optional" ;will not be logged because it already exists | |||
${AddItem} "$OUTDIR\something.nsh" | |||
File /oname=something.nsh "${NSISDIR}\include\sections.nsh" | |||
${CopyFiles} "$OUTDIR\something.nsh" "$OUTDIR\somethingelse.nsh" ;will be logged, but... | |||
${Rename} "$OUTDIR\somethingelse.nsh" "$OUTDIR\somethingelse.dat" | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
sectionend | |||
section /O "Optional stuff 2, an additional log file" | |||
!undef UNINSTLOG | |||
!define UNINSTLOG "uninstall2.dat" | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
${SetOutPath} "$INSTDIR" | |||
${File} "${NSISDIR}\" "makensis.exe" | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
!undef UNINSTLOG | |||
sectionend | |||
section /O "Demonstrate FileDated, update uninstlog.nsh" | |||
;Back to first log. | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
${SetOutPath} "$INSTDIR" | |||
sleep 1000 | |||
; Update uninstlog.nsh | |||
fileopen $0 "uninstlog.nsh" a | |||
filewrite $0 ";Appended by update test.$\r$\n" | |||
fileclose $0 | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
sectionend | |||
section /O "Test wildcards" | |||
!insertmacro UNINSTLOG_OPENINSTALL | |||
${CreateDirectory} "$INSTDIR\wildcard" ;will be logged | |||
${SetOutPath} "$INSTDIR\wildcard" ;will not be logged because it already exists | |||
${File} "${NSISDIR}\include\" "*.nsh" | |||
!insertmacro UNINSTLOG_CLOSEINSTALL | |||
sectionend | |||
section "uninstall" | |||
; We have to do uninstall2.dat first because install.log removes $INSTDIR. | |||
IfFileExists "$INSTDIR\uninstall2.dat" 0 NotInstall2 | |||
!undef UninstLog | |||
!define UninstLog "uninstall2.dat" | |||
!insertmacro UNINSTLOG_UNINSTALL | |||
NotInstall2: | |||
!undef UninstLog | |||
!insertmacro UNINSTLOG_UNINSTALL | |||
End: | |||
sectionend | |||
</highlight-nsis> | |||
[[User:Campg2003|Campg2003]] 21:59, 27 July 2012 (UTC) |
Latest revision as of 07:07, 11 September 2012
The solution presented here to uninstall only the files we installed works fine. There is only one shortcoming: the list of files has to be known beforehand. This solution actually overloads File in a way that denies the recursive switch. Meaning that the created installation log is only useful if you don’t grab all the files from one source directory in your installer: File /r <my_dir>
I searched a lot for a way to combine these 2 features using the NSIS script and I finally gave up. I wrote a small Python script that generates a pair of lists, to be included in a pair of sections (install and uninstall) in your .nsi script. I post hereafter the python scrip. Here is the small .bat that you would use to call in sequence that script and the NSIS build.
Good luck and have fun,
adrian DOT neagu AT scansoft DOT com
python gen_list_files_for_nsis.py %LOCATION_SOURCES% install_list.nsh uninstall_list.nsh
"C:\Program Files\NSIS\makensis.exe" /DINST_LIST=install_list.nsh /DUNINST_LIST=uninstall_list.nsh my_product.nsi
The Script
""" This script generates 2 lists of NSIS commands (install&uninstall) for all files in a given directory Usage: gen_list_files_for_nsis.py <dir src> <inst list> <uninst list> Where <dir src> : dir with sources; must exist <inst list> : list of files to install (NSIS syntax) <uninst list> : list of files to uninstall (NSIS syntax) (both these will be overwriten each time) """ import sys, os, glob # global settings just_print_flag = 0 # turn to 1 for debugging # templates for the output inst_dir_tpl = ' SetOutPath "$INSTDIR%s"' inst_file_tpl = ' File "${FILES_SOURCE_PATH}%s"' uninst_file_tpl = ' Delete "$INSTDIR%s"' uninst_dir_tpl = ' RMDir "$INSTDIR%s"' # check args if len(sys.argv) != 4: print __doc__ sys.exit(1) source_dir = sys.argv[1] if not os.path.isdir(source_dir): print __doc__ sys.exit(1) def open_file_for_writting(filename): "return a handle to the file to write to" try: h = file(filename, "w") except: print "Problem opening file %s for writting"%filename print __doc__ sys.exit(1) return h inst_list = sys.argv[2] uninst_list = sys.argv[3] if not just_print_flag: ih= open_file_for_writting(inst_list) uh= open_file_for_writting(uninst_list) stack_of_visited = [] counter_files = 0 counter_dirs = 0 print "Generating the install & uninstall list of files" print " for directory", source_dir print >> ih, " ; Files to install\n" print >> uh, " ; Files and dirs to remove\n" # man page of walk() in Python 2.2 (the new one in 2.4 is easier to use) # os.path.walk(path, visit, arg) #~ Calls the function visit with arguments (arg, dirname, names) for each directory #~ in the directory tree rooted at path (including path itself, if it is a directory). #~ The argument dirname specifies the visited directory, the argument names lists #~ the files in the directory (gotten from os.listdir(dirname)). The visit function #~ may modify names to influence the set of directories visited below dirname, #~ e.g., to avoid visiting certain parts of the tree. (The object referred to by names #~ must be modified in place, using del or slice assignment.) def my_visitor(my_stack, cur_dir, files_and_dirs): "add files to the install list and accumulate files for the uninstall list" global counter_dirs, counter_files, stack_of_visited counter_dirs += 1 if just_print_flag: print "here", my_dir return # first separate files my_files = [x for x in files_and_dirs if os.path.isfile(cur_dir+os.sep+x)] # and truncate dir name my_dir = cur_dir[len(source_dir):] if my_dir=="": my_dir = "\\." # save it for uninstall stack_of_visited.append( (my_files, my_dir) ) # build install list if len(my_files): print >> ih, inst_dir_tpl % my_dir for f in my_files: print >> ih, inst_file_tpl % (my_dir+os.sep+f) counter_files += 1 print >> ih, " " os.path.walk( source_dir, my_visitor, stack_of_visited) ih.close() print "Install list done" print " ", counter_files, "files in", counter_dirs, "dirs" stack_of_visited.reverse() # Now build the uninstall list for (my_files, my_dir) in stack_of_visited: for f in my_files: print >> uh, uninst_file_tpl % (my_dir+os.sep+f) print >> uh, uninst_dir_tpl % my_dir print >> uh, " " # now close everything uh.close() print "Uninstall list done. Got to end.\n"
And your NSIS script (my_product.nsi) would look something like this:
;-------------------------------- ; ; the stuff to install ; ;-------------------------------- Section "" ; No components page, name is not important !include ${INST_LIST} ; the payload of this installer is described in an externally generated list of files ;File /r "${FILES_SOURCE_PATH}\*.*" ; not OK because with /r we can not log what was installed ; and without logging we cannot uninstall only the files installed by us SectionEnd ;-------------------------------- ; ; uninstaller ; ;-------------------------------- Section "Uninstall" ; Remove the files (using externally generated file list) !include ${UNINST_LIST} ; Remove uninstaller Delete $INSTDIR\uninst*.exe RMDir $INSTDIR ; this is safe; it's not forced if you still have private files there. ; Important note: RMDir /r "$INSTDIR" ; is NOT OK! ; if the user installed in "C:\Program Files" by mistake, then we totally screw up his machine! SectionEnd ; Uninstall
There is actually another solution, but I don't really have time to do a formal write-up right now. Essentially, however, the idea is to dump the console log to a file, and parse that. This means you can still use all the regular File functions without calling a special function, and without needing to know the source files in advance.
In a nutshell, parse through the console log dump looking for "Output folder: " and "Extract: ". For each output folder, push the folder location on the stack. For each file extraction, just delete the file. Once done deleting files, create a loop that pops the stack and delete the folder, halting once the popped value is no longer an output folder (and push it back on the stack).
There are still some snags, of course, such as having to make sure that all folders are reported, and when doing so manually that you do not report them in inverse hierarchical sequence, but that's not that big of a problem.
The question is, really, whether the existing method described in this wiki page is really so bad... yes, you have to know the files in advance, so anytime there's a file change you have to adjust the installer as well - but it does also prevent files from accidentally getting included, and you always know exactly what files are in the installer and where they are getting extracted to.
Here's VB script that does essentially the same thing, so it runs fine on any windows machine (no need for python). It recursively generates a list of files and directories to add and remove. I've only semi-tested it on my installation, so take the script with a grain of salt...
sub dorecurse(dpath, spath) if fs.folderexists(spath) then out1.writeline "SetOutPath """ & dpath & """" set d = fs.getfolder(spath) for each f in d.files str = "File """ & f.Path & """" out1.writeline str str = "Delete """ & dpath & "\" & f.Name & """" out2.writeline str next for each d2 in d.subfolders str = "CreateDirectory """ & dpath & "\" & d2.Name & """" out1.writeline str dorecurse dpath & "\" & d2.Name, spath & "\" & d2.Name str = "RMDir """ & dpath & "\" & d2.Name & """" out2.writeline str next end if end sub set fs = createobject("Scripting.FileSystemObject") instdir = wscript.arguments(0) rootdir = wscript.arguments(1) filename1 = wscript.arguments(2) filename2 = wscript.arguments(3) set out1 = fs.createtextfile(filename1, true) set out2 = fs.createtextfile(filename2, true) dorecurse instdir, rootdir out1.close out2.close
Your nsis script looks something like this:
Section "Main (required)" SectionIn RO !system 'cscript /nologo listfiles.vbs "$INSTDIR" "C:\somedir" "list.txt" "unlist.txt"' !include list.txt !system 'del list.txt' WriteUninstaller "uninstall.exe" SectionEnd Section "Uninstall" !include "unlist.txt" !system 'del unlist.txt' SectionEnd
Modified script, date/size stamp, multiple install logs
This is a modification of the script on this page as of 8/4/2011. It doesn't contain the INI file macros, although they could easily be added. I haven't looked at the modification to add rmdir to see if it makes sense or not. I replaced sections with macros because it fit into the project I was writing at the time. It also supports the ability to write multiple install logs, and to warn about files that have been changed since they were installed. I also added documentation and an test script.
Modifications
- In UninstallLog changed SetOutPath so that it doesn't log the path if it already exists.
- In WriteRegDWORD changed WriteRegStr to WriteRegDWORD.
- Converted uninstallLog to check file date and size of selected files and offer to not uninstall if files have been changed.
- Made uninstall code into a function, moved to end of header.
- Moved close and delete of log file to right after it has been read. This allows the INSTDIR to be removed.
- Made section -openlogfile into macro UNINSTLOG_OPENINSTALL.
- Added macro UNINSTLOG_CLOSEINSTALL to close log file, don't think it was done in original code.
- Added !ifndef INSTALLLOGINCLUDED around header file contents.
- In the uninstall section registry key removal code changed UNINSTALLPATH to REG_UNINSTALL_PATH. Added !ifdefs so that if this or REG_APP_PATH are not defined, code for deleting the respective registry paths is not executed.
- Made open install code into a function with macro that calls it.
- Changed all macros so that they don't try to write if the log file is closed. You can now disable logging by not calling INSTALLOPEN.
- Added define UNINSTLOGDEBUG.
- Added variable $UninstLogAlwaysLog to log files even if they already exist, also Macro AddItemAlways.
Documentation
(for version 0.0.2 dated 7/27/2012)
This header file supports the ability to uninstall only the installed files.
It expects the following defines: REG_ROOT, REG_APP_PATH, and REG_UNINSTALL_PATH. If either of the latter two are undefined these registry keys won't be deleted even if they appear in the log file.
To use:
- !include this file at the top of your scrip.
(Note: it gets an error if it is before "SetCompressor /SOLID LZMA": "Error: can't change compressor after data already got compressed or header already changed!".)
- Define REG_ROOT, and REG_APP_PATH and/or REG_UNINSTALL_PATH if you want to use ${WriteRegStr} or ${WriteRegDWORD}.
Start and end each install section like this:
section "Install section" !insertmacro UNINSTLOG_OPENINSTALL ;Install section code !insertmacro UNINSTLOG_CLOSEINSTALL sectionend
When the log is closed the commands will be executed but not logged.
If you want the $INSTDIR to be removed automatically, you need to place ${AddItemAlways} "$INSTDIR" after you initially open the log. This is because the $INSTDIR is already created at the time INSTLOG_OPENINSTALL is called, so it won't automatically be logged. UNINSTLOG_OPENINSTALL does not automatically log it so that you don't add a directory automatically if you close and reopen a new install block. (Automatically writing $INSTDIR on open makes a messy log but it might be okay since there are still files in the directory.) If you run the installer again to add additional files, it will again write the $INSTDIR to the log.
In your uninstall section do:
!insertmacro UNINSTLOG_UNINSTALL
Note that this will push one entry on the stack for every entry in the log.
The following commands are provided, most of them simple forms of the similar NSIS commands:
- ${AddItem} Path -- adds a file or directory when the provided commands won't do the job. Does not add the item if it exists, so you need to call this before the command that creates it.
- ${AddItemAlways} -- Like AddItem but adds item even if it exists.
- ${File} Path FileName -- path is path on source machine, must be empty or end with backslash.
- ${CreateShortcut} FilePath FilePointer Parameters Icon IconIndex -- create shortcut FilePath that links to FilePointer.
- ${CopyFiles} source Dest - use full paths.
- ${Rename} Source Dest - use full paths.
- ${CreateDirectory} Path
- ${SetOutPath} Path
- ${WriteUninstaller} Path -- should use a fully-qualified path to make the logged value right.
- ${WriteRegStr} Root path Key Value-- path is the subkey, must be the value of either ${rEG_APP_PATH} or ${REG_UNINSTALL_PATH), which must be defined prior to use.
- ${WriteRegDWORD} -- see ${WriteRegStr}
- ${AddItemDated} -- same as AddItem but includes the date/time and size of the installed file so that the uninstaller can detect if files are modified.
- ${FileDated} -- same as ItemDated for the ${File} command.
Uses the following additional defines:
- UninstLog -- name of the log file, defaults to uninstall.log. You can define this just before calling UNINSTLOG_OPENINSTALL and UNINSTLOG_UNINSTALL to change the name of the log file. You could even create separate logs. You would then need to do more than one UNINSTLOG_UNINSTALL for each log file. The name will stay defined until you change it.
- UNINSTLOGDSEP -- separator used to separate file path and date-size for ${FileDated} and $ItemDated} macros (|)
- INSTLOGDEBUG -- Activates debugging messages if defined.
and variables:
- $UninstLog -- handle of log file, empty if log closed. When the log is closed commands are executed but not logged.
- $UninstLogAlwaysLog -- Normally a file is not logged if it exists on the target system. You can cause files to always be logged even if they exist by setting the variable $UninstLogAlwaysLog to nonempty:
section "something" ;... StrCpy $UninstLogAlwaysLog 1 ${File} "" "MyFile.txt" ; will be logged even if it exists. ${FileDated} "" "AnotherFile" ;so will this one. StrCpy $UninstLogAlwaysLog "" ; turn it off ${File} "" "filexxx" ; won't be logged if it exists. ;... sectionend
Note that this header uses strfunc.nsh function ${UnStrTok}. If you also include strfunc.nsh and use ${UnstrTok} after this file you can check to see if it has been initialized by:
!ifndef UnStrTokINCLUDED ${UnStrTok} !endif
(This was discovered by inspection in strfunc.nsh v1.09.)
A note about wildcards
Although you can use wildcards in ${File}, ${Rename}, and ${CopyFiles}, it could be dangerous and is not recommended unless you are sure you know what you are doing. Suppose you do ${File} "*.txt" where the source directory has a few text files, but the directory into which they are installed already contains text files. The value written to the log will be "*.txt", and this will cause the uninstaller to uninstall all of the .txt files in the folder, including the ones that were already installed. You can't use wildcards at all with ${FileDated}, although there is no check for this. (I need a way to run FindFirst/FindNext on the source system to do this. See above for an alternative.)
The project for which I modified this header file uses ${AddItem}, ${AddItemAlways}, ${AddItemDated}, ${File}, ${FileDated}, ${CreateDirectory}, ${CreateShortcut}, ${SetOutPath}, and ${WriteUninstaller}. I have retained the other commands but I have not tested them much.
Bugs/enhancements
- Issue with ${AddItemDated}: If ${AddItemDated} follows the command that creates its file then it won't log unless UninstLogAlwaysLog is on. If it precedes the command, the date-size stamp will be empty because it doesn't exist yet. It was not modified to always log regardless of the value of $UninstLogAlwaysLog to make it clear that it is set.
- Issue with ${WriteReg...} macros: For these macros to work REG_APP_PATH and REG_UNINSTALL_PATH must be defined, and these are the only registry paths that can be written. This means that you must define these and use them in the path of any ${WriteReg...} calls to make them be deleted. If there were an installed file with the same name as these values that was installed but had been deleted before the uninstall, it could cause these keys to be deleted from the registry unintentionally. (I presume that's why the uninstaller code checks for these values before removing them-- otherwise any file in the log that doesn't exist on the system when uninstalled would be interpreted as a registry key to delete.)
- To do: make all macros, variables, and !defines defined by this header start with "UninstLog" to avoid name collisions with other header files.
The script
;uninstlog.nsh /* Adapted by GaryC from code from http://nsis.sourceforge.net/Uninstall_only_installed_files by Afrow UK with modifications by others, taken 8/3/11. Version 0.0.2 Last modified 7/27/2012 Modifications: 7/27/12 by GaryC: Added display of time stamps in modified file message. Moved documentation to another file. Removed example script (provided by testuninstlog.nsh). 7/25/12 by GaryC: Updated documentation. Added version number. Macro AddItemAlways added sometime earlier, probably before or around 8/15/11. 8/15/11 by GaryC: Added note about AddItem needing to be called before the command it applies to. 8/15/11 by GaryC: Added file existence checks in more macros. Added check of $UninstLogAlwaysLog to macros that write files. Added note about not working before SetCompressor /SOLID LZMA. 8/15/11 Initial modifications from WIKI code by GaryC: In UninstallLog changed SetOutPath so that it doesn't log the path if it already exists. In WriteRegDWORD changed WriteRegStr to WriteRegDWORD. Converted uninstallLog to check file date and size of selected files and offer to not uninstall if files have been changed. Made uninstall code into a function, moved to end of header. Moved close and delete of log file to right after it has been read. This allows the INSTDIR to be removed. Made section -openlogfile into macro UNINSTLOG_OPENINSTALL. Added macro UNINSTLOG_CLOSEINSTALL to close log file, don't think it was done in original code. Added !ifndef INSTALLLOGINCLUDED around header file contents. In the uninstall section registry key removal code changed UNINSTALLPATH to REG_UNINSTALL_PATH. Added !ifdefs so that if this or REG_APP_PATH are not defined, code for deleting the respective registry paths is not executed. Made open install code into a function with macro that calls it. Changed all macros so that they don't try to write if the log file is closed. You can now disable logging by not calling INSTALLOPEN. Added initialization call for ${UnStrTok}. Added define UNINSTLOGDEBUG. Commented out section to open uninstall log. Added variable $UninstLogAlwaysLog to log files even if they already exist. Added documentation and example script. */ !ifndef UNINSTALLLOGINCLUDED !define UNINSTALLLOGINCLUDED !define UNINSTLOGDEBUG !include "strfunc.nsh" !include "filefunc.nsh" ;-------------------------------- ; Configure UnInstall log to only remove what is installed ;-------------------------------- ;The symbol that separates the date-size stamp from the file name. !define UNINSTLOGDSEP | Var UninstLog ; handle of log file Var UninstLogAlwaysLog ;If nonempty, FileDated logs the file even if it exists. ;Ex: ;StrCpy $UninstLogAlwaysLog 1 ;${FileDated} "" "something" ;StrCpy $UninstLogAlwaysLog "" ;turn it back off. ;Uninstall log file missing. LangString UninstLogMissing ${LANG_ENGLISH} "${UninstLog} not found!$\r$\nUninstallation cannot proceed!" LangString UninstLogModified ${LANG_ENGLISH} "File $R0 has been modified since it was installed. Do you want to delete it?$\r$\nOriginal: $R3$\r$\nCurrent: $R4" LangString UninstLogShowDateSize ${LANG_ENGLISH} "$1 UTC $2 bytes" ;We need to make sure these functions haven't already been initialized outside this header. Not documented, found by inspection. ;!ifndef StrTokINCLUDED ;${StrTok} ;!endif !ifndef UnStrTokINCLUDED ${UnStrTok} !endif ;AddItem macro-- Writes an item to the log, for times when you need options the macros don't support. !macro AddItem Path StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${Path}" +3 ;if it exists we don't log it. StrCmp $UninstLog "" +2 FileWrite $UninstLog "${Path}$\r$\n" !macroend ;AddItemAlways - Like AddItem but turns on $UninstLogAlwaysLog and restores it afterwards. !macro AddItemAlways Path push $UninstLogAlwaysLog StrCpy $UninstLogAlwaysLog "1" !insertmacro AddItem ${Path} pop $UninstLogAlwaysLog !macroend ;AddItemDated macro. like AddItem but allows you to add date and size information to the entry so the uninstaller can tell if the file has been modified. ;Writes path with date-size appended. path must exist. !macro AddItemDated Path StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${Path}" +9 ;if it exists we don't log it. StrCmp $UninstLog "" +8 ;bail if uninstall log closed push $0 push $1 strCpy $0 ${Path} call UninstLogMakeDateSize FileWrite $UninstLog "${Path}${UNINSTLOGDSEP}$1$\r$\n" pop $1 pop $0 !macroend ;Consider ItemDated2 macro that would receive path Date (string containing YYYYMMDDhhmmss) size (string containing number of bytes in decimal). ;File macro ;Filepath is path on machine generating installer, must be empty or terminated with backslash. ;Use regular file command and AddItem macro for anything more exhotic. !macro File FilePath FileName StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "$OUTDIR\${FileName}" +3 StrCmp $UninstLog "" +2 ;detailprint "File: checking existence of $OUTDIR\${FileName}, $$UninstLog=$UninstLog" ; debug ;IfFileExists "$OUTDIR\${FileName}" +4 ; debug ;StrCmp $UninstLog "" +3 ; debug ;detailprint "File: logging $OUTDIR\${FileName} to $UninstLog" FileWrite $UninstLog "$OUTDIR\${FileName}$\r$\n" ;detailprint "File: executing File for ${FilePath}${FileName}" File "${FilePath}${FileName}" !macroend ;FileDated macro ;If $UninstLogAlwaysLog is nonempty, this will log the entry even if it exists on the target machine, which means it will be removed when uninstalled. Otherwise it will not be logged if it exists. !macro FileDated FilePath FileName push $0 push $1 push $2 StrCpy $2 "" StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "$OUTDIR\${FileName}" +3 ;if it exists we don't log it. strCmp $UninstLog "" +2 ; if log file not opened don't log strCpy $2 1 ;set flag to log file File "${FilePath}${FileName}" StrCmp $2 "" +4 ;skip logging StrCpy $0 "$OUTDIR\${FileName}" ;file on target system is here call UninstLogMakeDateSize ;Write something like Outdir\filename|201108041600005234 FileWrite $UninstLog "$0${UNINSTLOGDSEP}$1$\r$\n" pop $2 pop $1 pop $0 !macroend ;$0 - (in) file path (if it is a path it is so on the source system) ;$1 - (out) date-size yyyymmddhhmmsssize. ; We use a macro so we can get an install and uninstall version. Prefix is either "" or "un." !macro UninstLogInsertMakeDateSize prefix function ${prefix}UninstLogMakeDateSize push $R0 push $R1 push $R2 push $R3 push $R4 push $R5 push $R6 push $R7 push $R8 ${GetTime} "$0" "MS" $R0 $R1 $R2 $R3 $R4 $R5 $R6 ; Get file size. FileOpen $R8 "$0" r FileSeek $R8 0 END $R7 FileClose $R8 ;return something like 201108041600005234 in $1 StrCpy $1 "$R2$R1$R0$R4$R5$R6$R7" pop $R8 pop $R7 pop $R6 pop $R5 pop $R4 pop $R3 pop $R2 pop $R1 pop $R0 functionend !macroend !insertmacro UninstLogInsertMakeDateSize "" !insertmacro UninstLogInsertMakeDateSize "un." ; Produce a string containing display of a stamp returned by UninstLogMakeDateSize. ; Input and output values are on the stack. function un.UninstLogShowDateSize exch $0 push $1 push $2 strcpy $1 $0 14 ; copy the time part strcpy $2 $0 "" 14 ; copy the size (everything after the time) strcpy $0 "$(UninstLogShowDateSize)" pop $2 pop $1 exch $0 functionend ;CreateShortcut macro !macro CreateShortcut FilePath FilePointer Parameters Icon IconIndex !ifdef UNINSTLOGDEBUG ; debug StrCpy $0 "doesn't" IfFileExists "${FilePath}" 0 +2 StrCpy $0 "does" DetailPrint 'CreateShortcut: Checking existence of ${FilePath} which $0 exist.' ; debug !endif ; debug StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${FilePath}" +3 ;if it exists we don't log it. StrCmp $UninstLog "" +2 FileWrite $UninstLog "${FilePath}$\r$\n" CreateShortcut "${FilePath}" "${FilePointer}" "${Parameters}" "${Icon}" "${IconIndex}" !macroend ;Copy files macro !macro CopyFiles SourcePath DestPath StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${DestPath}" +3 StrCmp $UninstLog "" +2 FileWrite $UninstLog "${DestPath}$\r$\n" CopyFiles "${SourcePath}" "${DestPath}" !macroend ;Rename macro !macro Rename SourcePath DestPath StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${DestPath}" +3 StrCmp $UninstLog "" +2 FileWrite $UninstLog "${DestPath}$\r$\n" Rename "${SourcePath}" "${DestPath}" !macroend ;CreateDirectory macro !macro CreateDirectory Path StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${Path}\*.*" +3 ;if it exists we don't log it. StrCmp $UninstLog "" +2 FileWrite $UninstLog "${Path}$\r$\n" CreateDirectory "${Path}" !macroend /* ;SetOutPath macro ; WARNING: If Path already exists the uninstaller will delete it.--GaryC !macro SetOutPath Path SetOutPath "${Path}" StrCmp $UninstLog "" +2 FileWrite $UninstLog "${Path}$\r$\n" !macroend */ ;SetOutPath macro ;Modified to not log Path if it already exists.--GaryC ;If you use this macro, the path you specify will be removed by the uninstaller if it does not already exist! !macro SetOutPath Path StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${Path}\*.*" +3 StrCmp $UninstLog "" +2 FileWrite $UninstLog "${Path}$\r$\n" SetOutPath "${Path}" !macroend ;WriteUninstaller macro !macro WriteUninstaller Path StrCmp $UninstLogAlwaysLog "" 0 +2 ; if nonempty, don't check existence. IfFileExists "${Path}" +3 ;if it exists we don't log it. StrCmp $UninstLog "" +2 FileWrite $UninstLog "${Path}$\r$\n" WriteUninstaller "${Path}" !macroend ;WriteRegStr macro !macro WriteRegStr RegRoot UnInstallPath Key Value StrCmp $UninstLog "" +2 FileWrite $UninstLog "${RegRoot} ${UnInstallPath}$\r$\n" WriteRegStr "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" !macroend ;WriteRegDWORD macro ;WARNING: This writes spaces between items while WriteRegStr does not.--GaryC !macro WriteRegDWORD RegRoot UnInstallPath Key Value StrCmp $UninstLog "" +2 FileWrite $UninstLog "${RegRoot} ${UnInstallPath}$\r$\n" ;WriteRegStr "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" WriteRegDWord "${RegRoot}" "${UnInstallPath}" "${Key}" "${Value}" !macroend ;Defines for commands ;AddItem macro !define AddItem "!insertmacro AddItem" ;AddItemAlways macro !define AddItemAlways "!insertmacro AddItemAlways" ;AddItemDated macro !define AddItemDated "!insertmacro AddItemDated" ;File macro !define File "!insertmacro File" ;FileDated macro !define FileDated "!insertmacro FileDated" ;CreateShortcut macro !define CreateShortcut "!insertmacro CreateShortcut" ;Copy files macro !define CopyFiles "!insertmacro CopyFiles" ;Rename macro !define Rename "!insertmacro Rename" ;CreateDirectory macro !define CreateDirectory "!insertmacro CreateDirectory" ;SetOutPath macro !define SetOutPath "!insertmacro SetOutPath" ;WriteUninstaller macro !define WriteUninstaller "!insertmacro WriteUninstaller" ;WriteRegStr macro !define WriteRegStr "!insertmacro WriteRegStr" ;WriteRegDWORD macro !define WriteRegDWORD "!insertmacro WriteRegDWORD" ;Need to invoke before items are logged. !macro UNINSTLOG_OPENINSTALL ;Set the name of the uninstall log !ifndef UninstLog ;Default value if not defined outside. !define UninstLog "uninstall.log" !endif !ifdef UNINSTLOGDEBUG !echo "Opening ${UninstLog} at line ${__LINE__}" !endif push $0 StrCpy $0 "${UninstLog}" call __UninstLogOpenInstall pop $0 !macroend ; $0 -- path/filename of uninstall log. function __UninstLogOpenInstall push !1 StrCpy $1 "" ;should we log $INSTDIR? IfFileExists $INSTDIR +2 StrCpy $1 1 ;Doesn't exist, log it. CreateDirectory "$INSTDIR" IfFileExists "$INSTDIR\$0" LogAppend !ifdef UNINSTLOGDEBUG detailprint "Opening $0" !endif FileOpen $UninstLog "$INSTDIR\$0" w GoTo Opened LogAppend: !ifdef UNINSTLOGDEBUG detailprint "Opening $0 for append" !endif SetFileAttributes "$INSTDIR\$0" NORMAL FileOpen $UninstLog "$INSTDIR\$0" a FileSeek $UninstLog 0 END Opened: IntCmp $1 0 End ${AddItem} "$INSTDIR" End: pop $1 functionend ; __UninstLogOpenInstall ;Need to invoke at end of installation. !macro UNINSTLOG_CLOSEINSTALL FileClose $UninstLog StrCpy $UninstLog "" !ifdef UNINSTLOGDEBUG !echo "Closing install log at line ${__LINE__}" detailprint "Closing install log at line ${__LINE__}" !endif !macroend ; $0 -- name of uninstall log file. function un.UninstLogUninstall ;Can't uninstall if uninstall log is missing! IfFileExists "$INSTDIR\$0" +3 MessageBox MB_OK|MB_ICONSTOP "$(UninstLogMissing)" Abort Push $1 Push $R0 Push $R1 Push $R2 push $R3 push $R4 SetFileAttributes "$INSTDIR\$0" NORMAL FileOpen $UninstLog "$INSTDIR\$0" r ;Set $OUTDIR to something we aren't going to remove so we can delete $INSTDIR. This works because all of the paths in the log are absolute. SetOutPath $PROGRAMFILES ;Read in the uninstall log and put it on the stack. StrCpy $R1 -1 ; line count GetLineCount: ClearErrors FileRead $UninstLog $R0 IntOp $R1 $R1 + 1 StrCpy $R0 $R0 -2 ; remove $|R$\N Push $R0 IfErrors 0 GetLineCount FileClose $UninstLog Delete "$INSTDIR\$0" Pop $R0 !ifdef UNINSTLOGDEBUG DetailPrint "Read $R1 log entries" ; debug !endif LoopRead: StrCmp $R1 0 LoopDone Pop $R0 ; log entry IfFileExists "$R0\*.*" 0 NotDir !ifdef UNINSTLOGDEBUG ; debug DetailPrint "Attempting to remove directory $R0" ; debug !endif ; debug RMDir $R0 #is dir !ifdef UNINSTLOGDEBUG ; debug IfErrors 0 +2 ; debug DetailPrint "Error after trying to remove directory $0" ; debug !endif ; debug Goto LoopNext NotDir: ${UnStrTok} $R2 $R0 ${UNINSTLOGDSEP} 1 0 ; date/size, 2nd token ${UnStrTok} $R0 "$R0" ${UNINSTLOGDSEP} 0 0 ;remove date/size from path. !ifdef UNINSTLOGDEBUG DetailPrint "After separating time stamp, time stamp=$R2, file=$R0" ; debug !endif StrCmp $R2 "" NoDateSize ;Skip call if no timestamp push $0 StrCpy $0 $R0 Call un.UninstLogMakeDateSize pop $0 ;$1 contains date + size from file, $R2 is same from log entry. NoDateSize: IfFileExists $R0 0 NotFile StrCmp $R2 "" NoDateSize2 ;If this log entry had no date-size, skip compare !ifdef UNINSTLOGDEBUG DetailPrint "UninstLog: file $0 has time stamp $1, entry stamp is $R2" ; debug !endif StrCmp $R2 $1 DateSizeMatch push $R2 ; log entry stamp call un.UninstLogShowDateSize pop $R3 ; display of log entry stamp push $1 ; current stamp call un.UninstLogShowDateSize pop $R4 ; current file stamp MessageBox MB_YESNO $(UninstLogModified) IDNO NoDelete pop $2 pop $1 DateSizeMatch: NoDateSize2: Delete $R0 #is file NoDelete: Goto LoopNext NotFile: !ifdef REG_APP_PATH StrCmp $R0 "${REG_ROOT} ${REG_APP_PATH}" 0 NotRegAppPath DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" #is Reg Element Goto LoopNext NotRegAppPath: !endif ; REG_APP_PATH !ifdef REG_UNINSTALL_PATH StrCmp $R0 "${REG_ROOT} ${REG_UNINSTALL_PATH}" 0 +2 DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" #is Reg Element !endif ; REG_UNINSTALL_PATH LoopNext: IntOp $R1 $R1 - 1 Goto LoopRead LoopDone: pop $R4 pop $R3 Pop $R2 Pop $R1 Pop $R0 Pop $1 ;Remove registry keys ;DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" ;DeleteRegKey ${REG_ROOT} "${REG_UNINSTALL_PATH}" functionend !macro UNINSTLOG_UNINSTALL !ifndef UninstLog ;Default value if not defined outside. !define UninstLog "uninstall.log" !endif push $0 StrCpy $0 "${UninstLog}" call un.UninstLogUninstall pop $0 !macroend ;-- end header file addition !endif ; UNINSTALLLOGINCLUDED ;--- end uninstalllog.nsh code
Test Script/Example
/* 7/24/12 Added section to test ${File} with wildcards. */ !include "uninstlog.nsh" !define REG_ROOT "HKLM" !define REG_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\testuninstlog" !define SHORTCUTNAME "Uninstall testuninstlog" installdir "$PROGRAMFILES\testuninstlog" OutFile "testuninstlog.exe" Name "Test Uninstlog.nsh" RequestExecutionLevel user ShowInstDetails show ShowUninstDetails show ComponentText "This demonstrates the uninstlog.nsh header file which provides logging of installed files. File uninstlog.nsh has a date stamp. The shortcut ${SHORTCUTNAME} on the desktop will uninstall this test." page components page Directory page InstFiles UninstPage uninstConfirm UninstPage instfiles section "-install" !insertmacro UNINSTLOG_OPENINSTALL ${AddItemAlways} "$INSTDIR" ${SetOutPath} "$INSTDIR" ${FileDated} "" "uninstlog.nsh" ${File} "${NSISDIR}\Include\" "strfunc.nsh" ${CreateShortcut} "$DESKTOP\${SHORTCUTNAME}.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 ${WriteUninstaller} "$INSTDIR\uninstall.exe" ${WriteRegStr} ${rEG_ROOT} "${REG_UNINSTALL_PaTH}" "UninstallString" "$INSTDIR\uninstall.exe" !insertmacro UNINSTLOG_CLOSEINSTALL sectionend section /O "Optional stuff, copy and rename" !insertmacro UNINSTLOG_OPENINSTALL ${CreateDirectory} "$INSTDIR\optional" ;will be logged ${SetOutPath} "$INSTDIR\optional" ;will not be logged because it already exists ${AddItem} "$OUTDIR\something.nsh" File /oname=something.nsh "${NSISDIR}\include\sections.nsh" ${CopyFiles} "$OUTDIR\something.nsh" "$OUTDIR\somethingelse.nsh" ;will be logged, but... ${Rename} "$OUTDIR\somethingelse.nsh" "$OUTDIR\somethingelse.dat" !insertmacro UNINSTLOG_CLOSEINSTALL sectionend section /O "Optional stuff 2, an additional log file" !undef UNINSTLOG !define UNINSTLOG "uninstall2.dat" !insertmacro UNINSTLOG_OPENINSTALL ${SetOutPath} "$INSTDIR" ${File} "${NSISDIR}\" "makensis.exe" !insertmacro UNINSTLOG_CLOSEINSTALL !undef UNINSTLOG sectionend section /O "Demonstrate FileDated, update uninstlog.nsh" ;Back to first log. !insertmacro UNINSTLOG_OPENINSTALL ${SetOutPath} "$INSTDIR" sleep 1000 ; Update uninstlog.nsh fileopen $0 "uninstlog.nsh" a filewrite $0 ";Appended by update test.$\r$\n" fileclose $0 !insertmacro UNINSTLOG_CLOSEINSTALL sectionend section /O "Test wildcards" !insertmacro UNINSTLOG_OPENINSTALL ${CreateDirectory} "$INSTDIR\wildcard" ;will be logged ${SetOutPath} "$INSTDIR\wildcard" ;will not be logged because it already exists ${File} "${NSISDIR}\include\" "*.nsh" !insertmacro UNINSTLOG_CLOSEINSTALL sectionend section "uninstall" ; We have to do uninstall2.dat first because install.log removes $INSTDIR. IfFileExists "$INSTDIR\uninstall2.dat" 0 NotInstall2 !undef UninstLog !define UninstLog "uninstall2.dat" !insertmacro UNINSTLOG_UNINSTALL NotInstall2: !undef UninstLog !insertmacro UNINSTLOG_UNINSTALL End: sectionend
Campg2003 21:59, 27 July 2012 (UTC)