fix building of forked upstream packages
[ta/rpmbuilder.git] / rpmbuilder / rpmtools.py
1 # Copyright 2019 Nokia
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """For handling rpm related work"""
16 import logging
17 import os
18 import re
19 import subprocess
20 from rpmUtils.miscutils import splitFilename
21
22 from rpmbuilder.baseerror import RpmbuilderError
23 from rpmbuilder.executor import Executor
24
25
26 class Specworker(object):
27     """ Working with spec files """
28
29     def __init__(self, directory, specfile=None):
30         self.logger = logging.getLogger(__name__)
31         if specfile:
32             if self.__verify_specfile_exists(os.path.join(directory, specfile)):
33                 self.specfilename = specfile
34             else:
35                 self.logger.critical("Specfile %s not found", specfile)
36                 raise SpecError("Spec file not found")
37         else:
38             self.specfilename = self.__locate_spec_file(directory)
39
40         self.specfilefullpath = os.path.join(directory, self.specfilename)
41
42         self.name = ""
43         self.version = ""
44         self.release = ""
45         self.source_files = []
46         self.source_file_extension = None
47         self.patch_files = []
48         self.buildrequires = []
49         self.requires = []
50         self.packages = []
51         self.files = []
52         self.spec_globals = {}
53         self.read_spec()
54
55     def __str__(self):
56         return 'name:%s version:%s' % (self.name, self.version)
57
58     def __getattr__(self, item):
59         return self.spec_globals.get(item)
60
61     @staticmethod
62     def __locate_spec_file(directory):
63         """ Finding spec files from directory """
64         logger = logging.getLogger(__name__)
65         logger.debug("Searching for spec files under: %s", directory)
66         specfile = ''
67
68         for occurence in os.listdir(directory):
69             filefullpath = os.path.join(directory, occurence)
70             if os.path.isfile(filefullpath) and filefullpath.endswith(".spec"):
71                 logger.info("Found spec file: %s", occurence)
72                 if specfile:
73                     logger.critical("Project has more than one spec files."
74                                     "I don't know which one to use.")
75                     raise SpecError("Multiple spec files")
76                 else:
77                     specfile = occurence
78         if specfile:
79             return specfile
80         else:
81             raise SpecError("No spec file available")
82
83     def _read_spec_sources(self):
84         cmd = ['spectool', '-n', '-S', self.specfilefullpath]
85         sources = self._parse_spectool_output(Executor().run(cmd))
86         self.source_file_extension = self.__get_source_file_extension(sources[0])
87         return sources
88
89     def _read_spec_patches(self):
90         cmd = ['spectool', '-n', '-P', self.specfilefullpath]
91         return self._parse_spectool_output(Executor().run(cmd))
92
93     def _parse_spectool_output(self, output):
94         return [line.split(':', 1)[1].strip() for line in output.splitlines()]
95
96     def _get_package_names(self):
97         cmd = ['rpm', '-q', '--qf', '%{NAME}\n', '--specfile', self.specfilefullpath]
98         return Executor().run(cmd).splitlines()
99
100     def _get_version(self):
101         cmd = ['rpmspec', '-q', '--queryformat', '%{VERSION}\n', self.specfilefullpath]
102         return Executor().run(cmd).splitlines()[0]
103
104     def read_spec(self):
105         """ Reading spec file values to variables """
106         self.logger.debug("Reading spec file %s", self.specfilefullpath)
107         self.source_files = self._read_spec_sources()
108         self.patch_files = self._read_spec_patches()
109         self.packages = self._get_package_names()
110         self.name = self.packages[0]
111         self.version = self._get_version()
112
113         with open(self.specfilefullpath, 'r') as filep:
114             name_found = False
115             for line in filep:
116                 linestripped = line.strip()
117
118                 if linestripped.startswith("#") or not linestripped:
119                     continue
120
121                 if linestripped.lower().startswith("%global"):
122                     try:
123                         var, val = re.match(r'^%global (\w+) (.+)$', linestripped).groups()
124                         self.spec_globals[var] = val
125                     except Exception as err:
126                         logger = logging.getLogger(__name__)
127                         logger.warning(
128                             'Failed to parse %global macro "{}" (error: {})'.format(linestripped,
129                                                                                     str(err)))
130
131                 elif linestripped.lower().startswith("buildrequires:") and "only_builddep_resolve" not in self.spec_globals:
132                     self.buildrequires.extend(self.__get_value_from_line(linestripped))
133
134                 elif linestripped.lower().startswith("requires:") and "only_builddep_resolve" not in self.spec_globals:
135                     self.requires.extend(self.__get_value_from_line(linestripped))
136
137                 elif linestripped.lower().startswith("release:"):
138                     templist = self.__get_value_from_line(linestripped)
139                     self.release = templist[0]
140
141                 elif linestripped.lower().startswith("name:"):
142                     name_found = True
143
144                 elif linestripped.lower().startswith("%package"):
145                     if not name_found:
146                         self.logger.error(
147                             "SPEC file is faulty. Name of the package should be defined before defining subpackages")
148                         raise SpecError(
149                             "Problem in spec file. Subpackages defined before %packages")
150
151                 elif linestripped.lower().startswith("%files"):
152                     if name_found:
153                         templist = self.__get_package_names_from_line(self.name, linestripped)
154                         self.files.extend(templist)
155                     else:
156                         self.logger.critical(
157                             "SPEC file is faulty. Name of the package should be defined before defining subpackages")
158                         raise SpecError("Problem in spec file. No %files defined")
159
160         if not self.verify_spec_ok():
161             raise SpecError("Inspect file %s" % self.specfilefullpath)
162         self.logger.info("Reading spec file done: %s", str(self))
163
164     def verify_spec_ok(self):
165         """ Check that spec file contains the necessary building blocks """
166         spec_status = True
167         if not self.name:
168             self.logger.critical("Spec does not have name defined")
169             spec_status = False
170         if not self.version:
171             self.logger.critical("Spec does not contain version")
172             spec_status = False
173         if not self.release:
174             self.logger.critical("Spec does not contain release")
175             spec_status = False
176         if not self.source_file_extension:
177             self.logger.critical(
178                 "Spec does not define source information with understandable archive method")
179             spec_status = False
180         return spec_status
181
182     @staticmethod
183     def __get_source_file_extension(line):
184         """ Read source file archive file end """
185
186         if line.endswith('.tar.gz'):
187             return "tar.gz"
188         elif line.endswith('.tgz'):
189             return "tgz"
190         elif line.endswith('.tar'):
191             return "tar"
192         elif line.endswith('.tar.bz2'):
193             return "tar.bz2"
194         elif line.endswith('.tar.xz'):
195             return "tar.xz"
196         elif line.endswith('.zip'):
197             return "zip"
198         else:
199             raise SpecError(
200                 "Unknown source archive format. Supported are: tar.gz, tgz, tar, tar.bz2, tar.xz, zip")
201
202     @staticmethod
203     def __get_value_from_line(line):
204         """ Read spec line where values come after double-colon """
205         valuelist = []
206         linewithgroups = re.search('(.*):(.*)$', line)
207         linevalues = linewithgroups.group(2).strip().replace(' ', ',').split(',')
208         for linevalue in linevalues:
209             valuelist.append(linevalue.strip(' \t\n\r'))
210         return valuelist
211
212     @staticmethod
213     def __get_package_names_from_line(name, line):
214         """ Read spec line where package names are defined """
215         linewithgroups = re.search('%(.*) (.*)$', line)
216         if linewithgroups:
217             value = linewithgroups.group(2).strip(' \t\n\r')
218             return [name + '-' + value]
219         return [name]
220
221     def __verify_specfile_exists(self, specfile):
222         """ Check that the given spec file exists """
223         if not specfile.endswith(".spec"):
224             self.logger.error("Given specfile %s does not end with .spec prefix", specfile)
225             return False
226
227         if os.path.isfile(specfile):
228             return True
229         self.logger.error("Could not locate specfile %s", specfile)
230         return False
231
232
233 class Repotool(object):
234     """ Module for handling rpm related functions """
235
236     def __init__(self):
237         self.logger = logging.getLogger(__name__)
238
239     def createrepo(self, directory):
240         """ Create a yum repository of the given directory """
241         createrepo_executable = "/usr/bin/createrepo"
242         createrepocommand = [createrepo_executable, '--update', directory]
243         outputfile = os.path.join(directory, 'log.txt')
244         with open(outputfile, 'w') as filep:
245             try:
246                 subprocess.check_call(createrepocommand, shell=False, stdout=filep,
247                                       stderr=subprocess.STDOUT)
248             except subprocess.CalledProcessError:
249                 self.logger.critical("There was error running createrepo")
250                 raise RepotoolError("There was error running createrepo")
251             except OSError:
252                 self.logger.error(createrepo_executable + "command not available")
253                 raise RepotoolError("No createrepo tool available")
254
255     def latest_release_of_package(self, directory, package, version):
256         """ Return latest release of the given package """
257         self.logger.debug("Looking for latest %s - %s under %s",
258                           package, version, directory)
259         latest_found_release = 0
260         if os.path.isdir(directory):
261             for occurence in os.listdir(directory):
262                 filefullpath = os.path.join(directory, occurence)
263                 if os.path.isfile(filefullpath) \
264                         and filefullpath.endswith(".rpm") \
265                         and not filefullpath.endswith(".src.rpm"):
266                     (rpmname, rpmversion, rpmrelease, _, _) = splitFilename(occurence)
267                     if rpmname == package and rpmversion == version:
268                         self.logger.debug("Found rpm " + filefullpath)
269                         if latest_found_release < rpmrelease:
270                             self.logger.debug("Found rpm to match and to be the latest")
271                             latest_found_release = rpmrelease
272         if latest_found_release == 0:
273             self.logger.debug("Did not find any previous releases of %s", package)
274         return str(latest_found_release)
275
276     def next_release_of_package(self, directory, package, version, oldrelease):
277         """ Return next release of the given package """
278         self.logger.debug("Looking for next release number for %s - %s under %s ", package, version,
279                           directory)
280
281         specreleasematch = re.search('^([0-9]+)(.*)$', oldrelease)
282         if specreleasematch and specreleasematch.group(2):
283             releasesuffix = specreleasematch.group(2)
284         else:
285             releasesuffix = ''
286
287         latest_release = self.latest_release_of_package(directory, package, version)
288         self.logger.debug("Latest release of the package: " + latest_release)
289         rematches = re.search('^([0-9]+)(.*)$', latest_release)
290         if rematches.group(1).isdigit():
291             nextrelease = str(int(rematches.group(1)) + 1) + releasesuffix
292             self.logger.debug("Next release of the package: " + nextrelease)
293             return nextrelease
294         else:
295             self.logger.critical("Could not parse release \"%s\" from package \"%s\"",
296                                  latest_release, package)
297             raise RepotoolError("Could not process release in rpm")
298
299
300 class RepotoolError(RpmbuilderError):
301     """ Exceptions originating from repotool """
302     pass
303
304
305 class SpecError(RpmbuilderError):
306     """ Exceptions originating from spec content """
307     pass