| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 
 | #!/usr/bin/env python2
# This script generates three app cast feeds for macOS Sparkle updates from Swift releases in the download folder on the Swift website.
from xml.etree import ElementTree as ET
import argparse
import datetime
import email.utils as eut
import fnmatch
import jinja2
import os.path
import re
import time
import urllib2
import urlparse
class Release:
    def __init__(self, version, absoluteURL, sizeInBytes, date):
        # This is the version string used for update detection.
        self.fullVersion = version.split('-', 1)[1]
        # This is a human readable version string, only used for presentation.
        self.presentationVersion = version
        self.url = absoluteURL
        self.sizeInBytes = sizeInBytes
        self.date = date
        dateTumple = date.timetuple()
        dateTimestamp = time.mktime(dateTumple)
        self.dateString = eut.formatdate(dateTimestamp)
    def __str__(self):
        return "Release(%s, %s, %s, %s)" % (self.fullVersion, self.url, self.sizeInBytes, self.date)
    def __repr__(self):
        return "Release(%s, %s, %s, %s)" % (self.fullVersion, self.url, self.sizeInBytes, self.date)
def getReleaseFromAbsoluteFilePath(absolutePath, downloadsFolder, releasesURL):
    version = os.path.splitext(absolutePath.split('/')[-1])[0]
    sizeInBytes = os.path.getsize(absolutePath)
    date = datetime.datetime.fromtimestamp(os.path.getmtime(absolutePath))
    absoluteURL = urlparse.urljoin(releasesURL, os.path.relpath(absolutePath, downloadsFolder))
    return Release(version, absoluteURL, sizeInBytes, date)
def getReleaseFromReleaseFolder(releaseFolder, downloadsFolder, releasesURL, extension):
    release = None
    regex = re.compile(fnmatch.translate(extension))
    files = [f for f in os.listdir(releaseFolder) if os.path.isfile(os.path.join(releaseFolder, f))]
    for file in files:
        fileFullPath = os.path.join(releaseFolder, file)
        if regex.match(fileFullPath):
            release = getReleaseFromAbsoluteFilePath(fileFullPath, downloadsFolder, releasesURL)
    return release
def getReleaseFilesInReleasesFolder(releasesFolder, releasesURL, extension):
    releases = []
    dirs = [d for d in os.listdir(releasesFolder) if os.path.isdir(os.path.join(releasesFolder, d))]
    for d in dirs:
        release = getReleaseFromReleaseFolder(os.path.join(releasesFolder, d), releasesFolder, releasesURL, extension)
        if release:
            releases.append(release)
    return releases
def getReleaseFilesInDevelopmentFolder(developmentMacFolder, developmentMacURL, extension):
    extensionRegex = re.compile(fnmatch.translate(extension))
    devPatternRegex = re.compile(".+-dev\d+")
    releases = []
    files = [f for f in os.listdir(developmentMacFolder) if os.path.isfile(os.path.join(developmentMacFolder, f))]
    for f in files:
        # Only use dev builds from the development folder.
        if devPatternRegex.match(f):
            fileFullPath = os.path.join(developmentMacFolder, f)
            if extensionRegex.match(fileFullPath):
                releases.append(getReleaseFromAbsoluteFilePath(fileFullPath, developmentMacFolder, developmentMacURL))
    return releases
def writeAppcastFile(filename, title, description, regexPattern, appcastURL, releases):
    template = jinja2.Template('''<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
    <channel>
    <title>{{ title }}</title>
    <link>{{ appcast_url }}</link>
    <description>{{ description }}</description>
    <language>en</language>
    {% for item in releases %}<item>
        <title>Swift version {{ item.fullVersion }}</title>
            <pubDate>{{ item.dateString }}</pubDate>
            <enclosure url="{{ item.url }}"
                sparkle:version="{{ item.fullVersion }}"
                sparkle:shortVersionString="{{ item.presentationVersion }}"
                length="{{ item.sizeInBytes }}"
                type="application/octet-stream" />
    </item>
    {% endfor %}</channel>
</rss>''')
    matchingReleases = [i for i in releases if re.match(regexPattern, i.fullVersion)]
    matchingReleases = matchingReleases[:2] # only include the first two matches in the appcast
    appcastContent = template.render(title=title, appcast_url=appcastURL, description=description, releases=matchingReleases)
    contentParsesOK = False
    try:
        x = ET.fromstring(appcastContent)
        contentParsesOK = True
    except :
        contentParsesOK = False
    if contentParsesOK:
        with open(filename, 'w') as file:
            file.write(appcastContent)
    else:
        print("Failed to generate valid appcast feed %s." % filename)
parser = argparse.ArgumentParser(description='Generate stable/testing/development appcast feeds for Sparkle updater.')
parser.add_argument('downloadsFolder', type=str, help="e.g. /Users/foo/website/downloads/")
parser.add_argument('downloadsURL', type=str, help="e.g. https://swift.im/downloads/")
parser.add_argument('outputFolder', type=str, help="e.g. /Users/foo/website/downloads/")
args = parser.parse_args()
releasesPath = os.path.join(args.downloadsFolder, "releases")
developmentMacPath = os.path.join(args.downloadsFolder, "development", "mac")
manualReleases = getReleaseFilesInReleasesFolder(releasesPath, urlparse.urljoin(args.downloadsURL, "releases/"), "*.dmg")
manualReleases.sort(key=lambda release: release.date, reverse=True)
automaticReleases = manualReleases
automaticReleases.extend(getReleaseFilesInDevelopmentFolder(developmentMacPath, urlparse.urljoin(args.downloadsURL, "development/mac/"), "*.dmg"))
automaticReleases.sort(key=lambda release: release.date, reverse=True)
writeAppcastFile(filename=os.path.join(args.outputFolder, "swift-stable-appcast-mac.xml"),
    title="Swift Stable Releases",
    description="",
    regexPattern="^\d+(\.\d+)?(\.\d+)?$",
    appcastURL=urlparse.urljoin(args.downloadsURL, "swift-stable-appcast-mac.xml"),
    releases=manualReleases)
writeAppcastFile(filename=os.path.join(args.outputFolder, "swift-testing-appcast-mac.xml"),
    title="Swift Testing Releases",
    description="",
    regexPattern="^\d+(\.\d+)?(\.\d+)?(beta\d+)?(rc\d+)?$",
    appcastURL=urlparse.urljoin(args.downloadsURL, "swift-testing-appcast-mac.xml"),
    releases=manualReleases)
writeAppcastFile(filename=os.path.join(args.outputFolder, "swift-development-appcast-mac.xml"),
    title="Swift Development Releases",
    description="",
    regexPattern="^\d+(\.\d+)?(\.\d+)?(alpha\d*)?(beta\d+)?(rc\d+)?(-dev\d+)?$",
    appcastURL=urlparse.urljoin(args.downloadsURL, "swift-development-appcast-mac.xml"),
    releases=automaticReleases)
 |