How to create a symbolic link with SCons?
Asked Answered
C

5

13

I'm using SCons for building a project and need to add a symbolic link to a file it is installing via env.Install. What command(s) will make a link that's the equivalent of running ln -s on the command line?

Curd answered 20/8, 2010 at 15:20 Comment(0)
C
11

SCons doesn't have a dedicated symbolic link command, but you can use os.symlink(src, dst) from Python's os module:

import os
env = Environment()
def SymLink(target, source, env):
    os.symlink(os.path.abspath(str(source[0])), os.path.abspath(str(target[0])))
env.Command("file.out", "file.in", SymLink)

This may not work correctly on Windows, I've only tried it on Linux.

Catcall answered 20/8, 2010 at 19:19 Comment(2)
For some reason it doesn't work when trying to create a symlink inside a subdir, like "env.Command (flavour + '/Resources', 'src/Resources', SymLink)" where flavour is 'debug' or 'release'.Ozonide
It is not supposed to work on Windows, because this OS does not support linksKornher
C
8

There seems to be little advancement in the SCons core code for symbolic link support and I wasn't satisfied any one solution I found on the web. Here is a potential builder which incorporates aspects of both Nick's and richq's answers. Additionally, it will catch name changes (due to the emitter method) and is as platform-agnostic as I could get it.

I prefer this builder because it will make links relative to the directory in which they are installed. One could add an option to force the link to be absolute I suppose, but I have not needed or wanted that yet.

Currently, if the OS doesn't support symlinks, I just pass and do nothing, but one could use os.copytree() for example however the dependency becomes messy if the source is a directory so the emitter would need to do something fancy. I'm up for any suggestions here.

One can put the following code into the file site_scons/site_tools/symlink.py (with blank _init_.py files in the appropriate places). Then do this in the SConstruct file:

SConstruct:

env = Environment()
env.Tool('symlink')
env.SymLink('link_name.txt', 'real_file.txt')

symlink.py:

import os
from os import path

from SCons.Node import FS
from SCons.Script import Action, Builder

def generate(env):
    '''
    SymLink(link_name,source)
    env.SymLink(link_name,source)

    Makes a symbolic link named "link_name" that points to the
    real file or directory "source". The link produced is always
    relative.
    '''
    bldr = Builder(action = Action(symlink_builder,symlink_print),
        target_factory = FS.File,
        source_factory = FS.Entry,
        single_target = True,
        single_source = True,
        emitter = symlink_emitter)
    env.Append(BUILDERS = {'SymLink' : bldr})

def exists(env):
    '''
    we could test if the OS supports symlinks here, or we could
    use copytree as an alternative in the builder.
    '''
    return True

def symlink_print(target, source, env):
    lnk = path.basename(target[0].abspath)
    src = path.basename(source[0].abspath)
    return 'Link: '+lnk+' points to '+src

def symlink_emitter(target, source, env):
    '''
    This emitter removes the link if the source file name has changed
    since scons does not seem to catch this case.
    '''
    lnk = target[0].abspath
    src = source[0].abspath
    lnkdir,lnkname = path.split(lnk)
    srcrel = path.relpath(src,lnkdir)

    if int(env.get('verbose',0)) > 3:
        ldir = path.relpath(lnkdir,env.Dir('#').abspath)
        if rellnkdir[:2] == '..':
            ldir = path.abspath(ldir)
        print '  symbolic link in directory: %s' % ldir
        print '      %s -> %s' % (lnkname,srcrel)

    try:
        if path.exists(lnk):
            if os.readlink(lnk) != srcrel:
                os.remove(lnk)
    except AttributeError:
        # no symlink available, so we remove the whole tree? (or pass)
        #os.rmtree(lnk)
        print 'no os.symlink capability on this system?'

    return (target, source)

def symlink_builder(target, source, env):
    lnk = target[0].abspath
    src = source[0].abspath
    lnkdir,lnkname = path.split(lnk)
    srcrel = path.relpath(src,lnkdir)

    if int(env.get('verbose',0)) > 4:
        print 'target:', target
        print 'source:', source
        print 'lnk:', lnk
        print 'src:', src
        print 'lnkdir,lnkname:', lnkdir, lnkname
        print 'srcrel:', srcrel

    if int(env.get('verbose',0)) > 4:
        print 'in directory: %s' % path.relpath(lnkdir,env.Dir('#').abspath)
        print '    symlink: %s -> %s' % (lnkname,srcrel)

    try:
        os.symlink(srcrel,lnk)
    except AttributeError:
        # no symlink available, so we make a (deep) copy? (or pass)
        #os.copytree(srcrel,lnk)
        print 'no os.symlink capability on this system?'

    return None
Carvey answered 1/8, 2012 at 15:21 Comment(1)
Have you written a variant for hard links, aswell? I want the same interface as builtin Install where first argument is a directory.Martie
R
3

This creates a builder to perform the job:

mylib = env.SharedLibrary("foobar", SRCS)

builder = Builder(action = "ln -s ${SOURCE.file} ${TARGET.file}", chdir = True)

env.Append(BUILDERS = {"Symlink" : builder})

mylib_link = env.Symlink("_foobar.so", mylib)

env.Default(mylib)
env.Default(mylib_link)

Again, this solution is for Linux.

Redemptioner answered 20/6, 2011 at 18:8 Comment(6)
Unfortunately, it doesn't work well (at all) on directories: "TypeError: Directory /home/septi/Dropbox/Code/StreetCleaner/src/Resources found where file expected.:"Ozonide
@Septagram: How do fix the directory problem?Martie
@Nordlöw, sorry, but it's been a while and I don't know :( Please try other answers and comment if you find something.Ozonide
It seems SCons doesn't support target being a directory. However, a wrapper function does solve this problem.Martie
Probably using target_factory=Dir as an arg to your Builder will work: builder = Builder(action=..., target_factory=Dir) see the man page under target_factory for more.Fairish
@Ozonide I found a solution.... Well, workaround, see my answer.Ongoing
O
0

If you wanted to issue the command directly to the shell and know the OS, subprocess can be used as well.

E.g.: subprocess.call(['ln', '-s', '</src/path>', '</dest/path>'])

Ogilvie answered 11/12, 2018 at 3:10 Comment(0)
O
0

In addition to Nicks solution, you can add a directory symlink by using a file as a directory name carrier. It's not the cleanest solution and debugging path names is a pain, but this works well:

def symlink_last(target_source_env):
    src = os.path.basename(os.path.dirname(str(source[0])))
    link = "deliverables/last"
    print "Symlinking "+ src + "as" + link
    os.symlink(src, link)

BUILD_TARGETS.append('link')
install_dir = "deliverables/subdir"
carrier_file = "filename"
builder = Builder(action = symlink_last, chdir=False)
env.Append(BUILDERS={ "Symlink" : builder }) 
env.Alias(target="link", source=env.Symlink(dir="deliverables", source = install_dir + carrier_file)

This will make a link to deliverables/subdir named deliverables/last, provided that a file deliverables/subdir/filename exists.

Ongoing answered 26/4, 2021 at 15:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.