def build_app(target, source, env): """ PLUGINS - a list of plugins to install; as a feature/hack/bug (inspired by Qt, but probably needed by other libs) you can pass a tuple where the first is the file/node and the second is the folder under PlugIns/ that you want it installed to """ #TODO: make it strip(1) the installed binary (saves about 1Mb) #EEEP: this code is pretty flakey because I can't figure out how to force; have asked the scons list about it #This doesn't handle Frameworks correctly, only .dylibs #useful to know: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html#//apple_ref/doc/uid/20002253 #^ so you do have to copy in and _entire_ framework to be sure... #but for some frameworks it's okay to pretend they are regular bundle = target[0] binary = source[0] #this is copied from emit_app, which is unfortunate contents = Dir(os.path.join(str(bundle), "Contents")) MacOS = Dir(os.path.join(str(contents), "MacOS/")) frameworks = Dir( os.path.join(str(contents), "Frameworks") ) #we put both frameworks and standard unix sharedlibs in here plugins = Dir(os.path.join(str(contents), "PlugIns")) #installed_bin = source[-1] #env['APP_INSTALLED_BIN'] installed_bin = os.path.join(str(MacOS), os.path.basename(str(binary))) strip = bool(env.get('STRIP', False)) otool_local_paths = env.get('OTOOL_LOCAL_PATHS', []) otool_system_paths = env.get('OTOOL_SYSTEM_PATHS', []) "todo: expose the ability to override the list of System dirs" #ugh, I really don't like this... I wish I could package it up nicer. I could use a Builder but then I would have to pass in to the builder installed_bin which seems backwards since #could we use patch_lib on the initial binary itself???? def embed_lib(abs): "get the path to embed library abs in the bundle" name = os.path.basename(abs) return os.path.join(str(frameworks), name) def relative(emb): "compute the path of the given embedded binary relative to the binary, i.e. @executable_path/../+..." # assume that we start in X.app/Contents/, since we know neccessarily that @executable_path/../ gives us that # so then we only need base = os.path.abspath(str(installed_bin)) emb = os.path.abspath(emb) #XXX is abspath really necessary? down = emb[len( os.path.commonprefix([base, emb]) ):] #the path from Contents/ down to the file. Since we are taking away the length of the common prefix we are left with only what is unique to the embedded library's path return os.path.join("@executable_path/../", down) #todo: precache all this shit, in case we have to change the install names of a lot of libraries def automagic_references(embedded): #XXX bad name "modify a binary file to patch up all it's references" for ref in otool.dependencies(embedded): if ref in locals: embd = locals[ref][ 1] #the path that this reference is getting embedded at otool.change_ref(str(embedded), ref, relative(embd)) def patch_lib(embedded): otool.change_id( embedded, relative(embedded)) #change the name the library knows itself as automagic_references(embedded) if strip: #XXX stripping seems to only work on libs compiled a certain way, todo: try out ALL the options, see if can adapt it to work on every sort of lib system( "strip -S '%s' 2>/dev/null" % embedded ), #(the stripping fails with ""symbols referenced by relocation entries that can't be stripped"" for some obscure Apple-only reason sometimes, related to their hacks to gcc---it depends on how the file was compiled; since we don't /really/ care about this we just let it silently fail) #Workarounds for a bug/feature in SCons such that it doesn't neccessarily run the source builders before the target builders (wtf scons??) Execute(Mkdir(contents)) Execute(Mkdir(MacOS)) Execute(Mkdir(frameworks)) Execute(Mkdir(plugins)) #XXX locals should be keyed by absolute path to the lib, not by reference; that way it's easy to tell when a lib referenced in two different ways is actually the same #XXX rename locals => embeds #precache the list of names of libs we are using so we can figure out if a lib is local or not (and therefore a ref to it needs to be updated) #XXX it seems kind of wrong to only look at the basename (even if, by the nature of libraries, that must be enough) but there is no easy way to compute the abspath locals = { } # [ref] => (absolute_path, embedded_path) (ref is the original reference from looking at otool -L; we use this to decide if two libs are the same) #XXX it would be handy if embed_dependencies returned the otool list for each ref it reads.. for ref, path in otool.embed_dependencies(str(binary), LOCAL=otool_local_paths, SYSTEM=otool_system_paths): locals[ref] = (path, embed_lib(path)) plugins_l = [ ] #XXX bad name #list of tuples (source, embed) of plugins to stick under the plugins/ dir for p in env['PLUGINS']: #build any neccessary dirs for plugins (siiiigh) embedded_p = os.path.join(str(plugins), os.path.basename(str(p))) plugins_l.append((str(p), embedded_p)) for subdir, p in env['QT_HACK']: Execute(Mkdir(os.path.join(str(plugins), subdir))) embedded_p = os.path.join(str(plugins), subdir, os.path.basename(str(p))) plugins_l.append((p, embedded_p)) print "Scanning plugins for new dependencies:" for p, ep in plugins_l: print "Scanning plugin", p for ref, path in otool.embed_dependencies(p, LOCAL=otool_local_paths, SYSTEM=otool_system_paths): if ref not in locals: locals[ref] = path, embed_lib(path) else: assert path == locals[ref][0], "Path '%s' is not '%s'" % ( path, locals[ref][0]) #we really should have a libref-to-abspath function somewhere... right now it's inline in embed_dependencies() #better yet, make a Frameworks type that you say Framework("QtCore") and then can use that as a dependency print "Installing main binary:" Execute( Copy(installed_bin, binary) ) #e.g. this SHOULD be an env.Install() call, but if scons decides to run build_app before that env.Install then build_app fails and brings the rest of the build with it, of course for ref in otool.dependencies(str(installed_bin)): if ref in locals: embedded = locals[ref][1] otool.change_ref( str(installed_bin), ref, relative(embedded) ) #change the reference to the library in the program binary if strip: system("strip '%s'" % installed_bin) print "Installing embedded libs:" for ref, (abs, embedded) in locals.iteritems(): real_abs = os.path.realpath(abs) print "installing", real_abs, "to", embedded # NOTE(rryan): abs can be a symlink. we want to copy the binary it is # pointing to. os.path.realpath does this for us. Execute(Copy(embedded, real_abs)) if not os.access(embedded, os.W_OK): print "Adding write permissions to %s" % embedded_p mode = os.stat(embedded).st_mode os.chmod(embedded, mode | stat.S_IWUSR) patch_lib(embedded) print "Installing plugins:" for p, embedded_p in plugins_l: real_p = os.path.realpath(p) print "installing", real_p, "to", embedded_p # NOTE(rryan): p can be a symlink. we want to copy the binary it is # pointing to. os.path.realpath does this for us. Execute(Copy(embedded_p, real_p)) #:/ patch_lib(str(embedded_p))
def build_app(target, source, env): """ PLUGINS - a list of plugins to install; as a feature/hack/bug (inspired by Qt, but probably needed by other libs) you can pass a tuple where the first is the file/node and the second is the folder under PlugIns/ that you want it installed to """ #TODO: make it strip(1) the installed binary (saves about 1Mb) #EEEP: this code is pretty flakey because I can't figure out how to force; have asked the scons list about it #This doesn't handle Frameworks correctly, only .dylibs #useful to know: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html#//apple_ref/doc/uid/20002253 #^ so you do have to copy in and _entire_ framework to be sure... #but for some frameworks it's okay to pretend they are regular bundle = target[0] binary = source[0] #this is copied from emit_app, which is unfortunate contents = Dir(os.path.join(str(bundle), "Contents")) MacOS = Dir(os.path.join(str(contents), "MacOS/")) frameworks = Dir(os.path.join(str(contents), "Frameworks")) #we put both frameworks and standard unix sharedlibs in here plugins = Dir(os.path.join(str(contents), "PlugIns")) #installed_bin = source[-1] #env['APP_INSTALLED_BIN'] installed_bin = os.path.join(str(MacOS), os.path.basename(str(binary))) strip = bool(env.get('STRIP',False)) otool_local_paths = env.get('OTOOL_LOCAL_PATHS', []) otool_system_paths = env.get('OTOOL_SYSTEM_PATHS', []) "todo: expose the ability to override the list of System dirs" #ugh, I really don't like this... I wish I could package it up nicer. I could use a Builder but then I would have to pass in to the builder installed_bin which seems backwards since #could we use patch_lib on the initial binary itself???? def embed_lib(abs): "get the path to embed library abs in the bundle" name = os.path.basename(abs) return os.path.join(str(frameworks), name) def relative(emb): "compute the path of the given embedded binary relative to the binary, i.e. @executable_path/../+..." # assume that we start in X.app/Contents/, since we know necessarily that @executable_path/../ gives us that # so then we only need base = os.path.abspath(str(installed_bin)) emb = os.path.abspath(emb) #XXX is abspath really necessary? down = emb[len(os.path.commonprefix([base, emb])):] #the path from Contents/ down to the file. Since we are taking away the length of the common prefix we are left with only what is unique to the embedded library's path return os.path.join("@executable_path/../", down) #todo: precache all this shit, in case we have to change the install names of a lot of libraries def automagic_references(embedded): #XXX bad name "modify a binary file to patch up all it's references" for ref in otool.dependencies(embedded): if ref in locals: embd = locals[ref][1] #the path that this reference is getting embedded at otool.change_ref(str(embedded), ref, relative(embd)) def patch_lib(embedded): otool.change_id(embedded, relative(embedded)) #change the name the library knows itself as automagic_references(embedded) if strip: #XXX stripping seems to only work on libs compiled a certain way, todo: try out ALL the options, see if can adapt it to work on every sort of lib system("strip -S '%s' 2>/dev/null" % embedded), #(the stripping fails with ""symbols referenced by relocation entries that can't be stripped"" for some obscure Apple-only reason sometimes, related to their hacks to gcc---it depends on how the file was compiled; since we don't /really/ care about this we just let it silently fail) #Workarounds for a bug/feature in SCons such that it doesn't necessarily run the source builders before the target builders (wtf scons??) Execute(Mkdir(contents)) Execute(Mkdir(MacOS)) Execute(Mkdir(frameworks)) Execute(Mkdir(plugins)) #XXX locals should be keyed by absolute path to the lib, not by reference; that way it's easy to tell when a lib referenced in two different ways is actually the same #XXX rename locals => embeds #precache the list of names of libs we are using so we can figure out if a lib is local or not (and therefore a ref to it needs to be updated) #XXX it seems kind of wrong to only look at the basename (even if, by the nature of libraries, that must be enough) but there is no easy way to compute the abspath locals = {} # [ref] => (absolute_path, embedded_path) (ref is the original reference from looking at otool -L; we use this to decide if two libs are the same) #XXX it would be handy if embed_dependencies returned the otool list for each ref it reads.. binary_rpaths = otool.rpaths(str(binary)) otool_local_paths = binary_rpaths + otool_local_paths for ref, path in otool.embed_dependencies(str(binary), LOCAL=otool_local_paths, SYSTEM=otool_system_paths): locals[ref] = (path, embed_lib(path)) plugins_l = [] #XXX bad name #list of tuples (source, embed) of plugins to stick under the plugins/ dir for p in env['PLUGINS']: #build any necessary dirs for plugins (siiiigh) embedded_p = os.path.join(str(plugins), os.path.basename(str(p))) plugins_l.append( (str(p), embedded_p) ) for subdir, p in env['QT_HACK']: Execute(Mkdir(os.path.join(str(plugins), subdir))) embedded_p = os.path.join(str(plugins), subdir, os.path.basename(str(p))) plugins_l.append( (p, embedded_p) ) print("Scanning plugins for new dependencies:") for p, ep in plugins_l: print("Scanning plugin", p) for ref, path in otool.embed_dependencies(p, LOCAL=otool_local_paths, SYSTEM=otool_system_paths): if ref not in locals: locals[ref] = path, embed_lib(path) else: assert path == locals[ref][0], "Path '%s' is not '%s'" % (path, locals[ref][0]) #we really should have a libref-to-abspath function somewhere... right now it's inline in embed_dependencies() #better yet, make a Frameworks type that you say Framework("QtCore") and then can use that as a dependency print("Installing main binary:") Execute(Copy(installed_bin, binary)) #e.g. this SHOULD be an env.Install() call, but if scons decides to run build_app before that env.Install then build_app fails and brings the rest of the build with it, of course for ref in otool.dependencies(str(installed_bin)): if ref in locals: embedded = locals[ref][1] otool.change_ref(str(installed_bin), ref, relative(embedded)) #change the reference to the library in the program binary if strip: system("strip '%s'" % installed_bin) print("Installing embedded libs:") for ref, (abs, embedded) in locals.items(): real_abs = os.path.realpath(abs) print("installing", real_abs, "to", embedded) # NOTE(rryan): abs can be a symlink. we want to copy the binary it is # pointing to. os.path.realpath does this for us. Execute(Copy(embedded, real_abs)) if not os.access(embedded, os.W_OK): print("Adding write permissions to %s" % embedded_p) mode = os.stat(embedded).st_mode os.chmod(embedded, mode | stat.S_IWUSR) patch_lib(embedded) print("Installing plugins:") for p, embedded_p in plugins_l: real_p = os.path.realpath(p) print("installing", real_p, "to", embedded_p) # NOTE(rryan): p can be a symlink. we want to copy the binary it is # pointing to. os.path.realpath does this for us. Execute(Copy(embedded_p, real_p)) #:/ patch_lib(str(embedded_p))