Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions gnovm/cmd/gno/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type runCmd struct {
debug bool
debugAddr string
pkgPath string
xVars *xFlag
}

func newRunCmd(cio commands.IO) *commands.Command {
Expand Down Expand Up @@ -85,6 +86,14 @@ func (c *runCmd) RegisterFlags(fs *flag.FlagSet) {
"",
"run with this package path, overriding the \"// PKGPATH:\" file directive and the gnomod.toml module path",
)

c.xVars = newXFlag()
fs.Var(
c.xVars,
"X",
"set the value of a package-level string variable, e.g. -X myVar=override (may be repeated); "+
"like 'go build -ldflags \"-X ...\"', only simple 'var name = \"...\"' declarations are patched",
)
}

func packageNameFromFiles(args []string) (string, error) {
Expand Down Expand Up @@ -272,7 +281,7 @@ func execRun(cfg *runCmd, args []string, cio commands.IO) error {
}

// read files
files, err := parseFiles(m, args, stderr)
files, err := parseFiles(m, args, stderr, cfg.xVars.values)
if err != nil {
return err
}
Expand Down Expand Up @@ -335,7 +344,7 @@ func derivePkgPath(arg string) (string, error) {
return mod.Module, nil
}

func parseFiles(m *gno.Machine, fpaths []string, stderr io.WriteCloser) ([]*gno.FileNode, error) {
func parseFiles(m *gno.Machine, fpaths []string, stderr io.WriteCloser, xOverrides map[string]string) ([]*gno.FileNode, error) {
files := make([]*gno.FileNode, 0, len(fpaths))
var didPanic bool
for _, fpath := range fpaths {
Expand All @@ -344,7 +353,7 @@ func parseFiles(m *gno.Machine, fpaths []string, stderr io.WriteCloser) ([]*gno.
if err != nil {
return nil, err
}
subFiles, err := parseFiles(m, subFns, stderr)
subFiles, err := parseFiles(m, subFns, stderr, xOverrides)
if err != nil {
return nil, err
}
Expand All @@ -358,7 +367,7 @@ func parseFiles(m *gno.Machine, fpaths []string, stderr io.WriteCloser) ([]*gno.

dir, fname := filepath.Split(fpath)
didPanic = catchPanic(dir, fname, stderr, func() {
files = append(files, m.MustReadFile(fpath))
files = append(files, mustReadAndPatchFile(m, fpath, xOverrides))
})
}

Expand All @@ -368,6 +377,22 @@ func parseFiles(m *gno.Machine, fpaths []string, stderr io.WriteCloser) ([]*gno.
return files, nil
}

// mustReadAndPatchFile reads fpath, applies any -X overrides to its
// package-level string variable declarations (see patchXVars), and parses
// the (possibly patched) result. It panics on error, like
// (*gno.Machine).MustReadFile, which it replaces as the read path here so
// that overrides can be applied before parsing.
func mustReadAndPatchFile(m *gno.Machine, fpath string, xOverrides map[string]string) *gno.FileNode {
bz, err := os.ReadFile(fpath)
if err != nil {
panic(err)
}

body := patchXVars(fpath, string(bz), xOverrides)

return m.MustParseFile(fpath, body)
}

func listNonTestFiles(dir string) ([]string, error) {
fs, err := os.ReadDir(dir)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions gnovm/cmd/gno/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ func TestRunApp(t *testing.T) {
args: []string{"run", "../../tests/integ/run_main/"},
stdoutShouldContain: "hello world!",
},
{
args: []string{"run", "../../tests/integ/run_xflag/main.gno"},
stdoutShouldContain: "default",
},
{
args: []string{"run", "-X", "myVar=overridden", "../../tests/integ/run_xflag/main.gno"},
stdoutShouldContain: "overridden",
},
{
args: []string{"run", "-X", "invalidnoequals", "../../tests/integ/run_xflag/main.gno"},
errShouldContain: "invalid -X value",
},
{
args: []string{"run", "../../tests/integ/does_not_exist"},
errShouldContain: "no such file or directory",
Expand Down
14 changes: 14 additions & 0 deletions gnovm/cmd/gno/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type testCmd struct {
printEvents bool
debug bool
parallel int
xVars *xFlag
}

func newTestCmd(io commands.IO) *commands.Command {
Expand Down Expand Up @@ -187,6 +188,14 @@ func (c *testCmd) RegisterFlags(fs *flag.FlagSet) {
"-debug enforces -p 1.",
runtime.GOMAXPROCS(0)),
)

c.xVars = newXFlag()
fs.Var(
c.xVars,
"X",
"set the value of a package-level string variable, e.g. -X myVar=override (may be repeated); "+
"like 'go build -ldflags \"-X ...\"', only simple 'var name = \"...\"' declarations are patched",
)
}

func execTest(cmd *testCmd, args []string, io commands.IO) error {
Expand Down Expand Up @@ -437,6 +446,11 @@ func (c *testCmd) testPkg(

// Read MemPackage with all files.
mpkg := gno.MustReadMemPackage(pkg.Dir, pkgPath, gno.MPAnyAll)
if len(c.xVars.values) > 0 {
for _, f := range mpkg.Files {
f.Body = patchXVars(f.Name, f.Body, c.xVars.values)
}
}
var didPanic, didError bool
startedAt := time.Now()
didPanic = catchPanic(pkg.Dir, pkgPath, io.Err(), func() {
Expand Down
128 changes: 128 additions & 0 deletions gnovm/cmd/gno/xflag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"strconv"
"strings"
)

// patchXVars parses body as Go source and rewrites the string-literal
// initializer of any package-level `var name = "..."` (or
// `var name T = "..."`, including grouped `var ( ... )` blocks)
// declaration whose name matches a key in overrides, similar to
// `go build -ldflags "-X pkg.name=value"`.
//
// Unlike a text-based find/replace, this walks the parsed AST and only
// descends into top-level declarations, so:
// - a string or raw string literal elsewhere in the file that merely
// looks like a var declaration (e.g. inside a backtick-quoted
// template string) is never touched, and
// - local variables declared inside function bodies are never touched,
// even if they shadow a package-level name in overrides.
//
// If overrides is empty, body is returned unchanged. If body fails to
// parse, body is also returned unchanged: the caller's own subsequent
// parse of the file (with gno's parser) will surface the real syntax
// error, so this function doesn't need to duplicate that diagnostic.
func patchXVars(fname, body string, overrides map[string]string) string {
if len(overrides) == 0 {
return body
}

fset := token.NewFileSet()

astFile, err := parser.ParseFile(fset, fname, body, parser.ParseComments)
if err != nil {
return body
}

patched := false

for _, decl := range astFile.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.VAR {
continue
}

for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}

for i, name := range valueSpec.Names {
if i >= len(valueSpec.Values) {
continue // no initializer to patch, e.g. `var x string`
}

value, ok := overrides[name.Name]
if !ok {
continue
}

lit, ok := valueSpec.Values[i].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue // not a simple string literal initializer
}

lit.Value = strconv.Quote(value)
patched = true
}
}
}

if !patched {
return body
}

var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, astFile); err != nil {
return body
}

return buf.String()
}

// xFlag implements flag.Value, collecting repeated `-X name=value` pairs
// into a map, mirroring the semantics of `go build -ldflags "-X ...=..."`.
type xFlag struct {
values map[string]string
}

// newXFlag returns an initialized, empty xFlag.
func newXFlag() *xFlag {
return &xFlag{values: map[string]string{}}
}

// String implements flag.Value.
func (x *xFlag) String() string {
if x == nil || len(x.values) == 0 {
return ""
}

parts := make([]string, 0, len(x.values))
for k, v := range x.values {
parts = append(parts, k+"="+v)
}

return strings.Join(parts, ",")
}

// Set implements flag.Value. It parses a single "name=value" pair and
// records it, overwriting any previous value for the same name. It may be
// called multiple times (once per -X flag occurrence on the command line).
func (x *xFlag) Set(s string) error {
name, value, found := strings.Cut(s, "=")
if !found || name == "" {
return fmt.Errorf("invalid -X value %q: expected format name=value", s)
}

x.values[name] = value

return nil
}
Loading
Loading