Excellent. I'm not sure what the process is for code review, but here's a
patch against SVN trunk @ r1551525.

The patch adds an OS X .pkg installer generation script under
'release/build-osx-pkg.py' and updates build.xml to conditionally build an
OS X .pkg installer when it's run on OS X hosts.

>From bf8c0746ef979aff0e439b4e2fe917e78023356a Mon Sep 17 00:00:00 2001
From: Ben Gertzfield <b...@fb.com>
Date: Tue, 17 Dec 2013 10:43:53 -0800
Subject: [PATCH] Add OS X build script. Split targets zip_distribution,
 tar_distribution, pkg_distribution off main_distribution.

---
 build.xml                |  27 ++++++-
 release/build-osx-pkg.py | 179
+++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 204 insertions(+), 2 deletions(-)
 create mode 100755 release/build-osx-pkg.py

diff --git a/build.xml b/build.xml
index af9123c..c1e58f1 100644
--- a/build.xml
+++ b/build.xml
@@ -1148,8 +1148,7 @@
          Create the binary distribution
        ===================================================================
   -->
-  <target name="main_distribution" depends="jars-sources,test-jar-source"
-    description="--> creates the zip and tar distributions">
+  <target name="-distribution_prep">
     <delete dir="${dist.base}"/>
     <delete dir="${dist.name}"/>
     <delete dir="${java-repository.dir}"/>
@@ -1161,6 +1160,10 @@
     <antcall inheritAll="false" target="internal_dist">
       <param name="dist.dir" value="${dist.name}"/>
     </antcall>
+  </target>
+
+  <target name="zip_distribution" depends="jars,-distribution_prep"
+    description="--> creates the zip distribution">
     <zip destfile="${dist.base.binaries}/${dist.name}-bin.zip">
       <zipfileset dir="${dist.name}/.." filemode="755">
         <include name="${dist.name}/bin/ant"/>
@@ -1176,6 +1179,22 @@
         <exclude name="${dist.name}/bin/*.py"/>
       </fileset>
     </zip>
+  </target>
+
+  <condition property="buildosxpackage">
+    <os family="mac"/>
+  </condition>
+
+  <target name="pkg_distribution" depends="zip_distribution"
if="buildosxpackage">
+    <exec executable="release/build-osx-pkg.py">
+      <arg value="--output-dir"/>
+      <arg value="${dist.base.binaries}"/>
+      <arg value="${dist.base.binaries}/${dist.name}-bin.zip"/>
+    </exec>
+  </target>
+
+  <target name="tar_distribution" depends="jars,-distribution_prep"
+    description="--> creates the tar distribution">
     <tar longfile="gnu"
       destfile="${dist.base.binaries}/${dist.name}-bin.tar">
       <!-- removes redundant definition of permissions, but seems to
@@ -1201,6 +1220,10 @@
     <bzip2 destfile="${dist.base.binaries}/${dist.name}-bin.tar.bz2"
       src="${dist.base.binaries}/${dist.name}-bin.tar"/>
     <delete file="${dist.base.binaries}/${dist.name}-bin.tar"/>
+  </target>
+
+  <target name="main_distribution"
depends="zip_distribution,pkg_distribution,tar_distribution,jars-sources,test-jar-source"
+    description="--> creates the zip, pkg, and tar distributions">

     <copy todir="${java-repository.dir}">
       <fileset dir="${dist.name}/lib">
