Skip to content

Commit a012057

Browse files
committed
Add guard support to CMake generator
1 parent ba115d3 commit a012057

29 files changed

Lines changed: 494 additions & 14 deletions

docs/cmake/cmake_bindings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ See [Output](output.md) for details on the generated files.
1616

1717
## Filtering
1818

19-
See [Filtering](filtering.md) for how to exclude files from the generated CMake build.
19+
See [Filtering](filtering.md) for how to exclude files from the generated CMake build and how to conditionally include generated sources and directories with `guards`.

docs/cmake/filtering.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,24 @@
33
## Skipping Files
44

55
The CMake config supports a `skip` option to exclude specific `*-rb.cpp` files from the generated `CMakeLists.txt`. However, in most cases it's better to add skip patterns to your Rice config instead — that way the problematic files are never generated, and CMake won't find them to include. The CMake `skip` is useful as a quick fix when you have stale generated files on disk that you don't want to recompile.
6+
7+
## Guarding Files And Directories
8+
9+
Use `guards` when generated paths should remain on disk but only be compiled when a CMake condition is true.
10+
11+
```yaml
12+
guards:
13+
OpenCV_HAS_CUDA:
14+
- opencv2/cuda*-rb.cpp
15+
TARGET OpenCV::dnn:
16+
- opencv2/dnn
17+
```
18+
19+
Guard keys are emitted as raw `if(...)` conditions. Guard values may be exact paths or globs and may match generated directories and `*-rb.cpp` files.
20+
21+
- Matching directories are emitted inside guarded `add_subdirectory(...)` blocks.
22+
- Matching `*-rb.cpp` files are emitted inside guarded `target_sources(...)` blocks.
23+
- A guard pattern that matches nothing emits a warning.
24+
- If the same path matches multiple guards, generation fails with an error.
25+
26+
Use `skip` for permanent exclusion. Use `guards` for conditional inclusion.

docs/cmake/getting_started.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ project: my_extension
1515
output: ./ext/generated
1616
format: CMake
1717

18+
guards:
19+
TARGET MyLib::gpu:
20+
- gpu
21+
- gpu/**/*-rb.cpp
22+
1823
include_dirs:
1924
- "${CMAKE_CURRENT_SOURCE_DIR}/../include"
2025
```
@@ -23,6 +28,7 @@ Key options:
2328
2429
- `project` — name used in the CMake `project()` command and build target. When omitted, only subdirectory `CMakeLists.txt` files are generated — useful when you manage the root project files yourself.
2530
- `output` — directory containing the Rice `*-rb.cpp` files. `input` defaults to `output` for CMake.
31+
- `guards` — raw CMake conditions mapped to generated path patterns. Use this when a generated subdirectory or `*-rb.cpp` file should only be compiled when a module or feature is available.
2632
- `include_dirs` — include directories added via `target_include_directories`. These are CMake expressions written directly into the generated `CMakeLists.txt`.
2733

2834
See [Configuration](../configuration.md) for all options.

docs/cmake/output.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,34 @@ target_sources(${CMAKE_PROJECT_NAME} PUBLIC
7777
"image-rb.cpp"
7878
)
7979
```
80+
81+
When `guards` are configured, the generator groups matching directories and files under raw CMake `if(...)` blocks in the generated `CMakeLists.txt`:
82+
83+
```yaml
84+
guards:
85+
TARGET OpenCV::dnn:
86+
- opencv2/dnn
87+
OpenCV_HAS_CUDA:
88+
- opencv2/cuda*-rb.cpp
89+
```
90+
91+
```cmake
92+
# Subdirectories
93+
add_subdirectory("core")
94+
95+
# Sources
96+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
97+
"core-rb.cpp"
98+
)
99+
100+
if(TARGET OpenCV::dnn)
101+
add_subdirectory("dnn")
102+
endif()
103+
104+
if(OpenCV_HAS_CUDA)
105+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
106+
"cudaarithm-rb.cpp"
107+
"cudaimgproc-rb.cpp"
108+
)
109+
endif()
110+
```

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ These options apply to all formats.
5353
|----------------|---------|-------------|
5454
| `project` | none | Project name used in the CMake `project()` command and build target name. When provided, generates the root `CMakeLists.txt` (with project setup, Rice fetch, Ruby detection) and `CMakePresets.json`. When omitted, only subdirectory `CMakeLists.txt` files are generated — useful when you manage the root project files yourself. |
5555
| `include_dirs` | `[]` | List of include directory expressions added via `target_include_directories`. These are CMake expressions written directly into `CMakeLists.txt` (e.g., `${CMAKE_CURRENT_SOURCE_DIR}/../headers`). |
56+
| `guards` | `{}` | Map of raw CMake condition expressions to arrays of generated path patterns. Matching directories are emitted inside guarded `add_subdirectory(...)` blocks; matching `*-rb.cpp` files are emitted inside guarded `target_sources(...)` blocks. Exact paths and globs are both supported. |
5657

