utils: add new tool xash-clang-format, a wrapper around clang-format that adds our formatting extensions

This commit is contained in:
Alibek Omarov
2026-06-30 23:40:01 +05:00
parent 2166dbde66
commit 9f2b8954e9
13 changed files with 1647 additions and 3 deletions

View File

@@ -0,0 +1,91 @@
/*
config.c - .clang-format extension parser
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include <string.h>
#include "crtlib.h"
#include "xcf.h"
#define XCF_KEY "XashCollapseParens"
/*
============
ConfigDefault
============
*/
static void ConfigDefault( xcf_config_t *c )
{
c->collapse_parens = false;
}
/*
============
SkipWhitespace
============
*/
static const char *SkipWhitespace( const char *p, const char *end )
{
for( ; p < end && ( *p == ' ' || *p == '\t' ); p++ )
;
return p;
}
/*
============
ParseConfigText
Parse extension keys out of a YAML text body.
We only look at lines that begin (after optional whitespace) with `#`.
============
*/
qboolean ParseConfigText( const char *text, xcf_config_t *cfg )
{
ConfigDefault( cfg );
if( !text )
return true;
for( const char *p = text; *p; )
{
const char *end = Q_strchrnul( p, '\n' );
const char *q;
const size_t klen = sizeof( XCF_KEY ) - 1;
q = SkipWhitespace( p, end );
if( q < end && *q == '#' )
{
q = SkipWhitespace( q + 1, end );
if( end - q >= klen && memcmp( q, XCF_KEY, klen ) == 0 )
{
q = SkipWhitespace( q + klen, end );
if( q < end && *q == ':' )
{
q = SkipWhitespace( q + 1, end );
if( end - q >= 4 && memcmp( q, "true", 4 ) == 0 )
cfg->collapse_parens = true;
else if( end - q >= 5 && memcmp( q, "false", 5 ) == 0 )
cfg->collapse_parens = false;
}
}
}
if( !*end )
break;
p = end + 1;
}
return true;
}

View File

