Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a shortcut in startmenu using setuptools windows installer

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?

like image 237
yasar Avatar asked Jul 19 '14 13:07

yasar


3 Answers

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'Software\Microsoft\Windows\CurrentVersion\Explorer\User 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

like image 197
johan Avatar answered Nov 20 '22 02:11

johan


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)
like image 22
mmitchell Avatar answered Nov 20 '22 00:11

mmitchell


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:\Python36\python.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("Opened\r\n")
        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)
like image 1
szmoore Avatar answered Nov 20 '22 01:11

szmoore