How to create a shortcut in startmenu using setuptools windows installer

Question:

I want to create a start menu or Desktop shortcut for my Python windows installer package. I am trying to follow https://docs.python.org/3.4/distutils/builtdist.html#the-postinstallation-script

Here is my script;

import sys

from os.path import dirname, join, expanduser

pyw_executable = sys.executable.replace('python.exe','pythonw.exe')
script_file = join(dirname(pyw_executable), 'Scripts', 'tklsystem-script.py')
w_dir = expanduser(join('~','lsf_files'))

print(sys.argv)

if sys.argv[1] == '-install':
    print('Creating Shortcut')
    create_shortcut(
        target=pyw_executable,
        description='A program to work with L-System Equations',
        filename='L-System Tool',
        arguments=script_file,
        workdir=wdir
    )

I also specified this script in scripts setup option, as indicated by aforementioned docs.

Here is the command I use to create my installer;

python setup.py bdist_wininst --install-script tklsystem-post-install.py

After I install my package using created windows installer, I can’t find where my shorcut is created, nor I can confirm whether my script run or not?

How can I make setuptools generated windows installer to create desktop or start menu shortcuts?

Asked By: yasar

||

Answers:

If you want to confirm whether the script is running or not, you can print to a file instead of the console. Looks like text you print to console in the post-install script won’t show up.

Try this:

import sys
from os.path import expanduser, join

pyw_executable = join(sys.prefix, "pythonw.exe")
shortcut_filename = "L-System Toolsss.lnk"
working_dir = expanduser(join('~','lsf_files'))
script_path = join(sys.prefix, "Scripts", "tklsystem-script.py")

if sys.argv[1] == '-install':
    # Log output to a file (for test)
    f = open(r"C:test.txt",'w')
    print('Creating Shortcut', file=f)

    # Get paths to the desktop and start menu
    desktop_path = get_special_folder_path("CSIDL_COMMON_DESKTOPDIRECTORY")
    startmenu_path = get_special_folder_path("CSIDL_COMMON_STARTMENU")

    # Create shortcuts.
    for path in [desktop_path, startmenu_path]:
        create_shortcut(pyw_executable,
                    "A program to work with L-System Equations",
                    join(path, shortcut_filename),
                    script_path,
                    working_dir)
Answered By: mmitchell

Like others have commented here and elsewhere the support functions don’t seem to work at all (at least not with setuptools). After a good day’s worth of searching through various resources I found a way to create at least the Desktop shortcut. I’m sharing my solution (basically an amalgam of code I found here and here). I should add that my case is slightly different from yasar‘s, because it creates a shortcut to an installed package (i.e. an .exe file in Python’s Scripts directory) instead of a script.

In short, I added a post_install function to my setup.py, and then used the Python extensions for Windows to create the shortcut. The location of the Desktop folder is read from the Windows registry (there are other methods for this, but they can be unreliable if the Desktop is at a non-standard location).

#!/usr/bin/env python

import os
import sys
import sysconfig
if sys.platform == 'win32':
    from win32com.client import Dispatch
    import winreg

def get_reg(name,path):
    # Read variable from Windows Registry
    # From https://stackoverflow.com/a/35286642
    try:
        registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
                                       winreg.KEY_READ)
        value, regtype = winreg.QueryValueEx(registry_key, name)
        winreg.CloseKey(registry_key)
        return value
    except WindowsError:
        return None

def post_install():
    # Creates a Desktop shortcut to the installed software

    # Package name
    packageName = 'mypackage'

    # Scripts directory (location of launcher script)
    scriptsDir = sysconfig.get_path('scripts')

    # Target of shortcut
    target = os.path.join(scriptsDir, packageName + '.exe')

    # Name of link file
    linkName = packageName + '.lnk'

    # Read location of Windows desktop folder from registry
    regName = 'Desktop'
    regPath = r'SoftwareMicrosoftWindowsCurrentVersionExplorerUser Shell Folders'
    desktopFolder = os.path.normpath(get_reg(regName,regPath))

    # Path to location of link file
    pathLink = os.path.join(desktopFolder, linkName)
    shell = Dispatch('WScript.Shell')
    shortcut = shell.CreateShortCut(pathLink)
    shortcut.Targetpath = target
    shortcut.WorkingDirectory = scriptsDir
    shortcut.IconLocation = target
    shortcut.save()

setup(name='mypackage',
      ...,
      ...)

if sys.argv[1] == 'install' and sys.platform == 'win32':
    post_install()

Here’s a link to a full setup script in which I used this:

https://github.com/KBNLresearch/iromlab/blob/master/setup.py

Answered By: johan

