forked from SelfControlApp/selfcontrol
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathappcast_automation.rb
396 lines (330 loc) · 15.5 KB
/
appcast_automation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env ruby -w
#################################################################################
# #
# appcast_automation.rb #
# #
# author: Craig Williams #
# created: 2009-01-09 #
# #
#################################################################################
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
#################################################################################
=begin
INTRO:
In his article, Marc uses a bash script to automate the process of signing your
Sparkle enabled app and explains how to put your private and public keys in your
keychain. I wrote a Ruby version that extends this functionality to
include a few more options…
IMPORTANT: You will need to read Marc's article first before moving forward.
Here is a quick list of added features:
1. config.yaml file for configuration information
2. build_now option - when set to 'NO' will not run during Release style build
3. Creates project release folder if it does not exist
4. Creates sub-folders based on version number
5. Creates css file
6. Creates xml file containing the '<item>' info generated by the script
7. Creates an html template file
8. Copies the newly created archive to the project folder
The project release folder is not the 'Release' folder Xcode creates.
This is a folder that contains this and future Sparkle release builds.
See 'appcast_basefolder' below.
INSTRUCTIONS:
Config YAML file
A YAML file is easily editable and is a good place for us to set up our
configuration information. It also enables us not to have to change the
script once we have it the way we want it.
Create a 'config.yaml' file and place it in your projects 'Release' folder
and include the following making the necessary changes.
---
build_now: 'YES'
download_base_url: 'http://www.your_website.com/app_folder/'
appcast_basefolder: '/users/user_name/desktop/app_name/'
appcast_xml_name: 'appcast.xml'
keychain_privkey_name: 'Sparkle Private Key'
css_file_name: 'rnotes.css'
IMPORTANT: If you change the variable names here you also
need to change them in the script.
VARIABLE EXPLANATION
build_now:
Will only include this script in the build process if this is set to 'YES'
The script automatically checks that the build style is 'Release'
download_base_url:
Your website url where you will place your updated project
appcast_basefolder:
The base file is created for you and a project folder inside that with
the name of your project and version number.
eg - ProjectName
- ProjectName 1.0
- appcast.xml (contains the '<item>' info)
- 1.0.html
- rnotes.css
- ProjectName 1.0.zip
- ProjectName 1.1
- appcast.xml (contains the '<item>' info)
- 1.1.html
- rnotes.css
- ProjectName 1.1.zip
The following files are created for you if they do not already exist.
appcast_xml_name:
css_file_name:
version_number.html
Your archived project file is also copied to the project folder
AppName 1.1.zip
appcast_xml_name:
This file holds the results of the script. What is between the '' tags that
you will copy into your complete appcast.xml file.
Name to your liking.
keychain_privkey_name:
You should understand this after reading Marc's article.
Name to your liking.
css_file_name:
This name will be used to create your css file and the link in the xml file.
If you change this after these are created make sure also change the xml file
or if you created the css file first use that name here.
The css is at the bottom of the script and is in 'flat' form. One liners.
When written to file it is expanded to standard format for easy editing.
Once your config.yaml file is created and placed in your projects 'Release' folder,
add this script as a 'Run Script' build phase. Set the bash to /usr/bin/ruby
and you are finished!
If you have questions, ideas or bug reports please post them at:
http://allancraig.net/blog/?p=65
=end
class AppCast
require 'yaml'
require 'tmpdir'
require 'fileutils'
MESSAGE_HEADER = 'RUN SCRIPT DURING BUILD MESSAGE'
def initialize
@signature = ''
require_release_build
instantiate_project_variables
load_config_file
# the build_now setting in the config.yaml file
# determines whether you want to perform this script
# set to 'NO' until you are ready to publish
exit_if_build_not_set_to_yes
instantiate_appcast_variables
end
def main_worker_bee
create_appcast_folder_and_files
remove_old_zip_create_new_zip
file_stats
create_key
create_appcast_xml
copy_archive_to_appcast_path
end
# Only works for Release builds
# Exits upon failure
def require_release_build
if ENV["BUILD_STYLE"] == 'Debug'
log_message("Distribution target requires 'Release' build style")
exit
end
end
# Exits if no config.yaml file found.
def load_config_file
config_file_path = "#{@proj_dir}/config.yaml"
if !File.exists?(config_file_path)
log_message("No 'config.yaml' file found in project directory.")
exit
end
@config = YAML.load_file(config_file_path)
end
def exit_if_build_not_set_to_yes
if @config['build_now'] != 'YES'
log_message("The 'build_now' setting in 'config.yaml' set to 'NO'\nIf you are wanting to include this script in\nthe build process change this setting to 'YES'")
exit
end
end
def instantiate_project_variables
@proj_dir = ENV['BUILT_PRODUCTS_DIR']
@proj_name = ENV['PROJECT_NAME']
@version = `defaults read "#{@proj_dir}/#{@proj_name}.app/Contents/Info" CFBundleVersion`
@archive_filename = "#{@proj_name} #{@version.chomp}.zip"
@archive_path = "#{@proj_dir}/#{@archive_filename}"
end
def instantiate_appcast_variables
@appcast_xml_name = @config['appcast_xml_name'].chomp
@appcast_basefolder = @config['appcast_basefolder'].chomp
@appcast_proj_folder = "#{@config['appcast_basefolder']}/#{@proj_name}_#{@version}".chomp
@appcast_xml_path = "#{@appcast_proj_folder}/#{@appcast_xml_name}"
@download_base_url = @config['download_base_url']
@keychain_privkey_name = @config['keychain_privkey_name']
@css_file_name = @config['css_file_name']
@releasenotes_url = "#{@download_base_url}#{@version.chomp}.html"
@download_url = "#{@download_base_url}#{@archive_filename}"
end
def remove_old_zip_create_new_zip
Dir.chdir(@proj_dir)
`rm -f #{@proj_name}*.zip`
`zip -qr "#{@archive_filename}" "#{@proj_name}.app"`
end
def copy_archive_to_appcast_path
begin
FileUtils.cp(@archive_path, @appcast_proj_folder)
rescue
log_message("There was an error copying the zip file to appcast folder\nError: #{$!}")
end
end
def file_stats
@size = File.size(@archive_filename)
@pubdate = `date +"%a, %d %b %G %T %z"`
end
def create_key
priv_key_path = "#{Dir.tmpdir}/priv_key.pem"
key = `security find-generic-password -g -s "#{@keychain_privkey_name}" 2>&1 1>/dev/null | perl -pe '($_) = /"(.+)"/'`
if key == ''
log_message("Unable to load signing private key with name '#{@keychain_privkey_name}' from keychain\nFor file #{@archive_filename}")
exit
end
File.open(priv_key_path, 'w') { |f| f.puts key.split("\\012") }
@signature = `openssl dgst -sha1 -binary < '#{@archive_path}' \
| openssl dgst -dss1 -sign '#{priv_key_path}' \
| openssl enc -base64`
`rm -fP #{priv_key_path}`
log_message(@signature)
if @signature == ''
log_message("Unable to sign file #{@archive_filename}")
exit
end
end
def create_appcast_xml
appcast_xml =
"<item>
<title>Version #{@version.chomp}</title>
<sparkle:releaseNotesLink>
#{@releasenotes_url.chomp}
</sparkle:releaseNotesLink>
<pubDate>#{@pubdate.chomp}</pubDate>
<enclosure
url=\"#{@download_url.chomp}\"
sparkle:version=\"#{@version.chomp}\"
type=\"application/octet-stream\"
length=\"#{@size}\"
sparkle:dsaSignature=\"#{@signature.chomp}\"
/>
</item>"
File.open(@appcast_xml_path, 'w') { |f| f.puts appcast_xml }
end
# Creates the appcast folder if it does not exist
# or is accidently moved or deleted
# Creates an html file with generic note template if it does not exist
# This way the notes file is named correctly as well
# Creates a css file named from yaml file with default css
def create_appcast_folder_and_files
base_folder = @appcast_basefolder
project_folder = @appcast_proj_folder
notes_file = "#{project_folder}/#{File.basename(@releasenotes_url.chomp)}"
css_file_path = "#{project_folder}/#{@css_file_name}"
Dir.mkdir(base_folder) if !File.exists?(base_folder)
Dir.mkdir(project_folder) if !File.exists?(project_folder)
File.open(notes_file, 'w') { |f| f.puts release_notes_generic_text } if !File.exists?(notes_file)
File.open(css_file_path, 'w') { |f| f.puts decompressed_css } if !File.exists?(css_file_path)
end
def log_message(msg)
puts "\n\n----------------------------------------------"
puts MESSAGE_HEADER
puts msg
puts "----------------------------------------------\n\n"
end
def decompressed_css
return css_text.gsub(/\{\s+/, "{\n\t").gsub(/;/, ";\n\t").gsub(/^\s+\}/, "}").gsub(/^\s+/, "\t")
end
def release_notes_generic_text
return "<html>
<head>
<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">
<title>What's new in #{@proj_name}?</title>
<meta name=\"robots\" content=\"anchors\">
<link href=\"rnotes.css\" type=\"text/css\" rel=\"stylesheet\" media=\"all\">
</head>
<body>
<br />
<table class=\"dots\" width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" summary=\"Two column table with heading\">
<tr>
<td class=\"blue\" colspan=\"2\">
<h3>TITLE</h3>
</td>
</tr>
<tr>
<td valign=\"top\" width=\"150\"><img src=\"IMAGE_NAME.png\" alt=\"ALT_TITLE\" width=\"150\" border=\"0\"></td>
<td valign=\"top\">
<p>DESCRIPTION</p>
</td>
</tr>
</table>
<br>
<table class=\"dots\" width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" summary=\"Two column table with heading\">
<tr>
<td class=\"blue\" colspan=\"2\">
<h3>TITLE</h3>
</td>
</tr>
<tr>
<td valign=\"top\" width=\"150\"><img src=\"IMAGE_NAME.png\" alt=\"ALT_TITLE\" width=\"150\" border=\"0\"></td>
<td valign=\"top\">
<p>DESCRIPTION</p>
</td>
</tr>
</table>
<br>
</body>
</html>"
end
# This css will be expanded to a normal, easily editable form when written to file
def css_text
return "body { margin: 2px 12px 12px; }
h1 h2 h3 p ol ul a a:hover { font-family: \"Lucida Grande\", Arial, sans-serif; }
h1 { font-size: 11pt; margin-bottom: 0; }
h2 { font-size: 9pt; margin-top: 0; margin-bottom: -10px; }
h3 { font-size: 9pt; font-weight: bold; margin-top: -4px; margin-bottom: -4px; }
p { font-size: 9pt; line-height: 12pt; text-decoration: none; }
ol { font-size: 9pt; line-height: 12pt; list-style-position: outside; margin-top: 12px; margin-bottom: 12px; margin-left: -18px; padding-left: 40px; }
ol li { margin-top: 6px; margin-bottom: 6px; }
ol p { margin-top: 6px; margin-bottom: 6px; }
ul { font-size: 9pt; line-height: 12pt; list-style-type: square; list-style-position: outside; margin-top: 12px; margin-bottom: 12px; margin-left: -24px; padding-left: 40px; }
ul li { margin-top: 6px; margin-bottom: 6px; }
ul p { margin-top: 6px; margin-bottom: 6px; }
a { color: #00f; font-size: 9pt; line-height: 12pt; text-decoration: none; }
a:hover { color: #00f; text-decoration: underline; }
hr { text-decoration: none; border: solid 1px #bfbfbf; }
td { padding: 6px; }
#banner { background-color: #f2f2f2; background-repeat: no-repeat; padding: -2px 6px 0; position: fixed; top: 0; left: 0; width: 100%; height: 1.2em; float: left; border: solid 1px #bfbfbf; }
#caticon { margin-top: 3px; margin-bottom: -3px; margin-right: 5px; float: left; }
#pagetitle { margin-top: 12px; margin-bottom: 0px; margin-left: 40px; width: 88%; border: solid 1px #fff; }
#mainbox { margin-top: 2349px; padding-right: 6px; }
#taskbox { background-color: #e6edff; list-style-type: decimal; list-style-position: outside; margin: 12px 0; padding: 2px 12px; border: solid 1px #bfbfbf; }
#taskbox h2 { margin-top: 8; margin-bottom: -4px; }
#machelp { position: absolute; top: 2px; left: 10px ; }
#index { background-color: #f2f2f2; padding-right: 25px; top: 2px; right: 12px; width: auto; float: right; }
#next { position: absolute; top: 49px; left: 88%; }
#asindent { margin-left: 22px; font-size: 9pt; font-family: Verdana, Courier, sans-serif; }
.bread { color: #00f; font-size: 8pt; margin: -9px 0 -6px; }
.leftborder { color: #00f; font-size: 8pt; margin: -9px 0 -6px; padding-top: 2px; padding-bottom: 3px; padding-left: 8px; border-left: 1px solid #bfbfbf; }
.mult { margin-top: -8px; }
.blue { background-color: #e6edff; margin-top: -3px; margin-bottom: -3px; padding-top: -3px; padding-bottom: -3px; }
.rightfloater { float: right; margin-left: 15px; }
.rules { border-bottom: 1px dotted #ccc; }
.dots { border: dotted 1px #ccc; }
.seealso { margin-top: 4px; margin-bottom: 4px; }
code { color: black; font-size: 9pt; font-family: Verdana, Courier, sans-serif; }"
end
end
if __FILE__ == $0
newAppcast = AppCast.new
newAppcast.main_worker_bee
newAppcast.log_message("It appears all went well with the build script!")
end