sparkle_appcast 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1371a567e13d8c53f4625dc93c5a85a05121fcc8229f7d715abada093726da47
4
+ data.tar.gz: d6d952e901c2abe3fa1f1035b3ff900c68f963d0804475da26e3db6405889dd3
5
+ SHA512:
6
+ metadata.gz: 67a6eaed29a811a75075f3b47b61667d6e0fcd484641524a4b10dfc471310b6fb6ccf9d614c1ca28202bd0d4b29584342c11f01997c6219d114a8822470fec43
7
+ data.tar.gz: fb6649d00d9bd4be1fe454a9471781fb11afc81b31e6bf98b85f02186e997cabae86ed258a29cae8438d79932dc35ebf336dd4fe5dca67ad111ca18da0525895
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2018 Yoshimasa Niwa
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ sparkle_appcast
2
+ ===============
3
+
4
+
5
+ NAME
6
+ ----
7
+
8
+ `sparkle_appcast` -- A simple Sparkle `appcast.xml` tool
9
+
10
+
11
+ SYNOPSIS
12
+ --------
13
+
14
+ sparkle_appcast COMMAND [OPTIONS...] [ARGS...]
15
+
16
+
17
+ DESCRIPTION
18
+ -----------
19
+
20
+ `sparkle_appcast` is a Ruby gem that provides a command line interface and a Ruby library
21
+ to create and update `appcast.xml` for [Sparkle](https://sparkle-project.org).
22
+
23
+ `sparkle_appcast` command line interface takes next commands.
24
+
25
+ ### `appcast [OPTIONS...] FILE_PATH`
26
+
27
+ Create `appcast.xml` with an application archive at `FILE_PATH`.
28
+ The application archive file must contain exact one application bundle.
29
+
30
+ * `--key=KEY`
31
+
32
+ Path to DSA private key file. Required.
33
+
34
+ * `--url=URL`
35
+
36
+ URL to the application archive file published. Required.
37
+
38
+ * `--release-note=RELEASE_NOTE`
39
+
40
+ Path to a release note text file in Markdown format. Required.
41
+
42
+ * `--output=OUTPUT`
43
+
44
+ Path to an output `appcast.xml`. Optional.
45
+ Default to puts in the standard output.
46
+
47
+ * `--title=TITLE`
48
+
49
+ Title for the release note. Optional.
50
+ Default to `"{{Bundle Name}} {Bundle Short Version String}} ({{Bundle Version}})"`.
51
+
52
+ * `--publish-date=PUBLISH_DATE`
53
+
54
+ Publish date time in local timezone. Optional.
55
+ Default to the creation time of the application archive file.
56
+
57
+ * `--channel-title=CHANNEL_TITLE`
58
+
59
+ Title of the channel. Optional.
60
+ Default to `"Change log"`.
61
+
62
+ * `--channel-description=CHANNEL_DESCRIPTION`
63
+
64
+ Description of the channel. Optional.
65
+ Default to `"The most recent changes."`.
66
+
67
+ ### `sign [OPTIONS...] [FILE_PATH]`
68
+
69
+ Sign data at `FILE_PATH` or reading from the standard input with `DSA_PRIVATE_KEY_PATH`
70
+ and print signature that can be used in `appcast.xml`.
71
+ Use this for testing private key.
72
+
73
+ * `--key=KEY`
74
+
75
+ Path to DSA private key file. Required.
76
+
77
+ ### `markdown [FILE_PATH]`
78
+
79
+ Format Markdown text file at `FILE_PATH` or reading from the standard input in HTML.
80
+ Use this for writing the release note.
81
+
82
+
83
+ USAGE
84
+ -----
85
+
86
+ Use Ruby Gems to install `sparkle_appcast`.
87
+
88
+ gem install sparkle_appcast
89
+
90
+ Or use [bundler](http://bundler.io/), add next line to `Gemfile` in your project.
91
+
92
+ gem "sprkle_appcast"
93
+
94
+
95
+ SEE ALSO
96
+ --------
97
+
98
+ * [generate-key](https://github.com/sparkle-project/Sparkle/blob/master/bin/generate_keys)
99
+ * [sign-update](https://github.com/sparkle-project/Sparkle/blob/master/bin/sign_update)
100
+ * [generate-appcast](https://github.com/sparkle-project/Sparkle/tree/master/generate_appcast)
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
4
+
5
+ require "sparkle_appcast"
6
+
7
+ SparkleAppcast::Cli.start(ARGV)
@@ -0,0 +1,42 @@
1
+ module SparkleAppcast
2
+ class Appcast
3
+ attr_reader :signer, :archive, :release_note, :url, :params
4
+
5
+ def initialize(archive, signer, url, release_note, params = {})
6
+ @archive = archive
7
+ @signer = signer
8
+ @url = url
9
+ @release_note = release_note
10
+ @params = params
11
+ end
12
+
13
+ def rss
14
+ Rss.new(rss_params)
15
+ end
16
+
17
+ private
18
+
19
+ def rss_params
20
+ {
21
+ channel_link: archive.feed_url,
22
+ title: title,
23
+ description: release_note.html,
24
+ publish_date: archive.created_at,
25
+ url: url,
26
+ length: archive.size,
27
+ version: archive.bundle_version,
28
+ short_version_string: archive.bundle_short_version_string,
29
+ minimum_system_version: archive.minimum_system_version,
30
+ dsa_signature: dsa_signature
31
+ }.merge(params)
32
+ end
33
+
34
+ def title
35
+ "#{archive.bundle_name} #{archive.bundle_short_version_string} (#{archive.bundle_version})"
36
+ end
37
+
38
+ def dsa_signature
39
+ @dsa_signature ||= signer.sign(archive.data)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ require "tmpdir"
2
+ require "plist"
3
+
4
+ module SparkleAppcast
5
+ class Archive
6
+ attr_reader :path
7
+
8
+ def initialize(path)
9
+ @path = File.expand_path(path)
10
+ end
11
+
12
+ def bundle_name
13
+ info_plist["CFBundleDisplayName"] || info_plist["CFBundleName"]
14
+ end
15
+
16
+ def bundle_version
17
+ info_plist["CFBundleVersion"]
18
+ end
19
+
20
+ def bundle_short_version_string
21
+ info_plist["CFBundleShortVersionString"]
22
+ end
23
+
24
+ def minimum_system_version
25
+ info_plist["LSMinimumSystemVersion"]
26
+ end
27
+
28
+ def feed_url
29
+ info_plist["SUFeedURL"]
30
+ end
31
+
32
+ def created_at
33
+ File.birthtime(path)
34
+ end
35
+
36
+ def size
37
+ File.size(path)
38
+ end
39
+
40
+ def data
41
+ File.binread(path)
42
+ end
43
+
44
+ private
45
+
46
+ # TODO: Support localizable string.
47
+
48
+ def info_plist
49
+ @info_plist ||= Dir.mktmpdir do |tmpdir_path|
50
+ case File.basename(path)
51
+ when /\.zip\z/i
52
+ Kernel.system("/usr/bin/ditto", "-x", "-k", path, tmpdir_path)
53
+ when /\.tar\z/i
54
+ Kernel.system("/usr/bin/tar", "-x", "-f", path, "-C", tmpdir_path)
55
+ when /\.tar\.gz\z/i, /\.tgz\z/i, /\.tar\.xz\z/i, /\.txz\z/i, /\.tar\.lzma\z/i
56
+ Kernel.system("/usr/bin/tar", "-x", "-z", "-f", path, "-C", tmpdir_path)
57
+ when /\.tar\.bz2\z/i, /\.tbz\z/i
58
+ Kernel.system("/usr/bin/tar", "-x", "-j", "-f", path, "-C", tmpdir_path)
59
+ else
60
+ raise NotImplementedError.new("Disk image support is not implemented yet.")
61
+ end
62
+
63
+ unless $?.success?
64
+ raise RuntimeError.new("Failed to expand archive: #{path}")
65
+ end
66
+
67
+ app_paths = Dir.glob(File.join(tmpdir_path, "*.app"), File::FNM_CASEFOLD)
68
+ if app_paths.size == 0
69
+ raise RuntimeError.new("No application bundle found: #{path}")
70
+ elsif app_paths.size > 1
71
+ raise RuntimeError.new("Found multiple application bundles: #{app_paths.map{|path| File.basename(path)}}")
72
+ else
73
+ app_path = app_paths.first
74
+ info_plist_path = File.join(app_path, "Contents", "Info.plist")
75
+ Plist.parse_xml(info_plist_path)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ require "rubygems"
2
+ require "thor"
3
+ require "kramdown"
4
+ require "time"
5
+
6
+ module SparkleAppcast
7
+ class Cli < Thor
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ desc "appcast FILE", "Create `appcast.xml` with an application archive file."
13
+ long_desc <<-END_OF_DESCRIPTION
14
+ Create `appcast.xml` with an application archive file.
15
+ The application archive file must contain exact one application bundle.
16
+ END_OF_DESCRIPTION
17
+ option :key, type: :string, required: true, desc: "Path to a DSA private key."
18
+ option :url, type: :string, required: true, desc: "URL to the application archive file published."
19
+ option :release_note, type: :string, required: true, desc: "Path to a release note text file in Markdown format."
20
+ option :output, type: :string, desc: "Path to an output `appcast.xml`."
21
+ option :title, type: :string, desc: "Title for the release note."
22
+ option :publish_date, type: :string, desc: "Publish date time in local timezone."
23
+ option :channel_title, type: :string, default: "Change log", desc: "Title of the channel."
24
+ option :channel_description, type: :string, default: "The most recent changes.", desc: "Description of the channel."
25
+ def appcast(file)
26
+ params = {
27
+ channel_title: options[:channel_title],
28
+ channel_description: options[:channel_description]
29
+ }
30
+ params[:title] = options[:title] if options[:title]
31
+ params[:publish_date] = Time.parse(options[:publish_date]) if options[:publish_date]
32
+
33
+ appcast = Appcast.new(
34
+ Archive.new(file),
35
+ Signer.new(options[:key]),
36
+ options[:url],
37
+ ReleaseNote.new(options[:release_note]),
38
+ params
39
+ )
40
+
41
+ rss = appcast.rss.to_s
42
+ if options[:output]
43
+ File.open(options[:output], "w") do |output|
44
+ output.puts(rss)
45
+ end
46
+ else
47
+ STDOUT.puts(rss)
48
+ end
49
+ end
50
+
51
+ desc "sign [FILE]", "Sign a file with a DSA private key."
52
+ option :key, type: :string, required: true, desc: "Path to a DSA private key."
53
+ def sign(file = nil)
54
+ source = if file
55
+ File.binread(file)
56
+ else
57
+ STDIN.read
58
+ end
59
+ puts Signer.new(options[:key]).sign(source)
60
+ end
61
+
62
+ desc "markdown [FILE]", "Format Markdown text file in HTML."
63
+ def markdown(file = nil)
64
+ text = if file
65
+ File.read(file)
66
+ else
67
+ STDIN.read
68
+ end
69
+ puts ReleaseNote.markdown(text)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,21 @@
1
+ module SparkleAppcast
2
+ class ReleaseNote
3
+ def self.markdown(text)
4
+ Kramdown::Document.new(text, auto_ids: false).to_html
5
+ end
6
+
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def html
14
+ @html ||= self.class.markdown(text)
15
+ end
16
+
17
+ def text
18
+ @text ||= File.read(path)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,88 @@
1
+ require "rexml/document"
2
+ require "time"
3
+
4
+ module SparkleAppcast
5
+ class Rss
6
+ REQUIRED_FILEDS = [
7
+ :url,
8
+ :length,
9
+ :version,
10
+ :dsa_signature
11
+ ]
12
+
13
+ attr_reader :params
14
+
15
+ def initialize(params)
16
+ REQUIRED_FILEDS.each do |field|
17
+ unless params[field]
18
+ raise ArgumentError.new("Missing #{field} param")
19
+ end
20
+ end
21
+ @params = params
22
+ end
23
+
24
+ def to_s
25
+ StringIO.new.tap do |output|
26
+ output << %(<?xml version="1.0" encoding="UTF-8"?>\n)
27
+ formatter = REXML::Formatters::Pretty.new(2)
28
+ formatter.compact = true
29
+ formatter.write(document, output)
30
+ end.string
31
+ end
32
+
33
+ private
34
+
35
+ def document
36
+ @document ||= REXML::Document.new.tap do |document|
37
+ document.context = {
38
+ # Use double quote for attributes escape.
39
+ attribute_quote: :quote
40
+ }
41
+
42
+ # <rss ... > ... </rss>
43
+ document.add_element("rss").tap do |rss|
44
+ rss.add_namespace("xmlns:sparkle", "http://www.andymatuschak.org/xml-namespaces/sparkle")
45
+ rss.add_namespace("xmlns:dc", "http://purl.org/dc/elements/1.1/")
46
+ rss.add_attribute("version", "2.0")
47
+
48
+ # <channel> ... </channel>
49
+ rss.add_element("channel").tap do |channel|
50
+ channel.add_element("title").add_text(params[:channel_title]) if params[:channel_title]
51
+ channel.add_element("description").add_text(params[:channel_description]) if params[:channel_description]
52
+ channel.add_element("link").add_text(params[:channel_link]) if params[:channel_link]
53
+ channel.add_element("language").add_text(params[:language]) if params[:language]
54
+
55
+ # <item> ... </item>
56
+ channel.add_element("item").tap do |item|
57
+ item.add_element("title").add_text(params[:title]) if params[:title]
58
+ item.add_element("description").add(REXML::CData.new(params[:description])) if params[:description]
59
+ item.add_element("pubDate").add_text(publish_date) if publish_date
60
+
61
+ # <enclosure ... />
62
+ item.add_element("enclosure").tap do |enclosure|
63
+ enclosure.add_attribute("url", params[:url])
64
+ enclosure.add_attribute("type", "application/octet-stream")
65
+ enclosure.add_attribute("length", params[:length])
66
+ enclosure.add_attribute("sparkle:version", params[:version])
67
+ enclosure.add_attribute("sparkle:shortVersionString", params[:short_version_string]) if params[:short_version_string]
68
+ enclosure.add_attribute("sparkle:dsaSignature", params[:dsa_signature])
69
+ enclosure.add_attribute("sparkle:minimumSystemVersion", params[:minimum_system_version]) if params[:minimum_system_version]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def publish_date
78
+ case params[:publish_date]
79
+ when nil
80
+ nil
81
+ when Time
82
+ params[:publish_date].utc.rfc2822
83
+ else
84
+ params[:publish_date]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,26 @@
1
+ require "openssl"
2
+ require "base64"
3
+
4
+ module SparkleAppcast
5
+ class Signer
6
+ attr_reader :private_key_path
7
+
8
+ def initialize(private_key_path)
9
+ @private_key_path = private_key_path
10
+ end
11
+
12
+ def sign(data)
13
+ # Sparkle is signing SHA1 digest with DSA private key and encoding it in Base64.
14
+ # See <https://github.com/sparkle-project/Sparkle/blob/master/bin/sign_update>.
15
+ digest = OpenSSL::Digest::SHA1.digest(data)
16
+ signature = private_key.sign(OpenSSL::Digest::SHA1.new, digest)
17
+ Base64.strict_encode64(signature)
18
+ end
19
+
20
+ private
21
+
22
+ def private_key
23
+ @private_key ||= OpenSSL::PKey::DSA.new(File.binread(private_key_path))
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ module SparkleAppcast
2
+ autoload :Appcast, "sparkle_appcast/appcast"
3
+ autoload :Archive, "sparkle_appcast/archive"
4
+ autoload :Cli, "sparkle_appcast/cli"
5
+ autoload :ReleaseNote, "sparkle_appcast/release_note"
6
+ autoload :Rss, "sparkle_appcast/rss"
7
+ autoload :Signer, "sparkle_appcast/signer"
8
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sparkle_appcast
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yoshimasa Niwa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: kramdown
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: plist
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Generate Sparkle appcast.xml
84
+ email:
85
+ - niw@niw.at
86
+ executables:
87
+ - sparkle_appcast
88
+ extensions: []
89
+ extra_rdoc_files:
90
+ - LICENSE
91
+ - README.md
92
+ files:
93
+ - LICENSE
94
+ - README.md
95
+ - bin/sparkle_appcast
96
+ - lib/sparkle_appcast.rb
97
+ - lib/sparkle_appcast/appcast.rb
98
+ - lib/sparkle_appcast/archive.rb
99
+ - lib/sparkle_appcast/cli.rb
100
+ - lib/sparkle_appcast/release_note.rb
101
+ - lib/sparkle_appcast/rss.rb
102
+ - lib/sparkle_appcast/signer.rb
103
+ homepage:
104
+ licenses: []
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.7.3
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Generate Sparkle appcast.xml
126
+ test_files: []