@@ -0,0 +1,402 @@
/*
main.c - xash-clang-format wrapper entry point
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include "crtlib.h"
#include "xcf.h"
#ifndef XCF_CLANG_FORMAT_DEFAULT
#define XCF_CLANG_FORMAT_DEFAULT "clang-format"
#endif
// TODO: move to custom config keys
#define XCF_MIN_CLANG_FORMAT_MAJOR 17
static const char *g_clang_format = NULL;
/*
============
ResolveClangFormat
============
*/
static const char *ResolveClangFormat( void )
{
const char *e = getenv( "XASH_CLANG_FORMAT_BIN" );
if( !COM_StringEmptyOrNULL( e ))
return e;
return XCF_CLANG_FORMAT_DEFAULT;
}
/*
============
IsFileArg
Attempt to distinguish files from other args.
But to keep parsing args simple, it will parse value as --key value as
filename. :(
============
*/
static qboolean IsFileArg( const char *a )
{
if( COM_StringEmptyOrNULL( a ))
return false;
if( a[0] == '-' )
return false;
return true;
}
/*
============
ReadTheFile
============
*/
static qboolean ReadTheFile( const char *path, xcf_buf_t *out )
{
FILE *f = fopen( path, "rb" );
if( !f )
return false;
char buf[8192];
size_t n;
while(( n = fread( buf, 1, sizeof( buf ), f )) > 0 )
{
if( !BufPutMem( out, buf, n ))
{
fclose( f );
return false;
}
}
fclose( f );
return true;
}
/*
============
WriteTheFile
============
*/
static qboolean WriteTheFile( const char *path, const char *data, size_t len )
{
FILE *f = fopen( path, "wb" );
if( !f )
return false;
if( fwrite( data, 1, len, f ) != len )
{
fclose( f );
return false;
}
if( fclose( f ) != 0 )
return false;
return true;
}
/*
============
FindConfig
============
*/
static char *FindConfig( const char *startdir )
{
char dir[MAX_SYSPATH];
if( startdir )
{
Q_strncpy( dir, startdir, sizeof( dir ));
}
else if( !getcwd( dir, sizeof( dir )))
{
return NULL;
}
for( ;; )
{
char path[MAX_SYSPATH];
struct stat st;
char *slash;
Q_snprintf( path, sizeof( path ), "%s/.clang-format", dir );
if( stat( path, &st ) == 0 && S_ISREG( st.st_mode ))
{
size_t len = Q_strlen( path ) + 1;
char *r = malloc( len );
if( r )
Q_strncpy( r, path, len );
return r;
}
slash = Q_strrchr( dir, '/' );
if( !slash || slash == dir )
break;
*slash = 0;
}
return NULL;
}
/*
============
LoadConfig
============
*/
static qboolean LoadConfig( const char *path, xcf_config_t *cfg )
{
xcf_buf_t buf = { 0 };
qboolean ok = false;
if( path && ReadTheFile( path, &buf ) && BufPutChar( &buf, 0 ))
ok = ParseConfigText( buf.data, cfg );
else
ParseConfigText( "", cfg );
BufFree( &buf );
return ok;
}
/*
============
Emit
Transform pass on buffer and output to file
============
*/
static qboolean Emit( const xcf_config_t *cfg, const xcf_buf_t *raw, FILE *dst, const char *write_path )
{
xcf_buf_t out = { 0 };
const char *data;
size_t len;
if( cfg->collapse_parens && Transform( raw->data, raw->len, &out ))
{
data = out.data;
len = out.len;
}
else
{
data = raw->data;
len = raw->len;
}
qboolean ok = true;
if( write_path )
{
if( !WriteTheFile( write_path, data, len ))
{
fprintf( stderr, "xash-clang-format: cannot write %s: %s\n", write_path, strerror( errno ));
ok = false;
}
}
else if( len )
{
if( fwrite( data, 1, len, dst ) != len )
ok = false;
}
BufFree( &out );
return ok;
}
/*
============
PipeMode
Process stdout (when clang-format called without -i)
============
*/
static int PipeMode( int argc, char **argv, const xcf_config_t *cfg )
{
char **fwd = malloc( sizeof( char * ) * ( argc + 1 ));
if( !fwd )
return 1;
fwd[0] = (char *)g_clang_format;
for( int i = 1; i < argc; i++ )
fwd[i] = argv[i];
fwd[argc] = NULL;
xcf_buf_t raw = { 0 };
int rc = RunCapture( fwd, &raw, false );
free( fwd );
if( rc != 0 )
{
// pass through whatever clang-format produced
if( raw.len )
fwrite( raw.data, 1, raw.len, stdout );
BufFree( &raw );
return rc < 0 ? 1 : rc;
}
rc = Emit( cfg, &raw, stdout, NULL ) ? 0 : 1;
BufFree( &raw );
return rc;
}
/*
============
PostProcessFile
Read `path`, run the paren-collapse pass, write result back.
============
*/
static qboolean PostProcessFile( const char *path )
{
xcf_buf_t in = { 0 };
xcf_buf_t out = { 0 };
qboolean ok = false;
if( ReadTheFile( path, &in ) && Transform( in.data, in.len, &out ))
ok = WriteTheFile( path, out.data, out.len );
BufFree( &in );
BufFree( &out );
return ok;
}
/*
============
InPlaceMode
Post-process mode, when clang-format is called with -i
============
*/
static int InPlaceMode( int argc, char **argv, const xcf_config_t *cfg )
{
char **fwd = malloc( sizeof( char * ) * ( argc + 1 ));
if( !fwd )
return 1;
fwd[0] = (char *)g_clang_format;
for( int i = 1; i < argc; i++ )
fwd[i] = argv[i];
fwd[argc] = NULL;
xcf_buf_t sink = { 0 };
int rc = RunCapture( fwd, &sink, false );
BufFree( &sink );
free( fwd );
if( rc != 0 )
return rc < 0 ? 1 : rc;
if( !cfg->collapse_parens )
return 0;
for( int i = 1; i < argc; i++ )
{
if( !IsFileArg( argv[i] ))
continue;
if( !PostProcessFile( argv[i] ))
{
fprintf( stderr, "xash-clang-format: post-process failed for %s\n", argv[i] );
rc = 1;
}
}
return rc;
}
static void Usage( FILE *dst )
{
fprintf( dst,
"xash-clang-format: clang-format wrapper with xash3d-fwgs extensions\n"
"\n"
"Usage: xash-clang-format [clang-format options] [<file> ...]\n"
" xash-clang-format -i <file> ...\n"
"\n"
"Most clang-format flags are forwarded as-is but there are limitations:\n"
"* --key value must be passed as --key=value\n"
"\n"
"Use XASH_CLANG_FORMAT_BIN envvar to override the clang-format binary;\n"
" setting it also skips the version probe.\n"
"\n"
"Wrapper's config is shared with clang-format config, but extended keys\n"
" start with comment hash.\n"
"New keys:\n"
" XashCollapseParens, accepts 'true' or 'false'\n" );
}
int main( int argc, char **argv )
{
g_clang_format = ResolveClangFormat();
qboolean has_inplace = false;
qboolean has_version = false;
qboolean has_help = false;
for( int i = 1; i < argc; i++ )
{
if( Q_strcmp( argv[i], "-i" ) == 0 )
has_inplace = true;
else if( Q_strcmp( argv[i], "--version" ) == 0 || Q_strcmp( argv[i], "-version" ) == 0 )
has_version = true;
else if( Q_strcmp( argv[i], "--help" ) == 0 || Q_strcmp( argv[i], "-h" ) == 0 || Q_strcmp( argv[i], "-help" ) == 0 )
has_help = true;
}
if( has_help )
{
Usage( stdout );
return 0;
}
if( has_version )
{
// transparent passthrough so tools that scrape `clang-format --version`
// (e.g. git clang-format auto-detection) keep working
execlp( g_clang_format, g_clang_format, "--version", (char *)NULL );
fprintf( stderr, "xash-clang-format: cannot exec %s: %s\n", g_clang_format, strerror( errno ));
return 127;
}
// if caller sets XASH_CLANG_FORMAT_BIN — they own version compatibility
// so skip the probe (lets ./waf format do the check once for parallel invocation)
if( COM_StringEmptyOrNULL( getenv( "XASH_CLANG_FORMAT_BIN" )))
{
int major = 0;
if( !ClangFormatVersion( g_clang_format, &major ))
{
fprintf( stderr, "xash-clang-format: cannot probe clang-format version (binary: %s)\n", g_clang_format );
return 2;
}
if( major < XCF_MIN_CLANG_FORMAT_MAJOR )
{
fprintf( stderr, "xash-clang-format: clang-format %d found, need >= %d\n",
major, XCF_MIN_CLANG_FORMAT_MAJOR );
return 2;
}
}
xcf_config_t cfg;
char *cfgpath = FindConfig( NULL );
LoadConfig( cfgpath, &cfg );
free( cfgpath );
return has_inplace ? InPlaceMode( argc, argv, &cfg ) : PipeMode( argc, argv, &cfg );
}

