outsider 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,160 @@
1
+ ==Introduction
2
+ Outsider is a rubygems plugin which allows a gem to install files outside its own directory.
3
+
4
+ ===History
5
+ Outsider was born because an application I am developing needed to install icons and desktop
6
+ files in a system wide directory (such as <tt>/usr/share</tt>) to correctly integrate
7
+ with the desktop environment. Since (as far as I know) rubygems doesn't offer this
8
+ possibility, but I definitly wanted the user to install my application using a
9
+ simple <tt>gem install</tt> command, rather than using a more complicated build
10
+ system, I decided to write a general rubygems plugin allowing this.
11
+
12
+ ===Features
13
+ * Allows a gem developer to specify which files should be installed system-wide,
14
+ and where exactly to install them, in a YAML file called outsider_files
15
+ placed in the top level directory of the gem
16
+ * Missing directories in the installation path are created automatically
17
+ * Allows ERB tags in the installation paths, so that they can be tailored on the
18
+ user's system
19
+ * Automatically detects whether the gem is being installed globally or in the user's
20
+ home directory. Allows to specify two different installation paths or changes
21
+ the default installation path so that files are installed in the home directory
22
+ in case of a user install
23
+ * Automatically handles uninstalls of the files when the gem is installed
24
+ * In case two gems (including multiple versions of the same gem) install a file
25
+ in the same place, automatically reinstalls the file owned by the older gem when
26
+ the newer one is installed
27
+
28
+ ===Drawbacks
29
+ * To find out whether a user or a system-wide install is being performed, Outsider
30
+ checks whether the gem installation directory is a subdirectory of <tt>ENV['HOME']</tt>.
31
+ This means that things will break if you have the +GEM_HOME+ environment variable
32
+ also pointing to a subdirectory of your home directory. This should be a rare
33
+ situation, however.
34
+ * When multiple gems install a file in the same place, the gem installed last will
35
+ overwrite the file installed by the other gem, so only the last version of the
36
+ file will be availlable. This should only be an issue when having different versions
37
+ of the same gem installed (as different gems shouldn't install the same file).
38
+ This can lead to problems if an older version of the gem is loaded and if the
39
+ files installed by different versions of the gem are incompatible.
40
+ * It is only tested on linux. It should work on UNIX-like systems. As it is, I'm
41
+ almost positive it doesn't work on Windows. Most of the plugin is system-independent,
42
+ but there will be issues with default paths and user-installs.
43
+
44
+ ==Usage
45
+ To have Outsider install files outside the gem directory, you need to include in
46
+ the gem sources a file called +outsider_files+ containing a YAML hash. Note that
47
+ both the +outsider_files+ and the files to install should be added to the +source+
48
+ attribute of the gem specification object.
49
+
50
+ Keys in the hash represent the paths of the files to install relative to the gem directory,
51
+ while entries can be either strings or arrays with two elements. If the entry is
52
+ an array, the first element is the installation path in case of a system-wide install,
53
+ while the second is the the path in case of a user install. In all cases, installation
54
+ paths can contain ERB templates. In the case of a user install, you can specify
55
+ the user's home directory with ~ (but only at the beginning of the file). You *can't*
56
+ use this way to specify another user's home directory, however.
57
+
58
+ In all cases, if the destination path (after all replacements have been carried out)
59
+ ends with a slash (+/+), the name of the original file (as returned by File.basename)
60
+ is appended to it.
61
+
62
+ ===User installs
63
+ The following algorithm is used to determine the path to install a given file
64
+ in case of user install:
65
+ * if the entry associated with a file is an array, the second value in the array
66
+ will be used. If it's a relative path (that is, it doesn't start with +/+), then
67
+ it is assumed to be relative to the home directory. For example, if the second
68
+ entry of the array is <tt>abc/def.rb</tt>, then the file will be installed in
69
+ <tt>ENV['HOME']/abc/def.rb</tt>
70
+ * if the entry associated with the file is a string, the path it contains is considered
71
+ relative to the user's home directory (even if the path is absolute). So, if the
72
+ path is <tt>/xyz/abc.rb</tt>, it will be installed in <tt>ENV['HOME']/xyz/abc.rb</tt>.
73
+ However, often this is not what one wants. For example, a file usually installed
74
+ in <tt>/usr/local/</tt> should be installed directly in <tt>ENV['HOME']</tt>,
75
+ not in <tt>ENV['HOME']/usr/local</tt>. To avoid this, the following replacements
76
+ are performed *at the beginning of the string*:
77
+ * <tt>/bin/</tt> -> <tt>ENV['HOME']/bin</tt>
78
+ * <tt>/sbin/</tt> -> <tt>ENV['HOME']/bin</tt>
79
+ * <tt>/usr/sbin/</tt> -> <tt>ENV['HOME']/bin</tt>
80
+ * <tt>/usr/local/share/</tt> -> <tt>ENV['HOME']/.local/share</tt>
81
+ * <tt>/usr/share/</tt> -> <tt>ENV['HOME']/.local/share</tt>
82
+ * <tt>/usr/</tt> -> <tt>ENV['HOME']</tt>
83
+ * <tt>/usr/local/</tt> -> <tt>ENV['HOME']</tt>
84
+ * <tt>/var/</tt> -> <tt>ENV['HOME']</tt>
85
+ * <tt>/opt/</tt> -> <tt>ENV['HOME']</tt>
86
+ * <tt>/etc/</tt> -> <tt>ENV['HOME']</tt>
87
+
88
+ For example, if the installation path is <tt>/usr/local/share/my_dir/my_file</tt>,
89
+ in case of a user install the file will be installed in <tt>ENV['HOME']/.local/share/my_dir/my_file</tt>.
90
+ If you don't want this replacements to be performed, then you need to specify
91
+ a separate path for the user install (by using an array rather than a string
92
+ in +outsider_files+).
93
+
94
+ In all cases, all these operations are carried out *after* having expanded the
95
+ ERB templates.
96
+
97
+ ===Example
98
+
99
+ This is an example +outsider_files+.
100
+
101
+ file1: /usr/share/file1
102
+ file2: "<%= `kde4-config --path apps`.strip.split(':')[-1]%/file2>"
103
+ dir/file3: [/usr/file3, "<%=File.join ENV['HOME'], 'dir', 'file3'%>"]
104
+ file4: [/etc/file4, my_dir/file4]
105
+ file5: /usr/dir/
106
+ file6: [/usr/file6, ~/test/file6]
107
+
108
+ In case of a global installation, this produces the following files (note that the
109
+ third entry depends on your system and will break if you dont't have the kde4-config
110
+ program in your PATH):
111
+ * <tt>/usr/share/file1</tt>
112
+ * <tt>/usr/share/applnk/file2</tt>
113
+ * <tt>/usr/file3</tt>
114
+ * <tt>/etc/file4</tt>
115
+ * <tt>/usr/dir/file5</tt>
116
+ * <tt>/usr/file6</tt>
117
+
118
+ In case of a user install, this is what you'd get (assuming your home directory
119
+ is <tt>/home/your_name</tt>):
120
+ * <tt>/home/your_name/.local/share/file1</tt>
121
+ * <tt>/home/your_name/applnk/file2</tt>
122
+ * <tt>/home/your_name/dir/file3</tt>
123
+ * <tt>/home/your_name/file4</tt>
124
+ * <tt>/home/your_name/file5</tt>
125
+ * <tt>/home/your_name/test/file6</tt>
126
+
127
+ === The record file
128
+ To keep track of which files each gem has installed, Outsider uses files called
129
+ +record_files+.
130
+
131
+ A record file contains a list of files installed using Outsider, together with
132
+ the gems which installed them and the _full path_ of the original files from which
133
+ they were installed.
134
+
135
+ There are two record files, one used to keep trace of files installed during global
136
+ installations and one used for user installations. The latter is always
137
+ <tt>ENV['HOME']/.outsider/installed_files</tt>, while the former is determined
138
+ according to the following algorithm:
139
+ * if the +OUTSIDER_RECORD_FILE+ environment variable is set, the path it contains
140
+ will be used (this is mainly for testing, you shouldn't use it)
141
+ * if the +OUTSIDER_RECORD_FILE+ environment variable isn't set and the file
142
+ <tt>/etc/outsider.conf</tt> exists and contains a single filename, then that
143
+ file will be used
144
+ * otherwise the file <tt>/var/lib/outsider/installed_files</tt> will be used
145
+
146
+ If a file with the same name as the record file already exists but is not a valid
147
+ record file, a warning will be issued, the file will be renamed and a new record
148
+ file will be created.
149
+
150
+ If the record file is deleted or becomes broken, already installed files won't
151
+ be uninstalled any more. The next time you install a gem, a new record file will
152
+ be created.
153
+
154
+ ==Author
155
+ Stefano Crocco (stefano.crocco@alice.it)
156
+
157
+ == License
158
+ Outsider is Copyright © 2010 Stefano Crocco.
159
+
160
+ Outside is free software and is distributed under the terms of the Ruby license
@@ -0,0 +1,305 @@
1
+ # Copyright (c) 2010 Stefano Crocco <stefano.crocco@alice.it>
2
+ # Distributed under the terms of the Ruby license
3
+
4
+ require 'yaml'
5
+ require 'fileutils'
6
+ require 'erb'
7
+ require 'pathname'
8
+
9
+ # Namespace for the Outsider gem
10
+ module Outsider
11
+
12
+ # Class which installs and uninstalls files
13
+ class Installer
14
+
15
+ # The path of the record file to use if the +OUTSIDER_RECORD_FILE+
16
+ # environment variable is unset
17
+ DEFAULT_RECORD_FILE = File.join '/', 'var', 'lib', 'outsider', 'installed_files'
18
+
19
+ # Exception raised when the record file is not a valid record file
20
+ class InvalidRecordFile < StandardError
21
+
22
+ # The path of the invalid record file
23
+ attr_reader :file
24
+
25
+ # Creates a new instance
26
+ #
27
+ # _file_ is the name of the invalid record file, while _msg_ is a message
28
+ # describing why the file is invalid
29
+ def initialize file, msg
30
+ @file = file
31
+ super msg + ": #{@file}"
32
+ end
33
+
34
+ end
35
+
36
+ # Creates a new instance
37
+ #
38
+ # _dir_ is the directory where to look for the outsider_files file and the
39
+ # files to install
40
+ def initialize dir
41
+ @gem_dir = dir
42
+ @user_install = dir.start_with? ENV['HOME']
43
+ @data = begin
44
+ YAML.load File.read(File.join(dir, 'outsider_files'))
45
+ rescue SystemCallError
46
+ end
47
+ # If either the outsider_files file doesn't exist or it's empt
48
+ # (in which case YAML.load returns false), set @data to an empty hash
49
+ @data ||={}
50
+ @gem_name = File.basename(dir)
51
+ end
52
+
53
+ # Installs the files according to the instructions in the outsider_files
54
+ # file
55
+ #
56
+ # Returns *nil*
57
+ def install_files
58
+ installed_files = []
59
+ @data.each_pair do |k, v|
60
+ dest = install_destination v
61
+ dest = File.join dest, File.basename(k) if dest.end_with? '/'
62
+ orig = File.join(@gem_dir, k)
63
+ installed_files << [orig, dest] if install_file orig, dest
64
+ end
65
+ record_installed_files installed_files unless installed_files.empty?
66
+ nil
67
+ end
68
+
69
+ # Uninstalls the files associated with the gem in the current directory
70
+ #
71
+ # If any of those file is owned also by other gems (including by other versions
72
+ # of the same gem), then the files are only uninstalled if the gem is the last to
73
+ # have been installed. In this case, the file belonging to the previously installed
74
+ # gem is copied. If that file doesn't exist, then the one previous to it is tried,
75
+ # and so on.
76
+ #
77
+ # The record is updated to remove all mentions of the gem being uninstalled
78
+ #
79
+ # Returns *nil*
80
+ def uninstall_files
81
+ rec_file = record_file
82
+ files = begin read_record_file rec_file
83
+ rescue InvalidRecordFile then nil
84
+ end
85
+ return unless files
86
+ files.each_pair do |f, data|
87
+ gem_data = data.find{|i| i[:gem] == @gem_name}
88
+ next unless gem_data
89
+ if data.last[:gem] == @gem_name
90
+ FileUtils.rm_f f
91
+ data[0..-2].reverse_each do |d|
92
+ if File.exist? d[:origin]
93
+ FileUtils.cp d[:origin], f
94
+ break
95
+ end
96
+ end
97
+ end
98
+ data.delete gem_data
99
+ end
100
+ files.delete_if{|k, v| v.empty?}
101
+ write_record_file rec_file, files
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ # The path of the record file
108
+ #
109
+ # In case of a user install, the file is always <tt>ENV['HOME']/.outsider/installed_files</tt>.
110
+ # In case of a global install, instead, the following algorithm is used:
111
+ # * if the +OUTSIDER_RECORD_FILE+ environment variable is set,
112
+ # its value is used
113
+ # * otherwise, if the file /etc/outsider.conf exists and isn't
114
+ # empty, the name of the record file is read from there
115
+ # * if all the above fails, then DEFAULT_RECORD_FILE is used
116
+ #
117
+ # Returns the name of the record file to use
118
+ def record_file
119
+ if @user_install then File.join ENV["HOME"], '.outsider', 'installed_files'
120
+ elsif ENV['OUTSIDER_RECORD_FILE'] then ENV['OUTSIDER_RECORD_FILE']
121
+ elsif File.exist?('/etc/outsider.conf')
122
+ file = File.read '/etc/outsider.conf'
123
+ file.empty? ? DEFAULT_RECORD_FILE : file
124
+ else DEFAULT_RECORD_FILE
125
+ end
126
+ end
127
+
128
+ # Creates the installation path for an entry in the +outsider_files+ file
129
+ #
130
+ # _data_ is the entry and can be a string or an array with two elements. If
131
+ # doing a global install, then the destination file is found simply by passing the
132
+ # string or the first entry of the array through ERB.
133
+ #
134
+ # In case of a user install, things are a bit more complex and change depending
135
+ # on whether _data_ is an array or a string
136
+ # * if it is an array, then the second entry will be used as path, after passing
137
+ # it through ERB. If it isn't an absolute file, it'll be considered relative
138
+ # to the user's home page.
139
+ # * if it is a string, what ERB returns will be considered a path relative to the user's
140
+ # home directory. There are a few exceptions, however. If the path starts
141
+ # with /usr/, /usr/local/, /var/, /opt/ or /etc/ then that directory will be
142
+ # replaced with the home directory. If it starts with /usr/share or /usr/local/share,
143
+ # that directory will be replaced with <tt>ENV["HOME"]/.local/share</tt>.
144
+ # If it starts with /bin/, /sbin/ or /usr/sbin/, that directory will be
145
+ # replaced by <tt>ENV["HOME"]/bin</tt>.
146
+ #
147
+ # Returns the destination path
148
+ def install_destination data
149
+ home = ENV['HOME']
150
+ if data.is_a? Array then path = data[@user_install ? 1 : 0]
151
+ else path = data
152
+ end
153
+ path = ERB.new(path).result
154
+ if @user_install and data.is_a? Array
155
+ File.expand_path path, home
156
+ elsif @user_install
157
+ replacements = [
158
+ [%r{^/bin/}, File.join(home, 'bin')],
159
+ [%r{^/sbin/}, File.join(home, 'bin')],
160
+ [%r{^/usr/sbin/}, File.join(home, 'bin')],
161
+ [%r{^/usr/local/share/}, File.join(home, '.local', 'share')],
162
+ [%r{^/usr/share/}, File.join(home, '.local', 'share')],
163
+ [%r{^/usr/local/}, home],
164
+ [%r{^/usr/}, home],
165
+ [%r[^/var/], home],
166
+ [%r[^/opt/], home],
167
+ [%r[^/etc/], home],
168
+ [%r{^~/}, home],
169
+ [%r{^(?=.)}, home]
170
+ ]
171
+ match = replacements.find{|reg, _| path.match reg}
172
+ File.join match[1], path.sub(match[0], '')
173
+ else path
174
+ end
175
+ end
176
+
177
+ # Copies the file _orig_ to _dest_
178
+ #
179
+ # If the path to _dest_ doesn't exist, then the missing directories are created.
180
+ # If the file _orig_ doesn't exist, nothing is done.
181
+ #
182
+ # Returns _dest_ if the file was installed successfully and *nil* if _orig_ didn't
183
+ # exist. Raises a subclass of SystemCallError if the file couldn't be copied, for
184
+ # example due to wrong permissions
185
+ def install_file orig, dest
186
+ if File.exist? orig
187
+ begin FileUtils.cp orig, dest
188
+ rescue SystemCallError
189
+ path = Pathname.new dest
190
+ path.descend do |pth|
191
+ if pth.exist? then next
192
+ elsif pth == path
193
+ FileUtils.cp orig, dest
194
+ else FileUtils.mkdir pth.to_s
195
+ end
196
+ end
197
+ end
198
+ dest
199
+ else nil
200
+ end
201
+ end
202
+
203
+ # Adds the given files to the record file
204
+ #
205
+ # The path to the record file is given by the OUTSIDER_RECORD_FILE
206
+ # environment variable. If the variable is unset, +/var/lib/outsider/installed_files+
207
+ # is used.
208
+ #
209
+ # If the record file doesn't exist, it's created, together with any missing directory.
210
+ #
211
+ # If a file with the same name as the record file exists but it's not a valid record
212
+ # file, it will be renamed by appending a <tt>-n</tt> suffix, where _n_ is a number,
213
+ # and a new record file will be created. A warning will be issued in this case
214
+ #
215
+ # Returns *nil*
216
+ def record_installed_files files
217
+ file = record_file
218
+ begin
219
+ data = read_record_file file
220
+ data ||= {}
221
+ rescue InvalidRecordFile
222
+ data = rename_invalid_record_file file
223
+ retry
224
+ end
225
+ files.each do |f|
226
+ (data[f[1]] ||= []) << {:gem => @gem_name, :origin => File.join(@gem_dir, f[0])}
227
+ end
228
+ write_record_file file, data
229
+ nil
230
+ end
231
+
232
+ # Write data to a given record file
233
+ #
234
+ # _file_ is the name of the record file and will be created, if it doesn't exist,
235
+ # together with the directory composing its path. _data_ is the hash to write.
236
+ #
237
+ # Returns *nil*
238
+ def write_record_file file, data
239
+ record_dir = File.dirname(file)
240
+ FileUtils.mkdir_p record_dir unless File.directory? record_dir
241
+ File.open(file, 'w'){|f| YAML.dump(data, f)}
242
+ nil
243
+ end
244
+
245
+ # Reads the files installed from the given record file
246
+ #
247
+ # If the file isn't a valid record file, InvalidRecordFile is raised.
248
+ #
249
+ # _file_ is the name of the record file
250
+ #
251
+ # Returns the hash contained in the record file or *nil* if the file doesn't exist
252
+ def read_record_file file
253
+ data = begin YAML.load File.read(file)
254
+ rescue ArgumentError then raise InvalidRecordFile.new file, "Invalid YAML file"
255
+ rescue SystemCallError
256
+ return nil if !File.exist? file
257
+ raise
258
+ end
259
+ unless valid_record_contents? data
260
+ raise InvalidRecordFile.new file, "Invalid record file format"
261
+ end
262
+ data
263
+ end
264
+
265
+ # Checks whether the given object is valid content for a record file
266
+ #
267
+ # Returns *true* if _data_ is a legitimate content for a record file and *false*
268
+ # otherwise
269
+ #
270
+ # A valid record file has the following format (in YAML):
271
+ #
272
+ # /path/to/installed_file_1:
273
+ # - {:gem: name_and_version_of_gem1, :origin: path/to/original/file1}
274
+ # - {:gem: name_and_version_of_gem2, :origin: path/to/original/file2}
275
+ # /path/to/installed_file_2
276
+ # - {:gem: name_and_version_of_gem3, :origin: path/to/original/file3}
277
+ # - {:gem: name_and_version_of_gem4, :origin: path/to/original/file4}
278
+ def valid_record_contents? data
279
+ return false unless data.is_a? Hash
280
+ data.each_pair do |dest, v|
281
+ return false unless dest.is_a? String and v.is_a? Array
282
+ v.each do |gem|
283
+ return false unless gem.is_a? Hash
284
+ return false unless gem[:gem].is_a? String and gem[:origin].is_a? String
285
+ end
286
+ end
287
+ true
288
+ end
289
+
290
+ # Renames the invalid record file _file_ giving it a new unique name and issues a
291
+ # warning
292
+ #
293
+ # Returns *nil*
294
+ def rename_invalid_record_file file
295
+ n = 1
296
+ n+=1 while File.exist?( file + "-#{n}")
297
+ new_file = file + "-#{n}"
298
+ warn "The file #{file} isn't a valid record file and will be moved to #{new_file}"
299
+ FileUtils.mv file, new_file
300
+ nil
301
+ end
302
+
303
+ end
304
+
305
+ end
@@ -0,0 +1,11 @@
1
+ require File.join(File.dirname(__FILE__), 'outsider','outsider')
2
+
3
+ Gem.post_install do |inst|
4
+ global = Outsider::Installer.new inst.spec.full_gem_path
5
+ global.install_files
6
+ end
7
+
8
+ Gem.pre_uninstall do |uninst|
9
+ global = Outsider::Installer.new uninst.spec.full_gem_path
10
+ global.uninstall_files
11
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: outsider
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Stefano Crocco
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-04 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email: stefano.crocco@alice.it
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/rubygems_plugin.rb
31
+ - lib/outsider/outsider.rb
32
+ - README.rdoc
33
+ has_rdoc: true
34
+ homepage: http://github.com/stcrocco/outsider
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.3.7
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: rubygems plugin to allow a gem to install files outside its own directory
65
+ test_files: []
66
+