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

Adds support for go modules outside of GOPATH #548

Closed
wants to merge 9 commits into from
Closed
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
8 changes: 8 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ required = ["github.com/vektah/dataloaden"]
[[constraint]]
name = "github.com/rs/cors"
version = "1.6.0"

[[constraint]]
name = "github.com/fortytw2/leaktest"
version = "1.3.0"
16 changes: 3 additions & 13 deletions codegen/testserver/generated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"runtime"
"sort"
"sync"
"testing"
"time"

"github.com/99designs/gqlgen/graphql/introspection"

"github.com/99designs/gqlgen/client"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection"
"github.com/99designs/gqlgen/handler"
"github.com/fortytw2/leaktest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -125,7 +123,7 @@ func TestGeneratedServer(t *testing.T) {

t.Run("subscriptions", func(t *testing.T) {
t.Run("wont leak goroutines", func(t *testing.T) {
initialGoroutineCount := runtime.NumGoroutine()
defer leaktest.Check(t)()

sub := c.Websocket(`subscription { updated }`)

Expand All @@ -141,14 +139,6 @@ func TestGeneratedServer(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "message", msg.resp.Updated)
sub.Close()

// need a little bit of time for goroutines to settle
start := time.Now()
for time.Since(start).Seconds() < 2 && initialGoroutineCount != runtime.NumGoroutine() {
time.Sleep(5 * time.Millisecond)
}

require.Equal(t, initialGoroutineCount, runtime.NumGoroutine())
})

t.Run("will parse init payload", func(t *testing.T) {
Expand Down
24 changes: 5 additions & 19 deletions docs/content/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@ You can find the finished code for this tutorial [here](https://github.com/vekta

## Install gqlgen

This article uses [`dep`](https://github.com/golang/dep) to install gqlgen. [Follow the instructions for your environment](https://github.com/golang/dep) to install.

Assuming you already have a working [Go environment](https://golang.org/doc/install), create a directory for the project in your `$GOPATH`:
Assuming you already have a working [Go environment](https://golang.org/doc/install) using Go 1.11 or higher, create a directory for the project:

```sh
$ mkdir -p $GOPATH/src/github.com/[username]/gqlgen-todos
$ mkdir -p ~/github.com/[username]/gqlgen-todos
$ cd ~/github.com/[username]/gqlgen-todos
$ go mod init github.com/[username]/gqlgen-todos
```

> Go Modules
>
> Currently `gqlgen` does not support Go Modules. This is due to the [`loader`](https://godoc.org/golang.org/x/tools/go/loader) package, that also does not yet support Go Modules. We are looking at solutions to this and the issue is tracked in Github.

Add the following file to your project under `scripts/gqlgen.go`:

```go
Expand All @@ -42,12 +38,6 @@ func main() {
}
```

Lastly, initialise dep. This will inspect any imports you have in your project, and pull down the latest tagged release.

```sh
$ dep init
```

## Building the server

### Define the schema
Expand Down Expand Up @@ -85,6 +75,7 @@ type Mutation {
### Create the project skeleton

```bash
$ mkdir server
$ go run scripts/gqlgen.go init
```

Expand All @@ -96,11 +87,6 @@ This has created an empty skeleton with all files you need:
- `resolver.go` — This is where your application code lives. `generated.go` will call into this to get the data the user has requested.
- `server/server.go` — This is a minimal entry point that sets up an `http.Handler` to the generated GraphQL server.

Now run dep ensure, so that we can ensure that the newly generated code's dependencies are all present:

```sh
$ dep ensure
```

### Create the database models

Expand Down
29 changes: 29 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/99designs/gqlgen

require (
github.com/agnivade/levenshtein v0.0.0-20180303095733-1787a73e302c
github.com/davecgh/go-spew v1.1.0
github.com/go-chi/chi v3.3.2+incompatible
github.com/gogo/protobuf v1.0.0
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f
github.com/gorilla/mux v1.6.1
github.com/gorilla/websocket v1.2.0
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047
github.com/opentracing/basictracer-go v1.0.0
github.com/opentracing/opentracing-go v1.0.2
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0
github.com/rs/cors v1.6.0
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0
github.com/stretchr/testify v1.2.1
github.com/urfave/cli v1.20.0
github.com/vektah/dataloaden v0.0.0-20180713105249-314ac81052ee
github.com/vektah/gqlparser v1.1.0
golang.org/x/net v0.0.0-20180404174746-b3c676e531a6
golang.org/x/tools v0.0.0-20180215025520-ce871d178848
gopkg.in/yaml.v2 v2.2.1
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67
)
69 changes: 68 additions & 1 deletion internal/gopath/gopath.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package gopath
import (
"fmt"
"go/build"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
)

var NotFound = fmt.Errorf("not on GOPATH")
var ModuleNameRegexp = regexp.MustCompile(`module\s+(.*)`)

// Contains returns true if the given directory is in the GOPATH
func Contains(dir string) bool {
Expand All @@ -24,7 +28,32 @@ func Dir2Import(dir string) (string, error) {
return dir[len(gopath)+1:], nil
}
}
return "", NotFound

// The following code handles the go modules in a manner that does not break existing code. However, it is not
// efficient because it requires several round trips to the filesystem. It also should technically be the first
// method we try (before GOPATH) because if someone creates a 'go mod' project that also happens to be in the
// GOPATH, the import path will be calculated from GOPATH and not from the go.mod file.
//
// Possible Fixes:
// 1) Cache the results of scanning the filesystem. The code is typically called at compile-time, so its unlikely
// for a go.mod file to be added or altered during compile.
// 2) Add an optional switch that turns off GOPATH checks entirely when go.mod is used on a project.
// 3) Rewrite gqlgen to make use of 'golang.org/x/tools/go/packages' for determining import paths instead.

// Scan the path tree for a directory that contains a 'go.mod' file and read the module name from it
modDirectory := findGoMod(dir)
if 0 == len(modDirectory) {
return "", NotFound
}
modName := moduleName(filepath.Join(modDirectory, "go.mod"))
if 0 == len(modName) {
return "", NotFound
}

// At this point 'dir' looks something like '/root/path/to/some/dir', 'modDirectory' looks like '/root/path',
// and 'modName' looks like 'grabhub.com/myname/vunderprojekt'. The correct import path is:
// 'grabhub.com/myname/vunderprojekt/to/some/dir'
return fmt.Sprintf("%s%s", modName, strings.TrimPrefix(dir, filepath.ToSlash(modDirectory))), nil
}

// MustDir2Import takes an *absolute* path and returns a golang import path for the package, and panics if it isn't on the gopath
Expand All @@ -35,3 +64,41 @@ func MustDir2Import(dir string) string {
}
return pkg
}

// Returns the path to the first go.mod file in the parent tree starting with the specified directory. Returns "" if not found
func findGoMod(srcDir string) string {
abs, err := filepath.Abs(srcDir)
if err != nil {
return ""
}
for {
info, err := os.Stat(filepath.Join(abs, "go.mod"))
if err == nil && !info.IsDir() {
break
}
d := filepath.Dir(abs)
if len(d) >= len(abs) {
return "" // reached top of file system, no go.mod
}
abs = d
}

return abs
}

// Returns the main module name from a go.mod file. Returns "" if it cannot be found
func moduleName(file string) string {
data, err := ioutil.ReadFile(file)
if nil != err {
return ""
}

// Search for `module some/name`
matches := ModuleNameRegexp.FindSubmatch(data)
if len(matches) < 2 {
return ""
}

// The first element is the whole line, the second is the capture group we specified for the module name.
return string(matches[1])
}
30 changes: 30 additions & 0 deletions internal/gopath/gopath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package gopath

import (
"go/build"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"

Expand All @@ -12,6 +15,13 @@ func TestContains(t *testing.T) {
origBuildContext := build.Default
defer func() { build.Default = origBuildContext }()

// Make a temporary directory and add a go.mod file for the package 'foo'
fooDir, err := ioutil.TempDir("", "gopath")
assert.Nil(t, err)
defer func() { e := os.RemoveAll(fooDir); assert.Nil(t, e) }()
err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644)
assert.Nil(t, err)

if runtime.GOOS == "windows" {
build.Default.GOPATH = `C:\go;C:\Users\user\go`

Expand All @@ -22,6 +32,10 @@ func TestContains(t *testing.T) {
assert.False(t, Contains(`C:\tmp`))
assert.False(t, Contains(`C:\Users\user`))
assert.False(t, Contains(`C:\Users\another\go`))

// C:/Users/someone/AppData/Local/Temp/gopath123456/bar
assert.True(t, Contains(filepath.Join(fooDir, "bar")))

} else {
build.Default.GOPATH = "/go:/home/user/go"

Expand All @@ -31,13 +45,23 @@ func TestContains(t *testing.T) {
assert.False(t, Contains("/tmp"))
assert.False(t, Contains("/home/user"))
assert.False(t, Contains("/home/another/go"))

// /tmp/gopath123456/bar
assert.True(t, Contains(filepath.Join(fooDir, "bar")))
}
}

func TestDir2Package(t *testing.T) {
origBuildContext := build.Default
defer func() { build.Default = origBuildContext }()

// Make a temporary directory and add a go.mod file for the package 'foo'
fooDir, err := ioutil.TempDir("", "gopath")
assert.Nil(t, err)
defer func() { e := os.RemoveAll(fooDir); assert.Nil(t, e) }()
err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644)
assert.Nil(t, err)

if runtime.GOOS == "windows" {
build.Default.GOPATH = "C:/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;C:/a/y;C:/b/"

Expand All @@ -49,6 +73,9 @@ func TestDir2Package(t *testing.T) {
assert.PanicsWithValue(t, NotFound, func() {
MustDir2Import("C:/tmp/foo")
})

// C:/Users/someone/AppData/Local/Temp/gopath123456/bar
assert.Equal(t, "foo/bar", MustDir2Import(filepath.Join(fooDir, "bar")))
} else {
build.Default.GOPATH = "/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:/a/y:/b/"

Expand All @@ -58,5 +85,8 @@ func TestDir2Package(t *testing.T) {
assert.PanicsWithValue(t, NotFound, func() {
MustDir2Import("/tmp/foo")
})

// /tmp/gopath123456/bar
assert.Equal(t, "foo/bar", MustDir2Import(filepath.Join(fooDir, "bar")))
}
}