diff --git a/internal/build/build.go b/internal/build/build.go index b7be325e..4eec5338 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -301,29 +301,29 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs // need to be linked with external library // format: ';' separated alternative link methods. e.g. // link: $LLGO_LIB_PYTHON; $(pkg-config --libs python3-embed); -lpython3 - expd := "" altParts := strings.Split(param, ";") + expdArgs := make([]string, 0, len(altParts)) for _, param := range altParts { param = strings.TrimSpace(param) if strings.ContainsRune(param, '$') { - expd = env.ExpandEnv(param) + expdArgs = append(expdArgs, env.ExpandEnvToArgs(param)...) ctx.nLibdir++ } else { - expd = param + expdArgs = append(expdArgs, param) } - if len(expd) > 0 { + if len(expdArgs) > 0 { break } } - if expd == "" { + if len(expdArgs) == 0 { panic(fmt.Sprintf("'%s' cannot locate the external library", param)) } pkgLinkArgs := make([]string, 0, 3) - if expd[0] == '-' { - pkgLinkArgs = append(pkgLinkArgs, strings.Split(expd, " ")...) + if expdArgs[0][0] == '-' { + pkgLinkArgs = append(pkgLinkArgs, expdArgs...) } else { - linkFile := expd + linkFile := expdArgs[0] dir, lib := filepath.Split(linkFile) pkgLinkArgs = append(pkgLinkArgs, "-l"+lib) if dir != "" { @@ -332,7 +332,7 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs } } if err := clangCheck.CheckLinkArgs(pkgLinkArgs); err != nil { - panic(fmt.Sprintf("test link args '%s' failed\n\texpanded to: %s\n\tresolved to: %v\n\terror: %v", param, expd, pkgLinkArgs, err)) + panic(fmt.Sprintf("test link args '%s' failed\n\texpanded to: %v\n\tresolved to: %v\n\terror: %v", param, expdArgs, pkgLinkArgs, err)) } aPkg.LinkArgs = append(aPkg.LinkArgs, pkgLinkArgs...) } @@ -714,9 +714,9 @@ func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(li args := make([]string, 0, 16) if strings.HasPrefix(files, "$") { // has cflags if pos := strings.IndexByte(files, ':'); pos > 0 { - cflags := env.ExpandEnv(files[:pos]) + cflags := env.ExpandEnvToArgs(files[:pos]) files = files[pos+1:] - args = append(args, strings.Split(cflags, " ")...) + args = append(args, cflags...) } } for _, file := range strings.Split(files, ";") { diff --git a/internal/build/cgo.go b/internal/build/cgo.go index 160e2a8f..4443596b 100644 --- a/internal/build/cgo.go +++ b/internal/build/cgo.go @@ -28,12 +28,13 @@ import ( "strings" "github.com/goplus/llgo/internal/buildtags" + "github.com/goplus/llgo/internal/safesplit" ) type cgoDecl struct { tag string - cflags string - ldflags string + cflags []string + ldflags []string } type cgoPreamble struct { @@ -66,11 +67,11 @@ func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs map[string ldflags := []string{} for _, cdecl := range cdecls { if cdecl.tag == "" || tagUsed[cdecl.tag] { - if cdecl.cflags != "" { - cflags = append(cflags, cdecl.cflags) + if len(cdecl.cflags) > 0 { + cflags = append(cflags, cdecl.cflags...) } - if cdecl.ldflags != "" { - ldflags = append(ldflags, cdecl.ldflags) + if len(cdecl.ldflags) > 0 { + ldflags = append(ldflags, cdecl.ldflags...) } } } @@ -121,7 +122,7 @@ func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs map[string }, verbose) } for _, ldflag := range ldflags { - cgoLdflags = append(cgoLdflags, strings.Split(ldflag, " ")...) + cgoLdflags = append(cgoLdflags, safesplit.SplitPkgConfigFlags(ldflag)...) } return } @@ -296,18 +297,18 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { } cgoDecls = append(cgoDecls, cgoDecl{ tag: tag, - cflags: strings.TrimSpace(string(cflags)), - ldflags: strings.TrimSpace(string(ldflags)), + cflags: safesplit.SplitPkgConfigFlags(string(cflags)), + ldflags: safesplit.SplitPkgConfigFlags(string(ldflags)), }) case "CFLAGS": cgoDecls = append(cgoDecls, cgoDecl{ tag: tag, - cflags: arg, + cflags: safesplit.SplitPkgConfigFlags(arg), }) case "LDFLAGS": cgoDecls = append(cgoDecls, cgoDecl{ tag: tag, - ldflags: arg, + ldflags: safesplit.SplitPkgConfigFlags(arg), }) } return diff --git a/internal/safesplit/safesplit.go b/internal/safesplit/safesplit.go new file mode 100644 index 00000000..58f2c242 --- /dev/null +++ b/internal/safesplit/safesplit.go @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 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 safesplit + +import "strings" + +// SplitPkgConfigFlags splits a pkg-config outputs string into parts. +// Each part starts with "-" followed by a single character flag. +// Spaces after the flag character are ignored. +// Content is read until the next space, unless escaped with "\". +func SplitPkgConfigFlags(s string) []string { + var result []string + var current strings.Builder + i := 0 + + // Skip leading whitespace + for i < len(s) && (s[i] == ' ' || s[i] == '\t') { + i++ + } + + for i < len(s) { + // Start a new part + if current.Len() > 0 { + result = append(result, strings.TrimSpace(current.String())) + current.Reset() + } + // Write "-" and the flag character + current.WriteByte('-') + i++ + if i < len(s) { + current.WriteByte(s[i]) + i++ + } + // Skip spaces after flag character + for i < len(s) && (s[i] == ' ' || s[i] == '\t') { + i++ + } + // Read content until next space + for i < len(s) { + if s[i] == '\\' && i+1 < len(s) && (s[i+1] == ' ' || s[i+1] == '\t') { + // Skip backslash and write the escaped space + i++ + current.WriteByte(s[i]) + i++ + continue + } + if s[i] == ' ' || s[i] == '\t' { + // Skip consecutive spaces + j := i + for j < len(s) && (s[j] == ' ' || s[j] == '\t') { + j++ + } + // If we've seen content, check for new flag + if j < len(s) && s[j] == '-' { + i = j + break + } + // Otherwise, include one space and continue + current.WriteByte(' ') + i = j + } else { + current.WriteByte(s[i]) + i++ + } + } + } + // Add the last part + if current.Len() > 0 { + result = append(result, strings.TrimSpace(current.String())) + } + return result +} diff --git a/internal/safesplit/safesplit_test.go b/internal/safesplit/safesplit_test.go new file mode 100644 index 00000000..93eb953d --- /dev/null +++ b/internal/safesplit/safesplit_test.go @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 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 safesplit + +import ( + "strings" + "testing" +) + +func TestSplitPkgConfigFlags(t *testing.T) { + ftest := func(s, want string) { + t.Helper() // for better error message + got := toString(SplitPkgConfigFlags(s)) + if got != want { + t.Errorf("\nSplitPkgConfigFlags(%q) =\n got %v\nwant %v", s, got, want) + } + } + + t.Run("basic", func(t *testing.T) { + ftest("-I/usr/include -L/usr/lib", `["-I/usr/include" "-L/usr/lib"]`) + ftest("-I /usr/include -L /usr/lib", `["-I/usr/include" "-L/usr/lib"]`) + ftest("-L/opt/homebrew/Cellar/bdw-gc/8.2.8/lib -lgc", + `["-L/opt/homebrew/Cellar/bdw-gc/8.2.8/lib" "-lgc"]`) + }) + + t.Run("spaces_in_path", func(t *testing.T) { + ftest("-I/usr/local/include directory -L/usr/local/lib path", + `["-I/usr/local/include directory" "-L/usr/local/lib path"]`) + }) + + t.Run("multiple_spaces", func(t *testing.T) { + ftest(" -I /usr/include -L /usr/lib ", `["-I/usr/include" "-L/usr/lib"]`) + }) + + t.Run("consecutive_flags", func(t *testing.T) { + ftest("-I -L", `["-I-L"]`) + ftest("-I -L /usr/lib", `["-I-L /usr/lib"]`) + }) + + t.Run("edge_cases", func(t *testing.T) { + ftest("", "[]") + ftest(" ", "[]") + ftest("-", `["-"]`) + ftest("-I", `["-I"]`) + ftest("-I -", `["-I-"]`) + }) + + t.Run("escaped_spaces", func(t *testing.T) { + ftest(`-I/path\ with\ spaces -L/lib`, `["-I/path with spaces" "-L/lib"]`) + ftest(`-I /first\ path -L /second\ long path`, `["-I/first path" "-L/second long path"]`) + }) + + t.Run("macro_flags", func(t *testing.T) { + ftest("-DMACRO -I/usr/include", `["-DMACRO" "-I/usr/include"]`) + ftest("-D MACRO -I/usr/include", `["-DMACRO" "-I/usr/include"]`) + ftest("-DMACRO=value -I/usr/include", `["-DMACRO=value" "-I/usr/include"]`) + ftest("-D MACRO=value -I/usr/include", `["-DMACRO=value" "-I/usr/include"]`) + ftest("-D_DEBUG -D_UNICODE -DWIN32", `["-D_DEBUG" "-D_UNICODE" "-DWIN32"]`) + ftest("-D _DEBUG -D _UNICODE -D WIN32", `["-D_DEBUG" "-D_UNICODE" "-DWIN32"]`) + ftest("-DVERSION=2.1 -DDEBUG=1", `["-DVERSION=2.1" "-DDEBUG=1"]`) + ftest("-D VERSION=2.1 -D DEBUG=1", `["-DVERSION=2.1" "-DDEBUG=1"]`) + }) +} + +func toString(ss []string) string { + if ss == nil { + return "[]" + } + s := "[" + for i, v := range ss { + if i > 0 { + s += " " + } + v = strings.ReplaceAll(v, `"`, `\"`) + s += `"` + v + `"` + } + return s + "]" +} diff --git a/xtool/env/env.go b/xtool/env/env.go index 4285deb3..f5932df9 100644 --- a/xtool/env/env.go +++ b/xtool/env/env.go @@ -22,6 +22,8 @@ import ( "os/exec" "regexp" "strings" + + "github.com/goplus/llgo/internal/safesplit" ) var ( @@ -29,6 +31,10 @@ var ( reFlag = regexp.MustCompile(`[^ \t\n]+`) ) +func ExpandEnvToArgs(s string) []string { + return safesplit.SplitPkgConfigFlags(expandEnvWithCmd(s)) +} + func ExpandEnv(s string) string { return expandEnvWithCmd(s) }