ocran 1.3.15 → 1.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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