PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/addons_xml_generator.py

https://gitlab.com/cloudwordinst/cloudword-repository
Python | 319 lines | 307 code | 1 blank | 11 comment | 2 complexity | 04d3f66b3c65bbedc325592a71595d01 MD5 | raw file
  1. script_name = "repo_prep.py"
  2. revision_number = 5
  3. homepage = 'http://forum.xbmc.org/showthread.php?tid=129401'
  4. script_credits = 'All code copyleft (GNU GPL v3) by Unobtanium @ XBMC Forums'
  5. """
  6. Please bump the version number one decimal point and add your name to credits when making changes.
  7. This is an:
  8. - addons.xml generator
  9. - addons.xml.md5 generator
  10. - optional auto-compressor (including handling of icons, fanart and changelog)
  11. Compression of addons in repositories has many benefits, including:
  12. - Protects addon downloads from corruption.
  13. - Smaller addon filesize resulting in faster downloads and less space / bandwidth used on the repository.
  14. - Ability to "roll back" addon updates in XBMC to previous versions.
  15. To enable the auto-compressor, set the compress_addons setting to True
  16. NOTE: the settings.py of repository aggregator will override this setting.
  17. If you do this you must make sure the "datadir zip" parameter in the addon.xml of your repository file is set to "true".
  18. """
  19. import os
  20. import shutil
  21. import md5
  22. import zipfile
  23. import re
  24. ########## SETTINGS
  25. # Set whether you want your addons compressed or not. Values are True or False
  26. # NOTE: the settings.py of repository aggregator will override this
  27. compress_addons = True
  28. #Optional set a custom directory of where your addons are. False will use the current directory.
  29. # NOTE: the settings.py of repository aggregator will override this
  30. repo_root = False
  31. ########## End SETTINGS
  32. # check if repo-prep.py is being run standalone or called from another python file
  33. if __name__ == "__main__": standalone = True
  34. else: standalone = False
  35. # this 'if' block adds support for the repo aggregator script
  36. # set the repository's root folder here, if the script user has not set a custom path.
  37. if standalone:
  38. if repo_root == False: repo_root = os.getcwd()
  39. print script_name + ' v' + str(revision_number)
  40. print script_credits
  41. print 'Homepage and updates: ' + homepage
  42. print ' '
  43. else:
  44. #so that we can import stuff from parent dir (settings)
  45. import sys
  46. sys.path.append('..')
  47. import settings
  48. repo_root = settings.aggregate_repo_path
  49. # use repository aggregator settings.py to determine whether to compress
  50. compress_addons = settings.compress_addons
  51. def is_addon_dir( addon ):
  52. # this function is used by both classes.
  53. # very very simple and weak check that it is an addon dir.
  54. # intended to be fast, not totally accurate.
  55. # skip any file or .svn folder
  56. if not os.path.isdir( addon ) or addon == ".svn": return False
  57. else: return True
  58. class Generator:
  59. """
  60. Generates a new addons.xml file from each addons addon.xml file
  61. and a new addons.xml.md5 hash file. Must be run from the root of
  62. the checked-out repo. Only handles single depth folder structure.
  63. """
  64. def __init__( self ):
  65. #paths
  66. self.addons_xml = os.path.join( repo_root, "addons.xml" )
  67. self.addons_xml_md5 = os.path.join( repo_root, "addons.xml.md5" )
  68. # call master function
  69. self._generate_addons_files()
  70. def _generate_addons_files( self ):
  71. # addon list
  72. addons = os.listdir( repo_root )
  73. # final addons text
  74. addons_xml = u"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<addons>\n"
  75. found_an_addon = False
  76. # loop thru and add each addons addon.xml file
  77. for addon in addons:
  78. try:
  79. # skip any file or .svn folder
  80. if is_addon_dir( addon ):
  81. # create path
  82. _path = os.path.join( addon, "addon.xml" )
  83. if os.path.exists(_path): found_an_addon = True
  84. # split lines for stripping
  85. xml_lines = open( _path, "r" ).read().splitlines()
  86. # new addon
  87. addon_xml = ""
  88. # loop thru cleaning each line
  89. for line in xml_lines:
  90. # skip encoding format line
  91. if ( line.find( "<?xml" ) >= 0 ): continue
  92. # add line
  93. addon_xml += unicode( line.rstrip() + "\n", "UTF-8" )
  94. # we succeeded so add to our final addons.xml text
  95. addons_xml += addon_xml.rstrip() + "\n\n"
  96. except Exception, e:
  97. # missing or poorly formatted addon.xml
  98. print "Excluding %s for %s" % ( _path, e, )
  99. # clean and add closing tag
  100. addons_xml = addons_xml.strip() + u"\n</addons>\n"
  101. # only generate files if we found an addon.xml
  102. if found_an_addon:
  103. # save files
  104. self._save_file( addons_xml.encode( "UTF-8" ), self.addons_xml )
  105. self._generate_md5_file()
  106. # notify user
  107. print "Updated addons xml and addons.xml.md5 files"
  108. else: print "Could not find any addons, so script has done nothing."
  109. def _generate_md5_file( self ):
  110. try:
  111. # create a new md5 hash
  112. m = md5.new( open( self.addons_xml ).read() ).hexdigest()
  113. # save file
  114. self._save_file( m, self.addons_xml_md5 )
  115. except Exception, e:
  116. # oops
  117. print "An error occurred creating addons.xml.md5 file!\n%s" % ( e, )
  118. def _save_file( self, data, the_path ):
  119. try:
  120. # write data to the file
  121. open( the_path, "w" ).write( data )
  122. except Exception, e:
  123. # oops
  124. print "An error occurred saving %s file!\n%s" % ( the_path, e, )
  125. class Compressor:
  126. def __init__( self ):
  127. # variables used later on
  128. self.addon_name = None
  129. self.addon_path = None
  130. self.addon_folder_contents = None
  131. self.addon_xml = None
  132. self.addon_version_number = None
  133. self.addon_zip_path = None
  134. # run the master method of the class, when class is initialised.
  135. # only do so if we want addons compressed.
  136. if compress_addons: self.master()
  137. def master( self ):
  138. mydir = os.listdir( repo_root )
  139. for addon in mydir:
  140. # set variables
  141. self.addon_name = str(addon)
  142. self.addon_path = os.path.join( repo_root, addon )
  143. # skip any file or .svn folder.
  144. if is_addon_dir( self.addon_path ):
  145. # set another variable
  146. self.addon_folder_contents = os.listdir( self.addon_path )
  147. # check if addon has a current zipped release in it.
  148. addon_zip_exists = self._get_zipped_addon_path()
  149. # checking for addon.xml and try reading it.
  150. addon_xml_exists = self._read_addon_xml()
  151. # generator class relies on addon.xml being in release folder. so if need be, fix a zipped addon release folder lacking an addon.xml
  152. if addon_zip_exists:
  153. if not addon_xml_exists:
  154. # extract the addon_xml from the zip archive into the addon release folder.
  155. self._extract_addon_xml_to_release_folder()
  156. else:
  157. if addon_xml_exists:
  158. # now addon.xml has been read, scrape version number from it. we need this when naming the zip (and if it exists the changelog)
  159. self._read_version_number()
  160. print 'Create compressed addon release for -- ' + self.addon_name + ' v' + self.addon_version_number
  161. self._create_compressed_addon_release()
  162. def _get_zipped_addon_path( self ):
  163. # get name of addon zip file. returns False if not found.
  164. for the_file in self.addon_folder_contents:
  165. if '.zip' in the_file:
  166. if ( self.addon_name + '-') in the_file:
  167. self.addon_zip_path = os.path.join ( self.addon_path, the_file )
  168. return True
  169. # if loop is not broken by returning the addon path, zip was not found so return False
  170. self.addon_zip_path = None
  171. return False
  172. def _extract_addon_xml_to_release_folder():
  173. the_zip = zipfile.ZipFile( self.addon_zip_path, 'r' )
  174. for filename in the_zip.namelist():
  175. if filename.find('addon.xml'):
  176. the_zip.extract( filename, self.addon_path )
  177. break
  178. def _recursive_zipper( self, dir, zip_file ):
  179. #initialize zipping module
  180. zip = zipfile.ZipFile( zip_file, 'w', compression=zipfile.ZIP_DEFLATED )
  181. # get length of characters of what we will use as the root path
  182. root_len = len( os.path.dirname(os.path.abspath(dir)) )
  183. #recursive writer
  184. for root, dirs, files in os.walk(dir):
  185. # subtract the source file's root from the archive root - ie. make /Users/me/desktop/zipme.txt into just /zipme.txt
  186. archive_root = os.path.abspath(root)[root_len:]
  187. for f in files:
  188. fullpath = os.path.join( root, f )
  189. archive_name = os.path.join( archive_root, f )
  190. zip.write( fullpath, archive_name, zipfile.ZIP_DEFLATED )
  191. zip.close()
  192. def _create_compressed_addon_release( self ):
  193. # create a zip of the addon into repo root directory, tagging it with '-x.x.x' release number scraped from addon.xml
  194. zipname = self.addon_name + '-' + self.addon_version_number + '.zip'
  195. zippath = os.path.join( repo_root, zipname )
  196. # zip full directories
  197. self._recursive_zipper( self.addon_path , zippath )
  198. # now move the zip into the addon folder, which we will now treat as the 'addon release directory'
  199. os.rename( zippath, os.path.join( self.addon_path, zipname ) )
  200. # in the addon release directory, delete every file apart from addon.xml, changelog, fanart, icon and the zip we just constructed. also rename changelog.
  201. for the_file in self.addon_folder_contents:
  202. the_path = os.path.join( self.addon_path, the_file )
  203. # delete directories
  204. if not os.path.isfile( the_path ):
  205. shutil.rmtree( the_path )
  206. # list of files we specifically need to retain for the addon release folder (folder containing the zip
  207. elif not ( ('addon.xml' in the_file) or ('hangelog' in the_file) or ('fanart' in the_file) or ('icon' in the_file) or (zipname in the_file)):
  208. os.remove( the_path )
  209. # tag the changelog with '-x.x.x' release number
  210. elif 'hangelog' in the_file: # hangelog so that it is detected irrespective of whether C is capitalised
  211. changelog = 'changelog-' + self.addon_version_number + '.txt'
  212. os.rename( the_path, os.path.join( self.addon_path, changelog ) )
  213. def _read_addon_xml( self ):
  214. # check for addon.xml and try and read it.
  215. addon_xml_path = os.path.join( self.addon_path, 'addon.xml' )
  216. if os.path.exists( addon_xml_path ):
  217. # load whole text into string
  218. f = open( addon_xml_path, "r")
  219. self.addon_xml = f.read()
  220. f.close()
  221. # return True if we found and read the addon.xml
  222. return True
  223. # return False if we couldn't find the addon.xml
  224. else: return False
  225. def _read_version_number( self ):
  226. # find the header of the addon.
  227. headers = re.compile( "\<addon id\=(.+?)>", re.DOTALL ).findall( self.addon_xml )
  228. for header in headers:
  229. #if this is the header for the addon, proceed
  230. if self.addon_name in header:
  231. # clean line of quotation characters so that it is easier to read.
  232. header = re.sub( '"','', header )
  233. header = re.sub( "'",'', header )
  234. # scrape the version number from the line
  235. self.addon_version_number = (( re.compile( "version\=(.+?) " , re.DOTALL ).findall( header ) )[0]).strip()
  236. def execute():
  237. Compressor()
  238. Generator()
  239. # standalone is equivalent of if name == main
  240. if standalone: execute()