From 382303a7b99ebc6b06f0319b253b245b5256b650 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Mon, 24 Mar 2025 20:48:40 +0600 Subject: [PATCH 1/4] Create FUNDING.yml --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..22c9a3a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [itsfuad] +buy_me_a_coffee: itsfuad From 91d7b200e0b1053ff5ad235230e8270e6cf6f0ef Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Tue, 25 Mar 2025 16:48:33 +0600 Subject: [PATCH 2/4] Refactor generator and compiler detection: switch to html/template for Python binding security and validate compiler paths before execution. --- binding/generator.go | 4 ++-- compiler/detect.go | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/binding/generator.go b/binding/generator.go index e322e10..a0c6d98 100644 --- a/binding/generator.go +++ b/binding/generator.go @@ -2,10 +2,10 @@ package binding import ( "fmt" + "html/template" "os" "path/filepath" "runtime" - "text/template" "cp2p/config" ) @@ -57,7 +57,7 @@ func (g *Generator) generate() error { } func (g *Generator) generateBindingCode(file *os.File) error { - // Define the template for the Python binding + // Define the template for the Python binding using html/template for security tmpl := template.Must(template.New("binding").Parse(pythonBindingTemplate)) // Define type mappings diff --git a/compiler/detect.go b/compiler/detect.go index 783c422..f8fc224 100644 --- a/compiler/detect.go +++ b/compiler/detect.go @@ -94,13 +94,14 @@ func detectUnixCompiler() (*CompilerInfo, error) { } func checkGCC() (*CompilerInfo, error) { - cmd := exec.Command("g++", "--version") - output, err := cmd.Output() + // Validate g++ path + path, err := exec.LookPath("g++") if err != nil { return nil, err } - path, err := exec.LookPath("g++") + cmd := exec.Command(path, "--version") + output, err := cmd.Output() if err != nil { return nil, err } @@ -113,13 +114,14 @@ func checkGCC() (*CompilerInfo, error) { } func checkClang() (*CompilerInfo, error) { - cmd := exec.Command("clang++", "--version") - output, err := cmd.Output() + // Validate clang++ path + path, err := exec.LookPath("clang++") if err != nil { return nil, err } - path, err := exec.LookPath("clang++") + cmd := exec.Command(path, "--version") + output, err := cmd.Output() if err != nil { return nil, err } From 48f2f30a8d9e0cf9cdb6d9f10c21b1231c094c7f Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Tue, 1 Apr 2025 23:41:28 +0600 Subject: [PATCH 3/4] Enhance compiler path validation and execution: Introduce context for command execution and ensure absolute paths for compiler and batch files are validated before running commands. --- compiler/compile.go | 17 +++++++++++-- compiler/detect.go | 58 ++++++++++++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/compiler/compile.go b/compiler/compile.go index 44ca7fe..691e61d 100644 --- a/compiler/compile.go +++ b/compiler/compile.go @@ -1,6 +1,7 @@ package compiler import ( + "context" "fmt" "os" "os/exec" @@ -62,7 +63,13 @@ call "%s" %s } // Run the batch file - cmd := exec.Command(compiler.EnvSetup.SetupCmd, batchFile) + // Validate paths are safe + if !filepath.IsAbs(compiler.EnvSetup.SetupCmd) || !filepath.IsAbs(batchFile) { + return "", fmt.Errorf("invalid command or batch file path") + } + + ctx := context.Background() + cmd := exec.CommandContext(ctx, compiler.EnvSetup.SetupCmd, batchFile) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { @@ -72,7 +79,13 @@ call "%s" %s } // For compilers that don't need environment setup, run directly - cmd := exec.Command(compiler.Path, args...) + // Validate compiler path is safe + if !filepath.IsAbs(compiler.Path) { + return "", fmt.Errorf("invalid compiler path: %s", compiler.Path) + } + + ctx := context.Background() + cmd := exec.CommandContext(ctx, compiler.Path, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/compiler/detect.go b/compiler/detect.go index f8fc224..76753f3 100644 --- a/compiler/detect.go +++ b/compiler/detect.go @@ -1,6 +1,7 @@ package compiler import ( + "context" "fmt" "os" "os/exec" @@ -9,6 +10,10 @@ import ( "strings" ) +const ( + ErrInvalidCompilerPath = "invalid compiler path: %s" +) + // CompilerType represents the type of C++ compiler type CompilerType string @@ -100,7 +105,13 @@ func checkGCC() (*CompilerInfo, error) { return nil, err } - cmd := exec.Command(path, "--version") + // Validate path is safe + if !filepath.IsAbs(path) || !strings.HasPrefix(path, "/") { + return nil, fmt.Errorf(ErrInvalidCompilerPath, path) + } + + ctx := context.Background() + cmd := exec.CommandContext(ctx, path, "--version") output, err := cmd.Output() if err != nil { return nil, err @@ -120,7 +131,13 @@ func checkClang() (*CompilerInfo, error) { return nil, err } - cmd := exec.Command(path, "--version") + // Validate path is safe + if !filepath.IsAbs(path) || !strings.HasPrefix(path, "/") { + return nil, fmt.Errorf(ErrInvalidCompilerPath, path) + } + + ctx := context.Background() + cmd := exec.CommandContext(ctx, path, "--version") output, err := cmd.Output() if err != nil { return nil, err @@ -161,6 +178,20 @@ func findSDKIncludePath() string { return "" } +func findVisualStudioPath(clPath string) string { + dir := filepath.Dir(clPath) + for { + if dir == "" || dir == "." || dir == "/" { + break + } + if strings.Contains(filepath.Base(dir), "Microsoft Visual Studio") { + return dir + } + dir = filepath.Dir(dir) + } + return "" +} + func checkMSVC() (*CompilerInfo, error) { // First check if cl.exe is available path, err := exec.LookPath("cl.exe") @@ -168,26 +199,21 @@ func checkMSVC() (*CompilerInfo, error) { return nil, err } + // Validate path is safe + if !filepath.IsAbs(path) { + return nil, fmt.Errorf(ErrInvalidCompilerPath, path) + } + // Get the version info from cl.exe - cmd := exec.Command("cl.exe") + ctx := context.Background() + cmd := exec.CommandContext(ctx, path) output, err := cmd.Output() if err != nil { return nil, err } - // Find Visual Studio path by looking for cl.exe's parent directory - vsPath := "" - dir := filepath.Dir(path) - for { - if dir == "" || dir == "." || dir == "/" { - break - } - if strings.Contains(filepath.Base(dir), "Microsoft Visual Studio") { - vsPath = dir - break - } - dir = filepath.Dir(dir) - } + // Find Visual Studio path + vsPath := findVisualStudioPath(path) includePaths := []string{} var envSetup *CompilerEnvSetup From 61ff13b3f588488afacf76848f2609c9387f428f Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Wed, 2 Apr 2025 00:04:11 +0600 Subject: [PATCH 4/4] Update .gitignore and README for improved clarity and functionality: Add *.obj to .gitignore and enhance README with clearer installation and usage instructions, including automatic compiler detection and updated command line arguments. --- .gitignore | 1 + compiler/detect.go | 142 ++++++++---------- compiler/detect_test.go | 320 ++++++++++++++++++++++++++++++++++++++++ readme.md | 209 +++++++++++++------------- 4 files changed, 488 insertions(+), 184 deletions(-) create mode 100644 compiler/detect_test.go diff --git a/.gitignore b/.gitignore index 672feff..894f9ca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ bindings *.lib *.exp *.def +*.obj *.manifest \ No newline at end of file diff --git a/compiler/detect.go b/compiler/detect.go index 76753f3..8105ab8 100644 --- a/compiler/detect.go +++ b/compiler/detect.go @@ -2,16 +2,22 @@ package compiler import ( "context" + "errors" "fmt" "os" "os/exec" "path/filepath" "runtime" - "strings" ) const ( ErrInvalidCompilerPath = "invalid compiler path: %s" + ErrNoCompilerFound = "no supported compiler found" + ErrNoWindowsCompiler = "no supported compiler found on Windows" + ErrUnsupportedOS = "unsupported operating system: %s" + ErrUnsupportedCompiler = "unsupported compiler type: %s" + ErrCompilerNotFound = "compiler not found: %s" + ErrVersionCheckFailed = "failed to get compiler version: %v" ) // CompilerType represents the type of C++ compiler @@ -53,7 +59,7 @@ func DetectCompiler(preferred CompilerType) (*CompilerInfo, error) { case "linux", "darwin": return detectUnixCompiler() default: - return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + return nil, fmt.Errorf(ErrUnsupportedOS, runtime.GOOS) } } @@ -64,9 +70,12 @@ func detectSpecificCompiler(compiler CompilerType) (*CompilerInfo, error) { case CompilerClang: return checkClang() case CompilerMSVC: + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("MSVC compiler is only supported on Windows") + } return checkMSVC() default: - return nil, fmt.Errorf("unsupported compiler type: %s", compiler) + return nil, fmt.Errorf(ErrUnsupportedCompiler, compiler) } } @@ -81,7 +90,7 @@ func detectWindowsCompiler() (*CompilerInfo, error) { return info, nil } - return nil, fmt.Errorf("no supported compiler found on Windows") + return nil, errors.New(ErrNoWindowsCompiler) } func detectUnixCompiler() (*CompilerInfo, error) { @@ -95,18 +104,30 @@ func detectUnixCompiler() (*CompilerInfo, error) { return info, nil } - return nil, fmt.Errorf("no supported compiler found") + return nil, errors.New(ErrNoCompilerFound) } func checkGCC() (*CompilerInfo, error) { - // Validate g++ path - path, err := exec.LookPath("g++") + // Try different possible GCC names based on OS + compilerNames := []string{"g++", "gcc"} + if runtime.GOOS == "windows" { + compilerNames = append(compilerNames, "mingw32-g++", "x86_64-w64-mingw32-g++") + } + + var path string + var err error + for _, name := range compilerNames { + path, err = exec.LookPath(name) + if err == nil { + break + } + } if err != nil { - return nil, err + return nil, fmt.Errorf(ErrCompilerNotFound, "g++") } // Validate path is safe - if !filepath.IsAbs(path) || !strings.HasPrefix(path, "/") { + if !filepath.IsAbs(path) { return nil, fmt.Errorf(ErrInvalidCompilerPath, path) } @@ -114,7 +135,7 @@ func checkGCC() (*CompilerInfo, error) { cmd := exec.CommandContext(ctx, path, "--version") output, err := cmd.Output() if err != nil { - return nil, err + return nil, fmt.Errorf(ErrVersionCheckFailed, err) } return &CompilerInfo{ @@ -125,14 +146,26 @@ func checkGCC() (*CompilerInfo, error) { } func checkClang() (*CompilerInfo, error) { - // Validate clang++ path - path, err := exec.LookPath("clang++") + // Try different possible Clang names based on OS + compilerNames := []string{"clang++", "clang"} + if runtime.GOOS == "windows" { + compilerNames = append(compilerNames, "llvm-clang++") + } + + var path string + var err error + for _, name := range compilerNames { + path, err = exec.LookPath(name) + if err == nil { + break + } + } if err != nil { - return nil, err + return nil, fmt.Errorf(ErrCompilerNotFound, "clang++") } // Validate path is safe - if !filepath.IsAbs(path) || !strings.HasPrefix(path, "/") { + if !filepath.IsAbs(path) { return nil, fmt.Errorf(ErrInvalidCompilerPath, path) } @@ -140,7 +173,7 @@ func checkClang() (*CompilerInfo, error) { cmd := exec.CommandContext(ctx, path, "--version") output, err := cmd.Output() if err != nil { - return nil, err + return nil, fmt.Errorf(ErrVersionCheckFailed, err) } return &CompilerInfo{ @@ -150,53 +183,11 @@ func checkClang() (*CompilerInfo, error) { }, nil } -func findMSVCIncludePath(vsPath string) string { - msvcPath := filepath.Join(vsPath, "VC\\Tools\\MSVC") - entries, err := os.ReadDir(msvcPath) - if err != nil { - return "" - } - for _, entry := range entries { - if entry.IsDir() { - return filepath.Join(msvcPath, entry.Name(), "include") - } - } - return "" -} - -func findSDKIncludePath() string { - sdkPath := "C:\\Program Files (x86)\\Windows Kits\\10\\Include" - entries, err := os.ReadDir(sdkPath) - if err != nil { - return "" - } - for _, entry := range entries { - if entry.IsDir() { - return filepath.Join(sdkPath, entry.Name(), "ucrt") - } - } - return "" -} - -func findVisualStudioPath(clPath string) string { - dir := filepath.Dir(clPath) - for { - if dir == "" || dir == "." || dir == "/" { - break - } - if strings.Contains(filepath.Base(dir), "Microsoft Visual Studio") { - return dir - } - dir = filepath.Dir(dir) - } - return "" -} - func checkMSVC() (*CompilerInfo, error) { // First check if cl.exe is available path, err := exec.LookPath("cl.exe") if err != nil { - return nil, err + return nil, fmt.Errorf(ErrCompilerNotFound, "cl.exe") } // Validate path is safe @@ -209,31 +200,23 @@ func checkMSVC() (*CompilerInfo, error) { cmd := exec.CommandContext(ctx, path) output, err := cmd.Output() if err != nil { - return nil, err + return nil, fmt.Errorf(ErrVersionCheckFailed, err) } - // Find Visual Studio path - vsPath := findVisualStudioPath(path) - + // Find include paths relative to cl.exe location includePaths := []string{} - var envSetup *CompilerEnvSetup - if vsPath != "" { - if msvcPath := findMSVCIncludePath(vsPath); msvcPath != "" { - includePaths = append(includePaths, msvcPath) - } - if sdkPath := findSDKIncludePath(); sdkPath != "" { - includePaths = append(includePaths, sdkPath) - } + compilerDir := filepath.Dir(path) - // Set up MSVC environment configuration - vcvarsall := filepath.Join(vsPath, "VC\\Auxiliary\\Build\\vcvarsall.bat") - if _, err := os.Stat(vcvarsall); err == nil { - envSetup = &CompilerEnvSetup{ - SetupScript: vcvarsall, - SetupArgs: []string{"x64"}, - SetupCmd: "cmd /c", - } - } + // Look for include directory in the same directory as cl.exe + includeDir := filepath.Join(compilerDir, "include") + if _, err := os.Stat(includeDir); err == nil { + includePaths = append(includePaths, includeDir) + } + + // Look for include directory in parent directory + parentIncludeDir := filepath.Join(filepath.Dir(compilerDir), "include") + if _, err := os.Stat(parentIncludeDir); err == nil { + includePaths = append(includePaths, parentIncludeDir) } return &CompilerInfo{ @@ -241,6 +224,5 @@ func checkMSVC() (*CompilerInfo, error) { Version: string(output), Path: path, IncludePaths: includePaths, - EnvSetup: envSetup, }, nil } diff --git a/compiler/detect_test.go b/compiler/detect_test.go new file mode 100644 index 0000000..516edb1 --- /dev/null +++ b/compiler/detect_test.go @@ -0,0 +1,320 @@ +package compiler + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +const ( + errExpectedAbsPath = "Expected absolute path, got %v" + errExpectedVersion = "Expected version info, got empty string" +) + +// mockCompiler creates a mock compiler executable that returns a predefined version string +func mockCompiler(t *testing.T, dir, name, version string) string { + path := filepath.Join(dir, name) + + // Create a Go program that will be compiled into our mock compiler + content := []byte(`package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("` + version + `") + os.Exit(0) +}`) + + // Write the Go source + srcPath := path + ".go" + if err := os.WriteFile(srcPath, content, 0644); err != nil { + t.Fatalf("Failed to create mock compiler source: %v", err) + } + + // Compile the mock compiler + cmd := exec.Command("go", "build", "-o", path, srcPath) + if err := cmd.Run(); err != nil { + t.Fatalf("Failed to build mock compiler: %v", err) + } + + // Clean up the source file + os.Remove(srcPath) + + return path +} + +func TestDetectCompiler(t *testing.T) { + tests := []struct { + name string + os string + compiler CompilerType + wantErr bool + }{ + { + name: "Auto detect on Windows", + os: "windows", + compiler: CompilerAuto, + wantErr: false, + }, + { + name: "Auto detect on Linux", + os: "linux", + compiler: CompilerAuto, + wantErr: false, + }, + { + name: "MSVC on non-Windows", + os: "linux", + compiler: CompilerMSVC, + wantErr: true, + }, + { + name: "Unsupported compiler type", + os: "linux", + compiler: "unsupported", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Skip tests that don't match the current OS + if tt.os != runtime.GOOS { + t.Skipf("Skipping test for OS %s on %s", tt.os, runtime.GOOS) + } + + _, err := DetectCompiler(tt.compiler) + if (err != nil) != tt.wantErr { + t.Errorf("DetectCompiler() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func getGCCTestCase(t *testing.T, tmpDir string) struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) +} { + return struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) + }{ + name: "GCC detection", + compiler: CompilerGCC, + setup: func() { + mockCompiler(t, tmpDir, "g++", "g++ (GCC) 9.4.0") + }, + check: func(t *testing.T, info *CompilerInfo, err error) { + if err != nil { + t.Skipf("Skipping GCC test: %v", err) + return + } + if info.Type != CompilerGCC { + t.Errorf("Expected compiler type GCC, got %v", info.Type) + } + if !filepath.IsAbs(info.Path) { + t.Errorf(errExpectedAbsPath, info.Path) + } + if info.Version == "" { + t.Error(errExpectedVersion) + } + }, + } +} + +func getClangTestCase(t *testing.T, tmpDir string) struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) +} { + return struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) + }{ + name: "Clang detection", + compiler: CompilerClang, + setup: func() { + mockCompiler(t, tmpDir, "clang++", "clang version 12.0.0") + }, + check: func(t *testing.T, info *CompilerInfo, err error) { + if err != nil { + t.Skipf("Skipping Clang test: %v", err) + return + } + if info.Type != CompilerClang { + t.Errorf("Expected compiler type Clang, got %v", info.Type) + } + if !filepath.IsAbs(info.Path) { + t.Errorf(errExpectedAbsPath, info.Path) + } + if info.Version == "" { + t.Error(errExpectedVersion) + } + }, + } +} + +func getMSVCTestCase(t *testing.T, tmpDir string) struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) +} { + return struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) + }{ + name: "MSVC detection on Windows", + compiler: CompilerMSVC, + setup: func() { + if runtime.GOOS == "windows" { + mockCompiler(t, tmpDir, "cl.exe", "Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133") + } + }, + check: func(t *testing.T, info *CompilerInfo, err error) { + if runtime.GOOS != "windows" { + t.Skip("Skipping MSVC test on non-Windows platform") + return + } + if err != nil { + t.Skipf("Skipping MSVC test: %v", err) + return + } + if info.Type != CompilerMSVC { + t.Errorf("Expected compiler type MSVC, got %v", info.Type) + } + if !filepath.IsAbs(info.Path) { + t.Errorf(errExpectedAbsPath, info.Path) + } + if info.Version == "" { + t.Error(errExpectedVersion) + } + }, + } +} + +func getNotFoundTestCase() struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) +} { + return struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) + }{ + name: "Compiler not found", + compiler: CompilerGCC, + setup: func() { + // Don't create any mock compiler + }, + check: func(t *testing.T, info *CompilerInfo, err error) { + if err == nil { + t.Skip("Skipping 'compiler not found' test as a compiler was found") + } + }, + } +} + +func getCompilerTestCases(t *testing.T, tmpDir string) []struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) +} { + return []struct { + name string + compiler CompilerType + setup func() + check func(*testing.T, *CompilerInfo, error) + }{ + getGCCTestCase(t, tmpDir), + getClangTestCase(t, tmpDir), + getMSVCTestCase(t, tmpDir), + getNotFoundTestCase(), + } +} + +func TestCompilerDetection(t *testing.T) { + tmpDir := t.TempDir() + origPath := os.Getenv("PATH") + defer os.Setenv("PATH", origPath) + os.Setenv("PATH", tmpDir+string(os.PathListSeparator)+origPath) + + for _, tt := range getCompilerTestCases(t, tmpDir) { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + info, err := detectSpecificCompiler(tt.compiler) + tt.check(t, info, err) + }) + } +} + +func TestIncludePathDetection(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Include path detection test is Windows-specific") + } + + tmpDir := t.TempDir() + + // Create mock MSVC installation structure + clPath := mockCompiler(t, tmpDir, "cl.exe", "Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133") + includeDir := filepath.Join(tmpDir, "include") + parentIncludeDir := filepath.Join(filepath.Dir(tmpDir), "include") + + // Create include directories + if err := os.MkdirAll(includeDir, 0755); err != nil { + t.Fatalf("Failed to create include directory: %v", err) + } + if err := os.MkdirAll(parentIncludeDir, 0755); err != nil { + t.Fatalf("Failed to create parent include directory: %v", err) + } + + // Save original PATH + origPath := os.Getenv("PATH") + defer os.Setenv("PATH", origPath) + + // Add our temporary directory to PATH + os.Setenv("PATH", filepath.Dir(clPath)+string(os.PathListSeparator)+origPath) + + info, err := checkMSVC() + if err != nil { + t.Skipf("Skipping MSVC include path test: %v", err) + return + } + + // Check include paths + foundInclude := false + foundParentInclude := false + for _, path := range info.IncludePaths { + if path == includeDir { + foundInclude = true + } + if path == parentIncludeDir { + foundParentInclude = true + } + } + + if !foundInclude { + t.Error("Expected to find include directory in compiler directory") + } + if !foundParentInclude { + t.Error("Expected to find include directory in parent directory") + } +} diff --git a/readme.md b/readme.md index a2ad43c..230afbe 100644 --- a/readme.md +++ b/readme.md @@ -1,56 +1,51 @@ -# Cp2P: Seamless C++ to Python Binding Generator +# C++ to Python Bindings Generator -Cp2P is a powerful CLI tool that automatically generates Python bindings for C++ code. It supports multiple compilers and platforms, making it easy to create Python interfaces for your C++ libraries. +A tool to generate Python bindings for C++ code, supporting multiple compilers and platforms. ## Features -- **Multi-Compiler Support**: Works with GCC, Clang, and MSVC -- **Cross-Platform**: Supports Windows, Linux, and macOS -- **Type-Safe**: Generates proper type hints and docstrings -- **Easy to Use**: Simple command-line interface -- **Configurable**: Supports JSON/YAML configuration files -- **Modern Python**: Generates Python 3.7+ compatible code +- Automatic compiler detection (GCC, Clang, MSVC) +- Cross-platform support (Windows, Linux, macOS) +- Generates Python bindings using pybind11 +- Handles C++ class and function bindings +- Supports custom include paths and compiler flags +- Configurable through JSON or C++ file annotations -## Installation - -### From Source +## How It Works -```bash -git clone https://github.com/itsfuad/Cp2P.git -cd Cp2P -go build -``` +1. **Compiler Detection**: + - Automatically detects available C++ compilers in the system PATH + - Supports GCC, Clang, and MSVC (Windows only) + - Verifies compiler version and capabilities -### Using Go +2. **C++ Code Analysis**: + - Parses C++ source files to identify classes and functions + - Supports both automatic detection and manual configuration + - Handles inheritance and virtual functions -```bash -go install github.com/itsfuad/Cp2P@latest -``` +3. **Binding Generation**: + - Creates pybind11-based Python bindings + - Generates proper type conversions + - Handles memory management and object lifetimes + - Supports both static and dynamic library generation -## Quick Start - -1. Create a C++ source file (`example.cpp`): - -```cpp -extern "C" { - int add(int a, int b) { - return a + b; - } -} -``` +4. **Build Process**: + - Compiles C++ code into a shared library + - Links against Python and pybind11 + - Handles platform-specific build requirements -2. Generate Python bindings: +## Installation ```bash -Cp2P --input example.cpp --output ./bindings -``` - -3. Use in Python: +# Clone the repository +git clone https://github.com/yourusername/cp2p.git +cd cp2p -```python -from bindings.example import add +# Install dependencies +go mod download -result = add(5, 3) # Returns 8 +# Build the tool +go build -o cp2p ``` ## Usage @@ -58,99 +53,105 @@ result = add(5, 3) # Returns 8 ### Basic Usage ```bash -Cp2P --input --output [options] +# Generate bindings from a C++ file +cp2p --input example.cpp --output ./bindings + +# Use a specific compiler +cp2p --input example.cpp --output ./bindings --compiler gcc + +# Use a configuration file +cp2p --input example.cpp --output ./bindings --config config.json ``` -### Options +### Command Line Arguments -- `--input`: Path to C++ source file (required) -- `--output`: Output directory (default: ./bindings) +- `--input`: Path to the C++ source file or project entry point +- `--output`: Output directory for generated bindings (default: ./bindings) - `--compiler`: Compiler choice (gcc, clang, msvc, auto) -- `--config`: JSON/YAML config file for custom bindings +- `--config`: Optional JSON config file (if not provided, will parse C++ file) ### Configuration File Example ```json { - "functions": [ - { - "name": "add", - "return_type": "int", - "parameters": [ - {"name": "a", "type": "int"}, - {"name": "b", "type": "int"} - ], - "docstring": "Adds two integers" - } - ] + "classes": [ + { + "name": "MyClass", + "methods": ["method1", "method2"], + "constructors": ["default", "withParams"] + } + ], + "functions": ["globalFunc1", "globalFunc2"], + "include_paths": ["/path/to/includes"], + "compiler_flags": ["-std=c++17", "-O2"] } ``` -## Development +### C++ Code Example -### Prerequisites +```cpp +// example.cpp +#include -- Go 1.21 or later -- C++ compiler (GCC, Clang, or MSVC) -- Python 3.7 or later +class MyClass { +public: + MyClass() {} + std::string getMessage() { return "Hello from C++!"; } +}; -### Building +// Bindings will be automatically generated for this class +``` -```bash -# Clone the repository -git clone https://github.com/itsfuad/Cp2P.git -cd Cp2P +### Using Generated Bindings -# Install dependencies -go mod download +```python +# Python code using the generated bindings +from bindings import MyClass + +obj = MyClass() +print(obj.getMessage()) # Output: Hello from C++! +``` + +## Compiler Detection -# Build -go build +The tool automatically detects available compilers in the following order: -# Run tests +### Windows +1. MSVC (cl.exe) +2. GCC/MinGW (g++) +3. Clang (clang++) + +### Linux/macOS +1. Clang (clang++) +2. GCC (g++) + +## Development + +### Running Tests + +```bash +# Run all tests go test ./... -# Run linter -golangci-lint run +# Run specific test +go test ./compiler -run TestCompilerDetection ``` -### Project Structure +### Adding New Compiler Support -``` -Cp2P/ -├── cmd/ -│ └── Cp2P.go # CLI entry point -├── compiler/ -│ ├── detect.go # Compiler detection -│ └── compile.go # Compilation logic -├── binding/ -│ └── generator.go # Python binding generator -├── config/ -│ └── parser.go # Config file parser -├── util/ -│ ├── os_utils.go # OS utilities -│ └── logger.go # Logging -├── tests/ # Test files -├── .github/ # GitHub Actions -├── README.md -├── go.mod -└── LICENSE -``` +1. Add new compiler type to `CompilerType` enum +2. Implement detection logic in `detect.go` +3. Add compiler-specific flags and options +4. Update tests in `detect_test.go` ## Contributing 1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request ## License -This project is licensed under the Mozilla Public License Version 2.0 - -## Acknowledgments - -- Inspired by pybind11 and cppyy -- Built with Go and Python -- Uses ctypes for Python bindings \ No newline at end of file +MIT License - see LICENSE file for details \ No newline at end of file