View File

@@ -0,0 +1,99 @@
/*
proc.c - subprocess utilities
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "xcf.h"
/*
============
RunCapture
Spawn argv as a child, capture stdout into `out`.
============
*/
int RunCapture( char *const argv[], xcf_buf_t *out, qboolean silent_stderr )
{
int p[2];
pid_t pid;
char buf[8192];
int status;
if( pipe( p ) < 0 )
return -1;
pid = fork();
if( pid < 0 )
{
close( p[0] );
close( p[1] );
return -1;
}
if( pid == 0 )
{
close( p[0] );
dup2( p[1], STDOUT_FILENO );
close( p[1] );
if( silent_stderr )
{
int devnull = open( "/dev/null", O_WRONLY );
if( devnull >= 0 )
{
dup2( devnull, STDERR_FILENO );
close( devnull );
}
}
execvp( argv[0], argv );
_exit( 127 );
}
close( p[1] );
for( ;; )
{
ssize_t r = read( p[0], buf, sizeof( buf ));
if( r < 0 )
{
if( errno == EINTR )
continue;
break;
}
if( r == 0 )
break;
if( !BufPutMem( out, buf, r ))
{
close( p[0] );
kill( pid, SIGKILL );
waitpid( pid, NULL, 0 );
return -1;
}
}
close( p[0] );
if( waitpid( pid, &status, 0 ) < 0 )
return -1;
// heh wife excited
if( WIFEXITED( status ))
return WEXITSTATUS( status );
return -1;
}

View File

