Merged revisions 1502-1677,1679-1682,1684-1918,1920-1968,1970-2116,2118-2125,2127...
[scons.git] / src / engine / SCons / Tool / packaging / rpm.py
1 """SCons.Tool.Packaging.rpm
2
3 The rpm packager.
4 """
5
6 #
7 # __COPYRIGHT__
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
21 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
22 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28
29 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
30
31 import os
32 import string
33
34 import SCons.Builder
35
36 from SCons.Tool.packaging import stripinstall_emitter, packageroot_emitter, src_targz
37
38 def package(env, target, source, PACKAGEROOT, NAME, VERSION,
39             PACKAGEVERSION, DESCRIPTION, SUMMARY, X_RPM_GROUP, LICENSE,
40             **kw):
41     # initialize the rpm tool
42     SCons.Tool.Tool('rpm').generate(env)
43
44     # create the neccesary builder
45     bld = env['BUILDERS']['Rpm']
46     env['RPMFLAGS'] = SCons.Util.CLVar('-ta')
47
48     bld.push_emitter(targz_emitter)
49     bld.push_emitter(specfile_emitter)
50     bld.push_emitter(stripinstall_emitter())
51
52     # override the default target, with the rpm specific ones.
53     if str(target[0])=="%s-%s"%(NAME, VERSION):
54         # This should be overridable from the construction environment,
55         # which it is by using ARCHITECTURE=.
56         # Guessing based on what os.uname() returns at least allows it
57         # to work for both i386 and x86_64 Linux systems.
58         archmap = {
59             'i686'  : 'i386',
60             'i586'  : 'i386',
61             'i486'  : 'i386',
62         }
63
64         buildarchitecture = os.uname()[4]
65         buildarchitecture = archmap.get(buildarchitecture, buildarchitecture)
66
67         if kw.has_key('ARCHITECTURE'):
68             buildarchitecture = kw['ARCHITECTURE']
69
70         srcrpm = '%s-%s-%s.src.rpm' % (NAME, VERSION, PACKAGEVERSION)
71         binrpm = string.replace(srcrpm, 'src', buildarchitecture)
72
73         target = [ srcrpm, binrpm ]
74
75     # get the correct arguments into the kw hash
76     loc=locals()
77     del loc['kw']
78     kw.update(loc)
79     del kw['source'], kw['target'], kw['env']
80
81     # if no "SOURCE_URL" tag is given add a default one.
82     if not kw.has_key('SOURCE_URL'):
83         kw['SOURCE_URL']=(str(target[0])+".tar.gz").replace('.rpm', '')
84
85     # now call the rpm builder to actually build the packet.
86     return apply(bld, [env, target, source], kw)
87
88
89 def targz_emitter(target, source, env):
90     """ Puts all source files into a tar.gz file. """
91     # the rpm tool depends on a source package, until this is chagned
92     # this hack needs to be here that tries to pack all sources in.
93     sources = env.FindSourceFiles()
94
95     # filter out the target we are building the source list for.
96     #sources = [s for s in sources if not (s in target)]
97     sources = filter(lambda s, t=target: not (s in t), sources)
98
99     # find the .spec file for rpm and add it since it is not necessarily found
100     # by the FindSourceFiles function.
101     #sources.extend( [s for s in source if str(s).rfind('.spec')!=-1] )
102     spec_file = lambda s: string.rfind(str(s), '.spec') != -1
103     sources.extend( filter(spec_file, source) )
104
105     # as the source contains the url of the source package this rpm package
106     # is built from, we extract the target name
107     #tarball = (str(target[0])+".tar.gz").replace('.rpm', '')
108     tarball = string.replace(str(target[0])+".tar.gz", '.rpm', '')
109     try:
110         #tarball = env['SOURCE_URL'].split('/')[-1]
111         tarball = string.split(env['SOURCE_URL'], '/')[-1]
112     except KeyError, e:
113         raise SCons.Errors.UserError( "Missing PackageTag '%s' for RPM packager" % e.args[0] )
114
115     tarball = src_targz.package(env, source=sources, target=tarball,
116                                 PACKAGEROOT=env['PACKAGEROOT'], )
117
118     return (target, tarball)
119
120 def specfile_emitter(target, source, env):
121     specfile = "%s-%s" % (env['NAME'], env['VERSION'])
122
123     bld = SCons.Builder.Builder(action         = build_specfile,
124                                 suffix         = '.spec',
125                                 target_factory = SCons.Node.FS.File)
126
127     source.extend(bld(env, specfile, source))
128
129     return (target,source)
130
131 def build_specfile(target, source, env):
132     """ Builds a RPM specfile from a dictionary with string metadata and
133     by analyzing a tree of nodes.
134     """
135     file = open(target[0].abspath, 'w')
136     str  = ""
137
138     try:
139         file.write( build_specfile_header(env) )
140         file.write( build_specfile_sections(env) )
141         file.write( build_specfile_filesection(env, source) )
142         file.close()
143
144         # call a user specified function
145         if env.has_key('CHANGE_SPECFILE'):
146             env['CHANGE_SPECFILE'](target, source)
147
148     except KeyError, e:
149         raise SCons.Errors.UserError( '"%s" package field for RPM is missing.' % e.args[0] )
150
151
152 #
153 # mandatory and optional package tag section
154 #
155 def build_specfile_sections(spec):
156     """ Builds the sections of a rpm specfile.
157     """
158     str = ""
159
160     mandatory_sections = {
161         'DESCRIPTION'  : '\n%%description\n%s\n\n', }
162
163     str = str + SimpleTagCompiler(mandatory_sections).compile( spec )
164
165     optional_sections = {
166         'DESCRIPTION_'        : '%%description -l %s\n%s\n\n',
167         'CHANGELOG'           : '%%changelog\n%s\n\n',
168         'X_RPM_PREINSTALL'    : '%%pre\n%s\n\n',
169         'X_RPM_POSTINSTALL'   : '%%post\n%s\n\n',
170         'X_RPM_PREUNINSTALL'  : '%%preun\n%s\n\n',
171         'X_RPM_POSTUNINSTALL' : '%%postun\n%s\n\n',
172         'X_RPM_VERIFY'        : '%%verify\n%s\n\n',
173
174         # These are for internal use but could possibly be overriden
175         'X_RPM_PREP'          : '%%prep\n%s\n\n',
176         'X_RPM_BUILD'         : '%%build\n%s\n\n',
177         'X_RPM_INSTALL'       : '%%install\n%s\n\n',
178         'X_RPM_CLEAN'         : '%%clean\n%s\n\n',
179         }
180
181     # Default prep, build, install and clean rules
182     # TODO: optimize those build steps, to not compile the project a second time
183     if not spec.has_key('X_RPM_PREP'):
184         spec['X_RPM_PREP'] = 'rm -rf "$RPM_BUILD_ROOT"' + '\n%setup -q'
185
186     if not spec.has_key('X_RPM_BUILD'):
187         spec['X_RPM_BUILD'] = 'mkdir "$RPM_BUILD_ROOT"'
188
189     if not spec.has_key('X_RPM_INSTALL'):
190         spec['X_RPM_INSTALL'] = 'scons --install-sandbox="$RPM_BUILD_ROOT" "$RPM_BUILD_ROOT"'
191
192     if not spec.has_key('X_RPM_CLEAN'):
193         spec['X_RPM_CLEAN'] = 'rm -rf "$RPM_BUILD_ROOT"'
194
195     str = str + SimpleTagCompiler(optional_sections, mandatory=0).compile( spec )
196
197     return str
198
199 def build_specfile_header(spec):
200     """ Builds all section but the %file of a rpm specfile
201     """
202     str = ""
203
204     # first the mandatory sections
205     mandatory_header_fields = {
206         'NAME'           : '%%define name %s\nName: %%{name}\n',
207         'VERSION'        : '%%define version %s\nVersion: %%{version}\n',
208         'PACKAGEVERSION' : '%%define release %s\nRelease: %%{release}\n',
209         'X_RPM_GROUP'    : 'Group: %s\n',
210         'SUMMARY'        : 'Summary: %s\n',
211         'LICENSE'        : 'License: %s\n', }
212
213     str = str + SimpleTagCompiler(mandatory_header_fields).compile( spec )
214
215     # now the optional tags
216     optional_header_fields = {
217         'VENDOR'              : 'Vendor: %s\n',
218         'X_RPM_URL'           : 'Url: %s\n',
219         'SOURCE_URL'          : 'Source: %s\n',
220         'SUMMARY_'            : 'Summary(%s): %s\n',
221         'X_RPM_DISTRIBUTION'  : 'Distribution: %s\n',
222         'X_RPM_ICON'          : 'Icon: %s\n',
223         'X_RPM_PACKAGER'      : 'Packager: %s\n',
224         'X_RPM_GROUP_'        : 'Group(%s): %s\n',
225
226         'X_RPM_REQUIRES'      : 'Requires: %s\n',
227         'X_RPM_PROVIDES'      : 'Provides: %s\n',
228         'X_RPM_CONFLICTS'     : 'Conflicts: %s\n',
229         'X_RPM_BUILDREQUIRES' : 'BuildRequires: %s\n',
230
231         'X_RPM_SERIAL'        : 'Serial: %s\n',
232         'X_RPM_EPOCH'         : 'Epoch: %s\n',
233         'X_RPM_AUTOREQPROV'   : 'AutoReqProv: %s\n',
234         'X_RPM_EXCLUDEARCH'   : 'ExcludeArch: %s\n',
235         'X_RPM_EXCLUSIVEARCH' : 'ExclusiveArch: %s\n',
236         'X_RPM_PREFIX'        : 'Prefix: %s\n',
237         'X_RPM_CONFLICTS'     : 'Conflicts: %s\n',
238
239         # internal use
240         'X_RPM_BUILDROOT'     : 'BuildRoot: %s\n', }
241
242     # fill in default values:
243     # Adding a BuildRequires renders the .rpm unbuildable under System, which
244     # are not managed by rpm, since the database to resolve this dependency is
245     # missing (take Gentoo as an example)
246 #    if not s.has_key('x_rpm_BuildRequires'):
247 #        s['x_rpm_BuildRequires'] = 'scons'
248
249     if not spec.has_key('X_RPM_BUILDROOT'):
250         spec['X_RPM_BUILDROOT'] = '%{_tmppath}/%{name}-%{version}-%{release}'
251
252     str = str + SimpleTagCompiler(optional_header_fields, mandatory=0).compile( spec )
253     return str
254
255 #
256 # mandatory and optional file tags
257 #
258 def build_specfile_filesection(spec, files):
259     """ builds the %file section of the specfile
260     """
261     str  = '%files\n'
262
263     if not spec.has_key('X_RPM_DEFATTR'):
264         spec['X_RPM_DEFATTR'] = '(-,root,root)'
265
266     str = str + '%%defattr %s\n' % spec['X_RPM_DEFATTR']
267
268     supported_tags = {
269         'PACKAGING_CONFIG'           : '%%config %s',
270         'PACKAGING_CONFIG_NOREPLACE' : '%%config(noreplace) %s',
271         'PACKAGING_DOC'              : '%%doc %s',
272         'PACKAGING_UNIX_ATTR'        : '%%attr %s',
273         'PACKAGING_LANG_'            : '%%lang(%s) %s',
274         'PACKAGING_X_RPM_VERIFY'     : '%%verify %s',
275         'PACKAGING_X_RPM_DIR'        : '%%dir %s',
276         'PACKAGING_X_RPM_DOCDIR'     : '%%docdir %s',
277         'PACKAGING_X_RPM_GHOST'      : '%%ghost %s', }
278
279     for file in files:
280         # build the tagset
281         tags = {}
282         for k in supported_tags.keys():
283             try:
284                 tags[k]=getattr(file, k)
285             except AttributeError:
286                 pass
287
288         # compile the tagset
289         str = str + SimpleTagCompiler(supported_tags, mandatory=0).compile( tags )
290
291         str = str + ' '
292         str = str + file.PACKAGING_INSTALL_LOCATION
293         str = str + '\n\n'
294
295     return str
296
297 class SimpleTagCompiler:
298     """ This class is a simple string substition utility:
299     the replacement specfication is stored in the tagset dictionary, something
300     like:
301      { "abc"  : "cdef %s ",
302        "abc_" : "cdef %s %s" }
303
304     the compile function gets a value dictionary, which may look like:
305     { "abc"    : "ghij",
306       "abc_gh" : "ij" }
307
308     The resulting string will be:
309      "cdef ghij cdef gh ij"
310     """
311     def __init__(self, tagset, mandatory=1):
312         self.tagset    = tagset
313         self.mandatory = mandatory
314
315     def compile(self, values):
316         """ compiles the tagset and returns a str containing the result
317         """
318         def is_international(tag):
319             #return tag.endswith('_')
320             return tag[-1:] == '_'
321
322         def get_country_code(tag):
323             return tag[-2:]
324
325         def strip_country_code(tag):
326             return tag[:-2]
327
328         replacements = self.tagset.items()
329
330         str = ""
331         #domestic = [ (k,v) for k,v in replacements if not is_international(k) ]
332         domestic = filter(lambda t, i=is_international: not i(t[0]), replacements)
333         for key, replacement in domestic:
334             try:
335                 str = str + replacement % values[key]
336             except KeyError, e:
337                 if self.mandatory:
338                     raise e
339
340         #international = [ (k,v) for k,v in replacements if is_international(k) ]
341         international = filter(lambda t, i=is_international: i(t[0]), replacements)
342         for key, replacement in international:
343             try:
344                 #int_values_for_key = [ (get_country_code(k),v) for k,v in values.items() if strip_country_code(k) == key ]
345                 x = filter(lambda t,key=key,s=strip_country_code: s(t[0]) == key, values.items())
346                 int_values_for_key = map(lambda t,g=get_country_code: (g(t[0]),t[1]), x)
347                 for v in int_values_for_key:
348                     str = str + replacement % v
349             except KeyError, e:
350                 if self.mandatory:
351                     raise e
352
353         return str
354