mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-14 18:23:15 +02:00
The variables in the common settings struct are only updated on save, so Python must use the ones from Pgm to get its values. This does lead to the observation that Python's changes to the variables do affect KiCad (they can break resolvers). So apparently it can mess with us but we can't mess with it easily. Also, improve the environment variable tracing infrastructure to capture more changes.
649 lines
18 KiB
C++
649 lines
18 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2012 NBEE Embedded Systems, Miguel Angel Ajo <miguelangel@nbee.es>
|
|
* Copyright (C) 1992-2019 KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
/**
|
|
* @file python_scripting.cpp
|
|
* @brief methods to add scripting capabilities inside pcbnew
|
|
*/
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <python_scripting.h>
|
|
#include <sstream>
|
|
|
|
#include <fctsys.h>
|
|
#include <eda_base_frame.h>
|
|
#include <common.h>
|
|
#include <gal/color4d.h>
|
|
#include <macros.h>
|
|
#include <trace_helpers.h>
|
|
|
|
#include <pgm_base.h>
|
|
|
|
/* init functions defined by swig */
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
extern "C" PyObject* PyInit__pcbnew( void );
|
|
#else
|
|
extern "C" void init_kicad( void );
|
|
|
|
extern "C" void init_pcbnew( void );
|
|
#endif
|
|
|
|
#define EXTRA_PYTHON_MODULES 10 // this is the number of python
|
|
// modules that we want to add into the list
|
|
|
|
|
|
/* python inittab that links module names to module init functions
|
|
* we will rebuild it to include the original python modules plus
|
|
* our own ones
|
|
*/
|
|
|
|
struct _inittab* SwigImportInittab;
|
|
static int SwigNumModules = 0;
|
|
|
|
/// True if the wxPython scripting layer was successfully loaded.
|
|
static bool wxPythonLoaded = false;
|
|
|
|
|
|
bool IsWxPythonLoaded()
|
|
{
|
|
return wxPythonLoaded;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a name + initfuction to our SwigImportInittab
|
|
*/
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
static void swigAddModule( const char* name, PyObject* (* initfunc)() )
|
|
#else
|
|
static void swigAddModule( const char* name, void (* initfunc)() )
|
|
#endif
|
|
{
|
|
SwigImportInittab[SwigNumModules].name = (char*) name;
|
|
SwigImportInittab[SwigNumModules].initfunc = initfunc;
|
|
SwigNumModules++;
|
|
SwigImportInittab[SwigNumModules].name = (char*) 0;
|
|
SwigImportInittab[SwigNumModules].initfunc = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add the builtin python modules
|
|
*/
|
|
static void swigAddBuiltin()
|
|
{
|
|
int i = 0;
|
|
|
|
/* discover the length of the pyimport inittab */
|
|
while( PyImport_Inittab[i].name )
|
|
i++;
|
|
|
|
/* allocate memory for the python module table */
|
|
SwigImportInittab = (struct _inittab*) malloc(
|
|
sizeof( struct _inittab ) * ( i + EXTRA_PYTHON_MODULES ) );
|
|
|
|
/* copy all pre-existing python modules into our newly created table */
|
|
i = 0;
|
|
|
|
while( PyImport_Inittab[i].name )
|
|
{
|
|
swigAddModule( PyImport_Inittab[i].name, PyImport_Inittab[i].initfunc );
|
|
i++;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Add the internal modules to the python scripting so they will be available to the scripts.
|
|
*/
|
|
static void swigAddModules()
|
|
{
|
|
#if PY_MAJOR_VERSION >= 3
|
|
swigAddModule( "_pcbnew", PyInit__pcbnew );
|
|
#else
|
|
swigAddModule( "_pcbnew", init_pcbnew );
|
|
#endif
|
|
|
|
// finally it seems better to include all in just one module
|
|
// but in case we needed to include any other modules,
|
|
// it must be done like this:
|
|
// swigAddModule( "_kicad", init_kicad );
|
|
}
|
|
|
|
|
|
/**
|
|
* Switch the python module table to the Pcbnew built one.
|
|
*/
|
|
static void swigSwitchPythonBuiltin()
|
|
{
|
|
PyImport_Inittab = SwigImportInittab;
|
|
}
|
|
|
|
|
|
PyThreadState* g_PythonMainTState;
|
|
|
|
|
|
/**
|
|
* Initialize the python environment and publish the Pcbnew interface inside it.
|
|
*
|
|
* This initializes all the wxPython interface and returns the python thread control structure
|
|
*/
|
|
bool pcbnewInitPythonScripting( const char * aUserScriptingPath )
|
|
{
|
|
int retv;
|
|
char cmd[1024];
|
|
|
|
swigAddBuiltin(); // add builtin functions
|
|
swigAddModules(); // add our own modules
|
|
swigSwitchPythonBuiltin(); // switch the python builtin modules to our new list
|
|
|
|
Py_Initialize();
|
|
PySys_SetArgv( Pgm().App().argc, Pgm().App().argv );
|
|
|
|
#ifdef KICAD_SCRIPTING_WXPYTHON
|
|
PyEval_InitThreads();
|
|
|
|
#ifndef KICAD_SCRIPTING_WXPYTHON_PHOENIX
|
|
#ifndef __WINDOWS__ // import wxversion.py currently not working under winbuilder, and not useful.
|
|
// Make sure that that the correct version of wxPython is loaded. In systems where there
|
|
// are different versions of wxPython installed this can lead to select wrong wxPython
|
|
// version being selected.
|
|
snprintf( cmd, sizeof( cmd ), "import wxversion; wxversion.select( '%s' )", WXPYTHON_VERSION );
|
|
|
|
retv = PyRun_SimpleString( cmd );
|
|
|
|
if( retv != 0 )
|
|
{
|
|
wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, cmd );
|
|
return false;
|
|
}
|
|
#endif // ifndef __WINDOWS__
|
|
|
|
// Load the wxPython core API. Imports the wx._core_ module and sets a
|
|
// local pointer to a function table located there. The pointer is used
|
|
// internally by the rest of the API functions.
|
|
if( !wxPyCoreAPI_IMPORT() )
|
|
{
|
|
wxLogError( "***** Error importing the wxPython API! *****" );
|
|
PyErr_Print();
|
|
Py_Finalize();
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
#if defined( DEBUG )
|
|
RedirectStdio();
|
|
#endif
|
|
|
|
wxPythonLoaded = true;
|
|
|
|
// Save the current Python thread state and release the
|
|
// Global Interpreter Lock.
|
|
g_PythonMainTState = PyEval_SaveThread();
|
|
|
|
#endif // ifdef KICAD_SCRIPTING_WXPYTHON
|
|
|
|
// Load pcbnew inside Python and load all the user plugins, TODO: add system wide plugins
|
|
{
|
|
PyLOCK lock;
|
|
|
|
// Load os so that we can modify the environment variables through python
|
|
snprintf( cmd, sizeof( cmd ), "import sys, os, traceback\n"
|
|
"sys.path.append(\".\")\n"
|
|
"import pcbnew\n"
|
|
"pcbnew.LoadPlugins(\"%s\")", aUserScriptingPath );
|
|
retv = PyRun_SimpleString( cmd );
|
|
|
|
if( retv != 0 )
|
|
wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, cmd );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Run a python method from the pcbnew module.
|
|
*
|
|
* @param aMethodName is the name of the method (like "pcbnew.myfunction" )
|
|
* @param aNames will contain the returned string
|
|
*/
|
|
static void pcbnewRunPythonMethodWithReturnedString( const char* aMethodName, wxString& aNames )
|
|
{
|
|
aNames.Clear();
|
|
|
|
PyLOCK lock;
|
|
PyErr_Clear();
|
|
|
|
PyObject* builtins = PyImport_ImportModule( "pcbnew" );
|
|
wxASSERT( builtins );
|
|
|
|
if( !builtins ) // Something is wrong in pcbnew.py module (incorrect version?)
|
|
return;
|
|
|
|
PyObject* globals = PyDict_New();
|
|
PyDict_SetItemString( globals, "pcbnew", builtins );
|
|
Py_DECREF( builtins );
|
|
|
|
// Build the python code
|
|
char cmd[1024];
|
|
snprintf( cmd, sizeof(cmd), "result = %s()", aMethodName );
|
|
|
|
// Execute the python code and get the returned data
|
|
PyObject* localDict = PyDict_New();
|
|
PyObject* pobj = PyRun_String( cmd, Py_file_input, globals, localDict);
|
|
Py_DECREF( globals );
|
|
|
|
if( pobj )
|
|
{
|
|
PyObject* str = PyDict_GetItemString(localDict, "result" );
|
|
#if PY_MAJOR_VERSION >= 3
|
|
const char* str_res = NULL;
|
|
|
|
if(str)
|
|
{
|
|
PyObject* temp_bytes = PyUnicode_AsEncodedString( str, "UTF-8", "strict" );
|
|
|
|
if( temp_bytes != NULL )
|
|
{
|
|
str_res = PyBytes_AS_STRING( temp_bytes );
|
|
str_res = strdup( str_res );
|
|
Py_DECREF( temp_bytes );
|
|
}
|
|
else
|
|
{
|
|
wxLogMessage( "cannot encode unicode python string" );
|
|
}
|
|
}
|
|
#else
|
|
const char* str_res = str ? PyString_AsString( str ) : 0;
|
|
#endif
|
|
aNames = FROM_UTF8( str_res );
|
|
Py_DECREF( pobj );
|
|
}
|
|
|
|
Py_DECREF( localDict );
|
|
|
|
if( PyErr_Occurred() )
|
|
wxLogMessage( PyErrStringWithTraceback() );
|
|
}
|
|
|
|
|
|
void pcbnewGetUnloadableScriptNames( wxString& aNames )
|
|
{
|
|
pcbnewRunPythonMethodWithReturnedString( "pcbnew.GetUnLoadableWizards", aNames );
|
|
}
|
|
|
|
|
|
void pcbnewGetScriptsSearchPaths( wxString& aNames )
|
|
{
|
|
pcbnewRunPythonMethodWithReturnedString( "pcbnew.GetWizardsSearchPaths", aNames );
|
|
}
|
|
|
|
|
|
void pcbnewGetWizardsBackTrace( wxString& aNames )
|
|
{
|
|
pcbnewRunPythonMethodWithReturnedString( "pcbnew.GetWizardsBackTrace", aNames );
|
|
}
|
|
|
|
|
|
void pcbnewFinishPythonScripting()
|
|
{
|
|
#ifdef KICAD_SCRIPTING_WXPYTHON
|
|
PyEval_RestoreThread( g_PythonMainTState );
|
|
#endif
|
|
Py_Finalize();
|
|
}
|
|
|
|
|
|
wxString PyEscapeString( const wxString& aSource )
|
|
{
|
|
wxString converted;
|
|
|
|
for( wxUniChar c: aSource )
|
|
{
|
|
if( c == '\\' )
|
|
converted += "\\\\";
|
|
else if( c == '\'' )
|
|
converted += "\\\'";
|
|
else if( c == '\"' )
|
|
converted += "\\\"";
|
|
else
|
|
converted += c;
|
|
}
|
|
|
|
return converted;
|
|
}
|
|
|
|
|
|
void pcbnewUpdatePythonEnvVar( const wxString& aVar, const wxString& aValue )
|
|
{
|
|
char cmd[1024];
|
|
|
|
// Ensure the interpreter is initalized before we try to interact with it
|
|
if( !Py_IsInitialized() )
|
|
return;
|
|
|
|
wxLogTrace( traceEnvVars, "pcbnewUpdatePythonEnvVar: Updating Python variable %s = %s",
|
|
aVar, aValue );
|
|
|
|
wxString escapedVar = PyEscapeString( aVar );
|
|
wxString escapedVal = PyEscapeString( aValue );
|
|
|
|
snprintf( cmd, sizeof( cmd ),
|
|
"# coding=utf-8\n" // The values could potentially be UTF8
|
|
"os.environ[\"%s\"]=\"%s\"\n",
|
|
TO_UTF8( escapedVar ),
|
|
TO_UTF8( escapedVal ) );
|
|
|
|
PyLOCK lock;
|
|
|
|
int retv = PyRun_SimpleString( cmd );
|
|
|
|
if( retv != 0 )
|
|
wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, cmd );
|
|
}
|
|
|
|
|
|
#if defined( KICAD_SCRIPTING_WXPYTHON )
|
|
void RedirectStdio()
|
|
{
|
|
// This is a helpful little tidbit to help debugging and such. It
|
|
// redirects Python's stdout and stderr to a window that will popup
|
|
// only on demand when something is printed, like a traceback.
|
|
const char* python_redirect =
|
|
"import sys\n"
|
|
"import wx\n"
|
|
"output = wx.PyOnDemandOutputWindow()\n"
|
|
"sys.stderr = output\n";
|
|
|
|
PyLOCK lock;
|
|
|
|
int retv = PyRun_SimpleString( python_redirect );
|
|
|
|
if( retv != 0 )
|
|
wxLogError( "Python error %d occurred running command:\n\n`%s`", retv, python_redirect );
|
|
}
|
|
|
|
|
|
wxWindow* CreatePythonShellWindow( wxWindow* parent, const wxString& aFramenameId )
|
|
{
|
|
// parent is actually *PCB_EDIT_FRAME
|
|
const int parentId = parent->GetId();
|
|
{
|
|
wxWindow* parent2 = wxWindow::FindWindowById( parentId );
|
|
wxASSERT( parent2 == parent );
|
|
}
|
|
|
|
// passing window ids instead of pointers is because wxPython is not
|
|
// exposing the needed c++ apis to make that possible.
|
|
std::stringstream pcbnew_pyshell_one_step;
|
|
pcbnew_pyshell_one_step << "import kicad_pyshell\n";
|
|
pcbnew_pyshell_one_step << "import wx\n";
|
|
pcbnew_pyshell_one_step << "\n";
|
|
pcbnew_pyshell_one_step << "parent = wx.FindWindowById( " << parentId << " )\n";
|
|
pcbnew_pyshell_one_step << "newshell = kicad_pyshell.makePcbnewShellWindow( parent )\n";
|
|
pcbnew_pyshell_one_step << "newshell.SetName( \"" << aFramenameId << "\" )\n";
|
|
// return value goes into a "global". It's not actually global, but rather
|
|
// the dict that is passed to PyRun_String
|
|
pcbnew_pyshell_one_step << "retval = newshell.GetId()\n";
|
|
|
|
// As always, first grab the GIL
|
|
PyLOCK lock;
|
|
|
|
// Now make a dictionary to serve as the global namespace when the code is
|
|
// executed. Put a reference to the builtins module in it.
|
|
|
|
PyObject* globals = PyDict_New();
|
|
#if PY_MAJOR_VERSION >= 3
|
|
PyObject* builtins = PyImport_ImportModule( "builtins" );
|
|
#else
|
|
PyObject* builtins = PyImport_ImportModule( "__builtin__" );
|
|
#endif
|
|
|
|
wxASSERT( builtins );
|
|
|
|
PyDict_SetItemString( globals, "__builtins__", builtins );
|
|
Py_DECREF( builtins );
|
|
|
|
#ifdef KICAD_SCRIPTING_WXPYTHON_PHOENIX
|
|
auto app_ptr = wxTheApp;
|
|
#endif
|
|
// Execute the code to make the makeWindow function we defined above
|
|
PyObject* result = PyRun_String( pcbnew_pyshell_one_step.str().c_str(), Py_file_input,
|
|
globals, globals );
|
|
|
|
#ifdef KICAD_SCRIPTING_WXPYTHON_PHOENIX
|
|
// This absolutely ugly hack is to work around the pyshell re-writing the global
|
|
// wxApp variable. Once we can initialize Phoenix to access the appropriate instance
|
|
// of wx, we can remove this line
|
|
wxApp::SetInstance( app_ptr );
|
|
#endif
|
|
// Was there an exception?
|
|
if( !result )
|
|
{
|
|
PyErr_Print();
|
|
return NULL;
|
|
}
|
|
|
|
Py_DECREF( result );
|
|
|
|
result = PyDict_GetItemString( globals, "retval" );
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
if( !PyLong_Check( result ) )
|
|
#else
|
|
if( !PyInt_Check( result ) )
|
|
#endif
|
|
{
|
|
wxLogError( "creation of scripting window didn't return a number" );
|
|
return NULL;
|
|
}
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
const long windowId = PyLong_AsLong( result );
|
|
#else
|
|
const long windowId = PyInt_AsLong( result );
|
|
#endif
|
|
|
|
// It's important not to decref globals before extracting the window id.
|
|
// If you do it early, globals, and the retval int it contains, may/will be garbage collected.
|
|
// We do not need to decref result, because GetItemString returns a borrowed reference.
|
|
Py_DECREF( globals );
|
|
|
|
wxWindow* window = wxWindow::FindWindowById( windowId );
|
|
|
|
if( !window )
|
|
{
|
|
wxLogError( "unable to find pyshell window with id %d", windowId );
|
|
return NULL;
|
|
}
|
|
|
|
return window;
|
|
}
|
|
#endif
|
|
|
|
|
|
wxString PyStringToWx( PyObject* aString )
|
|
{
|
|
wxString ret;
|
|
|
|
if( !aString )
|
|
return ret;
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
const char* str_res = NULL;
|
|
PyObject* temp_bytes = PyUnicode_AsEncodedString( aString, "UTF-8", "strict" );
|
|
|
|
if( temp_bytes != NULL )
|
|
{
|
|
str_res = PyBytes_AS_STRING( temp_bytes );
|
|
ret = FROM_UTF8( str_res );
|
|
Py_DECREF( temp_bytes );
|
|
}
|
|
else
|
|
{
|
|
wxLogMessage( "cannot encode unicode python string" );
|
|
}
|
|
#else
|
|
const char* str_res = PyString_AsString( aString );
|
|
ret = FROM_UTF8( str_res );
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
wxArrayString PyArrayStringToWx( PyObject* aArrayString )
|
|
{
|
|
wxArrayString ret;
|
|
|
|
if( !aArrayString )
|
|
return ret;
|
|
|
|
int list_size = PyList_Size( aArrayString );
|
|
|
|
for( int n = 0; n < list_size; n++ )
|
|
{
|
|
PyObject* element = PyList_GetItem( aArrayString, n );
|
|
|
|
if( element )
|
|
{
|
|
#if PY_MAJOR_VERSION >= 3
|
|
const char* str_res = NULL;
|
|
PyObject* temp_bytes = PyUnicode_AsEncodedString( element, "UTF-8", "strict" );
|
|
|
|
if( temp_bytes != NULL )
|
|
{
|
|
str_res = PyBytes_AS_STRING( temp_bytes );
|
|
ret.Add( FROM_UTF8( str_res ), 1 );
|
|
Py_DECREF( temp_bytes );
|
|
}
|
|
else
|
|
{
|
|
wxLogMessage( "cannot encode unicode python string" );
|
|
}
|
|
#else
|
|
ret.Add( FROM_UTF8( PyString_AsString( element ) ), 1 );
|
|
#endif
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
wxString PyErrStringWithTraceback()
|
|
{
|
|
wxString err;
|
|
|
|
if( !PyErr_Occurred() )
|
|
return err;
|
|
|
|
PyObject* type;
|
|
PyObject* value;
|
|
PyObject* traceback;
|
|
|
|
PyErr_Fetch( &type, &value, &traceback );
|
|
|
|
PyErr_NormalizeException( &type, &value, &traceback );
|
|
|
|
if( traceback == NULL )
|
|
{
|
|
traceback = Py_None;
|
|
Py_INCREF( traceback );
|
|
}
|
|
|
|
#if PY_MAJOR_VERSION >= 3
|
|
PyException_SetTraceback( value, traceback );
|
|
|
|
PyObject* tracebackModuleString = PyUnicode_FromString( "traceback" );
|
|
#else
|
|
PyObject* tracebackModuleString = PyString_FromString( "traceback" );
|
|
#endif
|
|
PyObject* tracebackModule = PyImport_Import( tracebackModuleString );
|
|
Py_DECREF( tracebackModuleString );
|
|
|
|
PyObject* formatException = PyObject_GetAttrString( tracebackModule,
|
|
"format_exception" );
|
|
Py_DECREF( tracebackModule );
|
|
|
|
PyObject* args = Py_BuildValue( "(O,O,O)", type, value, traceback );
|
|
PyObject* result = PyObject_CallObject( formatException, args );
|
|
Py_XDECREF( formatException );
|
|
Py_XDECREF( args );
|
|
Py_XDECREF( type );
|
|
Py_XDECREF( value );
|
|
Py_XDECREF( traceback );
|
|
|
|
wxArrayString res = PyArrayStringToWx( result );
|
|
|
|
for( unsigned i = 0; i<res.Count(); i++ )
|
|
{
|
|
err += res[i] + wxT( "\n" );
|
|
}
|
|
|
|
PyErr_Clear();
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find the Python scripting path.
|
|
*/
|
|
wxString PyScriptingPath()
|
|
{
|
|
wxString path;
|
|
|
|
//@todo This should this be a user configurable variable eg KISCRIPT?
|
|
#if defined( __WXMAC__ )
|
|
path = GetOSXKicadDataDir() + wxT( "/scripting" );
|
|
#else
|
|
path = Pgm().GetExecutablePath() + wxT( "../share/kicad/scripting" );
|
|
#endif
|
|
|
|
wxFileName scriptPath( path );
|
|
scriptPath.MakeAbsolute();
|
|
|
|
// Convert '\' to '/' in path, because later python script read \n or \r
|
|
// as escaped sequence, and create issues, when calling it by PyRun_SimpleString() method.
|
|
// It can happen on Windows.
|
|
path = scriptPath.GetFullPath();
|
|
path.Replace( '\\', '/' );
|
|
|
|
return path;
|
|
}
|
|
|
|
|
|
wxString PyPluginsPath()
|
|
{
|
|
// Note we are using unix path separator, because window separator sometimes
|
|
// creates issues when passing a command string to a python method by PyRun_SimpleString
|
|
return PyScriptingPath() + '/' + "plugins";
|
|
}
|