438134759771fc434eebe9b697610d51e4bde136
[platform/upstream/rpmlint.git] / TagsCheck.py
1 # -*- coding: utf-8 -*-
2 #############################################################################
3 # File          : TagsCheck.py
4 # Package       : rpmlint
5 # Author        : Frederic Lepied
6 # Created on    : Tue Sep 28 00:03:24 1999
7 # Version       : $Id: TagsCheck.py 1892 2011-11-23 20:21:05Z scop $
8 # Purpose       : Check a package to see if some rpm tags are present
9 #############################################################################
10
11 import calendar
12 import os
13 import re
14 import time
15 try:
16     from urlparse import urlparse
17 except ImportError: # Python 3
18     from urllib.parse import urlparse
19
20 import rpm
21
22 from Filter import addDetails, printError, printInfo, printWarning
23 import AbstractCheck
24 import Config
25 import FilesCheck
26 import Pkg
27
28 _use_enchant = Config.getOption("UseEnchant", None)
29 if _use_enchant or _use_enchant is None:
30     try:
31         import enchant
32         import enchant.checker
33     except ImportError:
34         enchant = None
35 else:
36     enchant = None
37 del _use_enchant
38
39 DEFAULT_VALID_LICENSES = (
40     # OSI approved licenses, http://www.opensource.org/licenses/ (unversioned,
41     # trailing "license" dropped based on fuzzy logic, and in well-known cases,
42     # the abbreviation used instead of the full name, but list kept sorted by
43     # the full name).  Updated 2010-02-01.
44     'Academic Free License',
45     'Adaptive Public License',
46     'AGPLv3', # Affero GNU Public License
47     'AGPLv3+', # Affero GNU Public License
48     'Apache License',
49     'Apache Software License',
50     'Apple Public Source License',
51     'Artistic',
52     'Attribution Assurance License',
53     'BSD',
54     'Boost Software License',
55     'Computer Associates Trusted Open Source License',
56     'CDDL', # Common Development and Distribution License
57     'Common Public Attribution License',
58     'CUA Office Public License',
59     'EU DataGrid Software License',
60     'Eclipse Public License',
61     'Educational Community License',
62     'Eiffel Forum License',
63     'Entessa Public License',
64     'European Union Public License',
65     'Fair License',
66     'Frameworx License',
67     'GPLv1',
68     'GPLv1+',
69     'GPLv2',
70     'GPLv2+',
71     'GPLv3',
72     'GPLv3+',
73     'LGPLv2',
74     'LGPLv2+',
75     'LGPLv3',
76     'LGPLv3+',
77     'Historical Permission Notice and Disclaimer',
78     'IBM Public License',
79     'IPA Font License',
80     'ISC License',
81     'Lucent Public License',
82     'Microsoft Public License',
83     'Microsoft Reciprocal License',
84     'MirOS License',
85     'MIT',
86     'Motosoto License',
87     'MPL', # Mozilla Public License
88     'Multics License',
89     'NASA Open Source Agreement',
90     'Naumen Public License',
91     'Nethack General Public License',
92     'Nokia Open Source License',
93     'Non-profit Open Software License',
94     'NTP License',
95     'OCLC Research Public License',
96     'OFL', # Open Font License
97     'Open Group Test Suite License',
98     'Open Software License',
99     'PHP License',
100     'Python license', # CNRI Python License
101     'Python Software Foundation License',
102     'QPL', # Qt Public License
103     'RealNetworks Public Source License',
104     'Reciprocal Public License',
105     'Ricoh Source Code Public License',
106     'Simple Public License',
107     'Sleepycat License',
108     'Sun Public License',
109     'Sybase Open Watcom Public License',
110     'University of Illinois/NCSA Open Source License',
111     'Vovida Software License',
112     'W3C License',
113     'wxWindows Library License',
114     'X.Net License',
115     'Zope Public License',
116     'zlib/libpng License',
117     # Creative commons licenses, http://creativecommons.org/licenses/:
118     'Creative Commons Attribution',
119     'Creative Commons Attribution-NoDerivs',
120     'Creative Commons Attribution-NonCommercial-NoDerivs',
121     'Creative Commons Attribution-NonCommercial',
122     'Creative Commons Attribution-NonCommercial-ShareAlike',
123     'Creative Commons Attribution-ShareAlike',
124     # Others:
125     'Design Public License', # ???
126     'GFDL', # GNU Free Documentation License
127     'LaTeX Project Public License',
128     'OpenContent License',
129     'Open Publication License',
130     'Public Domain',
131     'Ruby License',
132     'SIL Open Font License',
133     # Non open source licences:
134     'Charityware',
135     'Commercial',
136     'Distributable',
137     'Freeware',
138     'Non-distributable',
139     'Proprietary',
140     'Shareware',
141     )
142
143 BAD_WORDS = {
144     'alot': 'a lot',
145     'accesnt': 'accent',
146     'accelleration': 'acceleration',
147     'accessable': 'accessible',
148     'accomodate': 'accommodate',
149     'acess': 'access',
150     'acording': 'according',
151     'additionaly': 'additionally',
152     'adress': 'address',
153     'adresses': 'addresses',
154     'adviced': 'advised',
155     'albumns': 'albums',
156     'alegorical': 'allegorical',
157     'algorith': 'algorithm',
158     'allpication': 'application',
159     'altough': 'although',
160     'alows': 'allows',
161     'amoung': 'among',
162     'amout': 'amount',
163     'analysator': 'analyzer',
164     'ang': 'and',
165     'appropiate': 'appropriate',
166     'arraival': 'arrival',
167     'artifical': 'artificial',
168     'artillary': 'artillery',
169     'attemps': 'attempts',
170     'automatize': 'automate',
171     'automatized': 'automated',
172     'automatizes': 'automates',
173     'auxilliary': 'auxiliary',
174     'availavility': 'availability',
175     'availble': 'available',
176     'avaliable': 'available',
177     'availiable': 'available',
178     'backgroud': 'background',
179     'baloons': 'balloons',
180     'becomming': 'becoming',
181     'becuase': 'because',
182     'cariage': 'carriage',
183     'challanges': 'challenges',
184     'changable': 'changeable',
185     'charachters': 'characters',
186     'charcter': 'character',
187     'choosen': 'chosen',
188     'colorfull': 'colorful',
189     'comand': 'command',
190     'commerical': 'commercial',
191     'comminucation': 'communication',
192     'commoditiy': 'commodity',
193     'compability': 'compatibility',
194     'compatability': 'compatibility',
195     'compatable': 'compatible',
196     'compatibiliy': 'compatibility',
197     'compatibilty': 'compatibility',
198     'compleatly': 'completely',
199     'complient': 'compliant',
200     'compres': 'compress',
201     'containes': 'contains',
202     'containts': 'contains',
203     'contence': 'contents',
204     'continous': 'continuous',
205     'contraints': 'constraints',
206     'convertor': 'converter',
207     'convinient': 'convenient',
208     'cryptocraphic': 'cryptographic',
209     'deamon': 'daemon',
210     'debians': 'Debian\'s',
211     'decompres': 'decompress',
212     'definate': 'definite',
213     'definately': 'definitely',
214     'dependancies': 'dependencies',
215     'dependancy': 'dependency',
216     'dependant': 'dependent',
217     'developement': 'development',
218     'developped': 'developed',
219     'deveolpment': 'development',
220     'devided': 'divided',
221     'dictionnary': 'dictionary',
222     'diplay': 'display',
223     'disapeared': 'disappeared',
224     'dissapears': 'disappears',
225     'documentaion': 'documentation',
226     'docuentation': 'documentation',
227     'documantation': 'documentation',
228     'dont': 'don\'t',
229     'easilly': 'easily',
230     'ecspecially': 'especially',
231     'edditable': 'editable',
232     'editting': 'editing',
233     'eletronic': 'electronic',
234     'enchanced': 'enhanced',
235     'encorporating': 'incorporating',
236     'enlightnment': 'enlightenment',
237     'enterily': 'entirely',
238     'enviroiment': 'environment',
239     'environement': 'environment',
240     'excellant': 'excellent',
241     'exlcude': 'exclude',
242     'exprimental': 'experimental',
243     'extention': 'extension',
244     'failuer': 'failure',
245     'familar': 'familiar',
246     'fatser': 'faster',
247     'fetaures': 'features',
248     'forse': 'force',
249     'fortan': 'fortran',
250     'framwork': 'framework',
251     'fuction': 'function',
252     'fuctions': 'functions',
253     'functionnality': 'functionality',
254     'functonality': 'functionality',
255     'functionaly': 'functionally',
256     'futhermore': 'furthermore',
257     'generiously': 'generously',
258     'grahical': 'graphical',
259     'grahpical': 'graphical',
260     'grapic': 'graphic',
261     'guage': 'gauge',
262     'halfs': 'halves',
263     'heirarchically': 'hierarchically',
264     'helpfull': 'helpful',
265     'hierachy': 'hierarchy',
266     'hierarchie': 'hierarchy',
267     'howver': 'however',
268     'implemantation': 'implementation',
269     'incomming': 'incoming',
270     'incompatabilities': 'incompatibilities',
271     'indended': 'intended',
272     'indendation': 'indentation',
273     'independant': 'independent',
274     'informatiom': 'information',
275     'initalize': 'initialize',
276     'inofficial': 'unofficial',
277     'integreated': 'integrated',
278     'integrety': 'integrity',
279     'integrey': 'integrity',
280     'intendet': 'intended',
281     'interchangable': 'interchangeable',
282     'intermittant': 'intermittent',
283     'jave': 'java',
284     'langage': 'language',
285     'langauage': 'language',
286     'langugage': 'language',
287     'lauch': 'launch',
288     'lesstiff': 'lesstif',
289     'libaries': 'libraries',
290     'licenceing': 'licencing',
291     'loggin': 'login',
292     'logile': 'logfile',
293     'loggging': 'logging',
294     'mandrivalinux': 'Mandriva Linux',
295     'maintainance': 'maintenance',
296     'maintainence': 'maintenance',
297     'makeing': 'making',
298     'managable': 'manageable',
299     'manoeuvering': 'maneuvering',
300     'ment': 'meant',
301     'modulues': 'modules',
302     'monochromo': 'monochrome',
303     'multidimensionnal': 'multidimensional',
304     'navagating': 'navigating',
305     'nead': 'need',
306     'neccesary': 'necessary',
307     'neccessary': 'necessary',
308     'necesary': 'necessary',
309     'nescessary': 'necessary',
310     'noticable': 'noticeable',
311     'optionnal': 'optional',
312     'orientied': 'oriented',
313     'pacakge': 'package',
314     'pachage': 'package',
315     'packacge': 'package',
316     'packege': 'package',
317     'packge': 'package',
318     'pakage': 'package',
319     'particularily': 'particularly',
320     'persistant': 'persistent',
321     'plattform': 'platform',
322     'ploting': 'plotting',
323     'posible': 'possible',
324     'powerfull': 'powerful',
325     'prefered': 'preferred',
326     'prefferably': 'preferably',
327     'prepaired': 'prepared',
328     'princliple': 'principle',
329     'priorty': 'priority',
330     'proccesors': 'processors',
331     'proces': 'process',
332     'processsing': 'processing',
333     'processessing': 'processing',
334     'progams': 'programs',
335     'programers': 'programmers',
336     'programm': 'program',
337     'programms': 'programs',
338     'promps': 'prompts',
339     'pronnounced': 'pronounced',
340     'prononciation': 'pronunciation',
341     'pronouce': 'pronounce',
342     'protcol': 'protocol',
343     'protocoll': 'protocol',
344     'recieve': 'receive',
345     'recieved': 'received',
346     'redircet': 'redirect',
347     'regulamentations': 'regulations',
348     'remoote': 'remote',
349     'repectively': 'respectively',
350     'replacments': 'replacements',
351     'requiere': 'require',
352     'runnning': 'running',
353     'safly': 'safely',
354     'savable': 'saveable',
355     'searchs': 'searches',
356     'separatly': 'separately',
357     'seperate': 'separate',
358     'seperately': 'separately',
359     'seperatly': 'separately',
360     'serveral': 'several',
361     'setts': 'sets',
362     'similiar': 'similar',
363     'simliar': 'similar',
364     'speach': 'speech',
365     'standart': 'standard',
366     'staically': 'statically',
367     'staticly': 'statically',
368     'succesful': 'successful',
369     'succesfully': 'successfully',
370     'suplied': 'supplied',
371     'suport': 'support',
372     'suppport': 'support',
373     'supportin': 'supporting',
374     'synchonized': 'synchronized',
375     'syncronize': 'synchronize',
376     'syncronizing': 'synchronizing',
377     'syncronus': 'synchronous',
378     'syste': 'system',
379     'sythesis': 'synthesis',
380     'taht': 'that',
381     'throught': 'through',
382     'useable': 'usable',
383     'usefull': 'useful',
384     'usera': 'users',
385     'usetnet': 'Usenet',
386     'utilites': 'utilities',
387     'utillities': 'utilities',
388     'utilties': 'utilities',
389     'utiltity': 'utility',
390     'utitlty': 'utility',
391     'variantions': 'variations',
392     'varient': 'variant',
393     'verson': 'version',
394     'vicefersa': 'vice-versa',
395     'yur': 'your',
396     'wheter': 'whether',
397     'wierd': 'weird',
398     'xwindows': 'X'
399     }
400
401 DEFAULT_INVALID_REQUIRES = ('^is$', '^not$', '^owned$', '^by$', '^any$', '^package$', '^libsafe\.so\.')
402
403 VALID_GROUPS = Config.getOption('ValidGroups', None)
404 VALID_DOMAINS = Config.getOption('ValidDomains', None)
405 VALID_SUBDOMAINS = Config.getOption('ValidSubDomains', None)
406
407 if VALID_GROUPS is None: # get defaults from rpm package only if it's not set
408     VALID_GROUPS = Pkg.get_default_valid_rpmgroups()
409 VALID_LICENSES = Config.getOption('ValidLicenses', DEFAULT_VALID_LICENSES)
410 INVALID_REQUIRES = map(re.compile, Config.getOption('InvalidRequires', DEFAULT_INVALID_REQUIRES))
411 packager_regex = re.compile(Config.getOption('Packager'))
412 changelog_version_regex = re.compile('[^>]([^ >]+)\s*$')
413 changelog_text_version_regex = re.compile('^\s*-\s*((\d+:)?[\w\.]+-[\w\.]+)')
414 release_ext = Config.getOption('ReleaseExtension')
415 extension_regex = release_ext and re.compile(release_ext)
416 use_version_in_changelog = Config.getOption('UseVersionInChangelog', True)
417 devel_number_regex = re.compile('(.*?)([0-9.]+)(_[0-9.]+)?-devel')
418 lib_devel_number_regex = re.compile('^lib(.*?)([0-9.]+)(_[0-9.]+)?-devel')
419 invalid_url_regex = re.compile(Config.getOption('InvalidURL'), re.IGNORECASE)
420 lib_package_regex = re.compile('(?:^(?:compat-)?lib.*?(\.so.*)?|libs?[\d-]*)$', re.IGNORECASE)
421 leading_space_regex = re.compile('^\s+')
422 pkg_config_regex = re.compile('^/usr/(?:lib\d*|share)/pkgconfig/')
423 license_regex = re.compile('\(([^)]+)\)|\s(?:and|or)\s')
424 invalid_version_regex = re.compile('([0-9](?:rc|alpha|beta|pre).*)', re.IGNORECASE)
425 # () are here for grouping purpose in the regexp
426 forbidden_words_regex = re.compile('(' + Config.getOption('ForbiddenWords') + ')', re.IGNORECASE)
427 valid_buildhost_regex = re.compile(Config.getOption('ValidBuildHost'))
428 valid_filedep_regex=re.compile('(?:/s?bin/|^/etc/|^/usr/lib/sendmail$)')
429 use_epoch = Config.getOption('UseEpoch', False)
430 use_utf8 = Config.getOption('UseUTF8', Config.USEUTF8_DEFAULT)
431 max_line_len = Config.getOption('MaxLineLength', 79)
432 tag_regex = re.compile('^((?:Auto(?:Req|Prov|ReqProv)|Build(?:Arch(?:itectures)?|Root)|(?:Build)?Conflicts|(?:Build)?(?:Pre)?Requires|Copyright|(?:CVS|SVN)Id|Dist(?:ribution|Tag|URL)|DocDir|(?:Build)?Enhances|Epoch|Exclu(?:de|sive)(?:Arch|OS)|Group|Icon|License|Name|No(?:Patch|Source)|Obsoletes|Packager|Patch\d*|Prefix(?:es)?|Provides|(?:Build)?Recommends|Release|RHNPlatform|Serial|Source\d*|(?:Build)?Suggests|Summary|(?:Build)?Supplements|(?:Bug)?URL|Vendor|Version)(?:\([^)]+\))?:)\s*\S', re.IGNORECASE)
433 punct = '.,:;!?'
434 sentence_break_regex = re.compile(r'(^|[.:;!?])\s*$')
435 so_dep_regex = re.compile(r'\.so(\.[0-9a-zA-z]+)*(\([^)]*\))*$')
436 # we assume that no rpm packages existed before rpm itself existed...
437 oldest_changelog_timestamp = calendar.timegm(time.strptime("1995-01-01", "%Y-%m-%d"))
438
439 _enchant_checkers = {}
440 def spell_check(pkg, str, fmt, lang, ignored):
441
442     dict_found = True
443     warned = set()
444     if enchant:
445         if lang == 'C':
446             lang = 'en_US'
447
448         checker = _enchant_checkers.get(lang)
449         if not checker and lang not in _enchant_checkers:
450             try:
451                 checker = enchant.checker.SpellChecker(
452                     lang, filters = [ enchant.tokenize.EmailFilter,
453                                       enchant.tokenize.URLFilter,
454                                       enchant.tokenize.WikiWordFilter ])
455             except enchant.DictNotFoundError:
456                 printInfo(pkg, 'enchant-dictionary-not-found', lang)
457                 pass
458             _enchant_checkers[lang] = checker
459
460         if checker:
461             # squeeze whitespace to ease leading context check
462             checker.set_text(re.sub(r'\s+', ' ', str))
463             uppername = pkg.name.upper()
464             upperparts = uppername.split('-')
465             if lang.startswith('en'):
466                 ups = [x + "'S" for x in upperparts]
467                 upperparts.extend(ups)
468             for err in checker:
469
470                 # Skip already warned and ignored words
471                 if err.word in warned or err.word in ignored:
472                     continue
473
474                 # Skip all capitalized words that do not start a sentence
475                 if err.word[0].isupper() and not \
476                         sentence_break_regex.search(checker.leading_context(3)):
477                     continue
478
479                 upperword = err.word.upper()
480
481                 # Skip all uppercase words
482                 if err.word == upperword:
483                     continue
484
485                 # Skip errors containing package name or equal to a
486                 # "component" of it, case insensitively
487                 if uppername in upperword or upperword in upperparts:
488                     continue
489
490                 # Work around enchant's digit tokenizing behavior:
491                 # http://github.com/rfk/pyenchant/issues/issue/3
492                 if checker.leading_context(1).isdigit() or \
493                         checker.trailing_context(1).isdigit():
494                     continue
495
496                 # Warn and suggest
497                 sug = ', '.join(checker.suggest()[:3])
498                 if sug:
499                     sug = '-> %s' % sug
500                 printWarning(pkg, 'spelling-error', fmt % lang, err.word, sug)
501                 warned.add(err.word)
502
503         else:
504             dict_found = False
505
506     if not enchant or not dict_found:
507         for seq in str.split():
508             for word in re.split('[^a-z]+', seq.lower()):
509                 if len(word) == 0:
510                     continue
511                 correct = BAD_WORDS.get(word)
512                 if not correct:
513                     continue
514                 if word[0] == '\'':
515                     word = word[1:]
516                 if word[-1] == '\'':
517                     word = word[:-1]
518                 if word in warned or word in ignored:
519                     continue
520                 printWarning(pkg, 'spelling-error', fmt % lang, word, '->',
521                              correct)
522                 warned.add(word)
523
524
525 class TagsCheck(AbstractCheck.AbstractCheck):
526
527     def __init__(self):
528         AbstractCheck.AbstractCheck.__init__(self, 'TagsCheck')
529
530     def _unexpanded_macros(self, pkg, tagname, value, is_url=False):
531         if not value:
532             return
533         for match in AbstractCheck.macro_regex.findall(str(value)):
534             # Do not warn about %XX URL escapes
535             if is_url and re.match('^%[0-9A-F][0-9A-F]$', match, re.I):
536                 continue
537             printWarning(pkg, 'unexpanded-macro', tagname, match)
538
539     def check(self, pkg):
540
541         packager = pkg[rpm.RPMTAG_PACKAGER]
542         self._unexpanded_macros(pkg, 'Packager', packager)
543         if not packager:
544             printError(pkg, 'no-packager-tag')
545         elif Config.getOption('Packager') and \
546                 not packager_regex.search(packager):
547             printWarning(pkg, 'invalid-packager', packager)
548
549         version = pkg[rpm.RPMTAG_VERSION]
550         self._unexpanded_macros(pkg, 'Version', version)
551         if not version:
552             printError(pkg, 'no-version-tag')
553         else:
554             res = invalid_version_regex.search(version)
555             if res:
556                 printError(pkg, 'invalid-version', version)
557
558         release = pkg[rpm.RPMTAG_RELEASE]
559         self._unexpanded_macros(pkg, 'Release', release)
560         if not release:
561             printError(pkg, 'no-release-tag')
562         elif release_ext and not extension_regex.search(release):
563             printWarning(pkg, 'not-standard-release-extension', release)
564
565         epoch = pkg[rpm.RPMTAG_EPOCH]
566         if epoch is None:
567             if use_epoch:
568                 printError(pkg, 'no-epoch-tag')
569         else:
570             if epoch > 99:
571                 printWarning(pkg, 'unreasonable-epoch', epoch)
572             epoch = str(epoch)
573
574         if use_epoch:
575             for o in (x for x in pkg.obsoletes() if x[1] and x[2][0] is None):
576                 printWarning(pkg, 'no-epoch-in-obsoletes',
577                              apply(Pkg.formatRequire, o))
578             for c in (x for x in pkg.conflicts() if x[1] and x[2][0] is None):
579                 printWarning(pkg, 'no-epoch-in-conflicts',
580                              apply(Pkg.formatRequire, c))
581             for p in (x for x in pkg.provides() if x[1] and x[2][0] is None):
582                 printWarning(pkg, 'no-epoch-in-provides',
583                              apply(Pkg.formatRequire, p))
584
585         name = pkg.name
586         deps = pkg.requires() + pkg.prereq()
587         devel_depend = False
588         is_devel = FilesCheck.devel_regex.search(name)
589         is_source = pkg.isSource()
590         for d in deps:
591             value = apply(Pkg.formatRequire, d)
592             if use_epoch and d[1] and d[2][0] is None \
593                     and d[0][0:7] != 'rpmlib(':
594                 printWarning(pkg, 'no-epoch-in-dependency', value)
595             for r in INVALID_REQUIRES:
596                 if r.search(d[0]):
597                     printError(pkg, 'invalid-dependency', d[0])
598
599             if d[0].startswith('/usr/local/'):
600                 printError(pkg, 'invalid-dependency', d[0])
601
602             if d[0].startswith('/') and not valid_filedep_regex.search(d[0]):
603                 printWarning(pkg, 'invalid-filepath-dependency', d[0])
604
605             if not devel_depend and not is_devel and not is_source and \
606                     FilesCheck.devel_regex.search(d[0]):
607                 printError(pkg, 'devel-dependency', d[0])
608                 devel_depend = True
609             if is_source and lib_devel_number_regex.search(d[0]):
610                 printError(pkg, 'invalid-build-requires', d[0])
611             if not is_source and not is_devel:
612                 res = lib_package_regex.search(d[0])
613                 if res and not res.group(1) and not d[1]:
614                     printError(pkg, 'explicit-lib-dependency', d[0])
615             if d[1] == rpm.RPMSENSE_EQUAL and d[2][2] is not None:
616                 printWarning(pkg, 'requires-on-release', value)
617             self._unexpanded_macros(pkg, 'dependency %s' % (value,), value)
618
619         self._unexpanded_macros(pkg, 'Name', name)
620         if not name:
621             printError(pkg, 'no-name-tag')
622         else:
623             if is_devel and not is_source:
624                 base = is_devel.group(1)
625                 dep = None
626                 has_so = False
627                 has_pc = False
628                 for fname in pkg.files():
629                     if fname.endswith('.so'):
630                         has_so = True
631                     if pkg_config_regex.match(fname) and fname.endswith('.pc'):
632                         has_pc = True
633                 if has_so:
634                     base_or_libs = base + '*/' + base + '-libs/lib' + base + '*'
635                     # try to match *%_isa as well (e.g. "(x86-64)", "(x86-32)")
636                     base_or_libs_re = re.compile(
637                         '^(lib)?%s(-libs)?[\d_]*(\(\w+-\d+\))?' % re.escape(base))
638                     for d in deps:
639                         if base_or_libs_re.match(d[0]):
640                             dep = d
641                             break
642                     if not dep:
643                         printWarning(pkg, 'no-dependency-on', base_or_libs)
644                     elif version:
645                         exp = (epoch, version, None)
646                         sexp = Pkg.versionToString(exp)
647                         if not dep[1]:
648                             printWarning(pkg, 'no-version-dependency-on',
649                                          base_or_libs, sexp)
650                         elif dep[2][:2] != exp[:2]:
651                             printWarning(pkg,
652                                          'incoherent-version-dependency-on',
653                                          base_or_libs,
654                                          Pkg.versionToString((dep[2][0],
655                                                               dep[2][1], None)),
656                                          sexp)
657                     res = devel_number_regex.search(name)
658                     if not res:
659                         printWarning(pkg, 'no-major-in-name', name)
660                     else:
661                         if res.group(3):
662                             prov = res.group(1) + res.group(2) + '-devel'
663                         else:
664                             prov = res.group(1) + '-devel'
665
666                         if prov not in (x[0] for x in pkg.provides()):
667                             printWarning(pkg, 'no-provides', prov)
668
669                 if has_pc:
670                     found_pkg_config_dep = False
671                     for p in (x[0] for x in pkg.provides()):
672                         if (p.startswith("pkgconfig(")):
673                             found_pkg_config_dep = True
674                             break
675                     if not found_pkg_config_dep:
676                         printWarning(pkg, 'no-pkg-config-provides')
677
678         # List of words to ignore in spell check
679         ignored_words = set()
680         for pf in pkg.files():
681             ignored_words.update(pf.split('/'))
682         ignored_words.update((x[0] for x in pkg.provides()))
683         ignored_words.update((x[0] for x in pkg.requires()))
684         ignored_words.update((x[0] for x in pkg.conflicts()))
685         ignored_words.update((x[0] for x in pkg.obsoletes()))
686
687         summary = pkg[rpm.RPMTAG_SUMMARY]
688         if not summary:
689             printError(pkg, 'no-summary-tag')
690         else:
691             if not pkg[rpm.RPMTAG_HEADERI18NTABLE]:
692                 self._unexpanded_macros(pkg, 'Summary', summary)
693             else:
694                 for lang in pkg[rpm.RPMTAG_HEADERI18NTABLE]:
695                     self.check_summary(pkg, lang, ignored_words)
696
697         description = pkg[rpm.RPMTAG_DESCRIPTION]
698         if not description:
699             printError(pkg, 'no-description-tag')
700         else:
701             if len(pkg[rpm.RPMTAG_DESCRIPTION].partition('Authors:')[0])-4 < len(pkg[rpm.RPMTAG_SUMMARY]):
702                 printWarning(pkg, 'description-shorter-than-summary')
703
704             if not pkg[rpm.RPMTAG_HEADERI18NTABLE]:
705                 self._unexpanded_macros(pkg, '%description', description)
706             else:
707                 for lang in pkg[rpm.RPMTAG_HEADERI18NTABLE]:
708                     self.check_description(pkg, lang, ignored_words)
709
710         valid_groups = VALID_GROUPS
711         app_groups = ()
712         for d in VALID_DOMAINS:
713             if d == 'Applications':
714                 for dd in ['Multimedia', 'Social', 'Web', 'Telephony', 'Messaging', 'PIM', 'Network', 'Navigation', 'Other', 'Game', 'Tasks', 'Music', 'Photo', 'Video']:
715                     app_groups = app_groups + ("%s/%s" %(d,dd), )
716                 continue
717             for sd in VALID_SUBDOMAINS:
718                 valid_groups = valid_groups + ("%s/%s" %(d,sd), )
719
720         valid_groups = valid_groups + app_groups
721
722         group = pkg[rpm.RPMTAG_GROUP]
723         self._unexpanded_macros(pkg, 'Group', group)
724         if group:
725             for p in ['TBD', 'TO BE', 'FILLED', 'Unspecified', 'TO_BE' ]:
726                 if p in group:
727                     printWarning(pkg, 'group-placeholder-not-allowed', group)
728         if not group:
729             printError(pkg, 'no-group-tag')
730         elif pkg.name.endswith('-devel') and not group.startswith('Development/') and not group.endswith('/Development'):
731             printWarning(pkg, 'devel-package-with-non-devel-group', group)
732         elif group not in valid_groups:
733             printWarning(pkg, 'non-standard-group', group)
734
735         buildhost = pkg[rpm.RPMTAG_BUILDHOST]
736         self._unexpanded_macros(pkg, 'BuildHost', buildhost)
737         if not buildhost:
738             printError(pkg, 'no-buildhost-tag')
739         elif Config.getOption('ValidBuildHost') and \
740                 not valid_buildhost_regex.search(buildhost):
741             printWarning(pkg, 'invalid-buildhost', buildhost)
742
743         changelog = pkg[rpm.RPMTAG_CHANGELOGNAME]
744         if not changelog:
745             printError(pkg, 'no-changelogname-tag')
746         else:
747             clt = pkg[rpm.RPMTAG_CHANGELOGTEXT]
748             if use_version_in_changelog:
749                 ret = changelog_version_regex.search(changelog[0])
750                 if not ret and clt:
751                     # we also allow the version specified as the first
752                     # thing on the first line of the text
753                     ret = changelog_text_version_regex.search(clt[0])
754                 if not ret:
755                     printWarning(pkg, 'no-version-in-last-changelog')
756                 elif version and release:
757                     srpm = pkg[rpm.RPMTAG_SOURCERPM] or ''
758                     # only check when source name correspond to name
759                     if srpm[0:-8] == '%s-%s-%s' % (name, version, release):
760                         expected = [version + '-' + release]
761                         if epoch is not None: # regardless of use_epoch
762                             expected[0] = str(epoch) + ':' + expected[0]
763                         # Allow EVR in changelog without release extension,
764                         # the extension is often a macro or otherwise dynamic.
765                         if release_ext:
766                             expected.append(
767                                 extension_regex.sub('', expected[0]))
768                         if ret.group(1) not in expected:
769                             if len(expected) == 1:
770                                 expected = expected[0]
771                             printWarning(pkg, 'incoherent-version-in-changelog',
772                                          ret.group(1), expected)
773
774             if clt:
775                 changelog = changelog + clt
776             if use_utf8 and not Pkg.is_utf8_str(' '.join(changelog)):
777                 printError(pkg, 'tag-not-utf8', '%changelog')
778
779             clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0]
780             if clt:
781                 clt -= clt % (24*3600) # roll back to 00:00:00, see #246
782                 if clt < oldest_changelog_timestamp:
783                     printWarning(pkg, 'changelog-time-overflow',
784                                  time.strftime("%Y-%m-%d", time.gmtime(clt)))
785                 elif clt > time.time():
786                     printError(pkg, 'changelog-time-in-future',
787                                  time.strftime("%Y-%m-%d", time.gmtime(clt)))
788
789 #         for provide_name in (x[0] for x in pkg.provides()):
790 #             if name == provide_name:
791 #                 printWarning(pkg, 'package-provides-itself')
792 #                 break
793
794         def split_license(license):
795             return (x.strip() for x in
796                     (l for l in license_regex.split(license) if l))
797
798         rpm_license = pkg[rpm.RPMTAG_LICENSE]
799         if not rpm_license:
800             printError(pkg, 'no-license')
801         else:
802             valid_license = True
803             for p in ['TBD', 'TO BE', 'FILLED', 'Unspecified', 'TO_BE', 'TIZEN', 'samsung', 'Samsung'  ]:
804                 if p in rpm_license:
805                     printWarning(pkg, 'license-placeholder-not-allowed', rpm_license)
806                     valid_license = False
807                     break
808             if valid_license and rpm_license not in VALID_LICENSES:
809                 for l1 in split_license(rpm_license):
810                     if l1 in VALID_LICENSES:
811                         continue
812                     for l2 in split_license(l1):
813                         if l2 not in VALID_LICENSES:
814                             printWarning(pkg, 'invalid-license', l2)
815                             valid_license = False
816
817
818             if not valid_license:
819                 self._unexpanded_macros(pkg, 'License', rpm_license)
820
821         for tag in ('URL', 'BugURL'):
822             if hasattr(rpm, 'RPMTAG_%s' % tag.upper()):
823                 url = pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())]
824                 self._unexpanded_macros(pkg, tag, url, is_url = True)
825                 if url:
826                     (scheme, netloc) = urlparse(url)[0:2]
827                     if not scheme or not netloc or "." not in netloc or \
828                             scheme not in ('http', 'https', 'ftp') or \
829                             (Config.getOption('InvalidURL') and \
830                              invalid_url_regex.search(url)):
831                         printWarning(pkg, 'invalid-url', tag, url)
832                     else:
833                         self.check_url(pkg, tag, url)
834                 elif tag == 'URL':
835                     printWarning(pkg, 'no-url-tag')
836
837         obs_names = [x[0] for x in pkg.obsoletes()]
838         prov_names = [x[0].split(':/')[0] for x in pkg.provides()]
839         conf_names = map(lambda x: x[0].split(':/')[0], pkg.conflicts())
840
841         for o in (x for x in obs_names if x not in prov_names):
842             printWarning(pkg, 'obsolete-not-provided', o)
843         for o in pkg.obsoletes():
844             value = apply(Pkg.formatRequire, o)
845             self._unexpanded_macros(pkg, 'Obsoletes %s' % (value,), value)
846
847         # TODO: should take versions, <, <=, =, >=, > into account here
848         #       https://bugzilla.redhat.com/460872
849         useless_provides = []
850         for p in prov_names:
851             if p in conf_names:
852                 printWarning(pkg, 'conflicts-with-provides', p)
853             if prov_names.count(p) != 1 and p not in useless_provides:
854                 useless_provides.append(p)
855         for p in useless_provides:
856             printError(pkg, 'useless-provides', p)
857
858         for p in pkg.provides():
859             value = apply(Pkg.formatRequire, p)
860             self._unexpanded_macros(pkg, 'Provides %s' % (value,), value)
861
862         for c in pkg.conflicts():
863             value = apply(Pkg.formatRequire, c)
864             self._unexpanded_macros(pkg, 'Conflicts %s' % (value,), value)
865
866         obss = pkg.obsoletes()
867         if obss:
868             provs = pkg.provides()
869             for prov in provs:
870                 for obs in obss:
871                     if Pkg.rangeCompare(obs, prov):
872                         printWarning(pkg, 'self-obsoletion', '%s obsoletes %s' %
873                                      (apply(Pkg.formatRequire, obs),
874                                       apply(Pkg.formatRequire, prov)))
875
876         for tag in ('Distribution', 'DistTag', 'ExcludeArch', 'ExcludeOS',
877                     'Vendor'):
878             if hasattr(rpm, 'RPMTAG_%s' % tag.upper()):
879                 self._unexpanded_macros(
880                     pkg, tag, pkg[getattr(rpm, 'RPMTAG_%s' % tag.upper())])
881
882
883     def check_description(self, pkg, lang, ignored_words):
884         description = pkg.langtag(rpm.RPMTAG_DESCRIPTION, lang)
885         self._unexpanded_macros(pkg, '%%description -l %s' % lang, description)
886         utf8desc = description
887         if use_utf8:
888             utf8desc = Pkg.to_utf8(description).decode('utf-8')
889         spell_check(pkg, utf8desc, '%%description -l %s', lang, ignored_words)
890         for l in utf8desc.splitlines():
891             if len(l) > max_line_len:
892                 printError(pkg, 'description-line-too-long', lang, l)
893             res = forbidden_words_regex.search(l)
894             if res and Config.getOption('ForbiddenWords'):
895                 printWarning(pkg, 'description-use-invalid-word', lang,
896                              res.group(1))
897             res = tag_regex.search(l)
898             if res:
899                 printWarning(pkg, 'tag-in-description', lang, res.group(1))
900         if use_utf8 and not Pkg.is_utf8_str(description):
901             printError(pkg, 'tag-not-utf8', '%description', lang)
902
903     def check_summary(self, pkg, lang, ignored_words):
904         summary = pkg.langtag(rpm.RPMTAG_SUMMARY, lang)
905         self._unexpanded_macros(pkg, 'Summary(%s)' % lang, summary)
906         utf8summary = summary
907         if use_utf8:
908             utf8summary = Pkg.to_utf8(summary).decode('utf-8')
909         spell_check(pkg, utf8summary, 'Summary(%s)', lang, ignored_words)
910         if '\n' in summary:
911             printError(pkg, 'summary-on-multiple-lines', lang)
912         if summary[0] != summary[0].upper() and not summary.startswith("openSUSE"):
913             printWarning(pkg, 'summary-not-capitalized', lang, summary)
914         if summary[-1] == '.':
915             printWarning(pkg, 'summary-ended-with-dot', lang, summary)
916         if len(utf8summary) > max_line_len:
917             printError(pkg, 'summary-too-long', lang, summary)
918         if leading_space_regex.search(summary):
919             printError(pkg, 'summary-has-leading-spaces', lang, summary)
920         res = forbidden_words_regex.search(summary)
921         if res and Config.getOption('ForbiddenWords'):
922             printWarning(pkg, 'summary-use-invalid-word', lang, res.group(1))
923         if pkg.name:
924             sepchars = '[\s' + punct + ']'
925             res = re.search('(?:^|\s)(%s)(?:%s|$)' %
926                             (re.escape(pkg.name), sepchars),
927                             summary, re.IGNORECASE | re.UNICODE)
928             if res:
929                 printWarning(pkg, 'name-repeated-in-summary', lang,
930                              res.group(1))
931         if use_utf8 and not Pkg.is_utf8_str(summary):
932             printError(pkg, 'tag-not-utf8', 'Summary', lang)
933
934
935 # Create an object to enable the auto registration of the test
936 check = TagsCheck()
937
938 # Add information about checks
939 addDetails(
940 'summary-too-long',
941 'The "Summary:" must not exceed %d characters.' % max_line_len,
942
943 'invalid-version',
944 '''The version string must not contain the pre, alpha, beta or rc suffixes
945 because when the final version will be out, you will have to use an Epoch tag
946 to make the package upgradable. Instead put it in the release tag, prefixed
947 with something you have control over.''',
948
949 'spelling-error',
950 '''The value of this tag appears to be misspelled. Please double-check.''',
951
952 'no-packager-tag',
953 '''There is no Packager tag in your package. You have to specify a packager
954 using the Packager tag. Ex: Packager: John Doe <john.doe@example.com>.''',
955
956 'invalid-packager',
957 '''The packager email must end with an email compatible with the Packager
958 option of rpmlint. Please change it and rebuild your package.''',
959
960 'no-version-tag',
961 '''There is no Version tag in your package. You have to specify a version using
962 the Version tag.''',
963
964 'no-release-tag',
965 '''There is no Release tag in your package. You have to specify a release using
966 the Release tag.''',
967
968 'not-standard-release-extension',
969 'Your release tag must match the regular expression ' + release_ext + '.',
970
971 'no-name-tag',
972 '''There is no Name tag in your package. You have to specify a name using the
973 Name tag.''',
974
975 'conflicts-with-provides',
976 '''The same symbolic name is provided and conflicted. This package might be
977 uninstallable, if versioning matches''',
978
979 'non-coherent-filename',
980 '''The file which contains the package should be named
981 <NAME>-<VERSION>-<RELEASE>.<ARCH>.rpm.''',
982
983 'no-dependency-on',
984 '''
985 ''',
986
987 'incoherent-version-dependency-on',
988 '''
989 ''',
990
991 'no-version-dependency-on',
992 '''
993 ''',
994
995 'no-major-in-name',
996 '''The major number of the library isn't included in the package's name.
997 ''',
998
999 'description-shorter-than-summary',
1000 '''The package description should be longer than the summary. be a bit more
1001 verbose, please.''',
1002
1003 'no-provides',
1004 '''Your library package doesn't provide the -devel name without the major
1005 version included.''',
1006
1007 'no-summary-tag',
1008 '''There is no Summary tag in your package. You have to describe your package
1009 using this tag. To insert it, just insert a tag 'Summary'.''',
1010
1011 'summary-on-multiple-lines',
1012 '''Your summary must fit on one line. Please make it shorter and rebuild the
1013 package.''',
1014
1015 'summary-not-capitalized',
1016 '''Summary doesn't begin with a capital letter.''',
1017
1018 'summary-ended-with-dot',
1019 '''Summary ends with a dot.''',
1020
1021 'summary-has-leading-spaces',
1022 '''Summary begins with whitespace which will waste space when displayed.''',
1023
1024 'no-description-tag',
1025 '''The description of the package is empty or missing. To add it, insert a
1026 %description section in your spec file, add a textual description of the
1027 package after it, and rebuild the package.''',
1028
1029 'description-line-too-long',
1030 '''Your description lines must not exceed %d characters. If a line is exceeding
1031 this number, cut it to fit in two lines.''' % max_line_len,
1032
1033 'tag-in-description',
1034 '''Something that looks like a tag was found in the package's description.
1035 This may indicate a problem where the tag was not actually parsed as a tag
1036 but just textual description content, thus being a no-op.  Verify if this is
1037 the case, and move the tag to a place in the specfile where %description
1038 won't fool the specfile parser, and rebuild the package.''',
1039
1040 'no-group-tag',
1041 '''There is no Group tag in your package. You have to specify a valid group
1042 in your spec file using the Group tag.''',
1043
1044 'devel-package-with-non-devel-group',
1045 '''The package ends with -devel but does not have a RPM group starting with
1046 Development/''',
1047
1048 'non-standard-group',
1049 '''The value of the Group tag in the package is not valid.  Valid groups are
1050 listed here: https://wiki.tizen.org/wiki/Packaging/Guidelines#Group_Tag''',
1051
1052 'group-placeholder-not-allowed',
1053 '''The value of the Group tag is a placeholder, please use a proper value.
1054 Valid groups are listed here:
1055 https://wiki.tizen.org/wiki/Packaging/Guidelines#Group_Tag
1056 ''',
1057
1058 'no-changelogname-tag',
1059 '''There is no changelog. Please insert a '%changelog' section heading in your
1060 spec file and prepare your changes file using e.g. the 'gbs ch' command.''',
1061
1062 'no-version-in-last-changelog',
1063 '''The latest changelog entry doesn't contain a version. Please insert the
1064 version that is coherent with the version of the package and rebuild it.''',
1065
1066 'incoherent-version-in-changelog',
1067 '''The latest entry in %changelog contains a version identifier that is not
1068 coherent with the epoch:version-release tuple of the package.''',
1069
1070 'changelog-time-overflow',
1071 '''The timestamp of the latest entry in %changelog is suspiciously far away in
1072 the past; it is possible that it is actually so much in the future that it
1073 has overflowed rpm's timestamp representation.''',
1074
1075 'changelog-time-in-future',
1076 '''The timestamp of the latest entry in %changelog is in the future.''',
1077
1078 'no-license',
1079 '''There is no License tag in your spec file. You have to specify one license
1080 for your program (eg. GPL). To insert this tag, just insert a 'License' in
1081 your specfile.''',
1082
1083 'invalid-license',
1084 '''The value of the License tag was not recognized.  Known values are:
1085 "%s".''' % '", "'.join(VALID_LICENSES),
1086
1087 'license-placeholder-not-allowed',
1088 '''The value of the License tag was not recognized and seems to be a placeholder.  Known values are:
1089 "%s".''' % '", "'.join(VALID_LICENSES),
1090
1091 'obsolete-not-provided',
1092 '''If a package is obsoleted by a compatible replacement, the obsoleted package
1093 should also be provided in order to not cause unnecessary dependency breakage.
1094 If the obsoleting package is not a compatible replacement for the old one,
1095 leave out the Provides.''',
1096
1097 'invalid-dependency',
1098 '''An invalid dependency has been detected. It usually means that the build of
1099 the package was buggy.''',
1100
1101 'no-epoch-tag',
1102 '''There is no Epoch tag in your package. You have to specify an epoch using the
1103 Epoch tag.''',
1104
1105 'unreasonable-epoch',
1106 '''The value of your Epoch tag is unreasonably large (> 99).''',
1107
1108 'no-epoch-in-obsoletes',
1109 '''Your package contains a versioned Obsoletes entry without an Epoch.''',
1110
1111 'no-epoch-in-conflicts',
1112 '''Your package contains a versioned Conflicts entry without an Epoch.''',
1113
1114 'no-epoch-in-provides',
1115 '''Your package contains a versioned Provides entry without an Epoch.''',
1116
1117 'no-epoch-in-dependency',
1118 '''Your package contains a versioned dependency without an Epoch.''',
1119
1120 'devel-dependency',
1121 '''Your package has a dependency on a devel package but it's not a devel
1122 package itself.''',
1123
1124 'invalid-build-requires',
1125 '''Your source package contains a dependency not compliant with the lib64
1126 naming. This BuildRequires dependency will not be resolved on lib64 platforms
1127 (eg. amd64).''',
1128
1129 'explicit-lib-dependency',
1130 '''You must let rpm find the library dependencies by itself. Do not put unneeded
1131 explicit Requires: tags.''',
1132
1133 'useless-provides',
1134 '''This package provides 2 times the same capacity. It should only provide it
1135 once.''',
1136
1137 'invalid-filepath-dependency',
1138 '''A package has a file or path based dependency that is not resolveable for
1139 package solvers because it is not in the whitelist for path based dependencies
1140 and therefore not available in repository metadata. Please use a symbolic requires
1141 instead or require a file in bin or /etc.''',
1142
1143 'tag-not-utf8',
1144 '''The character encoding of the value of this tag is not UTF-8.''',
1145
1146 'requires-on-release',
1147 '''This rpm requires a specific release of another package.''',
1148
1149 'no-url-tag',
1150 '''The URL tag is missing. Please add a http or ftp link to the project location.''',
1151
1152 'no-pkg-config-provides',
1153 '''The package installs a .pc file but does not provide pkgconfig(..) provides.
1154 The most likely reason for that is that it was built without BuildRequires: pkg-config.
1155 Please double check your build dependencies.''',
1156
1157 'name-repeated-in-summary',
1158 '''The name of the package is repeated in its summary.  This is often redundant
1159 information and looks silly in various programs' output.  Make the summary
1160 brief and to the point without including redundant information in it.''',
1161
1162 'enchant-dictionary-not-found',
1163 '''A dictionary for the Enchant spell checking library is not available for
1164 the language given in the info message.  Spell checking will proceed with
1165 rpmlint's built-in implementation for localized tags in this language.
1166 For better spell checking results in this language, install the appropriate
1167 dictionary that Enchant will use for this language, often for example
1168 hunspell-* or aspell-*.''',
1169
1170 'self-obsoletion',
1171 '''The package obsoletes itself.  This is known to cause errors in various
1172 tools and should thus be avoided, usually by using appropriately versioned
1173 Obsoletes and/or Provides and avoiding unversioned ones.''',
1174
1175 'unexpanded-macro',
1176 '''This tag contains something that looks like an unexpanded macro; this is
1177 often the sign of a misspelling. Please check your specfile.''',
1178
1179 'private-shared-object-provides',
1180 '''A shared object soname provides is provided by a file in a path from which
1181 other packages should not directly load shared objects from.  Such shared
1182 objects should thus not be depended on and they should not result in provides
1183 in the containing package.  Get rid of the provides if appropriate, for example
1184 by filtering it out during build.  Note that in some cases this may require
1185 disabling rpmbuild's internal dependency generator.''',
1186 )
1187
1188 # TagsCheck.py ends here
1189
1190 # Local variables:
1191 # indent-tabs-mode: nil
1192 # py-indent-offset: 4
1193 # End:
1194 # ex: ts=4 sw=4 et