Revision: 25686
          http://sourceforge.net/p/bibdesk/svn/25686
Author:   hofman
Date:     2021-04-03 14:11:50 +0000 (Sat, 03 Apr 2021)
Log Message:
-----------
Add a python script to build a BibDesk release, including codesigning and 
notaraization. Should be called with the codesign identity and username for 
notarization.

Added Paths:
-----------
    trunk/bibdesk/build_release.py

Added: trunk/bibdesk/build_release.py
===================================================================
--- trunk/bibdesk/build_release.py                              (rev 0)
+++ trunk/bibdesk/build_release.py      2021-04-03 14:11:50 UTC (rev 25686)
@@ -0,0 +1,525 @@
+#!/usr/bin/python
+
+#
+# This script is part of BibDesk.  Various paths are hardcoded near
+# the top, along with other paths that are dependent on those.  The script
+# builds, codesigns, and notarizes a zip compressed app bundle for release
+# and create an appcast item for the release.
+#
+
+#
+# SYNOPSIS
+#   build_release.sh [-i identity] [-u username] [-p password] [-o out]
+#
+# OPTIONS
+#   -i --identity
+#       Codesign identity, not codesigned when empty
+#   -u, --username
+#       Username for notarization, not notarized when empty
+#   -p, --password
+#       Password for notarization, defaults to @keychain:AC_PASSWORD
+#   -o, --out
+#      Output directory for the final zip and appcast, defaults to the user's 
Desktop
+#
+
+#
+# Created by Adam Maxwell on 12/28/08.
+#
+# This software is Copyright (c) 2008-2021
+# Adam Maxwell. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 
+# - Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 
+# - Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+# 
+# - Neither the name of Adam Maxwell nor the names of any
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+import os, sys, io, getopt
+from subprocess import Popen, PIPE
+from stat import ST_SIZE
+import tarfile
+from time import gmtime, strftime, localtime, sleep
+import plistlib
+import tempfile
+from getpass import getuser
+
+from Foundation import NSXMLDocument, NSUserDefaults, NSURL, 
NSXMLNodePrettyPrint, NSXMLNodePreserveCDATA
+
+# determine the path based on the path of this program
+SOURCE_DIR = os.path.dirname(os.path.abspath(sys.argv[0]))
+assert len(SOURCE_DIR)
+assert SOURCE_DIR.startswith("/")
+
+# name of secure note in Keychain
+KEY_NAME = "BibDesk Sparkle Key"
+
+APPCAST_URL = "https://bibdesk.sourceforge.io/bibdesk.xml";
+
+# create a private temporary directory
+BUILD_ROOT = os.path.join("/tmp", "BibDesk-%s" % (getuser()))
+try:
+    # should already exist after the first run
+    os.mkdir(BUILD_ROOT)
+except Exception as e:
+    assert os.path.isdir(BUILD_ROOT), "%s does not exist" % (BUILD_ROOT)
+
+# derived paths
+SYMROOT = os.path.join(BUILD_ROOT, "Products")
+BUILD_DIR = os.path.join(SYMROOT, "Release")
+BUILT_APP = os.path.join(BUILD_DIR, "BibDesk.app")
+DERIVED_DATA_DIR = os.path.join(BUILD_ROOT, "DerivedData")
+PLIST_PATH = os.path.join(BUILT_APP, "Contents", "Info.plist")
+
+def read_versions():
+
+    # read CFBundleVersion, CFBundleShortVersionString, LSMinimumSystemVersion 
and from Info.plist
+    infoPlist = plistlib.readPlist(PLIST_PATH)
+    assert infoPlist is not None, "unable to read Info.plist"
+    newVersion = infoPlist["CFBundleVersion"]
+    newVersionString = infoPlist["CFBundleShortVersionString"]
+    minimumSystemVersion = infoPlist["LSMinimumSystemVersion"]
+    assert newVersion is not None, "unable to read old version from Info.plist"
+    assert newVersionString is not None, "unable to read old version from 
Info.plist"
+    
+    return newVersion, newVersionString , minimumSystemVersion
+
+def clean_and_build():
+    
+    # clean and rebuild the Xcode project
+    buildCmd = ["/usr/bin/xcodebuild", "clean", "-configuration", "Release", 
"-target", "BibDesk", "-scheme", "BibDesk", "-derivedDataPath", 
DERIVED_DATA_DIR, "SYMROOT=" + SYMROOT]
+    nullDevice = open("/dev/null", "w")
+    x = Popen(buildCmd, cwd=SOURCE_DIR)
+    rc = x.wait()
+    print("xcodebuild clean exited with status %s" % (rc))
+
+    buildCmd = ["/usr/bin/xcodebuild", "-configuration", "Release", "-target", 
"BibDesk", "-scheme", "BibDesk", "-derivedDataPath", DERIVED_DATA_DIR, 
"SYMROOT=" + SYMROOT, "CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO"]
+    nullDevice = open("/dev/null", "w")
+    x = Popen(buildCmd, cwd=SOURCE_DIR)#, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "xcodebuild failed"
+    nullDevice.close()
+
+def codesign(identity):
+    
+    sign_cmd = [os.path.join(SOURCE_DIR, "codesign_bibdesk.sh"), identity, 
BUILT_APP]
+    x = Popen(sign_cmd, cwd=SOURCE_DIR)
+    rc = x.wait()
+    print("codesign_bibdesk.sh exited with status %s" % (rc))
+    assert rc == 0, "code signing failed"
+    
+def notarize_dmg_or_zip(dmg_path, username, password):
+    """dmg_path: zip file or dmg file"""
+    
+    notarize_cmd = ["xcrun", "altool", "--notarize-app", 
"--primary-bundle-id", "net.sourceforce.bibdesk.zip", "--username", username, 
"--password",  password, "--output-format", "xml", "--file", dmg_path]
+    notarize_task = Popen(notarize_cmd, cwd=SOURCE_DIR, stdout=PIPE, 
stderr=PIPE)
+    [output, error] = notarize_task.communicate()
+    rc = notarize_task.returncode
+    print("altool --notarize-app exited with status %s" % (rc))
+    assert rc == 0, "notarization failed"
+    
+    output_stream = io.BytesIO(output)
+    output_pl = plistlib.readPlist(output_stream)
+    output_stream.close()
+    sys.stderr.write("%s\n" % (output))
+    assert "notarization-upload" in output_pl, "missing notarization-upload 
key in reply %s" % (output)
+    
+    request_uuid = output_pl["notarization-upload"]["RequestUUID"]
+    
+    while True:
+    
+        sleep(20)
+        
+        notarize_cmd = ["xcrun", "altool", "--notarization-info", 
request_uuid, "--username", username, "--password",  password, 
"--output-format", "xml"]
+        notarize_task = Popen(notarize_cmd, cwd=SOURCE_DIR, stdout=PIPE, 
stderr=PIPE)
+        [output, error] = notarize_task.communicate()
+        rc = notarize_task.returncode
+        assert rc == 0, "status request failed"
+        
+        output_stream = io.BytesIO(output)
+        output_pl = plistlib.readPlist(output_stream)
+        assert "notarization-info" in output_pl, "missing notarization-upload 
key in reply %s" % (output)
+        status = output_pl["notarization-info"]["Status"]
+            
+        if status == "invalid":
+            # open the URL
+            log_url = output_pl["notarization-info"]["LogFileURL"]
+            Popen(["/usr/bin/open", log_url])
+            break
+        elif status == "in progress":
+            sys.stderr.write("notarization status not available yet for %s\n" 
% (request_uuid))
+            continue
+        else:
+            # staple?
+            sys.stderr.write("notarization succeeded\n")
+            sys.stdout.write("%s\n" % (output))
+                        
+            log_url = output_pl["notarization-info"]["LogFileURL"]
+            Popen(["/usr/bin/open", log_url])
+            
+            break
+
+def create_dmg_of_application(new_version_number):
+    
+    # Create a name for the dmg based on version number, instead
+    # of date, since I sometimes want to upload multiple betas per day.
+    final_dmg_name = os.path.join(BUILD_DIR, 
os.path.splitext(os.path.basename(BUILT_APP))[0] + "-" + new_version_number + 
".dmg")
+    
+    temp_dmg_path = "/tmp/BibDesk.dmg"
+    if os.path.exists(temp_dmg_path):
+        os.unlink(temp_dmg_path)
+
+    nullDevice = open("/dev/null", "w")
+    cmd = ["/usr/bin/hdiutil", "create", "-fs", "HFS+", "-srcfolder", 
BUILT_APP, temp_dmg_path]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "hdiutil create failed"
+
+    cmd = ["/usr/bin/hdiutil", "convert", temp_dmg_path, "-format", "UDZO", 
"-imagekey", "zlib-level=9", "-o", final_dmg_name]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "hdiutil convert failed"
+
+    nullDevice.close()
+    os.unlink(temp_dmg_path)
+    
+    return final_dmg_name
+
+def prepare_dmg_of_application(new_version_number):
+    
+    # Create a name for the dmg based on version number, instead
+    # of date, since I sometimes want to upload multiple betas per day.
+    final_dmg_name = os.path.join(BUILD_DIR, 
os.path.splitext(os.path.basename(BUILT_APP))[0] + "-" + new_version_number + 
".dmg")
+    
+    # template image in source folder
+    zip_dmg_name = os.path.join(SOURCE_DIR, "BibDesk.dmg.zip")
+    
+    # temporary image
+    temp_dmg_path = "/tmp/BibDesk.dmg"
+    
+    # temporary volume
+    dst_volume_name = "/Volumes/BibDesk"
+    
+    # see if this file already exists and bail
+    assert not os.path.exists(final_dmg_name), "%s exists" % (final_dmg_name)
+    
+    # see if a volume is already mounted or a
+    # previous cp operation was botched
+    assert not os.path.exists(dst_volume_name), "%s exists" % (dst_volume_name)
+    
+    nullDevice = open("/dev/null", "w")
+    
+    # remove temp image from a previous run
+    if os.path.exists(temp_dmg_path):
+        unlink(temp_dmg_path)
+    
+    # stored zipped in svn, so unzip if needed
+    # pass o to overwrite, or unzip waits for stdin
+    # when trying to unpack the resource fork/EA
+    
+    cmd = ["/usr/bin/unzip", "-uo", zip_dmg_name, "-d", "/tmp"]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "failed to unzip %s" % (zip_dmg_name)
+    
+    # mount image
+    cmd = ["/usr/bin/hdiutil", "attach", "-nobrowse", "-noautoopen", 
temp_dmg_path]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "failed to mount %s" % (temp_dmg_path)
+    
+    # use cp to copy all files
+    cmd = ["/bin/cp", "-R", BUILT_APP, dst_volume_name]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "failed to copy %s" % (BUILT_APP)
+    
+    # tell finder to set the icon position
+    cmd = ["/usr/bin/osascript", "-e", """tell application "Finder" to set the 
position of application file "BibDesk.app" of disk named "BibDesk" to {204, 
148}"""]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "Finder failed to set position"
+    
+    # data is copied, so unmount the volume, we may need to wait when the 
volume is in use
+    n_tries = 0
+    cmd = ["/usr/sbin/diskutil", "eject", dst_volume_name]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    while rc != 0:
+        assert n_tries < 12, "failed to eject %s" % (dst_volume_name)
+        n_tries += 1
+        sleep(5)
+        x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+        rc = x.wait()
+    
+    # resize image to fit
+    cmd = ["/usr/bin/hdiutil", "resize", temp_dmg_path]
+    x = Popen(cmd, stdout=PIPE, stderr=nullDevice)
+    size = x.communicate()[0].split(None, 1)[0]
+    cmd = ["/usr/bin/hdiutil", "resize", "-size", size + "b", temp_dmg_path]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    assert rc == 0, "failed to resize  %s" % (temp_dmg_path)
+    
+    # convert image to read only and compress
+    cmd = ["/usr/bin/hdiutil", "convert", temp_dmg_path, "-format", "UDZO", 
"-imagekey", "zlib-level=9", "-o", final_dmg_name]
+    x = Popen(cmd, stdout=nullDevice, stderr=nullDevice)
+    rc = x.wait()
+    assert rc == 0, "failed to convert %s" % (temp_dmg_path)
+    
+    # remove temp image
+    nullDevice.close()
+    os.unlink(temp_dmg_path)
+
+def create_zip_of_application(new_version_number):
+    
+    # Create a name for the zip file based on version number, instead
+    # of date, since I sometimes want to upload multiple betas per day.
+    final_zip_name = os.path.join(BUILD_DIR, 
os.path.splitext(os.path.basename(BUILT_APP))[0] + "-" + new_version_number + 
".zip")
+    
+    nullDevice = open("/dev/null", "w")
+    cmd = ["/usr/bin/ditto", "-c", "-k", "--keepParent", BUILT_APP, 
final_zip_name]
+    x = Popen(cmd)
+    rc = x.wait()
+    assert rc == 0, "zip creation failed"
+    
+    return final_zip_name 
+
+def keyFromSecureNote():
+    
+    # see 
http://www.entropy.ch/blog/Developer/2008/09/22/Sparkle-Appcast-Automation-in-Xcode.html
+    pwtask = Popen(["/usr/bin/security", "find-generic-password", "-g", "-s", 
KEY_NAME], stdout=PIPE, stderr=PIPE)
+    [output, error] = pwtask.communicate()
+    pwoutput = output + error
+    
+    # notes are evidently stored as archived RTF data, so find start/end 
markers
+    start = pwoutput.find("-----BEGIN DSA PRIVATE KEY-----")
+    stopString = "-----END DSA PRIVATE KEY-----"
+    stop = pwoutput.find(stopString)
+    
+    assert start is not -1 and stop is not -1, "failed to find DSA key in 
secure note"
+    
+    key = pwoutput[start:stop] + stopString
+    
+    # replace RTF end-of-lines
+    key = key.replace("\\134\\012", "\n")
+    key = key.replace("\\012", "\n")
+    
+    return key
+    
+def signature_and_size(dmg_or_zip_path):
+    
+    # write to a temporary file that's readably only by owner; minor security 
issue here since
+    # we have to use a named temp file, but it's better than storing 
unencrypted key
+    keyFile = tempfile.NamedTemporaryFile()
+    keyFile.write(keyFromSecureNote())
+    keyFile.flush()
+
+    # now run the signature for Sparkle...
+    sha_task = Popen(["/usr/bin/openssl", "dgst", "-sha1", "-binary"], 
stdin=open(dmg_or_zip_path, "rb"), stdout=PIPE)
+    dss_task = Popen(["/usr/bin/openssl", "dgst", "-dss1", "-sign", 
keyFile.name], stdin=sha_task.stdout, stdout=PIPE)
+    b64_task = Popen(["/usr/bin/openssl", "enc", "-base64"], 
stdin=dss_task.stdout, stdout=PIPE)
+
+    # now compute the variables we need for writing the new appcast
+    appcastSignature = b64_task.communicate()[0].strip()
+    fileSize = str(os.stat(dmg_or_zip_path)[ST_SIZE])
+    
+    return appcastSignature, fileSize
+    
+def write_appcast(newVersion, newVersionString, minimumSystemVersion, 
dmg_or_zip_path, outputPath):
+    
+    appcast_signature, fileSize = signature_and_size(dmg_or_zip_path)
+    download_url = 
"https://sourceforge.net/projects/bibdesk/files/BibDesk/BibDesk-"; + 
newVersionString + "/" + os.path.basename(dmg_or_zip_path) + "/download"
+    appcastDate = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
+    if dmg_or_zip_path.endswith("dmg"):
+        type = "application/x-apple-diskimage"
+    else:
+        type = "application/zip"
+    
+    # creating this from a string is easier than manipulating NSXMLNodes...
+    newItemString = """<?xml version="1.0" encoding="utf-8"?>
+    <rss version="2.0" 
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle";  
xmlns:dc="http://purl.org/dc/elements/1.1/";>
+        <channel>
+           <item>
+                <title>Version """ + newVersionString + """</title>
+                <description>
+                <![CDATA[
+    <h1>Version ${VERSION}</h1>
+    
+    <h2>New Features</h2>
+    <ul>
+    <li></li>
+    </ul>
+    
+    <h2>Bugs Fixed</h2>
+    <ul>
+    <li></li>
+    </ul>
+                ]]>
+                </description>
+                <pubDate>""" + appcastDate + """</pubDate>
+                <sparkle:minimumSystemVersion>""" + minimumSystemVersion + 
"""</sparkle:minimumSystemVersion>
+                <enclosure url=\"""" + download_url + """\" 
sparkle:version=\"""" + newVersion + """\" sparkle:shortVersionString=\"""" + 
newVersionString + """\" length=\"""" + fileSize + """\" type=\"""" + type + 
"""\" sparkle:dsaSignature=\"""" + appcastSignature + """\" />
+            </item>
+        </channel>
+    </rss>
+    """
+    
+    # read from the source directory
+    appcastURL = NSURL.URLWithString_(APPCAST_URL)
+    
+    # xml doc from the current appcast
+    (oldDoc, error) = 
NSXMLDocument.alloc().initWithContentsOfURL_options_error_(appcastURL, 
NSXMLNodePreserveCDATA, None)
+    assert oldDoc is not None, error
+    
+    # xml doc from the new appcast string
+    (newDoc, error) = 
NSXMLDocument.alloc().initWithXMLString_options_error_(newItemString, 
NSXMLNodePreserveCDATA, None)
+    assert newDoc is not None, error
+    
+    # get an arry of the current item titles
+    (oldTitles, error) = oldDoc.nodesForXPath_error_("//item/title", None)
+    assert oldTitles.count > 0, "oldTitles had no elements"
+    
+    # now get the title we just created
+    (newTitles, error) = newDoc.nodesForXPath_error_("//item/title", None)
+    assert newTitles.count() is 1, "newTitles must have a single element"
+    
+    # easy test to avoid duplicating items
+    if oldTitles.containsObject_(newTitles.lastObject()) is False:
+        
+        # get the parent node we'll be inserting to
+        (parentChannel, error) = oldDoc.nodesForXPath_error_("//channel", None)
+        assert parentChannel.count() is 1, "channel count must be one"
+        parentChannel = parentChannel.lastObject()
+        
+        # now get the new node
+        (newNodes, error) = newDoc.nodesForXPath_error_("//item", None)
+        assert newNodes is not None, error
+        
+        # insert a copy of the new node
+        parentChannel.addChild_(newNodes.lastObject().copy())
+        
+        # write to user Desktop
+        appcastPath = os.path.join(outputPath , "bibdesk.xml")
+        
+        # write to NSData, since pretty printing didn't work with 
NSXMLDocument writing
+        
oldDoc.XMLDataWithOptions_(NSXMLNodePrettyPrint).writeToFile_atomically_(appcastPath,
 True)
