191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
package gok
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gokrazy/internal/config"
|
|
"github.com/gokrazy/internal/httpclient"
|
|
"github.com/gokrazy/internal/humanize"
|
|
"github.com/gokrazy/internal/instanceflag"
|
|
"github.com/gokrazy/internal/progress"
|
|
"github.com/gokrazy/internal/updateflag"
|
|
"github.com/gokrazy/tools/packer"
|
|
"github.com/gokrazy/updater"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// runCmd is gok run.
|
|
func runCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
GroupID: "runtime",
|
|
Use: "run",
|
|
Short: "`go install` and run on a running gokrazy instance",
|
|
Long: "gok run uses `go install` to build the Go program in the current directory," + `
|
|
then it stores the program in RAM of a running gokrazy instance and runs the program.
|
|
|
|
This enables a quick feedback loop when working on a program that is running on gokrazy,
|
|
without having to do a full gok update every time you only want to update a single program.
|
|
|
|
Command line arguments to the program will be combined from the instance configuration file
|
|
and the ` + "`gok run`" + ` command line, in that order.
|
|
|
|
Examples:
|
|
% cd ~/go/src/github.com/stapelberg/scan2drive/cmd/scan2drive
|
|
% gok -i scan2drive run
|
|
|
|
# specify extra flags on the command line
|
|
% gok -i scan2drive run -- -tls_autocert_hosts=scan.example.com
|
|
`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
|
},
|
|
}
|
|
cmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
|
|
instanceflag.RegisterPflags(cmd.Flags())
|
|
return cmd
|
|
}
|
|
|
|
type runImplConfig struct {
|
|
keep bool
|
|
}
|
|
|
|
var runImpl runImplConfig
|
|
|
|
func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
cfg, err := config.ApplyInstanceFlag()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// best-effort compatibility for old setups
|
|
cfg = config.NewStruct(instanceflag.Instance())
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var tmp string
|
|
if r.keep {
|
|
tmp = os.TempDir()
|
|
} else {
|
|
var err error
|
|
tmp, err = os.MkdirTemp("", "gokrazy-bins-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
}
|
|
|
|
// Get the import path of the Go package in the current directory,
|
|
// e.g. github.com/stapelberg/scan2drive/cmd/scan2drive
|
|
list := exec.CommandContext(ctx, "go", "list")
|
|
list.Stderr = os.Stderr
|
|
listb, err := list.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %v", list.Args, err)
|
|
}
|
|
importPath := strings.TrimSpace(string(listb))
|
|
|
|
// basename of the current directory
|
|
basename := filepath.Base(importPath)
|
|
if cfg.PackageConfig[importPath].Basename != "" {
|
|
basename = cfg.PackageConfig[importPath].Basename
|
|
}
|
|
|
|
pkgs := []string{importPath}
|
|
var noBuildPkgs []string
|
|
packageBuildEnv := map[string][]string{
|
|
importPath: cfg.PackageConfig[importPath].GoBuildEnvironment,
|
|
}
|
|
packageBuildFlags := map[string][]string{
|
|
importPath: cfg.PackageConfig[importPath].GoBuildFlags,
|
|
}
|
|
packageBuildTags := map[string][]string{
|
|
importPath: cfg.PackageConfig[importPath].GoBuildTags,
|
|
}
|
|
basenames := map[string]string{
|
|
importPath: basename,
|
|
}
|
|
buildEnv := packer.BuildEnv{
|
|
// Remain in the current directory instead of building in a separate,
|
|
// per-package directory.
|
|
BuildDir: func(string) (string, error) { return "", nil },
|
|
}
|
|
if err := buildEnv.Build(tmp, pkgs, packageBuildFlags, packageBuildTags, packageBuildEnv, noBuildPkgs, basenames); err != nil {
|
|
return err
|
|
}
|
|
|
|
httpClient, _, updateBaseUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
target, err := updater.NewTarget(ctx, updateBaseUrl.String(), httpClient)
|
|
if err != nil {
|
|
return fmt.Errorf("checking target partuuid support: %v", err)
|
|
}
|
|
|
|
progctx, canc := context.WithCancel(ctx)
|
|
defer canc()
|
|
prog := &progress.Reporter{}
|
|
go prog.Report(progctx)
|
|
|
|
f, err := os.Open(filepath.Join(tmp, basename))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("binary %s not installed; are you not in a directory where .go files declare “package main”?", basename)
|
|
}
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
prog.SetStatus("uploading " + basename)
|
|
if st, err := f.Stat(); err == nil {
|
|
prog.SetTotal(uint64(st.Size()))
|
|
}
|
|
|
|
{
|
|
start := time.Now()
|
|
err := target.Put(ctx, "uploadtemp/gok-run/"+basename, io.TeeReader(f, &progress.Writer{}))
|
|
if err != nil {
|
|
return fmt.Errorf("uploading temporary binary: %v", err)
|
|
}
|
|
duration := time.Since(start)
|
|
transferred := progress.Reset()
|
|
fmt.Printf("\rTransferred %s (%s) at %.2f MiB/s (total: %v)\n",
|
|
basename,
|
|
humanize.Bytes(transferred),
|
|
float64(transferred)/duration.Seconds()/1024/1024,
|
|
duration.Round(time.Second))
|
|
|
|
}
|
|
|
|
// Make gokrazy use the temporary binary instead of
|
|
// /user/<basename>. Includes an automatic service restart.
|
|
{
|
|
err := target.Divert(
|
|
ctx,
|
|
"/user/"+basename,
|
|
"gok-run/"+basename,
|
|
cfg.PackageConfig[importPath].CommandLineFlags,
|
|
args)
|
|
if err != nil {
|
|
return fmt.Errorf("diverting %s: %v", basename, err)
|
|
}
|
|
}
|
|
|
|
// Stop progress reporting to not mess up the following logs output.
|
|
canc()
|
|
|
|
// stream stdout/stderr logs
|
|
logsCfg := &logsImplConfig{
|
|
service: basename,
|
|
}
|
|
return logsCfg.run(ctx, nil, stdout, stderr)
|
|
}
|