Skip to content
Draft
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
9 changes: 8 additions & 1 deletion cmd/internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

// Package run implements the gop run command.
// Package run implements the "gop run" command.
package run

import (
Expand Down Expand Up @@ -79,6 +79,13 @@ func runCmd(cmd *base.Command, args []string) {
panic("TODO: profile not impl")
}

if handled, err := runWithConfiguredRunner(proj, args, "."); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
} else if handled {
return
}

noChdir := *flagNoChdir
conf, err := tool.NewDefaultConf(".", tool.ConfFlagNoTestFiles, pass.Tags())
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions cmd/internal/run/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package run

import "github.com/goplus/xgo/x/xgoprojs"

func runWithConfiguredRunner(proj xgoprojs.Proj, args []string, workDir string) (bool, error) {
projectDirectory, err := resolveProjectDir(proj, workDir)
if err != nil {
return false, err
}

runner, err := loadProjectRunner(proj, projectDirectory)
if err != nil {
return false, err
}
if runner == nil {
return false, nil
}

runnerBinaryPath, cleanup, err := installRunnerBinary(runner)
if err != nil {
return true, err
}
defer cleanup()

return true, executeRunnerBinary(runnerBinaryPath, projectDirectory, args)
}
51 changes: 51 additions & 0 deletions cmd/internal/run/runner_binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package run

import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"

"github.com/goplus/mod/modfile"
)

func installRunnerBinary(runner *modfile.Runner) (string, func(), error) {

Check failure on line 13 in cmd/internal/run/runner_binary.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.24.x)

undefined: modfile.Runner

Check failure on line 13 in cmd/internal/run/runner_binary.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.25.x)

undefined: modfile.Runner
temporaryDirectory, err := os.MkdirTemp("", "xgo-runner-install-*")
if err != nil {
return "", nil, err
}

cleanup := func() {
_ = os.RemoveAll(temporaryDirectory)
}

binaryPath, err := installRunnerBinaryToDirectory(temporaryDirectory, runner.Path, runner.Version)
if err != nil {
cleanup()
return "", nil, err
}
return binaryPath, cleanup, nil
}

func installRunnerBinaryToDirectory(targetDirectory, packagePath, version string) (string, error) {
packageReference := packageRef(packagePath, version)
output, err := runGoCommand("", []string{"GOBIN=" + targetDirectory}, "install", packageReference)
if err != nil {
return "", fmt.Errorf("install runner %s: %w\n%s", packageReference, err, output)
}

binaryPath := filepath.Join(targetDirectory, runnerBinaryFilename(packagePath))
if _, err := os.Stat(binaryPath); err != nil {
return "", fmt.Errorf("installed runner binary %s: %w", binaryPath, err)
}
return binaryPath, nil
}

func runnerBinaryFilename(packagePath string) string {
filename := path.Base(packagePath)
if runtime.GOOS == "windows" {
filename += ".exe"
}
return filename
}
34 changes: 34 additions & 0 deletions cmd/internal/run/runner_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package run

import (
"os"
"os/exec"
"strings"
)

func executeRunnerBinary(binaryPath, projectDirectory string, args []string) error {
cmd := newCommandInDir(binaryPath, projectDirectory, append([]string{projectDirectory}, args...)...)
// Configured runners are project-controlled executables, so they intentionally
// inherit the caller environment just like other tools launched by xgo.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}

func runGoCommand(directory string, extraEnv []string, args ...string) (string, error) {
cmd := newCommandInDir("go", directory, args...)
// Runner installation/build should not be redirected by an ambient go.work file.
cmd.Env = append(os.Environ(), "GOWORK=off")
cmd.Env = append(cmd.Env, extraEnv...)
output, err := cmd.CombinedOutput()
return strings.TrimSpace(string(output)), err
}

func newCommandInDir(command, directory string, args ...string) *exec.Cmd {
cmd := exec.Command(command, args...)
if directory != "" {
cmd.Dir = directory
}
return cmd
}
69 changes: 69 additions & 0 deletions cmd/internal/run/runner_package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package run

import (
"errors"
"path/filepath"

"github.com/goplus/mod/modcache"
"github.com/goplus/mod/modfetch"
"github.com/goplus/mod/xgomod"
)

func downloadPackageDir(pkgPath, version string) (string, error) {
spec := packageRef(pkgPath, version)
modVer, relPath, err := modfetch.GetPkg(spec, "")
if err != nil {
return "", err
}
modDir, err := modcache.Path(modVer)
if err != nil {
return "", err
}
directory := modDir
if relPath != "" {
directory = filepath.Join(modDir, relPath)
}
return filepath.Abs(directory)
}

func lookupPackageDir(workDir, pkgPath string) (string, error) {
mod, err := xgomod.Load(workDir)
if err = ignoreMissing(err); err != nil {
return "", err
}
if mod == nil {
return "", nil
}

pkg, err := mod.Lookup(pkgPath)
if err = ignoreMissing(err); err != nil {
return "", err
}
if pkg == nil {
return "", nil
}

directory, err := filepath.Abs(pkg.Dir)
if err != nil {
return "", err
}
return directory, nil
}

func packageRef(pkgPath, version string) string {
if version == "" {
version = "latest"
}
return pkgPath + "@" + version
}

func ignoreMissing(err error) error {
if err == nil || xgomod.IsNotFound(err) {
return nil
}
var missing *xgomod.MissingError
if errors.As(err, &missing) {
return nil
}
return err
}
163 changes: 163 additions & 0 deletions cmd/internal/run/runner_project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package run

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/goplus/mod/modfile"
"github.com/goplus/xgo/x/xgoprojs"
"golang.org/x/mod/module"
)

