ocran 1.3.15 → 1.3.16
Sign up to get free protection for your applications and to get access to all the features.
- 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
|