@@ -0,0 +1,77 @@
#include <stdio.h>
#include "xcf.h"
static int run_case( const char *label, const char *yaml, qboolean expect )
{
xcf_config_t cfg;
ParseConfigText( yaml, &cfg );
if( cfg.collapse_parens != expect )
{
fprintf( stderr, "[%s] collapse_parens=%d, expected %d\n yaml:\n%s\n",
label, cfg.collapse_parens, expect, yaml );
return 0;
}
return 1;
}
int main( void )
{
int n = 0;
if( !run_case( "empty", "", false ))
return ++n;
xcf_config_t cfg2;
ParseConfigText( NULL, &cfg2 );
if( cfg2.collapse_parens )
return ++n;
if( !run_case( "explicit true",
"# XashCollapseParens: true\n", true ))
return ++n;
if( !run_case( "explicit false",
"# XashCollapseParens: false\n", false ))
return ++n;
if( !run_case( "leading ws",
" # XashCollapseParens: true\n", true ))
return ++n;
if( !run_case( "tabs",
"#\tXashCollapseParens\t:\ttrue\n", true ))
return ++n;
if( !run_case( "unrelated key",
"# SomeOtherKey: true\nIndentWidth: 4\n", false ))
return ++n;
if( !run_case( "in larger config",
"---\n"
"Language: Cpp\n"
"# XashCollapseParens: true\n"
"IndentWidth: 4\n",
true ))
return ++n;
if( !run_case( "junk value",
"# XashCollapseParens: maybe\n", false ))
return ++n;
if( !run_case( "no colon",
"# XashCollapseParens true\n", false ))
return ++n;
if( !run_case( "last wins",
"# XashCollapseParens: false\n# XashCollapseParens: true\n", true ))
return ++n;
if( !run_case( "no trailing nl",
"# XashCollapseParens: true", true ))
return ++n;
return 0;
}

View File

@@ -0,0 +1,171 @@
#include <stdio.h>
#include <string.h>
#include "xcf.h"
static int run_case( const char *label, const char *in, const char *expect )
{
xcf_buf_t out = { 0 };
int ok;
if( !Transform( in, strlen( in ), &out ))
{
fprintf( stderr, "[%s] Transform returned error\n", label );
BufFree( &out );
return 0;
}
ok = ( out.len == strlen( expect ) && memcmp( out.data, expect, out.len ) == 0 );
if( !ok )
{
fprintf( stderr, "[%s] mismatch\n in: %s\n out: %.*s\n expect: %s\n",
label, in, (int)out.len, out.data, expect );
}
BufFree( &out );
return ok;
}
int main( void )
{
int n = 0;
if( !run_case( "outer parens",
"if( ( a + b ) )\n",
"if(( a + b ))\n" ))
return ++n;
if( !run_case( "triple nesting",
"x = ( ( ( a ) ) );\n",
"x = ((( a )));\n" ))
return ++n;
if( !run_case( "mixed [ )",
"arr[i] )\n",
"arr[i] )\n" ))
return ++n;
if( !run_case( "mixed ) ]",
"f( a ) ];\n",
"f( a ) ];\n" ))
return ++n;
if( !run_case( "newline boundary",
"(\n(\n",
"(\n(\n" ))
return ++n;
if( !run_case( "string literal",
"x = \"( ( hello ) )\";\n",
"x = \"( ( hello ) )\";\n" ))
return ++n;
if( !run_case( "escaped quote in string",
"x = \"a \\\" ( ( b ) ) \";\n",
"x = \"a \\\" ( ( b ) ) \";\n" ))
return ++n;
if( !run_case( "line comment",
"a; // ( ( comment ) )\nb;\n",
"a; // ( ( comment ) )\nb;\n" ))
return ++n;
if( !run_case( "block comment",
"/* ( ( leave\nthis ) ) alone */ x;\n",
"/* ( ( leave\nthis ) ) alone */ x;\n" ))
return ++n;
if( !run_case( "char literal '('",
"if( c == '(' )\n",
"if( c == '(' )\n" ))
return ++n;
if( !run_case( "raw string",
"const char *s = R\"x(( (\nstays )x\";\n",
"const char *s = R\"x(( (\nstays )x\";\n" ))
return ++n;
if( !run_case( "raw string empty delim",
"R\"(( (\n))\"",
"R\"(( (\n))\"" ))
return ++n;
if( !run_case( "ident Rabbit not raw",
"int Rabbit = ( ( 1 ) );\n",
"int Rabbit = (( 1 ));\n" ))
return ++n;
if( !run_case( "u8R raw",
"const auto s = u8R\"(( ( )\";\n",
"const auto s = u8R\"(( ( )\";\n" ))
return ++n;
if( !run_case( "include angle",
"#include <foo.h>\nf( ( x ) );\n",
"#include <foo.h>\nf(( x ));\n" ))
return ++n;
if( !run_case( "tabs between parens",
"f(\t(\tx\t)\t);\n",
"f((\tx\t));\n" ))
return ++n;
if( !run_case( "empty", "", "" ))
return ++n;
if( !run_case( "idempotent",
"f((( x )));\n",
"f((( x )));\n" ))
return ++n;
if( !run_case( "already touching",
"((a))",
"((a))" ))
return ++n;
if( !run_case( "single paren",
"f( a, b )",
"f( a, b )" ))
return ++n;
if( !run_case( "multiple pairs",
"f( ( a ) ) + g( ( b ) );\n",
"f(( a )) + g(( b ));\n" ))
return ++n;
if( !run_case( "L wide string",
"wchar_t *s = L\"( ( x ) )\";\n",
"wchar_t *s = L\"( ( x ) )\";\n" ))
return ++n;
if( !run_case( "u utf16 string",
"auto s = u\"( ( x ) )\";\n",
"auto s = u\"( ( x ) )\";\n" ))
return ++n;
if( !run_case( "U utf32 string",
"auto s = U\"( ( x ) )\";\n",
"auto s = U\"( ( x ) )\";\n" ))
return ++n;
if( !run_case( "u8 utf8 string",
"auto s = u8\"( ( x ) )\";\n",
"auto s = u8\"( ( x ) )\";\n" ))
return ++n;
if( !run_case( "L wide char",
"if( c == L'(' )\n",
"if( c == L'(' )\n" ))
return ++n;
if( !run_case( "u8 char",
"if( c == u8'(' )\n",
"if( c == u8'(' )\n" ))
return ++n;
if( !run_case( "ident L not prefix",
"int callL = ( ( 1 ) );\n",
"int callL = (( 1 ));\n" ))
return ++n;
return 0;
}

