Moved test builder out of SConstruct and into a separate file.
[MidoriGraph.git] / sconsTools / macosx.py
blob046518e09b9e98b59ddd76d578a5767f8bd311b4
1 #!/usr/bin/env python
2 """Provides tools for building Mac application bundles."""
4 import os
5 import re
6 import shutil
7 import fnmatch
9 from SCons.Builder import *
10 from SCons.Defaults import SharedCheck, ProgScan
11 from SCons.Script.SConscript import SConsEnvironment
13 def TOOL_BUNDLE(env):
14 """defines env.Bundle() for linking bundles on Darwin/OSX, and
15 env.InstallBundle() for installing a bundle into its dir.
16 A bundle has this structure: (filenames are case SENSITIVE)
17 sapphire.bundle/
18 Contents/
19 Info.plist (an XML key->value database; defined by BUNDLE_INFO_PLIST)
20 PkgInfo (trivially short; defined by value of BUNDLE_PKGINFO)
21 MacOS/
22 executable (the executable or shared lib, linked with Bundle())
23 Resources/
24 """
25 if 'BUNDLE' in env['TOOLS']: return
26 if env['PLATFORM'] == 'darwin':
27 env.Append(TOOLS = 'BUNDLE')
28 # This is like the regular linker, but uses different vars.
29 LinkBundle = SCons.Builder.Builder(action=[SharedCheck, "$BUNDLECOM"],
30 emitter="$SHLIBEMITTER",
31 prefix = '$BUNDLEPREFIX',
32 suffix = '$BUNDLESUFFIX',
33 target_scanner = ProgScan,
34 src_suffix = '$BUNDLESUFFIX',
35 src_builder = 'SharedObject')
36 env['BUILDERS']['LinkBundle'] = LinkBundle
37 env['BUNDLEEMITTER'] = None
38 env['BUNDLEPREFIX'] = ''
39 env['BUNDLESUFFIX'] = ''
40 env['BUNDLEDIRSUFFIX'] = '.bundle'
41 env['FRAMEWORKS'] = ['Carbon', 'System']
42 env['BUNDLE'] = '$SHLINK'
43 env['BUNDLEFLAGS'] = ' -bundle'
44 env['BUNDLECOM'] = '$BUNDLE $BUNDLEFLAGS -o ${TARGET} $SOURCES $_LIBDIRFLAGS $_LIBFLAGS $FRAMEWORKS'
45 # This requires some other tools:
46 TOOL_WRITE_VAL(env)
47 TOOL_SUBST(env)
49 def ensureWritable(nodes):
50 for node in nodes:
51 if os.path.exists(node.path) and not (os.stat(node.path)[0] & 0200):
52 chmod(node.path, 0777)
53 return nodes
55 def InstallBundle (env, target_dir, bundle):
56 """Move a Mac OS-X bundle to its final destination"""
58 # check parameters!
60 if os.path.exists(target_dir) and not os.path.isdir (target_dir):
61 raise SCons.Errors.UserError, "InstallBundle: %s needs to be a directory!"%(target_dir)
63 outputs = []
64 bundlefiles = env.arg2nodes (bundle, env.fs.File)
65 outDirNode = env.Dir(target_dir)
66 for bundlefile in bundlefiles:
67 outputs += ensureWritable (env.InstallAs (outDirNode.abspath + '/' + str (bundlefile), bundlefile))
68 return outputs
70 # Common type codes are BNDL for generic bundle and APPL for application.
71 def MakeBundle(env, bundledir, app,
72 key, info_plist,
73 typecode='BNDL', creator='SapP',
74 icon_file='',
75 subst_dict=None,
76 resources=None):
77 """Install a bundle into its dir, in the proper format"""
78 resources = resources or []
79 # Substitute construction vars:
80 for a in [bundledir, key, info_plist, icon_file, typecode, creator]:
81 a = env.subst(a)
83 if SCons.Util.is_List(app):
84 app = app[0]
86 if SCons.Util.is_String(app):
87 app = env.subst(app)
88 appbase = os.path.basename(app)
89 else:
90 appbase = os.path.basename(str(app))
91 if not ('.' in bundledir):
92 bundledir += '.$BUNDLEDIRSUFFIX'
93 bundledir = env.subst(bundledir) # substitute again
94 suffix=bundledir[bundledir.rfind('.'):]
95 if (suffix=='.app' and typecode != 'APPL' or
96 suffix!='.app' and typecode == 'APPL'):
97 raise SCons.Errors.UserError, "MakeBundle: inconsistent dir suffix %s and type code %s: app bundles should end with .app and type code APPL."%(suffix, typecode)
98 else:
99 env.SideEffect (bundledir, app)
100 if subst_dict is None:
101 subst_dict={'%SHORTVERSION%': '$VERSION_NUM',
102 '%LONGVERSION%': '$VERSION_NAME',
103 '%YEAR%': '$COMPILE_YEAR',
104 '%BUNDLE_EXECUTABLE%': appbase,
105 '%ICONFILE%': os.path.basename(icon_file),
106 '%CREATOR%': creator,
107 '%TYPE%': typecode,
108 '%BUNDLE_KEY%': key}
109 inst_all = []
110 inst = env.Install(bundledir+'/Contents/MacOS', app)
111 inst_all.append (inst)
112 #env.AddPostAction (inst, env.Action ('strip $TARGET'))
113 f=env.SubstInFile(bundledir+'/Contents/Info.plist', info_plist,
114 SUBST_DICT=subst_dict)
115 env.Depends(f, SCons.Node.Python.Value(
116 key+creator+typecode+env['VERSION_NUM']+env['VERSION_NAME']))
117 inst_all.append (f)
118 inst_all.append (env.File (bundledir+'/Contents/PkgInfo'))
119 env.WriteVal(target=bundledir+'/Contents/PkgInfo',
120 source=SCons.Node.Python.Value(typecode+creator))
121 resources.append(icon_file)
122 for r in resources:
123 if SCons.Util.is_List(r):
124 inst_all.append (env.InstallAs(
125 "".join( (bundledir+'/Contents/Resources', r[1]) ) , r[0]) )
126 else:
127 inst_all.append (env.Install(bundledir+'/Contents/Resources', r))
128 return inst_all
130 # This is not a regular Builder; it's a wrapper function.
131 # So just make it available as a method of Environment.
132 SConsEnvironment.MakeBundle = MakeBundle
133 SConsEnvironment.InstallBundle = InstallBundle
135 def TOOL_SUBST(env):
136 """Adds SubstInFile builder, which substitutes the keys->values of SUBST_DICT
137 from the source to the target.
138 The values of SUBST_DICT first have any construction variables expanded
139 (its keys are not expanded).
140 If a value of SUBST_DICT is a python callable function, it is called and
141 the result is expanded as the value.
142 If there's more than one source and more than one target, each target gets
143 substituted from the corresponding source.
145 env.Append(TOOLS = 'SUBST')
146 def do_subst_in_file(targetfile, sourcefile, dict):
147 """Replace all instances of the keys of dict with their values.
148 For example, if dict is {'%VERSION%': '1.2345', '%BASE%': 'MyProg'},
149 then all instances of %VERSION% in the file will be replaced with 1.2345 etc.
151 try:
152 f = open(sourcefile, 'rb')
153 contents = f.read()
154 f.close()
155 except:
156 raise SCons.Errors.UserError, "Can't read source file %s"%sourcefile
157 for (k,v) in dict.items():
158 contents = re.sub(k, v, contents)
159 try:
160 f = open(targetfile, 'wb')
161 f.write(contents)
162 f.close()
163 except:
164 raise SCons.Errors.UserError, "Can't write target file %s"%targetfile
165 return 0 # success
167 def subst_in_file(target, source, env):
168 if not env.has_key('SUBST_DICT'):
169 raise SCons.Errors.UserError, "SubstInFile requires SUBST_DICT to be set."
170 d = dict(env['SUBST_DICT']) # copy it
171 for (k,v) in d.items():
172 if callable(v):
173 d[k] = env.subst(v())
174 elif SCons.Util.is_String(v):
175 d[k]=env.subst(v)
176 else:
177 raise SCons.Errors.UserError, "SubstInFile: key %s: %s must be a string or callable"%(k, repr(v))
178 for (t,s) in zip(target, source):
179 return do_subst_in_file(str(t), str(s), d)
181 def subst_in_file_string(target, source, env):
182 """This is what gets printed on the console."""
183 return '\n'.join(['Substituting vars from %s into %s'%(str(s), str(t))
184 for (t,s) in zip(target, source)])
186 def subst_emitter(target, source, env):
187 """Add dependency from substituted SUBST_DICT to target.
188 Returns original target, source tuple unchanged.
190 d = env['SUBST_DICT'].copy() # copy it
191 for (k,v) in d.items():
192 if callable(v):
193 d[k] = env.subst(v())
194 elif SCons.Util.is_String(v):
195 d[k]=env.subst(v)
196 env.Depends(target, SCons.Node.Python.Value(d))
197 # Depends(target, source) # this doesn't help the install-sapphire-linux.sh problem
198 return target, source
200 subst_action=SCons.Action.Action(subst_in_file, subst_in_file_string)
201 env['BUILDERS']['SubstInFile'] = Builder(action=subst_action, emitter=subst_emitter)
203 def TOOL_WRITE_VAL(env):
204 env.Append(TOOLS = 'WRITE_VAL')
205 def write_val(target, source, env):
206 """Write the contents of the first source into the target.
207 source is usually a Value() node, but could be a file."""
208 f = open(str(target[0]), 'wb')
209 f.write(source[0].get_contents())
210 f.close()
211 env['BUILDERS']['WriteVal'] = Builder(action=write_val)
214 def osx_copy( dest, source, env ):
215 from macostools import copy
216 copy( source, dest )
217 shutil.copymode(source, dest)