smc-get 0.1.0 → 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,329 @@
1
+ #Encoding: UTF-8
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ module CUICommands
24
+
25
+ class BuildCommand < Command
26
+
27
+ def self.help
28
+ <<-EOF
29
+ USAGE: #{File.basename($0)} build [DIRECTORY]
30
+
31
+ If DIRECTORY is given, validates it's structure against the SMC packaging
32
+ guidelines and then packages it into a .smcpak file. If DIRECTORY is not
33
+ given, enters an interactive process that queries you for the files you
34
+ want to include in your SMC package. In both cases you’ll end up with
35
+ a SMC package in your current directory.
36
+
37
+ Files you enter during the interrogative process are looked for in your
38
+ user-level SMC directory, i.e. ~/.smc.
39
+ EOF
40
+ end
41
+
42
+ def self.summary
43
+ "build\t\tBuild SMC packages."
44
+ end
45
+
46
+ def parse(args)
47
+ CUI.debug("Parsing #{args.count} args for build.")
48
+ raise(InvalidCommandline, "Too many arguments.") if args.count > 1
49
+ @directory = args.shift #nil if not given
50
+ end
51
+
52
+ def execute(config)
53
+ CUI.debug("Executing build.")
54
+ begin
55
+ if @directory
56
+ Package.create(@directory)
57
+ else #No directory given
58
+ puts(<<-MSG)
59
+ Welcome to the smc-get build process!
60
+ Answer the following questions properly and you'll end up with a ready-to-
61
+ install package you can either install locally or contribute to the repository
62
+ (which would make it installable via 'smc-get install' directly). When a question
63
+ asks you to input multiple files, you can end the query with an empty line.
64
+ If you like, you can specify multiple files at once by separating them
65
+ with a comma. Wildcards in filenames are allowed.
66
+
67
+ Files you don't specify via an absolute path (i.e. a path beginning with
68
+ either / on *nix or <letter>:\\ on Windows) are searched for in your
69
+ home directory's .smc directory and your SMC installation.
70
+ MSG
71
+ #All the information will be collected in this hash
72
+ spec = Hash.new{|hsh, key| hsh[key] = []}
73
+
74
+ #Start the questionaire
75
+ [:levels, :graphics, :music, :sounds, :worlds].each do |sym|
76
+ spec[sym].concat(input_files(sym))
77
+ end
78
+
79
+ puts
80
+ puts("Who participated in creating this package?")
81
+ loop do
82
+ print "> "
83
+ str = $stdin.gets.chomp
84
+
85
+ if str.empty?
86
+ if spec[:authors].empty?
87
+ $stderr.puts("You have to input at least one author.")
88
+ else
89
+ break
90
+ end
91
+ else #Something was entered
92
+ spec[:authors].concat(str.split(",").map(&:strip))
93
+ end
94
+ end
95
+
96
+ puts
97
+ puts "Enter this package's dependecy packages:"
98
+ loop do
99
+ print "> "
100
+ str = $stdin.gets.chomp
101
+ if str.empty?
102
+ break
103
+ else
104
+ spec[:dependencies].concat(str.split(",").map(&:strip))
105
+ end
106
+ end
107
+
108
+ loop do
109
+ puts
110
+ print("Enter the difficulty: ")
111
+ break unless (spec[:difficulty] = $stdin.gets.chomp).empty?
112
+ end
113
+
114
+ puts
115
+ puts("Enter the package's description. A single line containing containg")
116
+ puts("END")
117
+ puts("terminates the query.")
118
+ spec[:description] = ""
119
+ loop do
120
+ print "> "
121
+ str = $stdin.gets #No chomp here, the user may set spaces at the line end intentionally
122
+ if str == "END\n"
123
+ if spec[:description].strip.empty?
124
+ $stderr.puts("You *have* to input a description!")
125
+ $stderr.puts("And it must consist of something else than only whitespace!")
126
+ else
127
+ break
128
+ end
129
+ else
130
+ spec[:description] << str
131
+ end
132
+ end
133
+
134
+ [:install_message, :remove_message].each{|sym| spec[sym] = input_desc(sym)}
135
+
136
+ loop do
137
+ puts
138
+ print("Enter the package's full title (it can contain whitespace):")
139
+ spec[:title] = $stdin.gets.chomp
140
+ if spec[:title].strip.empty?
141
+ $stderr.puts "You *have* to specify a title that doesn't consist solely of whitespace!"
142
+ else
143
+ break
144
+ end
145
+ end
146
+
147
+ pkgname = nil
148
+ loop do
149
+ puts
150
+ print "Enter the package's name (this mustn't contain whitespace):"
151
+ pkgname = $stdin.gets.chomp
152
+ if pkgname.strip.empty?
153
+ $stderr.puts "You *have* to specify a name!"
154
+ elsif pkgname =~ /\s/
155
+ $stderr.puts "The package name mustn't contain whitespace!"
156
+ else
157
+ break
158
+ end
159
+ end
160
+
161
+ #Set the last_update spec field to now
162
+ spec[:last_update] = Time.now.utc
163
+
164
+ puts
165
+ #This is all. Start building the package.
166
+ Dir.mktmpdir("smc-get-build-package") do |tmpdir|
167
+ puts "Creating package..."
168
+ tmpdir = Pathname.new(tmpdir)
169
+ pkgdir = tmpdir + pkgname
170
+ pkgdir.mkdir
171
+ #Copy all the level, music, etc. files
172
+ [:levels, :music, :sounds, :worlds].each do |sym|
173
+ subdir = pkgdir + sym.to_s
174
+ subdir.mkdir
175
+ spec[sym].each{|file| FileUtils.cp(file, subdir)}
176
+ end
177
+ #The graphics for whatever reason have an own name...
178
+ subdir = pkgdir + "pixmaps"
179
+ subdir.mkdir
180
+ spec[:graphics].each{|file| FileUtils.cp(file, subdir)}
181
+
182
+ #Turn the file paths in the spec to relative ones and the
183
+ #keys to strings. Compute the checksums.
184
+ puts "Creating spec and computing SHA1 checksums..."
185
+ real_spec = {"checksums" => {}}
186
+ spec.each_pair do |key, value|
187
+ real_spec[key.to_s] = case key
188
+ when :levels, :graphics, :music, :sounds
189
+ real_spec["checksums"][key.to_s] = {}
190
+ value.map do |abs_path|
191
+ basename = File.basename(abs_path)
192
+ real_spec["checksums"][key.to_s][basename] = Digest::SHA1.hexdigest(File.read(abs_path))
193
+ basename #for map
194
+ end
195
+ when :worlds
196
+ real_spec["checksums"]["worlds"] = {}
197
+ value.map do |abs_path|
198
+ basename = File.basename(abs_path)
199
+ real_spec["checksums"]["worlds"][basename] = {
200
+ "description.xml" => Digest::SHA1.hexdigest(File.read(File.join(abs_path, "description.xml"))),
201
+ "layer.xml" => Digest::SHA1.hexdigest(File.read(File.join(abs_path, "layer.xml"))),
202
+ "world.xml" => Digest::SHA1.hexdigest(File.read(File.join(abs_path, "world.xml")))
203
+ }
204
+ basename #for map
205
+ end
206
+ else
207
+ value
208
+ end
209
+ end
210
+
211
+ #Create the spec
212
+ File.open(pkgdir + "#{pkgname}.yml", "w"){|file| YAML.dump(real_spec, file)}
213
+
214
+ puts "Compressing..."
215
+ pkg = Package.create(pkgdir)
216
+ puts "Copying..."
217
+ FileUtils.cp(pkg.path, "./")
218
+ #compressed_file_name returns only the name of the package file,
219
+ #no path. Expanding it therefore results in the current
220
+ #working directory prepended to it.
221
+ puts "Done. Your package is at #{File.expand_path(pkg.spec.compressed_file_name)}."
222
+ end
223
+
224
+ end
225
+ rescue Errors::BrokenPackageError => e
226
+ $stderr.puts("Failed to build SMC package:")
227
+ $stderr.puts(e.message)
228
+ if CUI.debug_mode?
229
+ $stderr.puts("Class: #{e.class}")
230
+ $stderr.puts("Message: #{e.message}")
231
+ $stderr.puts("Backtrace:")
232
+ $stderr.puts(e.backtrace.join("\n\t"))
233
+ end
234
+ return 1 #Exit code
235
+ end
236
+ end
237
+
238
+ private
239
+
240
+ #Queries the user for a set of file names in an uniform mannor.
241
+ #Pass in the pluralized symbol of the resource you want to query
242
+ #for, e.g. :levels or :graphics. Return value is an array of all
243
+ #found files which may be empty if no files were found.
244
+ def input_files(plural_name)
245
+ result = []
246
+ puts
247
+ puts "Enter the names of the #{plural_name} you want to include:"
248
+ loop do
249
+ print "> "
250
+ str = $stdin.gets.chomp
251
+
252
+ #Test if the user entered an empty line, i.e. wants to end the
253
+ #query for this question
254
+ if str.empty?
255
+ if result.empty?
256
+ print("No #{plural_name} specified. Is this correct?(y/n) ")
257
+ break if $stdin.gets.chomp.downcase == "y"
258
+ else
259
+ break
260
+ end
261
+ else #User entered something
262
+ str.split(",").each do |file|
263
+ file.strip! #Due to possible whitespace behind the comma
264
+ ary = get_file_paths(plural_name.to_s, file)
265
+ $stderr.puts("Warning: File(s) not found: #{file}. Ignoring.") if ary.empty?
266
+ result.concat(ary)
267
+ end
268
+ end
269
+ end
270
+ result
271
+ end
272
+
273
+ #Queries the user for a longer, but optional text that is returned. If
274
+ #the user enters no text beside the END marker, returns nil. Pass in
275
+ #what to tell the user is to be entered.
276
+ def input_desc(sym)
277
+ puts
278
+ puts("Enter the package's #{sym}. A single line containing containg")
279
+ puts("END")
280
+ puts("terminates the query. Enter END immediately if you don't want")
281
+ puts "a #{sym}."
282
+ result = ""
283
+ loop do
284
+ print "> "
285
+ str = $stdin.gets #No chomp here, the user may set spaces at the line end intentionally
286
+ if str == "END\n"
287
+ break
288
+ else
289
+ result << str
290
+ end
291
+ end
292
+ result.empty? ? nil : result
293
+ end
294
+
295
+ def get_file_paths(plural_name, path)
296
+ #Even on Windows we work with forward slash (Windows supports this,
297
+ #although it’s not well known)
298
+ path = path.gsub("\\", "/")
299
+ ary = []
300
+ #First check if it’s an absolute path
301
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/ and path =~ /^[a-z]:/i
302
+ ary.replace(Dir.glob(path)) #Works even without an actual escape char like *
303
+ elsif path.start_with?("/")
304
+ ary.replace(Dir.glob(path))
305
+ else #OK, relative path
306
+ plural_name = "pixmaps" if plural_name == "graphics" #As always...
307
+
308
+ #The user level directory only contains levels, but for the
309
+ #sake of simplicity I treat it as if sounds etc existed there
310
+ #as well. It doesn’t hurt if not, because that just causes
311
+ #an empty array.
312
+ user_level_dir = CUI::USER_SMC_DIR + plural_name
313
+ smc_install_dir = @cui.local_repository.path + plural_name
314
+ global_files = Dir.glob(smc_install_dir.join(path).to_s)
315
+ user_files = Dir.glob(user_level_dir.join(path).to_s)
316
+ #In case a file with the same name exists in both paths,
317
+ #the user-level file overrides the SMC installation ones’s.
318
+ ary.replace(user_files)
319
+ global_files.each{|path| ary << path unless ary.any?{|p2| File.basename(path) == File.basename(p2)}}
320
+ end
321
+ ary
322
+ end
323
+
324
+ end
325
+
326
+ end
327
+
328
+ end
329
+ # vim:set ts=8 sts=2 sw=2 et: #
@@ -1,81 +1,117 @@
1
- #Encoding: UTF-8
2
- ################################################################################
3
- # This file is part of smc-get.
4
- # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
- # Copyright (C) 2011 Marvin Gülker
6
- #
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- ################################################################################
20
-
21
- module SmcGet
22
-
23
- module CUICommands
24
-
25
- #Class for invalid command-line argument errors.
26
- class InvalidCommandline < Errors::SmcGetError
27
- end
28
-
29
- #This is the superclass of all CUI commands. To make your own command,
30
- #subclass it an overwrite ::help, ::summary, #parse and #execute.
31
- class Command
32
-
33
- #The string returned from this method will be displayed to the
34
- #user if he issues <tt>smc-get help YOURCOMMAND</tt>.
35
- def self.help
36
- "(nothing known)"
37
- end
38
-
39
- #One-line summary of the command that shows up in the COMMANDS
40
- #section of <tt>smc-get help</tt>. Should not be longer than 78
41
- #characters due to automatic indentation. You may have to insert
42
- #tabs to make it displaycorrectly; make sure to check the result by
43
- #issueing <tt>smc-get help</tt>.
44
- def self.summary
45
- ""
46
- end
47
-
48
- #Creates a new instance of this command. Do not override this, or
49
- #call at least +super+.
50
- def initialize(args)
51
- parse(args)
52
- end
53
-
54
- #This method gets all commandline options relevant for this subcommand
55
- #passed in the +args+ array. You should parse them destructively, i.e.
56
- #after you finished parsing, +args+ should be an empty array.
57
- #Set instance variables to save data.
58
- #
59
- #Note that SmcGet is not set up when this method is called, so you
60
- #cannot to things like <tt>Package.new</tt>.
61
- def parse(args)
62
- raise(NotImplementedError, "#{__method__} has to be overriden in a subclass!")
63
- end
64
-
65
- #Execute the command. You can use the instance variables set in #parse.
66
- #The method gets passed the parsed contents of smc-get's configuration
67
- #files and commandline parameters; you can use this to make your command
68
- #configurable via the configuration file, but make sure that
69
- #1. The keys you use for your configuration don't already exist,
70
- #2. options specified on the commandline take precedence over values
71
- # set in the configuration file and
72
- #3. you <b>do not alter</b> the hash.
73
- def execute(config)
74
- raise(NotImplementedError, "#{__method__} has to be overriden in a subclass!")
75
- end
76
-
77
- end
78
-
79
- end
80
-
81
- end
1
+ #Encoding: UTF-8
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ module CUICommands
24
+
25
+ #Class for invalid command-line argument errors.
26
+ class InvalidCommandline < Errors::SmcGetError
27
+ end
28
+
29
+ #This is the superclass of all CUI commands. To make your own command,
30
+ #subclass it an overwrite ::help, ::summary, #parse and #execute.
31
+ class Command
32
+
33
+ #The string returned from this method will be displayed to the
34
+ #user if he issues <tt>smc-get help YOURCOMMAND</tt>.
35
+ def self.help
36
+ "(nothing known)"
37
+ end
38
+
39
+ #One-line summary of the command that shows up in the COMMANDS
40
+ #section of <tt>smc-get help</tt>. Should not be longer than 78
41
+ #characters due to automatic indentation. You may have to insert
42
+ #tabs to make it display correctly; make sure to check the result by
43
+ #issueing <tt>smc-get help</tt>.
44
+ def self.summary
45
+ ""
46
+ end
47
+
48
+ #Creates a new instance of this command. Do not override this, or
49
+ #call at least +super+.
50
+ def initialize(cui, args)
51
+ @cui = cui
52
+ parse(args)
53
+ end
54
+
55
+ #This method gets all commandline options relevant for this subcommand
56
+ #passed in the +args+ array. You should parse them destructively, i.e.
57
+ #after you finished parsing, +args+ should be an empty array.
58
+ #Set instance variables to save data.
59
+ #
60
+ #Note that SmcGet is not set up when this method is called, so you
61
+ #cannot do things like <tt>Package.new</tt>.
62
+ def parse(args)
63
+ raise(NotImplementedError, "#{__method__} has to be overriden in a subclass!")
64
+ end
65
+
66
+ #Execute the command. You can use the instance variables set in #parse.
67
+ #The method gets passed the parsed contents of smc-get's configuration
68
+ #files and commandline parameters; you can use this to make your command
69
+ #configurable via the configuration file, but make sure that
70
+ #1. The keys you use for your configuration don't already exist,
71
+ #2. options specified on the commandline take precedence over values
72
+ # set in the configuration file and
73
+ #3. you <b>do not alter</b> the hash.
74
+ def execute(config)
75
+ raise(NotImplementedError, "#{__method__} has to be overriden in a subclass!")
76
+ end
77
+
78
+ private
79
+
80
+ #Downloads the package identified by +pkgname+ and displays the progress to the
81
+ #user. Example:
82
+ # download_package("cool_world")
83
+ #downloads the package "cool_world.smcpak". You mustn’t specify the file
84
+ #extension <tt>.smcpak</tt>.
85
+ def download_package(pkgname)
86
+ pkg_file = "#{pkgname}.smcpak"
87
+ #Windows doesn't understand ANSI escape sequences, so we have to
88
+ #use the carriage return and reprint the whole line.
89
+ base_str = "\rDownloading %s... (%.2f%%)"
90
+ tries = 0
91
+ begin
92
+ tries += 1
93
+ path_to_package = @cui.remote_repository.fetch_package(pkg_file, SmcGet.temp_dir) do |bytes_total, bytes_done|
94
+ percent = ((bytes_done.to_f / bytes_total) * 100)
95
+ print "\r", " " * 80 #Clear everything written before
96
+ printf(base_str, pkg_file, percent)
97
+ end
98
+ rescue OpenURI::HTTPError => e #Thrown even in case of FTP and HTTPS
99
+ if tries >= 3
100
+ raise #Bubble up
101
+ else
102
+ $stderr.puts("ERROR: #{e.message}")
103
+ $stderr.puts("Retrying.")
104
+ retry
105
+ end
106
+ end
107
+ puts #Terminating \n
108
+ return path_to_package
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ # vim:set ts=8 sts=2 sw=2 et: #