Oddly enough, I'm trying to do the (almost) exact same thing as @lpurdy, but I wanted it to work as an add-in that can be installed, instead of requiring that you package the binaries in your repo (since that'll probably break when you use a package that has binaries that do system checks and install different binaries based on the system).
I've combined @lpurdy and David Young's solution (after I found the right way to run the python executable) to make something that should be good to include in an add-in that can be deployed through the store.
My implementation is attempting to install pygame and falling back to using pyjoystick (which I had already update to be a relative module, but doesn't support a custom gamepad I made):
import os
import sys
import platform
import subprocess
from . import config
from .lib import fusionAddInUtils as futil
from adsk.core import LogLevels
def installPygameWindows():
virtualenvDirName = f"{config.ADDIN_NAME}Venv"
# Clean up path in case we crashed somewhere, sys should not contain our virtualenv yet
sys.path = [dir for dir in sys.path if dir.find(virtualenvDirName) == -1]
original_sys_path = sys.path.copy()
virtualenv = os.path.join(sys.path[0], virtualenvDirName)
python = os.path.join(sys.path[0], "Python", "python.exe")
virtualenvSitePackages = os.path.join(virtualenv, "Lib", "site-packages")
if not os.path.isdir(virtualenv):
futil.log(f"{config.ADDIN_NAME}: missing virtualenv, creating...", LogLevels.WarningLogLevel)
subprocess.check_call([python, '-m', 'venv', virtualenv])
futil.log(f"{config.ADDIN_NAME}: virtualenv exists, attempting to import from virtualenv", LogLevels.InfoLogLevel)
# in case of script failure, the virtualenv might already be in the path from a previous run
if not virtualenv in sys.path:
sys.path.insert(0, virtualenvSitePackages)
try:
import pygame
return(True, original_sys_path.copy())
except:
try:
futil.log(f"{config.ADDIN_NAME}: missing pygame, installing...", LogLevels.WarningLogLevel)
subprocess.check_call([os.path.join(virtualenv, "Scripts", "pip.exe"), "install", "--upgrade", "pygame"])
futil.log(f"{config.ADDIN_NAME}: pygame installed", LogLevels.InfoLogLevel)
return (True, original_sys_path.copy())
except:
futil.handle_error("Failed to install and import pygame. See text console for more details", True)
return (False, original_sys_path.copy())
installedPygame = False
if platform.system() is 'Windows':
(installedPygame, original_sys_path) = installPygameWindows()
if installedPygame:
try:
import pygame
futil.log(f"{config.ADDIN_NAME}: pygame installed", LogLevels.InfoLogLevel)
except:
futil.handle_error(f"{config.ADDIN_NAME}: Failed to import pygame, falling back to use pyjoystick (less gamepad support). See text console for more details", True)
installedPygame = False
sys.path = original_sys_path
else:
#TODO: figure out where the python executable is on mac
futil.handle_error("Sorry, this OS is unsupported, falling back to use pyjoystick (less gamepad support)", True)
I then use installedPygame to determine whether to use pygame or pyjoystick.
Note that this only works on Windows (tested only on Windows 11 too), I've yet to find the executables necessary to replicate these commands on Mac (we'd need to locate which python is being run, and probably find the virtualenv's pip, but on mac it might just work to use a bit of bash instead of using a specific pip executable).
Also of note: for some reason, this doesn't work if you stop the add-in and restart it. I have no idea why that is, but I assume it's something specific to pygame and the dynamic linking of SDL2, other packages might work just fine.
My add-in repo where I'm actually using this code: https://github.com/nivekmai/Joystick-Control (note that this add-in totally works for an xbox 360 controller I'm using, and after I update with this new setup, it could probably work for any controller).