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,80 @@
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
+ #This class allows for easy compression and decompression of .smcpak
24
+ #files. Please note that during this process no validations are performed,
25
+ #i.e. you have to ensure that the directory you want to compress is in
26
+ #smc package layout and the .smcpak files you want to decompress are
27
+ #really smc packages.
28
+ class PackageArchive
29
+
30
+ #The location of this archive.
31
+ attr_reader :path
32
+
33
+ #Compresses all files in +directory+ into a TAR.XZ file with the
34
+ #extension ".smcpak".
35
+ #Returns an object of this class.
36
+ # smcpak = PackageArchive.compress("mypackage")
37
+ # puts smcpak.path #=> mypackage.smcpak
38
+ def self.compress(directory, goal_file)
39
+ directory = Pathname.new(directory).expand_path
40
+ tar_file = Pathname.new("#{goal_file}.tar").expand_path
41
+ xz_file = Pathname.new(goal_file).expand_path
42
+
43
+ Dir.chdir(directory.parent) do
44
+ tar_file.open("wb") do |file|
45
+ Archive::Tar::Minitar.pack(directory.relative_path_from(Pathname.new(".").expand_path).to_s, file)
46
+ end
47
+ end
48
+ XZ.compress_file(tar_file, xz_file)
49
+
50
+ tar_file.delete #We don’t need it anymore
51
+
52
+ new(xz_file)
53
+ end
54
+
55
+ #Creates a new PackageArchive from an existing .smcpak file.
56
+ def initialize(archive)
57
+ @path = Pathname.new(archive)
58
+ end
59
+
60
+ #Decompresses this archive into +directory+, creating a subdirectory
61
+ #named after the archive without the extension, and returns the path
62
+ #to that subdirectory (a Pathname object).
63
+ # smcpak = PackageArchive.new("mypackage.smcpak")
64
+ # puts smcpak.decompress #=> #<Pathname:mypackage>
65
+ def decompress(directory)
66
+ directory = Pathname.new(directory)
67
+ tar_file = directory + @path.basename.to_s.sub(/\.smcpak$/, ".tar")
68
+ dest_dir = directory + tar_file.basename.to_s.sub(/\.tar$/, "")
69
+
70
+ XZ.decompress_file(@path.to_s, tar_file.to_s)
71
+ Archive::Tar::Minitar.unpack(tar_file.to_s, dest_dir.to_s)
72
+
73
+ tar_file.delete #We don’ŧ need it anymore
74
+
75
+ dest_dir
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,253 @@
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
+ #A PackageSpecification object is a mostly informational object
24
+ #directly related to Package objects. That is, each and every
25
+ #Package instance has a Package#spec method that will retrieve
26
+ #the PackageSpecification, an instance of this class, for you:
27
+ #
28
+ # puts pkg.spec.title #=> Cool package
29
+ #
30
+ #Instances of this class can be understood as the parsed content of
31
+ #a package’s specification file. It has getter and setter methods
32
+ #reflecting the keys that are allowed in the specification (see
33
+ #the smcpak.rdoc file), but unless you want to build smc packages,
34
+ #there’s no need for you to use the setter methods.
35
+ class PackageSpecification
36
+
37
+ #The keys listed here must be mentioned inside a package spec,
38
+ #otherwise the package is considered broken.
39
+ SPEC_MANDATORY_KEYS = [:title, :last_update, :authors, :difficulty, :description, :checksums].freeze
40
+
41
+ ##
42
+ # :attr_accessor: title
43
+ #The package’s title.
44
+
45
+ ##
46
+ # :attr_accessor: last_update
47
+ #The time (an instance of class Time) indicating when the package
48
+ #was last updated.
49
+
50
+ ##
51
+ # :attr_accessor: authors
52
+ #The authors of this package. An array.
53
+
54
+ ##
55
+ # :attr_accessor: difficulty
56
+ #The difficulty of this package as a string.
57
+
58
+ ##
59
+ # :attr_accessor: description
60
+ #The description of this package.
61
+
62
+ ##
63
+ # :attr_accessor: install_message
64
+ #A message to display during installation of this package or nil if
65
+ #no message shall be displayed.
66
+
67
+ ##
68
+ # :attr_accessor: remove_message
69
+ #A message to display during removing of this package or nil if no
70
+ #message shall be displayed.
71
+
72
+ ##
73
+ # :attr_accessor: dependecies
74
+ #An array of package names this package depends on, i.e. packages that
75
+ #need to be installed before this one can be installed. The array is
76
+ #empty if no dependecies exist.
77
+
78
+ ##
79
+ # :attr_accessor: levels
80
+ #An array of level file names (strings).
81
+
82
+ ##
83
+ # :attr_accessor: music
84
+ #An array of music file names (strings).
85
+
86
+ ##
87
+ # :attr_accessor: sounds
88
+ #An array of sound file names (strings).
89
+
90
+ ##
91
+ # :attr_accessor: graphics
92
+ #An array of graphic file names (strings).
93
+
94
+ ##
95
+ # :attr_accessor: worlds
96
+ #An array of graphic file names (strings).
97
+
98
+ ##
99
+ # :attr_accessor: checksums
100
+ #A hash that maps each filename in this package to it’s SHA1 checksum.
101
+
102
+ #The name of the package this specification is used in, without any
103
+ #file extension.
104
+ attr_reader :name
105
+
106
+ ##
107
+ # :attr_reader: compressed_file_name
108
+ #The name of the compressed file this specification should belong to.
109
+ #The same as name, but the extension .smcpak was appended.
110
+
111
+ ##
112
+ # :attr_reader: spec_file_name
113
+ #The name of the specification file. The same as name, but
114
+ #the extension .yml was appended.
115
+
116
+ #Creates a PackageSpecification by directly reading a complete
117
+ #spec from a YAML file.
118
+ #==Parameter
119
+ #[path] The path to the file to read.
120
+ #==Return value
121
+ #An instance of this class.
122
+ #==Raises
123
+ #[InvalidSpecification] +path+ was not found or was malformatted.
124
+ #==Example
125
+ # path = remote_repo.fetch_spec("cool_pkg.yml")
126
+ # spec = SmcGet::PackageSpecification.from_file(path)
127
+ # puts spec.title #=> "Cool package"
128
+ #==Remarks
129
+ #This method may be useful if you don’t need a full-blown
130
+ #Package object and just want to deal with it’s most important
131
+ #attributes.
132
+ def self.from_file(path)
133
+ info = nil
134
+ begin
135
+ info = YAML.load_file(path.to_s)
136
+ rescue Errno::ENOENT => e
137
+ raise(Errors::InvalidSpecification, "File '#{path}' doesn't exist!")
138
+ rescue => e
139
+ raise(Errors::InvalidSpecification, "Invalid YAML: #{e.message}")
140
+ end
141
+
142
+ spec = new(File.basename(path).sub(/\.yml$/, ""))
143
+ info.each_pair do |key, value|
144
+ spec.send(:"#{key}=", value)
145
+ end
146
+ #TODO: Convert the strings in :checksums to strings, except the
147
+ #filenames, those should be strings. Anyone???
148
+
149
+ raise(Errors::InvalidSpecification, spec.validate.first) unless spec.valid?
150
+
151
+ spec
152
+ end
153
+
154
+ #Returns the matching package name from the package specification’s name
155
+ #by replacing the .yml extension with .smcpak.
156
+ #==Return value
157
+ #A string ending in ".smcpak".
158
+ #==Example
159
+ # p SmcGet::PackageSpecification.spec2pkg("cool_pkg.yml") #=> "cool_pkg.smcpak")
160
+ def self.spec2pkg(spec_file_name) # :nodoc:
161
+ spec_file_name.to_s.sub(/\.yml$/, ".smcpak")
162
+ end
163
+
164
+ #Returns the matching specification file name from the package’s name
165
+ #by replacing the .smcpak extension with .yml.
166
+ #==Return value
167
+ #A string ending in ".yml"
168
+ #==Example
169
+ # p SmcGet::PackageSpecification.pkg2spec("cool_pkg.smcpak") #=> "cool_pkg.yml"
170
+ def self.pkg2spec(package_file_name) # :nodoc:
171
+ package_file_name.to_s.sub(/\.smcpak$/, ".yml")
172
+ end
173
+
174
+ def initialize(pkg_name)
175
+ @info = {:dependencies => [], :levels => [], :music => [], :sounds => [], :graphics => [], :worlds => []}
176
+ @name = pkg_name
177
+ end
178
+
179
+ #See attribute.
180
+ def compressed_file_name # :nodoc:
181
+ "#@name.smcpak"
182
+ end
183
+
184
+ #See attribute.
185
+ def spec_file_name # :nodoc:
186
+ "#@name.yml"
187
+ end
188
+
189
+ [:title, :last_update, :authors, :difficulty, :description, :install_message, :remove_message, :dependencies, :levels, :music, :sounds, :graphics, :worlds, :checksums].each do |sym|
190
+ define_method(sym){@info[sym]}
191
+ define_method(:"#{sym}="){|val| @info[sym] = val}
192
+ end
193
+
194
+ def [](sym)
195
+ if respond_to?(sym)
196
+ send(sym)
197
+ else
198
+ raise(IndexError, "No such specification key: #{sym}!")
199
+ end
200
+ end
201
+
202
+ def valid?
203
+ validate.empty?
204
+ end
205
+
206
+ def validate
207
+ errors = []
208
+
209
+ SPEC_MANDATORY_KEYS.each do |sym|
210
+ errors << "Mandatory key #{sym} is missing!" unless @info.has_key?(sym)
211
+ end
212
+
213
+ errors
214
+ end
215
+
216
+ #Compares two specifications. They are considered equal if all their
217
+ #attributes (levels, difficulty, etc.) are equal.
218
+ def ==(other)
219
+ return false unless other.respond_to? :info
220
+ @info == other.info
221
+ end
222
+
223
+ #Saves the package specification in YAML format into a file.
224
+ #==Parameter
225
+ #[directory] The directory where to save the file to. The filename is automatically
226
+ # detected from the attributes set for the specification.
227
+ #==Raises
228
+ #[InvalidSpecification] The specification was not valid, i.e. contained incorrect
229
+ # or missing values.
230
+ #==Example
231
+ # p spec.name #=> "cool_pkg"
232
+ # spec.save(".")
233
+ # p File.file?("cool_pkg.yml") #=> true
234
+ def save(directory)
235
+ raise(Errors::InvalidSpecification, validate.first) unless valid?
236
+
237
+ path = Pathname.new(directory) + "#{@name}.yml"
238
+ #Turn the spec keys for serialization into strings
239
+ hsh = {}
240
+ @info.each_pair{|k, v| hsh[k.to_s] = v}
241
+ path.open("w"){|f| YAML.dump(hsh, f)}
242
+ end
243
+
244
+ protected
245
+
246
+ #Returns the complete internal information hash.
247
+ def info
248
+ @info
249
+ end
250
+
251
+ end
252
+
253
+ end
@@ -0,0 +1,272 @@
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
+ #A RemoteRepository represents a web location from which <tt>smc-get</tt>
24
+ #is able to download packages. The structure of those repositories is
25
+ #explained in the smcpak.rdoc file in the <i>Repostories</i> section.
26
+ class RemoteRepository < Repository
27
+
28
+ #Name of the remote directory containing the <tt>.smcpak</tt> files.
29
+ PACKAGES_DIR = "packages"
30
+ #Name of the remote directory containing the packages specifications.
31
+ SPECS_DIR = "specs"
32
+ #Name of the file containg the names of all packages in this repository.
33
+ LIST_FILE = "packages.lst"
34
+ #Number of bytes to read while downloading a file.
35
+ CHUNK_SIZE = 1024
36
+
37
+ ##
38
+ # :attr_reader: packages
39
+ #Returns an array of remote Packages that contains all packages
40
+ #in this repository. This is a very time-intensive operation, depending
41
+ #on how many packages the repository contains, because it downloads all
42
+ #the package specifications of all the packages. It’s not a good idea
43
+ #to call this.
44
+
45
+ #An URI object representing the repository’s URI.
46
+ attr_reader :uri
47
+ #A list of all package names (without an extension).
48
+ attr_reader :packages_list
49
+
50
+ #Creates a "new" Repository.
51
+ #==Parameters
52
+ #[uri] The URI of the repository, something like
53
+ # "\http://my-host.org/smc-repo".
54
+ #==Raises
55
+ #[InvalidRepository] The repository is either nonexistant or doesn’t obey
56
+ # the repository structure format.
57
+ #==Return value
58
+ #The newly created repository.
59
+ #==Usage
60
+ # r = Repository.new("http://myrepo.org")
61
+ # r = Repository.new("https://my-host.org/smc-repo")
62
+ # r = Repository.new("ftp://ftp.my-host.org/smc")
63
+ def initialize(uri)
64
+ @uri = URI.parse(uri)
65
+ #Download the packages list. Usually it’s small enough to fit into RAM.
66
+ begin
67
+ @packages_list = open(@uri + LIST_FILE){|tmpfile| tmpfile.read}.split
68
+ rescue SocketError, Errno::ECONNREFUSED, OpenURI::HTTPError => e #open-uri raises HTTPError even in case of other protocols
69
+ raise(Errors::InvalidRepository.new(@uri), e.message)
70
+ end
71
+ end
72
+
73
+ #Downloads the given package specification from this repository and
74
+ #places it in +directory+.
75
+ #
76
+ #If the file already exists, it’s overwritten.
77
+ #==Parameters
78
+ #[spec_file] The package specification file you want to download. It must
79
+ # end with the <tt>.yml</tt> extension.
80
+ #[directory] (".") The directory where you want to download the file to.
81
+ # Created if it doesn’t exist.
82
+ #==Raises
83
+ #[NoSuchResourceError] The package name for this specification couldn’t be
84
+ # found in the repository. Note that the spec may be
85
+ # there despite of this, but packages not listed
86
+ # in the repository’s contents file are treated as if
87
+ # they weren’t there.
88
+ #==Return value
89
+ #The path to the downloaded file as a Pathname object.
90
+ #==Usage
91
+ # my_repo.fetch_spec("mypackage.yml", "/home/freak/Downloads")
92
+ #
93
+ # my_repo.fetch_spec("mypackage.yml")
94
+ def fetch_spec(spec_file, directory = ".")
95
+ directory = Pathname.new(directory)
96
+ pkg_name = spec_file.sub(/\.yml$/, "")
97
+ goal_file = directory + spec_file
98
+
99
+ unless @packages_list.include?(pkg_name)
100
+ raise(Errors::NoSuchResourceError.new(:spec, spec_file), "Package '#{pkg_name}' not found in the repository!")
101
+ end
102
+
103
+ directory.mktree unless directory.directory?
104
+
105
+ #I am quite sure that URI#merge has a bug. Example:
106
+ # uri = URI.parse("http://www.ruby-lang.org")
107
+ #Now try to append the path test/test2:
108
+ # uri2 = uri + "test" + "test2"
109
+ #What do you think contains uri2? This:
110
+ # http://www.ruby-lang.org/test2
111
+ #Something missing, eh? Even more surprising, this one works as
112
+ #expected:
113
+ # uri + "test/test2"
114
+ #The second one is the workaround I use in the following line.
115
+ open(@uri.merge("#{SPECS_DIR}/#{spec_file}")) do |tempfile|
116
+ File.open(goal_file, "w") do |file|
117
+ file.write(tempfile.read) #Specs almost ever are small enough to fit in RAM
118
+ end
119
+ end
120
+ goal_file
121
+ end
122
+
123
+ #Downloads the given package from this repository and places it in
124
+ #+directory+. Yields the package’s total size in bytes and how many
125
+ #bytes have already been downloaded. If the size is unknown for some
126
+ #reason, +bytes_total+ is nil. For the last block call, +bytes_total+
127
+ #and +bytes_done+ are guaranteed to be equal, except if +bytes_total+
128
+ #couldn’t be determined in which case it’s still nil.
129
+ #
130
+ #If the file does already exist in +directory+, it is overwritten.
131
+ #==Parameters
132
+ #[pkg_file] The package file you want to download. It must end in
133
+ # the <tt>.smcpak</tt> extension.
134
+ #[directory] (".") The directory where you want to download the file to.
135
+ # Created if it doesn’t exist.
136
+ #==Raises
137
+ #[NoSuchPackageError] The package name for this package couldn’t be
138
+ # found in the repository. Note that the package may be
139
+ # there despite of this, but packages not listed
140
+ # in the repository’s contents file are treated as if
141
+ # they weren’t there.
142
+ #[OpenURI::HTTPError] Connection error.
143
+ #==Return value
144
+ #The path to the downloaded file as a Pathname object.
145
+ #==Usage
146
+ # my_repo.fetch_package("mypackage.smcpak", "/home/freak/downloads")
147
+ #
148
+ # my_repo.fetch_package("mypackage.smcpak") do |bytes_total, bytes_done|
149
+ # print("\rDownloaded #{bytes_done} bytes of #{bytes_total}.") if bytes_total
150
+ # end
151
+ def fetch_package(pkg_file, directory = ".")
152
+ directory = Pathname.new(directory)
153
+ pkg_name = pkg_file.sub(/\.smcpak$/, "")
154
+ goal_file = directory + pkg_file
155
+
156
+ unless @packages_list.include?(pkg_name)
157
+ raise(Errors::NoSuchPackageError.new(pkg_name), "ERROR: Package '#{pkg_name}' not found in the repository!")
158
+ end
159
+
160
+ directory.mktree unless directory.directory?
161
+
162
+ bytes_total = nil
163
+ size_proc = lambda{|content_length| bytes_total = content_length}
164
+ prog_proc = lambda{|bytes_done| yield(bytes_total, bytes_done)}
165
+
166
+ #See the source of #fetch_spec for an explanation on the obscure
167
+ #URI concatenation.
168
+ open(@uri + "#{PACKAGES_DIR}/#{pkg_file}", "rb", content_length_proc: size_proc, progress_proc: prog_proc) do |tempfile|
169
+ #The packages may be too big for fitting into RAM, therefore we’re going
170
+ #to read and write the packages chunk by chunk. Btw. please notice me
171
+ #if you find a SMC package that’s larger than 4 GiB! I’d be curious
172
+ #about what it contains!
173
+ File.open(goal_file, "wb") do |file|
174
+ while chunk = tempfile.read(CHUNK_SIZE)
175
+ file.write(chunk)
176
+ end
177
+ end
178
+ end
179
+
180
+ goal_file
181
+ end
182
+
183
+ #Not implemented yet.
184
+ def install(package, &block)
185
+ raise(NotImplementedError, "Can't automatically upload to remote repositories yet!")
186
+ end
187
+
188
+ #Not implemented yet.
189
+ def uninstall(pkg_name)
190
+ raise(NotImplementedError, "Can't automatically remove from remote repositories yet!")
191
+ end
192
+
193
+ #Returns the URI of the remote repository.
194
+ def to_s
195
+ @uri.to_s
196
+ end
197
+
198
+ #See attribute.
199
+ # def packages # :nodoc:
200
+ # @packages_list.map do |pkg_name|
201
+ # Package.from_repository(self, "#{pkg_name}.yml")
202
+ # end
203
+ # end
204
+
205
+ #True if a package with the given name (without the .smcpak extension)
206
+ #exists in the repository.
207
+ def contain?(pkg)
208
+ if pkg.kind_of? Package
209
+ @packages_list.include?(pkg.spec.name)
210
+ else
211
+ @packages_list.include?(pkg_name)
212
+ end
213
+ end
214
+ alias contains? contain?
215
+
216
+ #call-seq:
217
+ # search(regexp [, *attributes ]){|pkgname|...}
218
+ #
219
+ #Searches for a specific package and yields each candidate to the
220
+ #given block.
221
+ #==Parameters
222
+ #[regexp] The Regular Expression to use as the search pattern.
223
+ #[*attributes] (<tt>[:name]</tt>) A list of all attributes to match
224
+ # the Regular Expression against (note that only
225
+ # passing :name is siginificantly faster, because
226
+ # there’s no need to download the specs).
227
+ # Possible attributes:
228
+ # * name
229
+ # * title
230
+ # * authors
231
+ # * difficulty
232
+ # * description
233
+ # * levels
234
+ # * music
235
+ # * sounds
236
+ # * graphics
237
+ # * worlds
238
+ #[pkgname] *Blockargument*. The package name (not title!) of
239
+ # a package matching the search criteria.
240
+ #==Examples
241
+ # rp.search(/cool/){|pkgname| p pkgname} #=> "cool_levels"
242
+ # rp.search(/Luiji/i, :authors){|pkgname| p pkgname} #=> All packages created by Luiji or luiji...
243
+ def search(regexp, *attributes)
244
+ attributes << :name if attributes.empty? #Default value
245
+ if attributes == [:name] #Good, no need to download all the specs
246
+ @packages_list.each{|name| yield(name) if name =~ regexp}
247
+ else #OK, so we need to download one spec after the other...
248
+ @packages_list.each do |pkgname|
249
+ spec = PackageSpecification.from_file(fetch_spec("#{pkgname}.yml", SmcGet.temp_dir))
250
+ attributes.each do |att|
251
+ case att
252
+ when :name then yield(pkgname) if spec.name =~ regexp
253
+ when :title then yield(pkgname) if spec.title =~ regexp
254
+ when :authors then yield(pkgname) if spec.authors.any?{|a| a =~ regexp}
255
+ when :difficulty then yield(pkgname) if spec.difficulty =~ regexp
256
+ when :description then yield(pkgname) if spec.description =~ regexp
257
+ when :levels then yield(pkgname) if spec.levels.any?{|l| l =~ regexp}
258
+ when :music then yield(pkgname) if spec.music.any?{|m| m =~ regexp}
259
+ when :sounds then yield(pkgname) if spec.sound.any?{|s| s =~ regexp}
260
+ when :graphics then yield(pkgname) if spec.graphics.any?{|g| g =~ regexp}
261
+ when :worlds then yield(pkgname) if spec.worlds.any?{|w| w =~ regexp}
262
+ else
263
+ $stderr.puts("Warning: Unknown attribute #{att}, ignoring it.")
264
+ end #case
265
+ end #attributes.each
266
+ end # @packages.each
267
+ end #if only :name
268
+ end #search
269
+
270
+ end #RemoteRepository
271
+
272
+ end #SmcGet