smc-get 0.1.0 → 0.2.0.beta1

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.
@@ -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: #