summaryrefslogtreecommitdiff
path: root/create_c_project.py
diff options
context:
space:
mode:
Diffstat (limited to 'create_c_project.py')
-rwxr-xr-xcreate_c_project.py647
1 files changed, 647 insertions, 0 deletions
diff --git a/create_c_project.py b/create_c_project.py
new file mode 100755
index 0000000..b3ba6dd
--- /dev/null
+++ b/create_c_project.py
@@ -0,0 +1,647 @@
+#!/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 <<EOF
+Configure script for {project_name}
+
+Options:
+ --prefix=PREFIX Installation prefix (default: /usr/local)
+ --debug Build with debug symbols, no optimization
+ --release Build with optimizations (default)
+ --help Show this help message
+
+Environment variables:
+ CC C compiler
+ AR Archiver
+ RANLIB ranlib
+ CFLAGS Additional C flags
+ LDFLAGS Additional linker flags
+EOF
+ exit 0
+ ;;
+ *)
+ die "Unknown option: $1"
+ ;;
+ esac
+ shift
+done
+
+# Find required tools
+if [ -z "$CC" ]; then
+ CC=$(find_program CC cc clang gcc) || die "No C compiler found"
+fi
+
+# Use basename only for better tool compatibility (e.g., bear)
+CC=$(basename "$CC")
+
+# Detect compiler type
+if $CC --version 2>&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 <<EOF_CONFIG
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define PACKAGE_VERSION "$PACKAGE_VERSION"
+#define PACKAGE_STRING "$PACKAGE_STRING"
+#define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT"
+#define PACKAGE_URL "$PACKAGE_URL"
+
+#endif /* CONFIG_H */
+EOF_CONFIG
+
+# Determine source directory
+SRCDIR="$(dirname "$0")"
+SRCDIR="$(cd "$SRCDIR" && pwd)"
+
+# Generate Makefile from Makefile.in
+sed \\
+ -e "s|@PREFIX@|$PREFIX|g" \\
+ -e "s|@CC@|$CC|g" \\
+ -e "s|@CCOM@|$CCOM|g" \\
+ -e "s|@AR@|$AR|g" \\
+ -e "s|@RANLIB@|$RANLIB|g" \\
+ -e "s|@RM@|$RM|g" \\
+ -e "s|@INSTALL@|$INSTALL|g" \\
+ -e "s|@OPTIMISE@|$OPTIMISE|g" \\
+ -e "s|@VERSION@|$PACKAGE_VERSION|g" \\
+ -e "s|@SRCDIR@|$SRCDIR|g" \\
+ < "$SRCDIR/Makefile.in" > 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 <stdio.h>
+#include <stdlib.h>
+#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()) \ No newline at end of file