summaryrefslogtreecommitdiffstats
blob: e7493df2ece1a9d2bcc8cc6e0c03db35c73235e3 (plain)
1
2
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)