Repository: qutebrowser/qutebrowser

Issue:
Improvements for NSIS installer
The new NSIS Windows installer needs some small improvements:

- [ ] Use UAC correctly (only when needed)
- [ ] Add a finish page with an option to start qutebrowser (not as admin!)
- [ ] Add option to uninstaller to remove appdata
- [ ] Detect if qutebrowser is still running (see [Check whether your application is running](http://nsis.sourceforge.net/Check_whether_your_application_is_running)) or already installed (see [Auto-uninstall old before installing new](http://nsis.sourceforge.net/Auto-uninstall_old_before_installing_new))

cc @Link-Satonaka 


Discussion:
@The-Compiler  - I have some more suggestions for NSIS, and i could help with the ones you like:

1. Proper support for per-machine and per-user installations (See [Installation Context on MSDN](https://msdn.microsoft.com/en-us/library/windows/desktop/dd765197.aspx)). Doing this right with NSIS can be tricky, but will also take care of the first two proposed improvements. I think the easiest way is to use the [NsisMultiUser plugin by Drizin](https://github.com/Drizin/NsisMultiUser). It's a rewrite of the original `MultiUser.nsh` and also includes and uses the [UAC plug-in](http://nsis.sourceforge.net/UAC_plug-in).
2. Safe uninstall. Blindly erasing a directory recursively is dangerous. Should at least check it's not a system/user directory (e.g. the user chose `C:\Program Files` by mistake), or create a log of installed files and remove only those.
3. Script directive to increase lzma dictionary size for maximum compression. The default size is 8mb, could be increased to 128mb. The lzma codec needs a little more than the dictionary size in ram for decompression, but 10x of that for compression.
4. Script directive for using upx to pack the exe header. If there is a use case for a runtime packer, it's this. Won't save much though, it's a small file.
5. The script could get the version number directly from `qutebrowser.exe`. This will require to embed a [VERSIONINFO resource](https://msdn.microsoft.com/en-us/library/windows/desktop/aa381058(v=vs.85).aspx) to the `exe`. Pyinstaller does this by referring to a file containing a VSVersionInfo object. Its class is found in `utils/win32/versioninfo.py` of Pyinstaller's directory.
6. A `VERSIONINFO resource` should also be added to the installer's `exe`. This is supported by NSIS directly with recent versions. Parts of this information is displayed to users by Windows Explorer (e.g. on the `Details` tab of the file `Properties` window, the file pop-up description, the `Tiles` view, etc.), other programs can access it using the `GetFileVersionInfo[Ex]` win32 function, and administrators using powershell: `(Get-Item .\installer.exe).VersionInfo | Select-Object *`.
7. It would also be nice for the version to be displayed on the installer's window.  Maybe replacing the text `Nullsoft Install System vX.YZ` with `qutebrowser vX.Y.Z installer`.
8. Don't use the same icon as the main executable.The default Windows taskbar setting, displays icons without names. It might be confusing if different programs with the same icon, are running at the same time.

@bitraid Sorry, I must've missed your comment! All of those sound like very good points. I'm not sure about the custom icon is worth the effort (especially if it potentially results in licensing issues), but the others all sound like good ideas to me.

The Windows installer certainly can use some love - thanks for taking care of that so far! :+1: 
Ok, great. I'm onto it!

Resolving patch:
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -44,7 +44,7 @@
 
 import qutebrowser
 from scripts import utils
-# from scripts.dev import update_3rdparty
+from scripts.dev import update_3rdparty
 
 
 def call_script(name, *args, python=sys.executable):
@@ -221,8 +221,8 @@ def patch_windows(out_dir):
 def build_windows():
     """Build windows executables/setups."""
     utils.print_title("Updating 3rdparty content")
-    # Currently disabled because QtWebEngine has no pdfjs support
-    # update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
+    # pdfjs is currently disabled because it's not supported by QtWebEngine
+    update_3rdparty.run(nsis=True, ace=False, pdfjs=False, fancy_dmg=False)
 
     utils.print_title("Building Windows binaries")
     parts = str(sys.version_info.major), str(sys.version_info.minor)
@@ -256,9 +256,8 @@ def build_windows():
 
     utils.print_title("Building installers")
     subprocess.run(['makensis.exe',
-                    '/DX64',
                     '/DVERSION={}'.format(qutebrowser.__version__),
-                    'misc/qutebrowser.nsi'], check=True)
+                    'misc/nsis/qutebrowser.nsi'], check=True)
 
     name_64 = 'qutebrowser-{}-amd64.exe'.format(qutebrowser.__version__)
 
diff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py
--- a/scripts/dev/update_3rdparty.py
+++ b/scripts/dev/update_3rdparty.py
@@ -35,6 +35,35 @@
 from qutebrowser.config import configdata
 
 
+def download_nsis_plugins():
+    """Download the plugins required by the NSIS script"""
+    github_url = 'https://raw.githubusercontent.com/Drizin/NsisMultiUser'
+    git_commit = 'master'
+    nsh_files = ('Include/NsisMultiUser.nsh', 'Include/NsisMultiUserLang.nsh',
+                 'Include/UAC.nsh', 'Include/StdUtils.nsh',
+                 'Demos/Common/Utils.nsh')
+    dll_files = ('Plugins/x86-unicode/UAC.dll',
+                 'Plugins/x86-unicode/StdUtils.dll')
+    include_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                               '..', '..', 'misc', 'nsis',
+                                               'include')
+    plugins_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                               '..', '..', 'misc', 'nsis',
+                                               'plugins', 'x86-unicode')
+    os.makedirs(include_dir, exist_ok=True)
+    os.makedirs(plugins_dir, exist_ok=True)
+    print("=> Downloading NSIS plugins")
+    for nsh_file in nsh_files:
+        target_path = os.path.join(include_dir, os.path.basename(nsh_file))
+        urllib.request.urlretrieve('{}/{}/{}'.format(github_url, git_commit,
+                                                     nsh_file), target_path)
+    for dll_file in dll_files:
+        target_path = os.path.join(plugins_dir, os.path.basename(dll_file))
+        urllib.request.urlretrieve('{}/{}/{}'.format(github_url, git_commit,
+                                                     dll_file), target_path)
+    urllib.request.urlcleanup()
+
+
 def get_latest_pdfjs_url():
     """Get the URL of the latest pdf.js prebuilt package.
 
@@ -130,9 +159,11 @@ def test_dicts():
             print('ERROR: {}'.format(response.status))
 
 
-def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
+def run(nsis=False, ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
         dicts=False):
     """Update components based on the given arguments."""
+    if nsis:
+        download_nsis_plugins()
     if pdfjs:
         update_pdfjs(pdfjs_version)
     if ace:
@@ -145,6 +176,8 @@ def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
 
 def main():
     parser = argparse.ArgumentParser()
+    parser.add_argument('--nsis', '-n', help='Download NSIS plugins.',
+                        required=False, action='store_true')
     parser.add_argument(
         '--pdfjs', '-p',
         help='Specify pdfjs version. If not given, '
@@ -158,7 +191,7 @@ def main():
         'can be reached at the remote repository.',
         required=False, action='store_true')
     args = parser.parse_args()
-    run(ace=True, pdfjs=True, fancy_dmg=args.fancy_dmg,
+    run(nsis=False, ace=True, pdfjs=True, fancy_dmg=args.fancy_dmg,
         pdfjs_version=args.pdfjs, dicts=args.dicts)
 
 


Test patch: