Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gnomod): forbid require and find dependencies without it #3123

Open
wants to merge 65 commits into
base: master
Choose a base branch
from

Conversation

n0izn0iz
Copy link
Contributor

@n0izn0iz n0izn0iz commented Nov 14, 2024

A step towards the importer package (#2932) and future of gno.mod (#2904)

  • BREAKING CHANGE: remove require statement support from gno.mod
  • use .gno files import statements to find dependencies
  • extract package download routines in gnovm/pkg/gnopkgfetch
  • add a GNO_PKG_HOSTS env variable that takes domain=rpcURL coma-separated pairs to override pkg fetching behavior
  • add and use a mocked rpc client that "fetch" packages from the examples dir
  • extract imports gathering utils in gnovm/pkg/gnoimports

I decided to do this first to avoid having multiple ways to resolve dependencies lying around in the codebase and causing confusion in subsequent steps

Contributors' checklist...
  • Added new tests, or not needed, or not feasible
  • Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory
  • Updated the official documentation or not needed
  • No breaking changes were made, or a BREAKING CHANGE: xxx message was included in the description
  • Added references to related issues and PRs
  • Provided any useful hints for running manual tests

Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
Signed-off-by: Norman Meier <[email protected]>
@zivkovicmilos zivkovicmilos added the breaking change Functionality that contains breaking changes label Nov 22, 2024
Copy link
Member

@zivkovicmilos zivkovicmilos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments, that you for the require removal 🙏

If this is the route we wanna go with, the PR I think accomplishes what it needs to accomplish.
Please check some comments for testing code, it's the only thing I believe we need to modify

res, err := gnoimports.PackageImports(root)
_ = err

entries, err := os.ReadDir(root)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this go before the PackageImports call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we sort, it doesn't matter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we do error handling here?

gnoimports.PackageImports does the same ReadDir, but we swallow errors from both calls?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better proposal: let's use fs.WalkDir instead of a recursive fn

gnovm/pkg/doc/dirs.go Show resolved Hide resolved
gnovm/pkg/doc/dirs.go Show resolved Hide resolved
gnovm/pkg/gnoimports/imports_test.go Outdated Show resolved Hide resolved
gnovm/pkg/gnoimports/imports.go Show resolved Hide resolved
gnovm/pkg/gnopkgfetch/gnopkgfetch.go Outdated Show resolved Hide resolved
// we need to know that gno.land/r/foo is not downloaded.
// We do this by checking for the presence of gno.land/r/foo/gno.mod
if err := os.WriteFile(modFilePath, []byte("module "+pkgPath+"\n"), 0o644); err != nil {
return fmt.Errorf("failed to write modfile at %q: %w", modFilePath, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no cleanup if this fails, this is why I suggest you break up this mega func

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As was the case with the previous implementation

I agree with you, we also miss a filelock to avoid conflict between multiple gno command instances. I intend to improve all this in the subsequent PRs towards the unified package loader but I feel it's out of scope of this one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As was the case with the previous implementation

💀

@@ -0,0 +1,99 @@
package gnopkgfetch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a test file, so it's compiled into the binary. Please change it 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to do it any other way, pls see #3123 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you drop it now, after this fix you mentioned?

gnovm/pkg/gnopkgfetch/gnopkgfetch_testing.go Outdated Show resolved Hide resolved
examplesRoot string
}

func (m *examplesMockClient) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we clean up this mock a bit? 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very hard to read

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll clean up if you validate the new way it's injected :) otherwise I'll probably have to scrap it


res := ctypes.ResultABCIQuery{}

finfo, err := os.Stat(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
}

if finfo.IsDir() {
entries, err := os.ReadDir(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
}
res.Response.Data = []byte(strings.Join(files, "\n"))
} else {
content, err := os.ReadFile(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

res := ctypes.ResultABCIQuery{}

finfo, err := os.Stat(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1). This path depends on a [user-provided value](2). This path depends on a [user-provided value](3). This path depends on a [user-provided value](4).
}

if finfo.IsDir() {
entries, err := os.ReadDir(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1). This path depends on a [user-provided value](2). This path depends on a [user-provided value](3). This path depends on a [user-provided value](4).
}
res.Response.Data = []byte(strings.Join(files, "\n"))
} else {
content, err := os.ReadFile(target)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression

This path depends on a [user-provided value](1). This path depends on a [user-provided value](2). This path depends on a [user-provided value](3). This path depends on a [user-provided value](4).
Copy link
Member

@zivkovicmilos zivkovicmilos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changes are good for now. Thank you for applying the fixes 🙏

Please see my leftover comments, so we can avoid having gnopkgfetch_testing compiled 🙏

When we fix this, I think we can move forward with the PR

Pinging @thehowl for a second screen

Copy link
Member

@thehowl thehowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some re-organizations, a bunch of nits; but requesting changes as I want to have another look before merging.

@@ -105,7 +105,7 @@ func Parse(file string, data []byte) (*File, error) {
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
})
continue
case "module", "require", "replace":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it should go here or elsewhere, but gno mod tidy should be capable of removing require blocks. This way gno mod tidy can automatically fix other repos

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add this extra complexity and maintenance burden when we can just recommended a well crafted sed one-liner?

@@ -0,0 +1,72 @@
package gnoimports
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely shouldn't be called gnoimports; and thinking that this should possibly be together with the package loader logic eventually. Can we use packages for now (inspired by x/tools/go/packages), with the following godoc?

// Package packages provides utility functions to statically analyze Gno MemPackages.

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The go x/tools/go/packages is a wrapper around go list command invocations.

As stated in it's doc, it's responsibility is: loads Go packages for inspection and analysis.
I dived deep into the "loads Go packages" part code and it involves a lot more things than utility functions to statically analyze packages.
It has to pass by the filesystem since it's also responsible for downloading missing packages so in our case it couldn't operate on only MemPackages.

I think we should keep the gnoimports name with this doc:

// Package gnoimports provides utility functions to get the list of imports from a gno file or package

Added the doc file in d90f89a

"strings"
)

// PackageImports returns the list of gno imports from a given path.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the previous proposal, I'd say to call this function Imports instead (usage: packages.Imports), and to accept a MemPackage rather than a path to the filesystem.

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No call site have a MemPackage handy, I could refacto things but it seems out of scope and not really an improvement

}

