Followon to PR #4348: more bool fixes
[scons.git] / SCons / Tool / packaging / rpm.py
blob03633db269883f0e5691b6a520783e58b79ec0b1
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """The rpm packager."""
26 import SCons.Builder
27 import SCons.Tool.rpmutils
28 from SCons.Environment import OverrideEnvironment
29 from SCons.Tool.packaging import stripinstallbuilder, src_targz
30 from SCons.Errors import UserError
32 def package(env, target, source, PACKAGEROOT, NAME, VERSION,
33 PACKAGEVERSION, DESCRIPTION, SUMMARY, X_RPM_GROUP, LICENSE,
34 **kw):
35 # initialize the rpm tool
36 SCons.Tool.Tool('rpm').generate(env)
38 bld = env['BUILDERS']['Rpm']
40 # Generate a UserError whenever the target name has been set explicitly,
41 # since rpm does not allow for controlling it. This is detected by
42 # checking if the target has been set to the default by the Package()
43 # Environment function.
44 if str(target[0])!="%s-%s"%(NAME, VERSION):
45 raise UserError( "Setting target is not supported for rpm." )
46 else:
47 # Deduce the build architecture, but allow it to be overridden
48 # by setting ARCHITECTURE in the construction env.
49 buildarchitecture = SCons.Tool.rpmutils.defaultMachine()
50 if 'ARCHITECTURE' in kw:
51 buildarchitecture = kw['ARCHITECTURE']
53 fmt = '%s-%s-%s.%s.rpm'
54 srcrpm = fmt % (NAME, VERSION, PACKAGEVERSION, 'src')
55 binrpm = fmt % (NAME, VERSION, PACKAGEVERSION, buildarchitecture)
57 target = [ srcrpm, binrpm ]
59 # get the correct arguments into the kw hash
60 loc=locals()
61 del loc['kw']
62 kw.update(loc)
63 del kw['source'], kw['target'], kw['env']
65 # if no "SOURCE_URL" tag is given add a default one.
66 if 'SOURCE_URL' not in kw:
67 kw['SOURCE_URL']=(str(target[0])+".tar.gz").replace('.rpm', '')
69 # mangle the source and target list for the rpmbuild
70 env = OverrideEnvironment(env, kw)
71 target, source = stripinstallbuilder(target, source, env)
72 target, source = addspecfile(target, source, env)
73 target, source = collectintargz(target, source, env)
75 # now call the rpm builder to actually build the packet.
76 return bld(env, target, source, **kw)
78 def collectintargz(target, source, env):
79 """ Puts all source files into a tar.gz file. """
80 # the rpm tool depends on a source package, until this is changed
81 # this hack needs to be here that tries to pack all sources in.
82 sources = env.FindSourceFiles()
84 # filter out the target we are building the source list for.
85 sources = [s for s in sources if s not in target]
87 # find the .spec file for rpm and add it since it is not necessarily found
88 # by the FindSourceFiles function.
89 sources.extend( [s for s in source if str(s).rfind('.spec')!=-1] )
90 # sort to keep sources from changing order across builds
91 sources.sort()
93 # as the source contains the url of the source package this rpm package
94 # is built from, we extract the target name
95 tarball = (str(target[0])+".tar.gz").replace('.rpm', '')
96 try:
97 tarball = env['SOURCE_URL'].split('/')[-1]
98 except KeyError as e:
99 raise SCons.Errors.UserError( "Missing PackageTag '%s' for RPM packager" % e.args[0] )
101 tarball = src_targz.package(env, source=sources, target=tarball,
102 PACKAGEROOT=env['PACKAGEROOT'], )
104 return (target, tarball)
106 def addspecfile(target, source, env):
107 specfile = "%s-%s" % (env['NAME'], env['VERSION'])
109 bld = SCons.Builder.Builder(action = build_specfile,
110 suffix = '.spec',
111 target_factory = SCons.Node.FS.File)
113 source.extend(bld(env, specfile, source))
115 return (target,source)
117 def build_specfile(target, source, env):
118 """ Builds a RPM specfile from a dictionary with string metadata and
119 by analyzing a tree of nodes.
121 with open(target[0].get_abspath(), 'w') as ofp:
122 try:
123 ofp.write(build_specfile_header(env))
124 ofp.write(build_specfile_sections(env))
125 ofp.write(build_specfile_filesection(env, source))
127 # call a user specified function
128 if 'CHANGE_SPECFILE' in env:
129 env['CHANGE_SPECFILE'](target, source)
131 except KeyError as e:
132 raise SCons.Errors.UserError('"%s" package field for RPM is missing.' % e.args[0])
136 # mandatory and optional package tag section
138 def build_specfile_sections(spec):
139 """ Builds the sections of a rpm specfile.
141 str = ""
143 mandatory_sections = {
144 'DESCRIPTION' : '\n%%description\n%s\n\n', }
146 str = str + SimpleTagCompiler(mandatory_sections).compile( spec )
148 optional_sections = {
149 'DESCRIPTION_' : '%%description -l %s\n%s\n\n',
150 'CHANGELOG' : '%%changelog\n%s\n\n',
151 'X_RPM_PREINSTALL' : '%%pre\n%s\n\n',
152 'X_RPM_POSTINSTALL' : '%%post\n%s\n\n',
153 'X_RPM_PREUNINSTALL' : '%%preun\n%s\n\n',
154 'X_RPM_POSTUNINSTALL' : '%%postun\n%s\n\n',
155 'X_RPM_VERIFY' : '%%verify\n%s\n\n',
157 # These are for internal use but could possibly be overridden
158 'X_RPM_PREP' : '%%prep\n%s\n\n',
159 'X_RPM_BUILD' : '%%build\n%s\n\n',
160 'X_RPM_INSTALL' : '%%install\n%s\n\n',
161 'X_RPM_CLEAN' : '%%clean\n%s\n\n',
164 # Default prep, build, install and clean rules
165 # TODO: optimize those build steps, to not compile the project a second time
166 if 'X_RPM_PREP' not in spec:
167 spec['X_RPM_PREP'] = '[ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT"' + '\n%setup -q'
169 if 'X_RPM_BUILD' not in spec:
170 spec['X_RPM_BUILD'] = '[ ! -e "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && mkdir "$RPM_BUILD_ROOT"'
172 if 'X_RPM_INSTALL' not in spec:
173 spec['X_RPM_INSTALL'] = 'scons --install-sandbox="$RPM_BUILD_ROOT" "$RPM_BUILD_ROOT"'
175 if 'X_RPM_CLEAN' not in spec:
176 spec['X_RPM_CLEAN'] = '[ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT"'
178 str = str + SimpleTagCompiler(optional_sections, mandatory=0).compile( spec )
180 return str
182 def build_specfile_header(spec):
183 """ Builds all sections but the %file of a rpm specfile
185 str = ""
187 # first the mandatory sections
188 mandatory_header_fields = {
189 'NAME' : '%%define name %s\nName: %%{name}\n',
190 'VERSION' : '%%define version %s\nVersion: %%{version}\n',
191 'PACKAGEVERSION' : '%%define release %s\nRelease: %%{release}\n',
192 'X_RPM_GROUP' : 'Group: %s\n',
193 'SUMMARY' : 'Summary: %s\n',
194 'LICENSE' : 'License: %s\n',
197 str = str + SimpleTagCompiler(mandatory_header_fields).compile( spec )
199 # now the optional tags
200 optional_header_fields = {
201 'VENDOR' : 'Vendor: %s\n',
202 'X_RPM_URL' : 'Url: %s\n',
203 'SOURCE_URL' : 'Source: %s\n',
204 'SUMMARY_' : 'Summary(%s): %s\n',
205 'ARCHITECTURE' : 'BuildArch: %s\n',
206 'X_RPM_DISTRIBUTION' : 'Distribution: %s\n',
207 'X_RPM_ICON' : 'Icon: %s\n',
208 'X_RPM_PACKAGER' : 'Packager: %s\n',
209 'X_RPM_GROUP_' : 'Group(%s): %s\n',
211 'X_RPM_REQUIRES' : 'Requires: %s\n',
212 'X_RPM_PROVIDES' : 'Provides: %s\n',
213 'X_RPM_CONFLICTS' : 'Conflicts: %s\n',
214 'X_RPM_BUILDREQUIRES' : 'BuildRequires: %s\n',
216 'X_RPM_SERIAL' : 'Serial: %s\n',
217 'X_RPM_EPOCH' : 'Epoch: %s\n',
218 'X_RPM_AUTOREQPROV' : 'AutoReqProv: %s\n',
219 'X_RPM_EXCLUDEARCH' : 'ExcludeArch: %s\n',
220 'X_RPM_EXCLUSIVEARCH' : 'ExclusiveArch: %s\n',
221 'X_RPM_PREFIX' : 'Prefix: %s\n',
223 # internal use
224 'X_RPM_BUILDROOT' : 'BuildRoot: %s\n',
227 # fill in default values:
228 # Adding a BuildRequires renders the .rpm unbuildable under systems which
229 # are not managed by rpm, since the database to resolve this dependency is
230 # missing (take Gentoo as an example)
231 #if 'X_RPM_BUILDREQUIRES' not in spec:
232 # spec['X_RPM_BUILDREQUIRES'] = 'scons'
234 if 'X_RPM_BUILDROOT' not in spec:
235 spec['X_RPM_BUILDROOT'] = '%{_tmppath}/%{name}-%{version}-%{release}'
237 str = str + SimpleTagCompiler(optional_header_fields, mandatory=0).compile( spec )
239 # Add any extra specfile definitions the user may have supplied.
240 # These flags get no processing, they are just added.
241 # github #3164: if we don't turn off debug package generation
242 # the tests which build packages all fail. If there are no
243 # extra flags, default to adding this one. If the user wants
244 # to turn this back on, supply the flag set to None.
246 if 'X_RPM_EXTRADEFS' not in spec:
247 spec['X_RPM_EXTRADEFS'] = ['%global debug_package %{nil}']
248 for extra in spec['X_RPM_EXTRADEFS']:
249 str += extra + '\n'
251 return str
254 # mandatory and optional file tags
256 def build_specfile_filesection(spec, files):
257 """ builds the %file section of the specfile
259 str = '%files\n'
261 if 'X_RPM_DEFATTR' not in spec:
262 spec['X_RPM_DEFATTR'] = '(-,root,root)'
264 str = str + '%%defattr %s\n' % spec['X_RPM_DEFATTR']
266 supported_tags = {
267 'PACKAGING_CONFIG' : '%%config %s',
268 'PACKAGING_CONFIG_NOREPLACE' : '%%config(noreplace) %s',
269 'PACKAGING_DOC' : '%%doc %s',
270 'PACKAGING_UNIX_ATTR' : '%%attr %s',
271 'PACKAGING_LANG_' : '%%lang(%s) %s',
272 'PACKAGING_X_RPM_VERIFY' : '%%verify %s',
273 'PACKAGING_X_RPM_DIR' : '%%dir %s',
274 'PACKAGING_X_RPM_DOCDIR' : '%%docdir %s',
275 'PACKAGING_X_RPM_GHOST' : '%%ghost %s', }
277 for file in files:
278 # build the tagset
279 tags = {}
280 for k in supported_tags.keys():
281 try:
282 v = file.GetTag(k)
283 if v:
284 tags[k] = v
285 except AttributeError:
286 pass
288 # compile the tagset
289 str = str + SimpleTagCompiler(supported_tags, mandatory=0).compile( tags )
291 str = str + ' '
292 str = str + file.GetTag('PACKAGING_INSTALL_LOCATION')
293 str = str + '\n\n'
295 return str
297 class SimpleTagCompiler:
298 """ Compile RPM tags by doing simple string substitution.
300 The replacement specfication is stored in the *tagset* dictionary,
301 something like::
303 {"abc" : "cdef %s ", "abc_": "cdef %s %s"}
305 The :func:`compile` function gets a value dictionary, which may look like::
307 {"abc": "ghij", "abc_gh": "ij"}
309 The resulting string will be::
311 "cdef ghij cdef gh ij"
314 def __init__(self, tagset, mandatory: int=1) -> None:
315 self.tagset = tagset
316 self.mandatory = mandatory
318 def compile(self, values):
319 """ Compiles the tagset and returns a str containing the result
321 def is_international(tag) -> bool:
322 return tag.endswith('_')
324 def get_country_code(tag):
325 return tag[-2:]
327 def strip_country_code(tag):
328 return tag[:-2]
330 replacements = list(self.tagset.items())
332 str = ""
333 domestic = [t for t in replacements if not is_international(t[0])]
334 for key, replacement in domestic:
335 try:
336 str = str + replacement % values[key]
337 except KeyError as e:
338 if self.mandatory:
339 raise e
341 international = [t for t in replacements if is_international(t[0])]
342 for key, replacement in international:
343 try:
344 x = [t for t in values.items() if strip_country_code(t[0]) == key]
345 int_values_for_key = [(get_country_code(t[0]),t[1]) for t in x]
346 for v in int_values_for_key:
347 str = str + replacement % v
348 except KeyError as e:
349 if self.mandatory:
350 raise e
352 return str
354 # Local Variables:
355 # tab-width:4
356 # indent-tabs-mode:nil
357 # End:
358 # vim: set expandtab tabstop=4 shiftwidth=4: