ognivo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +89 -0
- data/bin/spark +8 -0
- data/lib/ognivo/agvtool.rb +8 -0
- data/lib/ognivo/appcast.rb +71 -0
- data/lib/ognivo/build.rb +127 -0
- data/lib/ognivo/cli.rb +14 -0
- data/lib/ognivo/cli_helpers.rb +8 -0
- data/lib/ognivo/commands/build.rb +38 -0
- data/lib/ognivo/commands/init.rb +33 -0
- data/lib/ognivo/commands/release.rb +69 -0
- data/lib/ognivo/commands/upload.rb +40 -0
- data/lib/ognivo/s3client.rb +40 -0
- data/lib/ognivo/upload.rb +140 -0
- data/lib/ognivo/utils.rb +17 -0
- data/lib/ognivo/version.rb +3 -0
- data/lib/ognivo/xcodebuild.rb +113 -0
- data/lib/ognivo.rb +14 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 889f43c8d630e634474055e081089c1dc8c9b4f9
|
4
|
+
data.tar.gz: a7b60eec0a81b8b0567cb96dc5dbef80e8642039
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4d0f0193fc3a57686bab29604a369635e2baaa5a860e1de07d561952c426b98cb6250c38452f8adfdd3c77b97cfab064ef6bc822d2b9d62455840d029911e830
|
7
|
+
data.tar.gz: bd6120a79231e77962f254874268caeadde9bb9ac4f32a7c081bdc783a1fadc4a68800c2863f87feabecd7c70c8f10745f99d558e7508e864f6bb4f42f7da78e
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Anatoliy Plastinin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Ognivo
|
2
|
+
|
3
|
+
Automates MacOS app updates distributing using the [Sparkle project](https://github.com/sparkle-project/Sparkle) and [Amazon S3](http://aws.amazon.com/s3/).
|
4
|
+
|
5
|
+
> **NOTE** about gem's name:
|
6
|
+
> *ognivo* is a russian word that means *fire striker*,
|
7
|
+
> so it is a device for making sparkles.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Execute:
|
12
|
+
|
13
|
+
$ gem install ognivo
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Gem adds `spark` command-line app. The app provides following commands:
|
18
|
+
|
19
|
+
build Builds and packs your app into zip archive
|
20
|
+
init Creates an appcast
|
21
|
+
upload Uploads app archive and updates appcast
|
22
|
+
release Builds and packs your app into zip archive
|
23
|
+
|
24
|
+
### Building and Archiving
|
25
|
+
|
26
|
+
$ cd /path/to/XCodeProject
|
27
|
+
$ spark build
|
28
|
+
|
29
|
+
Ognivo will find workspace/project file and build default configuration,
|
30
|
+
after that it will pack it into zip archive and save it in a current directory.
|
31
|
+
|
32
|
+
Default behavior can be overriden using these options:
|
33
|
+
|
34
|
+
-w, --workspace WORKSPACE_FILE Workspace (.xcworkspace) file to use to build app (automatically detected in current directory)
|
35
|
+
-c, --configuration CONFIGURATION Configuration used to build
|
36
|
+
-s, --scheme SCHEME Scheme used to build app
|
37
|
+
-d, --destination DESTINATION Destination. Defaults to current directory
|
38
|
+
|
39
|
+
### Preparing an appcast
|
40
|
+
|
41
|
+
First, you have to create an S3 bucket that will be used to store an appcast and
|
42
|
+
update files. Then run:
|
43
|
+
|
44
|
+
$ spark init -a aws_access_key -s aws_secret_key -b bucket_name
|
45
|
+
|
46
|
+
Tool will ask a few questions to prepare an empty appcast, and then app will
|
47
|
+
upload new `appcast.xml` file into provided bucket.
|
48
|
+
|
49
|
+
You can change default appcast file name using `-c, --appcast` option.
|
50
|
+
|
51
|
+
### Distributing an update
|
52
|
+
|
53
|
+
$ spark upload -a aws_access_key -s aws_secret_key -b bucket_name MyApp.zip
|
54
|
+
|
55
|
+
The tool will ask you for update's title, version and release notes, and then it
|
56
|
+
uploads specified file and updates appcast with new version.
|
57
|
+
|
58
|
+
If you don't sign your app with your Developer Certificate,
|
59
|
+
you can specify DSA private key to calculate code signature for Sparkle using
|
60
|
+
`-d, --dsa-private-key` option.
|
61
|
+
Read more about code signing
|
62
|
+
[here](https://github.com/sparkle-project/Sparkle/wiki#3-segue-for-security-concerns).
|
63
|
+
|
64
|
+
### Releasing an update
|
65
|
+
|
66
|
+
Most of the time you will use `release` command, that builds a new version and
|
67
|
+
then uploads archive.
|
68
|
+
|
69
|
+
$ spark release -a aws_access_key -s aws_secret_key -b bucket_name
|
70
|
+
|
71
|
+
The `release` command supports options for both `build` and `upload` commands.
|
72
|
+
It will try to get application version from Xcode project settings.
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Contributions are welcome.
|
77
|
+
|
78
|
+
## Thanks
|
79
|
+
|
80
|
+
Ognivo is inspired by [shenzhen gem](https://github.com/nomad/shenzhen).
|
81
|
+
Many thanks to [@mattt](https://github.com/mattt) for his work on such awesome tool.
|
82
|
+
|
83
|
+
## Credits
|
84
|
+
|
85
|
+
Ognivo is by [Anatoliy Plastinin](http://antlypls.com).
|
86
|
+
|
87
|
+
## License
|
88
|
+
|
89
|
+
MIT.
|
data/bin/spark
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Ognivo
|
4
|
+
class Appcast
|
5
|
+
class Item < Struct.new(:title, :description, :sparkle_version, :pub_date, :url,
|
6
|
+
:length, :type, :dsa_signature)
|
7
|
+
def to_node(xml)
|
8
|
+
xml.item do
|
9
|
+
xml.title title
|
10
|
+
xml.description do
|
11
|
+
xml.cdata description
|
12
|
+
end
|
13
|
+
xml.pubDate pub_date.rfc2822
|
14
|
+
xml.enclosure enclosure
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def enclosure
|
21
|
+
{
|
22
|
+
'url' => url,
|
23
|
+
'sparkle:version' => sparkle_version,
|
24
|
+
'length' => length,
|
25
|
+
'type' => type,
|
26
|
+
'sparkle:dsaSignature' => dsa_signature
|
27
|
+
}.reject { |_, v| v.nil? }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :title
|
32
|
+
attr_accessor :link
|
33
|
+
attr_accessor :description
|
34
|
+
attr_accessor :language
|
35
|
+
|
36
|
+
RSS_ATTRIBUTES = {
|
37
|
+
'version' => '2.0',
|
38
|
+
'xmlns:sparkle' => 'http://www.andymatuschak.org/xml-namespaces/sparkle',
|
39
|
+
'xmlns:dc' => 'http://purl.org/dc/elements/1.1/'
|
40
|
+
}
|
41
|
+
|
42
|
+
def initialize(title, link, description, language)
|
43
|
+
@title = title
|
44
|
+
@link = link
|
45
|
+
@description = description
|
46
|
+
@language = language
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate
|
50
|
+
Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml|
|
51
|
+
xml.rss(RSS_ATTRIBUTES) do
|
52
|
+
xml.channel do
|
53
|
+
xml.title title
|
54
|
+
xml.link link
|
55
|
+
xml.description description
|
56
|
+
xml.language language
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end.to_xml
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.add_item(xml_text, item)
|
63
|
+
doc = Nokogiri::XML(xml_text) { |cfg| cfg.noblanks }
|
64
|
+
Nokogiri::XML::Builder.with(doc.at('channel')) do |xml|
|
65
|
+
item.to_node(xml)
|
66
|
+
end
|
67
|
+
|
68
|
+
doc.to_xml
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/ognivo/build.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module Ognivo
|
2
|
+
class Build
|
3
|
+
include CLIHelpers
|
4
|
+
|
5
|
+
OPTIONS = [:destination_dir, :workspace, :project, :scheme, :verbose,
|
6
|
+
:build_configuration, :append_version]
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
OPTIONS.each { |v| instance_variable_set("@#{v}", opts[v]) }
|
10
|
+
@project = nil if @workspace
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
collect_settings
|
15
|
+
error_and_abort('Build failed') unless system(xcodebuild_cmd)
|
16
|
+
package_app
|
17
|
+
say_ok 'App has been successfully built'
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_version
|
21
|
+
@build_version ||= Agvtool.marketing_version.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :zip_file
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate_settings(build_settings)
|
29
|
+
return if build_settings
|
30
|
+
error_and_abort('App settings could not be found.')
|
31
|
+
end
|
32
|
+
|
33
|
+
def collect_settings
|
34
|
+
build_info = XcodeBuild.info(workspace: @workspace, project: @project)
|
35
|
+
|
36
|
+
ensure_workspace_project
|
37
|
+
|
38
|
+
@scheme = select_option('scheme', build_info.schemes) unless @scheme
|
39
|
+
|
40
|
+
build_settings = XcodeBuild.settings(*build_flags).find_app
|
41
|
+
validate_settings(build_settings)
|
42
|
+
|
43
|
+
@build_configuration ||= build_settings['CONFIGURATION']
|
44
|
+
|
45
|
+
@app_path = File.join(build_settings['BUILT_PRODUCTS_DIR'],
|
46
|
+
build_settings['WRAPPER_NAME'])
|
47
|
+
|
48
|
+
@destination_dir ||= Dir.pwd
|
49
|
+
end
|
50
|
+
|
51
|
+
def xcodebuild_cmd
|
52
|
+
cmd = ['xcodebuild', *build_flags, *build_actions]
|
53
|
+
cmd << '1> /dev/null' unless @verbose
|
54
|
+
cmd.join(' ')
|
55
|
+
end
|
56
|
+
|
57
|
+
def version_suffix
|
58
|
+
return unless @append_version
|
59
|
+
build_version.empty? ? '' : "-#{build_version}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def execute_in_tmpdir(*cmds)
|
63
|
+
Dir.mktmpdir('ognivo') do |dir|
|
64
|
+
cmd = ["cd #{dir}", *cmds].join(' && ')
|
65
|
+
`#{cmd}`
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def package_app
|
70
|
+
app_dir = File.basename(@app_path)
|
71
|
+
app_name = File.basename(app_dir, '.app')
|
72
|
+
@zip_file = "#{app_name}#{version_suffix}.zip"
|
73
|
+
|
74
|
+
execute_in_tmpdir(
|
75
|
+
"cp -R #{@app_path} ./",
|
76
|
+
"zip -9 -y -r #{@zip_file} #{app_dir}",
|
77
|
+
"mv #{@zip_file} #{@destination_dir}"
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_flags
|
82
|
+
flags = []
|
83
|
+
flags << %(-workspace "#{@workspace}") if @workspace
|
84
|
+
flags << %(-project "#{@project}") if @project
|
85
|
+
flags << %(-scheme "#{@scheme}") if @scheme
|
86
|
+
flags << %(-configuration "#{@build_configuration}") if @build_configuration
|
87
|
+
|
88
|
+
flags
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_actions
|
92
|
+
[:clean, :build, :archive]
|
93
|
+
end
|
94
|
+
|
95
|
+
def ensure_workspace_project
|
96
|
+
return if @workspace || @project
|
97
|
+
@workspace = find_workspace
|
98
|
+
@project = find_project unless @workspace
|
99
|
+
|
100
|
+
verify_workspace_project
|
101
|
+
end
|
102
|
+
|
103
|
+
def verify_workspace_project
|
104
|
+
return if @workspace || @project
|
105
|
+
error_and_abort('No Xcode projects or workspaces found in current directory')
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_workspace
|
109
|
+
find_xcfile('workspace', '*.xcworkspace')
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_project
|
113
|
+
find_xcfile('project', '*.xcodeproj')
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_xcfile(name, pattern)
|
117
|
+
files = Dir[pattern]
|
118
|
+
select_option("a #{name}", files)
|
119
|
+
end
|
120
|
+
|
121
|
+
def select_option(name, collection)
|
122
|
+
return unless collection && !collection.empty?
|
123
|
+
return collection.first if collection.count == 1
|
124
|
+
choose "Select #{name}:\n", *collection
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/ognivo/cli.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# program :name, 'ognivo'
|
2
|
+
program :version, Ognivo::VERSION
|
3
|
+
program :description, 'Automates MacOS app updates publishing using sparkle and S3'
|
4
|
+
|
5
|
+
program :help, 'Author', 'Anatoliy Plastinin <hello@antlypls.com>'
|
6
|
+
program :help, 'Website', 'http://github.com/antlypls/ognivo'
|
7
|
+
program :help_formatter, :compact
|
8
|
+
|
9
|
+
default_command :help
|
10
|
+
|
11
|
+
require 'ognivo/commands/build'
|
12
|
+
require 'ognivo/commands/upload'
|
13
|
+
require 'ognivo/commands/init'
|
14
|
+
require 'ognivo/commands/release'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
command :build do |c|
|
2
|
+
c.syntax = 'build [options]'
|
3
|
+
c.summary = 'Builds and packs your app into zip archive'
|
4
|
+
c.description = ''
|
5
|
+
|
6
|
+
c.option '-w', '--workspace WORKSPACE_FILE',
|
7
|
+
'Workspace (.xcworkspace) file to use to build app ' \
|
8
|
+
'(automatically detected in current directory)'
|
9
|
+
|
10
|
+
c.option '-p', '--project PROJECT_FILE',
|
11
|
+
'Project (.xcodeproj) file to use to build app ' \
|
12
|
+
'(automatically detected in current directory, ' \
|
13
|
+
'overridden by --workspace option, if passed)'
|
14
|
+
|
15
|
+
c.option '-c', '--configuration CONFIGURATION',
|
16
|
+
'Configuration used to build'
|
17
|
+
|
18
|
+
c.option '-s', '--scheme SCHEME',
|
19
|
+
'Scheme used to build app'
|
20
|
+
|
21
|
+
c.option '-d', '--destination DESTINATION',
|
22
|
+
'Destination. Defaults to current directory'
|
23
|
+
|
24
|
+
c.option '--verbose', 'Show build output'
|
25
|
+
|
26
|
+
c.action do |_, options|
|
27
|
+
opts = {
|
28
|
+
destination_dir: options.destination,
|
29
|
+
workspace: options.workspace,
|
30
|
+
build_configuration: options.configuration,
|
31
|
+
scheme: options.scheme,
|
32
|
+
verbose: options.verbose,
|
33
|
+
append_version: true
|
34
|
+
}
|
35
|
+
|
36
|
+
Ognivo::Build.new(opts).build
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
command :init do |c|
|
2
|
+
c.syntax = 'init [options]'
|
3
|
+
c.summary = 'Creates an appcast'
|
4
|
+
c.description = ''
|
5
|
+
|
6
|
+
c.option '-a', '--access-key-id ACCESS_KEY_ID',
|
7
|
+
'AWS Access Key ID'
|
8
|
+
|
9
|
+
c.option '-s', '--secret-access-key SECRET_ACCESS_KEY',
|
10
|
+
'AWS Secret Access Key'
|
11
|
+
|
12
|
+
c.option '-b', '--bucket BUCKET',
|
13
|
+
'S3 Bucket user to store appcast'
|
14
|
+
|
15
|
+
c.option '-c', '--appcast APPCAST',
|
16
|
+
'Appcast file name, appcast.xml is used by default'
|
17
|
+
|
18
|
+
c.action do |_, options|
|
19
|
+
if options.bucket.nil? || options.bucket.empty?
|
20
|
+
say_error 'Bucket name is missing'
|
21
|
+
abort
|
22
|
+
end
|
23
|
+
|
24
|
+
opts = {
|
25
|
+
access_key_id: ENV['AWS_ACCESS_KEY_ID'] || options.access_key_id,
|
26
|
+
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] || options.secret_access_key,
|
27
|
+
bucket_name: options.bucket,
|
28
|
+
appcast_name: options.appcast || 'appcast.xml'
|
29
|
+
}
|
30
|
+
|
31
|
+
Ognivo::Upload.new(opts).init_appcast
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
command :release do |c|
|
2
|
+
c.syntax = 'release [options]'
|
3
|
+
c.summary = 'Builds and packs your app into zip archive'
|
4
|
+
c.description = ''
|
5
|
+
|
6
|
+
c.option '--workspace WORKSPACE_FILE',
|
7
|
+
'Workspace (.xcworkspace) file to use to build app ' \
|
8
|
+
'(automatically detected in current directory)'
|
9
|
+
|
10
|
+
c.option '-p', '--project PROJECT_FILE',
|
11
|
+
'Project (.xcodeproj) file to use to build app ' \
|
12
|
+
'(automatically detected in current directory, ' \
|
13
|
+
'overridden by --workspace option, if passed)'
|
14
|
+
|
15
|
+
c.option '--configuration CONFIGURATION',
|
16
|
+
'Configuration used to build'
|
17
|
+
|
18
|
+
c.option '--scheme SCHEME',
|
19
|
+
'Scheme used to build app'
|
20
|
+
|
21
|
+
# upload options
|
22
|
+
|
23
|
+
c.option '-a', '--access-key-id ACCESS_KEY_ID',
|
24
|
+
'AWS Access Key ID'
|
25
|
+
|
26
|
+
c.option '-s', '--secret-access-key SECRET_ACCESS_KEY',
|
27
|
+
'AWS Secret Access Key'
|
28
|
+
|
29
|
+
c.option '-b', '--bucket BUCKET',
|
30
|
+
'S3 Bucket user to store appcast and releases'
|
31
|
+
|
32
|
+
c.option '-c', '--appcast APPCAST',
|
33
|
+
'Appcast file name, appcast.xml is used by default'
|
34
|
+
|
35
|
+
c.option '-d', '--dsa-private-key DSA_PRIVATE_KEY',
|
36
|
+
'DSA private key file used to sign app archive, ' \
|
37
|
+
'if not specified dsa signature will not be included into appcast.'
|
38
|
+
|
39
|
+
c.option '--app-version VERSION',
|
40
|
+
'Application version to be used in appcast item'
|
41
|
+
|
42
|
+
c.action do |_, options|
|
43
|
+
Dir.mktmpdir('ognivo') do |destination|
|
44
|
+
opts = {
|
45
|
+
destination_dir: destination,
|
46
|
+
workspace: options.workspace,
|
47
|
+
build_configuration: options.configuration,
|
48
|
+
scheme: options.scheme,
|
49
|
+
verbose: options.verbose,
|
50
|
+
append_version: false
|
51
|
+
}
|
52
|
+
|
53
|
+
build = Ognivo::Build.new(opts)
|
54
|
+
build.build
|
55
|
+
|
56
|
+
opts = {
|
57
|
+
access_key_id: ENV['AWS_ACCESS_KEY_ID'] || options.access_key_id,
|
58
|
+
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] || options.secret_access_key,
|
59
|
+
bucket_name: options.bucket,
|
60
|
+
appcast_name: options.appcast || 'appcast.xml',
|
61
|
+
app_filename: File.join(destination, build.zip_file),
|
62
|
+
dsa_filename: options.dsa_private_key,
|
63
|
+
version: options.app_version || build.build_version
|
64
|
+
}
|
65
|
+
|
66
|
+
Ognivo::Upload.new(opts).upload
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
command :upload do |c|
|
2
|
+
c.syntax = 'upload [options] app_archive'
|
3
|
+
c.summary = 'Uploads app archive and updates appcast'
|
4
|
+
c.description = ''
|
5
|
+
|
6
|
+
c.option '-a', '--access-key-id ACCESS_KEY_ID',
|
7
|
+
'AWS Access Key ID'
|
8
|
+
|
9
|
+
c.option '-s', '--secret-access-key SECRET_ACCESS_KEY',
|
10
|
+
'AWS Secret Access Key'
|
11
|
+
|
12
|
+
c.option '-b', '--bucket BUCKET',
|
13
|
+
'S3 Bucket user to store appcast and releases'
|
14
|
+
|
15
|
+
c.option '-c', '--appcast APPCAST',
|
16
|
+
'Appcast file name, appcast.xml is used by default'
|
17
|
+
|
18
|
+
c.option '-d', '--dsa-private-key DSA_PRIVATE_KEY',
|
19
|
+
'DSA private key file used to sign app archive, ' \
|
20
|
+
'if not specified dsa signature will not be included into appcast'
|
21
|
+
|
22
|
+
c.option '--app-version VERSION',
|
23
|
+
'Application version to be used in appcast item'
|
24
|
+
|
25
|
+
c.action do |args, options|
|
26
|
+
app_archive = args.first
|
27
|
+
|
28
|
+
opts = {
|
29
|
+
access_key_id: ENV['AWS_ACCESS_KEY_ID'] || options.access_key_id,
|
30
|
+
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] || options.secret_access_key,
|
31
|
+
bucket_name: options.bucket,
|
32
|
+
appcast_name: options.appcast || 'appcast.xml',
|
33
|
+
app_filename: app_archive,
|
34
|
+
dsa_filename: options.dsa_private_key,
|
35
|
+
version: options.app_version
|
36
|
+
}
|
37
|
+
|
38
|
+
Ognivo::Upload.new(opts).upload
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Ognivo
|
4
|
+
class S3Client
|
5
|
+
def initialize(access_key_id, secret_access_key, bucket)
|
6
|
+
@s3 = AWS::S3.new(
|
7
|
+
access_key_id: access_key_id,
|
8
|
+
secret_access_key: secret_access_key
|
9
|
+
)
|
10
|
+
|
11
|
+
@bucket = @s3.buckets[bucket]
|
12
|
+
end
|
13
|
+
|
14
|
+
def upload_file(file_path, key)
|
15
|
+
File.open(file_path) do |file|
|
16
|
+
upload(file, key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def upload(io, key)
|
21
|
+
@bucket.objects[key].write(io, acl: 'public_read')
|
22
|
+
end
|
23
|
+
|
24
|
+
def public_url(name)
|
25
|
+
@bucket.objects[name].public_url
|
26
|
+
end
|
27
|
+
|
28
|
+
def key_exists?(name)
|
29
|
+
@bucket.objects[name].exists?
|
30
|
+
end
|
31
|
+
|
32
|
+
def read(name)
|
33
|
+
@bucket.objects[name].read
|
34
|
+
end
|
35
|
+
|
36
|
+
def bucket_exists?
|
37
|
+
@bucket.exists?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'redcarpet'
|
2
|
+
|
3
|
+
module Ognivo
|
4
|
+
class Upload
|
5
|
+
include CLIHelpers
|
6
|
+
|
7
|
+
OPTIONS = [:access_key_id, :secret_access_key, :bucket_name, :appcast_name,
|
8
|
+
:app_filename, :dsa_filename, :version]
|
9
|
+
|
10
|
+
def initialize(opts = {})
|
11
|
+
OPTIONS.each { |v| instance_variable_set("@#{v}", opts[v]) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def upload
|
15
|
+
ensure_appfile
|
16
|
+
ensure_bucket
|
17
|
+
ensure_appcast
|
18
|
+
|
19
|
+
item = create_item
|
20
|
+
|
21
|
+
upload_build
|
22
|
+
say_ok 'Build has been uploaded to S3'
|
23
|
+
|
24
|
+
add_item_to_app_cast(item)
|
25
|
+
say_ok 'Appcast succesfully updated'
|
26
|
+
end
|
27
|
+
|
28
|
+
def init_appcast
|
29
|
+
ensure_bucket
|
30
|
+
create_cast
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def ensure_appfile
|
36
|
+
return if @app_filename && File.exist?(@app_filename)
|
37
|
+
error_and_abort "Bad filename: \nfilename is empty or file doesn't exist"
|
38
|
+
end
|
39
|
+
|
40
|
+
def ensure_appcast
|
41
|
+
return if appcast_exists?
|
42
|
+
|
43
|
+
unless agree('There is no appcast in a bucket. lets create one? [y/n]')
|
44
|
+
error_and_abort "can't continue"
|
45
|
+
end
|
46
|
+
|
47
|
+
create_cast
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_cast
|
51
|
+
cast = Appcast.new(cast_title, cast_link, cast_description, cast_language)
|
52
|
+
|
53
|
+
data = cast.generate
|
54
|
+
|
55
|
+
s3_client.upload(data, @appcast_name)
|
56
|
+
|
57
|
+
say_ok "Appcast has been created at #{cast_link}\n" \
|
58
|
+
'Use this link as a value for SUFeedURL key in an Info.plist'
|
59
|
+
end
|
60
|
+
|
61
|
+
def ensure_bucket
|
62
|
+
return if s3_client.bucket_exists?
|
63
|
+
error_and_abort "Bucket \"#{@bucket_name}\" doesn't exist"
|
64
|
+
end
|
65
|
+
|
66
|
+
def s3_client
|
67
|
+
@s3_client ||=
|
68
|
+
S3Client.new(@access_key_id, @secret_access_key, @bucket_name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def cast_title
|
72
|
+
ask('Appcast Title: ')
|
73
|
+
end
|
74
|
+
|
75
|
+
def cast_link
|
76
|
+
s3_client.public_url(@appcast_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
def cast_description
|
80
|
+
ask('Appcast Description: ')
|
81
|
+
end
|
82
|
+
|
83
|
+
def cast_language
|
84
|
+
ask('Appcast Language (e.g. en): ') { |q| q.default = 'en' }
|
85
|
+
end
|
86
|
+
|
87
|
+
def appcast_exists?
|
88
|
+
s3_client.key_exists?(@appcast_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def item_title
|
92
|
+
ask('Update Title: ')
|
93
|
+
end
|
94
|
+
|
95
|
+
def item_description
|
96
|
+
text = ask_editor "Write update description. What's new in this update, etc...\n"
|
97
|
+
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
|
98
|
+
markdown.render(text)
|
99
|
+
end
|
100
|
+
|
101
|
+
def item_version
|
102
|
+
@version ||= ask('Update Version: ')
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_item
|
106
|
+
say 'lets create an update entry'
|
107
|
+
item = Appcast::Item.new(item_title, item_description, item_version)
|
108
|
+
item.url = item_url
|
109
|
+
item.type = 'application/octet-stream'
|
110
|
+
|
111
|
+
Utils.update_item_for_file(@app_filename, item, @dsa_filename)
|
112
|
+
|
113
|
+
item
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_item_to_app_cast(item)
|
117
|
+
xml = s3_client.read(@appcast_name)
|
118
|
+
|
119
|
+
new_xml = Appcast.add_item(xml, item)
|
120
|
+
|
121
|
+
s3_client.upload(new_xml, @appcast_name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def s3_build_path
|
125
|
+
file_name = File.basename(@app_filename, '.*')
|
126
|
+
ext = File.extname(@app_filename)
|
127
|
+
version = item_version
|
128
|
+
|
129
|
+
"releases/#{file_name}-#{version}#{ext}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def item_url
|
133
|
+
s3_client.public_url(s3_build_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def upload_build
|
137
|
+
s3_client.upload_file(@app_filename, s3_build_path)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/ognivo/utils.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Ognivo
|
2
|
+
module Utils
|
3
|
+
def self.update_item_for_file(file, item, dsa_file)
|
4
|
+
item.pub_date = File.ctime(file)
|
5
|
+
item.length = File.size(file)
|
6
|
+
item.dsa_signature = signature(file, dsa_file) if dsa_file
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.signature(file, dsa_file)
|
10
|
+
sha_cmd = "openssl dgst -sha1 -binary < #{file}"
|
11
|
+
sign_cmd = "openssl dgst -dss1 -sign #{dsa_file}"
|
12
|
+
base64_cmd = 'openssl enc -base64'
|
13
|
+
output = `#{sha_cmd} | #{sign_cmd} | #{base64_cmd}`
|
14
|
+
output.strip
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# based on source code from nomad/shenzhen project
|
2
|
+
# see https://github.com/nomad/shenzhen/blob/master/lib/shenzhen/xcodebuild.rb
|
3
|
+
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module Ognivo
|
7
|
+
module XcodeBuild
|
8
|
+
Error = Class.new(StandardError)
|
9
|
+
NilOutputError = Class.new(Error)
|
10
|
+
|
11
|
+
Info = Class.new(OpenStruct)
|
12
|
+
|
13
|
+
class Settings < Hash
|
14
|
+
def initialize(hash = {})
|
15
|
+
merge!(hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_app
|
19
|
+
values.find { |settings| settings['WRAPPER_EXTENSION'] == 'app' }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.info(*args)
|
24
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
25
|
+
output = `xcodebuild -list #{(args + args_from_options(options)).join(' ')} 2>&1`
|
26
|
+
fail Error, $1 if output =~ /^xcodebuild\: error\: (.+)$/
|
27
|
+
fail NilOutputError unless output =~ /\S/
|
28
|
+
|
29
|
+
info = parse_info_output(output)
|
30
|
+
Info.new(info)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.parse_info_output(output)
|
34
|
+
lines = output.split(/\n/)
|
35
|
+
|
36
|
+
project_name = parse_info_project_name(lines.first)
|
37
|
+
|
38
|
+
info, _ = lines.drop(1).reduce([{}, nil]) do |(info, group), line|
|
39
|
+
parse_info_line(line, info, group)
|
40
|
+
end
|
41
|
+
|
42
|
+
info.each do |_, values|
|
43
|
+
values.delete('')
|
44
|
+
values.uniq!
|
45
|
+
end
|
46
|
+
|
47
|
+
info.merge(project: project_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.parse_info_project_name(line)
|
51
|
+
$1 if line =~ /\"(.+)\"\:/
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.parse_info_line(line, info, group)
|
55
|
+
if line =~ /\:$/
|
56
|
+
group = line.strip[0...-1].downcase.gsub(/\s+/, '_')
|
57
|
+
info[group] = []
|
58
|
+
else
|
59
|
+
info[group] << line.strip unless group.nil? || line =~ /\.$/
|
60
|
+
end
|
61
|
+
|
62
|
+
[info, group]
|
63
|
+
end
|
64
|
+
private_class_method :parse_info_line
|
65
|
+
|
66
|
+
def self.settings(*args)
|
67
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
68
|
+
cmd_args = (args + args_from_options(options)).join(' ')
|
69
|
+
output = `xcodebuild #{cmd_args} -showBuildSettings 2> /dev/null`
|
70
|
+
fail Error, $1 if output =~ /^xcodebuild\: error\: (.+)$/
|
71
|
+
fail NilOutputError unless output =~ /\S/
|
72
|
+
|
73
|
+
settings = parse_settings_output(output)
|
74
|
+
Settings.new(settings)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.parse_settings_output(output)
|
78
|
+
lines = output.split(/\n/)
|
79
|
+
|
80
|
+
lines.reduce([{}, nil]) do |(settings, target), line|
|
81
|
+
parse_settings_line(line, settings, target)
|
82
|
+
end.first
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.parse_settings_line(line, settings, target)
|
86
|
+
case line
|
87
|
+
when /Build settings for action build and target \"?([^":]+)/
|
88
|
+
target = $1
|
89
|
+
settings[target] = {}
|
90
|
+
else
|
91
|
+
key, value = line.split(/\=/).map(&:strip)
|
92
|
+
settings[target][key] = value if target
|
93
|
+
end
|
94
|
+
|
95
|
+
[settings, target]
|
96
|
+
end
|
97
|
+
private_class_method :parse_settings_line
|
98
|
+
|
99
|
+
def self.version
|
100
|
+
output = `xcodebuild -version`
|
101
|
+
parse_xcode_version(output)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.parse_xcode_version(output)
|
105
|
+
$1 if output =~ /([\d+\.?]+)/
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.args_from_options(options = {})
|
109
|
+
options.reject { |_, value| value.nil? }.map { |key, value| "-#{key} '#{value}'" }
|
110
|
+
end
|
111
|
+
private_class_method :args_from_options
|
112
|
+
end
|
113
|
+
end
|
data/lib/ognivo.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'ognivo/version'
|
2
|
+
|
3
|
+
require 'ognivo/appcast'
|
4
|
+
require 'ognivo/utils'
|
5
|
+
require 'ognivo/xcodebuild'
|
6
|
+
require 'ognivo/agvtool'
|
7
|
+
|
8
|
+
require 'ognivo/cli_helpers'
|
9
|
+
require 'ognivo/build'
|
10
|
+
require 'ognivo/s3client'
|
11
|
+
require 'ognivo/upload'
|
12
|
+
|
13
|
+
module Ognivo
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ognivo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anatoliy Plastinin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: commander
|
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: nokogiri
|
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: redcarpet
|
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: aws-sdk
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
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: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.6'
|
83
|
+
description: Ognivo is CLI tool that automates publishing of MacOS application updates
|
84
|
+
using sparkle by AWS S3
|
85
|
+
email:
|
86
|
+
- hello@antlypls.com
|
87
|
+
executables:
|
88
|
+
- spark
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- LICENSE
|
93
|
+
- README.md
|
94
|
+
- bin/spark
|
95
|
+
- lib/ognivo.rb
|
96
|
+
- lib/ognivo/agvtool.rb
|
97
|
+
- lib/ognivo/appcast.rb
|
98
|
+
- lib/ognivo/build.rb
|
99
|
+
- lib/ognivo/cli.rb
|
100
|
+
- lib/ognivo/cli_helpers.rb
|
101
|
+
- lib/ognivo/commands/build.rb
|
102
|
+
- lib/ognivo/commands/init.rb
|
103
|
+
- lib/ognivo/commands/release.rb
|
104
|
+
- lib/ognivo/commands/upload.rb
|
105
|
+
- lib/ognivo/s3client.rb
|
106
|
+
- lib/ognivo/upload.rb
|
107
|
+
- lib/ognivo/utils.rb
|
108
|
+
- lib/ognivo/version.rb
|
109
|
+
- lib/ognivo/xcodebuild.rb
|
110
|
+
homepage: ''
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.4.1
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Ognivo
|
134
|
+
test_files: []
|