ocran 1.3.15 → 1.3.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ module BuildConstants
5
+ # Alias for the temporary directory where files are extracted.
6
+ EXTRACT_ROOT = Pathname.new("|")
7
+ # Directory for source files in temporary directory.
8
+ SRCDIR = Pathname.new("src")
9
+ # Directory for Ruby binaries in temporary directory.
10
+ BINDIR = Pathname.new("bin")
11
+ # Directory for gem files in temporary directory.
12
+ GEMDIR = Pathname.new("gems")
13
+ # Directory for Ruby library in temporary directory.
14
+ LIBDIR = Pathname.new("lib")
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ class BuildFacade
5
+ def initialize(filer, launcher)
6
+ @filer, @launcher = filer, launcher
7
+ end
8
+
9
+ def mkdir(...) = @filer.__send__(__method__, ...)
10
+
11
+ def cp(...) = @filer.__send__(__method__, ...)
12
+
13
+ def export(...) = @launcher.__send__(__method__, ...)
14
+
15
+ def exec(...) = @launcher.__send__(__method__, ...)
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+ require_relative "refine_pathname"
4
+ require_relative "command_output"
5
+ require_relative "build_constants"
6
+
7
+ module Ocran
8
+ module BuildHelper
9
+ using RefinePathname
10
+
11
+ include BuildConstants, CommandOutput
12
+
13
+ EMPTY_SOURCE = File.expand_path("empty_source", __dir__).freeze
14
+
15
+ def mkdir(target)
16
+ verbose "mkdir #{target}"
17
+ super
18
+ end
19
+
20
+ def cp(source, target)
21
+ verbose "cp #{source} #{target}"
22
+ super
23
+ end
24
+
25
+ def exec(image, script, *argv)
26
+ args = argv.map { |s| replace_placeholder(s) }.join(" ")
27
+ verbose "exec #{image} #{script} #{args}"
28
+ super
29
+ end
30
+
31
+ def export(name, value)
32
+ verbose "export #{name}=#{replace_placeholder(value)}"
33
+ super
34
+ end
35
+
36
+ def replace_placeholder(s)
37
+ s.to_s.gsub(EXTRACT_ROOT.to_s, "<tempdir>")
38
+ end
39
+ private :replace_placeholder
40
+
41
+ def copy_to_bin(source, target)
42
+ cp(source, BINDIR / target)
43
+ end
44
+
45
+ def copy_to_gem(source, target)
46
+ cp(source, GEMDIR / target)
47
+ end
48
+
49
+ def copy_to_lib(source, target)
50
+ cp(source, LIBDIR / target)
51
+ end
52
+
53
+ def duplicate_to_exec_prefix(source)
54
+ cp(source, Pathname(source).relative_path_from(HostConfigHelper.exec_prefix))
55
+ end
56
+
57
+ def duplicate_to_gem_home(source, gem_path)
58
+ copy_to_gem(source, Pathname(source).relative_path_from(gem_path))
59
+ end
60
+
61
+ def resolve_source_path(source, root_prefix)
62
+ source = Pathname(source)
63
+
64
+ if source.subpath?(HostConfigHelper.exec_prefix)
65
+ source.relative_path_from(HostConfigHelper.exec_prefix)
66
+ elsif source.subpath?(root_prefix)
67
+ SRCDIR / source.relative_path_from(root_prefix)
68
+ else
69
+ SRCDIR / source.basename
70
+ end
71
+ end
72
+
73
+ # Sets an environment variable with a joined path value.
74
+ # This method processes an array of path strings or Pathname objects, accepts
75
+ # absolute paths as is, and appends a placeholder to relative paths to convert
76
+ # them into absolute paths. The converted paths are then joined into a single
77
+ # string using the system's path separator.
78
+ #
79
+ # @param name [String] the name of the environment variable to set.
80
+ # @param paths [Array<String, Pathname>] an array of path arguments which can
81
+ # be either absolute or relative.
82
+ #
83
+ # Example:
84
+ # set_env_path("RUBYLIB", "lib", "ext", "vendor/lib")
85
+ # # This sets RUBYLIB to a string such as "C:/ProjectRoot/lib;C:/ProjectRoot/ext;C:/ProjectRoot/vendor/lib"
86
+ # # assuming each path is correctly converted to an absolute path through a placeholder.
87
+ #
88
+ def set_env_path(name, *paths)
89
+ value = paths.map { |path|
90
+ if File.absolute_path?(path)
91
+ path
92
+ else
93
+ File.join(EXTRACT_ROOT, path)
94
+ end
95
+ }.join(File::PATH_SEPARATOR)
96
+
97
+ export(name, value)
98
+ end
99
+
100
+ def touch(target)
101
+ verbose "touch #{target}"
102
+ cp(EMPTY_SOURCE, target)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ load File.expand_path("../ocran.rb", __dir__)
3
+
4
+ module Ocran
5
+ module CommandOutput
6
+ def say(s)
7
+ puts "=== #{s}" unless Ocran.option&.quiet?
8
+ end
9
+
10
+ def verbose(s)
11
+ puts s if Ocran.option&.verbose?
12
+ end
13
+
14
+ def warning(s)
15
+ STDERR.puts "WARNING: #{s}" if Ocran.option&.warning?
16
+ end
17
+
18
+ def error(s)
19
+ STDERR.puts "ERROR: #{s}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,386 @@
1
+ # frozen_string_literal: true
2
+ require "rbconfig"
3
+ require "pathname"
4
+ require_relative "refine_pathname"
5
+ require_relative "host_config_helper"
6
+ require_relative "command_output"
7
+ require_relative "build_constants"
8
+
9
+ module Ocran
10
+ class Direction
11
+ using RefinePathname
12
+
13
+ # Match the load path against standard library, site_ruby, and vendor_ruby paths
14
+ # This regular expression matches:
15
+ # - /ruby/3.0.0/
16
+ # - /ruby/site_ruby/3.0.0/
17
+ # - /ruby/vendor_ruby/3.0.0/
18
+ RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+)/?$}i
19
+
20
+ include BuildConstants, CommandOutput, HostConfigHelper
21
+
22
+ attr_reader :ruby_executable, :rubyopt
23
+
24
+ def initialize(post_env, pre_env, option)
25
+ @post_env, @pre_env, @option = post_env, pre_env, option
26
+ @ruby_executable = @option.windowed? ? rubyw_exe : ruby_exe
27
+
28
+ # Initializes @rubyopt with the user-intended RUBYOPT environment variable.
29
+ # This ensures that RUBYOPT matches the user's initial settings before any
30
+ # modifications that may occur during script execution.
31
+ @rubyopt = @option.rubyopt || pre_env.env["RUBYOPT"] || ""
32
+
33
+ # FIXME: Remove the absolute path to bundler/setup from RUBYOPT
34
+ # This is a temporary measure to ensure compatibility with self-extracting executables
35
+ # built in a bundle exec environment, particularly for Ruby 3.2 and later where
36
+ # absolute paths are included in RUBYOPT.
37
+ # In the future, we plan to implement a more appropriate solution.
38
+ @rubyopt = @rubyopt.gsub(%r(-r#{Regexp.escape(RbConfig::TOPDIR)}(/.*/bundler/setup)), "")
39
+ end
40
+
41
+ # Resolves the common root directory prefix from an array of absolute paths.
42
+ # This method iterates over each file path, checking if they have a subpath
43
+ # that matches a given execution prefix.
44
+ def resolve_root_prefix(files)
45
+ files.inject(files.first.dirname) do |current_root, file|
46
+ next current_root if file.subpath?(exec_prefix)
47
+
48
+ current_root.ascend.find do |candidate_root|
49
+ path_from_root = file.relative_path_from(candidate_root)
50
+ rescue ArgumentError
51
+ raise "No common directory contains all specified files"
52
+ else
53
+ path_from_root.each_filename.first != ".."
54
+ end
55
+ end
56
+ end
57
+
58
+ # For RubyInstaller environments supporting Ruby 2.4 and above,
59
+ # this method checks for the existence of a required manifest file
60
+ def ruby_builtin_manifest
61
+ manifest_path = exec_prefix / "bin/ruby_builtin_dlls/ruby_builtin_dlls.manifest"
62
+ manifest_path.exist? ? manifest_path : nil
63
+ end
64
+
65
+ def detect_dlls
66
+ require_relative "library_detector"
67
+ LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
68
+ end
69
+
70
+ def find_gemspecs(features)
71
+ require_relative "gem_spec_queryable"
72
+
73
+ specs = []
74
+ # If a Bundler Gemfile was provided, add all gems it specifies
75
+ if @option.gemfile
76
+ say "Scanning Gemfile"
77
+ specs += GemSpecQueryable.scanning_gemfile(@option.gemfile).each do |spec|
78
+ verbose "From Gemfile, adding gem #{spec.full_name}"
79
+ end
80
+ end
81
+ if defined?(Gem)
82
+ specs += Gem.loaded_specs.values
83
+ # Now, we also detect gems that are not included in Gem.loaded_specs.
84
+ # Therefore, we look for any loaded file from a gem path.
85
+ specs += GemSpecQueryable.detect_gems_from(features, verbose: @option.verbose?)
86
+ end
87
+ # Prioritize the spec detected from Gemfile.
88
+ specs.uniq!(&:name)
89
+ specs
90
+ end
91
+
92
+ def normalized_features
93
+ features = @post_env.loaded_features.map { |feature| Pathname(feature) }
94
+
95
+ # Since https://github.com/rubygems/rubygems/commit/cad4cf16cf8fcc637d9da643ef97cf0be2ed63cb
96
+ # rubygems/core_ext/kernel_require.rb is evaled and thus missing in $LOADED_FEATURES, so we can't find it and need to add it manually
97
+ features.push(Pathname("rubygems/core_ext/kernel_require.rb"))
98
+
99
+ # Convert all relative paths to absolute paths before building.
100
+ # NOTE: In the future, different strategies may be needed before and after script execution.
101
+ features.filter_map do |feature|
102
+ if feature.absolute?
103
+ feature
104
+ elsif (load_path = @post_env.find_load_path(feature))
105
+ feature.expand_path(@post_env.expand_path(load_path))
106
+ else
107
+ # This message occurs when paths for core library files (e.g., enumerator.so,
108
+ # rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
109
+ # found. These are integral to Ruby's standard libraries or extensions and
110
+ # may not be located via normal load path searches, especially in RubyInstaller
111
+ # environments.
112
+ verbose "Load path not found for #{feature}, skip this feature"
113
+ nil
114
+ end
115
+ end
116
+ end
117
+
118
+ def construct(builder)
119
+ # Store the currently loaded files
120
+ features = normalized_features
121
+
122
+ say "Building #{@option.output_executable}"
123
+ require_relative "build_helper"
124
+ builder.extend(BuildHelper)
125
+
126
+ # Add the ruby executable and DLL
127
+ say "Adding ruby executable #{ruby_executable}"
128
+ builder.copy_to_bin(bindir / ruby_executable, ruby_executable)
129
+ if libruby_so
130
+ builder.copy_to_bin(bindir / libruby_so, libruby_so)
131
+ end
132
+
133
+ # Add detected DLLs
134
+ if @option.auto_detect_dlls?
135
+ detect_dlls.each do |dll|
136
+ next unless dll.subpath?(exec_prefix) && dll.extname?(".dll") && dll.basename != libruby_so
137
+
138
+ say "Adding detected DLL #{dll}"
139
+ if dll.subpath?(exec_prefix)
140
+ builder.duplicate_to_exec_prefix(dll)
141
+ else
142
+ builder.copy_to_bin(dll, dll.basename)
143
+ end
144
+ end
145
+ end
146
+
147
+ # Add external manifest files
148
+ if (manifest = ruby_builtin_manifest)
149
+ say "Adding external manifest #{manifest}"
150
+ builder.duplicate_to_exec_prefix(manifest)
151
+ end
152
+
153
+ # Add extra DLLs specified on the command line
154
+ @option.extra_dlls.each do |dll|
155
+ say "Adding supplied DLL #{dll}"
156
+ builder.copy_to_bin(bindir / dll, dll)
157
+ end
158
+
159
+ # Searches for features that are loaded from gems, then produces a
160
+ # list of files included in those gems' manifests. Also returns a
161
+ # list of original features that caused those gems to be included.
162
+ gem_files = find_gemspecs(features).flat_map do |spec|
163
+ spec_file = Pathname(spec.loaded_from)
164
+ # FIXME: From Ruby 3.2 onwards, launching Ruby with bundle exec causes
165
+ # Bundler's loaded_from to point to the root directory of the
166
+ # bundler gem, not returning the path to gemspec files. Here, we
167
+ # are only collecting gemspec files.
168
+ unless spec_file.file?
169
+ verbose "Gem #{spec.full_name} root folder was not found, skipping"
170
+ next []
171
+ end
172
+
173
+ # Add gemspec files
174
+ if spec_file.subpath?(exec_prefix)
175
+ builder.duplicate_to_exec_prefix(spec_file)
176
+ elsif (gem_path = GemSpecQueryable.find_gem_path(spec_file))
177
+ builder.duplicate_to_gem_home(spec_file, gem_path)
178
+ else
179
+ raise "Gem spec #{spec_file} does not exist in the Ruby installation. Don't know where to put it."
180
+ end
181
+
182
+ # Determine which set of files to include for this particular gem
183
+ include = GemSpecQueryable.gem_inclusion_set(spec.name, @option.gem_options)
184
+ say "Detected gem #{spec.full_name} (#{include.join(", ")})"
185
+
186
+ spec.extend(GemSpecQueryable)
187
+
188
+ actual_files = spec.find_gem_files(include, features)
189
+ say "\t#{actual_files.size} files, #{actual_files.sum(0, &:size)} bytes"
190
+
191
+ # Decide where to put gem files, either the system gem folder, or
192
+ # GEMDIR.
193
+ actual_files.each do |gemfile|
194
+ if gemfile.subpath?(exec_prefix)
195
+ builder.duplicate_to_exec_prefix(gemfile)
196
+ elsif (gem_path = GemSpecQueryable.find_gem_path(gemfile))
197
+ builder.duplicate_to_gem_home(gemfile, gem_path)
198
+ else
199
+ raise "Don't know where to put gemfile #{gemfile}"
200
+ end
201
+ end
202
+
203
+ actual_files
204
+ end
205
+ gem_files.uniq!
206
+
207
+ features -= gem_files
208
+
209
+ # If requested, add all ruby standard libraries
210
+ if @option.add_all_core?
211
+ say "Will include all ruby core libraries"
212
+ @pre_env.load_path.each do |load_path|
213
+ path = Pathname.new(load_path)
214
+ # Match the load path against standard library, site_ruby, and vendor_ruby paths
215
+ path.to_posix.match(RUBY_LIBRARY_PATH_REGEX) do |m|
216
+ subdir = m[1]
217
+ path.find.each do |src|
218
+ next if src.directory?
219
+ builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # Include encoding support files
226
+ if @option.add_all_encoding?
227
+ @post_env.load_path.each do |load_path|
228
+ load_path = Pathname(@post_env.expand_path(load_path))
229
+ next unless load_path.subpath?(exec_prefix)
230
+
231
+ enc_dir = load_path / "enc"
232
+ next unless enc_dir.directory?
233
+
234
+ enc_files = enc_dir.find.select { |path| path.file? && path.extname?(".so") }
235
+ say "Including #{enc_files.size} encoding support files (#{enc_files.sum(0, &:size)} bytes, use --no-enc to exclude)"
236
+ enc_files.each do |path|
237
+ builder.duplicate_to_exec_prefix(path)
238
+ end
239
+ end
240
+ else
241
+ say "Not including encoding support files"
242
+ end
243
+
244
+ # Workaround: RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
245
+ # Add an empty file to /msys64/usr/bin/msys-2.0.dll if the dll was not required otherwise
246
+ builder.touch('msys64/usr/bin/msys-2.0.dll')
247
+
248
+ # Find the source root and adjust paths
249
+ source_files = @option.source_files.dup
250
+ src_prefix = resolve_root_prefix(source_files)
251
+
252
+ # Find features and decide where to put them in the temporary
253
+ # directory layout.
254
+ src_load_path = []
255
+ # Add loaded libraries (features, gems)
256
+ say "Adding library files"
257
+ added_load_paths = (@post_env.load_path - @pre_env.load_path).map { |load_path| Pathname(@post_env.expand_path(load_path)) }
258
+ pre_working_directory = Pathname(@pre_env.pwd)
259
+ working_directory = Pathname(@post_env.pwd)
260
+ features.each do |feature|
261
+ load_path = @post_env.find_load_path(feature)
262
+ if load_path.nil?
263
+ source_files << feature
264
+ next
265
+ end
266
+ abs_load_path = Pathname(@post_env.expand_path(load_path))
267
+ if abs_load_path == pre_working_directory
268
+ source_files << feature
269
+ elsif feature.subpath?(exec_prefix)
270
+ # Features found in the Ruby installation are put in the
271
+ # temporary Ruby installation.
272
+ builder.duplicate_to_exec_prefix(feature)
273
+ elsif (gem_path = GemSpecQueryable.find_gem_path(feature))
274
+ # Features found in any other Gem path (e.g. ~/.gems) is put
275
+ # in a special 'gems' folder.
276
+ builder.duplicate_to_gem_home(feature, gem_path)
277
+ elsif feature.subpath?(src_prefix) || abs_load_path == working_directory
278
+ # Any feature found inside the src_prefix automatically gets
279
+ # added as a source file (to go in 'src').
280
+ source_files << feature
281
+ # Add the load path unless it was added by the script while
282
+ # running (or we assume that the script can also set it up
283
+ # correctly when running from the resulting executable).
284
+ src_load_path << abs_load_path unless added_load_paths.include?(abs_load_path)
285
+ elsif added_load_paths.include?(abs_load_path)
286
+ # Any feature that exist in a load path added by the script
287
+ # itself is added as a file to go into the 'src' (src_prefix
288
+ # will be adjusted below to point to the common parent).
289
+ source_files << feature
290
+ else
291
+ # All other feature that can not be resolved go in the the
292
+ # Ruby sitelibdir. This is automatically in the load path
293
+ # when Ruby starts.
294
+ inst_sitelibdir = sitelibdir.relative_path_from(exec_prefix)
295
+ builder.cp(feature, inst_sitelibdir / feature.relative_path_from(abs_load_path))
296
+ end
297
+ end
298
+
299
+ # Recompute the src_prefix. Files may have been added implicitly
300
+ # while scanning through features.
301
+ inst_src_prefix = resolve_root_prefix(source_files)
302
+
303
+ # Add explicitly mentioned files
304
+ say "Adding user-supplied source files"
305
+ source_files.each do |source|
306
+ target = builder.resolve_source_path(source, inst_src_prefix)
307
+
308
+ if source.directory?
309
+ builder.mkdir(target)
310
+ else
311
+ builder.cp(source, target)
312
+ end
313
+ end
314
+
315
+ # Set environment variable
316
+ builder.export("RUBYOPT", rubyopt)
317
+ # Add the load path that are required with the correct path after
318
+ # src_prefix was adjusted.
319
+ load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
320
+ builder.set_env_path("RUBYLIB", *load_path)
321
+ builder.set_env_path("GEM_PATH", GEMDIR)
322
+
323
+ # Add the opcode to launch the script
324
+ installed_ruby_exe = BINDIR / ruby_executable
325
+ target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
326
+ builder.exec(installed_ruby_exe, target_script, *@option.argv)
327
+ end
328
+
329
+ def to_proc
330
+ method(:construct).to_proc
331
+ end
332
+
333
+ def build_inno_setup_installer
334
+ require_relative "inno_setup_script_builder"
335
+ iss_builder = InnoSetupScriptBuilder.new(@option.inno_setup_script)
336
+
337
+ require_relative "launcher_batch_builder"
338
+ launcher_builder = LauncherBatchBuilder.new(
339
+ chdir_before: @option.chdir_before?,
340
+ title: @option.output_executable.basename.sub_ext("")
341
+ )
342
+
343
+ require_relative "build_facade"
344
+ builder = BuildFacade.new(iss_builder, launcher_builder)
345
+
346
+ if @option.icon_filename
347
+ builder.cp(@option.icon_filename, File.basename(@option.icon_filename))
348
+ end
349
+
350
+ construct(builder)
351
+
352
+ say "Build launcher batch file"
353
+ launcher_path = launcher_builder.build
354
+ verbose File.read(launcher_path)
355
+ builder.cp(launcher_path, "launcher.bat")
356
+
357
+ say "Build inno setup script file"
358
+ iss_path = iss_builder.build
359
+ verbose File.read(iss_path)
360
+
361
+ say "Running Inno Setup Command-Line compiler (ISCC)"
362
+ iss_builder.compile(verbose: @option.verbose?)
363
+
364
+ say "Finished building installer file"
365
+ end
366
+
367
+ def build_stab_exe
368
+ require_relative "stub_builder"
369
+
370
+ if @option.enable_debug_mode?
371
+ say "Enabling debug mode in executable"
372
+ end
373
+
374
+ StubBuilder.new(@option.output_executable,
375
+ chdir_before: @option.chdir_before?,
376
+ debug_extract: @option.enable_debug_extract?,
377
+ debug_mode: @option.enable_debug_mode?,
378
+ enable_compression: @option.enable_compression?,
379
+ gui_mode: @option.windowed?,
380
+ icon_path: @option.icon_filename,
381
+ &to_proc) => builder
382
+ say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
383
+ say "After decompression, the data will expand to #{builder.data_size} bytes."
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+ require_relative "refine_pathname"
4
+
5
+ module Ocran
6
+ class FilePathSet
7
+ using RefinePathname
8
+ include Enumerable
9
+
10
+ def initialize
11
+ @set = {}
12
+ end
13
+
14
+ def add(source, target)
15
+ add?(source, target)
16
+ self
17
+ end
18
+
19
+ # Adds a source and target path pair to the set and validates the paths before adding.
20
+ # This method performs various checks to ensure the source path is an absolute path
21
+ # and the target path is a relative path that does not include '.' or '..'.
22
+ # If a conflict is detected (i.e., different source for the same target),
23
+ # it raises an exception.
24
+ #
25
+ # @param [String, Pathname] source - The source file path; must be an absolute path.
26
+ # @param [String, Pathname] target - The target file path; must be a relative path.
27
+ # @return [self, nil] Returns self if the path pair is added successfully,
28
+ # returns nil if the same source and target pair is already present.
29
+ # @raise [ArgumentError] If the source is not an absolute path, if the target is not a relative path,
30
+ # or if the target includes '.' or '..'.
31
+ # @raise [RuntimeError] If a conflicting source is found for the same target.
32
+ def add?(source, target)
33
+ source = Pathname.new(source) unless source.is_a?(Pathname)
34
+ source = source.cleanpath
35
+ unless source.absolute?
36
+ raise ArgumentError, "Source path must be absolute, given: #{source}"
37
+ end
38
+
39
+ target = Pathname.new(target) unless target.is_a?(Pathname)
40
+ target = target.cleanpath
41
+ unless target.relative?
42
+ raise ArgumentError, "Target path must be relative, given: #{target}"
43
+ end
44
+ if %w(. ..).include?(target.each_filename.first)
45
+ raise ArgumentError, "Relative paths such as '.' or '..' are not allowed, given: #{target}"
46
+ end
47
+
48
+ if (path = @set[target])
49
+ if path.eql?(source)
50
+ return nil
51
+ else
52
+ raise "Conflicting sources for the same target. Target: #{target}, Existing Source: #{path}, Given Source: #{source}"
53
+ end
54
+ end
55
+
56
+ @set[target] = source
57
+ self
58
+ end
59
+
60
+ def each
61
+ return to_enum(__method__) unless block_given?
62
+ @set.each { |target, source| yield(source, target) }
63
+ end
64
+
65
+ def to_a
66
+ each.to_a
67
+ end
68
+ end
69
+ end