pkgbuild.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env python
  2. import os
  3. import subprocess
  4. import re
  5. import tarfile
  6. import tempfile
  7. from collections import UserDict
  8. class InvalidPackage(Exception):
  9. pass
  10. class PKGBUILD(UserDict):
  11. """Representation of an Archlinux package"""
  12. def __init__(self, file):
  13. UserDict.__init__(self)
  14. self._required_fields = (
  15. 'name', 'description', 'version', 'release',
  16. 'licenses', 'arch',
  17. )
  18. self._validated = False
  19. self._is_valid = False
  20. self._errors = []
  21. self._warnings = []
  22. self.load(file)
  23. def load(self, file):
  24. """Parse a PKGBUILD (can be within a tar file) and import the variables"""
  25. if not os.path.exists(file):
  26. raise Exception("file does not exist")
  27. script_dir = os.path.dirname(__file__)
  28. if not script_dir:
  29. script_dir = os.path.abspath(script_dir)
  30. is_temporary = False
  31. # Check if it's a tarballed PKGBUILD and extract it
  32. try:
  33. tar = tarfile.open(file, "r")
  34. except:
  35. if os.path.basename(file) != "PKGBUILD":
  36. raise
  37. else:
  38. to_extract = None
  39. for member in tar.getnames():
  40. if member.find("PKGBUILD") >= 0:
  41. to_extract = member
  42. break
  43. if not to_extract:
  44. raise InvalidPackage('tar file does not contain a PKGBUILD')
  45. # Create a temporary directory and extract to it
  46. directory = tempfile.mkdtemp()
  47. tar.extract(to_extract, directory)
  48. file = os.path.join(directory, to_extract)
  49. is_temporary = True
  50. # Find the current directory and filename
  51. working_dir = os.path.dirname(file)
  52. if working_dir == '':
  53. working_dir = None
  54. filename = os.path.basename(file)
  55. # Let's parse the PKGBUILD
  56. process = subprocess.Popen([
  57. os.path.join(script_dir, 'parsepkgbuild.sh'), filename],
  58. stdout=subprocess.PIPE, cwd=working_dir)
  59. process.wait()
  60. # "Import" variables into local namespace
  61. for expression in process.stdout.readlines():
  62. self.update(eval('dict({})'.format(expression.rstrip().decode("utf-8"))))
  63. # Remove the temporary file since we don't need it
  64. if is_temporary:
  65. for root, dirs, files in os.walk(directory, topdown=False):
  66. for name in files:
  67. os.remove(os.path.join(root, name))
  68. for name in dirs:
  69. os.rmdir(os.path.join(root, name))
  70. os.rmdir(directory)
  71. def validate(self):
  72. """Validate PKGBUILD for missing or invalid fields"""
  73. # Search for missing fields
  74. for field in self._required_fields:
  75. if not self[field]:
  76. self._errors.append('%s field is required' % field)
  77. if not re.compile("^[\w\d_-]+$").match(self['name']):
  78. self._errors.append('package name must be alphanumeric')
  79. elif not re.compile("[a-z\d_-]+").match(self['name']):
  80. self._warnings.append('package name should be in lower case')
  81. if self['version'].find('-') >= 0:
  82. self._errors.append('version field is not allowed to contain hyphens')
  83. if str(self['release']).find('-') >= 0:
  84. self._errors.append('release field is not allowed to contain hyphens')
  85. # Description isn't supposed to be longer than 80 characters
  86. if self['description'] and len(self['description']) > 80:
  87. self._warnings.append('description should not exceed 80 characters')
  88. # Make sure the number of sources and checksums is the same
  89. found_sums = False
  90. for checksum in ('md5sums', 'sha1sums', 'sha256sums', 'sha384sums',
  91. 'sha512sums'):
  92. if self[checksum]:
  93. found_sums = True
  94. if len(self[checksum]) != len(self['source']):
  95. self._errors.append('amount of %s and sources does not match'
  96. % checksum)
  97. if self['source'] and not found_sums:
  98. self._errors.append('sources exist without checksums')
  99. # Set some variables to quickly determine whether the package is valid
  100. self._validated = True
  101. if not self.has_errors():
  102. self._is_valid = True
  103. def is_valid(self):
  104. """If Package wasn't validated already, validate and report whether
  105. package is valid"""
  106. if not self._validated:
  107. self.validate()
  108. return self._is_valid
  109. def has_errors(self):
  110. """Determine whether package has any errors"""
  111. return len(self._errors) > 0
  112. def has_warnings(self):
  113. """Determin whether the package has any warnings"""
  114. return len(self._warnings) > 0
  115. def get_errors(self):
  116. """Retrieve a list of errors"""
  117. return self._errors
  118. def get_warnings(self):
  119. """Retrieve a list of warnings"""
  120. return self._warnings