Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting UAC to requireAdministrator using PyInstaller onefile option and manifest

Okay, I've been going around and around trying to figure this one out. I'm building an application called GraphicScriptWizard.exe using PyInstaller version 2.0 using the -i -F -w and -m options.

The manifest file that I've defined to use with the -m option is called GraphicScriptWizard.exe.manifest and has the following content:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
  <assemblyIdentity version="1.0.0.0"
     processorArchitecture="x86"
     name="GraphicScriptWizard"
     type="win32"/> 

  <!-- Identify the application security requirements. -->
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="requireAdministrator"
          uiAccess="false"/>
        </requestedPrivileges>
       </security>
  </trustInfo>
</assembly>

Using this manifest and the command line options, I'm not getting an executable that prompts for elevation.

For the sake of completeness, the spec file that gets generated by Pyinstaller is :

# -*- mode: python -*-
a = Analysis(['GraphicScriptWizard.py'],
             pathex=[<Full Development Path>],
             hiddenimports=[],
             hookspath=None)
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name=os.path.join('dist', 'GraphicScriptWizard.exe'),
          debug=False,
          strip=None,
          upx=True,
          console=False , icon='SP.ico', manifest='GraphicScriptWizard.exe.manifest')
app = BUNDLE(exe,
             name=os.path.join('dist', 'GraphicScriptWizard.exe.app'))

I've tried compiling with pyinstaller without the -m option and embedding with mt using the command:

mt.exe -manifest GraphicScriptWizard.exe.manifest -outputresource:GraphicScriptWizard.exe;#1

and when I do that, the application prompts me for elevation, but I get an error when the program runs:

"Cannot open self <Full path to exe>\GraphicScriptWizard.exe or archive..."

I'm really at my wit's end and am hoping that someone with more familiarity with Windows resources and manifests can shed some more light on this for me. Is my manifest XML wrong? Is my methodology with Pyinstaller wrong?

like image 550
sid16rgt Avatar asked Dec 20 '12 03:12

sid16rgt


1 Answers

I've just gone down this road myself, and here are my observations and experience.

The first thing to know is that there are two valid locations for the manifest:

  1. Embedded in the executable (or dll) as a resource

  2. Next to the executable (or dll), as you are currently doing.

Read up about manifests here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa374191(v=vs.85).aspx

In my experience, you can use both at the same time, but if there is any overlap, the embedded manifest takes precedence. This is what is screwing you up. The executable created by Pyinstaller has an embedded manifest that sets the requested execution level to "asInvoker" which overrides the level in the manifest you are using.

The --manifest (or manifest parameter to EXE() ) merely modifies the manifest that Pyinstaller places next to the executable/dll, rather than the embedded manifest.

So, we could turn to mt.exe to change the embedded manifest, but as you have found, this results in an application that doesn't run. This is because the application created by Pyinstaller are really two parts; a small executable that extracts an archive then sets up and launches your code in its bundled python environment, and that archive that the executable operates on. This works because the specification for an executable allows there to be arbitrary data appended to the end of the executable file, but outside of the executable itself, as defined by the size recorded in the executable's header. When we run mt.exe on the Pyinstaller created executable, it looks at the header to get the size, and ignores anything beyond that, thus discarding that archive data when it saves your executable with the new manifest, which results in the error you have seen.

The solution I am using is to modify the manifest before the archive data gets appended to the executable, which requires modification of the Pyinstaller source code. The Pyinstaller source has utilities to update the resources in an executable/dll, which it uses as part of its build process. You'll want to look at build.py, winmanifest.py and maybe winresource.py in your Pyinstaller location. I added a parameter to the EXE class and then a step in the assemble method of that class to update the manifest, I do this right before it appends the archive. The meat of what I added is like so:

if self.manifest_override != None:
        print "Overriding default manifest"
        tmpnm = tempfile.mktemp()
        shutil.copy2(exe, tmpnm)
        os.chmod(tmpnm, 0755)
        winmanifest.UpdateManifestResourcesFromXMLFile(tmpnm, self.manifest_override, names=[1], languages=[1033])
        exe = tmpnm
        trash.append(tmpnm)

I have this placed right before the line that reads: exe = checkCache(exe, ... and it should be above, but close to a print "Appending archive to EXE"...

This solution has been working for me, but I am not super happy with it. I would rather override the default manifest that gets embedded, instead of updating it, but my efforts have been fruitless thus far.

Sorry for the wall of text, but there is a lot going on here.

like image 180
gigyas Avatar answered Nov 12 '22 12:11

gigyas