imgui 1.0.0

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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +273 -0
  4. data/Rakefile +97 -0
  5. data/api/v1.json +53 -0
  6. data/ext/CMakeLists.txt +128 -0
  7. data/ext/Rakefile +31 -0
  8. data/ext/backend_function_bridge.cpp +180 -0
  9. data/ext/backend_function_bridge.h +40 -0
  10. data/ext/build_cimgui.rb +59 -0
  11. data/ext/extconf.rb +25 -0
  12. data/ext/glfw_vulkan_bridge.cpp +14 -0
  13. data/ext/imgui_ruby_build_config.h +17 -0
  14. data/ext/imgui_ruby_glfw.cpp +73 -0
  15. data/ext/imgui_ruby_sdl3.cpp +70 -0
  16. data/ext/imgui_ruby_wgpu.cpp +75 -0
  17. data/ext/install_cimgui.rb +25 -0
  18. data/ext/webgpu_function_bridge.cpp +245 -0
  19. data/generator/api_emitter.rb +164 -0
  20. data/generator/emitter.rb +317 -0
  21. data/generator/generate.rb +47 -0
  22. data/generator/native-dependencies.yml +88 -0
  23. data/generator/native_dependency_lock.rb +78 -0
  24. data/generator/native_dependency_snapshot.rb +159 -0
  25. data/generator/overrides.yml +11 -0
  26. data/generator/type_mapper.rb +112 -0
  27. data/generator/update_vendor.rb +30 -0
  28. data/lib/imgui/api.rb +88 -0
  29. data/lib/imgui/api_generated.rb +1633 -0
  30. data/lib/imgui/api_support.rb +144 -0
  31. data/lib/imgui/backends/glfw.rb +75 -0
  32. data/lib/imgui/backends/opengl3.rb +44 -0
  33. data/lib/imgui/backends/sdl3.rb +133 -0
  34. data/lib/imgui/backends/wgpu.rb +176 -0
  35. data/lib/imgui/backends.rb +60 -0
  36. data/lib/imgui/draw_data.rb +43 -0
  37. data/lib/imgui/dsl.rb +311 -0
  38. data/lib/imgui/dsl_support.rb +107 -0
  39. data/lib/imgui/easy_loop.rb +25 -0
  40. data/lib/imgui/errors.rb +11 -0
  41. data/lib/imgui/fonts.rb +46 -0
  42. data/lib/imgui/io.rb +91 -0
  43. data/lib/imgui/layout.rb +138 -0
  44. data/lib/imgui/memory_pool.rb +18 -0
  45. data/lib/imgui/native/enums.rb +2251 -0
  46. data/lib/imgui/native/functions.rb +1470 -0
  47. data/lib/imgui/native/structs.rb +1926 -0
  48. data/lib/imgui/native/typedefs.rb +93 -0
  49. data/lib/imgui/native.rb +130 -0
  50. data/lib/imgui/plot/api.rb +273 -0
  51. data/lib/imgui/plot/native/enums.rb +593 -0
  52. data/lib/imgui/plot/native/functions.rb +749 -0
  53. data/lib/imgui/plot/native/structs.rb +363 -0
  54. data/lib/imgui/plot/native/typedefs.rb +79 -0
  55. data/lib/imgui/plot.rb +13 -0
  56. data/lib/imgui/struct_value.rb +33 -0
  57. data/lib/imgui/style.rb +44 -0
  58. data/lib/imgui/value.rb +104 -0
  59. data/lib/imgui/version.rb +5 -0
  60. data/lib/imgui/widgets.rb +187 -0
  61. data/lib/imgui.rb +30 -0
  62. data/rakelib/platform_gem.rake +54 -0
  63. data/rakelib/source_gem.rake +75 -0
  64. metadata +129 -0
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "yaml"
6
+
7
+ require_relative "type_mapper"
8
+
9
+ module ImGuiRuby
10
+ module Generator
11
+ class Emitter
12
+ GENERATED_HEADER = <<~RUBY.freeze
13
+ # frozen_string_literal: true
14
+
15
+ # This file is generated by `rake generate`. Do not edit it manually.
16
+ RUBY
17
+
18
+ def initialize(
19
+ metadata_dir:,
20
+ output_dir:,
21
+ overrides_path:,
22
+ dependency_metadata_dirs: [],
23
+ public_module: "ImGui",
24
+ public_enum_prefix: nil,
25
+ predeclare_structs: false,
26
+ implementation_prefixes: nil
27
+ )
28
+ @metadata_dir = metadata_dir
29
+ @output_dir = output_dir
30
+ @public_module = public_module
31
+ @public_enum_prefix = public_enum_prefix
32
+ @predeclare_structs = predeclare_structs
33
+ @overrides = YAML.safe_load_file(overrides_path, aliases: false) || {}
34
+ @definitions = read_json("definitions.json")
35
+ implementation_path = File.join(@metadata_dir, "impl_definitions.json")
36
+ if File.file?(implementation_path)
37
+ implementations = JSON.parse(File.read(implementation_path))
38
+ if implementation_prefixes
39
+ implementations.select! do |name, _definitions|
40
+ implementation_prefixes.any? { |prefix| name.start_with?(prefix) }
41
+ end
42
+ end
43
+ @definitions = @definitions.merge(implementations)
44
+ end
45
+ @types = read_json("structs_and_enums.json")
46
+ @typedefs = read_json("typedefs_dict.json")
47
+ @structs = @types.fetch("structs")
48
+ vector_layout = @types.fetch("templated_structs", {})["ImVector"]
49
+ @structs["ImVector"] = vector_layout if vector_layout
50
+ @enums = @types.fetch("enums")
51
+ @early_struct_names = @predeclare_structs ? callback_value_struct_names : []
52
+ dependency_structs, dependency_enums, dependency_typedefs = dependency_types(dependency_metadata_dirs)
53
+ @mapper = TypeMapper.new(
54
+ typedefs: dependency_typedefs.merge(@typedefs),
55
+ structs: dependency_structs.merge(@structs),
56
+ enum_types: (dependency_enums.keys + @enums.keys).map { |name| name.delete_suffix("_") },
57
+ struct_references: dependency_structs.reject { |name, _members| @structs.key?(name) }.to_h do |name, _members|
58
+ [name, "ImGui::Native::#{name}"]
59
+ end
60
+ )
61
+ end
62
+
63
+ def generate!
64
+ FileUtils.mkdir_p(@output_dir)
65
+ write("typedefs.rb", emit_typedefs)
66
+ write("enums.rb", emit_enums)
67
+ write("structs.rb", emit_structs)
68
+ write("functions.rb", emit_functions)
69
+ end
70
+
71
+ private
72
+
73
+ def emit_typedefs
74
+ declarations = @typedefs.filter_map do |name, c_type|
75
+ next if @structs.key?(name) || c_type.start_with?("struct ")
76
+
77
+ callback = callback_signature(c_type)
78
+ if callback
79
+ arguments = callback.fetch(:arguments).map { |argument| @mapper.ffi_type(argument) }
80
+ " callback :#{name}, [#{arguments.join(", ")}], #{@mapper.ffi_type(callback.fetch(:result))}"
81
+ else
82
+ " typedef #{@mapper.typedef_type(c_type)}, :#{name}"
83
+ end
84
+ end
85
+
86
+ predeclarations = if @predeclare_structs
87
+ @structs.keys.sort.map { |name| " class #{name} < FFI::Struct; end" }
88
+ else
89
+ []
90
+ end
91
+ early_layouts = @early_struct_names.filter_map do |name|
92
+ format_struct_layout(name, @structs.fetch(name))
93
+ end
94
+ wrap_native((predeclarations + early_layouts + declarations).join("\n"))
95
+ end
96
+
97
+ def emit_enums
98
+ native_constants = []
99
+ public_modules = []
100
+
101
+ @enums.sort.each do |enum_name, values|
102
+ values.each do |value|
103
+ native_constants << " #{value.fetch("name")} = #{value.fetch("calc_value")}"
104
+ end
105
+
106
+ public_name = public_enum_name(enum_name)
107
+ prefix = enum_name.end_with?("_") ? enum_name : "#{enum_name}_"
108
+ constants = values.map do |value|
109
+ name = ruby_constant_name(value.fetch("name").delete_prefix(prefix))
110
+ " #{name} = #{native_constant_reference}::#{value.fetch("name")}"
111
+ end
112
+ public_modules << " module #{public_name}\n#{constants.join("\n")}\n end"
113
+ end
114
+
115
+ return core_enum_module(native_constants, public_modules) if @public_module == "ImGui"
116
+
117
+ <<~RUBY
118
+ #{GENERATED_HEADER}
119
+
120
+ module ImGui
121
+ module Native
122
+ #{native_constants.join("\n")}
123
+ end
124
+ end
125
+
126
+ module #{@public_module}
127
+ #{public_modules.join("\n\n")}
128
+ end
129
+ RUBY
130
+ end
131
+
132
+ def callback_signature(c_type)
133
+ match = c_type.match(/\A(.+?)\s*\(\*\)\((.*)\);?\z/)
134
+ return unless match
135
+
136
+ arguments = match[2] == "void" || match[2].empty? ? [] : match[2].split(",").map do |argument|
137
+ argument.strip.sub(/\s+[a-zA-Z_]\w*\z/, "")
138
+ end
139
+ { result: match[1].strip, arguments: arguments }
140
+ end
141
+
142
+ def core_enum_module(native_constants, public_modules)
143
+ <<~RUBY
144
+ #{GENERATED_HEADER}
145
+
146
+ module ImGui
147
+ module Native
148
+ #{native_constants.join("\n")}
149
+ end
150
+
151
+ #{public_modules.join("\n\n")}
152
+ end
153
+ RUBY
154
+ end
155
+
156
+ def emit_structs
157
+ declarations = @structs.keys.sort.map { |name| " class #{name} < FFI::Struct; end" }
158
+ layouts = ordered_structs.filter_map do |name, members|
159
+ next if @early_struct_names.include?(name)
160
+
161
+ format_struct_layout(name, members)
162
+ end
163
+
164
+ wrap_native((declarations + [""] + layouts).join("\n"))
165
+ end
166
+
167
+ def emit_functions
168
+ skipped = Array(@overrides["skip"])
169
+ renames = @overrides.fetch("rename", {})
170
+ declarations = @definitions.values.flatten.filter_map do |definition|
171
+ name = definition["ov_cimguiname"] || definition.fetch("cimguiname")
172
+ next if definition["skipped"] || definition["templated"] || definition["isvararg"] || skipped.include?(name)
173
+
174
+ arguments = Array(definition["argsT"]).map do |argument|
175
+ @mapper.ffi_type(argument.fetch("type"))
176
+ end
177
+ return_type = definition.fetch("ret") do
178
+ definition["constructor"] ? "#{definition.fetch("stname")}*" : "void"
179
+ end
180
+ result = @mapper.ffi_type(return_type)
181
+ ruby_name = renames.fetch(name, name)
182
+ if ruby_name == name
183
+ " attach_function :#{name}, [#{arguments.join(", ")}], #{result}"
184
+ else
185
+ " attach_function :#{ruby_name}, :#{name}, [#{arguments.join(", ")}], #{result}"
186
+ end
187
+ end
188
+
189
+ wrap_native(declarations.sort.join("\n"))
190
+ end
191
+
192
+ def public_enum_name(enum_name)
193
+ name = enum_name.delete_suffix("_")
194
+ return name.delete_prefix(@public_enum_prefix) if @public_enum_prefix
195
+
196
+ name.sub(/\AImGui/, "").sub(/\AIm/, "")
197
+ end
198
+
199
+ def native_constant_reference
200
+ @public_module == "ImGui" ? "Native" : "ImGui::Native"
201
+ end
202
+
203
+ def ruby_constant_name(name)
204
+ clean_name = name.sub(/\A_+/, "").gsub(/[^a-zA-Z0-9_]/, "_")
205
+ clean_name = "Num#{clean_name}" if clean_name.match?(/\A\d/)
206
+ clean_name.empty? ? "Value" : clean_name
207
+ end
208
+
209
+ def struct_fields(members)
210
+ anonymous_index = 0
211
+ previous_bitfield_type = nil
212
+
213
+ members.flat_map do |member|
214
+ if member["bitfield"]
215
+ type = @mapper.ffi_type(member.fetch("type"))
216
+ next [] if type == previous_bitfield_type
217
+
218
+ previous_bitfield_type = type
219
+ anonymous_index += 1
220
+ next ["_bitfield_#{anonymous_index}".inspect, type]
221
+ end
222
+
223
+ previous_bitfield_type = nil
224
+ name = member.fetch("name")
225
+ if name.empty?
226
+ anonymous_index += 1
227
+ name = "_anonymous_#{anonymous_index}"
228
+ end
229
+ [name.inspect, @mapper.ffi_type(member.fetch("type"), member: member)]
230
+ end
231
+ end
232
+
233
+ def format_struct_layout(name, members)
234
+ fields = struct_fields(members)
235
+ return if fields.empty?
236
+
237
+ formatted = fields.each_slice(2).map { |field, type| " #{field}, #{type}" }.join(",\n")
238
+ " #{name}.layout(\n#{formatted}\n )"
239
+ end
240
+
241
+ def callback_value_struct_names
242
+ @typedefs.values.filter_map do |c_type|
243
+ signature = callback_signature(c_type)
244
+ next unless signature
245
+
246
+ types = [signature.fetch(:result), *signature.fetch(:arguments)]
247
+ types.find do |type|
248
+ bare = type.gsub(/\bconst\b|\bstruct\b/, "").strip
249
+ @structs.key?(bare)
250
+ end&.then do |type|
251
+ type.gsub(/\bconst\b|\bstruct\b/, "").strip
252
+ end
253
+ end.uniq.sort
254
+ end
255
+
256
+ def ordered_structs
257
+ ordered = []
258
+ state = {}
259
+ visit = lambda do |name|
260
+ return if state[name] == :done
261
+ return if state[name] == :visiting
262
+
263
+ state[name] = :visiting
264
+ @structs.fetch(name).each do |member|
265
+ dependency = by_value_struct_name(member.fetch("type"))
266
+ visit.call(dependency) if dependency
267
+ end
268
+ state[name] = :done
269
+ ordered << [name, @structs.fetch(name)]
270
+ end
271
+ @structs.keys.sort.each { |name| visit.call(name) }
272
+ ordered
273
+ end
274
+
275
+ def by_value_struct_name(c_type)
276
+ type = c_type.gsub(/\bconst\b|\bstruct\b/, "").gsub(/\[[^\]]+\]/, "").strip
277
+ return if type.include?("*") || type.include?("&")
278
+
279
+ return type if @structs.key?(type)
280
+
281
+ template_base = type.split("_").first
282
+ @structs.key?(template_base) ? template_base : nil
283
+ end
284
+
285
+ def wrap_native(body)
286
+ <<~RUBY
287
+ #{GENERATED_HEADER}
288
+
289
+ module ImGui
290
+ module Native
291
+ #{body}
292
+ end
293
+ end
294
+ RUBY
295
+ end
296
+
297
+ def read_json(filename)
298
+ JSON.parse(File.read(File.join(@metadata_dir, filename)))
299
+ end
300
+
301
+ def write(filename, content)
302
+ File.write(File.join(@output_dir, filename), content)
303
+ end
304
+
305
+ def dependency_types(metadata_dirs)
306
+ metadata_dirs.each_with_object([{}, {}, {}]) do |directory, (structs, enums, typedefs)|
307
+ types = JSON.parse(File.read(File.join(directory, "structs_and_enums.json")))
308
+ structs.merge!(types.fetch("structs"))
309
+ vector_layout = types.fetch("templated_structs", {})["ImVector"]
310
+ structs["ImVector"] = vector_layout if vector_layout
311
+ enums.merge!(types.fetch("enums"))
312
+ typedefs.merge!(JSON.parse(File.read(File.join(directory, "typedefs_dict.json"))))
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "optparse"
5
+
6
+ require_relative "emitter"
7
+ require_relative "api_emitter"
8
+
9
+ root = File.expand_path("..", __dir__)
10
+ options = {
11
+ metadata: File.join(root, "tmp", "vendor", "cimgui", "generator", "output"),
12
+ output: File.join(root, "lib", "imgui", "native"),
13
+ overrides: File.join(__dir__, "overrides.yml")
14
+ }
15
+
16
+ OptionParser.new do |parser|
17
+ parser.banner = "Usage: ruby generator/generate.rb [options]"
18
+ parser.on("--metadata DIR", "cimgui generator/output directory") { |value| options[:metadata] = value }
19
+ parser.on("--output DIR", "generated Ruby output directory") { |value| options[:output] = value }
20
+ parser.on("--overrides FILE", "generation overrides YAML") { |value| options[:overrides] = value }
21
+ end.parse!
22
+
23
+ ImGuiRuby::Generator::Emitter.new(
24
+ metadata_dir: options.fetch(:metadata),
25
+ output_dir: options.fetch(:output),
26
+ overrides_path: options.fetch(:overrides),
27
+ implementation_prefixes: %w[ImGui_ImplGlfw_ ImGui_ImplOpenGL3_ ImGui_ImplSDL3_]
28
+ ).generate!
29
+
30
+ ImGuiRuby::Generator::ApiEmitter.new(
31
+ metadata_dir: options.fetch(:metadata),
32
+ output_path: File.join(root, "lib", "imgui", "api_generated.rb"),
33
+ overrides_path: options.fetch(:overrides)
34
+ ).generate!
35
+
36
+ plot_metadata = File.join(root, "tmp", "vendor", "cimplot", "generator", "output")
37
+ if File.directory?(plot_metadata)
38
+ ImGuiRuby::Generator::Emitter.new(
39
+ metadata_dir: plot_metadata,
40
+ output_dir: File.join(root, "lib", "imgui", "plot", "native"),
41
+ overrides_path: options.fetch(:overrides),
42
+ dependency_metadata_dirs: [options.fetch(:metadata)],
43
+ public_module: "ImPlot",
44
+ public_enum_prefix: "ImPlot",
45
+ predeclare_structs: true
46
+ ).generate!
47
+ end
@@ -0,0 +1,88 @@
1
+ ---
2
+ schema: 1
3
+ sources:
4
+ cimgui:
5
+ repository: https://github.com/cimgui/cimgui.git
6
+ ref: docking_inter
7
+ revision: 140d107be52cf2cfe66aa2cb859fd8e07f9f6bf5
8
+ checksum: sha256:3a63513f59a355493c1faaa77667ac4f513754b9a622aa15b25ebd524c4d3e54
9
+ destination: cimgui
10
+ files:
11
+ - LICENSE
12
+ - cimconfig.h
13
+ - cimgui.cpp
14
+ - cimgui.h
15
+ - cimgui_impl.cpp
16
+ - cimgui_impl.h
17
+ - generator/output/definitions.json
18
+ - generator/output/impl_definitions.json
19
+ - generator/output/structs_and_enums.json
20
+ - generator/output/typedefs_dict.json
21
+ imgui:
22
+ repository: https://github.com/ocornut/imgui.git
23
+ revision: 126d004f9e1eef062bf4b044b3b2faaf58d48c51
24
+ checksum: sha256:dd1328bdf2f47e9f664a73996fbba2d85b23b926641964a2553b08a8e00cb30b
25
+ parent:
26
+ source: cimgui
27
+ path: imgui
28
+ destination: cimgui/imgui
29
+ files:
30
+ - LICENSE.txt
31
+ - "*.cpp"
32
+ - "*.h"
33
+ - backends/imgui_impl_glfw.cpp
34
+ - backends/imgui_impl_glfw.h
35
+ - backends/imgui_impl_opengl3.cpp
36
+ - backends/imgui_impl_opengl3.h
37
+ - backends/imgui_impl_opengl3_loader.h
38
+ - backends/imgui_impl_sdl3.cpp
39
+ - backends/imgui_impl_sdl3.h
40
+ - backends/imgui_impl_wgpu.cpp
41
+ - backends/imgui_impl_wgpu.h
42
+ - examples/libs/glfw/include/GLFW/*.h
43
+ cimplot:
44
+ repository: https://github.com/cimgui/cimplot.git
45
+ ref: master
46
+ revision: 381ed68cc0688f16d027d67edc3bc060afa4dde9
47
+ checksum: sha256:50bdbdef021bf080d189c14a11916c6c068fafeab661d780bf422a56a610faa0
48
+ destination: cimplot
49
+ files:
50
+ - LICENSE
51
+ - cimplot.cpp
52
+ - cimplot.h
53
+ - generator/output/definitions.json
54
+ - generator/output/structs_and_enums.json
55
+ - generator/output/typedefs_dict.json
56
+ implot:
57
+ repository: https://github.com/epezent/implot.git
58
+ revision: 47522f47054d33178e7defa780042bd2a06b09f9
59
+ checksum: sha256:88d83afafceb919b4171b3308716188d34116b32811f6eba0e9df7d9cfad42dc
60
+ parent:
61
+ source: cimplot
62
+ path: implot
63
+ destination: cimplot/implot
64
+ files:
65
+ - LICENSE
66
+ - implot.cpp
67
+ - implot_demo.cpp
68
+ - implot.h
69
+ - implot_internal.h
70
+ - implot_items.cpp
71
+ sdl:
72
+ repository: https://github.com/libsdl-org/SDL.git
73
+ ref: release-3.2.x
74
+ revision: f6864924f76e1a0b4abaefc76ae2ed22b1a8916e
75
+ checksum: sha256:843dafeeb13f7ce22130cf234a6ca48d40a43bb19d7f56851419fc9aee1c8165
76
+ destination: sdl
77
+ files:
78
+ - LICENSE.txt
79
+ - include/SDL3/*.h
80
+ webgpu:
81
+ repository: https://github.com/webgpu-native/webgpu-headers.git
82
+ ref: main
83
+ revision: bac520839ff5ed2e2b648ed540bd9ec45edbccbc
84
+ checksum: sha256:22f9cfe67c2da486351eea6f0504a4aa80e3b17c1d42ea7b0929eb2247b8b81b
85
+ destination: webgpu
86
+ files:
87
+ - LICENSE
88
+ - webgpu.h
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "yaml"
5
+
6
+ module ImGuiRuby
7
+ class NativeDependencyLock
8
+ class VerificationError < StandardError; end
9
+
10
+ REVISION_PATTERN = /\A[0-9a-f]{40}\z/
11
+ CHECKSUM_PATTERN = /\Asha256:[0-9a-f]{64}\z/
12
+
13
+ attr_reader :sources
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ @manifest = YAML.safe_load_file(path, aliases: false)
18
+ @sources = @manifest.fetch("sources")
19
+ end
20
+
21
+ def validate!
22
+ raise VerificationError, "unsupported manifest" unless @manifest["schema"] == 1
23
+
24
+ @sources.each do |name, source|
25
+ raise VerificationError, "#{name}: invalid revision" unless REVISION_PATTERN.match?(source.fetch("revision"))
26
+ raise VerificationError, "#{name}: invalid checksum" unless CHECKSUM_PATTERN.match?(source.fetch("checksum"))
27
+ end
28
+ end
29
+
30
+ def files(name, source, root)
31
+ base = File.join(root, source.fetch("destination"))
32
+ source.fetch("files").flat_map do |pattern|
33
+ matches = Dir.glob(File.join(base, pattern)).select { |path| File.file?(path) }
34
+ raise VerificationError, "#{name}: #{pattern} did not match a file" if matches.empty?
35
+
36
+ matches
37
+ end
38
+ end
39
+
40
+ def verify_checksum!(name, source, root)
41
+ return if source.fetch("checksum") == checksum(name, source, root)
42
+
43
+ raise VerificationError, "#{name}: checksum mismatch"
44
+ end
45
+
46
+ def checksums(root)
47
+ @sources.to_h { |name, source| [name, checksum(name, source, root)] }
48
+ end
49
+
50
+ def verify_checksums!(checksums)
51
+ @sources.each do |name, source|
52
+ next if source.fetch("checksum") == checksums.fetch(name)
53
+
54
+ raise VerificationError, "#{name}: downloaded content checksum mismatch"
55
+ end
56
+ end
57
+
58
+ def update!(resolved, checksums)
59
+ resolved.each do |name, data|
60
+ source = @sources.fetch(name)
61
+ source["revision"] = data.fetch(:revision)
62
+ source["checksum"] = checksums.fetch(name)
63
+ end
64
+ File.write(@path, YAML.dump(@manifest))
65
+ end
66
+
67
+ private
68
+
69
+ def checksum(name, source, root)
70
+ base = File.join(root, source.fetch("destination"))
71
+ digest = Digest::SHA256.new
72
+ files(name, source, root).sort.each do |path|
73
+ digest << path.delete_prefix("#{base}/") << "\0" << File.binread(path) << "\0"
74
+ end
75
+ "sha256:#{digest.hexdigest}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+ require "tmpdir"
6
+
7
+ require_relative "native_dependency_lock"
8
+
9
+ module ImGuiRuby
10
+ class NativeDependencySnapshot
11
+ ROOT = File.expand_path("..", __dir__)
12
+ MANIFEST_PATH = File.join(__dir__, "native-dependencies.yml")
13
+ CACHE_ROOT = File.join(ROOT, "tmp", "vendor")
14
+ REVISION_PATTERN = NativeDependencyLock::REVISION_PATTERN
15
+ VerificationError = NativeDependencyLock::VerificationError
16
+
17
+ def initialize
18
+ @lock = NativeDependencyLock.new(MANIFEST_PATH)
19
+ @sources = @lock.sources
20
+ end
21
+
22
+ def fetch!
23
+ unless File.directory?(CACHE_ROOT)
24
+ warn "Native dependency cache is missing; fetching pinned revisions."
25
+ return refresh!
26
+ end
27
+
28
+ verify!
29
+ rescue VerificationError => error
30
+ warn "Native dependency cache is unavailable: #{error.message}"
31
+ refresh!
32
+ end
33
+
34
+ def verify!
35
+ @lock.validate!
36
+ expected_files = @sources.flat_map do |name, source|
37
+ @lock.verify_checksum!(name, source, CACHE_ROOT)
38
+ @lock.files(name, source, CACHE_ROOT)
39
+ end.uniq
40
+ actual_files = all_files(CACHE_ROOT)
41
+ unexpected = actual_files - expected_files
42
+ raise VerificationError, "unexpected files:\n#{relative_list(unexpected)}" unless unexpected.empty?
43
+
44
+ puts "Verified #{@sources.length} native dependency snapshots (#{actual_files.length} files)."
45
+ end
46
+
47
+ def refresh!(latest: false)
48
+ resolved = {}
49
+ FileUtils.mkdir_p(File.dirname(CACHE_ROOT))
50
+
51
+ Dir.mktmpdir("native-dependencies-", File.dirname(CACHE_ROOT)) do |temporary_root|
52
+ snapshot_root = File.join(temporary_root, "vendor")
53
+ checkout_root = File.join(temporary_root, "checkouts")
54
+ populate_snapshot!(snapshot_root, checkout_root, resolved, latest:)
55
+ checksums = @lock.checksums(snapshot_root)
56
+ @lock.verify_checksums!(checksums) unless latest
57
+ replace_snapshot!(snapshot_root)
58
+ @lock.update!(resolved, checksums) if latest
59
+ end
60
+
61
+ verify!
62
+ end
63
+
64
+ def checkout_source!(name, destination)
65
+ source = @sources.fetch(name) { raise "unknown native dependency: #{name}" }
66
+ path = File.expand_path(destination, ROOT)
67
+ raise "checkout destination already exists: #{path}" if File.exist?(path)
68
+
69
+ checkout!(source.fetch("repository"), source.fetch("revision"), path)
70
+ puts "Checked out #{name} at #{source.fetch('revision')} to #{relative(path)}."
71
+ end
72
+
73
+ private
74
+
75
+ def populate_snapshot!(snapshot_root, checkout_root, resolved, latest:)
76
+ @sources.each do |name, source|
77
+ revision = revision_for(name, source, resolved, latest:)
78
+ checkout = File.join(checkout_root, name)
79
+ checkout!(source.fetch("repository"), revision, checkout)
80
+ resolved[name] = { revision:, checkout: }
81
+ copy_files!(name, source, checkout, snapshot_root)
82
+ end
83
+ end
84
+
85
+ def revision_for(name, source, resolved, latest:)
86
+ return source.fetch("revision") unless latest
87
+ return remote_revision!(source) unless source["parent"]
88
+
89
+ parent = source.fetch("parent")
90
+ checkout = resolved.fetch(parent.fetch("source")).fetch(:checkout)
91
+ revision = run("git", "-C", checkout, "rev-parse", "HEAD:#{parent.fetch('path')}").strip
92
+ raise "#{name}: parent did not resolve to a commit" unless REVISION_PATTERN.match?(revision)
93
+
94
+ revision
95
+ end
96
+
97
+ def remote_revision!(source)
98
+ ref = source.fetch("ref")
99
+ output = run("git", "ls-remote", source.fetch("repository"), "refs/heads/#{ref}", "refs/tags/#{ref}")
100
+ revision = output.lines.first&.split&.first
101
+ raise "could not resolve #{source.fetch('repository')} #{ref}" unless REVISION_PATTERN.match?(revision)
102
+
103
+ revision
104
+ end
105
+
106
+ def checkout!(repository, revision, destination)
107
+ FileUtils.mkdir_p(destination)
108
+ run("git", "-C", destination, "init", "--quiet")
109
+ run("git", "-C", destination, "config", "core.autocrlf", "false")
110
+ run("git", "-C", destination, "config", "core.eol", "lf")
111
+ run("git", "-C", destination, "remote", "add", "origin", repository)
112
+ run("git", "-C", destination, "fetch", "--quiet", "--depth", "1", "origin", revision)
113
+ run("git", "-C", destination, "checkout", "--quiet", "--detach", "FETCH_HEAD")
114
+ end
115
+
116
+ def copy_files!(name, source, checkout, snapshot_root)
117
+ source.fetch("files").each do |pattern|
118
+ matches = Dir.glob(File.join(checkout, pattern)).select { |path| File.file?(path) }
119
+ raise "#{name}: #{pattern} did not match upstream" if matches.empty?
120
+
121
+ matches.each do |path|
122
+ destination = File.join(snapshot_root, source.fetch("destination"), path.delete_prefix("#{checkout}/"))
123
+ FileUtils.mkdir_p(File.dirname(destination))
124
+ FileUtils.cp(path, destination)
125
+ end
126
+ end
127
+ end
128
+
129
+ def replace_snapshot!(snapshot_root)
130
+ previous_root = "#{CACHE_ROOT}.previous"
131
+ FileUtils.rm_rf(previous_root)
132
+ FileUtils.mv(CACHE_ROOT, previous_root) if File.exist?(CACHE_ROOT)
133
+ FileUtils.mv(snapshot_root, CACHE_ROOT)
134
+ FileUtils.rm_rf(previous_root)
135
+ rescue StandardError
136
+ FileUtils.mv(previous_root, CACHE_ROOT) if File.exist?(previous_root) && !File.exist?(CACHE_ROOT)
137
+ raise
138
+ end
139
+
140
+ def all_files(root)
141
+ Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH).select { |path| File.file?(path) }
142
+ end
143
+
144
+ def run(*command)
145
+ output, error, status = Open3.capture3(*command, chdir: ROOT)
146
+ return output if status.success?
147
+
148
+ raise "command failed (#{command.join(' ')}):\n#{error}"
149
+ end
150
+
151
+ def relative(path)
152
+ path.delete_prefix("#{ROOT}/")
153
+ end
154
+
155
+ def relative_list(paths)
156
+ paths.map { |path| relative(path) }.sort.join("\n")
157
+ end
158
+ end
159
+ end