Search code examples
pythonwindowsexepyinstallergpo

Adding a single python executable to windows system PATH for multiple computers?


I've created a command line program that I would like to distribute to some folks at work. Getting them all to install the python interpreter is simply unrealistic. Hence, I've created a single .exe file using PyInstaller. I am coming to realize however, that most people don't even know how to navigate to the directory where the .exe is, in order to invoke it. (And as of now, I haven't yet figured out how to get the program to run when clicked.) Is there a way to make the program add it's self to the users sys PATH when it is run the first time or would this require an installer? Thanks!


Solution

  • The common trap would be to read the PATH env. variable by using os.environ('PATH'). That would be a big mistake because this variable contains user & system paths mixed together. That's a special case for the PATH variable.

    What you need to do is to fetch PATH env variable from the registry (user part), update it if needed, and write it back.

    You can achieve that using winreg module, modifying the user PATH environment variable (or create if doesn't exist for this particular user)

    • read user PATH variable
    • if exists, tokenize the paths (else, path list defaults to empty)
    • compute the path of the current module (using os.path.dirname(__file__))
    • check if already in the path, if so, exit (I print the path list in that case so you can test)
    • create/update PATH user env. variable with the updated path list if necessary

    Code:

    import winreg,os
    
    script_directory = os.path.dirname(__file__)
    
    paths = []
    key_type = winreg.REG_EXPAND_SZ  # default if PATH doesn't exist
    try:
        keyQ = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_QUERY_VALUE)
        path_old, key_type = winreg.QueryValueEx(keyQ, "PATH")
        winreg.CloseKey(keyQ)
        paths = path_old.split(os.pathsep)
    except WindowsError:
        pass
    
    if script_directory in paths:
        # already set, do nothing
        print(paths)
    else:
        # add the new path
        paths.append(script_directory)
        # change registry
        keyQ = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_WRITE)
        winreg.SetValueEx(keyQ, 'PATH', 0, key_type, os.pathsep.join(paths))
        winreg.CloseKey(keyQ)
    

    Note that the user will have to logoff/logon for changes to take effect. Another solution would be to call setx on the PATH variable. System call, ugly, but effective immediately.

    # change registry with immediate effect
    import subprocess
    subprocess.call(["setx","PATH",os.pathsep.join(paths)])
    

    Or, courtesy to eryksun, some python code to propagate the registry changes to new processes. No need to logoff, no need for ugly setx, just call broadcast_change('Environment') using the code below:

    import ctypes
    
    user32 = ctypes.WinDLL('user32', use_last_error=True)
    
    HWND_BROADCAST = 0xFFFF
    WM_SETTINGCHANGE = 0x001A
    SMTO_ABORTIFHUNG = 0x0002
    ERROR_TIMEOUT = 0x05B4
    
    def broadcast_change(lparam):
        result = user32.SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE,
                    0, ctypes.c_wchar_p(lparam), SMTO_ABORTIFHUNG, 1000, None)
        if not result:
            err = ctypes.get_last_error()
            if err != ERROR_TIMEOUT:
                raise ctypes.WinError(err)
    

    (seems that I have to refactor some code of my own with that last bit :))

    env. variable read code took from here: How to return only user Path in environment variables without access to Registry?