Wiki

Clone wiki

SCons / InstallFiles

InstallFiles is a pseudo-builder that scans a source directory for files matching a certain pattern and installs the results in a destination direction. Support for glob patterns, exclude patterns, and recursion are provided. Glob patterns only apply to files, but the exclude patterns also apply to directories. If the source is a file, it is copied to the destination. If the source is a directory, it is scanned, and items in that directory are copied to the destination. This uses the InstallAs builder, so using --install-sandbox=/path/to/sandbox will affect it as well.

Sample usage:

#!python 
TOOL_INSTALL(env)

# Prevent installation of certain undesired items
env.InstallExclude('.*', '*~', '*.tmp', '*.bak', '*.pyc', '*.pyo')

# Install a file to a destination.  When the source is a file
# glob/exclude/recursion is not important
env.InstallFiles('$PREFIX/bin', program)

# Install contents of a directory.  When the source is a directory
# glob/exclude/recursion is important and will affect items in it
env.InstallFiles('$PREFIX/share/myapp/pixmaps', '#data/pixmaps')

# You can also specify a glob to match or additional excludes in
# addition to those specified with InstallExclude
env.InstallFiles('$PREFIX/share/myapp/help', '#data/help', glob=['*.html', '*.png'], exclude=['*.svg'])

In addition you can tell it to create named packages of files and then later install those packages into the correct location.

#!python 

# File: data/SConscript:

# Make files under 'pixmaps' and 'pixmaps-extra' install to the 'pixmaps' directory of the 'data' package
env.InstallPackageAccum('data', 'pixmaps', 'pixmaps', scan=1)
env.InstallPackageAccum('data', 'pixmaps', 'pixmaps-extra', scan=1)

# File: installers/linux/SConscript:

# Install the 'data' package to the data directory
env.InstallPackage('/usr/share/myapplication', 'data')

The new builder code that also scans the build nodes as well

#!python 
# File:         install.py
# Author:       Brian A. Vanderburg II
# Purpose:      An install builder to install subdirectories and files
# Copyright:    This file is placed in the public domain.
##############################################################################


# Requirements
##############################################################################
import os
import fnmatch

import SCons
import SCons.Node.FS
import SCons.Errors
from SCons.Script import *


# Install files code
##############################################################################

def _is_excluded(name, exclude):
    """
    Determine if the name is to be excluded based on the exclusion list
    """
    if not exclude:
        return False
    return any( (fnmatch.fnmatchcase(name, i) for i in exclude) )


def _is_globbed(name, glob):
    """
    Determine if the name is globbed based on the glob list
    """
    if not glob:
        return True
    return any( (fnmatch.fnmatchcase(name, i) for i in glob) )


def _get_files(env, source, exclude, glob, recursive, reldir=os.curdir):
    """
    Find files that match the criteria.

    source - absolute path to source file or directory (not a node)
    exclude - additional exclusion masks in addition to default
    glob - glob masks
    recursive - scan recursively or not

    Returns a list of 2-element tuples where the first element is
    relative to the source, the second is the absolute file name.
    """

    # Make sure it exists and is not a link
    if not os.path.exists(source):
        return []

    if os.path.islink(source):
        return []

    # Handle file directly
    if os.path.isfile(source):
        return [ (os.path.join(reldir, os.path.basename(source)), source) ]

    # Scan source files on disk
    if not os.path.isdir(source):
        return []

    results = []
    for entry in os.listdir(source):
        fullpath = os.path.join(source, entry)
        if os.path.islink(fullpath):
            continue

        # Excluded (both files and directories)
        if _is_excluded(entry, exclude):
            continue

        # File
        if os.path.isfile(fullpath):
            if _is_globbed(entry, glob):
                results.append( (os.path.join(reldir, entry), fullpath) )
        elif os.path.isdir(fullpath):
            if recursive:
                newrel = os.path.join(reldir, entry)
                results.extend(_get_files(env, fullpath, exclude, glob, recursive, newrel))

    return results


def _get_built_files(env, source, exclude, glob, recursive, reldir=os.curdir):
    """
    Find files that match the criteria.

    source - source file or directory node
    exclude - additional exclusion masks in addition to default
    glob - glob masks
    recursive - scan recursively or not

    Returns a list of 2-element tuples where the first element is
    relative to the source, the second is the absolute file name.
    """

    # Source
    source = source.disambiguate()

    # If a file, return it without scanning
    if isinstance(source, SCons.Node.FS.File):
        if source.is_derived():
            source = source.abspath
            return [ (os.path.join(reldir, os.path.basename(source)), source) ]
        else:
            return []

    if not isinstance(source, SCons.Node.FS.Dir):
        return []

    # Walk the children
    results = []

    for child in source.children():
        child = child.disambiguate()
        name = os.path.basename(child.abspath)

        # Ignore '.' and '..'
        if name == '.' or name == '..':
            continue

        # Exclude applies to files and directories
        if _is_excluded(name, exclude):
            continue

        if isinstance(child, SCons.Node.FS.File):
            if child.is_derived() and _is_globbed(name, glob):
                results.append( (os.path.join(reldir, name), child.abspath) )
        elif isinstance(child, SCons.Node.FS.Dir):
            if recursive:
                newrel = os.path.join(reldir, name)
                results.extend(_get_built_files(env, child, exclude, glob, recursive, newrel))

    return results


