Distributing Python Plugins that have Dependencies



  • Thanks for pointing to theses limitations I was not aware of.

    However, if you execute pip into a running Cinema 4D as CommandData in the Execute function it shouldn't be an issue:

    • The pip code assumes that is in sole control of the global state of the program. It's the case in theCommandData::Execute you are in charge of the global state of the program.
    • Pip’s code is not thread safe. You are in the Main Thread.
    • Pip assumes that once it has finished its work, the process will terminate. A Python script in Cinema 4D can't close Cinema 4D or the python interpreter.

    So while I never tested, looking at the limitation it should work without any issue. As you can have you plugin checking if import can be done, if not simply leave. Then have another CommandData plugin to install your stuffs. Restart Cinema 4D and it should work.

    Cheers,
    Maxime.



  • @zipit Request's dependency chain isn't too bad, but a couple of those libraries also have dependencies which is where it starts to get a little ugly. I'll give it another go.



  • @m_adam Thanks, I'll have a look at pip and/or an installer script. I'm a bit concerned about adding anything directly to the resource/modules/python/libs folder as there's a chance some other plugin or script will have installed/need a different version of the same library. But, I suppose that I can report this as an installation error to the user and let them sort out which is more important for them to have installed.

    I'll be sure to let you know if I come to a resolution.



  • While I know is not the perfect solution, and is a really quick and dirty I would say find bellow a script to setup a build with some python module.

    Just call c4dpy file.py.

    import subprocess
    import urllib
    import sys
    import c4d
    import os
    import ssl
    import shutil
    import importlib
    import maxon
    
    currentDir = os.path.dirname(__file__)
    c4dpyPath = sys.executable
    c4dTempFolder = c4d.storage.GeGetC4DPath(c4d.C4D_PATH_STARTUPWRITE)
    
    c4dDir = maxon.Application.GetUrl(maxon.APPLICATION_URLTYPE.STARTUP_DIR).GetPath()
    if os.name != "nt" and not c4dDir.startswith(os.sep):
        c4dDir = os.sep + c4dDir
    
    try:
        import pip
        print "pip already installed"
    except ImportError:
        print('start downloading get-pip.py')
    
        url = 'https://bootstrap.pypa.io/get-pip.py'
        pipPath = os.path.join(c4dTempFolder, "get-pip.py")
    
        # Quick hack for MAC until https://plugincafe.maxon.net/topic/11370/urllib2-urlopen-fails-on-c4d-for-mac/8 is fixed
        f = os.path.join(c4dDir, "resource", "ssl", "cacert.pem")
        context = ssl.create_default_context(cafile=f)
    
        # Downloads pip
        urllib.urlretrieve(url, pipPath, context=context)
    
        print('start installing pip')
    
        if os.name == "nt":
            subprocess.call([c4dpyPath, pipPath, "--no-warn-script-location"], shell=True)
        else:
            os.system("{0} {1} {2}".format(c4dpyPath, pipPath, "--no-warn-script-location"))
    
        shutil.rmtree(pipPath, ignore_errors=True)
    
    def installModule(moduleName):
        try:
            importlib.import_module(moduleName)
            print "{0} already installed".format(moduleName)
        except ImportError:
    
            print('start installing {0}'.format(moduleName))
    
            if os.name == "nt":
                subprocess.call([c4dpyPath, "-m", "pip", "install", moduleName, "--no-warn-script-location"], shell=True)
            else:
                os.system("{0} {1} {2} {3}".format(c4dpyPath, "-m pip install", moduleName,"--no-warn-script-location"))
    
            print('{0} installation is done'.format(moduleName))
    
    installModule("numpy")
    

    Cheers,
    Maxime



  • @m_adam said in Distributing Python Plugins that have Dependencies:

    Just call c4dpy file.py.

    I'm not having much luck running or calling c4dpy in R21.

    Microsoft Windows [Version 10.0.17763.737]
    (c) 2018 Microsoft Corporation. All rights reserved.
    
    C:\Users\donovan>cd "C:\Program Files\Maxon Cinema 4D R21"
    
    C:\Program Files\Maxon Cinema 4D R21>c4dpy
    Error running authentication: invalid http response 400.  (https://id.maxon.net/oauth2/access?scope=openid+profile+email&grant_type=refresh_token&refresh_token=[[REDACTED]]&client_secret=[[REDACTED]]) [http_file.cpp(313)]
    ----
    Enter Maxon Account Settings (ENTER to keep input):
    License Check error: invalid http response 400.  (https://id.maxon.net/oauth2/access?scope=openid+profile+email&grant_type=refresh_token&refresh_token=[[REDACTED]]&&client_id=[[REDACTED]]&client_secret=[[REDACTED]]) [http_file.cpp(313)]
    Error: Invalid License
    

    I'm able to open Cinema 4D and don't have any licensing issues there. I also did a complete uninstall/reinstall of C4D and had the same issues.

    Thank you for writing that script, it seems like it will do the trick if I can get c4dpy to run.



  • Try to open Cinema 4D.
    Go to the preference opens the preference folder.
    Go one level before. There is normally another cinéma 4d pref folder called like the previous one with the prefix _p (or _c Im on my phone writting by memory i will confirme tomorrow). Delete thid folder. Try again.

    Cheers,
    Maxime.



  • @m_adam It's a _p suffix. Deleting it eliminated the error messages and allowed me to log in. Thank you!

    Is there any chance that c4dpy.exe can get its license from the C4D installation without having to manually enter it in the command prompt? It's not too much of an issue for me, but I imagine seeing the terminal will be a bit intimidating for end users.



  • @m_adam said in Distributing Python Plugins that have Dependencies:

    While I know is not the perfect solution, and is a really quick and dirty I would say find bellow a script to setup a build with some python module.

    Hi Maxime,

    Your script got me most of the way there, but I ran into some issues.

    It installs libraries to the default location for system python libraries rather than C4D's lib folder. So, I started modifying it to install to userprefs/python27/lib using the --target flag. But I ran into issues with spaces in the pathname. Which led me to switch to calling with a list of commands/parameters rather than a string.

    After some tweaking, I arrived at this:

    """Install C4D Python Modules
    Installs python modules in C4D's embedded python installation
    
    ## Authors
    
    Maxime of Maxon Computer
    Donovan Keith of Buck Design
    
    Reference:
    https://plugincafe.maxon.net/topic/11775/distributing-python-plugins-that-have-dependencies/8
    
    ## Requirements
    
    Cinema 4D R21+
    
    ## Usage Instructions
    
    1. Copy this file onto your desktop.
    2. Open `terminal` (Mac OS) or `cmd` (Windows) and navigate to your Cinema 4D installation.
    3. `$ c4dpy`
        1. This will run a Cinema 4D specific version of python.
        2. If prompted to login, do so.
        3. If you get an HTTP/authentication error:
            1. Open Cinema 4D.
            2. Edit > Preferences and click on "Open Preferences Folder"
            3. Go up one level in finder/explorer.
            4. Look for a folder with the same name and a `_p` suffix.
            5. Delete this folder.
            6. Try running `c4dpy` again from the console.
    4. Exit the interactive python session by typing `exit()`
    5. Run this script.  
    `>>> c4dpy "/path/to/this/script/install_c4dpy_modules.py"`
    
    ## Known Limitations
    
    1. Hasn't been tested on Mac OS
    2. Is checking the `c4dpy` installation for whether a python module has been installed, but is installing in the `c4d` installation.
    
    """
    
    print "C4D Py Requirements Installer"
    
    import subprocess
    import urllib
    import sys
    import c4d
    import os
    import ssl
    import shutil
    import importlib
    import maxon
    
    currentDir = os.path.dirname(__file__)
    c4dpyPath = sys.executable
    c4dpyTempFolder = c4d.storage.GeGetC4DPath(c4d.C4D_PATH_STARTUPWRITE)
    
    # Because we're using `c4dpy`, it will pull a different prefs folder from the user's c4d installation.
    # `Maxon Cinema 4D R21_64C2B3BD_p` becomes `Maxon Cinema 4D R21_64C2B3BD`
    c4dTempFolder = c4dpyTempFolder[:-2] if c4dpyTempFolder.endswith("_p") else c4dpyTempFolder
    
    c4dDir = maxon.Application.GetUrl(maxon.APPLICATION_URLTYPE.STARTUP_DIR).GetPath()
    if os.name != "nt" and not c4dDir.startswith(os.sep):
        c4dDir = os.sep + c4dDir
    
    # Just using the `--user` flag with `pip` will install to the System python dir, rather than the
    # C4D embedded python's dir. So we specify exactly where we want to install.
    c4dUserPythonLibPath = os.path.join(c4dTempFolder, "python27", "libs")
    
    try:
        import pip
        print "pip already installed"
    except ImportError:
        print('start downloading get-pip.py')
    
        url = 'https://bootstrap.pypa.io/get-pip.py'
        pipPath = os.path.join(c4dTempFolder, "get-pip.py")
    
        # Quick hack for MAC until https://plugincafe.maxon.net/topic/11370/urllib2-urlopen-fails-on-c4d-for-mac/8 is fixed
        f = os.path.join(c4dDir, "resource", "ssl", "cacert.pem")
        context = ssl.create_default_context(cafile=f)
    
        # Downloads pip
        urllib.urlretrieve(url, pipPath, context=context)
    
        print('start installing pip')
    
        if os.name == "nt":
            subprocess.call([c4dpyPath, pipPath, "--no-warn-script-location", "--user"], shell=True)
        else:
            os.system("{0} {1} {2} {3}".format(c4dpyPath, pipPath, "--no-warn-script-location", "--user"))
    
        shutil.rmtree(pipPath, ignore_errors=True)
    
    def installModule(moduleName):
        try:
            importlib.import_module(moduleName)
            print "{0} already installed".format(moduleName)
        except ImportError:
    
            try:
                print('start installing {0}'.format(moduleName))
    
                # We build up the command as list rather than a string so that Windows will properly handle paths that include spaces
                cmd = [c4dpyPath, '-m', 'pip', 'install', moduleName, '--target', c4dUserPythonLibPath]
                subprocess.call(cmd)
            except subprocess.CalledProcessError as e:
                print e.output
            else:
                print('{0} installation is done'.format(moduleName))
    
    # Add any modules you want installed to this list.
    required_modules = ["requests"]
    
    for module in required_modules:
        installModule(module)
    


  • Hi, @dskeithbuck thanks for sharing yours though. And I'm glad if it works for you. But I still understand that an interpreter can be quiet intimidating for the user. And if you found a nicer solution, don't hesitate to share.

    As an Idea but didn't try and will not have the time to do it today (so if you try, do it at your own risks), but you could try to install things using directly the python executable from the resource folder (as previously when c4dpy didn't exist). Keep in mind this python is a standard python so that means it does not come with any c4d related stuff (so no Maxon/c4d module). This way you don't need the user to log in and can execute the whole thing without the need to pop up the console, and maybe a check if Cinema 4D is running is safer to avoid any issue. You can retrieve a tempfile using the tempfile module in python which comes by default with python.

    With that's said regarding the previous error 400, did you already used this particular c4dpy previously, or it was the first time? Did you already "setupped" this particular c4dpy in any IDE? Any more information to reproduce is welcome.

    Cheers,
    Maxime.



  • @m_adam said in Distributing Python Plugins that have Dependencies:

    As an Idea but didn't try and will not have the time to do it today (so if you try, do it at your own risks), but you could try to install things using directly the python executable from the resource folder

    I'm moving onto different parts of development, but I'll likely want to investigate this later and will post an update if I do. Thanks!