View File

@@ -0,0 +1,58 @@
#include <stdio.h>
#include "xcf.h"
static int run_case( const char *label, const char *s, qboolean expect_ok, int expect_major )
{
int major = -1;
qboolean ok = ParseVersion( s, &major );
if( ok != expect_ok )
{
fprintf( stderr, "[%s] ok=%d expected %d (input: %s)\n", label, ok, expect_ok, s ? s : "(null)" );
return 0;
}
if( ok && major != expect_major )
{
fprintf( stderr, "[%s] major=%d expected %d (input: %s)\n", label, major, expect_major, s );
return 0;
}
return 1;
}
int main( void )
{
int n = 0;
if( !run_case( "vanilla 17",
"clang-format version 17.0.6 (Fedora 17.0.6-1.fc40)\n", true, 17 ))
return ++n;
if( !run_case( "ubuntu 18",
"Ubuntu clang-format version 18.1.3 (1ubuntu1)\n", true, 18 ))
return ++n;
if( !run_case( "large major",
"clang-format version 100.0.0\n", true, 100 ))
return ++n;
if( !run_case( "garbage",
"this has no version here\n", false, 0 ))
return ++n;
if( !run_case( "null", NULL, false, 0 ))
return ++n;
if( !run_case( "distro tag with digits",
"Ubuntu99 clang-format version 17.0.6\n", true, 17 ))
return ++n;
if( !run_case( "bare digits",
"foo 42 bar\n", false, 0 ))
return ++n;
if( !run_case( "no version word",
"fake clang-format 19.1.0\n", false, 0 ))
return ++n;
return 0;
}

View File

