- NSIS Discussion
- uninstall.exe and an external way of patching
Archive: uninstall.exe and an external way of patching
coco_vc
10th July 2006 15:31 UTC
uninstall.exe and an external way of patching
Hi all,
I use NSIS to install/uninstall my application.
My application has itself a feature of updating all the application files, so my_app is able to download updated files and replace the ones installed with the ones downloaded. So this works like this:
- I build vers 1
- release it to the market
- then later I build vers 2
- create somehow a diff between these 2 versions
- release vers 2 on the market for new users
- release the diff between 1 and 2 so that my_app updates the version already installed on the users machines
Is there a way of updating the uninstall.exe also? I mean, this is generated on the fly when installing the sw on the target machine, but what I need is to somehow get the same uninstall.exe when building the software, so that:
- either do a diff with the previous one and then my_app can cary the diff
- or at least my_app will overwrite the old unistall.exe with the new uinstall.exe
So, can I do smth like this? Can I generate an uninstall.exe on build-time that would be identical with uninstall.exe generated by the installer with WriteUninstaller on runtime?
Thx,
Viv
Afrow UK
10th July 2006 15:52 UTC
With SpamExperts I wrote a separate NSIS installer to generate the uninstall.exe. This NSIS installer just needs a dummy Section containing the WriteUninstaller line which then calls Quit and a Section Uninstall containing the uninstall code. In the main installer script, it is compiled with !system "makensis ..." and then executed with !execute to create the uninstall executable.
For having different versions, it's best to just have a version string in a file on your server. It could either contain a build number, or probably better would be a date and time.
-Stu
coco_vc
10th July 2006 16:53 UTC
I hoped there is a simpler way, like a build cmd line parameter that would right away generate the uninstall too. Probably there is no such way.
What you did is what I also was thinking: running the script on the buildserver and get the uninstaller, but the way you describe is at least nicer b/c you don't really do anything while installing, just generate the uninstaller.
I'm not sure I understand the: "In the main installer script, it is compiled with !system "makensis ..." and then executed with !execute to create the uninstall executable."
Why not just running the second dummy installer to generate the uninstaller?
Viv
Afrow UK
10th July 2006 22:59 UTC
You should re-compile the installer that generates the uninstaller first, because you may make changes to the uninstall code. If you don't you may miss these changes from your update.
-Stu
coco_vc
11th July 2006 15:23 UTC
Hi Stu,
Thx for your answer, but I still fail to understand :(
Basicall what we want is to get the uninstall.exe, while building, so that we have the same binary that the user will have after installing.
The steps you describe, as far as I understood, are:
1) on build-time create somehow a dummy.nsi that contains:
- a section where is WriteUninstaller line and then Quit
- and a Section Uninstall containing the uninstall code, which somehow (how???) gets copied from your real installer
2) run the dummy.exe generate after building the above created dummy.nsi, so that you'll get the uninstall.exe which is the same binary the user will have.
Questions:
a) how can you generate the uninstall code on build-time? I mean how do you know automatically what to copy from your real installer into the dummy.nsi as besides Section Uninstall, there might be more functions and hidden section needed?
b) where and how do you use the !system and !execute calls?
Thx a lot,
Viv
Afrow UK
11th July 2006 16:57 UTC
a) You write the uninstall code in dummy.nsi yourself. It doesn't have to be in the same .nsi script - after all as far as makensis can tell you're just building another installer completely dependent of your main one.
b) See the documentation for !system and !execute. They are compile-time instructions and should be used before you include the uninstall executable in your main installer with the File instruction (you won't be using WriteUninstaller anymore for the main installer). !system executes commands like typing it in command-prompt, whereas !execute just allows a path to a program. You could use !system for both... it doesn't matter.
-Stu
coco_vc
12th July 2006 11:11 UTC
Ahaa, now I finally understood. Thx for the idea Stu. I'll give it a try.
Thx,
Viv
coco_vc
4th August 2006 17:35 UTC
Hi Stu,
I did as you suggested here, but I have a question. In my uninstaller-generator.nsi I have the following code:
!include ...
!define ...
Name "${PRODUCT_NAME}"
InstallDir "$PROGRAMFILES\${PRODUCT_COMPANY_LS}"
InstallDirRegKey HKLM "Software\${PRODUCT_COMPANY_NAME}" "${PATH_TO_MAIN_APP_REG_KEY}"
; Uninstaller pages
!insertmacro MUI_LANGUAGE "English"
ReserveFile "${NSISDIR}\Plugins\AdvSplash.dll"
!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
Section -Post
WriteUninstaller "${MY_UNINSTALLER_NAME}"
Quit ; to avoid any human interaction
SectionEnd
Function un.onInit
....
and in my main installer I have smth like:
!system 'makensis /V4 /Ouninstaller.log uninstaller-generator.nsi' ; builds the uninstaller-generator.nsi to get the ${MY_UNINST_GENERATOR}
!system '${MY_UNINST_GENERATOR}' ; generates the ${MY_UNINSTALLER_NAME} for being later added to the package
Problem is that on build-time (when building the installer.nsi), when ${MY_UNINST_GENERATOR} is run to generate the uninst.exe, the uninst.exe is generated in "$PROGRAMFILES\${PRODUCT_COMPANY_LS}" which is annoying, as this is smth that doesn't exist on the buildserver. Is there a way to force the WriteUninstaller to generate the exe in the directory where the .nsi is and from where the original build was started without using command line params?
Thx,
Viv
Afrow UK
4th August 2006 17:42 UTC
MY_UNINSTALLER_NAME in the uninstall script needs to be
$EXEDIR\uninst.exe
-Stu
coco_vc
4th August 2006 17:55 UTC
Yes, great, works! :)
Thx Stu,
Viv
coco_vc
7th August 2006 17:01 UTC
Hi Stu and *,
One last question related to the issue (hope so) ...
As we did above is: just one command to build the installer which actually does 3 things:
- builds the uninstall-generator.nsi
- runs the obtained exe to get the uninst.exe
- builds the final installer (that has in the package the uninst.exe)
In some situations I need to do this in 2 different steps. So I need to:
- run the installer.nsi with a param showing that I just need the uninstaller to be generated
- run the installer.nsi with a param showing that I just need the final package
- sometimes run the installer.nsi with a param showing that I need both of the above
For this I wrote the following code:
!if ${GENERATE_UNINST_AND_FINAL_PACKAGE} == "0"
!else
!system 'makensis /V4 /Ouninstaller.log Uninstaller.nsi
!system '${MY_UNINST_GENERATOR}'
!if ${GENERATE_UNINST_AND_FINAL_PACKAGE} == "1"
!error "NOT a real ERROR: It was desired only the step of running the uninstaller-generator which was done, so stop here!"
!endif
!endif
So, this means the following behaviour depending on the value of GENERATE_UNINST_AND_FINAL_PACKAGE:
- 0 means - don't generate the uninstaller, just create the final package (so skip the 2 !system calls)
- 1 means - generate the uninstaller only, so don't create also the final package (so run the 2 !system calls only and then right away exit)
- 2 means - generate both: the uninstaller and the final package (so run the 2 !system calls and the go further)
- not defined - it's like 2, meaning generate both
Everything works fine, but I would like to quit the compile session without an error, as my perl-script that calls the makensis checks the return value and in case this is an error it stops, so in my case it will always stop b/c I quit the compile session with !error. Is there a way to stop the compilation (like I do now with !error), but without the compiler to return an error?
Thx,
Viv
coco_vc
8th August 2006 11:58 UTC
I didn't get any answer to my last question. Does that mean that there is no other way to stop compilation besides the !error command? (which actually would be normal, but I anyhow asked as maybe smbd could have gave me an idea on how to do the above).
Thx,
Viv
Afrow UK
8th August 2006 12:15 UTC
No there isn't.
-Stu
coco_vc
8th August 2006 13:21 UTC
Thx Stu.
btw, my second !system call
!system '${MY_UNINST_GENERATOR}'
returns 2.
This is b/c in the uninstaller-generator.nsi I have:
Section -Post
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
Quit ; to avoid any human interaction
SectionEnd
and seems that Quit makes the return code to be 2. I need Quit, as I don't want any dialog to be shown (Abort doesn't do the job), but I would also like to get the return value 0 if everything ok. I know I could do:
Section -Post
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
SetErrorLevel 0
Quit ; to avoid any human interaction
SectionEnd
but then I will never notice when WriteUninstaller returned failure.
So, how could I do that? How could I use Quit, but still get the "correct" return value?
Thx,
Viv
Afrow UK
8th August 2006 13:48 UTC
I don't think you can.
-Stu
coco_vc
8th August 2006 16:09 UTC
Can I then maybe replace the Quit command with smth else that supresses the dialog?
Afrow UK
8th August 2006 16:13 UTC
How about making the installer run silently with SilentInstall silent.
-Stu
coco_vc
9th August 2006 14:11 UTC
Great idea Stu! Thx!
I changed the uninstaller-generator.nsi so that it looks like:
SilentInstall silent
Section -Post
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
SectionEnd
Function un.onInit
...
so that now the return value is 0 and also I don't need human interaction. Nice. Exactly what I needed.
But, what is weird is that I have tried to make the WriteUninstaller call to fail, so that the return value would be different than 0, but I wasn't able to. I even tried:
WriteUninstaller "K:\${MY_UNINSTALLER_NAME}"
without having a K: on my hdd, but the return value is always 0, even though the uninst.exe wasn't obviously created.
Any clue why it didn't return failure?
And any clue how could I simulate a situation in which this installer would return failure?
Thx,
Viv
Afrow UK
9th August 2006 14:35 UTC
Check if the uninstaller was created with IfFileExists and set the required error level with SetErrorLevel. If that does not work, then I guess silent installers always return 0 or something.
-Stu
coco_vc
9th August 2006 15:04 UTC
Actually even without silent the WriteUninstaller "K:\${MY_UNINSTALLER_NAME}" doesn't return error which I assume it's wrong, but for this problem I'll start another thread.
I changed the code as you suggested:
Section -Post
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
SetErrorLevel 5
done.Post:
SectionEnd
and this works fine, but this one doesn't detect all failure cases for instance: there was already an uninst.exe which was read-only so the WriteUninstaller couldn't have overwritten the file so it basically failed. Would have been nice the WriteUninstaller to actually return an error when failing for whatever reason.
Thx,
Viv
Afrow UK
9th August 2006 15:29 UTC
You could delete the existing installer first with Delete which would set the error flag:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfErrors +2
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" +2
SetErrorLevel 5
SectionEnd
-Stu
coco_vc
9th August 2006 15:58 UTC
Thx Stu! Actually I wrote the code like:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
SetErrorLevel 5
done.Post:
SectionEnd
as I assumed that if Delete fails, automatically the return value of the running exe will be different than 0. I hope this is ok, is it?
The surprise was, that actually Delete is able to delete the file even if the file is read-only. I wasn't able to make the Delete fail on my file (I tried read-only and file in use, but in booth cases Delete succedes).
Thx,
Viv
Afrow UK
9th August 2006 16:58 UTC
Yes, but if Delete fails, then an old uninstall executable will exist which will be included in your installer. Surely you don't want that to happen?
The IfErrors will prevent that.
-Stu
coco_vc
10th August 2006 13:59 UTC
Hi Stu,
I don't really understand the diff between your code and my code. I'll try to explain, just from theoretical pov, as practically I wasn't able to make the Delete to fail.
Your code:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfErrors +2
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" +2
SetErrorLevel 5
SectionEnd
which means: if delete fails, the IfErrors is true so the command "SetErrorLevel 5" will be executed, so the installer will return an error, in this case error number 5.
My code:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
SetErrorLevel 5
done.Post:
SectionEnd
which means: if delete fails, automatically the error flag will be set (by the Delete call), so automatically the installer will return an error code, so smth different than 0 (let's say 3, or whatever error number is set when Delete fails)
So, what is the diff between the 2 chunks of code, besides the fact that the returned error code might be different, like in one case is 5 and in another is let's say 3? The behaviour of the 2 parts of code is the same right?
Thx,
Viv
Afrow UK
10th August 2006 14:11 UTC
I see, in which case it needs to be like this:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfErrors +3
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" +2
SetErrorLevel 5both
SectionEnd
If the file is locked, then Delete will fail and we want to return the error code for it (say 3). If Delete was successful, but WriteUninstaller was not, then we want the error code to be 5.
Without the IfErrors in there, if the old uninstaller isn't deleted then you will get an apparently successful uninstall generation with no error code.
-Stu
coco_vc
10th August 2006 14:24 UTC
Maybe I am missing some important information, b/c I still don't see the point of having the:
IfErrors +3
in your above code.
I mean, if in the code above the error was set by the failed Delete call, it will _not_ be reseted regardless if we have the IfErrors line or not.
I mean, if the IfErrors from above code is missing: Delete fails, so the internal error code is let's say 3. Then we do the IfFileExists which b/c the file exists (as Delete failed) will jump to the SectionEnd, but the internal error set by the failed Delete is still 3, so the uninstall generation will return 3 right?
Thx,
Viv
Afrow UK
10th August 2006 14:34 UTC
Ah yes I see :)
-Stu
coco_vc
10th August 2006 15:39 UTC
Ok :) So, this is clear now ... Still, any clue how can I make Delete to fail? I tried to make the file read-only, to open it for being in use, but actually the Delete always succedes. How come?
Viv
Afrow UK
10th August 2006 15:43 UTC
Because the uninstaller copies itself to the temp folder and runs itself again from there so that the original executable can be deleted.
You need to pass it the _?=C:\tempfolder parameters to stop it being copied.
-Stu
coco_vc
11th August 2006 10:13 UTC
Hi Stu,
I wasn't talking about that. I was talking about the following scenario:
- having an uninstaller-generator.nsi file that contains in the installer section only:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
SetErrorLevel 5
done.Post:
SectionEnd
- by building the above nsi I get the exe called, let's say run_me_to_generate_uninst.exe
- by running this exe, the only thing that should happen is that in the same directory the ${MY_UNINSTALLER_NAME} should be generated
Now, the problem is that if the ${MY_UNINSTALLER_NAME} already exists in the exe dir and it's read-only, or it's in use, sttill when running the run_me_to_generate_uninst.exe the code:
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
succedes, even though the "$EXEDIR\${MY_UNINSTALLER_NAME}" is in use or read-only.
And I'm not sure how can this succede?
Thx,
Viv
Afrow UK
11th August 2006 10:37 UTC
I very much doubt the read only flag will stop it being deleted. If the uninstaller is in use, then what I've said applies.
-Stu
coco_vc
16th August 2006 15:04 UTC
Hi Stu,
Yes, you are right, so I started the uninst.exe with _?= param.
What I have noticed is that the following code works just fine:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfErrors done_err.Post
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
done_err.Post:
SetErrorLevel 5
done.Post:
SectionEnd
meaning it returns 5 when the file is in use so Delete fails.
But I would have expected also a return error code for the following code:
Section -Post
ClearErrors
Delete "$EXEDIR\${MY_UNINSTALLER_NAME}"
WriteUninstaller "$EXEDIR\${MY_UNINSTALLER_NAME}"
IfFileExists "$EXEDIR\${MY_UNINSTALLER_NAME}" done.Post 0
done_err.Post:
SetErrorLevel 5
done.Post:
SectionEnd
as I would expect that b/c Delete fails, the running of this exe would return an error code, but actually it returns 0. Why is that?
Thx,
Viv
Afrow UK
16th August 2006 17:28 UTC
I don't think now that the error flag has anything to do with the error levels.
These are the error levels in the documentation:
0 - Normal execution (no error)
1 - Installation aborted by user (cancel button)
2 - Installation aborted by script
You need to set the error level yourself then like in your first example.
-Stu
coco_vc
17th August 2006 11:33 UTC
I don't think now that the error flag has anything to do with the error levels.
Yes, this seems to be the case. I wasn't aware of that. I somehow was sure that it will return a value diff than 0 if one instruction fails.
Thx Stu,
Viv