diff --git a/.circleci/config.yml b/.circleci/config.yml index 150a475e..141687aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,7 @@ templates: (cd test/requestconflict/mod6 && go build ./...) (cd test/nested && go build ./...) (cd test/nested/nestedmod && go build ./...) + (cd test/invoke/mod && go build ./...) - run: name: gobuilds command: ./gobuilds.sh @@ -258,6 +259,7 @@ jobs: (cd test/requestconflict/mod6 && go mod tidy && go mod verify) (cd test/nested && go mod tidy && go mod verify) (cd test/nested/nestedmod && go mod tidy && go mod verify) + (cd test/invoke/mod && go mod tidy && go mod verify) git status if [[ -n "$(git status --porcelain .)" ]]; then echo 'go.mod/go.sum is out-of-date: run `go mod tidy` in the right module directories (see git status) and then check in the changes' @@ -279,6 +281,7 @@ jobs: (cd test/requestconflict/mod6 && go vet ./...) (cd test/nested && go vet ./...) (cd test/nested/nestedmod && go vet ./...) + (cd test/invoke/mod && go vet ./...) - run: name: gofmt command: | diff --git a/gobuilds.sh b/gobuilds.sh index b9a59766..2552b04a 100755 --- a/gobuilds.sh +++ b/gobuilds.sh @@ -8,3 +8,4 @@ set -eux (cd ./test/implicitimport && ./test.sh) (cd ./test/nameconflict && ./test.sh) (cd ./test/12-fancy-cmd && ./test.sh) +(cd ./test/invoke && ./test.sh) diff --git a/src/pkg/bb/bbmain/cmd/main.go b/src/pkg/bb/bbmain/cmd/main.go index 8ff01845..1f02ac35 100644 --- a/src/pkg/bb/bbmain/cmd/main.go +++ b/src/pkg/bb/bbmain/cmd/main.go @@ -79,7 +79,68 @@ func main() { run() } +// A gobusybox has 3 possible ways of invocation: +// +// ## Direct +// +// ./bb ls -l +// +// For the gobusybox, argv in this case is ["./bb", "ls", "-l"] on all OS. +// +// +// ## Symlink +// +// ln -s bb ls +// ./ls +// +// For the gobusybox, argv in this case is ["./ls"] on all OS. +// +// +// ## Interpreted +// +// This way exists because Plan 9 does not have symlinks. Some Linux file +// system such as VFAT also do not support symlinks. +// +// echo "#!/bin/bb #!gobb!#" >> /tmp/ls +// /tmp/ls +// +// For the gobusybox, argv depends on the OS: +// +// Plan 9: ["ls", "#!gobb!#", "/tmp/ls"] +// Linux/Unix: ["/bin/bb", "#!gobb!#", "/tmp/ls"] +// +// Unix and Plan 9 evaluate arguments in a #! file differently, and, further, +// invoke the arguments in a different way. +// +// (1) The absolute path for /bin/bb is required, else Linux will throw an +// error as bb is not in the list of allowed interpreters. +// +// (2) On Plan 9, the arguments following the interpreter are tokenized (split +// on space) and on Linux, they are not. That means we should restrict +// ourselves to only ever using one argument in the she-bang line (#!). +// +// (3) Which gobusybox tool to use is always in argv[2]. +// +// (4) Because of the differences in how arguments are presented to #! on +// different kernels, there should be a reasonably unique magic value so +// that bb can have confidence that it is running as an interpreter, rather +// than on the command-line in direct mode. +// +// The code needs to change the arguments to look like an exec: ["/tmp/ls", ...] +// +// In each case, the second arg must be "#!gobb!#", which is extremely +// unlikely to appear in any other context (save testing files, of course). +// +// The result is that the kernel, given a path to a #!gobb#! file, will +// read that file, then exec bin with the argument from argv[2] and any +// additional arguments from the exec. func init() { + // Interpreted mode: If this has been run from a #!gobb!# file, it + // will have at least 3 args, and os.Args needs to be reconstructed. + if len(os.Args) > 2 && os.Args[1] == "#!gobb!#" { + os.Args = os.Args[2:] + } + m := func() { if len(os.Args) == 1 { log.Fatalf("Invalid busybox command: %q", os.Args) diff --git a/src/pkg/bb/bbmain_src.go b/src/pkg/bb/bbmain_src.go index b523478c..ffb4edaa 100644 --- a/src/pkg/bb/bbmain_src.go +++ b/src/pkg/bb/bbmain_src.go @@ -1,3 +1,3 @@ package bb -var bbMainSource = []byte("// Copyright 2018 the u-root Authors. All rights reserved\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package main is the busybox main.go template.\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/u-root/gobusybox/src/pkg/bb/bbmain\"\n\t// There MUST NOT be any other dependencies here.\n\t//\n\t// It is preferred to copy minimal code necessary into this file, as\n\t// dependency management for this main file is... hard.\n)\n\n// AbsSymlink returns an absolute path for the link from a file to a target.\nfunc AbsSymlink(originalFile, target string) string {\n\tif !filepath.IsAbs(originalFile) {\n\t\tvar err error\n\t\toriginalFile, err = filepath.Abs(originalFile)\n\t\tif err != nil {\n\t\t\t// This should not happen on Unix systems, or you're\n\t\t\t// already royally screwed.\n\t\t\tlog.Fatalf(\"could not determine absolute path for %v: %v\", originalFile, err)\n\t\t}\n\t}\n\t// Relative symlinks are resolved relative to the original file's\n\t// parent directory.\n\t//\n\t// E.g. /bin/defaultsh -> ../bbin/elvish\n\tif !filepath.IsAbs(target) {\n\t\treturn filepath.Join(filepath.Dir(originalFile), target)\n\t}\n\treturn target\n}\n\n// IsTargetSymlink returns true if a target of a symlink is also a symlink.\nfunc IsTargetSymlink(originalFile, target string) bool {\n\ts, err := os.Lstat(AbsSymlink(originalFile, target))\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn (s.Mode() & os.ModeSymlink) == os.ModeSymlink\n}\n\n// ResolveUntilLastSymlink resolves until the last symlink.\n//\n// This is needed when we have a chain of symlinks and want the last\n// symlink, not the file pointed to (which is why we don't use\n// filepath.EvalSymlinks)\n//\n// I.e.\n//\n// /foo/bar -> ../baz/foo\n// /baz/foo -> bla\n//\n// ResolveUntilLastSymlink(/foo/bar) returns /baz/foo.\nfunc ResolveUntilLastSymlink(p string) string {\n\tfor target, err := os.Readlink(p); err == nil && IsTargetSymlink(p, target); target, err = os.Readlink(p) {\n\t\tp = AbsSymlink(p, target)\n\t}\n\treturn p\n}\n\nfunc run() {\n\tname := filepath.Base(os.Args[0])\n\tif err := bbmain.Run(name); err != nil {\n\t\tlog.Fatalf(\"%s: %v\", name, err)\n\t}\n}\n\nfunc main() {\n\tos.Args[0] = ResolveUntilLastSymlink(os.Args[0])\n\n\trun()\n}\n\nfunc init() {\n\tm := func() {\n\t\tif len(os.Args) == 1 {\n\t\t\tlog.Fatalf(\"Invalid busybox command: %q\", os.Args)\n\t\t}\n\t\t// Use argv[1] as the name.\n\t\tos.Args = os.Args[1:]\n\t\trun()\n\t}\n\tbbmain.Register(\"bbdiagnose\", bbmain.Noop, bbmain.ListCmds)\n\tbbmain.RegisterDefault(bbmain.Noop, m)\n}\n") +var bbMainSource = []byte("// Copyright 2018 the u-root Authors. All rights reserved\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package main is the busybox main.go template.\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/u-root/gobusybox/src/pkg/bb/bbmain\"\n\t// There MUST NOT be any other dependencies here.\n\t//\n\t// It is preferred to copy minimal code necessary into this file, as\n\t// dependency management for this main file is... hard.\n)\n\n// AbsSymlink returns an absolute path for the link from a file to a target.\nfunc AbsSymlink(originalFile, target string) string {\n\tif !filepath.IsAbs(originalFile) {\n\t\tvar err error\n\t\toriginalFile, err = filepath.Abs(originalFile)\n\t\tif err != nil {\n\t\t\t// This should not happen on Unix systems, or you're\n\t\t\t// already royally screwed.\n\t\t\tlog.Fatalf(\"could not determine absolute path for %v: %v\", originalFile, err)\n\t\t}\n\t}\n\t// Relative symlinks are resolved relative to the original file's\n\t// parent directory.\n\t//\n\t// E.g. /bin/defaultsh -> ../bbin/elvish\n\tif !filepath.IsAbs(target) {\n\t\treturn filepath.Join(filepath.Dir(originalFile), target)\n\t}\n\treturn target\n}\n\n// IsTargetSymlink returns true if a target of a symlink is also a symlink.\nfunc IsTargetSymlink(originalFile, target string) bool {\n\ts, err := os.Lstat(AbsSymlink(originalFile, target))\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn (s.Mode() & os.ModeSymlink) == os.ModeSymlink\n}\n\n// ResolveUntilLastSymlink resolves until the last symlink.\n//\n// This is needed when we have a chain of symlinks and want the last\n// symlink, not the file pointed to (which is why we don't use\n// filepath.EvalSymlinks)\n//\n// I.e.\n//\n// /foo/bar -> ../baz/foo\n// /baz/foo -> bla\n//\n// ResolveUntilLastSymlink(/foo/bar) returns /baz/foo.\nfunc ResolveUntilLastSymlink(p string) string {\n\tfor target, err := os.Readlink(p); err == nil && IsTargetSymlink(p, target); target, err = os.Readlink(p) {\n\t\tp = AbsSymlink(p, target)\n\t}\n\treturn p\n}\n\nfunc run() {\n\tname := filepath.Base(os.Args[0])\n\tif err := bbmain.Run(name); err != nil {\n\t\tlog.Fatalf(\"%s: %v\", name, err)\n\t}\n}\n\nfunc main() {\n\tos.Args[0] = ResolveUntilLastSymlink(os.Args[0])\n\n\trun()\n}\n\n// A gobusybox has 3 possible ways of invocation:\n//\n// ## Direct\n//\n// ./bb ls -l\n//\n// For the gobusybox, argv in this case is [\"./bb\", \"ls\", \"-l\"] on all OS.\n//\n//\n// ## Symlink\n//\n// ln -s bb ls\n// ./ls\n//\n// For the gobusybox, argv in this case is [\"./ls\"] on all OS.\n//\n//\n// ## Interpreted\n//\n// This way exists because Plan 9 does not have symlinks. Some Linux file\n// system such as VFAT also do not support symlinks.\n//\n// echo \"#!/bin/bb #!gobb!#\" >> /tmp/ls\n// /tmp/ls\n//\n// For the gobusybox, argv depends on the OS:\n//\n// Plan 9: [\"ls\", \"#!gobb!#\", \"/tmp/ls\"]\n// Linux/Unix: [\"/bin/bb\", \"#!gobb!#\", \"/tmp/ls\"]\n//\n// Unix and Plan 9 evaluate arguments in a #! file differently, and, further,\n// invoke the arguments in a different way.\n//\n// (1) The absolute path for /bin/bb is required, else Linux will throw an\n// error as bb is not in the list of allowed interpreters.\n//\n// (2) On Plan 9, the arguments following the interpreter are tokenized (split\n// on space) and on Linux, they are not. That means we should restrict\n// ourselves to only ever using one argument in the she-bang line (#!).\n//\n// (3) Which gobusybox tool to use is always in argv[2].\n//\n// (4) Because of the differences in how arguments are presented to #! on\n// different kernels, there should be a reasonably unique magic value so\n// that bb can have confidence that it is running as an interpreter, rather\n// than on the command-line in direct mode.\n//\n// The code needs to change the arguments to look like an exec: [\"/tmp/ls\", ...]\n//\n// In each case, the second arg must be \"#!gobb!#\", which is extremely\n// unlikely to appear in any other context (save testing files, of course).\n//\n// The result is that the kernel, given a path to a #!gobb#! file, will\n// read that file, then exec bin with the argument from argv[2] and any\n// additional arguments from the exec.\nfunc init() {\n\t// Interpreted mode: If this has been run from a #!gobb!# file, it\n\t// will have at least 3 args, and os.Args needs to be reconstructed.\n\tif len(os.Args) > 2 && os.Args[1] == \"#!gobb!#\" {\n\t\tos.Args = os.Args[2:]\n\t}\n\n\tm := func() {\n\t\tif len(os.Args) == 1 {\n\t\t\tlog.Fatalf(\"Invalid busybox command: %q\", os.Args)\n\t\t}\n\t\t// Use argv[1] as the name.\n\t\tos.Args = os.Args[1:]\n\t\trun()\n\t}\n\tbbmain.Register(\"bbdiagnose\", bbmain.Noop, bbmain.ListCmds)\n\tbbmain.RegisterDefault(bbmain.Noop, m)\n}\n") diff --git a/test/invoke/.gitignore b/test/invoke/.gitignore new file mode 100644 index 00000000..455ec813 --- /dev/null +++ b/test/invoke/.gitignore @@ -0,0 +1,5 @@ +/bb +/bb-on +/bb-auto +/bb2 +/makebb diff --git a/test/invoke/mod/cmd/helloworld/.gitignore b/test/invoke/mod/cmd/helloworld/.gitignore new file mode 100644 index 00000000..31e0fce5 --- /dev/null +++ b/test/invoke/mod/cmd/helloworld/.gitignore @@ -0,0 +1 @@ +helloworld diff --git a/test/invoke/mod/cmd/helloworld/hello.go b/test/invoke/mod/cmd/helloworld/hello.go new file mode 100644 index 00000000..52a7925c --- /dev/null +++ b/test/invoke/mod/cmd/helloworld/hello.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("hello world") +} diff --git a/test/invoke/mod/go.mod b/test/invoke/mod/go.mod new file mode 100644 index 00000000..cdfad733 --- /dev/null +++ b/test/invoke/mod/go.mod @@ -0,0 +1,3 @@ +module github.com/u-root/gobusybox/test/invoke/mod + +go 1.13 diff --git a/test/invoke/test.sh b/test/invoke/test.sh new file mode 100755 index 00000000..d982fefb --- /dev/null +++ b/test/invoke/test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -eux + +(cd ../../src/cmd/makebb && GO111MODULE=on go build .) +MAKEBB=../../src/cmd/makebb/makebb + +for GO111MODULE in on auto; +do + GO111MODULE=$GO111MODULE $MAKEBB -o bb-$GO111MODULE ./mod/cmd/helloworld + test -f ./bb-$GO111MODULE + + HW=$(./bb-$GO111MODULE helloworld); + test "$HW" == "hello world" || (echo "hello world not right: direct invocation failed" && exit 1) + + ln -s bb-$GO111MODULE helloworld + HW=$(./helloworld) + test "$HW" == "hello world" || (echo "hello world not right: symlink invocation failed" && exit 1) + unlink helloworld + + # add an interpreter file that contains #!/bin/bb #!gobb#! + echo "#!$(pwd)/bb-$GO111MODULE #!gobb!#" > helloworld + chmod +x helloworld + HW=$(./helloworld) + test "$HW" == "hello world" || (echo "hello world not right: interpreter invocation failed" && exit 1) + unlink helloworld +done + +# check reproducible +cmp bb-on bb-auto +rm bb-on bb-auto