ocran 1.4.6 → 1.5.1
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/CHANGELOG.txt +82 -0
- data/README.md +519 -15
- data/ext/extconf.rb +5 -1
- data/lib/ocran/aot_toolchain.rb +154 -0
- data/lib/ocran/build_helper.rb +7 -0
- data/lib/ocran/command_output.rb +8 -1
- data/lib/ocran/compatibility_scan.rb +414 -0
- data/lib/ocran/cosmo_toolchain.rb +29 -7
- data/lib/ocran/dir_builder.rb +88 -38
- data/lib/ocran/direction.rb +292 -44
- data/lib/ocran/ed_resource.rb +113 -0
- data/lib/ocran/ed_resource_builder.rb +199 -0
- data/lib/ocran/env_hook.rb +98 -0
- data/lib/ocran/file_path_set.rb +9 -5
- data/lib/ocran/gem_spec_queryable.rb +13 -5
- data/lib/ocran/host_config_helper.rb +11 -1
- data/lib/ocran/inno_setup_script_builder.rb +24 -14
- data/lib/ocran/launcher_batch_builder.rb +8 -11
- data/lib/ocran/npm_package.rb +106 -0
- data/lib/ocran/option.rb +465 -35
- data/lib/ocran/pe_imports.rb +96 -0
- data/lib/ocran/pe_resources.rb +226 -0
- data/lib/ocran/refine_pathname.rb +29 -32
- data/lib/ocran/roundhouse_builder.rb +316 -0
- data/lib/ocran/runner.rb +52 -11
- data/lib/ocran/runtime_sdk.rb +259 -0
- data/lib/ocran/spinel_builder.rb +183 -0
- data/lib/ocran/spinel_compatibility.rb +149 -0
- data/lib/ocran/static_runtime/boot.rb +443 -0
- data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
- data/lib/ocran/static_runtime_builder.rb +814 -0
- data/lib/ocran/stub_builder.rb +70 -25
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/wasm_builder.rb +570 -0
- data/lib/ocran/wasm_compatibility.rb +198 -0
- data/lib/ocran/windows_command_escaping.rb +21 -0
- data/lib/ocran/zip_payload_builder.rb +18 -9
- data/lib/ocran/zip_writer.rb +48 -9
- data/src/Makefile +18 -2
- data/src/Makefile.mswin +65 -0
- data/src/inst_dir.c +34 -6
- data/src/path_utils.c +144 -0
- data/src/script_info.c +5 -0
- data/src/stub.c +77 -19
- data/src/stub.rc +1 -1
- data/src/system_utils.c +276 -155
- data/src/system_utils.h +46 -3
- data/src/system_utils_posix.c +243 -148
- data/src/unpack.c +9 -0
- metadata +25 -4
data/lib/ocran/direction.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "refine_pathname"
|
|
|
6
6
|
require_relative "host_config_helper"
|
|
7
7
|
require_relative "command_output"
|
|
8
8
|
require_relative "build_constants"
|
|
9
|
+
require_relative "env_hook"
|
|
9
10
|
|
|
10
11
|
module Ocran
|
|
11
12
|
class Direction
|
|
@@ -16,7 +17,9 @@ module Ocran
|
|
|
16
17
|
# - /ruby/3.0.0/
|
|
17
18
|
# - /ruby/site_ruby/3.0.0/
|
|
18
19
|
# - /ruby/vendor_ruby/3.0.0/
|
|
19
|
-
|
|
20
|
+
# The version directory is RbConfig's ruby_version, which development
|
|
21
|
+
# builds suffix with their ABI version (e.g. "4.1.0+5").
|
|
22
|
+
RUBY_LIBRARY_PATH_REGEX = %r{/(ruby/(?:site_ruby/|vendor_ruby/)?\d+\.\d+\.\d+(?:\+\d+)?)/?$}i
|
|
20
23
|
|
|
21
24
|
# Core libraries provided by glibc and the dynamic loader. These must
|
|
22
25
|
# never be bundled: they are tightly coupled to the target system's
|
|
@@ -35,6 +38,9 @@ module Ocran
|
|
|
35
38
|
# payload, which is statically linked and has no dlopen.
|
|
36
39
|
NATIVE_BINARY_EXTENSIONS = %w[.so .bundle .dll].freeze
|
|
37
40
|
|
|
41
|
+
# Visual C++ 2015-2022 runtime DLLs (Microsoft.VC14x.CRT redistributables)
|
|
42
|
+
VC_RUNTIME_DLL_RE = /\A(?:vcruntime140(?:_\d+)?|msvcp140(?:_\w+)?|concrt140|vccorlib140|vcomp140)\.dll\z/i
|
|
43
|
+
|
|
38
44
|
# Native binaries a gem ships or has had built for it, i.e. the files
|
|
39
45
|
# that make it unusable under the cosmopolitan Ruby payload.
|
|
40
46
|
#
|
|
@@ -176,7 +182,48 @@ module Ocran
|
|
|
176
182
|
else
|
|
177
183
|
require_relative "library_detector_posix"
|
|
178
184
|
end
|
|
179
|
-
LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
|
|
185
|
+
dlls = LibraryDetector.loaded_dlls.map { |path| Pathname.new(path).cleanpath }
|
|
186
|
+
# A Ruby reached through a junction (the "current" link of rb or
|
|
187
|
+
# scoop) loads its DLLs by the junction path, while RbConfig reports
|
|
188
|
+
# the real prefix. Resolve them, or no DLL is recognised as part of
|
|
189
|
+
# the Ruby installation.
|
|
190
|
+
dlls.map! { |dll| dll.realpath rescue dll } if Gem.win_platform?
|
|
191
|
+
dlls
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The DLLs that the packed ruby.exe (or rubyw.exe), the Ruby DLL and
|
|
195
|
+
# the native extensions among +features+ import, directly or through
|
|
196
|
+
# each other, that are found in the Ruby bin directory, and the Visual
|
|
197
|
+
# C++ runtime DLLs among them (from the system directory). System DLLs,
|
|
198
|
+
# and DLLs elsewhere that only the dependency run can find, are not
|
|
199
|
+
# included.
|
|
200
|
+
def windows_imported_dlls(features)
|
|
201
|
+
require_relative "pe_imports"
|
|
202
|
+
|
|
203
|
+
bin_dlls = bindir.children.select { |path| path.extname?(".dll") && path.file? }
|
|
204
|
+
.to_h { |path| [path.basename.to_s.downcase, path] }
|
|
205
|
+
system_dir = Pathname((ENV["SystemRoot"] || "C:/Windows").tr("\\", "/")) / "System32"
|
|
206
|
+
seen = Set.new
|
|
207
|
+
seen << libruby_so.to_s.downcase if libruby_so
|
|
208
|
+
found = []
|
|
209
|
+
queue = [bindir / ruby_executable]
|
|
210
|
+
queue << bindir / libruby_so if libruby_so
|
|
211
|
+
queue += features.select { |f| f.extname?(".so") }
|
|
212
|
+
while (image = queue.shift)
|
|
213
|
+
next unless image.file?
|
|
214
|
+
|
|
215
|
+
PEImports.dll_names(image).each do |name|
|
|
216
|
+
next unless seen.add?(name.downcase)
|
|
217
|
+
|
|
218
|
+
dll = bin_dlls[name.downcase]
|
|
219
|
+
dll ||= system_dir / name if name.match?(VC_RUNTIME_DLL_RE) && (system_dir / name).file?
|
|
220
|
+
next unless dll
|
|
221
|
+
|
|
222
|
+
found << dll
|
|
223
|
+
queue << dll
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
found
|
|
180
227
|
end
|
|
181
228
|
|
|
182
229
|
def find_gemspecs(features)
|
|
@@ -350,19 +397,30 @@ module Ocran
|
|
|
350
397
|
# Convert all relative paths to absolute paths before building.
|
|
351
398
|
# NOTE: In the future, different strategies may be needed before and after script execution.
|
|
352
399
|
features.filter_map do |feature|
|
|
353
|
-
|
|
354
|
-
feature
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
400
|
+
path =
|
|
401
|
+
if feature.absolute?
|
|
402
|
+
feature
|
|
403
|
+
elsif (load_path = @post_env.find_load_path(feature))
|
|
404
|
+
feature.expand_path(@post_env.expand_path(load_path))
|
|
405
|
+
else
|
|
406
|
+
# This message occurs when paths for core library files (e.g., enumerator.so,
|
|
407
|
+
# rational.so, complex.so, fiber.so, thread.rb, ruby2_keywords.rb) are not
|
|
408
|
+
# found. These are integral to Ruby's standard libraries or extensions and
|
|
409
|
+
# may not be located via normal load path searches, especially in RubyInstaller
|
|
410
|
+
# environments.
|
|
411
|
+
verbose "Load path not found for #{feature}, skip this feature"
|
|
412
|
+
next
|
|
413
|
+
end
|
|
414
|
+
# An entry can be in $LOADED_FEATURES without a file behind it: a
|
|
415
|
+
# racc parser generated with its runtime embedded marks
|
|
416
|
+
# <dir>/racc/parser.rb and racc/info.rb as loaded so that racc is not
|
|
417
|
+
# loaded twice (rdoc's RD parsers in Ruby 3.4 do). Copying it failed
|
|
418
|
+
# the whole build with ENOENT; there is nothing to pack.
|
|
419
|
+
unless path.file?
|
|
420
|
+
verbose "#{path} is marked as loaded but is not a file, skip this feature"
|
|
421
|
+
next
|
|
365
422
|
end
|
|
423
|
+
path
|
|
366
424
|
end
|
|
367
425
|
end
|
|
368
426
|
|
|
@@ -425,6 +483,19 @@ module Ocran
|
|
|
425
483
|
end
|
|
426
484
|
end
|
|
427
485
|
|
|
486
|
+
# String#unicode_normalize (and unicode_normalized?, unicode_normalize!)
|
|
487
|
+
# is core, but core requires its implementation from the standard
|
|
488
|
+
# library (unicode_normalize/normalize.rb, which loads tables.rb) on
|
|
489
|
+
# first use. An app that does not call it during the dependency run
|
|
490
|
+
# fails with a LoadError once packed, so always pack these files.
|
|
491
|
+
# Not with --cosmo-ruby: the payload has its own standard library.
|
|
492
|
+
unless @option.cosmo_ruby
|
|
493
|
+
known = features.to_set { |f| f.path_key }
|
|
494
|
+
(Pathname(RbConfig::CONFIG["rubylibdir"]) / "unicode_normalize").glob("*.rb").sort.each do |path|
|
|
495
|
+
features << path unless known.include?(path.path_key)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
428
499
|
say "Building #{@option.output_executable}"
|
|
429
500
|
require_relative "build_helper"
|
|
430
501
|
builder.extend(BuildHelper)
|
|
@@ -484,8 +555,17 @@ module Ocran
|
|
|
484
555
|
end
|
|
485
556
|
end
|
|
486
557
|
|
|
558
|
+
# Windows-only: DLLs bundled into bin, next to ruby.exe, by basename
|
|
559
|
+
dlls_in_bin = Set.new
|
|
560
|
+
add_dll_to_bin = proc do |dll|
|
|
561
|
+
next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
|
|
562
|
+
|
|
563
|
+
say "Adding detected DLL #{dll} to bin"
|
|
564
|
+
builder.copy_to_bin(dll, dll.basename)
|
|
565
|
+
end
|
|
566
|
+
|
|
487
567
|
# Windows-only: Add detected DLLs
|
|
488
|
-
if Gem.win_platform? && @option.auto_detect_dlls?
|
|
568
|
+
if Gem.win_platform? && @option.auto_detect_dlls? && !@option.static_runtime?
|
|
489
569
|
# The Windows loader resolves the imports of a native extension from
|
|
490
570
|
# the extension's own directory, the application directory of the
|
|
491
571
|
# packed ruby.exe (bin) plus its SxS assembly (ruby_builtin_dlls),
|
|
@@ -502,16 +582,17 @@ module Ocran
|
|
|
502
582
|
# LD_LIBRARY_PATH. DLLs from the Windows directory always come from
|
|
503
583
|
# the target system and are never bundled.
|
|
504
584
|
windows_dir = Pathname(ENV["SystemRoot"] || "C:/Windows")
|
|
505
|
-
dlls_in_bin = Set.new
|
|
506
|
-
add_dll_to_bin = proc do |dll|
|
|
507
|
-
next if dll.subpath?(bindir) || !dlls_in_bin.add?(dll.basename.to_s.downcase)
|
|
508
|
-
|
|
509
|
-
say "Adding detected DLL #{dll} to bin"
|
|
510
|
-
builder.copy_to_bin(dll, dll.basename)
|
|
511
|
-
end
|
|
512
585
|
|
|
513
586
|
detect_dlls.each do |dll|
|
|
514
587
|
next unless dll.extname?(".dll") && dll.basename != libruby_so
|
|
588
|
+
# The Visual C++ runtime is not part of Windows: an mswin Ruby
|
|
589
|
+
# (and any MSVC-built DLL a gem loads) fails to start with
|
|
590
|
+
# 0xC0000135 where the VC++ Redistributable is not installed.
|
|
591
|
+
# Microsoft permits deploying it app-locally, next to ruby.exe.
|
|
592
|
+
if dll.basename.to_s.match?(VC_RUNTIME_DLL_RE)
|
|
593
|
+
add_dll_to_bin.call(dll)
|
|
594
|
+
next
|
|
595
|
+
end
|
|
515
596
|
next if dll.subpath?(windows_dir)
|
|
516
597
|
|
|
517
598
|
if dll.subpath?(exec_prefix)
|
|
@@ -531,7 +612,7 @@ module Ocran
|
|
|
531
612
|
# the same extension packed at a gem path (openssl and psych are
|
|
532
613
|
# gems since Ruby 3.x) resolves its imports from bin.
|
|
533
614
|
features.select { |f| f.extname?(".so") && f.subpath?(exec_prefix) }
|
|
534
|
-
.map(&:dirname).uniq
|
|
615
|
+
.map(&:dirname).uniq { |dir| dir.path_key }
|
|
535
616
|
.each do |dir|
|
|
536
617
|
dir.each_child do |path|
|
|
537
618
|
next unless path.file? && path.extname?(".dll")
|
|
@@ -542,6 +623,25 @@ module Ocran
|
|
|
542
623
|
end
|
|
543
624
|
end
|
|
544
625
|
|
|
626
|
+
# Windows-only: DLLs that ruby.exe, the Ruby DLL and the packed native
|
|
627
|
+
# extensions import from the Ruby bin directory, plus the Visual C++
|
|
628
|
+
# runtime. RubyInstaller keeps such DLLs in ruby_builtin_dlls, which is
|
|
629
|
+
# packed whole below; an mswin Ruby keeps them (gmp, libffi, OpenSSL,
|
|
630
|
+
# libyaml, zlib from vcpkg) next to ruby.exe, and its interpreter does
|
|
631
|
+
# not even start without gmp and vcruntime140. They are read from the
|
|
632
|
+
# PE import tables, so they are packed with --no-autodll too, and for
|
|
633
|
+
# extensions the dependency run did not exercise.
|
|
634
|
+
if Gem.win_platform?
|
|
635
|
+
windows_imported_dlls(features).each do |dll|
|
|
636
|
+
if dll.subpath?(bindir)
|
|
637
|
+
verbose "Adding imported DLL #{dll}"
|
|
638
|
+
builder.duplicate_to_exec_prefix(dll)
|
|
639
|
+
else
|
|
640
|
+
add_dll_to_bin.call(dll)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
545
645
|
# Linux: bundle detected shared libraries (e.g. libyaml, libssl,
|
|
546
646
|
# libcrypt) next to libruby so the executable also runs on systems
|
|
547
647
|
# where they are missing or have different sonames (the stub already
|
|
@@ -549,7 +649,7 @@ module Ocran
|
|
|
549
649
|
# libraries and the loader are never bundled - they must come from the
|
|
550
650
|
# target system. Ruby native extensions are packed as features, not
|
|
551
651
|
# here. Not needed with --cosmo-ruby: the APE payload is static.
|
|
552
|
-
if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls? && !@option.cosmo_ruby
|
|
652
|
+
if RUBY_PLATFORM.include?("linux") && @option.auto_detect_dlls? && !@option.cosmo_ruby && !@option.static_runtime?
|
|
553
653
|
feature_set = features.to_set
|
|
554
654
|
feature_realpaths = features.filter_map { |f| f.realpath rescue nil }.to_set
|
|
555
655
|
# Ruby native extensions live in these directories and are packed as
|
|
@@ -851,7 +951,9 @@ module Ocran
|
|
|
851
951
|
|
|
852
952
|
actual_files
|
|
853
953
|
end
|
|
854
|
-
|
|
954
|
+
# Keyed on Pathname#path_key here and below: Array#uniq and Set do not
|
|
955
|
+
# see the refined, case-insensitive Pathname#eql? on Windows.
|
|
956
|
+
gem_files.uniq! { |file| file.path_key }
|
|
855
957
|
|
|
856
958
|
# On some distros parts of a gem are reachable through symlinks at other
|
|
857
959
|
# locations (e.g. Fedora symlinks /usr/share/ruby/psych.rb into the
|
|
@@ -859,16 +961,17 @@ module Ocran
|
|
|
859
961
|
# the same file under different paths. Compare realpaths when removing
|
|
860
962
|
# gem files from the feature list; otherwise a stdlib-level duplicate
|
|
861
963
|
# would be packed as well and shadow the packed gem at runtime.
|
|
862
|
-
gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil })
|
|
964
|
+
gem_file_set = (gem_files + gem_files.filter_map { |file| file.realpath rescue nil })
|
|
965
|
+
.to_set { |file| file.path_key }
|
|
863
966
|
features = features.reject do |feature|
|
|
864
|
-
next true if gem_file_set.include?(feature)
|
|
967
|
+
next true if gem_file_set.include?(feature.path_key)
|
|
865
968
|
|
|
866
969
|
real = begin
|
|
867
970
|
feature.realpath
|
|
868
971
|
rescue SystemCallError
|
|
869
972
|
nil
|
|
870
973
|
end
|
|
871
|
-
real && gem_file_set.include?(real)
|
|
974
|
+
real && gem_file_set.include?(real.path_key)
|
|
872
975
|
end
|
|
873
976
|
|
|
874
977
|
# If requested, add all ruby standard libraries
|
|
@@ -877,15 +980,23 @@ module Ocran
|
|
|
877
980
|
elsif @option.add_all_core?
|
|
878
981
|
say "Will include all ruby core libraries"
|
|
879
982
|
all_core_dir.each do |path|
|
|
983
|
+
# A configured directory that was never created (Debian's
|
|
984
|
+
# sitelibdir, /usr/local/lib/site_ruby/<ver>) holds nothing.
|
|
985
|
+
next unless path.directory?
|
|
986
|
+
|
|
880
987
|
# Match the load path against standard library, site_ruby, and vendor_ruby paths
|
|
881
988
|
unless (subdir = path.to_posix.match(RUBY_LIBRARY_PATH_REGEX)&.[](1))
|
|
882
989
|
raise "Unexpected library path format (does not match core dirs): #{path}"
|
|
883
990
|
end
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
a = Pathname(subdir) / src.relative_path_from(path)
|
|
991
|
+
files, skipped = self.class.core_library_files(path)
|
|
992
|
+
files.each do |src|
|
|
887
993
|
builder.copy_to_lib(src, Pathname(subdir) / src.relative_path_from(path))
|
|
888
994
|
end
|
|
995
|
+
next if skipped.empty?
|
|
996
|
+
|
|
997
|
+
warning "Not packed: #{skipped.size} entr#{skipped.size == 1 ? "y" : "ies"} of #{path} that " \
|
|
998
|
+
"#{skipped.size == 1 ? "is" : "are"} not a readable file (#{skipped.first(3).join(", ")}" \
|
|
999
|
+
"#{skipped.size > 3 ? ", ..." : ""})"
|
|
889
1000
|
end
|
|
890
1001
|
end
|
|
891
1002
|
|
|
@@ -912,7 +1023,8 @@ module Ocran
|
|
|
912
1023
|
end
|
|
913
1024
|
|
|
914
1025
|
# Windows-only: Workaround for RubyInstaller MSYS folder detection
|
|
915
|
-
|
|
1026
|
+
# (mswin Ruby, built with Visual C++, has no MSYS2 devkit)
|
|
1027
|
+
if Gem.win_platform? && !RUBY_PLATFORM.include?("mswin")
|
|
916
1028
|
# RubyInstaller cannot find the msys folder if ../msys64/usr/bin/msys-2.0.dll is not present
|
|
917
1029
|
# (since RubyInstaller-2.4.1 rubyinstaller 2 issue 23)
|
|
918
1030
|
builder.touch('msys64/usr/bin/msys-2.0.dll')
|
|
@@ -1082,7 +1194,7 @@ module Ocran
|
|
|
1082
1194
|
neutralize_bundler_env(builder)
|
|
1083
1195
|
# Add the load path that are required with the correct path after
|
|
1084
1196
|
# src_prefix was adjusted.
|
|
1085
|
-
load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq
|
|
1197
|
+
load_path = src_load_path.map { |path| SRCDIR / path.relative_path_from(inst_src_prefix) }.uniq { |path| path.path_key }
|
|
1086
1198
|
|
|
1087
1199
|
# On POSIX systems, also add the packed Ruby standard library directories
|
|
1088
1200
|
# to RUBYLIB. The Ruby binary has a compiled-in prefix pointing to the build
|
|
@@ -1121,7 +1233,7 @@ module Ocran
|
|
|
1121
1233
|
prefix_gem_dirs = (Gem.path.map { |dir| Pathname(dir) } + [Pathname(Gem.default_dir)])
|
|
1122
1234
|
.select { |dir| dir.subpath?(exec_prefix) }
|
|
1123
1235
|
.map { |dir| dir.relative_path_from(exec_prefix) }
|
|
1124
|
-
.uniq
|
|
1236
|
+
.uniq { |dir| dir.path_key }
|
|
1125
1237
|
gem_paths += prefix_gem_dirs
|
|
1126
1238
|
# RubyGems probes the default gem dir for writability at startup and
|
|
1127
1239
|
# prints "Can't determine writability of default gem path" on stderr
|
|
@@ -1137,6 +1249,8 @@ module Ocran
|
|
|
1137
1249
|
end
|
|
1138
1250
|
builder.set_env_path("GEM_PATH", *gem_paths)
|
|
1139
1251
|
|
|
1252
|
+
pack_env_hook(builder)
|
|
1253
|
+
|
|
1140
1254
|
# Add the opcode to launch the script
|
|
1141
1255
|
installed_ruby_exe = BINDIR / ruby_executable
|
|
1142
1256
|
target_script = builder.resolve_source_path(@option.script, inst_src_prefix)
|
|
@@ -1146,6 +1260,58 @@ module Ocran
|
|
|
1146
1260
|
builder.exec(installed_ruby_exe, target_script, *@option.argv)
|
|
1147
1261
|
end
|
|
1148
1262
|
|
|
1263
|
+
# Packs the RubyGems startup hook that sets the exported variables for a
|
|
1264
|
+
# bare bin/ruby of the package (see EnvHook), e.g. a Windows service
|
|
1265
|
+
# registered as `bin\ruby.exe src\service.rb`. It goes into the packed
|
|
1266
|
+
# sitelibdir, the first default load path entry, in every output that
|
|
1267
|
+
# carries the host Ruby: the stub and the launch scripts set the
|
|
1268
|
+
# variables themselves, so there it only loads the original file.
|
|
1269
|
+
# Not with --cosmo-ruby and --static-runtime, which bring their own
|
|
1270
|
+
# Ruby and set up their environment themselves.
|
|
1271
|
+
def pack_env_hook(builder)
|
|
1272
|
+
return if @option.cosmo_ruby || @option.static_runtime?
|
|
1273
|
+
|
|
1274
|
+
site = Pathname(sitelibdir)
|
|
1275
|
+
unless site.subpath?(exec_prefix)
|
|
1276
|
+
verbose "Not packing #{EnvHook::FEATURE}: sitelibdir #{site} is outside #{exec_prefix}"
|
|
1277
|
+
return
|
|
1278
|
+
end
|
|
1279
|
+
if (site / EnvHook::FEATURE).exist?
|
|
1280
|
+
warning "Not packing ocran's #{EnvHook::FEATURE}: #{site} has one already, so a bare " \
|
|
1281
|
+
"#{BINDIR / ruby_executable} of the package will not get the launch script's environment"
|
|
1282
|
+
return
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
target = site.relative_path_from(exec_prefix) / EnvHook::FEATURE
|
|
1286
|
+
say "Adding #{target} (sets the launch environment for a bare #{BINDIR / ruby_executable})"
|
|
1287
|
+
require "tempfile"
|
|
1288
|
+
hook_file = Tempfile.new(["ocran-operating_system", ".rb"])
|
|
1289
|
+
hook_file.write(EnvHook.source(builder.exported_env, target.dirname.each_filename.count))
|
|
1290
|
+
hook_file.close
|
|
1291
|
+
verbose File.read(hook_file.path)
|
|
1292
|
+
# Kept until the build finishes, like the RUBYOPT launcher.
|
|
1293
|
+
@env_hook_file = hook_file
|
|
1294
|
+
builder.cp(hook_file.path, target)
|
|
1295
|
+
end
|
|
1296
|
+
private :pack_env_hook
|
|
1297
|
+
|
|
1298
|
+
# The files below +dir+ that --add-all-core packs, and the entries it
|
|
1299
|
+
# cannot: a Ruby installation can list what is not there - Debian's
|
|
1300
|
+
# rdoc links its fonts into a fonts-lato package that may not be
|
|
1301
|
+
# installed, and a stdlib entry that vanished or cannot be read used to
|
|
1302
|
+
# abort the whole build (ENOENT) over a file no application needs.
|
|
1303
|
+
# Returns [files, skipped], both lists of Pathname.
|
|
1304
|
+
def self.core_library_files(dir)
|
|
1305
|
+
files = []
|
|
1306
|
+
skipped = []
|
|
1307
|
+
dir.find do |src|
|
|
1308
|
+
next if src.directory?
|
|
1309
|
+
|
|
1310
|
+
(src.file? && src.readable? ? files : skipped) << src
|
|
1311
|
+
end
|
|
1312
|
+
[files, skipped]
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1149
1315
|
# Maps an absolute build-machine path to its location (relative to the
|
|
1150
1316
|
# extraction root) inside the packed application, mirroring where
|
|
1151
1317
|
# #construct places files. Returns nil when the path is not packed.
|
|
@@ -1295,6 +1461,20 @@ module Ocran
|
|
|
1295
1461
|
end
|
|
1296
1462
|
end
|
|
1297
1463
|
|
|
1464
|
+
# The PE resource options (version info, string table, manifest), for
|
|
1465
|
+
# every Windows executable the user launches: the self-extracting exe and
|
|
1466
|
+
# the wrapper that installer, directory and zip output start from.
|
|
1467
|
+
def pe_resource_options
|
|
1468
|
+
{
|
|
1469
|
+
version_strings: @option.version_strings,
|
|
1470
|
+
file_version: @option.file_version,
|
|
1471
|
+
product_version: @option.product_version,
|
|
1472
|
+
resource_strings: @option.resource_strings,
|
|
1473
|
+
execution_level: @option.execution_level,
|
|
1474
|
+
application_manifest: @option.application_manifest&.to_s,
|
|
1475
|
+
}
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1298
1478
|
# Builds the small RUN_IN_EXE_DIR wrapper stub that starts the deployed
|
|
1299
1479
|
# application directly from the directory the wrapper resides in.
|
|
1300
1480
|
def build_wrapper_exe(wrapper_path)
|
|
@@ -1307,7 +1487,8 @@ module Ocran
|
|
|
1307
1487
|
gui_mode: @option.windowed?,
|
|
1308
1488
|
icon_path: @option.icon_filename,
|
|
1309
1489
|
run_in_exe_dir: true,
|
|
1310
|
-
stub_path: cosmo_stub_path
|
|
1490
|
+
stub_path: cosmo_stub_path,
|
|
1491
|
+
**pe_resource_options) do |stub|
|
|
1311
1492
|
yield(stub)
|
|
1312
1493
|
end
|
|
1313
1494
|
end
|
|
@@ -1317,7 +1498,11 @@ module Ocran
|
|
|
1317
1498
|
|
|
1318
1499
|
path = Pathname(path)
|
|
1319
1500
|
say "Building directory #{path}"
|
|
1320
|
-
builder = DirBuilder.new(path,
|
|
1501
|
+
builder = DirBuilder.new(path,
|
|
1502
|
+
script_name: @option.script.basename.sub_ext("").to_s,
|
|
1503
|
+
chdir_before: @option.chdir_before?,
|
|
1504
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1505
|
+
&to_proc)
|
|
1321
1506
|
|
|
1322
1507
|
if @option.wrapper_exe?
|
|
1323
1508
|
# Same wrapper as in Inno Setup builds: a doubleclickable executable
|
|
@@ -1381,30 +1566,41 @@ module Ocran
|
|
|
1381
1566
|
end
|
|
1382
1567
|
|
|
1383
1568
|
bundle_id = @option.bundle_identifier || "com.example.#{app_name}"
|
|
1384
|
-
|
|
1569
|
+
File.write(contents_dir / "Info.plist",
|
|
1570
|
+
self.class.info_plist(app_name, bundle_id, icon: !!@option.icon_filename))
|
|
1571
|
+
|
|
1572
|
+
say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
|
|
1573
|
+
end
|
|
1385
1574
|
|
|
1386
|
-
|
|
1575
|
+
XML_ESCAPES = { "&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'" }.freeze
|
|
1576
|
+
|
|
1577
|
+
# The Info.plist of a macOS app bundle. The values are XML-escaped: an
|
|
1578
|
+
# app named "R&D" must not produce a property list macOS cannot parse.
|
|
1579
|
+
def self.info_plist(app_name, bundle_id, icon: false)
|
|
1580
|
+
name = app_name.to_s.gsub(/[&<>"']/, XML_ESCAPES)
|
|
1581
|
+
id = bundle_id.to_s.gsub(/[&<>"']/, XML_ESCAPES)
|
|
1582
|
+
icon_entry = icon ? " <key>CFBundleIconFile</key>\n <string>AppIcon</string>\n" : ""
|
|
1583
|
+
|
|
1584
|
+
<<~PLIST
|
|
1387
1585
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1388
1586
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1389
1587
|
<plist version="1.0">
|
|
1390
1588
|
<dict>
|
|
1391
1589
|
<key>CFBundleName</key>
|
|
1392
|
-
<string>#{
|
|
1590
|
+
<string>#{name}</string>
|
|
1393
1591
|
<key>CFBundleDisplayName</key>
|
|
1394
|
-
<string>#{
|
|
1592
|
+
<string>#{name}</string>
|
|
1395
1593
|
<key>CFBundleIdentifier</key>
|
|
1396
|
-
<string>#{
|
|
1594
|
+
<string>#{id}</string>
|
|
1397
1595
|
<key>CFBundleVersion</key>
|
|
1398
1596
|
<string>1.0</string>
|
|
1399
1597
|
<key>CFBundlePackageType</key>
|
|
1400
1598
|
<string>APPL</string>
|
|
1401
1599
|
<key>CFBundleExecutable</key>
|
|
1402
|
-
<string>#{
|
|
1600
|
+
<string>#{name}</string>
|
|
1403
1601
|
#{icon_entry}</dict>
|
|
1404
1602
|
</plist>
|
|
1405
1603
|
PLIST
|
|
1406
|
-
|
|
1407
|
-
say "Finished building #{bundle_path} (#{builder.data_size} bytes decompressed)"
|
|
1408
1604
|
end
|
|
1409
1605
|
|
|
1410
1606
|
# Builds the executable by copying the cosmopolitan Ruby and injecting
|
|
@@ -1419,6 +1615,7 @@ module Ocran
|
|
|
1419
1615
|
ZipPayloadBuilder.new(output,
|
|
1420
1616
|
cosmo_ruby: @option.cosmo_ruby,
|
|
1421
1617
|
chdir_before: @option.chdir_before?,
|
|
1618
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1422
1619
|
debug_mode: @option.enable_debug_mode?,
|
|
1423
1620
|
&to_proc) => builder
|
|
1424
1621
|
|
|
@@ -1441,6 +1638,56 @@ module Ocran
|
|
|
1441
1638
|
say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
|
|
1442
1639
|
end
|
|
1443
1640
|
|
|
1641
|
+
# --static-runtime: the application appended as a ZIP archive to a statically
|
|
1642
|
+
# linked Windows Ruby (see StaticRuntimeBuilder).
|
|
1643
|
+
def build_static_runtime_exe
|
|
1644
|
+
require_relative "static_runtime_builder"
|
|
1645
|
+
|
|
1646
|
+
runtime = StaticRuntime.load(@option.static_runtime)
|
|
1647
|
+
runtime.verify!
|
|
1648
|
+
host_api = RbConfig::CONFIG["ruby_version"]
|
|
1649
|
+
# Gems, default gem specs and the host files the runtime does not
|
|
1650
|
+
# replace were resolved for the build host's Ruby; they only fit a
|
|
1651
|
+
# runtime with the same API version.
|
|
1652
|
+
unless host_api == runtime.ruby_api_version
|
|
1653
|
+
raise "The static runtime in #{runtime.dir} is Ruby #{runtime.ruby_version} (API #{runtime.ruby_api_version}), " \
|
|
1654
|
+
"but this is Ruby #{RUBY_VERSION} (API #{host_api}): package with a Ruby #{runtime.ruby_api_version.sub(/\.0\z/, "")}"
|
|
1655
|
+
end
|
|
1656
|
+
if RUBY_VERSION != runtime.ruby_version
|
|
1657
|
+
warning "Dependency detection ran under Ruby #{RUBY_VERSION}; the static runtime is Ruby #{runtime.ruby_version}"
|
|
1658
|
+
end
|
|
1659
|
+
say "Packaging for the static runtime #{runtime.executable} (Ruby #{runtime.ruby_version} #{runtime.arch})"
|
|
1660
|
+
if @option.extra_dlls.any?
|
|
1661
|
+
warning "--dll has no effect with --static-runtime: a DLL cannot be loaded from inside the executable"
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
output = @option.output_executable
|
|
1665
|
+
StaticRuntimeBuilder.new(output,
|
|
1666
|
+
runtime: runtime,
|
|
1667
|
+
host_ruby_api_version: host_api,
|
|
1668
|
+
host_arch: RbConfig::CONFIG["arch"],
|
|
1669
|
+
chdir_before: @option.chdir_before?,
|
|
1670
|
+
chdir_exe_dir: @option.chdir_exe_dir?,
|
|
1671
|
+
windowed: @option.windowed?,
|
|
1672
|
+
debug_mode: @option.enable_debug_mode?,
|
|
1673
|
+
dlls: @option.runtime_dlls,
|
|
1674
|
+
resources: { icon_path: @option.icon_filename&.to_s, **pe_resource_options },
|
|
1675
|
+
&to_proc) => builder
|
|
1676
|
+
|
|
1677
|
+
builder.skipped.each do |reason, names|
|
|
1678
|
+
verbose "Not packed (#{reason.to_s.tr("_", " ")}): #{names.size} file(s)"
|
|
1679
|
+
names.each { |name| verbose "\t#{name}" }
|
|
1680
|
+
end
|
|
1681
|
+
unless builder.skipped[:not_in_runtime_stdlib].empty?
|
|
1682
|
+
say "#{builder.skipped[:not_in_runtime_stdlib].size} file(s) of this Ruby's standard library are not " \
|
|
1683
|
+
"in the runtime's and were left out (--verbose lists them)"
|
|
1684
|
+
end
|
|
1685
|
+
say "Finished building #{output} (#{output.size} bytes, #{builder.data_size} bytes of application data)"
|
|
1686
|
+
unless builder.shipped_dlls.empty?
|
|
1687
|
+
say "Ship these DLLs next to #{output.basename}: #{builder.shipped_dlls.map { |d| d.basename.to_s }.join(", ")}"
|
|
1688
|
+
end
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1444
1691
|
def build_stab_exe
|
|
1445
1692
|
require_relative "stub_builder"
|
|
1446
1693
|
|
|
@@ -1457,6 +1704,7 @@ module Ocran
|
|
|
1457
1704
|
gui_mode: @option.windowed?,
|
|
1458
1705
|
icon_path: @option.icon_filename,
|
|
1459
1706
|
stub_path: cosmo_stub_path,
|
|
1707
|
+
**pe_resource_options,
|
|
1460
1708
|
&to_proc) => builder
|
|
1461
1709
|
say "Finished building #{@option.output_executable} (#{@option.output_executable.size} bytes)"
|
|
1462
1710
|
say "After decompression, the data will expand to #{builder.data_size} bytes."
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "fiddle/import"
|
|
3
|
+
require "fiddle/types"
|
|
4
|
+
# The language/codepage constants and the pure-Ruby blob builder live in a
|
|
5
|
+
# separate, dlload-free file so they can be unit-tested on any platform.
|
|
6
|
+
require_relative "ed_resource_builder"
|
|
7
|
+
|
|
8
|
+
module Ocran
|
|
9
|
+
# Edits PE resources in an executable, in the style of rcedit:
|
|
10
|
+
# * RT_VERSION - version info (VS_VERSIONINFO / VS_FIXEDFILEINFO / StringFileInfo)
|
|
11
|
+
# * RT_MANIFEST - application manifest / requested execution level
|
|
12
|
+
# * RT_STRING - string table entries by numeric ID
|
|
13
|
+
#
|
|
14
|
+
# This is a sibling of EdIcon and uses the same Windows resource-update API
|
|
15
|
+
# (BeginUpdateResourceW / UpdateResourceW / EndUpdateResourceW) via Fiddle.
|
|
16
|
+
module EdResource
|
|
17
|
+
extend Fiddle::Importer
|
|
18
|
+
dlload "kernel32.dll"
|
|
19
|
+
|
|
20
|
+
include Fiddle::Win32Types
|
|
21
|
+
typealias "LPVOID", "void*"
|
|
22
|
+
typealias "LPCWSTR", "char*"
|
|
23
|
+
|
|
24
|
+
MAKEINTRESOURCE = -> (i) { Fiddle::Pointer.new(i) }
|
|
25
|
+
RT_VERSION = MAKEINTRESOURCE.(16)
|
|
26
|
+
RT_STRING = MAKEINTRESOURCE.(6)
|
|
27
|
+
# CREATEPROCESS_MANIFEST_RESOURCE_ID == 1; the manifest is stored under
|
|
28
|
+
# resource type RT_MANIFEST (24), name id 1.
|
|
29
|
+
RT_MANIFEST = MAKEINTRESOURCE.(24)
|
|
30
|
+
MANIFEST_RESOURCE_ID = 1
|
|
31
|
+
|
|
32
|
+
# Language/codepage constants (MAKELANGID, LANGID, LANGID_US_ENGLISH,
|
|
33
|
+
# CODEPAGE_UNICODE, STRING_TABLE_KEY), EXECUTION_LEVELS and
|
|
34
|
+
# MANIFEST_TEMPLATE are defined in ed_resource_builder.rb, required above.
|
|
35
|
+
|
|
36
|
+
extern "DWORD GetLastError()"
|
|
37
|
+
extern "HANDLE BeginUpdateResourceW(LPCWSTR, BOOL)"
|
|
38
|
+
extern "BOOL EndUpdateResourceW(HANDLE, BOOL)"
|
|
39
|
+
extern "BOOL UpdateResourceW(HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD)"
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Perform every requested resource edit under a single
|
|
43
|
+
# BeginUpdateResource/EndUpdateResource cycle (one PE rewrite).
|
|
44
|
+
def update(executable_filename, version_strings: nil, file_version: nil,
|
|
45
|
+
product_version: nil, resource_strings: nil, manifest_xml: nil,
|
|
46
|
+
execution_level: nil)
|
|
47
|
+
wants_version = !blank?(version_strings) || file_version || product_version
|
|
48
|
+
wants_manifest = manifest_xml || execution_level
|
|
49
|
+
wants_strings = !blank?(resource_strings)
|
|
50
|
+
return unless wants_version || wants_manifest || wants_strings
|
|
51
|
+
|
|
52
|
+
update_resource(executable_filename) do |handle|
|
|
53
|
+
write_version_info(handle, version_strings || {}, file_version, product_version) if wants_version
|
|
54
|
+
write_manifest(handle, manifest_xml, execution_level) if wants_manifest
|
|
55
|
+
write_resource_strings(handle, resource_strings) if wants_strings
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def blank?(value)
|
|
62
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def write_version_info(handle, strings, file_version, product_version)
|
|
66
|
+
merged = {}
|
|
67
|
+
strings.each { |k, v| merged[k.to_s] = v }
|
|
68
|
+
merged["FileVersion"] = file_version if file_version
|
|
69
|
+
merged["ProductVersion"] = product_version if product_version
|
|
70
|
+
|
|
71
|
+
blob = Build.version_info(
|
|
72
|
+
strings: merged,
|
|
73
|
+
file_version: file_version || merged["FileVersion"] || "0.0.0.0",
|
|
74
|
+
product_version: product_version || merged["ProductVersion"] || "0.0.0.0"
|
|
75
|
+
)
|
|
76
|
+
put(handle, RT_VERSION, 1, blob)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def write_manifest(handle, manifest_xml, execution_level)
|
|
80
|
+
xml = Build.manifest(manifest_xml, execution_level)
|
|
81
|
+
put(handle, RT_MANIFEST, MANIFEST_RESOURCE_ID, xml.b)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def write_resource_strings(handle, id_to_value)
|
|
85
|
+
id_to_value.group_by { |id, _| id.to_i / 16 }.each do |bundle_index, pairs|
|
|
86
|
+
bundle = Build.string_bundle(pairs.to_h)
|
|
87
|
+
put(handle, RT_STRING, bundle_index + 1, bundle)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def put(handle, type, name, data)
|
|
92
|
+
name_arg = name.is_a?(Integer) ? MAKEINTRESOURCE.(name) : name
|
|
93
|
+
if UpdateResourceW(handle, type, name_arg, LANGID, data, data.bytesize) == 0
|
|
94
|
+
raise "Failed to UpdateResourceW(#{GetLastError()})"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def update_resource(executable_filename)
|
|
99
|
+
path = File.expand_path(executable_filename).encode("UTF-16LE").b + "\x00\x00".b
|
|
100
|
+
handle = BeginUpdateResourceW(path, 0)
|
|
101
|
+
if handle == Fiddle::NULL
|
|
102
|
+
raise "Failed to BeginUpdateResourceW(#{GetLastError()})"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
yield(handle)
|
|
106
|
+
|
|
107
|
+
if EndUpdateResourceW(handle, 0) == 0
|
|
108
|
+
raise "Failed to EndUpdateResourceW(#{GetLastError()})"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|