5758
## Compiler Toolchain
5859

lib/ruby-bindgen/generators/cmake/cmake.rb

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module RubyBindgen
22
module Generators
33
class CMake < Generator
4+
GuardedEntry = Data.define(:condition, :directories, :files)
5+
46
def self.template_dir
57
__dir__
68
end
@@ -9,11 +11,22 @@ def include_dirs
911
config[:include_dirs] || []
1012
end
1113

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+
1223
def generate
1324
base = Pathname.new(@inputter.base_path)
1425

1526
# Collect files from inputter, grouped by relative directory
16-
files_by_dir = Hash.new { |h, k| h[k] = [] }
27+
files_by_dir = Hash.new do |hash, key|
28+
hash[key] = []
29+
end
1730

1831
@inputter.each do |path, relative_path|
1932
dir = File.dirname(relative_path)
@@ -32,20 +45,29 @@ def generate
3245
end
3346

3447
# Build parent -> immediate child directories mapping
35-
child_dirs_of = Hash.new { |h, k| h[k] = [] }
48+
child_dirs_of = Hash.new do |hash, key|
49+
hash[key] = []
50+
end
3651
all_dirs.each do |dir|
3752
parent = File.dirname(dir)
3853
parent = "." if parent == dir
3954
child_dirs_of[parent] << base.join(dir)
4055
end
4156
child_dirs_of.each_value(&:sort!)
4257

58+
file_guards, directory_guards = build_guard_maps(files_by_dir, all_dirs.to_a)
59+
4360
if @project
4461
# Root CMakeLists.txt
4562
content = render_template("project",
4663
:project => self.project,
47-
:directories => child_dirs_of["."],
48-
:files => files_by_dir["."].sort,
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),
4971
:include_dirs => self.include_dirs)
5072
self.outputter.write("CMakeLists.txt", content)
5173

@@ -58,11 +80,92 @@ def generate
5880
all_dirs.sort.each do |dir|
5981
content = render_template("directory",
6082
:project => self.project,
61-
:directories => child_dirs_of[dir],
62-
:files => (files_by_dir[dir] || []).sort)
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))
6390
self.outputter.write(File.join(dir, "CMakeLists.txt"), content)
6491
end
6592
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+
66167
end
67168
end
68169
end
170+
171+
require_relative 'guard'

lib/ruby-bindgen/generators/cmake/directory.erb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,22 @@ add_subdirectory ("<%= directory.relative_path_from(directory.parent) %>")
88
# Sources
99
<% if !files.empty? -%>
1010
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
11-
<% files.each do |file| -%>
11+
<% files.each do |file| -%>
1212
"<%= file.relative_path_from(file.parent) %>"
13-
<% end -%>
13+
<% end -%>
1414
)
1515
<% 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 -%>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/ruby-bindgen/generators/cmake/project.erb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,17 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
7373
<% end -%>
7474
)
7575
<% end -%>
76+
<% guarded_entries.each do |entry| -%>
77+
if(<%= entry.condition %>)
78+
<% entry.directories.each do |directory| -%>
79+
add_subdirectory("<%= directory.relative_path_from(directory.parent) %>")
80+
<% end -%>
81+
<% if !entry.files.empty? -%>
82+
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
83+
<% entry.files.each do |file| -%>
84+
"<%= file.relative_path_from(file.parent) %>"
85+
<% end -%>
86+
)
87+
<% end -%>
88+
endif()
89+
<% end -%>

test/abstract_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'bundler/setup'
2+
require 'fileutils'
23

34
if ENV['COVERAGE']
45
require 'simplecov'
@@ -57,6 +58,7 @@ def validate_result(outputter)
5758
outputter.output_paths.each do |path, content|
5859
generated_paths << path
5960
if ENV["UPDATE_EXPECTED"]
61+
FileUtils.mkdir_p(File.dirname(path))
6062
File.open(path, 'wb') do |file|
6163
file << content
6264
end

0 commit comments

Comments
 (0)