use std::collections::HashSet;
use anyhow::{Result, bail};
use clap::{Args, Parser, Subcommand};
use crate::ir::Version;
#[derive(Parser, Debug)]
#[command(
name = "gloam",
version = crate::build_info::VERSION,
about = "Vulkan/OpenGL/GLES/EGL/GLX/WGL loader generator"
)]
pub struct Cli {
#[arg(long)]
pub promoted: bool,
#[arg(long)]
pub predecessors: bool,
#[arg(long, required = true)]
pub api: String,
#[arg(long)]
pub extensions: Option<String>,
#[arg(long)]
pub baseline: Option<String>,
#[arg(long)]
pub merge: bool,
#[arg(long, default_value = ".")]
pub out_path: String,
#[arg(long)]
pub quiet: bool,
#[cfg(feature = "fetch")]
#[arg(long)]
pub fetch: bool,
#[command(subcommand)]
pub generator: Generator,
}
#[derive(Subcommand, Debug)]
pub enum Generator {
C(CArgs),
}
#[derive(Args, Debug)]
pub struct CArgs {
#[arg(long)]
pub alias: bool,
#[arg(long)]
pub loader: bool,
#[arg(long)]
pub external_headers: bool,
}
impl Cli {
pub fn api_requests(&self) -> Result<Vec<ApiRequest>> {
self.api
.split(',')
.map(|s| ApiRequest::parse(s.trim()))
.collect()
}
pub fn use_fetch(&self) -> bool {
#[cfg(feature = "fetch")]
{
self.fetch
}
#[cfg(not(feature = "fetch"))]
{
false
}
}
pub fn extension_filter(&self) -> Result<ExtensionFilter> {
let Some(ref spec) = self.extensions else {
return Ok(ExtensionFilter::all());
};
let raw_names: Vec<String> = if std::path::Path::new(spec).exists() {
let text = std::fs::read_to_string(spec)?;
text.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(str::to_string)
.collect()
} else {
spec.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
};
let mut include_all = false;
let mut includes: Vec<String> = Vec::new();
let mut excludes: HashSet<String> = HashSet::new();
for name in raw_names {
if name.eq_ignore_ascii_case("all") {
include_all = true;
} else if let Some(stripped) = name.strip_prefix('-') {
if !stripped.is_empty() {
excludes.insert(stripped.to_string());
}
} else {
includes.push(name);
}
}
let (include, keep) = if include_all {
(None, includes.into_iter().collect())
} else {
(Some(includes), HashSet::new())
};
Ok(ExtensionFilter {
include,
exclude: excludes,
keep,
})
}
pub fn baseline_requests(&self) -> Result<Vec<ApiRequest>> {
let Some(ref spec) = self.baseline else {
return Ok(Vec::new());
};
spec.split(',')
.map(|s| ApiRequest::parse(s.trim()))
.collect()
}
}
#[derive(Debug)]
pub struct ExtensionFilter {
pub include: Option<Vec<String>>,
pub exclude: HashSet<String>,
pub keep: HashSet<String>,
}
impl ExtensionFilter {
pub fn all() -> Self {
Self {
include: None,
exclude: HashSet::new(),
keep: HashSet::new(),
}
}
}
pub fn canonical_api_name(name: &str) -> &str {
match name {
"vulkan" => "vk",
other => other,
}
}
pub fn xml_api_name(name: &str) -> &str {
match name {
"vk" => "vulkan",
other => other,
}
}
#[derive(Debug, Clone)]
pub struct ApiRequest {
pub name: String,
pub profile: Option<String>,
pub version: Option<Version>,
}
impl ApiRequest {
pub fn parse(s: &str) -> Result<Self> {
let (name_profile, ver_str) = match s.find('=') {
Some(i) => (&s[..i], Some(&s[i + 1..])),
None => (s, None),
};
let (name, profile) = match name_profile.find(':') {
Some(i) => (&name_profile[..i], Some(&name_profile[i + 1..])),
None => (name_profile, None),
};
if name.is_empty() {
bail!("empty API name in --api argument");
}
let version = ver_str
.map(|v| {
let (maj, min) = v.split_once('.').ok_or_else(|| {
anyhow::anyhow!("invalid version '{}', expected major.minor", v)
})?;
Ok::<_, anyhow::Error>(Version::new(maj.parse()?, min.parse()?))
})
.transpose()?;
Ok(Self {
name: canonical_api_name(name).to_string(),
profile: profile.map(str::to_string),
version,
})
}
pub fn spec_name(&self) -> &str {
match self.name.as_str() {
"gl" | "gles1" | "gles2" | "glcore" => "gl",
"egl" => "egl",
"glx" => "glx",
"wgl" => "wgl",
"vk" | "vulkan" => "vk",
other => other,
}
}
#[allow(dead_code)]
pub fn is_gl_family(&self) -> bool {
matches!(self.name.as_str(), "gl" | "gles1" | "gles2" | "glcore")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_gl_core_versioned() {
let r = ApiRequest::parse("gl:core=3.3").unwrap();
assert_eq!(r.name, "gl");
assert_eq!(r.profile.as_deref(), Some("core"));
assert_eq!(r.version, Some(Version::new(3, 3)));
}
#[test]
fn parse_gl_compat_no_version() {
let r = ApiRequest::parse("gl:compat").unwrap();
assert_eq!(r.name, "gl");
assert_eq!(r.profile.as_deref(), Some("compat"));
assert!(r.version.is_none());
}
#[test]
fn parse_gles2_versioned() {
let r = ApiRequest::parse("gles2=3.0").unwrap();
assert_eq!(r.name, "gles2");
assert!(r.profile.is_none());
assert_eq!(r.version, Some(Version::new(3, 0)));
}
#[test]
fn parse_vk_versioned() {
let r = ApiRequest::parse("vk=1.3").unwrap();
assert_eq!(r.name, "vk");
assert_eq!(r.version, Some(Version::new(1, 3)));
}
#[test]
fn parse_vulkan_normalizes_to_vk() {
let r = ApiRequest::parse("vulkan=1.3").unwrap();
assert_eq!(r.name, "vk", "vulkan should normalize to vk");
assert_eq!(r.version, Some(Version::new(1, 3)));
}
#[test]
fn parse_vulkan_bare_normalizes_to_vk() {
let r = ApiRequest::parse("vulkan").unwrap();
assert_eq!(r.name, "vk");
assert!(r.version.is_none());
}
#[test]
fn parse_bare_name_no_version() {
let r = ApiRequest::parse("egl").unwrap();
assert_eq!(r.name, "egl");
assert!(r.profile.is_none());
assert!(r.version.is_none());
}
#[test]
fn parse_empty_name_errors() {
assert!(ApiRequest::parse("=1.0").is_err());
}
#[test]
fn parse_version_missing_minor_errors() {
assert!(ApiRequest::parse("gl:core=3").is_err());
}
#[test]
fn parse_version_non_numeric_errors() {
assert!(ApiRequest::parse("gl:core=three.three").is_err());
}
#[test]
fn spec_name_gl_family_maps_to_gl() {
for name in &["gl", "gles1", "gles2", "glcore"] {
let r = ApiRequest::parse(name).unwrap();
assert_eq!(r.spec_name(), "gl", "failed for api name '{name}'");
}
}
#[test]
fn spec_name_passthrough() {
for name in &["egl", "glx", "wgl"] {
let r = ApiRequest::parse(name).unwrap();
assert_eq!(r.spec_name(), *name);
}
}
#[test]
fn spec_name_vulkan_alias() {
assert_eq!(ApiRequest::parse("vk").unwrap().spec_name(), "vk");
assert_eq!(ApiRequest::parse("vulkan").unwrap().spec_name(), "vk");
}
}