ognivo 0.0.1

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
+ 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: []