@@ -0,0 +1,325 @@
/*
transform.c - source post-processing pass
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "xcf.h"
#define XCF_RAW_DELIM_MAX 16 // C++11: 16 chars max in raw-string delimiter
/*
============
BufFree
============
*/
void BufFree( xcf_buf_t *b )
{
free( b->data );
b->data = NULL;
b->len = b->cap = 0;
}
/*
============
BufGrow
============
*/
static qboolean BufGrow( xcf_buf_t *b, size_t need )
{
size_t cap;
char *p;
if( b->len + need <= b->cap )
return true;
cap = b->cap ? b->cap : 256;
while( cap < b->len + need )
cap *= 2;
p = realloc( b->data, cap );
if( !p )
return false;
b->data = p;
b->cap = cap;
return true;
}
/*
============
BufPutChar
============
*/
qboolean BufPutChar( xcf_buf_t *b, char c )
{
if( !BufGrow( b, 1 ))
return false;
b->data[b->len++] = c;
return true;
}
/*
============
BufPutMem
============
*/
qboolean BufPutMem( xcf_buf_t *b, const char *src, size_t n )
{
if( !n )
return true;
if( !BufGrow( b, n ))
return false;
memcpy( b->data + b->len, src, n );
b->len += n;
return true;
}
/*
============
TryCollapse
Collapse spaces between the same type parentheses
============
*/
static void TryCollapse( xcf_buf_t *out, char paren )
{
size_t i = out->len;
while( i > 0 && ( out->data[i - 1] == ' ' || out->data[i - 1] == '\t' ))
i--;
if( i == 0 || i == out->len )
return; // nothing to collapse, or no whitespace between
if( out->data[i - 1] != paren )
return; // different bracket types or different char entirely
out->len = i;
}
/*
============
DetectRaw
Detect C++11 raw string because why not
============
*/
static qboolean DetectRaw( const char *s, size_t len, size_t i, size_t *prefix_end, size_t *paren )
{
size_t j = i, k;
// must not be in the middle of an identifier
if( i > 0 && ( isalnum( s[i - 1] ) || s[i - 1] == '_' ))
return false;
if( j + 2 < len && s[j] == 'u' && s[j + 1] == '8' && s[j + 2] == 'R' )
j += 3;
else if( j + 1 < len && ( s[j] == 'u' || s[j] == 'U' || s[j] == 'L' ) && s[j + 1] == 'R' )
j += 2;
else if( j < len && s[j] == 'R' )
j += 1;
else
return false;
if( j >= len || s[j] != '"' )
return false;
j++; // past the opening quote
*prefix_end = j;
k = j;
while( k < len && s[k] != '(' && k - j < XCF_RAW_DELIM_MAX )
k++;
if( k >= len || s[k] != '(' )
return false;
*paren = k;
return true;
}
enum
{
S_NORMAL,
S_LINE_CMT,
S_BLOCK_CMT,
S_STRING,
S_CHAR,
S_RAW,
};
/*
============
Transform
Parse source, find parens and try collapse them
============
*/
qboolean Transform( const char *in, size_t in_len, xcf_buf_t *out )
{
size_t raw_dlen = 0;
char raw_delim[XCF_RAW_DELIM_MAX + 1];
int state = S_NORMAL;
for( size_t i = 0; i < in_len; i++ )
{
char c1 = in[i];
char c2 = ( i + 1 < in_len ) ? in[i + 1] : 0;
switch( state )
{
case S_NORMAL:
{
size_t pe, pp;
if( c1 == '/' && c2 == '/' )
{
if( !BufPutMem( out, in + i, 2 ))
return false;
i++;
state = S_LINE_CMT;
break;
}
if( c1 == '/' && c2 == '*' )
{
if( !BufPutMem( out, in + i, 2 ))
return false;
i++;
state = S_BLOCK_CMT;
break;
}
if( DetectRaw( in, in_len, i, &pe, &pp ))
{
size_t dl = pp - pe;
if( dl > XCF_RAW_DELIM_MAX )
dl = XCF_RAW_DELIM_MAX;
if( !BufPutMem( out, in + i, pp - i + 1 ))
return false;
memcpy( raw_delim, in + pe, dl );
raw_delim[dl] = 0;
raw_dlen = dl;
i = pp; // sit on the '('
state = S_RAW;
break;
}
if( c1 == '"' )
{
if( !BufPutChar( out, c1 ))
return false;
state = S_STRING;
break;
}
if( c1 == '\'' )
{
if( !BufPutChar( out, c1 ))
return false;
state = S_CHAR;
break;
}
if( c1 == '(' || c1 == ')' )
TryCollapse( out, c1 );
if( !BufPutChar( out, c1 ))
return false;
break;
}
case S_LINE_CMT:
if( !BufPutChar( out, c1 ))
return false;
if( c1 == '\n' )
state = S_NORMAL;
break;
case S_BLOCK_CMT:
if( !BufPutChar( out, c1 ))
return false;
if( c1 == '*' && c2 == '/' )
{
if( !BufPutChar( out, c2 ))
return false;
i++;
state = S_NORMAL;
}
break;
case S_STRING:
if( !BufPutChar( out, c1 ))
return false;
if( c1 == '\\' && c2 )
{
if( !BufPutChar( out, c2 ))
return false;
i++;
}
else if( c1 == '"' )
{
state = S_NORMAL;
}
else if( c1 == '\n' )
{
// unterminated; recover at next line
state = S_NORMAL;
}
break;
case S_CHAR:
if( !BufPutChar( out, c1 ))
return false;
if( c1 == '\\' && c2 )
{
if( !BufPutChar( out, c2 ))
return false;
i++;
}
else if( c1 == '\'' )
{
state = S_NORMAL;
}
else if( c1 == '\n' )
{
state = S_NORMAL;
}
break;
case S_RAW:
if( !BufPutChar( out, c1 ))
return false;
if( c1 == ')'
&& i + 1 + raw_dlen + 1 <= in_len
&& memcmp( in + i + 1, raw_delim, raw_dlen ) == 0
&& in[i + 1 + raw_dlen] == '"' )
{
if( !BufPutMem( out, in + i + 1, raw_dlen + 1 ))
return false;
i += raw_dlen + 1;
state = S_NORMAL;
}
break;
}
}
return true;
}

