ognivo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'ognivo'
6
+
7
+ require 'commander/import'
8
+ require 'ognivo/cli'
@@ -0,0 +1,8 @@
1
+ module Ognivo
2
+ module Agvtool
3
+ def self.marketing_version
4
+ output = `agvtool what-marketing-version -terse`
5
+ output.scan(/\=(.+)$/).flatten.first
6
+ end
7
+ end
8
+ end
@@ -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
@@ -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,8 @@
1
+ module Ognivo
2
+ module CLIHelpers
3
+ def error_and_abort(error_text)
4
+ say_error(error_text)
5
+ abort
6
+ end
7
+ end
8
+ end
@@ -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
@@ -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,3 @@
1
+ module Ognivo
2
+ VERSION = '0.0.1'
3
+ 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: []