+        
+    else:
+        
+        appcastPath = os.path.join(outputPath , "BibDesk-" + newVersionString 
+ ".xml")
+        appcastFile = open(appcastPath, "w")
+        appcastFile.write(newItemString)
+        appcastFile.close()
+
+def user_and_pass_for_upload():
+    
+    pwtask = Popen(["/usr/bin/security", "find-internet-password", "-g", "-s", 
UPLOAD_KEYCHAIN_ITEM, "-t", "dflt"], stdout=PIPE, stderr=PIPE)
+    [output, error] = pwtask.communicate()
+    pwoutput = output + error
+        
+    username = None
+    password = None
+    for line in pwoutput.split("\n"):
+        line = line.strip()
+        acct_prefix = "\"acct\"<blob>="
+        pw_prefix = "password: "
+        if line.startswith(acct_prefix):
+            assert username == None, "already found username"
+            username = line[len(acct_prefix):].strip("\"")
+        elif line.startswith(pw_prefix):
+            assert password == None, "already found password"
+            password = line[len(pw_prefix):].strip("\"")
+    
+    assert username and password, "unable to find username and password for 
%s" % (UPLOAD_KEYCHAIN_ITEM)
+    return username, password
+
+def get_options():
+    
+    identity = ""
+    username = ""
+    password = "@keychain:AC_PASSWORD"
+    out = os.path.join(os.getenv("HOME"), "Desktop")
+    
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "i:u:p:o:", ["identity=", 
"username=", "password=", "out="])
+    except:
+        sys.stderr.write("error reading options\n")
+    
+    for opt, arg in opts:
+        if opt in ["-i", "--identity"]:
+            identity = arg
+        elif opt in ["-u", "--username"]:
+            username = arg
+        elif opt in ["-p", "--password"]:
+            password = arg
+        elif opt in ["-o", "--out"]:
+            out = arg
+    
+    return identity, username, password, out
+
+if __name__ == '__main__':
+    
+    identity, username, password, out = get_options()
+    
+    clean_and_build()
+    
+    if identity != "":
+        codesign(identity)
+    
+    new_version, new_version_string, minimum_system_version = read_versions()
+    
+    dmg_or_zip_path = create_zip_of_application(new_version_string)
+    
+    if username != "":
+        # will bail if any part fails
+        notarize_dmg_or_zip(dmg_or_zip_path, username, password)
+        
+        if dmg_or_zip_path.endswith("dmg"):
+            # xcrun stapler staple BibDesk.app-1.4.zip
+            x = Popen(["xcrun", "stapler", "staple", dmg_or_zip_path])
+            rc = x.wait()
+            assert rc == 0, "stapler failed"
+        else:
+            # staple the application, then delete the zip we notarized
+            # and make a new zip of the stapled application, because stapler
+            # won't staple a damn zip file 
https://developer.apple.com/forums/thread/115670
+            x = Popen(["xcrun", "stapler", "staple", BUILT_APP])
+            rc = x.wait()
+            assert rc == 0, "stapler failed"
+            os.unlink(dmg_or_zip_path)
+            dmg_or_zip_path = create_zip_of_application(new_version_string)
+        
+    try:
+        # probably already exists
+        os.mkdirs(out)
+    except Exception as e:
+        assert os.path.isdir(out), "%s does not exist" % (out)
+    
+    write_appcast(new_version, new_version_string, minimum_system_version, 
dmg_or_zip_path, out)
+    
+    target_path = os.path.join(out, os.path.basename(dmg_or_zip_path))
+    if (os.path.exists(target_path)):
+        os.unlink(target_path)
+    os.rename(dmg_or_zip_path, target_path)


Property changes on: trunk/bibdesk/build_release.py
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.



_______________________________________________
Bibdesk-commit mailing list
Bibdesk-commit@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/bibdesk-commit

Reply via email to