def _get_both(env, source, exclude, glob, recursive, scan):
    """
    Get both the built and source files that match the criteria.  Built
    files take priority over a source file of the same path and name.
    """

    src_nodes = []
    results = []

    # Get built files
    if scan == 0 or scan == 2:
        results.extend(_get_built_files(env, source, exclude, glob, recursive))
        for (relsrc, src) in results:
            node = env.File(src)
            src_nodes.append(node.srcnode())

    # Get source files
    if scan == 1 or scan == 2:
        files = _get_files(env, source.srcnode().abspath, exclude, glob, recursive)
        for (relsrc, src) in files:
            node = env.File(src)
            if not node in src_nodes:
                results.append( (relsrc, src) )

    return results


def InstallFiles(env, target, source, exclude=None, glob=None, recursive=True, scan=2):
    """
    InstallFiles pseudo-builder

    target - target directory to install to
    source - source file or directory to scan
    exclude - a list of patterns to exclude in files and directories
    glob - a list of patterns to include in files
    recursive - scan directories recursively
    scan - 0=scan built nodes, 1=scan source files, 2=both

    All argument except target and source should be used as keyword arguments
    """

    # Information
    if exclude:
        exclude = Flatten(exclude)
    else:
        exclude = []
    exclude.extend(env['INSTALLFILES_EXCLUDES'])

    if glob:
        glob = Flatten(glob)
    else:
        glob = []

    # Flatten source/target
    target = Flatten(target)
    source = Flatten(source)

    if len(target) != len(source):
        if len(target) == 1:
            # If only one target, assume it is for all the sources
            target = target * len(source)
        else:
            raise SCons.Errors.UserError('InstallFiles expects only one target directory or one for each source')

    # Scan
    files = []
    for (t, s) in zip(target, source):
        if not isinstance(t, SCons.Node.FS.Base):
            t = env.Dir(t)
        if not isinstance(s, SCons.Node.FS.Base):
            s = env.Entry(s)

        for (relsrc, src) in _get_both(env, s, exclude, glob, recursive, scan):
            dest = os.path.normpath(os.path.join(t.abspath, relsrc))
            files.append( (dest, src) )

    # Install
    results = []
    for (dest, src) in files:
        results.extend(env.InstallAs(dest, src))

    # Done
    return results


def InstallPackageAccum(env, name, target, source, exclude=None, glob=None, recursive=True, scan=2):
    """
    InstallPackageAccum accumulate files for a package name

    name - the name of the package of files
    target - relative target directory under the package directory, can be '.'
    source - source file or directory to scan
    exclude - a list of patterns to exclude in files and directories
    glob - a list of patterns to include in files
    recursive - scan directories recursively
    scan - 0=scan built nodes, 1=scan source files, 2=both

    All argument except target and source should be used as keyword arguments and
    target should NOT be nodes but strings such as '.'
    """

    # Information
    if exclude:
        exclude = Flatten(exclude)
    else:
        exclude = []
    exclude.extend(env['INSTALLFILES_EXCLUDES'])

    if glob:
        glob = Flatten(glob)
    else:
        glob = []

    # Flatten target/source
    target = Flatten(target)
    source = Flatten(source)

    if len(target) != len(source):
        if len(target) == 1:
            target = target * len(source)
        else:
            raise SCons.Errors.UserError('InstallPackageAccum expects only one target directory or one for each source')

    # Scan
    files = []
    for (t, s) in zip(target, source):
        t = env.subst(t)
        if not isinstance(s, SCons.Node.FS.Base):
            s = env.Entry(s)

        for (relsrc, src) in _get_both(env, s, exclude, glob, recursive, scan):
            dest = os.path.normpath(os.path.join(t, relsrc))
            files.append( (dest, src) )

    # Add package if needed
    packages = env['INSTALLFILES_PACKAGES']
    if not name in packages:
        packages[name] = []
    packages[name].extend(files)


