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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +273 -0
- data/Rakefile +97 -0
- data/api/v1.json +53 -0
- data/ext/CMakeLists.txt +128 -0
- data/ext/Rakefile +31 -0
- data/ext/backend_function_bridge.cpp +180 -0
- data/ext/backend_function_bridge.h +40 -0
- data/ext/build_cimgui.rb +59 -0
- data/ext/extconf.rb +25 -0
- data/ext/glfw_vulkan_bridge.cpp +14 -0
- data/ext/imgui_ruby_build_config.h +17 -0
- data/ext/imgui_ruby_glfw.cpp +73 -0
- data/ext/imgui_ruby_sdl3.cpp +70 -0
- data/ext/imgui_ruby_wgpu.cpp +75 -0
- data/ext/install_cimgui.rb +25 -0
- data/ext/webgpu_function_bridge.cpp +245 -0
- data/generator/api_emitter.rb +164 -0
- data/generator/emitter.rb +317 -0
- data/generator/generate.rb +47 -0
- data/generator/native-dependencies.yml +88 -0
- data/generator/native_dependency_lock.rb +78 -0
- data/generator/native_dependency_snapshot.rb +159 -0
- data/generator/overrides.yml +11 -0
- data/generator/type_mapper.rb +112 -0
- data/generator/update_vendor.rb +30 -0
- data/lib/imgui/api.rb +88 -0
- data/lib/imgui/api_generated.rb +1633 -0
- data/lib/imgui/api_support.rb +144 -0
- data/lib/imgui/backends/glfw.rb +75 -0
- data/lib/imgui/backends/opengl3.rb +44 -0
- data/lib/imgui/backends/sdl3.rb +133 -0
- data/lib/imgui/backends/wgpu.rb +176 -0
- data/lib/imgui/backends.rb +60 -0
- data/lib/imgui/draw_data.rb +43 -0
- data/lib/imgui/dsl.rb +311 -0
- data/lib/imgui/dsl_support.rb +107 -0
- data/lib/imgui/easy_loop.rb +25 -0
- data/lib/imgui/errors.rb +11 -0
- data/lib/imgui/fonts.rb +46 -0
- data/lib/imgui/io.rb +91 -0
- data/lib/imgui/layout.rb +138 -0
- data/lib/imgui/memory_pool.rb +18 -0
- data/lib/imgui/native/enums.rb +2251 -0
- data/lib/imgui/native/functions.rb +1470 -0
- data/lib/imgui/native/structs.rb +1926 -0
- data/lib/imgui/native/typedefs.rb +93 -0
- data/lib/imgui/native.rb +130 -0
- data/lib/imgui/plot/api.rb +273 -0
- data/lib/imgui/plot/native/enums.rb +593 -0
- data/lib/imgui/plot/native/functions.rb +749 -0
- data/lib/imgui/plot/native/structs.rb +363 -0
- data/lib/imgui/plot/native/typedefs.rb +79 -0
- data/lib/imgui/plot.rb +13 -0
- data/lib/imgui/struct_value.rb +33 -0
- data/lib/imgui/style.rb +44 -0
- data/lib/imgui/value.rb +104 -0
- data/lib/imgui/version.rb +5 -0
- data/lib/imgui/widgets.rb +187 -0
- data/lib/imgui.rb +30 -0
- data/rakelib/platform_gem.rake +54 -0
- data/rakelib/source_gem.rake +75 -0
- 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
|