#!/usr/bin/env python3 """ Create a C project structure based on the gcli build system pattern. Simplified version with minimal files. """ import argparse import os import sys from pathlib import Path import urllib.request def create_configure_script(project_name, version): """Generate a configure script.""" return f'''#!/usr/bin/env sh # # Configure script for {project_name} # CONFIGURE_CMD_ARGS="${{*}}" PACKAGE_VERSION="{version}" PACKAGE_DATE="$(date +%Y-%m-%d)" PACKAGE_STRING="{project_name} $PACKAGE_VERSION" PACKAGE_BUGREPORT="your-email@example.com" PACKAGE_URL="https://example.com/{project_name}" find_program() {{ varname=$1 shift printf "Checking for $varname ..." >&2 for x in $*; do if command -v $x >/dev/null 2>&1 && [ -x $(command -v $x) ]; then binary="$(command -v $x)" printf " $binary\\n" >&2 echo "${{binary}}" return 0 fi done printf " not found\\n" >&2 return 1 }} die() {{ printf "%s\\n" "${{*}}" exit 1 }} # Default values PREFIX="/usr/local" OPTIMISE="release" CC="" AR="ar" RANLIB="ranlib" RM="rm" INSTALL="install" # Parse arguments while [ $# -gt 0 ]; do case "$1" in --prefix=*) PREFIX="${{1#--prefix=}}" ;; --prefix) shift PREFIX="$1" ;; --debug) OPTIMISE="debug" ;; --release) OPTIMISE="release" ;; --help) cat <&1 | grep -i clang >/dev/null; then CCOM="clang" elif $CC --version 2>&1 | grep -i gcc >/dev/null; then CCOM="gcc" else CCOM="unknown" fi printf "Compiler: %s (%s)\\n" "$CC" "$CCOM" # Generate config.h cat > config.h < Makefile echo "Configuration complete. Run 'make' to build." ''' def create_makefile_in(project_name): """Generate a Makefile.in template.""" return f'''# Makefile for {project_name} VERSION = @VERSION@ # Environment and values saved by the configure script CC = @CC@ CCOM = @CCOM@ AR = @AR@ RANLIB = @RANLIB@ RM = @RM@ INSTALL = @INSTALL@ OPTIMISE = @OPTIMISE@ PREFIX = @PREFIX@ SRCDIR = @SRCDIR@ # Find all .c files in src/ recursively SRCS := $(shell find $(SRCDIR)/src -name '*.c' -type f) # Generate object file names (strip SRCDIR/src/ prefix and change .c to .o) OBJS := $(patsubst $(SRCDIR)/src/%.c,%.o,$(SRCS)) # VPATH for finding source files VPATH = $(SRCDIR)/src:$(shell find $(SRCDIR)/src -type d 2>/dev/null | tr '\\n' ':') # Compiler flags COPTFLAGS_gcc_debug = -O0 -g3 -Wall -Wextra COPTFLAGS_gcc_release = -O2 -DNDEBUG COPTFLAGS_clang_debug = -O0 -g3 -Wall -Wextra COPTFLAGS_clang_release = -O2 -DNDEBUG COPTFLAGS = $(COPTFLAGS_$(CCOM)_$(OPTIMISE)) CSTDFLAGS_gcc = -std=c11 -pedantic CSTDFLAGS_clang = -std=c11 -pedantic CSTDFLAGS = $(CSTDFLAGS_$(CCOM)) CFLAGS = $(CSTDFLAGS) $(COPTFLAGS) -I$(SRCDIR)/include -I$(SRCDIR)/thirdparty/utest -I. -DHAVE_CONFIG_H=1 LDFLAGS = # Targets BINARY = {project_name} .PHONY: all clean install check all: $(BINARY) # Create subdirectories for object files if needed $(BINARY): $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) # Suffix rule for compiling (uses VPATH to find .c files) .SUFFIXES: .c .o .c.o: @test -d $(dir $@) || mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -o $@ $< clean: $(RM) -f $(BINARY) $(OBJS) $(RM) -f test_main test_main.o find . -type f -name '*.o' -delete 2>/dev/null || true install: $(BINARY) $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin $(INSTALL) -m 755 $(BINARY) $(DESTDIR)$(PREFIX)/bin/ # Test target - links with all objects except main.o TEST_OBJS := $(filter-out main.o,$(OBJS)) check: test_main ./test_main test_main: test_main.o $(TEST_OBJS) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ test_main.o: $(SRCDIR)/tests/test_main.c $(SRCDIR)/include/{project_name}/base.h $(CC) $(CFLAGS) -c -o $@ $< ''' def create_main_c(project_name): """Generate a main.c file.""" return f'''#include #include #include "{project_name}/base.h" #include "config.h" int main(int argc, char **argv) {{ (void)argc; (void)argv; printf("%s version %s\\n", PACKAGE_STRING, PACKAGE_VERSION); /* Call a function from base.h */ {project_name}_init(); printf("Hello from {project_name}!\\n"); {project_name}_cleanup(); return 0; }} ''' def create_base_c(project_name): """Generate a base.c implementation file.""" return f'''#include "{project_name}/base.h" void {project_name}_init(void) {{ /* Initialize resources here */ }} void {project_name}_cleanup(void) {{ /* Cleanup resources here */ }} ''' def create_base_h(project_name): """Generate a base.h header file.""" guard = f"{project_name.upper()}_BASE_H" return f'''#ifndef {guard} #define {guard} /** * Initialize {project_name} */ void {project_name}_init(void); /** * Cleanup {project_name} resources */ void {project_name}_cleanup(void); #endif /* {guard} */ ''' def create_test_c(project_name): """Generate a test file using utest.h.""" return f'''#include "utest.h" #include "{project_name}/base.h" UTEST({project_name}, init_cleanup) {{ {project_name}_init(); {project_name}_cleanup(); ASSERT_TRUE(1); }} UTEST({project_name}, basic_test) {{ ASSERT_EQ(1, 1); ASSERT_NE(1, 0); ASSERT_TRUE(1); ASSERT_FALSE(0); }} UTEST_MAIN() ''' def create_readme(project_name, version): """Generate a README.md file.""" return f'''# {project_name.upper()} Version {version} ## Description A C project created with the gcli-style build system. ## Building ### Dependencies Required: - C11 compiler (gcc or clang) - make ### Build Instructions ```bash ./configure make ``` ### Debug Build ```bash ./configure --debug make ``` ### Out-of-tree builds (optional) You can also build in a separate directory: ```bash mkdir build cd build ../configure make ``` ### Installation ```bash sudo make install ``` Default prefix is `/usr/local`. To change: ```bash ./configure --prefix=/usr ``` ## Testing ```bash make check ``` ## IDE Support ### clangd / LSP To generate `compile_commands.json` for clangd, use one of these methods: **Option 1: Using compiledb (recommended)** ```bash pip install compiledb compiledb make ``` **Option 2: Using bear** ```bash bear -- make ``` If the file is empty, try a clean build: ```bash make clean bear -- make ``` ## Usage ```bash ./{project_name} ``` ''' def create_gitignore(): """Generate a .gitignore file.""" return '''# Build artifacts *.o *.a *.so *.dylib *.dll *.exe # Build directories build/ debug/ release/ dist/ # Generated files config.h Makefile # Editor files .vscode/ .idea/ *.swp *.swo *~ # OS files .DS_Store Thumbs.db ''' def create_clangd_config(): """Generate a .clangd configuration file.""" return '''# clangd configuration CompileFlags: Add: - -Wall - -Wextra Diagnostics: UnusedIncludes: Strict MissingIncludes: Strict ''' def create_license(project_name): """Generate a simple LICENSE file.""" import datetime year = datetime.datetime.now().year return f'''Copyright (c) {year}, {project_name} authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' def create_gentarball_sh(project_name): """Generate a gentarball.sh script for creating release tarballs.""" return f'''#!/bin/sh # # Generate release tarballs for {project_name} command -v git > /dev/null 2>&1 || (echo "error: you need git to run this script" && exit 1) findversion() {{ eval $(grep PACKAGE_VERSION $(dirname $0)/../configure | sed 1q) echo $PACKAGE_VERSION }} VERSION=$(findversion) DIR=dist/{project_name}-${{VERSION}} mkdir -p $DIR if [ -d .got ]; then repodir=$(got info | grep '^repository' | cut -d: -f2 | xargs) head=$(got br) else repodir=$(git rev-parse --show-toplevel)/.git head=@ fi echo "Making BZIP tarball" git --git-dir="${{repodir}}" archive --format=tar --prefix={project_name}-$VERSION/ $head \\ | bzip2 -v > $DIR/{project_name}-$VERSION.tar.bz2 echo "Making XZ tarball" git --git-dir="${{repodir}}" archive --format=tar --prefix={project_name}-$VERSION/ $head \\ | xz -v > $DIR/{project_name}-$VERSION.tar.xz echo "Making GZIP tarball" git --git-dir="${{repodir}}" archive --format=tar --prefix={project_name}-$VERSION/ $head \\ | gzip -v > $DIR/{project_name}-$VERSION.tar.gz ( cd $DIR echo "Calculating SHA256SUMS" sha256sum *.tar* > SHA256SUMS ) echo "Release Tarballs are at $DIR" ''' def create_project(project_name, version, output_dir): """Create the complete project structure.""" project_path = Path(output_dir) / project_name if project_path.exists(): print(f"Error: Directory {project_path} already exists!", file=sys.stderr) return False print(f"Creating project: {project_name} v{version}") print(f"Output directory: {project_path}") # Create directory structure dirs = [ project_path, project_path / "src", project_path / "include" / project_name, project_path / "tests", project_path / "thirdparty" / "utest", project_path / "tools", ] for dir_path in dirs: dir_path.mkdir(parents=True, exist_ok=True) print(f" Created: {dir_path.relative_to(output_dir)}/") # Create files files = { project_path / "configure": create_configure_script(project_name, version), project_path / "Makefile.in": create_makefile_in(project_name), project_path / "src" / "main.c": create_main_c(project_name), project_path / "src" / "base.c": create_base_c(project_name), project_path / "include" / project_name / "base.h": create_base_h(project_name), project_path / "tests" / "test_main.c": create_test_c(project_name), project_path / "tools" / "gentarball.sh": create_gentarball_sh(project_name), project_path / "README.md": create_readme(project_name, version), project_path / ".gitignore": create_gitignore(), project_path / ".clangd": create_clangd_config(), project_path / "LICENSE": create_license(project_name), } for file_path, content in files.items(): file_path.write_text(content) print(f" Created: {file_path.relative_to(output_dir)}") # Download utest.h print(" Downloading utest.h...") utest_url = "https://raw.githubusercontent.com/sheredom/utest.h/master/utest.h" utest_path = project_path / "thirdparty" / "utest" / "utest.h" try: with urllib.request.urlopen(utest_url) as response: utest_content = response.read().decode('utf-8') utest_path.write_text(utest_content) print(f" Created: {utest_path.relative_to(output_dir)}") except Exception as e: print(f" Warning: Could not download utest.h: {e}", file=sys.stderr) print(f" You can manually download it from {utest_url}", file=sys.stderr) # Make configure executable (project_path / "configure").chmod(0o755) (project_path / "tools" / "gentarball.sh").chmod(0o755) print(f"\\n✓ Project created successfully!") print(f"\\nNext steps:") print(f" cd {project_path}") print(f" ./configure") print(f" make") print(f" ./{project_name}") return True def main(): parser = argparse.ArgumentParser( description="Create a C project with gcli-style build system", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s myproject %(prog)s myproject --version 0.1.0 %(prog)s myproject --output ~/projects """ ) parser.add_argument( "name", help="Project name (lowercase, no spaces)" ) parser.add_argument( "--version", default="0.1.0", help="Initial version number (default: 0.1.0)" ) parser.add_argument( "--output", default=".", help="Output directory (default: current directory)" ) args = parser.parse_args() # Validate project name if not args.name.replace("_", "").replace("-", "").isalnum(): print("Error: Project name must contain only letters, numbers, hyphens, and underscores", file=sys.stderr) return 1 if not args.name.islower(): print("Warning: Project name should be lowercase", file=sys.stderr) # Create the project if create_project(args.name, args.version, args.output): return 0 else: return 1 if __name__ == "__main__": sys.exit(main())