From 1d76f515e0d98e6a43831a546bde5c49a17fdc2b Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 19 Aug 2025 20:20:31 +0800 Subject: [PATCH] internal/build:support relocatable lib --- .github/workflows/llgo.yml | 36 +++++++++- _demo/cargs/demo.go | 144 +++++++++++++++++++++++++++++++++++++ internal/build/build.go | 18 +++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 _demo/cargs/demo.go diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index f26c5957..bd6d4f70 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -47,6 +47,27 @@ jobs: with: name: llama2-model path: ./_demo/llama2-c/ + - name: Download platform-specific demo libs + run: | + if ${{ startsWith(matrix.os, 'macos') }}; then + DEMO_PKG="cargs_darwin_arm64.zip" + else + DEMO_PKG="cargs_linux_amd64.zip" + fi + + mkdir -p ./_demo/cargs/libs + cd ./_demo/cargs/libs + wget https://github.com/goplus/llpkg/releases/download/cargs/v1.0.0/${DEMO_PKG} + unzip ${DEMO_PKG} + + # Process pc template files - replace {{.Prefix}} with actual path + ACTUAL_PREFIX="$(pwd)" + for tmpl in lib/pkgconfig/*.pc.tmpl; do + pc_file="${tmpl%.tmpl}" + sed "s|{{.Prefix}}|${ACTUAL_PREFIX}|g" "$tmpl" > "$pc_file" + done + + echo "PKG_CONFIG_PATH=${ACTUAL_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" >> $GITHUB_ENV - name: Install further optional dependencies for demos run: | py_deps=( @@ -70,6 +91,18 @@ jobs: with: go-version: ${{matrix.go}} + - name: Test demo without RPATH (expect failure) + run: | + echo "Testing demo without RPATH (should fail)..." + export LLGO_FULL_RPATH=false + pkg-config --libs cargs + if (cd ./_demo/cargs && llgo run .); then + echo "ERROR: cargs demo should have failed without RPATH!" + exit 1 + else + echo "✓ cargs demo correctly failed without RPATH" + fi + - name: Test demos run: | # TODO(lijie): force python3-embed to be linked with python-3.12-embed @@ -80,7 +113,8 @@ jobs: libdir=$(pkg-config --variable=libdir python-3.12-embed) echo "libdir: $libdir" ln -s $libdir/pkgconfig/python-3.12-embed.pc $pcdir/python3-embed.pc - export PKG_CONFIG_PATH=$pcdir + export PKG_CONFIG_PATH=$pcdir:${PKG_CONFIG_PATH} + export LLGO_FULL_RPATH=true bash .github/workflows/test_demo.sh - name: _xtool build tests diff --git a/_demo/cargs/demo.go b/_demo/cargs/demo.go new file mode 100644 index 00000000..4ac382da --- /dev/null +++ b/_demo/cargs/demo.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "os" + _ "unsafe" + + "github.com/goplus/lib/c" +) + +const LLGoPackage string = "link: $(pkg-config --libs cargs);" + +type Option struct { + Identifier c.Char + AccessLetters *c.Char + AccessName *c.Char + ValueName *c.Char + Description *c.Char +} + +type OptionContext struct { + Options *Option + OptionCount c.SizeT + Argc c.Int + Argv **c.Char + Index c.Int + InnerIndex c.Int + ErrorIndex c.Int + ErrorLetter c.Char + ForcedEnd bool + Identifier c.Char + Value *c.Char +} + +// llgo:type C +type Printer func(__llgo_arg_0 c.Pointer, __llgo_arg_1 *c.Char, __llgo_va_list ...interface{}) c.Int + +// llgo:link (*OptionContext).OptionInit C.cag_option_init +func (recv_ *OptionContext) OptionInit(options *Option, option_count c.SizeT, argc c.Int, argv **c.Char) { +} + +// llgo:link (*OptionContext).OptionFetch C.cag_option_fetch +func (recv_ *OptionContext) OptionFetch() bool { + return false +} + +// llgo:link (*OptionContext).OptionGetIdentifier C.cag_option_get_identifier +func (recv_ *OptionContext) OptionGetIdentifier() c.Char { + return 0 +} + +// llgo:link (*OptionContext).OptionGetValue C.cag_option_get_value +func (recv_ *OptionContext) OptionGetValue() *c.Char { + return nil +} + +// llgo:link (*OptionContext).OptionGetIndex C.cag_option_get_index +func (recv_ *OptionContext) OptionGetIndex() c.Int { + return 0 +} + +// llgo:link (*OptionContext).OptionGetErrorIndex C.cag_option_get_error_index +func (recv_ *OptionContext) OptionGetErrorIndex() c.Int { + return 0 +} + +// llgo:link (*OptionContext).OptionGetErrorLetter C.cag_option_get_error_letter +func (recv_ *OptionContext) OptionGetErrorLetter() c.Char { + return 0 +} + +// llgo:link (*OptionContext).OptionPrintError C.cag_option_print_error +func (recv_ *OptionContext) OptionPrintError(destination *c.FILE) { +} + +// llgo:link (*OptionContext).OptionPrinterError C.cag_option_printer_error +func (recv_ *OptionContext) OptionPrinterError(printer Printer, printer_ctx c.Pointer) { +} + +// llgo:link (*Option).OptionPrint C.cag_option_print +func (recv_ *Option) OptionPrint(option_count c.SizeT, destination *c.FILE) { +} + +// llgo:link (*Option).OptionPrinter C.cag_option_printer +func (recv_ *Option) OptionPrinter(option_count c.SizeT, printer Printer, printer_ctx c.Pointer) { +} + +// llgo:link (*OptionContext).OptionPrepare C.cag_option_prepare +func (recv_ *OptionContext) OptionPrepare(options *Option, option_count c.SizeT, argc c.Int, argv **c.Char) { +} + +// llgo:link (*OptionContext).OptionGet C.cag_option_get +func (recv_ *OptionContext) OptionGet() c.Char { + return 0 +} + +func main() { + options := []Option{ + { + Identifier: 'h', + AccessLetters: c.Str("h"), + AccessName: c.Str("help"), + ValueName: nil, + Description: c.Str("Show help information"), + }, + { + Identifier: 'v', + AccessLetters: c.Str("v"), + AccessName: c.Str("version"), + ValueName: nil, + Description: c.Str("Show version information"), + }, + } + + args := os.Args + + // Convert Go string array to C-style argv + argv := make([]*int8, len(args)) + for i, arg := range args { + argv[i] = c.AllocaCStr(arg) + } + + // Initialize option context + var context OptionContext + context.OptionInit(&options[0], uintptr(len(options)), c.Int(len(args)), &argv[0]) + + // Process all options + identifierFound := false + for context.OptionFetch() { + identifierFound = true + identifier := context.OptionGetIdentifier() + switch identifier { + case 'h': + fmt.Println("Help: This is a simple command-line parser demo") + case 'v': + fmt.Println("Version: 1.0.0") + } + } + + // Default output if no identifier is found + if !identifierFound { + fmt.Println("Demo Command-line Tool\nIdentifier:\n\t-h: Help\n\t-v: Version") + } +} diff --git a/internal/build/build.go b/internal/build/build.go index 4c37457c..e2fa4a10 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -606,6 +606,19 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l objFiles = append(objFiles, export) } + if IsFullRpathEnabled() { + exargs := make([]string, 0, ctx.nLibdir<<1) + // Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well. + // This is to ensure the final executable can locate libraries with a relocatable install_name + // (e.g., "@rpath/libfoo.dylib") at runtime. + for _, arg := range linkArgs { + if strings.HasPrefix(arg, "-L") { + exargs = append(exargs, "-rpath", arg[2:]) + } + } + linkArgs = append(linkArgs, exargs...) + } + err = linkObjFiles(ctx, app, objFiles, linkArgs, verbose) check(err) @@ -1002,6 +1015,7 @@ const llgoWasmRuntime = "LLGO_WASM_RUNTIME" const llgoWasiThreads = "LLGO_WASI_THREADS" const llgoStdioNobuf = "LLGO_STDIO_NOBUF" const llgoCheckLinkArgs = "LLGO_CHECK_LINKARGS" +const llgoFullRpath = "LLGO_FULL_RPATH" const defaultWasmRuntime = "wasmtime" @@ -1053,6 +1067,10 @@ func IsCheckLinkArgsEnabled() bool { return isEnvOn(llgoCheckLinkArgs, false) } +func IsFullRpathEnabled() bool { + return isEnvOn(llgoFullRpath, true) +} + func WasmRuntime() string { return defaultEnv(llgoWasmRuntime, defaultWasmRuntime) }