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.
- checksums.yaml +4 -4
- data/bin/ocran +2 -1314
- data/lib/ocran/build_constants.rb +16 -0
- data/lib/ocran/build_facade.rb +17 -0
- data/lib/ocran/build_helper.rb +105 -0
- data/lib/ocran/command_output.rb +22 -0
- data/lib/ocran/direction.rb +386 -0
- data/lib/ocran/file_path_set.rb +69 -0
- data/lib/ocran/gem_spec_queryable.rb +172 -0
- data/lib/ocran/host_config_helper.rb +37 -0
- data/lib/ocran/inno_setup_script_builder.rb +111 -0
- data/lib/ocran/launcher_batch_builder.rb +85 -0
- data/lib/ocran/library_detector.rb +61 -0
- data/lib/ocran/option.rb +273 -0
- data/lib/ocran/refine_pathname.rb +104 -0
- data/lib/ocran/runner.rb +105 -0
- data/lib/ocran/runtime_environment.rb +46 -0
- data/lib/ocran/stub_builder.rb +224 -0
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/windows_command_escaping.rb +15 -0
- data/lib/ocran.rb +3 -2
- data/share/ocran/edicon.exe +0 -0
- data/share/ocran/stub.exe +0 -0
- data/share/ocran/stubw.exe +0 -0
- metadata +23 -6
- /data/{share/ocran/empty-msys-2.0.dll → lib/ocran/empty_source} +0 -0
@@ -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
|