2 """Provides tools for building Mac application bundles."""
9 from SCons
.Builder
import *
10 from SCons
.Defaults
import SharedCheck
, ProgScan
11 from SCons
.Script
.SConscript
import SConsEnvironment
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)
19 Info.plist (an XML key->value database; defined by BUNDLE_INFO_PLIST)
20 PkgInfo (trivially short; defined by value of BUNDLE_PKGINFO)
22 executable (the executable or shared lib, linked with Bundle())
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:
49 def ensureWritable(nodes
):
51 if os
.path
.exists(node
.path
) and not (os
.stat(node
.path
)[0] & 0200):
52 chmod(node
.path
, 0777)
55 def InstallBundle (env
, target_dir
, bundle
):
56 """Move a Mac OS-X bundle to its final destination"""
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)
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
))
70 # Common type codes are BNDL for generic bundle and APPL for application.
71 def MakeBundle(env
, bundledir
, app
,
73 typecode
='BNDL', creator
='SapP',
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
]:
83 if SCons
.Util
.is_List(app
):
86 if SCons
.Util
.is_String(app
):
88 appbase
= os
.path
.basename(app
)
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
)
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
,
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']))
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
)
123 if SCons
.Util
.is_List(r
):
124 inst_all
.append (env
.InstallAs(
125 "".join( (bundledir
+'/Contents/Resources', r
[1]) ) , r
[0]) )
127 inst_all
.append (env
.Install(bundledir
+'/Contents/Resources', r
))
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
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.
152 f
= open(sourcefile
, 'rb')
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
)
160 f
= open(targetfile
, 'wb')
164 raise SCons
.Errors
.UserError
, "Can't write target file %s"%targetfile
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():
173 d
[k
] = env
.subst(v())
174 elif SCons
.Util
.is_String(v
):
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():
193 d
[k
] = env
.subst(v())
194 elif SCons
.Util
.is_String(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())
211 env
['BUILDERS']['WriteVal'] = Builder(action
=write_val
)
214 def osx_copy( dest
, source
, env
):
215 from macostools
import copy
217 shutil
.copymode(source
, dest
)