func resolveProjectDir(proj xgoprojs.Proj, workDir string) (string, error) {
switch v := proj.(type) {
case *xgoprojs.DirProj:
return resolvePath(workDir, v.Dir)
case *xgoprojs.FilesProj:
return resolveFilesProjectDir(workDir, v.Files)
case *xgoprojs.PkgPathProj:
return resolvePackageProjectDir(workDir, v.Path)
default:
return "", fmt.Errorf("unsupported project type %T", proj)
}
}

func resolveFilesProjectDir(workDir string, files []string) (string, error) {
if len(files) == 0 {
return "", fmt.Errorf("no files in project")
}
return resolvePath(workDir, filepath.Dir(files[0]))
}

func resolvePath(workDir, target string) (string, error) {
if filepath.IsAbs(target) {
return filepath.Clean(target), nil
}
if workDir == "" {
workDir = "."
}
return filepath.Abs(filepath.Join(workDir, target))
}

func resolvePackageProjectDir(workDir, pkgPath string) (string, error) {
pkgPath, version, _ := strings.Cut(pkgPath, "@")
if workDir == "" {
workDir = "."
}
if packageDirectory, err := lookupPackageDir(workDir, pkgPath); err != nil {
return "", err
} else if packageDirectory != "" {
return packageDirectory, nil
}
return downloadPackageDir(pkgPath, version)
}

func loadProjectRunner(proj xgoprojs.Proj, projectDir string) (*modfile.Runner, error) {

Check failure on line 59 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.24.x)

undefined: modfile.Runner

Check failure on line 59 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.25.x)

undefined: modfile.Runner
gopModPath, data, err := readProjectGopMod(projectDir)
if err != nil {
return nil, err
}
if data == nil {
return nil, nil
}

parsed, err := modfile.ParseLax(gopModPath, data, nil)
if err != nil {
return nil, err
}
project, err := selectTargetProject(parsed.Projects, proj, projectDir)
if err != nil {
return nil, err
}
if project == nil || project.Runner == nil {

Check failure on line 76 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.24.x)

project.Runner undefined (type *"github.com/goplus/mod/modfile".Project has no field or method Runner)

Check failure on line 76 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.25.x)

project.Runner undefined (type *"github.com/goplus/mod/modfile".Project has no field or method Runner)
return nil, nil
}
runner := project.Runner

Check failure on line 79 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.24.x)

project.Runner undefined (type *"github.com/goplus/mod/modfile".Project has no field or method Runner)

Check failure on line 79 in cmd/internal/run/runner_project.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 1.25.x)

project.Runner undefined (type *"github.com/goplus/mod/modfile".Project has no field or method Runner)
if err := module.CheckImportPath(runner.Path); err != nil {
return nil, fmt.Errorf("invalid runner path %q: %w", runner.Path, err)
}
return runner, nil
}

func selectTargetProject(projects []*modfile.Project, proj xgoprojs.Proj, projectDir string) (*modfile.Project, error) {
switch len(projects) {
case 0:
return nil, nil
case 1:
return projects[0], nil
}

targetFilenames, err := collectTargetFilenames(proj, projectDir)
if err != nil {
return nil, err
}

var matched *modfile.Project
for _, filename := range targetFilenames {
for _, project := range projects {
if !projectMatchesFilename(project, filename) {
continue
}
if matched != nil && matched != project {
return nil, fmt.Errorf("multiple projects in %s match run target", filepath.Join(projectDir, "gop.mod"))
}
matched = project
}
}
return matched, nil
}

func projectMatchesFilename(project *modfile.Project, filename string) bool {
ext := modfile.ClassExt(filename)
if ext == project.Ext {
return project.IsProj(ext, filename)
}
for _, work := range project.Works {
if work.Ext == ext {
return project.IsProj(ext, filename)
}
}
return false
}

func collectTargetFilenames(proj xgoprojs.Proj, projectDir string) ([]string, error) {
switch v := proj.(type) {
case *xgoprojs.FilesProj:
filenames := make([]string, 0, len(v.Files))
for _, file := range v.Files {
filenames = append(filenames, filepath.Base(file))
}
return filenames, nil
case *xgoprojs.DirProj, *xgoprojs.PkgPathProj:
entries, err := os.ReadDir(projectDir)
if err != nil {
return nil, err
}
files := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
files = append(files, entry.Name())
}
return files, nil
default:
return nil, fmt.Errorf("unsupported project type %T", proj)
}
}

func readProjectGopMod(projectDir string) (string, []byte, error) {
gopModPath := filepath.Join(projectDir, "gop.mod")
data, err := os.ReadFile(gopModPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return gopModPath, nil, nil
}
return "", nil, err
}
return gopModPath, data, nil
}
Loading
Loading