outsider 0.0.1

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.
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
+