diff --git a/_cmptest/llcppgend_test.go b/_cmptest/llcppgend_test.go index 5cc09fb5..6515a137 100644 --- a/_cmptest/llcppgend_test.go +++ b/_cmptest/llcppgend_test.go @@ -1,8 +1,10 @@ package _cmptest import ( + "encoding/json" "fmt" "io" + "maps" "os" "os/exec" "path/filepath" @@ -114,7 +116,7 @@ var mkdirTempLazily = sync.OnceValue(func() string { return dir }) -func logFile(tc testCase) (*os.File, error) { +func logFile(tc testCase, isStatic bool) (*os.File, error) { caseName := fmt.Sprintf("%s-%s-llcppg-%s-%s", runtime.GOOS, runtime.GOARCH, tc.pkg.Name, tc.pkg.Version) dirPath := filepath.Join(mkdirTempLazily(), caseName) @@ -123,6 +125,9 @@ func logFile(tc testCase) (*os.File, error) { return nil, err } + if isStatic { + return os.Create(filepath.Join(dirPath, fmt.Sprintf("%s-static.log", caseName))) + } return os.Create(filepath.Join(dirPath, fmt.Sprintf("%s.log", caseName))) } @@ -131,13 +136,23 @@ func TestEnd2End(t *testing.T) { tc := tc t.Run(fmt.Sprintf("%s/%s", tc.pkg.Name, tc.pkg.Version), func(t *testing.T) { t.Parallel() - testFrom(t, tc, false) + testFrom(t, tc, false, false) + }) + t.Run(fmt.Sprintf("%s/%s-static", tc.pkg.Name, tc.pkg.Version), func(t *testing.T) { + t.Parallel() + testFrom(t, tc, true, false) }) } } -func testFrom(t *testing.T, tc testCase, gen bool) { - logFile, err := logFile(tc) +func testFrom(t *testing.T, tc testCase, isStatic bool, gen bool) { + null, err := os.OpenFile(os.DevNull, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + defer null.Close() + + logFile, err := logFile(tc, isStatic) if err != nil { t.Fatal(err) } @@ -160,10 +175,42 @@ func testFrom(t *testing.T, tc testCase, gen bool) { cfgPath := filepath.Join(wd, tc.dir, tc.pkg.Name, config.LLCPPG_CFG) processCfgPath := filepath.Join(resultDir, config.LLCPPG_CFG) - copyFile(cfgPath, processCfgPath) + + // when isStatic is true, replace the staticLib=False to staticLib=True + if !isStatic { + copyFile(cfgPath, processCfgPath) + } else { + cfgContent, err := os.ReadFile(cfgPath) + if err != nil { + t.Fatal(err) + } + var cfg map[string]interface{} + err = json.Unmarshal(cfgContent, &cfg) + if err != nil { + t.Fatal(err) + } + cfg["staticLib"] = true + cfgContent, err = json.Marshal(cfg) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(processCfgPath, cfgContent, os.ModePerm) + if err != nil { + t.Fatal(err) + } + } + + if tc.config == nil { + tc.config = make(map[string]string) + } + + conanOpt := maps.Clone(tc.config) + if isStatic { + conanOpt["options"] = "*:shared=False " + conanOpt["options"] + } conanInstallMutex.Lock() - _, err = conan.NewConanInstaller(tc.config).Install(tc.pkg, conanDir) + _, err = conan.NewConanInstaller(conanOpt).Install(tc.pkg, conanDir) conanInstallMutex.Unlock() if err != nil { t.Fatal(err) @@ -180,7 +227,7 @@ func testFrom(t *testing.T, tc testCase, gen bool) { // llcppg.symb.json is a middle file os.Remove(filepath.Join(resultDir, config.LLCPPG_SYMB)) - copyFile(processCfgPath, filepath.Join(resultDir, tc.pkg.Name, config.LLCPPG_CFG)) + copyFile(cfgPath, filepath.Join(resultDir, tc.pkg.Name, config.LLCPPG_CFG)) if gen { os.RemoveAll(dir) diff --git a/_xtool/llcppsymg/internal/symg/lib.go b/_xtool/llcppsymg/internal/symg/lib.go index 5b987df7..0217c90d 100644 --- a/_xtool/llcppsymg/internal/symg/lib.go +++ b/_xtool/llcppsymg/internal/symg/lib.go @@ -28,9 +28,9 @@ func ParseLibs(libs string) *Libs { type LibMode = symbol.Mode // searches for each library name in the provided paths and default paths, -// appending the appropriate file extension (.dylib for macOS, .so for Linux). +// appending the appropriate file extension (.dylib for macOS, .so for Linux at dylib mode, .a for static mode). // -// Example: For "-L/opt/homebrew/lib -llua -lm": +// Example: For "-L/opt/homebrew/lib -llua -lm" and at dylib mode: // - It will search for liblua.dylib (on macOS) or liblua.so (on Linux) // - System libs like -lm are ignored and included in notFound // diff --git a/_xtool/llcppsymg/internal/symg/lib_test.go b/_xtool/llcppsymg/internal/symg/lib_test.go index fcbc08bb..5ee33d23 100644 --- a/_xtool/llcppsymg/internal/symg/lib_test.go +++ b/_xtool/llcppsymg/internal/symg/lib_test.go @@ -165,7 +165,7 @@ func TestGenDylibPaths(t *testing.T) { } if err != nil { - t.Fatalf("expected no error, got %w", err) + t.Fatalf("expected no error, got %v", err) } if !reflect.DeepEqual(notFounds, tc.wantNotFound) { diff --git a/_xtool/llcppsymg/internal/symg/symg.go b/_xtool/llcppsymg/internal/symg/symg.go index b5c5af48..338b0e42 100644 --- a/_xtool/llcppsymg/internal/symg/symg.go +++ b/_xtool/llcppsymg/internal/symg/symg.go @@ -10,6 +10,7 @@ import ( "github.com/goplus/llcppg/_xtool/internal/clangtool" "github.com/goplus/llcppg/_xtool/internal/header" "github.com/goplus/llcppg/_xtool/internal/ld" + "github.com/goplus/llcppg/_xtool/internal/symbol" llcppg "github.com/goplus/llcppg/config" "github.com/goplus/llgo/xtool/nm" ) @@ -37,11 +38,11 @@ type Config struct { TrimPrefixes []string SymMap map[string]string IsCpp bool - libMode LibMode + LibMode LibMode } func Do(conf *Config) (symbolTable []*llcppg.SymbolInfo, err error) { - symbols, err := FetchSymbols(conf.Libs, conf.libMode) + symbols, err := FetchSymbols(conf.Libs, conf.LibMode) if err != nil { return } @@ -100,7 +101,7 @@ func FetchSymbols(lib string, mode LibMode) ([]*nm.Symbol, error) { libFiles, notFounds, err := lbs.Files(sysPaths, mode) if err != nil { - return nil, fmt.Errorf("failed to generate some dylib paths: %v", err) + return nil, fmt.Errorf("failed to generate some lib paths: %v", err) } if dbgSymbol { @@ -117,13 +118,13 @@ func FetchSymbols(lib string, mode LibMode) ([]*nm.Symbol, error) { for _, libFile := range libFiles { args := []string{"-g"} - if runtime.GOOS == "linux" { + if runtime.GOOS == "linux" && mode == symbol.ModeDynamic { args = append(args, "-D") } files, err := nm.New("llvm-nm").List(libFile, args...) if err != nil { - parseErrors = append(parseErrors, fmt.Sprintf("fetchSymbols:Failed to list symbols in dylib %s: %v", libFile, err)) + parseErrors = append(parseErrors, fmt.Sprintf("fetchSymbols:Failed to list symbols in lib %s: %v", libFile, err)) continue } diff --git a/_xtool/llcppsymg/internal/symg/symg_test.go b/_xtool/llcppsymg/internal/symg/symg_test.go index a0670824..42771987 100644 --- a/_xtool/llcppsymg/internal/symg/symg_test.go +++ b/_xtool/llcppsymg/internal/symg/symg_test.go @@ -73,13 +73,13 @@ func TestGenMethodName(t *testing.T) { func TestGetCommonSymbols(t *testing.T) { testCases := []struct { name string - dylibSymbols []*nm.Symbol + libSymbols []*nm.Symbol headerSymbols map[string]*symg.SymbolInfo expect []*llcppg.SymbolInfo }{ { name: "Lua symbols", - dylibSymbols: []*nm.Symbol{ + libSymbols: []*nm.Symbol{ {Name: addSymbolPrefixUnder("lua_absindex", false)}, {Name: addSymbolPrefixUnder("lua_arith", false)}, {Name: addSymbolPrefixUnder("lua_atpanic", false)}, @@ -102,7 +102,7 @@ func TestGetCommonSymbols(t *testing.T) { }, { name: "INIReader and Std library symbols", - dylibSymbols: []*nm.Symbol{ + libSymbols: []*nm.Symbol{ {Name: addSymbolPrefixUnder("ZNK9INIReader12GetInteger64ERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_x", true)}, {Name: addSymbolPrefixUnder("ZNK9INIReader7GetRealERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_d", true)}, {Name: addSymbolPrefixUnder("ZNK9INIReader10ParseErrorEv", true)}, @@ -124,7 +124,7 @@ func TestGetCommonSymbols(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - commonSymbols := symg.GetCommonSymbols(tc.dylibSymbols, tc.headerSymbols) + commonSymbols := symg.GetCommonSymbols(tc.libSymbols, tc.headerSymbols) if !reflect.DeepEqual(commonSymbols, tc.expect) { t.Fatalf("expect %v, but got %v", tc.expect, commonSymbols) } @@ -381,14 +381,14 @@ class INIReader { func TestGen(t *testing.T) { gen := false testCases := []struct { - name string - path string - dylibSymbols []string + name string + path string + libSymbols []string }{ { name: "c", path: "./testdata/c", - dylibSymbols: []string{ + libSymbols: []string{ "Foo_Print", "Foo_ParseWithLength", "Foo_Delete", @@ -410,7 +410,7 @@ func TestGen(t *testing.T) { { name: "cpp", path: "./testdata/cpp", - dylibSymbols: []string{ + libSymbols: []string{ "ZN3FooC1EPKc", "ZN3FooC1EPKcl", "ZN3FooD1Ev", @@ -422,7 +422,7 @@ func TestGen(t *testing.T) { { name: "inireader", path: "./testdata/inireader", - dylibSymbols: []string{ + libSymbols: []string{ "ZN9INIReaderC1EPKc", "ZN9INIReaderC1EPKcl", "ZN9INIReaderD1Ev", @@ -433,7 +433,7 @@ func TestGen(t *testing.T) { { name: "lua", path: "./testdata/lua", - dylibSymbols: []string{ + libSymbols: []string{ "lua_error", "lua_next", "lua_concat", @@ -443,7 +443,7 @@ func TestGen(t *testing.T) { { name: "cjson", path: "./testdata/cjson", - dylibSymbols: []string{ + libSymbols: []string{ "cJSON_Print", "cJSON_ParseWithLength", "cJSON_Delete", @@ -454,14 +454,14 @@ func TestGen(t *testing.T) { { name: "isl", path: "./testdata/isl", - dylibSymbols: []string{ + libSymbols: []string{ "isl_pw_qpolynomial_get_ctx", }, }, { name: "gpgerror", path: "./testdata/gpgerror", - dylibSymbols: []string{ + libSymbols: []string{ "gpg_strsource", "gpg_strerror_r", "gpg_strerror", @@ -500,11 +500,11 @@ func TestGen(t *testing.T) { } // trim to nm symbols - var dylibsymbs []*nm.Symbol - for _, symb := range tc.dylibSymbols { - dylibsymbs = append(dylibsymbs, &nm.Symbol{Name: addSymbolPrefixUnder(symb, cfg.Cplusplus)}) + var libSymbols []*nm.Symbol + for _, symb := range tc.libSymbols { + libSymbols = append(libSymbols, &nm.Symbol{Name: addSymbolPrefixUnder(symb, cfg.Cplusplus)}) } - symbols := symg.GetCommonSymbols(dylibsymbs, headerSymbolMap) + symbols := symg.GetCommonSymbols(libSymbols, headerSymbolMap) if err != nil { t.Fatal(err) } @@ -569,57 +569,103 @@ const char* test_function_3(void) { } defer os.Remove(cSourcePath) - var libPath string - var compileCmd []string - if runtime.GOOS == "darwin" { - libPath = filepath.Join(tempDir, "libtest.dylib") - compileCmd = []string{"clang", "-shared", "-fPIC", "-o", libPath, cSourcePath} - } else if runtime.GOOS == "linux" { - libPath = filepath.Join(tempDir, "libtest.so") - compileCmd = []string{"gcc", "-shared", "-fPIC", "-o", libPath, cSourcePath} - } else { - t.Skip("Unsupported platform for this test") + testCases := []struct { + name string + mode symbol.Mode + libExt string + }{ + { + name: "Dynamic Library", + mode: symbol.ModeDynamic, + libExt: getDynamicLibExt(), + }, + { + name: "Static Library", + mode: symbol.ModeStatic, + libExt: ".a", + }, } - cmd := exec.Command(compileCmd[0], compileCmd[1:]...) - output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to compile test library: %v\nOutput: %s", err, output) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + libPath := filepath.Join(tempDir, "libtest"+tc.libExt) + var compileCmd []string + + if tc.mode == symbol.ModeDynamic { + if runtime.GOOS == "darwin" { + compileCmd = []string{"clang", "-shared", "-fPIC", "-o", libPath, cSourcePath} + } else if runtime.GOOS == "linux" { + compileCmd = []string{"gcc", "-shared", "-fPIC", "-o", libPath, cSourcePath} + } else { + t.Fatal("Unsupported platform for this test") + } + } else { // ModeStatic + objPath := filepath.Join(tempDir, "test.o") + if runtime.GOOS == "darwin" { + compileCmd = []string{"clang", "-c", "-o", objPath, cSourcePath} + } else if runtime.GOOS == "linux" { + compileCmd = []string{"gcc", "-c", "-o", objPath, cSourcePath} + } else { + t.Fatal("Unsupported platform for this test") + } - if _, err := os.Stat(libPath); os.IsNotExist(err) { - t.Fatal("Dynamic library was not created") - } + cmd := exec.Command(compileCmd[0], compileCmd[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile object file: %v\nOutput: %s", err, output) + } - libDir := tempDir - libFlag := fmt.Sprintf("-L%s -ltest", libDir) + compileCmd = []string{"ar", "rcs", libPath, objPath} + } - symbols, err := symg.FetchSymbols(libFlag, symbol.ModeDynamic) - if err != nil { - t.Fatalf("FetchSymbols failed: %v", err) - } + cmd := exec.Command(compileCmd[0], compileCmd[1:]...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to compile test library: %v\nOutput: %s", err, output) + } - if len(symbols) == 0 { - t.Fatal("No symbols found") - } + if _, err := os.Stat(libPath); os.IsNotExist(err) { + t.Fatalf("%s library was not created", tc.name) + } - expectedSymbols := []string{"test_function_1", "test_function_2", "test_function_3"} - foundSymbols := make(map[string]bool) + libDir := tempDir + libFlag := fmt.Sprintf("-L%s -ltest", libDir) - for _, sym := range symbols { - // On Darwin, symbols have '_' prefix, so trim it - symName := sym.Name - if runtime.GOOS == "darwin" { - symName = strings.TrimPrefix(symName, "_") - } - foundSymbols[symName] = true - } + symbols, err := symg.FetchSymbols(libFlag, tc.mode) + if err != nil { + t.Fatalf("FetchSymbols failed: %v", err) + } - for _, expected := range expectedSymbols { - if !foundSymbols[expected] { - t.Errorf("Expected symbol %s not found in library symbols", expected) - } + if len(symbols) == 0 { + t.Fatal("No symbols found") + } + + expectedSymbols := []string{"test_function_1", "test_function_2", "test_function_3"} + foundSymbols := make(map[string]bool) + + for _, sym := range symbols { + // On Darwin, symbols have '_' prefix, so trim it + symName := sym.Name + if runtime.GOOS == "darwin" { + symName = strings.TrimPrefix(symName, "_") + } + foundSymbols[symName] = true + } + + for _, expected := range expectedSymbols { + if !foundSymbols[expected] { + t.Errorf("Expected symbol %s not found in library symbols", expected) + } + } + + fmt.Printf("Successfully found %d symbols including expected test functions\n", len(symbols)) + }) } +} - t.Logf("Successfully found %d symbols including expected test functions", len(symbols)) +func getDynamicLibExt() string { + if runtime.GOOS == "darwin" { + return ".dylib" + } + return ".so" } diff --git a/_xtool/llcppsymg/llcppsymg.go b/_xtool/llcppsymg/llcppsymg.go index aef49436..787b1e93 100644 --- a/_xtool/llcppsymg/llcppsymg.go +++ b/_xtool/llcppsymg/llcppsymg.go @@ -21,6 +21,7 @@ import ( "fmt" "os" + "github.com/goplus/llcppg/_xtool/internal/symbol" "github.com/goplus/llcppg/_xtool/llcppsymg/internal/symg" llcppg "github.com/goplus/llcppg/config" args "github.com/goplus/llcppg/internal/arg" @@ -64,6 +65,11 @@ func main() { fmt.Fprintln(os.Stderr, "Failed to parse config file:", ags.CfgFile) } + libMode := symbol.ModeDynamic + if conf.StaticLib { + libMode = symbol.ModeStatic + } + symbolTable, err := symg.Do(&symg.Config{ Libs: conf.Libs, CFlags: conf.CFlags, @@ -72,6 +78,7 @@ func main() { TrimPrefixes: conf.TrimPrefixes, SymMap: conf.SymMap, IsCpp: conf.Cplusplus, + LibMode: libMode, }) check(err) diff --git a/config/config.go b/config/config.go index 44486a97..2433e5f7 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,7 @@ type Config struct { Mix bool `json:"mix,omitempty"` SymMap map[string]string `json:"symMap,omitempty"` TypeMap map[string]string `json:"typeMap,omitempty"` + StaticLib bool `json:"staticLib,omitempty"` } func NewDefault() *Config { diff --git a/config/config_test.go b/config/config_test.go index fff625f0..21870f1e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -120,6 +120,25 @@ func TestGetConfByByte(t *testing.T) { mode: useStdin, }, + { + name: "Static library configuration", + input: `{ + "name": "mylib", + "cflags": "-I/opt/homebrew/include", + "include": ["mylib.h"], + "libs": "-L/opt/homebrew/lib -lmylib", + "staticLib": true + }`, + expect: llconfig.Config{ + Name: "mylib", + CFlags: "-I/opt/homebrew/include", + Include: []string{"mylib.h"}, + Libs: "-L/opt/homebrew/lib -lmylib", + StaticLib: true, + }, + mode: useFile, + }, + { name: "Invalid JSON", input: `{invalid json}`,