// FileImports returns the list of gno imports in a given file.
func FileImports(fname string) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should process a filename + body rather than a filename.

Can you change pkg/test.LoadImports to use this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

246bfbc

I'm not sure it's better this way though

}
res := make([]string, len(f.Imports))
for i, im := range f.Imports {
importPath := strings.TrimPrefix(strings.TrimSuffix(im.Path.Value, `"`), `"`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strconv.Unquote

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func fetchPackage(io commands.IO, pkgPath string, dst string) error {
modFilePath := filepath.Join(dst, "gno.mod")

if _, err := os.Stat(modFilePath); err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a way to force download (ie. -u)

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems out of scope but I can do it in this PR if you really want

@@ -0,0 +1,164 @@
package gnopkgfetch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose to have this package with a simpler API: Download(pkgPath, dst string). The code that walks the directory and loads the imports, and detects whether they're downloaded should go in cmd/gno; so we also restrict the output from happening within there.

This can then be called pkgdownload; likely this should eventually sit as a subpackage of the modules loader (maybe add this as a note comment in the package-level godoc, which you should also write ;) )

"strings"
"unicode"

"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: please remove this dependency

(I'm tempted to think that eventually, we could have gnoweb serve a tarball of the package for downloaded; and avoid any tm2 client here; but this is for another time.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +115 to +118
if rpcURL == "gno-examples" {
examplesDir := filepath.Join(gnoenv.RootDir(), "examples")
return tm2client.NewRPCClient(&examplesMockClient{examplesRoot: examplesDir}), nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still unclear how this works and whether it is a good idea.

Draft bool // whether the package is a draft
Dir string // absolute path to package dir
Name string // package name
Imports []string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a comment on the fact that this is transitional as well

Copy link
Contributor Author

@n0izn0iz n0izn0iz Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only direct imports actually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Functionality that contains breaking changes in focus Core team is prioritizing this work 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Status: In Review
Development

Successfully merging this pull request may close these issues.

5 participants