diff --git a/.gitignore b/.gitignore index 8a4df88d..c1d19160 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.so *.dylib *.DS_Store +*_autogen.go # Test binary, built with `go test -c` *.test diff --git a/cmd/internal/base/base.go b/cmd/internal/base/base.go new file mode 100644 index 00000000..8d12a076 --- /dev/null +++ b/cmd/internal/base/base.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package base defines shared basic pieces of the llgo command, +// in particular logging and the Command structure. +package base + +import ( + "bytes" + "flag" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/goplus/llcppg/config" +) + +// A Command is an implementation of a llcppgx command +// like llcppgx gensym or llcppgx genpkg. +type Command struct { + // Run runs the command. + // The args are the arguments after the command name. + Run func(cmd *Command, args []string) + + // UsageLine is the one-line usage message. + // The words between "llcppgx" and the first flag or argument in the line are taken to be the command name. + UsageLine string + + // Short is the short description shown in the 'llcppgx help' output. + Short string + + // Flag is a set of flags specific to this command. + Flag flag.FlagSet + + // Commands lists the available commands and help topics. + // The order here is the order in which they are printed by 'llcppgx help'. + // Note that subcommands are in general best avoided. + Commands []*Command +} + +// Llcppg command +var Llcppg = &Command{ + UsageLine: "llcppgx", + Short: `llcppgx aims to be a tool for automatically generating LLGo bindings for C/C++ libraries.`, + // Commands initialized in package main +} + +// LongName returns the command's long name: all the words in the usage line between "llcppgx" and a flag or argument, +func (c *Command) LongName() string { + name := c.UsageLine + if i := strings.Index(name, " ["); i >= 0 { + name = name[:i] + } + if name == "llcppgx" { + return "" + } + return strings.TrimPrefix(name, "llcppgx ") +} + +// Name returns the command's short name: the last word in the usage line before a flag or argument. +func (c *Command) Name() string { + name := c.LongName() + if i := strings.LastIndex(name, " "); i >= 0 { + name = name[i+1:] + } + return name +} + +// Usage show the command usage. +func (c *Command) Usage(w io.Writer) { + fmt.Fprintf(w, "%s\n\nUsage: %s\n", c.Short, c.UsageLine) + + // restore output of flag + defer c.Flag.SetOutput(c.Flag.Output()) + + c.Flag.SetOutput(w) + c.Flag.PrintDefaults() + fmt.Fprintln(w) +} + +// Runnable reports whether the command can be run; otherwise +// it is a documentation pseudo-command. +func (c *Command) Runnable() bool { + return c.Run != nil +} + +func RunCmdWithName(cmd *Command, args []string, name string, out *io.PipeWriter) { + err := cmd.Flag.Parse(args) + if err != nil { + return + } + + cfgFile := config.LLCPPG_CFG + + bytesOfConf, err := config.MarshalConfigFile(cfgFile) + Check(err) + + if cmd.Flag.NArg() == 0 { + args = append(args, "-") + } + + nameCmd := exec.Command(name, args...) + nameCmd.Stdin = bytes.NewReader(bytesOfConf) + nameCmd.Stdout = os.Stdout + if out != nil { + nameCmd.Stdout = out + } + nameCmd.Stderr = os.Stderr + Check(nameCmd.Run()) +} + +func Check(err error) { + if err != nil { + panic(err) + } +} + +// Usage is the usage-reporting function, filled in by package main +// but here for reference by other packages. +// +// flag.Usage func() + +// CmdName - "build", "install", "list", "mod tidy", etc. +var CmdName string + +// Main runs a command. +func Main(c *Command, app string, args []string) { + name := c.UsageLine + if i := strings.Index(name, " ["); i >= 0 { + c.UsageLine = app + name[i:] + } + c.Run(c, args) +} diff --git a/cmd/internal/gencfg/flags.go b/cmd/internal/gencfg/flags.go new file mode 100644 index 00000000..2b5ffdb0 --- /dev/null +++ b/cmd/internal/gencfg/flags.go @@ -0,0 +1,17 @@ +package gencfg + +import "flag" + +var dependencies string +var extsString string +var excludes string +var cpp, help, tab bool + +func addFlags(fs *flag.FlagSet) { + fs.BoolVar(&cpp, "cpp", false, "if it is C++ lib") + fs.BoolVar(&help, "help", false, "print help message") + fs.BoolVar(&tab, "tab", true, "generate .cfg config file with tab indent") + fs.StringVar(&excludes, "excludes", "", "exclude all header files in subdir of include example -excludes=\"internal impl\"") + fs.StringVar(&extsString, "exts", ".h", "extra include file extensions for example -exts=\".h .hpp .hh\"") + fs.StringVar(&dependencies, "deps", "", "deps for autofilling dependencies") +} diff --git a/cmd/internal/gencfg/gencfg.go b/cmd/internal/gencfg/gencfg.go new file mode 100644 index 00000000..bdd15df7 --- /dev/null +++ b/cmd/internal/gencfg/gencfg.go @@ -0,0 +1,60 @@ +package gencfg + +import ( + "os" + "strings" + + "github.com/goplus/llcppg/cmd/internal/base" + "github.com/goplus/llcppg/cmd/llcppcfg/gen" + "github.com/goplus/llcppg/config" +) + +var Cmd = &base.Command{ + UsageLine: "llcppg init", + Short: "init llcppg.cfg config file", +} + +func init() { + Cmd.Run = runCmd + addFlags(&Cmd.Flag) +} + +func runCmd(cmd *base.Command, args []string) { + + if err := cmd.Flag.Parse(args); err != nil { + return + } + + name := "" + if len(cmd.Flag.Args()) > 0 { + name = cmd.Flag.Arg(0) + } + + if len(name) == 0 { + return + } + + exts := strings.Fields(extsString) + deps := strings.Fields(dependencies) + + excludeSubdirs := []string{} + if len(excludes) > 0 { + excludeSubdirs = strings.Fields(excludes) + } + var flagMode gen.FlagMode + if cpp { + flagMode |= gen.WithCpp + } + if tab { + flagMode |= gen.WithTab + } + buf, err := gen.Do(gen.NewConfig(name, flagMode, exts, deps, excludeSubdirs)) + if err != nil { + panic(err) + } + outFile := config.LLCPPG_CFG + err = os.WriteFile(outFile, buf, 0600) + if err != nil { + panic(err) + } +} diff --git a/cmd/internal/genpkg/flags.go b/cmd/internal/genpkg/flags.go new file mode 100644 index 00000000..6fe6b32a --- /dev/null +++ b/cmd/internal/genpkg/flags.go @@ -0,0 +1,13 @@ +package genpkg + +import ( + "flag" +) + +var modulePath string +var verbose bool + +func addFlags(fs *flag.FlagSet) { + fs.BoolVar(&verbose, "v", false, "enable verbose output") + fs.StringVar(&modulePath, "mod", "", "the module path of the generated code,if not set,will not init a new module") +} diff --git a/cmd/internal/genpkg/genpkg.go b/cmd/internal/genpkg/genpkg.go new file mode 100644 index 00000000..de8121c9 --- /dev/null +++ b/cmd/internal/genpkg/genpkg.go @@ -0,0 +1,75 @@ +package genpkg + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + + "github.com/goplus/llcppg/cmd/internal/base" + "github.com/goplus/llcppg/config" + "golang.org/x/mod/module" +) + +var Cmd = &base.Command{ + UsageLine: "llcppg genpkg", + Short: "generate a go package by signature information of symbols", +} + +func init() { + Cmd.Run = runCmd + addFlags(&Cmd.Flag) +} + +func runCmd(cmd *base.Command, args []string) { + err := cmd.Flag.Parse(args) + if err != nil { + return + } + + err = module.CheckPath(modulePath) + base.Check(err) + + cfgFile := config.LLCPPG_CFG + + config.HandleMarshalConfigFile(cfgFile, func(b []byte, err error) { + + base.Check(err) + + r, w := io.Pipe() + + errCh := make(chan error, 1) + go func() { + defer w.Close() + llcppsigfetchCmdArgs := make([]string, 0) + if verbose { + llcppsigfetchCmdArgs = append(llcppsigfetchCmdArgs, "-v") + } + if cmd.Flag.NArg() == 0 { + llcppsigfetchCmdArgs = append(llcppsigfetchCmdArgs, "-") + } + llcppsigfetchCmd := exec.Command("llcppsigfetch", llcppsigfetchCmdArgs...) + llcppsigfetchCmd.Stdin = bytes.NewReader(b) + llcppsigfetchCmd.Stdout = w + llcppsigfetchCmd.Stderr = os.Stderr + errCh <- llcppsigfetchCmd.Run() + }() + + gogensigCmdArgs := make([]string, 0) + gogensigCmdArgs = append(gogensigCmdArgs, fmt.Sprintf("-mod=%s", modulePath)) + if verbose { + gogensigCmdArgs = append(gogensigCmdArgs, "-v") + } + gogensigCmd := exec.Command("gogensig", gogensigCmdArgs...) + gogensigCmd.Stdin = r + gogensigCmd.Stdout = os.Stdout + gogensigCmd.Stderr = os.Stderr + err = gogensigCmd.Run() + base.Check(err) + + if fetchErr := <-errCh; fetchErr != nil { + base.Check(fetchErr) + } + }) +} diff --git a/cmd/internal/gensig/flags.go b/cmd/internal/gensig/flags.go new file mode 100644 index 00000000..b75ffa6f --- /dev/null +++ b/cmd/internal/gensig/flags.go @@ -0,0 +1,9 @@ +package gensig + +import "flag" + +var verbose bool + +func addFlags(fs *flag.FlagSet) { + fs.BoolVar(&verbose, "v", false, "enable verbose output") +} diff --git a/cmd/internal/gensig/gensig.go b/cmd/internal/gensig/gensig.go new file mode 100644 index 00000000..9238af49 --- /dev/null +++ b/cmd/internal/gensig/gensig.go @@ -0,0 +1,19 @@ +package gensig + +import ( + "github.com/goplus/llcppg/cmd/internal/base" +) + +var Cmd = &base.Command{ + UsageLine: "llcppg gensig", + Short: "generate signature information of C/C++ symbols", +} + +func init() { + Cmd.Run = runCmd + addFlags(&Cmd.Flag) +} + +func runCmd(cmd *base.Command, args []string) { + base.RunCmdWithName(cmd, args, "llcppsigfetch", nil) +} diff --git a/cmd/internal/gensym/flags.go b/cmd/internal/gensym/flags.go new file mode 100644 index 00000000..745db3fa --- /dev/null +++ b/cmd/internal/gensym/flags.go @@ -0,0 +1,9 @@ +package gensym + +import "flag" + +var verbose bool + +func addFlags(fs *flag.FlagSet) { + fs.BoolVar(&verbose, "v", false, "enable verbose output") +} diff --git a/cmd/internal/gensym/gensym.go b/cmd/internal/gensym/gensym.go new file mode 100644 index 00000000..eda40235 --- /dev/null +++ b/cmd/internal/gensym/gensym.go @@ -0,0 +1,19 @@ +package gensym + +import ( + "github.com/goplus/llcppg/cmd/internal/base" +) + +var Cmd = &base.Command{ + UsageLine: "llcppg gensym", + Short: "generate symbol table for a C/C++ library", +} + +func init() { + Cmd.Run = runCmd + addFlags(&Cmd.Flag) +} + +func runCmd(cmd *base.Command, args []string) { + base.RunCmdWithName(cmd, args, "llcppsymg", nil) +} diff --git a/cmd/internal/version/version.go b/cmd/internal/version/version.go new file mode 100644 index 00000000..697da6e3 --- /dev/null +++ b/cmd/internal/version/version.go @@ -0,0 +1,20 @@ +package version + +import ( + "fmt" + + "github.com/goplus/llcppg/cmd/internal/base" +) + +var Cmd = &base.Command{ + UsageLine: "llcppg version", + Short: "Print llcppg version", +} + +func init() { + Cmd.Run = runCmd +} + +func runCmd(cmd *base.Command, args []string) { + fmt.Println("todo print version") +} diff --git a/cmd/llcppgx/completion_cmd.gox b/cmd/llcppgx/completion_cmd.gox new file mode 100644 index 00000000..e69de29b diff --git a/cmd/llcppgx/genpkg_cmd.gox b/cmd/llcppgx/genpkg_cmd.gox new file mode 100644 index 00000000..a5e487ce --- /dev/null +++ b/cmd/llcppgx/genpkg_cmd.gox @@ -0,0 +1,13 @@ +import ( + self "github.com/goplus/llcppg/cmd/internal/genpkg" +) + +use "genpkg [flags]" + +short "generate a go package by signature information of symbols" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} diff --git a/cmd/llcppgx/gensym_cmd.gox b/cmd/llcppgx/gensym_cmd.gox new file mode 100644 index 00000000..00b2e41c --- /dev/null +++ b/cmd/llcppgx/gensym_cmd.gox @@ -0,0 +1,13 @@ +import ( + self "github.com/goplus/llcppg/cmd/internal/gensym" +) + +use "gensym [flags]" + +short "generate symbol table for a C/C++ library" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} diff --git a/cmd/llcppgx/init_cmd.gox b/cmd/llcppgx/init_cmd.gox new file mode 100644 index 00000000..776aa87a --- /dev/null +++ b/cmd/llcppgx/init_cmd.gox @@ -0,0 +1,13 @@ +import ( + self "github.com/goplus/llcppg/cmd/internal/gencfg" +) + +use "init [flags] [cpackage]" + +short "init llcppg.cfg config file" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} diff --git a/cmd/llcppgx/main_app.gox b/cmd/llcppgx/main_app.gox new file mode 100644 index 00000000..08274939 --- /dev/null +++ b/cmd/llcppgx/main_app.gox @@ -0,0 +1,15 @@ +short ` +llcppgx aims to be a tool for automatically generating LLGo bindings for C/C++ libraries. + +In order to convert C/C++ library to go package, you should do the steps as follows: + +1、llcppgx init +For example, run 'llcppgx init libcjson', it will init llcppg.cfg for libcjson library. +In order to successfully convert the C/C++ library, you should carefully inspect and edit llcppg.cfg. + +2、llcppgx gensym +It will generate and edits the symbol table for the libcjson library. + +3、llcppgx genpkg -mod "github.com/goplus/cjson" +It will generate a go package for the libcjson library. +` diff --git a/config/config.go b/config/config.go index ae703d72..ba5e7e0e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,17 +1,11 @@ package config import ( - "encoding/json" "fmt" "github.com/goplus/llcppg/ast" ) -const LLCPPG_CFG = "llcppg.cfg" -const LLCPPG_SYMB = "llcppg.symb.json" -const LLCPPG_SIGFETCH = "llcppg.sigfetch.json" -const LLCPPG_PUB = "llcppg.pub" - type Condition struct { OS []string `json:"os"` Arch []string `json:"arch"` @@ -41,30 +35,6 @@ type Config struct { HeaderOnly bool `json:"headerOnly,omitempty"` } -// json middleware for validating -func (c *Config) UnmarshalJSON(data []byte) error { - // create a new type here to avoid unmarshalling infinite loop. - type newConfig Config - - var config newConfig - err := json.Unmarshal(data, &config) - - if err != nil { - return err - } - - *c = Config(config) - - // do some check - - // when headeronly mode is disabled, libs must not be empty. - if c.Libs == "" && !c.HeaderOnly { - return fmt.Errorf("%w: libs must not be empty", ErrConfig) - } - - return nil -} - func NewDefault() *Config { return &Config{} } diff --git a/config/parse.go b/config/parse.go new file mode 100644 index 00000000..e8b2fb8c --- /dev/null +++ b/config/parse.go @@ -0,0 +1,97 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/goplus/llgo/xtool/env" +) + +const LLCPPG_CFG = "llcppg.cfg" +const LLCPPG_SYMB = "llcppg.symb.json" +const LLCPPG_SIGFETCH = "llcppg.sigfetch.json" +const LLCPPG_PUB = "llcppg.pub" + +// json middleware for validating +func (c *Config) UnmarshalJSON(data []byte) error { + // create a new type here to avoid unmarshalling infinite loop. + type newConfig Config + + var config newConfig + err := json.Unmarshal(data, &config) + + if err != nil { + return err + } + + *c = Config(config) + + // do some check + + // when headeronly mode is disabled, libs must not be empty. + if c.Libs == "" && !c.HeaderOnly { + return fmt.Errorf("%w: libs must not be empty", ErrConfig) + } + + return nil +} + +func isPathWithinCurrentDirectory(targetPath string) (bool, error) { + // Get the absolute path of the current working directory + cwd, err := os.Getwd() + if err != nil { + return false, fmt.Errorf("failed to get current working directory: %w", err) + } + + // Get the absolute path of the target path + absTargetPath, err := filepath.Abs(targetPath) + if err != nil { + return false, fmt.Errorf("failed to get absolute path for target: %w", err) + } + + // Clean both paths for consistent comparison + cleanedCwd := filepath.Clean(cwd) + cleanedAbsTargetPath := filepath.Clean(absTargetPath) + + // Check if the cleaned absolute target path starts with the cleaned absolute current directory path + return strings.HasPrefix(cleanedAbsTargetPath, cleanedCwd), nil +} + +func ParseConfigFile(cfgFile string) (*Config, error) { + ok, _ := isPathWithinCurrentDirectory(cfgFile) + if !ok { + return nil, fmt.Errorf("ParseConfigFile:%s is not within current directory", cfgFile) + } + + openCfgFile, err := os.Open(cfgFile) + if err != nil { + return nil, err + } + defer openCfgFile.Close() + var conf Config + err = json.NewDecoder(openCfgFile).Decode(&conf) + if err != nil { + return nil, err + } + conf.CFlags = env.ExpandEnv(conf.CFlags) + conf.Libs = env.ExpandEnv(conf.Libs) + return &conf, nil +} + +func MarshalConfigFile(cfgFile string) ([]byte, error) { + conf, err := ParseConfigFile(cfgFile) + if err != nil { + return nil, err + } + return json.MarshalIndent(&conf, "", " ") +} + +func HandleMarshalConfigFile(cfgFile string, handle func(b []byte, err error)) { + b, err := MarshalConfigFile(cfgFile) + if handle != nil { + handle(b, err) + } +} diff --git a/go.mod b/go.mod index 10001380..27e51138 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/goplus/llcppg go 1.23.0 require ( + github.com/goplus/cobra v1.9.12 //gop:class github.com/goplus/gogen v1.19.5 - github.com/goplus/lib v0.3.0 + github.com/goplus/lib v0.2.0 github.com/goplus/llgo v0.11.6-0.20250824004317-e4218f90d792 github.com/goplus/mod v0.17.1 github.com/qiniu/x v1.15.1 ) -require golang.org/x/mod v0.27.0 // indirect +require golang.org/x/mod v0.27.0 diff --git a/go.sum b/go.sum index e668a973..37a82142 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ +github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s= +github.com/goplus/cobra v1.9.12/go.mod h1:p4LhfNJDKEpiGjGiNn0crUXL5dUPA5DX2ztYpEJR34E= github.com/goplus/gogen v1.19.5 h1:YWPwpRA1PusPhptv9jKg/XiN+AQGiAD9r6I86mJ3lR4= github.com/goplus/gogen v1.19.5/go.mod h1:owX2e1EyU5WD+Nm6oH2m/GXjLdlBYcwkLO4wN8HHXZI= -github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA= -github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.2.0 h1:AjqkN1XK5H23wZMMlpaUYAMCDAdSBQ2NMFrLtSh7W4g= +github.com/goplus/lib v0.2.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= github.com/goplus/llgo v0.11.6-0.20250824004317-e4218f90d792 h1:EbF48QxuTaklX5MPwSuskZhu+dI9CHDIPW9S05uyhsM= github.com/goplus/llgo v0.11.6-0.20250824004317-e4218f90d792/go.mod h1:GeJLuuvv1yU+XBX+45SITayPgj7tsHVntEY+LEFPx+I= github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94=