Merged revisions 2136-2200,2202-2290,2292-2301 via svnmerge from
[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     bld = env['BUILDERS']['Rpm']
45
46     bld.push_emitter(targz_emitter)
47     bld.push_emitter(specfile_emitter)
48     bld.push_emitter(stripinstall_emitter())
49
50     # override the default target, with the rpm specific ones.
51     if str(target[0])=="%s-%s"%(NAME, VERSION):
52         # This should be overridable from the construction environment,
53         # which it is by using ARCHITECTURE=.
54         # Guessing based on what os.uname() returns at least allows it
55         # to work for both i386 and x86_64 Linux systems.
56         archmap = {
57             'i686'  : 'i386',
58             'i586'  : 'i386',
59             'i486'  : 'i386',
60         }
61
62         buildarchitecture = os.uname()[4]
63         buildarchitecture = archmap.get(buildarchitecture, buildarchitecture)
64
65         if kw.has_key('ARCHITECTURE'):
66             buildarchitecture = kw['ARCHITECTURE']
67
68         fmt = '%s-%s-%s.%s.rpm'
69         srcrpm = fmt % (NAME, VERSION, PACKAGEVERSION, 'src')
70         binrpm = fmt % (NAME, VERSION, PACKAGEVERSION, buildarchitecture)
71
72         target = [ srcrpm, binrpm ]
73
74     # get the correct arguments into the kw hash
75     loc=locals()
76     del loc['kw']
77     kw.update(loc)
78     del kw['source'], kw['target'], kw['env']
79
80     # if no "SOURCE_URL" tag is given add a default one.
81     if not kw.has_key('SOURCE_URL'):
82         kw['SOURCE_URL']=(str(target[0])+".tar.gz").replace('.rpm', '')
83
84     # now call the rpm builder to actually build the packet.
85     return apply(bld, [env, target, source], kw)
86
87
88 def targz_emitter(target, source, env):
89     """ Puts all source files into a tar.gz file. """
90     # the rpm tool depends on a source package, until this is chagned
91     # this hack needs to be here that tries to pack all sources in.
92     sources = env.FindSourceFiles()
93
94     # filter out the target we are building the source list for.
95     #sources = [s for s in sources if not (s in target)]
96     sources = filter(lambda s, t=target: not (s in t), sources)
97
98     # find the .spec file for rpm and add it since it is not necessarily found
99     # by the FindSourceFiles function.
100     #sources.extend( [s for s in source if str(s).rfind('.spec')!=-1] )
101     spec_file = lambda s: string.rfind(str(s), '.spec') != -1
102     sources.extend( filter(spec_file, source) )
103
104     # as the source contains the url of the source package this rpm package
105     # is built from, we extract the target name
106     #tarball = (str(target[0])+".tar.gz").replace('.rpm', '')
107     tarball = string.replace(str(target[0])+".tar.gz", '.rpm', '')
108     try:
109         #tarball = env['SOURCE_URL'].split('/')[-1]
110         tarball = string.split(env['SOURCE_URL'], '/')[-1]
111     except KeyError, e:
112         raise SCons.Errors.UserError( "Missing PackageTag '%s' for RPM packager" % e.args[0] )
113
114     tarball = src_targz.package(env, source=sources, target=tarball,
115                                 PACKAGEROOT=env['PACKAGEROOT'], )
116
117     return (target, tarball)
118
119 def specfile_emitter(target, source, env):
120     specfile = "%s-%s" % (env['NAME'], env['VERSION'])
121
122     bld = SCons.Builder.Builder(action         = build_specfile,
123                                 suffix         = '.spec',
124                                 target_factory = SCons.Node.FS.File)
125
126     source.extend(bld(env, specfile, source))
127
128     return (target,source)
129
130 def build_specfile(target, source, env):
131     """ Builds a RPM specfile from a dictionary with string metadata and
132     by analyzing a tree of nodes.
133     """
134     file = open(target[0].abspath, 'w')
135     str  = ""
136
137     try:
138         file.write( build_specfile_header(env) )
139         file.write( build_specfile_sections(env) )
140         file.write( build_specfile_filesection(env, source) )
141         file.close()
142
143         # call a user specified function
144         if env.has_key('CHANGE_SPECFILE'):
145             env['CHANGE_SPECFILE'](target, source)
146
147     except KeyError, e:
148         raise SCons.Errors.UserError( '"%s" package field for RPM is missing.' % e.args[0] )
149
150
151 #
152 # mandatory and optional package tag section
153 #
154 def build_specfile_sections(spec):
155     """ Builds the sections of a rpm specfile.
156     """
157     str = ""
158
159     mandatory_sections = {
160         'DESCRIPTION'  : '\n%%description\n%s\n\n', }
161
162     str = str + SimpleTagCompiler(mandatory_sections).compile( spec )
163
164     optional_sections = {
165         'DESCRIPTION_'        : '%%description -l %s\n%s\n\n',
166         'CHANGELOG'           : '%%changelog\n%s\n\n',
167         'X_RPM_PREINSTALL'    : '%%pre\n%s\n\n',
168         'X_RPM_POSTINSTALL'   : '%%post\n%s\n\n',
169         'X_RPM_PREUNINSTALL'  : '%%preun\n%s\n\n',
170         'X_RPM_POSTUNINSTALL' : '%%postun\n%s\n\n',
171         'X_RPM_VERIFY'        : '%%verify\n%s\n\n',
172
173         # These are for internal use but could possibly be overriden
174         'X_RPM_PREP'          : '%%prep\n%s\n\n',
175         'X_RPM_BUILD'         : '%%build\n%s\n\n',
176         'X_RPM_INSTALL'       : '%%install\n%s\n\n',
177         'X_RPM_CLEAN'         : '%%clean\n%s\n\n',
178         }
179
180     # Default prep, build, install and clean rules
181     # TODO: optimize those build steps, to not compile the project a second time
182     if not spec.has_key('X_RPM_PREP'):
183         spec['X_RPM_PREP'] = 'rm -rf "$RPM_BUILD_ROOT"' + '\n%setup -q'
184
185     if not spec.has_key('X_RPM_BUILD'):
186         spec['X_RPM_BUILD'] = 'mkdir "$RPM_BUILD_ROOT"'
187
188     if not spec.has_key('X_RPM_INSTALL'):
189         spec['X_RPM_INSTALL'] = 'scons --install-sandbox="$RPM_BUILD_ROOT" "$RPM_BUILD_ROOT"'
190
191     if not spec.has_key('X_RPM_CLEAN'):
192         spec['X_RPM_CLEAN'] = 'rm -rf "$RPM_BUILD_ROOT"'
193
194     str = str + SimpleTagCompiler(optional_sections, mandatory=0).compile( spec )
195
196     return str
197
198 def build_specfile_header(spec):
199     """ Builds all section but the %file of a rpm specfile
200     """
201     str = ""
202
203     # first the mandatory sections
204     mandatory_header_fields = {
205         'NAME'           : '%%define name %s\nName: %%{name}\n',
206         'VERSION'        : '%%define version %s\nVersion: %%{version}\n',
207         'PACKAGEVERSION' : '%%define release %s\nRelease: %%{release}\n',
208         'X_RPM_GROUP'    : 'Group: %s\n',
209         'SUMMARY'        : 'Summary: %s\n',
210         'LICENSE'        : 'License: %s\n', }
211
212     str = str + SimpleTagCompiler(mandatory_header_fields).compile( spec )
213
214     # now the optional tags
215     optional_header_fields = {
216         'VENDOR'              : 'Vendor: %s\n',
217         'X_RPM_URL'           : 'Url: %s\n',
218         'SOURCE_URL'          : 'Source: %s\n',
219         'SUMMARY_'            : 'Summary(%s): %s\n',
220         'X_RPM_DISTRIBUTION'  : 'Distribution: %s\n',
221         'X_RPM_ICON'          : 'Icon: %s\n',
222         'X_RPM_PACKAGER'      : 'Packager: %s\n',
223         'X_RPM_GROUP_'        : 'Group(%s): %s\n',
224
225         'X_RPM_REQUIRES'      : 'Requires: %s\n',
226         'X_RPM_PROVIDES'      : 'Provides: %s\n',
227         'X_RPM_CONFLICTS'     : 'Conflicts: %s\n',
228         'X_RPM_BUILDREQUIRES' : 'BuildRequires: %s\n',
229
230         'X_RPM_SERIAL'        : 'Serial: %s\n',
231         'X_RPM_EPOCH'         : 'Epoch: %s\n',
232         'X_RPM_AUTOREQPROV'   : 'AutoReqProv: %s\n',
233         'X_RPM_EXCLUDEARCH'   : 'ExcludeArch: %s\n',
234         'X_RPM_EXCLUSIVEARCH' : 'ExclusiveArch: %s\n',
235         'X_RPM_PREFIX'        : 'Prefix: %s\n',
236         'X_RPM_CONFLICTS'     : 'Conflicts: %s\n',
237
238         # internal use
239         'X_RPM_BUILDROOT'     : 'BuildRoot: %s\n', }
240
241     # fill in default values:
242     # Adding a BuildRequires renders the .rpm unbuildable under System, which
243     # are not managed by rpm, since the database to resolve this dependency is
244     # missing (take Gentoo as an example)
245 #    if not s.has_key('x_rpm_BuildRequires'):
246 #        s['x_rpm_BuildRequires'] = 'scons'
247
248     if not spec.has_key('X_RPM_BUILDROOT'):
249         spec['X_RPM_BUILDROOT'] = '%{_tmppath}/%{name}-%{version}-%{release}'
250
251     str = str + SimpleTagCompiler(optional_header_fields, mandatory=0).compile( spec )
252     return str
253
254 #
255 # mandatory and optional file tags
256 #
257 def build_specfile_filesection(spec, files):
258     """ builds the %file section of the specfile
259     """
260     str  = '%files\n'
261
262     if not spec.has_key('X_RPM_DEFATTR'):
263         spec['X_RPM_DEFATTR'] = '(-,root,root)'
264
265     str = str + '%%defattr %s\n' % spec['X_RPM_DEFATTR']
266
267     supported_tags = {
268         'PACKAGING_CONFIG'           : '%%config %s',
269         'PACKAGING_CONFIG_NOREPLACE' : '%%config(noreplace) %s',
270         'PACKAGING_DOC'              : '%%doc %s',
271         'PACKAGING_UNIX_ATTR'        : '%%attr %s',
272         'PACKAGING_LANG_'            : '%%lang(%s) %s',
273         'PACKAGING_X_RPM_VERIFY'     : '%%verify %s',
274         'PACKAGING_X_RPM_DIR'        : '%%dir %s',
275         'PACKAGING_X_RPM_DOCDIR'     : '%%docdir %s',
276         'PACKAGING_X_RPM_GHOST'      : '%%ghost %s', }
277
278     for file in files:
279         # build the tagset
280         tags = {}
281         for k in supported_tags.keys():
282             try:
283                 tags[k]=getattr(file, k)
284             except AttributeError:
285                 pass
286
287         # compile the tagset
288         str = str + SimpleTagCompiler(supported_tags, mandatory=0).compile( tags )
289
290         str = str + ' '
291         str = str + file.PACKAGING_INSTALL_LOCATION
292         str = str + '\n\n'
293
294     return str
295
296 class SimpleTagCompiler:
297     """ This class is a simple string substition utility:
298     the replacement specfication is stored in the tagset dictionary, something
299     like:
300      { "abc"  : "cdef %s ",
301        "abc_" : "cdef %s %s" }
302
303     the compile function gets a value dictionary, which may look like:
304     { "abc"    : "ghij",
305       "abc_gh" : "ij" }
306
307     The resulting string will be:
308      "cdef ghij cdef gh ij"
309     """
310     def __init__(self, tagset, mandatory=1):
311         self.tagset    = tagset
312         self.mandatory = mandatory
313
314     def compile(self, values):
315         """ compiles the tagset and returns a str containing the result
316         """
317         def is_international(tag):
318             #return tag.endswith('_')
319             return tag[-1:] == '_'
320
321         def get_country_code(tag):
322             return tag[-2:]
323
324         def strip_country_code(tag):
325             return tag[:-2]
326
327         replacements = self.tagset.items()
328
329         str = ""
330         #domestic = [ (k,v) for k,v in replacements if not is_international(k) ]
331         domestic = filter(lambda t, i=is_international: not i(t[0]), replacements)
332         for key, replacement in domestic:
333             try:
334                 str = str + replacement % values[key]
335             except KeyError, e:
336                 if self.mandatory:
337                     raise e
338
339         #international = [ (k,v) for k,v in replacements if is_international(k) ]
340         international = filter(lambda t, i=is_international: i(t[0]), replacements)
341         for key, replacement in international:
342             try:
343                 #int_values_for_key = [ (get_country_code(k),v) for k,v in values.items() if strip_country_code(k) == key ]
344                 x = filter(lambda t,key=key,s=strip_country_code: s(t[0]) == key, values.items())
345                 int_values_for_key = map(lambda t,g=get_country_code: (g(t[0]),t[1]), x)
346                 for v in int_values_for_key:
347                     str = str + replacement % v
348             except KeyError, e:
349                 if self.mandatory:
350                     raise e
351
352         return str
353