Skip to content

Commit 2ccfec7

Browse files
committed
feat(cli): add extension exclusion syntax and --baseline flag
New --extensions syntax supports `-` prefix for exclusions: --extensions all,-GL_EXT_direct_state_access --extensions GL_KHR_debug,-GL_ARB_debug_output New --baseline flag excludes extensions fully promoted into the specified API versions (all commands present in core via same-name or alias). Format matches --api: --baseline gl:core=3.3,gles2=3.0 Both compose: explicit exclusions and baseline exclusions are unioned into a single veto set applied after all selection passes (explicit, dependency, promoted, predecessor). Exclusion info flows into FeatureSet and the preamble comment now shows what was excluded and why. Signed-off-by: Steven Noonan <steven@uplinklabs.net>
1 parent 6c5a92e commit 2ccfec7

3 files changed

Lines changed: 367 additions & 52 deletions

File tree

src/cli.rs

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Command-line interface definitions.
22
3+
use std::collections::HashSet;
4+
35
use anyhow::{Result, bail};
46
use clap::{Args, Parser, Subcommand};
57

@@ -35,11 +37,22 @@ pub struct Cli {
3537
#[arg(long, required = true)]
3638
pub api: String,
3739

38-
/// Extension filter: path to a file (one per line) or a comma-separated
39-
/// list of extension names. Omit to include all possible extensions.
40+
/// Extension filter: path to a file (one per line), a comma-separated
41+
/// list of extension names, or "all" (the default if omitted). Prefix a
42+
/// name with `-` to exclude it. Examples:
43+
/// --extensions all,-GL_EXT_direct_state_access
44+
/// --extensions GL_KHR_debug,GL_ARB_sync
45+
/// --extensions "" (include no extensions)
4046
#[arg(long)]
4147
pub extensions: Option<String>,
4248

49+
/// Baseline API versions. Extensions that are fully promoted into these
50+
/// versions or earlier are excluded — they're guaranteed to be present
51+
/// in a context of at least the baseline version. Format matches --api:
52+
/// --baseline gl:core=3.3,gles2=3.0
53+
#[arg(long)]
54+
pub baseline: Option<String>,
55+
4356
/// Merge multiple APIs of the same spec into a single output file.
4457
/// Required when combining gl and gles2; behaviour is undefined otherwise.
4558
#[arg(long)]
@@ -86,31 +99,86 @@ impl Cli {
8699
.collect()
87100
}
88101

89-
/// Returns None (include all) or Some(list of extension names).
90-
pub fn extension_filter(&self) -> Result<Option<Vec<String>>> {
102+
/// Parse the --extensions argument into an `ExtensionFilter`.
103+
pub fn extension_filter(&self) -> Result<ExtensionFilter> {
91104
let Some(ref spec) = self.extensions else {
92-
return Ok(None);
105+
return Ok(ExtensionFilter::all());
93106
};
94107

95-
if std::path::Path::new(spec).exists() {
108+
// Read names from a file or inline comma-separated list.
109+
let raw_names: Vec<String> = if std::path::Path::new(spec).exists() {
96110
let text = std::fs::read_to_string(spec)?;
97-
let list = text
98-
.lines()
111+
text.lines()
99112
.map(str::trim)
100113
.filter(|l| !l.is_empty() && !l.starts_with('#'))
101114
.map(str::to_string)
102-
.collect();
103-
return Ok(Some(list));
115+
.collect()
116+
} else {
117+
spec.split(',')
118+
.map(str::trim)
119+
.filter(|s| !s.is_empty())
120+
.map(str::to_string)
121+
.collect()
122+
};
123+
124+
// Split into includes and excludes based on `-` prefix.
125+
let mut include_all = false;
126+
let mut includes: Vec<String> = Vec::new();
127+
let mut excludes: HashSet<String> = HashSet::new();
128+
129+
for name in raw_names {
130+
if name.eq_ignore_ascii_case("all") {
131+
include_all = true;
132+
} else if let Some(stripped) = name.strip_prefix('-') {
133+
if !stripped.is_empty() {
134+
excludes.insert(stripped.to_string());
135+
}
136+
} else {
137+
includes.push(name);
138+
}
104139
}
105140

106-
// Treat as an inline comma-separated list.
107-
let list = spec
108-
.split(',')
109-
.map(str::trim)
110-
.filter(|s| !s.is_empty())
111-
.map(str::to_string)
112-
.collect();
113-
Ok(Some(list))
141+
let include = if include_all { None } else { Some(includes) };
142+
Ok(ExtensionFilter {
143+
include,
144+
exclude: excludes,
145+
})
146+
}
147+
148+
/// Parse the --baseline argument into API requests (same format as --api).
149+
pub fn baseline_requests(&self) -> Result<Vec<ApiRequest>> {
150+
let Some(ref spec) = self.baseline else {
151+
return Ok(Vec::new());
152+
};
153+
spec.split(',')
154+
.map(|s| ApiRequest::parse(s.trim()))
155+
.collect()
156+
}
157+
}
158+
159+
// ---------------------------------------------------------------------------
160+
// ExtensionFilter
161+
// ---------------------------------------------------------------------------
162+
163+
/// Parsed extension filter from --extensions.
164+
///
165+
/// `include` is `None` for "all extensions" or `Some(list)` for an explicit set.
166+
/// `exclude` is always a set of names to unconditionally remove — applied as a
167+
/// final veto after all selection passes (explicit, dependency, promoted,
168+
/// predecessor, baseline).
169+
#[derive(Debug)]
170+
pub struct ExtensionFilter {
171+
pub include: Option<Vec<String>>,
172+
pub exclude: HashSet<String>,
173+
}
174+
175+
impl ExtensionFilter {
176+
/// No filter — include everything, exclude nothing.
177+
pub fn all() -> Self {
178+
Self {
179+
include: None,
180+
exclude: HashSet::new(),
181+
}
114182
}
115183
}
116184

src/preamble.rs

Lines changed: 117 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ pub fn build_preamble(fs: &FeatureSet, command_line: &str) -> String {
2626
lines.push(format!(" * {command_line}"));
2727

2828
// ---- Extension provenance ----
29-
if !fs.extensions.is_empty() {
29+
if !fs.extensions.is_empty()
30+
|| !fs.excluded_explicit.is_empty()
31+
|| !fs.excluded_baseline.is_empty()
32+
{
3033
lines.push(" *".to_string());
3134
lines.push(format!(" * {}", extension_summary(fs)));
3235

@@ -47,6 +50,20 @@ pub fn build_preamble(fs: &FeatureSet, command_line: &str) -> String {
4750
lines.push(format!(" * {}: {}", label, names.join(", ")));
4851
}
4952
}
53+
54+
// List excluded extensions.
55+
if !fs.excluded_baseline.is_empty() {
56+
lines.push(format!(
57+
" * excluded by baseline: {} extensions",
58+
fs.excluded_baseline.len()
59+
));
60+
}
61+
if !fs.excluded_explicit.is_empty() {
62+
lines.push(format!(
63+
" * excluded explicitly: {}",
64+
fs.excluded_explicit.join(", ")
65+
));
66+
}
5067
}
5168

5269
// ---- gloam license ----
@@ -81,11 +98,13 @@ pub fn build_preamble(fs: &FeatureSet, command_line: &str) -> String {
8198
/// Build a one-line summary of extension selection.
8299
///
83100
/// Examples:
84-
/// "Extensions: all (451 total)"
85-
/// "Extensions: 5 explicit, 12 promoted, 3 predecessor (20 total)"
86-
/// "Extensions: 2 mandatory, 5 explicit (7 total)"
101+
/// "Extensions: all (451 included)"
102+
/// "Extensions: all, 68 excluded by baseline, 3 excluded explicitly (380 included)"
103+
/// "Extensions: 5 explicit, 12 promoted, 3 predecessor (20 included)"
87104
fn extension_summary(fs: &FeatureSet) -> String {
88105
let total = fs.extensions.len();
106+
let n_baseline_excluded = fs.excluded_baseline.len();
107+
let n_explicit_excluded = fs.excluded_explicit.len();
89108

90109
let count = |reason: SelectionReason| -> usize {
91110
fs.extensions.iter().filter(|e| e.reason == reason).count()
@@ -98,29 +117,37 @@ fn extension_summary(fs: &FeatureSet) -> String {
98117
let n_promoted = count(SelectionReason::Promoted);
99118
let n_predecessor = count(SelectionReason::Predecessor);
100119

120+
let mut parts: Vec<String> = Vec::new();
121+
101122
// If everything came from "all extensions" (no filter), use the short form.
102123
if n_all + n_mandatory == total {
103-
return format!("Extensions: all ({total} total)");
124+
parts.push("all".to_string());
125+
} else {
126+
if n_explicit > 0 {
127+
parts.push(format!("{n_explicit} explicit"));
128+
}
129+
if n_mandatory > 0 {
130+
parts.push(format!("{n_mandatory} mandatory"));
131+
}
132+
if n_dependency > 0 {
133+
parts.push(format!("{n_dependency} dependency"));
134+
}
135+
if n_promoted > 0 {
136+
parts.push(format!("{n_promoted} promoted"));
137+
}
138+
if n_predecessor > 0 {
139+
parts.push(format!("{n_predecessor} predecessor"));
140+
}
104141
}
105142

106-
let mut parts: Vec<String> = Vec::new();
107-
if n_explicit > 0 {
108-
parts.push(format!("{n_explicit} explicit"));
109-
}
110-
if n_mandatory > 0 {
111-
parts.push(format!("{n_mandatory} mandatory"));
112-
}
113-
if n_dependency > 0 {
114-
parts.push(format!("{n_dependency} dependency"));
143+
if n_baseline_excluded > 0 {
144+
parts.push(format!("{n_baseline_excluded} excluded by baseline"));
115145
}
116-
if n_promoted > 0 {
117-
parts.push(format!("{n_promoted} promoted"));
118-
}
119-
if n_predecessor > 0 {
120-
parts.push(format!("{n_predecessor} predecessor"));
146+
if n_explicit_excluded > 0 {
147+
parts.push(format!("{n_explicit_excluded} excluded explicitly"));
121148
}
122149

123-
format!("Extensions: {} ({total} total)", parts.join(", "))
150+
format!("Extensions: {} ({total} included)", parts.join(", "))
124151
}
125152

126153
/// Returns true if this feature set includes ANGLE extension supplementals.
@@ -155,6 +182,8 @@ mod tests {
155182
ext_subset_indices: Default::default(),
156183
alias_pairs: vec![],
157184
required_headers: vec![],
185+
excluded_explicit: vec![],
186+
excluded_baseline: vec![],
158187
}
159188
}
160189

@@ -255,7 +284,7 @@ mod tests {
255284
stub_ext("VK_KHR_swapchain", SelectionReason::AllExtensions),
256285
stub_ext("VK_KHR_surface", SelectionReason::AllExtensions),
257286
];
258-
assert_eq!(extension_summary(&fs), "Extensions: all (2 total)");
287+
assert_eq!(extension_summary(&fs), "Extensions: all (2 included)");
259288
}
260289

261290
#[test]
@@ -265,7 +294,7 @@ mod tests {
265294
stub_ext("WGL_ARB_extensions_string", SelectionReason::Mandatory),
266295
stub_ext("WGL_ARB_pixel_format", SelectionReason::AllExtensions),
267296
];
268-
assert_eq!(extension_summary(&fs), "Extensions: all (2 total)");
297+
assert_eq!(extension_summary(&fs), "Extensions: all (2 included)");
269298
}
270299

271300
#[test]
@@ -275,7 +304,10 @@ mod tests {
275304
stub_ext("VK_KHR_swapchain", SelectionReason::Explicit),
276305
stub_ext("VK_KHR_surface", SelectionReason::Explicit),
277306
];
278-
assert_eq!(extension_summary(&fs), "Extensions: 2 explicit (2 total)");
307+
assert_eq!(
308+
extension_summary(&fs),
309+
"Extensions: 2 explicit (2 included)"
310+
);
279311
}
280312

281313
#[test]
@@ -292,7 +324,7 @@ mod tests {
292324
];
293325
assert_eq!(
294326
extension_summary(&fs),
295-
"Extensions: 1 explicit, 2 promoted, 1 predecessor (4 total)"
327+
"Extensions: 1 explicit, 2 promoted, 1 predecessor (4 included)"
296328
);
297329
}
298330

@@ -341,7 +373,7 @@ mod tests {
341373
];
342374
assert_eq!(
343375
extension_summary(&fs),
344-
"Extensions: 1 explicit, 1 dependency (2 total)"
376+
"Extensions: 1 explicit, 1 dependency (2 included)"
345377
);
346378
}
347379

@@ -351,4 +383,64 @@ mod tests {
351383
let p = build_preamble(&fs, "gloam --api vk=1.3 c");
352384
assert!(!p.contains("Extensions:"));
353385
}
386+
387+
// ---- Exclusion display ----
388+
389+
#[test]
390+
fn summary_with_baseline_exclusions() {
391+
let mut fs = stub_fs("gl");
392+
fs.extensions = vec![stub_ext("GL_KHR_debug", SelectionReason::AllExtensions)];
393+
fs.excluded_baseline = vec![
394+
"GL_ARB_copy_buffer".to_string(),
395+
"GL_ARB_multitexture".to_string(),
396+
];
397+
assert_eq!(
398+
extension_summary(&fs),
399+
"Extensions: all, 2 excluded by baseline (1 included)"
400+
);
401+
}
402+
403+
#[test]
404+
fn summary_with_explicit_exclusions() {
405+
let mut fs = stub_fs("gl");
406+
fs.extensions = vec![stub_ext("GL_KHR_debug", SelectionReason::AllExtensions)];
407+
fs.excluded_explicit = vec!["GL_EXT_direct_state_access".to_string()];
408+
assert_eq!(
409+
extension_summary(&fs),
410+
"Extensions: all, 1 excluded explicitly (1 included)"
411+
);
412+
}
413+
414+
#[test]
415+
fn summary_with_both_exclusion_types() {
416+
let mut fs = stub_fs("gl");
417+
fs.extensions = vec![stub_ext("GL_KHR_debug", SelectionReason::AllExtensions)];
418+
fs.excluded_baseline = vec!["GL_ARB_copy_buffer".to_string()];
419+
fs.excluded_explicit = vec!["GL_EXT_direct_state_access".to_string()];
420+
assert_eq!(
421+
extension_summary(&fs),
422+
"Extensions: all, 1 excluded by baseline, 1 excluded explicitly (1 included)"
423+
);
424+
}
425+
426+
#[test]
427+
fn preamble_shows_baseline_exclusion_count() {
428+
let mut fs = stub_fs("gl");
429+
fs.extensions = vec![stub_ext("GL_KHR_debug", SelectionReason::AllExtensions)];
430+
fs.excluded_baseline = vec![
431+
"GL_ARB_copy_buffer".to_string(),
432+
"GL_ARB_multitexture".to_string(),
433+
];
434+
let p = build_preamble(&fs, "gloam --api gl:core c");
435+
assert!(p.contains("excluded by baseline: 2 extensions"));
436+
}
437+
438+
#[test]
439+
fn preamble_shows_explicit_exclusion_names() {
440+
let mut fs = stub_fs("gl");
441+
fs.extensions = vec![stub_ext("GL_KHR_debug", SelectionReason::AllExtensions)];
442+
fs.excluded_explicit = vec!["GL_EXT_direct_state_access".to_string()];
443+
let p = build_preamble(&fs, "gloam --api gl:core c");
444+
assert!(p.contains("excluded explicitly: GL_EXT_direct_state_access"));
445+
}
354446
}

0 commit comments

Comments
 (0)