View File

@@ -0,0 +1,67 @@
/*
version.c - clang-format version probe
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "crtlib.h"
#include "xcf.h"
/*
============
ParseVersion
Parse the major version number out of a `clang-format --version`
============
*/
qboolean ParseVersion( const char *s, int *major )
{
const char *p;
if( COM_StringEmptyOrNULL( s ) || !major )
return false;
// anchor on "version "
p = Q_strstr( s, "version " );
if( !p )
return false;
p += sizeof( "version " ) - 1;
while( *p == ' ' || *p == '\t' )
p++;
if( *p < '0' || *p > '9' )
return false;
*major = Q_atoi( p );
return true;
}
/*
============
ClangFormatVersion
============
*/
qboolean ClangFormatVersion( const char *exe, int *major )
{
char *argv[] = { (char *)exe, (char *)"--version", NULL };
xcf_buf_t out = { 0 };
qboolean ok = false;
if( !exe || !major )
return false;
if( RunCapture( argv, &out, true ) == 0 && BufPutChar( &out, 0 ))
ok = ParseVersion( out.data, major );
BufFree( &out );
return ok;
}

View File

@@ -0,0 +1,42 @@
#! /usr/bin/env python
# encoding: utf-8
def options(opt):
grp = opt.get_option_group('Utilities options')
grp.add_option('--disable-utils-xcf', action='store_true',
dest='DISABLE_UTILS_XCF', default = False,
help='disable xash-clang-format wrapper utility [default: %(default)s]')
def configure(conf):
# wrapper relies on POSIX, disable on Windows for now
conf.env.DISABLE_UTILS_XCF = conf.options.DISABLE_UTILS_XCF or conf.env.DEST_OS == 'win32'
def build(bld):
if bld.env.DISABLE_UTILS_XCF:
return
bld.stlib(source = bld.path.ant_glob(['*.c', '*.h'], excl='main.c'),
target = 'xcflib',
features = 'format',
includes = '.',
use = 'public werror',
install_path = None)
bld.program(source = 'main.c',
target = 'xash-clang-format',
features = 'format',
use = 'xcflib werror',
install_path = bld.env.BINDIR,
subsystem = bld.env.CONSOLE_SUBSYSTEM)
if bld.env.TESTS:
tests = ['transform', 'config', 'version']
for name in tests:
bld.program(features = 'test format',
source = 'tests/test_%s.c' % name,
target = 'test_xcflib_%s' % name,
includes = '.',
use = 'xcflib werror',
install_path = None)

View File

@@ -0,0 +1,57 @@
/*
xcf.h - clang-format wrapper internals
Copyright (C) 2026 Xash3D FWGS contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#ifndef XCF_H
#define XCF_H
#include <stddef.h>
#include "xash3d_types.h"
//
// transform.c
//
typedef struct
{
char *data;
size_t len;
size_t cap;
} xcf_buf_t;
void BufFree( xcf_buf_t *b );
qboolean BufPutChar( xcf_buf_t *b, char c );
qboolean BufPutMem( xcf_buf_t *b, const char *src, size_t n );
qboolean Transform( const char *in, size_t in_len, xcf_buf_t *out );
//
// config.c
//
typedef struct
{
qboolean collapse_parens; // default: false
} xcf_config_t;
qboolean ParseConfigText( const char *text, xcf_config_t *cfg );
//
// version.c
//
qboolean ParseVersion( const char *s, int *major );
qboolean ClangFormatVersion( const char *exe, int *major );
//
// proc.c
//
int RunCapture( char *const argv[], xcf_buf_t *out, qboolean silent_stderr );
#endif // XCF_H