Talk:Uninstall only installed files: Difference between revisions

From NSIS Wiki
Jump to navigationJump to search
No edit summary
Line 180: Line 180:


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.
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>

Revision as of 23:19, 6 August 2008

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