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,172 @@
1
+ # frozen_string_literal: true
2
+ require "rubygems"
3
+ require "pathname"
4
+ require_relative "refine_pathname"
5
+
6
+ module Ocran
7
+ module GemSpecQueryable
8
+ using RefinePathname
9
+
10
+ GEM_SCRIPT_RE = /\.rbw?$/
11
+ GEM_EXTRA_RE = %r{(
12
+ # Auxiliary files in the root of the gem
13
+ ^(\.\/)?(History|Install|Manifest|README|CHANGES|Licen[sc]e|Contributors|ChangeLog|BSD|GPL).*$ |
14
+ # Installation files in the root of the gem
15
+ ^(\.\/)?(Rakefile|setup.rb|extconf.rb)$ |
16
+ # Documentation/test directories in the root of the gem
17
+ ^(\.\/)?(doc|ext|examples|test|tests|benchmarks|spec)\/ |
18
+ # Directories anywhere
19
+ (^|\/)(\.autotest|\.svn|\.cvs|\.git)(\/|$) |
20
+ # Unlikely extensions
21
+ \.(rdoc|c|cpp|c\+\+|cxx|h|hxx|hpp|obj|o|a)$
22
+ )}xi
23
+ GEM_NON_FILE_RE = /(#{GEM_EXTRA_RE}|#{GEM_SCRIPT_RE})/
24
+
25
+ class << self
26
+ # find_gem_path method searches for the path of the gem containing the
27
+ # specified path. The 'path' argument is a file or directory path.
28
+ # It checks each gem's installation path in Gem.path to see if the
29
+ # specified path is a subpath. Returns the gem's path if found, or
30
+ # nil if not found.
31
+ def find_gem_path(path)
32
+ return unless defined?(Gem)
33
+
34
+ Gem.path.find { |gem_path| Pathname(path).subpath?(gem_path) }
35
+ end
36
+
37
+ # find_spec_file method searches for the path of the gemspec file of
38
+ # the gem containing the specified path. The 'path' argument is a file
39
+ # or directory path. It searches within the "gems" directory in each
40
+ # directory listed in Gem.path to check if the specified path is a
41
+ # subpath. If the gemspec file exists, it returns its path; otherwise,
42
+ # it returns nil.
43
+ def find_spec_file(path)
44
+ return unless defined?(Gem)
45
+
46
+ feature = Pathname(path)
47
+ Gem.path.each do |gem_path|
48
+ gems_dir = File.join(gem_path, "gems")
49
+ next unless feature.subpath?(gems_dir)
50
+
51
+ full_name = feature.relative_path_from(gems_dir).each_filename.first
52
+ spec_path = File.join(gem_path, "specifications", "#{full_name}.gemspec")
53
+ return spec_path if File.exist?(spec_path)
54
+ end
55
+ nil
56
+ end
57
+
58
+ # find_spec method searches and returns a Gem::Specification object
59
+ # based on the specified path. Internally, it uses find_spec_file to
60
+ # obtain the path to the gemspec file, and if that file exists, it
61
+ # calls Gem::Specification.load to load the gem's specifications.
62
+ # Returns the loaded Gem::Specification object, or nil if the gemspec
63
+ # file does not exist.
64
+ def find_spec(path)
65
+ return unless defined?(Gem)
66
+
67
+ spec_file = find_spec_file(path)
68
+ spec_file && Gem::Specification.load(spec_file)
69
+ end
70
+
71
+ def scanning_gemfile(gemfile_path)
72
+ # Ensure the necessary libraries are loaded to scan the Gemfile.
73
+ # This is particularly useful in custom-built Ruby environments or
74
+ # where certain libraries might be excluded.
75
+ %w[rubygems bundler].each do |lib|
76
+ require lib
77
+ rescue LoadError
78
+ raise "Couldn't scan Gemfile, unable to load #{lib}"
79
+ end
80
+
81
+ ENV["BUNDLE_GEMFILE"] = gemfile_path.to_s
82
+ # Bundler.load.specs includes the spec for Bundler itself
83
+ Bundler.load.specs.to_a
84
+ end
85
+
86
+ # Fall back to gem detection
87
+ def detect_gems_from(features, verbose: false)
88
+ features.inject([]) do |gems, feature|
89
+ if gems.any? { |spec| feature.subpath?(spec.gem_dir) }
90
+ # Skip if found in known Gem dir
91
+ elsif (spec = GemSpecQueryable.find_spec(feature))
92
+ gems << spec
93
+ else
94
+ puts "Failed to load gemspec for #{feature}" if verbose
95
+ end
96
+
97
+ gems
98
+ end
99
+ end
100
+
101
+ def gem_inclusion_set(spec_name, gem_options)
102
+ include = [:loaded, :files]
103
+ gem_options.each do |negate, option, list|
104
+ next unless list.nil? || list.include?(spec_name)
105
+
106
+ case option
107
+ when :minimal
108
+ include = [:loaded]
109
+ when :guess
110
+ include = [:loaded, :files]
111
+ when :all
112
+ include = [:scripts, :files]
113
+ when :full
114
+ include = [:scripts, :files, :extras]
115
+ when :spec
116
+ include = [:spec]
117
+ when :scripts, :files, :extras
118
+ if negate
119
+ include.delete(option)
120
+ else
121
+ include.push(option)
122
+ end
123
+ else
124
+ raise "Invalid Gem content detection option: #{option}"
125
+ end
126
+ end
127
+ include.uniq!
128
+ include
129
+ end
130
+ end
131
+
132
+ def gem_root = Pathname(gem_dir)
133
+
134
+ # Find the selected files
135
+ def gem_root_files = gem_root.find.select(&:file?)
136
+
137
+ def script_files
138
+ gem_root_files.select { |path| path.extname =~ GEM_SCRIPT_RE }
139
+ end
140
+
141
+ def extra_files
142
+ gem_root_files.select { |path| path.relative_path_from(gem_root).to_posix =~ GEM_EXTRA_RE }
143
+ end
144
+
145
+ def resource_files
146
+ files = gem_root_files.select { |path| path.relative_path_from(gem_root).to_posix !~ GEM_NON_FILE_RE }
147
+ files << Pathname(gem_build_complete_path) if File.exist?(gem_build_complete_path)
148
+ files
149
+ end
150
+
151
+ def find_gem_files(file_sets, features_from_gems)
152
+ actual_files = file_sets.flat_map do |set|
153
+ case set
154
+ when :spec
155
+ files.map { |file| Pathname(file) }
156
+ when :loaded
157
+ features_from_gems.select { |feature| feature.subpath?(gem_dir) }
158
+ when :files
159
+ resource_files
160
+ when :extras
161
+ extra_files
162
+ when :scripts
163
+ script_files
164
+ else
165
+ raise "Invalid file set: #{set}. Please specify a valid file set (:spec, :loaded, :files, :extras, :scripts)."
166
+ end
167
+ end
168
+ actual_files.uniq
169
+ actual_files
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,37 @@
1
+ require "rbconfig"
2
+ require "pathname"
3
+
4
+ module Ocran
5
+ # Variables describing the host's build environment.
6
+ module HostConfigHelper
7
+ module_function
8
+
9
+ def exec_prefix
10
+ @exec_prefix ||= Pathname.new(RbConfig::CONFIG["exec_prefix"])
11
+ end
12
+
13
+ def sitelibdir
14
+ @sitelibdir ||= Pathname.new(RbConfig::CONFIG["sitelibdir"])
15
+ end
16
+
17
+ def bindir
18
+ @bindir ||= Pathname.new(RbConfig::CONFIG["bindir"])
19
+ end
20
+
21
+ def libruby_so
22
+ @libruby_so ||= Pathname.new(RbConfig::CONFIG["LIBRUBY_SO"])
23
+ end
24
+
25
+ def exe_extname
26
+ RbConfig::CONFIG["EXEEXT"] || ".exe"
27
+ end
28
+
29
+ def rubyw_exe
30
+ @rubyw_exe ||= (RbConfig::CONFIG["rubyw_install_name"] || "rubyw") + exe_extname
31
+ end
32
+
33
+ def ruby_exe
34
+ @ruby_exe ||= (RbConfig::CONFIG["ruby_install_name"] || "ruby") + exe_extname
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ require "tempfile"
3
+ require_relative "file_path_set"
4
+ require_relative "windows_command_escaping"
5
+
6
+ module Ocran
7
+ class InnoSetupScriptBuilder
8
+ ISCC_CMD = "ISCC"
9
+ ISCC_SUCCESS = 0
10
+ ISCC_INVALID_PARAMS = 1
11
+ ISCC_COMPILATION_FAILED = 2
12
+
13
+ extend WindowsCommandEscaping
14
+
15
+ class << self
16
+ def compile(iss_filename, quiet: false)
17
+ unless system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
18
+ raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
19
+ end
20
+
21
+ cmd_line = [ISCC_CMD]
22
+ cmd_line << "/Q" if quiet
23
+ cmd_line << iss_filename
24
+ system(*cmd_line)
25
+
26
+ case $?&.exitstatus
27
+ when ISCC_SUCCESS
28
+ # ISCC reported success
29
+ when ISCC_INVALID_PARAMS
30
+ raise "ISCC reports invalid command line parameters"
31
+ when ISCC_COMPILATION_FAILED
32
+ raise "ISCC reports that compilation failed"
33
+ else
34
+ raise "ISCC failed to run"
35
+ end
36
+ end
37
+ end
38
+
39
+ include WindowsCommandEscaping
40
+
41
+ def initialize(inno_setup_script)
42
+ # ISSC generates the installer files relative to the directory of the
43
+ # ISS file. Therefore, it is necessary to create Tempfiles in the
44
+ # working directory.
45
+ @build_file = Tempfile.new("", Dir.pwd)
46
+ if inno_setup_script
47
+ IO.copy_stream(inno_setup_script, @build_file)
48
+ end
49
+ @dirs = FilePathSet.new
50
+ @files = FilePathSet.new
51
+ end
52
+
53
+ def build
54
+ @build_file.tap do |f|
55
+ if @dirs.any?
56
+ f.puts
57
+ f.puts "[Dirs]"
58
+ @dirs.each { |_source, target| f.puts build_dir_item(target) }
59
+ end
60
+
61
+ if @files.any?
62
+ f.puts
63
+ f.puts "[Files]"
64
+ @files.each { |source, target| f.puts build_file_item(source, target) }
65
+ end
66
+ end.close
67
+ path
68
+ end
69
+
70
+ def path
71
+ @build_file.to_path
72
+ end
73
+
74
+ def compile(verbose: false)
75
+ InnoSetupScriptBuilder.compile(path, quiet: !verbose)
76
+ end
77
+
78
+ def mkdir(target)
79
+ @dirs.add?("/", target)
80
+ end
81
+
82
+ def cp(source, target)
83
+ unless File.exist?(source)
84
+ raise "The file does not exist (#{source})"
85
+ end
86
+
87
+ @files.add?(source, target)
88
+ end
89
+
90
+ def build_dir_item(target)
91
+ name = File.join("{app}", target)
92
+ "Name: #{quote_and_escape(name)};"
93
+ end
94
+ private :build_dir_item
95
+
96
+ def build_file_item(source, target)
97
+ dest_dir = File.join("{app}", File.dirname(target))
98
+ s = [
99
+ "Source: #{quote_and_escape(source)};",
100
+ "DestDir: #{quote_and_escape(dest_dir)};"
101
+ ]
102
+ src_name = File.basename(source)
103
+ dest_name = File.basename(target)
104
+ if src_name != dest_name
105
+ s << "DestName: #{quote_and_escape(dest_name)};"
106
+ end
107
+ s.join(" ")
108
+ end
109
+ private :build_file_item
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ require "tempfile"
3
+ require_relative "windows_command_escaping"
4
+ require_relative "build_constants"
5
+
6
+ module Ocran
7
+ class LauncherBatchBuilder
8
+ include BuildConstants, WindowsCommandEscaping
9
+
10
+ # BATCH_FILE_DIR is a parameter expansion used in Windows batch files,
11
+ # representing the full path to the directory where the batch file resides.
12
+ # It allows for the use of pseudo-relative paths by referencing the
13
+ # batch file's own location without changing the working directory.
14
+ BATCH_FILE_DIR = "%~dp0"
15
+
16
+ # BATCH_FILE_PATH is a parameter expansion used in Windows batch files,
17
+ # representing the full path to the batch file itself, including the file name.
18
+ BATCH_FILE_PATH = "%~f0"
19
+
20
+ def initialize(chdir_before: nil, title: nil)
21
+ @build_file = Tempfile.new
22
+ @title = title
23
+ @chdir_before = chdir_before
24
+ @environments = {}
25
+ end
26
+
27
+ def build
28
+ @build_file.tap do |f|
29
+ f.puts "@echo off"
30
+ @environments.each { |name, val| f.puts build_set_command(name, val) }
31
+ f.puts build_set_command("OCRAN_EXECUTABLE", BATCH_FILE_PATH)
32
+ f.puts build_start_command(@title, @executable, @script, *@args, chdir_before: @chdir_before)
33
+ f
34
+ end.close
35
+ path
36
+ end
37
+
38
+ def path
39
+ @build_file.to_path
40
+ end
41
+
42
+ def export(name, value)
43
+ @environments[name] = value
44
+ end
45
+
46
+ def exec(executable, script, *args)
47
+ @executable, @script, @args = executable, script, args
48
+ end
49
+
50
+ def replace_inst_dir_placeholder(s)
51
+ s.to_s.gsub(/#{Regexp.escape(EXTRACT_ROOT.to_s)}[\/\\]/, BATCH_FILE_DIR)
52
+ end
53
+ private :replace_inst_dir_placeholder
54
+
55
+ def build_set_command(name, value)
56
+ "set \"#{name}=#{replace_inst_dir_placeholder(value)}\""
57
+ end
58
+ private :build_set_command
59
+
60
+ def build_start_command(title, executable, script, *args, chdir_before: nil)
61
+ cmd = ["start"]
62
+
63
+ # Title for Command Prompt window title bar
64
+ cmd << quote_and_escape(title)
65
+
66
+ # Use /d to set the startup directory for the process,
67
+ # which will be BATCH_FILE_DIR/SRCDIR. This path is where
68
+ # the script is located, establishing the working directory
69
+ # at process start.
70
+ if chdir_before
71
+ cmd << "/d #{quote_and_escape("#{BATCH_FILE_DIR}#{SRCDIR}")}"
72
+ end
73
+
74
+ cmd << quote_and_escape("#{BATCH_FILE_DIR}#{executable}")
75
+ cmd << quote_and_escape("#{BATCH_FILE_DIR}#{script}")
76
+ cmd += args.map { |arg| quote_and_escape(replace_inst_dir_placeholder(arg)) }
77
+
78
+ # Forward batch file arguments to the command with `%*`
79
+ cmd << "%*"
80
+
81
+ cmd.join(" ")
82
+ end
83
+ private :build_start_command
84
+ end
85
+ end
@@ -0,0 +1,61 @@
1
+ require "fiddle/import"
2
+ require "fiddle/types"
3
+
4
+ module Ocran
5
+ module LibraryDetector
6
+ # Windows API functions for handling files may return long paths,
7
+ # with a maximum character limit of 32,767.
8
+ # "\\?\" prefix(4 characters) + long path(32762 characters) + NULL = 32767 characters
9
+ # https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
10
+ MAX_PATH = 32767
11
+
12
+ # The byte size of the buffer given as an argument to the EnumProcessModules function.
13
+ # This buffer is used to store the handles of the loaded modules.
14
+ # If the buffer size is smaller than the number of loaded modules,
15
+ # it will automatically increase the buffer size and call the EnumProcessModules function again.
16
+ # Increasing the initial buffer size can reduce the number of iterations required.
17
+ # https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-modules-for-a-process
18
+ DEFAULT_HMODULE_BUFFER_SIZE = 1024
19
+
20
+ extend Fiddle::Importer
21
+ dlload "kernel32.dll", "psapi.dll"
22
+
23
+ include Fiddle::Win32Types
24
+ # https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
25
+ typealias "HMODULE", "HINSTANCE"
26
+ typealias "LPDWORD", "PDWORD"
27
+ typealias "LPWSTR", "char*"
28
+
29
+ extern "BOOL EnumProcessModules(HANDLE, HMODULE*, DWORD, LPDWORD)"
30
+ extern "DWORD GetModuleFileNameW(HMODULE, LPWSTR, DWORD)"
31
+ extern "HANDLE GetCurrentProcess()"
32
+ extern "DWORD GetLastError()"
33
+
34
+ class << self
35
+ def loaded_dlls
36
+ dword = "L" # A DWORD is a 32-bit unsigned integer.
37
+ bytes_needed = [0].pack(dword)
38
+ bytes = DEFAULT_HMODULE_BUFFER_SIZE
39
+ process_handle = GetCurrentProcess()
40
+ handles = while true
41
+ buffer = "\x00" * bytes
42
+ if EnumProcessModules(process_handle, buffer, buffer.bytesize, bytes_needed) == 0
43
+ raise "EnumProcessModules failed with error code #{GetLastError()}"
44
+ end
45
+ bytes = bytes_needed.unpack1(dword)
46
+ if bytes <= buffer.bytesize
47
+ break buffer.unpack("J#{bytes / Fiddle::SIZEOF_VOIDP}")
48
+ end
49
+ end
50
+ str = "\x00".encode("UTF-16LE") * MAX_PATH
51
+ handles.map do |handle|
52
+ length = GetModuleFileNameW(handle, str, str.bytesize)
53
+ if length == 0
54
+ raise "GetModuleFileNameW failed with error code #{GetLastError()}"
55
+ end
56
+ str[0, length].encode("UTF-8")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end