mingw.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. import argparse
  4. import errno
  5. import logging
  6. import os
  7. import platform
  8. import re
  9. import sys
  10. import subprocess
  11. import tempfile
  12. try:
  13. import winreg
  14. except ImportError:
  15. import _winreg as winreg
  16. try:
  17. import urllib.request as request
  18. except ImportError:
  19. import urllib as request
  20. try:
  21. import urllib.parse as parse
  22. except ImportError:
  23. import urlparse as parse
  24. class EmptyLogger(object):
  25. '''
  26. Provides an implementation that performs no logging
  27. '''
  28. def debug(self, *k, **kw):
  29. pass
  30. def info(self, *k, **kw):
  31. pass
  32. def warn(self, *k, **kw):
  33. pass
  34. def error(self, *k, **kw):
  35. pass
  36. def critical(self, *k, **kw):
  37. pass
  38. def setLevel(self, *k, **kw):
  39. pass
  40. urls = (
  41. 'http://downloads.sourceforge.net/project/mingw-w64/Toolchains%20'
  42. 'targetting%20Win32/Personal%20Builds/mingw-builds/installer/'
  43. 'repository.txt',
  44. 'http://downloads.sourceforge.net/project/mingwbuilds/host-windows/'
  45. 'repository.txt'
  46. )
  47. '''
  48. A list of mingw-build repositories
  49. '''
  50. def repository(urls = urls, log = EmptyLogger()):
  51. '''
  52. Downloads and parse mingw-build repository files and parses them
  53. '''
  54. log.info('getting mingw-builds repository')
  55. versions = {}
  56. re_sourceforge = re.compile(r'http://sourceforge.net/projects/([^/]+)/files')
  57. re_sub = r'http://downloads.sourceforge.net/project/\1'
  58. for url in urls:
  59. log.debug(' - requesting: %s', url)
  60. socket = request.urlopen(url)
  61. repo = socket.read()
  62. if not isinstance(repo, str):
  63. repo = repo.decode();
  64. socket.close()
  65. for entry in repo.split('\n')[:-1]:
  66. value = entry.split('|')
  67. version = tuple([int(n) for n in value[0].strip().split('.')])
  68. version = versions.setdefault(version, {})
  69. arch = value[1].strip()
  70. if arch == 'x32':
  71. arch = 'i686'
  72. elif arch == 'x64':
  73. arch = 'x86_64'
  74. arch = version.setdefault(arch, {})
  75. threading = arch.setdefault(value[2].strip(), {})
  76. exceptions = threading.setdefault(value[3].strip(), {})
  77. revision = exceptions.setdefault(int(value[4].strip()[3:]),
  78. re_sourceforge.sub(re_sub, value[5].strip()))
  79. return versions
  80. def find_in_path(file, path=None):
  81. '''
  82. Attempts to find an executable in the path
  83. '''
  84. if platform.system() == 'Windows':
  85. file += '.exe'
  86. if path is None:
  87. path = os.environ.get('PATH', '')
  88. if type(path) is type(''):
  89. path = path.split(os.pathsep)
  90. return list(filter(os.path.exists,
  91. map(lambda dir, file=file: os.path.join(dir, file), path)))
  92. def find_7zip(log = EmptyLogger()):
  93. '''
  94. Attempts to find 7zip for unpacking the mingw-build archives
  95. '''
  96. log.info('finding 7zip')
  97. path = find_in_path('7z')
  98. if not path:
  99. key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\7-Zip')
  100. path, _ = winreg.QueryValueEx(key, 'Path')
  101. path = [os.path.join(path, '7z.exe')]
  102. log.debug('found \'%s\'', path[0])
  103. return path[0]
  104. find_7zip()
  105. def unpack(archive, location, log = EmptyLogger()):
  106. '''
  107. Unpacks a mingw-builds archive
  108. '''
  109. sevenzip = find_7zip(log)
  110. log.info('unpacking %s', os.path.basename(archive))
  111. cmd = [sevenzip, 'x', archive, '-o' + location, '-y']
  112. log.debug(' - %r', cmd)
  113. with open(os.devnull, 'w') as devnull:
  114. subprocess.check_call(cmd, stdout = devnull)
  115. def download(url, location, log = EmptyLogger()):
  116. '''
  117. Downloads and unpacks a mingw-builds archive
  118. '''
  119. log.info('downloading MinGW')
  120. log.debug(' - url: %s', url)
  121. log.debug(' - location: %s', location)
  122. re_content = re.compile(r'attachment;[ \t]*filename=(")?([^"]*)(")?[\r\n]*')
  123. stream = request.urlopen(url)
  124. try:
  125. content = stream.getheader('Content-Disposition') or ''
  126. except AttributeError:
  127. content = stream.headers.getheader('Content-Disposition') or ''
  128. matches = re_content.match(content)
  129. if matches:
  130. filename = matches.group(2)
  131. else:
  132. parsed = parse.urlparse(stream.geturl())
  133. filename = os.path.basename(parsed.path)
  134. try:
  135. os.makedirs(location)
  136. except OSError as e:
  137. if e.errno == errno.EEXIST and os.path.isdir(location):
  138. pass
  139. else:
  140. raise
  141. archive = os.path.join(location, filename)
  142. with open(archive, 'wb') as out:
  143. while True:
  144. buf = stream.read(1024)
  145. if not buf:
  146. break
  147. out.write(buf)
  148. unpack(archive, location, log = log)
  149. os.remove(archive)
  150. possible = os.path.join(location, 'mingw64')
  151. if not os.path.exists(possible):
  152. possible = os.path.join(location, 'mingw32')
  153. if not os.path.exists(possible):
  154. raise ValueError('Failed to find unpacked MinGW: ' + possible)
  155. return possible
  156. def root(location = None, arch = None, version = None, threading = None,
  157. exceptions = None, revision = None, log = EmptyLogger()):
  158. '''
  159. Returns the root folder of a specific version of the mingw-builds variant
  160. of gcc. Will download the compiler if needed
  161. '''
  162. # Get the repository if we don't have all the information
  163. if not (arch and version and threading and exceptions and revision):
  164. versions = repository(log = log)
  165. # Determine some defaults
  166. version = version or max(versions.keys())
  167. if not arch:
  168. arch = platform.machine().lower()
  169. if arch == 'x86':
  170. arch = 'i686'
  171. elif arch == 'amd64':
  172. arch = 'x86_64'
  173. if not threading:
  174. keys = versions[version][arch].keys()
  175. if 'posix' in keys:
  176. threading = 'posix'
  177. elif 'win32' in keys:
  178. threading = 'win32'
  179. else:
  180. threading = keys[0]
  181. if not exceptions:
  182. keys = versions[version][arch][threading].keys()
  183. if 'seh' in keys:
  184. exceptions = 'seh'
  185. elif 'sjlj' in keys:
  186. exceptions = 'sjlj'
  187. else:
  188. exceptions = keys[0]
  189. if revision == None:
  190. revision = max(versions[version][arch][threading][exceptions].keys())
  191. if not location:
  192. location = os.path.join(tempfile.gettempdir(), 'mingw-builds')
  193. # Get the download url
  194. url = versions[version][arch][threading][exceptions][revision]
  195. # Tell the user whatzzup
  196. log.info('finding MinGW %s', '.'.join(str(v) for v in version))
  197. log.debug(' - arch: %s', arch)
  198. log.debug(' - threading: %s', threading)
  199. log.debug(' - exceptions: %s', exceptions)
  200. log.debug(' - revision: %s', revision)
  201. log.debug(' - url: %s', url)
  202. # Store each specific revision differently
  203. slug = '{version}-{arch}-{threading}-{exceptions}-rev{revision}'
  204. slug = slug.format(
  205. version = '.'.join(str(v) for v in version),
  206. arch = arch,
  207. threading = threading,
  208. exceptions = exceptions,
  209. revision = revision
  210. )
  211. if arch == 'x86_64':
  212. root_dir = os.path.join(location, slug, 'mingw64')
  213. elif arch == 'i686':
  214. root_dir = os.path.join(location, slug, 'mingw32')
  215. else:
  216. raise ValueError('Unknown MinGW arch: ' + arch)
  217. # Download if needed
  218. if not os.path.exists(root_dir):
  219. downloaded = download(url, os.path.join(location, slug), log = log)
  220. if downloaded != root_dir:
  221. raise ValueError('The location of mingw did not match\n%s\n%s'
  222. % (downloaded, root_dir))
  223. return root_dir
  224. def str2ver(string):
  225. '''
  226. Converts a version string into a tuple
  227. '''
  228. try:
  229. version = tuple(int(v) for v in string.split('.'))
  230. if len(version) is not 3:
  231. raise ValueError()
  232. except ValueError:
  233. raise argparse.ArgumentTypeError(
  234. 'please provide a three digit version string')
  235. return version
  236. def main():
  237. '''
  238. Invoked when the script is run directly by the python interpreter
  239. '''
  240. parser = argparse.ArgumentParser(
  241. description = 'Downloads a specific version of MinGW',
  242. formatter_class = argparse.ArgumentDefaultsHelpFormatter
  243. )
  244. parser.add_argument('--location',
  245. help = 'the location to download the compiler to',
  246. default = os.path.join(tempfile.gettempdir(), 'mingw-builds'))
  247. parser.add_argument('--arch', required = True, choices = ['i686', 'x86_64'],
  248. help = 'the target MinGW architecture string')
  249. parser.add_argument('--version', type = str2ver,
  250. help = 'the version of GCC to download')
  251. parser.add_argument('--threading', choices = ['posix', 'win32'],
  252. help = 'the threading type of the compiler')
  253. parser.add_argument('--exceptions', choices = ['sjlj', 'seh', 'dwarf'],
  254. help = 'the method to throw exceptions')
  255. parser.add_argument('--revision', type=int,
  256. help = 'the revision of the MinGW release')
  257. group = parser.add_mutually_exclusive_group()
  258. group.add_argument('-v', '--verbose', action='store_true',
  259. help='increase the script output verbosity')
  260. group.add_argument('-q', '--quiet', action='store_true',
  261. help='only print errors and warning')
  262. args = parser.parse_args()
  263. # Create the logger
  264. logger = logging.getLogger('mingw')
  265. handler = logging.StreamHandler()
  266. formatter = logging.Formatter('%(message)s')
  267. handler.setFormatter(formatter)
  268. logger.addHandler(handler)
  269. logger.setLevel(logging.INFO)
  270. if args.quiet:
  271. logger.setLevel(logging.WARN)
  272. if args.verbose:
  273. logger.setLevel(logging.DEBUG)
  274. # Get MinGW
  275. root_dir = root(location = args.location, arch = args.arch,
  276. version = args.version, threading = args.threading,
  277. exceptions = args.exceptions, revision = args.revision,
  278. log = logger)
  279. sys.stdout.write('%s\n' % os.path.join(root_dir, 'bin'))
  280. if __name__ == '__main__':
  281. try:
  282. main()
  283. except IOError as e:
  284. sys.stderr.write('IO error: %s\n' % e)
  285. sys.exit(1)
  286. except OSError as e:
  287. sys.stderr.write('OS error: %s\n' % e)
  288. sys.exit(1)
  289. except KeyboardInterrupt as e:
  290. sys.stderr.write('Killed\n')
  291. sys.exit(1)