ruby-bindgen 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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +25 -0
  4. data/README.md +68 -0
  5. data/Rakefile +8 -0
  6. data/bin/ruby-bindgen +133 -0
  7. data/docs/architecture.md +238 -0
  8. data/docs/c/c_bindings.md +65 -0
  9. data/docs/c/constants.md +21 -0
  10. data/docs/c/customizing.md +19 -0
  11. data/docs/c/filtering.md +24 -0
  12. data/docs/c/getting_started.md +56 -0
  13. data/docs/c/library_loading.md +53 -0
  14. data/docs/c/output.md +96 -0
  15. data/docs/c/types.md +61 -0
  16. data/docs/c/version_guards.md +105 -0
  17. data/docs/cmake/cmake_bindings.md +19 -0
  18. data/docs/cmake/filtering.md +26 -0
  19. data/docs/cmake/getting_started.md +52 -0
  20. data/docs/cmake/output.md +110 -0
  21. data/docs/configuration.md +351 -0
  22. data/docs/contributing.md +68 -0
  23. data/docs/cpp/buffers.md +24 -0
  24. data/docs/cpp/classes.md +139 -0
  25. data/docs/cpp/cpp_bindings.md +29 -0
  26. data/docs/cpp/customizing.md +124 -0
  27. data/docs/cpp/enums.md +42 -0
  28. data/docs/cpp/filtering.md +35 -0
  29. data/docs/cpp/getting_started.md +80 -0
  30. data/docs/cpp/iterators.md +94 -0
  31. data/docs/cpp/operators.md +170 -0
  32. data/docs/cpp/output.md +125 -0
  33. data/docs/cpp/templates.md +114 -0
  34. data/docs/examples.md +133 -0
  35. data/docs/index.md +133 -0
  36. data/docs/prior_art.md +37 -0
  37. data/docs/troubleshooting.md +243 -0
  38. data/docs/type_spelling.md +200 -0
  39. data/docs/updating_bindings.md +55 -0
  40. data/docs/version_guards.md +69 -0
  41. data/lib/ruby-bindgen/config.rb +63 -0
  42. data/lib/ruby-bindgen/generators/cmake/cmake.rb +171 -0
  43. data/lib/ruby-bindgen/generators/cmake/directory.erb +29 -0
  44. data/lib/ruby-bindgen/generators/cmake/guard.rb +55 -0
  45. data/lib/ruby-bindgen/generators/cmake/presets.erb +232 -0
  46. data/lib/ruby-bindgen/generators/cmake/project.erb +89 -0
  47. data/lib/ruby-bindgen/generators/ffi/callback.erb +1 -0
  48. data/lib/ruby-bindgen/generators/ffi/constant.erb +1 -0
  49. data/lib/ruby-bindgen/generators/ffi/enum_constant_decl.erb +1 -0
  50. data/lib/ruby-bindgen/generators/ffi/enum_decl.erb +4 -0
  51. data/lib/ruby-bindgen/generators/ffi/enum_decl_anonymous.erb +4 -0
  52. data/lib/ruby-bindgen/generators/ffi/ffi.rb +687 -0
  53. data/lib/ruby-bindgen/generators/ffi/field_decl.erb +1 -0
  54. data/lib/ruby-bindgen/generators/ffi/function.erb +5 -0
  55. data/lib/ruby-bindgen/generators/ffi/library.erb +39 -0
  56. data/lib/ruby-bindgen/generators/ffi/project.erb +18 -0
  57. data/lib/ruby-bindgen/generators/ffi/struct.erb +6 -0
  58. data/lib/ruby-bindgen/generators/ffi/translation_unit.erb +9 -0
  59. data/lib/ruby-bindgen/generators/ffi/typedef_decl.erb +1 -0
  60. data/lib/ruby-bindgen/generators/ffi/union.erb +6 -0
  61. data/lib/ruby-bindgen/generators/ffi/variable.erb +1 -0
  62. data/lib/ruby-bindgen/generators/ffi/version.erb +9 -0
  63. data/lib/ruby-bindgen/generators/ffi/version_method.erb +5 -0
  64. data/lib/ruby-bindgen/generators/generator.rb +52 -0
  65. data/lib/ruby-bindgen/generators/rice/auto_generated_base_class.erb +5 -0
  66. data/lib/ruby-bindgen/generators/rice/class.erb +31 -0
  67. data/lib/ruby-bindgen/generators/rice/class_template.erb +9 -0
  68. data/lib/ruby-bindgen/generators/rice/class_template_specialization.erb +10 -0
  69. data/lib/ruby-bindgen/generators/rice/constant.erb +9 -0
  70. data/lib/ruby-bindgen/generators/rice/constructor.erb +1 -0
  71. data/lib/ruby-bindgen/generators/rice/conversion_function.erb +4 -0
  72. data/lib/ruby-bindgen/generators/rice/cxx_iterator_method.erb +1 -0
  73. data/lib/ruby-bindgen/generators/rice/cxx_method.erb +6 -0
  74. data/lib/ruby-bindgen/generators/rice/enum_constant_decl.erb +7 -0
  75. data/lib/ruby-bindgen/generators/rice/enum_decl.erb +6 -0
  76. data/lib/ruby-bindgen/generators/rice/field_decl.erb +8 -0
  77. data/lib/ruby-bindgen/generators/rice/function.erb +6 -0
  78. data/lib/ruby-bindgen/generators/rice/function_pointer.rb +68 -0
  79. data/lib/ruby-bindgen/generators/rice/incomplete_class.erb +3 -0
  80. data/lib/ruby-bindgen/generators/rice/iterator_alias.erb +1 -0
  81. data/lib/ruby-bindgen/generators/rice/iterator_collector.rb +159 -0
  82. data/lib/ruby-bindgen/generators/rice/namespace.erb +5 -0
  83. data/lib/ruby-bindgen/generators/rice/non_member_operator_binary.erb +4 -0
  84. data/lib/ruby-bindgen/generators/rice/non_member_operator_inspect.erb +6 -0
  85. data/lib/ruby-bindgen/generators/rice/non_member_operator_unary.erb +4 -0
  86. data/lib/ruby-bindgen/generators/rice/operator[].erb +4 -0
  87. data/lib/ruby-bindgen/generators/rice/project.cpp.erb +18 -0
  88. data/lib/ruby-bindgen/generators/rice/project.hpp.erb +13 -0
  89. data/lib/ruby-bindgen/generators/rice/reference_qualifier.rb +495 -0
  90. data/lib/ruby-bindgen/generators/rice/rice.rb +1724 -0
  91. data/lib/ruby-bindgen/generators/rice/rice_include.hpp.erb +7 -0
  92. data/lib/ruby-bindgen/generators/rice/signature_builder.rb +230 -0
  93. data/lib/ruby-bindgen/generators/rice/template_resolver.rb +585 -0
  94. data/lib/ruby-bindgen/generators/rice/translation_unit.cpp.erb +40 -0
  95. data/lib/ruby-bindgen/generators/rice/translation_unit.hpp.erb +7 -0
  96. data/lib/ruby-bindgen/generators/rice/translation_unit.ipp.erb +3 -0
  97. data/lib/ruby-bindgen/generators/rice/type_index.rb +117 -0
  98. data/lib/ruby-bindgen/generators/rice/type_speller.rb +509 -0
  99. data/lib/ruby-bindgen/generators/rice/union.erb +5 -0
  100. data/lib/ruby-bindgen/generators/rice/variable.erb +5 -0
  101. data/lib/ruby-bindgen/inputter.rb +54 -0
  102. data/lib/ruby-bindgen/name_mapper.rb +65 -0
  103. data/lib/ruby-bindgen/namer.rb +138 -0
  104. data/lib/ruby-bindgen/outputter.rb +40 -0
  105. data/lib/ruby-bindgen/parser.rb +82 -0
  106. data/lib/ruby-bindgen/refinements/cursor.rb +57 -0
  107. data/lib/ruby-bindgen/refinements/string.rb +41 -0
  108. data/lib/ruby-bindgen/symbol_candidates.rb +282 -0
  109. data/lib/ruby-bindgen/symbol_entry.rb +21 -0
  110. data/lib/ruby-bindgen/symbols.rb +107 -0
  111. data/lib/ruby-bindgen/type_pointer_formatter.rb +35 -0
  112. data/lib/ruby-bindgen/version.rb +3 -0
  113. data/lib/ruby-bindgen.rb +19 -0
  114. data/ruby-bindgen.gemspec +52 -0
  115. metadata +260 -0