At least with Python 3.6.5, 32bit on Windows, setuptools does work for this. But based on the accepted answer, by trial and error I found some issues that may have caused your script to fail to do what you wanted.

  1. create_shortcut does not accept keyword arguments, only positional, so its usage in your code is invalid
  2. You must add a .lnk extension for Windows to recognise the shortcut
  3. I found sys.executable will be the name of the installer executable, not the python executable
  4. As mentioned, you can’t see stdout or stderr so you might want to log to a text file. I would suggest also redirecting sys.stdout and sys.stderr to the log file.
  5. (Maybe not relevant) as mentioned in this question there appears to be a bug with the version string generated by bdist_wininst. I used the hexediting hack from an answer there to work around this. The location in the answer is not the same, you have to find the -32 yourself.

Full example script:

import sys
import os
import datetime
global datadir
datadir = os.path.join(get_special_folder_path("CSIDL_APPDATA"), "mymodule")
def main(argv):
    if "-install" in argv:
        desktop = get_special_folder_path("CSIDL_DESKTOPDIRECTORY")
        print("Desktop path: %s" % repr(desktop))
        if not os.path.exists(datadir):
            os.makedirs(datadir)
            dir_created(datadir)
            print("Created data directory: %s" % repr(datadir))
        else:
            print("Data directory already existed at %s" % repr(datadir))

        shortcut = os.path.join(desktop, "MyModule.lnk")
        if os.path.exists(shortcut):
            print("Remove existing shortcut at %s" % repr(shortcut))
            os.unlink(shortcut)

        print("Creating shortcut at %s...n" % shortcut)
        create_shortcut(
            r'C:Python36python.exe',
            "MyModuleScript",
            shortcut, 
            "",
            datadir)
        file_created(shortcut)
        print("Successfull!")
    elif "-remove" in sys.argv:
        print("Removing...")
        pass


if __name__ == "__main__":
    logfile = r'C:mymodule_install.log' # Fallback location
    if os.path.exists(datadir):
        logfile = os.path.join(datadir, "install.log")
    elif os.environ.get("TEMP") and os.path.exists(os.environ.get("TEMP"),""):
        logfile = os.path.join(os.environ.get("TEMP"), "mymodule_install.log")

    with open(logfile, 'a+') as f:
        f.write("Openedrn")
        f.write("Ran %s %s at %s" % (sys.executable, " ".join(sys.argv), datetime.datetime.now().isoformat()))
        sys.stdout = f
        sys.stderr = f
        try:
            main(sys.argv)
        except Exception as e:
            raise
        f.close()

    sys.exit(0)
Answered By: szmoore

UPD: on an off chance that the client machine has pywin32 installed, we try in-process creation first. Somewhat cleaner that way.


Here is another take. This assumes the package is called myapp, and that also becomes the executable that you want a shortcut to. Substitute your own package name and your own shortcut text.

Uses a Windows Scripting Host COM class – in process if possible, inside a Powershell command line as a subprocess if not. Tested on Python 3.6+.

from setuptools import setup
from setuptools.command.install import install
import platform, sys, os, site
from os import path, environ

def create_shortcut_under(root, exepath):
    # Root is an env variable name - 
    # either ALLUSERSPROFILE for the all users' Start menu,
    # or APPDATA for the current user specific one
    profile = environ[root]
    linkpath = path.join(profile, "Microsoft", "Windows", "Start Menu", "Programs", "My Python app.lnk")
    try:
        from win32com.client import Dispatch
        from pywintypes import com_error
        try:
            sh = Dispatch('WScript.Shell')
            link = sh.CreateShortcut(linkpath)
            link.TargetPath = exepath
            link.Save()
            return True
        except com_error:
            return False
    except ImportError:
        import subprocess
        s = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + linkpath + "');$s.TargetPath='" + exepath + "';$s.Save()"
        return subprocess.call(['powershell', s], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) == 0

def create_shortcut(inst):
    try:
        exepath = path.join(path.dirname(sys.executable), "Scripts", "myapp.exe")
        if not path.exists(exepath):
            # Support for "pip install --user"
            exepath = path.join(path.dirname(site.getusersitepackages()), "Scripts", "myapp.exe")

        # If can't modify the global menu, fall back to the
        # current user's one
        if not create_shortcut_under('ALLUSERSPROFILE', exepath):
            create_shortcut_under('APPDATA', exepath)
    except:
        pass

class my_install(install):
    def run(self):
        install.run(self)
        if platform.system() == 'Windows':
            create_shortcut(self)

#...
setup(
#...
    cmdclass={'install': my_install},
    entry_points={"gui_scripts": ["myapp = myapp.__main__:main"]},

Answered By: Seva Alekseyev