You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

build-osx-pkg.py 7.1 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python
  2. # Licensed to the Apache Software Foundation (ASF) under one or more
  3. # contributor license agreements. See the NOTICE file distributed with
  4. # this work for additional information regarding copyright ownership.
  5. # The ASF licenses this file to You under the Apache License, Version 2.0
  6. # (the "License"); you may not use this file except in compliance with
  7. # the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. # Builds a Mac OS X .pkg from a binary ZIP archive of Apache Ant.
  17. import collections
  18. import contextlib
  19. import os
  20. ApacheAntURL = collections.namedtuple(
  21. 'ApacheAntURL',
  22. ('url', 'url_scheme', 'version', 'directory_name'))
  23. @contextlib.contextmanager
  24. def make_temp_directory():
  25. '''Creates a temporary directory which is recursively deleted when out of scope.'''
  26. import shutil
  27. import tempfile
  28. temp_dir = tempfile.mkdtemp()
  29. yield temp_dir
  30. shutil.rmtree(temp_dir)
  31. @contextlib.contextmanager
  32. def self_closing_url(url):
  33. '''Opens a URL and returns a self-closing file-like object.'''
  34. import urllib2
  35. url_fp = urllib2.urlopen(url)
  36. yield url_fp
  37. url_fp.close()
  38. def apache_ant_url(url_string):
  39. '''Parses a URL string into an ApacheAntURL object.'''
  40. import argparse, collections, os.path, urlparse
  41. parse_result = urlparse.urlparse(url_string)
  42. filename = os.path.split(parse_result.path)[1]
  43. if not (filename.startswith('apache-ant-') and filename.endswith('-bin.zip')):
  44. raise argparse.ArgumentTypeError(
  45. 'Expected [%s] to end with apache-ant-X.Y.Z-bin.zip' % (url_string))
  46. extracted_directory = filename.replace('-bin.zip', '')
  47. extracted_version = extracted_directory.replace('apache-ant-', '')
  48. return ApacheAntURL(
  49. url=url_string,
  50. url_scheme=parse_result.scheme,
  51. version=extracted_version,
  52. directory_name=extracted_directory)
  53. def fetch_url(url, local_output_file):
  54. '''Downloads the contents of 'url' and writes them the opened file 'output_file'.'''
  55. import shutil
  56. import urllib2
  57. CHUNK_SIZE = 16 * 1024
  58. print 'Fetching {url}...'.format(url=url)
  59. with self_closing_url(url) as url_input_file:
  60. while True:
  61. chunk = url_input_file.read(CHUNK_SIZE)
  62. if not chunk:
  63. break
  64. local_output_file.write(chunk)
  65. local_output_file.seek(0)
  66. def fetch_apache_ant_url(apache_ant_url, temp_dir):
  67. '''If the ApacheAntURL object is remote, fetches and returns the local file object.
  68. Otherwise, opens and returns a file object.'''
  69. import tempfile
  70. if apache_ant_url.url_scheme == '' or apache_ant_url.url_scheme == 'file':
  71. return open(apache_ant_url.url, 'rb')
  72. else:
  73. fp = tempfile.TemporaryFile(dir=temp_dir)
  74. fetch_url(apache_ant_url.url, fp)
  75. return fp
  76. def uncompress_contents(temp_dir, archive_file, directory_name, path_prefix):
  77. '''Uncompresses the contents of 'archive_file' to 'temp_dir'.
  78. Strips the prefix 'directory_name' and prepends 'path_prefix' to each entry
  79. of the zip file.
  80. '''
  81. import shutil, zipfile
  82. output_path = os.path.join(temp_dir, 'pkg')
  83. os.mkdir(output_path)
  84. z = zipfile.ZipFile(archive_file)
  85. print 'Extracting archive to {output_path}...'.format(
  86. output_path=output_path)
  87. for entry in z.infolist():
  88. # We can't just extract directly, since we want to map:
  89. #
  90. # apache-ant-X.Y.Z/bin/foo
  91. #
  92. # to
  93. #
  94. # usr/local/ant/bin/foo
  95. #
  96. # So, we strip out the apache-ant-X.Y.Z prefix, then instead of
  97. # using ZipFile.extract(), we use ZipFile.open() to get a read fd to
  98. # the source file, then os.fdopen() with the appropriate permissions
  99. # to geta write fd to the modified destination path.
  100. expected_prefix = directory_name + '/'
  101. if not entry.filename.startswith(expected_prefix):
  102. raise Exeption('Unexpected entry in zip file: [{filename}]'.format(
  103. filename=entry.filename))
  104. entry_path = entry.filename.replace(expected_prefix, '', 1)
  105. # Using os.path.join is annoying here (we'd have to explode output_path
  106. # and entry_path).
  107. entry_output_path = output_path + path_prefix + '/' + entry_path
  108. # Zip file paths are normalized with '/' at the end for directories.
  109. if entry_output_path.endswith('/'):
  110. print 'Creating directory {path}'.format(path=entry_output_path)
  111. os.makedirs(entry_output_path)
  112. else:
  113. # Yes, this is really how you extract permissions from a ZipInfo entry.
  114. perms = (entry.external_attr >> 16L) & 0777
  115. print 'Extracting {entry_filename} to {path} with mode 0{mode:o}'.format(
  116. entry_filename=entry.filename, path=entry_output_path, mode=perms)
  117. with z.open(entry) as source:
  118. with os.fdopen(
  119. os.open(entry_output_path, os.O_WRONLY | os.O_CREAT, perms), 'w') \
  120. as destination:
  121. shutil.copyfileobj(source, destination)
  122. return output_path
  123. def write_paths_d_entry(paths_d_directory, filename):
  124. os.makedirs(paths_d_directory)
  125. output_file = os.path.join(paths_d_directory, filename)
  126. with open(output_file, 'w') as f:
  127. print >>f, '/usr/local/ant/bin'
  128. def make_pkg(pkg_dir, pkg_identifier, pkg_version, output_pkg_path):
  129. import subprocess
  130. print 'Building package at {output_pkg_path}...'.format(
  131. output_pkg_path=output_pkg_path)
  132. subprocess.call(
  133. ['pkgbuild',
  134. '--root', pkg_dir,
  135. '--identifier', pkg_identifier,
  136. '--version', pkg_version,
  137. output_pkg_path])
  138. def main():
  139. import argparse
  140. parser = argparse.ArgumentParser(description='Builds a Mac OS X .pkg of ant.')
  141. parser.add_argument(
  142. 'apache_ant_url',
  143. metavar='file-or-url',
  144. help='Source file or URL from which to uncompress apache-ant-X.Y.Z-bin.zip',
  145. type=apache_ant_url)
  146. parser.add_argument(
  147. '--output-dir',
  148. default='.',
  149. help='Directory to which .pkg will be written. Defaults to current directory.')
  150. args = parser.parse_args()
  151. with make_temp_directory() as temp_dir:
  152. archive_file = fetch_apache_ant_url(args.apache_ant_url, temp_dir)
  153. pkg_dir = uncompress_contents(
  154. temp_dir, archive_file, args.apache_ant_url.directory_name, '/usr/local/ant')
  155. etc_paths_d_dir = os.path.join(pkg_dir, 'etc', 'paths.d')
  156. write_paths_d_entry(etc_paths_d_dir, 'org.apache.ant')
  157. pkg_identifier = 'org.apache.ant'
  158. output_pkg_path = os.path.join(
  159. args.output_dir, args.apache_ant_url.directory_name + '.pkg')
  160. make_pkg(pkg_dir, pkg_identifier, args.apache_ant_url.version, output_pkg_path)
  161. if __name__ == '__main__':
  162. main()