From a503b5793c20d8f143a829fee483f1013e93023c Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Sat, 26 Dec 2015 15:21:22 +0100 Subject: [PATCH 1/5] Add support for appcast generation Appcasts are a RSS extension[1] that can be used to publish updates to client. It is used by the popular sparkle[2] framework to publish updates to OS X clients. [1] http://microformats.org/wiki/appcast-formats [2] http://sparkle-project.org --- src/app/FakeLib/Appcast.fs | 93 ++++++++++++++++++++++++++++++++++ src/app/FakeLib/FakeLib.fsproj | 1 + 2 files changed, 94 insertions(+) create mode 100644 src/app/FakeLib/Appcast.fs diff --git a/src/app/FakeLib/Appcast.fs b/src/app/FakeLib/Appcast.fs new file mode 100644 index 00000000000..5dfd817262b --- /dev/null +++ b/src/app/FakeLib/Appcast.fs @@ -0,0 +1,93 @@ +/// Contains code to configure FAKE for Appcast handling +module Fake.Appcast +open System.Xml +open System.Xml.Linq + + +//private XMLHelper inspired by https://nbevans.wordpress.com/2015/04/15/super-skinny-xml-document-generation-with-f/ +let private XDeclaration version encoding standalone = XDeclaration(version, encoding, standalone) +let private XName expandedName = XName.Get(expandedName) +let private XDocument xdecl content = XDocument(xdecl, content |> Seq.map (fun v -> v :> obj) |> Seq.toArray) +let private XElement expandedName content = XElement(XName expandedName, content |> Seq.map (fun v -> v :> obj) |> Seq.toArray) :> obj +let private XAttribute expandedName value = XAttribute(XName expandedName, value) :> obj +let private XAttributeXName expandedName value = Linq.XAttribute(expandedName, value) :> obj + +/// Sparkel namespace used for RSS extension +let private sparkle = XNamespace.Get("http://www.andymatuschak.org/xml-namespaces/sparkle") + +/// Mime type of the download file +type MimeType = + /// Octetstream use for exe or zip files + | OctetStream + /// Custom mimetype + | Custom of string + +/// Download details for the appcast +type AppcastItem = { + /// The name of the update + title : string; + /// Date when update is published + pubdate : System.DateTime; + /// URI where the update files are found + url : System.Uri; + /// Machine readable version number used to determine if an update is available by the client (should follow semver) + version : string; + /// Optional human readable version number. This will be shown to the user if present otherwise + /// the technical version number will be used + shortVersion : string option; + /// Mime type of the update file, usualy octetstream + mimetype : MimeType +} + +/// Configuration data for the appcast +type Appcast = { + /// A titel, usually the app name + title : string; + /// Short description + description : string; + /// Language of your app + language : string; + /// Updates published to client, can habe multiple updates e.g. for different OS versions + items : AppcastItem list; +} + +/// writes an appcast to a file +let writeAppcast (path : string) (cast : Appcast) = + let toXml (cast : Appcast) = + let mtToString mimetype = + match mimetype with + | OctetStream -> "application/octet-stream" + | Custom(s) -> s + let choose a b = + match a with + | Some(c) -> c + | None -> b + + let item (e : AppcastItem) = + XElement "item" [ + XElement "title" e.title + XElement "pubDate" (e.pubdate.ToString("r")) + XElement "enclosure" [ + XAttribute "url" e.url + XAttributeXName (sparkle + "version") e.version + XAttribute "type" (mtToString e.mimetype) + XAttributeXName (sparkle + "shortVersionString") (choose e.shortVersion e.version) + ] + ] + + let doc = XDocument (XDeclaration "1.0" "UTF-8" "no") [ + XElement "rss" [ + XAttribute "version" "2.0" + XAttributeXName (XNamespace.Xmlns + "sparkle") "http://www.andymatuschak.org/xml-namespaces/sparkle" + XAttributeXName (XNamespace.Xmlns + "dc") "http://purl.org/dc/elements/1.1/" + XElement "channel" ([ + XElement "title" cast.title + XElement "description" cast.description + XElement "language" cast.language] + @ List.map item cast.items) + ] + ] + doc + let xml = toXml cast + use writer = XmlWriter.Create(path) + xml.Save(writer) \ No newline at end of file diff --git a/src/app/FakeLib/FakeLib.fsproj b/src/app/FakeLib/FakeLib.fsproj index 62ad2572b02..a23969d2fdd 100644 --- a/src/app/FakeLib/FakeLib.fsproj +++ b/src/app/FakeLib/FakeLib.fsproj @@ -166,6 +166,7 @@ + From 649ff6b1b1a4821ae54a4b171788438cb3bfabe0 Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Sat, 26 Dec 2015 16:57:26 +0100 Subject: [PATCH 2/5] add optional version and signature --- src/app/FakeLib/Appcast.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/FakeLib/Appcast.fs b/src/app/FakeLib/Appcast.fs index 5dfd817262b..2160cdc55a3 100644 --- a/src/app/FakeLib/Appcast.fs +++ b/src/app/FakeLib/Appcast.fs @@ -37,6 +37,10 @@ type AppcastItem = { shortVersion : string option; /// Mime type of the update file, usualy octetstream mimetype : MimeType + /// Optional DSA signature for the archive. It is recommended to use this if the app itself is not signed + dsaSignature : string option; + /// Optional miminal system version for the update + minimumSystemVersion : string option; } /// Configuration data for the appcast From fd27f7f0b7c7c9835eadefada8a7887af48cbfc9 Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Tue, 12 Jan 2016 21:26:57 +0100 Subject: [PATCH 3/5] appcast: add missing fields --- src/app/FakeLib/Appcast.fs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/FakeLib/Appcast.fs b/src/app/FakeLib/Appcast.fs index 2160cdc55a3..335cfeaec18 100644 --- a/src/app/FakeLib/Appcast.fs +++ b/src/app/FakeLib/Appcast.fs @@ -41,6 +41,8 @@ type AppcastItem = { dsaSignature : string option; /// Optional miminal system version for the update minimumSystemVersion : string option; + /// Length of the file in bytes + length : uint32; } /// Configuration data for the appcast @@ -66,17 +68,28 @@ let writeAppcast (path : string) (cast : Appcast) = match a with | Some(c) -> c | None -> b - + let appendSome option consequence content = + match option with + | Some(data) -> (consequence data) :: content + | None -> content + + let item (e : AppcastItem) = + let appendMinimumVersion = appendSome e.minimumSystemVersion (fun d -> XAttributeXName (sparkle + "minimumSystemVersion") d) + let appendSig = appendSome e.dsaSignature (fun d -> XAttributeXName (sparkle + "dsaSignature") d) + XElement "item" [ XElement "title" e.title XElement "pubDate" (e.pubdate.ToString("r")) - XElement "enclosure" [ + XElement "enclosure" ([ XAttribute "url" e.url XAttributeXName (sparkle + "version") e.version XAttribute "type" (mtToString e.mimetype) + XAttribute "length" e.length XAttributeXName (sparkle + "shortVersionString") (choose e.shortVersion e.version) - ] + ] + |> appendMinimumVersion + |> appendSig) ] let doc = XDocument (XDeclaration "1.0" "UTF-8" "no") [ From 6d7951230cfe5565d247c5d1b157c29abdd9ce9b Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Sun, 17 Jan 2016 23:20:50 +0100 Subject: [PATCH 4/5] make length int64 for better interop with System.IO.FileInfo --- src/app/FakeLib/Appcast.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/FakeLib/Appcast.fs b/src/app/FakeLib/Appcast.fs index 335cfeaec18..016d039f8fd 100644 --- a/src/app/FakeLib/Appcast.fs +++ b/src/app/FakeLib/Appcast.fs @@ -42,7 +42,7 @@ type AppcastItem = { /// Optional miminal system version for the update minimumSystemVersion : string option; /// Length of the file in bytes - length : uint32; + length : int64; } /// Configuration data for the appcast From 8e419fdff46e794a7aec4601df9a74e55580d2ed Mon Sep 17 00:00:00 2001 From: Kolja Dummann Date: Sun, 17 Jan 2016 23:21:21 +0100 Subject: [PATCH 5/5] Add docs --- src/app/FakeLib/Appcast.fs | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/app/FakeLib/Appcast.fs b/src/app/FakeLib/Appcast.fs index 016d039f8fd..809a1550c53 100644 --- a/src/app/FakeLib/Appcast.fs +++ b/src/app/FakeLib/Appcast.fs @@ -58,6 +58,51 @@ type Appcast = { } /// writes an appcast to a file +/// ##Parameters +/// +/// - `path` - The file where the appcast should be written +/// - `cast` - The appcast to write +/// ##Sample +/// // This target creates the app cast for our app. I contains two version 1.X and 2.X while 2.X requires at least OS X 10.10 Yosemite. +/// Target "CreateAppcast" (fun _ -> +/// let server = "https://example.com/files/" +/// let fileLength file = +/// let info = new System.IO.FileInfo(file) +/// info.Length +/// +/// let latestSize = fileLength "build/download-2.0.1.zip" +/// let legacySize = fileLength "build/download-1.1.4.zip" +/// +/// { +/// title = "My Awesome App" +/// description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus." +/// language = "en" +/// items = [ +/// { +/// title = "Hawk Nickel Greyhound" +/// pubdate = System.DateTime.Now +/// url = new System.Uri(server + "download-2.0.1.zip") +/// version = "2014" // This our internal build number +/// shortVersion = Some("2.0.1") //This is what we show to the user +/// mimetype = OctetStream +/// minimumSystemVersion = Some("10.10") +/// length = latestSize +/// dsaSignature = None +/// }; +/// { +/// title = "Sparrow Platinum Beagle" +/// pubdate = System.DateTime.Now +/// url = new System.Uri(server + "download-1.1.4.zip") +/// version = "1142" // This our internal build number +/// shortVersion = Some("1.1.4") //This is what we show to the user +/// mimetype = OctetStream +/// length = legacySize +/// minimumSystemVersion = None +/// dsaSignature = None +/// } +/// ] +/// } |> writeAppcast "build/updates.xml" +/// ) let writeAppcast (path : string) (cast : Appcast) = let toXml (cast : Appcast) = let mtToString mimetype =