Search code examples
pythonscons

Reusing Environment from a different project's SConstruct


I have been given a completed source archive (call it legacyProject) that has a SConstruct build script. This build script does a lot of work to create a nice customized Environment that understands a custom toolchain for a microcontroller. It also has a couple of helper functions that simplify the generation of Program statements for a rather large build matrix.

I am starting a related project that could reuse this Environment and related code almost verbatim. I don't want to just copy-paste everything into a new SConstruct file, because the original might receive patches (not to mention that it's simply duplication of code). Currently these projects are side-by-side in the filesystem:

myProject/
    SConstruct
legacyProject/
    SConstruct

I'll probably rearrange them so that legacyProject is a subdirectory of myProject, so I can track exact revisions with version control.

myProject/
    SConstruct
    legacyProject/
        SConstruct

Is there a way to import all the code from legacyProject/SConstruct? With Python modules, this is trivial with import, but I don't know if this is possible with Scons. My attempt:

SConscript('legacyProject/SConstruct')

just returns None.


Solution

  • Without refactoring some of the functionality from the legacy SConstruct into a helper file, I don't believe there is a way to do this.

    If you can refactor the code out, then there are three possible alternatives approaches available - one is to use the existing SConscript mechanism built into SCons, another is to use python modules and the final approach is to use the site-dir option.

    Reuse Via SConscripts

    Assuming a project structure like:

    .
    ├── common
    │   └── SConscript
    ├── legacyProject
    │   └── SConstruct
    └── myProject
        └── SConstruct
    

    Your SConstruct files will create an environment, then execute the common/SConscript, which returns a modified environment. For example, if you wanted to collect some options in the common directory you might end up with something like:

    # common/SConscript
    Import('env')
    env['CCFLAGS']  = '-Wall -Wextra -pedantic'
    Return('env')
    

    And

    # myProject/SConstruct (similar for legacy/SConstruct)
    env = Environment()
    print 'before:', env['CCFLAGS']
    env = SConscript('../common/SConscript', exports = 'env')
    print 'after:', env['CCFLAGS']
    

    pick this approach if:

    • you're familiar with SCons, but less familiar with Python
    • you don't want to pass extra flags to scons or set environment variables
    • you're trying to be agnostic of which installation of Python you're using
    • you're not using variant directories

    Reuse via Python Modules

    If you're going to package scons utilities as python packages, there are two routes you can take.

    The first is to use traditional Python packaging techniques. There are some good guides around - I recommend the Python Packaging User Guide. To get this working you'll need to write a setup.py, and installing using either pip or python setup.py install

    What makes this tricky is that you can have multiple side-by-side installations of Python and SCons, which are subtly coupled through your PATH environment variable. This may mean something that works on one machine may not work on another machine.

    A more common approach is to modify the path python uses to search for modules in your SConstructs - poor practice from a Python perspective, but slightly more maintainable, particularly if your build machines are complex.

    Assuming a project structure like:

    .
    ├── common
    │   └── __init__.py
    ├── legacyProject
    │   └── SConstruct
    └── myProject
        └── SConscript
    

    Your python module can be quite simple:

    # common/__init__.py
    def set_warning_flags(env):
        env['CCFLAGS']  = '-Wall -Wextra -pedantic'
    

    And:

    # myProject/SConstruct
    import sys
    sys.path.insert(0, '..')
    import common
    
    env = Environment()
    print 'before:', env['CCFLAGS']
    common.set_warning_flags(env)
    print 'after:', env['CCFLAGS']
    

    Pick this approach if:

    • you have a lot of external configuration (many builders / complex environments) that only need to be pulled in from a few places.
    • you aren't using variant builds

    Reuse Via site-dir / site-init

    If you want to add functionality on the fly to multiple SConstructs without changing them, you can use the in-built extensibility supported by SCons (see the sections in the user manual Where To Put Your Custom Builders and Tools or search for --site-dir=dir in the SCons Man Page

    In this case you would have:

    .
    ├── common
    │   └── site_init.py
    ├── legacyProject
    │   └── SConstruct
    └── myProject
        └── SConstruct
    

    Where site_init.py was:

    # common/site_init.py
    def set_warning_flags(env):
        env['CCFLAGS']  = '-Wall -Wextra -pedantic'
    

    And the SConstruct was:

    env = Environment()
    print 'before:', env['CCFLAGS']
    set_warning_flags(env)
    print 'after:', env['CCFLAGS']
    

    But you need to invoke SCons (from the myProject / legacyProject directories) with:

    scons --site-dir=../common
    

    Pick this approach if:

    • you want to make changes to both projects builds without changing the code for either.