def InstallPackage(env, target, name):
    """
    Install the files of a given package to a certain location

    target - the directory to install the package to
    name - the name of the package to install
    """

    # Flatten target/name
    target = Flatten(target)
    name = Flatten(name)

    if len(target) != len(name):
        if len(target) == 1:
            target = target * len(name)
        else:
            raise SCons.Errors.UserError('InstallPackage expects only one target directory or one for each package')

    # Install
    results = []
    packages = env['INSTALLFILES_PACKAGES']

    for (t, n) in zip(target, name):
        if not n in packages:
            raise SCons.Errors.UserError('InstallPackage package name does not exist: ' + n)

        for (relsrc, src) in packages[n]:
            dest = os.path.normpath(os.path.join(t, relsrc))
            results.extend(env.InstallAs(dest, src))

    # Done
    return results


def InstallExclude(env, *args):
    env['INSTALLFILES_EXCLUDES'] = []

    for i in args:
        env['INSTALLFILES_EXCLUDES'].extend(Flatten(i))


# Register this with the environment
def TOOL_INSTALL(env):
    env.AddMethod(InstallFiles)
    env.AddMethod(InstallPackageAccum)
    env.AddMethod(InstallPackage)
    env.AddMethod(InstallExclude)

    env['INSTALLFILES_EXCLUDES'] = []
    env['INSTALLFILES_PACKAGES'] = {}

The old builder code that only scans source files:

#!python 
# File:         install.py
# Author:       Brian A. Vanderburg II
# Purpose:      An install builder to install subdirectories and files
# Copyright:    This file is placed in the public domain.
##############################################################################


# Requirements
##############################################################################
import os
import fnmatch

import SCons
import SCons.Node.FS
from SCons.Script import *


# Install files code
##############################################################################

def _is_excluded(name, exclude):
    """
    Determine if the name is to be excluded based on the exclusion list
    """
    if not exclude:
        return False
    return any( (fnmatch.fnmatchcase(name, i) for i in exclude) )


def _is_globbed(name, glob):
    """
    Determine if the name is globbed based on the glob list
    """
    if not glob:
        return True
    return any( (fnmatch.fnmatchcase(name, i) for i in glob) )


def _get_files(env, target, source, exclude, glob, recursive):
    """
    Find files that match the criteria.

    target - target directory node
    source - source file or directory node
    exclude - additional exclusion masks in addition to default
    glob - glob masks
    recursive - scan recursively or not

    Returns a list of 2-element tuples where the first element is
    the destination file name, the second is the source file name.
    """

    # Source and target path
    # abspath intentionally used instead of str()
    spath = os.path.normpath(source.abspath)
    tpath = os.path.normpath(target.abspath)

    # Since we are scanning now, the directory must exist now,
    # otherwise treat it like a file
    if not os.path.isdir(spath):
        return [ (os.path.join(tpath, os.path.basename(spath)), spath) ]

    # Scan source files on disk
    results = []
    for(dir, dirs, files) in os.walk(spath):
        # Target for any file in this directory
        reldir = dir[len(spath):]
        while reldir and reldir[0] == os.sep:
            reldir = reldir[1:]

        if reldir:
            destdir = os.path.join(tpath, reldir)
        else:
            destdir = tpath

        # Files
        for i in files:
            if _is_excluded(i, exclude):
                continue
            if not _is_globbed(i, glob):
                continue

            results.append( (os.path.join(destdir, i), os.path.join(dir, i)) )

        # Directories
        i = 0
        while i < len(dirs):
            if recursive and not _is_excluded(dirs[i], exclude):
                i += 1
            else:
                del dirs[i]

    return results

def InstallFiles(env, target, source, exclude=None, glob=None, recursive=True):
    """
    InstallFiles pseudo-builder
    """

    # Information
    if exclude:
        exclude = Flatten(exclude)
    else:
        exclude = []
    exclude.extend(env.get('INSTALL_EXCLUDES', []))

    if glob:
        glob = Flatten(glob)
    else:
        glob = []

    # Install
    target = Flatten(target)
    source = Flatten(source)

    if len(target) != len(source):
        if len(target) == 1:
            # If only one target, assume it is for all the sources
            target = target * len(source)
        else:
            raise SCons.Errors.UserError('InstallFiles expects only one target directory or one for each source')

    results = []
    for (t, s) in zip(target, source):
        if not isinstance(t, SCons.Node.FS.Base):
            t = env.Dir(t)
        if not isinstance(s, SCons.Node.FS.Base):
            s = env.Entry(s)

        files = _get_files(env, t, s, exclude, glob, recursive)
        for (dest, src) in files:
            results.extend(env.InstallAs(dest, src))

    # Success
    return results


def InstallExclude(env, *args):
    env['INSTALL_EXCLUDES'] = []

    for i in args:
        env['INSTALL_EXCLUDES'].extend(Flatten(i))


# Register this with the environment
def TOOL_INSTALL(env):
    env.AddMethod(InstallFiles)
    env.AddMethod(InstallExclude)

Updated