├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── conf └── conf.go ├── debian └── DEBIAN │ └── control ├── exec └── cmd.go ├── gopathfs ├── attr.go ├── attr_darwin.go ├── attr_linux.go ├── dir.go ├── file.go └── gopathfs.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | *.deb 7 | debian/usr 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, linuxerwang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=0.1.10 2 | 3 | all: 4 | @echo "make binary: build gobazel binary" 5 | @echo "make debian: build gobazel deb package" 6 | @echo "make fmt: format golang source code" 7 | 8 | binary: 9 | go install -a -ldflags "-s -w -X main.version=${VERSION}" github.com/linuxerwang/gobazel 10 | 11 | deb: 12 | go build -o debian/usr/bin/gobazel -a -ldflags "-s -w -X main.version=${VERSION}" github.com/linuxerwang/gobazel 13 | cd debian; fakeroot dpkg -b . .. 14 | 15 | fmt: 16 | go fmt . 17 | go fmt ./conf 18 | go fmt ./exec 19 | go fmt ./gopathfs 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gobazel 2 | 3 | gobazel is a tool to help golang bazel developers to map bazel's folder 4 | structure to golang's standard folder structure, through FUSE & FsNotify 5 | (thus only works for Linux users). 6 | 7 | ## Why gobazel 8 | 9 | First, I love to use [bazel](https://bazel.io) and 10 | [rules_go](https://github.com/bazelbuild/rules_go) to manage my projects. 11 | Bazel is created by Google for huge amounts of inter-related projects 12 | (imagine you have thousands of projects and they depend on each other in 13 | the most complex way). Besides the many great features of Bazel, its way 14 | of dependency management makes it much easier to manage such projects. 15 | 16 | ### What's the Problem with bazel (and rules_go)? 17 | 18 | Bazel's folder layout is very different from most existing languages, 19 | thus causes many problems for developers. The top issue is the poor IDE 20 | and tools support. Bazel team released demo plugins for Eclipse and Intellij, 21 | but none of them works smoothly for me. 22 | 23 | For Golang programming, the problem is much worse. The standard Golang project 24 | structure is with a GOPATH, in which three standard folders are expected: bin, 25 | pkg, and src. All source codes are put in src folder, both your code and the 26 | dependant 3rd-party code. The following shows a typical GOPATH layout: 27 | 28 | ``` 29 | $TOP 30 | |- bin/ 31 | |- pkg/ 32 | |- src/ 33 | |- github.com/ 34 | | |- ... 35 | | 36 | |- mycompany.com/ 37 | |- my-prod-1/ 38 | |- my-prod-2/ 39 | |- my-prod-3/ 40 | |- ... 41 | ``` 42 | 43 | In comparison, the same projects can be organized better with bazel: 44 | 45 | ``` 46 | $TOP 47 | |- bazel-bin/ 48 | |- bazel-genfiles/ 49 | |- ... 50 | |- my-prod-1/ 51 | |- my-prod-2/ 52 | |- my-prod-3/ 53 | |- ... 54 | |- third-party-go/ 55 | | |- vendor/ 56 | | |- github.com/ 57 | | |- ... 58 | |- WORKSPACE 59 | ``` 60 | 61 | rules_go lets you specify a Go package prefix, for example "mycompany.com", 62 | which is not represented in the above layout. Instead, rules_go uses certain 63 | techniques to trick the Go compiler to work the prefix around. 64 | 65 | It causes serious problems because there is no GOPATH any more, and most of 66 | the existing Golang tools depend on the GOPATH! So now godoc stops working, 67 | go-guru stops working, and most IDEs stop working. 68 | 69 | ### gobazel to the Rescue 70 | 71 | So Go tools and IDEs requires a GOPATH, which is not in alignment with bazel's 72 | way of organizing source codes. That's why gobazel was born: utilizing the 73 | FUSE virtual file system, it simulates a $GOPATH/src by mapping folders 74 | properly, thus satisfies both sides: 75 | 76 | - A top folder outside of bazel workspace should be created as the GOPATH. 77 | 78 | - Within $GOPATH three folders should be created: bin, src, pkg. 79 | 80 | - FUSE mount the virtual file system on $GOPATH/src. 81 | 82 | - A $GOPATH/src/ is simulated, such as: 83 | $GOPATH/src/mycompany.com. 84 | 85 | - All top level folders (my-prod-1, ...) except the third-party-go will 86 | be mapped under $GOPATH/src/. 87 | 88 | - All folders under third-party-go/vendor will be mapped to under $GOPATH/src. 89 | 90 | - The bazel-* links will be ignored, except that all entries in bazel-genfiles 91 | will be mapped under $GOPATH/src/. 92 | 93 | Once the above has been done, the GOPATH can be set to the new top folder and 94 | everything else will just work by itself, because from Golang tools it's 95 | simply a real GOPATH. 96 | 97 | ## Installation and Setup 98 | 99 | ```bash 100 | $ go get github.com/linuxerwang/gobazel 101 | ``` 102 | 103 | Make sure the compiled command "gobazel" be put into your $PATH. Yes, you 104 | need a normal GOPATH to go get gobazel. Once you get the command, you don't 105 | need this GOPATH any more. 106 | 107 | Next, make a new empty GOPATH. Suppose your bazel workspace is at ~/my-bazel 108 | and your Go package prefix is "mycompany.com". You could create it at 109 | ~/my-bazel-gopath. 110 | 111 | Execute command "gobazel" under ~/my-bazel: 112 | 113 | ```bash 114 | me@laptop:~/my-bazel$ gobazel 115 | Created gobazel config file /home/me/my-bazel/.gobazelrc, 116 | please customize it and run the command again. 117 | ``` 118 | 119 | Customize the file .gobazelrc to fit your layout. Mine looks like: 120 | 121 | ``` 122 | gobazel { 123 | go-path: "/home/me/my-bazel-gopath" 124 | go-pkg-prefix: "mycompany.com" 125 | go-ide-cmd: "/usr/bin/atom" 126 | # go-ide-cmd: "/home/tools/liteide/bin/liteide" 127 | 128 | vendor-dirs: [ 129 | "third-party-go/vendor", 130 | ] 131 | 132 | ignore-dirs: [ 133 | "bazel-.*", 134 | "third-party.*", 135 | ] 136 | 137 | fall-through-dirs: [ 138 | ".vscode", 139 | ] 140 | } 141 | ``` 142 | 143 | You can set up your favorite IDE, or specify empty. 144 | 145 | The last step, execute "gobazel" command again (in the bazel workspace), 146 | and you should see the IDE launched and everything worked. 147 | 148 | Flag --build enables gobazel to build all bazel targets which satisfy the 149 | given criteria. An example config looks as follows: 150 | 151 | ``` 152 | gobazel { 153 | ... 154 | 155 | build { 156 | rules: [ 157 | "genproto_go", 158 | ] 159 | ignore-dirs: [ 160 | "bazel-.*", 161 | "third-party.*", 162 | ] 163 | } 164 | 165 | ... 166 | } 167 | 168 | ``` 169 | 170 | Flag --debug enables gobazel to print out verbose debug information. 171 | 172 | ## Remote Debug with Delve (dlv) 173 | 174 | Start your binary with dlv: 175 | 176 | ```bash 177 | $ dlv exec bazel-bin/myserver/cmd/myserver/myserver --headless --listen=:2345 --log [-- ] 178 | ``` 179 | 180 | In VS code, add a debug configuration: 181 | 182 | ```js 183 | { 184 | "version": "0.2.0", 185 | "configurations": [ 186 | { 187 | "name": "bazel-bin/myserver/cmd/myserver/myserver", 188 | "type": "go", 189 | "request": "launch", 190 | "mode": "remote", 191 | "remotePath": "/home/linuxerwang/.cache/bazel/_bazel_linuxerwang/eedeac95b950221f7e2a454b8c435113/bazel-sandbox/93167466216111235/execroot/__main__", 192 | "port": 2345, 193 | "host": "127.0.0.1", 194 | "program": "${workspaceRoot}/mycompany.com", 195 | "env": {}, 196 | "args": [] 197 | } 198 | ] 199 | } 200 | ``` 201 | 202 | The "remotePath" field can be extracted with gdb: 203 | 204 | ``` 205 | $ gdb bazel-bin/myserver/cmd/myserver/myserver -batch -ex "list" -ex "info source" 206 | warning: Missing auto-load script at offset 0 in section .debug_gdb_scripts of file /home/linuxerwang/.cache/bazel/_bazel_linuxerwang/eedeac95b950221f7e2a454b8c435113/execroot/__main__/bazel-out/k8-dbg/bin/myserver/cmd/myserver/myserver. 207 | Use `info auto-load python-scripts [REGEXP]' to list them. 208 | warning: Source file is more recent than executable. 209 | 24 flagFile = flag.String("f", "", "The CSV file") 210 | 25 211 | 26 success, failure int32 212 | 27 ) 213 | 28 214 | 29 func usage() { 215 | 30 fmt.Println("Usage:") 216 | Current source file is myserver/cmd/myserver/main.go 217 | Compilation directory is /home/linuxerwang/.cache/bazel/_bazel_linuxerwang/eedeac95b950221f7e2a454b8c435113/bazel-sandbox/93167466216111235/execroot/__main__ 218 | Located in /home/linuxerwang/my-client/myserver/cmd/myserver/main.go 219 | Contains 129 lines. 220 | Source language is asm. 221 | Producer is Go cmd/compile go1.9.2. 222 | Compiled with DWARF 2 debugging format. 223 | Does not include preprocessor macro info. 224 | ``` 225 | 226 | Note that the path is in the line starting with "Compilation directory". 227 | 228 | Now you can do remote debug in vscode with F5. It can trace the Go-SDK (using 229 | the Go-SDK in bazel external directory) and third party code correctly. 230 | 231 | ## Caveates 232 | 233 | - At present it only works on Linux and OSX (thanks excavador for adding the OSX 234 | support). It requires FUSE, fsnotify. 235 | 236 | - It works for Golang programming with bazel. Might also work for Java 237 | after code change (not planned for now). 238 | 239 | - Do not change files both in bazel workspace and in the simulated GOPATH! 240 | At right now the fsnotify doesn't work between the simulated GOPATH 241 | and bazel workspace. So if a file is changed in bazel workspace, 242 | the IDE (on simulated GOPATH) will not notice the change thus you might 243 | overwrite files accidentally (and even without realizing it). Some 244 | reference documents: 245 | 246 | - https://github.com/libfuse/libfuse/wiki/Fsnotify-and-FUSE 247 | - https://www.howtoforge.com/tutorial/monitoring-file-changes-with-linux-over-the-network/ 248 | 249 | I didn't make it work. Fix's welcome. 250 | 251 | - For files generated in bazel-genfiles, you have to manually run bazel 252 | command in bazel workspace. gobazel will not automatically run it. 253 | 254 | - Tested on LiteIDE and Atom. Tested godoc, go-guru. 255 | 256 | - The file deletion in Atom is through moving to trash, which is not 257 | supported in gobazel. You can install the package "permanent-delete": 258 | https://atom.io/packages/permanent-delete. 259 | 260 | ## Acknowledgement 261 | 262 | - FUSE bindings for Go: https://github.com/hanwen/go-fuse 263 | - File system event notification: https://github.com/rjeczalik/notify 264 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/linuxerwang/confish" 8 | ) 9 | 10 | // GobazelConf represents the bazel build config. 11 | type BuildConf struct { 12 | Rules []string `cfg-attr:"rules"` 13 | Ignores []string `cfg-attr:"ignore-dirs"` 14 | } 15 | 16 | // GobazelConf represents the gobazel global config. 17 | type GobazelConf struct { 18 | GoPath string `cfg-attr:"go-path"` 19 | GoPkgPrefix string `cfg-attr:"go-pkg-prefix"` 20 | GoIdeCmd string `cfg-attr:"go-ide-cmd"` 21 | Ignores []string `cfg-attr:"ignore-dirs"` 22 | Vendors []string `cfg-attr:"vendor-dirs"` 23 | FallThrough []string `cfg-attr:"fall-through-dirs"` 24 | Build *BuildConf `cfg-attr:"build"` 25 | 26 | IgnoreSet map[string]struct{} 27 | VendorSet map[string]struct{} 28 | FallThroughSet map[string]struct{} 29 | } 30 | 31 | type confWrapper struct { 32 | Conf *GobazelConf `cfg-attr:"gobazel"` 33 | } 34 | 35 | // LoadConfig loads gobazel config from the given file. 36 | func LoadConfig(cfgPath string) *GobazelConf { 37 | cfg := confWrapper{} 38 | if err := confish.ParseFile(cfgPath, &cfg); err != nil { 39 | fmt.Printf("Failed to parse gobazel config file %s, %+v.\n", cfgPath, err) 40 | os.Exit(2) 41 | } 42 | cfg.Conf.IgnoreSet = toSet(cfg.Conf.Ignores) 43 | cfg.Conf.VendorSet = toSet(cfg.Conf.Vendors) 44 | cfg.Conf.FallThroughSet = toSet(cfg.Conf.FallThrough) 45 | return cfg.Conf 46 | } 47 | 48 | func toSet(slice []string) map[string]struct{} { 49 | set := map[string]struct{}{} 50 | for _, ele := range slice { 51 | set[ele] = struct{}{} 52 | } 53 | return set 54 | } 55 | -------------------------------------------------------------------------------- /debian/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: gobazel 2 | Version: 0.1.10 3 | Architecture: amd64 4 | Maintainer: linuxerwang@gmail.com 5 | Section: utils 6 | Priority: optional 7 | Description: A tool to help golang bazel developers to map bazel's folder structure to golang's standard folder structure. 8 | Installed-Size: 2436 9 | -------------------------------------------------------------------------------- /exec/cmd.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/linuxerwang/gobazel/conf" 11 | ) 12 | 13 | // RunGoInstall executes the "go install" command on the given Go package. 14 | func RunGoInstall(cfg *conf.GobazelConf, goPkg string) { 15 | cmd := fmt.Sprintf("go install %s", goPkg) 16 | fmt.Print(cmd) 17 | if err := RunCommand(cfg, cmd); err != nil { 18 | fmt.Println(" (failed!)") 19 | } else { 20 | fmt.Println(" (done)") 21 | } 22 | } 23 | 24 | // RunGoWalkInstall walks the given proj directory and run "go install" 25 | // for each go package. 26 | func RunGoWalkInstall(cfg *conf.GobazelConf, workspace, proj string) { 27 | filepath.Walk(filepath.Join(workspace, proj), func(path string, info os.FileInfo, err error) error { 28 | if info.Name() == "BUILD" { 29 | if dir, err := filepath.Rel(workspace, path); err == nil { 30 | dir = filepath.Dir(dir) 31 | for _, v := range cfg.Vendors { 32 | if strings.HasPrefix(dir, v) { 33 | // Ignore third party Go vendor directories. 34 | return nil 35 | } 36 | } 37 | 38 | RunGoInstall(cfg, filepath.Join(cfg.GoPkgPrefix, dir)) 39 | } 40 | } 41 | return nil 42 | }) 43 | } 44 | 45 | // RunBazelQuery executes "bazel query" for the given folder and returns 46 | // all bazel build targets under this sub tree. 47 | func RunBazelQuery(workspace, folder string, command []string, targets map[string]struct{}) { 48 | cmd := exec.Command(command[0], command[1:]...) 49 | cmd.Dir = workspace 50 | out, _ := cmd.Output() 51 | 52 | for _, line := range strings.Split(string(out), "\n") { 53 | line = strings.TrimSpace(line) 54 | if line == "" { 55 | continue 56 | } 57 | if strings.HasPrefix(line, "//"+folder+"/") { 58 | targets[line] = struct{}{} 59 | } 60 | } 61 | } 62 | 63 | // RunBazelBuild executes "bazel build" for the given bazel build target. 64 | func RunBazelBuild(workspace, target string) { 65 | cmd := exec.Command("bazel", "build", target) 66 | cmd.Dir = workspace 67 | 68 | fmt.Printf("bazel build %s", target) 69 | if err := cmd.Run(); err != nil { 70 | fmt.Println(" (failed!)") 71 | } else { 72 | fmt.Println(" (done)") 73 | } 74 | } 75 | 76 | // RunCommand executes the given command. 77 | func RunCommand(cfg *conf.GobazelConf, command string) error { 78 | parts := strings.Split(command, " ") 79 | cmd := exec.Command(parts[0], parts[1:]...) 80 | cmd.Env = replaceGoPath(cfg) 81 | return cmd.Run() 82 | } 83 | 84 | func replaceGoPath(cfg *conf.GobazelConf) []string { 85 | environ := []string{fmt.Sprintf("GOPATH=%s", cfg.GoPath)} 86 | env := os.Environ() 87 | for _, e := range env { 88 | if strings.HasPrefix(e, "GOPATH=") { 89 | continue 90 | } 91 | environ = append(environ, e) 92 | } 93 | return environ 94 | } 95 | -------------------------------------------------------------------------------- /gopathfs/attr.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/hanwen/go-fuse/fuse" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | // GetAttr overwrites the parent's GetAttr method. 12 | func (gpf *GoPathFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { 13 | if name == "" { 14 | return gpf.getTopDirAttr() 15 | } 16 | 17 | // Handle the virtual Golang prefix package. 18 | if name == gpf.cfg.GoPkgPrefix { 19 | return gpf.getFirstPartyDirAttr() 20 | } 21 | 22 | // Handle the children of the virtual Golang prefix package. 23 | prefix := gpf.cfg.GoPkgPrefix + pathSeparator 24 | if strings.HasPrefix(name, prefix) { 25 | name = name[len(prefix):] 26 | attr, status := gpf.getFirstPartyChildDirAttr(name) 27 | if status == fuse.OK { 28 | return attr, fuse.OK 29 | } 30 | } 31 | 32 | // Search in fall-through directories. 33 | for _, v := range gpf.cfg.FallThrough { 34 | if name == v || strings.HasPrefix(name, v) { 35 | return gpf.getRealDirAttr(filepath.Join(gpf.dirs.Workspace, name)) 36 | } 37 | } 38 | 39 | // Search in vendor directories. 40 | for _, v := range gpf.cfg.Vendors { 41 | fname := filepath.Join(gpf.dirs.Workspace, v, name) 42 | attr, status := gpf.getRealDirAttr(fname) 43 | if status == fuse.OK { 44 | return attr, fuse.OK 45 | } 46 | 47 | // Also search in bezel-genfiles. 48 | fname = filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", v, name) 49 | attr, status = gpf.getRealDirAttr(fname) 50 | if status == fuse.OK { 51 | return attr, fuse.OK 52 | } 53 | } 54 | 55 | return nil, fuse.ENOENT 56 | } 57 | 58 | func (gpf *GoPathFs) getTopDirAttr() (*fuse.Attr, fuse.Status) { 59 | return &fuse.Attr{ 60 | Mode: fuse.S_IFDIR | 0755, 61 | }, fuse.OK 62 | } 63 | 64 | func (gpf *GoPathFs) getFirstPartyDirAttr() (*fuse.Attr, fuse.Status) { 65 | return &fuse.Attr{ 66 | Mode: fuse.S_IFDIR | 0755, 67 | }, fuse.OK 68 | } 69 | 70 | func (gpf *GoPathFs) getFirstPartyChildDirAttr(name string) (*fuse.Attr, fuse.Status) { 71 | // Search in GOROOT (for debugger). 72 | if name == "GOROOT" || strings.HasPrefix(name, "GOROOT"+pathSeparator) { 73 | return gpf.getRealDirAttr(filepath.Join(gpf.dirs.GoSDKDir, name[len("GOROOT"):])) 74 | } 75 | 76 | nm := filepath.Join(gpf.dirs.Workspace, name) 77 | attr, status := gpf.getRealDirAttr(name) 78 | if status == fuse.OK { 79 | return attr, fuse.OK 80 | } 81 | 82 | // Search in bazel-genfiles directories. 83 | nm = filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", name) 84 | return gpf.getRealDirAttr(nm) 85 | } 86 | 87 | func (gpf *GoPathFs) getRealDirAttr(name string) (*fuse.Attr, fuse.Status) { 88 | t := unix.Stat_t{} 89 | err := unix.Stat(name, &t) 90 | if err != nil { 91 | return nil, fuse.ENOENT 92 | } 93 | 94 | attr := unixAttrToFuseAttr(t) 95 | 96 | return &attr, fuse.OK 97 | } 98 | -------------------------------------------------------------------------------- /gopathfs/attr_darwin.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "github.com/hanwen/go-fuse/fuse" 5 | "golang.org/x/sys/unix" 6 | ) 7 | 8 | func unixAttrToFuseAttr(from unix.Stat_t) (result fuse.Attr) { 9 | result.Ino = from.Ino 10 | result.Size = uint64(from.Size) 11 | result.Blocks = uint64(from.Blocks) 12 | result.Mode = uint32(from.Mode) 13 | 14 | sec, nsec := from.Atimespec.Unix() 15 | result.Atime = uint64(sec) 16 | result.Atimensec = uint32(nsec) 17 | 18 | sec, nsec = from.Ctimespec.Unix() 19 | result.Ctime = uint64(sec) 20 | result.Ctimensec = uint32(nsec) 21 | 22 | sec, nsec = from.Mtimespec.Unix() 23 | result.Mtime = uint64(sec) 24 | result.Mtimensec = uint32(nsec) 25 | 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /gopathfs/attr_linux.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "github.com/hanwen/go-fuse/fuse" 5 | "golang.org/x/sys/unix" 6 | ) 7 | 8 | func unixAttrToFuseAttr(from unix.Stat_t) (result fuse.Attr) { 9 | result.Ino = from.Ino 10 | result.Size = uint64(from.Size) 11 | result.Blocks = uint64(from.Blocks) 12 | result.Mode = from.Mode 13 | 14 | sec, nsec := from.Atim.Unix() 15 | result.Atime = uint64(sec) 16 | result.Atimensec = uint32(nsec) 17 | 18 | sec, nsec = from.Ctim.Unix() 19 | result.Ctime = uint64(sec) 20 | result.Ctimensec = uint32(nsec) 21 | 22 | sec, nsec = from.Mtim.Unix() 23 | result.Mtime = uint64(sec) 24 | result.Mtimensec = uint32(nsec) 25 | 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /gopathfs/dir.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/hanwen/go-fuse/fuse" 10 | ) 11 | 12 | // OpenDir overwrites the parent's OpenDir method. 13 | func (gpf *GoPathFs) OpenDir(name string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { 14 | if name == "" { 15 | return gpf.openTopDir() 16 | } 17 | 18 | if name == gpf.cfg.GoPkgPrefix { 19 | return gpf.openFirstPartyDir() 20 | } 21 | 22 | if strings.HasPrefix(name, gpf.cfg.GoPkgPrefix+pathSeparator) { 23 | return gpf.openFirstPartyChildDir(name) 24 | } 25 | 26 | entries := []fuse.DirEntry{} 27 | var status fuse.Status 28 | 29 | // Search in fall-through directories. 30 | for _, dir := range gpf.cfg.FallThrough { 31 | if dir == name || strings.HasPrefix(name, dir) { 32 | fname := filepath.Join(gpf.dirs.Workspace, name) 33 | entries, status = gpf.openUnderlyingDir(fname, nil /* excludes */, entries) 34 | if status == fuse.OK { 35 | return entries, fuse.OK 36 | } 37 | fmt.Printf("failed to open entry %s\n", fname) 38 | return nil, fuse.ENOENT 39 | } 40 | } 41 | 42 | // Search in vendor directories. 43 | for _, vendor := range gpf.cfg.Vendors { 44 | entries, status = gpf.openVendorChildDir(vendor, name, entries) 45 | if status == fuse.OK { 46 | return entries, fuse.OK 47 | } 48 | } 49 | 50 | return nil, fuse.ENOENT 51 | } 52 | 53 | // Mkdir overwrites the parent's Mkdir method. 54 | func (gpf *GoPathFs) Mkdir(name string, mode uint32, context *fuse.Context) fuse.Status { 55 | prefix := gpf.cfg.GoPkgPrefix + pathSeparator 56 | if strings.HasPrefix(name, prefix) { 57 | return gpf.mkFirstPartyChildDir(name[len(prefix):], mode, context) 58 | } 59 | 60 | return gpf.mkThirdPartyChildDir(name, mode, context) 61 | } 62 | 63 | // Rmdir overwrites the parent's Rmdir method. 64 | func (gpf *GoPathFs) Rmdir(name string, context *fuse.Context) fuse.Status { 65 | prefix := gpf.cfg.GoPkgPrefix + pathSeparator 66 | if strings.HasPrefix(name, prefix) { 67 | return gpf.rmFirstPartyChildDir(name[len(prefix):], context) 68 | } 69 | 70 | return gpf.rmThirdPartyChildDir(name, context) 71 | } 72 | 73 | func (gpf *GoPathFs) openTopDir() ([]fuse.DirEntry, fuse.Status) { 74 | entries := []fuse.DirEntry{ 75 | { 76 | Name: gpf.cfg.GoPkgPrefix, 77 | Mode: fuse.S_IFDIR, 78 | }, 79 | } 80 | 81 | // Vendor directories. 82 | for _, vendor := range gpf.cfg.Vendors { 83 | entries, _ = gpf.openUnderlyingDir(filepath.Join(gpf.dirs.Workspace, vendor), gpf.cfg.FallThroughSet /* excludes */, entries) 84 | } 85 | 86 | // Fall-through directories. 87 | for _, dir := range gpf.cfg.FallThrough { 88 | dir = filepath.Join(gpf.dirs.Workspace, dir) 89 | fi, err := os.Stat(dir) 90 | if err != nil { 91 | fmt.Printf("Failed to access %s, %v", dir, err) 92 | continue 93 | } 94 | 95 | entry := fuse.DirEntry{ 96 | Name: fi.Name(), 97 | Mode: fuse.S_IFREG, 98 | } 99 | if fi.IsDir() { 100 | entry.Mode = fuse.S_IFDIR 101 | } 102 | entries = append(entries, entry) 103 | } 104 | 105 | return entries, fuse.OK 106 | } 107 | 108 | func (gpf *GoPathFs) openFirstPartyDir() ([]fuse.DirEntry, fuse.Status) { 109 | h, err := os.Open(gpf.dirs.Workspace) 110 | if err != nil { 111 | return nil, fuse.ENOENT 112 | } 113 | defer h.Close() 114 | 115 | fis, err := h.Readdir(-1) 116 | if err != nil { 117 | return nil, fuse.ENOENT 118 | } 119 | 120 | entries := []fuse.DirEntry{} 121 | for _, fi := range fis { 122 | if gpf.isIgnored(fi.Name()) { 123 | continue 124 | } 125 | 126 | if gpf.isVendorDir(fi.Name()) { 127 | continue 128 | } 129 | 130 | if fi.IsDir() { 131 | entry := fuse.DirEntry{ 132 | Name: fi.Name(), 133 | Mode: fuse.S_IFREG, 134 | } 135 | entry.Mode = fuse.S_IFDIR 136 | entries = append(entries, entry) 137 | } 138 | } 139 | 140 | return entries, fuse.OK 141 | } 142 | 143 | func (gpf *GoPathFs) openFirstPartyChildDir(name string) ([]fuse.DirEntry, fuse.Status) { 144 | name = name[len(gpf.cfg.GoPkgPrefix+pathSeparator):] 145 | entries := []fuse.DirEntry{} 146 | 147 | // Search in GOROOT (for debugger). 148 | if name == "GOROOT" || strings.HasPrefix(name, "GOROOT"+pathSeparator) { 149 | fname := filepath.Join(gpf.dirs.GoSDKDir, name[len("GOROOT"):]) 150 | entries, status := gpf.openUnderlyingDir(fname, nil /* excludes */, entries) 151 | if status == fuse.OK { 152 | return entries, fuse.OK 153 | } 154 | fmt.Printf("failed to open entry %s\n", fname) 155 | return nil, fuse.ENOENT 156 | } 157 | 158 | entries, _ = gpf.openUnderlyingDir(filepath.Join(gpf.dirs.Workspace, name), gpf.cfg.FallThroughSet /* excludes */, entries) 159 | // Also search in bazel-genfiles. 160 | entries, _ = gpf.openUnderlyingDir(filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", name), gpf.cfg.FallThroughSet /* excludes */, entries) 161 | 162 | return entries, fuse.OK 163 | } 164 | 165 | func (gpf *GoPathFs) openVendorChildDir(vendor, name string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { 166 | entries, _ = gpf.openUnderlyingDir(filepath.Join(gpf.dirs.Workspace, vendor, name), gpf.cfg.FallThroughSet /* excludes */, entries) 167 | // Also search in bazel-genfiles. 168 | entries, _ = gpf.openUnderlyingDir(filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", vendor, name), gpf.cfg.FallThroughSet /* excludes */, entries) 169 | 170 | return entries, fuse.OK 171 | } 172 | 173 | func (gpf *GoPathFs) openUnderlyingDir(dir string, excludes map[string]struct{}, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { 174 | h, err := os.Open(dir) 175 | if err != nil { 176 | return entries, fuse.ENOENT 177 | } 178 | defer h.Close() 179 | 180 | fis, err := h.Readdir(-1) 181 | if err != nil { 182 | return entries, fuse.ENOENT 183 | } 184 | 185 | outterLoop: 186 | for _, fi := range fis { 187 | if fi.IsDir() { 188 | for _, e := range entries { 189 | if fi.Name() == e.Name { 190 | // The generated folder has the same name as the original 191 | // one. 192 | continue outterLoop 193 | } 194 | if _, ok := excludes[fi.Name()]; ok { 195 | // The folder should be excluded, e.g., when it has the same 196 | // name as a fall-through folder. 197 | continue outterLoop 198 | } 199 | } 200 | } 201 | 202 | entry := fuse.DirEntry{ 203 | Name: fi.Name(), 204 | Mode: fuse.S_IFREG, 205 | } 206 | if fi.IsDir() { 207 | entry.Mode = fuse.S_IFDIR 208 | } 209 | entries = append(entries, entry) 210 | } 211 | 212 | return entries, fuse.OK 213 | } 214 | 215 | func (gpf *GoPathFs) mkFirstPartyChildDir(name string, mode uint32, context *fuse.Context) fuse.Status { 216 | name = filepath.Join(gpf.dirs.Workspace, name) 217 | if err := os.MkdirAll(name, os.FileMode(mode)); err != nil { 218 | return fuse.ENOENT 219 | } 220 | return fuse.OK 221 | } 222 | 223 | func (gpf *GoPathFs) mkThirdPartyChildDir(name string, mode uint32, context *fuse.Context) fuse.Status { 224 | if len(gpf.cfg.Vendors) == 0 { 225 | return fuse.ENOENT 226 | } 227 | 228 | name = filepath.Join(gpf.dirs.Workspace, gpf.cfg.Vendors[0], name) 229 | if err := os.MkdirAll(name, os.FileMode(mode)); err != nil { 230 | return fuse.ENOENT 231 | } 232 | return fuse.OK 233 | } 234 | 235 | func (gpf *GoPathFs) rmFirstPartyChildDir(name string, context *fuse.Context) fuse.Status { 236 | name = filepath.Join(gpf.dirs.Workspace, name) 237 | if err := os.RemoveAll(name); err != nil { 238 | return fuse.ENOENT 239 | } 240 | return fuse.OK 241 | } 242 | 243 | func (gpf *GoPathFs) rmThirdPartyChildDir(name string, context *fuse.Context) fuse.Status { 244 | if len(gpf.cfg.Vendors) == 0 { 245 | return fuse.ENOENT 246 | } 247 | 248 | name = filepath.Join(gpf.dirs.Workspace, gpf.cfg.Vendors[0], name) 249 | if err := os.RemoveAll(name); err != nil { 250 | return fuse.ENOENT 251 | } 252 | return fuse.OK 253 | } 254 | -------------------------------------------------------------------------------- /gopathfs/file.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/hanwen/go-fuse/fuse" 10 | "github.com/hanwen/go-fuse/fuse/nodefs" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // Open overwrites the parent's Open method. 15 | func (gpf *GoPathFs) Open(name string, flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) { 16 | if gpf.debug { 17 | fmt.Printf("\nReqeusted to open file %s.\n", name) 18 | } 19 | 20 | if strings.HasPrefix(name, gpf.cfg.GoPkgPrefix+pathSeparator) { 21 | return gpf.openFirstPartyChildFile(name, flags, context) 22 | } 23 | 24 | // Search in fall-through directories. 25 | for _, path := range gpf.cfg.FallThrough { 26 | if path == name || strings.HasPrefix(name, path) { 27 | f, status := gpf.openUnderlyingFile(filepath.Join(gpf.dirs.Workspace, name), flags, context) 28 | if status == fuse.OK { 29 | return f, status 30 | } 31 | return nil, fuse.ENOENT 32 | } 33 | } 34 | 35 | // Search in vendor directories. 36 | for _, vendor := range gpf.cfg.Vendors { 37 | f, status := gpf.openVendorChildFile(vendor, name, flags, context) 38 | if status == fuse.OK { 39 | return f, status 40 | } 41 | } 42 | 43 | return nil, fuse.ENOENT 44 | } 45 | 46 | // Create overwrites the parent's Create method. 47 | func (gpf *GoPathFs) Create(name string, flags uint32, mode uint32, 48 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 49 | 50 | if gpf.debug { 51 | fmt.Printf("\nReqeusted to create file %s.\n", name) 52 | } 53 | 54 | prefix := gpf.cfg.GoPkgPrefix + pathSeparator 55 | if strings.HasPrefix(name, prefix) { 56 | return gpf.createFirstPartyChildFile(name[len(prefix):], flags, mode, context) 57 | } 58 | 59 | return gpf.createThirdPartyChildFile(name, flags, mode, context) 60 | } 61 | 62 | // Unlink overwrites the parent's Unlink method. 63 | func (gpf *GoPathFs) Unlink(name string, context *fuse.Context) (code fuse.Status) { 64 | if gpf.debug { 65 | fmt.Printf("\nReqeusted to unlink file %s.\n", name) 66 | } 67 | 68 | prefix := gpf.cfg.GoPkgPrefix + pathSeparator 69 | if strings.HasPrefix(name, prefix) { 70 | name = filepath.Join(gpf.dirs.Workspace, name[len(prefix):]) 71 | return gpf.unlinkUnderlyingFile(name, context) 72 | } 73 | 74 | // Vendor directories. 75 | for _, vendor := range gpf.cfg.Vendors { 76 | name = filepath.Join(gpf.dirs.Workspace, vendor, name) 77 | if status := gpf.unlinkUnderlyingFile(name, context); status == fuse.OK { 78 | return status 79 | } 80 | } 81 | 82 | return fuse.ENOSYS 83 | } 84 | 85 | // Rename overwrites the parent's Rename method. 86 | func (gpf *GoPathFs) Rename(oldName string, newName string, context *fuse.Context) (code fuse.Status) { 87 | if gpf.debug { 88 | fmt.Printf("\nReqeusted to rename from %s to %s.\n", oldName, newName) 89 | } 90 | 91 | if strings.HasPrefix(oldName, gpf.cfg.GoPkgPrefix+pathSeparator) { 92 | oldName = filepath.Join(gpf.dirs.Workspace, oldName[len(gpf.cfg.GoPkgPrefix):]) 93 | newName = filepath.Join(gpf.dirs.Workspace, newName[len(gpf.cfg.GoPkgPrefix):]) 94 | } else { 95 | // Vendor directories. 96 | for _, vendor := range gpf.cfg.Vendors { 97 | oldName = filepath.Join(vendor, oldName) 98 | if _, err := os.Stat(oldName); err == nil { 99 | newName = filepath.Join(vendor, newName) 100 | break 101 | } 102 | } 103 | if newName == "" || oldName == "" { 104 | return fuse.ENOSYS 105 | } 106 | } 107 | 108 | if gpf.debug { 109 | fmt.Printf("Actual rename from %s to %s ... ", oldName, newName) 110 | } 111 | if err := os.Rename(oldName, newName); err != nil { 112 | if gpf.debug { 113 | fmt.Println("failed to rename file %s,", oldName, err) 114 | } 115 | return fuse.ENOSYS 116 | } 117 | if gpf.debug { 118 | fmt.Println("Succeeded to rename file %s.\n", oldName) 119 | } 120 | return fuse.OK 121 | } 122 | 123 | func (gpf *GoPathFs) openFirstPartyChildFile(name string, flags uint32, 124 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 125 | 126 | name = name[len(gpf.cfg.GoPkgPrefix+pathSeparator):] 127 | 128 | // Search in GOROOT (for debugger). 129 | if name == "GOROOT" || strings.HasPrefix(name, "GOROOT"+pathSeparator) { 130 | f, status := gpf.openUnderlyingFile(filepath.Join(gpf.dirs.GoSDKDir, name[len("GOROOT"):]), flags, context) 131 | if status == fuse.OK { 132 | return f, status 133 | } 134 | return nil, fuse.ENOENT 135 | } 136 | 137 | f, status := gpf.openUnderlyingFile(filepath.Join(gpf.dirs.Workspace, name), flags, context) 138 | if status == fuse.OK { 139 | return f, status 140 | } 141 | 142 | // Also search in bazel-genfiles. 143 | return gpf.openUnderlyingFile(filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", name), flags, context) 144 | } 145 | 146 | func (gpf *GoPathFs) openVendorChildFile(vendor, name string, flags uint32, 147 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 148 | 149 | f, status := gpf.openUnderlyingFile(filepath.Join(gpf.dirs.Workspace, vendor, name), flags, context) 150 | if status == fuse.OK { 151 | return f, status 152 | } 153 | 154 | // Also search in bazel-genfiles. 155 | return gpf.openUnderlyingFile(filepath.Join(gpf.dirs.Workspace, "bazel-genfiles", vendor, name), flags, context) 156 | } 157 | 158 | func (gpf *GoPathFs) openUnderlyingFile(name string, flags uint32, 159 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 160 | 161 | if gpf.debug { 162 | fmt.Printf("Actually opening file %s.\n", name) 163 | } 164 | 165 | if _, err := os.Stat(name); err != nil { 166 | if os.IsNotExist(err) { 167 | return nil, fuse.ENOENT 168 | } 169 | } 170 | 171 | if flags&fuse.O_ANYWRITE != 0 && unix.Access(name, unix.W_OK) != nil { 172 | fmt.Printf("File not writable: %s.\n", name) 173 | return nil, fuse.EPERM 174 | } 175 | 176 | f, err := os.OpenFile(name, int(flags), 0) 177 | if err != nil { 178 | fmt.Printf("Failed to open file: %s, %+v.\n", name, err) 179 | return nil, fuse.ENOENT 180 | } 181 | 182 | if gpf.debug { 183 | fmt.Printf("Succeeded to open file: %s.\n", name) 184 | } 185 | return nodefs.NewLoopbackFile(f), fuse.OK 186 | } 187 | 188 | func (gpf *GoPathFs) createFirstPartyChildFile(name string, flags uint32, mode uint32, 189 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 190 | 191 | name = filepath.Join(gpf.dirs.Workspace, name) 192 | 193 | if gpf.debug { 194 | fmt.Printf("Actually creating file %s.\n", name) 195 | } 196 | 197 | f, err := os.Create(name) 198 | if err != nil { 199 | if gpf.debug { 200 | fmt.Printf("Failed to create file %s.\n", name) 201 | } 202 | return nil, fuse.EIO 203 | } 204 | 205 | if err = os.Chmod(name, os.FileMode(mode)); err != nil { 206 | fmt.Printf("Fail to chmod. file: %s, mode: %s, err: %#v.", name, os.FileMode(mode).String(), err) 207 | } 208 | 209 | if gpf.debug { 210 | fmt.Printf("Succeeded to create file %s.\n", name) 211 | } 212 | return nodefs.NewLoopbackFile(f), fuse.OK 213 | } 214 | 215 | func (gpf *GoPathFs) createThirdPartyChildFile(name string, flags uint32, mode uint32, 216 | context *fuse.Context) (file nodefs.File, code fuse.Status) { 217 | if len(gpf.cfg.Vendors) == 0 { 218 | return nil, fuse.EIO 219 | } 220 | 221 | name = filepath.Join(gpf.dirs.Workspace, gpf.cfg.Vendors[0], name) 222 | if gpf.debug { 223 | fmt.Printf("Actually creating file %s.\n", name) 224 | } 225 | 226 | f, err := os.Create(name) 227 | if err != nil { 228 | if gpf.debug { 229 | fmt.Printf("Failed to create file %s.\n", name) 230 | } 231 | return nil, fuse.EIO 232 | } 233 | 234 | if err = os.Chmod(name, os.FileMode(mode)); err != nil { 235 | fmt.Printf("Fail to chmod. file: %s, mode: %s, err: %#v.", name, os.FileMode(mode).String(), err) 236 | } 237 | 238 | if gpf.debug { 239 | fmt.Printf("Succeeded to create file %s.\n", name) 240 | } 241 | return nodefs.NewLoopbackFile(f), fuse.OK 242 | } 243 | 244 | func (gpf *GoPathFs) unlinkUnderlyingFile(name string, context *fuse.Context) (code fuse.Status) { 245 | if gpf.debug { 246 | fmt.Printf("Actually unlinking file %s.\n", name) 247 | } 248 | 249 | if err := os.Remove(name); err != nil { 250 | if gpf.debug { 251 | fmt.Printf("Failed to unlink file %s.\n", name) 252 | } 253 | return fuse.EIO 254 | } 255 | 256 | if gpf.debug { 257 | fmt.Printf("Succeeded to unlink file %s.\n", name) 258 | } 259 | return fuse.OK 260 | } 261 | -------------------------------------------------------------------------------- /gopathfs/gopathfs.go: -------------------------------------------------------------------------------- 1 | package gopathfs 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/hanwen/go-fuse/fuse" 12 | "github.com/hanwen/go-fuse/fuse/pathfs" 13 | "github.com/linuxerwang/gobazel/conf" 14 | "github.com/linuxerwang/gobazel/exec" 15 | "github.com/rjeczalik/notify" 16 | ) 17 | 18 | var ( 19 | pathSeparator = string(os.PathSeparator) 20 | ) 21 | 22 | // Dirs contains directory paths for GoPathFs. 23 | type Dirs struct { 24 | Workspace string 25 | GobzlConf string 26 | GobzlPid string 27 | BinDir string 28 | PkgDir string 29 | SrcDir string 30 | GoSDKDir string 31 | } 32 | 33 | // GoPathFs implements a virtual tree for src folder of GOPATH. 34 | type GoPathFs struct { 35 | pathfs.FileSystem 36 | debug bool 37 | dirs *Dirs 38 | cfg *conf.GobazelConf 39 | ignoreRegexes []*regexp.Regexp 40 | notifyCh chan notify.EventInfo 41 | } 42 | 43 | // Access overwrites the parent's Access method. 44 | func (gpf *GoPathFs) Access(name string, mode uint32, context *fuse.Context) (code fuse.Status) { 45 | return fuse.OK 46 | } 47 | 48 | // OnMount overwrites the parent's OnMount method. 49 | func (gpf *GoPathFs) OnMount(nodeFs *pathfs.PathNodeFs) { 50 | if err := notify.Watch(filepath.Join(gpf.dirs.Workspace, "..."), gpf.notifyCh, notify.All); err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | go func() { 55 | for ei := range gpf.notifyCh { 56 | path := ei.Path()[len(gpf.dirs.Workspace+pathSeparator):] 57 | gpf.notifyFileChange(nodeFs, path) 58 | } 59 | }() 60 | } 61 | 62 | // OnUnmount overwrites the parent's OnUnmount method. 63 | func (gpf *GoPathFs) OnUnmount() { 64 | notify.Stop(gpf.notifyCh) 65 | } 66 | 67 | func (gpf *GoPathFs) notifyFileChange(nodeFs *pathfs.PathNodeFs, path string) { 68 | if gpf.isIgnored(path) { 69 | return 70 | } 71 | 72 | if strings.HasSuffix(path, pathSeparator+".git") || strings.Contains(path, pathSeparator+".git"+pathSeparator) { 73 | return 74 | } 75 | 76 | go nodeFs.Notify(filepath.Join(gpf.cfg.GoPkgPrefix, path)) 77 | 78 | isVendor := false 79 | for _, vendor := range gpf.cfg.Vendors { 80 | if strings.HasPrefix(path, vendor+pathSeparator) { 81 | isVendor = true 82 | nodeFs.FileNotify(path[len(vendor+pathSeparator):], 0, 0) 83 | break 84 | } 85 | } 86 | 87 | // If it's a proto file, run bazel build. 88 | if strings.HasSuffix(path, ".proto") { 89 | bzlPkg := filepath.Dir(path) + ":*" 90 | exec.RunBazelBuild(gpf.dirs.Workspace, bzlPkg) 91 | } 92 | 93 | // Run go install. 94 | if strings.HasSuffix(path, ".proto") || strings.HasSuffix(path, ".go") { 95 | goPkg := filepath.Dir(path) 96 | if !isVendor { 97 | goPkg = filepath.Join(gpf.cfg.GoPkgPrefix, goPkg) 98 | } 99 | exec.RunGoInstall(gpf.cfg, goPkg) 100 | } 101 | } 102 | 103 | func (gpf *GoPathFs) isIgnored(dir string) bool { 104 | if strings.HasPrefix(dir, ".") { 105 | return true 106 | } 107 | 108 | for _, re := range gpf.ignoreRegexes { 109 | if re.MatchString(dir) { 110 | return true 111 | } 112 | } 113 | return false 114 | } 115 | 116 | func (gpf *GoPathFs) isVendorDir(dir string) bool { 117 | for _, vendor := range gpf.cfg.Vendors { 118 | if dir == vendor { 119 | return true 120 | } 121 | if strings.HasPrefix(dir, vendor+pathSeparator) { 122 | return true 123 | } 124 | } 125 | return false 126 | } 127 | 128 | // NewGoPathFs returns a new GoPathFs. 129 | func NewGoPathFs(debug bool, cfg *conf.GobazelConf, dirs *Dirs) *GoPathFs { 130 | ignoreRegexes := make([]*regexp.Regexp, len(cfg.Ignores)) 131 | for i, ign := range cfg.Ignores { 132 | ignoreRegexes[i] = regexp.MustCompile(ign) 133 | } 134 | 135 | gpfs := GoPathFs{ 136 | FileSystem: pathfs.NewDefaultFileSystem(), 137 | debug: debug, 138 | dirs: dirs, 139 | cfg: cfg, 140 | ignoreRegexes: ignoreRegexes, 141 | notifyCh: make(chan notify.EventInfo, 10), 142 | } 143 | 144 | // Find the go-sdk in bazel external folder. The debugger can use the same 145 | // go-sdk source code for debugging. 146 | found := false 147 | if fi, err := os.Lstat("bazel-out"); err == nil { 148 | if fi.Mode()&os.ModeSymlink != 0 { 149 | if target, err := os.Readlink("bazel-out"); err == nil { 150 | target = filepath.ToSlash(target) 151 | suffix := filepath.Join("execroot", "__main__", "bazel-out") 152 | if strings.HasSuffix(target, suffix) { 153 | gpfs.dirs.GoSDKDir = filepath.Join(target[:len(target)-len(suffix)], "external", "go_sdk") 154 | found = true 155 | } 156 | } 157 | } 158 | } 159 | if !found { 160 | fmt.Println("Could not find symbolic link \"bazel-out\", debugger will not find Go SDK source codes.") 161 | } 162 | 163 | return &gpfs 164 | } 165 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | osexec "os/exec" 9 | "os/signal" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/hanwen/go-fuse/fuse/nodefs" 18 | "github.com/hanwen/go-fuse/fuse/pathfs" 19 | "github.com/linuxerwang/gobazel/conf" 20 | "github.com/linuxerwang/gobazel/exec" 21 | "github.com/linuxerwang/gobazel/gopathfs" 22 | ) 23 | 24 | const ( 25 | initialConf = `gobazel { 26 | go-path: "" 27 | go-pkg-prefix: "test.com" 28 | # go-ide-cmd: "/usr/bin/atom" 29 | go-ide-cmd: "/usr/bin/code" 30 | # go-ide-cmd: "/usr/bin/liteide" 31 | 32 | build { 33 | rules: [ 34 | ] 35 | ignore-dirs: [ 36 | "bazel-.*", 37 | "third-party.*", 38 | ] 39 | } 40 | 41 | vendor-dirs: [ 42 | "third-party-go/vendor", 43 | ] 44 | 45 | ignore-dirs: [ 46 | "bazel-.*", 47 | "third-party.*", 48 | ] 49 | 50 | fall-through-dirs: [ 51 | ".vscode", 52 | ] 53 | } 54 | ` 55 | bzlQuery = "kind(%s, deps(%s/...))" 56 | ) 57 | 58 | const ( 59 | gobzlPidFile = ".gobazelpid" 60 | gobzlRcFile = ".gobazelrc" 61 | bzlWsFile = "WORKSPACE" 62 | ) 63 | 64 | var ( 65 | debug = flag.Bool("debug", false, "Enable debug output.") 66 | build = flag.Bool("build", false, "Build all packages.") 67 | daemon = flag.Bool("daemon", true, "To detach from parent process.") 68 | detached = flag.Bool("detached", false, "The current process has been detached from parent process. Do not set it manually, it's only used by gobazel to detach itself.") 69 | 70 | dirs gopathfs.Dirs 71 | version string 72 | ) 73 | 74 | func init() { 75 | dirs = gopathfs.Dirs{} 76 | 77 | wd, err := os.Getwd() 78 | if err != nil { 79 | fmt.Println("Failed to get the current working directory,", err) 80 | os.Exit(2) 81 | } 82 | 83 | // The command has to be executed in a bazel workspace. 84 | dirs.Workspace = wd 85 | dirs.GobzlConf = filepath.Join(wd, gobzlRcFile) 86 | dirs.GobzlPid = filepath.Join(wd, gobzlPidFile) 87 | } 88 | 89 | func usage() { 90 | fmt.Println(`gobazel: A fuse mount tool for bazel to support Golang. 91 | 92 | Usage: 93 | gobazel [options] 94 | OR to show its version: 95 | gobazel version 96 | 97 | Note: 98 | This command has to be executed in a bazel workspace (where your WORKSPACE file reside). 99 | 100 | Options:`) 101 | 102 | flag.PrintDefaults() 103 | os.Exit(2) 104 | } 105 | 106 | func main() { 107 | flag.Usage = usage 108 | flag.Parse() 109 | 110 | for _, arg := range flag.Args() { 111 | if strings.ToLower(arg) == "version" { 112 | fmt.Println("Version:", version) 113 | return 114 | } 115 | } 116 | 117 | // The command has to be executed in a bazel workspace. 118 | if _, err := os.Stat(filepath.Join(dirs.Workspace, bzlWsFile)); err != nil { 119 | fmt.Println("Error, the command has to be run in a bazel workspace,", err) 120 | os.Exit(2) 121 | } 122 | 123 | for _, arg := range flag.Args() { 124 | if strings.ToLower(arg) == "stop" { 125 | fmt.Printf("Stopping existing gobazel process for workspace %s.\n", dirs.Workspace) 126 | stopExistingProcess() 127 | return 128 | } 129 | } 130 | 131 | cfg := loadConfig() 132 | 133 | if _, err := os.Stat(filepath.Join(dirs.Workspace, gobzlPidFile)); !os.IsNotExist(err) { 134 | fmt.Println("File .gobazelpid for another gobazel process exists. Start IDE") 135 | startIDE(cfg) 136 | return 137 | } 138 | 139 | if *daemon && !*detached { 140 | pid, err := detach() 141 | if err != nil { 142 | fmt.Println(err) 143 | os.Exit(2) 144 | } 145 | fmt.Printf("gobazel is running detached. To stop it, run \"kill -SIGQUIT %d\".\n", pid) 146 | return 147 | } 148 | 149 | 150 | // Create a FUSE virtual file system on dirs.SrcDir. 151 | nfs := pathfs.NewPathNodeFs(gopathfs.NewGoPathFs(*debug, cfg, &dirs), nil) 152 | server, _, err := nodefs.MountRoot(dirs.SrcDir, nfs.Root(), nil) 153 | if err != nil { 154 | fmt.Printf("Mount fail: %v\n", err) 155 | os.Exit(2) 156 | } 157 | fmt.Printf("Mounted bazel source folder to %s. You need to set %s as your GOPATH. \n\n Ctrl+C to exit.\n", dirs.SrcDir, cfg.GoPath) 158 | 159 | if err := ioutil.WriteFile(filepath.Join(dirs.Workspace, gobzlPidFile), []byte(fmt.Sprintf("%d", os.Getpid())), os.ModePerm); err != nil { 160 | fmt.Printf("Failed to write to file %s: %v\n", gobzlPidFile, err) 161 | os.Exit(2) 162 | } 163 | 164 | // Handle ctl+c. 165 | c := make(chan os.Signal, 1) 166 | signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGABRT, syscall.SIGQUIT, syscall.SIGINT) 167 | go func() { 168 | for { 169 | <-c 170 | fmt.Printf("\nUnmount %s.\n", dirs.SrcDir) 171 | if err := server.Unmount(); err != nil { 172 | fmt.Println("Error to unmount,", err) 173 | continue 174 | } 175 | os.Exit(0) 176 | } 177 | }() 178 | 179 | go func() { 180 | time.Sleep(time.Second) 181 | 182 | // If set to build all packages. 183 | if *build { 184 | fmt.Println("\nBuilding all package, it may take seconds to a few minutes, depending on how many pakcages you have ...") 185 | 186 | if cfg.Build == nil { 187 | fmt.Println("No build config found in .gobazelrc, ignored.") 188 | return 189 | } 190 | 191 | // Best effort run, errors are ignored. 192 | bazelBuild(cfg, &dirs) 193 | } 194 | 195 | // If a Go IDE is specified, start it with the proper GOPATH. 196 | if cfg.GoIdeCmd != "" { 197 | fmt.Println("\nStarting IDE ...") 198 | startIDE(cfg) 199 | } 200 | }() 201 | 202 | server.Serve() 203 | } 204 | 205 | func loadConfig() *conf.GobazelConf { 206 | // File gobazel.cfg holds configurations for gobazel. 207 | if _, err := os.Stat(dirs.GobzlConf); err != nil { 208 | if os.IsNotExist(err) { 209 | if err = ioutil.WriteFile(dirs.GobzlConf, []byte(initialConf), 0644); err != nil { 210 | fmt.Printf("Failed to create file %s, %+v.\n", dirs.GobzlConf, err) 211 | os.Exit(2) 212 | } 213 | 214 | fmt.Printf("Created gobazel config file %s, please customize it and run the command again.\n", dirs.GobzlConf) 215 | os.Exit(0) 216 | } else { 217 | fmt.Println(err) 218 | } 219 | } 220 | 221 | cfg := conf.LoadConfig(dirs.GobzlConf) 222 | if cfg.GoPath == "" { 223 | fmt.Println("Error, go-path has to be set in your .gobazelrc file.") 224 | os.Exit(2) 225 | } 226 | if cfg.GoPkgPrefix == "" { 227 | fmt.Println("Error, go-pkg-prefix has to be set in your .gobazelrc file.") 228 | os.Exit(2) 229 | } 230 | 231 | dirs.BinDir = filepath.Join(cfg.GoPath, "bin") 232 | os.Mkdir(dirs.BinDir, 0755) 233 | dirs.PkgDir = filepath.Join(cfg.GoPath, "pkg") 234 | os.Mkdir(dirs.PkgDir, 0755) 235 | dirs.SrcDir = filepath.Join(cfg.GoPath, "src") 236 | os.Mkdir(dirs.SrcDir, 0755) 237 | 238 | return cfg 239 | } 240 | 241 | func bazelBuild(cfg *conf.GobazelConf, dirs *gopathfs.Dirs) { 242 | ignoreRegexes := make([]*regexp.Regexp, len(cfg.Build.Ignores)) 243 | for i, ign := range cfg.Build.Ignores { 244 | ignoreRegexes[i] = regexp.MustCompile(ign) 245 | } 246 | 247 | f, err := os.Open(dirs.Workspace) 248 | if err != nil { 249 | fmt.Println("Failed to read workspace,", err) 250 | os.Exit(2) 251 | } 252 | defer f.Close() 253 | 254 | fis, err := f.Readdir(-1) 255 | if err != nil { 256 | fmt.Println("Failed to read workspace,", err) 257 | os.Exit(2) 258 | } 259 | 260 | targets := map[string]struct{}{} 261 | projects := []string{} 262 | 263 | outterLoop: 264 | for _, fi := range fis { 265 | fmt.Printf("Folder %s ... ", fi.Name()) 266 | if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") { 267 | fmt.Println("ignored.") 268 | continue 269 | } 270 | 271 | for _, re := range ignoreRegexes { 272 | if re.MatchString(fi.Name()) { 273 | fmt.Println("ignored.") 274 | continue outterLoop 275 | } 276 | } 277 | 278 | projects = append(projects, fi.Name()) 279 | 280 | // Check if there are given bazel build targets in this directory. 281 | cmd := [4]string{"bazel", "query", "--keep_going", ""} 282 | for _, rule := range cfg.Build.Rules { 283 | cmd[3] = fmt.Sprintf(bzlQuery, rule, fi.Name()) 284 | exec.RunBazelQuery(dirs.Workspace, fi.Name(), cmd[:], targets) 285 | } 286 | 287 | fmt.Println("done.") 288 | } 289 | 290 | // Execute bazel build. 291 | for target := range targets { 292 | exec.RunBazelBuild(dirs.Workspace, target) 293 | } 294 | 295 | // Run go install for all first party projects. 296 | for _, proj := range projects { 297 | exec.RunGoWalkInstall(cfg, dirs.Workspace, proj) 298 | } 299 | } 300 | 301 | func detach() (int, error) { 302 | cwd, err := os.Getwd() 303 | if err != nil { 304 | return 0, err 305 | } 306 | args := append(os.Args, "--detached") 307 | cmd := osexec.Command(args[0], args[1:]...) 308 | cmd.Dir = cwd 309 | err = cmd.Start() 310 | if err != nil { 311 | return 0, err 312 | } 313 | pid := cmd.Process.Pid 314 | cmd.Process.Release() 315 | return pid, nil 316 | } 317 | 318 | func stopExistingProcess() { 319 | pidFile := filepath.Join(dirs.Workspace, gobzlPidFile) 320 | if _, err := os.Stat(pidFile); err != nil { 321 | fmt.Printf("There is no file .gobazelpid in workspace %s.\n", dirs.Workspace) 322 | return 323 | } 324 | 325 | b, err := ioutil.ReadFile(pidFile) 326 | if err != nil { 327 | fmt.Printf("Failed to read from .gobazelpid, %v.\n", err) 328 | os.Exit(2) 329 | } 330 | 331 | pid, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 32) 332 | if err != nil { 333 | fmt.Printf("Invalid pid in .gobazelpid, %v.\n", string(b)) 334 | os.Exit(2) 335 | } 336 | 337 | p, err := os.FindProcess(int(pid)) 338 | if err != nil { 339 | fmt.Printf("Failed to find process %d.\n", pid) 340 | os.Remove(pidFile) 341 | os.Exit(2) 342 | } 343 | 344 | if err := p.Signal(syscall.SIGQUIT); err != nil { 345 | fmt.Printf("Failed to send SIGQUIT to process %d.\n", pid) 346 | os.Exit(2) 347 | } 348 | 349 | // Check if the mount point is still mounted (only works on linux). 350 | time.Sleep(time.Second) 351 | if b, err := ioutil.ReadFile("/proc/mounts"); err == nil { 352 | if idx := strings.Index(string(b), dirs.SrcDir); idx > -1 { 353 | osexec.Command("fusermount", "-u", dirs.SrcDir).CombinedOutput() 354 | } 355 | } 356 | os.Remove(pidFile) 357 | } 358 | 359 | func startIDE(cfg *conf.GobazelConf) { 360 | if err := exec.RunCommand(cfg, cfg.GoIdeCmd+" "+dirs.SrcDir); err != nil { 361 | fmt.Println("Error to run IDE, ", err) 362 | } 363 | } 364 | --------------------------------------------------------------------------------