diff --git a/release/build-osx-pkg.py b/release/build-osx-pkg.py
new file mode 100755
index 0000000..4144a03
--- /dev/null
+++ b/release/build-osx-pkg.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Builds a Mac OS X .pkg from a binary ZIP archive of Apache Ant.
+
+import collections
+import contextlib
+import os
+
+ApacheAntURL = collections.namedtuple(
+    'ApacheAntURL',
+    ('url', 'url_scheme', 'version', 'directory_name'))
+
+@contextlib.contextmanager
+def make_temp_directory():
+    '''Creates a temporary directory which is recursively deleted when out
of scope.'''
+    import shutil
+    import tempfile
+    temp_dir = tempfile.mkdtemp()
+    yield temp_dir
+    shutil.rmtree(temp_dir)
+
+@contextlib.contextmanager
+def self_closing_url(url):
+    '''Opens a URL and returns a self-closing file-like object.'''
+    import urllib2
+    url_fp = urllib2.urlopen(url)
+    yield url_fp
+    url_fp.close()
+
+def apache_ant_url(url_string):
+    '''Parses a URL string into an ApacheAntURL object.'''
+    import argparse, collections, os.path, urlparse
+    parse_result = urlparse.urlparse(url_string)
+    filename = os.path.split(parse_result.path)[1]
+    if not (filename.startswith('apache-ant-') and
filename.endswith('-bin.zip')):
+        raise argparse.ArgumentTypeError(
+            'Expected [%s] to end with apache-ant-X.Y.Z-bin.zip' %
(url_string))
+    extracted_directory = filename.replace('-bin.zip', '')
+    extracted_version = extracted_directory.replace('apache-ant-', '')
+    return ApacheAntURL(
+        url=url_string,
+        url_scheme=parse_result.scheme,
+        version=extracted_version,
+        directory_name=extracted_directory)
+
+def fetch_url(url, local_output_file):
+    '''Downloads the contents of 'url' and writes them the opened file
'output_file'.'''
+    import shutil
+    import urllib2
+    CHUNK_SIZE = 16 * 1024
+    print 'Fetching {url}...'.format(url=url)
+    with self_closing_url(url) as url_input_file:
+        while True:
+            chunk = url_input_file.read(CHUNK_SIZE)
+            if not chunk:
+                break
+            local_output_file.write(chunk)
+        local_output_file.seek(0)
+
+def fetch_apache_ant_url(apache_ant_url, temp_dir):
+    '''If the ApacheAntURL object is remote, fetches and returns the local
file object.
+    Otherwise, opens and returns a file object.'''
+    import tempfile
+    if apache_ant_url.url_scheme == '' or apache_ant_url.url_scheme ==
'file':
+        return open(apache_ant_url.url, 'rb')
+    else:
+        fp = tempfile.TemporaryFile(dir=temp_dir)
+        fetch_url(apache_ant_url.url, fp)
+        return fp
+
+def uncompress_contents(temp_dir, archive_file, directory_name,
path_prefix):
+    '''Uncompresses the contents of 'archive_file' to 'temp_dir'.
+
+    Strips the prefix 'directory_name' and prepends 'path_prefix' to each
entry
+    of the zip file.
+    '''
+    import shutil, zipfile
+    output_path = os.path.join(temp_dir, 'pkg')
+    os.mkdir(output_path)
+    z = zipfile.ZipFile(archive_file)
+    print 'Extracting archive to {output_path}...'.format(
+        output_path=output_path)
+    for entry in z.infolist():
+        # We can't just extract directly, since we want to map:
+        #
+        # apache-ant-X.Y.Z/bin/foo
+        #
+        # to
+        #
+        # usr/local/ant/bin/foo
+        #
+        # So, we strip out the apache-ant-X.Y.Z prefix, then instead of
+        # using ZipFile.extract(), we use ZipFile.open() to get a read fd
to
+        # the source file, then os.fdopen() with the appropriate
permissions
+        # to geta write fd to the modified destination path.
+        expected_prefix = directory_name + '/'
+        if not entry.filename.startswith(expected_prefix):
+            raise Exeption('Unexpected entry in zip file:
[{filename}]'.format(
+                    filename=entry.filename))
+        entry_path = entry.filename.replace(expected_prefix, '', 1)
+
+        # Using os.path.join is annoying here (we'd have to explode
output_path
+        # and entry_path).
+        entry_output_path = output_path + path_prefix + '/' + entry_path
+
+        # Zip file paths are normalized with '/' at the end for
directories.
+        if entry_output_path.endswith('/'):
+            print 'Creating directory
{path}'.format(path=entry_output_path)
+            os.makedirs(entry_output_path)
+        else:
+            # Yes, this is really how you extract permissions from a
ZipInfo entry.
+            perms = (entry.external_attr >> 16L) & 0777
+            print 'Extracting {entry_filename} to {path} with mode
0{mode:o}'.format(
+                entry_filename=entry.filename, path=entry_output_path,
mode=perms)
+            with z.open(entry) as source:
+                with os.fdopen(
+                    os.open(entry_output_path, os.O_WRONLY | os.O_CREAT,
perms), 'w') \
+                    as destination:
+                    shutil.copyfileobj(source, destination)
+    return output_path
+
+def write_paths_d_entry(paths_d_directory, filename):
+    os.makedirs(paths_d_directory)
+    output_file = os.path.join(paths_d_directory, filename)
+    with open(output_file, 'w') as f:
+        print >>f, '/usr/local/ant/bin'
+
+def make_pkg(pkg_dir, pkg_identifier, pkg_version, output_pkg_path):
+    import subprocess
+    print 'Building package at {output_pkg_path}...'.format(
+        output_pkg_path=output_pkg_path)
+    subprocess.call(
+        ['pkgbuild',
+         '--root', pkg_dir,
+         '--identifier', pkg_identifier,
+         '--version', pkg_version,
+         output_pkg_path])
+
+def main():
+    import argparse
+    parser = argparse.ArgumentParser(description='Builds a Mac OS X .pkg
of ant.')
+    parser.add_argument(
+        'apache_ant_url',
+        metavar='file-or-url',
+        help='Source file or URL from which to uncompress
apache-ant-X.Y.Z-bin.zip',
+        type=apache_ant_url)
+    parser.add_argument(
+        '--output-dir',
+        default='.',
+        help='Directory to which .pkg will be written. Defaults to current
directory.')
+    args = parser.parse_args()
+    with make_temp_directory() as temp_dir:
+        archive_file = fetch_apache_ant_url(args.apache_ant_url, temp_dir)
+        pkg_dir = uncompress_contents(
+            temp_dir, archive_file, args.apache_ant_url.directory_name,
'/usr/local/ant')
+        etc_paths_d_dir = os.path.join(pkg_dir, 'etc', 'paths.d')
+        write_paths_d_entry(etc_paths_d_dir, 'org.apache.ant')
+        pkg_identifier = 'org.apache.ant'
+        output_pkg_path = os.path.join(
+            args.output_dir, args.apache_ant_url.directory_name + '.pkg')
+        make_pkg(pkg_dir, pkg_identifier, args.apache_ant_url.version,
output_pkg_path)
+
+if __name__ == '__main__':
+    main()
-- 
1.8.3.4 (Apple Git-47)



On Mon, Dec 16, 2013 at 9:17 PM, Antoine Levy Lambert <anto...@gmx.de>wrote:

> Hello Ben,
>
> this should be living in ant core. Either in the root directory or in the
> release directory.
>
> Regards,
>
> Antoine
> On Dec 16, 2013, at 8:18 PM, Ben Gertzfield wrote:
>
> > Hi Ant folks,
> >
> > As of OS X version 10.9 ("Mavericks"), OS X no longer includes Apache
> Ant.
> >
> > To help people who don't want to install Homebrew or similar systems to
> > grab Ant, I wrote a Python script to build a OS X .pkg installer of Ant:
> >
> > http://pastebin.com/raw.php?i=StmYCeZd
> >
> > I'd like to contribute this script (which runs on OS X) to the project,
> but
> > I'm not sure which Ant repo it belongs in (core? build? antlibs?).
> >
> > Where should this script live?
> >
> > Thanks,
> >
> > Ben
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscr...@ant.apache.org
> For additional commands, e-mail: dev-h...@ant.apache.org
>
>

Reply via email to