build-files 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b40717a33b523317133659681b1ce50c021ac00
4
+ data.tar.gz: 61a65ca8c6d12d04a8a72bde0aac31557249e687
5
+ SHA512:
6
+ metadata.gz: e692d35c69a5f5896d8902ad3c4960cf8c3aefa4d9f4c06329c36ada616dd4f4d2a2f3f77cbcdec47ce67a0fc0312746451739d7426b5dc50006233a62af0714
7
+ data.tar.gz: ad4623c1d5a3ca483d0f06630b96e715c4da232c9d376918d7a4b1b52cf54ddffd8184eb73c995fa334b84e317e1640e4a842137386873427df1e8be6b0f87b2
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.0"
4
+ - "2.1"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in build-files.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Build::Files
2
+
3
+ Build::Files is a set of idiomatic classes for dealing with paths and monitoring directories. File paths are represented with both root and relative parts which makes copying directory structures intuitive.
4
+
5
+ [![Build Status](https://secure.travis-ci.org/ioquatix/build-files.png)](http://travis-ci.org/ioquatix/build-files)
6
+ [![Code Climate](https://codeclimate.com/github/ioquatix/build-files.png)](https://codeclimate.com/github/ioquatix/build-files)
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'build-files'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install build-files
21
+
22
+ ## Usage
23
+
24
+ The basic structure is the `Path`. Paths are stored with a root and relative part. By default, if no root is specified, it is the `dirname` part.
25
+
26
+ require 'build/files'
27
+
28
+ path = Build::Files::Path("/foo/bar/baz")
29
+ => "/foo/bar"/"baz"
30
+
31
+ > path.root
32
+ => "/foo/bar"
33
+ > path.relative_path
34
+ => "baz"
35
+
36
+ Paths can be coerced to strings and thus are suitable arguments to `exec`/`system` functions.
37
+
38
+ ## Contributing
39
+
40
+ 1. Fork it
41
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
42
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
43
+ 4. Push to the branch (`git push origin my-new-feature`)
44
+ 5. Create new Pull Request
45
+
46
+ ## License
47
+
48
+ Released under the MIT license.
49
+
50
+ Copyright, 2014, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
51
+
52
+ Permission is hereby granted, free of charge, to any person obtaining a copy
53
+ of this software and associated documentation files (the "Software"), to deal
54
+ in the Software without restriction, including without limitation the rights
55
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
56
+ copies of the Software, and to permit persons to whom the Software is
57
+ furnished to do so, subject to the following conditions:
58
+
59
+ The above copyright notice and this permission notice shall be included in
60
+ all copies or substantial portions of the Software.
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
63
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
64
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
65
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
66
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
67
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
68
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ end
7
+
8
+ desc "Run tests"
9
+ task :default => :test
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'build/files/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "build-files"
8
+ spec.version = Build::Files::VERSION
9
+ spec.authors = ["Samuel Williams"]
10
+ spec.email = ["samuel.williams@oriontransfer.co.nz"]
11
+ # spec.description = %q{}
12
+ spec.summary = %q{Build::Files is a set of idiomatic classes for dealing with paths and monitoring directories.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "minitest", "~> 5.3.2"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,68 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'paths'
22
+
23
+ module Build
24
+ module Files
25
+ class Directory < List
26
+ def initialize(root)
27
+ @root = root
28
+ @path = path
29
+ end
30
+
31
+ attr :root
32
+ attr :path
33
+
34
+ def roots
35
+ [@root]
36
+ end
37
+
38
+ def full_path
39
+ Path.join(@root, @path)
40
+ end
41
+
42
+ def each
43
+ return to_enum(:each) unless block_given?
44
+
45
+ Dir.glob(full_path + "**/*") do |path|
46
+ yield Path.new(path, @root)
47
+ end
48
+ end
49
+
50
+ def eql?(other)
51
+ other.kind_of?(self.class) and @root.eql?(other.root) and @path.eql?(other.path)
52
+ end
53
+
54
+ def hash
55
+ [@root, @path].hash
56
+ end
57
+
58
+ def include?(path)
59
+ # Would be true if path is a descendant of full_path.
60
+ path.start_with?(full_path)
61
+ end
62
+
63
+ def rebase(root)
64
+ self.class.new(root, @path)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,73 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'paths'
22
+
23
+ module Build
24
+ module Files
25
+ class Glob < List
26
+ def initialize(root, pattern)
27
+ @root = root
28
+ @pattern = pattern
29
+ end
30
+
31
+ attr :root
32
+ attr :pattern
33
+
34
+ def roots
35
+ [@root]
36
+ end
37
+
38
+ def full_pattern
39
+ Path.join(@root, @pattern)
40
+ end
41
+
42
+ # Enumerate all paths matching the pattern.
43
+ def each(&block)
44
+ return to_enum(:each) unless block_given?
45
+
46
+ Dir.glob(full_pattern) do |path|
47
+ yield Path.new(path, @root)
48
+ end
49
+ end
50
+
51
+ def eql?(other)
52
+ other.kind_of?(self.class) and @root.eql?(other.root) and @pattern.eql?(other.pattern)
53
+ end
54
+
55
+ def hash
56
+ [@root, @pattern].hash
57
+ end
58
+
59
+ def include?(path)
60
+ File.fnmatch(full_pattern, path)
61
+ end
62
+
63
+ def rebase(root)
64
+ self.class.new(root, @pattern)
65
+ end
66
+
67
+ def inspect
68
+ "<Glob #{full_pattern.inspect}>"
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'set'
22
+
23
+ require 'build/files/state'
24
+
25
+ module Build
26
+ module Files
27
+ class Monitor
28
+ def initialize
29
+ @directories = Hash.new { |hash, key| hash[key] = Set.new }
30
+
31
+ @updated = false
32
+ end
33
+
34
+ attr :updated
35
+
36
+ # Notify the monitor that files in these directories have changed.
37
+ def update(directories, *args)
38
+ directories.each do |directory|
39
+ # directory = File.realpath(directory)
40
+
41
+ @directories[directory].each do |handle|
42
+ handle.changed!(*args)
43
+ end
44
+ end
45
+ end
46
+
47
+ def roots
48
+ @directories.keys
49
+ end
50
+
51
+ def delete(handle)
52
+ handle.directories.each do |directory|
53
+ @directories[directory].delete(handle)
54
+
55
+ # Remove the entire record if there are no handles:
56
+ if @directories[directory].size == 0
57
+ @directories.delete(directory)
58
+
59
+ @updated = true
60
+ end
61
+ end
62
+ end
63
+
64
+ def track_changes(files, &block)
65
+ handle = Handle.new(self, files, &block)
66
+
67
+ add(handle)
68
+ end
69
+
70
+ def add(handle)
71
+ handle.directories.each do |directory|
72
+ @directories[directory] << handle
73
+
74
+ # We just added the first handle:
75
+ if @directories[directory].size == 1
76
+ # If the handle already existed, this might trigger unnecessarily.
77
+ @updated = true
78
+ end
79
+ end
80
+
81
+ handle
82
+ end
83
+ end
84
+
85
+ def self.run_with_fsevent(monitor, options = {}, &block)
86
+ require 'rb-fsevent'
87
+
88
+ fsevent ||= FSEvent.new
89
+
90
+ catch(:interrupt) do
91
+ while true
92
+ fsevent.watch monitor.roots do |directories|
93
+ monitor.update(directories)
94
+
95
+ yield
96
+
97
+ if monitor.updated
98
+ fsevent.stop
99
+ end
100
+ end
101
+
102
+ fsevent.run
103
+ end
104
+ end
105
+ end
106
+
107
+ def self.run_with_polling(monitor, options = {}, &block)
108
+ catch(:interrupt) do
109
+ while true
110
+ monitor.update(monitor.roots)
111
+
112
+ yield
113
+
114
+ sleep(options[:latency] || 5.0)
115
+ end
116
+ end
117
+ end
118
+
119
+ def self.run(monitor, options = {}, &block)
120
+ run_with_polling(monitor, options, &block)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,314 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'set'
22
+
23
+ module Build
24
+ module Files
25
+ # Represents a file path with an absolute root and a relative offset:
26
+ class Path
27
+ def self.relative_path(root, full_path)
28
+ relative_offset = root.length
29
+
30
+ # Deal with the case where the root may or may not end with the path separator:
31
+ relative_offset += 1 unless root.end_with?(File::SEPARATOR)
32
+
33
+ return full_path.slice(relative_offset..-1)
34
+ end
35
+
36
+ # Both paths must be full absolute paths, and path must have root as an prefix.
37
+ def initialize(full_path, root = nil)
38
+ # This is the object identity:
39
+ @full_path = full_path
40
+
41
+ if root
42
+ @root = root
43
+ @relative_path = nil
44
+ else
45
+ # Effectively dirname and basename:
46
+ @root, @relative_path = File.split(full_path)
47
+ end
48
+ end
49
+
50
+ attr :root
51
+
52
+ def to_str
53
+ @full_path
54
+ end
55
+
56
+ def to_path
57
+ @full_path
58
+ end
59
+
60
+ def length
61
+ @full_path.length
62
+ end
63
+
64
+ def parts
65
+ @parts ||= @full_path.split(File::SEPARATOR)
66
+ end
67
+
68
+ def relative_path
69
+ @relative_path ||= Path.relative_path(@root, @full_path)
70
+ end
71
+
72
+ def relative_parts
73
+ basename, _, filename = self.relative_path.rpartition(File::SEPARATOR)
74
+
75
+ return basename, filename
76
+ end
77
+
78
+ def +(extension)
79
+ self.class.new(@full_path + extension, @root)
80
+ end
81
+
82
+ def rebase(root)
83
+ self.class.new(File.join(root, relative_path), root)
84
+ end
85
+
86
+ def with(root: @root, extension: nil)
87
+ self.class.new(File.join(root, extension ? relative_path + extension : relative_path), root)
88
+ end
89
+
90
+ def self.join(root, relative_path)
91
+ self.new(File.join(root, relative_path), root)
92
+ end
93
+
94
+ def shortest_path(working_directory = Dir.pwd)
95
+ if start_with? working_directory
96
+ Path.new(working_directory, @full_path)
97
+ else
98
+ self
99
+ end
100
+ end
101
+
102
+ def to_s
103
+ @full_path
104
+ end
105
+
106
+ def inspect
107
+ "#{@root.inspect}/#{relative_path.inspect}"
108
+ end
109
+
110
+ def hash
111
+ @full_path.hash
112
+ end
113
+
114
+ def eql?(other)
115
+ @full_path.eql?(other.to_s)
116
+ end
117
+
118
+ def ==(other)
119
+ self.to_s == other.to_s
120
+ end
121
+
122
+ def for_reading
123
+ [@full_path, File::RDONLY]
124
+ end
125
+
126
+ def for_writing
127
+ [@full_path, File::CREAT|File::TRUNC|File::WRONLY]
128
+ end
129
+
130
+ def for_appending
131
+ [@full_path, File::CREAT|File::APPEND|File::WRONLY]
132
+ end
133
+ end
134
+
135
+ def self.Path(*args)
136
+ if Path === args[0]
137
+ args[0]
138
+ else
139
+ Path.new(*args)
140
+ end
141
+ end
142
+
143
+ # A list of paths, where #each yields instances of Path.
144
+ class List
145
+ include Enumerable
146
+
147
+ def roots
148
+ collect{|path| path.root}.sort.uniq
149
+ end
150
+
151
+ # Create a composite list out of two other lists:
152
+ def +(list)
153
+ Composite.new([self, list])
154
+ end
155
+
156
+ # Does this list of files include the path of any other?
157
+ def intersects? other
158
+ other.any?{|path| include?(path)}
159
+ end
160
+
161
+ def with(**args)
162
+ return to_enum(:with, **args) unless block_given?
163
+
164
+ paths = []
165
+
166
+ each do |path|
167
+ updated_path = path.with(args)
168
+
169
+ yield path, updated_path
170
+
171
+ paths << updated_path
172
+ end
173
+
174
+ return Paths.new(paths)
175
+ end
176
+
177
+ def rebase(root)
178
+ Paths.new(self.collect{|path| path.rebase(root)}, [root])
179
+ end
180
+
181
+ def to_paths
182
+ Paths.new(each.to_a)
183
+ end
184
+
185
+ def map
186
+ Paths.new(super)
187
+ end
188
+
189
+ def self.coerce(arg)
190
+ if arg.kind_of? self
191
+ arg
192
+ else
193
+ Paths.new(arg)
194
+ end
195
+ end
196
+ end
197
+
198
+ class Paths < List
199
+ def initialize(list, roots = nil)
200
+ @list = Array(list).freeze
201
+ @roots = roots
202
+ end
203
+
204
+ attr :list
205
+
206
+ # The list of roots for a given list of immutable files is also immutable, so we cache it for performance:
207
+ def roots
208
+ @roots ||= super
209
+ end
210
+
211
+ def count
212
+ @list.count
213
+ end
214
+
215
+ def each
216
+ return to_enum(:each) unless block_given?
217
+
218
+ @list.each{|path| yield path}
219
+ end
220
+
221
+ def eql?(other)
222
+ other.kind_of?(self.class) and @list.eql?(other.list)
223
+ end
224
+
225
+ def hash
226
+ @list.hash
227
+ end
228
+
229
+ def to_paths
230
+ self
231
+ end
232
+
233
+ def inspect
234
+ "<Paths #{@list.inspect}>"
235
+ end
236
+
237
+ def self.directory(root, relative_paths)
238
+ paths = relative_paths.collect do |path|
239
+ Path.join(root, path)
240
+ end
241
+
242
+ self.new(paths, [root])
243
+ end
244
+ end
245
+
246
+ class Composite < List
247
+ def initialize(files, roots = nil)
248
+ @files = []
249
+
250
+ files.each do |list|
251
+ if list.kind_of? Composite
252
+ @files += list.files
253
+ elsif List.kind_of? List
254
+ @files << list
255
+ else
256
+ # Try to convert into a explicit paths list:
257
+ @files << Paths.new(list)
258
+ end
259
+ end
260
+
261
+ @files.freeze
262
+ @roots = roots
263
+ end
264
+
265
+ attr :files
266
+
267
+ def each
268
+ return to_enum(:each) unless block_given?
269
+
270
+ @files.each do |files|
271
+ files.each{|path| yield path}
272
+ end
273
+ end
274
+
275
+ def roots
276
+ @roots ||= @files.collect(&:roots).flatten.uniq
277
+ end
278
+
279
+ def eql?(other)
280
+ other.kind_of?(self.class) and @files.eql?(other.files)
281
+ end
282
+
283
+ def hash
284
+ @files.hash
285
+ end
286
+
287
+ def +(list)
288
+ if list.kind_of? Composite
289
+ self.class.new(@files + list.files)
290
+ else
291
+ self.class.new(@files + [list])
292
+ end
293
+ end
294
+
295
+ def include?(path)
296
+ @files.any? {|list| list.include?(path)}
297
+ end
298
+
299
+ def rebase(root)
300
+ self.class.new(@files.collect{|list| list.rebase(root)}, [root])
301
+ end
302
+
303
+ def to_paths
304
+ self.class.new(@files.collect(&:to_paths), roots: @roots)
305
+ end
306
+
307
+ def inspect
308
+ "<Composite #{@files.inspect}>"
309
+ end
310
+ end
311
+
312
+ NONE = Composite.new([]).freeze
313
+ end
314
+ end
@@ -0,0 +1,224 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Build
22
+ module Files
23
+ # Represents a specific file on disk with a specific mtime.
24
+ class FileTime
25
+ include Comparable
26
+
27
+ def initialize(path, time)
28
+ @path = path
29
+ @time = time
30
+ end
31
+
32
+ attr :path
33
+ attr :time
34
+
35
+ def <=> other
36
+ @time <=> other.time
37
+ end
38
+ end
39
+
40
+ class State
41
+ def initialize(files)
42
+ raise ArgumentError.new("Invalid files list: #{files}") unless Files::List === files
43
+
44
+ @files = files
45
+
46
+ @times = {}
47
+
48
+ update!
49
+ end
50
+
51
+ attr :files
52
+
53
+ attr :added
54
+ attr :removed
55
+ attr :changed
56
+ attr :missing
57
+
58
+ attr :times
59
+
60
+ def update!
61
+ last_times = @times
62
+ @times = {}
63
+
64
+ @added = []
65
+ @removed = []
66
+ @changed = []
67
+ @missing = []
68
+
69
+ file_times = []
70
+
71
+ @files.each do |path|
72
+ # When processing the same path twice (perhaps by accident), we should skip it otherwise it might cause issues when being deleted from last_times multuple times.
73
+ next if @times.include? path
74
+
75
+ if File.exist?(path)
76
+ modified_time = File.mtime(path)
77
+
78
+ if last_time = last_times.delete(path)
79
+ # Path was valid last update:
80
+ if modified_time != last_time
81
+ @changed << path
82
+ # puts "Changed: #{path}"
83
+ end
84
+ else
85
+ # Path didn't exist before:
86
+ @added << path
87
+ # puts "Added: #{path}"
88
+ end
89
+
90
+ @times[path] = modified_time
91
+
92
+ unless File.directory?(path)
93
+ file_times << FileTime.new(path, modified_time)
94
+ end
95
+ else
96
+ @missing << path
97
+ # puts "Missing: #{path}"
98
+ end
99
+ end
100
+
101
+ @removed = last_times.keys
102
+
103
+ @oldest_time = file_times.min
104
+ @newest_time = file_times.max
105
+
106
+ return @added.size > 0 || @changed.size > 0 || @removed.size > 0 || @missing.size > 0
107
+ end
108
+
109
+ attr :oldest_time
110
+ attr :newest_time
111
+
112
+ attr :missing
113
+
114
+ def missing?
115
+ !@missing.empty?
116
+ end
117
+
118
+ # Outputs is a list of full paths and must not include any patterns/globs.
119
+ def intersects?(outputs)
120
+ @files.intersects?(outputs)
121
+ end
122
+
123
+ def empty?
124
+ @files.to_a.empty?
125
+ end
126
+ end
127
+
128
+ class IOState
129
+ def initialize(inputs, outputs)
130
+ @input_state = State.new(inputs)
131
+ @output_state = State.new(outputs)
132
+ end
133
+
134
+ attr :input_state
135
+ attr :output_state
136
+
137
+ # Output is dirty if files are missing or if latest input is older than any output.
138
+ def dirty?
139
+ if @output_state.missing?
140
+ # puts "Output file missing: #{output_state.missing.inspect}"
141
+
142
+ return true
143
+ end
144
+
145
+ # If there are no inputs, we are always clean as long as outputs exist:
146
+ # if @input_state.empty?
147
+ # return false
148
+ # end
149
+
150
+ oldest_output_time = @output_state.oldest_time
151
+ newest_input_time = @input_state.newest_time
152
+
153
+ if newest_input_time and oldest_output_time
154
+ # if newest_input_time > oldest_output_time
155
+ # puts "Out of date file: #{newest_input_time.inspect} > #{oldest_output_time.inspect}"
156
+ # end
157
+
158
+ return newest_input_time > oldest_output_time
159
+ end
160
+
161
+ # puts "Missing file dates: #{newest_input_time.inspect} < #{oldest_output_time.inspect}"
162
+
163
+ return true
164
+ end
165
+
166
+ def fresh?
167
+ not dirty?
168
+ end
169
+
170
+ def files
171
+ @input_state.files + @output_state.files
172
+ end
173
+
174
+ def added
175
+ @input_state.added + @output_state.added
176
+ end
177
+
178
+ def removed
179
+ @input_state.removed + @output_state.removed
180
+ end
181
+
182
+ def changed
183
+ @input_state.changed + @output_state.changed
184
+ end
185
+
186
+ def update!
187
+ input_changed = @input_state.update!
188
+ output_changed = @output_state.update!
189
+
190
+ input_changed or output_changed
191
+ end
192
+
193
+ def intersects?(outputs)
194
+ @input_state.intersects?(outputs) or @output_state.intersects?(outputs)
195
+ end
196
+ end
197
+
198
+ class Handle
199
+ def initialize(monitor, files, &block)
200
+ @monitor = monitor
201
+ @state = State.new(files)
202
+ @on_changed = block
203
+ end
204
+
205
+ attr :monitor
206
+
207
+ def commit!
208
+ @state.update!
209
+ end
210
+
211
+ def directories
212
+ @state.files.roots
213
+ end
214
+
215
+ def remove!
216
+ monitor.delete(self)
217
+ end
218
+
219
+ def changed!
220
+ @on_changed.call(@state) if @state.update!
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Build
22
+ module Files
23
+ VERSION = "0.1.0"
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'files/paths'
22
+ require_relative 'files/glob'
23
+ require_relative 'files/directory'
24
+
25
+ require_relative 'files/state'
26
+ require_relative 'files/monitor'
@@ -0,0 +1,75 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'minitest/autorun'
22
+
23
+ require 'build/files'
24
+
25
+ class TestFiles < MiniTest::Test
26
+ include Build::Files
27
+
28
+ def test_inclusion
29
+ # Glob all test files:
30
+ glob = Glob.new(__dir__, "*.rb")
31
+
32
+ assert glob.count > 0
33
+
34
+ # Should include this file:
35
+ assert_includes glob, __FILE__
36
+
37
+ # Glob should intersect self:
38
+ assert glob.intersects?(glob)
39
+ end
40
+
41
+ def test_composites
42
+ lib = File.join(__dir__, "../lib")
43
+
44
+ test_glob = Glob.new(__dir__, "*.rb")
45
+ lib_glob = Glob.new(lib, "*.rb")
46
+
47
+ both = test_glob + lib_glob
48
+
49
+ # List#roots is the generic accessor for Lists
50
+ assert both.roots.include? test_glob.root
51
+
52
+ # The composite should include both:
53
+ assert both.include?(__FILE__)
54
+ end
55
+
56
+ def test_roots
57
+ test_glob = Glob.new(__dir__, "*.rb")
58
+
59
+ assert_kind_of Path, test_glob.first
60
+
61
+ assert_equal __dir__, test_glob.first.root
62
+ end
63
+
64
+ def test_renaming
65
+ glob = Glob.new(__dir__, "*.rb")
66
+
67
+ paths = glob.map {|path| path + ".txt"}
68
+
69
+ assert_equal(paths.first, glob.first + ".txt")
70
+ end
71
+
72
+ def test_none
73
+ assert_equal 0, NONE.count
74
+ end
75
+ end
@@ -0,0 +1,111 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'minitest/autorun'
22
+
23
+ require 'build/files/paths'
24
+ require 'build/files/glob'
25
+
26
+ class TestPaths < MiniTest::Test
27
+ include Build::Files
28
+
29
+ def setup
30
+ @path = Path.new("/foo/bar/baz", "/foo")
31
+ end
32
+
33
+ def test_path_conversions
34
+ # The to_str method should return the full path (i.e. the same as to_s):
35
+ assert_equal @path.to_s, @path.to_str
36
+
37
+ # Checkt the equality operator:
38
+ assert_equal @path, @path.dup
39
+
40
+ # The length should be reported correctly:
41
+ assert_equal @path.length, @path.to_s.length
42
+ end
43
+
44
+ def test_path_parts
45
+ assert_equal ["", "foo", "bar", "baz"], @path.parts
46
+
47
+ assert_equal "/foo", @path.root
48
+
49
+ assert_equal "bar/baz", @path.relative_path
50
+
51
+ assert_equal ["bar", "baz"], @path.relative_parts
52
+ end
53
+
54
+ def test_path_with
55
+ path = @path.with(root: '/tmp', extension: '.txt')
56
+
57
+ assert_equal '/tmp', path.root
58
+
59
+ assert_equal 'bar/baz.txt', path.relative_path
60
+ end
61
+
62
+ def test_path_class
63
+ assert_instance_of Path, @path
64
+ assert_instance_of String, @path.root
65
+ assert_instance_of String, @path.relative_path
66
+ end
67
+
68
+ def test_path_manipulation
69
+ object_path = @path + ".o"
70
+
71
+ assert_equal "/foo", object_path.root
72
+ assert_equal "bar/baz.o", object_path.relative_path
73
+ end
74
+
75
+ def test_paths
76
+ paths = Paths.new([
77
+ Path.join('/foo/bar', 'alice'),
78
+ Path.join('/foo/bar', 'bob'),
79
+ Path.join('/foo/bar', 'charles'),
80
+ @path
81
+ ])
82
+
83
+ assert_includes paths, @path
84
+
85
+ assert paths.intersects?(paths)
86
+ refute paths.intersects?(NONE)
87
+
88
+ mapped_paths = paths.map {|path| path + ".o"}
89
+
90
+ assert_instance_of Paths, mapped_paths
91
+ assert_equal paths.roots, mapped_paths.roots
92
+ end
93
+
94
+ def test_glob
95
+ glob = Glob.new(__dir__, '*.rb')
96
+
97
+ assert glob.count > 0, "Found some files"
98
+
99
+ mapped_paths = glob.map {|path| path + ".txt"}
100
+
101
+ assert_equal glob.roots, mapped_paths.roots
102
+ end
103
+
104
+ def test_hashing
105
+ cache = {}
106
+
107
+ cache[Paths.new(@path)] = true
108
+
109
+ assert cache[Paths.new(@path)]
110
+ end
111
+ end
@@ -0,0 +1,61 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'minitest/autorun'
22
+
23
+ require 'build/files'
24
+
25
+ class TestState < MiniTest::Test
26
+ include Build::Files
27
+
28
+ def setup
29
+ @files = Glob.new(__dir__, "*.rb")
30
+ end
31
+
32
+ def test_basic_update
33
+ state = State.new(@files)
34
+
35
+ refute state.update!, "Files not changed"
36
+
37
+ assert_equal [], state.changed
38
+ assert_equal [], state.added
39
+ assert_equal [], state.removed
40
+ assert_equal [], state.missing
41
+ end
42
+
43
+ def test_missing
44
+ files = @files.to_paths.rebase(File.join(__dir__, 'program'))
45
+ state = State.new(files)
46
+
47
+ assert state.update!, "Files missing"
48
+ refute_empty state.missing
49
+ end
50
+
51
+ def test_duplicates
52
+ state = State.new(@files + @files)
53
+
54
+ refute state.update!, "Files not changed"
55
+
56
+ assert_equal [], state.changed
57
+ assert_equal [], state.added
58
+ assert_equal [], state.removed
59
+ assert_equal [], state.missing
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: build-files
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.3.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 5.3.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - samuel.williams@oriontransfer.co.nz
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".travis.yml"
63
+ - Gemfile
64
+ - README.md
65
+ - Rakefile
66
+ - build-files.gemspec
67
+ - lib/build/files.rb
68
+ - lib/build/files/directory.rb
69
+ - lib/build/files/glob.rb
70
+ - lib/build/files/monitor.rb
71
+ - lib/build/files/paths.rb
72
+ - lib/build/files/state.rb
73
+ - lib/build/files/version.rb
74
+ - test/test_files.rb
75
+ - test/test_paths.rb
76
+ - test/test_state.rb
77
+ homepage: ''
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.2.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Build::Files is a set of idiomatic classes for dealing with paths and monitoring
101
+ directories.
102
+ test_files:
103
+ - test/test_files.rb
104
+ - test/test_paths.rb
105
+ - test/test_state.rb