如何使用setuptools Windows安装程序创建开始菜单快捷方式

11 投票
4 回答
4739 浏览
提问于 2025-04-18 13:57

我想为我的Python Windows安装包创建一个开始菜单或桌面快捷方式。我正在尝试按照这个链接上的说明来做。

这是我的脚本:

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
    )

我还在脚本设置选项中指定了这个脚本,就像上面提到的文档所说的那样。

这是我用来创建安装程序的命令:

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

在我使用创建的Windows安装程序安装我的包后,我找不到我的快捷方式在哪里,也无法确认我的脚本是否运行了。

我该如何让setuptools生成的Windows安装程序创建桌面或开始菜单的快捷方式呢?

4 个回答

0

更新:如果客户端机器上安装了 pywin32,我们会先尝试在同一个进程中创建,这样会更干净一些。


这里还有另一种方法。这种方法假设你的包叫做 myapp,这个包也会变成你想要创建快捷方式的可执行文件。你可以用自己的包名和快捷方式文本来替换。

这个方法使用了Windows脚本宿主的COM类,如果可能的话会在同一个进程中运行,如果不行的话就会在Powershell命令行中作为子进程运行。已经在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"]},

2

在Windows上使用Python 3.6.5的32位版本时,setuptools 确实可以用来实现这个功能。不过根据接受的答案,通过反复尝试,我发现了一些可能导致你的脚本无法正常工作的原因。

  1. create_shortcut 这个函数不接受关键字参数,只能用位置参数,所以你在代码中的用法是错误的。
  2. 你必须为Windows添加一个 .lnk 后缀,这样系统才能识别这个快捷方式。
  3. 我发现 sys.executable 返回的是安装程序的可执行文件名,而不是Python的可执行文件名。
  4. 正如提到的,你无法看到 stdoutstderr 的输出,所以你可能需要把日志记录到一个文本文件中。我建议你也把 sys.stdoutsys.stderr 重定向到这个日志文件。
  5. (可能不太相关)正如在这个问题中提到的,bdist_wininst 生成的版本字符串似乎有个bug。我使用了那里的一个答案中的十六进制编辑技巧来解决这个问题。答案中的位置可能不一样,你需要自己找到 -32

完整的示例脚本:

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)
2

像其他人评论的那样,这些支持功能似乎根本不管用(至少在使用setuptools的时候)。经过一天的努力,我在各种资源中找到了一个方法,至少可以创建桌面快捷方式。我分享一下我的解决方案(基本上是我在这里这里找到的代码的结合)。我还要补充一点,我的情况和yasar的稍微不同,因为我创建的是指向已安装包的快捷方式(也就是Python的Scripts目录中的一个.exe文件),而不是一个脚本。

简单来说,我在我的setup.py文件中添加了一个post_install函数,然后使用Windows的Python扩展来创建快捷方式。桌面文件夹的位置是从Windows注册表中读取的(还有其他方法可以获取这个位置,但如果桌面在非标准位置,这些方法可能不太可靠)。

#!/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()

这里有一个完整的设置脚本链接,我在其中使用了这个方法:

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

1

如果你想确认脚本是否在运行,可以把输出结果打印到一个文件里,而不是打印到控制台。因为在安装后脚本中打印到控制台的内容可能不会显示出来。

你可以试试这样做:

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)

撰写回答