Skip to content
Mats Wichmann edited this page Nov 11, 2019 · 7 revisions

It's often useful to be able to run "scons install" to copy the programs and shared libraries to their correct locations. "install" is a familiar phony target in other build systems, where a phony target is one that does not build a file indicated by its name, but instead causes some other work to be done. SCons sets up phony targets using the Alias function.

Here's one way to do that:

prefix = "/usr/local"

someshlib = env.SharedLibrary('foo', "foo.c")
someprogram = env.Program("fooprog", "fooprog.c")

# the install target
env.Alias("install", env.Install(os.path.join(prefix, "lib"), someshlib))
env.Alias("install", env.Install(os.path.join(prefix, "bin"), someprogram))

Basically we alias 'install' to a couple of Install nodes, returned by the Install method. That's all.

NB: There is no need to add something like Depends(install, someshlib), since SCons computes the dependency automatically.

Now

$ scons install

will do the work defined by the install target. The installs won't happen if you just invoke SCons without arguments, since by default, SCons acts only on targets underneath the starting directory, which /usr/local is unlikely to be - this is largely the effect you want, only performing installs if they are asked for explicitly.

If you need some finer-grained install targets, you may use something like this:

Alias('install-lib', Install(os.path.join(prefix, "lib"), ...))
Alias('install-bin', Install(os.path.join(prefix, "bin"), ...))
Alias('install', ['install-bin', 'install-lib'])

Permissions

Question: how to set permissions properly (binaries get 755, headers get 644, etc.) after an install?

  • One way to do it is to create a method that acts like Install but has an additional permission argument. Wrappers with predefined permissions are useful for cleaner markup:
import SCons

# define the custom function
from SCons.Script.SConscript import SConsEnvironment
SConsEnvironment.Chmod = SCons.Action.ActionFactory(os.chmod,
        lambda dest, mode: 'Chmod("%s", 0%o)' % (dest, mode))

def InstallPerm(env, dest, files, perm):
    obj = env.Install(dest, files)
    for i in obj:
        env.AddPostAction(i, env.Chmod(str(i), perm))
    return dest

# put this function "in" scons
SConsEnvironment.InstallPerm = InstallPerm

# great, we're ready to use it!
env.InstallPerm(bindir, ['fooprog', 'barprog'], 0755)

# but let's say we're not happy yet, we'd prefer nicer names.
SConsEnvironment.InstallProgram = lambda env, dest, files: InstallPerm(env, dest, files, 0755)
SConsEnvironment.InstallHeader = lambda env, dest, files: InstallPerm(env, dest, files, 0644)

# great, now you can also install by calling a method named 'InstallHeader' or 'InstallProgram'!
env.InstallHeader(incdir, ['foo.h', 'bar.h'])

Don't forget to set the umask, or created directories might get wrong permissions on Unix and Windows:

try:
    umask = os.umask(0o22)
    print 'setting umask to 022 (was 0%o)' % umask
except OSError:     # ignore on systems that don't support umask
    pass
  • Another similar method to install data with correct permissions is to use a Command :
source="./data/icon.png"
target="/usr/local/share/X/icon.png"

env.Alias("install", target)
env.Command(target, source, [
            Copy("$TARGET","$SOURCE"),
            Chmod("$TARGET", 0o664),
])

where target and source could be set in a directory parsing loop for conveniance :

# where you need to implement 'RecursiveGlob' yourself 
for file in RecursiveGlob("./data", "*"):
    # strip 'data/' out to have the filepath relative to data dir
    index = file.find("data/") + len("data/")
    filename_relative = file[index:]
    source = os.path.join("./data", filename_relative)
    target = os.path.join(data_dir, filename_relative)
            
    env.Alias("install", target)
    env.Command(target, source, [
                Copy("$TARGET","$SOURCE"),
                Chmod("$TARGET", 0664),
    ])

For best results, also make sure the umask is set like described above.

Locale files

Installing locale files on UNIX systems can be a little tricky :

This is an example that will handle installing .mo files for a source layout of /po/[language code]/app_name.mo.

# install .mo files
locale_dir = "/usr/local/share/locale"

mo_files = Glob("./po/*/app_name.mo",strings=True)
for mo in mo_files:
    # extract language code
    index_lo = mo.find("po/") + len("po/")
    index_hi = mo.find("/app_name.mo")
    lang_name = mo[index_lo:index_hi]
    # copy file
    install_location = locale_dir + "/" + lang_name + "/LC_MESSAGES/app_name.mo"
    env.Alias("install", env.InstallAs(install_location, mo))

It should be simple enough to adapt this code to layouts like /po/[language code].mo or any other.

Uninstall targets

Here's a sample uninstall function :

def create_uninstall_target(env, path, is_glob):
    if is_glob:
        all_files = Glob(path,strings=True)
        for filei in all_files:
            env.Command("uninstall-"+filei, filei, [
                        Delete("$SOURCE"),
            ])
            env.Alias("uninstall", "uninstall-"+filei)   
    else:
        env.Command("uninstall-"+path, path, [
                    Delete("$SOURCE"),
        ])
        env.Alias("uninstall", "uninstall-"+path)  

You can use it like this :

if 'uninstall' in COMMAND_LINE_TARGETS:
    # create uninstall targets
    create_uninstall_target(env, "/usr/local/bin/myapp", False)
    create_uninstall_target(env, "/usr/local/share/myapp/", False)
    create_uninstall_target(env, "/usr/local/share/locale/*/LC_MESSAGES/myapp.mo", True)

If you want to uninstall all the files installed using Install or InstallAs, there is a more expeditive way:

env.Command("uninstall", None, Delete(FindInstalledFiles()))
Clone this wiki locally