Revision: 7346 http://playerstage.svn.sourceforge.net/playerstage/?rev=7346&view=rev Author: gbiggs Date: 2009-02-23 01:34:23 +0000 (Mon, 23 Feb 2009)
Log Message: ----------- New Python bindings generation from Luke Gumbley, fixed installation directory for Python bindings. Modified Paths: -------------- code/player/trunk/client_libs/libplayerc/bindings/python/CMakeLists.txt code/player/trunk/client_libs/libplayerc/bindings/python/playerc.i code/player/trunk/client_libs/libplayerc/bindings/python/playerc_swig_parse.py code/player/trunk/examples/libplayerc++/example3.cc Modified: code/player/trunk/client_libs/libplayerc/bindings/python/CMakeLists.txt =================================================================== --- code/player/trunk/client_libs/libplayerc/bindings/python/CMakeLists.txt 2009-02-19 03:47:45 UTC (rev 7345) +++ code/player/trunk/client_libs/libplayerc/bindings/python/CMakeLists.txt 2009-02-23 01:34:23 UTC (rev 7346) @@ -17,16 +17,21 @@ SET (CMAKE_SWIG_FLAGS "") - # Generate player_oo.i from playerc.h - SET (playerc_oo_i "${CMAKE_CURRENT_BINARY_DIR}/playerc_oo.i") - ADD_CUSTOM_COMMAND (OUTPUT ${playerc_oo_i} - COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/playerc_swig_parse.py ${PROJECT_SOURCE_DIR}/client_libs/libplayerc/playerc.h ${playerc_oo_i} + # Generate playerc_wrap.i and playerc_wrap.h from playerc.h + SET (playerc_wrap_prefix "${CMAKE_CURRENT_BINARY_DIR}/playerc_wrap") + ADD_CUSTOM_COMMAND (OUTPUT "${playerc_wrap_prefix}.i" + COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/playerc_swig_parse.py ${PROJECT_SOURCE_DIR}/client_libs/libplayerc/playerc.h ${playerc_wrap_prefix} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - VERBATIM - ) - # Empty target to ensure playerc_oo.i gets created - ADD_CUSTOM_TARGET (playerc_oo_i_target ALL - DEPENDS ${playerc_oo_i}) + VERBATIM) + + ADD_CUSTOM_COMMAND(OUTPUT "${playerc_wrap_prefix}.h" + DEPENDS "${playerc_wrap_prefix}.i") + + # Empty target to ensure playerc_wrap.i and playerc_wrap.h get created + ADD_CUSTOM_TARGET (playerc_wrap_i_target ALL + DEPENDS "${playerc_wrap_prefix}.i" + DEPENDS "${playerc_wrap_prefix}.h") + # Copy playerc.i to the build directory SET (playerc_i_in "${CMAKE_CURRENT_SOURCE_DIR}/playerc.i") SET (playerc_i "${CMAKE_CURRENT_BINARY_DIR}/playerc.i") @@ -34,18 +39,27 @@ SWIG_ADD_MODULE (playerc python ${playerc_i}) SWIG_LINK_LIBRARIES (playerc ${PYTHON_LIBRARIES}) - ADD_DEPENDENCIES (${SWIG_MODULE_playerc_REAL_NAME} playerc_oo_i_target) + ADD_DEPENDENCIES (${SWIG_MODULE_playerc_REAL_NAME} playerc_wrap_i_target) TARGET_LINK_LIBRARIES (${SWIG_MODULE_playerc_REAL_NAME} playerxdr playerc playererror) IF (HAVE_JPEG) TARGET_LINK_LIBRARIES (${SWIG_MODULE_playerc_REAL_NAME} playerjpeg) ENDIF (HAVE_JPEG) - # Generate the set up script -# CONFIGURE_FILE (${CMAKE_CURRENT_SOURCE_DIR}/setup.py.cmake ${CMAKE_CURRENT_BINARY_DIR}/setup.py) -# INSTALL (SCRIPT ${PLAYER_CMAKE_DIR}/internal/InstallPythonModule.cmake) + IF (PYTHON_OS_WIN) + GET_FILENAME_COMPONENT (playercpyInstallDir ${PYTHON_EXECUTABLE} PATH) + ELSE (PYTHON_OS_WIN) + # Get the Python version + EXECUTE_PROCESS (COMMAND ${PYTHON_EXECUTABLE} --version + ERROR_VARIABLE pythonVersionString + ERROR_STRIP_TRAILING_WHITESPACE) + STRING (REGEX REPLACE "^Python ([0-9]+\\.[0-9]+).*" "\\1" pythonVersion ${pythonVersionString}) + SET (playercpyInstallDir lib/python${pythonVersion}/site-packages) + ENDIF (PYTHON_OS_WIN) + SET (PYTHON_BINDINGS_INSTALL_DIR ${playercpyInstallDir} CACHE PATH "Python bindings installation directory under $prefix") + MARK_AS_ADVANCED (PYTHON_BINDINGS_INSTALL_DIR) INSTALL (FILES ${CMAKE_CURRENT_BINARY_DIR}/playerc.py ${CMAKE_CURRENT_BINARY_DIR}/_playerc.so - DESTINATION lib/python/site-packages) + DESTINATION ${PYTHON_BINDINGS_INSTALL_DIR}) ELSE (SWIG_FOUND) MESSAGE (STATUS "Python bindings for C client library will not be built - could not find Swig") ENDIF (SWIG_FOUND) @@ -55,3 +69,4 @@ ELSE (PYTHONINTERP_FOUND) MESSAGE (STATUS "Python bindings for C client library will not be built - could not find Python") ENDIF (PYTHONINTERP_FOUND) + Modified: code/player/trunk/client_libs/libplayerc/bindings/python/playerc.i =================================================================== --- code/player/trunk/client_libs/libplayerc/bindings/python/playerc.i 2009-02-19 03:47:45 UTC (rev 7345) +++ code/player/trunk/client_libs/libplayerc/bindings/python/playerc.i 2009-02-23 01:34:23 UTC (rev 7346) @@ -2,7 +2,7 @@ %module playerc %{ -#include "playerc.h" +#include "playerc_wrap.h" #ifndef Py_RETURN_NONE #define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None @@ -408,31 +408,8 @@ // Use this for object-oriented bindings; // e.g., client.connect(...) // This file is created by running ../parse_header.py -%include "playerc_oo.i" +%include "playerc_wrap.i" -%extend playerc_laser -{ -double get_range (int index) -{ - if (index < self->scan_count) - return self->ranges[index]; - else - return 0; -}; -} - -%extend playerc_sonar -{ -double get_scan (int index) -{ - if (index < self->scan_count) - return self->scan[index]; - else - return 0; -}; -} - - %extend playerc_blackboard { Modified: code/player/trunk/client_libs/libplayerc/bindings/python/playerc_swig_parse.py =================================================================== --- code/player/trunk/client_libs/libplayerc/bindings/python/playerc_swig_parse.py 2009-02-19 03:47:45 UTC (rev 7345) +++ code/player/trunk/client_libs/libplayerc/bindings/python/playerc_swig_parse.py 2009-02-23 01:34:23 UTC (rev 7346) @@ -4,309 +4,248 @@ import string import sys +# Global array of PlayerStruct objects (defined below) to hold information about +# structure definitions. +structures=[] -class Rule: +# Accessor functions to allow access to dynamic arrays (e.g. playerc_laser_t.ranges). +# accessor_header goes in playerc_wrap.h, accessor_interface goes in playerc_wrap.i. +accessor_header=""" +typedef struct +{ + TYPE *actual; +} TYPEArray; +""" - def __init__(self): +accessor_interface=""" +typedef struct +{ + TYPE *actual; - self.patterns = [] - self.replacements = [] - self.head = '' - self.foot = '' - return + %extend { + TYPE __getitem__(int index) {return $self->actual[index];} + void __setitem__(int index,TYPE value) {$self->actual[index]=value;} + } +} TYPEArray; +""" +# Class for holding information about a detected structure, including a header +# section for preprocessor directives and typedefs and a 'members' section for +# external functions that become members. +class PlayerStruct: + name='' + header="" + members="" + + def __init__(self,initName): + self.name=initName + + def __eq__(self,other): + if isinstance(other,str): + return self.name==other + return NotImplemented -class Replace: - pass +# Called for every function that matches the regex in main. If successful, +# the function will be removed, temporarily stored in a 'PlayerStruct' class, +# and later reconstituted as a member function. +def memberize(match): + # Used to remove the first parameter from a list of function parameters. + # The first parameter to a potential member function is a pointer to the structure + # to work with, which is not necessary after the transformation. + lastParams=re.compile('\(\s*\w+?.*?,\s*(?P<last>.*)\)',re.DOTALL) + funcName=match.group('name') + # The start of the loop checks chunks of the function name for a recognizable structure + # name. e.g. playerc_some_class_do_some_action will try playerc_some, playerc_some_class + # and so on until a match is found. + objName="playerc" + for i in range(1,funcName.count("_")): + objName=funcName[:funcName.find("_",len(objName)+1)] + action=funcName[len(objName)+1:] + # Once a match is found, transform the external function to a member function. + if objName in structures: + struct=structures[structures.index(objName)] + # e.g. playerc_client_create + if action=="create": + # add the function to a structure + struct.members+="\t\t" + objName + " " + match.group('params') + ";\n" + # add a preprocessor directive to ensure swig picks up the old library + # function name as a constructor. + struct.header+="\t#define new_" + objName + " " + funcName + "\n" + # e.g. playerc_client_destroy + elif action == "destroy": + # add the destructor to the structure. No destructors take parameters + # or return a value. + struct.members+="\t\tvoid destroy(void);\n" + struct.header+="\t#define del_" + objName + " " + funcName + "\n" + # e.g. playerc_client_connect + else: + # set 'last' to every parameter in the parameter list except the first. + # if no match, there are less than two parameters, in which case the + # transformed function will have no parameters (void). + lm=lastParams.match(match.group('params')) + if lm: + last=lm.group('last') + else: + last="void" + # add the function prototype to the structure. + struct.members+="\t\t" + match.group('retval') + " " + action + " (%s);\n" % last + # functions that are not constructors or destructors already match the required SWIG pattern, + # so no preprocessor directive is required. + # Since the function is a member function and will now have a prototype + # inside an 'extend' clause in the structure definition, remove the external + # reference by substituting with '' + return '' + # If the function is not a member function of a structure, take no action. + return match.group() +# This function is called for every structure. It attempts to find dynamically +# allocated arrays of simple types (which are not subscriptable in python with +# standard binding) and wrap them in an accessor structure defined at the top of this file. +def accessorize(match): + # 'text' is the new code for the matched structure (to be returned) + text=match.group('decl')+"{" + # 'body' is everything within the {}s of the structure definition + body=match.group('body'); + # 'arrayMember' is a regex designed to match certain member definitions, + # e.g. 'double *ranges;' (playerc_laser_t) + arrayMember=re.compile(r"\b(?P<type>int|double|float)\s*[*]\s*(?P<name>\w+);",re.MULTILINE) + + # performs a substitution on the member definitions described above, + # e.g. 'double *ranges;' becomes 'doubleArray ranges;', + # and appends the modified struct body to the code. + text+=arrayMember.sub("\g<type>Array \g<name>;",body) + + # terminate the modified structure and add the 'footer', usually just the end + # of the typedef. + text+="}"+match.group('footer') + return text; +# Called for every structure in the input stream. Modifies the code block to include +# an appropriate 'header' section with typedefs and preprocessor directives, +# and adds new member function prototypes in a SWIG 'extend' block at the end of the +# structure. +def genifacestruct(match): + # ensure the structure has previously been scanned (should always be the case) + if match.group('name') in structures: + # retrieve the data about the matched structure + struct=structures[structures.index(match.group('name'))] + # add the header block and a typedef to permit the change from structname_t to structname + # (for clarity in python) + text="%header\n%{\n"+struct.header+"\ttypedef " + struct.name+"_t "+struct.name+";\n%}\n\n" + # add the structure declaration and the body, omitting a closing brace + text+=match.group('decl')+"{"+match.group('body') + # add the extension block with member function protoypes, plus a closing brace. + text+="\t%extend\n\t{\n"+struct.members+"\t}\n}" + # add the remainder of the original structure definition (usually just the typedef name) + text+=match.group('footer') + return text + # if no match, print a statement to that effect and return the original structure definition. + # should never happen. + print "No match on %s" % match.group('name') + return match.group() -def compile_comment(): - """Compile comment rule.""" +# Used to remove _t from structure references. +# Had to do this due to ambiguity with typedef statements and callback function prototypes. +# e.g. typedef void (*playerc_putmsg_fn_t) (void *device, char *header, char *data); +# also typedef player_blobfinder_blob_t playerc_blobfinder_blob_t; +# This function ensures that only structures defined in playerc.h are modified. +def underscore(match): + if match.group('name') in structures: + return match.group('name') + return match.group() - rule = Rule() - rule.type = 'comment' - rule.patterns += [re.compile('/\*.*?\*/', re.DOTALL)] - rule.patterns += [re.compile('//.*')] - - return [rule] - - - -def compile(prefix): - """Compute the grammar.""" - - rules = [] - - # Create rule for typedefs - #rule = Rule() - #rule.type = 'typedef' - #rule.patterns += [re.compile('\s*\}\s*%s_t\s*;' % prefix)] - #rules += [rule] - - # Create rule for constructor - rule = Rule() - rule.type = 'constructor' - rule.patterns += [re.compile('%s_t\s*\*\s*%s_create\s*\(.*?;' % (prefix, prefix), re.DOTALL)] - - rule.head = '\n%%extend %s\n{\n' % prefix - rule.foot = '\n}\n' - - rep = Replace() - rep.src = re.compile('\s*%s_create\s*' % (prefix)) - rep.dst = '%s' % prefix - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\w*\s*\)' % prefix) - rep.dst = '()' - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\w*\s*,\s*' % prefix) - rep.dst = '(' - rule.replacements += [rep] - - rules += [rule] - - # Create rule for destructor - rule = Rule() - rule.type = 'destructor' - rule.patterns += [re.compile('\w*\s*%s_destroy\s*\(.*?;' % (prefix), re.DOTALL)] - - rule.head = '\n%%extend %s\n{\n' % prefix - rule.foot = '\n}\n' - - rep = Replace() - rep.src = re.compile('\w*\s*%s_destroy\s*' % (prefix)) - rep.dst = '~%s' % prefix - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\w*\s*\)' % prefix) - rep.dst = '()' - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\w*\s*,\s*' % prefix) - rep.dst = '(' - rule.replacements += [rep] - - rules += [rule] - - # Create rule for regular functions - rule = Rule() - rule.type = 'method' - rule.patterns += [re.compile('\w*\s*%s_\w*\s*\(.*?;' % prefix, re.DOTALL)] - #rule.patterns += [re.compile('\w*\s*%s_[a-zA-Z0-9]*\s*\(.*?;' % prefix, re.DOTALL)] - rule.patterns += [re.compile('\w*\s*\w*\s*%s_\w*\s*\(.*?;' % prefix, re.DOTALL)] - #rule.patterns += [re.compile('\w*\s*\w*\s*%s_[a-zA-Z0-9]*\s*\(.*?;' % prefix, re.DOTALL)] - rule.patterns += [re.compile('\w*\s*\*%s_\w*\s*\(.*?;' % prefix, re.DOTALL)] - #rule.patterns += [re.compile('\w*\s*\*%s_[a-zA-Z0-9]*\s*\(.*?;' % prefix, re.DOTALL)] - - rule.head = '\n%%extend %s\n{\n' % prefix - rule.foot = '\n}\n' - - rep = Replace() - rep.src = re.compile('%s_*' % prefix) - rep.dst = '' - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\s*\w*\s*\)' % prefix) - rep.dst = '()' - rule.replacements += [rep] - - rep = Replace() - rep.src = re.compile('\(*\s*%s_t\s*\*\s*\w*\s*,\s*' % prefix) - rep.dst = '(' - rule.replacements += [rep] - - rules += [rule] - - return rules - - -def extract_prefixes(instream): - """Extract class prefixes.""" - - # Remove block comments from the file - commentRule = re.compile('/\*.*?\*/', re.DOTALL) - tempstream = commentRule.sub ('', instream) - commentRule = re.compile('//.*') - tempstream = commentRule.sub ('', tempstream) - - src = re.compile('playerc_\w*_create') - constructors = src.findall(tempstream) - - prefixes = [] - for c in constructors: - prefixes += [c[:-7]] - - return prefixes - - -def parse_file(instream, rules): - """Apply replacement rules.""" - - outstream = '' - current_struct = None - - while instream: - - line = instream - m = None - - for rule in rules: - # See if this line matches the rule - for pattern in rule.patterns: - m = pattern.match(line) - if m: - break - if not m: - continue - - func = line[m.start():m.end()] - - # Parse comment blocks - if rule.type == 'comment': - #print instream[m.start():m.end()] - instream = instream[m.end():] - outstream += func - break - - # Parse function name and args - (name, sig) = string.split(func, '(', 1) - sig = '(' + sig - tokens = string.split(name) - (rval, name) = (string.join(tokens[:-1]), tokens[-1]) - if name[0] == '*': - name = name[1:] - rval = rval + ' *' - #print '%s | %s | %s' % (rval, name, sig) - - # Apply replacement rules to return type - if rule.type == 'constructor': - rval = '' - - # Apply replacement rules to name - for rep in rule.replacements: - mm = rep.src.search(name) - if not mm: - continue - name = name[:mm.start()] + rep.dst + name[mm.end():] - - # Apply replacement rules to signature - for rep in rule.replacements: - mm = rep.src.match(sig) - if not mm: - continue - sig = sig[:mm.start()] + rep.dst + sig[mm.end():] - - #print rval, name, sig - - outstream += rule.head - outstream += '%s %s %s' % (rval, name, sig) - outstream += rule.foot - - instream = instream[m.end():] - - break - - # If no rule matches - if not m: - outstream += instream[:1] - instream = instream[1:] - - return outstream - - - - if __name__ == '__main__': + # should always be playerc.h infilename = sys.argv[1] + # should always be 'playerc_wrap' outfilename = sys.argv[2] # Read in the entire file file = open(infilename, 'r') - instream = file.read() + ifaceStream = file.read() + file.close() - # Extract "class prefixes" from the header - prefixes = extract_prefixes(instream) + # Pattern to remove all double-spacing (used later) + blank=re.compile('([ \t\f\v\r]*\n){3,}',re.MULTILINE) - #prefixes = ['playerc_log'] + # Remove all comments (multi- and single-line). + comment=re.compile('((/\*.*?\*/)|(//.*?$))', re.DOTALL|re.MULTILINE) + ifaceStream=comment.sub('',ifaceStream) - # Compute the grammar - rules = [] - rules += compile_comment() - for prefix in prefixes: - print 'prefix: %s' % prefix - rules += compile(prefix) + # Pattern to recognise structure definitions + struct=re.compile(""" + (?P<decl>typedef\s+struct\s*\w*\s*) # Declaration + {(?P<body>.*?)}(?P<footer>\s* # Body + (?P<name>playerc_\w+?)(_t)? # Name + \s*;) + """, re.DOTALL|re.VERBOSE) + + # Match and replace all structure definitions with versions that have + # accessor structures for dynamically allocated arrays using the 'accessorize' + # function above. + ifaceStream=struct.sub(accessorize,ifaceStream) + + # remove all double-spacing (pattern defined above) + ifaceStream=blank.sub('\n\n',ifaceStream) - # Parse the file and appy replacement rules - outstream = parse_file(instream, rules) + # output to new header file. It is necessary to modify the original header file to + # allow for data types of structure members to be changed. Pointers are modified + # to structures containing a pointer, to allow the member to be subscriptable in python. + # see comments on the 'accessorize' function above. + file=open("%s.h" % outfilename,"w") + # write accessor structure definitions for arrays of ints, doubles and floats. + for type in ["int","double","float"]: + file.write(accessor_header.replace("TYPE",type)) + # write the modified header file. + file.write(ifaceStream) + file.close() + + # find the names of all the structures defined in the file, and make + # a list of objects to store information about the structures. + # could be done as part of 'accessorize' but left here for clarity. + am=struct.finditer(ifaceStream) + for m in am: + structures+=[PlayerStruct(m.group('name'))] - # Do some final replacements - for prefix in prefixes: - outstream = string.replace(outstream, '%s_t' % prefix, '%s' % prefix) + # Pattern to match function prototypes that could be changed to 'member functions' + # of structures, allowing object-oriented style calling from Python. + function=re.compile(""" + \s* + ((?P<retval> # Capture the return type: + \w+\s*?( # Type name, is a + \** | # pointer OR + &? | # reference OR + (\[\s*[0-9]*\s*\]) # array. + ) + )\s*)? # Only one (potential) return value. + (?P<name> # Capture function name + playerc_\w+ # Class name and member function name + )\s* + (?P<params>\(.*?\)) # Parameters + \s*; + """, re.DOTALL|re.VERBOSE) - guff = '' - propguff = '' - for prefix in prefixes: - guff += '%%header\n %%{\ntypedef %s_t %s;\n' % (prefix, prefix) - guff += '#define new_%s %s_create\n' % (prefix, prefix) - guff += '#define del_%s %s_destroy\n' % (prefix, prefix) - guff += '%}\n' - - # stuff for properties - if prefix != "playerc_mclient" and prefix != "playerc_client": - propguff += """ -%%extend %(prefix)s -{ -int get_intprop (char * propname) -{ - int ret; - if (playerc_device_get_intprop(&self->info,propname,&ret) == 0) - return ret; - else - return 0; -}; -int set_intprop (char * propname, int value) -{ - return playerc_device_set_intprop(&self->info,propname,value); -}; + # remove the _t from the end of all the struct references. + structNames=re.compile(r"\b(?P<name>playerc_\w+)_t\b") + ifaceStream=structNames.sub(underscore,ifaceStream) -double get_dblprop (char * propname) -{ - double ret; - if (playerc_device_get_dblprop(&self->info,propname,&ret) == 0) - return ret; - else - return 0; -}; -int set_dblprop (char * propname, double value) -{ - return playerc_device_set_dblprop(&self->info,propname,value); -}; + # using the 'memberize' function, find functions that should be struct 'members' and shift them. + ifaceStream=function.sub(memberize,ifaceStream) -char * get_strprop (char * propname) -{ - char * ret; - if (playerc_device_get_strprop(&self->info,propname,&ret) == 0) - return ret; - else - return NULL; -}; -int set_strprop (char * propname, char * value) -{ - return playerc_device_set_strprop(&self->info,propname,value); -}; -} """ % {"prefix": prefix} - + # Using the gathered information about functions and structures, rewrite the structure + # definitions in the interface file to include required extensions (function prototypes), + # typedefs and preprocessor directives. see comments on 'genifacestruct' function above. + ifaceStream=struct.sub(genifacestruct,ifaceStream) - outstream = guff + outstream + propguff - - file = open(outfilename, 'w+') - file.write(outstream) - + # remove all double spacing. + ifaceStream=blank.sub('\n\n',ifaceStream) + + # write the SWIG interface definition file. + file=open("%s.i" % outfilename,"w") + # write interfaces for the accessor structs including subscripting functions. + for type in ["int","double","float"]: + file.write(accessor_interface.replace("TYPE",type)) + file.write(ifaceStream) + file.close() + \ No newline at end of file Modified: code/player/trunk/examples/libplayerc++/example3.cc =================================================================== --- code/player/trunk/examples/libplayerc++/example3.cc 2009-02-19 03:47:45 UTC (rev 7345) +++ code/player/trunk/examples/libplayerc++/example3.cc 2009-02-23 01:34:23 UTC (rev 7346) @@ -2,6 +2,7 @@ #include <iostream> #include <list> #include <algorithm> +#include <functional> int main(int argc, char** argv) { This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ Open Source Business Conference (OSBC), March 24-25, 2009, San Francisco, CA -OSBC tackles the biggest issue in open source: Open Sourcing the Enterprise -Strategies to boost innovation and cut costs with open source participation -Receive a $600 discount off the registration fee with the source code: SFAD http://p.sf.net/sfu/XcvMzF8H _______________________________________________ Playerstage-commit mailing list Playerstage-commit@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/playerstage-commit