From 4c55d64ee55268ba8a21343b24d9c2cc6a6579d0 Mon Sep 17 00:00:00 2001 From: Dennis Gilmore Date: Sun, 30 Mar 2008 23:49:41 -0500 Subject: add scripts from wiki. they still need work --- src/fedora-qa | 1847 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1847 insertions(+) create mode 100644 src/fedora-qa (limited to 'src/fedora-qa') diff --git a/src/fedora-qa b/src/fedora-qa new file mode 100644 index 0000000..857cfea --- /dev/null +++ b/src/fedora-qa @@ -0,0 +1,1847 @@ +#!/usr/bin/env python +# +# QA check script. +# +# Input a bug id, and it downloads the SRPM and checks it. +# See --help for more info +# +# Created Mar 8 2004 by Erik S. LaBianca +# Modified by Aurelien Bompard +# Mostly rewritten in September 2005 by Aurelien Bompard for Fedora Extras +# +# License: GPL +# + +# TODO: Packaging/Guidelines#FileDeps +# Check that all files are UTF-8 encoded +# Packaging/Conflicts + +version = "$Rev: 181 $"[6:-2] + +REQUIRES = ["/usr/bin/mock", "/usr/bin/less", "/usr/bin/diff", "/usr/bin/rpmlint", + "/usr/bin/desktop-file-validate" ] + +GUIDELINES = [ { "name": "Guidelines", + "version": 119 }, + { "name": "ReviewGuidelines", + "version": 52 }, + ] + +SELF_URL = "http://gauret.free.fr/fichiers/rpms/fedora/fedora-qa" + +import sys +try: + import re + import os.path + import commands + import rpm + import rpmUtils + import getopt + import urlgrabber + import urlgrabber.progress + import md5 + import xml + from xml.dom import minidom + import ConfigParser + import yum + import glob + import stat +except ImportError, e: + print "ERROR importing a module:" + print e + sys.exit(1) +except KeyboardInterrupt: + print "Interrupted by user" + sys.exit(1) + + +########################### + +# General functions + +class QAError(Exception): pass + + +def pr(text, debug_text=1): + """print function taking debug mode into account""" + if debug == 0: + return + elif debug >= debug_text: + print text + +def parse_options(): + '''Parses command line options with getopts and returns a tuple''' + shortOpts = "dhiulc:bov" + longOpts = ["debug", "help", "livna", "usage", "list", "check=", "build", "local", "version"] + try: + optlist, args = getopt.gnu_getopt(sys.argv[1:], shortOpts, longOpts) + except getopt.GetoptError, e: + print "You asked for an invalid option:" + print e + print + print_usage() + debug = 1 + bugID = 0 + origin = "fedora" + do_checks = [] + local = False + for opt, arg in optlist: + if opt == "-d" or opt == "--debug": + debug = 2 + elif opt == "-h" or opt == "--help" \ + or opt == "-u" or opt == "--usage": + print_usage() + elif opt == "-i" or opt == "--livna": + origin = "livna" + elif opt == "-l" or opt == "--list": + for check in list_checks(): + print "%s : %s" % (check, getattr(eval(check),"__doc__")) + sys.exit(0) + elif opt == "-c" or opt == "--check": + do_checks = arg.split(",") + elif opt == "-b" or opt == "--build": + add_check = False + for check in list_checks(): + if check == "CheckBuildMock": + add_check = True + if add_check: + do_checks.append(check) + elif opt == "-o" or opt == "--local": + local = True + elif opt == "-v" or opt == "--version": + print "%s version %s" % (os.path.basename(sys.argv[0]), version) + sys.exit(0) + if len(args) != 1: + print_usage() + else: + arg = args[0] + return (arg, origin, debug, do_checks, local) + +def print_usage(): + scriptName = os.path.basename(sys.argv[0]) + print """Description of %s: +Starts the Fedora QA process by downloading the most recent SRPM from bugzilla, +and doing most of the QA checks automatically. + +Usage: + %s [options] [bugzilla_bug_id | srpm_filename] + +Options: + -d, --debug .................... debug mode + -l, --list .................... list all available checks + -c , --check= .... only run the specified check + -b, --build .................... only run the build and the following checks + -i, --livna .................... process a livna package + -o, --local .................... use the local files (don't download anything) + -h, --help ..................... this help message + -u, --usage .................... this help message + -v, --version .................. print the version +""" % (scriptName, scriptName) + sys.exit(1) + +def list_checks(): + """Lists available checks""" + checks = [] + for name in globals().keys(): + if type(eval(name)) == type(QACheck) and issubclass(eval(name), QACheck) and name != "QACheck": + checks.append(name) + checks.sort(lambda x,y: cmp(eval(x).order, eval(y).order)) + return checks + +def check_requires(requires_list): + for file in requires_list: + if not os.path.exists(file): + print "ERROR ! Missing dependency: "+file + sys.exit(1) + +def parse_config(): + conffilepath = "%s/.fedora-qa" % os.getenv("HOME") + if not os.path.exists(conffilepath): + conffile = open(conffilepath, "w") + conffile.write("[DEFAULTS]\nreportsdir = ~/reports\n") + conffile.close() + confparser = ConfigParser.ConfigParser() + confparser.read(conffilepath) + conf = {} + for name, value in confparser.items("DEFAULTS"): + conf[name] = os.path.expanduser(value) + return conf + +def check_guidelines(pages): + '''Checks if the guidelines have changed, and warn about it''' + baseurl = "http://fedoraproject.org/wiki/Packaging/" + for page in pages: + try: + data = urlgrabber.urlread("%s%s?action=info" % (baseurl, page["name"])) + #version_list = re.compile('\n ([0-9]+)\n [0-9: -]{19}\n [0-9]+\n').findall(data) + version_list = re.compile('\n ([0-9]+)\n').findall(data) + except urlgrabber.grabber.URLGrabError: + version_list = [] + if version_list == []: + print "Warning: I can't check if the wiki page %s has changed" % page["name"] + continue + version = version_list[0] # First version number in page -> last version of the page + if not version: + print "Warning: I can't check if the wiki page %s has changed" % page["name"] + return + if int(version) > page["version"]: + print "Warning: the guidelines have changed !" + print "Please check what changed in the following page:" + print baseurl+page["name"] + print "since revision %s, or get an updated version of this script from:" % page["version"] + print SELF_URL + + +############################ +############################ + +class QAPackageError(QAError): pass + +# Class for the src.rpm + +class QAPackage: + """This object is a package to run the QA tests on + The constructor takes the filename as its argument""" + + sources = {} + patches = {} + sourceMd5 = {} + sourcedir = "" + rpmfilenames = [] + needswork = [] + passed = [] + info = [] + checklist = [] + hdr = None + spec = None + total_checks = 0 + + def __init__(self, filename, origin="fedora", reportDir=""): + self.filename = filename + if not os.path.exists(filename): + raise QAPackageError("no such SRPM") + self.origin = origin + pr("Setting srpm variables", 2) + self.set_srpm_variables() + # create dir for reports + if reportDir == "": + self.reportDir = "reports/"+self.name + else: + self.reportDir = reportDir+"/"+self.name + if not os.path.exists(self.reportDir): + os.makedirs(self.reportDir) + self.notes = os.path.join(self.reportDir, "notes") + if not os.path.exists(self.notes): + notes = open(self.notes, "w") + notes.write("Notes:\n\n") + notes.close() + + def set_srpm_variables(self): + '''get the rpm variables from the srpm''' + # set specfile + ts = rpm.ts("", rpm._RPMVSF_NOSIGNATURES) + self.hdr = rpmUtils.miscutils.hdrFromPackage(ts, self.filename) + filelist = self.hdr[rpm.RPMTAG_FILENAMES] + fileflags = self.hdr[rpm.RPMTAG_FILEFLAGS] + try: + specfile = filelist[ fileflags.index(32) ] + except ValueError: + raise QAPackageError("ERROR: Didn't find spec file !") + specdir = commands.getoutput('rpm -q --qf "$(rpm -E %%{_specdir})" --nosignature -p %s 2>/dev/null' % self.filename) + self.specfile = os.path.join(specdir, specfile) + pr("Specfile found: %s" % self.specfile, 2) + # set tags + fedoraver = commands.getoutput('rpm -q --qf "%{version}" fedora-release') + naevr = rpmUtils.miscutils.pkgTupleFromHeader(self.hdr) + self.name = naevr[0] + self.version = naevr[3] + self.arch = naevr[1] + self.epoch = naevr[2] + self.release = naevr[4] + # arch is noarch or default buildarch + buildarch = commands.getoutput('rpm --eval "%_arch"') + if self.arch != "noarch": + self.arch = buildarch + # set paths + # do as specdir, if sourcedir contains %name... + self.sourcedir = commands.getoutput('rpm -q --qf "$(rpm -E %%{_sourcedir})" --nosignature -p %s 2>/dev/null' % self.filename) + # extract specfile + os.system('rpm2cpio %s | cpio --quiet -i %s 2>/dev/null' % (self.filename, specfile)) + # extracts sources and URLs from the spec file + self.spec = ts.parseSpec(specfile) + sourceList = self.spec.sources() + for (name, id, type) in sourceList: + if type == 1: # Source + self.sources[id] = name + elif type == 2: # Patch + self.patches[id] = name + # Get the list of binary rpms from the specfile - does not always work, see php-extras package + #rpmFileList = commands.getoutput('rpm -q --define "dist .fc%s" ' % fedoraver \ + # +'--qf "%%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm\n" --specfile %s' % specfile).split('\n') + #for file in rpmFileList: + # if not file.startswith(self.name+"-debuginfo"): + # self.rpmfilenames.append(file) + # remove extracted specfile + os.remove(specfile) + + + def rpmEval(self, text): + '''Evaluate a variable with rpm's variables set and returns it''' + command = "rpmbuild -bp --nodeps --force --define '__spec_prep_pre echo %s; exit 0' --define 'setup :' %s | tail -n 1" % (text, self.specfile) + return commands.getoutput(command) + + def installSRPM(self): + '''installs the SRPM on the system, and backs up the old spec file if it exists''' + if os.path.exists(self.specfile): + os.rename(self.specfile, self.specfile+".old") + installStatus, installOutput = commands.getstatusoutput('rpm --nosignature -i %s' % (self.filename)) + if installStatus > 0: + raise QAPackageError("ERROR: Unable to install SRPM: %s" % installOutput) + + def getReleaseFromSpec(self): + '''Call rpm on the specfile to get the dist tag right on the release tag. Can only be done after install''' + self.release = commands.getoutput('rpm -q --qf "%%{R}\n" --nosignature --specfile %s 2>/dev/null | head -1' % self.specfile) + + def setChecks(self, list): + self.checklist = list + + def addCheck(self, check): + self.checklist.append(check) + + def getReport(self, printonly=False): + '''Print out the QA report''' + report = "Review for release %s:\n" % self.release + for item in self.passed: + report += "* %s\n" % item + report += "* INSERT RESULT OF RUN TEST\n" + if self.needswork != []: + report += "\nNeeds work:\n" + for item in self.needswork: + report += "* %s\n" % item + if self.info != []: + report += "\nMinor:\n" + for item in self.info: + report += "* %s\n" % item + report += "\n(%s checks have been run)\n" % self.total_checks + report += "\n\n" + notesFD = open(self.notes, "r") + report += notesFD.read() + notesFD.close() + return report + + def printReport(self): + """Prints the report to stdout""" + report = self.getReport() + pr("----------------------------------------") + pr(report) + pr("----------------------------------------") + pr('All files can be found in the "%s" directory' % self.reportDir) + pr('The spec file is "%s"' % self.specfile) + + def saveReport(self): + """Saves the report to the reportDir""" + report = self.getReport() + reportFD = open(os.path.join(self.reportDir, "report"), "w") + reportFD.write(report) + reportFD.close() + + def runChecks(self): + pr("Checking %s, version %s, release %s..." % (self.name, self.version, self.release)) + self.checklist.sort() + for check_item in self.checklist: + check_item.check() + + + +########################## + +# Test Class : + +class QACheck: + + target = None + order = 0 + editor = "vim -o" # Vim in multiwindow mode. Use "Ctrl+W +" to enlarge the current window + + def __init__(self, target): + """Needs a QAPackage as argument: the package to test on""" + self.target = target + if os.environ.has_key("EDITOR"): + self.editor = os.environ["EDITOR"] + + def __cmp__(self, other): + if other.order > self.order: + return -1 + elif other.order == self.order: + return 0 + elif other.order < self.order: + return 1 + + def has_passed(self, message): + """ The test passed """ + self.target.passed.append(message) + + def has_failed(self, message, severity="needswork"): + """ The test failed """ + pr(message) + if severity == "info": + self.target.info.append(message) + else: + self.target.needswork.append(message) + + def approve(self, testname): + """asks the user if he likes what he sees, and if not, generate a needswork review""" + pr(testname+": is it OK ? (press Enter if yes, or else type your comment below)") + answer = raw_input().strip() + if answer == "": + self.has_passed(testname+" looks OK") + else: + self.has_failed(testname+": "+answer) + + def ask(self, prompt, default="y"): + """asks a question and returns true or false""" + if default == "y": + choice = " ([y]/n)" + else: + choice = " (y/[n])" + print prompt+choice + answer = raw_input().strip() + if (default == "y" and answer != "n") or answer == "y": + return 1 + else: + return 0 + + def check_built(self): + """Checks that the binary rpm is built.""" + files = "%s/*-%s-%s*.%s.rpm" % (self.target.reportDir, self.target.version, + self.target.release, self.target.arch) + rpms_list = glob.glob(files) + rpms_list = [ os.path.basename(r) for r in rpms_list ] + if not rpms_list: + pr("Binary package is not built yet") + #print files + return False + else: + self.target.rpmfilenames = rpms_list + return True + + def check(self): + """main checking function, to be redefined by subclasses""" + self.target.total_checks += 1 + pass + + +########################## +# Checks + +class CheckName(QACheck): + """ask the user if the name-version-release is correct""" + + order = 10 + + def check(self): + pr("Checking name", 2) + message_failed = "Package does not follow Fedora's package naming guildlines\n (wiki: PackageNamingGuidelines)" + if self.target.origin == "livna": + ext = ".lvn." + else: + ext = "" + if self.target.release.count(ext) == 0: + self.has_failed(message_failed) + return + version_regexp = re.compile(r"""^ + [\d\.]+ # version number + ([a-z]+)? # optional tag for quick bugfix release + $""", re.VERBOSE) + release_regexp = re.compile(r"""^ + ( # Either normal release tag, or non-numeric (pre-releases, snapshots) + [1-9]+\d* # normal release number, to be incremented. Does not start with 0 + | # now come the non-numeric cases + 0\. # pre-release: prefix with 0 + [1-9]+\d* # release number, to be incremented. Does not start with 0 + \.\w+ # alphatag or snapshot tag + ) # end of the non-numeric cases + (\.fc\d+ # dist tag + (\.\d+)? # minor release bumps for old branches + )? # the dist tag is optional, and minor release bumps for old branches may only be used if dist tag is used + $""", re.VERBOSE) + # tested with: all examples on the PackageNamingGuidelines, plus: + # "1" (good), + # "1.fc6" (good), + # "0.1.a" (good), + # "0.2a" (bad, "a" should be after a dot), + # "1.fc6.1" (good, branch-specific bump) + # "1.1" (bad, branch-specific bump without the dist tag) + + version_match = version_regexp.match(self.target.version) + release_match = release_regexp.match(self.target.release) + question = "Are the name, version and release correct according to Fedora's package naming guidelines " \ + +"(http://fedoraproject.org/wiki/Packaging/NamingGuidelines)" + if version_match and release_match: + question += "\n(looks good to my automatic check)" + else: + question += "\n(looks wrong to my automatic check)" + answer = self.ask(question) + if answer == 1: + self.has_passed("RPM name is OK") + else: + self.has_failed(message_failed) + self.target.total_checks += 1 + + +class CheckSpecDiff(QACheck): + """diff the spec files in case of update""" + + order = 20 + + def check(self): + pr("Checking spec diff", 2) + if os.path.exists(self.target.specfile+".old"): + diff = commands.getoutput('diff -bu "%s.old" "%s"' % (self.target.specfile, self.target.specfile)) + diffFilePath = os.path.join(self.target.reportDir,"spec.diff") + if diff == "": + pr("The spec file didn't change", 2) + if os.path.exists(diffFilePath): + if self.ask("View the last spec diff ?"): + os.system('%s "%s" "%s"' % (self.editor, diffFilePath, self.target.notes)) + else: + diffFd = open(diffFilePath, "w") + diffFd.write(diff) + diffFd.close() + if self.ask("Diff the spec files ?"): + os.system('%s "%s" "%s"' % (self.editor, diffFilePath, self.target.notes)) + + +class CheckSources(QACheck): + """check upstream sources: returns a list of lines for the report""" + + order = 30 + + def check(self): + pr("Checking upstream sources", 2) + for (sourceId, sourceUrl) in self.target.sources.iteritems(): + sourceFileName = os.path.basename(sourceUrl) + if sourceUrl.count('tp://') == 0 and sourceUrl.count('https://') == 0: + if self.ask("Source %s is not downloadable (%s). View it in 'less' ?" % (sourceId, sourceUrl)): + os.system('less "%s"' % self.target.sourcedir+"/"+sourceFileName) + else: + pr("Checking source %s" % sourceId) + if os.path.exists(sourceFileName): + pr("Using already downloaded source", 2) + else: + pr("Downloading source from %s" % sourceUrl, 2) + try: + urlgrabber.urlgrab(sourceUrl, reget='check_timestamp', progress_obj=urlgrabber.progress.TextMeter(fo=sys.stdout)) + except urlgrabber.grabber.URLGrabError, e: + self.has_failed("Source %s is not available (%s)\n (wiki: QAChecklist item 2)" % (sourceId, sourceUrl)) + pr(e, 2) + continue + if os.path.exists(sourceFileName): + upstream_file = open(sourceFileName, "r") + upstream_md5 = md5.new(upstream_file.read()).hexdigest() + upstream_file.close() + local_file = open(self.target.sourcedir+"/"+sourceFileName, "r") + local_md5 = md5.new(local_file.read()).hexdigest() + if upstream_md5 == local_md5: + self.has_passed("Source %s is the same as upstream" % sourceFileName) + else: + self.has_failed("Source "+sourceFileName+" is different from upstream\n (wiki: QAChecklist item 2)") + self.target.total_checks += 1 + + +class CheckPatches(QACheck): + """checks the patches in the srpm""" + + order = 40 + + def check(self): + pr("Checking patches", 2) + if not self.target.patches: + return + pr("Patches found: %s." % ", ".join(self.target.patches.values()) ) + if self.ask("Look at the patches ?"): + for (patchId, patchName) in self.target.patches.iteritems(): + os.system('%s "%s" "%s"' % (self.editor, self.target.sourcedir+"/"+os.path.basename(patchName), self.target.notes)) + self.target.total_checks += 1 + + +class CheckBRConsistency(QACheck): + """Checks that only one of $RPM_BUILD_ROOT or %{buildroot} is used""" + + order = 50 + + def check(self): + pr("Checking BuildRoot Consistency", 2) +# TODO: do it in python with re + use_shell = 0 + use_macro = 0 + status, output = commands.getstatusoutput("grep -qs '\$RPM_BUILD_ROOT' %s" % self.target.specfile) + if status == 0: + use_shell = 1 + status, output = commands.getstatusoutput("egrep -qs '%%\{?buildroot\}?' %s" % self.target.specfile) + if status == 0: + use_macro = 1 + if use_shell and use_macro: + self.has_failed("Use of buildroot is not consistant\n (wiki: Packaging/Guidelines#UsingBuildRootOptFlags)") + self.target.total_checks += 1 + + +class CheckBuildRoot(QACheck): + """Checks that the BuildRoot is the Fedora-preferred one""" + + order = 60 + + def check(self): + pr("Checking buildroot", 2) + buildroot = self.target.spec.buildRoot() + preferred_br_tag = "%{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)" + preferred_br = self.target.rpmEval(preferred_br_tag) + if buildroot != preferred_br: + self.has_failed("BuildRoot should be "+preferred_br_tag+"\n (wiki: Packaging/Guidelines#BuildRoot)") + self.target.total_checks += 1 + + +class CheckDFU(QACheck): + """Checks that the desktop-file-utils is required if desktop-file-install is used""" + + order = 70 + + def check(self): + pr("Checking desktop-file-utils", 2) + try: + install = self.target.spec.install() + except SystemError: + self.has_failed("There is no %install section") + return + if install.count("desktop-file-install") == 0: + return + buildrequires = self.target.hdr[rpm.RPMTAG_REQUIRES] + if "desktop-file-utils" not in buildrequires: + self.has_failed("BuildRequires: desktop-file-utils is missing") + self.target.total_checks += 1 + + +class CheckBRExceptions(QACheck): + """Checks that the package does not have excluded BuildRequires""" + + order = 80 + + def check(self): + pr("Checking buildrequires exceptions", 2) + # Try getting the list from the wiki + try: + data = urlgrabber.urlread("http://fedoraproject.org/wiki/Extras/FullExceptionList") + # Parse the HTML page + regexp = re.compile('''
([\w+\n-]+)
''') + br_exceptions = regexp.findall(data) + if len(br_exceptions) > 0: # If the format changed and the regexp fails + br_exceptions = br_exceptions[0].strip().split("\n") + except urlgrabber.grabber.URLGrabError: + br_exceptions = [] + + if br_exceptions == []: + # if it failed, use this list (from http://fedoraproject.org/wiki/Packaging/Guidelines#Exceptions) + br_exceptions = ["bash", "bzip2", "coreutils", "cpio", "diffutils", "fedora-release", + "findutils", "gawk", "gcc", "gcc-c++", "grep", "gzip", "info", "make", "patch", + "rpm-build", "redhat-rpm-config", "sed", "tar", "unzip", "util-linux-ng", "which",] + + buildrequires = self.target.hdr[rpm.RPMTAG_REQUIRES] + for br in buildrequires: + if br in br_exceptions: + self.has_failed("BuildRequires: %s should not be included\n (wiki: Packaging/Guidelines#Exceptions)" % br) + self.target.total_checks += 1 + + +class CheckEncoding(QACheck): + """Checks that the spec file is in ASCII or UTF-8""" + + order = 90 + + def check(self): + pr("Checking encoding", 2) + spec = open(self.target.specfile, "r") + spectext = spec.read() + spec.close() + enc_ok = False + for enc in ["utf-8", "ascii"]: + try: + unicode(spectext, enc, "strict") + except ValueError: + pass + else: + enc_ok = True + break + if not enc_ok: + self.has_failed("Encoding should be UTF-8") + self.target.total_checks += 1 + + +class CheckSpecName(QACheck): + """Checks that the spec filename is %{name}.spec""" + + order = 100 + + def check(self): + pr("Checking spec name", 2) + specfile = os.path.basename(self.target.specfile) + if not specfile == "%s.spec" % self.target.name: + self.has_failed("Specfile should be in the format %{name}.spec\n (wiki: Packaging/ReviewGuidelines)") + self.target.total_checks += 1 + + +class CheckSMPFlags(QACheck): + """Checks that the smp flags are set for make""" + + order = 110 + + def check(self): + pr("Checking SMP flags", 2) + try: + buildscript = self.target.spec.build() + except SystemError, e: # No build script + return + if self.target.arch == "noarch" or buildscript.count("make") == 0: + return + if buildscript.count(self.target.rpmEval("%{_smp_mflags}")) == 0: + self.has_failed("Missing SMP flags. If it doesn't build with it, please add a comment\n (wiki: Packaging/Guidelines#parallelmake)") + self.target.total_checks += 1 + + +class CheckQTEnvVars(QACheck): + """Checks that the QT environment variables are set""" + + order = 120 + + def check(self): + pr("Checking QT env vars", 2) + buildrequires = self.target.hdr[rpm.RPMTAG_REQUIRES] + #buildrequires = commands.getoutput("rpm -qpR --nosignature %s" % self.target.filename) + #if buildrequires.count("qt-devel") == 0 and buildrequires.count("kde") == 0: + needs_qt = False + for br in buildrequires: + if br == "qt-devel": + needs_qt = True + if br.startswith("kde") and br.endswith("-devel"): + needs_qt = True + if not needs_qt: + return + buildscript = self.target.spec.build() + if buildscript.count("unset QTDIR") == 0 and buildscript.count(". /etc/profile.d/qt.sh") == 0: + self.has_failed("QT environment variable are not sourced", "info") + self.target.total_checks += 1 + + +class CheckRelocatable(QACheck): + """Checks that the package is not marked Relocatable""" + + order = 130 + + def check(self): + pr("Checking relocatable instruction", 2) + spec = open(self.target.specfile, "r") + for line in spec: + if line.startswith("Prefix:") > 0: + self.has_failed("Package is marked as relocatable, please check.\n (wiki: Packaging/Guidelines#RelocatablePackages)") + spec.close() + return + spec.close() + self.target.total_checks += 1 + + +class CheckClean(QACheck): + """Checks that the specfile contains a proper %%clean section""" + + order = 140 + + def check(self): + pr("Checking %%clean", 2) + message = "there must be a proper %clean section\n (wiki: Packaging/ReviewGuidelines)" + buildroot = self.target.spec.buildRoot() + try: + clean = self.target.spec.clean() + except SystemError: + self.has_failed(message) + return + if clean.count(buildroot) == 0 and clean.count("$RPM_BUILD_ROOT") == 0: + self.has_failed(message) + self.target.total_checks += 1 + + +class CheckBRDuplicates(QACheck): + """Checks that the package does not have duplicate BuildRequires""" + + order = 150 + + buildrequires_clean = [] + already_required = [] + + class YumBaseNoLog(yum.YumBase): + def log(self, level, msg): + pass + + def check_br(self, pkgname): + exact,match,unmatch = yum.packages.parsePackages(self.repoq.pkgSack.returnNewestByNameArch(), [pkgname], casematch=1) + if not exact: + return + requires = exact[0].requiresList() + for req in requires: + if req.count("rpmlib") > 0: + continue # filter out rpmlib deps + if req.count(" ") > 0: + req = req.split(" ")[0] # versioned dependency + if req in self.buildrequires_clean: + pr("found duplicate: "+req, 2) + self.buildrequires_clean.remove(req) + self.already_required.append( (req, pkgname) ) + #self.check_br(req) # Check recursively. Maybe a little overkill... + + def check(self): + pr("Checking for duplicate buildrequires", 2) + buildrequires = self.target.hdr[rpm.RPMTAG_REQUIRES] + buildrequires = [br for br in buildrequires if br.count("rpmlib") == 0] # filter out rpmlib dependency + if len(buildrequires) < 2: + return + if not self.ask("Check for duplicate BuildRequires ? (may be long, %s of them)" % len(buildrequires), "n"): + return + self.buildrequires_clean = buildrequires[:] + self.repoq = self.YumBaseNoLog() + self.repoq.doConfigSetup() + cachedir = yum.misc.getCacheDir() + if cachedir is None: + pr("Error: could not make cachedir, aborting test.") + return + self.repoq.repos.setCacheDir(cachedir) + pr("setting up repositories...") + self.repoq.doRepoSetup() + try: + self.repoq.doSackSetup() + self.repoq.doTsSetup() + except yum.Errors.RepoError, e: + pr(e) + pr("Error, aborting test.") + return + pr("checking buildrequires...") + for br in buildrequires: + sys.stdout.write("\r[%s/%s]" % (buildrequires.index(br)+1, len(buildrequires))) + sys.stdout.flush() + self.check_br(br) + sys.stdout.write("\r") + sys.stdout.flush() + if self.already_required: + message = "Duplicate BuildRequires: " + for buildreq, by in self.already_required: + message += "%s (by %s), " % (buildreq, by) + self.has_failed(message[:-2], "info") + self.target.total_checks += 1 + + +class CheckRPMMacros(QACheck): + """Checks that paths are replaced with RPM macros""" + + order = 160 + + def check(self): + pr("Checking rpmmacros", 2) + spec = open(self.target.specfile, "r") + for line in spec: + if line.count("/usr") > 0 or line.count("/var") > 0: + self.has_failed("Spec file: some paths are not replaced with RPM macros\n (wiki: Packaging/Guidelines#macros)") + spec.close() + return + if line.startswith("%changelog"): + break + spec.close() + self.target.total_checks += 1 + + +class CheckRequiresPrePost(QACheck): + """Checks that the form "Requires(pre,post)" is not used""" + + order = 170 + + def check(self): + pr("Checking Requires(pre,post,...)", 2) + spec = open(self.target.specfile, "r") + expr = re.compile("^Requires\([\w\s]+,[\w\s]+(,[\w\s]+)*\):.*") + for line in spec: + if expr.match(line): + self.has_failed("Spec file: the \"Requires(*.*)\" notation should be split\n (wiki: Packaging/Guidelines#reqprepost)") + spec.close() + return + spec.close() + self.target.total_checks += 1 + + +class CheckLatestVersion(QACheck): + """Checks that the package uses the latest version""" + + order = 180 + + def fmcheck(self, xmlpage): + """Do the check on freshmeat""" + xmldoc = minidom.parse(xmlpage) + lastversion_node = xmldoc.getElementsByTagName('latest_release_version')[0].firstChild + if not lastversion_node: + return None + lastversion = xmldoc.getElementsByTagName('latest_release_version')[0].firstChild.data + return lastversion + + def check(self): + pr("Checking for the latest version", 2) + url = "http://freshmeat.net/projects-xml/%(name)s/%(name)s.xml" % {"name": self.target.name} + try: + xmlpage = urlgrabber.urlopen(url) + except urlgrabber.grabber.URLGrabError, e: + pr("Can't access Freshmeat, aborting test") + return + try: + lastversion = self.fmcheck(xmlpage) + except xml.parsers.expat.ExpatError: + pr("Project not found on Freshmeat. Please enter the project's Freshmeat name (or press Enter to abort):") + answer = raw_input().strip() + if not answer: + pr("Aborting test") + return + url = "http://freshmeat.net/projects-xml/%(name)s/%(name)s.xml" % {"name": answer} + xmlpage = urlgrabber.urlopen(url) + try: + lastversion = self.fmcheck(xmlpage) + except xml.parsers.expat.ExpatError: + pr("Project still not found. Aborting test.") + return + if lastversion is None: + pr("The Freshmeat page does not have version information, aborting.") + return + if lastversion != self.target.version: + answer = self.ask("According to Freshmeat, the latest version is %s.\n" % lastversion +\ + "Is this package using the latest version ?") + if answer != 1: + self.has_failed("The latest version is %s. Please update" % lastversion, "info") + else: + self.has_passed("This is the latest version") + else: + self.has_passed("This is the latest version") + self.target.total_checks += 1 + + +class CheckForbiddenTag(QACheck): + """Checks that the package does not contain forbidden tags""" + + order = 190 + + def check(self): + pr("Checking for forbidden tag", 2) + spec = open(self.target.specfile, "r") + for line in spec: + for tag in ["Vendor", "Packager", "Copyright"]: + if line.startswith(tag+":"): + self.has_failed("Spec file: tag %s is forbidden\n (wiki: Packaging/Guidelines#tags)" % tag) + spec.close() + self.target.total_checks += 1 + + +class CheckDownloadableSource(QACheck): + """checks if at least one of the source is an URL""" + + order = 200 + + def check(self): + pr("Checking for downloadable sources", 2) + for (sourceId, sourceUrl) in self.target.sources.iteritems(): + if sourceUrl.count('tp://') > 0: + return + self.has_failed("No downloadable source. Please give the full URL in the Source tag.") + self.target.total_checks += 1 + + +class CheckCleanBRInInstall(QACheck): + """Checks that the specfile cleans the BuildRoot in %install""" + + order = 210 + + def check(self): + pr("Checking for BR cleaning in %install", 2) + message = "The BuildRoot must be cleaned at the beginning of %install" + buildroot = self.target.spec.buildRoot() + try: + install = self.target.spec.install().split("\n") + except SystemError: + self.has_failed("There is no %install section") + return + self.target.total_checks += 1 + expr = re.compile("^(/bin/)?rm -(rf|fr) (\$RPM_BUILD_ROOT|%s)" % buildroot) + for line in install: + if expr.match(line): + return + self.has_failed(message) + + +class CheckFindLangGettext(QACheck): + """Checks that gettext is Required if the %find_lang macro is used""" + + order = 220 + + def check(self): + pr("Checking translations requirement", 2) + try: + install = self.target.spec.install() + except SystemError: + self.has_failed("There is no %install section") + return + if install.count("/usr/lib/rpm/redhat/find-lang.sh") == 0: + return + buildrequires = self.target.hdr[rpm.RPMTAG_REQUIRES] + if "gettext" not in buildrequires: + self.has_failed("BuildRequires: gettext is missing (required to build the translations)") + self.target.total_checks += 1 + + +class CheckNoarch(QACheck): + """checks if the package is improperly built for noarch""" + + order = 230 + + def check(self): + if not self.target.arch == "noarch": + return + pr("Checking invalid use of noarch", 2) + spec = open(self.target.specfile, "r") + for line in spec: + if line.count("%_libdir") > 0 or line.count("%{_libdir}") > 0: + self.has_failed("The package cannot be noarch since it installs files to %{_libdir}") + spec.close() + return + if line.startswith("%changelog"): + break + spec.close() + self.target.total_checks += 1 + + +class CheckMakeInstallMacro(QACheck): + """Checks that the %makeinstall macro is not used""" + + order = 240 + + def check(self): + pr("Checking for the %makeinstall macro", 2) + spec = open(self.target.specfile, "r") + for line in spec: + if line.count("%makeinstall") > 0 or line.count("%{makeinstall}") > 0: + self.has_failed("The %makeinstall macro should not be used\n (wiki: Packaging/Guidelines#MakeInstall)") + spec.close() + return + if line.startswith("%changelog"): + break + spec.close() + self.target.total_checks += 1 + + +class CheckGhostPyo(QACheck): + """checks if the *.pyo files are %%ghost'ed""" + + order = 250 + + def check(self): + pr("Checking ghosting of *.pyo files", 2) + spec = open(self.target.specfile, "r") + for line in spec: + if line.startswith("%ghost") and line.endswith(".pyo"): + self.has_failed("The .pyo files should not be %%ghost'ed\n (wiki: Packaging/Python#pyos)") + spec.close() + return + if line.startswith("%changelog"): + break + spec.close() + self.target.total_checks += 1 + + +class CheckSpec(QACheck): + '''Just read the spec file''' + + order = 980 + + def check(self): + pr("Checking spec file", 2) + if self.ask("Look at the spec file ?"): + os.system('%s "%s" "%s"' % (self.editor, self.target.specfile, self.target.notes)) + + +class CheckDiffTemplate(QACheck): + """Diff against the template for relevant packages""" + + order = 990 + + def check(self): + pr("Checking diff against template", 2) + for template in [ "python", "perl", "ruby" ]: + if self.target.name.startswith("%s-" % template): + if self.ask("Diff against the spec template ?"): + os.system('diff -bu "/usr/share/fedora/spectemplate-%s.spec" "%s" | less' % \ + (template, self.target.specfile)) + + +class CheckBuildMock(QACheck): + """Builds the RPM with mock""" + + order = 1000 + + def check(self, root="default"): + status, output = commands.getstatusoutput("which mock") + if os.WEXITSTATUS(status) == 1: + pr("mock is not available") + return + if not self.ask("Build the RPM in mock ?"): + return + pr("Starting build in mock") + command = 'mock --resultdir="%s" rebuild %s' % (self.target.reportDir, self.target.filename) + status = os.system(command) + if status > 0: + pr("WARNING: Build failed ! (command: %s)" % command) + self.has_failed("Build failed in mock") + pr("mock command was: '%s'" % command) + else: + self.has_passed("Builds fine in mock") + if self.ask("View the build log ?"): + rpmlog = os.path.join(self.target.reportDir, "build.log") + os.system('less "%s"' % rpmlog) + self.target.total_checks += 1 + + +class CheckRpmlint(QACheck): + """unleashes rpmlint at the srpm and the binary rpm if the build is complete""" + + order = 1100 + + def check(self): + pr("Launching rpmlint") + status, srpmRpmlint = commands.getstatusoutput("rpmlint "+self.target.filename) + if os.WEXITSTATUS(status) == 127: + pr("WARNING: rpmlint is not available, please install it") + return + srpmRpmlintFD = open(self.target.reportDir+"/rpmlint-srpm.log", "w") + srpmRpmlintFD.write(srpmRpmlint) + srpmRpmlintFD.close() + pr("Source RPM:") + pr(srpmRpmlint) + if self.check_built(): + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + rpmRpmlint = commands.getoutput("rpmlint "+filepath) + rpmRpmlintFD = open(self.target.reportDir+"/rpmlint-rpm.log", "a") + rpmRpmlintFD.write("rpmlint of %s:" % file) + rpmRpmlintFD.write(rpmRpmlint) + rpmRpmlintFD.write("\n\n") + rpmRpmlintFD.close() + subpackage = file[:file.rindex(self.target.version)-1] + pr("\nrpmlint of %s:" % subpackage) + pr(rpmRpmlint) + if len(self.target.rpmfilenames) == 1: + self.approve("rpmlint") + else: + self.approve("rpmlint of "+subpackage) + self.target.total_checks += 1 + + +class CheckFiles(QACheck): + """checks files list and ownership""" + + order = 1110 + + def check(self): + pr("Checking files", 2) + if not self.check_built(): + return + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + pr("Files in package %s:" % file) + os.system("rpm -qplv %s | less" % filepath) + subpackage = file[:file.rindex(self.target.version)-1] + if len(self.target.rpmfilenames) == 1: + self.approve("File list") + else: + self.approve("File list of "+subpackage) + + +class CheckDesktopFile(QACheck): + """checks the presence of a .desktop file if the package depends on xorg.""" + + order = 1120 + + def check(self): + pr("Checking desktop file", 2) + if not self.check_built(): + return + ts = rpm.ts() + is_graphical = False + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + requires = filehdr[rpm.RPMTAG_REQUIRES] + for r in requires: + if r.startswith("libX11.so."): + is_graphical = True + if not is_graphical: + return + has_desktopfile = False + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + for f in filelist: + if f.startswith("/usr/share/applications/") and f.endswith(".desktop"): + has_desktopfile = True + if not has_desktopfile: + self.has_failed("The package should contain a .desktop file\n (wiki: Packaging/Guidelines#desktop)") + self.target.total_checks += 1 + + +class CheckLicenseFile(QACheck): + """checks the presence of a license file.""" + + order = 1130 + + def check(self): + pr("Checking license file", 2) + if not self.check_built(): + return + ts = rpm.ts() + has_licensefile = False + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + for f in filelist: + if f.startswith("/usr/share/doc/") and ( f.lower().count("license") > 0 or \ + f.lower().count("copying") > 0 or f.lower().count("copyright") > 0 ): + has_licensefile = True + if not has_licensefile: + self.has_failed("The package should contain the text of the license\n (wiki: Packaging/ReviewGuidelines)") + self.target.total_checks += 1 + + +class CheckFileListedTwice(QACheck): + """checks if a file has been listed multiple times in %%files""" + + order = 1140 + + def check(self): + pr("Checking files listed multiple times", 2) + if not self.check_built(): + return + status, output = commands.getstatusoutput("grep '^warning: File listed twice:' %s/build.log" % self.target.reportDir) + if status == 0: + self.has_failed("File list: some files were listed multiple times\n (wiki: Packaging/ReviewGuidelines)") + self.target.total_checks += 1 + + +class CheckDefattr(QACheck): + """checks if all the %%files sections contain %%defattr() instructions""" + + order = 1150 + + def check(self): + pr("Checking defattr", 2) + if not self.check_built(): + return + files_num = commands.getoutput("grep '^%%files' %s | wc -l" % self.target.specfile) + defattr_num = commands.getoutput("grep '^%%defattr' %s | wc -l" % self.target.specfile) + if files_num > defattr_num: + self.has_failed("Each %files section should have a %defattr line\n (wiki: Packaging/ReviewGuidelines)") + self.target.total_checks += 1 + + +class CheckLibtoolArchives(QACheck): + """checks the presence of a .la libtool archives""" + + order = 1160 + + def check(self): + pr("Checking .la files", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + for f in filelist: + if os.path.dirname(f) == "/usr/lib/" and f.endswith(".la"): + self.has_failed("The package contains libtool archive files (*.la)\n (wiki: Packaging/Guidelines#StaticLibraries)") + return + self.target.total_checks += 1 + + +class CheckDesktopFileProperties(QACheck): + """checks if the desktop files are valid""" + + order = 1170 + valid_cats = None + + def check_categories(self, categories): + if "X-Fedora" in categories: + self.has_failed("Desktop file: the Categories tag should not contain X-Fedora any more\n (wiki: Packaging/Guidelines#desktop)") + if "Application" in categories: + self.has_failed("Desktop file: the Categories tag should not contain Application any more\n (wiki: Packaging/Guidelines#desktop)") + # check if the category is valid + valid_cats_url = "http://standards.freedesktop.org/menu-spec/latest/apa.html" + + # get the list of valid categories + if not self.valid_cats: + try: + data = urlgrabber.urlread(valid_cats_url) + except urlgrabber.grabber.URLGrabError: + return # Can't check... + # Parse the HTML page + regexp = re.compile('''([a-zA-Z0-9]+).+?.+?''') + self.valid_cats = regexp.findall(data) + + if len(self.valid_cats) == 0: # Regexp must have failed... don't check. + return + for cat in categories: + pr(" checking desktop files category '%s'" % cat, 2) + if not cat.startswith("X-") and cat not in self.valid_cats: + self.has_failed("Desktop file: the category %s is not valid\n (%s)" % (cat, valid_cats_url)) + + + def check(self): + pr("Checking desktop files validity", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + desktopfiles = [] + for f in filelist: + if f.startswith("/usr/share/applications/") and f.endswith(".desktop"): + desktopfiles.append(f) + if not desktopfiles: + continue + for dfile in desktopfiles: + + # Check vendor + if os.path.dirname(dfile).endswith("applications") and not os.path.basename(dfile).startswith("fedora"): + self.has_failed("Desktop file: vendor should be fedora\n (wiki: Packaging/Guidelines#desktop)") + self.target.total_checks += 1 + + # extract it + os.system('rpm2cpio %s | cpio --quiet -i -d .%s' % (filepath, dfile)) # exctract it + + # Validate with desktop-file-validate + status, output = commands.getstatusoutput("desktop-file-validate .%s" % dfile) + if status != 0: + self.has_failed("Desktop file: desktop-file-validate found errors in %s : %s" % (os.path.basename(dfile), output)) + self.target.total_checks += 1 + + # Various tests + dfile_fd = open("."+dfile, "r") + for line in dfile_fd: + # Check categories + if line.startswith("Categories"): + categories = line.strip().replace("Categories=", "").split(";") + if categories[-1] == "": + categories = categories[:-1] + self.check_categories(categories) + self.target.total_checks += 1 + # Check MimeType scriptlets + if line.startswith("MimeType") and line.strip().replace("MimeType=", "") != "": + scripts = { "post": filehdr[rpm.RPMTAG_POSTIN], + "postun": filehdr[rpm.RPMTAG_POSTUN], + } + if scripts["post"].count("update-desktop-database") == 0 \ + or scripts["postun"].count("update-desktop-database") == 0: + self.has_failed("Scriptlets: missing update-desktop-database\n (wiki: ScriptletSnippets)") + self.target.total_checks += 1 + # Check Icon + if line.startswith("Icon"): + icon = line.strip().replace("Icon=", "") + if not icon.startswith("/") and icon[-4:] in [".png", ".svg", ".xpm"]: + self.has_failed("Desktop file: the Icon tag should either use the full path to the icon or the icon name without extension\n (wiki:Packaging/Guidelines#desktop)") + self.target.total_checks += 1 + dfile_fd.close() + os.system("rm -rf usr") + + +class CheckScriptletsRequirements(QACheck): + """Checks that the usual programs found in the scriptlets are Required""" + + order = 1180 + + def check(self): + pr("Checking scriptlets requirements", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + scripts = { "pre": filehdr[rpm.RPMTAG_PREIN], + "preun": filehdr[rpm.RPMTAG_PREUN], + "post": filehdr[rpm.RPMTAG_POSTIN], + "postun": filehdr[rpm.RPMTAG_POSTUN], + } + requires = filehdr[rpm.RPMTAG_REQUIRES] + progs_check = {"service": "initscripts", + "chkconfig": "chkconfig", + "scrollkeeper-update": "scrollkeeper", + "install-info": "info", + "gconftool-2": "GConf2", + } + for prog, package in progs_check.iteritems(): + for scriptname, script in scripts.iteritems(): + if script.count(prog) > 0 and package not in requires: + filename_required = False + for r in requires: # Check if the filename is not required instead of the package + if r.endswith(prog): + filename_required = True + if not filename_required: + self.has_failed("Missing dependancy on %s for %%%s (package %s)" % (prog, scriptname, package)) + self.target.total_checks += 1 + + +class CheckLangTag(QACheck): + """checks that the translation files are tagged""" + + order = 1190 + + def check(self): + pr("Checking lang files", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + filelang = filehdr[rpm.RPMTAG_FILELANGS] + for filename, lang in zip(filelist, filelang): + if filename.startswith("/usr/share/locale/") and filename.endswith(".mo"): + if not lang: + self.has_failed("The translation files are not properly tagged\n (wiki: Packaging/ReviewGuidelines)") + return + self.target.total_checks += 1 + + +class CheckDBUpdate(QACheck): + """checks that the package updates the proper database in the scriptlets if it has corresponding files""" + + order = 1200 + + def checkFileUpdateDB(self, prefix, updater, scriptlist=["post","postun"]): + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + need_update = False + for f in filelist: + if f.startswith(prefix): + need_update = True + if need_update: + scripts = { "pre": filehdr[rpm.RPMTAG_PREIN], + "preun": filehdr[rpm.RPMTAG_PREUN], + "post": filehdr[rpm.RPMTAG_POSTIN], + "postun": filehdr[rpm.RPMTAG_POSTUN], + } + for scr in scriptlist: + if scripts[scr].count(updater) == 0: + message = "missing \"%s\" in %%%s (wiki: ScriptletSnippets)" % (updater, scr) + if len(self.target.rpmfilenames) > 1: + message += " (in subpackage %s)" % filehdr[rpm.RPMTAG_NAME] + self.has_failed("Scriptlets: "+message) + + def check(self): + pr("Checking for the need to update databases in scriptlets", 2) + if not self.check_built(): + return + # format : (, , ) + check_list = [ ("/usr/share/omf/", "scrollkeeper-update", ["post","postun"]), + ("/usr/share/mime/packages/", "update-mime-database", ["post","postun"]), + ("/usr/share/icons/", "gtk-update-icon-cache", ["post","postun"]), + ("/usr/share/info/", "install-info", ["post","preun"]), + ("/etc/gconf/schemas/", "gconftool-2", ["pre","post","preun"]), + ("/etc/rc.d/init.d/", "chkconfig", ["post","preun"]), + ("/etc/rc.d/init.d/", "service", ["preun","postun"]), + ] + for prefix, updater, scriptlist in check_list: + pr(" checking %s..." % updater, 2) + self.checkFileUpdateDB(prefix, updater, scriptlist) + self.target.total_checks += 1 + + +class CheckOwnedDirs(QACheck): + """checks that the package does not own standard dirs""" + + order = 1210 + + def check(self): + pr("Checking directory ownership", 2) + if not self.check_built(): + return + ts = rpm.ts() + standard_dirs = [] + # Get the list of files in packages "filesystem" and "man" + for pkg in ("filesystem", "man"): + mi = ts.dbMatch("name", pkg) + for hdr in mi: # there should be only one result, but we do a for loop anyway (follows the docs) + standard_dirs.extend(hdr[rpm.RPMTAG_FILENAMES]) + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + for filename in filelist: + if filename in standard_dirs: + self.has_failed("The package owns %s, which is a standard directory\n (wiki: Packaging/ReviewGuidelines)" % filename) + self.target.total_checks += 1 + + +class CheckConfigFiles(QACheck): + """checks config files list and ownership""" + + order = 1220 + + def check(self): + pr("Checking config files", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filetags = filehdr[rpm.RPMTAG_FILEFLAGS] + filenames = filehdr[rpm.RPMTAG_FILENAMES] + filemodes = filehdr[rpm.RPMTAG_FILEMODES] + filetags = filehdr[rpm.RPMTAG_FILEFLAGS] + files = zip(filenames, filemodes, filetags) + has_conf_file = False + for tag in filetags: + if tag & rpm.RPMFILE_CONFIG: + has_conf_file = True + if not has_conf_file: + continue + pr("Config files in package %s:" % file) + os.system("rpm -qpcv %s | less" % filepath) + subpackage = file[:file.rindex(self.target.version)-1] + self.approve("Config files of "+subpackage) + + +class CheckPkgconfig(QACheck): + """checks if the package needs to Require pkgconfig""" + + order = 1230 + + def check(self): + pr("Checking pkgconfig files", 2) + if not self.check_built(): + return + ts = rpm.ts() + is_graphical = False + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + has_pkgconfig = False + for f in filelist: + if f.startswith("/usr/lib/pkgconfig/") and f.endswith(".pc"): + has_pkgconfig = True + if not has_pkgconfig: + continue + requires = filehdr[rpm.RPMTAG_REQUIRES] + has_pkgconfig = False + for r in requires: + if r.count("pkgconfig"): + has_pkgconfig = True + if not has_pkgconfig: + self.has_failed("As %s ships a pkgconfig file (.pc), it should have \"Requires: pkgconfig\"" % filehdr[rpm.RPMTAG_NAME]) + self.target.total_checks += 1 + + +class CheckHicolor(QACheck): + """checks if the package needs to Require hicolor-icon-theme""" + + order = 1240 + + def check(self): + pr("Checking hicolor icons", 2) + if not self.check_built(): + return + ts = rpm.ts() + is_graphical = False + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + has_hicolor = False + for f in filelist: + if f.startswith("/usr/share/icons/hicolor/"): + has_hicolor = True + if not has_hicolor: + continue + requires = filehdr[rpm.RPMTAG_REQUIRES] + has_hicolor = False + for r in requires: + if r.count("hicolor-icon-theme"): + has_hicolor = True + if not has_hicolor: + self.has_failed("As %s ships icons in the hicolor directory, it should have \"Requires: hicolor-icon-theme\"\n https://www.redhat.com/archives/fedora-extras-list/2006-September/msg00282.html" % self.target.name) + self.target.total_checks += 1 + + +class CheckOptFlags(QACheck): + """checks if the rpm honors the compiler flags""" + + order = 1250 + + def check(self): + pr("Checking compiler flags", 2) + if not self.check_built(): + return + if self.target.arch == "noarch": + return + optflags = commands.getoutput("rpm --eval '%{optflags}'") + buildlog = open(self.target.reportDir+"/build.log", "r") + flags_count = 0 + for line in buildlog: + if line.count(optflags): + flags_count += 1 + buildlog.close() + if flags_count <= 3: # 3 because of the definitions by the %configure macro + self.has_failed("Does not seem to obey the compiler flags\n (wiki: Packaging/Guidelinesi#CompilerFlags)") + self.target.total_checks += 1 + +class CheckStaticLibs(QACheck): + """checks the presence of statically-linked libraries""" + + order = 1260 + + def check(self): + pr("Checking static libs", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filelist = filehdr[rpm.RPMTAG_FILENAMES] + for f in filelist: + if os.path.dirname(f) == "/usr/lib/" and f.endswith(".a"): + self.has_failed("The package contains static libraries\n (wiki: Packaging/Guidelines#StaticLinkage)") + return + self.target.total_checks += 1 + +class CheckConfigFilesLocation(QACheck): + """checks config files location""" + + order = 1270 + + def check(self): + pr("Checking config files location", 2) + if not self.check_built(): + return + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filenames = filehdr[rpm.RPMTAG_FILENAMES] + filetags = filehdr[rpm.RPMTAG_FILEFLAGS] + files = zip(filenames, filetags) + for filename, filetag in files: + if filetag & rpm.RPMFILE_CONFIG: + if filename.startswith("/usr"): + self.has_failed("The file '%s' is flagged as %%config and is in /usr\n (wiki: Packaging/Guidelines#Config)" % filename) + self.target.total_checks += 1 + +class CheckInitScripts(QACheck): + """checks init scripts""" + + order = 1280 + + def check(self): + pr("Checking init scripts", 2) + if not self.check_built(): + return + init_scripts_number = 0 + ts = rpm.ts() + for file in self.target.rpmfilenames: + filepath = self.target.reportDir+"/"+file + if not os.path.exists(filepath): + continue + filehdr = rpmUtils.miscutils.hdrFromPackage(ts, filepath) + filenames = filehdr[rpm.RPMTAG_FILENAMES] + filemodes = filehdr[rpm.RPMTAG_FILEMODES] + filetags = filehdr[rpm.RPMTAG_FILEFLAGS] + files = zip(filenames, filemodes, filetags) + for filename, filemode, filetag in files: + if not filename.startswith("/etc/rc.d/init.d/"): + continue + init_scripts_number += 1 + if filetag & rpm.RPMFILE_CONFIG: + self.has_failed("The init script '%s' is flagged as %%config\n (wiki: Packaging/Guidelines#Init)" % filename) + if stat.S_IMODE(filemode) != 493: # mode 755 : stat.S_IRWXU + stat.S_IRGRP + stat.S_IXGRP + stat.S_IROTH + stat.S_IXOTH + self.has_failed("The init script '%s' does not have mode 755\n (wiki: Packaging/Guidelines#Init)" % filename) + self.target.total_checks += init_scripts_number + + + + + + + + +########################## + +# Class for Bugzilla + +class QABugError(QAError): pass + +class QABug: + """These objects are the Bugzilla bugs from Fedora Extras or Livna.org""" + + bugID = None + webpage = None + + def __init__(self, bugID, origin="fedora"): + try: + self.bugID = int(bugID) + except ValueError: + raise QABugError("This does not look like a bug ID, and I can't find this SRPM.") + self.bugzillaURL = 'https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=%d' + if origin == "livna": + self.bugzillaURL = 'http://bugzilla.livna.org/show_bug.cgi?id=%d' + + def get_srpm(self, local=False): + '''Gets the SRPM from the web''' + srpmURL = self.get_srpm_url() + filename = os.path.basename(srpmURL) + if local and os.path.exists(filename): + return filename + elif local and not os.path.exists(filename): + pr("File %s not found, downloading..." % filename) + pr("Downloading SRPM from: %s" % srpmURL) + try: + urlgrabber.urlgrab(srpmURL, reget='check_timestamp', progress_obj=urlgrabber.progress.TextMeter(fo=sys.stdout)) + except urlgrabber.grabber.URLGrabError, e: + pr("ERROR: can't find SRPM") + sys.exit(1) + return filename + + def get_srpm_url(self): + '''Parses bugzilla to get the SRPM URL''' + if not self.webpage: + try: + self.webpage = urlgrabber.urlread(self.bugzillaURL % self.bugID) + except urlgrabber.grabber.URLGrabError: + raise QABugError("Can't access bugzilla, please download the srpm manually") + srpmList = re.compile('"((ht|f)tp(s)?://.*?\.src\.rpm)"', re.IGNORECASE).findall(self.webpage) + if srpmList == []: + raise QABugError("no srpm found in page, please download it manually") + srpmURL = srpmList[-1][0] # Use the last one. We could also use the newer one with rpmUtils.miscutils.compareEVR() + if not srpmURL: + errormsg = "impossible to find the srpm in the page, " \ + +"please download it manually.\n%s" % srpmList + raise QABugError(errormsg) + self.filename = os.path.basename(srpmURL) + pr("SRPM URL: "+srpmURL, 2) + return srpmURL + + def get_subject(self): + '''Parses bugzilla to get the Subject''' + if not self.webpage: + try: + self.webpage = urlgrabber.urlread(self.bugzillaURL % self.bugID) + except urlgrabber.grabber.URLGrabError: + return "" # that's not a blocking error. + subjectList = re.compile('Bug %s: (Review Request: )?(.*)' % self.bugID, re.IGNORECASE).findall(self.webpage) + if subjectList == []: + return "" + subject = subjectList[0][1] + if not subject: + return "" + pr("Found subject: "+subject, 2) + return subject + + +############################### + +## MAIN + + +if __name__ == "__main__": + print "Checking for updated guidelines on the wiki..." + check_requires(REQUIRES) + #pr("Checking for updated guidelines...", 2) + check_guidelines(GUIDELINES) + try: + conf = parse_config() + # get options and arguments + arg, origin, debug, do_checks, local = parse_options() + if do_checks: + for do_check in do_checks: + if do_check not in list_checks(): + print "ERROR: %s is not an available check" % do_check + sys.exit(1) + if not arg.endswith("src.rpm"): + bug = QABug(arg, origin) + print "Starting QA for bug %s (%s)" % (arg, bug.get_subject()) + srpmFileName = bug.get_srpm(local) + else: + print "Starting QA for local srpm %s" % arg + srpmFileName = arg + try: + srpm = QAPackage(srpmFileName, origin, conf["reportsdir"]) + except QAPackageError, e: + print "ERROR: %s" % e + sys.exit(1) + pr("Installing SRPM", 2) + srpm.installSRPM() + srpm.getReleaseFromSpec() + if do_checks: + for do_check in do_checks: + check = eval(do_check)(srpm) + srpm.addCheck(check) + else: + for name in list_checks(): + check = eval(name)(srpm) + srpm.addCheck(check) + srpm.runChecks() + if not do_checks: # We only save reports on full run (with all checks) + srpm.saveReport() + srpm.printReport() + except QABugError, e: + print "ERROR: %s" % e + sys.exit(1) + except KeyboardInterrupt: + print "Interrupted by user" + sys.exit(1) + + + +# vim: set expandtab tabstop=4 shiftwidth=4 : + -- cgit