@@ -0,0 +1,200 @@
1
+ # Type Spelling in `ruby-bindgen`
2
+
3
+ This document explains why generating correct, fully-qualified C++ type spellings from libclang is inherently complex, and how `ruby-bindgen` addresses that complexity.
4
+
5
+ ## TL;DR
6
+
7
+ - There is no single libclang API that always returns the right C++ spelling for code generation.
8
+ - Canonical types are often semantically correct but syntactically wrong for generated bindings.
9
+ - `ruby-bindgen` reconstructs type spellings based on cursor kind and context, with canonicalization only as a fallback.
10
+
11
+ The objective is **not** to compute a canonical or normalized type. The objective is to emit a C++ spelling that:
12
+
13
+ 1. Compiles correctly at the binding site
14
+ 2. Preserves user-facing API intent (typedefs, aliases, dependent names)
15
+ 3. Is stable across compilers and standard library implementations
16
+
17
+ ## The Problem
18
+
19
+ When generating Rice bindings, `ruby-bindgen` must emit fully-qualified C++ type names that are valid in generated code.
20
+
21
+ For example:
22
+
23
+ ```cpp
24
+ // Required
25
+ Constructor<cv::Mat, const cv::Range&, const cv::Range&>
26
+
27
+ // Incorrect (missing namespace qualification)
28
+ Constructor<Mat, const Range&, const Range&>
29
+ ```
30
+
31
+ libclang exposes multiple APIs for retrieving type information, but **none provide a complete, correct spelling for all C++ constructs**.
32
+
33
+ This is a fundamental consequence of C++’s type system and libclang’s design goals.
34
+
35
+ ## Why a Canonical-Based Approach Fails
36
+
37
+ An initial strategy was to rely on `type.canonical.spelling` and then filter out implementation details. This approach fails in several unavoidable cases.
38
+
39
+ ### Failure Modes
40
+
41
+ | Feature | Result from `canonical.spelling` | Why this is incorrect |
42
+ |-------|----------------------------------|------------------------|
43
+ | **Typedefs / aliases** | `SizeArray` → `int[3]` | Destroys alias intent and public API spelling |
44
+ | **Templates** | `iterator` → `std::iterator` | Loses specialization and template context |
45
+ | **Dependent types** | Missing `typename`, incorrect qualification | Canonical types erase dependency information |
46
+ | **Namespaces** | Over- or under-qualified names | Ignores lexical context |
47
+
48
+ In addition, canonical spellings frequently expose **implementation details** that must never appear in generated bindings:
49
+
50
+ - `__gnu_cxx::__normal_iterator<...>` (libstdc++)
51
+ - `_Ty`, `_Alloc`, `_Vector_iterator` (MSVC STL)
52
+
53
+ Filtering these reliably is not possible without reconstructing the original spelling logic, which defeats the purpose of canonicalization.
54
+
55
+ ### Key Insight
56
+
57
+ `canonical.spelling` answers:
58
+
59
+ > “What is this type semantically?”
60
+
61
+ `ruby-bindgen` must answer:
62
+
63
+ > “How must this type be written so that user code compiles correctly and reflects the original API?”
64
+
65
+ These questions are fundamentally different.
66
+
67
+ ## What libclang Provides
68
+
69
+ libclang exposes several partial representations of a type, each optimized for a different purpose:
70
+
71
+ | API | Returns | Limitation |
72
+ |----|---------|------------|
73
+ | `type.spelling` | Source-level spelling | Often unqualified |
74
+ | `type.canonical.spelling` | Fully desugared type | Erases typedefs and context |
75
+ | `declaration.qualified_name` | Namespace-qualified name | Drops template arguments |
76
+ | `declaration.qualified_display_name` | Name + template parameters | May omit enclosing namespaces |
77
+
78
+ No single API preserves both **spelling fidelity** and **correct qualification**.
79
+
80
+ ## `ruby-bindgen`’s Strategy
81
+
82
+ `ruby-bindgen` does not attempt to normalize all types through a single representation.
83
+
84
+ Instead, `type_spelling` reconstructs the correct spelling **based on cursor kind and context**, using canonical information only as a constrained fallback.
85
+
86
+ ### Design Principle
87
+
88
+ > **Spelling fidelity is primary; canonicalization is secondary and opportunistic.**
89
+
90
+ ### Cursor-Specific Handling
91
+
92
+ #### `cursor_class_template`
93
+
94
+ Template definitions (e.g., `template<typename T> class Vec`).
95
+
96
+ - Reconstruct template arguments explicitly
97
+ - Use `qualify_dependent_types_in_template_args`
98
+ - Do not consult `@type_name_map` (template parameters must remain dependent)
99
+
100
+ #### `cursor_typedef_decl` inside a class template
101
+
102
+ Dependent typedefs (e.g., `DataType<_Tp>::value_type`).
103
+
104
+ - Emit `typename` (required by the C++ standard)
105
+ - Combine `qualified_name` with `qualified_display_name`
106
+ - Preserve dependency instead of resolving it
107
+
108
+ #### `cursor_typedef_decl` (non-dependent)
109
+
110
+ Public typedefs (e.g., `typedef Point_<int> Point2i`).
111
+
112
+ - Preserve the typedef name
113
+ - Do not desugar to the underlying type
114
+ - Qualify template arguments via `@type_name_map`
115
+
116
+ #### `cursor_type_alias_decl`
117
+
118
+ C++11 `using` declarations.
119
+
120
+ - Treated identically to `cursor_typedef_decl`
121
+ - Required for cross-compiler support (MSVC favors `using`)
122
+
123
+ #### `cursor_class_decl` and related types
124
+
125
+ Concrete types and template instantiations.
126
+
127
+ - Start from `fully_qualified_name`
128
+ - Optionally consult `canonical.spelling`
129
+ - Only when it does not introduce implementation types
130
+ - Qualify template arguments using `qualify_template_args`
131
+
132
+ ## The `@type_name_map`
133
+
134
+ During translation unit processing, `ruby-bindgen` builds a map from simple identifiers to fully-qualified names:
135
+
136
+ ```ruby
137
+ {
138
+ "Range" => "cv::Range",
139
+ "Mat" => "cv::Mat",
140
+ "Pixel" => "iter::Pixel"
141
+ }
142
+ ```
143
+
144
+ This map is used to qualify **unqualified template arguments**, not to rewrite dependent names or template parameters.
145
+
146
+ ## Where Canonicalization Works
147
+
148
+ `canonical.spelling` is useful only in limited cases:
149
+
150
+ - Non-dependent `cursor_class_decl` types
151
+ - Situations where alias preservation is irrelevant
152
+ - As a fallback sanity check for namespace qualification
153
+
154
+ It is not the primary source of truth.
155
+
156
+ ## Why Not Use Clang’s C++ API?
157
+
158
+ Clang’s C++ API (`libTooling`) provides:
159
+
160
+ - `PrintingPolicy`
161
+ - Direct AST printers for fully-qualified names
162
+ - Precise control over dependent type emission
163
+
164
+ However:
165
+
166
+ - `ruby-bindgen` uses `ffi-clang`, which exposes only libclang’s C API
167
+ - libclang is designed for IDEs and static analysis tools, not code generation
168
+ - Reimplementing spelling logic is unavoidable in this environment
169
+
170
+ `ruby-bindgen`’s type spelling logic exists specifically to bridge this gap.
171
+
172
+ ## Summary
173
+
174
+ There is no single libclang call that can produce correct C++ type spellings in all cases.
175
+
176
+ The complexity in `ruby-bindgen` is inherent:
177
+
178
+ - C++ has typedefs, aliases, templates, dependent types, and contextual name lookup
179
+ - libclang exposes these differently depending on cursor kind
180
+ - Correct code generation requires spelling reconstruction, not canonicalization
181
+
182
+ The current design reflects these constraints deliberately.
183
+
184
+ ## Code Locations
185
+
186
+ All type-spelling logic lives in the `RubyBindgen::Generators::Rice::TypeSpeller` class:
187
+
188
+ - `lib/ruby-bindgen/generators/rice/type_speller.rb`
189
+ - Top-level entry points: `type_spelling`, `type_spellings`,
190
+ `qualified_class_name`, `qualified_display_name`
191
+ - Declared / unexposed type handling: `type_spelling_declared`,
192
+ `type_spelling_unexposed`, `type_spelling_pointer`
193
+ - Template handling: `qualify_template_args`,
194
+ `qualify_dependent_types_in_template_args`,
195
+ `qualify_template_parameter_packs`,
196
+ `qualify_class_template_typedefs`
197
+ - Static member qualification: `qualify_class_static_members`
198
+
199
+ The `fully_qualified_name` helper used by `TypeSpeller` is provided
200
+ directly by `FFI::Clang::Type` (ffi-clang ≥ 0.16).
@@ -0,0 +1,55 @@
1
+ # Updating Bindings
2
+
3
+ After generating bindings with `ruby-bindgen`, you'll need to regenerate them periodically as the upstream library evolves, `ruby-bindgen` improves, or you upgrade dependencies. This document covers strategies for managing that process.
4
+
5
+ ## Simple Case
6
+
7
+ If you have no [manual edits or refinements](cpp/customizing.md) to preserve, you can simply regenerate the bindings. Configuration changes such as adding new skip patterns or version guards are fine — they are read from the YAML file each time.
8
+
9
+ ```bash
10
+ ruby-bindgen bindings.yaml
11
+ ```
12
+
13
+ In most cases, the updated bindings should compile and work.
14
+
15
+ The rest of this page discusses strategies for more complex cases where you want to preserve [customizations](cpp/customizing.md) when regenerating bindings.
16
+
17
+ ## Preserving Refinements
18
+
19
+ [Refinements](cpp/customizing.md#refinements) live in a separate directory that `ruby-bindgen` never touches, so the source files survive regeneration automatically. However, the top-level `-rb.cpp` file is regenerated, so you will need to re-add the `#include` directives and `Init_*_Refinements` calls.
20
+
21
+ ## Preserving Manual Edits
22
+
23
+ If you have [manual edits](cpp/customizing.md#manual-edits) to generated files, you need a way to reapply them after regeneration. Two possible approaches include:
24
+
25
+ ### Option A: Diff File
26
+
27
+ Maintain a diff file that can be reapplied after regeneration:
28
+
29
+ ```bash
30
+ # After making all manual changes to generated files
31
+ git diff -- ext/ ':(exclude)ext/manual_updates.md' > ext/manual_updates.diff
32
+ ```
33
+
34
+ After regenerating:
35
+
36
+ ```bash
37
+ # Regenerate
38
+ ruby-bindgen bindings.yaml
39
+
40
+ # Reapply manual changes
41
+ cd ext/
42
+ git apply manual_updates.diff
43
+ ```
44
+
45
+ **Pros**: Simple, standard tooling, machine-readable.
46
+
47
+ **Cons**: Diffs are fragile. If `ruby-bindgen`'s output changes line numbers or formatting, the diff won't apply cleanly. You'll need to manually resolve failures and regenerate the diff.
48
+
49
+ ### Option B: Agent File
50
+
51
+ Maintain a structured document (`manual_updates.md`) that describes each change declaratively. An AI assistant or a human can follow the instructions after each regeneration. For an example, see opencv-ruby's [manual_updates.md](https://github.com/cfis/opencv-ruby/blob/main/ext/manual_updates.md).
52
+
53
+ **Pros**: Survives large formatting changes, human-readable, an AI assistant can apply the instructions even when line numbers shift.
54
+
55
+ **Cons**: Must be kept in sync with actual changes.
@@ -0,0 +1,69 @@
1
+ # Version Guards
2
+
3
+ Some libraries ship symbols that only exist in certain versions. For example, OpenCV 4.5 added CUDA codec APIs that don't exist in 4.1. Version guards let you generate bindings that work across multiple library versions by wrapping version-specific symbols in conditional checks.
4
+
5
+ The mechanism differs by format:
6
+
7
+ - **Rice (C++)** — `#if` / `#endif` preprocessor directives checked at compile time
8
+ - **FFI (C)** — Ruby `if` conditionals checked at runtime via a user-implemented version method
9
+
10
+ ## Rice (C++)
11
+
12
+ ### Configuration
13
+
14
+ Two config options work together:
15
+
16
+ 1. **`version_check`** — the C preprocessor macro to test (e.g., `CV_VERSION`)
17
+ 2. **`symbols.versions`** — which symbols to guard and at what version
18
+
19
+ ```yaml
20
+ format: Rice
21
+ version_check: CV_VERSION
22
+ symbols:
23
+ versions:
24
+ 40100:
25
+ - cv::Foo::newMethod
26
+ 40500:
27
+ - /cv::cuda::.*/
28
+ ```
29
+
30
+ Symbol names support the same syntax as skip symbols: simple names, fully qualified names, signatures, and regex patterns. See [Symbols](configuration.md#symbols) for details.
31
+
32
+ ### Generated Output
33
+
34
+ #### Class methods
35
+
36
+ Version-guarded methods within a class produce inline `#if` / `#endif` around the chained method definition:
37
+
38
+ ```cpp
39
+ Rice::Data_Type<cv::Foo> rb_cFoo = define_class<cv::Foo>("Foo")
40
+ .define_method("bar", &cv::Foo::bar)
41
+ #if CV_VERSION >= 40100
42
+ .define_method("new_method", &cv::Foo::newMethod)
43
+ #endif
44
+ ;
45
+ ```
46
+
47
+ #### Top-level functions
48
+
49
+ Version-guarded free functions are wrapped at the statement level:
50
+
51
+ ```cpp
52
+ #if CV_VERSION >= 40100
53
+ define_global_function("new_func", &cv::newFunc);
54
+ #endif
55
+ ```
56
+
57
+ ### How It Works
58
+
59
+ When `version_check` is set, the generator looks up each symbol's version via the `symbols` config. Symbols with a version are grouped by their version value. The code generator emits `#if VERSION_MACRO >= version` before the group and `#endif` after it. Unversioned symbols are emitted normally with no guards.
60
+
61
+ ## FFI (C)
62
+
63
+ For C libraries, version detection happens at runtime. When `symbols.versions` has entries, the generator:
64
+
65
+ 1. Wraps version-specific symbols in `if {version_check} >= version` conditionals
66
+ 2. Adds `require_relative '{project}_version'` to the project loader file
67
+ 3. Generates a `{project}_version.rb` skeleton (once — won't overwrite if it already exists)
68
+
69
+ The user implements the `version_check` method by calling the library's own version API. See [Version Detection](c/version_guards.md) for a complete example using PROJ.
@@ -0,0 +1,63 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module RubyBindgen
5
+ class Config
6
+ def initialize(config_path)
7
+ @config_dir = File.dirname(File.expand_path(config_path))
8
+ raw = YAML.safe_load(File.read(config_path), permitted_classes: [], permitted_symbols: [], aliases: true)
9
+ # YAML.safe_load returns nil for an empty file. Treat that as an empty
10
+ # config so the CLI's validate_config can produce its useful "Config
11
+ # must specify 'output'" message instead of a NoMethodError.
12
+ @data = raw.is_a?(Hash) ? symbolize_keys(raw) : {}
13
+ resolve_toolchain
14
+ resolve_paths
15
+ end
16
+
17
+ def [](key)
18
+ @data[key]
19
+ end
20
+
21
+ # @api private
22
+ # Used by tests for ad-hoc overrides and by the CLI to default :input to
23
+ # :output. Not intended for downstream code; the public contract is read-only.
24
+ def []=(key, value)
25
+ @data[key] = value
26
+ end
27
+
28
+ private
29
+
30
+ def resolve_toolchain
31
+ toolchain = RUBY_PLATFORM =~ /mswin/ ? :'clang-cl' : :clang
32
+
33
+ toolchain_config = @data[toolchain]
34
+ if toolchain_config.is_a?(Hash)
35
+ @data[:libclang] = toolchain_config[:libclang]
36
+ @data[:clang_args] = toolchain_config[:args]&.map do |arg|
37
+ # Resolve relative -I paths relative to config directory
38
+ arg.start_with?('-I') ? "-I#{File.expand_path(arg[2..], @config_dir)}" : arg
39
+ end
40
+ end
41
+ end
42
+
43
+ def resolve_paths
44
+ @data[:input] = resolve_path(@data[:input]) if @data[:input]
45
+ @data[:output] = resolve_path(@data[:output]) if @data[:output]
46
+ end
47
+
48
+ def resolve_path(path)
49
+ return path if Pathname.new(path).absolute?
50
+ File.expand_path(path, @config_dir)
51
+ end
52
+
53
+ def symbolize_keys(hash)
54
+ hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }.transform_values do |value|
55
+ case value
56
+ when Hash then symbolize_keys(value)
57
+ when Array then value.map { |v| v.is_a?(Hash) ? symbolize_keys(v) : v }
58
+ else value
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,171 @@
1
+ module RubyBindgen
2
+ module Generators
3
+ class CMake < Generator
4
+ GuardedEntry = Data.define(:condition, :directories, :files)
5
+
6
+ def self.template_dir
7
+ __dir__
8
+ end
9
+
10
+ def include_dirs
11
+ config[:include_dirs] || []
12
+ end
13
+
14
+ def guards
15
+ @guards ||= begin
16
+ config_guards = config[:guards] || {}
17
+ config_guards.map do |condition, patterns|
18
+ Guard.new(condition: condition, patterns: patterns, base_path: @inputter.base_path)
19
+ end
20
+ end
21
+ end
22
+
23
+ def generate
24
+ base = Pathname.new(@inputter.base_path)
25
+
26
+ # Collect files from inputter, grouped by relative directory
27
+ files_by_dir = Hash.new do |hash, key|
28
+ hash[key] = []
29
+ end
30
+
31
+ @inputter.each do |path, relative_path|
32
+ dir = File.dirname(relative_path)
33
+ files_by_dir[dir] << Pathname.new(path)
34
+ end
35
+
36
+ # Collect all directories that need CMakeLists.txt files
37
+ # (directories containing files, plus all ancestor directories)
38
+ all_dirs = Set.new
39
+ files_by_dir.each_key do |dir|
40
+ next if dir == "."
41
+ parts = Pathname.new(dir).each_filename.to_a
42
+ parts.length.times do |i|
43
+ all_dirs << parts[0..i].join("/")
44
+ end
45
+ end
46
+
47
+ # Build parent -> immediate child directories mapping
48
+ child_dirs_of = Hash.new do |hash, key|
49
+ hash[key] = []
50
+ end
51
+ all_dirs.each do |dir|
52
+ parent = File.dirname(dir)
53
+ parent = "." if parent == dir
54
+ child_dirs_of[parent] << base.join(dir)
55
+ end
56
+ child_dirs_of.each_value(&:sort!)
57
+
58
+ file_guards, directory_guards = build_guard_maps(files_by_dir, all_dirs.to_a)
59
+
60
+ if @project
61
+ # Root CMakeLists.txt
62
+ content = render_template("project",
63
+ :project => self.project,
64
+ :directories => unguarded_paths(child_dirs_of["."], directory_guards, base),
65
+ :files => unguarded_paths(files_by_dir["."].sort, file_guards, base),
66
+ :guarded_entries => guarded_entries(child_dirs_of["."],
67
+ directory_guards,
68
+ files_by_dir["."].sort,
69
+ file_guards,
70
+ base),
71
+ :include_dirs => self.include_dirs)
72
+ self.outputter.write("CMakeLists.txt", content)
73
+
74
+ # Presets
75
+ content = render_template("presets")
76
+ self.outputter.write("CMakePresets.json", content)
77
+ end
78
+
79
+ # Subdirectory CMakeLists.txt files
80
+ all_dirs.sort.each do |dir|
81
+ content = render_template("directory",
82
+ :project => self.project,
83
+ :directories => unguarded_paths(child_dirs_of[dir], directory_guards, base),
84
+ :files => unguarded_paths((files_by_dir[dir] || []).sort, file_guards, base),
85
+ :guarded_entries => guarded_entries(child_dirs_of[dir],
86
+ directory_guards,
87
+ (files_by_dir[dir] || []).sort,
88
+ file_guards,
89
+ base))
90
+ self.outputter.write(File.join(dir, "CMakeLists.txt"), content)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def build_guard_maps(files_by_dir, all_dirs)
97
+ file_paths = files_by_dir.values.flatten.map do |path|
98
+ expand_path(path)
99
+ end.sort
100
+ directory_paths = all_dirs.map do |dir|
101
+ expand_path(dir, @inputter.base_path)
102
+ end.sort
103
+ file_guards = {}
104
+ directory_guards = {}
105
+
106
+ guards.each do |guard|
107
+ match = guard.match(file_paths: file_paths, directory_paths: directory_paths)
108
+
109
+ match.files.each do |path|
110
+ assign_guard!(file_guards, path, match.condition)
111
+ end
112
+
113
+ match.directories.each do |path|
114
+ assign_guard!(directory_guards, path, match.condition)
115
+ end
116
+ end
117
+
118
+ [file_guards, directory_guards]
119
+ end
120
+
121
+ def assign_guard!(assignments, path, condition)
122
+ previous = assignments[path]
123
+ if previous && previous != condition
124
+ raise ArgumentError, "#{path} matched multiple guard conditions: #{previous.inspect}, #{condition.inspect}"
125
+ end
126
+
127
+ assignments[path] = condition
128
+ end
129
+
130
+ def guarded_entries(directories, directory_guards, files, file_guards, base)
131
+ grouped = Hash.new do |hash, key|
132
+ hash[key] = GuardedEntry.new(condition: key, directories: [], files: [])
133
+ end
134
+
135
+ directories.each do |directory|
136
+ condition = directory_guards[expand_path(directory, base)]
137
+ next unless condition
138
+
139
+ grouped[condition].directories << directory
140
+ end
141
+
142
+ files.each do |file|
143
+ condition = file_guards[expand_path(file, base)]
144
+ next unless condition
145
+
146
+ grouped[condition].files << file
147
+ end
148
+
149
+ guards.map(&:condition).filter_map do |condition|
150
+ entry = grouped[condition]
151
+ next if entry.nil? || (entry.directories.empty? && entry.files.empty?)
152
+
153
+ entry
154
+ end
155
+ end
156
+
157
+ def expand_path(path, base = @inputter.base_path)
158
+ File.expand_path(path.to_s, base.to_s)
159
+ end
160
+
161
+ def unguarded_paths(paths, guard_map, base)
162
+ paths.reject do |path|
163
+ guard_map.key?(expand_path(path, base))
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ end
170
+
171
+ require_relative 'guard'
@@ -0,0 +1,29 @@
1
+ # Generated by ruby-bindgen (<%= RubyBindgen::VERSION %>)
2
+
3
+ # Subdirectories
4
+ <% directories.each do |directory| -%>
5
+ add_subdirectory ("<%= directory.relative_path_from(directory.parent) %>")
6
+ <% end -%>
7
+
8
+ # Sources
9
+ <% if !files.empty? -%>
10
+ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
11
+ <% files.each do |file| -%>
12
+ "<%= file.relative_path_from(file.parent) %>"
13
+ <% end -%>
14
+ )
15
+ <% end -%>
16
+ <% guarded_entries.each do |entry| -%>
17
+ if(<%= entry.condition %>)
18
+ <% entry.directories.each do |directory| -%>
19
+ add_subdirectory ("<%= directory.relative_path_from(directory.parent) %>")
20
+ <% end -%>
21
+ <% if !entry.files.empty? -%>
22
+ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
23
+ <% entry.files.each do |file| -%>
24
+ "<%= file.relative_path_from(file.parent) %>"
25
+ <% end -%>
26
+ )
27
+ <% end -%>
28
+ endif()
29
+ <% end -%>
@@ -0,0 +1,55 @@
1
+ module RubyBindgen
2
+ module Generators
3
+ class CMake
4
+ class Guard
5
+ GuardMatch = Data.define(:condition, :directories, :files)
6
+
7
+ attr_reader :condition, :patterns
8
+
9
+ def initialize(condition:, patterns:, base_path:)
10
+ @condition = condition.to_s
11
+ @patterns = Array(patterns).map do |pattern|
12
+ expand_path(pattern.to_s, base_path)
13
+ end
14
+ end
15
+
16
+ def match(file_paths:, directory_paths:)
17
+ matched_files = []
18
+ matched_directories = []
19
+
20
+ patterns.each do |pattern|
21
+ files = file_paths.select do |path|
22
+ path_match?(pattern, path)
23
+ end
24
+ directories = directory_paths.select do |path|
25
+ path_match?(pattern, path)
26
+ end
27
+
28
+ warn_unmatched(pattern) if files.empty? && directories.empty?
29
+
30
+ matched_files.concat(files)
31
+ matched_directories.concat(directories)
32
+ end
33
+
34
+ GuardMatch.new(condition: condition,
35
+ directories: matched_directories.uniq.sort,
36
+ files: matched_files.uniq.sort)
37
+ end
38
+
39
+ private
40
+
41
+ def path_match?(pattern, path)
42
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME)
43
+ end
44
+
45
+ def expand_path(path, base)
46
+ File.expand_path(path.to_s, base.to_s)
47
+ end
48
+
49
+ def warn_unmatched(pattern)
50
+ warn "CMake guard #{condition.inspect} did not match any generated paths for pattern #{pattern.inspect}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end